1 /*
  2  * ModelPreviewComponent.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // Requires gl-matrix-min.js
 22 //          scene3d.js
 23 //          ModelManager.js
 24 //          HTMLCanvas3D.js
 25 
 26 /**
 27  * Creates a model preview component.
 28  * @param {string} canvasId  the ID of the 3D canvas where a model will be viewed
 29  * @param {boolean} pitchAndScaleChangeSupported if <code>true</code> the component 
 30  *           will handles events to let the user rotate the displayed model
 31  * @param {boolean} [transformationsChangeSupported] if <code>true</code> the component
 32  *           will handles events to let the user transform the displayed model
 33  * @constructor
 34  * @author Emmanuel Puybaret
 35  */
 36 function ModelPreviewComponent(canvasId, pitchAndScaleChangeSupported, transformationsChangeSupported) {
 37   if (transformationsChangeSupported === undefined) {
 38     transformationsChangeSupported = false;
 39   }
 40   this.canvas3D = new HTMLCanvas3D(canvasId);
 41   this.pickedMaterial = null;
 42   this.setDefaultTransform();
 43   
 44   if (pitchAndScaleChangeSupported) {
 45     var ANGLE_FACTOR = 0.02;
 46     var ZOOM_FACTOR = 0.02;
 47     var previewComponent = this;
 48     var userActionsListener = {
 49         pointerTouches : {},
 50         boundedPitch : false,
 51         pickedTransformGroup : null,
 52         pivotCenterPixel : null,
 53         translationFromOrigin : null,
 54         translationToOrigin : null,
 55         modelBounds : null,
 56         
 57         mousePressed : function(ev) {
 58           userActionsListener.mousePressedInCanvas = true;
 59           previewComponent.stopRotationAnimation();
 60           var rect = previewComponent.getHTMLElement().getBoundingClientRect();
 61           userActionsListener.updatePickedMaterial(ev.clientX - rect.left, ev.clientY - rect.top);
 62           ev.stopPropagation();
 63         },
 64         windowMouseMoved : function(ev) {
 65           if (userActionsListener.mousePressedInCanvas) {
 66             var rect = previewComponent.getHTMLElement().getBoundingClientRect();
 67             userActionsListener.mouseDragged(ev.clientX - rect.left, ev.clientY - rect.top, ev.altKey);
 68           }
 69         },
 70         windowMouseReleased : function(ev) {
 71           userActionsListener.mousePressedInCanvas = false;
 72         },
 73         pointerPressed : function(ev) {
 74           if (ev.pointerType == "mouse") {
 75             userActionsListener.mousePressed(ev);
 76           } else {
 77             // Multi touch support for IE and Edge
 78             userActionsListener.copyPointerToTargetTouches(ev);
 79             userActionsListener.touchStarted(ev);
 80           }
 81         },
 82         pointerMousePressed : function(ev) {
 83           ev.stopPropagation();
 84         },
 85         windowPointerMoved : function(ev) {
 86           if (ev.pointerType == "mouse") {
 87             userActionsListener.windowMouseMoved(ev);
 88           } else {
 89             // Multi touch support for IE and Edge
 90             userActionsListener.copyPointerToTargetTouches(ev);
 91             userActionsListener.touchMoved(ev);
 92           }
 93         },
 94         windowPointerReleased : function(ev) {
 95           if (ev.pointerType == "mouse") {
 96             userActionsListener.windowMouseReleased(ev);
 97           } else {
 98             delete userActionsListener.pointerTouches [ev.pointerId];
 99             userActionsListener.touchEnded(ev);
100           }
101         },
102         touchStarted : function(ev) {
103           ev.preventDefault();
104           if (ev.targetTouches.length == 1) {
105             userActionsListener.mousePressedInCanvas = true;
106             var rect = previewComponent.getHTMLElement().getBoundingClientRect();
107             userActionsListener.updatePickedMaterial(ev.targetTouches [0].clientX - rect.left, ev.targetTouches [0].clientY - rect.top);
108           } else if (ev.targetTouches.length == 2) {
109             userActionsListener.distanceLastPinch = userActionsListener.distance(
110                 ev.targetTouches [0].clientX, ev.targetTouches [0].clientY, ev.targetTouches [1].clientX, ev.targetTouches [1].clientY);
111           }
112           previewComponent.stopRotationAnimation();
113         },
114         touchMoved : function(ev) {
115           ev.preventDefault();
116           if (ev.targetTouches.length == 1) {
117             var rect = previewComponent.getHTMLElement().getBoundingClientRect();
118             userActionsListener.mouseDragged(ev.targetTouches [0].clientX - rect.left, ev.targetTouches [0].clientY - rect.top, false);
119           } else if (ev.targetTouches.length == 2) {
120             var newDistance = userActionsListener.distance(
121                 ev.targetTouches [0].clientX, ev.targetTouches [0].clientY, ev.targetTouches [1].clientX, ev.targetTouches [1].clientY);
122             var scale = userActionsListener.distanceLastPinch / newDistance;
123             previewComponent.setViewScale(Math.max(0.5, Math.min(1.3, previewComponent.viewScale * scale)));
124             userActionsListener.distanceLastPinch = newDistance;
125           }
126         },
127         touchEnded : function(ev) {
128           userActionsListener.mousePressedInCanvas = false;
129         },
130         copyPointerToTargetTouches : function (ev) {
131           // Copy the IE and Edge pointer location to ev.targetTouches
132           userActionsListener.pointerTouches [ev.pointerId] = {clientX : ev.clientX, clientY : ev.clientY};
133           ev.targetTouches = [];
134           for (var attribute in userActionsListener.pointerTouches) {
135             if (userActionsListener.pointerTouches.hasOwnProperty(attribute)) {
136               ev.targetTouches.push(userActionsListener.pointerTouches [attribute]);
137             }
138           }
139         },
140         distance : function(x1, y1, x2, y2) {
141           return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
142         },
143         mouseScrolled : function(ev) {
144           ev.preventDefault();
145           userActionsListener.zoomUpdater(ev.detail);
146         },
147         mouseWheelMoved : function(ev) {
148           ev.preventDefault();
149           userActionsListener.zoomUpdater(ev.deltaY !== undefined ? ev.deltaY / 2 : -ev.wheelDelta / 3);
150         },
151         visibilityChanged : function(ev) {
152           if (document.visibilityState == "hidden") {
153             previewComponent.stopRotationAnimation();
154           }
155         },
156         updatePickedMaterial : function(x, y) {
157           userActionsListener.xLastMove = x;
158           userActionsListener.yLastMove = y;
159           userActionsListener.pickedTransformGroup = null;
160           userActionsListener.pivotCenterPixel = null;
161           userActionsListener.boundedPitch = true;
162           previewComponent.pickedMaterial = null;
163           if (typeof HomeMaterial !== "undefined"
164               && previewComponent.getModelNode() !== null) {
165             var modelManager = ModelManager.getInstance();
166             if (transformationsChangeSupported) {
167               userActionsListener.boundedPitch = !modelManager.containsDeformableNode(previewComponent.getModelNode());
168             }
169             var rect = previewComponent.getHTMLElement().getBoundingClientRect();
170             var shape = previewComponent.canvas3D.getClosestShapeAt(x + rect.left, y + rect.top);
171             if (shape !== null) {
172               var materials = modelManager.getMaterials(shape);
173               if (materials.length > 0) {
174                 previewComponent.pickedMaterial = materials [0];
175               }
176               for (var node = shape; (node = node.getParent()) !== null; ) {
177                 if (node instanceof TransformGroup3D) {
178                   userActionsListener.pickedTransformGroup = node;
179                   break;
180                 }
181               }
182               if (transformationsChangeSupported
183                   && userActionsListener.pickedTransformGroup != null) {
184                 // The pivot node is the first sibling node which is not a transform group
185                 var group = userActionsListener.pickedTransformGroup.getParent();
186                 var i = group.getChildren().indexOf(userActionsListener.pickedTransformGroup) - 1;
187                 while (i >= 0 && (group.getChild(i) instanceof TransformGroup3D)) {
188                   i--;
189                 }
190                 if (i >= 0) {
191                   var referenceNode = group.getChild(i);
192                   var nodeCenter = modelManager.getCenter(referenceNode);
193                   var nodeCenterAtScreen = vec3.clone(nodeCenter);
194                   var pivotTransform = userActionsListener.getTransformBetweenNodes(referenceNode.getParent(), previewComponent.canvas3D.getScene(), false);
195                   vec3.transformMat4(nodeCenterAtScreen, nodeCenterAtScreen, pivotTransform);
196                   var transformToCanvas = previewComponent.canvas3D.getVirtualWorldToImageTransform(mat4.create());
197                   var viewPlatformTransform = previewComponent.canvas3D.getViewPlatformTransform(mat4.create());
198                   mat4.invert(viewPlatformTransform, viewPlatformTransform);
199                   mat4.mul(transformToCanvas, transformToCanvas, viewPlatformTransform);
200                   vec3.transformMat4(nodeCenterAtScreen, nodeCenterAtScreen, transformToCanvas);
201                   userActionsListener.pivotCenterPixel = [(nodeCenterAtScreen [0] / 2 + 0.5) * rect.width, 
202                       rect.height * (0.5 - nodeCenterAtScreen [1] / 2)];
203 
204                   var transformationName = userActionsListener.pickedTransformGroup.getName();
205                   userActionsListener.translationFromOrigin = mat4.create();
206                   userActionsListener.translationFromOrigin [12] = nodeCenter [0];
207                   userActionsListener.translationFromOrigin [13] = nodeCenter [1];
208                   userActionsListener.translationFromOrigin [14] = nodeCenter [2];
209 
210                   var pitchRotation = mat4.create();
211                   mat4.fromXRotation(pitchRotation, previewComponent.viewPitch);
212                   var yawRotation = mat4.create();
213                   mat4.fromYRotation(yawRotation, previewComponent.viewYaw);
214 
215                   if (transformationName.indexOf(ModelManager.HINGE_PREFIX) === 0
216                       || transformationName.indexOf(ModelManager.RAIL_PREFIX) === 0) {
217                     var rotation = mat4.create();
218                     var nodeSize = modelManager.getSize(referenceNode);
219                     var modelRoot = userActionsListener.getModelRoot(referenceNode);
220                     var transformBetweenRootAndModelNode = userActionsListener.getTransformBetweenNodes(modelRoot, previewComponent.getModelNode(), true);
221                     vec3.transformMat4(nodeSize, nodeSize, transformBetweenRootAndModelNode);
222                     nodeSize [0] = Math.abs(nodeSize [0]);
223                     nodeSize [1] = Math.abs(nodeSize [1]);
224                     nodeSize [2] = Math.abs(nodeSize [2]);
225 
226                     var modelRotationAtScreen = mat4.clone(yawRotation);
227                     mat4.mul(modelRotationAtScreen, modelRotationAtScreen, pitchRotation);
228                     mat4.invert(modelRotationAtScreen, modelRotationAtScreen);
229 
230                     // Set rotation around (or translation along) hinge largest dimension
231                     // taking into account the direction of the axis at screen
232                     if (nodeSize [1] > nodeSize [0] && nodeSize [1] > nodeSize [2]) {
233                       var yAxisAtScreen = vec3.fromValues(0, 1, 0);
234                       vec3.transformMat4(yAxisAtScreen, yAxisAtScreen, modelRotationAtScreen);
235                       if (transformationName.indexOf(ModelManager.RAIL_PREFIX) === 0
236                           ? yAxisAtScreen [1] > 0
237                           : yAxisAtScreen [2] < 0) {
238                         mat4.fromXRotation(rotation, Math.PI / 2);
239                       } else {
240                         mat4.fromXRotation(rotation, -Math.PI / 2);
241                       }
242                     } else if (nodeSize [2] > nodeSize [0] && nodeSize [2] > nodeSize [1]) {
243                       var zAxisAtScreen = vec3.fromValues(0, 0, 1);
244                       vec3.transformMat4(zAxisAtScreen, zAxisAtScreen, modelRotationAtScreen);
245                       if (transformationName.indexOf(ModelManager.RAIL_PREFIX) === 0
246                           ? zAxisAtScreen [0] > 0
247                           : zAxisAtScreen [2] < 0) {
248                         mat4.fromXRotation(rotation, Math.PI);
249                       }
250                     } else {
251                       var xAxisAtScreen = vec3.fromValues(1, 0, 0);
252                       vec3.transformMat4(xAxisAtScreen, xAxisAtScreen, modelRotationAtScreen);
253                       if (transformationName.indexOf(ModelManager.RAIL_PREFIX) === 0
254                           ? xAxisAtScreen [0] > 0
255                           : xAxisAtScreen [2] < 0) {
256                         mat4.fromYRotation(rotation, -Math.PI / 2);
257                       } else {
258                         mat4.fromYRotation(rotation, Math.PI / 2);
259                       }
260                     }
261 
262                     mat4.invert(transformBetweenRootAndModelNode, transformBetweenRootAndModelNode);
263                     mat4.mul(userActionsListener.translationFromOrigin, userActionsListener.translationFromOrigin, transformBetweenRootAndModelNode);
264                     mat4.mul(userActionsListener.translationFromOrigin, userActionsListener.translationFromOrigin, rotation);
265                   } else {
266                     // Set rotation in the screen plan for mannequin or ball handling
267                     mat4.mul(userActionsListener.translationFromOrigin, userActionsListener.translationFromOrigin,
268                         mat4.invert(mat4.create(), userActionsListener.getTransformBetweenNodes(referenceNode.getParent(), previewComponent.getModelNode(), true)));
269                     mat4.mul(userActionsListener.translationFromOrigin, userActionsListener.translationFromOrigin, yawRotation);
270                     mat4.mul(userActionsListener.translationFromOrigin, userActionsListener.translationFromOrigin, pitchRotation);
271                   }
272 
273                   userActionsListener.translationToOrigin = mat4.invert(mat4.create(), userActionsListener.translationFromOrigin);
274                   userActionsListener.modelBounds = modelManager.getBounds(previewComponent.getModelNode());
275                 }
276               }
277             }
278           }
279         },
280         getTransformBetweenNodes : function(node, parent, ignoreTranslation) {
281           var transform = mat4.create();
282           if (node instanceof TransformGroup3D) {
283             node.getTransform(transform);
284             if (ignoreTranslation) {
285               transform [12] = 0;
286               transform [13] = 0;
287               transform [14] = 0;
288             }
289           }
290           if (node !== parent) {
291             var nodeParent = node.getParent();
292             if (nodeParent instanceof Group3D) {
293               mat4.mul(transform, userActionsListener.getTransformBetweenNodes(nodeParent, parent, ignoreTranslation), transform);
294             } else {
295               throw new IllegalStateException("Can't retrieve node transform");
296             }
297           }
298           return transform;
299         },
300         getModelRoot : function(node) {
301           // Return the group parent which stores the model content (may be a group and not a branch group)
302           if (node instanceof Group3D
303               && node.getUserData() instanceof URLContent) {
304             return node;
305           } else if (node.getParent() != null) {
306             return userActionsListener.getModelRoot(node.getParent());
307           } else {
308             return null;
309           }
310         },
311         mouseDragged : function(x, y, altKeyPressed) {
312           if (previewComponent.getModelNode() !== null) {
313             if (userActionsListener.pivotCenterPixel !== null) {
314               var transformationName = userActionsListener.pickedTransformGroup.getName();
315               var additionalTransform = mat4.create();
316               if (transformationName.indexOf(ModelManager.RAIL_PREFIX) === 0) {
317                 mat4.translate(additionalTransform, additionalTransform,
318                     vec3.fromValues(0, 0,
319                         userActionsListener.distance(x, y, userActionsListener.xLastMove, userActionsListener.yLastMove) * (userActionsListener.xLastMove - x < 0 ? -1 : (userActionsListener.xLastMove - x === 0 ? 0 : 1))));
320               } else {
321                 var angle = Math.atan2(userActionsListener.pivotCenterPixel [1] - y, x - userActionsListener.pivotCenterPixel [0])
322                     - Math.atan2(userActionsListener.pivotCenterPixel [1] - userActionsListener.yLastMove, userActionsListener.xLastMove - userActionsListener.pivotCenterPixel [0]);
323                 mat4.fromZRotation(additionalTransform, angle);
324               }
325 
326               mat4.mul(additionalTransform, additionalTransform, userActionsListener.translationToOrigin);
327               mat4.mul(additionalTransform, userActionsListener.translationFromOrigin, additionalTransform);
328 
329               var newTransform = mat4.create();
330               userActionsListener.pickedTransformGroup.getTransform(newTransform);
331               mat4.mul(newTransform, additionalTransform, newTransform);
332               userActionsListener.pickedTransformGroup.setTransform(newTransform);
333 
334               // Update size with model normalization and main transformation
335               var modelLower = vec3.create();
336               userActionsListener.modelBounds.getLower(modelLower);
337               var modelUpper = vec3.create();
338               userActionsListener.modelBounds.getUpper(modelUpper);
339               var modelManager = ModelManager.getInstance();
340               var newBounds = modelManager.getBounds(previewComponent.getModelNode());
341               var newLower = vec3.create();
342               newBounds.getLower(newLower);
343               var newUpper = vec3.create();
344               newBounds.getUpper(newUpper);
345               var previewedPiece = previewComponent.previewedPiece;
346               previewedPiece.setX(previewedPiece.getX() + (newUpper [0] + newLower [0]) / 2 - (modelUpper [0] + modelLower [0]) / 2);
347               previewedPiece.setY(previewedPiece.getY() + (newUpper [2] + newLower [2]) / 2 - (modelUpper [2] + modelLower [2]) / 2);
348               previewedPiece.setElevation(previewedPiece.getElevation() + (newLower [1] - modelLower [1]));
349               previewedPiece.setWidth(newUpper [0] - newLower [0]);
350               previewedPiece.setDepth(newUpper [2] - newLower [2]);
351               previewedPiece.setHeight(newUpper [1] - newLower [1]);
352               userActionsListener.modelBounds = newBounds;
353 
354               // Update matching piece of furniture transformations array
355               var transformations = previewComponent.previewedPiece.getModelTransformations();
356               var transformationsArray = [];
357               if (transformations !== null) {
358                 transformationsArray.push.apply(transformationsArray, transformations);
359               }
360               transformationName = transformationName.substring(0, transformationName.length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length);
361               for (var i = 0; i < transformationsArray.length; i++) {
362                 if (transformationName == transformationsArray [i].getName()) {
363                   transformationsArray.splice(i, 1);
364                   break;
365                 }
366               }
367               transformationsArray.push(new Transformation(transformationName, 
368                   [[newTransform [0], newTransform [4], newTransform [8], newTransform [12]],
369                    [newTransform [1], newTransform [5], newTransform [9], newTransform [13]],
370                    [newTransform [2], newTransform [6], newTransform [10], newTransform [14]]]));
371               previewComponent.previewedPiece.setModelTransformations(transformationsArray);
372             } else {
373               if (!altKeyPressed) {
374                 previewComponent.setViewYaw(previewComponent.viewYaw - ANGLE_FACTOR * (x - userActionsListener.xLastMove));
375               }
376               
377               if (pitchAndScaleChangeSupported && altKeyPressed) {
378                 userActionsListener.zoomUpdater(y - userActionsListener.yLastMove);
379               } else if (pitchAndScaleChangeSupported && !altKeyPressed) {
380                 var viewPitch = previewComponent.viewPitch - ANGLE_FACTOR * (y - userActionsListener.yLastMove);
381                 if (userActionsListener.boundedPitch) {
382                   previewComponent.setViewPitch(Math.max(-Math.PI / 4, Math.min(0, viewPitch)));
383                 } else {
384                   // Allow any rotation around the model
385                   previewComponent.setViewPitch(viewPitch);
386                 }
387               }
388             }
389           }
390           userActionsListener.xLastMove = x;
391           userActionsListener.yLastMove = y;
392         },
393         zoomUpdater : function(delta) {
394           previewComponent.setViewScale(Math.max(0.5, Math.min(1.3, previewComponent.viewScale * Math.exp(delta * ZOOM_FACTOR))));
395           previewComponent.stopRotationAnimation();
396         }
397       };
398       
399     if (OperatingSystem.isInternetExplorerOrLegacyEdge()
400         && window.PointerEvent) {
401       // Multi touch support for IE and Edge
402       this.canvas3D.getHTMLElement().addEventListener("pointerdown", userActionsListener.pointerPressed);
403       this.canvas3D.getHTMLElement().addEventListener("mousedown", userActionsListener.pointerMousePressed);
404       // Add pointermove and pointerup event listeners to window to capture pointer events out of the canvas 
405       window.addEventListener("pointermove", userActionsListener.windowPointerMoved);
406       window.addEventListener("pointerup", userActionsListener.windowPointerReleased);
407     } else {
408       this.canvas3D.getHTMLElement().addEventListener("touchstart", userActionsListener.touchStarted);
409       this.canvas3D.getHTMLElement().addEventListener("touchmove", userActionsListener.touchMoved);
410       this.canvas3D.getHTMLElement().addEventListener("touchend", userActionsListener.touchEnded);
411       this.canvas3D.getHTMLElement().addEventListener("mousedown", userActionsListener.mousePressed);
412       // Add mousemove and mouseup event listeners to window to capture mouse events out of the canvas 
413       window.addEventListener("mousemove", userActionsListener.windowMouseMoved);
414       window.addEventListener("mouseup", userActionsListener.windowMouseReleased);
415     }
416     this.canvas3D.getHTMLElement().addEventListener("DOMMouseScroll", userActionsListener.mouseScrolled);
417     this.canvas3D.getHTMLElement().addEventListener("mousewheel", userActionsListener.mouseWheelMoved);
418     document.addEventListener("visibilitychange", userActionsListener.visibilityChanged);
419     this.userActionsListener = userActionsListener;
420   }
421 }
422 
423 /**
424  * Returns the HTML element used to view this component at screen.
425  */
426 ModelPreviewComponent.prototype.getHTMLElement = function() {
427   return this.canvas3D.getHTMLElement();
428 }
429 
430 /**
431  * @private
432  */
433 ModelPreviewComponent.prototype.setDefaultTransform = function() {
434   this.viewYaw = Math.PI / 8;
435   this.viewPitch = -Math.PI / 16; 
436   this.viewScale = 1;
437   this.updateViewPlatformTransform();
438 }
439 
440 /**
441  * Returns the <code>yaw</code> angle used by view platform transform.
442  * @return {number}
443  * @protected
444  */
445 ModelPreviewComponent.prototype.getViewYaw = function() {
446   return this.viewYaw;
447 }
448 
449 /**
450  * Sets the <code>yaw</code> angle used by view platform transform.
451  * @param {number} viewYaw
452  * @protected
453  */
454 ModelPreviewComponent.prototype.setViewYaw = function(viewYaw) {
455   this.viewYaw = viewYaw;
456   this.updateViewPlatformTransform();
457 }
458 
459 /**
460  * Returns the zoom factor used by view platform transform.
461  * @return {number}
462  * @protected
463  */
464 ModelPreviewComponent.prototype.getViewScale = function() {
465   return this.viewScale;
466 }
467 
468 /**
469  * Sets the zoom factor used by view platform transform.
470  * @param {number} viewScale
471  * @protected
472  */
473 ModelPreviewComponent.prototype.setViewScale = function(viewScale) {
474   this.viewScale = viewScale;
475   this.updateViewPlatformTransform();
476 }
477 
478 /**
479  * Returns the <code>pitch</code> angle used by view platform transform.
480  * @return {number}
481  * @protected
482  */
483 ModelPreviewComponent.prototype.getViewPitch = function() {
484   return this.viewPitch;
485 }
486 
487 /**
488  * Sets the <code>pitch</code> angle used by view platform transform.
489  * @param {number} viewPitch
490  * @protected
491  */
492 ModelPreviewComponent.prototype.setViewPitch = function(viewPitch) {
493   this.viewPitch = viewPitch;
494   this.updateViewPlatformTransform();
495 }
496 
497 /**
498  * @private
499  */
500 ModelPreviewComponent.prototype.updateViewPlatformTransform = function() {
501   // Default distance used to view a 2 unit wide scene
502   var nominalDistanceToCenter = 1.4 / Math.tan(Math.PI / 8);  
503   var translation = mat4.create();
504   mat4.translate(translation, translation, vec3.fromValues(0, 0, nominalDistanceToCenter));
505   var pitchRotation = mat4.create();
506   mat4.rotateX(pitchRotation, pitchRotation, this.viewPitch);
507   var yawRotation = mat4.create();
508   mat4.rotateY(yawRotation, yawRotation, this.viewYaw);
509   var scale = mat4.create();
510   mat4.scale(scale, scale, vec3.fromValues(this.viewScale, this.viewScale, this.viewScale));
511   
512   mat4.mul(pitchRotation, pitchRotation, translation);
513   mat4.mul(yawRotation, yawRotation, pitchRotation);
514   mat4.mul(scale, scale, yawRotation);
515   this.canvas3D.setViewPlatformTransform(scale);
516 }
517 
518 /**
519  * Loads and displays the given 3D model.
520  * @param {URLContent} model a content with a URL pointing to a 3D model to parse and view
521  * @param {boolean|Number} [modelFlags] if <code>true</code>, displays opposite faces
522  * @param {Array} modelRotation  a 3x3 array describing how to transform the 3D model
523  * @param {number} [width] optional width of the model
524  * @param {number} [depth] optional width of the model
525  * @param {number} [height] optional width of the model
526  * @param onerror       callback called in case of error while reading the model
527  * @param onprogression callback to follow the reading of the model
528  */
529 ModelPreviewComponent.prototype.setModel = function(model, modelFlags, modelRotation,
530                                                     width, depth, height,
531                                                     onerror, onprogression) {
532   if (depth === undefined 
533       && height === undefined                                                     
534       && onerror === undefined  
535       && onprogression === undefined) {
536     // Only model, modelRotation, onerror, onprogression parameters
537     onprogression = width;
538     onerror = modelRotation;
539     modelRotation = modelFlags;
540     modelFlags = 0;
541     width = -1;
542     depth = -1;
543     height = -1;
544   }
545   this.model = model;
546   this.canvas3D.clear();       
547   if (typeof HomePieceOfFurniture !== "undefined") {
548     this.previewedPiece = null;
549   }
550   if (model !== null) {
551     var previewComponent = this;
552     ModelManager.getInstance().loadModel(model,
553         {
554           modelUpdated : function(modelRoot) {
555             if (model === previewComponent.model) {
556               // Place model at origin in a box as wide as the canvas
557               var modelManager = ModelManager.getInstance();
558               var size = width < 0
559                   ? modelManager.getSize(modelRoot)
560                   : vec3.fromValues(width, height, depth); 
561               var scaleFactor = 1.8 / Math.max(Math.max(size[0], size[2]), size[1]);
562               
563               var modelTransformGroup;
564               if (typeof HomePieceOfFurniture !== "undefined") {
565                 if (typeof modelFlags === "boolean") {
566                   modelFlags = modelFlags ? PieceOfFurniture.SHOW_BACK_FACE : 0;
567                 }
568                 previewComponent.previewedPiece = new HomePieceOfFurniture(
569                     new CatalogPieceOfFurniture(null, null, model,
570                         size[0], size[2], size[1], 0, false, null, null, 
571                         modelRotation, modelFlags, null, null, 0, 0, 1, false));
572                 previewComponent.previewedPiece.setX(0);
573                 previewComponent.previewedPiece.setY(0);
574                 previewComponent.previewedPiece.setElevation(-previewComponent.previewedPiece.getHeight() / 2);
575                 
576                 var modelTransform = mat4.create();
577                 mat4.scale(modelTransform, modelTransform, vec3.fromValues(scaleFactor, scaleFactor, scaleFactor));
578                 modelTransformGroup = new TransformGroup3D(modelTransform);
579                 
580                 var piece3D = new HomePieceOfFurniture3D(previewComponent.previewedPiece, null, true);
581                 modelTransformGroup.addChild(piece3D);
582               } else {
583                 var modelTransform = modelRotation 
584                     ? modelManager.getRotationTransformation(modelRotation)  
585                     : mat4.create();
586                 mat4.scale(modelTransform, modelTransform, vec3.fromValues(scaleFactor, scaleFactor, scaleFactor));
587                 mat4.scale(modelTransform, modelTransform, size);
588                 mat4.mul(modelTransform, modelTransform, modelManager.getNormalizedTransform(modelRoot, null, 1));
589                 
590                 modelTransformGroup = new TransformGroup3D(modelTransform); 
591                 modelTransformGroup.addChild(modelRoot);
592               }
593               
594               var scene = new BranchGroup3D(); 
595               scene.addChild(modelTransformGroup);
596               // Add lights
597               scene.addChild(new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(1.732, -0.8, -1)));
598               scene.addChild(new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(-1.732, -0.8, -1))); 
599               scene.addChild(new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(0, -0.8, 1)));
600               scene.addChild(new DirectionalLight3D(vec3.fromValues(0.66, 0.66, 0.66), vec3.fromValues(0, 1, 0)));
601               scene.addChild(new AmbientLight3D(vec3.fromValues(0.2, 0.2, 0.2))); 
602               
603               previewComponent.setDefaultTransform();
604               previewComponent.canvas3D.setScene(scene, onprogression);
605               previewComponent.canvas3D.updateViewportSize();
606             }
607           },
608           modelError : function(err) {
609             if (model === previewComponent.model
610                 && onerror !== undefined) {
611               onerror(err);
612             }
613           },
614           progression : function(part, info, percentage) {
615             if (model === previewComponent.model
616                 && onprogression !== undefined) {
617               onprogression(part, info, percentage);
618             }
619           }
620         });
621   }
622 }
623 
624 /**
625  * Returns the 3D model node displayed by this component.
626  * @private
627  */
628 ModelPreviewComponent.prototype.getModelNode = function() {
629   var modelTransformGroup = this.canvas3D.getScene().getChild(0);
630   if (modelTransformGroup.getChildren().length > 0) {
631     return modelTransformGroup.getChild(0);
632   } else {
633     return null;
634   }
635 }
636 
637 /**
638  * Sets the materials applied to 3D model.
639  * @param {Array} materials
640  */
641 ModelPreviewComponent.prototype.setModelMaterials = function(materials) {
642   if (this.previewedPiece != null) {
643     this.previewedPiece.setModelMaterials(materials);
644     this.getModelNode().update();
645   }
646 }
647 
648 /**
649  * Sets the transformations applied to 3D model
650  * @param {Array} transformations
651  */
652 ModelPreviewComponent.prototype.setModelTransformations = function(transformations) {
653   if (this.previewedPiece != null) {
654     this.previewedPiece.setModelTransformations(transformations);
655     this.getModelNode().update();
656   }
657 }
658 
659 /**
660  * @param {Array} transformations
661  * @ignored
662  */
663 ModelPreviewComponent.prototype.setPresetModelTransformations = function(transformations) {
664   if (this.previewedPiece != null) {
665     var modelManager = ModelManager.getInstance();
666     var oldBounds = modelManager.getBounds(this.getModelNode());
667     var oldLower = vec3.create();
668     oldBounds.getLower(oldLower);
669     var oldUpper = vec3.create();
670     oldBounds.getUpper(oldUpper);
671 
672     this.setNodeTransformations(this.getModelNode(), transformations);
673 
674     var newBounds = modelManager.getBounds(this.getModelNode());
675     var newLower = vec3.create();
676     newBounds.getLower(newLower);
677     var newUpper = vec3.create();
678     newBounds.getUpper(newUpper);
679     this.previewedPiece.setX(this.previewedPiece.getX() + (newUpper [0] + newLower [0]) / 2 - (oldUpper [0] + oldLower [0]) / 2);
680     this.previewedPiece.setY(this.previewedPiece.getY() + (newUpper [2] + newLower [2]) / 2 - (oldUpper [2] + oldLower [2]) / 2);
681     this.previewedPiece.setElevation(this.previewedPiece.getElevation() + (newLower [1] - oldLower [1]));
682     this.previewedPiece.setWidth(newUpper [0] - newLower [0]);
683     this.previewedPiece.setDepth(newUpper [2] - newLower [2]);
684     this.previewedPiece.setHeight(newUpper [1] - newLower [1]);
685     this.previewedPiece.setModelTransformations(transformations);
686   }
687 }
688 
689 /**
690  * @ignored
691  */
692 ModelPreviewComponent.prototype.resetModelTransformations = function() {
693   this.setPresetModelTransformations(null);
694 }
695 
696 /**
697  * @param {Node3D} node
698  * @param {Array} transformations
699  * @private
700  */
701 ModelPreviewComponent.prototype.setNodeTransformations = function(node, transformations) {
702   if (node instanceof Group3D) {
703     if (node instanceof TransformGroup3D
704         && node.getName() !== null
705         && node.getName().lastIndexOf(ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX) === node.getName().length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length) {
706       node.setTransform(mat4.create());
707       if (transformations != null) {
708         var transformationName = node.getName();
709         transformationName = transformationName.substring(0, transformationName.length - ModelManager.DEFORMABLE_TRANSFORM_GROUP_SUFFIX.length);
710         for (var i = 0; i < transformations.length; i++) {
711           var transformation = transformations [i];
712           if (transformationName == transformation.getName()) {
713             var matrix = transformation.getMatrix();
714             var transformMatrix = mat4.create();
715             mat4.set(transformMatrix, 
716                 matrix[0][0], matrix[1][0], matrix[2][0], 0,
717                 matrix[0][1], matrix[1][1], matrix[2][1], 0,
718                 matrix[0][2], matrix[1][2], matrix[2][2], 0,
719                 matrix[0][3], matrix[1][3], matrix[2][3], 1);
720             node.setTransform(transformMatrix);
721           }
722         }
723       }
724     }
725     var children = node.getChildren();
726     for (var i = 0; i < children.length; i++) {
727       this.setNodeTransformations(children [i], transformations);
728     }
729   }
730 }
731 
732 /**
733  * Returns the transformations applied to 3D model.
734  * @return {Array}
735  */
736 ModelPreviewComponent.prototype.getModelTransformations = function() {
737   if (this.previewedPiece != null) {
738     return this.previewedPiece.getModelTransformations();
739   } else {
740     return null;
741   }
742 }
743 
744 /**
745  * Returns the abscissa of the 3D model.
746  * @return {number}
747  */
748 ModelPreviewComponent.prototype.getModelX = function() {
749   return this.previewedPiece.getX();
750 }
751 
752 /**
753  * Returns the ordinate of the 3D model.
754  * @return {number}
755  */
756 ModelPreviewComponent.prototype.getModelY = function() {
757   return this.previewedPiece.getY();
758 }
759 
760 /**
761  * Returns the elevation of the 3D model.
762  * @return {number}
763  */
764 ModelPreviewComponent.prototype.getModelElevation = function() {
765   return this.previewedPiece.getElevation();
766 }
767 
768 /**
769  * Returns the width of the 3D model.
770  * @return {number}
771  */
772 ModelPreviewComponent.prototype.getModelWidth = function() {
773   return this.previewedPiece.getWidth();
774 }
775 
776 /**
777  * Returns the depth of the 3D model.
778  * @return {number}
779  */
780 ModelPreviewComponent.prototype.getModelDepth = function() {
781   return this.previewedPiece.getDepth();
782 }
783 
784 /**
785  * Returns the height of the 3D model.
786  * @return {number}
787  */
788 ModelPreviewComponent.prototype.getModelHeight = function() {
789   return this.previewedPiece.getHeight();
790 }
791 
792 /**
793  * Returns the material of the shape last picked by the user.
794  * @return {HomeMaterial}
795  */
796 ModelPreviewComponent.prototype.getPickedMaterial = function() {
797   return this.pickedMaterial;
798 }
799 
800 /**
801  * Stops rotation animation and clears buffers used by its canvas.
802  */
803 ModelPreviewComponent.prototype.clear = function() {
804   this.stopRotationAnimation();
805   this.canvas3D.clear();
806 }
807 
808 /**
809  * Removes listeners bound to global objects and clears this component.
810  * This method should be called to free resources in the browser when this component is not needed anymore.
811  */
812 ModelPreviewComponent.prototype.dispose = function() {
813   if (OperatingSystem.isInternetExplorerOrLegacyEdge()
814       && window.PointerEvent) {
815     window.removeEventListener("pointermove", this.userActionsListener.windowPointerMoved);
816     window.removeEventListener("pointerup", this.userActionsListener.windowPointerReleased);
817   } else {
818     window.removeEventListener("mousemove", this.userActionsListener.windowMouseMoved);
819     window.removeEventListener("mouseup", this.userActionsListener.windowMouseReleased);
820   }
821   document.removeEventListener("visibilitychange", this.userActionsListener.visibilityChanged);
822   this.clear();
823 }
824 
825 /**
826  * Starts rotation animation.
827  * @param {number} [roundsPerMinute]  the rotation speed in rounds per minute, 5rpm if missing
828  */
829 ModelPreviewComponent.prototype.startRotationAnimation = function(roundsPerMinute) {
830   this.roundsPerMinute = roundsPerMinute !== undefined ? roundsPerMinute : 5;
831   if (!this.rotationAnimationStarted) {
832     this.rotationAnimationStarted = true;
833     this.animate();
834   }
835 }
836 
837 /**
838  * @private
839  */
840 ModelPreviewComponent.prototype.animate = function() {
841   if (this.rotationAnimationStarted) {
842     var now = Date.now();
843     if (this.lastRotationAnimationTime !== undefined) {
844       var angularSpeed = this.roundsPerMinute * 2 * Math.PI / 60000; 
845       this.viewYaw += ((now - this.lastRotationAnimationTime) * angularSpeed) % (2 * Math.PI);
846       this.updateViewPlatformTransform();
847     }
848     this.lastRotationAnimationTime = now;
849     var previewComponent = this;
850     requestAnimationFrame(
851         function() {
852           previewComponent.animate();
853         });
854   }
855 }
856 
857 /**
858  * Stops the running rotation animation.
859  */
860 ModelPreviewComponent.prototype.stopRotationAnimation = function() {
861   delete this.lastRotationAnimationTime;
862   delete this.rotationAnimationStarted;
863 }
864