1 /*
  2  * ModelManager.js
  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 
 21 // Requires URLContent.js
 22 //          scene3d.js
 23 //          ModelLoader.js
 24 //          OBJLoader.js
 25 // Uses     HomeObject.js 
 26 //          HomePieceOfFurniture.js
 27 //          HomeMaterial.js
 28 //          HomeTexture.js
 29 //          CatalogTexture.js
 30 //          ShapeTools.js
 31 // (used classes are not needed to view 3D models)
 32 
 33 /**
 34  * Singleton managing 3D models cache.
 35  * @constructor
 36  * @author Emmanuel Puybaret
 37  */
 38 function ModelManager() {
 39   this.loadedModelNodes = {};
 40   this.loadingModelObservers = {};
 41 }
 42 
 43 /**
 44  * Special shapes prefix;
 45  */
 46 ModelManager.SPECIAL_SHAPE_PREFIX = "sweethome3d_";
 47 /**
 48  * <code>Shape3D</code> name prefix for window pane shapes. 
 49  */
 50 ModelManager.WINDOW_PANE_SHAPE_PREFIX = ModelManager.SPECIAL_SHAPE_PREFIX + "window_pane";
 51 /**
 52  * <code>Shape3D</code> name prefix for mirror shapes. 
 53  */
 54 ModelManager.MIRROR_SHAPE_PREFIX = ModelManager.SPECIAL_SHAPE_PREFIX + "window_mirror";
 55 /**
 56  * <code>Shape3D</code> name prefix for lights. 
 57  */
 58 ModelManager.LIGHT_SHAPE_PREFIX = ModelManager.SPECIAL_SHAPE_PREFIX + "light";
 59 /**
 60  * <code>Node</code> user data prefix for mannequin parts.
 61  */
 62 ModelManager.MANNEQUIN_ABDOMEN_PREFIX        = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_abdomen";
 63 ModelManager.MANNEQUIN_CHEST_PREFIX          = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_chest";
 64 ModelManager.MANNEQUIN_PELVIS_PREFIX         = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_pelvis";
 65 ModelManager.MANNEQUIN_NECK_PREFIX           = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_neck";
 66 ModelManager.MANNEQUIN_HEAD_PREFIX           = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_head";
 67 ModelManager.MANNEQUIN_LEFT_SHOULDER_PREFIX  = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_shoulder";
 68 ModelManager.MANNEQUIN_LEFT_ARM_PREFIX       = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_arm";
 69 ModelManager.MANNEQUIN_LEFT_ELBOW_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_elbow";
 70 ModelManager.MANNEQUIN_LEFT_FOREARM_PREFIX   = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_forearm";
 71 ModelManager.MANNEQUIN_LEFT_WRIST_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_wrist";
 72 ModelManager.MANNEQUIN_LEFT_HAND_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_hand";
 73 ModelManager.MANNEQUIN_LEFT_HIP_PREFIX       = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_hip";
 74 ModelManager.MANNEQUIN_LEFT_THIGH_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_thigh";
 75 ModelManager.MANNEQUIN_LEFT_KNEE_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_knee";
 76 ModelManager.MANNEQUIN_LEFT_LEG_PREFIX       = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_leg";
 77 ModelManager.MANNEQUIN_LEFT_ANKLE_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_ankle";
 78 ModelManager.MANNEQUIN_LEFT_FOOT_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_left_foot";
 79 ModelManager.MANNEQUIN_RIGHT_SHOULDER_PREFIX = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_shoulder";
 80 ModelManager.MANNEQUIN_RIGHT_ARM_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_arm";
 81 ModelManager.MANNEQUIN_RIGHT_ELBOW_PREFIX    = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_elbow";
 82 ModelManager.MANNEQUIN_RIGHT_FOREARM_PREFIX  = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_forearm";
 83 ModelManager.MANNEQUIN_RIGHT_WRIST_PREFIX    = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_wrist";
 84 ModelManager.MANNEQUIN_RIGHT_HAND_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_hand";
 85 ModelManager.MANNEQUIN_RIGHT_HIP_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_hip";
 86 ModelManager.MANNEQUIN_RIGHT_THIGH_PREFIX    = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_thigh";
 87 ModelManager.MANNEQUIN_RIGHT_KNEE_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_knee";
 88 ModelManager.MANNEQUIN_RIGHT_LEG_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_leg";
 89 ModelManager.MANNEQUIN_RIGHT_ANKLE_PREFIX    = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_ankle";
 90 ModelManager.MANNEQUIN_RIGHT_FOOT_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_right_foot";
 91 
 92 ModelManager.MANNEQUIN_ABDOMEN_CHEST_PREFIX  = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_abdomen_chest";
 93 ModelManager.MANNEQUIN_ABDOMEN_PELVIS_PREFIX = ModelManager.SPECIAL_SHAPE_PREFIX + "mannequin_abdomen_pelvis";
 94 /**
 95  * <code>Node</code> user data prefix for ball / rotating  joints.
 96  */
 97 ModelManager.BALL_PREFIX                 = ModelManager.SPECIAL_SHAPE_PREFIX + "ball_";
 98 ModelManager.ARM_ON_BALL_PREFIX          = ModelManager.SPECIAL_SHAPE_PREFIX + "arm_on_ball_";
 99 /**
100  * <code>Node</code> user data prefix for hinge / rotating opening joints.
101  */
102 ModelManager.HINGE_PREFIX                = ModelManager.SPECIAL_SHAPE_PREFIX + "hinge_";
103 ModelManager.OPENING_ON_HINGE_PREFIX     = ModelManager.SPECIAL_SHAPE_PREFIX + "opening_on_hinge_";
104 ModelManager.WINDOW_PANE_ON_HINGE_PREFIX = ModelManager.WINDOW_PANE_SHAPE_PREFIX + "_on_hinge_";
105 ModelManager.MIRROR_ON_HINGE_PREFIX      = ModelManager.MIRROR_SHAPE_PREFIX + "_on_hinge_";
106 /**
107  * <code>Node</code> user data prefix for rail / sliding opening joints.
108  */
109 ModelManager.UNIQUE_RAIL_PREFIX          = ModelManager.SPECIAL_SHAPE_PREFIX + "unique_rail";
110 ModelManager.RAIL_PREFIX                 = ModelManager.SPECIAL_SHAPE_PREFIX + "rail_";
111 ModelManager.OPENING_ON_RAIL_PREFIX      = ModelManager.SPECIAL_SHAPE_PREFIX + "opening_on_rail_";
112 ModelManager.WINDOW_PANE_ON_RAIL_PREFIX  = ModelManager.WINDOW_PANE_SHAPE_PREFIX + "_on_rail_";
113 ModelManager.MIRROR_ON_RAIL_PREFIX       = ModelManager.MIRROR_SHAPE_PREFIX + "_on_rail_";
114 /**
115  * <code>Node</code> user data separator for sub transformations.
116  */
117 ModelManager.SUB_TRANSFORMATION_SEPARATOR = "_and_";
118 /**
119  * Deformable group suffix.
120  */
121 ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX = "_transformation";
122 
123 ModelManager.EDGE_COLOR_MATERIAL_PREFIX = "edge_color";
124 
125 // Singleton
126 ModelManager.instance = null;
127 
128 /**
129  * Returns an instance of this singleton.
130  * @return {ModelManager} 
131  */
132 ModelManager.getInstance = function() {
133   if (ModelManager.instance == null) {
134     ModelManager.instance = new ModelManager();
135   }
136   return ModelManager.instance;
137 }
138 
139 /**
140  * Clears loaded models cache. 
141  */
142 ModelManager.prototype.clear = function() {
143   this.loadedModelNodes = {};
144   this.loadingModelObservers = {};
145   if (this.modelLoaders) {
146     for (var i = 0; i < this.modelLoaders.length; i++) {
147       this.modelLoaders [i].clear();
148     } 
149   }
150 }
151 
152 /**
153  * Returns the minimum size of a model.
154  */
155 ModelManager.prototype.getMinimumSize = function() {
156   return 0.001;
157 }
158 
159 /**
160  * Returns the size of 3D shapes of node after an additional optional transformation.
161  * @param {Node3D} node  the root of a model 
162  * @param {Array}  [transformation] the optional transformation applied to the model  
163  */
164 ModelManager.prototype.getSize = function(node, transformation) {
165   if (transformation === undefined) {
166     transformation = mat4.create();
167   }
168   var bounds = this.getBounds(node, transformation);
169   var lower = vec3.create();
170   bounds.getLower(lower);
171   var upper = vec3.create();
172   bounds.getUpper(upper);
173   return vec3.fromValues(Math.max(this.getMinimumSize(), upper[0] - lower[0]), 
174       Math.max(this.getMinimumSize(), upper[1] - lower[1]), 
175       Math.max(this.getMinimumSize(), upper[2] - lower[2]));
176 }
177 
178 /**
179  * Returns the center of the bounds of <code>node</code> 3D shapes.
180  * @param node  the root of a model
181  */
182 ModelManager.prototype.getCenter = function(node) {
183   var bounds = this.getBounds(node);
184   var lower = vec3.create();
185   bounds.getLower(lower);
186   var upper = vec3.create();
187   bounds.getUpper(upper);
188   return vec3.fromValues((lower[0] + upper[0]) / 2, (lower[1] + upper[1]) / 2, (lower[2] + upper[2]) / 2);
189 }
190 
191 /**
192  * Returns the bounds of the 3D shapes of node with an additional optional transformation.
193  * @param {Node3D} node  the root of a model 
194  * @param {Array}  [transformation] the optional transformation applied to the model  
195  */
196 ModelManager.prototype.getBounds = function(node, transformation) {
197   if (transformation === undefined) {
198     transformation = mat4.create();
199   }
200   var objectBounds = new BoundingBox3D(
201       vec3.fromValues(Infinity, Infinity, Infinity), 
202       vec3.fromValues(-Infinity, -Infinity, -Infinity));
203   this.computeBounds(node, objectBounds, transformation, !this.isOrthogonalRotation(transformation), this.isDeformed(node));
204   return objectBounds;
205 }
206 
207 /**
208  * Returns true if the rotation matrix matches only rotations of 
209  * a multiple of 90° degrees around x, y or z axis.
210  * @private
211  */
212 ModelManager.prototype.isOrthogonalRotation = function(transformation) {
213   for (var i = 0; i < 3; i++) {
214     for (var j = 0; j < 3; j++) {
215       // Return false if the matrix contains a value different from 0 1 or -1
216       if (Math.abs(transformation[i * 4 + j]) > 1E-6
217           && Math.abs(transformation[i * 4 + j] - 1) > 1E-6
218           && Math.abs(transformation[i * 4 + j] + 1) > 1E-6) {
219         return false;
220       }
221     }
222   }
223   return true;
224 }
225 
226 /**
227  * @private
228  */
229 ModelManager.prototype.computeBounds = function(node, bounds, parentTransformation, transformShapeGeometry, deformedGeometry) {
230   if (node instanceof Group3D) {
231     if (node instanceof TransformGroup3D) {
232       parentTransformation = mat4.clone(parentTransformation);
233       mat4.mul(parentTransformation, parentTransformation, node.transform);
234     }
235     // Compute the bounds of all the node children
236     for (var i = 0; i < node.children.length; i++) {
237       this.computeBounds(node.children [i], bounds, parentTransformation, transformShapeGeometry, deformedGeometry);
238     }
239   } else if (node instanceof Link3D) {
240     this.computeBounds(node.getSharedGroup(), bounds, parentTransformation, transformShapeGeometry, deformedGeometry);
241   } else if (node instanceof Shape3D) {
242     var shapeBounds;
243     if (transformShapeGeometry
244         || deformedGeometry
245            && !this.isOrthogonalRotation(parentTransformation)) {
246       shapeBounds = this.computeTransformedGeometryBounds(node, parentTransformation);
247     } else {
248       shapeBounds = node.getBounds();
249       shapeBounds.transform(parentTransformation);
250     }
251     bounds.combine(shapeBounds);
252   }
253 }
254 
255 /**
256  * @private
257  */
258 ModelManager.prototype.computeTransformedGeometryBounds = function(shape, transformation) {
259   var lower = vec3.fromValues(Infinity, Infinity, Infinity);
260   var upper = vec3.fromValues(-Infinity, -Infinity, -Infinity);    
261   for (var i = 0; i < shape.geometries.length; i++) {
262     // geometry instanceof IndexedGeometryArray3D
263     var geometry = shape.geometries [i];
264     var vertex = vec3.create();
265     for (var index = 0; index < geometry.vertexIndices.length; index++) {
266       vec3.copy(vertex, geometry.vertices [geometry.vertexIndices [index]]);
267       this.updateBounds(vertex, transformation, lower, upper);
268     }
269   }
270   return new BoundingBox3D(lower, upper);
271 }
272 
273 /**
274  * @private
275  */
276 ModelManager.prototype.updateBounds = function(vertex, transformation, lower, upper) {
277   if (transformation !== null) {
278     vec3.transformMat4(vertex, vertex, transformation);
279   }
280   vec3.min(lower, lower, vertex);
281   vec3.max(upper, upper, vertex);
282 }
283 
284 /**
285  * Returns a transform group that will transform the model node
286  * to let it fill a box of the given width centered on the origin.
287  * @param {Node3D} node     the root of a model with any size and location
288  * @param {Array}  modelRotation the rotation applied to the model before normalization 
289  *                 or <code>null</code> if no transformation should be applied to node.
290  * @param {number} width    the width of the box
291  * @param {boolean} [modelCenteredAtOrigin] if <code>true</code> or missing, center will be moved 
292  *                 to match the origin after the model rotation is applied
293  */
294 ModelManager.prototype.getNormalizedTransformGroup = function(node, modelRotation, width, modelCenteredAtOrigin) {
295   return new TransformGroup3D(this.getNormalizedTransform(
296       node, modelRotation, width, modelCenteredAtOrigin !== false));
297 }
298 
299 /**
300  * Returns a transformation matrix that will transform the model node
301  * to let it fill a box of the given width centered on the origin.
302  * @param {Node3D} node     the root of a model with any size and location
303  * @param {?Array} modelRotation the rotation applied to the model before normalization 
304  *                 or <code>null</code> if no transformation should be applied to node.
305  * @param {number} width    the width of the box
306  * @param {boolean} [modelCenteredAtOrigin] if <code>true</code> center will be moved to match the origin 
307  *                 after the model rotation is applied
308  */
309 ModelManager.prototype.getNormalizedTransform = function(node, modelRotation, width, modelCenteredAtOrigin) {
310   // Get model bounding box size 
311   var modelBounds = this.getBounds(node);
312   var lower = vec3.create();
313   modelBounds.getLower(lower);
314   var upper = vec3.create();
315   modelBounds.getUpper(upper);
316   // Translate model to its center
317   var translation = mat4.create();
318   mat4.translate(translation, translation, vec3.fromValues(
319       -lower[0] - (upper[0] - lower[0]) / 2, 
320       -lower[1] - (upper[1] - lower[1]) / 2, 
321       -lower[2] - (upper[2] - lower[2]) / 2));
322   
323   var modelTransform;
324   if (modelRotation !== undefined && modelRotation !== null) {
325     // Get model bounding box size with model rotation
326     var rotationTransform = this.getRotationTransformation(modelRotation);
327     mat4.mul(rotationTransform, rotationTransform, translation);
328     var rotatedModelBounds = this.getBounds(node, rotationTransform);
329     rotatedModelBounds.getLower(lower);
330     rotatedModelBounds.getUpper(upper);
331     modelTransform = mat4.create();
332     if (modelCenteredAtOrigin) {
333       // Move model back to its new center
334       mat4.translate(modelTransform, modelTransform, vec3.fromValues(
335           -lower[0] - (upper[0] - lower[0]) / 2, 
336           -lower[1] - (upper[1] - lower[1]) / 2, 
337           -lower[2] - (upper[2] - lower[2]) / 2));
338     }
339     mat4.mul(modelTransform, modelTransform, rotationTransform);
340   } else {
341     modelTransform = translation;
342   }
343 
344   // Scale model to make it fill a 1 unit wide box
345   var scaleOneTransform = mat4.create();
346   mat4.scale(scaleOneTransform, scaleOneTransform,
347       vec3.fromValues(width / Math.max(this.getMinimumSize(), upper[0] - lower[0]), 
348           width / Math.max(this.getMinimumSize(), upper[1] - lower[1]), 
349           width / Math.max(this.getMinimumSize(), upper[2] - lower[2])));
350   mat4.mul(scaleOneTransform, scaleOneTransform, modelTransform);
351   return scaleOneTransform;
352 }
353 
354 /**
355  * Returns a transformation matching the given rotation.
356  * @param {Array}  modelRotation  the desired rotation.
357  * @ignore
358  */
359 ModelManager.prototype.getRotationTransformation = function(modelRotation) {
360   var modelTransform = mat4.create();
361   modelTransform [0] = modelRotation [0][0];
362   modelTransform [4] = modelRotation [0][1];
363   modelTransform [8] = modelRotation [0][2];
364   modelTransform [1] = modelRotation [1][0];
365   modelTransform [5] = modelRotation [1][1];
366   modelTransform [9] = modelRotation [1][2];
367   modelTransform [2] = modelRotation [2][0];
368   modelTransform [6] = modelRotation [2][1];
369   modelTransform [10] = modelRotation [2][2];
370   return modelTransform;
371 }
372 
373 /**
374  * Returns a transformation able to place in the scene the normalized model 
375  * of the given <code>piece</code>.
376  * @param {HomePieceOfFurniture} piece  a piece of furniture
377  * @param {Node3D} [normalizedModelNode]  the node matching the normalized model of the piece. 
378  *              This parameter is required only if the piece is rotated horizontally
379  * @ignore
380  */
381 ModelManager.prototype.getPieceOfFurnitureNormalizedModelTransformation = function(piece, normalizedModelNode) {
382   // Set piece size
383   var scale = mat4.create();
384   var pieceWidth = piece.getWidth();
385   // If piece model is mirrored, inverse its width
386   if (piece.isModelMirrored()) {
387     pieceWidth *= -1;
388   }
389   mat4.scale(scale, scale, vec3.fromValues(pieceWidth, piece.getHeight(), piece.getDepth()));
390   
391   var modelTransform;
392   var height;
393   if (piece.isHorizontallyRotated() && normalizedModelNode !== undefined && normalizedModelNode !== null) {
394     var horizontalRotationAndScale = mat4.create();
395     // Change its angle around horizontal axes
396     if (piece.getPitch() != 0) {
397       mat4.fromXRotation(horizontalRotationAndScale, -piece.getPitch());
398     } 
399     if (piece.getRoll() != 0) {
400       var rollRotation = mat4.create();
401       mat4.fromZRotation(rollRotation, -piece.getRoll());
402       mat4.mul(horizontalRotationAndScale, rollRotation, horizontalRotationAndScale); 
403     }
404     mat4.mul(horizontalRotationAndScale, horizontalRotationAndScale, scale);
405         
406     // Compute center location when the piece is rotated around horizontal axes
407     var rotatedModelBounds = this.getBounds(normalizedModelNode, horizontalRotationAndScale);
408     var lower = vec3.create();
409     rotatedModelBounds.getLower(lower);
410     var upper = vec3.create();
411     rotatedModelBounds.getUpper(upper);
412     modelTransform = mat4.create();
413     mat4.translate(modelTransform, modelTransform, vec3.fromValues(
414         -lower[0] - (upper[0] - lower[0]) / 2, 
415         -lower[1] - (upper[1] - lower[1]) / 2, 
416         -lower[2] - (upper[2] - lower[2]) / 2));
417     mat4.mul(modelTransform, modelTransform, horizontalRotationAndScale);
418     height = Math.max(this.getMinimumSize(), upper[1] - lower[1]);
419   } else {
420     modelTransform = scale;
421     height = piece.getHeight();
422   }
423   
424   // Change its angle around y axis
425   var verticalRotation = mat4.create();
426   mat4.fromYRotation(verticalRotation, -piece.getAngle());
427   mat4.mul(verticalRotation, verticalRotation, modelTransform);
428   
429   // Translate it to its location
430   var pieceTransform = mat4.create();
431   var levelElevation;
432   if (piece.getLevel() !== null) {
433     levelElevation = piece.getLevel().getElevation();
434   } else {
435     levelElevation = 0;
436   }
437   mat4.translate(pieceTransform, pieceTransform, vec3.fromValues(
438       piece.getX(), 
439       piece.getElevation() + height / 2 + levelElevation,
440       piece.getY()));      
441   mat4.mul(pieceTransform, pieceTransform, verticalRotation);
442   return pieceTransform;
443 }
444 
445 /**
446  * For backward compatibility.
447  * @deprecated
448  * @ignore
449  */
450 ModelManager.prototype.getPieceOFFurnitureNormalizedModelTransformation = ModelManager.prototype.getPieceOfFurnitureNormalizedModelTransformation;
451 
452 /**
453  * Reads a 3D node from content with supported loaders
454  * and notifies the loaded model to the given <code>modelObserver</code> once available
455  * with its <code>modelUpdated</code> and <code>modelError</code> methods. 
456  * @param {URLContent} content an object containing a model
457  * @param {boolean} [synchronous] optional parameter equal to false by default
458  * @param {{modelUpdated, modelError, progression}} modelObserver  
459  *            the observer containing <code>modelUpdated(model)</code>, <code>modelError(error)</code>, 
460  *            <code>progression(part, info, percentage)</code> optional methods that will be 
461  *            notified once the model is available or if an error happens,  
462  *            with <code>model<code> being an instance of <code>Node3D</code>, 
463  *            <code>error</code>, <code>part</code>, <code>info</code> strings 
464  *            and <code>percentage</code> a number.
465  */
466 ModelManager.prototype.loadModel = function(content, synchronous, modelObserver) {
467   if (modelObserver === undefined) {
468     modelObserver = synchronous;
469     synchronous = false;
470   }
471   var modelManager = this;
472   var contentUrl = content.getURL();
473   if (contentUrl in this.loadedModelNodes) {
474     // Notify cached model to observer with a clone of the model
475     var model = this.loadedModelNodes [contentUrl];
476     if (modelObserver.modelUpdated !== undefined) {
477       modelObserver.modelUpdated(this.cloneNode(model));
478     }
479   } else if (synchronous) {
480     this.load(content, synchronous, {
481         modelLoaded : function(loadedModel) {
482           modelManager.loadedModelNodes [contentUrl] = loadedModel;
483           if (modelObserver.modelUpdated !== undefined) {
484             modelObserver.modelUpdated(modelManager.cloneNode(loadedModel));
485           }
486         },
487         modelError : function(err) {
488           if (modelObserver.modelError !== undefined) {
489             modelObserver.modelError(err);
490           }
491         },
492         progression : function(part, info, percentage) {
493           if (modelObserver.progression !== undefined) {
494             modelObserver.progression(part, info, percentage);
495           }
496         }
497       });
498   } else {
499     if (contentUrl in this.loadingModelObservers) {
500       // If observers list exists, content model is already being loaded
501       // register observer for future notification
502       this.loadingModelObservers [contentUrl].push(modelObserver);
503     } else {
504       // Create a list of observers that will be notified once content model is loaded
505       var observers = [];
506       observers.push(modelObserver);
507       this.loadingModelObservers [contentUrl] = observers;
508       
509       this.load(content, synchronous, {
510           modelLoaded : function(loadedModel) {
511             modelManager.loadedModelNodes [contentUrl] = loadedModel;
512               var observers = modelManager.loadingModelObservers [contentUrl];
513               if (observers) {
514                 for (var i = 0; i < observers.length; i++) {
515                   if (observers [i].modelUpdated !== undefined) {
516                     observers [i].modelUpdated(modelManager.cloneNode(loadedModel));
517                   }
518                 }
519               }
520           },
521           modelError : function(err) {
522             var observers = modelManager.loadingModelObservers [contentUrl];
523             if (observers) {
524               delete modelManager.loadingModelObservers [contentUrl];
525               for (var i = 0; i < observers.length; i++) {
526                 if (observers [i].modelError !== undefined) {
527                   observers [i].modelError(err);
528                 }
529               }
530             }
531           },
532           progression : function(part, info, percentage) {
533             var observers = modelManager.loadingModelObservers [contentUrl];
534             if (observers) {
535               for (var i = 0; i < observers.length; i++) {
536                 if (observers [i].progression !== undefined) {
537                   observers [i].progression(part, info, percentage);
538                 }
539               } 
540             }
541           }
542         });
543     }
544   }
545 }
546 
547 /**
548  * Removes the model matching the given content from the manager. 
549  * @param {URLContent} content an object containing a model
550  * @param {boolean}    disposeGeometries if <code>true</code> model geometries will be disposed too
551  */
552 ModelManager.prototype.unloadModel = function(content, disposeGeometries) {
553   var contentUrl = content.getURL();
554   var modelRoot = this.loadedModelNodes [contentUrl];
555   delete this.loadedModelNodes [contentUrl];
556   delete this.loadingModelObservers [contentUrl];
557   if (disposeGeometries) {
558     this.disposeGeometries(modelRoot);
559   }
560 }
561 
562 /**
563  * Frees geometry data of the given <code>node</code>.
564  * @param {Node3D} node  the root of a model
565  * @package 
566  */
567 ModelManager.prototype.disposeGeometries = function(node) {
568   if (node instanceof Group3D) {
569     for (var i = 0; i < node.children.length; i++) {
570       this.disposeGeometries(node.children [i]);
571     }
572   } else if (node instanceof Link3D) {
573     // Not a problem to dispose more than once geometries of a shared group
574     this.disposeGeometries(node.getSharedGroup());
575   } else if (node instanceof Shape3D) {
576     var geometries = node.getGeometries();
577     for (var i = 0; i < geometries.length; i++) {
578       geometries [i].disposeCoordinates(); 
579     }
580   }
581 }
582 
583 /**
584  * Returns a clone of the given <code>node</code>.
585  * All the children and the attributes of the given node are duplicated except the geometries 
586  * and the texture images of shapes.
587  * @param {Node3D} node  the root of a model 
588  * @param {Array}  [clonedSharedGroups]
589  */
590 ModelManager.prototype.cloneNode = function(node, clonedSharedGroups) {
591   if (clonedSharedGroups === undefined) {
592     return this.cloneNode(node, []);
593   } else {
594     var clonedNode = node.clone();
595     if (node instanceof Shape3D) {
596       var clonedAppearance;
597       if (node.getAppearance()) {
598         clonedNode.setAppearance(node.getAppearance().clone());
599       }
600     } else if (node instanceof Link3D) {
601       var clonedLink = node.clone();
602       // Force duplication of shared groups too if not duplicated yet
603       var sharedGroup = clonedLink.getSharedGroup();
604       if (sharedGroup !== null) {
605         var clonedSharedGroup = null;
606         for (var i = 0; i < clonedSharedGroups.length; i++) {
607           if (clonedSharedGroups [i].sharedGroup === sharedGroup) {
608             clonedSharedGroup = clonedSharedGroups [i].clonedSharedGroup;
609             break;
610           }
611         }
612         if (clonedSharedGroup === null) {
613           clonedSharedGroup = this.cloneNode(sharedGroup, clonedSharedGroups);
614           clonedSharedGroups.push({sharedGroup : sharedGroup, 
615                                    clonedSharedGroup : clonedSharedGroup});          
616         }
617         clonedLink.setSharedGroup(clonedSharedGroup);
618       }
619       return clonedLink;
620     } else {
621       clonedNode = node.clone();
622       if (node instanceof Group3D) {
623         var children = node.getChildren();
624         for (var i = 0; i < children.length; i++) {
625           var clonedChild = this.cloneNode(children [i], clonedSharedGroups);
626           clonedNode.addChild(clonedChild);
627         }
628       }
629     }
630     return clonedNode;
631   }
632 }
633 
634 /**
635  * Loads the node from <code>content</code> with supported loaders.
636  * @param {URLContent} content an object containing a model
637  * @param {boolean} [synchronous] optional parameter equal to false by default
638  * @param {{modelLoaded, modelError, progression}} modelObserver  
639  *           the observer that will be notified once the model is available
640  *           or if an error happens
641  * @private
642  */
643 ModelManager.prototype.load = function(content, synchronous, modelObserver) {
644   if (modelObserver === undefined) {
645     // 2 parameters (content, modelObserver)
646     modelObserver = synchronous;
647     synchronous = false;
648   }
649 
650   var contentUrl = content.getURL();
651   if (!this.modelLoaders) {
652     // As model loaders are reentrant, use the same loaders for multiple loading
653     this.modelLoaders = [new OBJLoader()];
654     // Optional loaders
655     if (typeof DAELoader !== "undefined") {
656       this.modelLoaders.push(new DAELoader());
657     }
658     if (typeof Max3DSLoader !== "undefined") {
659       this.modelLoaders.push(new Max3DSLoader());
660     }
661   }
662   var modelManager = this;
663   var modelLoadingObserver = {
664       modelLoaderIndex : 0,
665       modelLoaded : function(model) {
666         var bounds = modelManager.getBounds(model);
667         if (!bounds.isEmpty()) {
668           modelManager.updateWindowPanesTransparency(model);
669           modelManager.updateDeformableModelHierarchy(model);
670           modelManager.replaceMultipleSharedShapes(model);
671           model.setUserData(content);
672           modelObserver.modelLoaded(model);
673         } else if (++this.modelLoaderIndex < modelManager.modelLoaders.length) {
674           modelManager.modelLoaders [this.modelLoaderIndex].load(contentUrl, synchronous, this);
675         } else {
676           this.modelError("Unsupported 3D format");
677         }
678       },
679       modelError : function(err) {
680         modelObserver.modelError(err);
681       },
682       progression : function(part, info, percentage) {
683         modelObserver.progression(part, info, percentage);
684       }
685     };
686   modelManager.modelLoaders [0].load(contentUrl, synchronous, modelLoadingObserver);
687 }
688 
689   /**
690  * Updates the transparency of window panes shapes.
691  * @private
692  */
693 ModelManager.prototype.updateWindowPanesTransparency = function(node) {
694   if (node instanceof Group3D) {
695     for (var i = 0; i < node.children.length; i++) {
696       this.updateWindowPanesTransparency(node.children [i]);
697     }
698   } else if (node instanceof Link3D) {
699     this.updateWindowPanesTransparency(node.getSharedGroup());
700   } else if (node instanceof Shape3D) {
701     var name = node.getName();
702     if (name 
703         && name.indexOf(ModelManager.WINDOW_PANE_SHAPE_PREFIX) === 0) {
704       var appearance = node.getAppearance();
705       if (appearance === null) {
706         appearance = new Appearance3D();
707         node.setAppearance(appearance);
708       }
709       if (appearance.getTransparency() === undefined) {
710         appearance.setTransparency(0.5);
711       }
712     }
713   }
714 }
715 
716 /**
717  * Updates the hierarchy of nodes with intermediate pickable nodes to help deforming models.
718  * @param {Group3D} group
719  * @private 
720 */
721 ModelManager.prototype.updateDeformableModelHierarchy = function(group) {
722   // Try to reorganize node hierarchy of mannequin model
723   if (this.containsNode(group, ModelManager.MANNEQUIN_ABDOMEN_PREFIX)
724       && this.containsNode(group, ModelManager.MANNEQUIN_CHEST_PREFIX)
725       && this.containsNode(group, ModelManager.MANNEQUIN_PELVIS_PREFIX)
726       && this.containsNode(group, ModelManager.MANNEQUIN_NECK_PREFIX)
727       && this.containsNode(group, ModelManager.MANNEQUIN_HEAD_PREFIX)
728       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_SHOULDER_PREFIX)
729       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_ARM_PREFIX)
730       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_ELBOW_PREFIX)
731       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_FOREARM_PREFIX)
732       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_WRIST_PREFIX)
733       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_HAND_PREFIX)
734       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_HIP_PREFIX)
735       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_THIGH_PREFIX)
736       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_KNEE_PREFIX)
737       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_LEG_PREFIX)
738       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_ANKLE_PREFIX)
739       && this.containsNode(group, ModelManager.MANNEQUIN_LEFT_FOOT_PREFIX)
740       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_SHOULDER_PREFIX)
741       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_ARM_PREFIX)
742       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_ELBOW_PREFIX)
743       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_FOREARM_PREFIX)
744       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_WRIST_PREFIX)
745       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_HAND_PREFIX)
746       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_HIP_PREFIX)
747       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_THIGH_PREFIX)
748       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_KNEE_PREFIX)
749       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_LEG_PREFIX)
750       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_ANKLE_PREFIX)
751       && this.containsNode(group, ModelManager.MANNEQUIN_RIGHT_FOOT_PREFIX)) {
752     // Head
753     var head = this.extractNodes(group, ModelManager.MANNEQUIN_HEAD_PREFIX, null);
754     var headGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_NECK_PREFIX, [head]);
755 
756     // Left arm
757     var leftHand = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_HAND_PREFIX, null);
758     var leftHandGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_LEFT_WRIST_PREFIX, [leftHand]);
759     var leftForearm = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_FOREARM_PREFIX, null);
760     var leftWrist = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_WRIST_PREFIX, null);
761     var leftForearmGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_LEFT_ELBOW_PREFIX, [leftForearm, leftWrist, leftHandGroup]);
762     var leftArm = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_ARM_PREFIX, null);
763     var leftElbow = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_ELBOW_PREFIX, null);
764     var leftArmGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_LEFT_SHOULDER_PREFIX, [leftArm, leftElbow, leftForearmGroup]);
765 
766     // Right arm
767     var rightHand = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_HAND_PREFIX, null);
768     var rightHandGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_RIGHT_WRIST_PREFIX, [rightHand]);
769     var rightForearm = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_FOREARM_PREFIX, null);
770     var rightWrist = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_WRIST_PREFIX, null);
771     var rightForearmGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_RIGHT_ELBOW_PREFIX, [rightForearm, rightWrist, rightHandGroup]);
772     var rightArm = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_ARM_PREFIX, null);
773     var rightElbow = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_ELBOW_PREFIX, null);
774     var rightArmGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_RIGHT_SHOULDER_PREFIX, [rightArm, rightElbow, rightForearmGroup]);
775 
776     // Chest
777     var chest = this.extractNodes(group, ModelManager.MANNEQUIN_CHEST_PREFIX, null);
778     var leftShoulder = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_SHOULDER_PREFIX, null);
779     var rightShoulder = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_SHOULDER_PREFIX, null);
780     var neck = this.extractNodes(group, ModelManager.MANNEQUIN_NECK_PREFIX, null);
781     var chestGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_ABDOMEN_CHEST_PREFIX, [chest, leftShoulder, leftArmGroup, rightShoulder, rightArmGroup, neck, headGroup]);
782 
783     // Left leg
784     var leftFoot = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_FOOT_PREFIX, null);
785     var leftFootGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_LEFT_ANKLE_PREFIX, [leftFoot]);
786     var leftLeg = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_LEG_PREFIX, null);
787     var leftAnkle = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_ANKLE_PREFIX, null);
788     var leftLegGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_LEFT_KNEE_PREFIX, [leftLeg, leftAnkle, leftFootGroup]);
789     var leftThigh = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_THIGH_PREFIX, null);
790     var leftKnee = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_KNEE_PREFIX, null);
791     var leftThighGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_LEFT_HIP_PREFIX, [leftThigh, leftKnee, leftLegGroup]);
792 
793     // Right leg
794     var rightFoot = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_FOOT_PREFIX, null);
795     var rightFootGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_RIGHT_ANKLE_PREFIX, [rightFoot]);
796     var rightLeg = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_LEG_PREFIX, null);
797     var rightAnkle = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_ANKLE_PREFIX, null);
798     var rightLegGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_RIGHT_KNEE_PREFIX, [rightLeg, rightAnkle, rightFootGroup]);
799     var rightThigh = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_THIGH_PREFIX, null);
800     var rightKnee = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_KNEE_PREFIX, null);
801     var rightThighGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_RIGHT_HIP_PREFIX, [rightThigh, rightKnee, rightLegGroup]);
802 
803     // Pelvis
804     var pelvis = this.extractNodes(group, ModelManager.MANNEQUIN_PELVIS_PREFIX, null);
805     var leftHip = this.extractNodes(group, ModelManager.MANNEQUIN_LEFT_HIP_PREFIX, null);
806     var rightHip = this.extractNodes(group, ModelManager.MANNEQUIN_RIGHT_HIP_PREFIX, null);
807     var pelvisGroup = this.createPickableTransformGroup(ModelManager.MANNEQUIN_ABDOMEN_PELVIS_PREFIX, [pelvis, leftHip, leftThighGroup, rightHip, rightThighGroup]);
808 
809     var abdomen = this.extractNodes(group, ModelManager.MANNEQUIN_ABDOMEN_PREFIX, null);
810     group.addChild(abdomen);
811     group.addChild(chestGroup);
812     group.addChild(pelvisGroup);
813   } else {
814     // Reorganize rotating openings
815     this.updateSimpleDeformableModelHierarchy(group, null, ModelManager.HINGE_PREFIX, ModelManager.OPENING_ON_HINGE_PREFIX, ModelManager.WINDOW_PANE_ON_HINGE_PREFIX, ModelManager.MIRROR_ON_HINGE_PREFIX);
816     this.updateSimpleDeformableModelHierarchy(group, null, ModelManager.BALL_PREFIX, ModelManager.ARM_ON_BALL_PREFIX, null, null);
817     // Reorganize sliding openings
818     this.updateSimpleDeformableModelHierarchy(group, ModelManager.UNIQUE_RAIL_PREFIX, ModelManager.RAIL_PREFIX, ModelManager.OPENING_ON_RAIL_PREFIX, ModelManager.WINDOW_PANE_ON_RAIL_PREFIX, ModelManager.MIRROR_ON_RAIL_PREFIX);
819     // Reorganize sub hierarchies
820     var movedNodes = [];
821     while (this.updateDeformableModelSubTransformedHierarchy(group, group, [ModelManager.HINGE_PREFIX, ModelManager.BALL_PREFIX, ModelManager.RAIL_PREFIX],
822         [ModelManager.OPENING_ON_HINGE_PREFIX, ModelManager.ARM_ON_BALL_PREFIX, ModelManager.OPENING_ON_RAIL_PREFIX], movedNodes)) {
823     }
824   }
825 }
826 
827 /**
828  * @param {Group3D} group
829  * @param {string} uniqueReferenceNodePrefix
830  * @param {string} referenceNodePrefix
831  * @param {string} openingPrefix
832  * @param {string} openingPanePrefix
833  * @param {string} openingMirrorPrefix
834  * @private 
835  */
836 ModelManager.prototype.updateSimpleDeformableModelHierarchy = function(group, uniqueReferenceNodePrefix, referenceNodePrefix,
837                                                                        openingPrefix, openingPanePrefix, openingMirrorPrefix) {
838   if (this.containsNode(group, openingPrefix + 1)
839       || (openingPanePrefix !== null && this.containsNode(group, openingPanePrefix + 1))
840       || (openingMirrorPrefix !== null && this.containsNode(group, openingMirrorPrefix + 1))) {
841     if (this.containsNode(group, referenceNodePrefix + 1)) {
842       // Reorganize openings with multiple reference nodes
843       var i = 1;
844       do {
845         var referenceNode = this.extractNodes(group, referenceNodePrefix + i, null);
846         var opening = this.extractNodes(group, openingPrefix + i, null);
847         var openingPane = openingPanePrefix !== null ? this.extractNodes(group, openingPanePrefix + i, null) : null;
848         var openingMirror = openingMirrorPrefix !== null ? this.extractNodes(group, openingMirrorPrefix + i, null) : null;
849         var openingGroup = this.createPickableTransformGroup(referenceNodePrefix + i, [opening, openingPane, openingMirror]);
850         group.addChild(referenceNode);
851         group.addChild(openingGroup);
852         i++;
853       } while (this.containsNode(group, referenceNodePrefix + i)
854           && (this.containsNode(group, openingPrefix + i)
855               || (openingPanePrefix !== null && this.containsNode(group, openingPanePrefix + i))
856               || (openingMirrorPrefix !== null && this.containsNode(group, openingMirrorPrefix + i))));
857     } else if (uniqueReferenceNodePrefix !== null
858                && this.containsNode(group, uniqueReferenceNodePrefix)) {
859       // Reorganize openings with a unique reference node
860       var referenceNode = this.extractNodes(group, uniqueReferenceNodePrefix, null);
861       group.addChild(referenceNode);
862       var i = 1;
863       do {
864         var opening = this.extractNodes(group, openingPrefix + i, null);
865         var openingPane = this.extractNodes(group, openingPanePrefix + i, null);
866         var openingMirror = this.extractNodes(group, openingMirrorPrefix + i, null);
867         group.addChild(this.createPickableTransformGroup(referenceNodePrefix + i, [opening, openingPane, openingMirror]));
868         i++;
869       } while (this.containsNode(group, openingPrefix + i)
870                || this.containsNode(group, openingPanePrefix + i)
871                || this.containsNode(group, openingMirrorPrefix + i));
872     }
873   }
874 }
875 
876 /**
877  * Returns <code>true</code> if the given <code>node</code> or a node in its hierarchy
878  * contains a node which name, stored in user data, starts with <code>prefix</code>.
879  * @param {Node3D} node   a node
880  * @param {string} prefix a string
881  */
882 ModelManager.prototype.containsNode = function(node, prefix) {
883   var name = node.getName();
884   if (name !== null
885       && name.indexOf(prefix) === 0) {
886     return true;
887   }
888   if (node instanceof Group3D) {
889     for (var i = node.getChildren().length - 1; i >= 0; i--) {
890       if (this.containsNode(node.getChild(i), prefix)) {
891         return true;
892       }
893     }
894   }
895   return false;
896 }
897 
898 /**
899  * Searches among the given <code>node</code> and its children the nodes which name, stored in user data, starts with <code>name</code>,
900  * then returns a group containing the found nodes.
901  * @param {Node3D} node
902  * @param {string} name
903  * @param {Group3D} destinationGroup
904  * @private
905  */
906 ModelManager.prototype.extractNodes = function(node, name, destinationGroup) {
907   if (node.getName() !== null
908       && node.getName().indexOf(name) === 0) {
909     node.getParent().removeChild(node);
910     if (destinationGroup === null) {
911       destinationGroup = new Group3D();
912     }
913     destinationGroup.addChild(node);
914   }
915   if (node instanceof Group3D) {
916     // Enumerate children
917     for (var i = node.getChildren().length - 1; i >= 0; i--) {
918       destinationGroup = this.extractNodes(node.getChild(i), name, destinationGroup);
919     }
920   }
921   return destinationGroup;
922 }
923 
924 /**
925  * Returns a pickable group with its <code>children</code> and the given reference node as user data.
926  * @param {string} deformableGroupPrefix
927  * @param {Array}  children
928  * @private
929  */
930 ModelManager.prototype.createPickableTransformGroup = function(deformableGroupPrefix, children) {
931   var transformGroup = new TransformGroup3D();
932   transformGroup.setCapability(TransformGroup3D.ALLOW_TRANSFORM_WRITE);
933   transformGroup.setName(deformableGroupPrefix + ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX);
934   // Store the node around which objects should turn
935   for (var i = 0; i < children.length; i++) {
936     if (children [i] !== null) {
937       transformGroup.addChild(children [i]);
938     }
939   }
940   return transformGroup;
941 }
942 
943 /**
944  * Updates the first node found in the given <code>group</code> which specifies a transformation
945  * which should depend on another transformed node.
946  * @param {Group3D} group
947  * @param {Node3D}  node  
948  * @param {Array}   referenceNodePrefixes
949  * @param {Array}   subTransformationOpeningPrefixes
950  * @param {Array}   movedNodes
951  * @return {boolean} <code>true</code> if such a node was found and attached to another transformation
952  * @private
953  */
954 ModelManager.prototype.updateDeformableModelSubTransformedHierarchy = function(group, node, referenceNodePrefixes, subTransformationOpeningPrefixes,
955                                                                                movedNodes) {
956   if (group !== node
957       && movedNodes.indexOf(node) < 0) {
958     var name = node.getName();
959     if (name !== null) {
960       for (var i = 0; i < referenceNodePrefixes.length; i++) {
961         var prefix = referenceNodePrefixes [i];
962         if (name.indexOf(prefix) === 0) {
963           var index = name.indexOf(ModelManager.SUB_TRANSFORMATION_SEPARATOR);
964           if (index > 0) {
965             for (var j = 0; j < subTransformationOpeningPrefixes.length; j++) {
966               var subTransformationIndex = name.indexOf(subTransformationOpeningPrefixes [j], index + ModelManager.SUB_TRANSFORMATION_SEPARATOR.length);
967               if (subTransformationIndex >= 0) {
968                 if (movedNodes.indexOf(node) < 0) {
969                   movedNodes.push(node); 
970                 }
971                 var referenceNode = node.getParent();
972                 var parent = referenceNode.getParent();
973                 var nodeIndex = parent.getChildren().indexOf(referenceNode);
974                 var pickableGroup = parent.getChild(++nodeIndex);
975                 while (!(pickableGroup instanceof TransformGroup3D)) {
976                   pickableGroup = parent.getChild(++nodeIndex);
977                 }
978                 var lastDigitIndex = subTransformationIndex + subTransformationOpeningPrefixes [j].length;
979                 while (lastDigitIndex < name.length && name.charAt(lastDigitIndex) >= '0' && name.charAt(lastDigitIndex) <= '9') {
980                   lastDigitIndex++;
981                 }
982                 // Remove node and its sibling group and attach it to parent transformation
983                 if (this.attachNodesToPickableTransformGroup(group,
984                       referenceNodePrefixes [j] + name.substring(subTransformationIndex + subTransformationOpeningPrefixes [j].length, lastDigitIndex),
985                       [referenceNode, pickableGroup])) {
986                   return true;
987                 }
988               }
989             }
990           }
991         }
992       }
993     }
994   }
995   if (node instanceof Group3D) {
996     var children = node.getChildren();
997     for (var i = children.length - 1; i >= 0; i--) {
998       if (this.updateDeformableModelSubTransformedHierarchy(group, children [i], referenceNodePrefixes, subTransformationOpeningPrefixes, movedNodes)) {
999         return true;
1000       }
1001     }
1002   }
1003   return false;
1004 }
1005 
1006 /**
1007  * @param {Node3D} node  the root of a model
1008  * @param {string} groupPrefix
1009  * @param {Array}  movedNodes
1010  * @return {boolean}
1011  * @private
1012  */
1013 ModelManager.prototype.attachNodesToPickableTransformGroup = function(node, groupPrefix, movedNodes) {
1014   if (node instanceof TransformGroup3D
1015       && (groupPrefix + ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) == node.getName()) {
1016     var group = node;
1017     for (var i = 0; i < movedNodes.length; i++) {
1018       var movedNode = movedNodes [i];
1019       movedNode.getParent().removeChild(movedNode);
1020       group.addChild(movedNode);
1021     }
1022     return true;
1023   } else if (node instanceof Group3D) {
1024     var children = node.getChildren();
1025     for (var i = 0; i < children.length; i++) {
1026       if (this.attachNodesToPickableTransformGroup(children [i], groupPrefix, movedNodes)) {
1027         return true;
1028       }
1029     }
1030   }
1031   return false;
1032 }
1033 
1034 /**
1035  * Returns <code>true</code> if the given <code>node</code> or its children contains at least a deformable group.
1036  * @param {Node3D} node  the root of a model
1037  * @return {boolean}
1038  */
1039 ModelManager.prototype.containsDeformableNode = function(node) {
1040   if (node instanceof TransformGroup3D
1041       && node.getName() !== null
1042       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) >= 0
1043       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) === (node.getName().length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length)) {
1044     return true;
1045   } else if (node instanceof Group3D) {
1046     var children = node.getChildren();
1047     for (var i = 0; i < children.length; i++) {
1048       if (this.containsDeformableNode(children [i])) {
1049         return true;
1050       }
1051     }
1052   }
1053   return false;
1054 }
1055 
1056 /**
1057  * Returns <code>true</code> if the given <code>node</code> or its children contains is a deformed transformed group.
1058  * @param {Node3D} node  a node
1059  * @return {boolean}
1060  * @private
1061  */
1062 ModelManager.prototype.isDeformed = function(node) {
1063   if (node instanceof TransformGroup3D
1064       && node.getName() !== null
1065       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) >= 0 
1066       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) === (node.getName().length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length)) {
1067     var transform = mat4.create();
1068     node.getTransform(transform);
1069     return !TransformGroup3D.isIdentity(transform);
1070   } else if (node instanceof Group3D) {
1071     var children = node.getChildren();
1072     for (var i = 0; i < children.length; i++) {
1073       if (this.isDeformed(children [i])) {
1074         return true;
1075       }
1076     }
1077   }
1078   return false;
1079 }
1080 
1081 /**
1082  * Returns the materials used by the children shapes of the given <code>node</code>,
1083  * attributing their <code>creator</code> to them.
1084  * @param {Node3D} node
1085  * @param {boolean} ignoreEdgeColorMaterial
1086  * @param {string} [creator]
1087  */
1088 ModelManager.prototype.getMaterials = function(node, ignoreEdgeColorMaterial, creator) {
1089   if (creator === undefined) {
1090     if (ignoreEdgeColorMaterial === undefined) {
1091       ignoreEdgeColorMaterial = false;
1092       creator = null;
1093     } else {
1094       creator = ignoreEdgeColorMaterial;
1095     }
1096   }
1097 
1098   var appearances = [];
1099   this.searchAppearances(node, ignoreEdgeColorMaterial, appearances);
1100   var materials = [];
1101   for (var i = 0; i < appearances.length; i++) {
1102     var appearance = appearances[i];
1103     var color = null;
1104     var shininess = null;
1105     var diffuseColor = appearance.getDiffuseColor();
1106     if (diffuseColor != null) {
1107       color = 0xFF000000
1108           | (Math.round(diffuseColor[0] * 255) << 16)
1109           | (Math.round(diffuseColor[1] * 255) << 8)
1110           | Math.round(diffuseColor[2] * 255);
1111       shininess = appearance.getShininess() != null ? appearance.getShininess() / 128 : null;
1112     }
1113     var appearanceTexture = appearance.getTextureImage();
1114     var texture = null;
1115     if (appearanceTexture != null) {
1116       var textureImageUrl = appearanceTexture.url;
1117       if (textureImageUrl != null) {
1118         var textureImage = new SimpleURLContent(textureImageUrl);
1119         var textureImageName = textureImageUrl.substring(textureImageUrl.lastIndexOf('/') + 1);
1120         var lastPoint = textureImageName.lastIndexOf('.');
1121         if (lastPoint !== -1) {
1122           textureImageName = textureImageName.substring(0, lastPoint);
1123         }
1124         texture = new HomeTexture(
1125             new CatalogTexture(null, textureImageName, textureImage, -1, -1, creator));
1126       }
1127     }
1128     var materialName = appearance.getName();
1129     if (materialName === undefined) {
1130       materialName = null;
1131     }
1132     var homeMaterial = new HomeMaterial(materialName, color, texture, shininess);
1133     for (var j = 0; j < materials.length; j++) {
1134       if (materials [j].getName() == homeMaterial.getName()) {
1135         // Don't add twice materials with the same name
1136         homeMaterial = null;
1137         break;
1138       }
1139     }
1140     if (homeMaterial != null) {
1141       materials.push(homeMaterial);
1142     }
1143   }
1144   materials.sort(function (m1, m2) {
1145       var name1 = m1.getName();
1146       var name2 = m2.getName();
1147       if (name1 != null) {
1148         if (name2 != null) {
1149           return name1.localeCompare(name2);
1150         } else {
1151           return 1;
1152         }
1153       } else if (name2 != null) {
1154         return -1;
1155       } else {
1156         return 0;
1157       }
1158     });
1159   return materials;
1160 }
1161 
1162 /**
1163  * @param {Node3D}  node
1164  * @param {boolean} ignoreEdgeColorMaterial
1165  * @param {Array}   appearances
1166  * @private
1167  */
1168 ModelManager.prototype.searchAppearances = function(node, ignoreEdgeColorMaterial, appearances) {
1169   if (node instanceof Group3D) {
1170     var children = node.getChildren();
1171     for (var i = 0; i < children.length; i++) {
1172       this.searchAppearances(children [i], ignoreEdgeColorMaterial, appearances);
1173     }
1174   } else if (node instanceof Link3D) {
1175     this.searchAppearances(node.getSharedGroup(), ignoreEdgeColorMaterial, appearances);
1176   } else if (node instanceof Shape3D) {
1177     var appearance = node.getAppearance();
1178     if (appearance !== null 
1179         && (!ignoreEdgeColorMaterial
1180             || !(appearance.getName().indexOf(ModelManager.EDGE_COLOR_MATERIAL_PREFIX) === 0))
1181         && appearances.indexOf(appearance) === -1) {
1182       appearances.push(appearance);
1183     }
1184   }
1185 }
1186 
1187 /**
1188  * Replaces multiple shared shapes of the given <code>node</code> with one shape with transformed geometries.
1189  * @param {BranchGroup3D} modelRoot
1190  * @private
1191  */
1192 ModelManager.prototype.replaceMultipleSharedShapes = function(modelRoot) {
1193   var sharedShapes = [];
1194   this.searchSharedShapes(modelRoot, sharedShapes, false);
1195   for (var i = 0; i < sharedShapes.length; i++) {
1196     if (sharedShapes [i].value > 1) {
1197       var transformations = [];
1198       var shape = sharedShapes [i].key;
1199       this.searchShapeTransformations(modelRoot, shape, transformations, mat4.create());
1200       // Replace shared shape by a unique shape with transformed geometries
1201       var newShape = shape.clone();
1202       var geometries = newShape.getGeometries();
1203       for (var j = 0; j < geometries.length; j++) {
1204         var newGeometry = this.getTransformedGeometry(geometries [j], transformations);
1205         if (newGeometry === null) {
1206           return;
1207         }
1208         newShape.setGeometry(newGeometry, j);
1209       }
1210       this.removeSharedShape(modelRoot, shape);
1211       modelRoot.addChild(newShape);
1212     }
1213   }
1214 }
1215 
1216 /**
1217  * Searches all the shapes which are shared among the children of the given <code>node</code>.
1218  * @param {Node3D} node  a node
1219  * @param {Array}  sharedShapes 
1220  * @param {boolean} childOfSharedGroup
1221  * @private
1222  */
1223 ModelManager.prototype.searchSharedShapes = function(node, sharedShapes, childOfSharedGroup) {
1224   if (node instanceof Group3D) {
1225     var children = node.getChildren();
1226     for (var i = 0; i < children.length; i++) {
1227       this.searchSharedShapes(children [i], sharedShapes, childOfSharedGroup);
1228     }
1229   } else if (node instanceof Link3D) {
1230     this.searchSharedShapes(node.getSharedGroup(), sharedShapes, true);
1231   } else if (node instanceof Shape3D) {
1232     if (childOfSharedGroup) {
1233       for (var i = 0; i < sharedShapes.length; i++) {
1234         if (sharedShapes [i].key === node) {
1235           sharedShapes [i].value++;
1236           return;
1237         }
1238       }
1239       sharedShapes.push({key : node, value : 1});
1240     }
1241   }
1242 }
1243 
1244 /**
1245  * Searches all the transformations applied to a shared <code>shape</code> child of the given <b>node</b>.
1246  * @param {Node3D}  node  a node
1247  * @param {Shape3D} shape 
1248  * @param {mat4[]} transformations
1249  * @param {mat4}    parentTransformations
1250  */
1251 ModelManager.prototype.searchShapeTransformations = function(node, shape, transformations, parentTransformations) {
1252   if (node instanceof Group3D) {
1253     if (!(node instanceof TransformGroup3D)
1254         || !this.isDeformed(node)) {
1255       if (node instanceof TransformGroup3D) {
1256         parentTransformations = mat4.clone(parentTransformations);
1257         var transform = mat4.create();
1258         node.getTransform(transform);
1259         mat4.mul(parentTransformations, parentTransformations, transform);
1260       }
1261       var children = node.getChildren();
1262       for (var i = 0; i < children.length; i++) {
1263         this.searchShapeTransformations(children [i], shape, transformations, parentTransformations);
1264       }
1265     }
1266   } else if (node instanceof Link3D) {
1267     this.searchShapeTransformations(node.getSharedGroup(), shape, transformations, parentTransformations);
1268   } else if (node === shape) {
1269     transformations.push(parentTransformations);
1270   }
1271 }
1272 
1273 /**
1274  * Returns a new geometry where coordinates are transformed with the given transformations.
1275  * @param {IndexedGeometryArray3D} geometry
1276  * @param {mat4[]} transformations
1277  * @return {IndexedGeometryArray3D}
1278  */
1279 ModelManager.prototype.getTransformedGeometry = function(geometry, transformations) {
1280   var offsetIndex = 0;
1281   var offsetVertex = 0;
1282   var newVertexIndices = new Array(transformations.length * geometry.vertexIndices.length);
1283   for (var i = 0; i < transformations.length; i++) {
1284     for (var j = 0, n = geometry.vertexIndices.length; j < n; j++) {
1285       newVertexIndices [offsetIndex + j] = offsetVertex + geometry.vertexIndices [j];
1286     }
1287     offsetIndex += geometry.vertexIndices.length;
1288     offsetVertex += geometry.vertices.length;
1289   }
1290 
1291   var newTextureCoordinateIndices = new Array(transformations.length * geometry.textureCoordinateIndices.length);
1292   offsetIndex = 0;
1293   for (var i = 0; i < transformations.length; i++) {
1294     for (var j = 0, n = geometry.textureCoordinateIndices.length; j < n; j++) {
1295       newTextureCoordinateIndices [offsetIndex + j] = geometry.textureCoordinateIndices [j];
1296     }
1297     offsetIndex += geometry.textureCoordinateIndices.length;
1298   }
1299   
1300   offsetVertex = 0;
1301   var newVertices = new Array(transformations.length * geometry.vertices.length);
1302   for (var i = 0; i < transformations.length; i++) {
1303     for (var j = 0, n = geometry.vertices.length; j < n; j++) {
1304       var vertex = vec3.clone(geometry.vertices [j]);
1305       vec3.transformMat4(vertex, vertex, transformations [i]);
1306       newVertices [offsetVertex + j] = vertex;
1307     }
1308     offsetVertex += geometry.vertices.length;
1309   }
1310 
1311   if (geometry instanceof IndexedLineArray3D) {
1312     return new IndexedLineArray3D(newVertices, newVertexIndices, geometry.textureCoordinates, newTextureCoordinateIndices);
1313   } else if (geometry instanceof IndexedTriangleArray3D) {
1314     var newNormalIndices = new Array(transformations.length * geometry.normalIndices.length);
1315     offsetIndex = 0;
1316     var offsetNormal = 0;
1317     for (var i = 0; i < transformations.length; i++) {
1318       for (var j = 0, n = geometry.normalIndices.length; j < n; j++) {
1319         newNormalIndices [offsetIndex + j] = offsetNormal + geometry.normalIndices [j];
1320       }
1321       offsetIndex += geometry.normalIndices.length;
1322       offsetNormal += geometry.normals.length;
1323     }
1324     
1325     var offsetNormal = 0;
1326     var newNormals = new Array(transformations.length * geometry.normals.length);
1327     for (var i = 0; i < transformations.length; i++) {
1328       for (var j = 0, n = geometry.normals.length; j < n; j++) {
1329         var normal = vec3.clone(geometry.normals [j]);
1330         vec3.transformMat4(normal, normal, transformations [i]);
1331         vec3.normalize(normal, normal);
1332         newNormals [offsetNormal + j] = normal;
1333       }
1334       offsetNormal += geometry.normals.length;
1335     }
1336 
1337     return new IndexedTriangleArray3D(newVertices, newVertexIndices, geometry.textureCoordinates, newTextureCoordinateIndices, newNormals, newNormalIndices);
1338   } else {
1339     return null;
1340   }
1341 }
1342 
1343 /**
1344  * Removes the shared shape from the children of the given <code>node</code>.
1345  * @param {Node3D} node  a node
1346  * @param {Shape3D} shape 
1347  */
1348 ModelManager.prototype.removeSharedShape = function(node, shape) {
1349   if (node instanceof Group3D) {
1350     if (!(node instanceof TransformGroup3D)
1351         || !this.isDeformed(node)) {
1352       var children = node.getChildren();
1353       for (var i = children.length - 1; i >= 0; i--) {
1354         this.removeSharedShape(children [i], shape);
1355       }
1356       if (children.length === 0
1357           && node.getParent() instanceof Group3D) {
1358         node.getParent().removeChild(node);
1359       }
1360     }
1361   } else if (node instanceof Link3D) {
1362     var sharedGroup = node.getSharedGroup();
1363     this.removeSharedShape(sharedGroup, shape);
1364     if (sharedGroup.children.length == 0) {
1365       node.getParent().removeChild(node);
1366     }
1367   } else if (node === shape) {
1368     node.getParent().removeChild(node);
1369   }
1370 }
1371 
1372 /**
1373  * Returns the shape matching the given cut out shape if not <code>null</code> 
1374  * or the 2D area of the 3D shapes children of the <code>node</code> 
1375  * projected on its front side. The returned area is normalized in a 1 unit square
1376  * centered at the origin.
1377  */
1378 ModelManager.prototype.getFrontArea = function(cutOutShape, node) {
1379   var frontArea; 
1380   if (cutOutShape !== null) {
1381     frontArea = new java.awt.geom.Area(this.getShape(cutOutShape));
1382     frontArea.transform(java.awt.geom.AffineTransform.getScaleInstance(1, -1));
1383     frontArea.transform(java.awt.geom.AffineTransform.getTranslateInstance(-0.5, 0.5));
1384   } else {
1385     var vertexCount = this.getVertexCount(node);
1386     if (vertexCount < 1000000) {
1387       var frontAreaWithHoles = new java.awt.geom.Area();
1388       this.computeBottomOrFrontArea(node, frontAreaWithHoles, mat4.create(), false, false);
1389       frontArea = new java.awt.geom.Area();
1390       var currentPathPoints = [];
1391       var previousRoomPoint = null;
1392       for (var it = frontAreaWithHoles.getPathIterator(null, 1); !it.isDone(); it.next()) {
1393         var areaPoint = [0, 0];
1394         switch (it.currentSegment(areaPoint)) {
1395           case java.awt.geom.PathIterator.SEG_MOVETO :
1396           case java.awt.geom.PathIterator.SEG_LINETO :
1397             if (previousRoomPoint === null 
1398                 || areaPoint[0] !== previousRoomPoint[0] 
1399                 || areaPoint[1] !== previousRoomPoint[1]) {
1400               currentPathPoints.push(areaPoint);
1401             }
1402             previousRoomPoint = areaPoint;
1403             break;
1404           case java.awt.geom.PathIterator.SEG_CLOSE :
1405             if (currentPathPoints[0][0] === previousRoomPoint[0] 
1406                 && currentPathPoints[0][1] === previousRoomPoint[1]) {
1407               currentPathPoints.splice(currentPathPoints.length - 1, 1);
1408             }
1409             if (currentPathPoints.length > 2) {
1410               var pathPoints = currentPathPoints.slice(0);
1411               var subRoom = new Room(pathPoints);
1412               if (subRoom.getArea() > 0) {
1413                 if (!subRoom.isClockwise()) {
1414                   var currentPath = new java.awt.geom.GeneralPath();
1415                   currentPath.moveTo(pathPoints[0][0], pathPoints[0][1]);
1416                   for (var i = 1; i < pathPoints.length; i++) {
1417                     currentPath.lineTo(pathPoints[i][0], pathPoints[i][1]);
1418                   }
1419                   currentPath.closePath();
1420                   frontArea.add(new java.awt.geom.Area(currentPath));
1421                 }
1422               }
1423             }
1424             currentPathPoints.length = 0;
1425             previousRoomPoint = null;
1426             break;
1427         }
1428       }
1429       var bounds = frontAreaWithHoles.getBounds2D();
1430       frontArea.transform(java.awt.geom.AffineTransform.getTranslateInstance(-bounds.getCenterX(), -bounds.getCenterY()));
1431       frontArea.transform(java.awt.geom.AffineTransform.getScaleInstance(1 / bounds.getWidth(), 1 / bounds.getHeight()));
1432     }
1433     else {
1434       frontArea = new java.awt.geom.Area(new java.awt.geom.Rectangle2D.Float(-0.5, -0.5, 1, 1));
1435     }
1436   }
1437   return frontArea;
1438 }
1439 
1440 /**
1441  * Returns the 2D area of the 3D shapes children of the given scene 3D <code>node</code>
1442  * projected on the floor (plan y = 0), or of the given staircase if <code>node</code> is an
1443  * instance of <code>HomePieceOfFurniture</code>.
1444  * @param {Node3D|HomePieceOfFurniture} node
1445  * @return {Area}
1446  */
1447 ModelManager.prototype.getAreaOnFloor = function(node) {
1448   if (node instanceof Node3D) {
1449     var modelAreaOnFloor;
1450     var vertexCount = this.getVertexCount(node);
1451     if (vertexCount < 10000) {
1452       modelAreaOnFloor = new java.awt.geom.Area();
1453       this.computeBottomOrFrontArea(node, modelAreaOnFloor, mat4.create(), true, true);
1454     } else {
1455       var vertices = [];
1456       this.computeVerticesOnFloor(node, vertices, mat4.create());
1457       if (vertices.length > 0) {
1458         var surroundingPolygon = this.getSurroundingPolygon(vertices.slice(0));
1459         var generalPath = new java.awt.geom.GeneralPath(java.awt.geom.Path2D.WIND_NON_ZERO, surroundingPolygon.length);
1460         generalPath.moveTo(surroundingPolygon[0][0], surroundingPolygon[0][1]);
1461         for (var i = 0; i < surroundingPolygon.length; i++) {
1462           generalPath.lineTo(surroundingPolygon[i][0], surroundingPolygon[i][1]);
1463         }
1464         generalPath.closePath();
1465         modelAreaOnFloor = new java.awt.geom.Area(generalPath);
1466       } else {
1467         modelAreaOnFloor = new java.awt.geom.Area();
1468       }
1469     }
1470     return modelAreaOnFloor;
1471   } else {
1472     var staircase = node;
1473     if (staircase.getStaircaseCutOutShape() === null) {
1474       throw new IllegalArgumentException("No cut out shape associated to piece");
1475     }
1476     var shape = this.getShape(staircase.getStaircaseCutOutShape());
1477     var staircaseArea = new java.awt.geom.Area(shape);
1478     if (staircase.isModelMirrored()) {
1479       staircaseArea = this.getMirroredArea(staircaseArea);
1480     }
1481     var staircaseTransform = java.awt.geom.AffineTransform.getTranslateInstance(
1482             staircase.getX() - staircase.getWidth() / 2, 
1483             staircase.getY() - staircase.getDepth() / 2);
1484     staircaseTransform.concatenate(java.awt.geom.AffineTransform.getRotateInstance(staircase.getAngle(), 
1485             staircase.getWidth() / 2, staircase.getDepth() / 2));
1486     staircaseTransform.concatenate(java.awt.geom.AffineTransform.getScaleInstance(staircase.getWidth(), staircase.getDepth()));
1487     staircaseArea.transform(staircaseTransform);
1488     return staircaseArea;
1489   }
1490 }
1491 
1492 /**
1493  * Returns the total count of vertices in all geometries.
1494  * @param {Node3D} node
1495  * @return {number}
1496  * @private
1497  */
1498 ModelManager.prototype.getVertexCount = function(node) {
1499   var count = 0;
1500   if (node instanceof Group3D) {
1501     var children = node.getChildren();
1502     for (var i = 0; i < children.length; i++) {
1503       count += this.getVertexCount(children [i]);
1504     }
1505   } else if (node instanceof Link3D) {
1506     count = this.getVertexCount(node.getSharedGroup());
1507   } else if (node instanceof Shape3D) {
1508     var appearance = node.getAppearance();
1509     if (appearance.isVisible()) {
1510       var geometries = node.getGeometries(); 
1511       for (var i = 0, n = geometries.length; i < n; i++) {
1512         var geometry = geometries[i];
1513         count += geometry.vertices.length;
1514       }
1515     }
1516   }
1517   return count;
1518 }
1519 
1520 /**
1521  * Computes the 2D area on floor or on front side of the 3D shapes children of <code>node</code>.
1522  * @param {Node3D} node
1523  * @param {Area} nodeArea
1524  * @param {mat4} parentTransformations
1525  * @param {boolean} ignoreTransparentShapes
1526  * @param {boolean} bottom
1527  * @private
1528  */
1529 ModelManager.prototype.computeBottomOrFrontArea = function(node, nodeArea, parentTransformations, ignoreTransparentShapes, bottom) {
1530   if (node instanceof Group3D) {
1531     if (node instanceof TransformGroup3D) {
1532       parentTransformations = mat4.clone(parentTransformations);
1533       var transform = mat4.create();
1534       node.getTransform(transform);
1535       mat4.mul(parentTransformations, parentTransformations, transform);
1536     }
1537     var children = node.getChildren();
1538     for (var i = 0; i < children.length; i++) {
1539       this.computeBottomOrFrontArea(children [i], nodeArea, parentTransformations, ignoreTransparentShapes, bottom);
1540     }
1541   } else if (node instanceof Link3D) {
1542     this.computeBottomOrFrontArea(node.getSharedGroup(), nodeArea, parentTransformations, ignoreTransparentShapes, bottom);
1543   } else if (node instanceof Shape3D) {
1544     var appearance = node.getAppearance();
1545     if (appearance.isVisible() 
1546         && (!ignoreTransparentShapes
1547             || appearance.getTransparency() === undefined
1548             || appearance.getTransparency() < 1)) {
1549       var geometries = node.getGeometries(); 
1550       for (var i = 0, n = geometries.length; i < n; i++) {
1551         var geometry = geometries[i];
1552         this.computeBottomOrFrontGeometryArea(geometry, nodeArea, parentTransformations, bottom);
1553       }
1554     }
1555   }
1556 }
1557 
1558 /**
1559  * Computes the bottom area of a 3D geometry if <code>bottom</code> is <code>true</code>,
1560  * and the front area if not.
1561  * @param {IndexedGeometryArray3D} geometryArray
1562  * @param {Area} nodeArea
1563  * @param {mat4} parentTransformations
1564  * @param {boolean} bottom
1565  * @private
1566  */
1567 ModelManager.prototype.computeBottomOrFrontGeometryArea = function(geometryArray, nodeArea, parentTransformations, bottom) {
1568   if (geometryArray instanceof IndexedTriangleArray3D) {
1569     var vertexCount = geometryArray.vertices.length;
1570     var vertices = new Array(vertexCount * 2);
1571     var vertex = vec3.create();
1572     for (var index = 0, i = 0; index < vertices.length; i++) {
1573       vec3.copy(vertex, geometryArray.vertices [i]);
1574       vec3.transformMat4(vertex, vertex, parentTransformations);
1575       vertices[index++] = vertex[0];
1576       if (bottom) {
1577         vertices[index++] = vertex[2];
1578       } else {
1579         vertices[index++] = vertex[1];
1580       }
1581     }
1582     
1583     geometryPath = new java.awt.geom.GeneralPath(java.awt.geom.Path2D.WIND_NON_ZERO, 1000);
1584     for (var i = 0, triangleIndex = 0, n = geometryArray.vertexIndices.length; i < n; i += 3) {
1585       this.addTriangleToPath(geometryArray, geometryArray.vertexIndices [i], geometryArray.vertexIndices [i + 1], geometryArray.vertexIndices [i + 2], vertices, 
1586           geometryPath, triangleIndex++, nodeArea);
1587     }
1588     nodeArea.add(new java.awt.geom.Area(geometryPath));
1589   }
1590 }
1591 
1592 /**
1593  * Adds to <code>nodePath</code> the triangle joining vertices at
1594  * vertexIndex1, vertexIndex2, vertexIndex3 indices,
1595  * only if the triangle has a positive orientation.
1596  * @param {javax.media.j3d.GeometryArray} geometryArray
1597  * @param {number} vertexIndex1
1598  * @param {number} vertexIndex2
1599  * @param {number} vertexIndex3
1600  * @param {Array} vertices
1601  * @param {GeneralPath} geometryPath
1602  * @param {number} triangleIndex
1603  * @param {Area} nodeArea
1604  * @private
1605  */
1606 ModelManager.prototype.addTriangleToPath = function(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertices, geometryPath, triangleIndex, nodeArea) {
1607   var xVertex1 = vertices[2 * vertexIndex1];
1608   var yVertex1 = vertices[2 * vertexIndex1 + 1];
1609   var xVertex2 = vertices[2 * vertexIndex2];
1610   var yVertex2 = vertices[2 * vertexIndex2 + 1];
1611   var xVertex3 = vertices[2 * vertexIndex3];
1612   var yVertex3 = vertices[2 * vertexIndex3 + 1];
1613   if ((xVertex2 - xVertex1) * (yVertex3 - yVertex2) - (yVertex2 - yVertex1) * (xVertex3 - xVertex2) > 0) {
1614     if (triangleIndex > 0 && triangleIndex % 1000 === 0) {
1615       nodeArea.add(new java.awt.geom.Area(geometryPath));
1616       geometryPath.reset();
1617     }
1618     geometryPath.moveTo(xVertex1, yVertex1);
1619     geometryPath.lineTo(xVertex2, yVertex2);
1620     geometryPath.lineTo(xVertex3, yVertex3);
1621     geometryPath.closePath();
1622   }
1623 }
1624 
1625 /**
1626  * Computes the vertices coordinates projected on floor of the 3D shapes children of <code>node</code>.
1627  * @param {Node3D} node
1628  * @param {Array} vertices
1629  * @param {mat4} parentTransformations
1630  * @private
1631  */
1632 ModelManager.prototype.computeVerticesOnFloor = function (node, vertices, parentTransformations) {
1633   if (node instanceof Group3D) {
1634     if (node instanceof TransformGroup3D) {
1635       parentTransformations = mat4.clone(parentTransformations);
1636       var transform = mat4.create();
1637       node.getTransform(transform);
1638       mat4.mul(parentTransformations, parentTransformations, transform);
1639     }
1640     var children = node.getChildren();
1641     for (var i = 0; i < children.length; i++) {
1642       this.computeVerticesOnFloor(children [i], vertices, parentTransformations);
1643     }
1644   } else if (node instanceof Link3D) {
1645     this.computeVerticesOnFloor(node.getSharedGroup(), vertices, parentTransformations);
1646   } else if (node instanceof Shape3D) {
1647     var appearance = node.getAppearance();
1648     if (appearance.isVisible() 
1649         && (appearance.getTransparency() === undefined
1650             || appearance.getTransparency() < 1)) {
1651       var geometries = node.getGeometries(); 
1652       for (var i = 0, n = geometries.length; i < n; i++) {
1653         var geometryArray = geometries[i];
1654         var vertexCount = geometryArray.vertices.length;
1655         var vertex = vec3.create();
1656         for (var index = 0, j = 0; index < vertexCount; j++, index++) {
1657           vec3.copy(vertex, geometryArray.vertices [j]);
1658           vec3.transformMat4(vertex, vertex, parentTransformations);
1659           vertices.push([vertex[0], vertex[2]]);
1660         }
1661       }
1662     }
1663   }
1664 }
1665 
1666 /**
1667  * Returns the convex polygon that surrounds the given <code>vertices</code>.
1668  * From Andrew's monotone chain 2D convex hull algorithm described at
1669  * http://softsurfer.com/Archive/algorithm%5F0109/algorithm%5F0109.htm
1670  * @param {Array} vertices
1671  * @return {Array}
1672  * @private
1673  */
1674 ModelManager.prototype.getSurroundingPolygon = function(vertices) {
1675   vertices.sort(function (vertex1, vertex2) {
1676       var testedValue;
1677       if (vertex1[0] === vertex2[0]) {
1678         testedValue = vertex2[1] - vertex1[1];
1679       } else {
1680         testedValue = vertex2[0] - vertex1[0];
1681       }
1682       if (testedValue > 0) {
1683         return 1;
1684       } else if (testedValue < 0) {
1685         return -1;
1686       } else {
1687         return 0;
1688       }
1689     });
1690   var polygon = new Array(vertices.length);
1691   var bottom = 0;
1692   var top = -1;
1693   var i;
1694   
1695   var minMin = 0;
1696   var minMax;
1697   var xmin = vertices[0][0];
1698   for (i = 1; i < vertices.length; i++) {
1699     if (vertices[i][0] !== xmin) {
1700       break;
1701     }
1702   }
1703   minMax = i - 1;
1704   if (minMax === vertices.length - 1) {
1705     polygon[++top] = vertices[minMin];
1706     if (vertices[minMax][1] !== vertices[minMin][1]) {
1707       polygon[++top] = vertices[minMax];
1708     }
1709     polygon[++top] = vertices[minMin];
1710     var surroundingPolygon = new Array(top + 1);
1711     System.arraycopy(polygon, 0, surroundingPolygon, 0, surroundingPolygon.length);
1712     return surroundingPolygon;
1713   }
1714   
1715   var maxMin;
1716   var maxMax = vertices.length - 1;
1717   var xMax = vertices[vertices.length - 1][0];
1718   for (i = vertices.length - 2; i >= 0; i--) {
1719     if (vertices[i][0] !== xMax) {
1720       break;
1721     }
1722   }
1723   maxMin = i + 1;
1724   
1725   polygon[++top] = vertices[minMin];
1726   i = minMax;
1727   while (++i <= maxMin) {
1728     if (this.isLeft(vertices[minMin], vertices[maxMin], vertices[i]) >= 0 && i < maxMin) {
1729       continue;
1730     }
1731     while (top > 0) {
1732       if (this.isLeft(polygon[top - 1], polygon[top], vertices[i]) > 0) {
1733         break;
1734       } else {
1735         top--;
1736       }
1737     }
1738     polygon[++top] = vertices[i];
1739   }
1740 
1741   if (maxMax !== maxMin) {
1742     polygon[++top] = vertices[maxMax];
1743   }
1744   bottom = top;
1745   i = maxMin;
1746   while (--i >= minMax) {
1747     if (this.isLeft(vertices[maxMax], vertices[minMax], vertices[i]) >= 0 && i > minMax) {
1748       continue;
1749     }
1750     while (top > bottom) {
1751       if (this.isLeft(polygon[top - 1], polygon[top], vertices[i]) > 0) {
1752         break;
1753       } else {
1754         top--;
1755       }
1756     }
1757     polygon[++top] = vertices[i];
1758   }
1759   if (minMax !== minMin) {
1760     polygon[++top] = vertices[minMin];
1761   }
1762   var surroundingPolygon = new Array(top + 1);
1763   System.arraycopy(polygon, 0, surroundingPolygon, 0, surroundingPolygon.length);
1764   return surroundingPolygon;
1765 }
1766 
1767 ModelManager.prototype.isLeft = function(vertex0, vertex1, vertex2) {
1768   return (vertex1[0] - vertex0[0]) * (vertex2[1] - vertex0[1]) 
1769        - (vertex2[0] - vertex0[0]) * (vertex1[1] - vertex0[1]);
1770 }
1771 
1772 /**
1773  * Returns the mirror area of the given <code>area</code>.
1774  * @param {Area} area
1775  * @return {Area}
1776  * @private
1777  */
1778 ModelManager.prototype.getMirroredArea = function (area) {
1779   var mirrorPath = new java.awt.geom.GeneralPath();
1780   var point = [0, 0, 0, 0, 0, 0];
1781   for (var it = area.getPathIterator(null); !it.isDone(); it.next()) {
1782     switch (it.currentSegment(point)) {
1783     case java.awt.geom.PathIterator.SEG_MOVETO :
1784       mirrorPath.moveTo(1 - point[0], point[1]);
1785       break;
1786     case java.awt.geom.PathIterator.SEG_LINETO :
1787       mirrorPath.lineTo(1 - point[0], point[1]);
1788       break;
1789     case java.awt.geom.PathIterator.SEG_QUADTO :
1790       mirrorPath.quadTo(1 - point[0], point[1], 1 - point[2], point[3]);
1791       break;
1792     case java.awt.geom.PathIterator.SEG_CUBICTO :
1793       mirrorPath.curveTo(1 - point[0], point[1], 1 - point[2], point[3], 1 - point[4], point[5]);
1794       break;
1795     case java.awt.geom.PathIterator.SEG_CLOSE :
1796       mirrorPath.closePath();
1797       break;
1798     }
1799   }
1800   return new java.awt.geom.Area(mirrorPath);
1801 }
1802 
1803 /**
1804  * Returns the shape matching the given <a href="http://www.w3.org/TR/SVG/paths.html">SVG path shape</a>.
1805  * @param {string} svgPathShape
1806  * @return {Shape}
1807  */
1808 ModelManager.prototype.getShape = function(svgPathShape) {
1809   return ShapeTools.getShape(svgPathShape);
1810 }
1811