Hi Jim, I made good progress on my PathClipFilter that works now perfectly with many test maps for the NZ rule (EO has artefacts if I enable the filter in that case).
Here is the updated code below to illustrate the approach: - use a new IndexStack in (D)Helpers to store only corner indexes (0/1 for Top/Bottom Left, 2/3 for Top/Bottom Right) - when the segment is out, I now check (L/R case) if the segment ends have different outcodes to insert needed corners that can be removed later if a segment does the same in the reverse order (same repeated corner is cancelled out): see IndexStack.push(int) - PathClipFilter: static final class PathClipFilter implements DPathConsumer2D { private DPathConsumer2D out; // Bounds of the drawing region, at pixel precision. private final double[] clipRect; private final double[] corners = new double[8]; private boolean init_corners = false; private final IndexStack stack; // the outcode of the starting point private int sOutCode = 0; // the current outcode of the current sub path private int cOutCode = 0; private boolean outside = false; private double cx0, cy0; PathClipFilter(final DRendererContext rdrCtx) { this.clipRect = rdrCtx.clipRect; this.stack = (rdrCtx.stats != null) ? new IndexStack(rdrCtx, rdrCtx.stats.stat_pcf_idxstack_indices, rdrCtx.stats.hist_pcf_idxstack_indices, rdrCtx.stats.stat_array_pcf_idxstack_indices) : new IndexStack(rdrCtx); } PathClipFilter init(final DPathConsumer2D out) { this.out = out; // Adjust the clipping rectangle with the renderer offsets final double rdrOffX = DRenderer.RDR_OFFSET_X; final double rdrOffY = DRenderer.RDR_OFFSET_Y; // add a small rounding error: final double margin = 1e-3d; final double[] _clipRect = this.clipRect; _clipRect[0] -= margin - rdrOffY; _clipRect[1] += margin + rdrOffY; _clipRect[2] -= margin - rdrOffX; _clipRect[3] += margin + rdrOffX; init_corners = true; return this; // fluent API } /** * Disposes this instance: * clean up before reusing this instance */ void dispose() { stack.dispose(); } @Override public void pathDone() { out.pathDone(); // TODO: fix possible leak if exception happened // Dispose this instance: dispose(); } @Override public void closePath() { if (outside) { this.outside = false; if (sOutCode == 0) { finish(); } else { stack.reset(); } } out.closePath(); this.cOutCode = sOutCode; } private void finish() { if (!stack.isEmpty()) { if (init_corners) { init_corners = false; // Top Left (0): corners[0] = clipRect[2]; corners[1] = clipRect[0]; // Bottom Left (1): corners[2] = clipRect[2]; corners[3] = clipRect[1]; // Top right (2): corners[4] = clipRect[3]; corners[5] = clipRect[0]; // Bottom Right (3): corners[6] = clipRect[3]; corners[7] = clipRect[1]; } stack.pullAll(corners, out); } out.lineTo(cx0, cy0); } @Override public void moveTo(final double x0, final double y0) { final int outcode = DHelpers.outcode(x0, y0, clipRect); this.sOutCode = outcode; this.cOutCode = outcode; this.outside = false; out.moveTo(x0, y0); } @Override public void lineTo(final double xe, final double ye) { final int outcode0 = this.cOutCode; final int outcode1 = DHelpers.outcode(xe, ye, clipRect); this.cOutCode = outcode1; final int sideCode = (outcode0 & outcode1); // basic rejection criteria: if (sideCode != 0) { // keep last point coordinate before entering the clip again: this.outside = true; this.cx0 = xe; this.cy0 = ye; clip(sideCode, outcode0, outcode1); return; } if (outside) { this.outside = false; finish(); } // clipping disabled: out.lineTo(xe, ye); } private void clip(final int sideCode, final int outcode0, final int outcode1) { // corner or cross-boundary on left or right side: if ((outcode0 != outcode1) && ((sideCode & DHelpers.OUTCODE_MASK_T_B) != 0)) { // combine outcodes: final int mergeCode = (outcode0 | outcode1); final int tbCode = mergeCode & DHelpers.OUTCODE_MASK_T_B; final int lrCode = mergeCode & DHelpers.OUTCODE_MASK_L_R; // add corners to outside stack: final int off = (lrCode == DHelpers.OUTCODE_LEFT) ? 0 : 2; switch (tbCode) { case DHelpers.OUTCODE_TOP: stack.push(off); // top return; case DHelpers.OUTCODE_BOTTOM: stack.push(off + 1); // bottom return; default: // both TOP / BOTTOM: if ((outcode0 & DHelpers.OUTCODE_TOP) != 0) { // top to bottom stack.push(off); // top stack.push(off + 1); // bottom } else { // bottom to top stack.push(off + 1); // bottom stack.push(off); // top } } } } @Override public void curveTo(final double x1, final double y1, final double x2, final double y2, final double xe, final double ye) { final int outcode0 = this.cOutCode; final int outcode3 = DHelpers.outcode(xe, ye, clipRect); this.cOutCode = outcode3; int sideCode = outcode0 & outcode3; if (sideCode != 0) { sideCode &= DHelpers.outcode(x1, y1, clipRect); sideCode &= DHelpers.outcode(x2, y2, clipRect); // basic rejection criteria: if (sideCode != 0) { // keep last point coordinate before entering the clip again: this.outside = true; this.cx0 = xe; this.cy0 = ye; clip(sideCode, outcode0, outcode3); return; } } if (outside) { this.outside = false; finish(); } // clipping disabled: out.curveTo(x1, y1, x2, y2, xe, ye); } @Override public void quadTo(final double x1, final double y1, final double xe, final double ye) { final int outcode0 = this.cOutCode; final int outcode2 = DHelpers.outcode(xe, ye, clipRect); this.cOutCode = outcode2; int sideCode = outcode0 & outcode2; if (outcode2 != 0) { sideCode &= DHelpers.outcode(x1, y1, clipRect); // basic rejection criteria: if (sideCode != 0) { // keep last point coordinate before entering the clip again: this.outside = true; this.cx0 = xe; this.cy0 = ye; clip(sideCode, outcode0, outcode2); return; } } if (outside) { this.outside = false; finish(); } // clipping disabled: out.quadTo(x1, y1, xe, ye); } @Override public long getNativeConsumer() { throw new InternalError("Not using a native peer"); } } - DHelpers.IndexStack: // a stack of integer indices static final class IndexStack { // integer capacity = edges count / 4 ~ 1024 private static final int INITIAL_COUNT = INITIAL_EDGES_COUNT >> 2; private int end; private int[] indices; // indices ref (dirty) private final IntArrayCache.Reference indices_ref; // used marks (stats only) private int indicesUseMark; private final StatLong stat_idxstack_indices; private final Histogram hist_idxstack_indices; private final StatLong stat_array_idxstack_indices; IndexStack(final DRendererContext rdrCtx) { this(rdrCtx, null, null, null); } IndexStack(final DRendererContext rdrCtx, final StatLong stat_idxstack_indices, final Histogram hist_idxstack_indices, final StatLong stat_array_idxstack_indices) { indices_ref = rdrCtx.newDirtyIntArrayRef(INITIAL_COUNT); // 4K indices = indices_ref.initial; end = 0; if (DO_STATS) { indicesUseMark = 0; } this.stat_idxstack_indices = stat_idxstack_indices; this.hist_idxstack_indices = hist_idxstack_indices; this.stat_array_idxstack_indices = stat_array_idxstack_indices; } /** * Disposes this PolyStack: * clean up before reusing this instance */ void dispose() { end = 0; if (DO_STATS) { stat_idxstack_indices.add(indicesUseMark); hist_idxstack_indices.add(indicesUseMark); // reset marks indicesUseMark = 0; } // Return arrays: // values is kept dirty indices = indices_ref.putArray(indices); } boolean isEmpty() { return (end == 0); } void reset() { end = 0; } void push(final int v) { // remove redundant values (reverse order): int[] _values = indices; final int nc = end; if (nc != 0) { if (_values[nc - 1] == v) { // remove both duplicated values: end--; return; } } if (_values.length <= nc) { if (DO_STATS) { stat_array_idxstack_indices.add(nc + 1); } indices = _values = indices_ref.widenArray(_values, nc, nc + 1); } _values[end++] = v; if (DO_STATS) { // update used marks: if (end > indicesUseMark) { indicesUseMark = end; } } } void pullAll(final double[] points, final DPathConsumer2D io) { final int nc = end; if (nc == 0) { return; } final int[] _values = indices; for (int i = 0, j; i < nc; i++) { j = _values[i] << 1; io.lineTo(points[j], points[j + 1]); } end = 0; } } Here is a screenshot illustrating the remaining paths in Renderer after > clipping a 4000x4000 spiral converted as stroked shape: > http://cr.openjdk.java.net/~lbourges/png/SpiralTest-dash-false.ser.png > Now all useless rounds are totally discarded from the path sent to the Renderer (removing lots of edges on the left/right sides) > clip off: ~ 145ms > clip on: ~ 106ms > clip on: ~ 68ms for this huge filled spiral ~ 50% faster Could you answer my previous email on EO questions ? How to deal with self intersections or is it possible to skip left segments in the EO case or not ? (I am a bit lost) I need a simple path to test clipping with the EO rule (redudant segments); any idea ? Cheers, Laurent