[education/rkward] rkward/rbackend/rkwarddevice: Implement clip paths

Thomas Friedrichsmeier null at kde.org
Sat Mar 19 21:57:45 GMT 2022


Git commit 96e194fd56865a2022afe44053d5e29a119d78ba by Thomas Friedrichsmeier.
Committed on 18/03/2022 at 22:11.
Pushed by tfry into branch 'master'.

Implement clip paths

M  +96   -10   rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
M  +13   -1    rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
M  +25   -0    rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
M  +9    -0    rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
M  +1    -1    rkward/rbackend/rkwarddevice/rkgraphicsdevice_setup.cpp
M  +61   -9    rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp

https://invent.kde.org/education/rkward/commit/96e194fd56865a2022afe44053d5e29a119d78ba

diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
index 49340b26..f5602df2 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.cpp
@@ -50,6 +50,7 @@ RKGraphicsDevice::RKGraphicsDevice (double width, double height, const QString &
 
 	interaction_opcode = -1;
 	dialog = 0;
+	recording_path = false;
 	view = new QLabel ();
 	view->installEventFilter (this);
 	view->setScaledContents (true);    // this is just for preview during scaling. The area will be re-sized and re-drawn from R.
@@ -73,18 +74,22 @@ void RKGraphicsDevice::beginPainter() {
 	if(!painter.isActive()) {
 		if (contexts.isEmpty()) {
 			painter.begin(&area);  // plain old painting on the canvas itself
+			recording_path = false;
 		} else {
 			auto &c = contexts.last();
 			painter.begin(&(c.surface));
 			painter.setTransform(c.transform);
+			recording_path = c.record_path;
 		}
 	}
 }
 
-void RKGraphicsDevice::pushContext(double width, double height, double x, double y) {
+void RKGraphicsDevice::pushContext(double width, double height, double x, double y, bool record_path) {
 	RK_TRACE (GRAPHICS_DEVICE);
 	painter.end();
 	PaintContext c;
+	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.
 // probably due to cairo internals, somehow. Here, instead we paint on a separate surface with the same coords,
@@ -113,13 +118,14 @@ RKGraphicsDevice::PaintContext RKGraphicsDevice::popContext() {
 
 	painter.end();
 	auto ret = contexts.takeLast();
+	recorded_path = ret.path_below;
 	beginPainter();
 	return ret;
 }
 
 void RKGraphicsDevice::startRecordTilingPattern(double width, double height, double x, double y) {
 	RK_TRACE (GRAPHICS_DEVICE);
-	pushContext(width, height, x, y);
+	pushContext(width, height, x, y, false);
 }
 
 int RKGraphicsDevice::finalizeTilingPattern(int extend) {
@@ -147,7 +153,7 @@ int RKGraphicsDevice::finalizeTilingPattern(int extend) {
 		return registerPattern(brush);
 	}
 
-	// else: GradientExtendRepeat
+	// else: GradientExtendRepeat. This is the standard QBrush behavior
 	QImage img = c.surface.copy(c.capture_coords);
 	QBrush brush(img);
 	brush.setTransform(QTransform().translate(c.capture_coords.left(), c.capture_coords.top()));
@@ -209,6 +215,12 @@ void RKGraphicsDevice::closeDevice (int devnum) {
 void RKGraphicsDevice::clear(const QBrush& brush) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
+	if (recording_path) {
+		recorded_path = QPainterPath();
+		setClip(area.rect());	// R's devX11.c resets clip on clear, so we do this, too.
+		return;
+	}
+
 	if (painter.isActive()) painter.end();
 	if (brush.style() == Qt::NoBrush) area.fill(QColor(255, 255, 255, 255));
 	else {
@@ -231,20 +243,57 @@ void RKGraphicsDevice::setAreaSize (const QSize& size) {
 	clear ();
 }
 
-
 int RKGraphicsDevice::registerPattern(const QBrush& brush) {
-	RK_TRACE (GRAPHICS_DEVICE);
+	RK_TRACE(GRAPHICS_DEVICE);
 	static int id = 0;
 	patterns.insert(++id, brush);
 	return id;
 }
 
 void RKGraphicsDevice::destroyPattern(int id) {
-	RK_TRACE (GRAPHICS_DEVICE);
+	RK_TRACE(GRAPHICS_DEVICE);
 	if (id == 0) patterns.clear();
 	else patterns.remove(id);
 }
 
+void RKGraphicsDevice::startRecordPath() {
+	RK_TRACE(GRAPHICS_DEVICE);
+	pushContext(0, 0, 0, 0, true);
+}
+
+QPainterPath RKGraphicsDevice::endRecordPath(int fillrule) {
+	RK_TRACE(GRAPHICS_DEVICE);
+
+	QPainterPath ret = recorded_path;
+	if (fillrule == NonZeroWindingRule) ret.setFillRule(Qt::WindingFill);
+	else ret.setFillRule(Qt::OddEvenFill);
+
+	popContext();
+	return ret;
+}
+
+int RKGraphicsDevice::cachePath(QPainterPath& path) {
+	RK_TRACE(GRAPHICS_DEVICE);
+	static int id = 0;
+	cached_paths.insert(++id, path);
+	return id;
+}
+
+void RKGraphicsDevice::destroyCachedPath(int index) {
+	RK_TRACE(GRAPHICS_DEVICE);
+	if (index < 0) cached_paths.clear();
+	else cached_paths.remove(index);
+}
+
+bool RKGraphicsDevice::setClipToCachedPath(int index){
+	RK_TRACE(GRAPHICS_DEVICE);
+	if (cached_paths.contains(index)) {
+		painter.setClipPath(cached_paths[index]);
+		return true;
+	}
+	return false;
+}
+
 void RKGraphicsDevice::setClip (const QRectF& new_clip) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
@@ -255,6 +304,10 @@ void RKGraphicsDevice::setClip (const QRectF& new_clip) {
 void RKGraphicsDevice::circle (double x, double y, double r, const QPen& pen, const QBrush& brush) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
+	if (recording_path) {
+		recorded_path.addEllipse(x-r, y-r, r+r, r+r);
+		return;
+	}
 	painter.setPen (pen);
 	painter.setBrush (brush);
 	painter.drawEllipse (x - r, y - r, r+r, r+r);
@@ -264,6 +317,11 @@ void RKGraphicsDevice::circle (double x, double y, double r, const QPen& pen, co
 void RKGraphicsDevice::line (double x1, double y1, double x2, double y2, const QPen& pen) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
+	if (recording_path) {
+		recorded_path.moveTo(x1,y1);
+		recorded_path.lineTo(x2, y2);
+		return;
+	}
 	painter.setPen (pen);
 	// HACK: There seems to be a bug in QPainter (Qt 4.8.4), which can shift connected lines (everything but the first polyline)
 	//       towards the direction where the previous line came from. The result is that line drawn via drawLine() and drawPolyline() do
@@ -280,6 +338,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 (recording_path) {
+		recorded_path.addRect(rec);
+		return;
+	}
 	painter.setPen (pen);
 	painter.setBrush (brush);
 	painter.drawRect (rec);
@@ -299,11 +361,20 @@ void RKGraphicsDevice::text(double x, double y, const QString& text, double rot,
 
 	painter.save();
 	QSizeF size = strSize(text, font);  // NOTE: side-effect of setting font!
+	if (recording_path) {
+		QPainterPath sub;
+		sub.addText(-(hadj * size.width()), y, font, text);
+		QMatrix trans;
+		trans.translate(x, y);
+		trans.rotate(-rot);
+		recorded_path.addPath(trans.map(sub));
+		return;
+	}
 //	painter.setFont(font);
 	painter.setPen(QPen(col));
-	painter.translate(x-(hadj * size.width()), y);
+	painter.translate(x, y);
 	painter.rotate(-rot);
-	painter.drawText(0, 0, text);
+	painter.drawText(-(hadj * size.width()), 0, text);
 //	painter.drawRect(painter.fontMetrics().boundingRect(text));  // for debugging
 	painter.restore();  // undo rotation / translation
 	triggerUpdate();
@@ -322,6 +393,11 @@ void RKGraphicsDevice::metricInfo (const QChar& c, const QFont& font, double* as
 
 void RKGraphicsDevice::polygon (const QPolygonF& pol, const QPen& pen, const QBrush& brush) {
 	RK_TRACE (GRAPHICS_DEVICE);
+
+	if (recording_path) {
+		recorded_path.addPolygon(pol);
+		return;
+	}
 	painter.setPen (pen);
 	painter.setBrush (brush);
 	painter.drawPolygon (pol);
@@ -331,6 +407,10 @@ void RKGraphicsDevice::polygon (const QPolygonF& pol, const QPen& pen, const QBr
 void RKGraphicsDevice::polyline (const QPolygonF& pol, const QPen& pen) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
+	if (recording_path) {
+		recorded_path.addPolygon(pol);
+		return;
+	}
 	painter.setPen (pen);
 	painter.drawPolyline (pol);
 	triggerUpdate ();
@@ -339,14 +419,20 @@ void RKGraphicsDevice::polyline (const QPolygonF& pol, const QPen& pen) {
 void RKGraphicsDevice::polypath (const QVector<QPolygonF>& polygons, bool winding, const QPen& pen, const QBrush& brush) {
 	RK_TRACE (GRAPHICS_DEVICE);
 
-	painter.setPen (pen);
-	painter.setBrush (brush);
 	QPainterPath path;
 	if (winding) path.setFillRule (Qt::WindingFill);
 	for (int i = 0; i < polygons.size (); ++i) {
 		path.addPolygon (polygons[i]);
 		path.closeSubpath ();
 	}
+
+	if (recording_path) {
+		recorded_path.addPath(path);
+		return;
+	}
+
+	painter.setPen (pen);
+	painter.setBrush (brush);
 	painter.drawPath (path);
 	triggerUpdate ();
 }
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
index d3a91627..954e3d9b 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice.h
@@ -98,6 +98,11 @@ public:
 	QBrush getPattern(int id) const { return patterns.value(id); };
 	void startRecordTilingPattern(double width, double height, double x, double y);
 	int finalizeTilingPattern(int extend);
+	void startRecordPath();
+	QPainterPath endRecordPath(int fillrule);
+	int cachePath(QPainterPath &path);
+	void destroyCachedPath(int index);
+	bool setClipToCachedPath(int index);
 public slots:
 	void stopInteraction ();
 signals:
@@ -126,6 +131,11 @@ private:
 	QString base_title;
 	QDialog *dialog;
 	QHash<int, QBrush> patterns;
+	QHash<int, QPainterPath> cached_paths;
+	// 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 interaction_opcode;	/**< Current interactive operation (from RKDOpcodes enum), or -1 is there is no current interactive operation */
 
@@ -136,11 +146,13 @@ private:
 		QImage surface;
 		QTransform transform;
 		QRect capture_coords;
+		QPainterPath path_below;
+		bool record_path;
 	};
 	QList<PaintContext> contexts;
 	// make sure the painter is active on the current context
 	void beginPainter();
-	void pushContext(double width, double height, double x, double y);
+	void pushContext(double width, double height, double x, double y, bool record_path);
 	PaintContext popContext();
 };
 
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
index 679312a6..81334090 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_frontendtransmitter.cpp
@@ -375,6 +375,31 @@ void RKGraphicsDeviceFrontendTransmitter::newData () {
 			streamer.instream >> extend;
 			streamer.outstream << (qint32) device->finalizeTilingPattern((RKDGradientExtend) extend);
 			streamer.writeOutBuffer();
+		} else if (opcode == RKDSetClipPath) {
+			qint32 index;
+			streamer.instream >> index;
+			bool ok = device->setClipToCachedPath(index);
+			streamer.outstream << ok;
+			streamer.writeOutBuffer();
+		} else if (opcode == RKDReleaseClipPath) {
+			qint32 len;
+			streamer.instream >> len;
+			if (len < 0) device->destroyCachedPath(-1);
+			for (int i = 0; i < len; ++i) {
+				qint32 index;
+				streamer.instream >> index;
+				device->destroyCachedPath(index);
+			}
+		} else if (opcode == RKDStartRecordClipPath) {
+			device->startRecordPath();
+		} else if (opcode == RKDEndRecordClipPath) {
+			qint8 fillrule;
+			streamer.instream >> fillrule;
+			QPainterPath p = device->endRecordPath(fillrule);
+			qint32 index = device->cachePath(p);
+			device->setClipToCachedPath(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_protocol_shared.h b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
index 1c91a004..a0310b10 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_protocol_shared.h
@@ -79,6 +79,11 @@ enum RKDGradientExtend {
 	GradientExtendRepeat
 };
 
+enum RKDFillRule {
+	NonZeroWindingRule,
+	EvenOddRule
+};
+
 enum RKDOpcodes {
 	// NOTE: the only point of the assigned int values is to ease debugging in case of trouble
 	// Asynchronous operations
@@ -102,6 +107,8 @@ enum RKDOpcodes {
 	RKDStopGettingEvents,
 	RKDReleasePattern,
 	RKDStartRecordTilingPattern,      // part of setPattern in R
+	RKDReleaseClipPath,
+	RKDStartRecordClipPath,
 
 	// Synchronous operations
 	RKDFetchNextEvent      = 100,
@@ -114,6 +121,8 @@ enum RKDOpcodes {
 	RKDGetSize,
 	RKDSetPattern,
 	RKDEndRecordTilingPattern,       // part of setPattern in R
+	RKDSetClipPath,       // 110
+	RKDEndRecordClipPath,
 
 	// Protocol operations
 	RKDCancel              = 200
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_setup.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_setup.cpp
index e0a57b18..da9e47a0 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_setup.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_setup.cpp
@@ -228,7 +228,7 @@ bool RKGraphicsDeviceDesc::init (pDevDesc dev, double pointsize, const QStringLi
 	dev->setMask = RKD_SetMask;
 	dev->releaseMask = RKD_ReleaseMask;
 	dev->deviceVersion = qMin(14, R_GE_version);
-	dev->deviceClip = FALSE; // for now
+	dev->deviceClip = TRUE; // for now
 #endif
 	return true;
 }
diff --git a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
index 8b511af0..cf992fd4 100644
--- a/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
+++ b/rkward/rbackend/rkwarddevice/rkgraphicsdevice_stubs.cpp
@@ -676,7 +676,7 @@ SEXP RKD_SetPattern (SEXP pattern, pDevDesc dev) {
 
 void RKD_ReleasePattern (SEXP ref, pDevDesc dev) {
 	qint32 index;
-	if (ref == R_NilValue) index = 0;  // means: destroy all patterns
+	if (Rf_isNull(ref)) index = 0;  // means: destroy all patterns
 	else index = INTEGER(ref)[0];
 	{
 		RKGraphicsDataStreamWriteGuard wguard;
@@ -685,17 +685,69 @@ void RKD_ReleasePattern (SEXP ref, pDevDesc dev) {
 	}
 }
 
-SEXP RKD_SetClipPath (SEXP path, SEXP ref, pDevDesc dd) {
-#ifdef __GNUC__
-#warning implement me
+SEXP RKD_SetClipPath (SEXP path, SEXP ref, pDevDesc dev) {
+	qint32 index = -1;
+	if (!Rf_isNull(ref)) index = INTEGER(ref)[0];
+	// NOTE: just because we have a reference, doesn't mean, it's also valid, according to R sources
+	if (index >= 0) {
+		{
+			RKGraphicsDataStreamWriteGuard wguard;
+			WRITE_HEADER(RKDSetClipPath, dev);
+			RKD_OUT_STREAM << index;
+		}
+		{
+			RKGraphicsDataStreamReadGuard rguard;
+			bool ok;
+			RKD_IN_STREAM >> ok;
+			if (!ok) Rf_warning("Invalid reference to clipping path");
+			else return R_NilValue;
+		}
+	}
+
+	// No index, or not a valid index: create new path
+	{
+		RKGraphicsDataStreamWriteGuard wguard;
+		WRITE_HEADER(RKDStartRecordClipPath, dev);
+	}
+	// Play generator function
+	int error;
+	SEXP path_func = PROTECT(Rf_lang1(path));
+	R_tryEval(path_func, R_GlobalEnv, &error);
+	UNPROTECT(1);
+	{
+		RKGraphicsDataStreamWriteGuard wguard;
+		WRITE_HEADER(RKDEndRecordClipPath, dev);
+#if R_VERSION >= R_Version(4, 2, 0)
+		if (R_GE_clipPathFillRule(path) == R_GE_nonZeroWindingRule) {
+			RKD_OUT_STREAM << (qint8) NonZeroWindingRule;
+		} else {
+			RKD_OUT_STREAM << (qint8) EvenOddRule;
+		}
+#else
+		RKD_OUT_STREAM << (qint8) EvenOddRule;
 #endif
-	return R_NilValue;
+	}
+	{
+		RKGraphicsDataStreamReadGuard rguard;
+		RKD_IN_STREAM >> index;
+	}
+	return makeInt(index);
 }
 
-void RKD_ReleaseClipPath (SEXP ref, pDevDesc dd) {
-#ifdef __GNUC__
-#warning implement me
-#endif
+void RKD_ReleaseClipPath (SEXP ref, pDevDesc dev) {
+	{
+		RKGraphicsDataStreamWriteGuard wguard;
+		WRITE_HEADER(RKDReleaseClipPath, dev);
+		if (Rf_isNull(ref)) {
+			RKD_OUT_STREAM << (qint32) -1; // means: destroy all clippaths
+		} else {
+			qint32 len = LENGTH(ref);
+			RKD_OUT_STREAM << len;
+			for (int i = 0; i < len; ++i) {
+				RKD_OUT_STREAM << (qint32) INTEGER(ref)[i];
+			}
+		}
+	}
 }
 
 SEXP RKD_SetMask (SEXP path, SEXP ref, pDevDesc dd) {



More information about the rkward-tracker mailing list