| /* |
| * Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package sun.java2d.marlin; |
| |
| import sun.awt.geom.PathConsumer2D; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.Path2D; |
| import java.util.Arrays; |
| import sun.java2d.marlin.Helpers.IndexStack; |
| import sun.java2d.marlin.Helpers.PolyStack; |
| |
| final class TransformingPathConsumer2D { |
| |
| // higher uncertainty in float variant for huge shapes > 10^7 |
| static final float CLIP_RECT_PADDING = 1.0f; |
| |
| private final RendererContext rdrCtx; |
| |
| // recycled ClosedPathDetector instance from detectClosedPath() |
| private final ClosedPathDetector cpDetector; |
| |
| // recycled PathClipFilter instance from pathClipper() |
| private final PathClipFilter pathClipper; |
| |
| // recycled PathConsumer2D instance from wrapPath2D() |
| private final Path2DWrapper wp_Path2DWrapper = new Path2DWrapper(); |
| |
| // recycled PathConsumer2D instances from deltaTransformConsumer() |
| private final DeltaScaleFilter dt_DeltaScaleFilter = new DeltaScaleFilter(); |
| private final DeltaTransformFilter dt_DeltaTransformFilter = new DeltaTransformFilter(); |
| |
| // recycled PathConsumer2D instances from inverseDeltaTransformConsumer() |
| private final DeltaScaleFilter iv_DeltaScaleFilter = new DeltaScaleFilter(); |
| private final DeltaTransformFilter iv_DeltaTransformFilter = new DeltaTransformFilter(); |
| |
| // recycled PathTracer instances from tracer...() methods |
| private final PathTracer tracerInput = new PathTracer("[Input]"); |
| private final PathTracer tracerCPDetector = new PathTracer("ClosedPathDetector"); |
| private final PathTracer tracerFiller = new PathTracer("Filler"); |
| private final PathTracer tracerStroker = new PathTracer("Stroker"); |
| private final PathTracer tracerDasher = new PathTracer("Dasher"); |
| |
| TransformingPathConsumer2D(final RendererContext rdrCtx) { |
| // used by RendererContext |
| this.rdrCtx = rdrCtx; |
| this.cpDetector = new ClosedPathDetector(rdrCtx); |
| this.pathClipper = new PathClipFilter(rdrCtx); |
| } |
| |
| PathConsumer2D wrapPath2D(Path2D.Float p2d) { |
| return wp_Path2DWrapper.init(p2d); |
| } |
| |
| PathConsumer2D traceInput(PathConsumer2D out) { |
| return tracerInput.init(out); |
| } |
| |
| PathConsumer2D traceClosedPathDetector(PathConsumer2D out) { |
| return tracerCPDetector.init(out); |
| } |
| |
| PathConsumer2D traceFiller(PathConsumer2D out) { |
| return tracerFiller.init(out); |
| } |
| |
| PathConsumer2D traceStroker(PathConsumer2D out) { |
| return tracerStroker.init(out); |
| } |
| |
| PathConsumer2D traceDasher(PathConsumer2D out) { |
| return tracerDasher.init(out); |
| } |
| |
| PathConsumer2D detectClosedPath(PathConsumer2D out) { |
| return cpDetector.init(out); |
| } |
| |
| PathConsumer2D pathClipper(PathConsumer2D out) { |
| return pathClipper.init(out); |
| } |
| |
| PathConsumer2D deltaTransformConsumer(PathConsumer2D out, |
| AffineTransform at) |
| { |
| if (at == null) { |
| return out; |
| } |
| final float mxx = (float) at.getScaleX(); |
| final float mxy = (float) at.getShearX(); |
| final float myx = (float) at.getShearY(); |
| final float myy = (float) at.getScaleY(); |
| |
| if (mxy == 0.0f && myx == 0.0f) { |
| if (mxx == 1.0f && myy == 1.0f) { |
| return out; |
| } else { |
| // Scale only |
| if (rdrCtx.doClip) { |
| // adjust clip rectangle (ymin, ymax, xmin, xmax): |
| adjustClipScale(rdrCtx.clipRect, mxx, myy); |
| } |
| return dt_DeltaScaleFilter.init(out, mxx, myy); |
| } |
| } else { |
| if (rdrCtx.doClip) { |
| // adjust clip rectangle (ymin, ymax, xmin, xmax): |
| adjustClipInverseDelta(rdrCtx.clipRect, mxx, mxy, myx, myy); |
| } |
| return dt_DeltaTransformFilter.init(out, mxx, mxy, myx, myy); |
| } |
| } |
| |
| private static void adjustClipOffset(final float[] clipRect) { |
| clipRect[0] += Renderer.RDR_OFFSET_Y; |
| clipRect[1] += Renderer.RDR_OFFSET_Y; |
| clipRect[2] += Renderer.RDR_OFFSET_X; |
| clipRect[3] += Renderer.RDR_OFFSET_X; |
| } |
| |
| private static void adjustClipScale(final float[] clipRect, |
| final float mxx, final float myy) |
| { |
| adjustClipOffset(clipRect); |
| |
| // Adjust the clipping rectangle (iv_DeltaScaleFilter): |
| clipRect[0] /= myy; |
| clipRect[1] /= myy; |
| clipRect[2] /= mxx; |
| clipRect[3] /= mxx; |
| } |
| |
| private static void adjustClipInverseDelta(final float[] clipRect, |
| final float mxx, final float mxy, |
| final float myx, final float myy) |
| { |
| adjustClipOffset(clipRect); |
| |
| // Adjust the clipping rectangle (iv_DeltaTransformFilter): |
| final float det = mxx * myy - mxy * myx; |
| final float imxx = myy / det; |
| final float imxy = -mxy / det; |
| final float imyx = -myx / det; |
| final float imyy = mxx / det; |
| |
| float xmin, xmax, ymin, ymax; |
| float x, y; |
| // xmin, ymin: |
| x = clipRect[2] * imxx + clipRect[0] * imxy; |
| y = clipRect[2] * imyx + clipRect[0] * imyy; |
| |
| xmin = xmax = x; |
| ymin = ymax = y; |
| |
| // xmax, ymin: |
| x = clipRect[3] * imxx + clipRect[0] * imxy; |
| y = clipRect[3] * imyx + clipRect[0] * imyy; |
| |
| if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } |
| if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } |
| |
| // xmin, ymax: |
| x = clipRect[2] * imxx + clipRect[1] * imxy; |
| y = clipRect[2] * imyx + clipRect[1] * imyy; |
| |
| if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } |
| if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } |
| |
| // xmax, ymax: |
| x = clipRect[3] * imxx + clipRect[1] * imxy; |
| y = clipRect[3] * imyx + clipRect[1] * imyy; |
| |
| if (x < xmin) { xmin = x; } else if (x > xmax) { xmax = x; } |
| if (y < ymin) { ymin = y; } else if (y > ymax) { ymax = y; } |
| |
| clipRect[0] = ymin; |
| clipRect[1] = ymax; |
| clipRect[2] = xmin; |
| clipRect[3] = xmax; |
| } |
| |
| PathConsumer2D inverseDeltaTransformConsumer(PathConsumer2D out, |
| AffineTransform at) |
| { |
| if (at == null) { |
| return out; |
| } |
| float mxx = (float) at.getScaleX(); |
| float mxy = (float) at.getShearX(); |
| float myx = (float) at.getShearY(); |
| float myy = (float) at.getScaleY(); |
| |
| if (mxy == 0.0f && myx == 0.0f) { |
| if (mxx == 1.0f && myy == 1.0f) { |
| return out; |
| } else { |
| return iv_DeltaScaleFilter.init(out, 1.0f/mxx, 1.0f/myy); |
| } |
| } else { |
| final float det = mxx * myy - mxy * myx; |
| return iv_DeltaTransformFilter.init(out, |
| myy / det, |
| -mxy / det, |
| -myx / det, |
| mxx / det); |
| } |
| } |
| |
| static final class DeltaScaleFilter implements PathConsumer2D { |
| private PathConsumer2D out; |
| private float sx, sy; |
| |
| DeltaScaleFilter() {} |
| |
| DeltaScaleFilter init(PathConsumer2D out, |
| float mxx, float myy) |
| { |
| this.out = out; |
| sx = mxx; |
| sy = myy; |
| return this; // fluent API |
| } |
| |
| @Override |
| public void moveTo(float x0, float y0) { |
| out.moveTo(x0 * sx, y0 * sy); |
| } |
| |
| @Override |
| public void lineTo(float x1, float y1) { |
| out.lineTo(x1 * sx, y1 * sy); |
| } |
| |
| @Override |
| public void quadTo(float x1, float y1, |
| float x2, float y2) |
| { |
| out.quadTo(x1 * sx, y1 * sy, |
| x2 * sx, y2 * sy); |
| } |
| |
| @Override |
| public void curveTo(float x1, float y1, |
| float x2, float y2, |
| float x3, float y3) |
| { |
| out.curveTo(x1 * sx, y1 * sy, |
| x2 * sx, y2 * sy, |
| x3 * sx, y3 * sy); |
| } |
| |
| @Override |
| public void closePath() { |
| out.closePath(); |
| } |
| |
| @Override |
| public void pathDone() { |
| out.pathDone(); |
| } |
| |
| @Override |
| public long getNativeConsumer() { |
| return 0; |
| } |
| } |
| |
| static final class DeltaTransformFilter implements PathConsumer2D { |
| private PathConsumer2D out; |
| private float mxx, mxy, myx, myy; |
| |
| DeltaTransformFilter() {} |
| |
| DeltaTransformFilter init(PathConsumer2D out, |
| float mxx, float mxy, |
| float myx, float myy) |
| { |
| this.out = out; |
| this.mxx = mxx; |
| this.mxy = mxy; |
| this.myx = myx; |
| this.myy = myy; |
| return this; // fluent API |
| } |
| |
| @Override |
| public void moveTo(float x0, float y0) { |
| out.moveTo(x0 * mxx + y0 * mxy, |
| x0 * myx + y0 * myy); |
| } |
| |
| @Override |
| public void lineTo(float x1, float y1) { |
| out.lineTo(x1 * mxx + y1 * mxy, |
| x1 * myx + y1 * myy); |
| } |
| |
| @Override |
| public void quadTo(float x1, float y1, |
| float x2, float y2) |
| { |
| out.quadTo(x1 * mxx + y1 * mxy, |
| x1 * myx + y1 * myy, |
| x2 * mxx + y2 * mxy, |
| x2 * myx + y2 * myy); |
| } |
| |
| @Override |
| public void curveTo(float x1, float y1, |
| float x2, float y2, |
| float x3, float y3) |
| { |
| out.curveTo(x1 * mxx + y1 * mxy, |
| x1 * myx + y1 * myy, |
| x2 * mxx + y2 * mxy, |
| x2 * myx + y2 * myy, |
| x3 * mxx + y3 * mxy, |
| x3 * myx + y3 * myy); |
| } |
| |
| @Override |
| public void closePath() { |
| out.closePath(); |
| } |
| |
| @Override |
| public void pathDone() { |
| out.pathDone(); |
| } |
| |
| @Override |
| public long getNativeConsumer() { |
| return 0; |
| } |
| } |
| |
| static final class Path2DWrapper implements PathConsumer2D { |
| private Path2D.Float p2d; |
| |
| Path2DWrapper() {} |
| |
| Path2DWrapper init(Path2D.Float p2d) { |
| this.p2d = p2d; |
| return this; |
| } |
| |
| @Override |
| public void moveTo(float x0, float y0) { |
| p2d.moveTo(x0, y0); |
| } |
| |
| @Override |
| public void lineTo(float x1, float y1) { |
| p2d.lineTo(x1, y1); |
| } |
| |
| @Override |
| public void closePath() { |
| p2d.closePath(); |
| } |
| |
| @Override |
| public void pathDone() {} |
| |
| @Override |
| public void curveTo(float x1, float y1, |
| float x2, float y2, |
| float x3, float y3) |
| { |
| p2d.curveTo(x1, y1, x2, y2, x3, y3); |
| } |
| |
| @Override |
| public void quadTo(float x1, float y1, float x2, float y2) { |
| p2d.quadTo(x1, y1, x2, y2); |
| } |
| |
| @Override |
| public long getNativeConsumer() { |
| throw new InternalError("Not using a native peer"); |
| } |
| } |
| |
| static final class ClosedPathDetector implements PathConsumer2D { |
| |
| private final RendererContext rdrCtx; |
| private final PolyStack stack; |
| |
| private PathConsumer2D out; |
| |
| ClosedPathDetector(final RendererContext rdrCtx) { |
| this.rdrCtx = rdrCtx; |
| this.stack = (rdrCtx.stats != null) ? |
| new PolyStack(rdrCtx, |
| rdrCtx.stats.stat_cpd_polystack_types, |
| rdrCtx.stats.stat_cpd_polystack_curves, |
| rdrCtx.stats.hist_cpd_polystack_curves, |
| rdrCtx.stats.stat_array_cpd_polystack_curves, |
| rdrCtx.stats.stat_array_cpd_polystack_types) |
| : new PolyStack(rdrCtx); |
| } |
| |
| ClosedPathDetector init(PathConsumer2D out) { |
| this.out = out; |
| return this; // fluent API |
| } |
| |
| /** |
| * Disposes this instance: |
| * clean up before reusing this instance |
| */ |
| void dispose() { |
| stack.dispose(); |
| } |
| |
| @Override |
| public void pathDone() { |
| // previous path is not closed: |
| finish(false); |
| out.pathDone(); |
| |
| // TODO: fix possible leak if exception happened |
| // Dispose this instance: |
| dispose(); |
| } |
| |
| @Override |
| public void closePath() { |
| // path is closed |
| finish(true); |
| out.closePath(); |
| } |
| |
| @Override |
| public void moveTo(float x0, float y0) { |
| // previous path is not closed: |
| finish(false); |
| out.moveTo(x0, y0); |
| } |
| |
| private void finish(final boolean closed) { |
| rdrCtx.closedPath = closed; |
| stack.pullAll(out); |
| } |
| |
| @Override |
| public void lineTo(float x1, float y1) { |
| stack.pushLine(x1, y1); |
| } |
| |
| @Override |
| public void curveTo(float x3, float y3, |
| float x2, float y2, |
| float x1, float y1) |
| { |
| stack.pushCubic(x1, y1, x2, y2, x3, y3); |
| } |
| |
| @Override |
| public void quadTo(float x2, float y2, float x1, float y1) { |
| stack.pushQuad(x1, y1, x2, y2); |
| } |
| |
| @Override |
| public long getNativeConsumer() { |
| throw new InternalError("Not using a native peer"); |
| } |
| } |
| |
| static final class PathClipFilter implements PathConsumer2D { |
| |
| private PathConsumer2D out; |
| |
| // Bounds of the drawing region, at pixel precision. |
| private final float[] clipRect; |
| |
| private final float[] corners = new float[8]; |
| private boolean init_corners = false; |
| |
| private final IndexStack stack; |
| |
| // the current outcode of the current sub path |
| private int cOutCode = 0; |
| |
| // the cumulated (and) outcode of the complete path |
| private int gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; |
| |
| private boolean outside = false; |
| |
| // The current point (TODO stupid repeated info) |
| private float cx0, cy0; |
| |
| // The current point OUTSIDE |
| private float cox0, coy0; |
| |
| private boolean subdivide = MarlinConst.DO_CLIP_SUBDIVIDER; |
| private final CurveClipSplitter curveSplitter; |
| |
| PathClipFilter(final RendererContext rdrCtx) { |
| this.clipRect = rdrCtx.clipRect; |
| this.curveSplitter = rdrCtx.curveClipSplitter; |
| |
| 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 PathConsumer2D out) { |
| this.out = out; |
| |
| // Adjust the clipping rectangle with the renderer offsets |
| final float rdrOffX = Renderer.RDR_OFFSET_X; |
| final float rdrOffY = Renderer.RDR_OFFSET_Y; |
| |
| // add a small rounding error: |
| final float margin = 1e-3f; |
| |
| final float[] _clipRect = this.clipRect; |
| _clipRect[0] -= margin - rdrOffY; |
| _clipRect[1] += margin + rdrOffY; |
| _clipRect[2] -= margin - rdrOffX; |
| _clipRect[3] += margin + rdrOffX; |
| |
| if (MarlinConst.DO_CLIP_SUBDIVIDER) { |
| // adjust padded clip rectangle: |
| curveSplitter.init(); |
| } |
| |
| this.init_corners = true; |
| this.gOutCode = MarlinConst.OUTCODE_MASK_T_B_L_R; |
| |
| return this; // fluent API |
| } |
| |
| /** |
| * Disposes this instance: |
| * clean up before reusing this instance |
| */ |
| void dispose() { |
| stack.dispose(); |
| } |
| |
| private void finishPath() { |
| if (outside) { |
| // criteria: inside or totally outside ? |
| if (gOutCode == 0) { |
| finish(); |
| } else { |
| this.outside = false; |
| stack.reset(); |
| } |
| } |
| } |
| |
| private void finish() { |
| this.outside = false; |
| |
| if (!stack.isEmpty()) { |
| if (init_corners) { |
| init_corners = false; |
| |
| final float[] _corners = corners; |
| final float[] _clipRect = clipRect; |
| // 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(cox0, coy0); |
| this.cx0 = cox0; |
| this.cy0 = coy0; |
| } |
| |
| @Override |
| public void pathDone() { |
| finishPath(); |
| |
| out.pathDone(); |
| |
| // TODO: fix possible leak if exception happened |
| // Dispose this instance: |
| dispose(); |
| } |
| |
| @Override |
| public void closePath() { |
| finishPath(); |
| |
| out.closePath(); |
| } |
| |
| @Override |
| public void moveTo(final float x0, final float y0) { |
| finishPath(); |
| |
| this.cOutCode = Helpers.outcode(x0, y0, clipRect); |
| this.outside = false; |
| out.moveTo(x0, y0); |
| this.cx0 = x0; |
| this.cy0 = y0; |
| } |
| |
| @Override |
| public void lineTo(final float xe, final float ye) { |
| final int outcode0 = this.cOutCode; |
| final int outcode1 = Helpers.outcode(xe, ye, clipRect); |
| |
| // Should clip |
| final int orCode = (outcode0 | outcode1); |
| if (orCode != 0) { |
| final int sideCode = (outcode0 & outcode1); |
| |
| // basic rejection criteria: |
| if (sideCode == 0) { |
| // ovelap clip: |
| if (subdivide) { |
| // avoid reentrance |
| subdivide = false; |
| boolean ret; |
| // subdivide curve => callback with subdivided parts: |
| if (outside) { |
| ret = curveSplitter.splitLine(cox0, coy0, xe, ye, |
| orCode, this); |
| } else { |
| ret = curveSplitter.splitLine(cx0, cy0, xe, ye, |
| orCode, this); |
| } |
| // reentrance is done: |
| subdivide = true; |
| if (ret) { |
| return; |
| } |
| } |
| // already subdivided so render it |
| } else { |
| this.cOutCode = outcode1; |
| this.gOutCode &= sideCode; |
| // keep last point coordinate before entering the clip again: |
| this.outside = true; |
| this.cox0 = xe; |
| this.coy0 = ye; |
| |
| clip(sideCode, outcode0, outcode1); |
| return; |
| } |
| } |
| |
| this.cOutCode = outcode1; |
| this.gOutCode = 0; |
| |
| if (outside) { |
| finish(); |
| } |
| // clipping disabled: |
| out.lineTo(xe, ye); |
| this.cx0 = xe; |
| this.cy0 = 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 & MarlinConst.OUTCODE_MASK_L_R) != 0)) |
| { |
| // combine outcodes: |
| final int mergeCode = (outcode0 | outcode1); |
| final int tbCode = mergeCode & MarlinConst.OUTCODE_MASK_T_B; |
| final int lrCode = mergeCode & MarlinConst.OUTCODE_MASK_L_R; |
| final int off = (lrCode == MarlinConst.OUTCODE_LEFT) ? 0 : 2; |
| |
| // add corners to outside stack: |
| switch (tbCode) { |
| case MarlinConst.OUTCODE_TOP: |
| stack.push(off); // top |
| return; |
| case MarlinConst.OUTCODE_BOTTOM: |
| stack.push(off + 1); // bottom |
| return; |
| default: |
| // both TOP / BOTTOM: |
| if ((outcode0 & MarlinConst.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 float x1, final float y1, |
| final float x2, final float y2, |
| final float xe, final float ye) |
| { |
| final int outcode0 = this.cOutCode; |
| final int outcode1 = Helpers.outcode(x1, y1, clipRect); |
| final int outcode2 = Helpers.outcode(x2, y2, clipRect); |
| final int outcode3 = Helpers.outcode(xe, ye, clipRect); |
| |
| // Should clip |
| final int orCode = (outcode0 | outcode1 | outcode2 | outcode3); |
| if (orCode != 0) { |
| final int sideCode = outcode0 & outcode1 & outcode2 & outcode3; |
| |
| // basic rejection criteria: |
| if (sideCode == 0) { |
| // ovelap clip: |
| if (subdivide) { |
| // avoid reentrance |
| subdivide = false; |
| // subdivide curve => callback with subdivided parts: |
| boolean ret; |
| if (outside) { |
| ret = curveSplitter.splitCurve(cox0, coy0, x1, y1, |
| x2, y2, xe, ye, |
| orCode, this); |
| } else { |
| ret = curveSplitter.splitCurve(cx0, cy0, x1, y1, |
| x2, y2, xe, ye, |
| orCode, this); |
| } |
| // reentrance is done: |
| subdivide = true; |
| if (ret) { |
| return; |
| } |
| } |
| // already subdivided so render it |
| } else { |
| this.cOutCode = outcode3; |
| this.gOutCode &= sideCode; |
| // keep last point coordinate before entering the clip again: |
| this.outside = true; |
| this.cox0 = xe; |
| this.coy0 = ye; |
| |
| clip(sideCode, outcode0, outcode3); |
| return; |
| } |
| } |
| |
| this.cOutCode = outcode3; |
| this.gOutCode = 0; |
| |
| if (outside) { |
| finish(); |
| } |
| // clipping disabled: |
| out.curveTo(x1, y1, x2, y2, xe, ye); |
| this.cx0 = xe; |
| this.cy0 = ye; |
| } |
| |
| @Override |
| public void quadTo(final float x1, final float y1, |
| final float xe, final float ye) |
| { |
| final int outcode0 = this.cOutCode; |
| final int outcode1 = Helpers.outcode(x1, y1, clipRect); |
| final int outcode2 = Helpers.outcode(xe, ye, clipRect); |
| |
| // Should clip |
| final int orCode = (outcode0 | outcode1 | outcode2); |
| if (orCode != 0) { |
| final int sideCode = outcode0 & outcode1 & outcode2; |
| |
| // basic rejection criteria: |
| if (sideCode == 0) { |
| // ovelap clip: |
| if (subdivide) { |
| // avoid reentrance |
| subdivide = false; |
| // subdivide curve => callback with subdivided parts: |
| boolean ret; |
| if (outside) { |
| ret = curveSplitter.splitQuad(cox0, coy0, x1, y1, |
| xe, ye, orCode, this); |
| } else { |
| ret = curveSplitter.splitQuad(cx0, cy0, x1, y1, |
| xe, ye, orCode, this); |
| } |
| // reentrance is done: |
| subdivide = true; |
| if (ret) { |
| return; |
| } |
| } |
| // already subdivided so render it |
| } else { |
| this.cOutCode = outcode2; |
| this.gOutCode &= sideCode; |
| // keep last point coordinate before entering the clip again: |
| this.outside = true; |
| this.cox0 = xe; |
| this.coy0 = ye; |
| |
| clip(sideCode, outcode0, outcode2); |
| return; |
| } |
| } |
| |
| this.cOutCode = outcode2; |
| this.gOutCode = 0; |
| |
| if (outside) { |
| finish(); |
| } |
| // clipping disabled: |
| out.quadTo(x1, y1, xe, ye); |
| this.cx0 = xe; |
| this.cy0 = ye; |
| } |
| |
| @Override |
| public long getNativeConsumer() { |
| throw new InternalError("Not using a native peer"); |
| } |
| } |
| |
| static final class CurveClipSplitter { |
| |
| static final float LEN_TH = MarlinProperties.getSubdividerMinLength(); |
| static final boolean DO_CHECK_LENGTH = (LEN_TH > 0.0f); |
| |
| private static final boolean TRACE = false; |
| |
| private static final int MAX_N_CURVES = 3 * 4; |
| |
| // clip rectangle (ymin, ymax, xmin, xmax): |
| final float[] clipRect; |
| |
| // clip rectangle (ymin, ymax, xmin, xmax) including padding: |
| final float[] clipRectPad = new float[4]; |
| private boolean init_clipRectPad = false; |
| |
| // This is where the curve to be processed is put. We give it |
| // enough room to store all curves. |
| final float[] middle = new float[MAX_N_CURVES * 8 + 2]; |
| // t values at subdivision points |
| private final float[] subdivTs = new float[MAX_N_CURVES]; |
| |
| // dirty curve |
| private final Curve curve; |
| |
| CurveClipSplitter(final RendererContext rdrCtx) { |
| this.clipRect = rdrCtx.clipRect; |
| this.curve = rdrCtx.curve; |
| } |
| |
| void init() { |
| this.init_clipRectPad = true; |
| } |
| |
| private void initPaddedClip() { |
| // bounds as half-open intervals: minX <= x < maxX and minY <= y < maxY |
| // adjust padded clip rectangle (ymin, ymax, xmin, xmax): |
| // add a rounding error (curve subdivision ~ 0.1px): |
| final float[] _clipRect = clipRect; |
| final float[] _clipRectPad = clipRectPad; |
| |
| _clipRectPad[0] = _clipRect[0] - CLIP_RECT_PADDING; |
| _clipRectPad[1] = _clipRect[1] + CLIP_RECT_PADDING; |
| _clipRectPad[2] = _clipRect[2] - CLIP_RECT_PADDING; |
| _clipRectPad[3] = _clipRect[3] + CLIP_RECT_PADDING; |
| |
| if (TRACE) { |
| System.out.println("clip: X [" + _clipRectPad[2] + " .. " + _clipRectPad[3] +"] " |
| + "Y ["+ _clipRectPad[0] + " .. " + _clipRectPad[1] +"]"); |
| } |
| } |
| |
| boolean splitLine(final float x0, final float y0, |
| final float x1, final float y1, |
| final int outCodeOR, |
| final PathConsumer2D out) |
| { |
| if (TRACE) { |
| System.out.println("divLine P0(" + x0 + ", " + y0 + ") P1(" + x1 + ", " + y1 + ")"); |
| } |
| |
| if (DO_CHECK_LENGTH && Helpers.fastLineLen(x0, y0, x1, y1) <= LEN_TH) { |
| return false; |
| } |
| |
| final float[] mid = middle; |
| mid[0] = x0; mid[1] = y0; |
| mid[2] = x1; mid[3] = y1; |
| |
| return subdivideAtIntersections(4, outCodeOR, out); |
| } |
| |
| boolean splitQuad(final float x0, final float y0, |
| final float x1, final float y1, |
| final float x2, final float y2, |
| final int outCodeOR, |
| final PathConsumer2D out) |
| { |
| if (TRACE) { |
| System.out.println("divQuad P0(" + x0 + ", " + y0 + ") P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ")"); |
| } |
| |
| if (DO_CHECK_LENGTH && Helpers.fastQuadLen(x0, y0, x1, y1, x2, y2) <= LEN_TH) { |
| return false; |
| } |
| |
| final float[] mid = middle; |
| mid[0] = x0; mid[1] = y0; |
| mid[2] = x1; mid[3] = y1; |
| mid[4] = x2; mid[5] = y2; |
| |
| return subdivideAtIntersections(6, outCodeOR, out); |
| } |
| |
| boolean splitCurve(final float x0, final float y0, |
| final float x1, final float y1, |
| final float x2, final float y2, |
| final float x3, final float y3, |
| final int outCodeOR, |
| final PathConsumer2D out) |
| { |
| if (TRACE) { |
| System.out.println("divCurve P0(" + x0 + ", " + y0 + ") P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + x3 + ", " + y3 + ")"); |
| } |
| |
| if (DO_CHECK_LENGTH && Helpers.fastCurvelen(x0, y0, x1, y1, x2, y2, x3, y3) <= LEN_TH) { |
| return false; |
| } |
| |
| final float[] mid = middle; |
| mid[0] = x0; mid[1] = y0; |
| mid[2] = x1; mid[3] = y1; |
| mid[4] = x2; mid[5] = y2; |
| mid[6] = x3; mid[7] = y3; |
| |
| return subdivideAtIntersections(8, outCodeOR, out); |
| } |
| |
| private boolean subdivideAtIntersections(final int type, final int outCodeOR, |
| final PathConsumer2D out) |
| { |
| final float[] mid = middle; |
| final float[] subTs = subdivTs; |
| |
| if (init_clipRectPad) { |
| init_clipRectPad = false; |
| initPaddedClip(); |
| } |
| |
| final int nSplits = Helpers.findClipPoints(curve, mid, subTs, type, |
| outCodeOR, clipRectPad); |
| |
| if (TRACE) { |
| System.out.println("nSplits: "+ nSplits); |
| System.out.println("subTs: "+Arrays.toString(Arrays.copyOfRange(subTs, 0, nSplits))); |
| } |
| if (nSplits == 0) { |
| // only curve support shortcut |
| return false; |
| } |
| float prevT = 0.0f; |
| |
| for (int i = 0, off = 0; i < nSplits; i++, off += type) { |
| final float t = subTs[i]; |
| |
| Helpers.subdivideAt((t - prevT) / (1.0f - prevT), |
| mid, off, mid, off, type); |
| prevT = t; |
| } |
| |
| for (int i = 0, off = 0; i <= nSplits; i++, off += type) { |
| if (TRACE) { |
| System.out.println("Part Curve "+Arrays.toString(Arrays.copyOfRange(mid, off, off + type))); |
| } |
| emitCurrent(type, mid, off, out); |
| } |
| return true; |
| } |
| |
| static void emitCurrent(final int type, final float[] pts, |
| final int off, final PathConsumer2D out) |
| { |
| // if instead of switch (perf + most probable cases first) |
| if (type == 8) { |
| out.curveTo(pts[off + 2], pts[off + 3], |
| pts[off + 4], pts[off + 5], |
| pts[off + 6], pts[off + 7]); |
| } else if (type == 4) { |
| out.lineTo(pts[off + 2], pts[off + 3]); |
| } else { |
| out.quadTo(pts[off + 2], pts[off + 3], |
| pts[off + 4], pts[off + 5]); |
| } |
| } |
| } |
| |
| static final class CurveBasicMonotonizer { |
| |
| private static final int MAX_N_CURVES = 11; |
| |
| // squared half line width (for stroker) |
| private float lw2; |
| |
| // number of splitted curves |
| int nbSplits; |
| |
| // This is where the curve to be processed is put. We give it |
| // enough room to store all curves. |
| final float[] middle = new float[MAX_N_CURVES * 6 + 2]; |
| // t values at subdivision points |
| private final float[] subdivTs = new float[MAX_N_CURVES - 1]; |
| |
| // dirty curve |
| private final Curve curve; |
| |
| CurveBasicMonotonizer(final RendererContext rdrCtx) { |
| this.curve = rdrCtx.curve; |
| } |
| |
| void init(final float lineWidth) { |
| this.lw2 = (lineWidth * lineWidth) / 4.0f; |
| } |
| |
| CurveBasicMonotonizer curve(final float x0, final float y0, |
| final float x1, final float y1, |
| final float x2, final float y2, |
| final float x3, final float y3) |
| { |
| final float[] mid = middle; |
| mid[0] = x0; mid[1] = y0; |
| mid[2] = x1; mid[3] = y1; |
| mid[4] = x2; mid[5] = y2; |
| mid[6] = x3; mid[7] = y3; |
| |
| final float[] subTs = subdivTs; |
| final int nSplits = Helpers.findSubdivPoints(curve, mid, subTs, 8, lw2); |
| |
| float prevT = 0.0f; |
| for (int i = 0, off = 0; i < nSplits; i++, off += 6) { |
| final float t = subTs[i]; |
| |
| Helpers.subdivideCubicAt((t - prevT) / (1.0f - prevT), |
| mid, off, mid, off, off + 6); |
| prevT = t; |
| } |
| |
| this.nbSplits = nSplits; |
| return this; |
| } |
| |
| CurveBasicMonotonizer quad(final float x0, final float y0, |
| final float x1, final float y1, |
| final float x2, final float y2) |
| { |
| final float[] mid = middle; |
| mid[0] = x0; mid[1] = y0; |
| mid[2] = x1; mid[3] = y1; |
| mid[4] = x2; mid[5] = y2; |
| |
| final float[] subTs = subdivTs; |
| final int nSplits = Helpers.findSubdivPoints(curve, mid, subTs, 6, lw2); |
| |
| float prevt = 0.0f; |
| for (int i = 0, off = 0; i < nSplits; i++, off += 4) { |
| final float t = subTs[i]; |
| Helpers.subdivideQuadAt((t - prevt) / (1.0f - prevt), |
| mid, off, mid, off, off + 4); |
| prevt = t; |
| } |
| |
| this.nbSplits = nSplits; |
| return this; |
| } |
| } |
| |
| static final class PathTracer implements PathConsumer2D { |
| private final String prefix; |
| private PathConsumer2D out; |
| |
| PathTracer(String name) { |
| this.prefix = name + ": "; |
| } |
| |
| PathTracer init(PathConsumer2D out) { |
| this.out = out; |
| return this; // fluent API |
| } |
| |
| @Override |
| public void moveTo(float x0, float y0) { |
| log("moveTo (" + x0 + ", " + y0 + ')'); |
| out.moveTo(x0, y0); |
| } |
| |
| @Override |
| public void lineTo(float x1, float y1) { |
| log("lineTo (" + x1 + ", " + y1 + ')'); |
| out.lineTo(x1, y1); |
| } |
| |
| @Override |
| public void curveTo(float x1, float y1, |
| float x2, float y2, |
| float x3, float y3) |
| { |
| log("curveTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ") P3(" + x3 + ", " + y3 + ')'); |
| out.curveTo(x1, y1, x2, y2, x3, y3); |
| } |
| |
| @Override |
| public void quadTo(float x1, float y1, float x2, float y2) { |
| log("quadTo P1(" + x1 + ", " + y1 + ") P2(" + x2 + ", " + y2 + ')'); |
| out.quadTo(x1, y1, x2, y2); |
| } |
| |
| @Override |
| public void closePath() { |
| log("closePath"); |
| out.closePath(); |
| } |
| |
| @Override |
| public void pathDone() { |
| log("pathDone"); |
| out.pathDone(); |
| } |
| |
| private void log(final String message) { |
| System.out.println(prefix + message); |
| } |
| |
| @Override |
| public long getNativeConsumer() { |
| throw new InternalError("Not using a native peer"); |
| } |
| } |
| } |