Java tutorial
/* * Copyright 1997-2008 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. * */ package javax.media.j3d; import java.awt.Font; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.font.GlyphMetrics; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.Iterator; import java.util.ServiceLoader; import javax.vecmath.Point3d; import javax.vecmath.Point3f; import javax.vecmath.Vector3f; /** * The Font3D object is used to store extruded 2D glyphs. These * 3D glyphs can then be used to construct Text3D NodeComponent * objects. * <P> * A 3D Font consists of a Java 2D font, a tesellation tolerance, * and an extrusion path. The extrusion * path creates depth by describing how the edge of a glyph varies * in the Z axis. * <P> * The construction of a Text3D object requires a Font3D object. * The Font3D object describes the style of the text, such as its * depth. The text also needs other classes, such as java.awt.Font and * FontExtrusion. The Font object describes the font name (Helvetica, * Courier, etc.), the font style (bold, Italic, etc.), and point * size. The FontExtrusion object extends Font3D by describing * the extrusion path for the Font3D object (how the edge of the * font glyph varies in the Z axis). *<P> * To ensure correct rendering, the 2D Font object should be created * with the default AffineTransform. The point size of the 2D font will * be used as a rough measure of how fine a tesselation to use when * creating the Font3D object: the larger the point size, in * general, the finer the tesselation. * <P> * Custom 3D fonts as well as methods to store 3D fonts * to disk will be addressed in a future release. * * @see java.awt.Font * @see FontExtrusion * @see Text3D */ public class Font3D extends NodeComponent { Font font; double tessellationTolerance; FontExtrusion fontExtrusion; FontRenderContext frc; // Used by triangulateGlyphs method to split contour data into islands. final static float EPS = 0.000001f; // Map glyph code to GeometryArrayRetained Hashtable<Character, GeometryArrayRetained> geomHash = new Hashtable<Character, GeometryArrayRetained>(20); /** * Constructs a Font3D object from the specified Font and * FontExtrusion objects, using the default value for the * tessellation tolerance. The default value is as follows: * * <ul> * tessellation tolerance : 0.01<br> * </ul> * <P> * The FontExtrusion object contains the extrusion path to use on * the 2D Font glyphs. To ensure correct rendering the font must * be created with the default AffineTransform. Passing null for * the FontExtrusion parameter results in no extrusion being done. * * @param font the Java 2D font used to create the 3D font object * @param extrudePath the extrusion path used to describe how * the edge of the font varies along the Z axis */ public Font3D(Font font, FontExtrusion extrudePath) { this(font, 0.01, extrudePath); } /** * Constructs a Font3D object from the specified Font and * FontExtrusion objects, using the specified tessellation * tolerance. * The FontExtrusion object contains the extrusion path to use on * the 2D Font glyphs. To ensure correct rendering, the font must * be created with the default AffineTransform. Passing null for * the FontExtrusion parameter results in no extrusion being done. * * @param font the Java 2D font used to create the 3D font object. * @param tessellationTolerance the tessellation tolerance value * used in tessellating the glyphs of the 2D Font. * This corresponds to the <code>flatness</code> parameter in * the <code>java.awt.Shape.getPathIterator</code> method. * @param extrudePath the extrusion path used to describe how * the edge of the font varies along the Z axis. * * @since Java 3D 1.2 */ public Font3D(Font font, double tessellationTolerance, FontExtrusion extrudePath) { this.font = font; this.tessellationTolerance = tessellationTolerance; this.fontExtrusion = extrudePath; this.frc = new FontRenderContext(new AffineTransform(), true, true); } /** * Returns the Java 2D Font used to create this Font3D object. * @return Font object used by this Font3D */ public Font getFont() { return this.font; } /** * Returns the tessellation tolerance with which this Font3D was * created. * @return the tessellation tolerance used by this Font3D * * @since Java 3D 1.2 */ public double getTessellationTolerance() { return tessellationTolerance; } /** * Copies the FontExtrusion object used to create this Font3D object * into the specified parameter. * * @param extrudePath object that will receive the * FontExtrusion information for this Font3D object */ public void getFontExtrusion(FontExtrusion extrudePath) { extrudePath = this.fontExtrusion; } /** * Returns the 3D bounding box of the specified glyph code. * * @param glyphCode the glyphCode from the original 2D Font * @param bounds the 3D glyph's bounds */ public void getBoundingBox(int glyphCode, BoundingBox bounds) { int[] gCodes = { glyphCode }; GlyphVector gVec = font.createGlyphVector(frc, gCodes); Rectangle2D.Float bounds2d = (Rectangle2D.Float) (((GlyphMetrics) (gVec.getGlyphMetrics(0))).getBounds2D()); Point3d lower = new Point3d(bounds2d.x, bounds2d.y, 0.0); Point3d upper; if (fontExtrusion != null) { upper = new Point3d(bounds2d.x + bounds2d.width, bounds2d.y + bounds2d.height, fontExtrusion.length); } else { upper = new Point3d(bounds2d.x + bounds2d.width, bounds2d.y + bounds2d.height, 0.0); } bounds.setLower(lower); bounds.setUpper(upper); } /** * An append-only array-based integer list */ private static class IntVector { int[] data; int size; IntVector() { data = new int[10]; size = 0; } void add(int i) { // need to expand backing if (size == data.length) data = Arrays.copyOf(data, 2 * size); data[size] = i; size++; } } // BY MIK OF CLASSX /** * Returns a GeometryArray of a glyph in this Font3D. * * @param c character from which to generate a tessellated glyph. * * @return a GeometryArray * * @since Java 3D 1.4 */ public GeometryArray getGlyphGeometry(char c) { char code[] = { c }; GlyphVector gv = font.createGlyphVector(frc, code); // triangulate the glyph GeometryArrayRetained glyph_gar = triangulateGlyphs(gv, code[0]); // Assume that triangulateGlyphs returns a triangle array with only coords & normals // (and without by-ref, interleaved, etc.) assert glyph_gar instanceof TriangleArrayRetained : "Font3D: GeometryArray is not an instance of TrangleArray"; assert glyph_gar.getVertexFormat() == (GeometryArray.COORDINATES | GeometryArray.NORMALS) : "Font3D: Illegal GeometryArray format -- only coordinates and normals expected"; // create a correctly sized TriangleArray TriangleArray ga = new TriangleArray(glyph_gar.getVertexCount(), glyph_gar.getVertexFormat()); // temp storage for coords, normals float tmp[] = new float[3]; int vertexCount = ga.getVertexCount(); for (int i = 0; i < vertexCount; i++) { // copy the glyph geometry to the TriangleArray glyph_gar.getCoordinate(i, tmp); ga.setCoordinate(i, tmp); glyph_gar.getNormal(i, tmp); ga.setNormal(i, tmp); } return ga; } // Triangulate glyph with 'unicode' if not already done. GeometryArrayRetained triangulateGlyphs(GlyphVector gv, char c) { Character ch = new Character(c); GeometryArrayRetained geo = geomHash.get(ch); if (geo == null) { // Font Y-axis is downwards, so send affine transform to flip it. Rectangle2D bnd = gv.getVisualBounds(); AffineTransform aTran = new AffineTransform(); double tx = bnd.getX() + 0.5 * bnd.getWidth(); double ty = bnd.getY() + 0.5 * bnd.getHeight(); aTran.setToTranslation(-tx, -ty); aTran.scale(1.0, -1.0); aTran.translate(tx, -ty); Shape shape = gv.getOutline(); PathIterator pIt = shape.getPathIterator(aTran, tessellationTolerance); int flag = -1, numPoints = 0, i, j, k, num = 0, vertCnt; UnorderList coords = new UnorderList(100, Point3f.class); float tmpCoords[] = new float[6]; float lastX = .0f, lastY = .0f; float firstPntx = Float.MAX_VALUE, firstPnty = Float.MAX_VALUE; IntVector contours = new IntVector(); float maxY = -Float.MAX_VALUE; int maxYIndex = 0, beginIdx = 0, endIdx = 0, start = 0; boolean setMaxY = false; while (!pIt.isDone()) { Point3f vertex = new Point3f(); flag = pIt.currentSegment(tmpCoords); if (flag == PathIterator.SEG_CLOSE) { if (num > 0) { if (setMaxY) { // Get Previous point beginIdx = start; endIdx = numPoints - 1; } contours.add(num); num = 0; } } else if (flag == PathIterator.SEG_MOVETO) { vertex.x = tmpCoords[0]; vertex.y = tmpCoords[1]; lastX = vertex.x; lastY = vertex.y; if ((lastX == firstPntx) && (lastY == firstPnty)) { pIt.next(); continue; } setMaxY = false; coords.add(vertex); firstPntx = lastX; firstPnty = lastY; if (num > 0) { contours.add(num); num = 0; } num++; numPoints++; // skip checking of first point, // since the last point will repeat this. start = numPoints; } else if (flag == PathIterator.SEG_LINETO) { vertex.x = tmpCoords[0]; vertex.y = tmpCoords[1]; //Check here for duplicate points. Code //later in this function can not handle //duplicate points. if ((vertex.x == lastX) && (vertex.y == lastY)) { pIt.next(); continue; } if (vertex.y > maxY) { maxY = vertex.y; maxYIndex = numPoints; setMaxY = true; } lastX = vertex.x; lastY = vertex.y; coords.add(vertex); num++; numPoints++; } pIt.next(); } // No data(e.g space, control characters) // Two point can't form a valid contour if (numPoints == 0) { return null; } // Determine font winding order use for side triangles Point3f p1 = new Point3f(), p2 = new Point3f(), p3 = new Point3f(); boolean flip_side_orient = true; Point3f vertices[] = (Point3f[]) coords.toArray(false); if (endIdx - beginIdx > 0) { // must be true unless it is a single line // define as "MoveTo p1 LineTo p2 Close" which is // not a valid font definition. if (maxYIndex == beginIdx) { p1.set(vertices[endIdx]); } else { p1.set(vertices[maxYIndex - 1]); } p2.set(vertices[maxYIndex]); if (maxYIndex == endIdx) { p3.set(vertices[beginIdx]); } else { p3.set(vertices[maxYIndex + 1]); } if (p3.x != p2.x) { if (p1.x != p2.x) { // Use the one with smallest slope if (Math.abs((p2.y - p1.y) / (p2.x - p1.x)) > Math.abs((p3.y - p2.y) / (p3.x - p2.x))) { flip_side_orient = (p3.x > p2.x); } else { flip_side_orient = (p2.x > p1.x); } } else { flip_side_orient = (p3.x > p2.x); } } else { // p1.x != p2.x, otherwise all three // point form a straight vertical line with // the middle point the highest. This is not a // valid font definition. flip_side_orient = (p2.x > p1.x); } } // Build a Tree of Islands int startIdx = 0; IslandsNode islandsTree = new IslandsNode(-1, -1); for (int cIdx = 0; cIdx < contours.size; cIdx++) { endIdx = startIdx + contours.data[cIdx]; islandsTree.insert(new IslandsNode(startIdx, endIdx), vertices); startIdx = endIdx; } coords = null; // Free memory contours = null; // Compute islandCounts[][] and outVerts[][] UnorderList islandsList = new UnorderList(10, IslandsNode.class); islandsTree.collectOddLevelNode(islandsList, 0); IslandsNode nodes[] = (IslandsNode[]) islandsList.toArray(false); int islandCounts[][] = new int[islandsList.arraySize()][]; Point3f outVerts[][] = new Point3f[islandCounts.length][]; int nchild, sum; IslandsNode node; for (i = 0; i < islandCounts.length; i++) { node = nodes[i]; nchild = node.numChild(); islandCounts[i] = new int[nchild + 1]; islandCounts[i][0] = node.numVertices(); sum = 0; sum += islandCounts[i][0]; for (j = 0; j < nchild; j++) { islandCounts[i][j + 1] = node.getChild(j).numVertices(); sum += islandCounts[i][j + 1]; } outVerts[i] = new Point3f[sum]; startIdx = 0; for (k = node.startIdx; k < node.endIdx; k++) { outVerts[i][startIdx++] = vertices[k]; } for (j = 0; j < nchild; j++) { endIdx = node.getChild(j).endIdx; for (k = node.getChild(j).startIdx; k < endIdx; k++) { outVerts[i][startIdx++] = vertices[k]; } } } islandsTree = null; // Free memory islandsList = null; vertices = null; int[] contourCounts = new int[1]; ArrayList<GeometryArray> triangData = new ArrayList<GeometryArray>(); Point3f q1 = new Point3f(), q2 = new Point3f(), q3 = new Point3f(); Vector3f n1 = new Vector3f(), n2 = new Vector3f(); numPoints = 0; for (i = 0; i < islandCounts.length; i++) { numPoints += outVerts[i].length; } final GeometryService gs = newGeometryService(); int vertOffset = gs.triangulateIslands(islandCounts, outVerts, contourCounts, triangData); // Multiply by 2 since we create 2 faces of the font // Second term is for side-faces along depth of the font if (fontExtrusion == null) vertCnt = vertOffset; else { if (fontExtrusion.shape == null) vertCnt = vertOffset * 2 + numPoints * 6; else { vertCnt = vertOffset * 2 + numPoints * 6 * (fontExtrusion.pnts.length - 1); } } // XXXX: Should use IndexedTriangleArray to avoid // duplication of vertices. To create triangles for // side faces, every vertex is duplicated currently. TriangleArray triAry = new TriangleArray(vertCnt, GeometryArray.COORDINATES | GeometryArray.NORMALS); boolean flip_orient[] = new boolean[islandCounts.length]; boolean findOrient; // last known non-degenerate normal Vector3f goodNormal = new Vector3f(); int currCoordIndex = 0; for (j = 0; j < islandCounts.length; j++) { GeometryArray ga = triangData.get(j); vertOffset = ga.getVertexCount(); findOrient = false; //Create the triangle array for (i = 0; i < vertOffset; i += 3, currCoordIndex += 3) { //Get 3 points. Since triangle is known to be flat, normal // must be same for all 3 points. ga.getCoordinate(i, p1); ga.getNormal(i, n1); ga.getCoordinate(i + 1, p2); ga.getCoordinate(i + 2, p3); if (!findOrient) { //Check here if triangles are wound incorrectly and need //to be flipped. if (!getNormal(p1, p2, p3, n2)) { continue; } if (n2.z >= EPS) { flip_orient[j] = false; } else if (n2.z <= -EPS) { flip_orient[j] = true; } else { continue; } findOrient = true; } if (flip_orient[j]) { //New Triangulator preserves contour orientation. If contour //input is wound incorrectly, swap 2nd and 3rd points to //sure all triangles are wound correctly for j3d. q1.x = p2.x; q1.y = p2.y; q1.z = p2.z; p2.x = p3.x; p2.y = p3.y; p2.z = p3.z; p3.x = q1.x; p3.y = q1.y; p3.z = q1.z; n1.x = -n1.x; n1.y = -n1.y; n1.z = -n1.z; } if (fontExtrusion != null) { n2.x = -n1.x; n2.y = -n1.y; n2.z = -n1.z; triAry.setCoordinate(currCoordIndex, p1); triAry.setNormal(currCoordIndex, n2); triAry.setCoordinate(currCoordIndex + 1, p3); triAry.setNormal(currCoordIndex + 1, n2); triAry.setCoordinate(currCoordIndex + 2, p2); triAry.setNormal(currCoordIndex + 2, n2); q1.x = p1.x; q1.y = p1.y; q1.z = p1.z + fontExtrusion.length; q2.x = p2.x; q2.y = p2.y; q2.z = p2.z + fontExtrusion.length; q3.x = p3.x; q3.y = p3.y; q3.z = p3.z + fontExtrusion.length; triAry.setCoordinate(currCoordIndex + vertOffset, q1); triAry.setNormal(currCoordIndex + vertOffset, n1); triAry.setCoordinate(currCoordIndex + 1 + vertOffset, q2); triAry.setNormal(currCoordIndex + 1 + vertOffset, n1); triAry.setCoordinate(currCoordIndex + 2 + vertOffset, q3); triAry.setNormal(currCoordIndex + 2 + vertOffset, n1); } else { triAry.setCoordinate(currCoordIndex, p1); triAry.setNormal(currCoordIndex, n1); triAry.setCoordinate(currCoordIndex + 1, p2); triAry.setNormal(currCoordIndex + 1, n1); triAry.setCoordinate(currCoordIndex + 2, p3); triAry.setNormal(currCoordIndex + 2, n1); } } if (fontExtrusion != null) { currCoordIndex += vertOffset; } } //Now add side triangles in both cases. // Since we duplicated triangles with different Z, make sure // currCoordIndex points to correct location. if (fontExtrusion != null) { if (fontExtrusion.shape == null) { boolean smooth; // we'll put a crease if the angle between the normals is // greater than 44 degrees float threshold = (float) Math.cos(44.0 * Math.PI / 180.0); float cosine; // need the previous normals to check for smoothing Vector3f pn1 = null, pn2 = null; // need the next normals to check for smoothing Vector3f n3 = new Vector3f(), n4 = new Vector3f(); // store the normals for each point because they are // the same for both triangles Vector3f p1Normal = new Vector3f(); Vector3f p2Normal = new Vector3f(); Vector3f p3Normal = new Vector3f(); Vector3f q1Normal = new Vector3f(); Vector3f q2Normal = new Vector3f(); Vector3f q3Normal = new Vector3f(); for (i = 0; i < islandCounts.length; i++) { for (j = 0, k = 0, num = 0; j < islandCounts[i].length; j++) { num += islandCounts[i][j]; p1.x = outVerts[i][num - 1].x; p1.y = outVerts[i][num - 1].y; p1.z = 0.0f; q1.x = p1.x; q1.y = p1.y; q1.z = p1.z + fontExtrusion.length; p2.z = 0.0f; q2.z = p2.z + fontExtrusion.length; for (int m = 0; m < num; m++) { p2.x = outVerts[i][m].x; p2.y = outVerts[i][m].y; q2.x = p2.x; q2.y = p2.y; if (getNormal(p1, q1, p2, n1)) { if (!flip_side_orient) { n1.negate(); } goodNormal.set(n1); break; } } for (; k < num; k++) { p2.x = outVerts[i][k].x; p2.y = outVerts[i][k].y; p2.z = 0.0f; q2.x = p2.x; q2.y = p2.y; q2.z = p2.z + fontExtrusion.length; if (!getNormal(p1, q1, p2, n1)) { n1.set(goodNormal); } else { if (!flip_side_orient) { n1.negate(); } goodNormal.set(n1); } if (!getNormal(p2, q1, q2, n2)) { n2.set(goodNormal); } else { if (!flip_side_orient) { n2.negate(); } goodNormal.set(n2); } // if there is a previous normal, see if we need to smooth // this normal or make a crease if (pn1 != null) { cosine = n1.dot(pn2); smooth = cosine > threshold; if (smooth) { p1Normal.x = (pn1.x + pn2.x + n1.x); p1Normal.y = (pn1.y + pn2.y + n1.y); p1Normal.z = (pn1.z + pn2.z + n1.z); normalize(p1Normal); q1Normal.x = (pn2.x + n1.x + n2.x); q1Normal.y = (pn2.y + n1.y + n2.y); q1Normal.z = (pn2.z + n1.z + n2.z); normalize(q1Normal); } // if smooth else { p1Normal.x = n1.x; p1Normal.y = n1.y; p1Normal.z = n1.z; q1Normal.x = n1.x + n2.x; q1Normal.y = n1.y + n2.y; q1Normal.z = n1.z + n2.z; normalize(q1Normal); } // else } // if pn1 != null else { pn1 = new Vector3f(); pn2 = new Vector3f(); p1Normal.x = n1.x; p1Normal.y = n1.y; p1Normal.z = n1.z; q1Normal.x = (n1.x + n2.x); q1Normal.y = (n1.y + n2.y); q1Normal.z = (n1.z + n2.z); normalize(q1Normal); } // else // if there is a next, check if we should smooth normal if (k + 1 < num) { p3.x = outVerts[i][k + 1].x; p3.y = outVerts[i][k + 1].y; p3.z = 0.0f; q3.x = p3.x; q3.y = p3.y; q3.z = p3.z + fontExtrusion.length; if (!getNormal(p2, q2, p3, n3)) { n3.set(goodNormal); } else { if (!flip_side_orient) { n3.negate(); } goodNormal.set(n3); } if (!getNormal(p3, q2, q3, n4)) { n4.set(goodNormal); } else { if (!flip_side_orient) { n4.negate(); } goodNormal.set(n4); } cosine = n2.dot(n3); smooth = cosine > threshold; if (smooth) { p2Normal.x = (n1.x + n2.x + n3.x); p2Normal.y = (n1.y + n2.y + n3.y); p2Normal.z = (n1.z + n2.z + n3.z); normalize(p2Normal); q2Normal.x = (n2.x + n3.x + n4.x); q2Normal.y = (n2.y + n3.y + n4.y); q2Normal.z = (n2.z + n3.z + n4.z); normalize(q2Normal); } else { // if smooth p2Normal.x = n1.x + n2.x; p2Normal.y = n1.y + n2.y; p2Normal.z = n1.z + n2.z; normalize(p2Normal); q2Normal.x = n2.x; q2Normal.y = n2.y; q2Normal.z = n2.z; } // else } else { // if k+1 < num p2Normal.x = (n1.x + n2.x); p2Normal.y = (n1.y + n2.y); p2Normal.z = (n1.z + n2.z); normalize(p2Normal); q2Normal.x = n2.x; q2Normal.y = n2.y; q2Normal.z = n2.z; } // else // add pts for the 2 tris // p1, q1, p2 and p2, q1, q2 if (flip_side_orient) { triAry.setCoordinate(currCoordIndex, p1); triAry.setNormal(currCoordIndex, p1Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, q1Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, p2Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, p2Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, q1Normal); currCoordIndex++; } else { triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, q1Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p1); triAry.setNormal(currCoordIndex, p1Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, p2Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, q1Normal); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, p2Normal); currCoordIndex++; } triAry.setCoordinate(currCoordIndex, q2); triAry.setNormal(currCoordIndex, q2Normal); currCoordIndex++; pn1.x = n1.x; pn1.y = n1.y; pn1.z = n1.z; pn2.x = n2.x; pn2.y = n2.y; pn2.z = n2.z; p1.x = p2.x; p1.y = p2.y; p1.z = p2.z; q1.x = q2.x; q1.y = q2.y; q1.z = q2.z; } // for k // set the previous normals to null when we are done pn1 = null; pn2 = null; } // for j } //for i } else { // if shape int m, offset = 0; Point3f P2 = new Point3f(), Q2 = new Point3f(), P1 = new Point3f(); Vector3f nn = new Vector3f(), nn1 = new Vector3f(), nn2 = new Vector3f(), nn3 = new Vector3f(); Vector3f nna = new Vector3f(), nnb = new Vector3f(); float length; boolean validNormal = false; // fontExtrusion.shape is specified, and is NOT straight line for (i = 0; i < islandCounts.length; i++) { for (j = 0, k = 0, offset = num = 0; j < islandCounts[i].length; j++) { num += islandCounts[i][j]; p1.x = outVerts[i][num - 1].x; p1.y = outVerts[i][num - 1].y; p1.z = 0.0f; q1.x = p1.x; q1.y = p1.y; q1.z = p1.z + fontExtrusion.length; p3.z = 0.0f; for (m = num - 2; m >= 0; m--) { p3.x = outVerts[i][m].x; p3.y = outVerts[i][m].y; if (getNormal(p3, q1, p1, nn1)) { if (!flip_side_orient) { nn1.negate(); } goodNormal.set(nn1); break; } } for (; k < num; k++) { p2.x = outVerts[i][k].x; p2.y = outVerts[i][k].y; p2.z = 0.0f; q2.x = p2.x; q2.y = p2.y; q2.z = p2.z + fontExtrusion.length; getNormal(p1, q1, p2, nn2); p3.x = outVerts[i][(k + 1) == num ? offset : (k + 1)].x; p3.y = outVerts[i][(k + 1) == num ? offset : (k + 1)].y; p3.z = 0.0f; if (!getNormal(p3, p2, q2, nn3)) { nn3.set(goodNormal); } else { if (!flip_side_orient) { nn3.negate(); } goodNormal.set(nn3); } // Calculate normals at the point by averaging normals // of two faces on each side of the point. nna.x = (nn1.x + nn2.x); nna.y = (nn1.y + nn2.y); nna.z = (nn1.z + nn2.z); normalize(nna); nnb.x = (nn3.x + nn2.x); nnb.y = (nn3.y + nn2.y); nnb.z = (nn3.z + nn2.z); normalize(nnb); P1.x = p1.x; P1.y = p1.y; P1.z = p1.z; P2.x = p2.x; P2.y = p2.y; P2.z = p2.z; Q2.x = q2.x; Q2.y = q2.y; Q2.z = q2.z; for (m = 1; m < fontExtrusion.pnts.length; m++) { q1.z = q2.z = fontExtrusion.pnts[m].x; q1.x = P1.x + nna.x * fontExtrusion.pnts[m].y; q1.y = P1.y + nna.y * fontExtrusion.pnts[m].y; q2.x = P2.x + nnb.x * fontExtrusion.pnts[m].y; q2.y = P2.y + nnb.y * fontExtrusion.pnts[m].y; if (!getNormal(p1, q1, p2, n1)) { n1.set(goodNormal); } else { if (!flip_side_orient) { n1.negate(); } goodNormal.set(n1); } if (flip_side_orient) { triAry.setCoordinate(currCoordIndex, p1); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; } else { triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p1); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; } triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; if (!getNormal(p2, q1, q2, n1)) { n1.set(goodNormal); } else { if (!flip_side_orient) { n1.negate(); } goodNormal.set(n1); } if (flip_side_orient) { triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; } else { triAry.setCoordinate(currCoordIndex, q1); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; triAry.setCoordinate(currCoordIndex, p2); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; } triAry.setCoordinate(currCoordIndex, q2); triAry.setNormal(currCoordIndex, n1); currCoordIndex++; p1.x = q1.x; p1.y = q1.y; p1.z = q1.z; p2.x = q2.x; p2.y = q2.y; p2.z = q2.z; } // for m p1.x = P2.x; p1.y = P2.y; p1.z = P2.z; q1.x = Q2.x; q1.y = Q2.y; q1.z = Q2.z; nn1.x = nn2.x; nn1.y = nn2.y; nn1.z = nn2.z; } // for k offset = num; } // for j } //for i } // if shape } // if fontExtrusion geo = (GeometryArrayRetained) triAry.retained; geomHash.put(ch, geo); } return geo; } private GeometryService newGeometryService() { final ServiceLoader<GeometryService> gsLoader = ServiceLoader.load(GeometryService.class); final Iterator<GeometryService> iter = gsLoader.iterator(); if (iter.hasNext()) return iter.next(); throw new IllegalStateException( "No GeometryService implementation found. " + "Please add j3d-core-utils to the classpath."); } static boolean getNormal(Point3f p1, Point3f p2, Point3f p3, Vector3f normal) { Vector3f v1 = new Vector3f(); Vector3f v2 = new Vector3f(); // Must compute normal v1.sub(p2, p1); v2.sub(p2, p3); normal.cross(v1, v2); normal.negate(); float length = normal.length(); if (length > 0) { length = 1 / length; normal.x *= length; normal.y *= length; normal.z *= length; return true; } return false; } // check if 2 contours are inside/outside/intersect one another // INPUT: // vertCnt1, vertCnt2 - number of vertices in 2 contours // begin1, begin2 - starting indices into vertices for 2 contours // vertices - actual vertex data // OUTPUT: // status == 1 - intersecting contours // 2 - first contour inside the second // 3 - second contour inside the first // 0 - disjoint contours(2 islands) static int check2Contours(int begin1, int end1, int begin2, int end2, Point3f[] vertices) { int i; boolean inside2, inside1; inside2 = pointInPolygon2D(vertices[begin1].x, vertices[begin1].y, begin2, end2, vertices); for (i = begin1 + 1; i < end1; i++) { if (pointInPolygon2D(vertices[i].x, vertices[i].y, begin2, end2, vertices) != inside2) { return 1; //intersecting contours } } // Since we are using point in polygon test and not // line in polygon test. There are cases we miss the interesting // if we are not checking the reverse for all points. This happen // when two points form a line pass through a polygon but the two // points are outside of it. inside1 = pointInPolygon2D(vertices[begin2].x, vertices[begin2].y, begin1, end1, vertices); for (i = begin2 + 1; i < end2; i++) { if (pointInPolygon2D(vertices[i].x, vertices[i].y, begin1, end1, vertices) != inside1) { return 1; //intersecting contours } } if (!inside2) { if (!inside1) { return 0; // disjoint countours } // inside2 = false and inside1 = true return 3; // second contour inside first } // must be inside2 = true and inside1 = false // Note that it is not possible inside2 = inside1 = true // unless two contour overlap to each others. // return 2; // first contour inside second } // Test if 2D point (x,y) lies inside polygon represented by verts. // z-value of polygon vertices is ignored. Sent only to avoid data-copy. // Uses ray-shooting algorithm to compute intersections along +X axis. // This algorithm works for all polygons(concave, self-intersecting) and // is best solution here due to large number of polygon vertices. // Point is INSIDE if number of intersections is odd, OUTSIDE if number // of intersections is even. static boolean pointInPolygon2D(float x, float y, int begIdx, int endIdx, Point3f[] verts) { int i, num_intersections = 0; float xi; for (i = begIdx; i < endIdx - 1; i++) { if ((verts[i].y >= y && verts[i + 1].y >= y) || (verts[i].y < y && verts[i + 1].y < y)) continue; xi = verts[i].x + (verts[i].x - verts[i + 1].x) * (y - verts[i].y) / (verts[i].y - verts[i + 1].y); if (x < xi) num_intersections++; } // Check for segment from last vertex to first vertex. if (!((verts[i].y >= y && verts[begIdx].y >= y) || (verts[i].y < y && verts[begIdx].y < y))) { xi = verts[i].x + (verts[i].x - verts[begIdx].x) * (y - verts[i].y) / (verts[i].y - verts[begIdx].y); if (x < xi) num_intersections++; } return ((num_intersections % 2) != 0); } static final boolean normalize(Vector3f v) { float len = v.length(); if (len > 0) { len = 1.0f / len; v.x *= len; v.y *= len; v.z *= len; return true; } return false; } // A Tree of islands form based on contour, each parent's contour // enclosed all the child. We built this since Triangular fail to // handle the case of multiple concentrated contours. i.e. if // 4 contours A > B > C > D. Triangular will fail recongized // two island, one form by A & B and the other by C & D. // Using this tree we can separate out every 2 levels and pass // in to triangular to workaround its limitation. static private class IslandsNode { private ArrayList<IslandsNode> islandsList = null; int startIdx, endIdx; IslandsNode(int startIdx, int endIdx) { this.startIdx = startIdx; this.endIdx = endIdx; islandsList = null; } void addChild(IslandsNode node) { if (islandsList == null) islandsList = new ArrayList<IslandsNode>(5); islandsList.add(node); } void removeChild(IslandsNode node) { islandsList.remove(islandsList.indexOf(node)); } IslandsNode getChild(int idx) { return islandsList.get(idx); } int numChild() { return (islandsList == null ? 0 : islandsList.size()); } int numVertices() { return endIdx - startIdx; } void insert(IslandsNode newNode, Point3f[] vertices) { boolean createNewLevel = false; if (islandsList != null) { IslandsNode childNode; int status; for (int i = numChild() - 1; i >= 0; i--) { childNode = getChild(i); status = check2Contours(newNode.startIdx, newNode.endIdx, childNode.startIdx, childNode.endIdx, vertices); switch (status) { case 2: // newNode inside childNode, go down recursively childNode.insert(newNode, vertices); return; case 3:// childNode inside newNode, // continue to search other childNode also // inside this one and group them together. newNode.addChild(childNode); createNewLevel = true; break; default: // intersecting or disjoint } } } if (createNewLevel) { // Remove child in newNode from this for (int i = newNode.numChild() - 1; i >= 0; i--) { removeChild(newNode.getChild(i)); } // Add the newNode to parent } addChild(newNode); } // Return a list of node with odd number of level void collectOddLevelNode(UnorderList list, int level) { if ((level % 2) == 1) { list.add(this); } if (islandsList != null) { level++; for (int i = numChild() - 1; i >= 0; i--) { getChild(i).collectOddLevelNode(list, level); } } } } }