/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.font; //import com.jaamsim.math.*; import java.awt.Font; import java.awt.Shape; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.Vector; import com.jogamp.opengl.GL2GL3; import com.jogamp.opengl.glu.GLU; import com.jogamp.opengl.glu.GLUtessellator; import com.jogamp.opengl.glu.GLUtessellatorCallbackAdapter; import com.jaamsim.math.Vec3d; import com.jaamsim.render.RenderUtils; import com.jaamsim.render.Renderer; import com.jaamsim.render.TessFontKey; /** * A simple tesselated font, takes an AWT font and creates renderable characters * from it The tesselator is based on the GLU tesselator. Vertex lists are * created lazily and cached indefinitely so this object may become quite large * as time goes on. * * In order to use this class, it should be passed to a TessString, which is a 'Renderable' * * @author Matt Chudleigh * */ public class TessFont { private HashMap<Integer, TessChar> _charMap; private final Font _font; private final TessFontKey _key; private final FontRenderContext _frc; private final ArrayList<double[]> _vertices; private boolean _glBufferDirty = true; // The height of the 'classic' character used to scale the overall rendering height private double _nominalHeight; // System wide asset ID private int _id; private int _glVertBuffer = -1; public TessFont(TessFontKey key) { _frc = new FontRenderContext(null, true, true); _font = new Font(key.getFontName(), key.getFontStyle(), 1); _key = key; _vertices = new ArrayList<>(); _charMap = new HashMap<>(); // Originally support all the basic latin characters (will lazily add new ones as needed) String initialChars= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890.,/<>?;':\"[]{}!@#$%^&*()_+-= \t"; for (int i = 0; i < initialChars.length(); ++i) { generateChar(initialChars.charAt(i)); // Note, none of these are supplementary, so this is safe } _id = Renderer.getAssetID(); _nominalHeight = _charMap.get((int)'A').getHeight(); } /** * Retrieve the tesselated character representation of 'c'. Will try to load * a cached version but may need to generate a new tesselation. This will * only need to be done once. See preloadCache() to avoid lazy * initialization * * @param c * @return */ public synchronized TessChar getTessChar(int cp) { TessChar cachedChar = _charMap.get(cp); // Load any characters this font has not loaded before if (cachedChar == null) { generateChar(cp); cachedChar = _charMap.get(cp); } return cachedChar; } private static class CharTesselator extends GLUtessellatorCallbackAdapter { private int _type; private Vector<Double> _verts; private int vertsInPrim; boolean oddStrip; // Need to wind every other triangle in a triangle strip backwards (it's just part of the strip) private double[] temp; // Used to build up a triangle fan or strip public CharTesselator() { _verts = new Vector<>(); temp = new double[4]; } @Override public void begin(int type) { _type = type; vertsInPrim = 0; oddStrip = false; // if (type == GL.GL_TRIANGLES) { // LogBox.formatRenderLog("New Triangles"); // } // if (type == GL.GL_TRIANGLE_FAN) { // LogBox.formatRenderLog("New Fan"); // } // if (type == GL.GL_TRIANGLE_STRIP) { // LogBox.formatRenderLog("New Strip"); // } } @Override public void end() { // Make sure we're still saving whole triangles assert((_verts.size() % 6) == 0); } @Override public void vertex(Object vertData) { double[] verts = (double[]) vertData; if (_type == GL2GL3.GL_TRIANGLES) { // For triangles, just add the vertices _verts.add(verts[0]); // x _verts.add(verts[1]); // y vertsInPrim = (vertsInPrim + 1) % 3; // if (vertsInPrim == 0) { // checkWinding("triangle"); // } return; } else if (_type == GL2GL3.GL_TRIANGLE_FAN) { triangleStripFanVerts(verts, false); return; } else if (_type == GL2GL3.GL_TRIANGLE_STRIP) { triangleStripFanVerts(verts, true); return; } else { assert(false); } } /** * Handle vertices for a triangle strip or fan * @param verts * @param isStrip */ private void triangleStripFanVerts(double[] verts, boolean isStrip) { if (vertsInPrim == 0) { temp[0] = verts[0]; temp[1] = verts[1]; vertsInPrim++; return; } else if (vertsInPrim == 1) { temp[2] = verts[0]; temp[3] = verts[1]; vertsInPrim++; return; } // Is the third or more vertex // If this is an odd number primitive in a strip, add the old vertices in reverse order if (isStrip && oddStrip) { _verts.add(temp[2]); _verts.add(temp[3]); _verts.add(temp[0]); _verts.add(temp[1]); _verts.add(verts[0]); _verts.add(verts[1]); } else { _verts.add(temp[0]); _verts.add(temp[1]); _verts.add(temp[2]); _verts.add(temp[3]); _verts.add(verts[0]); _verts.add(verts[1]); } oddStrip = !oddStrip; //checkWinding(isStrip ? "strip" : "fan"); // If this is a strip, the first temp is overridden (not for a fan though) if (isStrip) { temp[0] = temp[2]; temp[1] = temp[3]; } // Store this vert for the next triangle temp[2] = verts[0]; temp[3] = verts[1]; vertsInPrim++; return; } /** * Debug, check the winding of the current triangle, and output an error if it's wrong */ // private void checkWinding(String type) { // if (!type.equals("strip")) { // //return; // } // int vertLength = _verts.size(); // Vector4d vert0 = new Vector4d(_verts.get(vertLength - 6), _verts.get(vertLength - 5), 0); // Vector4d vert1 = new Vector4d(_verts.get(vertLength - 4), _verts.get(vertLength - 3), 0); // Vector4d vert2 = new Vector4d(_verts.get(vertLength - 2), _verts.get(vertLength - 1), 0); // Vector4d zeroToOne = new Vector4d(0.0d, 0.0d, 0.0d, 1.0d); // Vector4d oneToTwo = new Vector4d(0.0d, 0.0d, 0.0d, 1.0d); // vert1.sub3(vert0, zeroToOne); // vert2.sub3(vert1, oneToTwo); // Vector4d cross = new Vector4d(0.0d, 0.0d, 0.0d, 1.0d); // zeroToOne.cross(oneToTwo, cross); // if (cross.data[2] < 0) { // // This triangle is backwards wound // LogBox.formatRenderLog("Triangle is wound backwards for: " + type); // } // else // { // LogBox.formatRenderLog("Triangle is good for: " + type); // } // } @Override public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) { // We don't include any additional data double[] newCoord = new double[2]; newCoord[0] = coords[0]; newCoord[1] = coords[1]; outData[0] = newCoord; } @Override public void error(int errNum) { @SuppressWarnings("unused") String errorString = GLU.createGLU().gluErrorString(errNum); assert(false); // TODO: Handle this properly? } public Vector<Double> getVerts() { return _verts; } } private static class TessOutput { public Rectangle2D bounds; public double[] verts; public double[] advances; // one horizontal advance per character entered } private TessOutput tesselateString(String s) { GlyphVector gv = _font.createGlyphVector(_frc, s); Shape shape = gv.getOutline(); // AffineTransform at = new AffineTransform(); at.scale(1, -1); PathIterator pIt = shape.getPathIterator(at, _font.getSize()/200.0); // Create a GLU tesselator GLUtessellator tess = GLU.gluNewTess(); CharTesselator tessAdapt = new CharTesselator(); GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, tessAdapt); GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, tessAdapt); GLU.gluTessCallback(tess, GLU.GLU_TESS_END, tessAdapt); GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, tessAdapt); GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, tessAdapt); int winding = pIt.getWindingRule(); if (winding == PathIterator.WIND_EVEN_ODD) GLU.gluTessProperty(tess, GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_ODD); else if (winding == PathIterator.WIND_NON_ZERO) GLU.gluTessProperty(tess, GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_NONZERO); else assert(false); // PathIterator should only return these two winding rules GLU.gluBeginPolygon(tess); GLU.gluTessNormal(tess, 0, 0, 1); double[] first = null; double[] v; while (!pIt.isDone()) { v = new double[3]; int type = pIt.currentSegment(v); v[2] = 0.0; if (type == PathIterator.SEG_MOVETO) { first = v; GLU.gluNextContour(tess, GLU.GLU_UNKNOWN); GLU.gluTessVertex(tess, v, 0, v); } else if (type == PathIterator.SEG_LINETO) { GLU.gluTessVertex(tess, v, 0, v); } else if (type == PathIterator.SEG_CLOSE) { assert(first != null); // If this is true, there is an error in the AWT path iterator GLU.gluTessVertex(tess, first, 0, first); first = null; } else { assert(false); // The path itertor should not return other path types here } pIt.next(); } GLU.gluEndPolygon(tess); int numVerts = tessAdapt.getVerts().size(); double[] verts = new double[numVerts]; int count = 0; for (double d : tessAdapt.getVerts()) { verts[count++] = d; } TessOutput ret = new TessOutput(); ret.verts = verts; ret.bounds = gv.getVisualBounds(); ret.advances = new double[s.length()]; for (int i = 0; i < s.length(); ++i) { ret.advances[i] = gv.getGlyphMetrics(i).getAdvance(); } return ret; } private void generateChar(int cp) { StringBuilder sb = new StringBuilder(); sb.appendCodePoint(cp); String s = sb.toString(); TessOutput tessed = tesselateString(s); int totalVerts = 0; for (double[] ds : _vertices) { totalVerts += ds.length; } assert((totalVerts % 2) == 0); assert((tessed.verts.length % 2) == 0); // startIndex is the index of points in the GL buffer this character starts at int startIndex = totalVerts / 2; // numVerts is the number of vertices in the GL buffer to draw int numVerts = tessed.verts.length / 2; // Append the verts to the list _vertices.add(tessed.verts); TessChar tc = new TessChar(cp, startIndex, numVerts, tessed.bounds.getWidth(), tessed.bounds.getHeight(), tessed.advances[0]); _charMap.put(cp, tc); _glBufferDirty = true; } private void setupBuffer(GL2GL3 gl) { // Create an OpenGL buffer int[] buffs = new int[1]; gl.glGenBuffers(1, buffs, 0); _glVertBuffer = buffs[0]; _glBufferDirty = true; } public TessFontKey getFontKey() { return _key; } public int getAssetID() { return _id; } public synchronized int getGLBuffer(GL2GL3 gl) { // The buffer may not have been initialized yet if (_glVertBuffer == -1 ) { setupBuffer(gl); } if (_glBufferDirty) { int totalVerts = 0; for (double[] d : _vertices) { totalVerts += d.length; } FloatBuffer fb = FloatBuffer.allocate(totalVerts); for (double[] ds : _vertices) { for (double d : ds) { fb.put((float)d); } } fb.flip(); gl.glBindBuffer(GL2GL3.GL_ARRAY_BUFFER, _glVertBuffer); gl.glBufferData(GL2GL3.GL_ARRAY_BUFFER, totalVerts * 4, fb, GL2GL3.GL_STATIC_DRAW); gl.glBindBuffer(GL2GL3.GL_ARRAY_BUFFER, 0); _glBufferDirty = false; } return _glVertBuffer; } /** * Get the dimensions of the fully rendered string (useful for app level layout) * @param textHeight - the requested text height * @param string - the string to render * @return */ public Vec3d getStringSize(double textHeight, String string) { return new Vec3d(getStringLength(textHeight, string), textHeight, 0.0d); } public double getStringLength(double textHeight, String string) { if (string == null) return 0.0d; double width = 0.0d; for (int cp : RenderUtils.stringToCodePoints(string)) { TessChar tc = getTessChar(cp); width += tc.getAdvance(); } return width * textHeight / getNominalHeight(); } /** * Returns the index of the first character in the string whose x-coordinate * is closest to the given value. * @param textHeight - height of the text in metres * @param string - given string * @param x - x-coordinate whose index number is to be found * @return index number in the string. */ public int getStringPosition(double textHeight, String string, double x) { if (string == null) return 0; double scaledX = x / textHeight * getNominalHeight(); double width = 0.0d; int[] cpList = RenderUtils.stringToCodePoints(string); for (int i=0; i < cpList.length; i++) { TessChar tc = getTessChar(cpList[i]); if (width + 0.5d*tc.getAdvance() >= scaledX) return i; width += tc.getAdvance(); } return cpList.length; } public double getNominalHeight() { return _nominalHeight; } } // class TessFont