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