1 /* 2 * viewHome.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 /** 22 * Loads the home from the given URL and displays it in the 3D canvas with <code>canvasId</code>. 23 * <code>params.navigationPanel</code> may be equal to <code>"none"</code>, <code>"default"</code> 24 * or an HTML string which content will replace the default navigation panel. 25 * @param {string} canvasId the value of the id attribute of the 3D canvas 26 * @param {string} homeUrl the URL of the home to load and display 27 * @param onerror callback called in case of error with an exception as parameter 28 * @param onprogression callback with (part, info, percentage) parameters called during the download of the home 29 * and the 3D models it displays. 30 * @param {{roundsPerMinute: number, 31 * navigationPanel: string, 32 * aerialViewButtonId: string, 33 * virtualVisitButtonId: string, 34 * levelsAndCamerasListId: string, 35 * level: string, 36 * selectableLevels: string[], 37 * camera: string, 38 * selectableCameras: string[], 39 * activateCameraSwitchKey: boolean}} [params] the ids of the buttons and other information displayed in the user interface. 40 * If not provided, controls won't be managed if any, no animation and navigation arrows won't be displayed. 41 * @return {HomePreviewComponent} the returned object gives access to the loaded {@link Home} instance, 42 * the {@link HomeComponent3D} instance that displays it, the {@link HomeController3D} instance that manages 43 * camera changes and the {@link UserPreferences} in use. 44 */ 45 function viewHome(canvasId, homeUrl, onerror, onprogression, params) { 46 return new HomePreviewComponent(canvasId, homeUrl, onerror, onprogression, params); 47 } 48 49 /** 50 * Loads the home from the given URL and displays it in an overlay. 51 * Canvas size ratio is 4 / 3 by default. 52 * <code>params.navigationPanel</code> may be equal to <code>"none"</code>, <code>"default"</code> 53 * or an HTML string which content will replace the default navigation panel. 54 * If needed, the id of the created canvas is <code>viewerCanvas</code> and its <code>homePreviewComponent</code> 55 * property returns the instance of {@link HomePreviewComponent} associated to it. 56 * @param {string} homeUrl the URL of the home to display 57 * @param {{roundsPerMinute: number, 58 * widthByHeightRatio: number, 59 * navigationPanel: string, 60 * aerialViewButtonText: string, 61 * virtualVisitButtonText: string, 62 * level: string, 63 * selectableLevels: string[], 64 * camera: string, 65 * selectableCameras: string[], 66 * activateCameraSwitchKey: boolean, 67 * viewerControlsAdditionalHTML: string, 68 * readingHomeText: string, 69 * readingModelText: string, 70 * noWebGLSupportError: string, 71 * missingHomeXmlEntryError: string}} [params] the texts and other information displayed in the user interface. 72 * If not provided, there will be no controls, no animation and canvas size ratio will be 4/3 73 * with no navigation panel. 74 */ 75 function viewHomeInOverlay(homeUrl, params) { 76 var widthByHeightRatio = 4 / 3; 77 if (params && params.widthByHeightRatio) { 78 widthByHeightRatio = params.widthByHeightRatio; 79 } 80 81 // Ensure no two overlays are displayed 82 hideHomeOverlay(); 83 84 var overlayDiv = document.createElement("div"); 85 overlayDiv.setAttribute("id", "viewerOverlay"); 86 overlayDiv.style.position = "absolute"; 87 overlayDiv.style.left = "0"; 88 overlayDiv.style.top = "0"; 89 overlayDiv.style.zIndex = "100"; 90 overlayDiv.style.background = "rgba(127, 127, 127, .5)"; 91 92 var bodyElement = document.getElementsByTagName("body").item(0); 93 bodyElement.insertBefore(overlayDiv, bodyElement.firstChild); 94 95 var homeViewDiv = document.createElement("div"); 96 var divHTML = 97 '<canvas id="viewerCanvas" class="viewerComponent" style="background-color: #CCCCCC; border: 1px solid gray; position: absolute; outline: none; touch-action: none" tabIndex="1"></canvas>' 98 + '<div id="viewerProgressDiv" style="position:absolute; width: 300px; background-color: rgba(128, 128, 128, 0.7); padding: 20px; border-radius: 25px">' 99 + ' <progress id="viewerProgress" class="viewerComponent" value="0" max="200" style="width: 300px;"></progress>' 100 + ' <label id="viewerProgressLabel" class="viewerComponent" style="margin-top: 2px; margin-left: 10px; margin-right: 0px; display: block;"></label>' 101 + '</div>'; 102 if (params 103 && (params.aerialViewButtonText && params.virtualVisitButtonText 104 || params.viewerControlsAdditionalHTML)) { 105 divHTML += '<div id="viewerControls" style="position: absolute; padding: 10px; padding-top: 5px">'; 106 if (params.aerialViewButtonText && params.virtualVisitButtonText) { 107 divHTML += 108 ' <input id="aerialView" class="viewerComponent" name="cameraType" type="radio" style="visibility: hidden;"/>' 109 + ' <label class="viewerComponent" for="aerialView" style="visibility: hidden;">' + params.aerialViewButtonText + '</label>' 110 + ' <input id="virtualVisit" class="viewerComponent" name="cameraType" type="radio" style="visibility: hidden;">' 111 + ' <label class="viewerComponent" for="virtualVisit" style="visibility: hidden;">' + params.virtualVisitButtonText + '</label>' 112 + ' <select id="levelsAndCameras" class="viewerComponent" style="visibility: hidden;"></select>'; 113 } 114 if (params.viewerControlsAdditionalHTML) { 115 divHTML += params.viewerControlsAdditionalHTML; 116 } 117 divHTML += '</div>'; 118 } 119 homeViewDiv.innerHTML = divHTML; 120 overlayDiv.appendChild(homeViewDiv); 121 122 // Create close button image 123 var closeButtonImage = new Image(); 124 closeButtonImage.src = ZIPTools.getScriptFolder() + "close.png"; 125 closeButtonImage.style.position = "absolute"; 126 overlayDiv.appendChild(closeButtonImage); 127 128 overlayDiv.escKeyListener = function(ev) { 129 if (ev.keyCode === 27) { 130 hideHomeOverlay(); 131 } 132 }; 133 document.addEventListener("keydown", overlayDiv.escKeyListener); 134 closeButtonImage.addEventListener("click", hideHomeOverlay); 135 var mouseActionsListener = { 136 mousePressed : function(ev) { 137 mouseActionsListener.mousePressedInOverlay = true; 138 }, 139 mouseClicked : function(ev) { 140 if (mouseActionsListener.mousePressedInOverlay) { 141 delete mouseActionsListener.mousePressedInOverlay; 142 hideHomeOverlay(); 143 } 144 } 145 }; 146 overlayDiv.addEventListener("mousedown", mouseActionsListener.mousePressed); 147 overlayDiv.addEventListener("click", mouseActionsListener.mouseClicked); 148 overlayDiv.addEventListener("touchmove", 149 function(ev) { 150 ev.preventDefault(); 151 }); 152 153 // Place canvas in the middle of the window 154 var windowWidth = self.innerWidth; 155 var windowHeight = self.innerHeight; 156 var pageWidth = document.documentElement.clientWidth; 157 var pageHeight = document.documentElement.clientHeight; 158 if (bodyElement && bodyElement.scrollWidth) { 159 if (bodyElement.scrollWidth > pageWidth) { 160 pageWidth = bodyElement.scrollWidth; 161 } 162 if (bodyElement.scrollHeight > pageHeight) { 163 pageHeight = bodyElement.scrollHeight; 164 } 165 } 166 var pageXOffset = self.pageXOffset ? self.pageXOffset : 0; 167 var pageYOffset = self.pageYOffset ? self.pageYOffset : 0; 168 169 overlayDiv.style.height = Math.max(pageHeight, windowHeight) + "px"; 170 overlayDiv.style.width = pageWidth <= windowWidth 171 ? "100%" 172 : pageWidth + "px"; 173 overlayDiv.style.display = "block"; 174 175 var canvas = document.getElementById("viewerCanvas"); 176 if (windowWidth < windowHeight * widthByHeightRatio) { 177 canvas.width = 0.9 * windowWidth; 178 canvas.height = 0.9 * windowWidth / widthByHeightRatio; 179 } else { 180 canvas.height = 0.9 * windowHeight; 181 canvas.width = 0.9 * windowHeight * widthByHeightRatio; 182 } 183 canvas.style.width = canvas.width + "px"; 184 canvas.style.height = canvas.height + "px"; 185 var canvasLeft = pageXOffset + (windowWidth - canvas.width - 10) / 2; 186 canvas.style.left = canvasLeft + "px"; 187 var canvasTop = pageYOffset + (windowHeight - canvas.height - 10) / 2; 188 canvas.style.top = canvasTop + "px"; 189 190 // Place close button at top right of the canvas 191 closeButtonImage.style.left = (canvasLeft + canvas.width - 5) + "px"; 192 closeButtonImage.style.top = (canvasTop - 10) + "px"; 193 194 // Place controls below the canvas 195 var controlsDiv = document.getElementById("viewerControls"); 196 if (controlsDiv) { 197 controlsDiv.style.left = (canvasLeft - 10) + "px"; 198 controlsDiv.style.top = (canvasTop + canvas.height) + "px"; 199 controlsDiv.addEventListener("mousedown", 200 function(ev) { 201 // Ignore in overlay mouse clicks on controls 202 ev.stopPropagation(); 203 }); 204 } 205 206 // Place progress in the middle of the canvas 207 var progressDiv = document.getElementById("viewerProgressDiv"); 208 progressDiv.style.left = (canvasLeft + (canvas.width - 300) / 2) + "px"; 209 progressDiv.style.top = (canvasTop + (canvas.height - 50) / 2) + "px"; 210 progressDiv.style.visibility = "visible"; 211 212 var onerror = function(err) { 213 hideHomeOverlay(); 214 if (err == "No WebGL") { 215 var errorMessage = "Sorry, your browser doesn't support WebGL."; 216 if (params.noWebGLSupportError) { 217 errorMessage = params.noWebGLSupportError; 218 } 219 alert(errorMessage); 220 } else if (typeof err === "string" && err.indexOf("No Home.xml entry") == 0) { 221 var errorMessage = "Ensure your home file was saved with Sweet Home 3D 5.3 or a newer version."; 222 if (params.missingHomeXmlEntryError) { 223 errorMessage = params.missingHomeXmlEntryError; 224 } 225 alert(errorMessage); 226 } else { 227 console.log(err.stack); 228 alert("Error: " + (err.message ? err.constructor.name + " " + err.message : err)); 229 } 230 }; 231 var onprogression = function(part, info, percentage) { 232 var progress = document.getElementById("viewerProgress"); 233 if (progress) { 234 var text = null; 235 if (part === HomeRecorder.READING_HOME) { 236 progress.value = percentage * 100; 237 info = info.substring(info.lastIndexOf('/') + 1); 238 text = params && params.readingHomeText 239 ? params.readingHomeText : part; 240 } else if (part === ModelLoader.READING_MODEL) { 241 progress.value = 100 + percentage * 100; 242 if (percentage === 1) { 243 document.getElementById("viewerProgressDiv").style.visibility = "hidden"; 244 } 245 text = params && params.readingModelText 246 ? params.readingModelText : part; 247 } 248 249 if (text !== null) { 250 document.getElementById("viewerProgressLabel").innerHTML = 251 (percentage ? Math.floor(percentage * 100) + "% " : "") + text + " " + info; 252 } 253 } 254 }; 255 256 // Display home in canvas 3D 257 var homePreviewComponentContructor = HomePreviewComponent; 258 if (params) { 259 if (params.homePreviewComponentContructor) { 260 homePreviewComponentContructor = params.homePreviewComponentContructor; 261 } 262 if (params.aerialViewButtonText && params.virtualVisitButtonText) { 263 canvas.homePreviewComponent = new homePreviewComponentContructor( 264 "viewerCanvas", homeUrl, onerror, onprogression, 265 {roundsPerMinute : params.roundsPerMinute, 266 navigationPanel : params.navigationPanel, 267 aerialViewButtonId : "aerialView", 268 virtualVisitButtonId : "virtualVisit", 269 levelsAndCamerasListId : "levelsAndCameras", 270 level : params.level, 271 selectableLevels : params.selectableLevels, 272 camera: params.camera, 273 selectableCameras : params.selectableCameras, 274 activateCameraSwitchKey : params.activateCameraSwitchKey}); 275 } else { 276 canvas.homePreviewComponent = new homePreviewComponentContructor( 277 "viewerCanvas", homeUrl, onerror, onprogression, 278 {roundsPerMinute : params.roundsPerMinute, 279 navigationPanel : params.navigationPanel}); 280 } 281 } else { 282 canvas.homePreviewComponent = new homePreviewComponentContructor("viewerCanvas", homeUrl, onerror, onprogression); 283 } 284 } 285 286 /** 287 * Hides the overlay and disposes resources. 288 * @private 289 */ 290 function hideHomeOverlay() { 291 var overlayDiv = document.getElementById("viewerOverlay"); 292 if (overlayDiv) { 293 // Free caches and remove listeners bound to global objects 294 var canvas = document.getElementById("viewerCanvas"); 295 if (canvas.homePreviewComponent) { 296 canvas.homePreviewComponent.dispose(); 297 } 298 ModelManager.getInstance().clear(); 299 TextureManager.getInstance().clear(); 300 ZIPTools.clear(); 301 window.removeEventListener("keydown", overlayDiv.escKeyListener); 302 document.getElementsByTagName("body").item(0).removeChild(overlayDiv); 303 } 304 } 305 306 307 /** 308 * Creates a component that loads and displays a home in a 3D canvas. 309 * @param {string} canvasId the value of the id attribute of the 3D canvas 310 * @param {string} homeUrl the URL of the home to load and display 311 * @param onerror callback called in case of error with an exception as parameter 312 * @param onprogression callback with (part, info, percentage) parameters called during the download of the home 313 * and the 3D models it displays. 314 * @param {{roundsPerMinute: number, 315 * navigationPanel: string, 316 * aerialViewButtonId: string, 317 * virtualVisitButtonId: string, 318 * levelsAndCamerasListId: string, 319 * level: string, 320 * selectableLevels: string[], 321 * camera: string, 322 * selectableCameras: string[], 323 * activateCameraSwitchKey: boolean}} [params] the ids of the buttons and other information displayed in the user interface. 324 * If not provided, controls won't be managed if any, no animation and navigation arrows won't be displayed. 325 * @constructor 326 * @author Emmanuel Puybaret 327 */ 328 function HomePreviewComponent(canvasId, homeUrl, onerror, onprogression, params) { 329 if (document.getElementById(canvasId)) { 330 var previewComponent = this; 331 this.createHomeRecorder().readHome(homeUrl, 332 { 333 homeLoaded : function(home) { 334 try { 335 var canvas = document.getElementById(canvasId); 336 if (canvas) { 337 if (params 338 && params.navigationPanel != "none" 339 && params.navigationPanel != "default") { 340 // Create class with a getLocalizedString() method that returns the navigationPanel in parameter 341 function UserPreferencesWithNavigationPanel(navigationPanel) { 342 DefaultUserPreferences.call(this); 343 this.navigationPanel = navigationPanel; 344 } 345 UserPreferencesWithNavigationPanel.prototype = Object.create(DefaultUserPreferences.prototype); 346 UserPreferencesWithNavigationPanel.prototype.constructor = UserPreferencesWithNavigationPanel; 347 348 UserPreferencesWithNavigationPanel.prototype.getLocalizedString = function(resourceClass, resourceKey, resourceParameters) { 349 // Return navigationPanel in parameter for the navigationPanel.innerHTML resource requested by HomeComponent3D 350 if (resourceClass === HomeComponent3D && resourceKey == "navigationPanel.innerHTML") { 351 return this.navigationPanel; 352 } else { 353 return UserPreferences.prototype.getLocalizedString.call(this, resourceClass, resourceKey, resourceParameters); 354 } 355 } 356 previewComponent.preferences = new UserPreferencesWithNavigationPanel(params.navigationPanel); 357 } else { 358 previewComponent.preferences = new DefaultUserPreferences(); 359 } 360 previewComponent.home = home; 361 previewComponent.controller = new HomeController3D(home, previewComponent.preferences); 362 // Create component 3D with loaded home 363 previewComponent.component3D = previewComponent.createComponent3D( 364 canvasId, home, previewComponent.preferences, previewComponent.controller); 365 previewComponent.prepareComponent(canvasId, onprogression, 366 params ? {roundsPerMinute : params.roundsPerMinute, 367 navigationPanelVisible : params.navigationPanel && params.navigationPanel != "none", 368 aerialViewButtonId : params.aerialViewButtonId, 369 virtualVisitButtonId : params.virtualVisitButtonId, 370 levelsAndCamerasListId : params.levelsAndCamerasListId, 371 level : params.level, 372 selectableLevels : params.selectableLevels, 373 camera : params.camera, 374 selectableCameras : params.selectableCameras, 375 activateCameraSwitchKey : params.activateCameraSwitchKey} 376 : undefined); 377 } 378 } catch (ex) { 379 onerror(ex); 380 } 381 }, 382 homeError : function(err) { 383 onerror(err); 384 }, 385 progression : onprogression 386 }); 387 } else { 388 onerror("No canvas with id equal to " + canvasId); 389 } 390 } 391 392 /** 393 * Returns the recorder that will load the home from the given URL. 394 * @return {HomeRecorder} 395 * @protected 396 * @ignore 397 */ 398 HomePreviewComponent.prototype.createHomeRecorder = function() { 399 return new HomeRecorder(); 400 } 401 402 /** 403 * Returns the component 3D that will display the given home. 404 * @param {string} canvasId the value of the id attribute of the 3D canvas 405 * @return {HomeComponent3D} 406 * @protected 407 * @ignore 408 */ 409 HomePreviewComponent.prototype.createComponent3D = function(canvasId) { 410 return new HomeComponent3D(canvasId, this.getHome(), this.getUserPreferences(), null, this.getController()); 411 } 412 413 /** 414 * Prepares this component and its user interface. 415 * @param {string} canvasId the value of the id attribute of the 3D canvas 416 * @param onprogression callback with (part, info, percentage) parameters called during the download of the home 417 * and the 3D models it displays. 418 * @param {{roundsPerMinute: number, 419 * navigationPanelVisible: boolean, 420 * aerialViewButtonId: string, 421 * virtualVisitButtonId: string, 422 * levelsAndCamerasListId: string, 423 * level: string, 424 * selectableLevels: string[], 425 * camera: string, 426 * selectableCameras: string[], 427 * activateCameraSwitchKey: boolean}} [params] the ids of the buttons and other information displayed in the user interface. 428 * If not provided, controls won't be managed if any, no animation and navigation panel won't be displayed. 429 * @protected 430 * @ignore 431 */ 432 HomePreviewComponent.prototype.prepareComponent = function(canvasId, onprogression, params) { 433 var roundsPerMinute = params && params.roundsPerMinute ? params.roundsPerMinute : 0; 434 this.startRotationAnimationAfterLoading = roundsPerMinute != 0; 435 if (params && typeof params.navigationPanelVisible) { 436 this.getUserPreferences().setNavigationPanelVisible(params.navigationPanelVisible); 437 } 438 var home = this.getHome(); 439 if (home.structure) { 440 // Make always all levels visible if walls and rooms structure can be modified 441 home.getEnvironment().setAllLevelsVisible(true); 442 } else { 443 // Make all levels always visible when observer camera is used 444 var setAllLevelsVisibleWhenObserverCamera = function() { 445 home.getEnvironment().setAllLevelsVisible(home.getCamera() instanceof ObserverCamera); 446 }; 447 setAllLevelsVisibleWhenObserverCamera(); 448 home.addPropertyChangeListener("CAMERA", setAllLevelsVisibleWhenObserverCamera); 449 } 450 home.getEnvironment().setObserverCameraElevationAdjusted(true); 451 452 this.trackFurnitureModels(onprogression, roundsPerMinute); 453 454 // Configure camera type buttons and shortcut 455 var previewComponent = this; 456 var cameraTypeButtonsUpdater = function() { 457 previewComponent.stopRotationAnimation(); 458 if (params && params.aerialViewButtonId && params.virtualVisitButtonId) { 459 if (home.getCamera() === home.getTopCamera()) { 460 document.getElementById(params.aerialViewButtonId).checked = true; 461 } else { 462 document.getElementById(params.virtualVisitButtonId).checked = true; 463 } 464 } 465 }; 466 var toggleCamera = function() { 467 previewComponent.startRotationAnimationAfterLoading = false; 468 home.setCamera(home.getCamera() === home.getTopCamera() 469 ? home.getObserverCamera() 470 : home.getTopCamera()); 471 cameraTypeButtonsUpdater(); 472 }; 473 var canvas = document.getElementById(canvasId); 474 if (params === undefined 475 || params.activateCameraSwitchKey === undefined 476 || params.activateCameraSwitchKey) { 477 canvas.addEventListener("keydown", 478 function(ev) { 479 if (ev.keyCode === 32) { // Space bar 480 toggleCamera(); 481 } 482 }); 483 } 484 if (params && params.aerialViewButtonId && params.virtualVisitButtonId) { 485 var aerialViewButton = document.getElementById(params.aerialViewButtonId); 486 aerialViewButton.addEventListener("change", 487 function() { 488 previewComponent.startRotationAnimationAfterLoading = false; 489 home.setCamera(aerialViewButton.checked 490 ? home.getTopCamera() 491 : home.getObserverCamera()); 492 }); 493 var virtualVisitButton = document.getElementById(params.virtualVisitButtonId); 494 virtualVisitButton.addEventListener("change", 495 function() { 496 previewComponent.startRotationAnimationAfterLoading = false; 497 home.setCamera(virtualVisitButton.checked 498 ? home.getObserverCamera() 499 : home.getTopCamera()); 500 }); 501 cameraTypeButtonsUpdater(); 502 // Make radio buttons and their label visible 503 aerialViewButton.style.visibility = "visible"; 504 virtualVisitButton.style.visibility = "visible"; 505 var makeLabelVisible = function(buttonId) { 506 var labels = document.getElementsByTagName("label"); 507 for (var i = 0; i < labels.length; i++) { 508 if (labels [i].getAttribute("for") == buttonId) { 509 labels [i].style.visibility = "visible"; 510 } 511 } 512 } 513 makeLabelVisible(params.aerialViewButtonId); 514 makeLabelVisible(params.virtualVisitButtonId); 515 home.addPropertyChangeListener("CAMERA", 516 function() { 517 cameraTypeButtonsUpdater(); 518 if (home.structure && params && params.levelsAndCamerasListId) { 519 document.getElementById(params.levelsAndCamerasListId).disabled = home.getCamera() === home.getTopCamera(); 520 } 521 }); 522 } 523 524 if (params && params.level) { 525 var levels = home.getLevels(); 526 if (levels.length > 0) { 527 for (var i = 0; i < levels.length; i++) { 528 var level = levels [i]; 529 if (level.isViewable() 530 && level.getName() == params.level) { 531 home.setSelectedLevel(level); 532 break; 533 } 534 } 535 } 536 } 537 538 if (params && params.camera) { 539 var cameras = home.getStoredCameras(); 540 if (cameras.length > 0) { 541 for (var i = 0; i < cameras.length; i++) { 542 var camera = cameras [i]; 543 if (camera.getName() == params.camera) { 544 this.getController().goToCamera(camera); 545 break; 546 } 547 } 548 } 549 } 550 551 if (params && params.levelsAndCamerasListId) { 552 var levelsAndCamerasList = document.getElementById(params.levelsAndCamerasListId); 553 levelsAndCamerasList.disabled = home.structure !== undefined && home.getCamera() === home.getTopCamera(); 554 var levels = home.getLevels(); 555 if (levels.length > 0) { 556 for (var i = 0; i < levels.length; i++) { 557 var level = levels [i]; 558 if (level.isViewable() 559 && (!params.selectableLevels 560 || params.selectableLevels.indexOf(level.getName()) >= 0)) { 561 var option = document.createElement("option"); 562 option.text = level.getName(); 563 option.level = level; 564 levelsAndCamerasList.add(option); 565 if (level === home.getSelectedLevel()) { 566 levelsAndCamerasList.selectedIndex = levelsAndCamerasList.options.length - 1; 567 } 568 } 569 } 570 } 571 572 if (params.selectableCameras !== undefined) { 573 var cameras = home.getStoredCameras(); 574 if (cameras.length > 0) { 575 var addSeparator = levelsAndCamerasList.options.length > 0; 576 for (var i = 0; i < cameras.length; i++) { 577 var camera = cameras [i]; 578 if (params.selectableCameras.indexOf(camera.getName()) >= 0) { 579 if (addSeparator) { 580 levelsAndCamerasList.add(document.createElement("option")); 581 addSeparator = false; 582 } 583 var option = document.createElement("option"); 584 option.text = camera.getName(); 585 option.camera = camera; 586 levelsAndCamerasList.add(option); 587 } 588 } 589 } 590 } 591 592 if (levelsAndCamerasList.options.length > 1) { 593 var controller = this.getController(); 594 levelsAndCamerasList.addEventListener("change", 595 function() { 596 previewComponent.startRotationAnimationAfterLoading = false; 597 var selectedOption = levelsAndCamerasList.options [levelsAndCamerasList.selectedIndex]; 598 if (selectedOption.level !== undefined) { 599 home.setSelectedLevel(selectedOption.level); 600 } else if (selectedOption.camera !== undefined) { 601 controller.goToCamera(selectedOption.camera); 602 } 603 }); 604 levelsAndCamerasList.style.visibility = "visible"; 605 } 606 } 607 608 if (roundsPerMinute) { 609 var controller = this.getController(); 610 controller.goToCamera(home.getTopCamera()); 611 controller.rotateCameraPitch(Math.PI / 6 - home.getCamera().getPitch()); 612 controller.moveCamera(10000); 613 controller.moveCamera(-50); 614 this.clickListener = function(ev) { 615 previewComponent.startRotationAnimationAfterLoading = false; 616 previewComponent.stopRotationAnimation(); 617 }; 618 canvas.addEventListener("keydown", this.clickListener); 619 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 620 && window.PointerEvent) { 621 // Multi touch support for IE and Edge 622 canvas.addEventListener("pointerdown", this.clickListener); 623 canvas.addEventListener("pointermove", this.clickListener); 624 } else { 625 canvas.addEventListener("mousedown", this.clickListener); 626 canvas.addEventListener("touchstart", this.clickListener); 627 canvas.addEventListener("touchmove", this.clickListener); 628 } 629 var elements = this.component3D.getSimulatedKeyElements(document.getElementsByTagName("body").item(0)); 630 for (var i = 0; i < elements.length; i++) { 631 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 632 && window.PointerEvent) { 633 elements [i].addEventListener("pointerdown", this.clickListener); 634 } else { 635 elements [i].addEventListener("mousedown", this.clickListener); 636 } 637 } 638 this.visibilityChanged = function(ev) { 639 if (document.visibilityState == "hidden") { 640 previewComponent.stopRotationAnimation(); 641 } 642 } 643 document.addEventListener("visibilitychange", this.visibilityChanged); 644 var canvasBounds = canvas.getBoundingClientRect(); 645 // Request focus if canvas is fully visible 646 if (canvasBounds.top >= 0 && canvasBounds.bottom <= self.innerHeight) { 647 canvas.focus(); 648 } 649 } 650 } 651 652 /** 653 * Returns the home displayed by this component. 654 * @return {Home} 655 */ 656 HomePreviewComponent.prototype.getHome = function() { 657 return this.home; 658 } 659 660 /** 661 * Returns the component 3D that displays the home of this component. 662 * @return {HomeComponent3D} 663 */ 664 HomePreviewComponent.prototype.getComponent3D = function() { 665 return this.component3D; 666 } 667 668 /** 669 * Returns the controller that manages changes in the home bound to this component. 670 * @return {HomeController3D} 671 */ 672 HomePreviewComponent.prototype.getController = function() { 673 return this.controller; 674 } 675 676 /** 677 * Returns the user preferences used by this component. 678 * @return {UserPreferences} 679 */ 680 HomePreviewComponent.prototype.getUserPreferences = function() { 681 return this.preferences; 682 } 683 684 /** 685 * Tracks furniture models loading to dispose unneeded files and data once read. 686 * @private 687 */ 688 HomePreviewComponent.prototype.trackFurnitureModels = function(onprogression, roundsPerMinute) { 689 var loadedFurniture = []; 690 var loadedJars = {}; 691 var loadedModels = {}; 692 var home = this.getHome(); 693 var furniture = home.getFurniture(); 694 for (var i = 0; i < furniture.length; i++) { 695 var piece = furniture [i]; 696 var pieces = []; 697 if (piece instanceof HomeFurnitureGroup) { 698 var groupFurniture = piece.getAllFurniture(); 699 for (var j = 0; j < groupFurniture.length; j++) { 700 var childPiece = groupFurniture [j]; 701 if (!(childPiece instanceof HomeFurnitureGroup)) { 702 pieces.push(childPiece); 703 } 704 } 705 } else { 706 pieces.push(piece); 707 } 708 loadedFurniture.push.apply(loadedFurniture, pieces); 709 for (var j = 0; j < pieces.length; j++) { 710 var model = pieces [j].getModel(); 711 if (model.isJAREntry()) { 712 var jar = model.getJAREntryURL(); 713 if (jar in loadedJars) { 714 loadedJars [jar]++; 715 } else { 716 loadedJars [jar] = 1; 717 } 718 } 719 var modelUrl = model.getURL(); 720 if (modelUrl in loadedModels) { 721 loadedModels [modelUrl]++; 722 } else { 723 loadedModels [modelUrl] = 1; 724 } 725 } 726 } 727 728 if (loadedFurniture.length === 0) { 729 onprogression(ModelLoader.READING_MODEL, undefined, 1); 730 } else { 731 // Add an observer that will close ZIP files and free geometries once all models are loaded 732 var modelsCount = 0; 733 var previewComponent = this; 734 for (var i = 0; i < loadedFurniture.length; i++) { 735 var managerCall = function(piece) { 736 ModelManager.getInstance().loadModel(piece.getModel(), false, { 737 modelUpdated : function(modelRoot) { 738 var model = piece.getModel(); 739 if (model.isJAREntry()) { 740 var jar = model.getJAREntryURL(); 741 if (--loadedJars [jar] === 0) { 742 ZIPTools.disposeZIP(jar); 743 delete loadedJars [jar]; 744 } 745 } 746 var modelUrl = model.getURL(); 747 if (--loadedModels [modelUrl] === 0) { 748 ModelManager.getInstance().unloadModel(model); 749 delete loadedModels [modelUrl]; 750 } 751 onprogression(ModelLoader.READING_MODEL, piece.getName(), ++modelsCount / loadedFurniture.length); 752 if (modelsCount === loadedFurniture.length) { 753 // Home and its models fully loaded 754 // Free all other geometries (background, structure...) 755 previewComponent.component3D.disposeGeometries(); 756 loadedFurniture = []; 757 if (previewComponent.startRotationAnimationAfterLoading) { 758 delete previewComponent.startRotationAnimationAfterLoading; 759 previewComponent.startRotationAnimation(roundsPerMinute); 760 } 761 } 762 }, 763 modelError : function(ex) { 764 this.modelUpdated(); 765 } 766 }); 767 }; 768 managerCall(loadedFurniture [i]); 769 } 770 } 771 } 772 773 /** 774 * Stops animation, removes listeners bound to global objects and clears this component. 775 * This method should be called to free resources in the browser when this component is not needed anymore. 776 */ 777 HomePreviewComponent.prototype.dispose = function() { 778 this.stopRotationAnimation(); 779 if (this.component3D) { 780 if (this.clickListener) { 781 // Remove listeners bound to global objects 782 document.removeEventListener("visibilitychange", this.visibilityChanged); 783 var elements = this.component3D.getSimulatedKeyElements(document.getElementsByTagName("body").item(0)); 784 for (var i = 0; i < elements.length; i++) { 785 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 786 && window.PointerEvent) { 787 elements [i].removeEventListener("pointerdown", this.clickListener); 788 } else { 789 elements [i].removeEventListener("mousedown", this.clickListener); 790 } 791 } 792 } 793 this.component3D.dispose(); 794 } 795 } 796 797 /** 798 * Starts rotation animation. 799 * @param {number} [roundsPerMinute] the rotation speed in rounds per minute, 1rpm if missing 800 */ 801 HomePreviewComponent.prototype.startRotationAnimation = function(roundsPerMinute) { 802 this.roundsPerMinute = roundsPerMinute !== undefined ? roundsPerMinute : 1; 803 if (!this.rotationAnimationStarted) { 804 this.rotationAnimationStarted = true; 805 this.animate(); 806 } 807 } 808 809 /** 810 * @private 811 */ 812 HomePreviewComponent.prototype.animate = function() { 813 if (this.rotationAnimationStarted) { 814 var now = Date.now(); 815 if (this.lastRotationAnimationTime !== undefined) { 816 var angularSpeed = this.roundsPerMinute * 2 * Math.PI / 60000; 817 var yawDelta = ((now - this.lastRotationAnimationTime) * angularSpeed) % (2 * Math.PI); 818 yawDelta -= this.home.getCamera().getYaw() - this.lastRotationAnimationYaw; 819 if (yawDelta > 0) { 820 this.controller.rotateCameraYaw(yawDelta); 821 } 822 } 823 this.lastRotationAnimationTime = now; 824 this.lastRotationAnimationYaw = this.home.getCamera().getYaw(); 825 var previewComponent = this; 826 requestAnimationFrame( 827 function() { 828 previewComponent.animate(); 829 }); 830 } 831 } 832 833 /** 834 * Stops the running rotation animation. 835 */ 836 HomePreviewComponent.prototype.stopRotationAnimation = function() { 837 delete this.lastRotationAnimationTime; 838 delete this.lastRotationAnimationYaw; 839 delete this.rotationAnimationStarted; 840 } 841