[education/rkward] rkward/rbackend/rkwarddevice: Start implementing masks

Thomas Friedrichsmeier null at kde.org
Thu Mar 24 21:02:51 GMT 2022


Git commit 40413e4ac83963e0510620252b699871169ed339 by Thomas Friedrichsmeier.
Committed on 23/03/2022 at 20:27.
Pushed by tfry into branch 'master'.

Start implementing masks

M  +77   -5    rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
M  +7    -0    rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
M  +3    -2    rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
M  +25   -0    rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
M  +2    -0    rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
M  +62   -4    rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
M  +46   -3    rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp

https://invent.kde.org/education/rkward/commit/40413e4ac83963e0510620252b699871169ed339

diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
index 9ed62900..6c828c76 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
@@ -17,9 +17,6 @@
 
 #include "rkgraphicsdevice.h"
 
-#include <QGraphicsScene>
-#include <QGraphicsView>
-#include <QGraphicsRectItem>
 #include <QHBoxLayout>
 #include <QMouseEvent>
 #include <QDialog>
@@ -92,9 +89,9 @@ void RKGraphicsDevice::pushContext(double width, double height, double x, double
 	c.record_path = record_path;
 	c.path_below = recorded_path;
 // NOTE: R cairo device uses an all different method for pattern capture:
-// drawing is scaled up to full device coordinates, the shrunk and offset back to pattern size.
+// drawing is scaled up to full device coordinates, then shrunk and offset back to pattern size.
 // probably due to cairo internals, somehow. Here, instead we paint on a separate surface with the same coords,
-// the extract the rectangle of interest.
+// then extract the rectangle of interest.
 	c.surface = QImage(area.width(), area.height(), QImage::Format_ARGB32);
 	if (width < 0) { // may happen, at least in R 4.1.2
 		width = -width;
@@ -313,6 +310,51 @@ bool RKGraphicsDevice::setClipToCachedPath(int index){
 	return false;
 }
 
+void RKGraphicsDevice::startRecordMask() {
+	RK_TRACE (GRAPHICS_DEVICE);
+	pushContext(area.width(), area.height(), 0, 0, false);
+}
+
+QImage RKGraphicsDevice::endRecordMask(bool luminance) {
+	RK_TRACE (GRAPHICS_DEVICE);
+
+	QImage ret = popContext().surface;
+	// KF6 TODO: instead paint on a grayscale surface from the start?
+	if (luminance) {
+		for (int x = ret.width(); x >= 0; --x) {
+			for (int y = ret.height(); y >= 0; --y) {
+				// warning! inefficient!
+				QColor px = ret.pixelColor(x, y);
+				px.setAlpha(px.lightness());
+				ret.setPixelColor(x, y, px);
+			}
+		}
+	}
+	return ret;
+}
+
+int RKGraphicsDevice::registerMask(const QImage& mask) {
+	RK_TRACE (GRAPHICS_DEVICE);
+	static int id = 0;
+	cached_masks.insert(++id, mask);
+	return id;
+}
+
+void RKGraphicsDevice::destroyMask(int index) {
+	RK_TRACE (GRAPHICS_DEVICE);
+	if (index < 0) cached_paths.clear();
+	else cached_paths.remove(index);
+}
+
+bool RKGraphicsDevice::setMask(int index) {
+	RK_TRACE (GRAPHICS_DEVICE);
+
+	int set = 0;
+	if (index > 0 && cached_masks.contains(index)) set = index;
+	current_mask = set;
+	return set == index;
+}
+
 void RKGraphicsDevice::setClip (const QRectF& new_clip) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
@@ -357,6 +399,10 @@ void RKGraphicsDevice::line (double x1, double y1, double x2, double y2, const Q
 void RKGraphicsDevice::rect (const QRectF& rec, const QPen& pen, const QBrush& brush) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
+	if (current_mask) {
+		pushContext(area.width(), area.height(), 0, 0, false);
+	}
+
 	if (recording_path) {
 		recorded_path.addRect(rec);
 		return;
@@ -364,6 +410,21 @@ void RKGraphicsDevice::rect (const QRectF& rec, const QPen& pen, const QBrush& b
 	painter.setPen (pen);
 	painter.setBrush (brush);
 	painter.drawRect (rec);
+
+	if (current_mask) {
+		QImage mask = cached_masks.value(current_mask);
+		QImage masked = popContext().surface;
+		for (int x = masked.width(); x >= 0; --x) {
+			for (int y = masked.height(); y >= 0; --y) {
+				// warning! inefficient!
+				QColor px = masked.pixelColor(x, y);
+				px.setAlpha(qMin(px.alpha(), mask.pixelColor(x, y).alpha()));
+				masked.setPixelColor(x, y, px);
+			}
+		}
+		painter.drawImage(0, 0, masked);
+	}
+
 	triggerUpdate ();
 }
 
@@ -378,6 +439,10 @@ QSizeF RKGraphicsDevice::strSize(const QString& text, const QFont& font) {
 void RKGraphicsDevice::text(double x, double y, const QString& text, double rot, double hadj, const QColor& col, const QFont& font) {
 	RK_TRACE(GRAPHICS_DEVICE);
 
+	if (current_mask) {
+		pushContext(area.width(), area.height(), 0, 0, false);
+	}
+
 	painter.save();
 	QSizeF size = strSize(text, font);  // NOTE: side-effect of setting font!
 	if (recording_path) {
@@ -396,6 +461,13 @@ void RKGraphicsDevice::text(double x, double y, const QString& text, double rot,
 	painter.drawText(-(hadj * size.width()), 0, text);
 //	painter.drawRect(painter.fontMetrics().boundingRect(text));  // for debugging
 	painter.restore();  // undo rotation / translation
+
+	if (current_mask) {
+		painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
+		painter.drawImage(0, 0, cached_masks.value(current_mask));
+		QImage masked = popContext().surface;
+		painter.drawImage(0, 0, masked);
+	}
 	triggerUpdate();
 }
 
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
index 40efcaf5..b60a88a7 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
@@ -105,6 +105,11 @@ public:
 	void destroyCachedPath(int index);
 	bool setClipToCachedPath(int index);
 	void forceSync();
+	void startRecordMask();
+	QImage endRecordMask(bool luminance);
+	int registerMask(const QImage &mask);
+	void destroyMask(int index);
+	bool setMask(int index);
 public slots:
 	void stopInteraction ();
 signals:
@@ -134,10 +139,12 @@ private:
 	QDialog *dialog;
 	QHash<int, QBrush> patterns;
 	QHash<int, QPainterPath> cached_paths;
+	QHash<int, QImage> cached_masks;
 	// NOTE on path recording: In principle, we could really do _all_ painting on QPainterPath, but in regular operation stroke and fill right away.
 	// However, that is noticably slower.
 	QPainterPath recorded_path;
 	bool recording_path;
+	int current_mask;
 
 	int interaction_opcode;	/**< Current interactive operation (from RKDOpcodes enum), or -1 is there is no current interactive operation */
 	quint32 id;
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
index 4fd2998e..e9876464 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_backendtransmitter.h
@@ -22,9 +22,10 @@
 #include <QThread>
 #include <QMutex>
 
-#include "rkgraphicsdevice_protocol_shared.h"
 #include "../rkasyncdatastreamhelper.h"
 
+typedef quint32 RKGraphicsDeviceTransmittionLengthType;
+
 /** This simple class is responsible for handling the backend side of transmitting data / requests for the RKGraphicsDevice
  Also it provides the namespace for some statics.
  As the protocol is really quite simple (only the backend send requests, only one request at a time), so is the transmitter. */
@@ -35,7 +36,7 @@ public:
 	static void kill ();
 	static bool connectionAlive ();
 	static RKGraphicsDeviceBackendTransmitter* instance ();
-	static RKAsyncDataStreamHelper<RKGraphicsDeviceTransmittionLengthType> streamer;
+	static RKAsyncDataStreamHelper<quint32> streamer;
 	static QIODevice* connection;
 	static QMutex mutex;
 private:
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
index 22cc0734..c9f23f22 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
@@ -400,6 +400,31 @@ void RKGraphicsDeviceFrontendTransmitter::newData () {
 			device->setClipToCachedPath(index);
 			streamer.outstream << (qint32) index;
 			streamer.writeOutBuffer();
+		} else if (opcode == RKDSetMask) {
+			qint32 index;
+			streamer.instream >> index;
+			qint8 ok = device->setMask(index) ? 1 : 0;
+			streamer.outstream << ok;
+			streamer.writeOutBuffer();
+		} else if (opcode == RKDReleaseMask) {
+			qint32 len;
+			streamer.instream >> len;
+			if (len < 0) device->destroyMask(-1);
+			for (int i = 0; i < len; ++i) {
+				qint32 index;
+				streamer.instream >> index;
+				device->destroyMask(index);
+			}
+		} else if (opcode == RKDStartRecordMask) {
+			device->startRecordMask();
+		} else if (opcode == RKDEndRecordMask) {
+			qint8 luminance_mask;
+			streamer.instream >> luminance_mask;
+			QImage m = device->endRecordMask((bool) luminance_mask);
+			qint32 index = device->registerMask(m);
+			device->setMask(index);
+			streamer.outstream << (qint32) index;
+			streamer.writeOutBuffer();
 		} else if (opcode == RKDCapture) {
 			QImage image = device->capture ();
 			quint32 w = image.width ();
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
index a11853e6..fae6afff 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.h
@@ -18,7 +18,9 @@
 #ifndef RKGRAPHICSDEVICE_FRONTENDTRANSMITTER_H
 #define RKGRAPHICSDEVICE_FRONTENDTRANSMITTER_H
 
+#include <QPainter> // for enums
 #include "rkgraphicsdevice_protocol_shared.h"
+#include "rkgraphicsdevice_backendtransmitter.h"
 #include "../rkasyncdatastreamhelper.h"
 
 class QIODevice;
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
index 12daa58d..07d4e462 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
@@ -107,7 +107,9 @@ enum RKDOpcodes {
 	RKDReleasePattern,
 	RKDStartRecordTilingPattern,      // part of setPattern in R
 	RKDReleaseClipPath,
-	RKDStartRecordClipPath,
+	RKDStartRecordClipPath,//20
+	RKDReleaseMask,
+	RKDStartRecordMask,
 
 	// Synchronous operations
 	RKDFetchNextEvent      = 100,
@@ -122,8 +124,10 @@ enum RKDOpcodes {
 	RKDEndRecordTilingPattern,       // part of setPattern in R
 	RKDSetClipPath,       // 110
 	RKDEndRecordClipPath,
+	RKDSetMask,
+	RKDEndRecordMask,
 	RKDForceSync,
-	RKDClose,
+	RKDClose,             // 115
 
 	// Protocol operations
 	RKDCancel              = 200
@@ -145,7 +149,61 @@ enum RKDEventCodes {
 //	RKDMouseX2Button = 16
 };
 
-#include <QtGlobal>
-typedef quint32 RKGraphicsDeviceTransmittionLengthType;
+/** Common problem is that we need to map an R parameter enum onto the corresponding Qt parameter enum, e.g. line ending style, etc.
+ * While in most cases there _is_ a direct correspondence, the underlying int values cannot be assumed to be the same (and in many cases differ).
+ * Thus we need a lot of code along the lines "if(value = r_enum_value) return qt_enum_value;".
+ *
+ * As a further complication, we cannot easily include R headers in frontend code, and not easily (arbitray) Qt headers in backend code, thus the
+ * above pseudo code will lack either the Qt or the R defintion. At some point we need to map to a value we set ourselves, either a third, rk-specific, enum,
+ * or a naked int.
+ *
+ * To make this readable and easy to handle, the MapEnum() macro helps with the mapping. Since only _either_ the left hand side (R) or the right hand side (Qt)
+ * is actually expanded, it allows us to write a readable mapping that can be included in both frontend and backend code.
+ *
+ * We still need to provide an interim value (naked int). By convention this uses the same value as the Qt enum, and this is checked by a static_assert, helping
+ * to catch conceivable mistakes at compile time.
+ */
+#if defined(RKD_BACKEND_CODE)
+#define MapEnum(Rval,Ival,Qval) case Rval: return Ival;
+#define MapDefault(Ival,Qval) return Ival;
+#else
+#define MapEnum(Rval,Ival,Qval) case Ival: static_assert(Ival == (int) Qval, "Enum mismatch"); return Qval;
+#define MapDefault(Ival,Qval) return Qval;
+#define RKD_RGE_VERSION 99999
+#endif
+
+#if RKD_RGE_VERSION >= 15
+static int mapCompostionModeEnum(int from) {
+	switch(from) {
+		MapEnum(R_GE_compositeClear, 2, QPainter::CompositionMode_Clear);
+		MapEnum(R_GE_compositeSource, 3, QPainter::CompositionMode_Source);
+		MapEnum(R_GE_compositeOver, 0, QPainter::CompositionMode_SourceOver);
+		MapEnum(R_GE_compositeIn, 5, QPainter::CompositionMode_SourceIn);
+		MapEnum(R_GE_compositeOut, 7, QPainter::CompositionMode_SourceOut);
+		MapEnum(R_GE_compositeAtop, 9, QPainter::CompositionMode_SourceAtop);
+		MapEnum(R_GE_compositeDest, 4, QPainter::CompositionMode_Destination);
+		MapEnum(R_GE_compositeDestOver, 1, QPainter::CompositionMode_DestinationOver);
+		MapEnum(R_GE_compositeDestIn, 6, QPainter::CompositionMode_DestinationIn);
+		MapEnum(R_GE_compositeDestOut, 8, QPainter::CompositionMode_DestinationOut);
+		MapEnum(R_GE_compositeDestAtop, 10, QPainter::CompositionMode_DestinationAtop);
+		MapEnum(R_GE_compositeXor, 11, QPainter::CompositionMode_Xor);
+		MapEnum(R_GE_compositeAdd, 12, QPainter::CompositionMode_Plus);
+		MapEnum(R_GE_compositeMultiply, 13, QPainter::CompositionMode_Multiply);
+		MapEnum(R_GE_compositeScreen, 14, QPainter::CompositionMode_Screen);
+		MapEnum(R_GE_compositeOverlay, 15, QPainter::CompositionMode_Overlay);
+		MapEnum(R_GE_compositeDarken, 16, QPainter::CompositionMode_Darken);
+		MapEnum(R_GE_compositeLighten, 17, QPainter::CompositionMode_Lighten);
+		MapEnum(R_GE_compositeColorDodge, 18, QPainter::CompositionMode_ColorDodge);
+		MapEnum(R_GE_compositeColorBurn, 19, QPainter::CompositionMode_ColorBurn);
+		MapEnum(R_GE_compositeHardLight, 20, QPainter::CompositionMode_HardLight);
+		MapEnum(R_GE_compositeSoftLight, 21, QPainter::CompositionMode_SoftLight);
+		MapEnum(R_GE_compositeDifference, 22, QPainter::CompositionMode_Difference);
+		MapEnum(R_GE_compositeExclusion, 23, QPainter::CompositionMode_Exclusion);
+// Unsupported in Qt:
+// MapEnum(R_GE_compositeSaturate, xx, yy)
+	}
+	MapDefault(0, QPainter::CompositionMode_SourceOver);
+}
+#endif
 
 #endif
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
index e5864351..5c1fe048 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
@@ -19,6 +19,8 @@
  * not a compilation unit of its own.
  * It is meant to be included, there. */
 
+#define RKD_BACKEND_CODE
+#define RKD_RGE_VERSION R_GE_version
 #include "rkgraphicsdevice_protocol_shared.h"
 #include "rkgraphicsdevice_backendtransmitter.h"
 #include "../rkrbackend.h"
@@ -166,6 +168,7 @@ public:
 #define SAFE_LINE_JOIN(ljoin) (quint8) (ljoin == GE_ROUND_JOIN ? RoundJoin : (ljoin == GE_BEVEL_JOIN ? BevelJoin : MitreJoin))
 
 // I'd love to convert to QColor, directly, but that's in QtGui, not QtCore. QRgb used different byte ordering
+// TODO: is the check against NA_INTEGER needed at all?
 #define WRITE_COLOR_BYTES(col) \
 	RKD_OUT_STREAM << (quint8) R_RED (col) << (quint8) R_GREEN (col) << (quint8) R_BLUE (col) << (quint8) R_ALPHA (col)
 #define WRITE_HEADER_NUM(x,devnum) \
@@ -826,11 +829,51 @@ void RKD_ReleaseClipPath (SEXP ref, pDevDesc dev) {
 	}
 }
 
-SEXP RKD_SetMask (SEXP path, SEXP ref, pDevDesc dd) {
+SEXP RKD_SetMask (SEXP mask, SEXP ref, pDevDesc dev) {
 	RK_TRACE(GRAPHICS_DEVICE);
-#ifdef __GNUC__
-#warning implement me
+	// Same logic as RKD_SetClipPath
+
+	qint32 index = 0;
+	if (!Rf_isNull(ref)) index = INTEGER(ref)[0];  // ref==NULL means the mask is not yet registered, will be recorded, below
+	if (index > 0 || Rf_isNull(mask)) {  // mask==NULL means to unset the current mask. signalled to the frontend as index=0
+		{
+			RKGraphicsDataStreamWriteGuard wguard;
+			WRITE_HEADER(RKDSetMask, dev);
+			RKD_OUT_STREAM << index;
+		}
+		{
+			RKGraphicsDataStreamReadGuard rguard;
+			qint8 ok;
+			RKD_IN_STREAM >> ok;
+			if (!ok) Rf_warning("Invalid reference to mask");
+			else return R_NilValue;
+		}
+	}
+
+	// No index, or not a valid index: create new mask
+	{
+		RKGraphicsDataStreamWriteGuard wguard;
+		WRITE_HEADER(RKDStartRecordMask, dev);
+	}
+	// Play generator function
+	int error;
+	SEXP mask_func = PROTECT(Rf_lang1(mask));
+	R_tryEval(mask_func, R_GlobalEnv, &error);
+	UNPROTECT(1);
+	{
+		RKGraphicsDataStreamWriteGuard wguard;
+		WRITE_HEADER(RKDEndRecordMask, dev);
+#if R_VERSION >= R_Version(4,2,0)
+		RKD_OUT_STREAM << (qint8) R_GE_maskType(mask) == R_GE_luminanceMask ? 1 : 0;
+#else
+		RKD_OUT_STREAM << 0;
 #endif
+	}
+	{
+		RKGraphicsDataStreamReadGuard rguard;
+		RKD_IN_STREAM >> index;
+	}
+	return makeInt(index);
 	return R_NilValue;
 }
 



More information about the rkward-tracker mailing list