1 /*
  2  * HomePieceOfFurniture3D.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // Requires scene3d.js
 22 //          Object3DBranch.js
 23 //          ModelManager.js
 24 //          HomeObject.js
 25 //          HomePieceOfFurniture.js
 26 
 27 
 28 /**
 29  * Creates the 3D piece matching the given home <code>piece</code>.
 30  * @param {HomePieceOfFurniture} piece
 31  * @param {Home} home
 32  * @param {UserPreferences} [preferences]
 33  * @param {boolean|function} waitModelAndTextureLoadingEnd
 34  * @constructor
 35  * @extends Object3DBranch
 36  * @author Emmanuel Puybaret
 37  */
 38 function HomePieceOfFurniture3D(piece, home, preferences, waitModelAndTextureLoadingEnd) {
 39   if (waitModelAndTextureLoadingEnd === undefined) {
 40     // 3 parameters
 41     waitModelAndTextureLoadingEnd = preferences;
 42     preferences = null;
 43   }
 44   Object3DBranch.call(this, piece, home, preferences);
 45   
 46   this.createPieceOfFurnitureNode(piece, waitModelAndTextureLoadingEnd);
 47 }
 48 HomePieceOfFurniture3D.prototype = Object.create(Object3DBranch.prototype);
 49 HomePieceOfFurniture3D.prototype.constructor = HomePieceOfFurniture3D;
 50 
 51 HomePieceOfFurniture3D.DEFAULT_BOX = new Object();
 52 HomePieceOfFurniture3D.SELECTION_BOX_GEOMETRY = new IndexedLineArray3D(
 53     [vec3.fromValues(-0.5, -0.5, -0.5),
 54      vec3.fromValues(0.5, -0.5, -0.5),
 55      vec3.fromValues(0.5, 0.5, -0.5),
 56      vec3.fromValues(-0.5, 0.5, -0.5),
 57      vec3.fromValues(-0.5, -0.5, 0.5),
 58      vec3.fromValues(0.5, -0.5, 0.5),
 59      vec3.fromValues(0.5, 0.5, 0.5),
 60      vec3.fromValues(-0.5, 0.5, 0.5)],
 61     [0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7, 4, 6, 5, 7]);
 62 
 63 /**
 64  * Creates the piece node with its transform group and add it to the piece branch. 
 65  * @private
 66  */
 67 HomePieceOfFurniture3D.prototype.createPieceOfFurnitureNode = function(piece, waitModelAndTextureLoadingEnd) {
 68   var pieceTransformGroup = new TransformGroup3D();
 69   pieceTransformGroup.setCapability(Group3D.ALLOW_CHILDREN_EXTEND);
 70   pieceTransformGroup.setCapability(TransformGroup3D.ALLOW_TRANSFORM_WRITE);
 71   this.addChild(pieceTransformGroup);
 72   
 73   this.loadPieceOfFurnitureModel(waitModelAndTextureLoadingEnd);
 74 }
 75 
 76 /**
 77  * @private
 78  */
 79 HomePieceOfFurniture3D.prototype.loadPieceOfFurnitureModel = function(waitModelAndTextureLoadingEnd) {
 80   // While loading model use a temporary node that displays a white box  
 81   var waitBranch = new BranchGroup3D();
 82   var normalization = new TransformGroup3D();
 83   normalization.addChild(this.getModelBox(vec3.fromValues(1, 1, 1)));
 84   normalization.setUserData(PieceOfFurniture.IDENTITY_ROTATION);
 85   waitBranch.addChild(normalization);      
 86   
 87   var transformGroup = this.getChild(0);
 88   transformGroup.removeAllChildren();
 89   transformGroup.addChild(waitBranch);
 90   
 91   // Set piece model initial location, orientation and size      
 92   this.updatePieceOfFurnitureTransform();
 93   
 94   var piece = this.getUserData();
 95   // Store 3D model for possible future changes
 96   var model = piece.getModel();
 97   transformGroup.setUserData(model);
 98   // Load piece real 3D model
 99   var piece3D = this;
100   ModelManager.getInstance().loadModel(model, 
101       typeof waitModelAndTextureLoadingEnd == "function" ? false : waitModelAndTextureLoadingEnd, 
102       {
103         modelUpdated : function(modelRoot) {
104           piece3D.updateModelTransformations(modelRoot);
105   
106           var modelRotation = piece.getModelRotation();
107           // Add piece model scene to a normalized transform group
108           var modelTransformGroup = ModelManager.getInstance().getNormalizedTransformGroup(
109               modelRoot, modelRotation, 1, piece.isModelCenteredAtOrigin());
110           // Store model rotation for possible future changes
111           modelTransformGroup.setUserData(modelRotation);
112           piece3D.updatePieceOfFurnitureModelNode(modelRoot, modelTransformGroup, waitModelAndTextureLoadingEnd);            
113         },        
114         modelError : function(ex) {
115           // In case of problem use a default red box
116           piece3D.updatePieceOfFurnitureModelNode(piece3D.getModelBox(vec3.fromValues(1, 0, 0)), 
117               new TransformGroup3D(), waitModelAndTextureLoadingEnd);            
118         }
119       });
120 }
121 
122 /**
123  * Updates this branch from the home piece it manages.
124  */
125 HomePieceOfFurniture3D.prototype.update = function() {
126   if (this.isVisible()) {
127     var piece = this.getUserData();
128     var transformGroup = this.getChild(0);
129     var normalization = transformGroup.getChild(0).getChild(0);
130     if (piece.getModel().equals(transformGroup.getUserData())
131         && Object3DBranch.areModelRotationsEqual(piece.getModelRotation(), normalization.getUserData())) {
132       this.updatePieceOfFurnitureModelTransformations();
133       this.updatePieceOfFurnitureTransform();
134       this.updatePieceOfFurnitureColorAndTexture(false);      
135     } else {
136       this.loadPieceOfFurnitureModel(false);
137     }
138   }
139   this.updatePieceOfFurnitureVisibility();      
140 }
141 
142 /**
143  * Sets the transformation applied to piece model to match
144  * its location, its angle and its size.
145  * @private
146  */
147 HomePieceOfFurniture3D.prototype.updatePieceOfFurnitureTransform = function() {
148   var transformGroup = this.getChild(0); 
149   var pieceTransform = ModelManager.getInstance().getPieceOfFurnitureNormalizedModelTransformation(
150       this.getUserData(), transformGroup.getChild(0).getChild(0));
151   // Change model transformation      
152   transformGroup.setTransform(pieceTransform);
153 }
154 
155 /**
156  * Sets the color and the texture applied to piece model.
157  * @private
158  */
159 HomePieceOfFurniture3D.prototype.updatePieceOfFurnitureColorAndTexture = function(waitTextureLoadingEnd) {
160   var piece = this.getUserData();
161   var modelNode = this.getModelNode();
162   var modelChild = modelNode.getChild(0);
163   if (modelChild.getUserData() !== HomePieceOfFurniture3D.DEFAULT_BOX) {
164     if (piece.getColor() !== null) {
165       this.setColorAndTexture(modelNode, piece.getColor(), null, piece.getShininess(), null, piece.isModelMirrored(), piece.getModelFlags(), false, 
166           null, null, []);
167     } else if (piece.getTexture() !== null) {
168       this.setColorAndTexture(modelNode, null, piece.getTexture(), piece.getShininess(), null, piece.isModelMirrored(), piece.getModelFlags(), waitTextureLoadingEnd,
169           vec3.fromValues(piece.getWidth(), piece.getHeight(), piece.getDepth()), ModelManager.getInstance().getBounds(modelChild),
170           []);
171     } else if (piece.getModelMaterials() !== null) {
172       this.setColorAndTexture(modelNode, null, null, null, piece.getModelMaterials(), piece.isModelMirrored(), piece.getModelFlags(), waitTextureLoadingEnd,
173           vec3.fromValues(piece.getWidth(), piece.getHeight(), piece.getDepth()), ModelManager.getInstance().getBounds(modelChild), 
174           []);
175     } else {
176       // Set default material and texture of model
177       this.setColorAndTexture(modelNode, null, null, piece.getShininess(), null, piece.isModelMirrored(), piece.getModelFlags(), false, null, null, []);
178     }
179   }
180 }
181 
182 /**
183  * Returns the node of the filled model.
184  * @return {Node}
185  * @private
186  */
187 HomePieceOfFurniture3D.prototype.getModelNode = function() {
188   var transformGroup = this.getChild(0);
189   var branchGroup = transformGroup.getChild(0);
190   return branchGroup.getChild(0);
191 }
192 
193 /**
194  * Returns the selection node of the model.
195  * @return {Node}
196  * @private
197  */
198 HomePieceOfFurniture3D.prototype.getSelectionNode  = function() {
199   var transformGroup = this.getChild(0);
200   var branchGroup = transformGroup.getChild(0);
201   if (branchGroup.getChildren().length > 1
202       && branchGroup.getChild(1) instanceof Shape3D) {
203     return branchGroup.getChild(1);
204   } else {
205     return null;
206   }
207 }
208 
209 /**
210  * Sets whether this piece model is visible or not.
211  * @private
212  */
213 HomePieceOfFurniture3D.prototype.updatePieceOfFurnitureVisibility = function() {
214   var piece = this.getUserData();
215   // Update visibility of filled model shapes
216   var visible = this.isVisible();
217   var materials = piece.getColor() === null && piece.getTexture() === null
218       ? piece.getModelMaterials()
219       : null;
220   this.setVisible(this.getModelNode(), visible, piece.getModelFlags(), materials);
221   var selectionNode = this.getSelectionNode();
222   if (selectionNode != null) {
223     this.setVisible(selectionNode, this.getUserPreferences() != null
224         && this.getUserPreferences().isEditingIn3DViewEnabled()
225         && visible && this.getHome() != null
226         && this.isSelected(this.getHome().getSelectedItems()), 0, null);
227   }
228 }
229 
230 /**
231  * Sets the transformations applied to piece model parts.
232  * @private 
233  */
234 HomePieceOfFurniture3D.prototype.updatePieceOfFurnitureModelTransformations = function() {
235   var piece = this.getUserData();
236   var modelNode = this.getModelNode();
237   if (modelNode.getChild(0).getUserData() !== HomePieceOfFurniture3D.DEFAULT_BOX
238       && this.updateModelTransformations(this)) {
239     // Update normalized transform group
240     var modelTransform = ModelManager.getInstance().
241         getNormalizedTransform(modelNode.getChild(0), piece.getModelRotation(), 1, piece.isModelCenteredAtOrigin());
242     modelNode.setTransform(modelTransform);
243   }
244 }
245 
246 /**
247  * Sets the transformations applied to <code>node</code> children
248  * and returns <code>true</code> if a transformation was changed.
249  * @param {Node3D} node
250  * @private 
251  */
252 HomePieceOfFurniture3D.prototype.updateModelTransformations = function(node) {
253   var modifiedTransformations = false;
254   var transformations = this.getUserData().getModelTransformations();
255   var updatedTransformations = null;
256   if (transformations !== null) {
257     for (var i = 0; i < transformations.length; i++) {
258       var transformation = transformations [i];
259       var transformName = transformation.getName() + ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX;
260       if (updatedTransformations === null) {
261         updatedTransformations = [];
262       }
263       updatedTransformations.push(transformName);
264       modifiedTransformations |= this.updateTransformation(node, transformName, transformation.getMatrix());
265     }
266   }
267   modifiedTransformations |= this.setNotUpdatedTranformationsToIdentity(node, updatedTransformations);
268   return modifiedTransformations;
269 }
270 
271 /**
272  * Sets the transformation matrix of the children which user data is equal to <code>transformGroupName</code>.
273  * @param {Node3D} node
274  * @param {String} transformGroupName
275  * @param {Array}  matrix
276  * @private 
277  */
278 HomePieceOfFurniture3D.prototype.updateTransformation = function(node, transformGroupName, matrix) {
279   var modifiedTransformations = false;
280   if (node instanceof Group3D) {
281     if (node instanceof TransformGroup3D
282         && transformGroupName == node.getName()) {
283       var transformMatrix = mat4.create();
284       node.getTransform(transformMatrix);
285       if (matrix [0][0] !== transformMatrix [0]
286           || matrix [0][1] !== transformMatrix [4]
287           || matrix [0][2] !== transformMatrix [8]
288           || matrix [0][3] !== transformMatrix [12]
289           || matrix [1][0] !== transformMatrix [1]
290           || matrix [1][1] !== transformMatrix [5]
291           || matrix [1][2] !== transformMatrix [9]
292           || matrix [1][3] !== transformMatrix [13]
293           || matrix [2][0] !== transformMatrix [2]
294           || matrix [2][1] !== transformMatrix [6]
295           || matrix [2][2] !== transformMatrix [10]
296           || matrix [2][3] !== transformMatrix [14]) {
297         mat4.set(transformMatrix, 
298             matrix[0][0], matrix[1][0], matrix[2][0], 0,
299             matrix[0][1], matrix[1][1], matrix[2][1], 0,
300             matrix[0][2], matrix[1][2], matrix[2][2], 0,
301             matrix[0][3], matrix[1][3], matrix[2][3], 1);
302         node.setTransform(transformMatrix);
303         modifiedTransformations = true;
304       }
305     } else {
306       var children = node.getChildren(); 
307       for (var i = 0; i < children.length; i++) {
308         modifiedTransformations |= this.updateTransformation(children [i], transformGroupName, matrix);
309       }
310     }
311   }
312   // No Link parsing
313 
314   return modifiedTransformations;
315 }
316 
317 /**
318  * Sets the transformation matrix of the children which user data is not in <code>updatedTransformations</code> to identity.
319  * @param {Node3D} node
320  * @param {string[]} updatedTransformations
321  * @private 
322  */
323 HomePieceOfFurniture3D.prototype.setNotUpdatedTranformationsToIdentity = function(node, updatedTransformations) {
324   var modifiedTransformations = false;
325   if (node instanceof Group3D) {
326     if (node instanceof TransformGroup3D
327         && node.getName() !== null
328         && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) >= 0
329         && node.getName().indexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) === node.getName().length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length
330         && (updatedTransformations === null
331             || updatedTransformations.indexOf(node.getName()) < 0)) {
332       var group = node;
333       var transform = mat4.create();
334       group.getTransform(transform);
335       if (!TransformGroup3D.isIdentity(transform)) {
336         mat4.identity(transform);
337         group.setTransform(transform);
338         modifiedTransformations = true;
339       }
340     }
341     var children = node.getChildren(); 
342     for (var i = 0; i < children.length; i++) {
343       modifiedTransformations |= this.setNotUpdatedTranformationsToIdentity(children [i], updatedTransformations);
344     }
345   }
346 
347   return modifiedTransformations;
348 }
349 
350 /**
351  * Updates transform group children with <code>modelMode</code>.
352  * @private
353  */
354 HomePieceOfFurniture3D.prototype.updatePieceOfFurnitureModelNode = function(modelNode, normalization, waitTextureLoadingEnd) {
355   normalization.setCapability(TransformGroup3D.ALLOW_TRANSFORM_WRITE);
356   normalization.addChild(modelNode);
357   // Add model node to branch group
358   var modelBranch = new BranchGroup3D();
359   modelBranch.addChild(normalization);
360 
361   if (this.getHome() != null) {
362     // Add selection box node
363     var selectionBox = new Shape3D(HomePieceOfFurniture3D.SELECTION_BOX_GEOMETRY, this.getSelectionAppearance());
364     selectionBox.setPickable(false);
365     modelBranch.addChild(selectionBox);
366   }
367     
368   var piece = this.getUserData();
369   if (piece.isDoorOrWindow()) {
370     this.setTransparentShapeNotPickable(modelNode);
371   }
372 
373   var transformGroup = this.getChild(0);
374   // Remove previous nodes    
375   transformGroup.removeAllChildren();
376   // Add model branch to live scene
377   transformGroup.addChild(modelBranch);
378   if (piece.isHorizontallyRotated()) {
379     // Update piece transformation to ensure its center is correctly placed
380     this.updatePieceOfFurnitureTransform();
381   }
382 
383   // Flip normals if back faces of model are shown
384   if (piece.isBackFaceShown()) {
385     this.setBackFaceNormalFlip(this.getModelNode(), true);
386   }
387   // Update piece color, visibility and model mirror
388   this.modifiedTexturesCount = 0;
389   this.updatePieceOfFurnitureColorAndTexture(waitTextureLoadingEnd);      
390   this.updatePieceOfFurnitureVisibility();
391   // If no texture is customized, report loading end to waitTextureLoadingEnd
392   if (this.modifiedTexturesCount === 0 
393       && typeof waitTextureLoadingEnd == "function") {
394     waitTextureLoadingEnd(this);
395   }
396 }
397 
398 /**
399  * Returns a box that may replace model. 
400  * @private
401  */
402 HomePieceOfFurniture3D.prototype.getModelBox = function(color) {
403   var boxAppearance = new Appearance3D();
404   boxAppearance.setDiffuseColor(color);
405   boxAppearance.setAmbientColor(vec3.scale(vec3.create(), color, 0.7));
406   var box = new Box3D(0.5, 0.5, 0.5, boxAppearance);
407   box.setUserData(HomePieceOfFurniture3D.DEFAULT_BOX);
408   return box;
409 }
410 
411 /**
412  * Sets the material and texture attribute of all <code>Shape3D</code> children nodes of <code>node</code> 
413  * from the given <code>color</code> and <code>texture</code>. 
414  * @private
415  */
416 HomePieceOfFurniture3D.prototype.setColorAndTexture = function(node, color, texture, shininess, 
417                                                                materials, mirrored, modelFlags, waitTextureLoadingEnd, 
418                                                                pieceSize, modelBounds, modifiedAppearances) {
419   if (node instanceof Group3D) {
420     // Set material and texture of all children
421     var children = node.getChildren(); 
422     for (var i = 0; i < children.length; i++) {
423       this.setColorAndTexture(children [i], color, 
424           texture, shininess, materials, mirrored, modelFlags, waitTextureLoadingEnd, 
425           pieceSize, modelBounds, modifiedAppearances);
426     }
427   } else if (node instanceof Link3D) {
428     this.setColorAndTexture(node.getSharedGroup(), color,
429         texture, shininess, materials, mirrored, modelFlags, waitTextureLoadingEnd, 
430         pieceSize, modelBounds, modifiedAppearances);
431   } else if (node instanceof Shape3D) {
432     var shape = node;
433     var shapeName = shape.getName();
434     var appearance = shape.getAppearance();
435     if (appearance === null) {
436       appearance = new Appearance3D();
437       node.setAppearance(appearance);
438     }
439     
440     // Check appearance wasn't already changed
441     if (modifiedAppearances.indexOf(appearance) === -1) {
442       var defaultAppearance = null;
443       var colorModified = color !== null;
444       var textureModified = !colorModified 
445           && texture !== null;
446       var materialModified = !colorModified
447           && !textureModified
448           && materials !== null && materials.length > 0;
449       var appearanceModified = colorModified            
450           || textureModified
451           || materialModified
452           || shininess !== null
453           || mirrored
454           || modelFlags != 0;
455       var windowPane = shapeName !== null
456           && shapeName.indexOf(ModelManager.WINDOW_PANE_SHAPE_PREFIX) === 0;
457       if (!windowPane && appearanceModified            
458           || windowPane && materialModified) {
459         // Store shape default appearance 
460         // (global color or texture change doesn't have effect on window panes)
461         if (appearance.defaultAppearance === undefined) {
462           appearance.defaultAppearance = appearance.clone();
463         }
464         defaultAppearance = appearance.defaultAppearance;
465       }
466       var materialShininess = 0.;
467       if (appearanceModified) {
468         materialShininess = shininess !== null
469             ? shininess
470             : (appearance.getSpecularColor() !== undefined
471                 && appearance.getShininess() !== undefined
472                 ? appearance.getShininess() / 128
473                 : 0);
474       }
475       if (colorModified) {
476         // Change color only of shapes that are not window panes
477         if (windowPane) {
478           this.restoreDefaultAppearance(appearance, null);
479         } else {
480           // Change material if no default texture is displayed on the shape
481           // (textures always keep the colors of their image file)
482           this.updateAppearanceMaterial(appearance, color, color, materialShininess);
483           if (defaultAppearance.getTransparency() !== undefined) {
484             appearance.setTransparency(defaultAppearance.getTransparency());
485           }
486           if (defaultAppearance.getCullFace() !== undefined) {
487             appearance.setCullFace(defaultAppearance.getCullFace());
488           }
489           appearance.setTextureCoordinatesGeneration(defaultAppearance.getTextureCoordinatesGeneration());
490           appearance.setTextureImage(null);
491         }
492       } else if (textureModified) {            
493         // Change texture only of shapes that are not window panes
494         if (windowPane) {
495           this.restoreDefaultAppearance(appearance, null);
496         } else {
497           appearance.setTextureCoordinatesGeneration(this.getTextureCoordinates(appearance, texture, pieceSize, modelBounds));
498           this.updateTextureTransform(appearance, texture, true);
499           this.updateAppearanceMaterial(appearance, Object3DBranch.DEFAULT_COLOR, Object3DBranch.DEFAULT_AMBIENT_COLOR, materialShininess);
500           TextureManager.getInstance().loadTexture(texture.getImage(), 0,
501               typeof waitTextureLoadingEnd == "function" ? false : waitTextureLoadingEnd,
502               this.getTextureObserver(appearance, mirrored, modelFlags, waitTextureLoadingEnd));
503         }
504       } else if (materialModified) {
505         var materialFound = false;
506         // Apply color, texture and shininess of the material named as appearance name
507         for (var i = 0; i < materials.length; i++) {
508           var material = materials [i];
509           if (material !== null
510               && (material.getKey() != null
511                       && material.getKey() == appearance.getName()
512                   || material.getKey() == null
513                       && material.getName() == appearance.getName())) {
514             if (material.getShininess() !== null) {
515               materialShininess = material.getShininess();
516             }
517             color = material.getColor();                
518             if (color !== null
519                 && (color & 0xFF000000) != 0) {
520               this.updateAppearanceMaterial(appearance, color, color, materialShininess);
521               if (defaultAppearance.getTransparency() !== undefined) {
522                 appearance.setTransparency(defaultAppearance.getTransparency());
523               }
524               if (defaultAppearance.getCullFace() !== undefined) {
525                 appearance.setCullFace(defaultAppearance.getCullFace());
526               }
527               appearance.setTextureImage(null);
528             } else if (color === null && material.getTexture() !== null) {
529               var materialTexture = material.getTexture();
530               if (this.isTexturesCoordinatesDefined(shape)) {
531                 this.restoreDefaultTextureCoordinatesGeneration(appearance);
532                 this.updateTextureTransform(appearance, materialTexture);
533               } else {
534                 appearance.setTextureCoordinatesGeneration(this.getTextureCoordinates(appearance, material.getTexture(), pieceSize, modelBounds));
535                 this.updateTextureTransform(appearance, materialTexture, true);
536               }
537               this.updateAppearanceMaterial(appearance, Object3DBranch.DEFAULT_COLOR, Object3DBranch.DEFAULT_AMBIENT_COLOR, materialShininess);
538               var materialTexture = material.getTexture();
539               TextureManager.getInstance().loadTexture(materialTexture.getImage(), 0, 
540                   typeof waitTextureLoadingEnd == "function" ? false : waitTextureLoadingEnd,
541                   this.getTextureObserver(appearance, mirrored, modelFlags, waitTextureLoadingEnd));
542             } else {
543               this.restoreDefaultAppearance(appearance, material.getShininess());
544             }
545             materialFound = true;
546             break;
547           }
548         }
549         if (!materialFound) {
550           this.restoreDefaultAppearance(appearance, null);
551         }
552       } else {
553         this.restoreDefaultAppearance(appearance, shininess);
554       }
555 
556       this.setCullFace(appearance, mirrored, (modelFlags & PieceOfFurniture.SHOW_BACK_FACE) != 0);
557 
558       // Store modified appearances to avoid changing their values more than once
559       modifiedAppearances.push(appearance);
560     }
561   }
562 }
563 
564 /**
565  * Returns a texture observer that will update the given <code>appearance</code>.
566  * @private
567  */
568 HomePieceOfFurniture3D.prototype.getTextureObserver = function(appearance, mirrored, modelFlags, waitTextureLoadingEnd) {
569   var piece3D = this;
570   this.modifiedTexturesCount++;
571   return {
572       textureUpdated : function(textureImage) {
573         if (TextureManager.getInstance().isTextureTransparent(textureImage)) {
574           appearance.setCullFace(Appearance3D.CULL_NONE);
575         } else {
576           var defaultAppearance = appearance.defaultAppearance;
577           if (defaultAppearance !== null
578               && defaultAppearance.getCullFace() !== null) {
579             appearance.setCullFace(defaultAppearance.getCullFace());
580           }
581         }
582         if (appearance.getTextureImage() !== textureImage) {
583           appearance.setTextureImage(textureImage);
584         }
585 
586         piece3D.setCullFace(appearance, mirrored, (modelFlags & PieceOfFurniture.SHOW_BACK_FACE) != 0);
587         
588         // If all customized textures are loaded, report loading end to waitTextureLoadingEnd
589         if (--piece3D.modifiedTexturesCount === 0 
590             && typeof waitTextureLoadingEnd == "function") {
591           waitTextureLoadingEnd(piece3D);
592         }
593       },
594       textureError : function(error) {
595         return this.textureUpdated(TextureManager.getInstance().getErrorImage());
596       }
597     };
598 }
599 
600 /**
601  * Returns a texture coordinates generator that wraps the given texture on front face.
602  * @private
603  */
604 HomePieceOfFurniture3D.prototype.getTextureCoordinates = function(appearance, texture, pieceSize, modelBounds) {
605   var lower = vec3.create();
606   modelBounds.getLower(lower);
607   var upper = vec3.create();
608   modelBounds.getUpper(upper);
609   var minimumSize = ModelManager.getInstance().getMinimumSize();
610   var sx = pieceSize [0] / Math.max(upper [0] - lower [0], minimumSize);
611   var sw = -lower [0] * sx;
612   var ty = pieceSize [1] / Math.max(upper [1] - lower [1], minimumSize);
613   var tz = pieceSize [2] / Math.max(upper [2] - lower [2], minimumSize);
614   var tw = -lower [1] * ty + upper [2] * tz;
615   return {planeS : vec4.fromValues(sx, 0, 0, sw), 
616           planeT : vec4.fromValues(0, ty, -tz, tw)};
617 }
618 
619 /**
620  * Returns <code>true</code> if all the geometries of the given <code>shape</code> define some texture coordinates.
621  * @private
622  */
623 HomePieceOfFurniture3D.prototype.isTexturesCoordinatesDefined = function(shape) {
624   var geometries = shape.getGeometries();
625   for (var i = 0, n = geometries.length; i < n; i++) {
626     if (!geometries [i].hasTextureCoordinates()) {
627       return false;
628     }
629   }
630   return true;
631 }
632 
633 /**
634  * Sets the cull face of the given <code>appearance</code>.
635  * @private
636  */
637 HomePieceOfFurniture3D.prototype.setCullFace = function(appearance, mirrored, backFaceShown) {
638   // Change cull face 
639   if (appearance.getCullFace() !== Appearance3D.CULL_NONE) {
640     var cullFace = appearance.getCullFace() !== undefined 
641         ? appearance.getCullFace()
642         : Appearance3D.CULL_BACK;
643     var defaultCullFace = appearance.defaultCullFace; 
644     if (defaultCullFace === undefined) {
645       appearance.defaultCullFace = (defaultCullFace = cullFace);
646     }
647     appearance.setCullFace((mirrored ^ backFaceShown ^ defaultCullFace === Appearance3D.CULL_FRONT)
648         ? Appearance3D.CULL_FRONT 
649         : Appearance3D.CULL_BACK);
650   }
651 }
652 
653 /**
654  * Restores default material and texture of the given <code>appearance</code>.
655  * @private
656  */
657 HomePieceOfFurniture3D.prototype.restoreDefaultAppearance = function(appearance, shininess) {
658   if (appearance.defaultAppearance !== undefined) {
659     var defaultAppearance = appearance.defaultAppearance;
660     if (defaultAppearance.getAmbientColor() !== undefined) {
661       appearance.setAmbientColor(defaultAppearance.getAmbientColor());
662     }
663     if (defaultAppearance.getDiffuseColor() !== undefined) {
664       appearance.setDiffuseColor(defaultAppearance.getDiffuseColor());
665       if (shininess !== null) {
666         appearance.setSpecularColor(vec3.fromValues(shininess, shininess, shininess));
667         appearance.setShininess(shininess * 128);
668       } else {
669         appearance.setSpecularColor(defaultAppearance.getSpecularColor());
670         appearance.setShininess(defaultAppearance.getShininess());
671       }
672     }
673     if (defaultAppearance.getTransparency() !== undefined) {
674       appearance.setTransparency(defaultAppearance.getTransparency());
675     }
676     if (appearance.getCullFace() !== undefined) {
677       appearance.setCullFace(defaultAppearance.getCullFace());
678     }
679     if (defaultAppearance.getTextureCoordinatesGeneration() !== undefined) {
680       appearance.setTextureCoordinatesGeneration(defaultAppearance.getTextureCoordinatesGeneration());
681     }
682     if (appearance.getTextureImage() !== undefined) {
683       appearance.setTextureImage(defaultAppearance.getTextureImage());
684     }
685   }
686 }
687 
688 /**
689  * Restores default texture coordinates generation of the given <code>appearance</code>.
690  * @private
691  */
692 HomePieceOfFurniture3D.prototype.restoreDefaultTextureCoordinatesGeneration = function(appearance) {
693   if (appearance.defaultAppearance !== undefined) {
694     var defaultAppearance = appearance.defaultAppearance;
695     if (defaultAppearance.getTextureCoordinatesGeneration() !== undefined) {
696       appearance.setTextureCoordinatesGeneration(defaultAppearance.getTextureCoordinatesGeneration());
697     }
698   }
699 }
700 
701 /**
702  * Sets the visible attribute of the <code>Shape3D</code> children nodes of <code>node</code>.
703  * @private
704  */
705 HomePieceOfFurniture3D.prototype.setVisible = function(node, visible, modelFlags, materials) {
706   if (node instanceof Group3D) {
707     // Set visibility of all children
708     var children = node.getChildren(); 
709     for (var i = 0; i < children.length; i++) {
710       this.setVisible(children [i], visible, modelFlags, materials);
711     }
712   } else if (node instanceof Link3D) {
713     this.setVisible(node.getSharedGroup(), visible, modelFlags, materials);
714   } else if (node instanceof Shape3D) {
715     var shape = node;
716     var appearance = shape.getAppearance();
717     if (appearance === null) {
718       appearance = new Appearance3D();
719       node.setAppearance(appearance);
720     }
721     var shapeName = shape.getName();
722     if (visible 
723         && shapeName !== null
724         && shapeName.indexOf(ModelManager.LIGHT_SHAPE_PREFIX) === 0
725         && this.getHome() !== null
726         && !this.isSelected(this.getHome().getSelectedItems())
727         && (typeof HomeLight === "undefined"
728             || this.getUserData() instanceof HomeLight)) {
729       // Don't display light sources shapes of unselected lights
730       visible = false;
731     }
732     
733     if (visible) {
734       var appearanceName = appearance.getName();
735       if (appearanceName != null) {
736         if ((modelFlags & PieceOfFurniture.HIDE_EDGE_COLOR_MATERIAL) != 0
737             && appearanceName.indexOf(ModelManager.EDGE_COLOR_MATERIAL_PREFIX) === 0) {
738           visible = false;
739         } else if (materials != null) {
740           // Check whether the material color used by this shape isn't invisible 
741           for (var i = 0; i < materials.length; i++) {
742             var material = materials [i];
743             if (material !== null 
744                 && material.getName() == appearanceName) {
745               var color = material.getColor();  
746               visible = color === null
747                   || (color & 0xFF000000) !== 0;
748               break;
749             }
750           }
751         }
752       }
753     }  
754 
755     // Change visibility
756     appearance.setVisible(visible);
757   } 
758 } 
759 
760 /**
761  * Returns <code>true</code> if this 3D piece is visible.
762  * @private
763  */
764 HomePieceOfFurniture3D.prototype.isVisible = function() {
765   var piece = this.getUserData();
766   return piece.isVisible()
767       && (piece.getLevel() === null
768           || piece.getLevel().isViewableAndVisible());
769 }
770 
771 /**
772  * Returns <code>true</code> if this piece of furniture belongs to <code>selectedItems</code>.
773  * @private
774  */
775 HomePieceOfFurniture3D.prototype.isSelected = function(selectedItems) {
776   for (var i = 0; i < selectedItems.length; i++) {
777     var item = selectedItems [i];
778     if (item === this.getUserData()
779         || (item instanceof HomeFurnitureGroup
780             && this.isSelected(item.getFurniture()))) {
781       return true;
782     }
783   }
784   return false;
785 }
786 
787 /**
788  * Sets whether all <code>Shape3D</code> children nodes of <code>node</code> should have 
789  * their normal flipped or not.
790  * Caution !!! Should be executed only once per instance 
791  * @param backFaceNormalFlip <code>true</code> if normals should be flipped.
792  * @private
793  */
794 HomePieceOfFurniture3D.prototype.setBackFaceNormalFlip = function(node, backFaceNormalFlip) {
795   if (node instanceof Group3D) {
796     // Set back face normal flip of all children
797     var children = node.getChildren(); 
798     for (var i = 0; i < children.length; i++) {
799       this.setBackFaceNormalFlip(children [i], backFaceNormalFlip);
800     }
801   } else if (node instanceof Link3D) {
802     this.setBackFaceNormalFlip(node.getSharedGroup(), backFaceNormalFlip);
803   } else if (node instanceof Shape3D) {
804     var appearance = node.getAppearance();
805     if (appearance === null) {
806       appearance = new Appearance3D();
807       node.setAppearance(appearance);
808     }
809     // Change back face normal flip
810     appearance.setBackFaceNormalFlip(
811         backFaceNormalFlip ^ appearance.getCullFace() === Appearance3D.CULL_FRONT);
812   }
813 }
814 
815 /**
816  * Cancels the pickability of the <code>Shape3D</code> children nodes of <code>node</code> 
817  * when it uses a transparent appearance. 
818  * @private
819  */
820 HomePieceOfFurniture3D.prototype.setTransparentShapeNotPickable = function(node) {
821   if (node instanceof Group3D) {
822     var children = node.getChildren(); 
823     for (var i = 0; i < children.length; i++) {
824       this.setTransparentShapeNotPickable(children [i]);
825     }
826   } else if (node instanceof Link3D) {
827     this.setTransparentShapeNotPickable(node.getSharedGroup());
828   } else if (node instanceof Shape3D) {
829     var appearance = node.getAppearance();
830     if (appearance !== null
831         && appearance.getTransparency() > 0) {
832       node.setPickable(false);
833     }
834   }
835 }
836 
837