1 /*
  2  * Label3D.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2017 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 
 21 // Requires scene3d.js
 22 //          Object3DBranch.js
 23 
 24 
 25 /**
 26  * Creates the 3D label matching the given home <code>label</code>.
 27  * @param {Label} label
 28  * @param {Home} home
 29  * @param {UserPreferences} [preferences]
 30  * @param {boolean} waitModelAndTextureLoadingEnd
 31  * @constructor
 32  * @extends Object3DBranch
 33  * @author Emmanuel Puybaret
 34  */
 35 function Label3D(label, home, preferences, waitModelAndTextureLoadingEnd) {
 36   if (waitModelAndTextureLoadingEnd === undefined) {
 37     // 3 parameters
 38     waitModelAndTextureLoadingEnd = preferences;
 39     preferences = null;
 40   }
 41   Object3DBranch.call(this, label, home, preferences);
 42   
 43   this.setCapability(Group3D.ALLOW_CHILDREN_EXTEND);
 44   
 45   this.update();
 46 }
 47 Label3D.prototype = Object.create(Object3DBranch.prototype);
 48 Label3D.prototype.constructor = Label3D;
 49 
 50 Label3D.dummyContext = document.createElement("canvas").getContext("2d");
 51 
 52 Label3D.prototype.update = function() {
 53   var label = this.getUserData();
 54   var pitch = label.getPitch();
 55   var style = label.getStyle();
 56   if (pitch != null 
 57       && style != null 
 58       && (label.getLevel() == null 
 59           || label.getLevel().isViewableAndVisible())) {
 60     var text = label.getText();
 61     var color = label.getColor();
 62     var outlineColor = label.getOutlineColor();
 63     if (text != this.text 
 64         || (style == null && this.style != null) 
 65         || (style != null && !style.equals(this.style)) 
 66         || (color == null && this.color != null) 
 67         || (color != null && color !== this.color)) {
 68       var fontStyle = "";
 69       if (style.isBold()) {
 70         fontStyle = "bold ";
 71       }
 72       if (style.isItalic()) {
 73         fontStyle += "italic ";
 74       }
 75       var fontName = style.getFontName();
 76       if (fontName === null) {
 77         fontName = "sans-serif";
 78       }
 79       
 80       var fontSize = 50; // Size to get a similar outline as in Java
 81       var fontHeight = fontSize; 
 82       if (["Times", "Serif", "Helvetica"].indexOf(fontName) === -1) {
 83         fontHeight *= 1.18;
 84       }
 85       var fontScale = fontSize / style.getFontSize();
 86       var descent = 0.23 * fontHeight;
 87       var font = fontStyle + " " + fontSize + "px " + fontName;
 88       Label3D.dummyContext.font = font;
 89       
 90       var lines = text.replace(/\n*$/, "").split("\n");
 91       var lineWidths = new Array(lines.length);
 92       var textWidth = -Infinity;
 93       var baseLineShift = -descent + fontHeight * lines.length;
 94       for (var i = 0; i < lines.length; i++) {
 95         lineWidths [i] = Label3D.dummyContext.measureText(lines [i]).width;
 96         if (style.isItalic()) {
 97           lineWidths [i] += fontHeight * 0.154;
 98         }
 99         textWidth = Math.max(lineWidths [i], textWidth);
100       }
101       
102       var textHeight = fontHeight * lines.length;
103       var textRatio = Math.sqrt(textWidth / textHeight);
104       var textRatio = Math.sqrt(textWidth / textHeight);
105       var width;
106       var height;
107       var scale;
108       if (textRatio > 1) {
109         width = Math.ceil(Math.max(255 * textRatio, Math.min(textWidth, 511 * textRatio)));
110         scale = width / textWidth;
111         height = Math.ceil(scale * textHeight);
112       } else {
113         height = Math.ceil(Math.max(255 * textRatio, Math.min(textHeight, 511 / textRatio)));
114         scale = height / textHeight;
115         width = Math.ceil(scale * textWidth);
116       }
117       if (width > 0 && height > 0) {
118         var textureImage = document.createElement("canvas");
119         textureImage.width = Appearance3D.getNextHighestPowerOfTwo(width) / 2;
120         textureImage.height = Appearance3D.getNextHighestPowerOfTwo(height) / 2;
121         textureImage.transparent = true;
122         var context = textureImage.getContext("2d");
123         
124         context.scale(scale / width * textureImage.width, scale / height * textureImage.height);
125         context.translate(0, baseLineShift);
126         context.font = font;
127         if (color !== null) {
128           context.fillStyle = "rgb(" 
129              + ((color >>> 16) & 0xFF) + ","
130              + ((color >>> 8) & 0xFF) + ","
131              + (color & 0xFF) + ")";
132         }
133         if (outlineColor !== null) {
134           context.strokeStyle = "rgb(" 
135               + ((outlineColor >>> 16) & 0xFF) + ","
136               + ((outlineColor >>> 8) & 0xFF) + ","
137               + (outlineColor & 0xFF) + ")"
138         }
139         for (var i = lines.length - 1; i >= 0; i--) {
140           var line = lines [i];
141           var translationX;
142           if (style.getAlignment() === TextStyle.Alignment.LEFT) {
143             translationX = 0;
144           } else if (style.getAlignment() === TextStyle.Alignment.RIGHT) {
145             translationX = textWidth - lineWidths [i];
146           } else { // CENTER
147             translationX = (textWidth - lineWidths [i]) / 2;
148           }
149           context.translate(translationX, 0);
150           context.fillText(line, 0, 0);      
151           if (outlineColor !== null) {
152             // Fill then stroke to be able to view outline drawn in each character
153             context.strokeText(line, 0, 0);
154           }
155           context.translate(-translationX, -fontHeight);
156         }
157         
158         var scaleTransform = mat4.create();
159         mat4.scale(scaleTransform, scaleTransform, vec3.fromValues(textWidth / fontScale, 1, textHeight / fontScale));
160         this.baseLineTransform = mat4.create();
161         var translationX;
162         if (style.getAlignment() == TextStyle.Alignment.LEFT) {
163           translationX = textWidth / 2;
164         } else if (style.getAlignment() == TextStyle.Alignment.RIGHT) {
165           translationX = -textWidth / 2;
166         } else { // CENTER
167           translationX = 0;
168         }
169         mat4.fromTranslation(this.baseLineTransform, vec3.fromValues(translationX / fontScale, 0, (textHeight / 2 - baseLineShift) / fontScale));
170         mat4.mul(this.baseLineTransform, this.baseLineTransform, scaleTransform);
171         this.texture = textureImage;
172         this.text = text;
173         this.style = style;
174         this.color = color;
175       } else {
176         this.clear();
177       }
178     }
179     if (this.texture !== null) {
180       var transformGroup;
181       var selectionAppearance;
182       if (this.getChildren().length === 0) {
183         var group = new BranchGroup3D();
184         transformGroup = new TransformGroup3D();
185         transformGroup.setCapability(TransformGroup3D.ALLOW_TRANSFORM_WRITE);
186         group.addChild(transformGroup);
187         
188         var appearance = new Appearance3D();
189         this.updateAppearanceMaterial(appearance, Object3DBranch.DEFAULT_COLOR, Object3DBranch.DEFAULT_AMBIENT_COLOR, 0);
190         appearance.setCullFace(Appearance3D.CULL_NONE);
191         var shape = new Shape3D( 
192             new IndexedTriangleArray3D(
193                [vec3.fromValues(-0.5, 0, -0.5),
194                 vec3.fromValues(-0.5, 0, 0.5),
195                 vec3.fromValues(0.5, 0, 0.5),
196                 vec3.fromValues(0.5, 0, -0.5)],
197                [0, 1, 2, 0, 2, 3],
198                [vec2.fromValues(0., 0.),
199                 vec2.fromValues(1., 0.),
200                 vec2.fromValues(1., 1.),
201                 vec2.fromValues(0., 1.)],
202                [3, 0, 1, 3, 1, 2],
203                [vec3.fromValues(0., 1., 0.)],
204                [0, 0, 0, 0, 0, 0]), appearance);
205         transformGroup.addChild(shape);
206         
207         var selectionCoordinates = [vec3.fromValues(-0.5, 0, -0.5), vec3.fromValues(0.5, 0, -0.5),
208                                     vec3.fromValues(0.5, 0, 0.5),   vec3.fromValues(-0.5, 0, 0.5)];
209         var selectionGeometry = new IndexedLineArray3D(selectionCoordinates, [0, 1, 1, 2, 2, 3, 3, 0]);
210 
211         selectionAppearance = this.getSelectionAppearance();
212         var selectionLinesShape = new Shape3D(selectionGeometry, selectionAppearance);
213         selectionLinesShape.setPickable(false);
214         transformGroup.addChild(selectionLinesShape);
215 
216         this.addChild(group);
217       } else {
218         transformGroup = this.getChild(0).getChild(0);
219         var selectionLinesShape = transformGroup.getChild(1);
220         selectionAppearance = selectionLinesShape.getAppearance();
221       }
222       
223       var transformGroup = this.getChild(0).getChild(0);
224       var pitchRotation = mat4.create();
225       mat4.fromXRotation(pitchRotation, pitch);
226       mat4.mul(pitchRotation, pitchRotation, this.baseLineTransform);
227       var rotationY = mat4.create();
228       mat4.fromYRotation(rotationY, -label.getAngle());
229       mat4.mul(rotationY, rotationY, pitchRotation);
230       var transform = mat4.create();
231       mat4.fromTranslation(transform, vec3.fromValues(label.getX(), label.getGroundElevation() + (pitch == 0 && label.getElevation() < 0.1 ? 0.1 : 0), label.getY()));
232       mat4.mul(transform, transform, rotationY);
233       transformGroup.setTransform(transform);
234       transformGroup.getChild(0).getAppearance().setTextureImage(this.texture);
235       
236       selectionAppearance.setVisible(this.getUserPreferences() != null
237           && this.getUserPreferences().isEditingIn3DViewEnabled()
238           && this.getHome().isItemSelected(label));
239     }
240   } else {
241     this.clear();
242   }
243 }
244 
245 /**
246  * Removes children and clear fields.
247  * @private
248  */
249 Label3D.prototype.clear = function () {
250   this.removeAllChildren();
251   this.text = null;
252   this.style = null;
253   this.color = null;
254   this.texture = null;
255   this.baseLineTransform = null;
256 }