< prev index next >

modules/javafx.graphics/src/main/java/com/sun/prism/impl/shape/MarlinPrismUtils.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.PathConsumer2D;
  30 import com.sun.javafx.geom.PathIterator;
  31 import com.sun.javafx.geom.Path2D;
  32 import com.sun.javafx.geom.Rectangle;
  33 import com.sun.javafx.geom.Shape;
  34 import com.sun.javafx.geom.transform.BaseTransform;
  35 import com.sun.marlin.MarlinConst;
  36 import com.sun.marlin.MarlinProperties;
  37 import com.sun.marlin.MarlinRenderer;
  38 import com.sun.marlin.RendererContext;
  39 import com.sun.marlin.Stroker;
  40 import com.sun.marlin.TransformingPathConsumer2D;
  41 import com.sun.prism.BasicStroke;
  42 
  43 public final class MarlinPrismUtils {
  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 MarlinPrismUtils() {
  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 PathConsumer2D initStroker(
  68             final RendererContext rdrCtx,
  69             final BasicStroke stroke,
  70             final float lineWidth,
  71             final BaseTransform tx,
  72             final PathConsumer2D 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         float scale = 1.0f;


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




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



 175 
 176         if (dashes != null) {
 177             if (!recycleDashes) {
 178                 dashLen = dashes.length;
 179             }
 180             pc = rdrCtx.dasher.init(pc, dashes, dashLen, dashphase, recycleDashes);










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




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


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






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


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






 433         // mark context as DIRTY:
 434         rdrCtx.dirty = true;
 435 
 436         final float[] coords = rdrCtx.float6;
 437 
 438         // ported from DuctusRenderingEngine.feedConsumer() but simplified:
 439         // - removed skip flag = !subpathStarted
 440         // - removed pathClosed (ie subpathStarted not set to false)
 441         boolean subpathStarted = false;
 442 
 443         final float[] pCoords = p2d.getFloatCoordsNoClone();
 444         final byte[] pTypes = p2d.getCommandsNoClone();
 445         final int nsegs = p2d.getNumCommands();
 446 
 447         for (int i = 0, coff = 0; i < nsegs; i++) {
 448             switch (pTypes[i]) {
 449             case PathIterator.SEG_MOVETO:
 450                 if (xform == null) {
 451                     coords[0] = pCoords[coff];
 452                     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.PathConsumer2D;
  30 import com.sun.javafx.geom.PathIterator;
  31 import com.sun.javafx.geom.Path2D;
  32 import com.sun.javafx.geom.Rectangle;
  33 import com.sun.javafx.geom.Shape;
  34 import com.sun.javafx.geom.transform.BaseTransform;
  35 import com.sun.marlin.MarlinConst;
  36 import com.sun.marlin.MarlinProperties;
  37 import com.sun.marlin.MarlinRenderer;
  38 import com.sun.marlin.RendererContext;
  39 import com.sun.marlin.Stroker;
  40 import com.sun.marlin.TransformingPathConsumer2D;
  41 import com.sun.prism.BasicStroke;
  42 
  43 public final class MarlinPrismUtils {
  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 MarlinPrismUtils() {
  63     }
  64 




  65     private static PathConsumer2D initStroker(
  66             final RendererContext rdrCtx,
  67             final BasicStroke stroke,
  68             final float lineWidth,
  69             BaseTransform tx,
  70             final PathConsumer2D 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         float scale = 1.0f;


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


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


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


< prev index next >