1 /*
   2  * Copyright (c) 2013, 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
  23  * questions.
  24  */
  25 
  26 package javafx.scene.shape;
  27 
  28 import com.sun.javafx.scene.shape.ObservableFaceArrayImpl;
  29 import com.sun.javafx.collections.FloatArraySyncer;
  30 import com.sun.javafx.collections.IntegerArraySyncer;
  31 import com.sun.javafx.geom.BaseBounds;
  32 import com.sun.javafx.geom.BoxBounds;
  33 import com.sun.javafx.geom.PickRay;
  34 import com.sun.javafx.geom.Vec3d;
  35 import com.sun.javafx.scene.input.PickResultChooser;
  36 import com.sun.javafx.sg.prism.NGTriangleMesh;
  37 import javafx.collections.ArrayChangeListener;
  38 import javafx.collections.FXCollections;
  39 import javafx.collections.ObservableArray;
  40 import javafx.collections.ObservableFloatArray;
  41 import javafx.collections.ObservableIntegerArray;
  42 import javafx.geometry.Point2D;
  43 import javafx.geometry.Point3D;
  44 import javafx.scene.Node;
  45 import javafx.scene.input.PickResult;
  46 import javafx.scene.transform.Affine;
  47 import javafx.scene.transform.NonInvertibleTransformException;
  48 import javafx.scene.transform.Rotate;
  49 import sun.util.logging.PlatformLogger;
  50 
  51 /**
  52  * Defines a 3D geometric object contains separate arrays of points, 
  53  * texture coordinates, and faces that describe a triangulated 
  54  * geometric mesh.
  55  *<p>
  56  * Note that the term point, as used in the method names and method
  57  * descriptions, actually refers to a set of x, y, and z point
  58  * representing the position of a single vertex. The term points (plural) is
  59  * used to indicate sets of x, y, and z points for multiple vertices.
  60  * Similarly, the term texCoord is used to indicate a set of u and v texture
  61  * coordinates for a single vertex, while the term texCoords (plural) is used
  62  * to indicate sets of u and v texture coordinates for multiple vertices.
  63  * Lastly, the term face is used to indicate 3 set of interleaving points
  64  * and texture coordinates that together represent the geometric topology of a 
  65  * single triangle, while the term faces (plural) is used to indicate sets of 
  66  * triangles (each represent by a face).
  67  * <p>
  68  * For example, the faces that represent a single textured rectangle, using 2 triangles,
  69  * has the following data order: [
  70  * <p>
  71  * p0, t0, p1, t1, p3, t3,  // First triangle of a textured rectangle
  72  * <p>
  73  * p1, t1, p2, t2, p3, t3   // Second triangle of a textured rectangle
  74  * <p>
  75  * ]
  76  * <p>
  77  * where p0, p1, p2 and p3 are indices into the points array, and t0, t1, t2
  78  * and t3 are indices into the texCoords array.
  79  * 
  80  * <p>
  81  * A triangle has a front and back face. The winding order of a triangle's vertices
  82  * determines which side is the front face. JavaFX chooses the counter-clockwise
  83  * (or right-hand rule) winding order as the front face. By default, only the
  84  * front face of a triangle is rendered. See {@code CullFace} for more
  85  * information.
  86  *
  87  * <p>
  88  * The length of {@code points}, {@code texCoords}, and {@code faces} must be
  89  * divisible by 3, 2, and 6 respectively.
  90  * The values in the faces array must be within the range of the number of vertices
  91  * in the points array (0 to points.length / 3 - 1) for the point indices and 
  92  * within the range of the number of the vertices in 
  93  * the texCoords array (0 to texCoords.length / 2 - 1) for the texture coordinate indices.
  94  * 
  95  * <p> A warning will be recorded to the logger and the mesh will not be rendered
  96  * (and will have an empty bounds) if any of the array lengths are invalid
  97  * or if any of the values in the faces array are out of range.
  98  * 
  99  * @since JavaFX 8.0
 100  */
 101 public class TriangleMesh extends Mesh {
 102 
 103     // The values in faces must be within range and the length of points,
 104     // texCoords and faces must be divisible by 3, 2 and 6 respectively.
 105     private final ObservableFloatArray points = FXCollections.observableFloatArray();
 106     private final ObservableFloatArray texCoords = FXCollections.observableFloatArray();
 107     private final ObservableFaceArray faces = new ObservableFaceArrayImpl();
 108     private final ObservableIntegerArray faceSmoothingGroups = FXCollections.observableIntegerArray();
 109     
 110     private final Listener pointsSyncer = new Listener(points);
 111     private final Listener texCoordsSyncer = new Listener(texCoords);
 112     private final Listener facesSyncer = new Listener(faces);
 113     private final Listener faceSmoothingGroupsSyncer = new Listener(faceSmoothingGroups);
 114     private final boolean isPredefinedShape;
 115     private boolean isValidDirty = true;
 116     private boolean isPointsValid, isTexCoordsValid, isFacesValid, isFaceSmoothingGroupValid;
 117     private int refCount = 1;
 118 
 119     private BaseBounds cachedBounds;
 120     private VertexFormat vertexFormat = new VertexFormat();
 121 
 122     /**
 123      * Creates a new instance of {@code TriangleMesh} class.
 124      */
 125     public TriangleMesh() {
 126         this(false);
 127     }
 128 
 129     TriangleMesh(boolean isPredefinedShape) {
 130         this.isPredefinedShape = isPredefinedShape;
 131         if (isPredefinedShape) {
 132             isPointsValid = true;
 133             isTexCoordsValid = true;
 134             isFacesValid = true;
 135             isFaceSmoothingGroupValid = true;
 136         } else {
 137             isPointsValid = false;
 138             isTexCoordsValid = false;
 139             isFacesValid = false;
 140             isFaceSmoothingGroupValid = false;
 141         }
 142     }
 143 
 144     /**
 145      * Returns the number of elements that represents a Point.
 146      *
 147      * @return number of elements
 148      */
 149     public final int getPointElementSize() {
 150         return vertexFormat.getPointElementSize();
 151     }
 152 
 153     /**
 154      * Returns the number of elements that represents a TexCoord.
 155      *
 156      * @return number of elements
 157      */
 158     public final int getTexCoordElementSize() {
 159         return vertexFormat.getTexCoordElementSize();
 160     }
 161 
 162     /**
 163      * Returns the number of elements that represents a Face.
 164      *
 165      * @return number of elements
 166      */
 167     public final int getFaceElementSize() {
 168         return vertexFormat.getFaceElementSize();
 169     }
 170 
 171     /**
 172      * Gets the {@code points} array of this {@code TriangleMesh}.
 173      *
 174      * @return {@code points} array where each point is
 175      * represented by 3 float values x, y and z, in that order.
 176      */
 177     public final ObservableFloatArray getPoints() {
 178         return points;
 179     }
 180 
 181     /**
 182      * Gets the  {@code texCoords} array of this {@code TriangleMesh}.
 183      * The coordinates are proportional, so texture's top-left corner
 184      * is at [0, 0] and bottom-right corner is at [1, 1].
 185      *
 186      * @return {@code texCoord} array where each texture coordinate is represented
 187      * by 2 float values: u and v, in that order.
 188      */    
 189     public final ObservableFloatArray getTexCoords() {
 190         return texCoords;
 191     }
 192  
 193     /**
 194      * Gets the {@code faces} array, indices into the {@code points} 
 195      * and {@code texCoords} arrays, of this  {@code TriangleMesh}
 196      *
 197      * @return {@code faces} array where each face is
 198      * 6 integers p0, t0, p1, t1, p3, t3, where p0, p1 and p2 are indices of 
 199      * points in {@code points} and t0, t1 and t2 are 
 200      * indices of texture coordinates in {@code texCoords}.
 201      * Both indices are in terms of vertices (points or texture coordinates),
 202      * not individual floats.
 203      */    
 204     public final ObservableFaceArray getFaces() {
 205         return faces;
 206     }
 207 
 208     /**
 209      * Gets the {@code faceSmoothingGroups} array of this {@code TriangleMesh}.
 210      * Smoothing affects how a mesh is rendered but it does not effect its
 211      * geometry. The face smoothing group value is used to control the smoothing
 212      * between adjacent faces.
 213      *
 214      * <p> The face smoothing group value is represented by an array of bits and up to
 215      * 32 unique groups is possible; (1 << 0) to (1 << 31). The face smoothing
 216      * group value can range from 0 (no smoothing group) to all 32 groups. A face
 217      * can belong to zero or more smoothing groups. A face is a member of group
 218      * N if bit N is set, for example, groups |= (1 << N). A value of 0 implies
 219      * no smoothing group or hard edges.
 220      * Smoothing is applied when adjacent pair of faces shared a smoothing group.
 221      * Otherwise the faces are rendered with a hard edge between them.
 222      *
 223      * <p> An empty faceSmoothingGroups implies all faces in this mesh have a
 224      * smoothing group value of 1.
 225      *
 226      * <p> Note: If faceSmoothingGroups is not empty, is size must
 227      * be equal to number of faces.
 228      */    
 229     public final ObservableIntegerArray getFaceSmoothingGroups() {
 230         return faceSmoothingGroups;
 231     }
 232 
 233     @Override void setDirty(boolean value) {
 234         super.setDirty(value);
 235         if (!value) { // false
 236             pointsSyncer.setDirty(false);
 237             texCoordsSyncer.setDirty(false);
 238             facesSyncer.setDirty(false);
 239             faceSmoothingGroupsSyncer.setDirty(false);
 240         }
 241     }
 242 
 243     int getRefCount() {
 244         return refCount;
 245     }
 246 
 247     synchronized void incRef() {
 248         this.refCount += 1;
 249     }
 250 
 251     synchronized void decRef() {
 252         this.refCount -= 1;
 253     }
 254 
 255     private NGTriangleMesh peer;
 256 
 257     /**
 258      * @treatAsPrivate implementation detail
 259      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 260      */
 261     @Deprecated
 262     /** The peer node created by the graphics Toolkit/Pipeline implementation */
 263     NGTriangleMesh impl_getPGTriangleMesh() {
 264         if (peer == null) {
 265             peer = new NGTriangleMesh();
 266         }
 267         return peer;
 268     }
 269 
 270     @Override
 271     NGTriangleMesh getPGMesh() {
 272         return impl_getPGTriangleMesh();
 273     }
 274 
 275     private boolean validatePoints() {
 276         if (points.size() == 0) { // Valid but meaningless for picking or rendering.
 277             return false;
 278         }
 279         
 280         if ((points.size() % vertexFormat.getPointElementSize()) != 0) {
 281             String logname = TriangleMesh.class.getName();
 282             PlatformLogger.getLogger(logname).warning("points.size() has "
 283                     + "to be divisible by getPointElementSize(). It is to"
 284                     + " store multiple x, y, and z coordinates of this mesh");
 285             return false;
 286         }
 287         return true;
 288     }
 289 
 290     private boolean validateTexCoords() {
 291         if (texCoords.size() == 0) { // Valid but meaningless for picking or rendering.
 292             return false;
 293         }
 294 
 295         if ((texCoords.size() % vertexFormat.getTexCoordElementSize()) != 0) {
 296             String logname = TriangleMesh.class.getName();
 297             PlatformLogger.getLogger(logname).warning("texCoords.size() "
 298                     + "has to be divisible by getTexCoordElementSize()."
 299                     + " It is to store multiple u and v texture coordinates"
 300                     + " of this mesh");
 301             return false;
 302         }
 303         return true;
 304     }
 305 
 306     private boolean validateFaces() {
 307         if (faces.size() == 0) { // Valid but meaningless for picking or rendering.
 308             return false;
 309         }
 310         
 311         String logname = TriangleMesh.class.getName();
 312         if ((faces.size() % vertexFormat.getFaceElementSize()) != 0) {
 313             PlatformLogger.getLogger(logname).warning("faces.size() has "
 314                     + "to be divisible by getFaceElementSize().");
 315             return false;
 316         }
 317 
 318         int nVerts = points.size() / vertexFormat.getPointElementSize();
 319         int nTVerts = texCoords.size() / vertexFormat.getTexCoordElementSize();
 320         for (int i = 0; i < faces.size(); i++) {
 321             if (i % 2 == 0 && (faces.get(i) >= nVerts || faces.get(i) < 0)
 322                     || (i % 2 != 0 && (faces.get(i) >= nTVerts || faces.get(i) < 0))) {
 323                 PlatformLogger.getLogger(logname).warning("The values in the "
 324                         + "faces array must be within the range of the number "
 325                         + "of vertices in the points array (0 to points.length / 3 - 1) "
 326                         + "for the point indices and within the range of the "
 327                         + "number of the vertices in the texCoords array (0 to "
 328                         + "texCoords.length / 2 - 1) for the texture coordinate indices.");
 329                 return false;
 330             }
 331         }
 332         return true;
 333     }
 334 
 335     private boolean validateFaceSmoothingGroups() {
 336         if (faceSmoothingGroups.size() != 0
 337                 && faceSmoothingGroups.size() != (faces.size() / vertexFormat.getFaceElementSize())) {
 338             String logname = TriangleMesh.class.getName();
 339             PlatformLogger.getLogger(logname).warning("faceSmoothingGroups.size()"
 340                     + " has to equal to number of faces.");
 341             return false;
 342         }
 343         return true;
 344     }
 345 
 346     private boolean validate() {
 347         if (isPredefinedShape) {
 348             return true;
 349         }
 350         
 351         if (isValidDirty) {
 352             if (pointsSyncer.dirtyInFull) {
 353                 isPointsValid = validatePoints();
 354             }
 355             if (texCoordsSyncer.dirtyInFull) {
 356                 isTexCoordsValid = validateTexCoords();
 357             }
 358             if (facesSyncer.dirty || pointsSyncer.dirtyInFull || texCoordsSyncer.dirtyInFull) {
 359                 isFacesValid = isPointsValid && isTexCoordsValid && validateFaces();
 360             }
 361             if (faceSmoothingGroupsSyncer.dirtyInFull || facesSyncer.dirtyInFull) {
 362                 isFaceSmoothingGroupValid = isFacesValid && validateFaceSmoothingGroups();
 363             }
 364             isValidDirty = false;
 365         }
 366         return isPointsValid && isTexCoordsValid && isFaceSmoothingGroupValid && isFacesValid;
 367     }
 368 
 369     /**
 370      * @treatAsPrivate implementation detail
 371      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 372      */
 373     @Deprecated
 374     @Override
 375     void impl_updatePG() {
 376         if (!isDirty()) {
 377             return;
 378         }
 379 
 380         final NGTriangleMesh pgTriMesh = impl_getPGTriangleMesh();
 381         if (validate()) {
 382             pgTriMesh.syncPoints(pointsSyncer);
 383             pgTriMesh.syncTexCoords(texCoordsSyncer);
 384             pgTriMesh.syncFaces(facesSyncer);
 385             pgTriMesh.syncFaceSmoothingGroups(faceSmoothingGroupsSyncer);
 386         } else {
 387             pgTriMesh.syncPoints(null);
 388             pgTriMesh.syncTexCoords(null);
 389             pgTriMesh.syncFaces(null);
 390             pgTriMesh.syncFaceSmoothingGroups(null);
 391         }
 392         setDirty(false);
 393     }
 394 
 395     @Override
 396     BaseBounds computeBounds(BaseBounds bounds) {
 397         if (isDirty() || cachedBounds == null) {
 398             cachedBounds = new BoxBounds();
 399             if (validate()) {
 400                 final double len = points.size();
 401                 for (int i = 0; i < len; i += vertexFormat.getPointElementSize()) {
 402                     cachedBounds.add(points.get(i), points.get(i + 1), points.get(i + 2));
 403                 }
 404             }
 405         }
 406         return bounds.deriveWithNewBounds(cachedBounds);
 407     }
 408 
 409     /**
 410      * Computes the centroid of the given triangle
 411      * @param v0x x coord of first vertex of the triangle
 412      * @param v0y y coord of first vertex of the triangle
 413      * @param v0z z coord of first vertex of the triangle
 414      * @param v1x x coord of second vertex of the triangle
 415      * @param v1y y coord of second vertex of the triangle
 416      * @param v1z z coord of second vertex of the triangle
 417      * @param v2x x coord of third vertex of the triangle
 418      * @param v2y y coord of third vertex of the triangle
 419      * @param v2z z coord of third vertex of the triangle
 420      * @return the triangle centroid
 421      */
 422     private Point3D computeCentroid(
 423             double v0x, double v0y, double v0z,
 424             double v1x, double v1y, double v1z,
 425             double v2x, double v2y, double v2z) {
 426 
 427 //        Point3D center = v1.midpoint(v2);
 428 //        Point3D vec = center.subtract(v0);
 429 //        return v0.add(new Point3D(vec.getX() / 3.0, vec.getY() / 3.0, vec.getZ() / 3.0));
 430 
 431         return new Point3D(
 432             v0x + (v2x + (v1x - v2x) / 2.0 - v0x) / 3.0,
 433             v0y + (v2y + (v1y - v2y) / 2.0 - v0y) / 3.0,
 434             v0z + (v2z + (v1z - v2z) / 2.0 - v0z) / 3.0);
 435     }
 436 
 437     /**
 438      * Computes the centroid of the given triangle
 439      * @param v0 vertex of the triangle
 440      * @param v1 vertex of the triangle
 441      * @param v2 vertex of the triangle
 442      * @return the triangle centroid
 443      */
 444     private Point2D computeCentroid(Point2D v0, Point2D v1, Point2D v2) {
 445         Point2D center = v1.midpoint(v2);
 446 
 447         Point2D vec = center.subtract(v0);
 448         return v0.add(new Point2D(vec.getX() / 3.0, vec.getY() / 3.0));
 449     }
 450 
 451     /**
 452      * Computes intersection of a pick ray and a single triangle face.
 453      *
 454      * It takes pickRay, origin and dir. The latter two can be of course obtained
 455      * from the pickRay, but we need them to be converted to Point3D and don't
 456      * want to do that for all faces. Therefore the conversion is done just once
 457      * and passed to the method for all the faces.
 458      *
 459      * @param pickRay pick ray
 460      * @param origin pick ray's origin
 461      * @param dir pick ray's direction
 462      * @param faceIndex index of the face to test
 463      * @param cullFace cull face of the Node (and thus the tested face)
 464      * @param candidate the owner node (for the possible placement to the result)
 465      * @param reportFace whether or not to report he hit face
 466      * @param result the pick result to be updated if a closer intersection is found
 467      * @return true if the pick ray intersects with the face (regardless of whether
 468      *              the result has been updated)
 469      */
 470     private boolean computeIntersectsFace(
 471             PickRay pickRay, Vec3d origin, Vec3d dir, int faceIndex,
 472             CullFace cullFace, Node candidate, boolean reportFace, PickResultChooser result) {//, BoxBounds rayBounds) {
 473 
 474         // This computation was naturally done by Point3D and its operations,
 475         // but it needs a lot of points and there is often a lot of triangles
 476         // so it is vital for performance to use only primitive variables
 477         // and do the computing manually.
 478 
 479         int pointElementSize = vertexFormat.getPointElementSize();
 480         final int v0Idx = faces.get(faceIndex) * pointElementSize;
 481         final int v1Idx = faces.get(faceIndex + 2) * pointElementSize;
 482         final int v2Idx = faces.get(faceIndex + 4) * pointElementSize;
 483 
 484         final float v0x = points.get(v0Idx);
 485         final float v0y = points.get(v0Idx + 1);
 486         final float v0z = points.get(v0Idx + 2);
 487         final float v1x = points.get(v1Idx);
 488         final float v1y = points.get(v1Idx + 1);
 489         final float v1z = points.get(v1Idx + 2);
 490         final float v2x = points.get(v2Idx);
 491         final float v2y = points.get(v2Idx + 1);
 492         final float v2z = points.get(v2Idx + 2);
 493 
 494         // e1 = v1.subtract(v0)
 495         final float e1x = v1x - v0x;
 496         final float e1y = v1y - v0y;
 497         final float e1z = v1z - v0z;
 498         // e2 = v2.subtract(v0)
 499         final float e2x = v2x - v0x;
 500         final float e2y = v2y - v0y;
 501         final float e2z = v2z - v0z;
 502 
 503         // h = dir.crossProduct(e2)
 504         final double hx = dir.y * e2z - dir.z * e2y;
 505         final double hy = dir.z * e2x - dir.x * e2z;
 506         final double hz = dir.x * e2y - dir.y * e2x;
 507 
 508         // a = e1.dotProduct(h)
 509         final double a = e1x * hx + e1y * hy + e1z * hz;
 510         if (a == 0.0) {
 511             return false;
 512         }
 513         final double f = 1.0 / a;
 514 
 515         // s = origin.subtract(v0)
 516         final double sx = origin.x - v0x;
 517         final double sy = origin.y - v0y;
 518         final double sz = origin.z - v0z;
 519 
 520         // u = f * (s.dotProduct(h))
 521         final double u = f * (sx * hx + sy * hy + sz * hz);
 522 
 523         if (u < 0.0 || u > 1.0) {
 524             return false;
 525         }
 526 
 527         // q = s.crossProduct(e1)
 528         final double qx = sy * e1z - sz * e1y;
 529         final double qy = sz * e1x - sx * e1z;
 530         final double qz = sx * e1y - sy * e1x;
 531 
 532         // v = f * dir.dotProduct(q)
 533         double v = f * (dir.x * qx + dir.y * qy + dir.z * qz);
 534 
 535         if (v < 0.0 || u + v > 1.0) {
 536             return false;
 537         }
 538 
 539         // t = f * e2.dotProduct(q)
 540         final double t = f * (e2x * qx + e2y * qy + e2z * qz);
 541 
 542         if (t >= pickRay.getNearClip() && t <= pickRay.getFarClip()) {
 543             // This branch is entered only for hit triangles (not so often),
 544             // so we can get smoothly back to the nice code using Point3Ds.
 545 
 546             if (cullFace != CullFace.NONE) {
 547                 // normal = e1.crossProduct(e2)
 548                 final Point3D normal = new Point3D(
 549                     e1y * e2z - e1z * e2y,
 550                     e1z * e2x - e1x * e2z,
 551                     e1x * e2y - e1y * e2x);
 552 
 553                 final double nangle = normal.angle(
 554                         new Point3D(-dir.x, -dir.y, -dir.z));
 555                 if ((nangle >= 90 || cullFace != CullFace.BACK) &&
 556                         (nangle <= 90 || cullFace != CullFace.FRONT)) {
 557                     // hit culled face
 558                     return false;
 559                 }
 560             }
 561 
 562             if (Double.isInfinite(t) || Double.isNaN(t)) {
 563                 // we've got a nonsense pick ray or triangle
 564                 return false;
 565             }
 566 
 567             if (result == null || !result.isCloser(t)) {
 568                 // it intersects, but we are not interested in the result
 569                 // or we already have a better (closer) result
 570                 // so we can omit the point and texture computation
 571                 return true;
 572             }
 573 
 574             Point3D point = PickResultChooser.computePoint(pickRay, t);
 575 
 576             // Now compute texture mapping. First rotate the triangle
 577             // so that we can compute in 2D
 578 
 579             // centroid = computeCentroid(v0, v1, v2);
 580             final Point3D centroid = computeCentroid(
 581                     v0x, v0y, v0z,
 582                     v1x, v1y, v1z,
 583                     v2x, v2y, v2z);
 584 
 585             // cv0 = v0.subtract(centroid)
 586             final Point3D cv0 = new Point3D(
 587                     v0x - centroid.getX(),
 588                     v0y - centroid.getY(),
 589                     v0z - centroid.getZ());
 590             // cv1 = v1.subtract(centroid)
 591             final Point3D cv1 = new Point3D(
 592                     v1x - centroid.getX(),
 593                     v1y - centroid.getY(),
 594                     v1z - centroid.getZ());
 595             // cv2 = v2.subtract(centroid)
 596             final Point3D cv2 = new Point3D(
 597                     v2x - centroid.getX(),
 598                     v2y - centroid.getY(),
 599                     v2z - centroid.getZ());
 600 
 601             final Point3D ce1 = cv1.subtract(cv0);
 602             final Point3D ce2 = cv2.subtract(cv0);
 603             Point3D n = ce1.crossProduct(ce2);
 604             if (n.getZ() < 0) {
 605                 n = new Point3D(-n.getX(), -n.getY(), -n.getZ());
 606             }
 607             final Point3D ax = n.crossProduct(Rotate.Z_AXIS);
 608             final double angle = Math.atan2(ax.magnitude(), n.dotProduct(Rotate.Z_AXIS));
 609 
 610             Rotate r = new Rotate(Math.toDegrees(angle), ax);
 611             final Point3D crv0 = r.transform(cv0);
 612             final Point3D crv1 = r.transform(cv1);
 613             final Point3D crv2 = r.transform(cv2);
 614             final Point3D rPoint = r.transform(point.subtract(centroid));
 615 
 616             final Point2D flatV0 = new Point2D(crv0.getX(), crv0.getY());
 617             final Point2D flatV1 = new Point2D(crv1.getX(), crv1.getY());
 618             final Point2D flatV2 = new Point2D(crv2.getX(), crv2.getY());
 619             final Point2D flatPoint = new Point2D(rPoint.getX(), rPoint.getY());
 620 
 621             // Obtain the texture triangle
 622             int texCoordElementSize = vertexFormat.getTexCoordElementSize();
 623             final int t0Idx = faces.get(faceIndex + 1) * texCoordElementSize;
 624             final int t1Idx = faces.get(faceIndex + 3) * texCoordElementSize;
 625             final int t2Idx = faces.get(faceIndex + 5) * texCoordElementSize;
 626 
 627             final Point2D u0 = new Point2D(texCoords.get(t0Idx), texCoords.get(t0Idx + 1));
 628             final Point2D u1 = new Point2D(texCoords.get(t1Idx), texCoords.get(t1Idx + 1));
 629             final Point2D u2 = new Point2D(texCoords.get(t2Idx), texCoords.get(t2Idx + 1));
 630 
 631             final Point2D txCentroid = computeCentroid(u0, u1, u2);
 632 
 633             final Point2D cu0 = u0.subtract(txCentroid);
 634             final Point2D cu1 = u1.subtract(txCentroid);
 635             final Point2D cu2 = u2.subtract(txCentroid);
 636 
 637             // Find the transform between the two triangles
 638 
 639             final Affine src = new Affine(
 640                     flatV0.getX(), flatV1.getX(), flatV2.getX(),
 641                     flatV0.getY(), flatV1.getY(), flatV2.getY());
 642             final Affine trg = new Affine(
 643                     cu0.getX(), cu1.getX(), cu2.getX(),
 644                     cu0.getY(), cu1.getY(), cu2.getY());
 645 
 646             Point2D txCoords = null;
 647 
 648             try {
 649                 src.invert();
 650                 trg.append(src);
 651                 txCoords = txCentroid.add(trg.transform(flatPoint));
 652             } catch (NonInvertibleTransformException e) {
 653                 // Can't compute texture mapping, probably the coordinates
 654                 // don't make sense. Ignore it and return null tex coords.
 655             }
 656 
 657             result.offer(candidate, t, 
 658                     reportFace ? faceIndex / vertexFormat.getFaceElementSize() : PickResult.FACE_UNDEFINED,
 659                     point, txCoords);
 660             return true;
 661         }
 662 
 663         return false;
 664     }
 665 
 666 
 667     /**
 668      * @treatAsPrivate implementation detail
 669      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 670      */
 671     @Override
 672     @Deprecated
 673     protected boolean impl_computeIntersects(PickRay pickRay, PickResultChooser pickResult, 
 674             Node candidate, CullFace cullFace, boolean reportFace) {
 675 
 676         boolean found = false;
 677         if (validate()) {
 678             final int size = faces.size();
 679 
 680             final Vec3d o = pickRay.getOriginNoClone();
 681 
 682             final Vec3d d = pickRay.getDirectionNoClone();
 683 
 684             for (int i = 0; i < size; i += vertexFormat.getFaceElementSize()) {
 685                 if (computeIntersectsFace(pickRay, o, d, i, cullFace, candidate,
 686                         reportFace, pickResult)) {
 687                     found = true;
 688                 }
 689             }
 690         }
 691         return found;
 692     }
 693 
 694     private class Listener<T extends ObservableArray<T>> implements ArrayChangeListener<T>, FloatArraySyncer, IntegerArraySyncer {
 695         
 696         protected final T array;
 697         protected boolean dirty = true;
 698         /**
 699          * Array was replaced
 700          * @return true if array was replaced; false otherwise
 701          */
 702         protected boolean dirtyInFull = true;
 703         protected int dirtyRangeFrom;
 704         protected int dirtyRangeLength;
 705 
 706         public Listener(T array) {
 707             this.array = array;
 708             array.addListener(this);
 709         }
 710 
 711         /**
 712          * Adds a dirty range
 713          * @param from index of the first modified element
 714          * @param length length of the modified range
 715          */
 716         protected final void addDirtyRange(int from, int length) {
 717             if (length > 0 && !dirtyInFull) {
 718                 markDirty();
 719                 if (dirtyRangeLength == 0) {
 720                     dirtyRangeFrom = from;
 721                     dirtyRangeLength = length;
 722                 } else {
 723                     int fromIndex = Math.min(dirtyRangeFrom, from);
 724                     int toIndex = Math.max(dirtyRangeFrom + dirtyRangeLength, from + length);
 725                     dirtyRangeFrom = fromIndex;
 726                     dirtyRangeLength = toIndex - fromIndex;
 727                 }
 728             }
 729         }
 730 
 731         protected void markDirty() {
 732             dirty = true;
 733             TriangleMesh.this.setDirty(true);
 734         }
 735 
 736         @Override
 737         public void onChanged(T observableArray, boolean sizeChanged, int from, int to) {
 738             if (sizeChanged) {
 739                 setDirty(true);
 740             } else {
 741                 addDirtyRange(from, to - from);
 742             }
 743             isValidDirty = true;
 744         }
 745 
 746         /**
 747          * @param dirty if true, the whole collection is marked as dirty;
 748          * if false, the whole collection is marked as not-dirty
 749          */
 750         public final void setDirty(boolean dirty) {
 751             this.dirtyInFull = dirty;
 752             if (dirty) {
 753                 markDirty();
 754                 dirtyRangeFrom = 0;
 755                 dirtyRangeLength = array.size();
 756             } else {
 757                 this.dirty = false;
 758                 dirtyRangeFrom = dirtyRangeLength = 0;
 759             }
 760         }
 761 
 762         @Override
 763         public float[] syncTo(float[] array) {
 764             ObservableFloatArray floatArray = (ObservableFloatArray) this.array;
 765             if (dirtyInFull || array == null || array.length != floatArray.size()) {
 766                 // Always allocate a new array when size changes
 767                 return floatArray.toArray(null);
 768             }
 769             floatArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength);
 770             return array;
 771         }
 772 
 773         @Override
 774         public int[] syncTo(int[] array) {
 775             ObservableIntegerArray intArray = (ObservableIntegerArray) this.array;
 776             if (dirtyInFull || array == null || array.length != intArray.size()) {
 777                 // Always allocate a new array when size changes
 778                 return intArray.toArray(null);
 779             }
 780             intArray.copyTo(dirtyRangeFrom, array, dirtyRangeFrom, dirtyRangeLength);
 781             return array;
 782         }
 783     }
 784 }