[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