< prev index next >

modules/javafx.graphics/src/main/java/com/sun/prism/impl/shape/DMarlinPrismUtils.java

Print this page


   1 /*
   2  * Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any


  27 
  28 
  29 import com.sun.javafx.geom.PathIterator;
  30 import com.sun.javafx.geom.Path2D;
  31 import com.sun.javafx.geom.Rectangle;
  32 import com.sun.javafx.geom.Shape;
  33 import com.sun.javafx.geom.transform.BaseTransform;
  34 import com.sun.marlin.MarlinConst;
  35 import com.sun.marlin.MarlinProperties;
  36 import com.sun.marlin.DMarlinRenderer;
  37 import com.sun.marlin.DPathConsumer2D;
  38 import com.sun.marlin.DRendererContext;
  39 import com.sun.marlin.DStroker;
  40 import com.sun.marlin.DTransformingPathConsumer2D;
  41 import com.sun.prism.BasicStroke;
  42 
  43 public final class DMarlinPrismUtils {
  44 
  45     private static final boolean FORCE_NO_AA = false;
  46 
  47     static final float UPPER_BND = Float.MAX_VALUE / 2.0f;
  48     static final float LOWER_BND = -UPPER_BND;
  49 
  50     static final boolean DO_CLIP = MarlinProperties.isDoClip();
  51     static final boolean DO_CLIP_FILL = true;
  52 
  53     static final boolean DO_TRACE_PATH = false;
  54 


  55     static final boolean DO_CLIP_RUNTIME_ENABLE = MarlinProperties.isDoClipRuntimeFlag();
  56 



  57     /**
  58      * Private constructor to prevent instantiation.
  59      */
  60     private DMarlinPrismUtils() {
  61     }
  62 
  63     private static boolean nearZero(final double num) {
  64         return Math.abs(num) < 2.0d * Math.ulp(num);
  65     }
  66 
  67     private static DPathConsumer2D initStroker(
  68             final DRendererContext rdrCtx,
  69             final BasicStroke stroke,
  70             final float lineWidth,
  71             final BaseTransform tx,
  72             final DPathConsumer2D out)
  73     {
  74         // We use strokerat so that in Stroker and Dasher we can work only
  75         // with the pre-transformation coordinates. This will repeat a lot of
  76         // computations done in the path iterator, but the alternative is to
  77         // work with transformed paths and compute untransformed coordinates
  78         // as needed. This would be faster but I do not think the complexity
  79         // of working with both untransformed and transformed coordinates in
  80         // the same code is worth it.
  81         // However, if a path's width is constant after a transformation,
  82         // we can skip all this untransforming.
  83 
  84         // As pathTo() will check transformed coordinates for invalid values
  85         // (NaN / Infinity) to ignore such points, it is necessary to apply the
  86         // transformation before the path processing.
  87         BaseTransform strokerTx = null;
  88 
  89         int dashLen = -1;
  90         boolean recycleDashes = false;
  91         double scale = 1.0d;


 126 
 127                 // by now strokerat == null. Input paths to
 128                 // stroker (and maybe dasher) will have the full transform tx
 129                 // applied to them and nothing will happen to the output paths.
 130             } else {
 131                 strokerTx = tx;
 132 
 133                 // by now strokerat == tx. Input paths to
 134                 // stroker (and maybe dasher) will have the full transform tx
 135                 // applied to them, then they will be normalized, and then
 136                 // the inverse of *only the non translation part of tx* will
 137                 // be applied to the normalized paths. This won't cause problems
 138                 // in stroker, because, suppose tx = T*A, where T is just the
 139                 // translation part of tx, and A is the rest. T*A has already
 140                 // been applied to Stroker/Dasher's input. Then Ainv will be
 141                 // applied. Ainv*T*A is not equal to T, but it is a translation,
 142                 // which means that none of stroker's assumptions about its
 143                 // input will be violated. After all this, A will be applied
 144                 // to stroker's output.
 145             }




 146         }
 147 
 148         // Get renderer offsets:
 149         double rdrOffX = 0.0d, rdrOffY = 0.0d;
 150 
 151         if (rdrCtx.doClip && (tx != null)) {
 152             final DMarlinRenderer renderer = (DMarlinRenderer)out;
 153             rdrOffX = renderer.getOffsetX();
 154             rdrOffY = renderer.getOffsetY();
 155         }
 156 
 157         // Prepare the pipeline:
 158         DPathConsumer2D pc = out;
 159 
 160         final DTransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 161 
 162         if (DO_TRACE_PATH) {
 163             // trace Stroker:
 164             pc = transformerPC2D.traceStroker(pc);
 165         }
 166 
 167         if (MarlinConst.USE_SIMPLIFIER) {
 168             // Use simplifier after stroker before Renderer
 169             // to remove collinear segments (notably due to cap square)
 170             pc = rdrCtx.simplifier.init(pc);
 171         }
 172 
 173         // deltaTransformConsumer may adjust the clip rectangle:
 174         pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx, rdrOffX, rdrOffY);
 175 
 176         // stroker will adjust the clip rectangle (width / miter limit):
 177         pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(),
 178                 stroke.getLineJoin(), stroke.getMiterLimit(),
 179                 scale, rdrOffX, rdrOffY);



 180 
 181         if (dashesD != null) {
 182             pc = rdrCtx.dasher.init(pc, dashesD, dashLen, dashphase, recycleDashes);










 183         } else if (rdrCtx.doClip && (stroke.getEndCap() != DStroker.CAP_BUTT)) {
 184             if (DO_TRACE_PATH) {
 185                 pc = transformerPC2D.traceClosedPathDetector(pc);
 186             }
 187 
 188             // If no dash and clip is enabled:
 189             // detect closedPaths (polygons) for caps
 190             pc = transformerPC2D.detectClosedPath(pc);
 191         }
 192         pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx);
 193 
 194         if (DO_TRACE_PATH) {
 195             // trace Input:
 196             pc = transformerPC2D.traceInput(pc);
 197         }
 198         /*
 199          * Pipeline seems to be:
 200          * shape.getPathIterator(tx)
 201          * -> (inverseDeltaTransformConsumer)
 202          * -> (Dasher)
 203          * -> Stroker
 204          * -> (deltaTransformConsumer)
 205          *
 206          * -> (CollinearSimplifier) to remove redundant segments
 207          *
 208          * -> pc2d = Renderer (bounding box)
 209          */
 210         return pc;
 211     }
 212 




 213     private static DPathConsumer2D initRenderer(
 214             final DRendererContext rdrCtx,
 215             final BasicStroke stroke,
 216             final BaseTransform tx,
 217             final Rectangle clip,
 218             final int piRule,
 219             final DMarlinRenderer renderer)
 220     {
 221         if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) {
 222             // Define the initial clip bounds:
 223             final double[] clipRect = rdrCtx.clipRect;
 224 
 225             clipRect[0] = clip.y;
 226             clipRect[1] = clip.y + clip.height;
 227             clipRect[2] = clip.x;
 228             clipRect[3] = clip.x + clip.width;
 229 
 230             // Enable clipping:
 231             rdrCtx.doClip = true;
 232         }


 292         return r;
 293     }
 294 
 295     public static void strokeTo(
 296             final DRendererContext rdrCtx,
 297             final Shape shape,
 298             final BasicStroke stroke,
 299             final float lineWidth,
 300             final DPathConsumer2D out)
 301     {
 302         final DPathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out);
 303 
 304         if (shape instanceof Path2D) {
 305             feedConsumer(rdrCtx, (Path2D)shape, null, pc2d);
 306         } else {
 307             feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d);
 308         }
 309     }
 310 
 311     private static void feedConsumer(final DRendererContext rdrCtx, final PathIterator pi,
 312                                      final DPathConsumer2D pc2d)
 313     {






 314         // mark context as DIRTY:
 315         rdrCtx.dirty = true;
 316 
 317         final float[] coords = rdrCtx.float6;
 318 
 319         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 320         // - removed skip flag = !subpathStarted
 321         // - removed pathClosed (ie subpathStarted not set to false)
 322         boolean subpathStarted = false;
 323 
 324         for (; !pi.isDone(); pi.next()) {
 325             switch (pi.currentSegment(coords)) {
 326             case PathIterator.SEG_MOVETO:
 327                 /* Checking SEG_MOVETO coordinates if they are out of the
 328                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 329                  * and Infinity values. Skipping next path segment in case of
 330                  * invalid data.
 331                  */
 332                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 333                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)


 413                 break;
 414             case PathIterator.SEG_CLOSE:
 415                 if (subpathStarted) {
 416                     pc2d.closePath();
 417                     // do not set subpathStarted to false
 418                     // in case of missing moveTo() after close()
 419                 }
 420                 break;
 421             default:
 422             }
 423         }
 424         pc2d.pathDone();
 425 
 426         // mark context as CLEAN:
 427         rdrCtx.dirty = false;
 428     }
 429 
 430     private static void feedConsumer(final DRendererContext rdrCtx,
 431                                      final Path2D p2d,
 432                                      final BaseTransform xform,
 433                                      final DPathConsumer2D pc2d)
 434     {






 435         // mark context as DIRTY:
 436         rdrCtx.dirty = true;
 437 
 438         final float[] coords = rdrCtx.float6;
 439 
 440         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 441         // - removed skip flag = !subpathStarted
 442         // - removed pathClosed (ie subpathStarted not set to false)
 443         boolean subpathStarted = false;
 444 
 445         final float[] pCoords = p2d.getFloatCoordsNoClone();
 446         final byte[] pTypes = p2d.getCommandsNoClone();
 447         final int nsegs = p2d.getNumCommands();
 448 
 449         for (int i = 0, coff = 0; i < nsegs; i++) {
 450             switch (pTypes[i]) {
 451             case PathIterator.SEG_MOVETO:
 452                 if (xform == null) {
 453                     coords[0] = pCoords[coff];
 454                     coords[1] = pCoords[coff+1];


   1 /*
   2  * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any


  27 
  28 
  29 import com.sun.javafx.geom.PathIterator;
  30 import com.sun.javafx.geom.Path2D;
  31 import com.sun.javafx.geom.Rectangle;
  32 import com.sun.javafx.geom.Shape;
  33 import com.sun.javafx.geom.transform.BaseTransform;
  34 import com.sun.marlin.MarlinConst;
  35 import com.sun.marlin.MarlinProperties;
  36 import com.sun.marlin.DMarlinRenderer;
  37 import com.sun.marlin.DPathConsumer2D;
  38 import com.sun.marlin.DRendererContext;
  39 import com.sun.marlin.DStroker;
  40 import com.sun.marlin.DTransformingPathConsumer2D;
  41 import com.sun.prism.BasicStroke;
  42 
  43 public final class DMarlinPrismUtils {
  44 
  45     private static final boolean FORCE_NO_AA = false;
  46 
  47     // slightly slower ~2% if enabled stroker clipping (lines) but skipping cap / join handling is few percents faster in specific cases
  48     static final boolean DISABLE_2ND_STROKER_CLIPPING = true;



  49 
  50     static final boolean DO_TRACE_PATH = false;
  51 
  52     static final boolean DO_CLIP = MarlinProperties.isDoClip();
  53     static final boolean DO_CLIP_FILL = true;
  54     static final boolean DO_CLIP_RUNTIME_ENABLE = MarlinProperties.isDoClipRuntimeFlag();
  55 
  56     static final float UPPER_BND = Float.MAX_VALUE / 2.0f;
  57     static final float LOWER_BND = -UPPER_BND;
  58 
  59     /**
  60      * Private constructor to prevent instantiation.
  61      */
  62     private DMarlinPrismUtils() {
  63     }
  64 




  65     private static DPathConsumer2D initStroker(
  66             final DRendererContext rdrCtx,
  67             final BasicStroke stroke,
  68             final float lineWidth,
  69             BaseTransform tx,
  70             final DPathConsumer2D out)
  71     {
  72         // We use strokerat so that in Stroker and Dasher we can work only
  73         // with the pre-transformation coordinates. This will repeat a lot of
  74         // computations done in the path iterator, but the alternative is to
  75         // work with transformed paths and compute untransformed coordinates
  76         // as needed. This would be faster but I do not think the complexity
  77         // of working with both untransformed and transformed coordinates in
  78         // the same code is worth it.
  79         // However, if a path's width is constant after a transformation,
  80         // we can skip all this untransforming.
  81 
  82         // As pathTo() will check transformed coordinates for invalid values
  83         // (NaN / Infinity) to ignore such points, it is necessary to apply the
  84         // transformation before the path processing.
  85         BaseTransform strokerTx = null;
  86 
  87         int dashLen = -1;
  88         boolean recycleDashes = false;
  89         double scale = 1.0d;


 124 
 125                 // by now strokerat == null. Input paths to
 126                 // stroker (and maybe dasher) will have the full transform tx
 127                 // applied to them and nothing will happen to the output paths.
 128             } else {
 129                 strokerTx = tx;
 130 
 131                 // by now strokerat == tx. Input paths to
 132                 // stroker (and maybe dasher) will have the full transform tx
 133                 // applied to them, then they will be normalized, and then
 134                 // the inverse of *only the non translation part of tx* will
 135                 // be applied to the normalized paths. This won't cause problems
 136                 // in stroker, because, suppose tx = T*A, where T is just the
 137                 // translation part of tx, and A is the rest. T*A has already
 138                 // been applied to Stroker/Dasher's input. Then Ainv will be
 139                 // applied. Ainv*T*A is not equal to T, but it is a translation,
 140                 // which means that none of stroker's assumptions about its
 141                 // input will be violated. After all this, A will be applied
 142                 // to stroker's output.
 143             }
 144         } else {
 145             // either tx is null or it's the identity. In either case
 146             // we don't transform the path.
 147             tx = null;
 148         }
 149 
 150         // Get renderer offsets:
 151         double rdrOffX = 0.0d, rdrOffY = 0.0d;
 152 
 153         if (rdrCtx.doClip && (tx != null)) {
 154             final DMarlinRenderer renderer = (DMarlinRenderer)out;
 155             rdrOffX = renderer.getOffsetX();
 156             rdrOffY = renderer.getOffsetY();
 157         }
 158 
 159         // Prepare the pipeline:
 160         DPathConsumer2D pc = out;
 161 
 162         final DTransformingPathConsumer2D transformerPC2D = rdrCtx.transformerPC2D;
 163 
 164         if (DO_TRACE_PATH) {
 165             // trace Stroker:
 166             pc = transformerPC2D.traceStroker(pc);
 167         }
 168 
 169         if (MarlinConst.USE_SIMPLIFIER) {
 170             // Use simplifier after stroker before Renderer
 171             // to remove collinear segments (notably due to cap square)
 172             pc = rdrCtx.simplifier.init(pc);
 173         }
 174 
 175         // deltaTransformConsumer may adjust the clip rectangle:
 176         pc = transformerPC2D.deltaTransformConsumer(pc, strokerTx, rdrOffX, rdrOffY);
 177 
 178         // stroker will adjust the clip rectangle (width / miter limit):
 179         pc = rdrCtx.stroker.init(pc, width, stroke.getEndCap(),
 180                 stroke.getLineJoin(), stroke.getMiterLimit(),
 181                 scale, rdrOffX, rdrOffY, (dashesD == null));
 182 
 183         // Curve Monotizer:
 184         rdrCtx.monotonizer.init(width);
 185 
 186         if (dashesD != null) {
 187             if (DO_TRACE_PATH) {
 188                 pc = transformerPC2D.traceDasher(pc);
 189             }
 190             pc = rdrCtx.dasher.init(pc, dashesD, dashLen, dashphase,
 191                                     recycleDashes);
 192 
 193             if (DISABLE_2ND_STROKER_CLIPPING) {
 194                 // disable stoker clipping:
 195                 rdrCtx.stroker.disableClipping();
 196             }
 197 
 198         } else if (rdrCtx.doClip && (stroke.getEndCap() != DStroker.CAP_BUTT)) {
 199             if (DO_TRACE_PATH) {
 200                 pc = transformerPC2D.traceClosedPathDetector(pc);
 201             }
 202 
 203             // If no dash and clip is enabled:
 204             // detect closedPaths (polygons) for caps
 205             pc = transformerPC2D.detectClosedPath(pc);
 206         }
 207         pc = transformerPC2D.inverseDeltaTransformConsumer(pc, strokerTx);
 208 
 209         if (DO_TRACE_PATH) {
 210             // trace Input:
 211             pc = transformerPC2D.traceInput(pc);
 212         }
 213         /*
 214          * Pipeline seems to be:
 215          * shape.getPathIterator(tx)
 216          * -> (inverseDeltaTransformConsumer)
 217          * -> (Dasher)
 218          * -> Stroker
 219          * -> (deltaTransformConsumer)
 220          *
 221          * -> (CollinearSimplifier) to remove redundant segments
 222          *
 223          * -> pc2d = Renderer (bounding box)
 224          */
 225         return pc;
 226     }
 227 
 228     private static boolean nearZero(final double num) {
 229         return Math.abs(num) < 2.0d * Math.ulp(num);
 230     }
 231 
 232     private static DPathConsumer2D initRenderer(
 233             final DRendererContext rdrCtx,
 234             final BasicStroke stroke,
 235             final BaseTransform tx,
 236             final Rectangle clip,
 237             final int piRule,
 238             final DMarlinRenderer renderer)
 239     {
 240         if (DO_CLIP || (DO_CLIP_RUNTIME_ENABLE && MarlinProperties.isDoClipAtRuntime())) {
 241             // Define the initial clip bounds:
 242             final double[] clipRect = rdrCtx.clipRect;
 243 
 244             clipRect[0] = clip.y;
 245             clipRect[1] = clip.y + clip.height;
 246             clipRect[2] = clip.x;
 247             clipRect[3] = clip.x + clip.width;
 248 
 249             // Enable clipping:
 250             rdrCtx.doClip = true;
 251         }


 311         return r;
 312     }
 313 
 314     public static void strokeTo(
 315             final DRendererContext rdrCtx,
 316             final Shape shape,
 317             final BasicStroke stroke,
 318             final float lineWidth,
 319             final DPathConsumer2D out)
 320     {
 321         final DPathConsumer2D pc2d = initStroker(rdrCtx, stroke, lineWidth, null, out);
 322 
 323         if (shape instanceof Path2D) {
 324             feedConsumer(rdrCtx, (Path2D)shape, null, pc2d);
 325         } else {
 326             feedConsumer(rdrCtx, shape.getPathIterator(null), pc2d);
 327         }
 328     }
 329 
 330     private static void feedConsumer(final DRendererContext rdrCtx, final PathIterator pi,
 331                                      DPathConsumer2D pc2d)
 332     {
 333         if (MarlinConst.USE_PATH_SIMPLIFIER) {
 334             // Use path simplifier at the first step
 335             // to remove useless points
 336             pc2d = rdrCtx.pathSimplifier.init(pc2d);
 337         }
 338 
 339         // mark context as DIRTY:
 340         rdrCtx.dirty = true;
 341 
 342         final float[] coords = rdrCtx.float6;
 343 
 344         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 345         // - removed skip flag = !subpathStarted
 346         // - removed pathClosed (ie subpathStarted not set to false)
 347         boolean subpathStarted = false;
 348 
 349         for (; !pi.isDone(); pi.next()) {
 350             switch (pi.currentSegment(coords)) {
 351             case PathIterator.SEG_MOVETO:
 352                 /* Checking SEG_MOVETO coordinates if they are out of the
 353                  * [LOWER_BND, UPPER_BND] range. This check also handles NaN
 354                  * and Infinity values. Skipping next path segment in case of
 355                  * invalid data.
 356                  */
 357                 if (coords[0] < UPPER_BND && coords[0] > LOWER_BND &&
 358                     coords[1] < UPPER_BND && coords[1] > LOWER_BND)


 438                 break;
 439             case PathIterator.SEG_CLOSE:
 440                 if (subpathStarted) {
 441                     pc2d.closePath();
 442                     // do not set subpathStarted to false
 443                     // in case of missing moveTo() after close()
 444                 }
 445                 break;
 446             default:
 447             }
 448         }
 449         pc2d.pathDone();
 450 
 451         // mark context as CLEAN:
 452         rdrCtx.dirty = false;
 453     }
 454 
 455     private static void feedConsumer(final DRendererContext rdrCtx,
 456                                      final Path2D p2d,
 457                                      final BaseTransform xform,
 458                                      DPathConsumer2D pc2d)
 459     {
 460         if (MarlinConst.USE_PATH_SIMPLIFIER) {
 461             // Use path simplifier at the first step
 462             // to remove useless points
 463             pc2d = rdrCtx.pathSimplifier.init(pc2d);
 464         }
 465 
 466         // mark context as DIRTY:
 467         rdrCtx.dirty = true;
 468 
 469         final float[] coords = rdrCtx.float6;
 470 
 471         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 472         // - removed skip flag = !subpathStarted
 473         // - removed pathClosed (ie subpathStarted not set to false)
 474         boolean subpathStarted = false;
 475 
 476         final float[] pCoords = p2d.getFloatCoordsNoClone();
 477         final byte[] pTypes = p2d.getCommandsNoClone();
 478         final int nsegs = p2d.getNumCommands();
 479 
 480         for (int i = 0, coff = 0; i < nsegs; i++) {
 481             switch (pTypes[i]) {
 482             case PathIterator.SEG_MOVETO:
 483                 if (xform == null) {
 484                     coords[0] = pCoords[coff];
 485                     coords[1] = pCoords[coff+1];


< prev index next >