aboutsummaryrefslogtreecommitdiffstats
path: root/src/jogl/classes/com/sun/opengl/util/awt/TextRenderer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/jogl/classes/com/sun/opengl/util/awt/TextRenderer.java')
-rwxr-xr-xsrc/jogl/classes/com/sun/opengl/util/awt/TextRenderer.java1952
1 files changed, 1952 insertions, 0 deletions
diff --git a/src/jogl/classes/com/sun/opengl/util/awt/TextRenderer.java b/src/jogl/classes/com/sun/opengl/util/awt/TextRenderer.java
new file mode 100755
index 000000000..b7616d879
--- /dev/null
+++ b/src/jogl/classes/com/sun/opengl/util/awt/TextRenderer.java
@@ -0,0 +1,1952 @@
+/*
+ * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * - Redistribution of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistribution in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ *
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ *
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+package com.sun.opengl.util.awt;
+
+import com.sun.opengl.impl.*;
+import com.sun.opengl.impl.packrect.*;
+import com.sun.opengl.util.*;
+import com.sun.opengl.util.texture.*;
+import com.sun.opengl.util.texture.awt.*;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Composite;
+
+// For debugging purposes
+import java.awt.EventQueue;
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Point;
+import java.awt.RenderingHints;
+import java.awt.event.*;
+import java.awt.font.*;
+import java.awt.geom.*;
+
+import java.nio.*;
+
+import java.text.*;
+
+import java.util.*;
+
+import javax.media.nwi.*;
+import javax.media.opengl.*;
+import javax.media.opengl.glu.*;
+import javax.media.opengl.glu.gl2.*;
+import javax.media.opengl.awt.*;
+import javax.media.opengl.util.*;
+
+
+/** Renders bitmapped Java 2D text into an OpenGL window with high
+ performance, full Unicode support, and a simple API. Performs
+ appropriate caching of text rendering results in an OpenGL texture
+ internally to avoid repeated font rasterization. The caching is
+ completely automatic, does not require any user intervention, and
+ has no visible controls in the public API. <P>
+
+ Using the {@link TextRenderer TextRenderer} is simple. Add a
+ "<code>TextRenderer renderer;</code>" field to your {@link
+ javax.media.opengl.GLEventListener GLEventListener}. In your {@link
+ javax.media.opengl.GLEventListener#init init} method, add:
+
+ <PRE>
+ renderer = new TextRenderer(new Font("SansSerif", Font.BOLD, 36));
+ </PRE>
+
+ <P> In the {@link javax.media.opengl.GLEventListener#display display} method of your
+ {@link javax.media.opengl.GLEventListener GLEventListener}, add:
+ <PRE>
+ renderer.beginRendering(drawable.getWidth(), drawable.getHeight());
+ // optionally set the color
+ renderer.setColor(1.0f, 0.2f, 0.2f, 0.8f);
+ renderer.draw("Text to draw", xPosition, yPosition);
+ // ... more draw commands, color changes, etc.
+ renderer.endRendering();
+ </PRE>
+
+ Unless you are sharing textures and display lists between OpenGL
+ contexts, you do not need to call the {@link #dispose dispose}
+ method of the TextRenderer; the OpenGL resources it uses
+ internally will be cleaned up automatically when the OpenGL
+ context is destroyed. <P>
+
+ <b>Note</b> that the TextRenderer may cause the vertex and texture
+ coordinate array buffer bindings to change, or to be unbound. This
+ is important to note if you are using Vertex Buffer Objects (VBOs)
+ in your application. <P>
+
+ Internally, the renderer uses a rectangle packing algorithm to
+ pack both glyphs and full Strings' rendering results (which are
+ variable size) onto a larger OpenGL texture. The internal backing
+ store is maintained using a {@link
+ com.sun.opengl.util.awt.TextureRenderer TextureRenderer}. A least
+ recently used (LRU) algorithm is used to discard previously
+ rendered strings; the specific algorithm is undefined, but is
+ currently implemented by flushing unused Strings' rendering
+ results every few hundred rendering cycles, where a rendering
+ cycle is defined as a pair of calls to {@link #beginRendering
+ beginRendering} / {@link #endRendering endRendering}.
+
+ @author John Burkey
+ @author Kenneth Russell
+*/
+public class TextRenderer {
+ private static final boolean DEBUG = Debug.debug("TextRenderer");
+
+ // These are occasionally useful for more in-depth debugging
+ private static final boolean DISABLE_GLYPH_CACHE = false;
+ private static final boolean DRAW_BBOXES = false;
+
+ static final int kSize = 256;
+
+ // Every certain number of render cycles, flush the strings which
+ // haven't been used recently
+ private static final int CYCLES_PER_FLUSH = 100;
+
+ // The amount of vertical dead space on the backing store before we
+ // force a compaction
+ private static final float MAX_VERTICAL_FRAGMENTATION = 0.7f;
+ static final int kQuadsPerBuffer = 100;
+ static final int kCoordsPerVertVerts = 3;
+ static final int kCoordsPerVertTex = 2;
+ static final int kVertsPerQuad = 4;
+ static final int kTotalBufferSizeVerts = kQuadsPerBuffer * kVertsPerQuad;
+ static final int kTotalBufferSizeCoordsVerts = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertVerts;
+ static final int kTotalBufferSizeCoordsTex = kQuadsPerBuffer * kVertsPerQuad * kCoordsPerVertTex;
+ static final int kTotalBufferSizeBytesVerts = kTotalBufferSizeCoordsVerts * 4;
+ static final int kTotalBufferSizeBytesTex = kTotalBufferSizeCoordsTex * 4;
+ static final int kSizeInBytes_OneVertices_VertexData = kCoordsPerVertVerts * 4;
+ static final int kSizeInBytes_OneVertices_TexData = kCoordsPerVertTex * 4;
+ private Font font;
+ private boolean antialiased;
+ private boolean useFractionalMetrics;
+
+ // Whether we're attempting to use automatic mipmap generation support
+ private boolean mipmap;
+ private RectanglePacker packer;
+ private boolean haveMaxSize;
+ private RenderDelegate renderDelegate;
+ private TextureRenderer cachedBackingStore;
+ private Graphics2D cachedGraphics;
+ private FontRenderContext cachedFontRenderContext;
+ private Map /*<String,Rect>*/ stringLocations = new HashMap /*<String,Rect>*/();
+ private GlyphProducer mGlyphProducer;
+
+ private int numRenderCycles;
+
+ // Need to keep track of whether we're in a beginRendering() /
+ // endRendering() cycle so we can re-enter the exact same state if
+ // we have to reallocate the backing store
+ private boolean inBeginEndPair;
+ private boolean isOrthoMode;
+ private int beginRenderingWidth;
+ private int beginRenderingHeight;
+ private boolean beginRenderingDepthTestDisabled;
+
+ // For resetting the color after disposal of the old backing store
+ private boolean haveCachedColor;
+ private float cachedR;
+ private float cachedG;
+ private float cachedB;
+ private float cachedA;
+ private Color cachedColor;
+ private boolean needToResetColor;
+
+ // For debugging only
+ private Frame dbgFrame;
+
+ // Debugging purposes only
+ private boolean debugged;
+ Pipelined_QuadRenderer mPipelinedQuadRenderer;
+
+ //emzic: added boolean flag
+ private boolean useVertexArrays = true;
+
+ //emzic: added boolean flag
+ private boolean isExtensionAvailable_GL_VERSION_1_5;
+ private boolean checkFor_isExtensionAvailable_GL_VERSION_1_5;
+
+ // Whether GL_LINEAR filtering is enabled for the backing store
+ private boolean smoothing = true;
+
+ /** Creates a new TextRenderer with the given font, using no
+ antialiasing or fractional metrics, and the default
+ RenderDelegate. Equivalent to <code>TextRenderer(font, false,
+ false)</code>.
+
+ @param font the font to render with
+ */
+ public TextRenderer(Font font) {
+ this(font, false, false, null, false);
+ }
+
+ /** Creates a new TextRenderer with the given font, using no
+ antialiasing or fractional metrics, and the default
+ RenderDelegate. If <CODE>mipmap</CODE> is true, attempts to use
+ OpenGL's automatic mipmap generation for better smoothing when
+ rendering the TextureRenderer's contents at a distance.
+ Equivalent to <code>TextRenderer(font, false, false)</code>.
+
+ @param font the font to render with
+ @param mipmap whether to attempt use of automatic mipmap generation
+ */
+ public TextRenderer(Font font, boolean mipmap) {
+ this(font, false, false, null, mipmap);
+ }
+
+ /** Creates a new TextRenderer with the given Font, specified font
+ properties, and default RenderDelegate. The
+ <code>antialiased</code> and <code>useFractionalMetrics</code>
+ flags provide control over the same properties at the Java 2D
+ level. No mipmap support is requested. Equivalent to
+ <code>TextRenderer(font, antialiased, useFractionalMetrics,
+ null)</code>.
+
+ @param font the font to render with
+ @param antialiased whether to use antialiased fonts
+ @param useFractionalMetrics whether to use fractional font
+ metrics at the Java 2D level
+ */
+ public TextRenderer(Font font, boolean antialiased,
+ boolean useFractionalMetrics) {
+ this(font, antialiased, useFractionalMetrics, null, false);
+ }
+
+ /** Creates a new TextRenderer with the given Font, specified font
+ properties, and given RenderDelegate. The
+ <code>antialiased</code> and <code>useFractionalMetrics</code>
+ flags provide control over the same properties at the Java 2D
+ level. The <code>renderDelegate</code> provides more control
+ over the text rendered. No mipmap support is requested.
+
+ @param font the font to render with
+ @param antialiased whether to use antialiased fonts
+ @param useFractionalMetrics whether to use fractional font
+ metrics at the Java 2D level
+ @param renderDelegate the render delegate to use to draw the
+ text's bitmap, or null to use the default one
+ */
+ public TextRenderer(Font font, boolean antialiased,
+ boolean useFractionalMetrics, RenderDelegate renderDelegate) {
+ this(font, antialiased, useFractionalMetrics, renderDelegate, false);
+ }
+
+ /** Creates a new TextRenderer with the given Font, specified font
+ properties, and given RenderDelegate. The
+ <code>antialiased</code> and <code>useFractionalMetrics</code>
+ flags provide control over the same properties at the Java 2D
+ level. The <code>renderDelegate</code> provides more control
+ over the text rendered. If <CODE>mipmap</CODE> is true, attempts
+ to use OpenGL's automatic mipmap generation for better smoothing
+ when rendering the TextureRenderer's contents at a distance.
+
+ @param font the font to render with
+ @param antialiased whether to use antialiased fonts
+ @param useFractionalMetrics whether to use fractional font
+ metrics at the Java 2D level
+ @param renderDelegate the render delegate to use to draw the
+ text's bitmap, or null to use the default one
+ @param mipmap whether to attempt use of automatic mipmap generation
+ */
+ public TextRenderer(Font font, boolean antialiased,
+ boolean useFractionalMetrics, RenderDelegate renderDelegate,
+ boolean mipmap) {
+ this.font = font;
+ this.antialiased = antialiased;
+ this.useFractionalMetrics = useFractionalMetrics;
+ this.mipmap = mipmap;
+
+ // FIXME: consider adjusting the size based on font size
+ // (it will already automatically resize if necessary)
+ packer = new RectanglePacker(new Manager(), kSize, kSize);
+
+ if (renderDelegate == null) {
+ renderDelegate = new DefaultRenderDelegate();
+ }
+
+ this.renderDelegate = renderDelegate;
+
+ mGlyphProducer = new GlyphProducer(font.getNumGlyphs());
+ }
+
+ /** Returns the bounding rectangle of the given String, assuming it
+ was rendered at the origin. See {@link #getBounds(CharSequence)
+ getBounds(CharSequence)}. */
+ public Rectangle2D getBounds(String str) {
+ return getBounds((CharSequence) str);
+ }
+
+ /** Returns the bounding rectangle of the given CharSequence,
+ assuming it was rendered at the origin. The coordinate system of
+ the returned rectangle is Java 2D's, with increasing Y
+ coordinates in the downward direction. The relative coordinate
+ (0, 0) in the returned rectangle corresponds to the baseline of
+ the leftmost character of the rendered string, in similar
+ fashion to the results returned by, for example, {@link
+ java.awt.font.GlyphVector#getVisualBounds}. Most applications
+ will use only the width and height of the returned Rectangle for
+ the purposes of centering or justifying the String. It is not
+ specified which Java 2D bounds ({@link
+ java.awt.font.GlyphVector#getVisualBounds getVisualBounds},
+ {@link java.awt.font.GlyphVector#getPixelBounds getPixelBounds},
+ etc.) the returned bounds correspond to, although every effort
+ is made to ensure an accurate bound. */
+ public Rectangle2D getBounds(CharSequence str) {
+ // FIXME: this should be more optimized and use the glyph cache
+ Rect r = null;
+
+ if ((r = (Rect) stringLocations.get(str)) != null) {
+ TextData data = (TextData) r.getUserData();
+
+ // Reconstitute the Java 2D results based on the cached values
+ return new Rectangle2D.Double(-data.origin().x, -data.origin().y,
+ r.w(), r.h());
+ }
+
+ // Must return a Rectangle compatible with the layout algorithm --
+ // must be idempotent
+ return normalize(renderDelegate.getBounds(str, font,
+ getFontRenderContext()));
+ }
+
+ /** Returns the Font this renderer is using. */
+ public Font getFont() {
+ return font;
+ }
+
+ /** Returns a FontRenderContext which can be used for external
+ text-related size computations. This object should be considered
+ transient and may become invalidated between {@link
+ #beginRendering beginRendering} / {@link #endRendering
+ endRendering} pairs. */
+ public FontRenderContext getFontRenderContext() {
+ if (cachedFontRenderContext == null) {
+ cachedFontRenderContext = getGraphics2D().getFontRenderContext();
+ }
+
+ return cachedFontRenderContext;
+ }
+
+ /** Begins rendering with this {@link TextRenderer TextRenderer}
+ into the current OpenGL drawable, pushing the projection and
+ modelview matrices and some state bits and setting up a
+ two-dimensional orthographic projection with (0, 0) as the
+ lower-left coordinate and (width, height) as the upper-right
+ coordinate. Binds and enables the internal OpenGL texture
+ object, sets the texture environment mode to GL_MODULATE, and
+ changes the current color to the last color set with this
+ TextRenderer via {@link #setColor setColor}. This method
+ disables the depth test and is equivalent to
+ beginRendering(width, height, true).
+
+ @param width the width of the current on-screen OpenGL drawable
+ @param height the height of the current on-screen OpenGL drawable
+ @throws javax.media.opengl.GLException If an OpenGL context is not current when this method is called
+ */
+ public void beginRendering(int width, int height) throws GLException {
+ beginRendering(width, height, true);
+ }
+
+ /** Begins rendering with this {@link TextRenderer TextRenderer}
+ into the current OpenGL drawable, pushing the projection and
+ modelview matrices and some state bits and setting up a
+ two-dimensional orthographic projection with (0, 0) as the
+ lower-left coordinate and (width, height) as the upper-right
+ coordinate. Binds and enables the internal OpenGL texture
+ object, sets the texture environment mode to GL_MODULATE, and
+ changes the current color to the last color set with this
+ TextRenderer via {@link #setColor setColor}. Disables the depth
+ test if the disableDepthTest argument is true.
+
+ @param width the width of the current on-screen OpenGL drawable
+ @param height the height of the current on-screen OpenGL drawable
+ @param disableDepthTest whether to disable the depth test
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void beginRendering(int width, int height, boolean disableDepthTest)
+ throws GLException {
+ beginRendering(true, width, height, disableDepthTest);
+ }
+
+ /** Begins rendering of 2D text in 3D with this {@link TextRenderer
+ TextRenderer} into the current OpenGL drawable. Assumes the end
+ user is responsible for setting up the modelview and projection
+ matrices, and will render text using the {@link #draw3D draw3D}
+ method. This method pushes some OpenGL state bits, binds and
+ enables the internal OpenGL texture object, sets the texture
+ environment mode to GL_MODULATE, and changes the current color
+ to the last color set with this TextRenderer via {@link
+ #setColor setColor}.
+
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void begin3DRendering() throws GLException {
+ beginRendering(false, 0, 0, false);
+ }
+
+ /** Changes the current color of this TextRenderer to the supplied
+ one. The default color is opaque white.
+
+ @param color the new color to use for rendering text
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void setColor(Color color) throws GLException {
+ boolean noNeedForFlush = (haveCachedColor && (cachedColor != null) &&
+ color.equals(cachedColor));
+
+ if (!noNeedForFlush) {
+ flushGlyphPipeline();
+ }
+
+ getBackingStore().setColor(color);
+ haveCachedColor = true;
+ cachedColor = color;
+ }
+
+ /** Changes the current color of this TextRenderer to the supplied
+ one, where each component ranges from 0.0f - 1.0f. The alpha
+ component, if used, does not need to be premultiplied into the
+ color channels as described in the documentation for {@link
+ com.sun.opengl.util.texture.Texture Texture}, although
+ premultiplied colors are used internally. The default color is
+ opaque white.
+
+ @param r the red component of the new color
+ @param g the green component of the new color
+ @param b the blue component of the new color
+ @param a the alpha component of the new color, 0.0f = completely
+ transparent, 1.0f = completely opaque
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void setColor(float r, float g, float b, float a)
+ throws GLException {
+ boolean noNeedForFlush = (haveCachedColor && (cachedColor == null) &&
+ (r == cachedR) && (g == cachedG) && (b == cachedB) &&
+ (a == cachedA));
+
+ if (!noNeedForFlush) {
+ flushGlyphPipeline();
+ }
+
+ getBackingStore().setColor(r, g, b, a);
+ haveCachedColor = true;
+ cachedR = r;
+ cachedG = g;
+ cachedB = b;
+ cachedA = a;
+ cachedColor = null;
+ }
+
+ /** Draws the supplied CharSequence at the desired location using
+ the renderer's current color. The baseline of the leftmost
+ character is at position (x, y) specified in OpenGL coordinates,
+ where the origin is at the lower-left of the drawable and the Y
+ coordinate increases in the upward direction.
+
+ @param str the string to draw
+ @param x the x coordinate at which to draw
+ @param y the y coordinate at which to draw
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void draw(CharSequence str, int x, int y) throws GLException {
+ draw3D(str, x, y, 0, 1);
+ }
+
+ /** Draws the supplied String at the desired location using the
+ renderer's current color. See {@link #draw(CharSequence, int,
+ int) draw(CharSequence, int, int)}. */
+ public void draw(String str, int x, int y) throws GLException {
+ draw3D(str, x, y, 0, 1);
+ }
+
+ /** Draws the supplied CharSequence at the desired 3D location using
+ the renderer's current color. The baseline of the leftmost
+ character is placed at position (x, y, z) in the current
+ coordinate system.
+
+ @param str the string to draw
+ @param x the x coordinate at which to draw
+ @param y the y coordinate at which to draw
+ @param z the z coordinate at which to draw
+ @param scaleFactor a uniform scale factor applied to the width and height of the drawn rectangle
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void draw3D(CharSequence str, float x, float y, float z,
+ float scaleFactor) {
+ internal_draw3D(str, x, y, z, scaleFactor);
+ }
+
+ /** Draws the supplied String at the desired 3D location using the
+ renderer's current color. See {@link #draw3D(CharSequence,
+ float, float, float, float) draw3D(CharSequence, float, float,
+ float, float)}. */
+ public void draw3D(String str, float x, float y, float z, float scaleFactor) {
+ internal_draw3D(str, x, y, z, scaleFactor);
+ }
+
+ /** Returns the pixel width of the given character. */
+ public float getCharWidth(char inChar) {
+ return mGlyphProducer.getGlyphPixelWidth(inChar);
+ }
+
+ /** Causes the TextRenderer to flush any internal caches it may be
+ maintaining and draw its rendering results to the screen. This
+ should be called after each call to draw() if you are setting
+ OpenGL state such as the modelview matrix between calls to
+ draw(). */
+ public void flush() {
+ flushGlyphPipeline();
+ }
+
+ /** Ends a render cycle with this {@link TextRenderer TextRenderer}.
+ Restores the projection and modelview matrices as well as
+ several OpenGL state bits. Should be paired with {@link
+ #beginRendering beginRendering}.
+
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void endRendering() throws GLException {
+ endRendering(true);
+ }
+
+ /** Ends a 3D render cycle with this {@link TextRenderer TextRenderer}.
+ Restores several OpenGL state bits. Should be paired with {@link
+ #begin3DRendering begin3DRendering}.
+
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void end3DRendering() throws GLException {
+ endRendering(false);
+ }
+
+ /** Disposes of all resources this TextRenderer is using. It is not
+ valid to use the TextRenderer after this method is called.
+
+ @throws GLException If an OpenGL context is not current when this method is called
+ */
+ public void dispose() throws GLException {
+ packer.dispose();
+ packer = null;
+ cachedBackingStore = null;
+ cachedGraphics = null;
+ cachedFontRenderContext = null;
+
+ if (dbgFrame != null) {
+ dbgFrame.dispose();
+ }
+ }
+
+ //----------------------------------------------------------------------
+ // Internals only below this point
+ //
+
+ private static Rectangle2D preNormalize(Rectangle2D src) {
+ // Need to round to integer coordinates
+ // Also give ourselves a little slop around the reported
+ // bounds of glyphs because it looks like neither the visual
+ // nor the pixel bounds works perfectly well
+ int minX = (int) Math.floor(src.getMinX()) - 1;
+ int minY = (int) Math.floor(src.getMinY()) - 1;
+ int maxX = (int) Math.ceil(src.getMaxX()) + 1;
+ int maxY = (int) Math.ceil(src.getMaxY()) + 1;
+ return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
+ }
+
+
+ private Rectangle2D normalize(Rectangle2D src) {
+ // Give ourselves a boundary around each entity on the backing
+ // store in order to prevent bleeding of nearby Strings due to
+ // the fact that we use linear filtering
+
+ // NOTE that this boundary is quite heuristic and is related
+ // to how far away in 3D we may view the text --
+ // heuristically, 1.5% of the font's height
+ int boundary = (int) Math.max(1, 0.015 * font.getSize());
+
+ return new Rectangle2D.Double((int) Math.floor(src.getMinX() - boundary),
+ (int) Math.floor(src.getMinY() - boundary),
+ (int) Math.ceil(src.getWidth() + 2 * boundary),
+ (int) Math.ceil(src.getHeight()) + 2 * boundary);
+ }
+
+ private TextureRenderer getBackingStore() {
+ TextureRenderer renderer = (TextureRenderer) packer.getBackingStore();
+
+ if (renderer != cachedBackingStore) {
+ // Backing store changed since last time; discard any cached Graphics2D
+ if (cachedGraphics != null) {
+ cachedGraphics.dispose();
+ cachedGraphics = null;
+ cachedFontRenderContext = null;
+ }
+
+ cachedBackingStore = renderer;
+ }
+
+ return cachedBackingStore;
+ }
+
+ private Graphics2D getGraphics2D() {
+ TextureRenderer renderer = getBackingStore();
+
+ if (cachedGraphics == null) {
+ cachedGraphics = renderer.createGraphics();
+
+ // Set up composite, font and rendering hints
+ cachedGraphics.setComposite(AlphaComposite.Src);
+ cachedGraphics.setColor(Color.WHITE);
+ cachedGraphics.setFont(font);
+ cachedGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+ (antialiased ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON
+ : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF));
+ cachedGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
+ (useFractionalMetrics
+ ? RenderingHints.VALUE_FRACTIONALMETRICS_ON
+ : RenderingHints.VALUE_FRACTIONALMETRICS_OFF));
+ }
+
+ return cachedGraphics;
+ }
+
+ private void beginRendering(boolean ortho, int width, int height,
+ boolean disableDepthTestForOrtho) {
+ if (DEBUG && !debugged) {
+ debug();
+ }
+
+ inBeginEndPair = true;
+ isOrthoMode = ortho;
+ beginRenderingWidth = width;
+ beginRenderingHeight = height;
+ beginRenderingDepthTestDisabled = disableDepthTestForOrtho;
+
+ if (ortho) {
+ getBackingStore().beginOrthoRendering(width, height,
+ disableDepthTestForOrtho);
+ } else {
+ getBackingStore().begin3DRendering();
+ }
+
+ GL2 gl = GLUgl2.getCurrentGL2();
+
+ // Push client attrib bits used by the pipelined quad renderer
+ gl.glPushClientAttrib((int) GL2.GL_ALL_CLIENT_ATTRIB_BITS);
+
+ if (!haveMaxSize) {
+ // Query OpenGL for the maximum texture size and set it in the
+ // RectanglePacker to keep it from expanding too large
+ int[] sz = new int[1];
+ gl.glGetIntegerv(GL2.GL_MAX_TEXTURE_SIZE, sz, 0);
+ packer.setMaxSize(sz[0], sz[0]);
+ haveMaxSize = true;
+ }
+
+ if (needToResetColor && haveCachedColor) {
+ if (cachedColor == null) {
+ getBackingStore().setColor(cachedR, cachedG, cachedB, cachedA);
+ } else {
+ getBackingStore().setColor(cachedColor);
+ }
+
+ needToResetColor = false;
+ }
+
+ // Disable future attempts to use mipmapping if TextureRenderer
+ // doesn't support it
+ if (mipmap && !getBackingStore().isUsingAutoMipmapGeneration()) {
+ if (DEBUG) {
+ System.err.println("Disabled mipmapping in TextRenderer");
+ }
+
+ mipmap = false;
+ }
+ }
+
+ /**
+ * emzic: here the call to glBindBuffer crashes on certain graphicscard/driver combinations
+ * this is why the ugly try-catch block has been added, which falls back to the old textrenderer
+ *
+ * @param ortho
+ * @throws GLException
+ */
+ private void endRendering(boolean ortho) throws GLException {
+ flushGlyphPipeline();
+
+ inBeginEndPair = false;
+
+ GL2 gl = GLUgl2.getCurrentGL2();
+
+ // Pop client attrib bits used by the pipelined quad renderer
+ gl.glPopClientAttrib();
+
+ // The OpenGL spec is unclear about whether this changes the
+ // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER
+ // binding
+ if (is15Available(gl)) {
+ try {
+ gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
+ } catch (Exception e) {
+ isExtensionAvailable_GL_VERSION_1_5 = false;
+ }
+ }
+
+ if (ortho) {
+ getBackingStore().endOrthoRendering();
+ } else {
+ getBackingStore().end3DRendering();
+ }
+
+ if (++numRenderCycles >= CYCLES_PER_FLUSH) {
+ numRenderCycles = 0;
+
+ if (DEBUG) {
+ System.err.println("Clearing unused entries in endRendering()");
+ }
+
+ clearUnusedEntries();
+ }
+ }
+
+ private void clearUnusedEntries() {
+ final java.util.List deadRects = new ArrayList /*<Rect>*/();
+
+ // Iterate through the contents of the backing store, removing
+ // text strings that haven't been used recently
+ packer.visit(new RectVisitor() {
+ public void visit(Rect rect) {
+ TextData data = (TextData) rect.getUserData();
+
+ if (data.used()) {
+ data.clearUsed();
+ } else {
+ deadRects.add(rect);
+ }
+ }
+ });
+
+ for (Iterator iter = deadRects.iterator(); iter.hasNext();) {
+ Rect r = (Rect) iter.next();
+ packer.remove(r);
+ stringLocations.remove(((TextData) r.getUserData()).string());
+
+ int unicodeToClearFromCache = ((TextData) r.getUserData()).unicodeID;
+
+ if (unicodeToClearFromCache > 0) {
+ mGlyphProducer.clearCacheEntry(unicodeToClearFromCache);
+ }
+
+ // if (DEBUG) {
+ // Graphics2D g = getGraphics2D();
+ // g.setComposite(AlphaComposite.Clear);
+ // g.fillRect(r.x(), r.y(), r.w(), r.h());
+ // g.setComposite(AlphaComposite.Src);
+ // }
+ }
+
+ // If we removed dead rectangles this cycle, try to do a compaction
+ float frag = packer.verticalFragmentationRatio();
+
+ if (!deadRects.isEmpty() && (frag > MAX_VERTICAL_FRAGMENTATION)) {
+ if (DEBUG) {
+ System.err.println(
+ "Compacting TextRenderer backing store due to vertical fragmentation " +
+ frag);
+ }
+
+ packer.compact();
+ }
+
+ if (DEBUG) {
+ getBackingStore().markDirty(0, 0, getBackingStore().getWidth(),
+ getBackingStore().getHeight());
+ }
+ }
+
+ private void internal_draw3D(CharSequence str, float x, float y, float z,
+ float scaleFactor) {
+ List/*<Glyph>*/ glyphs = mGlyphProducer.getGlyphs(str);
+ for (Iterator iter = glyphs.iterator(); iter.hasNext(); ) {
+ Glyph glyph = (Glyph) iter.next();
+ float advance = glyph.draw3D(x, y, z, scaleFactor);
+ x += advance * scaleFactor;
+ }
+ }
+
+ private void flushGlyphPipeline() {
+ if (mPipelinedQuadRenderer != null) {
+ mPipelinedQuadRenderer.draw();
+ }
+ }
+
+ private void draw3D_ROBUST(CharSequence str, float x, float y, float z,
+ float scaleFactor) {
+ String curStr;
+ if (str instanceof String) {
+ curStr = (String) str;
+ } else {
+ curStr = str.toString();
+ }
+
+ // Look up the string on the backing store
+ Rect rect = (Rect) stringLocations.get(curStr);
+
+ if (rect == null) {
+ // Rasterize this string and place it on the backing store
+ Graphics2D g = getGraphics2D();
+ Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(curStr, font, getFontRenderContext()));
+ Rectangle2D bbox = normalize(origBBox);
+ Point origin = new Point((int) -bbox.getMinX(),
+ (int) -bbox.getMinY());
+ rect = new Rect(0, 0, (int) bbox.getWidth(),
+ (int) bbox.getHeight(),
+ new TextData(curStr, origin, origBBox, -1));
+
+ packer.add(rect);
+ stringLocations.put(curStr, rect);
+
+ // Re-fetch the Graphics2D in case the addition of the rectangle
+ // caused the old backing store to be thrown away
+ g = getGraphics2D();
+
+ // OK, should now have an (x, y) for this rectangle; rasterize
+ // the String
+ int strx = rect.x() + origin.x;
+ int stry = rect.y() + origin.y;
+
+ // Clear out the area we're going to draw into
+ g.setComposite(AlphaComposite.Clear);
+ g.fillRect(rect.x(), rect.y(), rect.w(), rect.h());
+ g.setComposite(AlphaComposite.Src);
+
+ // Draw the string
+ renderDelegate.draw(g, curStr, strx, stry);
+
+ if (DRAW_BBOXES) {
+ TextData data = (TextData) rect.getUserData();
+ // Draw a bounding box on the backing store
+ g.drawRect(strx - data.origOriginX(),
+ stry - data.origOriginY(),
+ (int) data.origRect().getWidth(),
+ (int) data.origRect().getHeight());
+ g.drawRect(strx - data.origin().x,
+ stry - data.origin().y,
+ rect.w(),
+ rect.h());
+ }
+
+ // Mark this region of the TextureRenderer as dirty
+ getBackingStore().markDirty(rect.x(), rect.y(), rect.w(),
+ rect.h());
+ }
+
+ // OK, now draw the portion of the backing store to the screen
+ TextureRenderer renderer = getBackingStore();
+
+ // NOTE that the rectangles managed by the packer have their
+ // origin at the upper-left but the TextureRenderer's origin is
+ // at its lower left!!!
+ TextData data = (TextData) rect.getUserData();
+ data.markUsed();
+
+ Rectangle2D origRect = data.origRect();
+
+ // Align the leftmost point of the baseline to the (x, y, z) coordinate requested
+ renderer.draw3DRect(x - (scaleFactor * data.origOriginX()),
+ y - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY())), z,
+ rect.x() + (data.origin().x - data.origOriginX()),
+ renderer.getHeight() - rect.y() - (int) origRect.getHeight() -
+ (data.origin().y - data.origOriginY()),
+ (int) origRect.getWidth(), (int) origRect.getHeight(), scaleFactor);
+ }
+
+ //----------------------------------------------------------------------
+ // Debugging functionality
+ //
+ private void debug() {
+ dbgFrame = new Frame("TextRenderer Debug Output");
+
+ GLCanvas dbgCanvas = new GLCanvas(new NWCapabilities(), null,
+ GLContext.getCurrent(), null);
+ dbgCanvas.addGLEventListener(new DebugListener(dbgFrame));
+ dbgFrame.add(dbgCanvas);
+
+ final FPSAnimator anim = new FPSAnimator(dbgCanvas, 10);
+ dbgFrame.addWindowListener(new WindowAdapter() {
+ public void windowClosing(WindowEvent e) {
+ // Run this on another thread than the AWT event queue to
+ // make sure the call to Animator.stop() completes before
+ // exiting
+ new Thread(new Runnable() {
+ public void run() {
+ anim.stop();
+ }
+ }).start();
+ }
+ });
+ dbgFrame.setSize(kSize, kSize);
+ dbgFrame.setVisible(true);
+ anim.start();
+ debugged = true;
+ }
+
+ /** Class supporting more full control over the process of rendering
+ the bitmapped text. Allows customization of whether the backing
+ store text bitmap is full-color or intensity only, the size of
+ each individual rendered text rectangle, and the contents of
+ each individual rendered text string. The default implementation
+ of this interface uses an intensity-only texture, a
+ closely-cropped rectangle around the text, and renders text
+ using the color white, which is modulated by the set color
+ during the rendering process. */
+ public static interface RenderDelegate {
+ /** Indicates whether the backing store of this TextRenderer
+ should be intensity-only (the default) or full-color. */
+ public boolean intensityOnly();
+
+ /** Computes the bounds of the given String relative to the
+ origin. */
+ public Rectangle2D getBounds(String str, Font font,
+ FontRenderContext frc);
+
+ /** Computes the bounds of the given character sequence relative
+ to the origin. */
+ public Rectangle2D getBounds(CharSequence str, Font font,
+ FontRenderContext frc);
+
+ /** Computes the bounds of the given GlyphVector, already
+ assumed to have been created for a particular Font,
+ relative to the origin. */
+ public Rectangle2D getBounds(GlyphVector gv, FontRenderContext frc);
+
+ /** Render the passed character sequence at the designated
+ location using the supplied Graphics2D instance. The
+ surrounding region will already have been cleared to the RGB
+ color (0, 0, 0) with zero alpha. The initial drawing context
+ of the passed Graphics2D will be set to use
+ AlphaComposite.Src, the color white, the Font specified in the
+ TextRenderer's constructor, and the rendering hints specified
+ in the TextRenderer constructor. Changes made by the end user
+ may be visible in successive calls to this method, but are not
+ guaranteed to be preserved. Implementors of this method
+ should reset the Graphics2D's state to that desired each time
+ this method is called, in particular those states which are
+ not the defaults. */
+ public void draw(Graphics2D graphics, String str, int x, int y);
+
+ /** Render the passed GlyphVector at the designated location using
+ the supplied Graphics2D instance. The surrounding region will
+ already have been cleared to the RGB color (0, 0, 0) with zero
+ alpha. The initial drawing context of the passed Graphics2D
+ will be set to use AlphaComposite.Src, the color white, the
+ Font specified in the TextRenderer's constructor, and the
+ rendering hints specified in the TextRenderer constructor.
+ Changes made by the end user may be visible in successive
+ calls to this method, but are not guaranteed to be preserved.
+ Implementors of this method should reset the Graphics2D's
+ state to that desired each time this method is called, in
+ particular those states which are not the defaults. */
+ public void drawGlyphVector(Graphics2D graphics, GlyphVector str,
+ int x, int y);
+ }
+
+ private static class CharSequenceIterator implements CharacterIterator {
+ CharSequence mSequence;
+ int mLength;
+ int mCurrentIndex;
+
+ CharSequenceIterator() {
+ }
+
+ CharSequenceIterator(CharSequence sequence) {
+ initFromCharSequence(sequence);
+ }
+
+ public void initFromCharSequence(CharSequence sequence) {
+ mSequence = sequence;
+ mLength = mSequence.length();
+ mCurrentIndex = 0;
+ }
+
+ public char last() {
+ mCurrentIndex = Math.max(0, mLength - 1);
+
+ return current();
+ }
+
+ public char current() {
+ if ((mLength == 0) || (mCurrentIndex >= mLength)) {
+ return CharacterIterator.DONE;
+ }
+
+ return mSequence.charAt(mCurrentIndex);
+ }
+
+ public char next() {
+ mCurrentIndex++;
+
+ return current();
+ }
+
+ public char previous() {
+ mCurrentIndex = Math.max(mCurrentIndex - 1, 0);
+
+ return current();
+ }
+
+ public char setIndex(int position) {
+ mCurrentIndex = position;
+
+ return current();
+ }
+
+ public int getBeginIndex() {
+ return 0;
+ }
+
+ public int getEndIndex() {
+ return mLength;
+ }
+
+ public int getIndex() {
+ return mCurrentIndex;
+ }
+
+ public Object clone() {
+ CharSequenceIterator iter = new CharSequenceIterator(mSequence);
+ iter.mCurrentIndex = mCurrentIndex;
+
+ return iter;
+ }
+
+ public char first() {
+ if (mLength == 0) {
+ return CharacterIterator.DONE;
+ }
+
+ mCurrentIndex = 0;
+
+ return current();
+ }
+ }
+
+ // Data associated with each rectangle of text
+ static class TextData {
+ // Back-pointer to String this TextData describes, if it
+ // represents a String rather than a single glyph
+ private String str;
+
+ // If this TextData represents a single glyph, this is its
+ // unicode ID
+ int unicodeID;
+
+ // The following must be defined and used VERY precisely. This is
+ // the offset from the upper-left corner of this rectangle (Java
+ // 2D coordinate system) at which the string must be rasterized in
+ // order to fit within the rectangle -- the leftmost point of the
+ // baseline.
+ private Point origin;
+
+ // This represents the pre-normalized rectangle, which fits
+ // within the rectangle on the backing store. We keep a
+ // one-pixel border around entries on the backing store to
+ // prevent bleeding of adjacent letters when using GL_LINEAR
+ // filtering for rendering. The origin of this rectangle is
+ // equivalent to the origin above.
+ private Rectangle2D origRect;
+
+ private boolean used; // Whether this text was used recently
+
+ TextData(String str, Point origin, Rectangle2D origRect, int unicodeID) {
+ this.str = str;
+ this.origin = origin;
+ this.origRect = origRect;
+ this.unicodeID = unicodeID;
+ }
+
+ String string() {
+ return str;
+ }
+
+ Point origin() {
+ return origin;
+ }
+
+ // The following three methods are used to locate the glyph
+ // within the expanded rectangle coming from normalize()
+ int origOriginX() {
+ return (int) -origRect.getMinX();
+ }
+
+ int origOriginY() {
+ return (int) -origRect.getMinY();
+ }
+
+ Rectangle2D origRect() {
+ return origRect;
+ }
+
+ boolean used() {
+ return used;
+ }
+
+ void markUsed() {
+ used = true;
+ }
+
+ void clearUsed() {
+ used = false;
+ }
+ }
+
+ class Manager implements BackingStoreManager {
+ private Graphics2D g;
+
+ public Object allocateBackingStore(int w, int h) {
+ // FIXME: should consider checking Font's attributes to see
+ // whether we're likely to need to support a full RGBA backing
+ // store (i.e., non-default Paint, foreground color, etc.), but
+ // for now, let's just be more efficient
+ TextureRenderer renderer;
+
+ if (renderDelegate.intensityOnly()) {
+ renderer = TextureRenderer.createAlphaOnlyRenderer(w, h, mipmap);
+ } else {
+ renderer = new TextureRenderer(w, h, true, mipmap);
+ }
+ renderer.setSmoothing(smoothing);
+
+ if (DEBUG) {
+ System.err.println(" TextRenderer allocating backing store " +
+ w + " x " + h);
+ }
+
+ return renderer;
+ }
+
+ public void deleteBackingStore(Object backingStore) {
+ ((TextureRenderer) backingStore).dispose();
+ }
+
+ public boolean preExpand(Rect cause, int attemptNumber) {
+ // Only try this one time; clear out potentially obsolete entries
+ // NOTE: this heuristic and the fact that it clears the used bit
+ // of all entries seems to cause cycling of entries in some
+ // situations, where the backing store becomes small compared to
+ // the amount of text on the screen (see the TextFlow demo) and
+ // the entries continually cycle in and out of the backing
+ // store, decreasing performance. If we added a little age
+ // information to the entries, and only cleared out entries
+ // above a certain age, this behavior would be eliminated.
+ // However, it seems the system usually stabilizes itself, so
+ // for now we'll just keep things simple. Note that if we don't
+ // clear the used bit here, the backing store tends to increase
+ // very quickly to its maximum size, at least with the TextFlow
+ // demo when the text is being continually re-laid out.
+ if (attemptNumber == 0) {
+ if (DEBUG) {
+ System.err.println(
+ "Clearing unused entries in preExpand(): attempt number " +
+ attemptNumber);
+ }
+
+ if (inBeginEndPair) {
+ // Draw any outstanding glyphs
+ flush();
+ }
+
+ clearUnusedEntries();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean additionFailed(Rect cause, int attemptNumber) {
+ // Heavy hammer -- might consider doing something different
+ packer.clear();
+ stringLocations.clear();
+ mGlyphProducer.clearAllCacheEntries();
+
+ if (DEBUG) {
+ System.err.println(
+ " *** Cleared all text because addition failed ***");
+ }
+
+ if (attemptNumber == 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean canCompact() {
+ return true;
+ }
+
+ public void beginMovement(Object oldBackingStore, Object newBackingStore) {
+ // Exit the begin / end pair if necessary
+ if (inBeginEndPair) {
+ // Draw any outstanding glyphs
+ flush();
+
+ GL2 gl = GLUgl2.getCurrentGL2();
+
+ // Pop client attrib bits used by the pipelined quad renderer
+ gl.glPopClientAttrib();
+
+ // The OpenGL spec is unclear about whether this changes the
+ // buffer bindings, so preemptively zero out the GL_ARRAY_BUFFER
+ // binding
+ if (is15Available(gl)) {
+ try {
+ gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
+ } catch (Exception e) {
+ isExtensionAvailable_GL_VERSION_1_5 = false;
+ }
+ }
+
+ if (isOrthoMode) {
+ ((TextureRenderer) oldBackingStore).endOrthoRendering();
+ } else {
+ ((TextureRenderer) oldBackingStore).end3DRendering();
+ }
+ }
+
+ TextureRenderer newRenderer = (TextureRenderer) newBackingStore;
+ g = newRenderer.createGraphics();
+ }
+
+ public void move(Object oldBackingStore, Rect oldLocation,
+ Object newBackingStore, Rect newLocation) {
+ TextureRenderer oldRenderer = (TextureRenderer) oldBackingStore;
+ TextureRenderer newRenderer = (TextureRenderer) newBackingStore;
+
+ if (oldRenderer == newRenderer) {
+ // Movement on the same backing store -- easy case
+ g.copyArea(oldLocation.x(), oldLocation.y(), oldLocation.w(),
+ oldLocation.h(), newLocation.x() - oldLocation.x(),
+ newLocation.y() - oldLocation.y());
+ } else {
+ // Need to draw from the old renderer's image into the new one
+ Image img = oldRenderer.getImage();
+ g.drawImage(img, newLocation.x(), newLocation.y(),
+ newLocation.x() + newLocation.w(),
+ newLocation.y() + newLocation.h(), oldLocation.x(),
+ oldLocation.y(), oldLocation.x() + oldLocation.w(),
+ oldLocation.y() + oldLocation.h(), null);
+ }
+ }
+
+ public void endMovement(Object oldBackingStore, Object newBackingStore) {
+ g.dispose();
+
+ // Sync the whole surface
+ TextureRenderer newRenderer = (TextureRenderer) newBackingStore;
+ newRenderer.markDirty(0, 0, newRenderer.getWidth(),
+ newRenderer.getHeight());
+
+ // Re-enter the begin / end pair if necessary
+ if (inBeginEndPair) {
+ if (isOrthoMode) {
+ ((TextureRenderer) newBackingStore).beginOrthoRendering(beginRenderingWidth,
+ beginRenderingHeight, beginRenderingDepthTestDisabled);
+ } else {
+ ((TextureRenderer) newBackingStore).begin3DRendering();
+ }
+
+ // Push client attrib bits used by the pipelined quad renderer
+ GL2 gl = GLUgl2.getCurrentGL2();
+ gl.glPushClientAttrib((int) GL2.GL_ALL_CLIENT_ATTRIB_BITS);
+
+ if (haveCachedColor) {
+ if (cachedColor == null) {
+ ((TextureRenderer) newBackingStore).setColor(cachedR,
+ cachedG, cachedB, cachedA);
+ } else {
+ ((TextureRenderer) newBackingStore).setColor(cachedColor);
+ }
+ }
+ } else {
+ needToResetColor = true;
+ }
+ }
+ }
+
+ public static class DefaultRenderDelegate implements RenderDelegate {
+ public boolean intensityOnly() {
+ return true;
+ }
+
+ public Rectangle2D getBounds(CharSequence str, Font font,
+ FontRenderContext frc) {
+ return getBounds(font.createGlyphVector(frc,
+ new CharSequenceIterator(str)),
+ frc);
+ }
+
+ public Rectangle2D getBounds(String str, Font font,
+ FontRenderContext frc) {
+ return getBounds(font.createGlyphVector(frc, str), frc);
+ }
+
+ public Rectangle2D getBounds(GlyphVector gv, FontRenderContext frc) {
+ return gv.getVisualBounds();
+ }
+
+ public void drawGlyphVector(Graphics2D graphics, GlyphVector str,
+ int x, int y) {
+ graphics.drawGlyphVector(str, x, y);
+ }
+
+ public void draw(Graphics2D graphics, String str, int x, int y) {
+ graphics.drawString(str, x, y);
+ }
+ }
+
+ //----------------------------------------------------------------------
+ // Glyph-by-glyph rendering support
+ //
+
+ // A temporary to prevent excessive garbage creation
+ private char[] singleUnicode = new char[1];
+
+ /** A Glyph represents either a single unicode glyph or a
+ substring of characters to be drawn. The reason for the dual
+ behavior is so that we can take in a sequence of unicode
+ characters and partition them into runs of individual glyphs,
+ but if we encounter complex text and/or unicode sequences we
+ don't understand, we can render them using the
+ string-by-string method. <P>
+
+ Glyphs need to be able to re-upload themselves to the backing
+ store on demand as we go along in the render sequence.
+ */
+
+ class Glyph {
+ // If this Glyph represents an individual unicode glyph, this
+ // is its unicode ID. If it represents a String, this is -1.
+ private int unicodeID;
+ // If the above field isn't -1, then these fields are used.
+ // The glyph code in the font
+ private int glyphCode;
+ // The GlyphProducer which created us
+ private GlyphProducer producer;
+ // The advance of this glyph
+ private float advance;
+ // The GlyphVector for this single character; this is passed
+ // in during construction but cleared during the upload
+ // process
+ private GlyphVector singleUnicodeGlyphVector;
+ // The rectangle of this glyph on the backing store, or null
+ // if it has been cleared due to space pressure
+ private Rect glyphRectForTextureMapping;
+ // If this Glyph represents a String, this is the sequence of
+ // characters
+ private String str;
+ // Whether we need a valid advance when rendering this string
+ // (i.e., whether it has other single glyphs coming after it)
+ private boolean needAdvance;
+
+ // Creates a Glyph representing an individual Unicode character
+ public Glyph(int unicodeID,
+ int glyphCode,
+ float advance,
+ GlyphVector singleUnicodeGlyphVector,
+ GlyphProducer producer) {
+ this.unicodeID = unicodeID;
+ this.glyphCode = glyphCode;
+ this.advance = advance;
+ this.singleUnicodeGlyphVector = singleUnicodeGlyphVector;
+ this.producer = producer;
+ }
+
+ // Creates a Glyph representing a sequence of characters, with
+ // an indication of whether additional single glyphs are being
+ // rendered after it
+ public Glyph(String str, boolean needAdvance) {
+ this.str = str;
+ this.needAdvance = needAdvance;
+ }
+
+ /** Returns this glyph's unicode ID */
+ public int getUnicodeID() {
+ return unicodeID;
+ }
+
+ /** Returns this glyph's (font-specific) glyph code */
+ public int getGlyphCode() {
+ return glyphCode;
+ }
+
+ /** Returns the advance for this glyph */
+ public float getAdvance() {
+ return advance;
+ }
+
+ /** Draws this glyph and returns the (x) advance for this glyph */
+ public float draw3D(float inX, float inY, float z, float scaleFactor) {
+ if (str != null) {
+ draw3D_ROBUST(str, inX, inY, z, scaleFactor);
+ if (!needAdvance) {
+ return 0;
+ }
+ // Compute and return the advance for this string
+ GlyphVector gv = font.createGlyphVector(getFontRenderContext(), str);
+ float totalAdvance = 0;
+ for (int i = 0; i < gv.getNumGlyphs(); i++) {
+ totalAdvance += gv.getGlyphMetrics(i).getAdvance();
+ }
+ return totalAdvance;
+ }
+
+ // This is the code path taken for individual glyphs
+ if (glyphRectForTextureMapping == null) {
+ upload();
+ }
+
+ try {
+ if (mPipelinedQuadRenderer == null) {
+ mPipelinedQuadRenderer = new Pipelined_QuadRenderer();
+ }
+
+ TextureRenderer renderer = getBackingStore();
+ // Handles case where NPOT texture is used for backing store
+ TextureCoords wholeImageTexCoords = renderer.getTexture().getImageTexCoords();
+ float xScale = wholeImageTexCoords.right();
+ float yScale = wholeImageTexCoords.bottom();
+
+ Rect rect = glyphRectForTextureMapping;
+ TextData data = (TextData) rect.getUserData();
+ data.markUsed();
+
+ Rectangle2D origRect = data.origRect();
+
+ float x = inX - (scaleFactor * data.origOriginX());
+ float y = inY - (scaleFactor * ((float) origRect.getHeight() - data.origOriginY()));
+
+ int texturex = rect.x() + (data.origin().x - data.origOriginX());
+ int texturey = renderer.getHeight() - rect.y() - (int) origRect.getHeight() -
+ (data.origin().y - data.origOriginY());
+ int width = (int) origRect.getWidth();
+ int height = (int) origRect.getHeight();
+
+ float tx1 = xScale * (float) texturex / (float) renderer.getWidth();
+ float ty1 = yScale * (1.0f -
+ ((float) texturey / (float) renderer.getHeight()));
+ float tx2 = xScale * (float) (texturex + width) / (float) renderer.getWidth();
+ float ty2 = yScale * (1.0f -
+ ((float) (texturey + height) / (float) renderer.getHeight()));
+
+ mPipelinedQuadRenderer.glTexCoord2f(tx1, ty1);
+ mPipelinedQuadRenderer.glVertex3f(x, y, z);
+ mPipelinedQuadRenderer.glTexCoord2f(tx2, ty1);
+ mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor), y,
+ z);
+ mPipelinedQuadRenderer.glTexCoord2f(tx2, ty2);
+ mPipelinedQuadRenderer.glVertex3f(x + (width * scaleFactor),
+ y + (height * scaleFactor), z);
+ mPipelinedQuadRenderer.glTexCoord2f(tx1, ty2);
+ mPipelinedQuadRenderer.glVertex3f(x,
+ y + (height * scaleFactor), z);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return advance;
+ }
+
+ /** Notifies this glyph that it's been cleared out of the cache */
+ public void clear() {
+ glyphRectForTextureMapping = null;
+ }
+
+ private void upload() {
+ GlyphVector gv = getGlyphVector();
+ Rectangle2D origBBox = preNormalize(renderDelegate.getBounds(gv, getFontRenderContext()));
+ Rectangle2D bbox = normalize(origBBox);
+ Point origin = new Point((int) -bbox.getMinX(),
+ (int) -bbox.getMinY());
+ Rect rect = new Rect(0, 0, (int) bbox.getWidth(),
+ (int) bbox.getHeight(),
+ new TextData(null, origin, origBBox, unicodeID));
+ packer.add(rect);
+ glyphRectForTextureMapping = rect;
+ Graphics2D g = getGraphics2D();
+ // OK, should now have an (x, y) for this rectangle; rasterize
+ // the glyph
+ int strx = rect.x() + origin.x;
+ int stry = rect.y() + origin.y;
+
+ // Clear out the area we're going to draw into
+ g.setComposite(AlphaComposite.Clear);
+ g.fillRect(rect.x(), rect.y(), rect.w(), rect.h());
+ g.setComposite(AlphaComposite.Src);
+
+ // Draw the string
+ renderDelegate.drawGlyphVector(g, gv, strx, stry);
+
+ if (DRAW_BBOXES) {
+ TextData data = (TextData) rect.getUserData();
+ // Draw a bounding box on the backing store
+ g.drawRect(strx - data.origOriginX(),
+ stry - data.origOriginY(),
+ (int) data.origRect().getWidth(),
+ (int) data.origRect().getHeight());
+ g.drawRect(strx - data.origin().x,
+ stry - data.origin().y,
+ rect.w(),
+ rect.h());
+ }
+
+ // Mark this region of the TextureRenderer as dirty
+ getBackingStore().markDirty(rect.x(), rect.y(), rect.w(),
+ rect.h());
+ // Re-register ourselves with our producer
+ producer.register(this);
+ }
+
+ private GlyphVector getGlyphVector() {
+ GlyphVector gv = singleUnicodeGlyphVector;
+ if (gv != null) {
+ singleUnicodeGlyphVector = null; // Don't need this anymore
+ return gv;
+ }
+ singleUnicode[0] = (char) unicodeID;
+ return font.createGlyphVector(getFontRenderContext(), singleUnicode);
+ }
+ }
+
+ class GlyphProducer {
+ final int undefined = -2;
+ FontRenderContext fontRenderContext;
+ List/*<Glyph>*/ glyphsOutput = new ArrayList/*<Glyph>*/();
+ // The mapping from unicode character to font-specific glyph ID
+ int[] unicodes2Glyphs;
+ // The mapping from glyph ID to Glyph
+ Glyph[] glyphCache;
+ // We re-use this for each incoming string
+ CharSequenceIterator iter = new CharSequenceIterator();
+
+ GlyphProducer(int fontLengthInGlyphs) {
+ unicodes2Glyphs = new int[512];
+ glyphCache = new Glyph[fontLengthInGlyphs];
+ clearAllCacheEntries();
+ }
+
+ public List/*<Glyph>*/ getGlyphs(CharSequence inString) {
+ glyphsOutput.clear();
+ iter.initFromCharSequence(inString);
+ GlyphVector fullRunGlyphVector = font.createGlyphVector(getFontRenderContext(),
+ iter);
+ boolean complex = (fullRunGlyphVector.getLayoutFlags() != 0);
+ if (complex || DISABLE_GLYPH_CACHE) {
+ // Punt to the robust version of the renderer
+ glyphsOutput.add(new Glyph(inString.toString(), false));
+ return glyphsOutput;
+ }
+
+ int lengthInGlyphs = fullRunGlyphVector.getNumGlyphs();
+ int i = 0;
+ while (i < lengthInGlyphs) {
+ Glyph glyph = getGlyph(inString, fullRunGlyphVector, i);
+ if (glyph != null) {
+ glyphsOutput.add(glyph);
+ i++;
+ } else {
+ // Assemble a run of characters that don't fit in
+ // the cache
+ StringBuffer buf = new StringBuffer();
+ while (i < lengthInGlyphs &&
+ getGlyph(inString, fullRunGlyphVector, i) == null) {
+ buf.append(inString.charAt(i++));
+ }
+ glyphsOutput.add(new Glyph(buf.toString(),
+ // Any more glyphs after this run?
+ i < lengthInGlyphs));
+ }
+ }
+ return glyphsOutput;
+ }
+
+ public void clearCacheEntry(int unicodeID) {
+ int glyphID = unicodes2Glyphs[unicodeID];
+ if (glyphID != undefined) {
+ Glyph glyph = glyphCache[glyphID];
+ if (glyph != null) {
+ glyph.clear();
+ }
+ glyphCache[glyphID] = null;
+ }
+ unicodes2Glyphs[unicodeID] = undefined;
+ }
+
+ public void clearAllCacheEntries() {
+ for (int i = 0; i < unicodes2Glyphs.length; i++) {
+ clearCacheEntry(i);
+ }
+ }
+
+ public void register(Glyph glyph) {
+ unicodes2Glyphs[glyph.getUnicodeID()] = glyph.getGlyphCode();
+ glyphCache[glyph.getGlyphCode()] = glyph;
+ }
+
+ public float getGlyphPixelWidth(char unicodeID) {
+ Glyph glyph = getGlyph(unicodeID);
+ if (glyph != null) {
+ return glyph.getAdvance();
+ }
+
+ // Have to do this the hard / uncached way
+ singleUnicode[0] = unicodeID;
+ GlyphVector gv = font.createGlyphVector(fontRenderContext,
+ singleUnicode);
+ return gv.getGlyphMetrics(0).getAdvance();
+ }
+
+ // Returns a glyph object for this single glyph. Returns null
+ // if the unicode or glyph ID would be out of bounds of the
+ // glyph cache.
+ private Glyph getGlyph(CharSequence inString,
+ GlyphVector fullRunGlyphVector,
+ int index) {
+ char unicodeID = inString.charAt(index);
+
+ if (unicodeID >= unicodes2Glyphs.length) {
+ return null;
+ }
+
+ int glyphID = unicodes2Glyphs[unicodeID];
+ if (glyphID != undefined) {
+ return glyphCache[glyphID];
+ }
+
+ // Must fabricate the glyph
+ singleUnicode[0] = unicodeID;
+ GlyphVector gv = font.createGlyphVector(getFontRenderContext(), singleUnicode);
+ return getGlyph(unicodeID, gv, fullRunGlyphVector.getGlyphMetrics(index));
+ }
+
+ // It's unclear whether this variant might produce less
+ // optimal results than if we can see the entire GlyphVector
+ // for the incoming string
+ private Glyph getGlyph(int unicodeID) {
+ if (unicodeID >= unicodes2Glyphs.length) {
+ return null;
+ }
+
+ int glyphID = unicodes2Glyphs[unicodeID];
+ if (glyphID != undefined) {
+ return glyphCache[glyphID];
+ }
+ singleUnicode[0] = (char) unicodeID;
+ GlyphVector gv = font.createGlyphVector(getFontRenderContext(), singleUnicode);
+ return getGlyph(unicodeID, gv, gv.getGlyphMetrics(0));
+ }
+
+ private Glyph getGlyph(int unicodeID,
+ GlyphVector singleUnicodeGlyphVector,
+ GlyphMetrics metrics) {
+ int glyphCode = singleUnicodeGlyphVector.getGlyphCode(0);
+ // Have seen huge glyph codes (65536) coming out of some fonts in some Unicode situations
+ if (glyphCode >= glyphCache.length) {
+ return null;
+ }
+ Glyph glyph = new Glyph(unicodeID,
+ glyphCode,
+ metrics.getAdvance(),
+ singleUnicodeGlyphVector,
+ this);
+ register(glyph);
+ return glyph;
+ }
+ }
+
+ class Pipelined_QuadRenderer {
+ int mOutstandingGlyphsVerticesPipeline = 0;
+ FloatBuffer mTexCoords;
+ FloatBuffer mVertCoords;
+ boolean usingVBOs;
+ int mVBO_For_ResuableTileVertices;
+ int mVBO_For_ResuableTileTexCoords;
+
+ Pipelined_QuadRenderer() {
+ GL2 gl = GLUgl2.getCurrentGL2();
+ mVertCoords = BufferUtil.newFloatBuffer(kTotalBufferSizeCoordsVerts);
+ mTexCoords = BufferUtil.newFloatBuffer(kTotalBufferSizeCoordsTex);
+
+ usingVBOs = is15Available(gl);
+
+ if (usingVBOs) {
+ try {
+ int[] vbos = new int[2];
+ gl.glGenBuffers(2, IntBuffer.wrap(vbos));
+
+ mVBO_For_ResuableTileVertices = vbos[0];
+ mVBO_For_ResuableTileTexCoords = vbos[1];
+
+ gl.glBindBuffer(GL2.GL_ARRAY_BUFFER,
+ mVBO_For_ResuableTileVertices);
+ gl.glBufferData(GL2.GL_ARRAY_BUFFER, kTotalBufferSizeBytesVerts,
+ null, GL2.GL_STREAM_DRAW); // stream draw because this is a single quad use pipeline
+
+ gl.glBindBuffer(GL2.GL_ARRAY_BUFFER,
+ mVBO_For_ResuableTileTexCoords);
+ gl.glBufferData(GL2.GL_ARRAY_BUFFER, kTotalBufferSizeBytesTex,
+ null, GL2.GL_STREAM_DRAW); // stream draw because this is a single quad use pipeline
+ } catch (Exception e) {
+ isExtensionAvailable_GL_VERSION_1_5 = false;
+ usingVBOs = false;
+ }
+ }
+ }
+
+ public void glTexCoord2f(float v, float v1) {
+ mTexCoords.put(v);
+ mTexCoords.put(v1);
+ }
+
+ public void glVertex3f(float inX, float inY, float inZ) {
+ mVertCoords.put(inX);
+ mVertCoords.put(inY);
+ mVertCoords.put(inZ);
+
+ mOutstandingGlyphsVerticesPipeline++;
+
+ if (mOutstandingGlyphsVerticesPipeline >= kTotalBufferSizeVerts) {
+ this.draw();
+ }
+ }
+
+ private void draw() {
+ if (useVertexArrays) {
+ drawVertexArrays();
+ } else {
+ drawIMMEDIATE();
+ }
+ }
+
+ private void drawVertexArrays() {
+ if (mOutstandingGlyphsVerticesPipeline > 0) {
+ GL2 gl = GLUgl2.getCurrentGL2();
+
+ TextureRenderer renderer = getBackingStore();
+ Texture texture = renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious?
+
+ mVertCoords.rewind();
+ mTexCoords.rewind();
+
+ gl.glEnableClientState(GL2.GL_VERTEX_ARRAY);
+
+ if (usingVBOs) {
+ gl.glBindBuffer(GL2.GL_ARRAY_BUFFER,
+ mVBO_For_ResuableTileVertices);
+ gl.glBufferSubData(GL2.GL_ARRAY_BUFFER, 0,
+ mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_VertexData,
+ mVertCoords); // upload only the new stuff
+ gl.glVertexPointer(3, GL2.GL_FLOAT, 0, 0);
+ } else {
+ gl.glVertexPointer(3, GL2.GL_FLOAT, 0, mVertCoords);
+ }
+
+ gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
+
+ if (usingVBOs) {
+ gl.glBindBuffer(GL2.GL_ARRAY_BUFFER,
+ mVBO_For_ResuableTileTexCoords);
+ gl.glBufferSubData(GL2.GL_ARRAY_BUFFER, 0,
+ mOutstandingGlyphsVerticesPipeline * kSizeInBytes_OneVertices_TexData,
+ mTexCoords); // upload only the new stuff
+ gl.glTexCoordPointer(2, GL2.GL_FLOAT, 0, 0);
+ } else {
+ gl.glTexCoordPointer(2, GL2.GL_FLOAT, 0, mTexCoords);
+ }
+
+ gl.glDrawArrays(GL2.GL_QUADS, 0,
+ mOutstandingGlyphsVerticesPipeline);
+
+ mVertCoords.rewind();
+ mTexCoords.rewind();
+ mOutstandingGlyphsVerticesPipeline = 0;
+ }
+ }
+
+ private void drawIMMEDIATE() {
+ if (mOutstandingGlyphsVerticesPipeline > 0) {
+ TextureRenderer renderer = getBackingStore();
+ Texture texture = renderer.getTexture(); // triggers texture uploads. Maybe this should be more obvious?
+
+ GL2 gl = GLUgl2.getCurrentGL2();
+ gl.glBegin(GL2.GL_QUADS);
+
+ try {
+ int numberOfQuads = mOutstandingGlyphsVerticesPipeline / 4;
+ mVertCoords.rewind();
+ mTexCoords.rewind();
+
+ for (int i = 0; i < numberOfQuads; i++) {
+ gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get());
+ gl.glVertex3f(mVertCoords.get(), mVertCoords.get(),
+ mVertCoords.get());
+
+ gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get());
+ gl.glVertex3f(mVertCoords.get(), mVertCoords.get(),
+ mVertCoords.get());
+
+ gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get());
+ gl.glVertex3f(mVertCoords.get(), mVertCoords.get(),
+ mVertCoords.get());
+
+ gl.glTexCoord2f(mTexCoords.get(), mTexCoords.get());
+ gl.glVertex3f(mVertCoords.get(), mVertCoords.get(),
+ mVertCoords.get());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ gl.glEnd();
+ mVertCoords.rewind();
+ mTexCoords.rewind();
+ mOutstandingGlyphsVerticesPipeline = 0;
+ }
+ }
+ }
+ }
+
+ class DebugListener implements GLEventListener {
+ private GLU glu = GLU.createGLU();
+ private Frame frame;
+
+ DebugListener(Frame frame) {
+ this.frame = frame;
+ }
+
+ public void display(GLAutoDrawable drawable) {
+ GL2 gl = GLUgl2.getCurrentGL2();
+ gl.glClear(GL2.GL_DEPTH_BUFFER_BIT | GL2.GL_COLOR_BUFFER_BIT);
+
+ if (packer == null) {
+ return;
+ }
+
+ TextureRenderer rend = getBackingStore();
+ final int w = rend.getWidth();
+ final int h = rend.getHeight();
+ rend.beginOrthoRendering(w, h);
+ rend.drawOrthoRect(0, 0);
+ rend.endOrthoRendering();
+
+ if ((frame.getWidth() != w) || (frame.getHeight() != h)) {
+ EventQueue.invokeLater(new Runnable() {
+ public void run() {
+ frame.setSize(w, h);
+ }
+ });
+ }
+ }
+
+ public void dispose(GLAutoDrawable drawable) {
+ glu.destroy();
+ glu=null;
+ frame=null;
+ }
+
+ // Unused methods
+ public void init(GLAutoDrawable drawable) {
+ }
+
+ public void reshape(GLAutoDrawable drawable, int x, int y, int width,
+ int height) {
+ }
+
+ public void displayChanged(GLAutoDrawable drawable,
+ boolean modeChanged, boolean deviceChanged) {
+ }
+ }
+
+ /**
+ * Sets whether vertex arrays are being used internally for
+ * rendering, or whether text is rendered using the OpenGL
+ * immediate mode commands. This is provided as a concession for
+ * certain graphics cards which have poor vertex array
+ * performance. Defaults to true.
+ */
+ public void setUseVertexArrays(boolean useVertexArrays) {
+ this.useVertexArrays = useVertexArrays;
+ }
+
+ /**
+ * Indicates whether vertex arrays are being used internally for
+ * rendering, or whether text is rendered using the OpenGL
+ * immediate mode commands. Defaults to true.
+ */
+ public boolean getUseVertexArrays() {
+ return useVertexArrays;
+ }
+
+ /**
+ * Sets whether smoothing (i.e., GL_LINEAR filtering) is enabled
+ * in the backing TextureRenderer of this TextRenderer. A few
+ * graphics cards do not behave well when this is enabled,
+ * resulting in fuzzy text. Defaults to true.
+ */
+ public void setSmoothing(boolean smoothing) {
+ this.smoothing = smoothing;
+ getBackingStore().setSmoothing(smoothing);
+ }
+
+ /**
+ * Indicates whether smoothing is enabled in the backing
+ * TextureRenderer of this TextRenderer. A few graphics cards do
+ * not behave well when this is enabled, resulting in fuzzy text.
+ * Defaults to true.
+ */
+ public boolean getSmoothing() {
+ return smoothing;
+ }
+
+ private boolean is15Available(GL gl) {
+ if (!checkFor_isExtensionAvailable_GL_VERSION_1_5) {
+ isExtensionAvailable_GL_VERSION_1_5 = gl.isExtensionAvailable("GL_VERSION_1_5");
+ checkFor_isExtensionAvailable_GL_VERSION_1_5 = true;
+ }
+ return isExtensionAvailable_GL_VERSION_1_5;
+ }
+}