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

Reply via email to