1 /* 2 * ModelPreviewComponent.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 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 * @ignore 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 * @ignore 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