/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions 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 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.font; import com.jme3.math.ColorRGBA; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.ShortBuffer; /** * LetterQuad contains the position, color, and UV texture information for a character in text. * @author YongHoon */ class LetterQuad { private static final Rectangle UNBOUNDED = new Rectangle(0, 0, Float.MAX_VALUE, Float.MAX_VALUE); private static final float LINE_DIR = -1; private final BitmapFont font; private final char c; private final int index; private int style; private BitmapCharacter bitmapChar = null; private float x0 = Integer.MIN_VALUE; private float y0 = Integer.MIN_VALUE; private float width = Integer.MIN_VALUE; private float height = Integer.MIN_VALUE; private float xAdvance = 0; private float u0; private float v0; private float u1; private float v1; private float lineY; private boolean eol; private LetterQuad previous; private LetterQuad next; private int colorInt = 0xFFFFFFFF; private boolean rightToLeft; private float alignX; private float alignY; private float sizeScale = 1; /** * create head / tail * @param font * @param rightToLeft */ protected LetterQuad(BitmapFont font, boolean rightToLeft) { this.font = font; this.c = Character.MIN_VALUE; this.rightToLeft = rightToLeft; this.index = -1; setBitmapChar(null); } /** * create letter and append to previous LetterQuad * * @param c * @param prev previous character */ protected LetterQuad(char c, LetterQuad prev) { this.font = prev.font; this.rightToLeft = prev.rightToLeft; this.c = c; this.index = prev.index+1; this.eol = isLineFeed(); setBitmapChar(c); prev.insert(this); } LetterQuad addNextCharacter(char c) { LetterQuad n = new LetterQuad(c, this); return n; } BitmapCharacter getBitmapChar() { return bitmapChar; } char getChar() { return c; } int getIndex() { return index; } private Rectangle getBound(StringBlock block) { if (block.getTextBox() != null) { return block.getTextBox(); } return UNBOUNDED; } LetterQuad getPrevious() { return previous; } LetterQuad getNext() { return next; } public float getU0() { return u0; } float getU1() { return u1; } float getV0() { return v0; } float getV1() { return v1; } boolean isRightToLeft(){ return rightToLeft; } boolean isInvalid() { return x0 == Integer.MIN_VALUE; } boolean isInvalid(StringBlock block) { return isInvalid(block, 0); } boolean isInvalid(StringBlock block, float gap) { if (isHead() || isTail()) return false; if (x0 == Integer.MIN_VALUE || y0 == Integer.MIN_VALUE) { return true; } Rectangle bound = block.getTextBox(); if (bound == null) { return false; } return x0 > 0 && bound.x+bound.width-gap < getX1(); } void clip(StringBlock block) { Rectangle bound = block.getTextBox(); if (bound == null) return; // Clip the right x position and texture coordinate // to the string block float x1 = Math.min(bound.x + bound.width, x0 + width); float newWidth = x1 - x0; if( newWidth == width ) return; float rescale = newWidth / width; u1 = u0 + (u1 - u0) * rescale; width = newWidth; } float getX0() { return x0; } float getX1() { return x0+width; } float getNextX() { return x0+xAdvance; } float getNextLine() { return lineY+LINE_DIR*font.getCharSet().getLineHeight() * sizeScale; } float getY0() { return y0; } float getY1() { return y0-height; } float getWidth() { return width; } float getHeight() { return height; } void insert(LetterQuad ins) { LetterQuad n = next; next = ins; ins.next = n; ins.previous = this; n.previous = ins; } void invalidate() { eol = isLineFeed(); setBitmapChar(font.getCharSet().getCharacter(c, style)); } boolean isTail() { return next == null; } boolean isHead() { return previous == null; } /** * @return next letter */ LetterQuad remove() { this.previous.next = next; this.next.previous = previous; return next; } void setPrevious(LetterQuad before) { this.previous = before; } void setStyle(int style) { this.style = style; invalidate(); } void setColor(ColorRGBA color) { this.colorInt = color.asIntRGBA(); invalidate(); } void setAlpha(float alpha) { int i = (int)(alpha * 255) & 0xFF; colorInt = (colorInt & 0xffffff00) | i; invalidate(); } void setBitmapChar(char c) { BitmapCharacterSet charSet = font.getCharSet(); BitmapCharacter bm = charSet.getCharacter(c, style); setBitmapChar(bm); } void setBitmapChar(BitmapCharacter bitmapChar) { x0 = Integer.MIN_VALUE; y0 = Integer.MIN_VALUE; width = Integer.MIN_VALUE; height = Integer.MIN_VALUE; alignX = 0; alignY = 0; BitmapCharacterSet charSet = font.getCharSet(); this.bitmapChar = bitmapChar; if (bitmapChar != null) { u0 = (float) bitmapChar.getX() / charSet.getWidth(); v0 = (float) bitmapChar.getY() / charSet.getHeight(); u1 = u0 + (float) bitmapChar.getWidth() / charSet.getWidth(); v1 = v0 + (float) bitmapChar.getHeight() / charSet.getHeight(); } else { u0 = 0; v0 = 0; u1 = 0; v1 = 0; } } void setNext(LetterQuad next) { this.next = next; } void update(StringBlock block) { final float[] tabs = block.getTabPosition(); final float tabWidth = block.getTabWidth(); final Rectangle bound = getBound(block); sizeScale = block.getSize() / font.getCharSet().getRenderedSize(); lineY = computeLineY(block); if (isHead()) { x0 = getBound(block).x; y0 = lineY; width = 0; height = 0; xAdvance = 0; } else if (isTab()) { x0 = previous.getNextX(); width = tabWidth; y0 = lineY; height = 0; if (tabs != null && x0 < tabs[tabs.length-1]) { for (int i = 0; i < tabs.length-1; i++) { if (x0 > tabs[i] && x0 < tabs[i+1]) { width = tabs[i+1] - x0; } } } xAdvance = width; } else if (bitmapChar == null) { x0 = getPrevious().getX1(); y0 = lineY; width = 0; height = 0; xAdvance = 0; } else { float xOffset = bitmapChar.getXOffset() * sizeScale; float yOffset = bitmapChar.getYOffset() * sizeScale; xAdvance = bitmapChar.getXAdvance() * sizeScale; width = bitmapChar.getWidth() * sizeScale; height = bitmapChar.getHeight() * sizeScale; float incrScale = rightToLeft ? -1f : 1f; float kernAmount = 0f; if (previous.isHead() || previous.eol) { x0 = bound.x; // The first letter quad will be drawn right at the first // position... but it does not offset by the characters offset // amount. This means that we've potentially accumulated extra // pixels and the next letter won't get drawn far enough unless // we add this offset back into xAdvance.. by subtracting it. // This is the same thing that's done below because we've // technically baked the offset in just like below. It doesn't // look like it at first glance so I'm keeping it separate with // this comment. xAdvance -= xOffset * incrScale; } else { x0 = previous.getNextX() + xOffset * incrScale; // Since x0 will have offset baked into it then we // need to counteract that in xAdvance. This is better // than removing it in getNextX() because we also need // to take kerning into account below... which will also // get baked in. // Without this, getNextX() will return values too far to // the left, for example. xAdvance -= xOffset * incrScale; } y0 = lineY + LINE_DIR*yOffset; // Adjust for kerning BitmapCharacter lastChar = previous.getBitmapChar(); if (lastChar != null && block.isKerning()) { kernAmount = lastChar.getKerning(c) * sizeScale; x0 += kernAmount * incrScale; // Need to unbake the kerning from xAdvance since it // is baked into x0... see above. //xAdvance -= kernAmount * incrScale; // No, kerning is an inter-character spacing and _does_ affect // all subsequent cursor positions. } } if (isEndOfLine()) { xAdvance = bound.x-x0; } } /** * add temporary linewrap indicator */ void setEndOfLine() { this.eol = true; } boolean isEndOfLine() { return eol; } boolean isLineWrap() { return !isHead() && !isTail() && bitmapChar == null && c == Character.MIN_VALUE; } private float computeLineY(StringBlock block) { if (isHead()) { return getBound(block).y; } else if (previous.eol) { return previous.getNextLine(); } else { return previous.lineY; } } boolean isLineStart() { return x0 == 0 || (previous != null && previous.eol); } boolean isBlank() { return c == ' ' || isTab(); } public void storeToArrays(float[] pos, float[] tc, short[] idx, byte[] colors, int quadIdx){ float x = x0+alignX; float y = y0-alignY; float xpw = x+width; float ymh = y-height; pos[0] = x; pos[1] = y; pos[2] = 0; pos[3] = x; pos[4] = ymh; pos[5] = 0; pos[6] = xpw; pos[7] = ymh; pos[8] = 0; pos[9] = xpw; pos[10] = y; pos[11] = 0; float v0 = 1f - this.v0; float v1 = 1f - this.v1; tc[0] = u0; tc[1] = v0; tc[2] = u0; tc[3] = v1; tc[4] = u1; tc[5] = v1; tc[6] = u1; tc[7] = v0; colors[3] = (byte) (colorInt & 0xff); colors[2] = (byte) ((colorInt >> 8) & 0xff); colors[1] = (byte) ((colorInt >> 16) & 0xff); colors[0] = (byte) ((colorInt >> 24) & 0xff); System.arraycopy(colors, 0, colors, 4, 4); System.arraycopy(colors, 0, colors, 8, 4); System.arraycopy(colors, 0, colors, 12, 4); short i0 = (short) (quadIdx * 4); short i1 = (short) (i0 + 1); short i2 = (short) (i0 + 2); short i3 = (short) (i0 + 3); idx[0] = i0; idx[1] = i1; idx[2] = i2; idx[3] = i0; idx[4] = i2; idx[5] = i3; } public void appendPositions(FloatBuffer fb){ float sx = x0+alignX; float sy = y0-alignY; float ex = sx+width; float ey = sy-height; // NOTE: subtracting the height here // because OGL's Ortho origin is at lower-left fb.put(sx).put(sy).put(0f); fb.put(sx).put(ey).put(0f); fb.put(ex).put(ey).put(0f); fb.put(ex).put(sy).put(0f); } public void appendPositions(ShortBuffer sb){ final float x1 = getX1(); final float y1 = getY1(); short x = (short) x0; short y = (short) y0; short xpw = (short) (x1); short ymh = (short) (y1); sb.put(x).put(y).put((short)0); sb.put(x).put(ymh).put((short)0); sb.put(xpw).put(ymh).put((short)0); sb.put(xpw).put(y).put((short)0); } public void appendTexCoords(FloatBuffer fb){ // flip coords to be compatible with OGL float v0 = 1 - this.v0; float v1 = 1 - this.v1; // upper left fb.put(u0).put(v0); // lower left fb.put(u0).put(v1); // lower right fb.put(u1).put(v1); // upper right fb.put(u1).put(v0); } public void appendColors(ByteBuffer bb){ bb.putInt(colorInt); bb.putInt(colorInt); bb.putInt(colorInt); bb.putInt(colorInt); } public void appendIndices(ShortBuffer sb, int quadIndex){ // each quad has 4 indices short v0 = (short) (quadIndex * 4); short v1 = (short) (v0 + 1); short v2 = (short) (v0 + 2); short v3 = (short) (v0 + 3); sb.put(v0).put(v1).put(v2); sb.put(v0).put(v2).put(v3); // sb.put(new short[]{ v0, v1, v2, // v0, v2, v3 }); } @Override public String toString() { return String.valueOf(c); } void setAlignment(float alignX, float alignY) { this.alignX = alignX; this.alignY = alignY; } float getAlignX() { return alignX; } float getAlignY() { return alignY; } boolean isLineFeed() { return c == '\n'; } boolean isTab() { return c == '\t'; } }