Importing a node containing BitmapText

I am having trouble importing a node containing a number of BitmapText objects.

The code used to generate them is below

[java]for (int i = 0; i < shipNM.getNumCells(); i++) {
Vector3f v0 = shipNM.getCell(i).getVertex(0);
Vector3f v1 = shipNM.getCell(i).getVertex(1);
Vector3f v2 = shipNM.getCell(i).getVertex(2);
Vector3f[][] vs = {{v0,v1}, {v2,v0}, {v1,v2}};

            guiFont = assetManager.loadFont(&quot;Interface/Fonts/Default.fnt&quot;);

            for (Vector3f[] vt : vs) {
                Mesh lineMesh = new Mesh();
                lineMesh.setMode(Mesh.Mode.Lines);
                lineMesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{vt[0].x, vt[0].y, vt[0].z, vt[1].x, vt[1].y, vt[1].z});
                lineMesh.setBuffer(VertexBuffer.Type.Index, 2, new short[]{0, 1});
                lineMesh.updateBound();
                lineMesh.updateCounts();
                Geometry lineGeometry = new Geometry(&quot;line&quot;, lineMesh);
                Material lineMaterial = new Material(assetManager, &quot;Common/MatDefs/Misc/Unshaded.j3md&quot;);
                lineMaterial.setColor(&quot;Color&quot;, ColorRGBA.White);
                lineGeometry.setMaterial(lineMaterial);
                nmH.attachChild(lineGeometry);

                BitmapText helloText = new BitmapText(guiFont, false);
                helloText.setSize(0.05f);
                helloText.setText(vt[0].toString());
                helloText.setLocalTranslation(vt[0].x, vt[0].y, vt[0].z);
                nmCH.attachChild(helloText);

            }
        }[/java] 

Both nmH and nmCH save absolutely fine to a .j3o, but only nmH will import again. Upon trying to import nmCH I get a
SEVERE: Could not access constructor of class 'com.jme3.font.BitmapText'!
Some types need to have the BinaryImporter set up in a special way. Please doublecheck the setup.

I have found this but since it is from over a year ago I was hoping the issue had been resolved? In which case some advice would be appreciated?
http://hub.jmonkeyengine.org/forum/topic/bitmaptext-saveread-issue/

Thanks

Hector

What version of jme are you using?

JME3 using the RC2 SDK
Fully updated using the IDE updater.

The full exceptions are about 100 of these (i presume one for each BitmapText item) -

[java]Feb 19, 2013 8:20:38 PM com.jme3.export.SavableClassUtil fromName
SEVERE: Could not access constructor of class ‘com.jme3.font.BitmapText’!
Some types need to have the BinaryImporter set up in a special way. Please doublecheck the setup.
Feb 19, 2013 8:20:38 PM class com.jme3.export.binary.BinaryImporter readObject(int id)
SEVERE: Exception
java.lang.InstantiationException: com.jme3.font.BitmapText
at java.lang.Class.newInstance0(Class.java:359)
at java.lang.Class.newInstance(Class.java:327)
at com.jme3.export.SavableClassUtil.fromName(SavableClassUtil.java:172)
at com.jme3.export.SavableClassUtil.fromName(SavableClassUtil.java:204)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:330)
at com.jme3.export.binary.BinaryInputCapsule.resolveIDs(BinaryInputCapsule.java:484)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArray(BinaryInputCapsule.java:472)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArrayList(BinaryInputCapsule.java:588)
at com.jme3.scene.Node.read(Node.java:598)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:341)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:243)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:126)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:110)
at com.jme3.asset.DesktopAssetManager.loadAsset(DesktopAssetManager.java:283)
at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:369)
at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:373)
at EvacSim.EvacSim.simpleInitApp(EvacSim.java:51)
at com.jme3.app.SimpleApplication.initialize(SimpleApplication.java:225)
at com.jme3.system.lwjgl.LwjglAbstractDisplay.initInThread(LwjglAbstractDisplay.java:130)
at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:207)
at java.lang.Thread.run(Thread.java:722)
[/java]

Looks like BitmapText is not set up for reading and writing through normal JME serialization. Must have been an oversight all this time… because I guess it has never worked.

1 Like

This seems like quite an oversight! Surely the ability to save text is an important one?

Any chance if it works in the nightly builds?

@hectorgrebbell said: This seems like quite an oversight! Surely the ability to save text is an important one?

Any chance if it works in the nightly builds?

No. I looked at the latest code. BitmapText does not implement support for saving/loading and it never has.

This is still an open issue…

I can’t remember how we worked round it, but i’ll take a look at the code later and see. I have a feeling we just saved what we could and generated the rest at runtime…

I created now a new class which can be loaded/saved correctly (first I tried to do the save functionallity in a subclass but it does not work with the given setters and getters). here it is, if you want to modify the existing BitmapText do so:

