1 package com.jme3.font; 2 3 import com.jme3.math.ColorRGBA; 4 import java.nio.ByteBuffer; 5 import java.nio.FloatBuffer; 6 import java.nio.ShortBuffer; 7 8 /** 9 * LetterQuad contains the position, color, uv texture information for a character in text. 10 * @author YongHoon 11 */ 12 class LetterQuad { 13 private static final Rectangle UNBOUNDED = new Rectangle(0, 0, Float.MAX_VALUE, Float.MAX_VALUE); 14 private static final float LINE_DIR = -1; 15 16 private final BitmapFont font; 17 private final char c; 18 private final int index; 19 private int style; 20 21 private BitmapCharacter bitmapChar = null; 22 private float x0 = Integer.MIN_VALUE; 23 private float y0 = Integer.MIN_VALUE; 24 private float width = Integer.MIN_VALUE; 25 private float height = Integer.MIN_VALUE; 26 private float xAdvance = 0; 27 private float u0; 28 private float v0; 29 private float u1; 30 private float v1; 31 private float lineY; 32 private boolean eol; 33 34 private LetterQuad previous; 35 private LetterQuad next; 36 private int colorInt = 0xFFFFFFFF; 37 38 private boolean rightToLeft; 39 private float alignX; 40 private float alignY; 41 private float sizeScale = 1; 42 43 /** 44 * create head / tail 45 * @param font 46 * @param rightToLeft 47 */ 48 protected LetterQuad(BitmapFont font, boolean rightToLeft) { 49 this.font = font; 50 this.c = Character.MIN_VALUE; 51 this.rightToLeft = rightToLeft; 52 this.index = -1; 53 setBitmapChar(null); 54 } 55 56 /** 57 * create letter and append to previous LetterQuad 58 * 59 * @param c 60 * @param prev previous character 61 */ 62 protected LetterQuad(char c, LetterQuad prev) { 63 this.font = prev.font; 64 this.rightToLeft = prev.rightToLeft; 65 this.c = c; 66 this.index = prev.index+1; 67 this.eol = isLineFeed(); 68 setBitmapChar(c); 69 prev.insert(this); 70 } 71 72 LetterQuad addNextCharacter(char c) { 73 LetterQuad n = new LetterQuad(c, this); 74 return n; 75 } 76 77 BitmapCharacter getBitmapChar() { 78 return bitmapChar; 79 } 80 81 char getChar() { 82 return c; 83 } 84 85 int getIndex() { 86 return index; 87 } 88 89 private Rectangle getBound(StringBlock block) { 90 if (block.getTextBox() != null) { 91 return block.getTextBox(); 92 } 93 return UNBOUNDED; 94 } 95 96 LetterQuad getPrevious() { 97 return previous; 98 } 99 100 LetterQuad getNext() { 101 return next; 102 } 103 104 public float getU0() { 105 return u0; 106 } 107 108 float getU1() { 109 return u1; 110 } 111 112 float getV0() { 113 return v0; 114 } 115 116 float getV1() { 117 return v1; 118 } 119 120 boolean isInvalid() { 121 return x0 == Integer.MIN_VALUE; 122 } 123 124 boolean isInvalid(StringBlock block) { 125 return isInvalid(block, 0); 126 } 127 128 boolean isInvalid(StringBlock block, float gap) { 129 if (isHead() || isTail()) 130 return false; 131 if (x0 == Integer.MIN_VALUE || y0 == Integer.MIN_VALUE) { 132 return true; 133 } 134 Rectangle bound = block.getTextBox(); 135 if (bound == null) { 136 return false; 137 } 138 return x0 > 0 && bound.x+bound.width-gap < getX1(); 139 } 140 141 float getX0() { 142 return x0; 143 } 144 145 float getX1() { 146 return x0+width; 147 } 148 149 float getNextX() { 150 return x0+xAdvance; 151 } 152 153 float getNextLine() { 154 return lineY+LINE_DIR*font.getCharSet().getLineHeight() * sizeScale; 155 } 156 157 float getY0() { 158 return y0; 159 } 160 161 float getY1() { 162 return y0-height; 163 } 164 165 float getWidth() { 166 return width; 167 } 168 169 float getHeight() { 170 return height; 171 } 172 173 void insert(LetterQuad ins) { 174 LetterQuad n = next; 175 next = ins; 176 ins.next = n; 177 ins.previous = this; 178 n.previous = ins; 179 } 180 181 void invalidate() { 182 eol = isLineFeed(); 183 setBitmapChar(font.getCharSet().getCharacter(c, style)); 184 } 185 186 boolean isTail() { 187 return next == null; 188 } 189 190 boolean isHead() { 191 return previous == null; 192 } 193 194 /** 195 * @return next letter 196 */ 197 LetterQuad remove() { 198 this.previous.next = next; 199 this.next.previous = previous; 200 return next; 201 } 202 203 void setPrevious(LetterQuad before) { 204 this.previous = before; 205 } 206 207 void setStyle(int style) { 208 this.style = style; 209 invalidate(); 210 } 211 212 void setColor(ColorRGBA color) { 213 this.colorInt = color.asIntRGBA(); 214 invalidate(); 215 } 216 217 void setBitmapChar(char c) { 218 BitmapCharacterSet charSet = font.getCharSet(); 219 BitmapCharacter bm = charSet.getCharacter(c, style); 220 setBitmapChar(bm); 221 } 222 223 void setBitmapChar(BitmapCharacter bitmapChar) { 224 x0 = Integer.MIN_VALUE; 225 y0 = Integer.MIN_VALUE; 226 width = Integer.MIN_VALUE; 227 height = Integer.MIN_VALUE; 228 alignX = 0; 229 alignY = 0; 230 231 BitmapCharacterSet charSet = font.getCharSet(); 232 this.bitmapChar = bitmapChar; 233 if (bitmapChar != null) { 234 u0 = (float) bitmapChar.getX() / charSet.getWidth(); 235 v0 = (float) bitmapChar.getY() / charSet.getHeight(); 236 u1 = u0 + (float) bitmapChar.getWidth() / charSet.getWidth(); 237 v1 = v0 + (float) bitmapChar.getHeight() / charSet.getHeight(); 238 } else { 239 u0 = 0; 240 v0 = 0; 241 u1 = 0; 242 v1 = 0; 243 } 244 } 245 246 void setNext(LetterQuad next) { 247 this.next = next; 248 } 249 250 void update(StringBlock block) { 251 final float[] tabs = block.getTabPosition(); 252 final float tabWidth = block.getTabWidth(); 253 final Rectangle bound = getBound(block); 254 sizeScale = block.getSize() / font.getCharSet().getRenderedSize(); 255 lineY = computeLineY(block); 256 257 if (isHead()) { 258 x0 = getBound(block).x; 259 y0 = lineY; 260 width = 0; 261 height = 0; 262 xAdvance = 0; 263 } else if (isTab()) { 264 x0 = previous.getNextX(); 265 width = tabWidth; 266 y0 = lineY; 267 height = 0; 268 if (tabs != null && x0 < tabs[tabs.length-1]) { 269 for (int i = 0; i < tabs.length-1; i++) { 270 if (x0 > tabs[i] && x0 < tabs[i+1]) { 271 width = tabs[i+1] - x0; 272 } 273 } 274 } 275 xAdvance = width; 276 } else if (bitmapChar == null) { 277 x0 = getPrevious().getX1(); 278 y0 = lineY; 279 width = 0; 280 height = 0; 281 xAdvance = 0; 282 } else { 283 float xOffset = bitmapChar.getXOffset() * sizeScale; 284 float yOffset = bitmapChar.getYOffset() * sizeScale; 285 xAdvance = bitmapChar.getXAdvance() * sizeScale; 286 width = bitmapChar.getWidth() * sizeScale; 287 height = bitmapChar.getHeight() * sizeScale; 288 float incrScale = rightToLeft ? -1f : 1f; 289 float kernAmount = 0f; 290 291 if (previous.isHead() || previous.eol) { 292 x0 = bound.x; 293 294 // The first letter quad will be drawn right at the first 295 // position... but it does not offset by the characters offset 296 // amount. This means that we've potentially accumulated extra 297 // pixels and the next letter won't get drawn far enough unless 298 // we add this offset back into xAdvance.. by subtracting it. 299 // This is the same thing that's done below because we've 300 // technically baked the offset in just like below. It doesn't 301 // look like it at first glance so I'm keeping it separate with 302 // this comment. 303 xAdvance -= xOffset * incrScale; 304 305 } else { 306 x0 = previous.getNextX() + xOffset * incrScale; 307 308 // Since x0 will have offset baked into it then we 309 // need to counteract that in xAdvance. This is better 310 // than removing it in getNextX() because we also need 311 // to take kerning into account below... which will also 312 // get baked in. 313 // Without this, getNextX() will return values too far to 314 // the left, for example. 315 xAdvance -= xOffset * incrScale; 316 } 317 y0 = lineY + LINE_DIR*yOffset; 318 319 // Adjust for kerning 320 BitmapCharacter lastChar = previous.getBitmapChar(); 321 if (lastChar != null && block.isKerning()) { 322 kernAmount = lastChar.getKerning(c) * sizeScale; 323 x0 += kernAmount * incrScale; 324 325 // Need to unbake the kerning from xAdvance since it 326 // is baked into x0... see above. 327 //xAdvance -= kernAmount * incrScale; 328 // No, kerning is an inter-character spacing and _does_ affect 329 // all subsequent cursor positions. 330 } 331 } 332 if (isEndOfLine()) { 333 xAdvance = bound.x-x0; 334 } 335 } 336 337 /** 338 * add temporary linewrap indicator 339 */ 340 void setEndOfLine() { 341 this.eol = true; 342 } 343 344 boolean isEndOfLine() { 345 return eol; 346 } 347 348 boolean isLineWrap() { 349 return !isHead() && !isTail() && bitmapChar == null && c == Character.MIN_VALUE; 350 } 351 352 private float computeLineY(StringBlock block) { 353 if (isHead()) { 354 return getBound(block).y; 355 } else if (previous.eol) { 356 return previous.getNextLine(); 357 } else { 358 return previous.lineY; 359 } 360 } 361 362 363 boolean isLineStart() { 364 return x0 == 0 || (previous != null && previous.eol); 365 } 366 367 boolean isBlank() { 368 return c == ' ' || isTab(); 369 } 370 371 public void storeToArrays(float[] pos, float[] tc, short[] idx, byte[] colors, int quadIdx){ 372 float x = x0+alignX; 373 float y = y0-alignY; 374 float xpw = x+width; 375 float ymh = y-height; 376 377 pos[0] = x; pos[1] = y; pos[2] = 0; 378 pos[3] = x; pos[4] = ymh; pos[5] = 0; 379 pos[6] = xpw; pos[7] = ymh; pos[8] = 0; 380 pos[9] = xpw; pos[10] = y; pos[11] = 0; 381 382 float v0 = 1f - this.v0; 383 float v1 = 1f - this.v1; 384 385 tc[0] = u0; tc[1] = v0; 386 tc[2] = u0; tc[3] = v1; 387 tc[4] = u1; tc[5] = v1; 388 tc[6] = u1; tc[7] = v0; 389 390 colors[3] = (byte) (colorInt & 0xff); 391 colors[2] = (byte) ((colorInt >> 8) & 0xff); 392 colors[1] = (byte) ((colorInt >> 16) & 0xff); 393 colors[0] = (byte) ((colorInt >> 24) & 0xff); 394 System.arraycopy(colors, 0, colors, 4, 4); 395 System.arraycopy(colors, 0, colors, 8, 4); 396 System.arraycopy(colors, 0, colors, 12, 4); 397 398 short i0 = (short) (quadIdx * 4); 399 short i1 = (short) (i0 + 1); 400 short i2 = (short) (i0 + 2); 401 short i3 = (short) (i0 + 3); 402 403 idx[0] = i0; idx[1] = i1; idx[2] = i2; 404 idx[3] = i0; idx[4] = i2; idx[5] = i3; 405 } 406 407 public void appendPositions(FloatBuffer fb){ 408 float sx = x0+alignX; 409 float sy = y0-alignY; 410 float ex = sx+width; 411 float ey = sy-height; 412 // NOTE: subtracting the height here 413 // because OGL's Ortho origin is at lower-left 414 fb.put(sx).put(sy).put(0f); 415 fb.put(sx).put(ey).put(0f); 416 fb.put(ex).put(ey).put(0f); 417 fb.put(ex).put(sy).put(0f); 418 } 419 420 public void appendPositions(ShortBuffer sb){ 421 final float x1 = getX1(); 422 final float y1 = getY1(); 423 short x = (short) x0; 424 short y = (short) y0; 425 short xpw = (short) (x1); 426 short ymh = (short) (y1); 427 428 sb.put(x).put(y).put((short)0); 429 sb.put(x).put(ymh).put((short)0); 430 sb.put(xpw).put(ymh).put((short)0); 431 sb.put(xpw).put(y).put((short)0); 432 } 433 434 public void appendTexCoords(FloatBuffer fb){ 435 // flip coords to be compatible with OGL 436 float v0 = 1 - this.v0; 437 float v1 = 1 - this.v1; 438 439 // upper left 440 fb.put(u0).put(v0); 441 // lower left 442 fb.put(u0).put(v1); 443 // lower right 444 fb.put(u1).put(v1); 445 // upper right 446 fb.put(u1).put(v0); 447 } 448 449 public void appendColors(ByteBuffer bb){ 450 bb.putInt(colorInt); 451 bb.putInt(colorInt); 452 bb.putInt(colorInt); 453 bb.putInt(colorInt); 454 } 455 456 public void appendIndices(ShortBuffer sb, int quadIndex){ 457 // each quad has 4 indices 458 short v0 = (short) (quadIndex * 4); 459 short v1 = (short) (v0 + 1); 460 short v2 = (short) (v0 + 2); 461 short v3 = (short) (v0 + 3); 462 463 sb.put(v0).put(v1).put(v2); 464 sb.put(v0).put(v2).put(v3); 465 // sb.put(new short[]{ v0, v1, v2, 466 // v0, v2, v3 }); 467 } 468 469 470 @Override 471 public String toString() { 472 return String.valueOf(c); 473 } 474 475 void setAlignment(float alignX, float alignY) { 476 this.alignX = alignX; 477 this.alignY = alignY; 478 } 479 480 float getAlignX() { 481 return alignX; 482 } 483 484 float getAlignY() { 485 return alignY; 486 } 487 488 boolean isLineFeed() { 489 return c == '\n'; 490 } 491 492 boolean isTab() { 493 return c == '\t'; 494 } 495 496 } 497