changeset 7778:b79b33bec0a5

Fix RT-37999: [Canvas] Provide GraphicsContext API to set dashed line stroke Reviewed by: kcr
author flar <James.Graham@oracle.com>
date Fri, 22 Aug 2014 15:56:31 -0700
parents 405f1f046967
children ae965899921b
files modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java modules/graphics/src/main/java/com/sun/openpisces/Dasher.java modules/graphics/src/main/java/com/sun/prism/BasicStroke.java modules/graphics/src/main/java/javafx/scene/canvas/GraphicsContext.java modules/graphics/src/main/native-prism/Dasher.c modules/graphics/src/test/java/javafx/scene/canvas/CanvasTest.java
diffstat 6 files changed, 305 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java	Fri Aug 22 14:21:21 2014 -0700
+++ b/modules/graphics/src/main/java/com/sun/javafx/sg/prism/NGCanvas.java	Fri Aug 22 15:56:31 2014 -0700
@@ -92,6 +92,8 @@
     public static final byte POP_CLIP      = ATTR_BASE + 14;
     public static final byte ARC_TYPE      = ATTR_BASE + 15;
     public static final byte FILL_RULE     = ATTR_BASE + 16;
+    public static final byte DASH_ARRAY    = ATTR_BASE + 17;
+    public static final byte DASH_OFFSET   = ATTR_BASE + 18;
 
     public static final byte                     OP_BASE = 20;
     public static final byte FILL_RECT         = OP_BASE + 0;
@@ -316,6 +318,8 @@
     private float linewidth;
     private int linecap, linejoin;
     private float miterlimit;
+    private double[] dashes;
+    private float dashOffset;
     private BasicStroke stroke;
     private Path2D path;
     private NGText ngtext;
@@ -359,6 +363,8 @@
         linecap = BasicStroke.CAP_SQUARE;
         linejoin = BasicStroke.JOIN_MITER;
         miterlimit = 10f;
+        dashes = null;
+        dashOffset = 0.0f;
         stroke = null;
         path.setWindingRule(Path2D.WIND_NON_ZERO);
         // ngtext stores no state between render operations
@@ -752,7 +758,7 @@
     private BasicStroke getStroke() {
         if (stroke == null) {
             stroke = new BasicStroke(linewidth, linecap, linejoin,
-                                     miterlimit);
+                                     miterlimit, dashes, dashOffset);
         }
         return stroke;
     }
@@ -964,6 +970,14 @@
                     miterlimit = buf.getFloat();
                     stroke = null;
                     break;
+                case DASH_ARRAY:
+                    dashes = (double[]) buf.getObject();
+                    stroke = null;
+                    break;
+                case DASH_OFFSET:
+                    dashOffset = buf.getFloat();
+                    stroke = null;
+                    break;
                 case FONT:
                 {
                     pgfont = (PGFont) buf.getObject();
--- a/modules/graphics/src/main/java/com/sun/openpisces/Dasher.java	Fri Aug 22 14:21:21 2014 -0700
+++ b/modules/graphics/src/main/java/com/sun/openpisces/Dasher.java	Fri Aug 22 15:56:31 2014 -0700
@@ -79,19 +79,50 @@
         curCurvepts = new float[8 * 2];
     }
 