[java]
package com.jme3.font;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.font.BitmapFont.Align;
import com.jme3.font.BitmapFont.VAlign;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Node;

/**

  • @author YongHoon
    */
    public class BitmapTextSavable extends Node {

    private BitmapFont font;
    private StringBlock block;
    private boolean needRefresh = true;
    private BitmapTextPage[] textPages;
    private Letters letters;
    private Rectangle box;

    public BitmapTextSavable() {
    }

    public BitmapTextSavable(BitmapFont font) {
    this(font, false, false);
    }

    public BitmapTextSavable(BitmapFont font, boolean rightToLeft) {
    this(font, rightToLeft, false);
    }

    public BitmapTextSavable(BitmapFont font, boolean rightToLeft,
    boolean arrayBased) {
    setFont(font, rightToLeft, arrayBased);
    }

    public void setFont(BitmapFont font, boolean rightToLeft, boolean arrayBased) {
    textPages = new BitmapTextPage[font.getPageSize()];
    for (int page = 0; page < textPages.length; page++) {
    textPages[page] = new BitmapTextPage(font, arrayBased, page);
    attachChild(textPages[page]);
    }

     this.font = font;
     this.block = new StringBlock();
     block.setSize(font.getPreferredSize());
     letters = new Letters(font, block, rightToLeft);
    

    }

    @Override
    public void read(JmeImporter e) throws IOException {
    super.read(e);
    InputCapsule capsule = e.getCapsule(this);
    setFont((BitmapFont) capsule.readSavable(“font”, null));
    setText(capsule.readString(“text”, “”));
    setAlpha(capsule.readFloat(“alpha”, 1));
    setColor((ColorRGBA) capsule.readSavable(“color”, null));
    setSize(capsule.readFloat(“size”, 1));
    float x = capsule.readFloat(“boxx”, -1);
    float y = capsule.readFloat(“boxy”, -1);
    float w = capsule.readFloat(“boxw”, -1);
    float h = capsule.readFloat(“boxh”, -1);
    if (x != -1) {
    setBox(new Rectangle(x, y, w, h));
    }
    switch (capsule.readInt(“alignment”, 0)) {
    case 1:
    setAlignment(Align.Left);
    break;
    case 2:
    setAlignment(Align.Center);
    break;
    case 3:
    setAlignment(Align.Right);
    break;
    }

    }

    @Override
    public void write(JmeExporter e) throws IOException {
    for (BitmapTextPage t : textPages) {
    detachChild(t);
    // exclude the textpages from the saving process
    }
    super.write(e);
    for (BitmapTextPage t : textPages) {
    attachChild(t);
    }

     OutputCapsule capsule = e.getCapsule(this);
     capsule.write(font, "font", null);
     capsule.write(getText(), "text", null);
     capsule.write(getAlpha(), "alpha", 1);
     capsule.write(getColor(), "color", null);
     capsule.write(getSize(), "size", 1);
     if (box != null) {
     	capsule.write(box.x, "boxx", -2);
     	capsule.write(box.y, "boxy", -2);
     	capsule.write(box.width, "boxw", -2);
     	capsule.write(box.height, "boxh", -2);
     }
     switch (getAlignment()) {
     case Left:
     	capsule.write(1, "alignment", 0);
     	break;
     case Center:
     	capsule.write(2, "alignment", 0);
     	break;
     case Right:
     	capsule.write(3, "alignment", 0);
     	break;
     }
    

    }

    private void setFont(BitmapFont font) {
    setFont(font, false, false);
    }

    @Override
    public BitmapTextSavable clone() {
    BitmapTextSavable clone = (BitmapTextSavable) super.clone();
    for (int i = 0; i < textPages.length; i++) {
    clone.textPages[i] = textPages[i].clone();
    }
    clone.block = block.clone();
    clone.needRefresh = true;
    return clone;
    }

    public BitmapFont getFont() {
    return font;
    }

    /**

    • Changes text size
    • @param size
    •        text size
      

    */
    public void setSize(float size) {
    block.setSize(size);
    needRefresh = true;
    letters.invalidate();
    }

    public float getSize() {
    return block.getSize();
    }

    /**

    • @param text
    •        charsequence to change text to
      

    */
    public void setText(CharSequence text) {
    // note: text.toString() is free if text is already a java.lang.String.
    setText(text != null ? text.toString() : null);
    }

    /**

    • @param text
    •        String to change text to
      

    */
    public void setText(String text) {
    text = text == null ? “” : text;

     if (text == block.getText() || block.getText().equals(text)) {
     	return;
     }
    
     /*
      * The problem with the below block is that StringBlock carries pretty
      * much all of the text-related state of the BitmapText such as size,
      * text box, alignment, etc.
      * 
      * I'm not sure why this change was needed and the commit message was
      * not entirely helpful because it purports to fix a problem that I've
      * never encountered.
      * 
      * If block.setText("") doesn't do the right thing then that's where the
      * fix should go because StringBlock carries too much information to be
      * blown away every time. -pspeed
      * 
      * Change was made:
      * http://code.google.com/p/jmonkeyengine/source/detail?
      * spec=svn9389&amp;r=9389 Diff:
      * http://code.google.com/p/jmonkeyengine/source
      * /diff?path=/trunk/engine/
      * src/core/com/jme3/font/BitmapText.java&amp;format
      * =side&amp;r=9389&amp;old_path=/trunk
      * /engine/src/core/com/jme3/font/BitmapText.java&amp;old=8843
      * 
      * // If the text is empty, reset if (text.isEmpty()) {
      * detachAllChildren();
      * 
      * for (int page = 0; page &lt; textPages.length; page++) { textPages[page]
      * = new BitmapTextPage(font, true, page); attachChild(textPages[page]);
      * }
      * 
      * block = new StringBlock(); letters = new Letters(font, block,
      * letters.getQuad().isRightToLeft()); }
      */
    
     // Update the text content
     block.setText(text);
     letters.setText(text);
    
     // Flag for refresh
     needRefresh = true;
    

    }

    /**

    • @return returns text
      */
      public String getText() {
      return block.getText();
      }

    /**

    • @return color of the text
      */
      public ColorRGBA getColor() {
      return letters.getBaseColor();
      }

    /**

    • changes text color. all substring colors are deleted.
    • @param color
    •        new color of text
      

    */
    public void setColor(ColorRGBA color) {
    letters.setColor(color);
    letters.invalidate(); // TODO: Don’t have to align.
    needRefresh = true;
    }

    /**

    • Sets an overall alpha that will be applied to all letters. If the alpha
    • passed is -1 then alpha reverts to default… which will be 1 for
    • anything unspecified and color tags will be reset to 1 or their encoded
    • alpha.
      */
      public void setAlpha(float alpha) {
      letters.setBaseAlpha(alpha);
      needRefresh = true;
      }

    public float getAlpha() {
    return letters.getBaseAlpha();
    }

    /**

    • Define area where bitmaptext will be rendered
    • @param rect
    •        position and size box where text is rendered
      

    */
    public void setBox(Rectangle rect) {
    this.box = rect;
    block.setTextBox(rect);
    letters.invalidate();
    needRefresh = true;
    }

    /**

    • @return height of the line
      */
      public float getLineHeight() {
      return font.getLineHeight(block);
      }

    /**

    • @return height of whole textblock
      */
      public float getHeight() {
      if (needRefresh) {
      assemble();
      }
      float height = getLineHeight() * block.getLineCount();
      Rectangle textBox = block.getTextBox();
      if (textBox != null) {
      return Math.max(height, textBox.height);
      }
      return height;
      }

    /**

    • @return width of line
      */
      public float getLineWidth() {
      if (needRefresh) {
      assemble();
      }
      Rectangle textBox = block.getTextBox();
      if (textBox != null) {
      return Math.max(letters.getTotalWidth(), textBox.width);
      }
      return letters.getTotalWidth();
      }

    /**

    • @return line count
      */
      public int getLineCount() {
      if (needRefresh) {
      assemble();
      }
      return block.getLineCount();
      }

    public LineWrapMode getLineWrapMode() {
    return block.getLineWrapMode();
    }

    /**

    • Set horizontal alignment. Applicable only when text bound is set.
    • @param align
      */
      public void setAlignment(BitmapFont.Align align) {
      if (block.getTextBox() == null && align != Align.Left) {
      throw new RuntimeException(“Bound is not set”);
      }
      block.setAlignment(align);
      letters.invalidate();
      needRefresh = true;
      }

    /**

    • Set vertical alignment. Applicable only when text bound is set.
    • @param align
      */
      public void setVerticalAlignment(BitmapFont.VAlign align) {
      if (block.getTextBox() == null && align != VAlign.Top) {
      throw new RuntimeException(“Bound is not set”);
      }
      block.setVerticalAlignment(align);
      letters.invalidate();
      needRefresh = true;
      }

    public BitmapFont.Align getAlignment() {
    return block.getAlignment();
    }

    public BitmapFont.VAlign getVerticalAlignment() {
    return block.getVerticalAlignment();
    }

    /**

    • 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 applied
    • @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 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;
      }

    /**

    • @param tabs
    •        tab positions
      

    */
    public void setTabPosition(float… tabs) {
    block.setTabPosition(tabs);
    letters.invalidate();
    needRefresh = false;
    }

    /**

    • used for the tabs over the last tab position.
    • @param width
    •        tab size
      

    */
    public void setTabWidth(float width) {
    block.setTabWidth(width);
    letters.invalidate();
    needRefresh = false;
    }

    /**

    • for setLineWrapType(LineWrapType.NoWrap), set the last character when the
    • text exceeds the bound.
    • @param c
      */
      public void setEllipsisChar(char c) {
      block.setEllipsisChar©;
      letters.invalidate();
      needRefresh = false;
      }

    /**

    • Available only when bounding is set. setBox() method call is
    • needed in advance. true when
    • @param wrap
    •        NoWrap : Letters over the text bound is not shown. the last
      
    •        character is set to '...'(0x2026) Character: Character is
      
    •        split at the end of the line. Word : Word is split at the end
      
    •        of the line. Clip : The text is hard-clipped at the border
      
    •        including showing only a partial letter if it goes beyond the
      
    •        text bound.
      

    */
    public void setLineWrapMode(LineWrapMode wrap) {
    if (block.getLineWrapMode() != wrap) {
    block.setLineWrapMode(wrap);
    letters.invalidate();
    needRefresh = true;
    }
    }

    @Override
    public void updateLogicalState(float tpf) {
    super.updateLogicalState(tpf);
    if (needRefresh) {
    assemble();
    }
    }

    private void assemble() {
    // first generate quadlist
    letters.update();
    for (int i = 0; i < textPages.length; i++) {
    textPages[i].assemble(letters);
    }
    needRefresh = false;
    }

    private ColorRGBA getColor(Material mat, String name) {
    MatParam mp = mat.getParam(name);
    if (mp == null) {
    return null;
    }
    return (ColorRGBA) mp.getValue();
    }

    public void render(RenderManager rm, ColorRGBA color) {
    for (BitmapTextPage page : textPages) {
    Material mat = page.getMaterial();
    mat.setTexture(“ColorMap”, page.getTexture());
    // ColorRGBA original = getColor(mat, “Color”);
    // mat.setColor(“Color”, color);
    mat.render(page, rm);

     	// if( original == null ) {
     	// mat.clearParam("Color");
     	// } else {
     	// mat.setColor("Color", original);
     	// }
     }
    

    }
    }
    [/java]

