1 /*
  2  * ModelManager.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.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                 if (parent !== null) {
974                   var nodeIndex = parent.getChildren().indexOf(referenceNode);
975                   var pickableGroup = parent.getChild(++nodeIndex);
976                   while (!(pickableGroup instanceof TransformGroup3D)) {
977                     pickableGroup = parent.getChild(++nodeIndex);
978                   }
979                   var lastDigitIndex = subTransformationIndex + subTransformationOpeningPrefixes [j].length;
980                   while (lastDigitIndex < name.length && name.charAt(lastDigitIndex) >= '0' && name.charAt(lastDigitIndex) <= '9') {
981                     lastDigitIndex++;
982                   }
983                   // Remove node and its sibling group and attach it to parent transformation
984                   if (this.attachNodesToPickableTransformGroup(group,
985                         referenceNodePrefixes [j] + name.substring(subTransformationIndex + subTransformationOpeningPrefixes [j].length, lastDigitIndex),
986                         [referenceNode, pickableGroup])) {
987                     return true;
988                   }
989                 }
990               }
991             }
992           }
993         }
994       }
995     }
996   }
997   if (node instanceof Group3D) {
998     var children = node.getChildren();
999     for (var i = children.length - 1; i >= 0; i--) {
1000       if (this.updateDeformableModelSubTransformedHierarchy(group, children [i], referenceNodePrefixes, subTransformationOpeningPrefixes, movedNodes)) {
1001         return true;
1002       }
1003     }
1004   }
1005   return false;
1006 }
1007 
1008 /**
1009  * @param {Node3D} node  the root of a model
1010  * @param {string} groupPrefix
1011  * @param {Array}  movedNodes
1012  * @return {boolean}
1013  * @private
1014  */
1015 ModelManager.prototype.attachNodesToPickableTransformGroup = function(node, groupPrefix, movedNodes) {
1016   if (node instanceof TransformGroup3D
1017       && (groupPrefix + ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) == node.getName()) {
1018     var group = node;
1019     for (var i = 0; i < movedNodes.length; i++) {
1020       var movedNode = movedNodes [i];
1021       movedNode.getParent().removeChild(movedNode);
1022       group.addChild(movedNode);
1023     }
1024     return true;
1025   } else if (node instanceof Group3D) {
1026     var children = node.getChildren();
1027     for (var i = 0; i < children.length; i++) {
1028       if (this.attachNodesToPickableTransformGroup(children [i], groupPrefix, movedNodes)) {
1029         return true;
1030       }
1031     }
1032   }
1033   return false;
1034 }
1035 
1036 /**
1037  * Returns <code>true</code> if the given <code>node</code> or its children contains at least a deformable group.
1038  * @param {Node3D} node  the root of a model
1039  * @return {boolean}
1040  */
1041 ModelManager.prototype.containsDeformableNode = function(node) {
1042   if (node instanceof TransformGroup3D
1043       && node.getName() !== null
1044       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) >= 0
1045       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) === (node.getName().length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length)) {
1046     return true;
1047   } else if (node instanceof Group3D) {
1048     var children = node.getChildren();
1049     for (var i = 0; i < children.length; i++) {
1050       if (this.containsDeformableNode(children [i])) {
1051         return true;
1052       }
1053     }
1054   }
1055   return false;
1056 }
1057 
1058 /**
1059  * Returns <code>true</code> if the given <code>node</code> or its children contains is a deformed transformed group.
1060  * @param {Node3D} node  a node
1061  * @return {boolean}
1062  * @private
1063  */
1064 ModelManager.prototype.isDeformed = function(node) {
1065   if (node instanceof TransformGroup3D
1066       && node.getName() !== null
1067       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) >= 0 
1068       && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) === (node.getName().length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length)) {
1069     var transform = mat4.create();
1070     node.getTransform(transform);
1071     return !TransformGroup3D.isIdentity(transform);
1072   } else if (node instanceof Group3D) {
1073     var children = node.getChildren();
1074     for (var i = 0; i < children.length; i++) {
1075       if (this.isDeformed(children [i])) {
1076         return true;
1077       }
1078     }
1079   }
1080   return false;
1081 }
1082 
1083 /**
1084  * Returns the materials used by the children shapes of the given <code>node</code>,
1085  * attributing their <code>creator</code> to them.
1086  * @param {Node3D} node
1087  * @param {boolean} ignoreEdgeColorMaterial
1088  * @param {string} [creator]
1089  */
1090 ModelManager.prototype.getMaterials = function(node, ignoreEdgeColorMaterial, creator) {
1091   if (creator === undefined) {
1092     if (ignoreEdgeColorMaterial === undefined) {
1093       ignoreEdgeColorMaterial = false;
1094       creator = null;
1095     } else {
1096       creator = ignoreEdgeColorMaterial;
1097     }
1098   }
1099 
1100   var appearances = [];
1101   this.searchAppearances(node, ignoreEdgeColorMaterial, appearances);
1102   var materials = [];
1103   for (var i = 0; i < appearances.length; i++) {
1104     var appearance = appearances[i];
1105     var color = null;
1106     var shininess = null;
1107     var diffuseColor = appearance.getDiffuseColor();
1108     if (diffuseColor != null) {
1109       color = 0xFF000000
1110           | (Math.round(diffuseColor[0] * 255) << 16)
1111           | (Math.round(diffuseColor[1] * 255) << 8)
1112           | Math.round(diffuseColor[2] * 255);
1113       shininess = appearance.getShininess() != null ? appearance.getShininess() / 128 : null;
1114     }
1115     var appearanceTexture = appearance.getTextureImage();
1116     var texture = null;
1117     if (appearanceTexture != null) {
1118       var textureImageUrl = appearanceTexture.url;
1119       if (textureImageUrl != null) {
1120         var textureImage = new SimpleURLContent(textureImageUrl);
1121         var textureImageName = textureImageUrl.substring(textureImageUrl.lastIndexOf('/') + 1);
1122         var lastPoint = textureImageName.lastIndexOf('.');
1123         if (lastPoint !== -1) {
1124           textureImageName = textureImageName.substring(0, lastPoint);
1125         }
1126         texture = new HomeTexture(
1127             new CatalogTexture(null, textureImageName, textureImage, -1, -1, creator));
1128       }
1129     }
1130     var materialName = appearance.getName();
1131     if (materialName === undefined) {
1132       materialName = null;
1133     }
1134     var homeMaterial = new HomeMaterial(materialName, color, texture, shininess);
1135     for (var j = 0; j < materials.length; j++) {
1136       if (materials [j].getName() == homeMaterial.getName()) {
1137         // Don't add twice materials with the same name
1138         homeMaterial = null;
1139         break;
1140       }
1141     }
1142     if (homeMaterial != null) {
1143       materials.push(homeMaterial);
1144     }
1145   }
1146   materials.sort(function (m1, m2) {
1147       var name1 = m1.getName();
1148       var name2 = m2.getName();
1149       if (name1 != null) {
1150         if (name2 != null) {
1151           return name1.localeCompare(name2);
1152         } else {
1153           return 1;
1154         }
1155       } else if (name2 != null) {
1156         return -1;
1157       } else {
1158         return 0;
1159       }
1160     });
1161   return materials;
1162 }
1163 
1164 /**
1165  * @param {Node3D}  node
1166  * @param {boolean} ignoreEdgeColorMaterial
1167  * @param {Array}   appearances
1168  * @private
1169  */
1170 ModelManager.prototype.searchAppearances = function(node, ignoreEdgeColorMaterial, appearances) {
1171   if (node instanceof Group3D) {
1172     var children = node.getChildren();
1173     for (var i = 0; i < children.length; i++) {
1174       this.searchAppearances(children [i], ignoreEdgeColorMaterial, appearances);
1175     }
1176   } else if (node instanceof Link3D) {
1177     this.searchAppearances(node.getSharedGroup(), ignoreEdgeColorMaterial, appearances);
1178   } else if (node instanceof Shape3D) {
1179     var appearance = node.getAppearance();
1180     if (appearance !== null 
1181         && (!ignoreEdgeColorMaterial
1182             || !(appearance.getName().indexOf(ModelManager.EDGE_COLOR_MATERIAL_PREFIX) === 0))
1183         && appearances.indexOf(appearance) === -1) {
1184       appearances.push(appearance);
1185     }
1186   }
1187 }
1188 
1189 /**
1190  * Replaces multiple shared shapes of the given <code>node</code> with one shape with transformed geometries.
1191  * @param {BranchGroup3D} modelRoot
1192  * @private
1193  */
1194 ModelManager.prototype.replaceMultipleSharedShapes = function(modelRoot) {
1195   var sharedShapes = [];
1196   this.searchSharedShapes(modelRoot, sharedShapes, false);
1197   for (var i = 0; i < sharedShapes.length; i++) {
1198     if (sharedShapes [i].value > 1) {
1199       var transformations = [];
1200       var shape = sharedShapes [i].key;
1201       this.searchShapeTransformations(modelRoot, shape, transformations, mat4.create());
1202       // Replace shared shape by a unique shape with transformed geometries
1203       var newShape = shape.clone();
1204       var geometries = newShape.getGeometries();
1205       for (var j = 0; j < geometries.length; j++) {
1206         var newGeometry = this.getTransformedGeometry(geometries [j], transformations);
1207         if (newGeometry === null) {
1208           return;
1209         }
1210         newShape.setGeometry(newGeometry, j);
1211       }
1212       this.removeSharedShape(modelRoot, shape);
1213       modelRoot.addChild(newShape);
1214     }
1215   }
1216 }
1217 
1218 /**
1219  * Searches all the shapes which are shared among the children of the given <code>node</code>.
1220  * @param {Node3D} node  a node
1221  * @param {Array}  sharedShapes 
1222  * @param {boolean} childOfSharedGroup
1223  * @private
1224  */
1225 ModelManager.prototype.searchSharedShapes = function(node, sharedShapes, childOfSharedGroup) {
1226   if (node instanceof Group3D) {
1227     var children = node.getChildren();
1228     for (var i = 0; i < children.length; i++) {
1229       this.searchSharedShapes(children [i], sharedShapes, childOfSharedGroup);
1230     }
1231   } else if (node instanceof Link3D) {
1232     this.searchSharedShapes(node.getSharedGroup(), sharedShapes, true);
1233   } else if (node instanceof Shape3D) {
1234     if (childOfSharedGroup) {
1235       for (var i = 0; i < sharedShapes.length; i++) {
1236         if (sharedShapes [i].key === node) {
1237           sharedShapes [i].value++;
1238           return;
1239         }
1240       }
1241       sharedShapes.push({key : node, value : 1});
1242     }
1243   }
1244 }
1245 
1246 /**
1247  * Searches all the transformations applied to a shared <code>shape</code> child of the given <b>node</b>.
1248  * @param {Node3D}  node  a node
1249  * @param {Shape3D} shape 
1250  * @param {mat4[]} transformations
1251  * @param {mat4}    parentTransformations
1252  */
1253 ModelManager.prototype.searchShapeTransformations = function(node, shape, transformations, parentTransformations) {
1254   if (node instanceof Group3D) {
1255     if (!(node instanceof TransformGroup3D)
1256         || !this.isDeformed(node)) {
1257       if (node instanceof TransformGroup3D) {
1258         parentTransformations = mat4.clone(parentTransformations);
1259         var transform = mat4.create();
1260         node.getTransform(transform);
1261         mat4.mul(parentTransformations, parentTransformations, transform);
1262       }
1263       var children = node.getChildren();
1264       for (var i = 0; i < children.length; i++) {
1265         this.searchShapeTransformations(children [i], shape, transformations, parentTransformations);
1266       }
1267     }
1268   } else if (node instanceof Link3D) {
1269     this.searchShapeTransformations(node.getSharedGroup(), shape, transformations, parentTransformations);
1270   } else if (node === shape) {
1271     transformations.push(parentTransformations);
1272   }
1273 }
1274 
1275 /**
1276  * Returns a new geometry where coordinates are transformed with the given transformations.
1277  * @param {IndexedGeometryArray3D} geometry
1278  * @param {mat4[]} transformations
1279  * @return {IndexedGeometryArray3D}
1280  */
1281 ModelManager.prototype.getTransformedGeometry = function(geometry, transformations) {
1282   var offsetIndex = 0;
1283   var offsetVertex = 0;
1284   var newVertexIndices = new Array(transformations.length * geometry.vertexIndices.length);
1285   for (var i = 0; i < transformations.length; i++) {
1286     for (var j = 0, n = geometry.vertexIndices.length; j < n; j++) {
1287       newVertexIndices [offsetIndex + j] = offsetVertex + geometry.vertexIndices [j];
1288     }
1289     offsetIndex += geometry.vertexIndices.length;
1290     offsetVertex += geometry.vertices.length;
1291   }
1292 
1293   var newTextureCoordinateIndices = new Array(transformations.length * geometry.textureCoordinateIndices.length);
1294   offsetIndex = 0;
1295   for (var i = 0; i < transformations.length; i++) {
1296     for (var j = 0, n = geometry.textureCoordinateIndices.length; j < n; j++) {
1297       newTextureCoordinateIndices [offsetIndex + j] = geometry.textureCoordinateIndices [j];
1298     }
1299     offsetIndex += geometry.textureCoordinateIndices.length;
1300   }
1301   
1302   offsetVertex = 0;
1303   var newVertices = new Array(transformations.length * geometry.vertices.length);
1304   for (var i = 0; i < transformations.length; i++) {
1305     for (var j = 0, n = geometry.vertices.length; j < n; j++) {
1306       var vertex = vec3.clone(geometry.vertices [j]);
1307       vec3.transformMat4(vertex, vertex, transformations [i]);
1308       newVertices [offsetVertex + j] = vertex;
1309     }
1310     offsetVertex += geometry.vertices.length;
1311   }
1312 
1313   if (geometry instanceof IndexedLineArray3D) {
1314     return new IndexedLineArray3D(newVertices, newVertexIndices, geometry.textureCoordinates, newTextureCoordinateIndices);
1315   } else if (geometry instanceof IndexedTriangleArray3D) {
1316     var newNormalIndices = new Array(transformations.length * geometry.normalIndices.length);
1317     offsetIndex = 0;
1318     var offsetNormal = 0;
1319     for (var i = 0; i < transformations.length; i++) {
1320       for (var j = 0, n = geometry.normalIndices.length; j < n; j++) {
1321         newNormalIndices [offsetIndex + j] = offsetNormal + geometry.normalIndices [j];
1322       }
1323       offsetIndex += geometry.normalIndices.length;
1324       offsetNormal += geometry.normals.length;
1325     }
1326     
1327     var offsetNormal = 0;
1328     var newNormals = new Array(transformations.length * geometry.normals.length);
1329     for (var i = 0; i < transformations.length; i++) {
1330       for (var j = 0, n = geometry.normals.length; j < n; j++) {
1331         var normal = vec3.clone(geometry.normals [j]);
1332         vec3.transformMat4(normal, normal, transformations [i]);
1333         vec3.normalize(normal, normal);
1334         newNormals [offsetNormal + j] = normal;
1335       }
1336       offsetNormal += geometry.normals.length;
1337     }
1338 
1339     return new IndexedTriangleArray3D(newVertices, newVertexIndices, geometry.textureCoordinates, newTextureCoordinateIndices, newNormals, newNormalIndices);
1340   } else {
1341     return null;
1342   }
1343 }
1344 
1345 /**
1346  * Removes the shared shape from the children of the given <code>node</code>.
1347  * @param {Node3D} node  a node
1348  * @param {Shape3D} shape 
1349  */
1350 ModelManager.prototype.removeSharedShape = function(node, shape) {
1351   if (node instanceof Group3D) {
1352     if (!(node instanceof TransformGroup3D)
1353         || !this.isDeformed(node)) {
1354       var children = node.getChildren();
1355       for (var i = children.length - 1; i >= 0; i--) {
1356         this.removeSharedShape(children [i], shape);
1357       }
1358       if (children.length === 0
1359           && node.getParent() instanceof Group3D) {
1360         node.getParent().removeChild(node);
1361       }
1362     }
1363   } else if (node instanceof Link3D) {
1364     var sharedGroup = node.getSharedGroup();
1365     this.removeSharedShape(sharedGroup, shape);
1366     if (sharedGroup.children.length == 0) {
1367       node.getParent().removeChild(node);
1368     }
1369   } else if (node === shape) {
1370     node.getParent().removeChild(node);
1371   }
1372 }
1373 
1374 /**
1375  * Returns the shape matching the given cut out shape if not <code>null</code> 
1376  * or the 2D area of the 3D shapes children of the <code>node</code> 
1377  * projected on its front side. The returned area is normalized in a 1 unit square
1378  * centered at the origin.
1379  */
1380 ModelManager.prototype.getFrontArea = function(cutOutShape, node) {
1381   var frontArea; 
1382   if (cutOutShape !== null) {
1383     frontArea = new java.awt.geom.Area(this.getShape(cutOutShape));
1384     frontArea.transform(java.awt.geom.AffineTransform.getScaleInstance(1, -1));
1385     frontArea.transform(java.awt.geom.AffineTransform.getTranslateInstance(-0.5, 0.5));
1386   } else {
1387     var vertexCount = this.getVertexCount(node);
1388     if (vertexCount < 1000000) {
1389       var frontAreaWithHoles = new java.awt.geom.Area();
1390       this.computeBottomOrFrontArea(node, frontAreaWithHoles, mat4.create(), false, false);
1391       frontArea = new java.awt.geom.Area();
1392       var currentPathPoints = [];
1393       var previousRoomPoint = null;
1394       for (var it = frontAreaWithHoles.getPathIterator(null, 1); !it.isDone(); it.next()) {
1395         var areaPoint = [0, 0];
1396         switch (it.currentSegment(areaPoint)) {
1397           case java.awt.geom.PathIterator.SEG_MOVETO :
1398           case java.awt.geom.PathIterator.SEG_LINETO :
1399             if (previousRoomPoint === null 
1400                 || areaPoint[0] !== previousRoomPoint[0] 
1401                 || areaPoint[1] !== previousRoomPoint[1]) {
1402               currentPathPoints.push(areaPoint);
1403             }
1404             previousRoomPoint = areaPoint;
1405             break;
1406           case java.awt.geom.PathIterator.SEG_CLOSE :
1407             if (currentPathPoints[0][0] === previousRoomPoint[0] 
1408                 && currentPathPoints[0][1] === previousRoomPoint[1]) {
1409               currentPathPoints.splice(currentPathPoints.length - 1, 1);
1410             }
1411             if (currentPathPoints.length > 2) {
1412               var pathPoints = currentPathPoints.slice(0);
1413               var subRoom = new Room(pathPoints);
1414               if (subRoom.getArea() > 0) {
1415                 if (!subRoom.isClockwise()) {
1416                   var currentPath = new java.awt.geom.GeneralPath();
1417                   currentPath.moveTo(pathPoints[0][0], pathPoints[0][1]);
1418                   for (var i = 1; i < pathPoints.length; i++) {
1419                     currentPath.lineTo(pathPoints[i][0], pathPoints[i][1]);
1420                   }
1421                   currentPath.closePath();
1422                   frontArea.add(new java.awt.geom.Area(currentPath));
1423                 }
1424               }
1425             }
1426             currentPathPoints.length = 0;
1427             previousRoomPoint = null;
1428             break;
1429         }
1430       }
1431       var bounds = frontAreaWithHoles.getBounds2D();
1432       frontArea.transform(java.awt.geom.AffineTransform.getTranslateInstance(-bounds.getCenterX(), -bounds.getCenterY()));
1433       frontArea.transform(java.awt.geom.AffineTransform.getScaleInstance(1 / bounds.getWidth(), 1 / bounds.getHeight()));
1434     }
1435     else {
1436       frontArea = new java.awt.geom.Area(new java.awt.geom.Rectangle2D.Float(-0.5, -0.5, 1, 1));
1437     }
1438   }
1439   return frontArea;
1440 }
1441 
1442 /**
1443  * Returns the 2D area of the 3D shapes children of the given scene 3D <code>node</code>
1444  * projected on the floor (plan y = 0), or of the given staircase if <code>node</code> is an
1445  * instance of <code>HomePieceOfFurniture</code>.
1446  * @param {Node3D|HomePieceOfFurniture} node
1447  * @return {Area}
1448  */
1449 ModelManager.prototype.getAreaOnFloor = function(node) {
1450   if (node instanceof Node3D) {
1451     var modelAreaOnFloor;
1452     var vertexCount = this.getVertexCount(node);
1453     if (vertexCount < 10000) {
1454       modelAreaOnFloor = new java.awt.geom.Area();
1455       this.computeBottomOrFrontArea(node, modelAreaOnFloor, mat4.create(), true, true);
1456     } else {
1457       var vertices = [];
1458       this.computeVerticesOnFloor(node, vertices, mat4.create());
1459       if (vertices.length > 0) {
1460         var surroundingPolygon = this.getSurroundingPolygon(vertices.slice(0));
1461         var generalPath = new java.awt.geom.GeneralPath(java.awt.geom.Path2D.WIND_NON_ZERO, surroundingPolygon.length);
1462         generalPath.moveTo(surroundingPolygon[0][0], surroundingPolygon[0][1]);
1463         for (var i = 0; i < surroundingPolygon.length; i++) {
1464           generalPath.lineTo(surroundingPolygon[i][0], surroundingPolygon[i][1]);
1465         }
1466         generalPath.closePath();
1467         modelAreaOnFloor = new java.awt.geom.Area(generalPath);
1468       } else {
1469         modelAreaOnFloor = new java.awt.geom.Area();
1470       }
1471     }
1472     return modelAreaOnFloor;
1473   } else {
1474     var staircase = node;
1475     if (staircase.getStaircaseCutOutShape() === null) {
1476       throw new IllegalArgumentException("No cut out shape associated to piece");
1477     }
1478     var shape = this.getShape(staircase.getStaircaseCutOutShape());
1479     var staircaseArea = new java.awt.geom.Area(shape);
1480     if (staircase.isModelMirrored()) {
1481       staircaseArea = this.getMirroredArea(staircaseArea);
1482     }
1483     var staircaseTransform = java.awt.geom.AffineTransform.getTranslateInstance(
1484             staircase.getX() - staircase.getWidth() / 2, 
1485             staircase.getY() - staircase.getDepth() / 2);
1486     staircaseTransform.concatenate(java.awt.geom.AffineTransform.getRotateInstance(staircase.getAngle(), 
1487             staircase.getWidth() / 2, staircase.getDepth() / 2));
1488     staircaseTransform.concatenate(java.awt.geom.AffineTransform.getScaleInstance(staircase.getWidth(), staircase.getDepth()));
1489     staircaseArea.transform(staircaseTransform);
1490     return staircaseArea;
1491   }
1492 }
1493 
1494 /**
1495  * Returns the total count of vertices in all geometries.
1496  * @param {Node3D} node
1497  * @return {number}
1498  * @private
1499  */
1500 ModelManager.prototype.getVertexCount = function(node) {
1501   var count = 0;
1502   if (node instanceof Group3D) {
1503     var children = node.getChildren();
1504     for (var i = 0; i < children.length; i++) {
1505       count += this.getVertexCount(children [i]);
1506     }
1507   } else if (node instanceof Link3D) {
1508     count = this.getVertexCount(node.getSharedGroup());
1509   } else if (node instanceof Shape3D) {
1510     var appearance = node.getAppearance();
1511     if (appearance.isVisible()) {
1512       var geometries = node.getGeometries(); 
1513       for (var i = 0, n = geometries.length; i < n; i++) {
1514         var geometry = geometries[i];
1515         count += geometry.vertices.length;
1516       }
1517     }
1518   }
1519   return count;
1520 }
1521 
1522 /**
1523  * Computes the 2D area on floor or on front side of the 3D shapes children of <code>node</code>.
1524  * @param {Node3D} node
1525  * @param {Area} nodeArea
1526  * @param {mat4} parentTransformations
1527  * @param {boolean} ignoreTransparentShapes
1528  * @param {boolean} bottom
1529  * @private
1530  */
1531 ModelManager.prototype.computeBottomOrFrontArea = function(node, nodeArea, parentTransformations, ignoreTransparentShapes, bottom) {
1532   if (node instanceof Group3D) {
1533     if (node instanceof TransformGroup3D) {
1534       parentTransformations = mat4.clone(parentTransformations);
1535       var transform = mat4.create();
1536       node.getTransform(transform);
1537       mat4.mul(parentTransformations, parentTransformations, transform);
1538     }
1539     var children = node.getChildren();
1540     for (var i = 0; i < children.length; i++) {
1541       this.computeBottomOrFrontArea(children [i], nodeArea, parentTransformations, ignoreTransparentShapes, bottom);
1542     }
1543   } else if (node instanceof Link3D) {
1544     this.computeBottomOrFrontArea(node.getSharedGroup(), nodeArea, parentTransformations, ignoreTransparentShapes, bottom);
1545   } else if (node instanceof Shape3D) {
1546     var appearance = node.getAppearance();
1547     if (appearance.isVisible() 
1548         && (!ignoreTransparentShapes
1549             || appearance.getTransparency() === undefined
1550             || appearance.getTransparency() < 1)) {
1551       var geometries = node.getGeometries(); 
1552       for (var i = 0, n = geometries.length; i < n; i++) {
1553         var geometry = geometries[i];
1554         this.computeBottomOrFrontGeometryArea(geometry, nodeArea, parentTransformations, bottom);
1555       }
1556     }
1557   }
1558 }
1559 
1560 /**
1561  * Computes the bottom area of a 3D geometry if <code>bottom</code> is <code>true</code>,
1562  * and the front area if not.
1563  * @param {IndexedGeometryArray3D} geometryArray
1564  * @param {Area} nodeArea
1565  * @param {mat4} parentTransformations
1566  * @param {boolean} bottom
1567  * @private
1568  */
1569 ModelManager.prototype.computeBottomOrFrontGeometryArea = function(geometryArray, nodeArea, parentTransformations, bottom) {
1570   if (geometryArray instanceof IndexedTriangleArray3D) {
1571     var vertexCount = geometryArray.vertices.length;
1572     var vertices = new Array(vertexCount * 2);
1573     var vertex = vec3.create();
1574     for (var index = 0, i = 0; index < vertices.length; i++) {
1575       vec3.copy(vertex, geometryArray.vertices [i]);
1576       vec3.transformMat4(vertex, vertex, parentTransformations);
1577       vertices[index++] = vertex[0];
1578       if (bottom) {
1579         vertices[index++] = vertex[2];
1580       } else {
1581         vertices[index++] = vertex[1];
1582       }
1583     }
1584     
1585     geometryPath = new java.awt.geom.GeneralPath(java.awt.geom.Path2D.WIND_NON_ZERO, 1000);
1586     for (var i = 0, triangleIndex = 0, n = geometryArray.vertexIndices.length; i < n; i += 3) {
1587       this.addTriangleToPath(geometryArray, geometryArray.vertexIndices [i], geometryArray.vertexIndices [i + 1], geometryArray.vertexIndices [i + 2], vertices, 
1588           geometryPath, triangleIndex++, nodeArea);
1589     }
1590     nodeArea.add(new java.awt.geom.Area(geometryPath));
1591   }
1592 }
1593 
1594 /**
1595  * Adds to <code>nodePath</code> the triangle joining vertices at
1596  * vertexIndex1, vertexIndex2, vertexIndex3 indices,
1597  * only if the triangle has a positive orientation.
1598  * @param {javax.media.j3d.GeometryArray} geometryArray
1599  * @param {number} vertexIndex1
1600  * @param {number} vertexIndex2
1601  * @param {number} vertexIndex3
1602  * @param {Array} vertices
1603  * @param {GeneralPath} geometryPath
1604  * @param {number} triangleIndex
1605  * @param {Area} nodeArea
1606  * @private
1607  */
1608 ModelManager.prototype.addTriangleToPath = function(geometryArray, vertexIndex1, vertexIndex2, vertexIndex3, vertices, geometryPath, triangleIndex, nodeArea) {
1609   var xVertex1 = vertices[2 * vertexIndex1];
1610   var yVertex1 = vertices[2 * vertexIndex1 + 1];
1611   var xVertex2 = vertices[2 * vertexIndex2];
1612   var yVertex2 = vertices[2 * vertexIndex2 + 1];
1613   var xVertex3 = vertices[2 * vertexIndex3];
1614   var yVertex3 = vertices[2 * vertexIndex3 + 1];
1615   if ((xVertex2 - xVertex1) * (yVertex3 - yVertex2) - (yVertex2 - yVertex1) * (xVertex3 - xVertex2) > 0) {
1616     if (triangleIndex > 0 && triangleIndex % 1000 === 0) {
1617       nodeArea.add(new java.awt.geom.Area(geometryPath));
1618       geometryPath.reset();
1619     }
1620     geometryPath.moveTo(xVertex1, yVertex1);
1621     geometryPath.lineTo(xVertex2, yVertex2);
1622     geometryPath.lineTo(xVertex3, yVertex3);
1623     geometryPath.closePath();
1624   }
1625 }
1626 
1627 /**
1628  * Computes the vertices coordinates projected on floor of the 3D shapes children of <code>node</code>.
1629  * @param {Node3D} node
1630  * @param {Array} vertices
1631  * @param {mat4} parentTransformations
1632  * @private
1633  */
1634 ModelManager.prototype.computeVerticesOnFloor = function (node, vertices, parentTransformations) {
1635   if (node instanceof Group3D) {
1636     if (node instanceof TransformGroup3D) {
1637       parentTransformations = mat4.clone(parentTransformations);
1638       var transform = mat4.create();
1639       node.getTransform(transform);
1640       mat4.mul(parentTransformations, parentTransformations, transform);
1641     }
1642     var children = node.getChildren();
1643     for (var i = 0; i < children.length; i++) {
1644       this.computeVerticesOnFloor(children [i], vertices, parentTransformations);
1645     }
1646   } else if (node instanceof Link3D) {
1647     this.computeVerticesOnFloor(node.getSharedGroup(), vertices, parentTransformations);
1648   } else if (node instanceof Shape3D) {
1649     var appearance = node.getAppearance();
1650     if (appearance.isVisible() 
1651         && (appearance.getTransparency() === undefined
1652             || appearance.getTransparency() < 1)) {
1653       var geometries = node.getGeometries(); 
1654       for (var i = 0, n = geometries.length; i < n; i++) {
1655         var geometryArray = geometries[i];
1656         var vertexCount = geometryArray.vertices.length;
1657         var vertex = vec3.create();
1658         for (var index = 0, j = 0; index < vertexCount; j++, index++) {
1659           vec3.copy(vertex, geometryArray.vertices [j]);
1660           vec3.transformMat4(vertex, vertex, parentTransformations);
1661           vertices.push([vertex[0], vertex[2]]);
1662         }
1663       }
1664     }
1665   }
1666 }
1667 
1668 /**
1669  * Returns the convex polygon that surrounds the given <code>vertices</code>.
1670  * From Andrew's monotone chain 2D convex hull algorithm described at
1671  * http://softsurfer.com/Archive/algorithm%5F0109/algorithm%5F0109.htm
1672  * @param {Array} vertices
1673  * @return {Array}
1674  * @private
1675  */
1676 ModelManager.prototype.getSurroundingPolygon = function(vertices) {
1677   vertices.sort(function (vertex1, vertex2) {
1678       var testedValue;
1679       if (vertex1[0] === vertex2[0]) {
1680         testedValue = vertex2[1] - vertex1[1];
1681       } else {
1682         testedValue = vertex2[0] - vertex1[0];
1683       }
1684       if (testedValue > 0) {
1685         return 1;
1686       } else if (testedValue < 0) {
1687         return -1;
1688       } else {
1689         return 0;
1690       }
1691     });
1692   var polygon = new Array(vertices.length);
1693   var bottom = 0;
1694   var top = -1;
1695   var i;
1696   
1697   var minMin = 0;
1698   var minMax;
1699   var xmin = vertices[0][0];
1700   for (i = 1; i < vertices.length; i++) {
1701     if (vertices[i][0] !== xmin) {
1702       break;
1703     }
1704   }
1705   minMax = i - 1;
1706   if (minMax === vertices.length - 1) {
1707     polygon[++top] = vertices[minMin];
1708     if (vertices[minMax][1] !== vertices[minMin][1]) {
1709       polygon[++top] = vertices[minMax];
1710     }
1711     polygon[++top] = vertices[minMin];
1712     var surroundingPolygon = new Array(top + 1);
1713     System.arraycopy(polygon, 0, surroundingPolygon, 0, surroundingPolygon.length);
1714     return surroundingPolygon;
1715   }
1716   
1717   var maxMin;
1718   var maxMax = vertices.length - 1;
1719   var xMax = vertices[vertices.length - 1][0];
1720   for (i = vertices.length - 2; i >= 0; i--) {
1721     if (vertices[i][0] !== xMax) {
1722       break;
1723     }
1724   }
1725   maxMin = i + 1;
1726   
1727   polygon[++top] = vertices[minMin];
1728   i = minMax;
1729   while (++i <= maxMin) {
1730     if (this.isLeft(vertices[minMin], vertices[maxMin], vertices[i]) >= 0 && i < maxMin) {
1731       continue;
1732     }
1733     while (top > 0) {
1734       if (this.isLeft(polygon[top - 1], polygon[top], vertices[i]) > 0) {
1735         break;
1736       } else {
1737         top--;
1738       }
1739     }
1740     polygon[++top] = vertices[i];
1741   }
1742 
1743   if (maxMax !== maxMin) {
1744     polygon[++top] = vertices[maxMax];
1745   }
1746   bottom = top;
1747   i = maxMin;
1748   while (--i >= minMax) {
1749     if (this.isLeft(vertices[maxMax], vertices[minMax], vertices[i]) >= 0 && i > minMax) {
1750       continue;
1751     }
1752     while (top > bottom) {
1753       if (this.isLeft(polygon[top - 1], polygon[top], vertices[i]) > 0) {
1754         break;
1755       } else {
1756         top--;
1757       }
1758     }
1759     polygon[++top] = vertices[i];
1760   }
1761   if (minMax !== minMin) {
1762     polygon[++top] = vertices[minMin];
1763   }
1764   var surroundingPolygon = new Array(top + 1);
1765   System.arraycopy(polygon, 0, surroundingPolygon, 0, surroundingPolygon.length);
1766   return surroundingPolygon;
1767 }
1768 
1769 ModelManager.prototype.isLeft = function(vertex0, vertex1, vertex2) {
1770   return (vertex1[0] - vertex0[0]) * (vertex2[1] - vertex0[1]) 
1771        - (vertex2[0] - vertex0[0]) * (vertex1[1] - vertex0[1]);
1772 }
1773 
1774 /**
1775  * Returns the mirror area of the given <code>area</code>.
1776  * @param {Area} area
1777  * @return {Area}
1778  * @private
1779  */
1780 ModelManager.prototype.getMirroredArea = function (area) {
1781   var mirrorPath = new java.awt.geom.GeneralPath();
1782   var point = [0, 0, 0, 0, 0, 0];
1783   for (var it = area.getPathIterator(null); !it.isDone(); it.next()) {
1784     switch (it.currentSegment(point)) {
1785     case java.awt.geom.PathIterator.SEG_MOVETO :
1786       mirrorPath.moveTo(1 - point[0], point[1]);
1787       break;
1788     case java.awt.geom.PathIterator.SEG_LINETO :
1789       mirrorPath.lineTo(1 - point[0], point[1]);
1790       break;
1791     case java.awt.geom.PathIterator.SEG_QUADTO :
1792       mirrorPath.quadTo(1 - point[0], point[1], 1 - point[2], point[3]);
1793       break;
1794     case java.awt.geom.PathIterator.SEG_CUBICTO :
1795       mirrorPath.curveTo(1 - point[0], point[1], 1 - point[2], point[3], 1 - point[4], point[5]);
1796       break;
1797     case java.awt.geom.PathIterator.SEG_CLOSE :
1798       mirrorPath.closePath();
1799       break;
1800     }
1801   }
1802   return new java.awt.geom.Area(mirrorPath);
1803 }
1804 
1805 /**
1806  * Returns the shape matching the given <a href="http://www.w3.org/TR/SVG/paths.html">SVG path shape</a>.
1807  * @param {string} svgPathShape
1808  * @return {Shape}
1809  */
1810 ModelManager.prototype.getShape = function(svgPathShape) {
1811   return ShapeTools.getShape(svgPathShape);
1812 }
1813