Added some useful features.
- Multi-style text (rich text)
- ex) plain and bold font can be handled in one BitmapFont
- by loading two font and merging (texture is not automatically generated yet)
- Texture Coloring
- current implementation also has one, but not easy to do it programatically.
- No Line Wrap Mode
- Don’t cross the border
I coded BitmapFont.updateText() from scratch because it is hard to understand and inefficient.
[patch]
Index: src/core/com/jme3/font/LetterQuad.java
===================================================================
— src/core/com/jme3/font/LetterQuad.java (revision 0)
+++ src/core/com/jme3/font/LetterQuad.java (revision 0)
@@ -0,0 +1,447 @@
+package com.jme3.font;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+import java.util.Arrays;
+
+import com.jme3.font.BitmapCharacter;
+import com.jme3.font.BitmapCharacterSet;
+import com.jme3.font.BitmapFont;
+import com.jme3.font.Rectangle;
+import com.jme3.font.StringBlock;
+import com.jme3.math.ColorRGBA;
+
+public 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 static final float TAB_WIDTH = 30;
+
- 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 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;
+
- /**
-
- create head / tail
- create head / tail
-
-
@param font
-
@param font
-
-
@param rightToLeft
-
@param rightToLeft
- */
- protected LetterQuad(BitmapFont font, boolean rightToLeft) {
-
this.font = font;<br />
-
this.c = Character.MIN_VALUE;<br />
-
this.rightToLeft = rightToLeft;<br />
-
this.index = -1;<br />
-
setBitmapChar(null);<br />
- }
+
- /**
-
- create letter and append to prev
- create letter and append to prev
- *
-
-
@param c
-
@param c
-
-
@param prev previous character
-
@param prev previous character
- */
- protected LetterQuad(char c, LetterQuad prev) {
-
this.font = prev.font;<br />
-
this.rightToLeft = prev.rightToLeft;<br />
-
this.c = c;<br />
-
this.index = prev.index+1;<br />
-
this.eol = isLineFeed();<br />
-
setBitmapChar(c);<br />
-
prev.insert(this);<br />
- }
+
- LetterQuad addNextCharacter(char c) {
-
LetterQuad n = new LetterQuad(c, this);<br />
-
return n;<br />
- }
+
- BitmapCharacter getBitmapChar() {
-
return bitmapChar;<br />
- }
+
- char getChar() {
-
return c;<br />
- }
+
- int getIndex() {
-
return index;<br />
- }
+
- private Rectangle getBound(StringBlock block) {
-
if (block.getTextBox() != null) {<br />
-
return block.getTextBox();<br />
-
}<br />
-
return UNBOUNDED;<br />
- }
+
- LetterQuad getPrevious() {
-
return previous;<br />
- }
+
- LetterQuad getNext() {
-
return next;<br />
- }
+
- public float getU0() {
-
return u0;<br />
- }
+
- float getU1() {
-
return u1;<br />
- }
+
- float getV0() {
-
return v0;<br />
- }
+
- float getV1() {
-
return v1;<br />
- }
+
- boolean isInvalid() {
-
return x0 == Integer.MIN_VALUE;<br />
- }
+
- boolean isInvalid(StringBlock block) {
-
return isInvalid(block, 0);<br />
- }
+
- boolean isInvalid(StringBlock block, float gap) {
-
if (isHead() || isTail())<br />
-
return false;<br />
-
if (x0 == Integer.MIN_VALUE || y0 == Integer.MIN_VALUE) {<br />
-
return true;<br />
-
}<br />
-
Rectangle bound = block.getTextBox();<br />
-
if (bound == null) {<br />
-
return false;<br />
-
}<br />
-
return x0 > 0 && bound.x+bound.width-gap < getX1();<br />
- }
+
- float getX0() {
-
return x0;<br />
- }
+
- float getX1() {
-
return x0+width;<br />
- }
+
- float getY0() {
-
return y0;<br />
- }
+
- float getY1() {
-
return y0-height;<br />
- }
+
- float getWidth() {
-
return width;<br />
- }
+
- float getHeight() {
-
return height;<br />
- }
+
- void insert(LetterQuad ins) {
-
LetterQuad n = next;<br />
-
next = ins;<br />
-
ins.next = n;<br />
-
ins.previous = this;<br />
-
n.previous = ins;<br />
- }
+
- void invalidate() {
-
eol = isLineFeed();<br />
-
setBitmapChar(font.getCharSet().getCharacter(c, style));<br />
- }
+
- boolean isTail() {
-
return next == null;<br />
- }
+
- boolean isHead() {
-
return previous == null;<br />
- }
+
- /**
-
-
@return next letter
-
@return next letter
- */
- LetterQuad remove() {
-
this.previous.next = next;<br />
-
this.next.previous = previous;<br />
-
return next;<br />
- }
+
- void setPrevious(LetterQuad before) {
-
this.previous = before;<br />
- }
+
- void setStyle(int style) {
-
this.style = style;<br />
-
invalidate();<br />
- }
+
- void setColor(ColorRGBA color) {
-
this.colorInt = color.asIntRGBA();<br />
-
invalidate();<br />
- }
+
- void setBitmapChar(char c) {
-
BitmapCharacterSet charSet = font.getCharSet();<br />
-
BitmapCharacter bm = charSet.getCharacter(c, style);<br />
-
setBitmapChar(bm);<br />
- }
+
- void setBitmapChar(BitmapCharacter bitmapChar) {
-
x0 = Integer.MIN_VALUE;<br />
-
y0 = Integer.MIN_VALUE;<br />
-
width = Integer.MIN_VALUE;<br />
-
height = Integer.MIN_VALUE;<br />
+
-
BitmapCharacterSet charSet = font.getCharSet();<br />
-
this.bitmapChar = bitmapChar;<br />
-
if (bitmapChar != null) {<br />
-
u0 = (float) bitmapChar.getX() / charSet.getWidth();<br />
-
v0 = (float) bitmapChar.getY() / charSet.getHeight();<br />
-
u1 = u0 + (float) bitmapChar.getWidth() / charSet.getWidth();<br />
-
v1 = v0 + (float) bitmapChar.getHeight() / charSet.getHeight();<br />
-
} else {<br />
-
u0 = 0;<br />
-
v0 = 0;<br />
-
u1 = 0;<br />
-
v1 = 0;<br />
-
}<br />
- }
+
- void setNext(LetterQuad next) {
-
this.next = next;<br />
- }
+
- void update(StringBlock block) {
-
Rectangle bound = getBound(block);<br />
-
float sizeScale = block.getSize() / font.getCharSet().getRenderedSize();<br />
-
computeLineY(block, sizeScale);<br />
+
-
if (isTab()) {<br />
-
x0 = previous.getX0();<br />
-
y0 = lineY;<br />
-
width = TAB_WIDTH;<br />
-
height = 0;<br />
-
} else if (bitmapChar == null) {<br />
-
x0 = getBound(block).x;<br />
-
y0 = lineY;<br />
-
width = 0;<br />
-
height = 0;<br />
-
} else {<br />
-
float xOffset = bitmapChar.getXOffset() * sizeScale;<br />
-
float yOffset = bitmapChar.getYOffset() * sizeScale;<br />
+// float xAdvance = bitmapChar.getXAdvance() * sizeScale;
-
width = bitmapChar.getWidth() * sizeScale;<br />
-
height = bitmapChar.getHeight() * sizeScale;<br />
-
float incrScale = rightToLeft ? -1f : 1f;<br />
-
float kernAmount = 0f;<br />
+
-
if (x0 == Integer.MIN_VALUE) {<br />
-
if (previous.isHead() || previous.eol) {<br />
-
x0 = bound.x;<br />
-
} else {<br />
-
x0 = previous.getX1() + xOffset * incrScale;<br />
-
}<br />
-
y0 = lineY + LINE_DIR*yOffset;<br />
-
}<br />
+
-
// Adjust for kerning<br />
-
BitmapCharacter lastChar = previous.getBitmapChar();<br />
-
if (lastChar != null && block.isKerning()) {<br />
-
int amount = lastChar.getKerning(c);<br />
-
if (amount != -1) {<br />
-
kernAmount = amount * sizeScale;<br />
-
x0 += kernAmount * incrScale;<br />
-
}<br />
-
}<br />
-
}<br />
- }
+
- private boolean isLineStartButNot1st() {
-
return !previous.isHead() && isLineStart();<br />
- }
- /**
-
* add temporary linewrap indicator<br />
-
*/<br />
- void setEndOfLine() {
-
this.eol = true;<br />
- }
+
- boolean isEndOfLine() {
-
return eol;<br />
- }
+
- boolean isLineWrap() {
-
return !isHead() && !isTail() && bitmapChar == null && c == Character.MIN_VALUE;<br />
- }
+
- void computeLineY(StringBlock block, float sizeScale) {
-
if (isHead()) {<br />
-
lineY = getBound(block).y;<br />
-
} else if (previous.eol) {<br />
-
lineY = previous.lineY+LINE_DIR*font.getCharSet().getLineHeight() * sizeScale;<br />
-
} else {<br />
-
lineY = previous.lineY;<br />
-
}<br />
- }
+
+
- boolean isLineStart() {
-
return x0 == 0 || (previous != null && previous.eol);<br />
- }
+
- boolean isBlank() {
-
return c == ' ' || isTab();<br />
- }
+
- public void storeToArrays(float[] pos, float[] tc, short[] idx, byte[] colors, int quadIdx){
-
float x = x0+alignX;<br />
-
float y = y0-alignY;<br />
-
float xpw = x0+width;<br />
-
float ymh = y0-height;<br />
+
-
pos[0] = x; pos[1] = y; pos[2] = 0;<br />
-
pos[3] = x; pos[4] = ymh; pos[5] = 0;<br />
-
pos[6] = xpw; pos[7] = ymh; pos[8] = 0;<br />
-
pos[9] = xpw; pos[10] = y; pos[11] = 0;<br />
+
-
float v0 = 1f - this.v0;<br />
-
float v1 = 1f - this.v1;<br />
+
-
tc[0] = u0; tc[1] = v0;<br />
-
tc[2] = u0; tc[3] = v1;<br />
-
tc[4] = u1; tc[5] = v1;<br />
-
tc[6] = u1; tc[7] = v0;<br />
+
-
colors[0] = (byte) (colorInt & 0xff);<br />
-
colors[1] = (byte) ((colorInt >> 8) & 0xff);<br />
-
colors[2] = (byte) ((colorInt >> 16) & 0xff);<br />
-
colors[3] = (byte) ((colorInt >> 24) & 0xff);<br />
-
System.arraycopy(colors, 0, colors, 4, 4);<br />
-
System.arraycopy(colors, 0, colors, 8, 4);<br />
-
System.arraycopy(colors, 0, colors, 12, 4);<br />
+
-
short i0 = (short) (quadIdx * 4);<br />
-
short i1 = (short) (i0 + 1);<br />
-
short i2 = (short) (i0 + 2);<br />
-
short i3 = (short) (i0 + 3);<br />
+
-
idx[0] = i0; idx[1] = i1; idx[2] = i2;<br />
-
idx[3] = i0; idx[4] = i2; idx[5] = i3;<br />
- }
+
- public void appendPositions(FloatBuffer fb){
-
// NOTE: subtracting the height here<br />
-
// because OGL's Ortho origin is at lower-left<br />
-
final float x1 = x0+width;<br />
-
final float y1 = y0-height;<br />
-
fb.put(x0).put(y0).put(0f);<br />
-
fb.put(x0).put(y1).put(0f);<br />
-
fb.put(x1).put(y1).put(0f);<br />
-
fb.put(x1).put(y0).put(0f);<br />
- }
+
- public void appendPositions(ShortBuffer sb){
-
final float x1 = getX1();<br />
-
final float y1 = getY1();<br />
-
short x = (short) x0;<br />
-
short y = (short) y0;<br />
-
short xpw = (short) (x1);<br />
-
short ymh = (short) (y1);<br />
+
-
sb.put(x).put(y).put((short)0);<br />
-
sb.put(x).put(ymh).put((short)0);<br />
-
sb.put(xpw).put(ymh).put((short)0);<br />
-
sb.put(xpw).put(y).put((short)0);<br />
- }
+
- public void appendTexCoords(FloatBuffer fb){
-
// flip coords to be compatible with OGL<br />
-
float v0 = 1 - this.v0;<br />
-
float v1 = 1 - this.v1;<br />
+
-
// upper left<br />
-
fb.put(u0).put(v0);<br />
-
// lower left<br />
-
fb.put(u0).put(v1);<br />
-
// lower right<br />
-
fb.put(u1).put(v1);<br />
-
// upper right<br />
-
fb.put(u1).put(v0);<br />
- }
+
- public void appendColors(ByteBuffer bb){
-
bb.putInt(colorInt);<br />
-
bb.putInt(colorInt);<br />
-
bb.putInt(colorInt);<br />
-
bb.putInt(colorInt);<br />
- }
+
- public void appendIndices(ShortBuffer sb, int quadIndex){
-
// each quad has 4 indices<br />
-
short v0 = (short) (quadIndex * 4);<br />
-
short v1 = (short) (v0 + 1);<br />
-
short v2 = (short) (v0 + 2);<br />
-
short v3 = (short) (v0 + 3);<br />
+
-
sb.put(v0).put(v1).put(v2);<br />
-
sb.put(v0).put(v2).put(v3);<br />
+// sb.put(new short[]{ v0, v1, v2,
+// v0, v2, v3 });
- }
+
+
-
@Override
- public String toString() {
-
return String.valueOf(c);<br />
- }
+
- void setAlignment(float alignX, float alignY) {
-
this.alignX = alignX;<br />
-
this.alignY = alignY;<br />
- }
+
- float getAlignX() {
-
return alignX;<br />
- }
+
- float getAlignY() {
-
return alignY;<br />
- }
+
- boolean isLineFeed() {
-
return c == 'n';<br />
- }
+
- boolean isTab() {
-
return c == 't';<br />
- }
+
+}
Index: src/core/com/jme3/font/LineWrapType.java
===================================================================
— src/core/com/jme3/font/LineWrapType.java (revision 0)
+++ src/core/com/jme3/font/LineWrapType.java (revision 0)
@@ -0,0 +1,7 @@
+package com.jme3.font;
+
+public enum LineWrapType {
- CHARACTER,
- WORD,
- ELLIPSIS
+}
Index: src/core/com/jme3/font/BitmapCharacterSet.java
===================================================================
— src/core/com/jme3/font/BitmapCharacterSet.java (revision 6754)
+++ src/core/com/jme3/font/BitmapCharacterSet.java (working copy)
@@ -32,14 +32,17 @@
package com.jme3.font;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
-import com.jme3.export.InputCapsule;
import com.jme3.export.OutputCapsule;
import com.jme3.export.Savable;
import com.jme3.util.IntMap;
import com.jme3.util.IntMap.Entry;
-import java.io.IOException;
public class BitmapCharacterSet implements Savable {
@@ -48,8 +51,9 @@
private int renderedSize;
private int width;
private int height;
- private IntMap<BitmapCharacter> characters;
- private IntMap<IntMap<BitmapCharacter>> characters;
-
@Override
public void write(JmeExporter ex) throws IOException {
OutputCapsule oc = ex.getCapsule(this);
oc.write(lineHeight, "lineHeight", 0);
@@ -58,20 +62,34 @@
oc.write(width, "width", 0);
oc.write(height, "height", 0);
-
int size = characters.size();<br />
-
int[] styles = new int[characters.size()];<br />
-
int index = 0;<br />
-
for (Entry<IntMap<BitmapCharacter>> entry : characters) {<br />
-
int style = entry.getKey();<br />
-
styles[index] = style;<br />
-
index++;<br />
-
IntMap<BitmapCharacter> charset = entry.getValue();<br />
-
writeCharset(oc, style, charset);<br />
-
}<br />
-
oc.write(styles, "styles", null);<br />
- }
+
- protected void writeCharset(OutputCapsule oc, int style, IntMap<BitmapCharacter> charset) throws IOException {
-
int size = charset.size();<br />
short[] indexes = new short[size];
BitmapCharacter[] chars = new BitmapCharacter[size];
int i = 0;
-
for (Entry<BitmapCharacter> chr : characters){<br />
-
for (Entry<BitmapCharacter> chr : charset){<br />
indexes = (short) chr.getKey();
chars = chr.getValue();
i++;
}
-
oc.write(indexes, "indexes", null);<br />
-
oc.write(chars, "chars", null);<br />
-
oc.write(indexes, "indexes"+style, null);<br />
-
oc.write(chars, "chars"+style, null);<br />
}
-
@Override
public void read(JmeImporter im) throws IOException {
InputCapsule ic = im.getCapsule(this);
lineHeight = ic.readInt("lineHeight", 0);
@@ -79,27 +97,51 @@
renderedSize = ic.readInt("renderedSize", 0);
width = ic.readInt("width", 0);
height = ic.readInt("height", 0);
-
int[] styles = ic.readIntArray("styles", null);<br />
+
-
for (int style : styles) {<br />
-
characters.put(style, readCharset(ic, style));<br />
-
}<br />
- }
-
short[] indexes = ic.readShortArray("indexes", null);<br />
-
Savable[] chars = ic.readSavableArray("chars", null);<br />
- private IntMap<BitmapCharacter> readCharset(InputCapsule ic, int style) throws IOException {
-
IntMap<BitmapCharacter> charset = new IntMap<BitmapCharacter>();<br />
-
short[] indexes = ic.readShortArray("indexes"+style, null);<br />
-
Savable[] chars = ic.readSavableArray("chars"+style, null);<br />
for (int i = 0; i < indexes.length; i++){
int index = indexes & 0xFFFF;
BitmapCharacter chr = (BitmapCharacter) chars;
- characters.put(index, chr);
+ charset.put(index, chr);
}
+ return charset;
}
public BitmapCharacterSet() {
- characters = new IntMap<BitmapCharacter>();
+ characters = new IntMap<IntMap<BitmapCharacter>>();
}
public BitmapCharacter getCharacter(int index){
- return characters.get(index);
+ return getCharacter(index, 0);
+ }
+
+ public BitmapCharacter getCharacter(int index, int style){
+ IntMap<BitmapCharacter> map = getCharacterSet(style);
+ if (map != null) {
+ return map.get(index);
+ }
+ return null;
+ }
+
+ private IntMap<BitmapCharacter> getCharacterSet(int style) {
+ if (characters.size() == 0) {
+ characters.put(style, new IntMap<BitmapCharacter>());
+ }
+ return characters.get(style);
}
public void addCharacter(int index, BitmapCharacter ch){
- characters.put(index, ch);
+ getCharacterSet(0).put(index, ch);
}
public int getLineHeight() {
@@ -141,4 +183,41 @@
public void setHeight(int height) {
this.height = height;
}
+
+ public void merge(BitmapCharacterSet that) {
+ if (this.renderedSize != that.renderedSize) {
+ throw new RuntimeException("Only support same font size");
+ }
+ for (Entry<IntMap<BitmapCharacter>> entry : that.characters) {
+ int style = entry.getKey();
+ IntMap<BitmapCharacter> charset = entry.getValue();
+ int shiftx = 0;
+ int shifty = 0;
+ if (this.height > this.width) {
+ shifty = this.height;
+ } else {
+ shiftx = this.width;
+ }
+ this.lineHeight = Math.max(this.lineHeight, that.lineHeight);
+ this.width += shiftx;
+ this.height += shifty;
+ this.characters.put(style, charset);
+
+ for (Entry<BitmapCharacter> charEntry : charset) {
+ BitmapCharacter ch = charEntry.getValue();
+ ch.setX(ch.getX()+shiftx);
+ ch.setY(ch.getY()+shifty);
+ }
+ }
+ }
+
+ public void setStyle(int style) {
+ if (characters.size() > 1) {
+ throw new RuntimeException("Applicable only for single style font");
+ }
+ Entry<IntMap<BitmapCharacter>> entry = characters.iterator().next();
+ IntMap<BitmapCharacter> charset = entry.getValue();
+ characters.remove(entry.getKey());
+ characters.put(style, charset);
+ }
}
No newline at end of file
Index: src/core/com/jme3/font/Letters.java
===================================================================
--- src/core/com/jme3/font/Letters.java (revision 0)
+++ src/core/com/jme3/font/Letters.java (revision 0)
@@ -0,0 +1,278 @@
+package com.jme3.font;
+
+import com.jme3.font.BitmapFont.Align;
+import com.jme3.math.ColorRGBA;
+
+class Letters {
+ private static final char ellipsisChar = 0x2026;
+ private final BitmapCharacter ellipsis;
+ private final float ellipsisWidth;
+ private final LetterQuad head;
+ private final LetterQuad tail;
+ private final BitmapFont font;
+ private LetterQuad current;
+ private StringBlock block;
+ private float totalWidth;
+ private float totalHeight;
+ private int lineCount;
+
+ Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
+ final String text = bound.getText();
+ this.block = bound;
+ this.font = font;
+ head = new LetterQuad(font, rightToLeft);
+ tail = new LetterQuad(font, rightToLeft);
+ setText(text);
+ ellipsis = font.getCharSet().getCharacter(ellipsisChar);
+ ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;
+ }
+
+ void setText(final CharSequence text) {
+ head.setNext(tail);
+ tail.setPrevious(head);
+ current = head;
+ if (text != null && text.length() > 0) {
+ LetterQuad l = head;
+ for (int i = 0; i < text.length(); i++) {
+ l = l.addNextCharacter(text.charAt(i));
+ }
+ }
+ }
+
+ LetterQuad getHead() {
+ return head;
+ }
+
+ LetterQuad getTail() {
+ return tail;
+ }
+
+ void update() {
+ LetterQuad l = head;
+ lineCount = 1;
+ while (!l.isTail()) {
+ if (l.isInvalid()) {
+ l.update(block);
+
+ if (l.isInvalid(block)) {
+ switch (block.getLineWrapType()) {
+ case CHARACTER:
+ lineWrap(l);
+ break;
+ case WORD:
+ if (!l.isBlank()) {
+ // search last blank character before this word
+ LetterQuad blank = l;
+ while (!blank.isBlank()) {
+ if (blank.isLineStart() || blank.isHead()) {
+ lineWrap(l);
+ blank = null;
+ break;
+ }
+ blank = blank.getPrevious();
+ }
+ if (blank != null) {
+ blank.setEndOfLine();
+ while (blank != l) {
+ blank = blank.getNext();
+ blank.invalidate();
+ blank.update(block);
+ }
+ }
+ }
+ break;
+ case ELLIPSIS:
+ // search last blank character before this word
+ LetterQuad cursor = l.getPrevious();
+ while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
+ cursor = cursor.getPrevious();
+ }
+ cursor.setBitmapChar(ellipsis);
+ cursor = cursor.getNext();
+ while (!cursor.isTail() && !cursor.isLineFeed()) {
+ cursor.setBitmapChar(null);
+ cursor = cursor.getNext();
+ }
+ break;
+ }
+ }
+ } else if (current.isInvalid(block)) {
+ invalidate(current);
+ }
+ if (l.isEndOfLine()) {
+ lineCount++;
+ }
+ l = l.getNext();
+ }
+
+ align(block.getAlignment());
+ rewind();
+ }
+
+ private void align(Align alignment) {
+ if (block.getTextBox() == null || alignment == Align.Left)
+ return;
+ LetterQuad cursor = tail.getPrevious();
+ cursor.setEndOfLine();
+ final float width = block.getTextBox().width;
+ float lineWidth = 0;
+ float gapX = 0;
+ float gapY = 0;
+ while (!cursor.isHead()) {
+ if (cursor.isEndOfLine()) {
+ if (cursor.isLineFeed()) {
+ lineWidth = cursor.getPrevious().getX1()-block.getTextBox().x;
+ } else {
+ lineWidth = cursor.getX1()-block.getTextBox().x;
+ }
+ if (alignment == Align.Center) {
+ gapX = (width-lineWidth)/2;
+ } else if (alignment == Align.Right) {
+ gapX = width-lineWidth;
+ } else {
+ gapX = 0;
+ }
+ }
+ cursor.setAlignment(gapX, gapY);
+ cursor = cursor.getPrevious();
+ }
+ }
+
+ private void lineWrap(LetterQuad l) {
+ if (l.isHead() || l.isBlank())
+ return;
+ l.getPrevious().setEndOfLine();
+ l.invalidate();
+ l.update(block);
+ }
+
+ float getX0() {
+ return current.getX0();
+ }
+
+ float getY0() {
+ return current.getY0();
+ }
+
+ float getX1() {
+ return current.getX1();
+ }
+
+ float getY1() {
+ return current.getY1();
+ }
+
+ float getAlignX() {
+ return current.getAlignX();
+ }
+
+ float getAlignY() {
+ return current.getAlignY();
+ }
+
+ float getWidth() {
+ return current.getWidth();
+ }
+
+ float getHeight() {
+ return current.getHeight();
+ }
+
+ int getLineCount() {
+ return lineCount;
+ }
+
+ public boolean nextCharacter() {
+ if (current.isTail())
+ return false;
+ current = current.getNext();
+ return true;
+ }
+
+ public int getPage() {
+ return current.getBitmapChar().getPage();
+ }
+
+ public LetterQuad getQuad() {
+ return current;
+ }
+
+ public void rewind() {
+ current = head;
+ }
+
+ public void invalidate() {
+ invalidate(head);
+ }
+
+ public void invalidate(LetterQuad cursor) {
+ totalWidth = -1;
+ totalHeight = -1;
+ lineCount = 0;
+
+ while (!cursor.isTail() && !cursor.isInvalid()) {
+ cursor.invalidate();
+ cursor = cursor.getNext();
+ }
+ }
+
+ float getScale() {
+ return block.getSize() / font.getCharSet().getRenderedSize();
+ }
+
+ public boolean isPrintable() {
+ return current.getBitmapChar() != null;
+ }
+
+ float getTotalWidth() {
+ validateSize();
+ return totalWidth;
+ }
+
+ float getTotalHeight() {
+ validateSize();
+ return totalHeight;
+ }
+
+ void validateSize() {
+ if (totalWidth < 0) {
+ LetterQuad l = head;
+ while (!l.isTail()) {
+ totalWidth = Math.max(totalWidth, l.getX1());
+ l = l.getNext();
+ totalHeight = Math.max(totalHeight, l.getY1());
+ }
+ }
+ }
+
+ /**
+ * @param start start index to set style. inclusive.
+ * @param end end index to set style. EXCLUSIVE.
+ * @param style
+ */
+ void setStyle(int start, int end, int style) {
+ LetterQuad cursor = head.getNext();
+ while (!cursor.isTail()) {
+ if (cursor.getIndex() >= start && cursor.getIndex() < end) {
+ cursor.setStyle(style);
+ }
+ cursor = cursor.getNext();
+ }
+ }
+
+ /**
+ * @param start start index to set style. inclusive.
+ * @param end end index to set style. EXCLUSIVE.
+ * @param color
+ */
+ void setColor(int start, int end, ColorRGBA color) {
+ LetterQuad cursor = head.getNext();
+ while (!cursor.isTail()) {
+ if (cursor.getIndex() >= start && cursor.getIndex() < end) {
+ cursor.setColor(color);
+ }
+ cursor = cursor.getNext();
+ }
+ }
+
+}
No newline at end of file
Index: src/core/com/jme3/font/StringBlock.java
===================================================================
--- src/core/com/jme3/font/StringBlock.java (revision 6754)
+++ src/core/com/jme3/font/StringBlock.java (working copy)
@@ -45,11 +45,12 @@
private StringBuilder text;
private Rectangle textBox;
- private BitmapFont.Align alignment;
+ private BitmapFont.Align alignment = Align.Left;
private float size;
private ColorRGBA color = new ColorRGBA(ColorRGBA.White);
private boolean kerning;
private int lineCount;
+ private LineWrapType wrapType = LineWrapType.WORD;
/**
*
@@ -156,5 +157,17 @@
void setLineCount(int lineCount) {
this.lineCount = lineCount;
}
+
+ public LineWrapType getLineWrapType() {
+ return wrapType;
+ }
+
+ /**
+ * available only when bounding is set. <code>setBox()</code> method call is needed in advance.
+ * @param wrap true when word need not be split at the end of the line.
+ */
+ public void setLineWrapType(LineWrapType wrap) {
+ this.wrapType = wrap;
+ }
}
No newline at end of file
Index: src/core/com/jme3/font/BitmapText.java
===================================================================
--- src/core/com/jme3/font/BitmapText.java (revision 6754)
+++ src/core/com/jme3/font/BitmapText.java (working copy)
@@ -31,6 +31,11 @@
*/
package com.jme3.font;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.RenderManager;
@@ -43,10 +48,9 @@
private StringBlock block;
private float lineWidth = 0f;
private boolean needRefresh = true;
- private boolean rightToLeft = false;
- private boolean wordWrap = true;
private final BitmapTextPage[] textPages;
private QuadList quadList = new QuadList();
+ private Letters letters;
public BitmapText(BitmapFont font) {
this(font, false, false);
@@ -68,6 +72,7 @@
this.font = font;
this.block = new StringBlock();
block.setSize(font.getPreferredSize());
+ letters = new Letters(font, block, rightToLeft);
}
@Override
@@ -92,6 +97,7 @@
public void setSize(float size) {
block.setSize(size);
needRefresh = true;
+ letters.invalidate();
}
/**
@@ -104,6 +110,7 @@
}
block.setText(text);
+ letters.setText(text);
needRefresh = true;
}
@@ -122,15 +129,12 @@
}
/**
- * changes text color
+ * changes text color. all substring colors are deleted.
* @param color new color of text
*/
public void setColor(ColorRGBA color) {
- if (block.getColor().equals(color)) {
- return;
- }
-
- block.setColor(color);
+ letters.setColor(0, block.getText().length(), color);
+ letters.invalidate(); // TODO: Don't have to align.
needRefresh = true;
}
@@ -140,6 +144,7 @@
*/
public void setBox(Rectangle rect) {
block.setTextBox(rect);
+ letters.invalidate();
needRefresh = true;
}
@@ -157,7 +162,7 @@
if (needRefresh) {
assemble();
}
- float height = getLineHeight()*block.getLineCount();
+ float height = getLineHeight()*letters.getLineCount();
Rectangle textBox = block.getTextBox();
if (textBox != null) {
return Math.max(height, textBox.height);
@@ -175,22 +180,77 @@
return lineWidth;
}
- public boolean isWordWrap() {
- return wordWrap;
+ public LineWrapType getLineWrap() {
+ return block.getLineWrapType();
}
public void setAlignment(BitmapFont.Align align) {
block.setAlignment(align);
+ letters.invalidate();
+ needRefresh = true;
}
/**
- * available only when bounding is set. <code>setBox()</code> method call is needed in advance.
- * @param wordWrap true when word need not be split at the end of the line.
+ * Set the font style of substring. If font doesn't contain style, default style is used
+ * @param start start index to set style. inclusive.
+ * @param end end index to set style. EXCLUSIVE.
+ * @param style
+ */
+ public void setStyle(int start, int end, int style) {
+ letters.setStyle(start, end, style);
+ }
+
+ /**
+ * Set the font style of substring. If font doesn't contain style, default style is used
+ * @param regexp regular expression
+ * @param style
+ */
+ public void setStyle(String regexp, int style) {
+ Pattern p = Pattern.compile(regexp);
+ Matcher m = p.matcher(block.getText());
+ while (m.find()) {
+ setStyle(m.start(), m.end(), style);
+ }
+ }
+
+ /**
+ * Set the color of substring.
+ * @param start start index to set style. inclusive.
+ * @param end end index to set style. EXCLUSIVE.
+ * @param color
*/
- public void setWordWrap(boolean wordWrap) {
- this.wordWrap = wordWrap;
+ public void setColor(int start, int end, ColorRGBA color) {
+ letters.setColor(start, end, color);
+ letters.invalidate();
needRefresh = true;
}
+
+ /**
+ * Set the color of substring.
+ * @param regexp regular expression
+ * @param color
+ */
+ public void setColor(String regexp, ColorRGBA color) {
+ Pattern p = Pattern.compile(regexp);
+ Matcher m = p.matcher(block.getText());
+ while (m.find()) {
+ letters.setColor(m.start(), m.end(), color);
+ }
+ letters.invalidate();
+ needRefresh = true;
+ }
+
+ /**
+ * available only when bounding is set. <code>setBox()</code> method call is needed in advance.
+ * @param wrap true when word need not be split at the end of the line.
+ */
+ public void setLineWrap(LineWrapType wrap) {
+ if (block.getLineWrapType() != wrap) {
+ block.setLineWrapType(wrap);
+ letters.invalidate();
+ needRefresh = true;
+ }
+ }
public BitmapFont.Align getAlignment() {
return block.getAlignment();
@@ -206,15 +266,10 @@
private void assemble() {
// first generate quadlist
- quadList.clear();
- if (block.getTextBox() == null) {
- lineWidth = font.updateText(block, quadList, rightToLeft);
- } else {
- lineWidth = font.updateTextRect(block, quadList, wordWrap);
- }
+ letters.update();
for (int i = 0; i < textPages.length; i++) {
- textPages.assemble(quadList);
+ textPages.assemble(letters);
}
needRefresh = false;
}
@@ -226,4 +281,22 @@
mat.render(page, rm);
}
}
+
+ public QuadList getQuadList() {
+ return quadList;
+ }
+
+ @Deprecated
+ public void setWordWrap(boolean wrap) {
+ if (wrap) {
+ setLineWrap(LineWrapType.WORD);
+ } else {
+ setLineWrap(LineWrapType.CHARACTER);
+ }
+ }
+
+ @Deprecated
+ public boolean isWordWrap() {
+ return isWordWrap();
+ }
}
Index: src/core/com/jme3/font/FontQuad.java
===================================================================
--- src/core/com/jme3/font/FontQuad.java (revision 6754)
+++ src/core/com/jme3/font/FontQuad.java (working copy)
@@ -227,6 +227,10 @@
public void setWordNumber(int number) {
wordNumber = number;
}
+
+ public float getCharacterWidth() {
+ return quadPosWidth;
+ }
public float getSizeScale() {
return sizeScale;
Index: src/core/com/jme3/font/BitmapFont.java
===================================================================
--- src/core/com/jme3/font/BitmapFont.java (revision 6754)
+++ src/core/com/jme3/font/BitmapFont.java (working copy)
@@ -39,6 +39,10 @@
import com.jme3.export.Savable;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
+import com.jme3.shader.VarType;
+import com.jme3.texture.Texture;
+
+import java.awt.Font;
import java.io.IOException;
/**
@@ -47,6 +51,9 @@
*/
public class BitmapFont implements Savable {
+ private static final char ELLIPSIS = '.';
+ private static final int ELLIPSIS_LENGTH = 3;
+
public enum Align {
Left, Center, Right
}
@@ -328,8 +335,10 @@
return Math.max(lineWidth, maxLineWidth);
}
- float updateTextRect(StringBlock b, QuadList target, boolean wordWrap) {
+ float updateTextRect(StringBlock b, QuadList target) {
+ final LineWrapType wrapType = b.getLineWrapType();
+ final int ellipsisWidth = charSet.getCharacter(ELLIPSIS).getWidth()*ELLIPSIS_LENGTH;
String text = b.getText();
float x = b.getTextBox().x;
float y = b.getTextBox().y;
@@ -345,6 +354,7 @@
boolean firstCharOfLine = true;
boolean useKerning = b.isKerning();
Align alignment = b.getAlignment();
+ int ellipsisCount = 0;
target.clear();
@@ -362,6 +372,7 @@
wordNumber = 1;
lineNumber++;
+ ellipsisCount = 0;
}else if (c == null){
System.out.println("Character '" + text.charAt(i) + "' is not in alphabet, skipping it.");
}else{
@@ -373,10 +384,10 @@
// Newline
if (lineWidth + xAdvance >= maxWidth){
- x = b.getTextBox().x;
- y -= charSet.getLineHeight() * sizeScale;
// float offset = 0f;
- if ((lineWidth + xAdvance >= maxWidth) && (wordNumber != 1) && wordWrap){
+ if ((lineWidth + xAdvance >= maxWidth) && (wordNumber != 1) && wrapType==LineWrapType.WORD){
+ x = b.getTextBox().x;
+ y -= charSet.getLineHeight() * sizeScale;
// Next character extends past text box width
// We have to move the last word down one line
char newLineLastChar = 0;
@@ -389,15 +400,15 @@
FontQuad q = target.getQuad(j);
BitmapCharacter localChar = q.getBitmapChar();
- float localxOffset = localChar.getXOffset() * sizeScale;
- float localyOffset = localChar.getYOffset() * sizeScale;
- float localxAdvance = localChar.getXAdvance() * sizeScale;
-
// Move current word to the left side of the text box
if ((q.getLineNumber() == lineNumber) && (q.getWordNumber() == wordNumber)){
if (alignment == Align.Left && q.getCharacter() == ' '){
continue;
}
+ float localxOffset = localChar.getXOffset() * sizeScale;
+ float localyOffset = localChar.getYOffset() * sizeScale;
+ float localxAdvance = localChar.getXAdvance() * sizeScale;
+
q.setLineNumber(q.getLineNumber() + 1);
q.setWordNumber(1);
float quadPosX = x + localxOffset;
@@ -435,18 +446,85 @@
}
}
}
+ wordNumber = 1;
+ lineNumber++;
+ } else if ((lineWidth + xAdvance >= maxWidth-ellipsisWidth) && wrapType==LineWrapType.ELLIPSIS){
+ // Next character extends past text box width
+ // We have to move the last word down one line
+ char newLineLastChar = 0;
+ maxLineWidth = Math.max(lineWidth, maxLineWidth);
+ lastLineWidth = lineWidth;
- }else{
+ for (int j = 0; j < target.getActualSize(); j++){
+ FontQuad q = target.getQuad(j);
+ BitmapCharacter localChar = q.getBitmapChar();
+
+ // Move current word to the left side of the text box
+ if ((q.getLineNumber() == lineNumber) && (q.getX()+q.getCharacterWidth() > maxWidth-ellipsisWidth)){
+// if (alignment == Align.Left && q.getCharacter() == ' '){
+// continue;
+// }
+ if (ellipsisCount < ELLIPSIS_LENGTH) {
+ localChar = charSet.getCharacter(ELLIPSIS);
+ setQuadCharacter(q, ELLIPSIS, sizeScale);
+ float localxOffset = localChar.getXOffset() * sizeScale;
+ float localxAdvance = localChar.getXAdvance() * sizeScale;
+
+ float quadPosX = q.getX() + localxOffset;
+ q.setPosition(quadPosX, y);
+
+ x = quadPosX + localxAdvance;
+ lastLineWidth = x;
+ lineWidth += localxAdvance;
+ int amount = findKerningAmount(newLineLastChar, q.getCharacter());
+ if (amount != -1 && useKerning){
+ x += amount * sizeScale;
+ lineWidth += amount * sizeScale;
+ }
+ ellipsisCount++;
+ xOffset = localChar.getXOffset() * sizeScale;
+ yOffset = localChar.getYOffset() * sizeScale;
+ xAdvance = localChar.getXAdvance() * sizeScale;
+ width = localChar.getWidth() * sizeScale;
+ height = localChar.getHeight() * sizeScale;
+ } else {
+ q.setSize(0, 0);
+ }
+ }
+
+ newLineLastChar = q.getCharacter();
+ }
+
+ // Justify the previous (now complete) line
+ if (alignment == Align.Center){
+ for (int k = 0; k < target.getQuantity(); k++){
+ FontQuad q = target.getQuad(k);
+
+ if (q.getLineNumber() == lineNumber){
+ q.setX(q.getX() + b.getTextBox().width / 2f - lastLineWidth / 2f);
+ }
+ }
+ }
+ if (alignment == Align.Right){
+ for (int k = 0; k < target.getQuantity(); k++){
+ FontQuad q = target.getQuad(k);
+ if (q.getLineNumber() == lineNumber){
+ q.setX(q.getX() + b.getTextBox().width - lastLineWidth);
+ }
+ }
+ }
+
+ } else{
+ x = b.getTextBox().x;
+ y -= charSet.getLineHeight() * sizeScale;
// New line without any "carry-down" word
firstCharOfLine = true;
maxLineWidth = Math.max(lineWidth, maxLineWidth);
lastLineWidth = lineWidth;
lineWidth = 0f;
+ wordNumber = 1;
+ lineNumber++;
}
-
- wordNumber = 1;
- lineNumber++;
-
} // End new line check
// Dont print these
@@ -478,16 +556,9 @@
float quadPosX = x + (xOffset);
float quadPosY = y - yOffset;
q.setPosition(quadPosX, quadPosY);
- q.setSize(width, height);
-
x += xAdvance;
lineWidth += xAdvance;
- float u0 = (float) c.getX() / charSet.getWidth();
- float v0 = (float) c.getY() / charSet.getHeight();
- float w = (float) c.getWidth() / charSet.getWidth();
- float h = (float) c.getHeight() / charSet.getHeight();
- q.setUV(u0, v0, w, h);
q.setColor(b.getColor());
q.setLineNumber(lineNumber);
@@ -498,9 +569,10 @@
q.setWordNumber(wordNumber);
wordWidth += xAdvance;
q.setWordWidth(wordWidth);
- q.setBitmapChar(c);
+// if (ellipsisCount == 0) {
+ setQuadCharacter(q, text.charAt(i), sizeScale);
+// }
q.setSizeScale(sizeScale);
- q.setCharacter(text.charAt(i));
lastChar = text.charAt(i);
@@ -529,4 +601,27 @@
return Math.max(maxLineWidth,lineWidth);
}
+ private void setQuadCharacter(FontQuad q, char ch, float sizeScale) {
+ BitmapCharacter c = charSet.getCharacter(ch);
+ q.setBitmapChar(c);
+ q.setCharacter(ch);
+ float u0 = (float) c.getX() / charSet.getWidth();
+ float v0 = (float) c.getY() / charSet.getHeight();
+ float w = (float) c.getWidth() / charSet.getWidth();
+ float h = (float) c.getHeight() / charSet.getHeight();
+ q.setUV(u0, v0, w, h);
+ q.setSize(c.getWidth()*sizeScale, c.getHeight()*sizeScale);
+ }
+
+ public void merge(BitmapFont font2, Texture[] fontTexture) {
+ charSet.merge(font2.charSet);
+ for (int i = 0; i < fontTexture.length; i++) {
+ getPage(i).setTextureParam("ColorMap", VarType.Texture2D, fontTexture);
+ }
+ }
+
+ public void setStyle(int style) {
+ charSet.setStyle(style);
+ }
+
}
No newline at end of file
Index: src/core/com/jme3/font/BitmapTextPage.java
===================================================================
--- src/core/com/jme3/font/BitmapTextPage.java (revision 6754)
+++ src/core/com/jme3/font/BitmapTextPage.java (working copy)
@@ -34,6 +34,7 @@
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
+import java.util.LinkedList;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
@@ -51,7 +52,7 @@
private final byte[] color;
private final int page;
private final Texture2D texture;
- private final QuadList quadList = new QuadList();
+ private final LinkedList<LetterQuad> pageQuads = new LinkedList<LetterQuad>();
BitmapTextPage(BitmapFont font, boolean arrayBased, int page) {
super("BitmapFont", new Mesh());
@@ -114,11 +115,19 @@
return clone;
}
- void assemble(QuadList quads) {
- quads.getPage(page, quadList);
+ void assemble(Letters quads) {
+ pageQuads.clear();
+ quads.rewind();
+ while (quads.nextCharacter()) {
+ if (quads.isPrintable()) {
+ if (quads.getPage() == page) {
+ pageQuads.add(quads.getQuad());
+ }
+ }
+ }
Mesh m = getMesh();
- m.setVertexCount(quadList.getQuantity() * 4);
- m.setTriangleCount(quadList.getQuantity() * 2);
+ m.setVertexCount(pageQuads.size() * 4);
+ m.setTriangleCount(pageQuads.size() * 2);
VertexBuffer pb = m.getBuffer(Type.Position);
VertexBuffer tb = m.getBuffer(Type.TexCoord);
@@ -153,8 +162,8 @@
// go for each quad and append it to the buffers
if (pos != null) {
- for (int i = 0; i < quadList.getActualSize(); i++) {
- FontQuad fq = quadList.getQuad(i);
+ for (int i = 0; i < pageQuads.size(); i++) {
+ LetterQuad fq = pageQuads.get(i);
fq.storeToArrays(pos, tc, idx, color, i);
fpb.put(pos);
ftb.put(tc);
@@ -162,8 +171,8 @@
bcb.put(color);
}
} else {
- for (int i = 0; i < quadList.getActualSize(); i++) {
- FontQuad fq = quadList.getQuad(i);
+ for (int i = 0; i < pageQuads.size(); i++) {
+ LetterQuad fq = pageQuads.get(i);
fq.appendPositions(fpb);
fq.appendTexCoords(ftb);
fq.appendIndices(sib, i);
[/patch]