I get When i use the code above:

[java]
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:526)[/java]

Caused by: java.lang.NullPointerException
at com.jme3.font.ColorTags.setBaseColor(ColorTags.java:121)
at com.jme3.font.Letters.setColor(Letters.java:352)
at com.jme3.font.BitmapText.setColor(BitmapText.java:216)
at com.jme3.font.BitmapText.read(BitmapText.java:69)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:344)
at com.jme3.export.binary.BinaryInputCapsule.resolveIDs(BinaryInputCapsule.java:483)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArray(BinaryInputCapsule.java:471)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArrayList(BinaryInputCapsule.java:587)
at com.jme3.scene.Node.read(Node.java:599)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:344)
at com.jme3.export.binary.BinaryInputCapsule.resolveIDs(BinaryInputCapsule.java:483)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArray(BinaryInputCapsule.java:471)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArrayList(BinaryInputCapsule.java:587)
at com.jme3.scene.Node.read(Node.java:599)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:344)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:242)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:125)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:109)
at com.jme3.asset.DesktopAssetManager.loadAsset(DesktopAssetManager.java:288)
at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:374)
at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:378)

I dont think setColor should set a null color >>>>>>>

[java]Caused by: java.lang.NullPointerException
at com.jme3.font.ColorTags.setBaseColor(ColorTags.java:121)
at com.jme3.font.Letters.setColor(Letters.java:352)
at com.jme3.font.BitmapText.setColor(BitmapText.java:216)
at com.jme3.font.BitmapText.read(BitmapText.java:69)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:344)
at com.jme3.export.binary.BinaryInputCapsule.resolveIDs(BinaryInputCapsule.java:483)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArray(BinaryInputCapsule.java:471)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArrayList(BinaryInputCapsule.java:587)
at com.jme3.scene.Node.read(Node.java:599)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:344)
at com.jme3.export.binary.BinaryInputCapsule.resolveIDs(BinaryInputCapsule.java:483)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArray(BinaryInputCapsule.java:471)
at com.jme3.export.binary.BinaryInputCapsule.readSavableArrayList(BinaryInputCapsule.java:587)
at com.jme3.scene.Node.read(Node.java:599)
at com.jme3.export.binary.BinaryImporter.readObject(BinaryImporter.java:344)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:242)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:125)
at com.jme3.export.binary.BinaryImporter.load(BinaryImporter.java:109)
at com.jme3.asset.DesktopAssetManager.loadAsset(DesktopAssetManager.java:288)
at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:374)
at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:378)[/java]

I avoided the issue altogether by loading the BitMap text later … however the issue now is the whole .j3o node tends to disappear / appear whenever I move the camera around. Any idea why this is happening? must be a frustum issue