/*
* Copyright (c) 2011, 2018, 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 com.sun.prism.impl.shape;
import com.sun.javafx.geom.PathConsumer2D;
import com.sun.javafx.geom.PathIterator;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.Rectangle;
import com.sun.javafx.geom.Shape;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.marlin.MarlinConst;
import com.sun.marlin.MarlinProperties;
import com.sun.marlin.MarlinRenderer;
import com.sun.marlin.MarlinUtils;
import com.sun.marlin.RendererContext;
import com.sun.marlin.Stroker;
import com.sun.marlin.TransformingPathConsumer2D;
import com.sun.prism.BasicStroke;
import java.util.Arrays;
public final class MarlinPrismUtils {
private static final boolean FORCE_NO_AA = false;
// slightly slower ~2% if enabled stroker clipping (lines) but skipping cap / join handling is few percents faster in specific cases
static final boolean DISABLE_2ND_STROKER_CLIPPING = true;
static final boolean DO_TRACE_PATH = false;
static final boolean DO_CLIP = MarlinProperties.isDoClip();
static final boolean DO_CLIP_FILL = true;
static final boolean DO_CLIP_RUNTIME_ENABLE = MarlinProperties.isDoClipRuntimeFlag();
static final float UPPER_BND = Float.MAX_VALUE / 2.0f;
static final float LOWER_BND = -UPPER_BND;
Private constructor to prevent instantiation.
/**
* Private constructor to prevent instantiation.
*/
private MarlinPrismUtils() {
}
private static PathConsumer2D initStroker(
final RendererContext rdrCtx,
final BasicStroke stroke,
final float lineWidth,
BaseTransform tx,
final PathConsumer2D out)
{
// We use strokerat so that in Stroker and Dasher we can work only
// with the pre-transformation coordinates. This will repeat a lot of
// computations done in the path iterator, but the alternative is to
// work with transformed paths and compute untransformed coordinates
// as needed. This would be faster but I do not think the complexity
// of working with both untransformed and transformed coordinates in
// the same code is worth it.
// However, if a path's width is constant after a transformation,
// we can skip all this untransforming.
// As pathTo() will check transformed coordinates for invalid values
// (NaN / Infinity) to ignore such points, it is necessary to apply the
// transformation before the path processing.
BaseTransform strokerTx = null;
int dashLen = -1;
boolean recycleDashes = false;
float width = lineWidth;
float[] dashes = stroke.getDashArray();
float dashphase = stroke.getDashPhase();
if ((tx != null) && !tx.isIdentity()) {
final double a = tx.getMxx();
final double b = tx.getMxy();
final double c = tx.getMyx();
final double d = tx.getMyy();
// If the transform is a constant multiple of an orthogonal transformation
// then every length is just multiplied by a constant, so we just
// need to transform input paths to stroker and tell stroker
// the scaled width. This condition is satisfied if
// a*b == -c*d && a*a+c*c == b*b+d*d. In the actual check below, we
// leave a bit of room for error.
if (nearZero(a*b + c*d) && nearZero(a*a + c*c - (b*b + d*d))) {
final float scale = (float) Math.sqrt(a*a + c*c);
if (dashes != null) {
recycleDashes = true;
dashLen = dashes.length;
dashes = rdrCtx.dasher.copyDashArray(dashes);
for (int i = 0; i < dashLen; i++) {
dashes[i] *= scale;
}
dashphase *= scale;
}
width *= scale;
// by now strokerat == null. Input paths to
// stroker (and maybe dasher) will have the full transform tx
// applied to them and nothing will happen to the output paths.
} else {
strokerTx = tx;
// by now strokerat == tx. Input paths to
// stroker (and maybe dasher) will have the full transform tx
// applied to them, then they will be normalized, and then
// the inverse of *only the non translation part of tx* will
// be applied to the normalized paths. This won't cause problems
// in stroker, because, suppose tx = T*A, where T is just the
// translation part of tx, and A is the rest. T*A has already
// been applied to Stroker/Dasher's input. Then Ainv will be
// applied. Ainv*T*A is not equal to T, but it is a translation,
// which means that none of stroker's assumptions about its
// input will be violated. After all this, A will be applied
// to stroker's output.
}
} else {
// either tx is null or it's the identity. In either case
// we don't transform the path.
tx = null;
}
// Prepare the pipeline:
PathConsumer2D pc = out;
final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
if (DO_TRACE_PATH) {
// trace Stroker:
pc = transformerPC2D.traceStroker(pc);
}
if (MarlinConst.USE_SIMPLIFIER) {
// Use simplifier after stroker before Renderer
// to remove collinear segments (notably due to cap square)
pc = rdrCtx.simplifier.init(pc);
}
// deltaTransformConsumer may adjust the clip rectangle:
pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx);
// stroker will adjust the clip rectangle (width / miter limit):
pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(),
stroke.getLineJoin(), stroke.getMiterLimit(),
(dashes == null));
// Curve Monotizer:
rdrCtx.monotonizer.init(width);
if (dashes != null) {
if (!recycleDashes) {
dashLen = dashes.length;
}
if (DO_TRACE_PATH) {
pc = transformerPC2D.traceDasher(pc);
}
pc = rdrCtx.dasher.init(pc, dashes, dashLen, dashphase,
recycleDashes);
if (DISABLE_2ND_STROKER_CLIPPING) {
// disable stoker clipping:
rdrCtx.stroker.disableClipping();
}
} else if (rdrCtx.doClip && (stroke.getEndCap() != Stroker.CAP_BUTT)) {
if (DO_TRACE_PATH) {
pc = transformerPC2D.traceClosedPathDetector(pc);
}
// If no dash and clip is enabled:
// detect closedPaths (polygons) for caps
pc = transformerPC2D.detectClosedPath(pc);
}
pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx);
if (DO_TRACE_PATH) {
// trace Input:
pc = transformerPC2D.traceInput(pc);
}
/*
* Pipeline seems to be:
* shape.getPathIterator(tx)
* -> (inverseDeltaTransformConsumer)
* -> (Dasher)
* -> Stroker
* -> (deltaTransformConsumer)
*
* -> (CollinearSimplifier) to remove redundant segments
*
* -> pc2d = Renderer (bounding box)
*/
return pc;
}
private static boolean nearZero(final double num) {
return Math.abs(num) < 2.0d * Math.ulp(num);
}
private static PathConsumer2D initRenderer(
final RendererContext rdrCtx,
final BasicStroke stroke,
final BaseTransform tx,
final Rectangle clip,
final int piRule,
final MarlinRenderer renderer)
{
if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) {
// Define the initial clip bounds:
final float[] clipRect = rdrCtx.clipRect;
// Adjust the clipping rectangle with the renderer offsets
final float rdrOffX = renderer.getOffsetX();
final float rdrOffY = renderer.getOffsetY();
// add a small rounding error:
final float margin = 1e-3f;
clipRect[0] = clip.y
- margin + rdrOffY;
clipRect[1] = clip.y + clip.height
+ margin + rdrOffY;
clipRect[2] = clip.x
- margin + rdrOffX;
clipRect[3] = clip.x + clip.width
+ margin + rdrOffX;
if (MarlinConst.DO_LOG_CLIP) {
MarlinUtils.logInfo("clipRect (clip): "
+ Arrays.toString(rdrCtx.clipRect));
}
// Enable clipping:
rdrCtx.doClip = true;
}
if (stroke != null) {
renderer.init(clip.x, clip.y, clip.width, clip.height,
MarlinConst.WIND_NON_ZERO);
return initStroker(rdrCtx, stroke, stroke.getLineWidth(), tx, renderer);
} else {
// Filler:
final int oprule = (piRule == PathIterator.WIND_EVEN_ODD) ?
MarlinConst.WIND_EVEN_ODD : MarlinConst.WIND_NON_ZERO;
renderer.init(clip.x, clip.y, clip.width, clip.height, oprule);
PathConsumer2D pc = renderer;
final TransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
if (DO_CLIP_FILL && rdrCtx.doClip) {
if (DO_TRACE_PATH) {
// trace Filler:
pc = rdrCtx.transformerPC2D.traceFiller(pc);
}
pc = rdrCtx.transformerPC2D.pathClipper(pc);
}
if (DO_TRACE_PATH) {
// trace Input:
pc = transformerPC2D.traceInput(pc);
}
return pc;
}
}
public static MarlinRenderer setupRenderer(
final RendererContext rdrCtx,
final Shape shape,
final BasicStroke stroke,
final BaseTransform xform,
final Rectangle rclip,
final boolean antialiasedShape)
{
// Test if transform is identity:
final BaseTransform tf = ((xform != null) && !xform.isIdentity()) ? xform : null;
final MarlinRenderer r = (!FORCE_NO_AA && antialiasedShape) ?
rdrCtx.renderer : rdrCtx.getRendererNoAA();
if (shape instanceof Path2D) {
final Path2D p2d = (Path2D)shape;
final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, p2d.getWindingRule(), r);
feedConsumer(rdrCtx, p2d, tf, pc2d);
} else {
final PathIterator pi = shape.getPathIterator(tf);
final PathConsumer2D pc2d = initRenderer(rdrCtx, stroke, tf, rclip, pi.getWindingRule(), r);
feedConsumer(rdrCtx, pi, pc2d);
}
return r;
}
public static void strokeTo(
final RendererContext rdrCtx,
final Shape shape,
final BasicStroke stroke,
final float lineWidth,
final PathConsumer2D out)
{
final PathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out);
if (shape instanceof Path2D) {
feedConsumer(rdrCtx, (Path2D)shape, null, pc2d);
} else {
feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d);
}
}
private static void feedConsumer(final RendererContext rdrCtx, final PathIterator pi,
PathConsumer2D pc2d)
{
if (MarlinConst.USE_PATH_SIMPLIFIER) {
// Use path simplifier at the first step
// to remove useless points
pc2d = rdrCtx.pathSimplifier.init(pc2d);
}
// mark context as DIRTY:
rdrCtx.dirty = true;
final float[] coords = rdrCtx.float6;
// ported from DuctusRenderingEngine.feedConsumer() but simplified:
// - removed skip flag = !subpathStarted
// - removed pathClosed (ie subpathStarted not set to false)
boolean subpathStarted = false;
for (; !pi.isDone(); pi.next()) {
switch (pi.currentSegment(coords)) {
case PathIterator.SEG_MOVETO:
/* Checking SEG_MOVETO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Skipping next path segment in case of
* invalid data.
*/
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND)
{
pc2d.moveTo(coords[0], coords[1]);
subpathStarted = true;
}
break;
case PathIterator.SEG_LINETO:
/* Checking SEG_LINETO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Ignoring current path segment in case
* of invalid data. If segment is skipped its endpoint
* (if valid) is used to begin new subpath.
*/
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND)
{
if (subpathStarted) {
pc2d.lineTo(coords[0], coords[1]);
} else {
pc2d.moveTo(coords[0], coords[1]);
subpathStarted = true;
}
}
break;
case PathIterator.SEG_QUADTO:
// Quadratic curves take two points
/* Checking SEG_QUADTO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Ignoring current path segment in case
* of invalid endpoints's data. Equivalent to the SEG_LINETO
* if endpoint coordinates are valid but there are invalid data
* among other coordinates
*/
if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
coords[3] < UPPER_BND && coords[3] > LOWER_BND)
{
if (subpathStarted) {
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND)
{
pc2d.quadTo(coords[0], coords[1],
coords[2], coords[3]);
} else {
pc2d.lineTo(coords[2], coords[3]);
}
} else {
pc2d.moveTo(coords[2], coords[3]);
subpathStarted = true;
}
}
break;
case PathIterator.SEG_CUBICTO:
// Cubic curves take three points
/* Checking SEG_CUBICTO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Ignoring current path segment in case
* of invalid endpoints's data. Equivalent to the SEG_LINETO
* if endpoint coordinates are valid but there are invalid data
* among other coordinates
*/
if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
coords[5] < UPPER_BND && coords[5] > LOWER_BND)
{
if (subpathStarted) {
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
coords[3] < UPPER_BND && coords[3] > LOWER_BND)
{
pc2d.curveTo(coords[0], coords[1],
coords[2], coords[3],
coords[4], coords[5]);
} else {
pc2d.lineTo(coords[4], coords[5]);
}
} else {
pc2d.moveTo(coords[4], coords[5]);
subpathStarted = true;
}
}
break;
case PathIterator.SEG_CLOSE:
if (subpathStarted) {
pc2d.closePath();
// do not set subpathStarted to false
// in case of missing moveTo() after close()
}
break;
default:
}
}
pc2d.pathDone();
// mark context as CLEAN:
rdrCtx.dirty = false;
}
private static void feedConsumer(final RendererContext rdrCtx,
final Path2D p2d,
final BaseTransform xform,
PathConsumer2D pc2d)
{
if (MarlinConst.USE_PATH_SIMPLIFIER) {
// Use path simplifier at the first step
// to remove useless points
pc2d = rdrCtx.pathSimplifier.init(pc2d);
}
// mark context as DIRTY:
rdrCtx.dirty = true;
final float[] coords = rdrCtx.float6;
// ported from DuctusRenderingEngine.feedConsumer() but simplified:
// - removed skip flag = !subpathStarted
// - removed pathClosed (ie subpathStarted not set to false)
boolean subpathStarted = false;
final float[] pCoords = p2d.getFloatCoordsNoClone();
final byte[] pTypes = p2d.getCommandsNoClone();
final int nsegs = p2d.getNumCommands();
for (int i = 0, coff = 0; i < nsegs; i++) {
switch (pTypes[i]) {
case PathIterator.SEG_MOVETO:
if (xform == null) {
coords[0] = pCoords[coff];
coords[1] = pCoords[coff+1];
} else {
xform.transform(pCoords, coff, coords, 0, 1);
}
coff += 2;
/* Checking SEG_MOVETO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Skipping next path segment in case of
* invalid data.
*/
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND)
{
pc2d.moveTo(coords[0], coords[1]);
subpathStarted = true;
}
break;
case PathIterator.SEG_LINETO:
if (xform == null) {
coords[0] = pCoords[coff];
coords[1] = pCoords[coff+1];
} else {
xform.transform(pCoords, coff, coords, 0, 1);
}
coff += 2;
/* Checking SEG_LINETO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Ignoring current path segment in case
* of invalid data. If segment is skipped its endpoint
* (if valid) is used to begin new subpath.
*/
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND)
{
if (subpathStarted) {
pc2d.lineTo(coords[0], coords[1]);
} else {
pc2d.moveTo(coords[0], coords[1]);
subpathStarted = true;
}
}
break;
case PathIterator.SEG_QUADTO:
if (xform == null) {
coords[0] = pCoords[coff];
coords[1] = pCoords[coff+1];
coords[2] = pCoords[coff+2];
coords[3] = pCoords[coff+3];
} else {
xform.transform(pCoords, coff, coords, 0, 2);
}
coff += 4;
// Quadratic curves take two points
/* Checking SEG_QUADTO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Ignoring current path segment in case
* of invalid endpoints's data. Equivalent to the SEG_LINETO
* if endpoint coordinates are valid but there are invalid data
* among other coordinates
*/
if (coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
coords[3] < UPPER_BND && coords[3] > LOWER_BND)
{
if (subpathStarted) {
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND)
{
pc2d.quadTo(coords[0], coords[1],
coords[2], coords[3]);
} else {
pc2d.lineTo(coords[2], coords[3]);
}
} else {
pc2d.moveTo(coords[2], coords[3]);
subpathStarted = true;
}
}
break;
case PathIterator.SEG_CUBICTO:
if (xform == null) {
coords[0] = pCoords[coff];
coords[1] = pCoords[coff+1];
coords[2] = pCoords[coff+2];
coords[3] = pCoords[coff+3];
coords[4] = pCoords[coff+4];
coords[5] = pCoords[coff+5];
} else {
xform.transform(pCoords, coff, coords, 0, 3);
}
coff += 6;
// Cubic curves take three points
/* Checking SEG_CUBICTO coordinates if they are out of the
* [LOWER_BND, UPPER_BND] range. This check also handles NaN
* and Infinity values. Ignoring current path segment in case
* of invalid endpoints's data. Equivalent to the SEG_LINETO
* if endpoint coordinates are valid but there are invalid data
* among other coordinates
*/
if (coords[4] < UPPER_BND && coords[4] > LOWER_BND &&
coords[5] < UPPER_BND && coords[5] > LOWER_BND)
{
if (subpathStarted) {
if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
coords[1] < UPPER_BND && coords[1] > LOWER_BND &&
coords[2] < UPPER_BND && coords[2] > LOWER_BND &&
coords[3] < UPPER_BND && coords[3] > LOWER_BND)
{
pc2d.curveTo(coords[0], coords[1],
coords[2], coords[3],
coords[4], coords[5]);
} else {
pc2d.lineTo(coords[4], coords[5]);
}
} else {
pc2d.moveTo(coords[4], coords[5]);
subpathStarted = true;
}
}
break;
case PathIterator.SEG_CLOSE:
if (subpathStarted) {
pc2d.closePath();
// do not set subpathStarted to false
// in case of missing moveTo() after close()
}
break;
default:
}
}
pc2d.pathDone();
// mark context as CLEAN:
rdrCtx.dirty = false;
}
}