1 /* 2 * Label3D.java 7 avr. 2015 3 * 4 * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 package com.eteks.sweethome3d.j3d; 21 22 import java.awt.BasicStroke; 23 import java.awt.Color; 24 import java.awt.Font; 25 import java.awt.FontMetrics; 26 import java.awt.Graphics2D; 27 import java.awt.RenderingHints; 28 import java.awt.font.TextLayout; 29 import java.awt.geom.AffineTransform; 30 import java.awt.geom.Rectangle2D; 31 import java.awt.image.BufferedImage; 32 33 import javax.media.j3d.Appearance; 34 import javax.media.j3d.BranchGroup; 35 import javax.media.j3d.Group; 36 import javax.media.j3d.PolygonAttributes; 37 import javax.media.j3d.Shape3D; 38 import javax.media.j3d.TexCoordGeneration; 39 import javax.media.j3d.Texture; 40 import javax.media.j3d.TextureAttributes; 41 import javax.media.j3d.Transform3D; 42 import javax.media.j3d.TransformGroup; 43 import javax.media.j3d.TransparencyAttributes; 44 import javax.swing.UIManager; 45 import javax.vecmath.Vector3d; 46 import javax.vecmath.Vector4f; 47 48 import com.eteks.sweethome3d.model.Home; 49 import com.eteks.sweethome3d.model.Label; 50 import com.eteks.sweethome3d.model.TextStyle; 51 import com.sun.j3d.utils.geometry.Box; 52 import com.sun.j3d.utils.image.TextureLoader; 53 54 /** 55 * Root of a label branch. 56 * @author Emmanuel Puybaret 57 */ 58 public class Label3D extends Object3DBranch { 59 private static final TransparencyAttributes DEFAULT_TRANSPARENCY_ATTRIBUTES = 60 new TransparencyAttributes(TransparencyAttributes.NICEST, 0); 61 private static final PolygonAttributes DEFAULT_POLYGON_ATTRIBUTES = 62 new PolygonAttributes(PolygonAttributes.POLYGON_FILL, PolygonAttributes.CULL_NONE, 0, false); 63 private static final TextureAttributes MODULATE_TEXTURE_ATTRIBUTES = new TextureAttributes(); 64 65 static { 66 MODULATE_TEXTURE_ATTRIBUTES.setTextureMode(TextureAttributes.MODULATE); 67 } 68 69 private String text; 70 private TextStyle style; 71 private Integer color; 72 private Transform3D baseLineTransform; 73 private Texture texture; 74 Label3D(Label label, Home home, boolean waitForLoading)75 public Label3D(Label label, Home home, boolean waitForLoading) { 76 setUserData(label); 77 78 // Allow piece branch to be removed from its parent 79 setCapability(BranchGroup.ALLOW_DETACH); 80 setCapability(BranchGroup.ALLOW_CHILDREN_READ); 81 setCapability(BranchGroup.ALLOW_CHILDREN_WRITE); 82 setCapability(BranchGroup.ALLOW_CHILDREN_EXTEND); 83 84 update(); 85 } 86 87 @Override update()88 public void update() { 89 Label label = (Label)getUserData(); 90 Float pitch = label.getPitch(); 91 TextStyle style = label.getStyle(); 92 if (pitch != null 93 && style != null 94 && (label.getLevel() == null 95 || label.getLevel().isViewableAndVisible())) { 96 String text = label.getText(); 97 Integer color = label.getColor(); 98 Integer outlineColor = label.getOutlineColor(); 99 if (!text.equals(this.text) 100 || (style == null && this.style != null) 101 || (style != null && !style.equals(this.style)) 102 || (color == null && this.color != null) 103 || (color != null && !color.equals(this.color))) { 104 // If text, style and color changed, recompute label texture 105 int fontStyle = Font.PLAIN; 106 if (style.isBold()) { 107 fontStyle = Font.BOLD; 108 } 109 if (style.isItalic()) { 110 fontStyle |= Font.ITALIC; 111 } 112 Font defaultFont; 113 if (style.getFontName() != null) { 114 defaultFont = new Font(style.getFontName(), fontStyle, 1); 115 } else { 116 defaultFont = UIManager.getFont("TextField.font"); 117 } 118 BasicStroke stroke = new BasicStroke(outlineColor != null ? style.getFontSize() * 0.05f : 0f); 119 Font font = defaultFont.deriveFont(fontStyle, style.getFontSize() - stroke.getLineWidth()); 120 121 BufferedImage dummyImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); 122 Graphics2D g2D = (Graphics2D)dummyImage.getGraphics(); 123 g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 124 g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); 125 FontMetrics fontMetrics = g2D.getFontMetrics(font); 126 127 String [] lines = text.split("\n"); 128 float [] lineWidths = new float [lines.length]; 129 float textWidth = -Float.MAX_VALUE; 130 float baseLineShift = 0; 131 for (int i = 0; i < lines.length; i++) { 132 Rectangle2D lineBounds = fontMetrics.getStringBounds(lines [i], g2D); 133 if (i == 0) { 134 baseLineShift = -(float)lineBounds.getY() + fontMetrics.getHeight() * (lines.length - 1); 135 } 136 lineWidths [i] = (float)lineBounds.getWidth() + 2 * stroke.getLineWidth(); 137 if (style.isItalic()) { 138 lineWidths [i] += fontMetrics.getAscent() * 0.2; 139 } 140 textWidth = Math.max(lineWidths [i], textWidth); 141 } 142 g2D.dispose(); 143 144 float textHeight = (float)fontMetrics.getHeight() * lines.length + 2 * stroke.getLineWidth(); 145 float textRatio = (float)Math.sqrt((float)textWidth / textHeight); 146 int width; 147 int height; 148 float scale; 149 // Ensure that text image size is between 256x256 and 512x512 pixels 150 if (textRatio > 1) { 151 width = (int)Math.ceil(Math.max(255 * textRatio, Math.min(textWidth, 511 * textRatio))); 152 scale = (float)(width / textWidth); 153 height = (int)Math.ceil(scale * textHeight); 154 } else { 155 height = (int)Math.ceil(Math.max(255 * textRatio, Math.min(textHeight, 511 / textRatio))); 156 scale = (float)(height / textHeight); 157 width = (int)Math.ceil(scale * textWidth); 158 } 159 160 if (width > 0 && height > 0) { 161 // Draw text in an image 162 BufferedImage textureImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 163 g2D = (Graphics2D)textureImage.getGraphics(); 164 g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 165 g2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 166 g2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); 167 168 g2D.setTransform(AffineTransform.getScaleInstance(scale, scale)); 169 g2D.translate(0, baseLineShift); 170 for (int i = lines.length - 1; i >= 0; i--) { 171 String line = lines [i]; 172 float translationX; 173 if (style.getAlignment() == TextStyle.Alignment.LEFT) { 174 translationX = 0; 175 } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) { 176 translationX = textWidth - lineWidths [i]; 177 } else { // CENTER 178 translationX = (textWidth - lineWidths [i]) / 2; 179 } 180 translationX += stroke.getLineWidth() / 2; 181 g2D.translate(translationX, 0); 182 if (outlineColor != null) { 183 g2D.setColor(new Color(outlineColor)); 184 g2D.setStroke(stroke); 185 if (line.length() > 0) { 186 TextLayout textLayout = new TextLayout(line, font, g2D.getFontRenderContext()); 187 g2D.draw(textLayout.getOutline(null)); 188 } 189 } 190 g2D.setFont(font); 191 g2D.setColor(color != null ? new Color(color) : UIManager.getColor("TextField.foreground")); 192 g2D.drawString(line, 0f, 0f); 193 g2D.translate(-translationX, -fontMetrics.getHeight()); 194 } 195 g2D.dispose(); 196 197 Transform3D scaleTransform = new Transform3D(); 198 scaleTransform.setScale(new Vector3d(textWidth, 1, textHeight)); 199 // Move to the middle of base line 200 this.baseLineTransform = new Transform3D(); 201 float translationX; 202 if (style.getAlignment() == TextStyle.Alignment.LEFT) { 203 translationX = textWidth / 2; 204 } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) { 205 translationX = -textWidth / 2; 206 } else { // CENTER 207 translationX = 0; 208 } 209 this.baseLineTransform.setTranslation(new Vector3d(translationX, 0, textHeight / 2 - baseLineShift)); 210 this.baseLineTransform.mul(scaleTransform); 211 this.texture = new TextureLoader(textureImage).getTexture(); 212 this.text = text; 213 this.style = style; 214 this.color = color; 215 } else { 216 clear(); 217 } 218 } 219 220 if (this.texture != null) { 221 if (numChildren() == 0) { 222 BranchGroup group = new BranchGroup(); 223 group.setCapability(BranchGroup.ALLOW_CHILDREN_READ); 224 group.setCapability(BranchGroup.ALLOW_DETACH); 225 226 TransformGroup transformGroup = new TransformGroup(); 227 // Allow the change of the transformation that sets label size, position and orientation 228 transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE); 229 transformGroup.setCapability(BranchGroup.ALLOW_CHILDREN_READ); 230 group.addChild(transformGroup); 231 232 Appearance appearance = new Appearance(); 233 appearance.setMaterial(getMaterial(DEFAULT_COLOR, DEFAULT_AMBIENT_COLOR, 0)); 234 appearance.setPolygonAttributes(DEFAULT_POLYGON_ATTRIBUTES); 235 appearance.setTextureAttributes(MODULATE_TEXTURE_ATTRIBUTES); 236 appearance.setTransparencyAttributes(DEFAULT_TRANSPARENCY_ATTRIBUTES); 237 appearance.setTexCoordGeneration(new TexCoordGeneration(TexCoordGeneration.OBJECT_LINEAR, 238 TexCoordGeneration.TEXTURE_COORDINATE_2, new Vector4f(1, 0, 0, .5f), new Vector4f(0, 1, -1, .5f))); 239 appearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE); 240 241 // Do not share box geometry or cleaning up the universe after an offscreen rendering may cause some bugs 242 Box box = new Box(0.5f, 0f, 0.5f, Box.GEOMETRY_NOT_SHARED | Box.GENERATE_NORMALS, appearance); 243 Shape3D shape = box.getShape(Box.TOP); 244 box.removeChild(shape); 245 transformGroup.addChild(shape); 246 247 addChild(group); 248 } 249 250 TransformGroup transformGroup = (TransformGroup)(((Group)getChild(0)).getChild(0)); 251 // Apply pitch rotation 252 Transform3D pitchRotation = new Transform3D(); 253 pitchRotation.rotX(pitch); 254 pitchRotation.mul(this.baseLineTransform); 255 // Apply rotation around vertical axis 256 Transform3D rotationY = new Transform3D(); 257 rotationY.rotY(-label.getAngle()); 258 rotationY.mul(pitchRotation); 259 Transform3D transform = new Transform3D(); 260 transform.setTranslation(new Vector3d(label.getX(), label.getGroundElevation() + (pitch == 0f && label.getElevation() < 0.1f ? 0.1f : 0), label.getY())); 261 transform.mul(rotationY); 262 transformGroup.setTransform(transform); 263 ((Shape3D)transformGroup.getChild(0)).getAppearance().setTexture(this.texture); 264 } 265 } else { 266 clear(); 267 } 268 } 269 270 /** 271 * Removes children and clear fields. 272 */ clear()273 private void clear() { 274 removeAllChildren(); 275 this.text = null; 276 this.style = null; 277 this.color = null; 278 this.texture = null; 279 this.baseLineTransform = null; 280 } 281 } 282