+    // More than 24 bits of mantissa means we can no longer accurately
+    // measure the number of times cycled through the dash array so we
+    // punt and override the phase to just be 0 past that point.
+    static float MAX_CYCLES = 16000000f;
     public void reset(float[] dash, float phase) {
-        if (phase < 0) {
-            throw new IllegalArgumentException("phase < 0 !");
-        }
-
         // Normalize so 0 <= phase < dash[0]
         int sidx = 0;
         dashOn = true;
-        float d;
-        while (phase >= (d = dash[sidx])) {
-            phase -= d;
-            sidx = (sidx + 1) % dash.length;
-            dashOn = !dashOn;
+        float sum = 0f;
+        for (float d : dash) {
+            sum += d;
+        }
+        float cycles = phase / sum;
+        if (phase < 0) {
+            if (-cycles >= MAX_CYCLES) {
+                phase = 0;
+            } else {
+                int fullcycles = (int) Math.floor(-cycles);
+                if ((fullcycles & dash.length & 1) != 0) {
+                    dashOn = !dashOn;
+                }
+                phase += fullcycles * sum;
+                while (phase < 0) {
+                    if (--sidx < 0) sidx = dash.length-1;
+                    phase += dash[sidx];
+                    dashOn = !dashOn;
+                }
+            }
+        } else if (phase > 0) {
+            if (cycles >= MAX_CYCLES) {
+                phase = 0;
+            } else {
+                int fullcycles = (int) Math.floor(cycles);
+                if ((fullcycles & dash.length & 1) != 0) {
+                    dashOn = !dashOn;
+                }
+                phase -= fullcycles * sum;
+                float d;
+                while (phase >= (d = dash[sidx])) {
+                    phase -= d;
+                    sidx = (sidx + 1) % dash.length;
+                    dashOn = !dashOn;
+                }
+            }
         }
 
         this.dash = dash;
--- a/modules/graphics/src/main/java/com/sun/prism/BasicStroke.java	Fri Aug 22 14:21:21 2014 -0700
+++ b/modules/graphics/src/main/java/com/sun/prism/BasicStroke.java	Fri Aug 22 15:56:31 2014 -0700
@@ -129,9 +129,6 @@
 
     public void set(float dash[], float dashPhase) {
         if (dash != null) {
-            if (dashPhase < 0.0f) {
-                throw new IllegalArgumentException("negative dash phase");
-            }
             boolean allzero = true;
             for (int i = 0; i < dash.length; i++) {
                 float d = dash[i];
@@ -151,23 +148,21 @@
 
     public void set(double dash[], float dashPhase) {
         if (dash != null) {
-            this.dash = new float[dash.length];
-            if (dashPhase < 0.0f) {
-                throw new IllegalArgumentException("negative dash phase");
-            }
+            float newdashes[] = new float[dash.length];
             boolean allzero = true;
             for (int i = 0; i < dash.length; i++) {
-                double d = dash[i];
+                float d = (float) dash[i];
                 if (d > 0.0) {
                     allzero = false;
                 } else if (d < 0.0) {
                     throw new IllegalArgumentException("negative dash length");
                 }
-                this.dash[i] = (float) d;
+                newdashes[i] = d;
             }
             if (allzero) {
                 throw new IllegalArgumentException("dash lengths all zero");
             }
+            this.dash = newdashes;
         } else {
             this.dash = null;
         }
--- a/modules/graphics/src/main/java/javafx/scene/canvas/GraphicsContext.java	Fri Aug 22 14:21:21 2014 -0700
+++ b/modules/graphics/src/main/java/javafx/scene/canvas/GraphicsContext.java	Fri Aug 22 15:56:31 2014 -0700
@@ -58,6 +58,7 @@
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.IntBuffer;
+import java.util.Arrays;
 import java.util.LinkedList;
 
 /**
@@ -184,6 +185,22 @@
  * boundary path of a shape, relative to the line width, before it is truncated
  * to a {@link StrokeLineJoin#BEVEL BEVEL} join in a stroke operation.
  * </td></tr>
+ * <tr class="altColor">
+ * <td class="colLast" style="width:15%">{@link #setLineDashes(double...) Dashes}</td>
+ * <td class="colLast" style="width:10%; text-align:center; color:#0c0">Yes</td>
+ * <td class="colLast" style="width:10%; text-align:center">{@code null}</td>
+ * <td class="colLast">
+ * The array of dash lengths to be applied to the segments in the boundary
+ * of shapes in a stroke operation.
+ * </td></tr>
+ * <tr class="rowColor">
+ * <td class="colLast" style="width:15%">{@link #setLineDashOffset(double) Dash Offset}</td>
+ * <td class="colLast" style="width:10%; text-align:center; color:#0c0">Yes</td>
+ * <td class="colLast" style="width:10%; text-align:center">{@code 0.0}</td>
+ * <td class="colLast">
+ * The distance offset into the array of dash lengths at which to start the
+ * dashing of the segments in the boundary of shapes in a stroke operation.
+ * </td></tr>
  * 
  * <tr><th colspan="3"><a name="text-attr"><p align="center">Text Attributes</p></a></th></tr>
  * <tr class="rowColor">
@@ -487,6 +504,8 @@
         StrokeLineCap linecap;
         StrokeLineJoin linejoin;
         double miterlimit;
+        double dashes[];
+        double dashOffset;
         int numClipPaths;
         Font font;
         TextAlignment textalign;
@@ -503,6 +522,7 @@
                 new Affine2D(),
                 Color.BLACK, Color.BLACK,
                 1.0, StrokeLineCap.SQUARE, StrokeLineJoin.MITER, 10.0,
+                null, 0.0,
                 0, Font.getDefault(), TextAlignment.LEFT, VPos.BASELINE,
                 null, FillRule.NON_ZERO);
         }
@@ -512,6 +532,7 @@
                 new Affine2D(copy.transform),
                 copy.fill, copy.stroke,
                 copy.linewidth, copy.linecap, copy.linejoin, copy.miterlimit,
+                copy.dashes, copy.dashOffset,
                 copy.numClipPaths,
                 copy.font, copy.textalign, copy.textbaseline,
                 copy.effect, copy.fillRule);
@@ -521,6 +542,7 @@
                      Affine2D transform, Paint fill, Paint stroke,
                      double linewidth, StrokeLineCap linecap,
                      StrokeLineJoin linejoin, double miterlimit,
+                     double dashes[], double dashOffset,
                      int numClipPaths,
                      Font font, TextAlignment align, VPos baseline,
                      Effect effect, FillRule fillRule)
@@ -528,7 +550,7 @@
             set(globalAlpha, blendop,
                 new Affine2D(transform),
                 fill, stroke,
-                linewidth, linecap, linejoin, miterlimit,
+                linewidth, linecap, linejoin, miterlimit, dashes, dashOffset,
                 numClipPaths,
                 font, textalign, textbaseline,
                 effect, fillRule);
@@ -538,6 +560,7 @@
                        Affine2D transform, Paint fill, Paint stroke,
                        double linewidth, StrokeLineCap linecap,
                        StrokeLineJoin linejoin, double miterlimit,
+                       double dashes[], double dashOffset,
                        int numClipPaths,
                        Font font, TextAlignment align, VPos baseline,
                        Effect effect, FillRule fillRule)
@@ -551,6 +574,8 @@
             this.linecap = linecap;
             this.linejoin = linejoin;
             this.miterlimit = miterlimit;
+            this.dashes = dashes;
+            this.dashOffset = dashOffset;
             this.numClipPaths = numClipPaths;
             this.font = font;
             this.textalign = align;
@@ -575,6 +600,8 @@
             ctx.setLineCap(linecap);
             ctx.setLineJoin(linejoin);
             ctx.setMiterLimit(miterlimit);
+            ctx.setLineDashes(dashes);
+            ctx.setLineDashOffset(dashOffset);
             GrowableDataBuffer buf = ctx.getBuffer();
             while (ctx.curState.numClipPaths > numClipPaths) {
                 ctx.curState.numClipPaths--;
@@ -1324,6 +1351,119 @@
     }
 
     /**
+     * Sets the current stroke line dash pattern to a normalized copy of
+     * the argument.
+     * The default value is {@code null}.
+     * The line dash array is a <a href="#strk-attr">stroke attribute</a>
+     * used for any of the stroke methods as specified in the
+     * <a href="#attr-ops-table">Rendering Attributes Table</a>.
+     * If the array is {@code null} or empty or contains all {@code 0} elements
+     * then dashing will be disabled and the current dash array will be set
+     * to {@code null}.
+     * If any of the elements of the array are a negative, infinite, or NaN
+     * value outside the range {@code [0, +inf)} then the entire array will
+     * be ignored and the current dash array will remain unchanged.
+     * If the array is an odd length then it will be treated as if it
+     * were two copies of the array appended to each other.
+     * 
+     * @param dashes the array of finite non-negative dash lengths
+     * @since JavaFX 8u40
+     */
+    public void setLineDashes(double... dashes) {
+        if (dashes == null || dashes.length == 0) {
+            if (curState.dashes == null) {
+                return;
+            }
+            curState.dashes = null;
+        } else {
+            boolean allZeros = true;
+            for (int i = 0; i < dashes.length; i++) {
+                double d = dashes[i];
+                if (d >= 0.0 && d < Double.POSITIVE_INFINITY) {
+                    // Non-NaN, finite, non-negative
+                    // Test cannot be inverted or it will not implicitly test for NaN
+                    if (d > 0) {
+                        allZeros = false;
+                    }
+                } else {
+                    return;
+                }
+            }
+            if (allZeros) {
+                if (curState.dashes == null) {
+                    return;
+                }
+                curState.dashes = null;
+            } else {
+                int dashlen = dashes.length;
+                if ((dashlen & 1) == 0) {
+                    curState.dashes = Arrays.copyOf(dashes, dashlen);
+                } else {
+                    curState.dashes = Arrays.copyOf(dashes, dashlen * 2);
+                    System.arraycopy(dashes, 0, curState.dashes, dashlen, dashlen);
+                }
+            }
+        }
+        GrowableDataBuffer buf = getBuffer();
+        buf.putByte(NGCanvas.DASH_ARRAY);
+        buf.putObject(curState.dashes);
+    }
+
+    /**
+     * Gets a copy of the current line dash array.
+     * The default value is {@code null}.
+     * The array may be normalized by the validation tests in the
+     * {@link #setLineDashes(double...)} method.
+     * The line dash array is a <a href="#strk-attr">stroke attribute</a>
+     * used for any of the stroke methods as specified in the
+     * <a href="#attr-ops-table">Rendering Attributes Table</a>.
+     * 
+     * @return a copy of the current line dash array.
+     * @since JavaFX 8u40
+     */
+    public double[] getLineDashes() {
+        if (curState.dashes == null) {
+            return null;
+        }
+        return Arrays.copyOf(curState.dashes, curState.dashes.length);
+    }
+
+    /**
+     * Sets the line dash offset.
+     * The default value is {@code 0.0}.
+     * The line dash offset is a <a href="#strk-attr">stroke attribute</a>
+     * used for any of the stroke methods as specified in the
+     * <a href="#attr-ops-table">Rendering Attributes Table</a>.
+     * An infinite or NaN value outside of the range {@code (-inf, +inf)}
+     * will be ignored and the current value will remain unchanged.
+     * 
+     * @param dashOffset the line dash offset in the range {@code (-inf, +inf)}
+     * @since JavaFX 8u40
+     */
+    public void setLineDashOffset(double dashOffset) {
+        // Per W3C spec: On setting, infinite, and NaN
+        // values must be ignored, leaving the value unchanged
+        if (dashOffset > Double.NEGATIVE_INFINITY && dashOffset < Double.POSITIVE_INFINITY) {
+            curState.dashOffset = dashOffset;
+            writeParam(dashOffset, NGCanvas.DASH_OFFSET);
+        }
+    }
+
+    /**
+     * Gets the current line dash offset.
+     * The default value is {@code 0.0}.
+     * The line dash offset is a <a href="#strk-attr">stroke attribute</a>
+     * used for any of the stroke methods as specified in the
+     * <a href="#attr-ops-table">Rendering Attributes Table</a>.
+     * 
+     * @return the line dash offset in the range {@code (-inf, +inf)}
+     * @since JavaFX 8u40
+     */
+    public double getLineDashOffset() {
+        return curState.dashOffset;
+    }
+
+    /**
      * Sets the current Font.
      * The default value is specified by {@link Font#getDefault()}.
      * The font is a <a href="#text-attr">text attribute</a>
@@ -2521,7 +2661,7 @@
         if (writer == null) {
             writer = new PixelWriter() {
                 @Override
-                public PixelFormat getPixelFormat() {
+                public PixelFormat<ByteBuffer> getPixelFormat() {
                     return PixelFormat.getByteBgraPreInstance();
                 }
 
@@ -2561,7 +2701,8 @@
                 }
 
                 private int[] checkBounds(int x, int y, int w, int h,
-                                          PixelFormat pf, int scan)
+                                          PixelFormat<? extends Buffer> pf,
+                                          int scan)
                 {
                     // assert (w >= 0 && h >= 0) - checked by caller
                     int cw = (int) Math.ceil(theCanvas.getWidth());
--- a/modules/graphics/src/main/native-prism/Dasher.c	Fri Aug 22 14:21:21 2014 -0700
+++ b/modules/graphics/src/main/native-prism/Dasher.c	Fri Aug 22 15:56:31 2014 -0700
@@ -102,22 +102,50 @@
     Dasher_reset(pDasher, dash, numdashes, phase);
 }
 
+#define MAX_CYCLES 16000000.0f
 void Dasher_reset(Dasher *pDasher, jfloat dash[], jint ndashes, jfloat phase) {
     jint sidx;
-    jfloat d;
-
-    if (phase < 0) {
-        phase = 0;
-//        throw new IllegalArgumentException("phase < 0 !");
-    }
+    jfloat d, sum, cycles;
+    jint i;
 
     // Normalize so 0 <= phase < dash[0]
     sidx = 0;
     this.dashOn = JNI_TRUE;
-    while (phase >= (d = dash[sidx])) {
-        phase -= d;
-        sidx = (sidx + 1) % ndashes;
-        this.dashOn = !this.dashOn;
+    sum = 0.0f;
+    for (i = 0; i < ndashes; i++) {
+        sum += dash[i];
+    }
+    cycles = phase / sum;
+    if (phase < 0) {
+        if (-cycles >= MAX_CYCLES) {
+            phase = 0;
+        } else {
+            jint fullcycles = (jint) floor(-cycles);
+            if ((fullcycles & ndashes & 1) != 0) {
+                this.dashOn = !this.dashOn;
+            }
+            phase += fullcycles * sum;
+            while (phase < 0) {
+                if (--sidx < 0) sidx = ndashes-1;
+                phase += dash[sidx];
+                this.dashOn = !this.dashOn;
+            }
+        }
+    } else if (phase > 0) {
+        if (cycles >= MAX_CYCLES) {
+            phase = 0;
+        } else {
+            jint fullcycles = (jint) floor(cycles);
+            if ((fullcycles & ndashes & 1) != 0) {
+                this.dashOn = !this.dashOn;
+            }
+            phase -= fullcycles * sum;
+            while (phase >= (d = dash[sidx])) {
+                phase -= d;
+                sidx = (sidx + 1) % ndashes;
+                this.dashOn = !this.dashOn;
+            }
+        }
     }
 
     this.dash = dash;
--- a/modules/graphics/src/test/java/javafx/scene/canvas/CanvasTest.java	Fri Aug 22 14:21:21 2014 -0700
+++ b/modules/graphics/src/test/java/javafx/scene/canvas/CanvasTest.java	Fri Aug 22 15:56:31 2014 -0700
@@ -43,6 +43,9 @@
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertNotNull;
 
 public class CanvasTest {
 
@@ -329,16 +332,22 @@
         gc.setLineJoin(StrokeLineJoin.BEVEL);
         gc.setLineWidth(1);
         gc.setMiterLimit(1);
+        gc.setLineDashes(10, 10);
+        gc.setLineDashOffset(100);
         assertEquals(gc.getLineCap(), StrokeLineCap.ROUND);
         assertEquals(gc.getLineJoin(), StrokeLineJoin.BEVEL);
         assertEquals(gc.getLineWidth(), 1, 0.00001);
         assertEquals(gc.getMiterLimit(), 1, 0.00001);
+        assertArrayEquals(gc.getLineDashes(), new double[] {10, 10}, 0.00001);
+        assertEquals(gc.getLineDashOffset(), 100, 0.00001);
         gc.restore();
         
         assertEquals(gc.getLineCap(), StrokeLineCap.BUTT);
         assertEquals(gc.getLineJoin(), StrokeLineJoin.MITER);
         assertEquals(gc.getLineWidth(), 5, 0.00001);
         assertEquals(gc.getMiterLimit(), 3, 0.00001);
+        assertNull(gc.getLineDashes());
+        assertEquals(gc.getLineDashOffset(), 0, 0.00001);
     }
 
     @Test
@@ -367,6 +376,61 @@
         assertEquals(gc.getLineJoin(), StrokeLineJoin.ROUND);
     }
 
+    @Test
+    public void testGCState_LineDashNonPositive() throws Exception {
+        gc.setLineDashes(20, 10);
+        assertArrayEquals(gc.getLineDashes(), new double[] {20, 10}, 0.00001);
+        gc.setLineDashes(1, Double.NaN);
+        assertArrayEquals(gc.getLineDashes(), new double[] {20, 10}, 0.00001);
+        gc.setLineDashes(1, Double.POSITIVE_INFINITY);
+        assertArrayEquals(gc.getLineDashes(), new double[] {20, 10}, 0.00001);
+        gc.setLineDashes(1, -1);
+        assertArrayEquals(gc.getLineDashes(), new double[] {20, 10}, 0.00001);
+    }
+
+    @Test
+    public void testGCState_LineDashNull() throws Exception {
+        gc.setLineDashes(10, 10);
+        assertNotNull(gc.getLineDashes());
+        gc.setLineDashes(null);
+        assertNull(gc.getLineDashes());
+
+        gc.setLineDashes(10, 10);
+        assertNotNull(gc.getLineDashes());
+        gc.setLineDashes();
+        assertNull(gc.getLineDashes());
+
+        gc.setLineDashes(10, 10);
+        assertNotNull(gc.getLineDashes());
+        gc.setLineDashes(new double[0]);
+        assertNull(gc.getLineDashes());
+
+        gc.setLineDashes(10, 10);
+        assertNotNull(gc.getLineDashes());
+        gc.setLineDashes(0, 0);
+        assertNull(gc.getLineDashes());
+    }
+
+    @Test
+    public void testGCState_LineDashOddLength() throws Exception {
+        gc.setLineDashes(10);
+        assertArrayEquals(gc.getLineDashes(), new double[] {10, 10}, 0.00001);
+        gc.setLineDashes(10, 20, 30);
+        assertArrayEquals(gc.getLineDashes(), new double[] {10, 20, 30, 10, 20, 30}, 0.00001);
+    }
+
+    @Test
+    public void testGCState_LineDashOffsetNonFinite() throws Exception {
+        gc.setLineDashOffset(5.0);
+        assertEquals(gc.getLineDashOffset(), 5.0, 0.00001);
+        gc.setLineDashOffset(Double.NaN);
+        assertEquals(gc.getLineDashOffset(), 5.0, 0.00001);
+        gc.setLineDashOffset(Double.NEGATIVE_INFINITY);
+        assertEquals(gc.getLineDashOffset(), 5.0, 0.00001);
+        gc.setLineDashOffset(Double.POSITIVE_INFINITY);
+        assertEquals(gc.getLineDashOffset(), 5.0, 0.00001);
+    }
+
     @Test public void testGCState_BlendMode() throws Exception {
         gc.setGlobalBlendMode(BlendMode.ADD);
         gc.setGlobalAlpha(0);