1 /* 2 * HomeComponent3D.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 core.js 22 // scene3d.js 23 // SweetHome3D.js 24 // ModelManager.js 25 // HomePieceOfFurniture3D.js 26 // Wall3D.js 27 // Room3D.js 28 // Polyline3D.js 29 // DimensionLine3D.js 30 // Label3D.js 31 // HomeController3D.js 32 // HTMLCanvas3D.js 33 34 /** 35 * Creates a 3D component that displays <code>home</code> walls, rooms and furniture. 36 * @param {string} canvasId the id of the HTML canvas associated to this component 37 * @param {Home} home the home to display in this component 38 * @param {UserPreferences} preferences user preferences 39 * @param {Object3DBranchFactory} object3dFactory a factory able to create 3D objects from <code>home</code> items 40 * or <code>null</code> to use default one. 41 * The <code>createObject3D</code> method of this factory is expected to return 42 * an instance of {@link Object3DBranch} in current implementation. 43 * @param {HomeController3D} controller the controller that manages modifications in <code>home</code> (optional). 44 * @constructor 45 * @author Emmanuel Puybaret 46 */ 47 function HomeComponent3D(canvasId, home, preferences, object3dFactory, controller) { 48 this.home = home; 49 this.preferences = preferences; 50 this.object3dFactory = object3dFactory !== null 51 ? object3dFactory 52 : new Object3DBranchFactory(preferences); 53 this.homeObjects = []; 54 this.homeObjects3D = []; 55 this.sceneLights = []; 56 this.camera = null; 57 this.windowResizeListener = null; 58 this.preferencesChangeListener = null; 59 // Listeners bound to home that updates 3D scene objects 60 this.cameraChangeListener = null; 61 this.homeCameraListener = null; 62 this.groundChangeListener = null; 63 this.backgroundChangeListener = null; 64 this.lightColorListener = null; 65 this.elevationChangeListener = null; 66 this.wallsAlphaListener = null; 67 this.selectionListener = null; 68 this.levelListener = null; 69 this.levelChangeListener = null; 70 this.wallListener = null; 71 this.wallChangeListener = null; 72 this.furnitureListener = null; 73 this.furnitureChangeListener = null; 74 this.roomListener = null; 75 this.roomChangeListener = null; 76 this.polylineChangeListener = null; 77 this.polylineListener = null; 78 this.dimensionLineChangeListener = null; 79 this.dimensionLineListener = null; 80 this.labelChangeListener = null; 81 this.labelListener = null; 82 this.approximateHomeBoundsCache = null; 83 this.homeHeightCache = null; 84 this.createComponent3D(canvasId, preferences, controller); 85 } 86 87 HomeComponent3D["__interfaces"] = ["com.eteks.sweethome3d.viewcontroller.View3D", "com.eteks.sweethome3d.viewcontroller.View"]; 88 89 HomeComponent3D.LONG_TOUCH_DELAY = 200; // ms 90 HomeComponent3D.LONG_TOUCH_DELAY_WHEN_DRAGGING = 400; // ms 91 HomeComponent3D.LONG_TOUCH_DURATION_AFTER_DELAY = 800; // ms 92 HomeComponent3D.DOUBLE_TOUCH_DELAY = 500; // ms 93 94 /** 95 * Creates the 3D canvas associated to the given <code>canvasId</code>. 96 * @private 97 */ 98 HomeComponent3D.prototype.createComponent3D = function(canvasId, preferences, controller) { 99 this.canvas3D = new HTMLCanvas3D(canvasId); 100 var component3D = this; 101 this.preferencesChangeListener = function(ev) { 102 switch (ev.getPropertyName()) { 103 case "DEFAULT_FONT_NAME" : 104 case "UNIT" : 105 component3D.updateObjects(component3D.home.getDimensionLines()); 106 break; 107 case "EDITING_IN_3D_VIEW_ENABLED" : 108 component3D.updateObjectsAndFurnitureGroups(component3D.home.getSelectedItems()); 109 break; 110 case "NAVIGATION_PANEL_VISIBLE" : 111 component3D.setNavigationPanelVisible(ev.getNewValue()); 112 break; 113 } 114 }; 115 if (controller) { 116 this.addMouseListeners(controller, preferences, this.canvas3D); 117 if (preferences !== null) { 118 this.navigationPanelId = this.createNavigationPanel(this.home, preferences, controller); 119 this.setNavigationPanelVisible(preferences.isNavigationPanelVisible()); 120 preferences.addPropertyChangeListener("NAVIGATION_PANEL_VISIBLE", this.preferencesChangeListener); 121 preferences.addPropertyChangeListener("EDITING_IN_3D_VIEW_ENABLED", this.preferencesChangeListener); 122 } 123 this.createActions(controller); 124 this.installKeyboardActions(); 125 } 126 127 // Update field of view from current camera 128 this.updateView(this.home.getCamera()); 129 // Update point of view from current camera 130 this.updateViewPlatformTransform(this.home.getCamera(), false); 131 // Add camera listeners to update later point of view from camera 132 this.addCameraListeners(); 133 134 this.canvas3D.setScene(this.createSceneTree(true, false)); 135 136 if (preferences !== null) { 137 preferences.addPropertyChangeListener("UNIT", this.preferencesChangeListener); 138 preferences.addPropertyChangeListener("DEFAULT_FONT_NAME", this.preferencesChangeListener); 139 } 140 } 141 142 /** 143 * Returns the HTML element used to view this component at screen. 144 */ 145 HomeComponent3D.prototype.getHTMLElement = function() { 146 return this.canvas3D.getHTMLElement(); 147 } 148 149 /** 150 * Disposes the 3D shapes geometries displayed by this component. 151 * @package 152 * @ignore 153 */ 154 HomeComponent3D.prototype.disposeGeometries = function() { 155 if (this.home.structure) { 156 ModelManager.getInstance().unloadModel(this.home.structure, true); 157 } 158 ModelManager.getInstance().disposeGeometries(this.canvas3D.getScene()); 159 } 160 161 /** 162 * Updates 3D component aspect and navigation panel after a resize. 163 * @package 164 * @ignore 165 */ 166 HomeComponent3D.prototype.revalidate = function() { 167 var canvas = this.canvas3D.getHTMLElement(); 168 var canvasBounds = canvas.getBoundingClientRect(); 169 if (this.navigationPanelId != null) { 170 navigationPanelDiv = document.getElementById(this.navigationPanelId); 171 if (navigationPanelDiv !== undefined && navigationPanelDiv.style !== undefined) { 172 navigationPanelDiv.style.left = (canvasBounds.left + window.pageXOffset) + "px"; 173 navigationPanelDiv.style.top = (canvasBounds.top + window.pageYOffset) + "px"; 174 } 175 } 176 this.canvas3D.updateViewportSize(); 177 } 178 179 /** 180 * Returns the id of a component displayed as navigation panel upon this 3D view. 181 * @private 182 */ 183 HomeComponent3D.prototype.createNavigationPanel = function(home, preferences, controller) { 184 // Retrieve body elements with a data-simulated-key attribute 185 var simulatedKeys = this.getSimulatedKeyElements(document.getElementsByTagName("body") [0]); 186 var navigationPanelDiv = null; 187 var innerHtml; 188 try { 189 innerHtml = preferences.getLocalizedString("HomeComponent3D", "navigationPanel.innerHTML"); 190 } catch (ex) { 191 innerHtml = 192 '<img src="' + ZIPTools.getScriptFolder("gl-matrix-min.js") + 'navigationPanel.png"' 193 + ' style="width: 56px; height:59px; margin:5px; user-drag: none; user-select: none; -moz-user-select: none; -webkit-user-drag: none; -webkit-user-select: none; -ms-user-select: none;"' 194 + ' usemap="#navigationPanelMap"/>' 195 + '<map name="navigationPanelMap" id="navigationPanelMap">' 196 + ' <area shape="poly" coords="28,4,33,8,33,19,22,19,22,8,29,4" data-simulated-key="UP" />' 197 + ' <area shape="poly" coords="4,28,8,23,19,23,19,34,8,34,4,29" data-simulated-key="LEFT" />' 198 + ' <area shape="poly" coords="28,54,33,50,33,39,22,39,22,50,29,54" data-simulated-key="DOWN" />' 199 + ' <area shape="poly" coords="51,28,47,23,36,23,36,34,47,34,51,29" data-simulated-key="RIGHT" />' 200 + ' <area shape="poly" coords="28,22,33,26,33,28,22,28,22,26,29,22" data-simulated-key="PAGE_UP" />' 201 + ' <area shape="poly" coords="28,36,33,32,33,30,22,30,22,32,29,36" data-simulated-key="PAGE_DOWN" />' 202 + '</map>'; 203 } 204 var component3D = this; 205 if (innerHtml !== null) { 206 navigationPanelDiv = document.createElement("div"); 207 navigationPanelDiv.setAttribute("id", "div" + Math.floor(Math.random() * 1E10)); 208 navigationPanelDiv.style.position = "absolute"; 209 this.windowResizeListener = function(ev) { 210 component3D.revalidate(); 211 }; 212 window.addEventListener("resize", this.windowResizeListener); 213 // Search the first existing zIndex among parents 214 var parentZIndex = 0; 215 for (var element = this.canvas3D.getHTMLElement(); 216 element && element.style && isNaN(parentZIndex = parseInt(element.style.zIndex)); 217 element = element.parentElement) { 218 } 219 navigationPanelDiv.style.zIndex = isNaN(parentZIndex) ? "1" : (parentZIndex + 1).toString(); 220 navigationPanelDiv.style.visibility = "hidden"; 221 navigationPanelDiv.innerHTML = innerHtml; 222 simulatedKeys.push.apply(simulatedKeys, this.getSimulatedKeyElements(navigationPanelDiv)); 223 var bodyElement = document.getElementsByTagName("body") [0]; 224 bodyElement.insertBefore(navigationPanelDiv, bodyElement.firstChild); 225 // Redirect mouse clicks out of div elements to this component 226 navigationPanelDiv.addEventListener("mousedown", 227 function(ev) { 228 component3D.mouseListener.mousePressed(ev); 229 }); 230 } 231 232 this.simulatedEventListener = { 233 mousePressed: function(ev) { 234 var simulatedElement = ev.target; 235 var repeatKeyAction = function() { 236 var attribute = simulatedElement.getAttribute("data-simulated-key"); 237 var keyName = attribute.substring(attribute.indexOf(":") + 1); 238 var keyStroke = ""; 239 if (ev.ctrlKey || keyName.indexOf("control ") != -1) { 240 keyStroke += "control "; 241 } 242 if (ev.altKey || keyName.indexOf("alt ") != -1) { 243 keyStroke += "alt "; 244 } 245 if (ev.metaKey || keyName.indexOf("meta ") != -1) { 246 keyStroke += "meta "; 247 } 248 if (ev.shiftKey || keyName.indexOf("shift ") != -1) { 249 keyStroke += "shift "; 250 } 251 keyStroke += "pressed " + keyName; 252 component3D.callAction(ev, keyStroke); 253 }; 254 var stopInterval = function(ev) { 255 window.clearInterval(intervalId); 256 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 257 && window.PointerEvent) { 258 simulatedElement.removeEventListener("pointerup", stopInterval); 259 simulatedElement.removeEventListener("pointerleave", stopInterval); 260 } else { 261 simulatedElement.removeEventListener("touchend", stopInterval); 262 simulatedElement.removeEventListener("mouseup", stopInterval); 263 simulatedElement.removeEventListener("mouseleave", stopInterval); 264 } 265 component3D.mouseListener.windowMouseReleased(ev); 266 ev.stopPropagation(); 267 }; 268 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 269 && window.PointerEvent) { 270 // Multi touch support for IE and Edge 271 simulatedElement.addEventListener("pointerup", stopInterval); 272 simulatedElement.addEventListener("pointerleave", stopInterval); 273 } else { 274 simulatedElement.addEventListener("touchend", stopInterval); 275 simulatedElement.addEventListener("mouseup", stopInterval); 276 simulatedElement.addEventListener("mouseleave", stopInterval); 277 } 278 repeatKeyAction(); 279 var intervalId = window.setInterval(repeatKeyAction, 80); 280 }, 281 pointerMousePressed : function(ev) { 282 // Required to avoid click simulation 283 ev.stopPropagation(); 284 }, 285 touchStarted : function(ev) { 286 // Prevent default behavior to avoid local zooming under iOS >= 15 287 ev.preventDefault(); 288 component3D.simulatedEventListener.mousePressed(ev); 289 } 290 }; 291 for (var i = 0; i < simulatedKeys.length; i++) { 292 // Add a listener that simulates the given key and repeats it until mouse is released 293 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 294 && window.PointerEvent) { 295 // Multi touch support for IE and Edge 296 simulatedKeys [i].addEventListener("pointerdown", this.simulatedEventListener.mousePressed); 297 simulatedKeys [i].addEventListener("mousedown", this.simulatedEventListener.pointerMousePressed); 298 } else { 299 simulatedKeys [i].addEventListener("touchstart", this.simulatedEventListener.touchStarted); 300 simulatedKeys [i].addEventListener("mousedown", this.simulatedEventListener.mousePressed); 301 } 302 } 303 304 if (navigationPanelDiv !== null) { 305 return navigationPanelDiv.getAttribute("id"); 306 } else { 307 return null; 308 } 309 } 310 311 /** 312 * Returns the child elements with a <code>data-simulated-key</code> attribute set. 313 * @package 314 * @ignore 315 */ 316 HomeComponent3D.prototype.getSimulatedKeyElements = function(element) { 317 var simulatedKeyElements = []; 318 if (element.hasChildNodes()) { 319 for (var i = 0; i < element.childNodes.length; i++) { 320 var child = element.childNodes [i]; 321 if (child.hasAttribute 322 && child.hasAttribute("data-simulated-key")) { 323 // Take into account only components with a data-simulated-key attribute 324 // that contains no colon or that starts with canvas id followed by a colon 325 var simulatedKey = child.getAttribute("data-simulated-key"); 326 if (simulatedKey.indexOf(":") === -1 327 || simulatedKey.indexOf(this.canvas3D.getHTMLElement().getAttribute("id") + ":") === 0) { 328 simulatedKeyElements.push(child); 329 } 330 } 331 simulatedKeyElements.push.apply(simulatedKeyElements, this.getSimulatedKeyElements(child)); 332 } 333 } 334 return simulatedKeyElements; 335 } 336 337 /** 338 * Sets the image that will be drawn upon the 3D component shown by this component. 339 * @private 340 */ 341 HomeComponent3D.prototype.setNavigationPanelVisible = function(visible) { 342 if (this.navigationPanelId != null) { 343 document.getElementById(this.navigationPanelId).style.visibility = visible ? "visible" : "hidden"; 344 if (visible) { 345 this.revalidate(); 346 } 347 } 348 } 349 350 /** 351 * Remove all listeners bound to home that updates 3D scene objects. 352 * @private 353 */ 354 HomeComponent3D.prototype.removeHomeListeners = function() { 355 this.home.removePropertyChangeListener("CAMERA", this.homeCameraListener); 356 var homeEnvironment = this.home.getEnvironment(); 357 homeEnvironment.removePropertyChangeListener("SKY_COLOR", this.backgroundChangeListener); 358 homeEnvironment.removePropertyChangeListener("SKY_TEXTURE", this.backgroundChangeListener); 359 homeEnvironment.removePropertyChangeListener("GROUND_COLOR", this.backgroundChangeListener); 360 homeEnvironment.removePropertyChangeListener("GROUND_TEXTURE", this.backgroundChangeListener); 361 homeEnvironment.removePropertyChangeListener("GROUND_COLOR", this.groundChangeListener); 362 homeEnvironment.removePropertyChangeListener("GROUND_TEXTURE", this.groundChangeListener); 363 homeEnvironment.removePropertyChangeListener("BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D", this.groundChangeListener); 364 this.home.removePropertyChangeListener("BACKGROUND_IMAGE", this.groundChangeListener); 365 homeEnvironment.removePropertyChangeListener("LIGHT_COLOR", this.lightColorListener); 366 homeEnvironment.removePropertyChangeListener("WALLS_ALPHA", this.wallsAlphaListener); 367 this.home.getCamera().removePropertyChangeListener(this.cameraChangeListener); 368 this.home.removePropertyChangeListener("CAMERA", this.elevationChangeListener); 369 this.home.getCamera().removePropertyChangeListener(this.elevationChangeListener); 370 this.home.removeSelectionListener(this.selectionListener); 371 this.home.removeLevelsListener(this.levelListener); 372 var levels = this.home.getLevels(); 373 for (var i = 0; i < levels.length; i++) { 374 levels[i].removePropertyChangeListener(this.levelChangeListener); 375 } 376 this.home.removeWallsListener(this.wallListener); 377 var walls = this.home.getWalls(); 378 for (var i = 0; i < walls.length; i++) { 379 walls[i].removePropertyChangeListener(this.wallChangeListener); 380 } 381 this.home.removeFurnitureListener(this.furnitureListener); 382 var furniture = this.home.getFurniture(); 383 for (var i = 0; i < furniture.length; i++) { 384 this.removePropertyChangeListener(furniture [i], this.furnitureChangeListener); 385 } 386 this.home.removeRoomsListener(this.roomListener); 387 var rooms = this.home.getRooms(); 388 for (var i = 0; i < rooms.length; i++) { 389 rooms[i].removePropertyChangeListener(this.roomChangeListener); 390 } 391 this.home.removePolylinesListener(this.polylineListener); 392 var polylines = this.home.getPolylines(); 393 for (var i = 0; i < polylines.length; i++) { 394 polylines[i].removePropertyChangeListener(this.polylineChangeListener); 395 } 396 this.home.removeDimensionLinesListener(this.dimensionLineListener); 397 var dimensionLines = this.home.getDimensionLines(); 398 for (var i = 0; i < dimensionLines.length; i++) { 399 dimensionLines [i].removePropertyChangeListener(this.dimensionLineChangeListener); 400 } 401 this.home.removeLabelsListener(this.labelListener); 402 var labels = this.home.getLabels(); 403 for (var i = 0; i < labels.length; i++) { 404 labels[i].removePropertyChangeListener(this.labelChangeListener); 405 } 406 } 407 408 /** 409 * Remove all mouse listeners bound to the canvas3D and window. 410 * @private 411 */ 412 HomeComponent3D.prototype.removeMouseListeners = function(canvas3D) { 413 if (this.mouseListener) { 414 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 415 && window.PointerEvent) { 416 // Multi touch support for IE and Edge 417 canvas3D.getHTMLElement().removeEventListener("pointerdown", this.mouseListener.pointerPressed); 418 canvas3D.getHTMLElement().removeEventListener("mousedown", this.mouseListener.pointerMousePressed); 419 canvas3D.getHTMLElement().removeEventListener("dblclick", this.mouseListener.mouseDoubleClicked); 420 window.removeEventListener("pointermove", this.mouseListener.windowPointerMoved); 421 window.removeEventListener("pointerup", this.mouseListener.windowPointerReleased); 422 } else { 423 canvas3D.getHTMLElement().removeEventListener("touchstart", this.mouseListener.touchStarted); 424 canvas3D.getHTMLElement().removeEventListener("touchmove", this.mouseListener.touchMoved); 425 canvas3D.getHTMLElement().removeEventListener("touchend", this.mouseListener.touchEnded); 426 canvas3D.getHTMLElement().removeEventListener("mousedown", this.mouseListener.mousePressed); 427 canvas3D.getHTMLElement().removeEventListener("dblclick", this.mouseListener.mouseDoubleClicked); 428 window.removeEventListener("mousemove", this.mouseListener.windowMouseMoved); 429 window.removeEventListener("mouseup", this.mouseListener.windowMouseReleased); 430 } 431 canvas3D.getHTMLElement().removeEventListener("contextmenu", this.mouseListener.contextMenuDisplayed); 432 canvas3D.getHTMLElement().removeEventListener("DOMMouseScroll", this.mouseListener.mouseScrolled); 433 canvas3D.getHTMLElement().removeEventListener("mousewheel", this.mouseListener.mouseWheelMoved); 434 } 435 } 436 437 /** 438 * Frees listeners and canvas data. 439 */ 440 HomeComponent3D.prototype.dispose = function() { 441 this.removeHomeListeners(); 442 this.removeMouseListeners(this.canvas3D); 443 if (this.navigationPanelId != null) { 444 this.preferences.removePropertyChangeListener("NAVIGATION_PANEL_VISIBLE", this.preferencesChangeListener); 445 window.removeEventListener("resize", this.windowResizeListener); 446 var simulatedKeys = this.getSimulatedKeyElements(document.getElementsByTagName("body") [0]); 447 for (var i = 0; i < simulatedKeys.length; i++) { 448 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 449 && window.PointerEvent) { 450 simulatedKeys [i].removeEventListener("pointerdown", this.simulatedEventListener.mousePressed); 451 simulatedKeys [i].removeEventListener("mousedown", this.simulatedEventListener.pointerMousePressed); 452 } else { 453 simulatedKeys [i].removeEventListener("touchstart", this.simulatedEventListener.touchStarted); 454 simulatedKeys [i].removeEventListener("mousedown", this.simulatedEventListener.mousePressed); 455 } 456 } 457 var navigationPanel = document.getElementById(this.navigationPanelId); 458 navigationPanel.parentElement.removeChild(navigationPanel); 459 this.navigationPanelId = null; 460 } 461 if (this.preferences !== null) { 462 this.preferences.removePropertyChangeListener("EDITING_IN_3D_VIEW_ENABLED", this.preferencesChangeListener); 463 this.preferences.removePropertyChangeListener("UNIT", this.preferencesChangeListener); 464 this.preferences.removePropertyChangeListener("DEFAULT_FONT_NAME", this.preferencesChangeListener); 465 } 466 this.canvas3D.clear(); 467 } 468 469 /** 470 * Adds listeners to home to update point of view from current camera. 471 * @private 472 */ 473 HomeComponent3D.prototype.addCameraListeners = function() { 474 var component3D = this; 475 var home = this.home; 476 this.cameraChangeListener = function(ev) { 477 if (!component3D.cameraChangeListener.updater) { 478 // Update view transform later to let finish camera changes 479 component3D.cameraChangeListener.updater = function() { 480 if (component3D.canvas3D) { 481 component3D.updateView(home.getCamera()); 482 component3D.updateViewPlatformTransform(home.getCamera(), true); 483 } 484 delete component3D.cameraChangeListener.updater; 485 }; 486 setTimeout(component3D.cameraChangeListener.updater, 0); 487 } 488 }; 489 home.getCamera().addPropertyChangeListener(this.cameraChangeListener); 490 this.homeCameraListener = function(ev) { 491 component3D.updateView(home.getCamera()); 492 component3D.updateViewPlatformTransform(home.getCamera(), false); 493 // Add camera change listener to new active camera 494 ev.getOldValue().removePropertyChangeListener(component3D.cameraChangeListener); 495 home.getCamera().addPropertyChangeListener(component3D.cameraChangeListener); 496 }; 497 this.home.addPropertyChangeListener("CAMERA", this.homeCameraListener); 498 } 499 500 /** 501 * Updates <code>view</code> from <code>camera</code> field of view. 502 * @private 503 */ 504 HomeComponent3D.prototype.updateView = function(camera) { 505 var fieldOfView = camera.getFieldOfView(); 506 if (fieldOfView === 0) { 507 fieldOfView = Math.PI * 63 / 180; 508 } 509 this.canvas3D.setFieldOfView(fieldOfView); 510 var frontClipDistance = 2.5; 511 var frontBackDistanceRatio = 500000; 512 if (this.canvas3D.getDepthBits() <= 16) { 513 // It's recommended to keep ratio between back and front clip distances under 3000 514 var frontBackDistanceRatio = 3000; 515 var approximateHomeBounds = this.getApproximateHomeBounds(); 516 // If camera is out of home bounds, adjust the front clip distance to the distance to home bounds 517 if (approximateHomeBounds != null 518 && !approximateHomeBounds.intersect(vec3.fromValues(camera.getX(), camera.getY(), camera.getZ()))) { 519 var distanceToClosestBoxSide = this.getDistanceToBox(camera.getX(), camera.getY(), camera.getZ(), approximateHomeBounds); 520 if (!isNaN(distanceToClosestBoxSide)) { 521 frontClipDistance = Math.max(frontClipDistance, 0.1 * distanceToClosestBoxSide); 522 } 523 } 524 } else { 525 var homeHeight = this.getHomeHeight(); 526 if (camera.getZ() > homeHeight) { 527 frontClipDistance = Math.max(frontClipDistance, (camera.getZ() - homeHeight) / 10); 528 } 529 } 530 var canvasBounds = this.canvas3D.getHTMLElement().getBoundingClientRect(); 531 if (camera.getZ() > 0 && canvasBounds.width !== 0 && canvasBounds.height !== 0) { 532 var halfVerticalFieldOfView = Math.atan(Math.tan(fieldOfView / 2) * canvasBounds.height / canvasBounds.width); 533 var fieldOfViewBottomAngle = camera.getPitch() + halfVerticalFieldOfView; 534 // If the horizon is above the frustrum bottom, take into account the distance to the ground 535 if (fieldOfViewBottomAngle > 0) { 536 var distanceToGroundAtFieldOfViewBottomAngle = (camera.getZ() / Math.sin(fieldOfViewBottomAngle)); 537 frontClipDistance = Math.min(frontClipDistance, 0.35 * distanceToGroundAtFieldOfViewBottomAngle); 538 if (frontClipDistance * frontBackDistanceRatio < distanceToGroundAtFieldOfViewBottomAngle) { 539 // Ensure the ground is always visible at the back clip distance 540 frontClipDistance = distanceToGroundAtFieldOfViewBottomAngle / frontBackDistanceRatio; 541 } 542 } 543 } 544 // Update front and back clip distance 545 this.canvas3D.setFrontClipDistance(frontClipDistance); 546 this.canvas3D.setBackClipDistance(frontClipDistance * frontBackDistanceRatio); 547 } 548 549 /** 550 * Returns quickly computed bounds of the objects in home. 551 * @private 552 */ 553 HomeComponent3D.prototype.getApproximateHomeBounds = function() { 554 if (this.approximateHomeBoundsCache === null) { 555 var approximateHomeBounds = null; 556 var furniture = this.home.getFurniture(); 557 for (var i = 0; i < furniture.length; i++) { 558 var piece = furniture[i]; 559 if (piece.isVisible() 560 && (piece.getLevel() === null 561 || piece.getLevel().isViewable())) { 562 var halfMaxDimension = Math.max(piece.getWidthInPlan(), piece.getDepthInPlan()) / 2; 563 var elevation = piece.getGroundElevation(); 564 var pieceLocation = vec3.fromValues( 565 piece.getX() - halfMaxDimension, piece.getY() - halfMaxDimension, elevation); 566 if (approximateHomeBounds === null) { 567 approximateHomeBounds = new BoundingBox3D(pieceLocation, pieceLocation); 568 } else { 569 approximateHomeBounds.combine(pieceLocation); 570 } 571 approximateHomeBounds.combine(vec3.fromValues( 572 piece.getX() + halfMaxDimension, piece.getY() + halfMaxDimension, elevation + piece.getHeightInPlan())); 573 } 574 } 575 var walls = this.home.getWalls(); 576 for (var i = 0; i < walls.length; i++) { 577 var wall = walls[i]; 578 if (wall.getLevel() === null 579 || wall.getLevel().isViewable()) { 580 var startPoint = vec3.fromValues(wall.getXStart(), wall.getYStart(), 581 wall.getLevel() !== null ? wall.getLevel().getElevation() : 0); 582 if (approximateHomeBounds === null) { 583 approximateHomeBounds = new BoundingBox3D(startPoint, startPoint); 584 } else { 585 approximateHomeBounds.combine(startPoint); 586 } 587 approximateHomeBounds.combine(vec3.fromValues(wall.getXEnd(), wall.getYEnd(), 588 startPoint.z + (wall.getHeight() !== null ? wall.getHeight() : this.home.getWallHeight()))); 589 } 590 } 591 var rooms = this.home.getRooms(); 592 for (var i = 0; i < rooms.length; i++) { 593 var room = rooms[i]; 594 if (room.getLevel() === null || room.getLevel().isViewable()) { 595 var center = vec3.fromValues(room.getXCenter(), room.getYCenter(), 596 room.getLevel() !== null ? room.getLevel().getElevation() : 0); 597 if (approximateHomeBounds === null) { 598 approximateHomeBounds = new BoundingBox3D(center, center); 599 } else { 600 approximateHomeBounds.combine(center); 601 } 602 } 603 } 604 var dimensionLines = this.home.getDimensionLines(); 605 for (var i = 0; i < dimensionLines.length; i++) { 606 var dimensionLine = dimensionLines [i]; 607 if ((dimensionLine.getLevel() == null 608 || dimensionLine.getLevel().isViewable()) 609 && dimensionLine.isVisibleIn3D()) { 610 var levelElevation = dimensionLine.getLevel() != null ? dimensionLine.getLevel().getElevation() : 0; 611 var startPoint = vec3.fromValues(dimensionLine.getXStart(), dimensionLine.getYStart(), 612 levelElevation + dimensionLine.getElevationStart()); 613 if (approximateHomeBounds == null) { 614 approximateHomeBounds = new BoundingBox3D(startPoint, startPoint); 615 } else { 616 approximateHomeBounds.combine(startPoint); 617 } 618 approximateHomeBounds.combine(vec3.fromValues(dimensionLine.getXEnd(), dimensionLine.getYEnd(), 619 levelElevation + dimensionLine.getElevationEnd())); 620 } 621 } 622 var labels = this.home.getLabels(); 623 for (var i = 0; i < labels.length; i++) { 624 var label = labels[i]; 625 if ((label.getLevel() === null 626 || label.getLevel().isViewable()) 627 && label.getPitch() !== null) { 628 var center = vec3.fromValues(label.getX(), label.getY(), label.getGroundElevation()); 629 if (approximateHomeBounds == null) { 630 approximateHomeBounds = new BoundingBox3D(center, center); 631 } else { 632 approximateHomeBounds.combine(center); 633 } 634 } 635 } 636 var polylines = this.home.getPolylines(); 637 for (var i = 0; i < polylines.length; i++) { 638 var polyline = polylines [i]; 639 if ((polyline.getLevel() == null 640 || polyline.getLevel().isViewable()) 641 && polyline.isVisibleIn3D()) { 642 var points = polyline.getPoints() 643 for (var j = 0; j < points.length; j++) { 644 var point3d = vec3.fromValues(points[j][0], points[j][1], polyline.getGroundElevation()); 645 if (approximateHomeBounds == null) { 646 approximateHomeBounds = new BoundingBox3D(point3d, point3d); 647 } else { 648 approximateHomeBounds.combine(point3d); 649 } 650 } 651 } 652 } 653 this.approximateHomeBoundsCache = approximateHomeBounds; 654 } 655 return this.approximateHomeBoundsCache; 656 } 657 658 /** 659 * Returns the distance between the point at the given coordinates (x,y,z) 660 * and the closest side of <code>box</code>. 661 * @private 662 */ 663 HomeComponent3D.prototype.getDistanceToBox = function (x, y, z, box) { 664 var point = vec3.fromValues(x, y, z); 665 var lower = vec3.create(); 666 box.getLower(lower); 667 var upper = vec3.create(); 668 box.getUpper(upper); 669 var boxVertices = [ 670 vec3.fromValues(lower[0], lower[1], lower[2]), 671 vec3.fromValues(upper[0], lower[1], lower[2]), 672 vec3.fromValues(lower[0], upper[1], lower[2]), 673 vec3.fromValues(upper[0], upper[1], lower[2]), 674 vec3.fromValues(lower[0], lower[1], upper[2]), 675 vec3.fromValues(upper[0], lower[1], upper[2]), 676 vec3.fromValues(lower[0], upper[1], upper[2]), 677 vec3.fromValues(upper[0], upper[1], upper[2])]; 678 var distancesToVertex = new Array(boxVertices.length); 679 for (var i = 0; i < distancesToVertex.length; i++) { 680 distancesToVertex[i] = vec3.squaredDistance(point, boxVertices[i]); 681 } 682 var distancesToSide = [ 683 this.getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 3, 2, 2), 684 this.getDistanceToSide(point, boxVertices, distancesToVertex, 0, 1, 5, 4, 1), 685 this.getDistanceToSide(point, boxVertices, distancesToVertex, 0, 2, 6, 4, 0), 686 this.getDistanceToSide(point, boxVertices, distancesToVertex, 4, 5, 7, 6, 2), 687 this.getDistanceToSide(point, boxVertices, distancesToVertex, 2, 3, 7, 6, 1), 688 this.getDistanceToSide(point, boxVertices, distancesToVertex, 1, 3, 7, 5, 0)]; 689 var distance = distancesToSide[0]; 690 for (var i = 1; i < distancesToSide.length; i++) { 691 distance = Math.min(distance, distancesToSide[i]); 692 } 693 return distance; 694 } 695 696 /** 697 * Returns the distance between the given <code>point</code> and the plane defined by four vertices. 698 * @private 699 */ 700 HomeComponent3D.prototype.getDistanceToSide = function (point, boxVertices, distancesSquaredToVertex, 701 index1, index2, index3, index4, axis) { 702 switch (axis) { 703 case 0: 704 if (point[1] <= boxVertices[index1][1]) { 705 if (point[2] <= boxVertices[index1][2]) { 706 return Math.sqrt(distancesSquaredToVertex[index1]); 707 } else if (point[2] >= boxVertices[index4][2]) { 708 return Math.sqrt(distancesSquaredToVertex[index4]); 709 } else { 710 return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index4]); 711 } 712 } else if (point[1] >= boxVertices[index2][1]) { 713 if (point[2] <= boxVertices[index2][2]) { 714 return Math.sqrt(distancesSquaredToVertex[index2]); 715 } else if (point[2] >= boxVertices[index3][2]) { 716 return Math.sqrt(distancesSquaredToVertex[index3]); 717 } else { 718 return this.getDistanceToLine(point, boxVertices[index2], boxVertices[index3]); 719 } 720 } else if (point[2] <= boxVertices[index1][2]) { 721 return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index2]); 722 } else if (point[2] >= boxVertices[index4][2]) { 723 return this.getDistanceToLine(point, boxVertices[index3], boxVertices[index4]); 724 } 725 break; 726 case 1 : // Normal along y axis 727 if (point[0] <= boxVertices[index1][0]) { 728 if (point[2] <= boxVertices[index1][2]) { 729 return Math.sqrt(distancesSquaredToVertex[index1]); 730 } else if (point[2] >= boxVertices[index4][2]) { 731 return Math.sqrt(distancesSquaredToVertex[index4]); 732 } else { 733 return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index4]); 734 } 735 } else if (point[0] >= boxVertices[index2][0]) { 736 if (point[2] <= boxVertices[index2][2]) { 737 return Math.sqrt(distancesSquaredToVertex[index2]); 738 } else if (point[2] >= boxVertices[index3][2]) { 739 return Math.sqrt(distancesSquaredToVertex[index3]); 740 } else { 741 return this.getDistanceToLine(point, boxVertices[index2], boxVertices[index3]); 742 } 743 } else if (point[2] <= boxVertices[index1][2]) { 744 return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index2]); 745 } else if (point[2] >= boxVertices[index4][2]) { 746 return this.getDistanceToLine(point, boxVertices[index3], boxVertices[index4]); 747 } 748 break; 749 case 2 : // Normal along z axis 750 if (point[0] <= boxVertices[index1][0]) { 751 if (point[1] <= boxVertices[index1][1]) { 752 return Math.sqrt(distancesSquaredToVertex[index1]); 753 } else if (point[1] >= boxVertices[index4][1]) { 754 return Math.sqrt(distancesSquaredToVertex[index4]); 755 } else { 756 return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index4]); 757 } 758 } else if (point[0] >= boxVertices[index2][0]) { 759 if (point[1] <= boxVertices[index2][1]) { 760 return Math.sqrt(distancesSquaredToVertex[index2]); 761 } else if (point[1] >= boxVertices[index3][1]) { 762 return Math.sqrt(distancesSquaredToVertex[index3]); 763 } else { 764 return this.getDistanceToLine(point, boxVertices[index2], boxVertices[index3]); 765 } 766 } else if (point[1] <= boxVertices[index1][1]) { 767 return this.getDistanceToLine(point, boxVertices[index1], boxVertices[index2]); 768 } else if (point[1] >= boxVertices[index4][1]) { 769 return this.getDistanceToLine(point, boxVertices[index3], boxVertices[index4]); 770 } 771 break; 772 } 773 774 // Return distance to plane 775 // from https://fr.wikipedia.org/wiki/Distance_d%27un_point_Ã _un_plan 776 var vector1 = vec3.fromValues(boxVertices[index2][0] - boxVertices[index1][0], 777 boxVertices[index2][1] - boxVertices[index1][1], 778 boxVertices[index2][2] - boxVertices[index1][2]); 779 var vector2 = vec3.fromValues(boxVertices[index3][0] - boxVertices[index1][0], 780 boxVertices[index3][1] - boxVertices[index1][1], 781 boxVertices[index3][2] - boxVertices[index1][2]); 782 var normal = vec3.create(); 783 vec3.cross(normal, vector1, vector2); 784 return Math.abs(vec3.dot(normal, vec3.fromValues(boxVertices[index1][0] - point[0], boxVertices[index1][1] - point[1], boxVertices[index1][2] - point[2]))) / vec3.length(normal); 785 } 786 787 /** 788 * Returns the distance between the given <code>point</code> and the line defined by two points. 789 * @private 790 */ 791 HomeComponent3D.prototype.getDistanceToLine = function (point, point1, point2) { 792 // From https://fr.wikipedia.org/wiki/Distance_d%27un_point_Ã _une_droite#Dans_l.27espace 793 var lineDirection = vec3.fromValues(point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]); 794 var vector = vec3.fromValues(point[0] - point1[0], point[1] - point1[1], point[2] - point1[2]); 795 var crossProduct = vec3.create(); 796 vec3.cross(crossProduct, lineDirection, vector); 797 return vec3.length(crossProduct) / vec3.length(lineDirection); 798 } 799 800 /** 801 * Returns quickly computed height of the home. 802 * @private 803 */ 804 HomeComponent3D.prototype.getHomeHeight = function() { 805 if (this.homeHeightCache === null) { 806 var homeHeight = 0; 807 var furniture = this.home.getFurniture(); 808 for (var i = 0; i < furniture.length; i++) { 809 var piece = furniture[i]; 810 if (piece.isVisible() 811 && (piece.getLevel() == null 812 || piece.getLevel().isViewable())) { 813 homeHeight = Math.max(homeHeight, piece.getGroundElevation() + piece.getHeight()); 814 } 815 } 816 var walls = this.home.getWalls(); 817 for (var i = 0; i < walls.length; i++) { 818 var wall = walls[i]; 819 if (wall.getLevel() == null 820 || wall.getLevel().isViewable()) { 821 var wallElevation = wall.getLevel() != null ? wall.getLevel().getElevation() : 0; 822 if (wall.getHeight() != null) { 823 homeHeight = Math.max(homeHeight, wallElevation + wall.getHeight()); 824 if (wall.getHeightAtEnd() != null) { 825 homeHeight = Math.max(homeHeight, wallElevation + wall.getHeightAtEnd()); 826 } 827 } else { 828 homeHeight = Math.max(homeHeight, wallElevation + this.home.getWallHeight()); 829 } 830 } 831 } 832 var rooms = this.home.getRooms(); 833 for (var i = 0; i < rooms.length; i++) { 834 var room = rooms[i]; 835 if (room.getLevel() != null 836 && room.getLevel().isViewable()) { 837 homeHeight = Math.max(homeHeight, room.getLevel().getElevation()); 838 } 839 } 840 var polylines = this.home.getPolylines(); 841 for (var i = 0; i < polylines.length; i++) { 842 var polyline = polylines[i]; 843 if (polyline.isVisibleIn3D() 844 && (polyline.getLevel() == null 845 || polyline.getLevel().isViewable())) { 846 homeHeight = Math.max(homeHeight, polyline.getGroundElevation()); 847 } 848 } 849 var dimensionLines = this.home.getDimensionLines(); 850 for (var i = 0; i < dimensionLines.length; i++) { 851 var dimensionLine = dimensionLines [i]; 852 if (dimensionLine.isVisibleIn3D() 853 && (dimensionLine.getLevel() == null 854 || dimensionLine.getLevel().isViewable())) { 855 var levelElevation = dimensionLine.getLevel() != null ? dimensionLine.getLevel().getElevation() : 0; 856 homeHeight = Math.max(homeHeight, 857 levelElevation + Math.max(dimensionLine.getElevationStart(), dimensionLine.getElevationEnd())); 858 } 859 } 860 var labels = this.home.getLabels(); 861 for (var i = 0; i < labels.length; i++) { 862 var label = labels[i]; 863 if (label.getPitch() != null 864 && (label.getLevel() == null 865 || label.getLevel().isViewable())) { 866 homeHeight = Math.max(homeHeight, label.getGroundElevation()); 867 } 868 } 869 this.homeHeightCache = homeHeight; 870 } 871 return this.homeHeightCache; 872 } 873 874 /** 875 * Updates view transform from <code>camera</code> angles and location. 876 * @private 877 */ 878 HomeComponent3D.prototype.updateViewPlatformTransform = function(camera, updateWithAnimation) { 879 if (updateWithAnimation) { 880 this.moveCameraWithAnimation(camera); 881 } else { 882 delete this.cameraInterpolator; // Stop camera animation if any 883 var viewPlatformTransform = mat4.create(); 884 this.computeViewPlatformTransform(viewPlatformTransform, camera.getX(), camera.getY(), 885 camera.getZ(), camera.getYaw(), camera.getPitch()); 886 this.canvas3D.setViewPlatformTransform(viewPlatformTransform); 887 } 888 } 889 890 /** 891 * Moves the camera to a new location using an animation for smooth moves. 892 * @private 893 */ 894 HomeComponent3D.prototype.moveCameraWithAnimation = function(finalCamera) { 895 if (this.cameraInterpolator === undefined) { 896 this.cameraInterpolator = {initialCamera : null, finalCamera : null, alpha : null}; 897 } 898 if (this.cameraInterpolator.finalCamera === null 899 || this.cameraInterpolator.finalCamera.getX() !== finalCamera.getX() 900 || this.cameraInterpolator.finalCamera.getY() !== finalCamera.getY() 901 || this.cameraInterpolator.finalCamera.getZ() !== finalCamera.getZ() 902 || this.cameraInterpolator.finalCamera.getYaw() !== finalCamera.getYaw() 903 || this.cameraInterpolator.finalCamera.getPitch() !== finalCamera.getPitch()) { 904 if (this.cameraInterpolator.alpha === null || this.cameraInterpolator.alpha === 1) { 905 this.cameraInterpolator.initialCamera = new Camera(this.camera.getX(), this.camera.getY(), this.camera.getZ(), 906 this.camera.getYaw(), this.camera.getPitch(), this.camera.getFieldOfView()); 907 } else if (this.cameraInterpolator.alpha < 0.3) { 908 var finalTransformation = mat4.create(); 909 // Jump directly to final location 910 this.computeViewPlatformTransform(finalTransformation, this.cameraInterpolator.finalCamera.getX(), this.cameraInterpolator.finalCamera.getY(), this.cameraInterpolator.finalCamera.getZ(), 911 this.cameraInterpolator.finalCamera.getYaw(), this.cameraInterpolator.finalCamera.getPitch()); 912 this.canvas3D.setViewPlatformTransform(finalTransformation); 913 this.cameraInterpolator.initialCamera = this.cameraInterpolator.finalCamera; 914 } else { 915 // Compute initial location from current alpha value 916 this.cameraInterpolator.initialCamera = new Camera(this.cameraInterpolator.initialCamera.getX() + (this.cameraInterpolator.finalCamera.getX() - this.cameraInterpolator.initialCamera.getX()) * this.cameraInterpolator.alpha, 917 this.cameraInterpolator.initialCamera.getY() + (this.cameraInterpolator.finalCamera.getY() - this.cameraInterpolator.initialCamera.getY()) * this.cameraInterpolator.alpha, 918 this.cameraInterpolator.initialCamera.getZ() + (this.cameraInterpolator.finalCamera.getZ() - this.cameraInterpolator.initialCamera.getZ()) * this.cameraInterpolator.alpha, 919 this.cameraInterpolator.initialCamera.getYaw() + (this.cameraInterpolator.finalCamera.getYaw() - this.cameraInterpolator.initialCamera.getYaw()) * this.cameraInterpolator.alpha, 920 this.cameraInterpolator.initialCamera.getPitch() + (this.cameraInterpolator.finalCamera.getPitch() - this.cameraInterpolator.initialCamera.getPitch()) * this.cameraInterpolator.alpha, 921 finalCamera.getFieldOfView()); 922 } 923 this.cameraInterpolator.finalCamera = new Camera(finalCamera.getX(), finalCamera.getY(), finalCamera.getZ(), 924 finalCamera.getYaw(), finalCamera.getPitch(), finalCamera.getFieldOfView()); 925 // Create an animation that will interpolate camera location 926 // between initial camera and final camera in 75 ms 927 if (this.cameraInterpolator.alpha === null) { 928 this.cameraInterpolator.animationDuration = 75; 929 } 930 // Start animation now 931 this.cameraInterpolator.startTime = Date.now(); 932 this.cameraInterpolator.alpha = 0; 933 var component3D = this; 934 requestAnimationFrame( 935 function() { 936 component3D.interpolateUntilAlphaEquals1(); 937 }); 938 } 939 } 940 941 /** 942 * Increases alpha according to elapsed time and interpolates transformation. 943 * @private 944 */ 945 HomeComponent3D.prototype.interpolateUntilAlphaEquals1 = function() { 946 if (this.cameraInterpolator) { 947 var now = Date.now(); 948 var alpha = Math.min(1, (now - this.cameraInterpolator.startTime) / this.cameraInterpolator.animationDuration); 949 if (this.cameraInterpolator.alpha !== alpha) { 950 var transform = mat4.create(); 951 this.computeTransform(alpha, transform); 952 this.canvas3D.setViewPlatformTransform(transform); 953 this.cameraInterpolator.alpha = alpha; 954 } 955 if (this.cameraInterpolator.alpha < 1) { 956 var component3D = this; 957 requestAnimationFrame( 958 function() { 959 component3D.interpolateUntilAlphaEquals1(); 960 }); 961 } 962 } 963 } 964 965 /** 966 * Computes the transformation interpolated between initial and final camera position 967 * according to alpha. 968 * @private 969 */ 970 HomeComponent3D.prototype.computeTransform = function(alpha, transform) { 971 this.computeViewPlatformTransform(transform, 972 this.cameraInterpolator.initialCamera.getX() + (this.cameraInterpolator.finalCamera.getX() - this.cameraInterpolator.initialCamera.getX()) * alpha, 973 this.cameraInterpolator.initialCamera.getY() + (this.cameraInterpolator.finalCamera.getY() - this.cameraInterpolator.initialCamera.getY()) * alpha, 974 this.cameraInterpolator.initialCamera.getZ() + (this.cameraInterpolator.finalCamera.getZ() - this.cameraInterpolator.initialCamera.getZ()) * alpha, 975 this.cameraInterpolator.initialCamera.getYaw() + (this.cameraInterpolator.finalCamera.getYaw() - this.cameraInterpolator.initialCamera.getYaw()) * alpha, 976 this.cameraInterpolator.initialCamera.getPitch() + (this.cameraInterpolator.finalCamera.getPitch() - this.cameraInterpolator.initialCamera.getPitch()) * alpha); 977 } 978 979 /** 980 * Updates view transform from camera angles and location. 981 * @private 982 */ 983 HomeComponent3D.prototype.computeViewPlatformTransform = function(transform, cameraX, cameraY, cameraZ, cameraYaw, cameraPitch) { 984 var yawRotation = mat4.create(); 985 mat4.fromYRotation(yawRotation, -cameraYaw + Math.PI); 986 987 var pitchRotation = mat4.create(); 988 mat4.fromXRotation(pitchRotation, -cameraPitch); 989 mat4.mul(yawRotation, yawRotation, pitchRotation); 990 991 mat4.identity(transform); 992 mat4.translate(transform, transform, vec3.fromValues(cameraX, cameraZ, cameraY)); 993 mat4.mul(transform, transform, yawRotation); 994 995 this.camera = new Camera(cameraX, cameraY, cameraZ, cameraYaw, cameraPitch, 0); 996 } 997 998 /** 999 * Adds mouse listeners to the canvas3D that calls back <code>controller</code> methods. 1000 * @private 1001 */ 1002 HomeComponent3D.prototype.addMouseListeners = function(controller, preferences, canvas3D) { 1003 var component3D = this; 1004 var mouseListener = { 1005 initialPointerLocation: null, 1006 lastPointerLocation: null, 1007 touchEventType : false, 1008 buttonPressed : -1, 1009 pointerTouches : {}, 1010 lastEventType : null, 1011 lastTargetTouches : [], 1012 distanceLastPinch : -1, 1013 firstTouchStartedTimeStamp: 0, 1014 longTouchActivated: false, 1015 doubleLongTouchActivated: false, 1016 longTouchStartTime: 0, 1017 actionStartedInComponent3D : false, 1018 contextMenuEventType: false, 1019 mousePressed : function(ev) { 1020 if (!mouseListener.touchEventType 1021 && !mouseListener.contextMenuEventType 1022 && ev.button === 0) { 1023 mouseListener.updateCoordinates(ev, "mousePressed"); 1024 mouseListener.initialPointerLocation = [ev.canvasX, ev.canvasY]; 1025 mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY]; 1026 mouseListener.actionStartedInComponent3D = true; 1027 controller.pressMouse(ev.canvasX, ev.canvasY, 1028 ev.clickCount, mouseListener.isShiftDown(ev), mouseListener.isAlignmentActivated(ev), 1029 mouseListener.isDuplicationActivated(ev), mouseListener.isMagnetismToggled(ev), View.PointerType.MOUSE); 1030 } 1031 ev.stopPropagation(); 1032 }, 1033 isShiftDown : function(ev) { 1034 return ev.shiftKey && !ev.altKey && !ev.ctrlKey && !ev.metaKey; 1035 }, 1036 isAlignmentActivated : function(ev) { 1037 return OperatingSystem.isWindows() || OperatingSystem.isMacOSX() 1038 ? ev.shiftKey 1039 : ev.shiftKey && !ev.altKey; 1040 }, 1041 isDuplicationActivated : function(ev) { 1042 return OperatingSystem.isMacOSX() 1043 ? ev.altKey 1044 : ev.ctrlKey; 1045 }, 1046 isMagnetismToggled : function(ev) { 1047 return OperatingSystem.isWindows() 1048 ? ev.altKey 1049 : (OperatingSystem.isMacOSX() 1050 ? ev.metaKey 1051 : ev.shiftKey && ev.altKey); 1052 }, 1053 mouseDoubleClicked: function(ev) { 1054 mouseListener.updateCoordinates(ev, "mouseDoubleClicked"); 1055 mouseListener.mousePressed(ev); 1056 mouseListener.actionStartedInComponent3D = false; 1057 }, 1058 windowMouseMoved: function(ev) { 1059 if (!mouseListener.touchEventType 1060 && !mouseListener.contextMenuEventType) { 1061 mouseListener.updateCoordinates(ev, "mouseMoved"); 1062 if (mouseListener.initialPointerLocation != null 1063 && !(mouseListener.initialPointerLocation[0] === ev.canvasX 1064 && mouseListener.initialPointerLocation[1] === ev.canvasY)) { 1065 mouseListener.initialPointerLocation = null; 1066 } 1067 1068 if (mouseListener.initialPointerLocation == null 1069 && (ev.buttons === 0 && mouseListener.isInCanvas(ev) 1070 || mouseListener.actionStartedInComponent3D)) { 1071 if (controller.isEditingState()) { 1072 controller.moveMouse(ev.canvasX, ev.canvasY); 1073 } else if (mouseListener.actionStartedInComponent3D 1074 && document.activeElement === component3D.canvas3D.getHTMLElement()) { 1075 mouseListener.moveCamera(ev.canvasX, ev.canvasY, mouseListener.lastPointerLocation [0], mouseListener.lastPointerLocation [1], 1076 ev.altKey, ev.shiftKey); 1077 } 1078 } 1079 mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY]; 1080 } 1081 }, 1082 windowMouseReleased: function(ev) { 1083 if (!mouseListener.touchEventType) { 1084 if (mouseListener.lastPointerLocation != null) { 1085 if (mouseListener.actionStartedInComponent3D 1086 && document.activeElement === component3D.canvas3D.getHTMLElement() 1087 && ev.button === 0) { 1088 if (mouseListener.contextMenuEventType) { 1089 controller.releaseMouse(mouseListener.initialPointerLocation[0], mouseListener.initialPointerLocation[1]); 1090 } else { 1091 mouseListener.updateCoordinates(ev, "mouseReleased"); 1092 controller.releaseMouse(ev.canvasX, ev.canvasY); 1093 } 1094 } 1095 mouseListener.initialPointerLocation = null; 1096 mouseListener.lastPointerLocation = null; 1097 mouseListener.actionStartedInComponent3D = false; 1098 } 1099 } 1100 mouseListener.contextMenuEventType = false; 1101 }, 1102 pointerPressed : function(ev) { 1103 if (ev.pointerType == "mouse") { 1104 mouseListener.mousePressed(ev); 1105 } else { 1106 // Multi touch support for IE and Edge 1107 mouseListener.copyPointerToTargetTouches(ev, false); 1108 mouseListener.touchStarted(ev); 1109 } 1110 }, 1111 pointerMousePressed : function(ev) { 1112 // Required to avoid click simulation 1113 ev.stopPropagation(); 1114 }, 1115 windowPointerMoved : function(ev) { 1116 if (ev.pointerType == "mouse") { 1117 mouseListener.windowMouseMoved(ev); 1118 } else { 1119 // Multi touch support for IE and Edge 1120 mouseListener.copyPointerToTargetTouches(ev, false) 1121 mouseListener.touchMoved(ev); 1122 } 1123 }, 1124 windowPointerReleased : function(ev) { 1125 if (ev.pointerType == "mouse") { 1126 mouseListener.windowMouseReleased(ev); 1127 } else { 1128 ev.preventDefault(); 1129 // Multi touch support for IE and legacy Edge 1130 mouseListener.copyPointerToTargetTouches(ev, true); 1131 mouseListener.touchEnded(ev); 1132 } 1133 }, 1134 contextMenuDisplayed : function(ev) { 1135 mouseListener.contextMenuEventType = true; 1136 }, 1137 touchStarted: function(ev) { 1138 // Do not prevent default behavior to ensure focus events will be fired if focus changed after a touch event 1139 // but track touch event types to avoid them to be managed also for mousedown and dblclick events 1140 mouseListener.touchEventType = ev.pointerType === undefined; 1141 // Prevent default behavior to ensure a second touchstart event will be received 1142 // for double taps under iOS >= 15 1143 ev.preventDefault(); 1144 if (document.activeElement !== component3D.canvas3D.getHTMLElement()) { 1145 // Request focus explicitly since default behavior is disabled 1146 component3D.canvas3D.getHTMLElement().focus(); 1147 } 1148 mouseListener.updateCoordinates(ev, "touchStarted"); 1149 if (mouseListener.longTouch != null) { 1150 clearTimeout(mouseListener.longTouch); 1151 mouseListener.longTouch = null; 1152 component3D.stopLongTouchAnimation(); 1153 } 1154 1155 if (ev.targetTouches.length === 1) { 1156 var clickCount = 1; 1157 if (mouseListener.initialPointerLocation != null 1158 && mouseListener.distance(ev.canvasX, ev.canvasY, 1159 mouseListener.initialPointerLocation [0], mouseListener.initialPointerLocation [1]) < 5 1160 && ev.timeStamp - mouseListener.firstTouchStartedTimeStamp <= HomeComponent3D.DOUBLE_TOUCH_DELAY) { 1161 clickCount = 2; 1162 mouseListener.firstTouchStartedTimeStamp = 0; 1163 mouseListener.initialPointerLocation = null; 1164 } else { 1165 mouseListener.firstTouchStartedTimeStamp = ev.timeStamp; 1166 mouseListener.initialPointerLocation = [ev.canvasX, ev.canvasY]; 1167 } 1168 1169 mouseListener.longTouchActivated = false; 1170 mouseListener.doubleLongTouchActivated = false; 1171 mouseListener.distanceLastPinch = null; 1172 mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY]; 1173 mouseListener.actionStartedInComponent3D = true; 1174 mouseListener.longTouchWhenDragged = false; 1175 if (preferences.isEditingIn3DViewEnabled() 1176 && clickCount == 1 1177 && component3D.getClosestSelectableItemAt(ev.canvasX, ev.canvasY) !== null) { 1178 mouseListener.longTouch = setTimeout(function() { 1179 component3D.startLongTouchAnimation(ev.canvasX, ev.canvasY, 1180 function() { 1181 // Simulate shift key press 1182 mouseListener.longTouchActivated = true; 1183 controller.setAlignmentActivated(true); 1184 }); 1185 }, HomeComponent3D.LONG_TOUCH_DELAY); 1186 mouseListener.longTouchStartTime = Date.now(); 1187 } 1188 1189 controller.pressMouse(ev.canvasX, ev.canvasY, 1190 clickCount, mouseListener.isShiftDown(ev), mouseListener.isAlignmentActivated(ev), 1191 mouseListener.isDuplicationActivated(ev), mouseListener.isMagnetismToggled(ev), View.PointerType.TOUCH); 1192 } else { 1193 if (mouseListener.longTouchActivated 1194 && ev.targetTouches.length === 2) { 1195 // Simulate alt + shift key press 1196 mouseListener.doubleLongTouchActivated = true; 1197 controller.setDuplicationActivated(true); 1198 } else { 1199 // Additional touch allows to escape current modification 1200 controller.escape(); 1201 } 1202 1203 if (ev.targetTouches.length === 2) { 1204 mouseListener.actionStartedInComponent3D = true; 1205 mouseListener.initialPointerLocation = null; 1206 mouseListener.distanceLastPinch = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 1207 ev.targetTouches[1].clientX, ev.targetTouches[1].clientY); 1208 } 1209 } 1210 }, 1211 touchMoved: function(ev) { 1212 if (mouseListener.actionStartedInComponent3D) { 1213 ev.preventDefault(); 1214 ev.stopPropagation(); 1215 if (mouseListener.updateCoordinates(ev, "touchMoved")) { 1216 mouseListener.initialPointerLocation = null; 1217 1218 if (ev.targetTouches.length == 1) { 1219 if (mouseListener.longTouch != null) { 1220 // Cancel long touch animation only when pointer moved during the past 200 ms 1221 clearTimeout(mouseListener.longTouch); 1222 mouseListener.longTouch = null; 1223 component3D.stopLongTouchAnimation(); 1224 } 1225 1226 if (controller.isEditingState()) { 1227 if (!mouseListener.doubleLongTouchActivated) { 1228 controller.moveMouse(ev.canvasX, ev.canvasY); 1229 } 1230 } else { 1231 if (component3D.home.getCamera() === component3D.home.getObserverCamera()) { 1232 mouseListener.moveCamera(-ev.canvasX, -ev.canvasY, 1233 -mouseListener.lastPointerLocation [0], -mouseListener.lastPointerLocation [1], false, false); 1234 } else { 1235 mouseListener.moveCamera(ev.canvasX, ev.canvasY, 1236 mouseListener.lastPointerLocation [0], mouseListener.lastPointerLocation [1], false, false); 1237 } 1238 } 1239 mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY]; 1240 } else if (ev.targetTouches.length == 2 1241 && mouseListener.distanceLastPinch != null) { 1242 if (controller.isEditingState()) { 1243 controller.moveMouse(ev.targetTouches[1].clientX, ev.targetTouches[1].clientY); 1244 } else { 1245 var newDistance = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 1246 ev.targetTouches[1].clientX, ev.targetTouches[1].clientY); 1247 var scaleDifference = newDistance / mouseListener.distanceLastPinch; 1248 mouseListener.zoomCamera((1 - scaleDifference) * 50, false); 1249 mouseListener.distanceLastPinch = newDistance; 1250 } 1251 } 1252 } 1253 } 1254 }, 1255 touchEnded: function(ev) { 1256 if (mouseListener.actionStartedInComponent3D) { 1257 mouseListener.updateCoordinates(ev, "touchEnded"); 1258 if (ev.targetTouches.length == 0) { 1259 if (mouseListener.longTouch != null) { 1260 clearTimeout(mouseListener.longTouch); 1261 mouseListener.longTouch = null; 1262 component3D.stopLongTouchAnimation(); 1263 } 1264 1265 controller.releaseMouse(mouseListener.lastPointerLocation [0], mouseListener.lastPointerLocation [1]); 1266 1267 if (mouseListener.isLongTouch()) { 1268 // Avoid firing contextmenu event 1269 ev.preventDefault(); 1270 } 1271 mouseListener.actionStartedInComponent3D = false; 1272 } else if (ev.targetTouches.length == 1) { 1273 mouseListener.lastPointerLocation = [ev.canvasX, ev.canvasY]; 1274 } else if (ev.targetTouches.length == 2 1275 && mouseListener.distanceLastPinch != null) { 1276 // If the user keeps 2 finger on screen after releasing other fingers 1277 mouseListener.distanceLastPinch = mouseListener.distance(ev.targetTouches[0].clientX, ev.targetTouches[0].clientY, 1278 ev.targetTouches[1].clientX, ev.targetTouches[1].clientY) 1279 } 1280 } 1281 }, 1282 copyPointerToTargetTouches : function(ev, touchEnded) { 1283 // Copy the IE and Edge pointer location to ev.targetTouches or ev.changedTouches 1284 if (touchEnded) { 1285 ev.changedTouches = [mouseListener.pointerTouches [ev.pointerId]]; 1286 delete mouseListener.pointerTouches [ev.pointerId]; 1287 } else { 1288 mouseListener.pointerTouches [ev.pointerId] = {clientX : ev.clientX, clientY : ev.clientY}; 1289 } 1290 ev.targetTouches = []; 1291 for (var attribute in mouseListener.pointerTouches) { 1292 if (mouseListener.pointerTouches.hasOwnProperty(attribute)) { 1293 ev.targetTouches.push(mouseListener.pointerTouches [attribute]); 1294 } 1295 } 1296 }, 1297 updateCoordinates : function(ev, type) { 1298 // Updates canvasX and canvasY properties and return true if they changed 1299 var rect = component3D.canvas3D.getHTMLElement().getBoundingClientRect(); 1300 var updated = true; 1301 if (type.indexOf("touch") === 0) { 1302 var minDistance = mouseListener.lastEventType == "touchStarted" 1303 ? 5 : 1.5; 1304 var touches; 1305 if (ev.targetTouches.length === 1 1306 && type == "touchMoved" 1307 && mouseListener.distance(mouseListener.lastTargetTouches [0].clientX, mouseListener.lastTargetTouches [0].clientY, 1308 ev.targetTouches[0].clientX, ev.targetTouches[0].clientY) < minDistance 1309 || ev.targetTouches.length === 0 1310 && type == "touchEnded" 1311 && mouseListener.distance(mouseListener.lastTargetTouches [0].clientX, mouseListener.lastTargetTouches [0].clientY, 1312 ev.changedTouches[0].clientX, ev.changedTouches[0].clientY) < minDistance) { 1313 touches = mouseListener.lastTargetTouches; 1314 updated = false; 1315 } else { 1316 if (ev.targetTouches.length == 0) { 1317 // touchend case 1318 touches = ev.changedTouches; 1319 } else { 1320 touches = ev.targetTouches; 1321 } 1322 mouseListener.lastEventType = type; 1323 } 1324 1325 if (touches.length == 1) { 1326 ev.canvasX = touches[0].clientX - rect.left; 1327 ev.canvasY = touches[0].clientY - rect.top; 1328 var rect = component3D.canvas3D.getHTMLElement().getBoundingClientRect(); 1329 ev.clientX = touches[0].clientX; 1330 ev.clientY = touches[0].clientY; 1331 ev.button = 0; 1332 } 1333 ev.clickCount = 1; 1334 1335 if (updated) { 1336 // Make a copy of touches because old iOS reuse the same ev.targetTouches array between events 1337 mouseListener.lastTargetTouches = []; 1338 for (var i = 0; touches[i] !== undefined; i++) { 1339 mouseListener.lastTargetTouches.push({clientX: touches[i].clientX, clientY: touches[i].clientY}); 1340 } 1341 } 1342 } else { 1343 ev.canvasX = ev.clientX - rect.left; 1344 ev.canvasY = ev.clientY - rect.top; 1345 } 1346 1347 if (ev.clickCount === undefined) { 1348 if (type == "mouseDoubleClicked") { 1349 ev.clickCount = 2; 1350 } else if (type == "mousePressed" || type == "mouseReleased") { 1351 ev.clickCount = 1; 1352 } else { 1353 ev.clickCount = 0; 1354 } 1355 } 1356 if (type == "mouseWheelMoved") { 1357 ev.wheelRotation = (ev.deltaY !== undefined 1358 ? ev.deltaX + ev.deltaY 1359 : -ev.wheelDelta) / 4; 1360 } 1361 1362 return updated; 1363 }, 1364 isLongTouch: function(dragging) { 1365 return Date.now() - mouseListener.longTouchStartTime 1366 > ((dragging 1367 ? HomeComponent3D.LONG_TOUCH_DELAY_WHEN_DRAGGING 1368 : HomeComponent3D.LONG_TOUCH_DELAY) + HomeComponent3D.LONG_TOUCH_DURATION_AFTER_DELAY); 1369 }, 1370 distance: function(x1, y1, x2, y2) { 1371 return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 1372 }, 1373 isInCanvas: function(ev) { 1374 return ev.canvasX >= 0 && ev.canvasX < component3D.canvas3D.getHTMLElement().clientWidth 1375 && ev.canvasY >= 0 && ev.canvasY < component3D.canvas3D.getHTMLElement().clientHeight; 1376 }, 1377 mouseScrolled : function(ev) { 1378 mouseListener.zoomCamera(ev.detail, ev.shiftKey); 1379 }, 1380 mouseWheelMoved : function(ev) { 1381 ev.preventDefault(); 1382 if (!controller.isEditingState()) { 1383 mouseListener.zoomCamera((ev.deltaY !== undefined ? ev.deltaY : -ev.wheelDelta) / 4, ev.shiftKey); 1384 } 1385 }, 1386 zoomCamera : function(delta, shiftKey) { 1387 // Mouse wheel changes camera location 1388 var delta = -2.5 * delta; 1389 // Multiply delta by 10 if shift is down 1390 if (shiftKey) { 1391 delta *= 5; 1392 } 1393 controller.moveCamera(delta); 1394 }, 1395 moveCamera: function(x, y, lastX, lastY, altKey, shiftKey) { 1396 if (mouseListener.actionStartedInComponent3D 1397 && (mouseListener.lastPointerLocation [0] !== x 1398 || mouseListener.lastPointerLocation [1] !== y)) { 1399 if (altKey) { 1400 // Mouse move along Y axis while alt is down changes camera location 1401 var delta = 1.25 * (lastY - y); 1402 // Multiply delta by 5 if shift is down 1403 if (shiftKey) { 1404 delta *= 5; 1405 } 1406 controller.moveCamera(delta); 1407 } else { 1408 var ANGLE_FACTOR = 0.005; 1409 // Mouse move along X axis changes camera yaw 1410 var yawDelta = ANGLE_FACTOR * (x - lastX); 1411 // Multiply yaw delta by 5 if shift is down 1412 if (shiftKey) { 1413 yawDelta *= 5; 1414 } 1415 controller.rotateCameraYaw(yawDelta); 1416 1417 // Mouse move along Y axis changes camera pitch 1418 var pitchDelta = ANGLE_FACTOR * (y - lastY); 1419 controller.rotateCameraPitch(pitchDelta); 1420 } 1421 } 1422 } 1423 }; 1424 1425 if (OperatingSystem.isInternetExplorerOrLegacyEdge() 1426 && window.PointerEvent) { 1427 // Multi touch support for IE and Edge 1428 // IE and Edge test from https://stackoverflow.com/questions/31757852/how-can-i-detect-internet-explorer-ie-and-microsoft-edge-using-javascript 1429 canvas3D.getHTMLElement().addEventListener("pointerdown", mouseListener.pointerPressed); 1430 canvas3D.getHTMLElement().addEventListener("mousedown", mouseListener.pointerMousePressed); 1431 canvas3D.getHTMLElement().addEventListener("dblclick", mouseListener.mouseDoubleClicked); 1432 // Add pointermove and pointerup event listeners to window to capture pointer events out of the canvas 1433 window.addEventListener("pointermove", mouseListener.windowPointerMoved); 1434 window.addEventListener("pointerup", mouseListener.windowPointerReleased); 1435 } else { 1436 canvas3D.getHTMLElement().addEventListener("touchstart", mouseListener.touchStarted); 1437 canvas3D.getHTMLElement().addEventListener("touchmove", mouseListener.touchMoved); 1438 canvas3D.getHTMLElement().addEventListener("touchend", mouseListener.touchEnded); 1439 canvas3D.getHTMLElement().addEventListener("mousedown", mouseListener.mousePressed); 1440 canvas3D.getHTMLElement().addEventListener("dblclick", mouseListener.mouseDoubleClicked); 1441 // Add mousemove and mouseup event listeners to window to capture mouse events out of the canvas 1442 window.addEventListener("mousemove", mouseListener.windowMouseMoved); 1443 window.addEventListener("mouseup", mouseListener.windowMouseReleased); 1444 } 1445 canvas3D.getHTMLElement().addEventListener("contextmenu", mouseListener.contextMenuDisplayed); 1446 canvas3D.getHTMLElement().addEventListener("DOMMouseScroll", mouseListener.mouseScrolled); 1447 canvas3D.getHTMLElement().addEventListener("mousewheel", mouseListener.mouseWheelMoved); 1448 1449 this.mouseListener = mouseListener; 1450 } 1451 1452 /** 1453 * @private 1454 */ 1455 HomeComponent3D.prototype.startLongTouchAnimation = function(x, y, animationPostTask) { 1456 if (this.touchOverlay === undefined) { 1457 this.touchOverlay = document.createElement("div"); 1458 this.touchOverlay.id = "touch-overlay-timer"; 1459 this.touchOverlay.classList.add("touch-overlay-timer"); 1460 this.touchOverlay.style.position = "absolute"; 1461 this.touchOverlay.style.top = "0px"; 1462 this.touchOverlay.style.left = "0px"; 1463 this.touchOverlay.innerHTML = '<div class="touch-overlay-timer-content"></div><div class="touch-overlay-timer-bg"></div><div class="touch-overlay-timer-hidder"></div><div class="touch-overlay-timer-loader1"></div><div class="touch-overlay-timer-loader2"></div>'; 1464 document.body.appendChild(this.touchOverlay); 1465 for (var i = 0; i < this.touchOverlay.children.length; i++) { 1466 var item = this.touchOverlay.children.item(i); 1467 if (item.classList.contains("overlay-timer-loader1") 1468 || item.classList.contains("overlay-timer-loader2")) { 1469 item.style.borderTopColor = "#0000B5"; 1470 item.style.borderRightColor = "#0000B5"; 1471 } 1472 if (item.classList.contains("touch-overlay-timer-content")) { 1473 item.style.color = "#000000"; 1474 item.innerHTML = "<span style='font-weight: bold; font-family: sans-serif; font-size: 140%; line-height: 90%'>⇪</span>"; 1475 } 1476 item.style.animationDuration = (PlanComponent.LONG_TOUCH_DURATION_AFTER_DELAY) + "ms"; 1477 } 1478 } 1479 this.touchOverlay.style.visibility = "visible"; 1480 this.touchOverlay.style.left = (this.getHTMLElement().getBoundingClientRect().left + x - this.touchOverlay.clientWidth / 2) + "px"; 1481 this.touchOverlay.style.top = (this.getHTMLElement().getBoundingClientRect().top + y - this.touchOverlay.clientHeight - 40) + "px"; 1482 for (var i = 0; i < this.touchOverlay.children.length; i++) { 1483 this.touchOverlay.children.item(i).classList.add("animated"); 1484 } 1485 if (animationPostTask !== undefined) { 1486 this.longTouchAnimationPostTask = setTimeout(animationPostTask, HomeComponent3D.LONG_TOUCH_DURATION_AFTER_DELAY); 1487 } 1488 } 1489 1490 /** 1491 * @private 1492 */ 1493 HomeComponent3D.prototype.stopLongTouchAnimation = function(x, y) { 1494 if (this.touchOverlay !== undefined) { 1495 this.touchOverlay.style.visibility = "hidden"; 1496 for (var i = 0; i < this.touchOverlay.children.length; i++) { 1497 this.touchOverlay.children.item(i).classList.remove("animated"); 1498 } 1499 if (this.longTouchAnimationPostTask !== undefined) { 1500 clearTimeout(this.longTouchAnimationPostTask); 1501 delete this.longTouchAnimationPostTask; 1502 } 1503 } 1504 } 1505 1506 /** 1507 * Installs keys bound to actions. 1508 * @private 1509 */ 1510 HomeComponent3D.prototype.installKeyboardActions = function() { 1511 // Tolerate alt modifier for forward and backward moves with UP and DOWN keys to avoid 1512 // the user to release the alt key when he wants to alternate forward/backward and sideways moves 1513 this.inputMap = { 1514 "shift pressed UP" : "MOVE_CAMERA_FAST_FORWARD", 1515 "alt shift pressed UP" : "MOVE_CAMERA_FAST_FORWARD", 1516 "shift pressed W" : "MOVE_CAMERA_FAST_FORWARD", 1517 "pressed UP" : "MOVE_CAMERA_FORWARD", 1518 "alt pressed UP" : "MOVE_CAMERA_FORWARD", 1519 "pressed W" : "MOVE_CAMERA_FORWARD", 1520 "shift pressed DOWN" : "MOVE_CAMERA_FAST_BACKWARD", 1521 "alt shift pressed DOWN" : "MOVE_CAMERA_FAST_BACKWARD", 1522 "shift pressed S" : "MOVE_CAMERA_FAST_BACKWARD", 1523 "pressed DOWN" : "MOVE_CAMERA_BACKWARD", 1524 "alt pressed DOWN" : "MOVE_CAMERA_BACKWARD", 1525 "pressed S" : "MOVE_CAMERA_BACKWARD", 1526 "alt shift pressed LEFT" : "MOVE_CAMERA_FAST_LEFT", 1527 "alt pressed LEFT" : "MOVE_CAMERA_LEFT", 1528 "alt shift pressed RIGHT" : "MOVE_CAMERA_FAST_RIGHT", 1529 "alt pressed RIGHT" : "MOVE_CAMERA_RIGHT", 1530 "shift pressed LEFT" : "ROTATE_CAMERA_YAW_FAST_LEFT", 1531 "shift pressed A" : "ROTATE_CAMERA_YAW_FAST_LEFT", 1532 "pressed LEFT" : "ROTATE_CAMERA_YAW_LEFT", 1533 "pressed A" : "ROTATE_CAMERA_YAW_LEFT", 1534 "shift pressed RIGHT" : "ROTATE_CAMERA_YAW_FAST_RIGHT", 1535 "shift pressed D" : "ROTATE_CAMERA_YAW_FAST_RIGHT", 1536 "pressed RIGHT" : "ROTATE_CAMERA_YAW_RIGHT", 1537 "pressed D" : "ROTATE_CAMERA_YAW_RIGHT", 1538 "shift pressed PAGE_UP" : "ROTATE_CAMERA_PITCH_FAST_UP", 1539 "pressed PAGE_UP" : "ROTATE_CAMERA_PITCH_UP", 1540 "shift pressed PAGE_DOWN" : "ROTATE_CAMERA_PITCH_FAST_DOWN", 1541 "pressed PAGE_DOWN" : "ROTATE_CAMERA_PITCH_DOWN", 1542 "shift pressed HOME" : "ELEVATE_CAMERA_FAST_UP", 1543 "pressed HOME" : "ELEVATE_CAMERA_UP", 1544 "shift pressed END" : "ELEVATE_CAMERA_FAST_DOWN", 1545 "pressed END" : "ELEVATE_CAMERA_DOWN", 1546 "pressed ESCAPE" : "ESCAPE", 1547 "shift pressed ESCAPE" : "ESCAPE" 1548 }; 1549 1550 if (OperatingSystem.isMacOSX()) { 1551 // Under Mac OS X, duplication with Alt key 1552 this.inputMap["alt pressed ALT"] = "ACTIVATE_DUPLICATION"; 1553 this.inputMap["released ALT"] = "DEACTIVATE_DUPLICATION"; 1554 this.inputMap["shift alt pressed ALT"] = "ACTIVATE_DUPLICATION"; 1555 this.inputMap["shift released ALT"] = "DEACTIVATE_DUPLICATION"; 1556 this.inputMap["meta alt pressed ALT"] = "ACTIVATE_DUPLICATION"; 1557 this.inputMap["meta released ALT"] = "DEACTIVATE_DUPLICATION"; 1558 this.inputMap["shift meta alt pressed ALT"] = "ACTIVATE_DUPLICATION"; 1559 this.inputMap["shift meta released ALT"] = "DEACTIVATE_DUPLICATION"; 1560 this.inputMap["alt pressed ESCAPE"] = "ESCAPE"; 1561 } else { 1562 // Under other systems, duplication with Ctrl key 1563 this.inputMap["control pressed CONTROL"] = "ACTIVATE_DUPLICATION"; 1564 this.inputMap["released CONTROL"] = "DEACTIVATE_DUPLICATION"; 1565 this.inputMap["shift control pressed CONTROL"] = "ACTIVATE_DUPLICATION"; 1566 this.inputMap["shift released CONTROL"] = "DEACTIVATE_DUPLICATION"; 1567 this.inputMap["meta control pressed CONTROL"] = "ACTIVATE_DUPLICATION"; 1568 this.inputMap["meta released CONTROL"] = "DEACTIVATE_DUPLICATION"; 1569 this.inputMap["shift meta control pressed CONTROL"] = "ACTIVATE_DUPLICATION"; 1570 this.inputMap["shift meta released CONTROL"] = "DEACTIVATE_DUPLICATION"; 1571 this.inputMap["control pressed ESCAPE"] = "ESCAPE"; 1572 } 1573 if (OperatingSystem.isWindows()) { 1574 // Under Windows, magnetism toggled with Alt key 1575 this.inputMap["alt pressed ALT"] = "TOGGLE_MAGNETISM_ON"; 1576 this.inputMap["released ALT"] = "TOGGLE_MAGNETISM_OFF"; 1577 this.inputMap["shift alt pressed ALT"] = "TOGGLE_MAGNETISM_ON"; 1578 this.inputMap["shift released ALT"] = "TOGGLE_MAGNETISM_OFF"; 1579 this.inputMap["control alt pressed ALT"] = "TOGGLE_MAGNETISM_ON"; 1580 this.inputMap["control released ALT"] = "TOGGLE_MAGNETISM_OFF"; 1581 this.inputMap["shift control alt pressed ALT"] = "TOGGLE_MAGNETISM_ON"; 1582 this.inputMap["shift control released ALT"] = "TOGGLE_MAGNETISM_OFF"; 1583 this.inputMap["alt pressed ESCAPE"] = "ESCAPE"; 1584 } else if (OperatingSystem.isMacOSX()) { 1585 // Under Mac OS X, magnetism toggled with cmd key 1586 this.inputMap["meta pressed META"] = "TOGGLE_MAGNETISM_ON"; 1587 this.inputMap["released META"] = "TOGGLE_MAGNETISM_OFF"; 1588 this.inputMap["shift meta pressed META"] = "TOGGLE_MAGNETISM_ON"; 1589 this.inputMap["shift released META"] = "TOGGLE_MAGNETISM_OFF"; 1590 this.inputMap["alt meta pressed META"] = "TOGGLE_MAGNETISM_ON"; 1591 this.inputMap["alt released META"] = "TOGGLE_MAGNETISM_OFF"; 1592 this.inputMap["shift alt meta pressed META"] = "TOGGLE_MAGNETISM_ON"; 1593 this.inputMap["shift alt released META"] = "TOGGLE_MAGNETISM_OFF"; 1594 this.inputMap["meta pressed ESCAPE"] = "ESCAPE"; 1595 } else { 1596 // Under other Unix systems, magnetism toggled with Alt + Shift key 1597 this.inputMap["shift alt pressed ALT"] = "TOGGLE_MAGNETISM_ON"; 1598 this.inputMap["alt shift pressed SHIFT"] = "TOGGLE_MAGNETISM_ON"; 1599 this.inputMap["alt released SHIFT"] = "TOGGLE_MAGNETISM_OFF"; 1600 this.inputMap["shift released ALT"] = "TOGGLE_MAGNETISM_OFF"; 1601 this.inputMap["control shift alt pressed ALT"] = "TOGGLE_MAGNETISM_ON"; 1602 this.inputMap["control alt shift pressed SHIFT"] = "TOGGLE_MAGNETISM_ON"; 1603 this.inputMap["control alt released SHIFT"] = "TOGGLE_MAGNETISM_OFF"; 1604 this.inputMap["control shift released ALT"] = "TOGGLE_MAGNETISM_OFF"; 1605 this.inputMap["alt shift ESCAPE"] = "ESCAPE"; 1606 this.inputMap["control alt shift pressed ESCAPE"] = "ESCAPE"; 1607 } 1608 1609 this.inputMap["shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT"; 1610 this.inputMap["released SHIFT"] = "DEACTIVATE_ALIGNMENT"; 1611 if (OperatingSystem.isWindows()) { 1612 this.inputMap["control shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT"; 1613 this.inputMap["control released SHIFT"] = "DEACTIVATE_ALIGNMENT"; 1614 this.inputMap["alt shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT"; 1615 this.inputMap["alt released SHIFT"] = "DEACTIVATE_ALIGNMENT"; 1616 } else if (OperatingSystem.isMacOSX()) { 1617 this.inputMap["alt shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT"; 1618 this.inputMap["alt released SHIFT"] = "DEACTIVATE_ALIGNMENT"; 1619 this.inputMap["meta shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT"; 1620 this.inputMap["meta released SHIFT"] = "DEACTIVATE_ALIGNMENT"; 1621 } else { 1622 this.inputMap["control shift pressed SHIFT"] = "ACTIVATE_ALIGNMENT"; 1623 this.inputMap["control released SHIFT"] = "DEACTIVATE_ALIGNMENT"; 1624 this.inputMap["shift released ALT"] = "ACTIVATE_ALIGNMENT"; 1625 this.inputMap["control shift released ALT"] = "ACTIVATE_ALIGNMENT"; 1626 } 1627 1628 var component3D = this; 1629 this.canvas3D.getHTMLElement().addEventListener("keydown", 1630 function(ev) { 1631 component3D.callAction(ev, KeyStroke.getKeyStrokeForEvent(ev, "keydown")); 1632 }, false); 1633 this.canvas3D.getHTMLElement().addEventListener("keyup", 1634 function(ev) { 1635 component3D.callAction(ev, KeyStroke.getKeyStrokeForEvent(ev, "keyup")); 1636 }, false); 1637 } 1638 1639 /** 1640 * Runs the action bound to the key stroke in parameter. 1641 * @param {UIEvent} ev 1642 * @param {string} keyStroke 1643 * @private 1644 */ 1645 HomeComponent3D.prototype.callAction = function(ev, keyStroke) { 1646 if (keyStroke !== undefined) { 1647 var actionKey = this.inputMap [keyStroke]; 1648 if (actionKey !== undefined) { 1649 var action = this.actionMap [actionKey]; 1650 if (action !== undefined) { 1651 action.actionPerformed(ev); 1652 } 1653 ev.stopPropagation(); 1654 } 1655 } 1656 } 1657 1658 /** 1659 * Creates actions that calls back <code>controller</code> methods. 1660 * @private 1661 */ 1662 HomeComponent3D.prototype.createActions = function(controller) { 1663 // Move camera action mapped to arrow keys. 1664 function MoveCameraAction(delta) { 1665 this.delta = delta; 1666 } 1667 1668 MoveCameraAction.prototype.actionPerformed = function(ev) { 1669 controller.moveCamera(this.delta); 1670 } 1671 1672 // Move camera sideways action mapped to arrow keys. 1673 function MoveCameraSidewaysAction (delta) { 1674 this.delta = delta; 1675 } 1676 1677 MoveCameraSidewaysAction.prototype.actionPerformed = function(ev) { 1678 controller.moveCameraSideways(this.delta); 1679 } 1680 1681 // Elevate camera action mapped to arrow keys. 1682 function ElevateCameraAction(delta) { 1683 this.delta = delta; 1684 } 1685 1686 ElevateCameraAction.prototype.actionPerformed = function(ev) { 1687 controller.elevateCamera(this.delta); 1688 } 1689 1690 // Rotate camera yaw action mapped to arrow keys. 1691 function RotateCameraYawAction (delta) { 1692 this.delta = delta; 1693 } 1694 1695 RotateCameraYawAction.prototype.actionPerformed = function(ev) { 1696 controller.rotateCameraYaw(this.delta); 1697 } 1698 1699 // Rotate camera pitch action mapped to arrow keys. 1700 function RotateCameraPitchAction(delta) { 1701 this.delta = delta; 1702 } 1703 1704 RotateCameraPitchAction.prototype.actionPerformed = function(ev) { 1705 controller.rotateCameraPitch(this.delta); 1706 } 1707 1708 this.actionMap = { 1709 "MOVE_CAMERA_FORWARD" : new MoveCameraAction(6.5), 1710 "MOVE_CAMERA_FAST_FORWARD" : new MoveCameraAction(32.5), 1711 "MOVE_CAMERA_BACKWARD" : new MoveCameraAction(-6.5), 1712 "MOVE_CAMERA_FAST_BACKWARD" : new MoveCameraAction(-32.5), 1713 "MOVE_CAMERA_LEFT" : new MoveCameraSidewaysAction(-2.5), 1714 "MOVE_CAMERA_FAST_LEFT" : new MoveCameraSidewaysAction(-10), 1715 "MOVE_CAMERA_RIGHT" : new MoveCameraSidewaysAction(2.5), 1716 "MOVE_CAMERA_FAST_RIGHT" : new MoveCameraSidewaysAction(10), 1717 "ELEVATE_CAMERA_DOWN" : new ElevateCameraAction(-2.5), 1718 "ELEVATE_CAMERA_FAST_DOWN" : new ElevateCameraAction(-10), 1719 "ELEVATE_CAMERA_UP" : new ElevateCameraAction(2.5), 1720 "ELEVATE_CAMERA_FAST_UP" : new ElevateCameraAction(10), 1721 "ROTATE_CAMERA_YAW_LEFT" : new RotateCameraYawAction(-Math.PI / 60), 1722 "ROTATE_CAMERA_YAW_FAST_LEFT" : new RotateCameraYawAction(-Math.PI / 12), 1723 "ROTATE_CAMERA_YAW_RIGHT" : new RotateCameraYawAction(Math.PI / 60), 1724 "ROTATE_CAMERA_YAW_FAST_RIGHT" : new RotateCameraYawAction(Math.PI / 12), 1725 "ROTATE_CAMERA_PITCH_UP" : new RotateCameraPitchAction(-Math.PI / 120), 1726 "ROTATE_CAMERA_PITCH_FAST_UP" : new RotateCameraPitchAction(-Math.PI / 24), 1727 "ROTATE_CAMERA_PITCH_DOWN" : new RotateCameraPitchAction(Math.PI / 120), 1728 "ROTATE_CAMERA_PITCH_FAST_DOWN" : new RotateCameraPitchAction(Math.PI / 24), 1729 "ESCAPE": { 1730 actionPerformed: function(ev) { 1731 controller.escape(); 1732 } 1733 }, 1734 "ACTIVATE_ALIGNMENT": { 1735 actionPerformed: function(ev) { 1736 controller.setAlignmentActivated(true); 1737 } 1738 }, 1739 "DEACTIVATE_ALIGNMENT": { 1740 actionPerformed: function(ev) { 1741 controller.setAlignmentActivated(false); 1742 } 1743 }, 1744 "TOGGLE_MAGNETISM_ON": { 1745 actionPerformed: function(ev) { 1746 controller.toggleMagnetism(true); 1747 } 1748 }, 1749 "TOGGLE_MAGNETISM_OFF": { 1750 actionPerformed: function(ev) { 1751 controller.toggleMagnetism(false); 1752 } 1753 }, 1754 "ACTIVATE_DUPLICATION": { 1755 actionPerformed: function(ev) { 1756 controller.setDuplicationActivated(true); 1757 } 1758 }, 1759 "DEACTIVATE_DUPLICATION": { 1760 actionPerformed: function(ev) { 1761 controller.setDuplicationActivated(false); 1762 } 1763 } 1764 }; 1765 } 1766 1767 /** 1768 * Returns the action map of this component. 1769 */ 1770 HomeComponent3D.prototype.getActionMap = function() { 1771 return this.actionMap; 1772 } 1773 1774 /** 1775 * Returns the input map of this component. 1776 */ 1777 HomeComponent3D.prototype.getInputMap = function() { 1778 return this.inputMap; 1779 } 1780 1781 /** 1782 * Returns the closest home item displayed at client coordinates (x, y). 1783 * @param {number} x 1784 * @param {number} y 1785 * @return {Object} 1786 * @deprecated 1787 */ 1788 HomeComponent3D.prototype.getClosestItemAt = function(x, y) { 1789 var node = this.canvas3D.getClosestShapeAt(x, y); 1790 var homeObjectIndex = -1; 1791 while (node !== null 1792 && (homeObjectIndex = this.homeObjects3D.indexOf(node)) < 0) { 1793 node = node.getParent(); 1794 } 1795 if (node != null) { 1796 return this.homeObjects [homeObjectIndex]; 1797 } else { 1798 return null; 1799 } 1800 } 1801 1802 /** 1803 * Returns the closest home item displayed at component coordinates (x, y), 1804 * or <code>null</code> if not found. 1805 * @param {number} x 1806 * @param {number} y 1807 * @return {Selectable} 1808 */ 1809 HomeComponent3D.prototype.getClosestSelectableItemAt = function(x, y) { 1810 var rect = component3D.canvas3D.getHTMLElement().getBoundingClientRect(); 1811 return this.getClosestItemAt(x + rect.left, y + rect.top); 1812 } 1813 1814 /** 1815 * Returns the 3D point matching the point (x, y) in component coordinates space. 1816 * @return {vec3} 1817 * @private 1818 */ 1819 HomeComponent3D.prototype.convertPixelLocationToVirtualWorldPoint = function(x, y) { 1820 // See http://webglfactory.blogspot.com/2011/05/how-to-convert-world-to-screen.html 1821 var transform = this.canvas3D.getVirtualWorldToImageTransform(mat4.create()); 1822 mat4.invert(transform, transform); 1823 mat4.mul(transform, this.canvas3D.getViewPlatformTransform(mat4.create()), transform); 1824 1825 var rect = this.getHTMLElement().getBoundingClientRect(); 1826 var point = vec3.fromValues((x / rect.width - 0.5) * 2, (0.5 - y / rect.height) * 2, 0); 1827 vec3.transformMat4(point, point, transform); 1828 return point; 1829 } 1830 1831 /** 1832 * Returns the 3D point matching the point (x, y) in component coordinates space. 1833 * @return {Array} 1834 */ 1835 HomeComponent3D.prototype.convertPixelLocationToVirtualWorld = function(x, y) { 1836 var point = this.convertPixelLocationToVirtualWorldPoint(x, y); 1837 return [point [0], point [2], point [1]]; 1838 } 1839 1840 /** 1841 * Returns the coordinates intersecting the floor of the selected level in the direction 1842 * joining camera location and component coordinates (x, y). 1843 */ 1844 HomeComponent3D.prototype.getVirtualWorldPointAt = function(x, y, elevation) { 1845 var point = this.convertPixelLocationToVirtualWorldPoint(x, y); 1846 var camera = this.home.getCamera(); 1847 var eye = vec3.fromValues(camera.getX(), camera.getZ(), camera.getY()); 1848 var eyePointDirection = vec3.sub(vec3.fromValues(0, 0, 0), point, eye); 1849 // If direction points to the sky, negate it to point to the ground 1850 if (eyePointDirection [1] > 0) { 1851 eyePointDirection [1] = -eyePointDirection [1]; 1852 } 1853 1854 // Compute coordinates of the intersection point between the line joining 1855 // eye and the given point with the plan y = elevation 1856 // Parametric equation of the line 1857 // x = point.x + t . direction.x 1858 // y = point.y + t . direction.y 1859 // z = point.z + t . direction.z 1860 var t = (elevation - point [1]) / eyePointDirection [1]; 1861 var xFloor = (point [0] + t * eyePointDirection [0]); 1862 var zFloor = (point [2] + t * eyePointDirection [2]); 1863 return [xFloor, zFloor, elevation]; 1864 } 1865 1866 /** 1867 * Returns a new scene tree root. 1868 * @private 1869 */ 1870 HomeComponent3D.prototype.createSceneTree = function(listenToHomeUpdates, waitForLoading) { 1871 var root = new Group3D(); 1872 // Build scene tree with background node first to ensure home structure will be loaded first if it exists 1873 root.addChild(this.createBackgroundNode(listenToHomeUpdates, waitForLoading)); 1874 // Limit ground area to 1 km x 1 km to avoid bad effects with a larger area 1875 var groundNode = this.createGroundNode(-0.5E5, -0.5E5, 1E5, 1E5, listenToHomeUpdates, waitForLoading); 1876 root.addChild(groundNode); 1877 root.addChild(this.createHomeTree(listenToHomeUpdates, waitForLoading)); 1878 1879 this.sceneLights = this.createLights(listenToHomeUpdates); 1880 for (var i = 0; i < this.sceneLights.length; i++) { 1881 root.addChild(this.sceneLights [i]); 1882 } 1883 1884 return root; 1885 } 1886 1887 /** 1888 * Returns a new background node. 1889 * @private 1890 */ 1891 HomeComponent3D.prototype.createBackgroundNode = function(listenToHomeUpdates, waitForLoading) { 1892 var skyBackgroundAppearance = new Appearance3D(); 1893 var topHalfSphereGeometry = this.createHalfSphereGeometry(true); 1894 var topHalfSphere = new Shape3D(topHalfSphereGeometry, skyBackgroundAppearance); 1895 var backgroundGroup = new BranchGroup3D(); 1896 backgroundGroup.addChild(topHalfSphere); 1897 backgroundGroup.addChild(new Shape3D(this.createHalfSphereGeometry(false))); 1898 1899 // Add a plane at ground level to complete landscape at the horizon when camera is above horizon 1900 var groundBackgroundAppearance = new Appearance3D(); 1901 var groundBackground = new Shape3D( 1902 new IndexedTriangleArray3D([vec3.fromValues(-1, -0.01, -1), 1903 vec3.fromValues(-1, -0.01, 1), 1904 vec3.fromValues(1, -0.01, 1), 1905 vec3.fromValues(1, -0.01, -1)], 1906 [0, 1, 2, 0, 2, 3], 1907 [], [], 1908 [vec3.fromValues(0., 1., 0.)], [0, 0, 0, 0, 0, 0]), 1909 groundBackgroundAppearance); 1910 backgroundGroup.addChild(groundBackground); 1911 1912 // No need of different lights for background because scene lights will have an effect on background too 1913 1914 var background = new Background3D(backgroundGroup); 1915 this.updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, this.home, waitForLoading); 1916 groundBackgroundAppearance.setVisible(this.home.getCamera().getZ() >= 0); 1917 1918 if (listenToHomeUpdates) { 1919 // Add a listener on home properties change 1920 var component3D = this; 1921 this.backgroundChangeListener = function(ev) { 1922 component3D.updateBackgroundColorAndTexture(skyBackgroundAppearance, groundBackgroundAppearance, 1923 component3D.home, waitForLoading); 1924 }; 1925 component3D.home.getEnvironment().addPropertyChangeListener("SKY_COLOR", this.backgroundChangeListener); 1926 component3D.home.getEnvironment().addPropertyChangeListener("SKY_TEXTURE", this.backgroundChangeListener); 1927 component3D.home.getEnvironment().addPropertyChangeListener("GROUND_COLOR", this.backgroundChangeListener); 1928 component3D.home.getEnvironment().addPropertyChangeListener("GROUND_TEXTURE", this.backgroundChangeListener); 1929 // Make groundBackground invisible if camera is below the ground 1930 this.elevationChangeListener = function(ev) { 1931 if (ev.getSource() === component3D.home) { 1932 // Move listener to the new camera 1933 ev.getOldValue().removePropertyChangeListener(component3D.elevationChangeListener); 1934 component3D.home.getCamera().addPropertyChangeListener(component3D.elevationChangeListener); 1935 } 1936 if (ev.getSource() === component3D.home 1937 || ev.getPropertyName() === "Z") { 1938 groundBackgroundAppearance.setVisible(component3D.home.getCamera().getZ() >= 0); 1939 } 1940 }; 1941 this.home.getCamera().addPropertyChangeListener(this.elevationChangeListener); 1942 this.home.addPropertyChangeListener("CAMERA", this.elevationChangeListener); 1943 } 1944 return background; 1945 } 1946 1947 /** 1948 * Returns a half sphere oriented inward and with texture ordinates 1949 * that spread along an hemisphere. 1950 * @param {boolean} top if true returns an upper geometry 1951 * @private 1952 */ 1953 HomeComponent3D.prototype.createHalfSphereGeometry = function(top) { 1954 var divisionCount = 48; 1955 var coords = []; 1956 var coordIndices = []; 1957 var textureCoords = []; 1958 for (var i = 0, k = 0; i < divisionCount; i++) { 1959 var alpha = i * 2 * Math.PI / divisionCount; 1960 var cosAlpha = Math.cos(alpha); 1961 var sinAlpha = Math.sin(alpha); 1962 var nextAlpha = (i + 1) * 2 * Math.PI / divisionCount; 1963 var cosNextAlpha = Math.cos(nextAlpha); 1964 var sinNextAlpha = Math.sin(nextAlpha); 1965 for (var j = 0, max = divisionCount / 4; j < max; j++, k += 4) { 1966 var beta = 2 * j * Math.PI / divisionCount; 1967 var cosBeta = Math.cos(beta); 1968 var sinBeta = Math.sin(beta); 1969 // Correct the bottom of the hemisphere to avoid seeing a bottom hemisphere at the horizon 1970 var y = j !== 0 ? (top ? sinBeta : -sinBeta) : -0.01; 1971 var nextBeta = 2 * (j + 1) * Math.PI / divisionCount; 1972 if (!top) { 1973 nextBeta = -nextBeta; 1974 } 1975 var cosNextBeta = Math.cos(nextBeta); 1976 var sinNextBeta = Math.sin(nextBeta); 1977 coords.push(vec3.fromValues(cosAlpha * cosBeta, y, sinAlpha * cosBeta)); 1978 coords.push(vec3.fromValues(cosNextAlpha * cosBeta, y, sinNextAlpha * cosBeta)); 1979 coords.push(vec3.fromValues(cosNextAlpha * cosNextBeta, sinNextBeta, sinNextAlpha * cosNextBeta)); 1980 coords.push(vec3.fromValues(cosAlpha * cosNextBeta, sinNextBeta, sinAlpha * cosNextBeta)); 1981 if (top) { 1982 coordIndices.push(k); 1983 coordIndices.push(k + 1); 1984 coordIndices.push(k + 2); 1985 coordIndices.push(k); 1986 coordIndices.push(k + 2); 1987 coordIndices.push(k + 3); 1988 textureCoords.push(vec2.fromValues(i / divisionCount, j / max)); 1989 textureCoords.push(vec2.fromValues((i + 1) / divisionCount, j / max)); 1990 textureCoords.push(vec2.fromValues((i + 1) / divisionCount, (j + 1) / max)); 1991 textureCoords.push(vec2.fromValues(i / divisionCount, (j + 1) / max)); 1992 } else { 1993 coordIndices.push(k); 1994 coordIndices.push(k + 2); 1995 coordIndices.push(k + 1); 1996 coordIndices.push(k); 1997 coordIndices.push(k + 3); 1998 coordIndices.push(k + 2); 1999 } 2000 } 2001 } 2002 2003 return new IndexedTriangleArray3D(coords, coordIndices, textureCoords, coordIndices, [], []); 2004 } 2005 2006 /** 2007 * Updates <code>skyBackgroundAppearance</code> and <code>groundBackgroundAppearance</code> 2008 * color / texture from <code>home</code> sky color and texture. 2009 * @param {Appearance3D} skyBackgroundAppearance the sky appearance to update 2010 * @param {Appearance3D} groundBackgroundAppearance the shape of the ground used in the background 2011 * @param {Home} home 2012 * @param {boolean} waitForLoading 2013 * @private 2014 */ 2015 HomeComponent3D.prototype.updateBackgroundColorAndTexture = function(skyBackgroundAppearance, groundBackgroundAppearance, 2016 home, waitForLoading) { 2017 var skyColor = home.getEnvironment().getSkyColor(); 2018 skyBackgroundAppearance.setDiffuseColor(vec3.fromValues(((skyColor >>> 16) & 0xFF) / 255., 2019 ((skyColor >>> 8) & 0xFF) / 255., 2020 (skyColor & 0xFF) / 255.)); 2021 var skyTexture = home.getEnvironment().getSkyTexture(); 2022 if (skyTexture !== null) { 2023 var transform = mat3.create(); 2024 mat3.fromTranslation(transform, vec2.fromValues(-skyTexture.getXOffset(), 0)); 2025 TextureManager.getInstance().loadTexture(skyTexture.getImage(), 0, waitForLoading, 2026 { 2027 textureUpdated : function(textureImage) { 2028 skyBackgroundAppearance.setTextureImage(textureImage); 2029 skyBackgroundAppearance.setTextureTransform(transform); 2030 }, 2031 textureError : function(error) { 2032 return this.textureUpdated(TextureManager.getInstance().getErrorImage()); 2033 } 2034 }); 2035 } else { 2036 skyBackgroundAppearance.setTextureImage(null); 2037 } 2038 2039 var groundColor = home.getEnvironment().getGroundColor(); 2040 var color = vec3.fromValues(((groundColor >>> 16) & 0xFF) / 255., 2041 ((groundColor >>> 8) & 0xFF) / 255., 2042 (groundColor & 0xFF) / 255.); 2043 groundBackgroundAppearance.setDiffuseColor(color); 2044 groundBackgroundAppearance.setAmbientColor(color); 2045 var groundTexture = home.getEnvironment().getGroundTexture(); 2046 if (groundTexture !== null) { 2047 TextureManager.getInstance().loadTexture(groundTexture.getImage(), 0, waitForLoading, 2048 { 2049 textureUpdated : function(textureImage) { 2050 // Display texture very small to get an average color at the horizon 2051 groundBackgroundAppearance.setTextureImage(textureImage); 2052 groundBackgroundAppearance.setTextureCoordinatesGeneration( 2053 {planeS : vec4.fromValues(1E5, 0, 0, 0), 2054 planeT : vec4.fromValues(0, 0, 1E5, 0)}); 2055 }, 2056 textureError : function(error) { 2057 return this.textureUpdated(TextureManager.getInstance().getErrorImage()); 2058 } 2059 }); 2060 } else { 2061 groundBackgroundAppearance.setTextureImage(null); 2062 } 2063 } 2064 2065 /** 2066 * Returns a new ground node. 2067 * @private 2068 */ 2069 HomeComponent3D.prototype.createGroundNode = function(groundOriginX, groundOriginY, groundWidth, groundDepth, 2070 listenToHomeUpdates, waitForLoading) { 2071 if (this.home.structure) { 2072 var structureGroup = new BranchGroup3D(); 2073 structureGroup.setCapability(Group3D.ALLOW_CHILDREN_EXTEND); 2074 ModelManager.getInstance().loadModel(this.home.structure, waitForLoading, 2075 { 2076 modelUpdated : function(structureNode) { 2077 structureGroup.addChild(structureNode); 2078 }, 2079 modelError : function(ex) { 2080 // Display a large red box at ground level 2081 var boxAppearance = new Appearance3D(); 2082 boxAppearance.setDiffuseColor(vec3.fromValues(1, 0, 0)); 2083 structureGroup.addChild(new Box3D(1E7, 0, 1E7, boxAppearance)); 2084 } 2085 }); 2086 2087 this.groundChangeListener = function(ev) {}; // Dummy listener 2088 return structureGroup; 2089 } else { 2090 var ground3D = typeof Ground3D !== "undefined" 2091 ? new Ground3D(this.home, groundOriginX, groundOriginY, groundWidth, groundDepth, waitForLoading) 2092 : new Box3D(1E7, 0, 1E7, new Appearance3D()); 2093 var translation = mat4.create(); 2094 mat4.translate(translation, translation, vec3.fromValues(0, -0.2, 0)); 2095 var transformGroup = new TransformGroup3D(translation); 2096 transformGroup.addChild(ground3D); 2097 2098 if (listenToHomeUpdates) { 2099 var component3D = this; 2100 // Add a listener on ground color and texture properties change 2101 this.groundChangeListener = function(ev) { 2102 if (!component3D.groundChangeListener.updater) { 2103 component3D.groundChangeListener.updater = function() { 2104 ground3D.update(); 2105 delete component3D.groundChangeListener.updater; 2106 }; 2107 setTimeout(component3D.groundChangeListener.updater, 0); 2108 } 2109 }; 2110 var homeEnvironment = this.home.getEnvironment(); 2111 homeEnvironment.addPropertyChangeListener("GROUND_COLOR", this.groundChangeListener); 2112 homeEnvironment.addPropertyChangeListener("BACKGROUND_IMAGE_VISIBLE_ON_GROUND_3D", this.groundChangeListener); 2113 homeEnvironment.addPropertyChangeListener("GROUND_TEXTURE", this.groundChangeListener); 2114 this.home.addPropertyChangeListener("BACKGROUND_IMAGE", this.groundChangeListener); 2115 } 2116 return transformGroup; 2117 } 2118 } 2119 2120 /** 2121 * Returns the lights of the scene. 2122 * @private 2123 */ 2124 HomeComponent3D.prototype.createLights = function(listenToHomeUpdates) { 2125 var lights = [ 2126 new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(1.5, -0.8, -1)), 2127 new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(-1.5, -0.8, -1)), 2128 new DirectionalLight3D(vec3.fromValues(0.9, 0.9, 0.9), vec3.fromValues(0, -0.8, 1)), 2129 new DirectionalLight3D(vec3.fromValues(0.7, 0.7, 0.7), vec3.fromValues(0, 1, 0)), 2130 new AmbientLight3D(vec3.fromValues(0.2, 0.2, 0.2))]; 2131 for (var i = 0; i < lights.length - 1; i++) { 2132 // Store default color 2133 lights [i].defaultColor = lights [i].getColor(); 2134 this.updateLightColor(lights [i]); 2135 } 2136 2137 if (listenToHomeUpdates) { 2138 // Add a listener on light color property change to home 2139 var component3D = this; 2140 this.lightColorListener = function(ev) { 2141 for (var i = 0; i < lights.length - 1; i++) { 2142 component3D.updateLightColor(lights [i]); 2143 } 2144 }; 2145 this.home.getEnvironment().addPropertyChangeListener( 2146 "LIGHT_COLOR", this.lightColorListener); 2147 } 2148 2149 return lights; 2150 } 2151 2152 /** 2153 * Updates<code>light</code> color from <code>home</code> light color. 2154 * @param {Light3D} light the light to update 2155 * @private 2156 */ 2157 HomeComponent3D.prototype.updateLightColor = function(light) { 2158 var defaultColor = light.defaultColor; 2159 var lightColor = this.home.getEnvironment().getLightColor(); 2160 light.setColor(vec3.fromValues(((lightColor >>> 16) & 0xFF) / 255 * defaultColor [0], 2161 ((lightColor >>> 8) & 0xFF) / 255 * defaultColor [1], 2162 (lightColor & 0xFF) / 255 * defaultColor [2])); 2163 } 2164 2165 /** 2166 * Returns a <code>home</code> new tree node, with branches for each wall 2167 * and piece of furniture of <code>home</code>. 2168 * @private 2169 */ 2170 HomeComponent3D.prototype.createHomeTree = function(listenToHomeUpdates, waitForLoading) { 2171 var homeRoot = new BranchGroup3D(); 2172 homeRoot.setCapability(Group3D.ALLOW_CHILDREN_EXTEND); 2173 // Add walls, pieces, rooms, polylines, dimension lines and labels already available 2174 var labels = this.home.getLabels(); 2175 for (var i = 0; i < labels.length; i++) { 2176 this.addObject(homeRoot, labels [i], listenToHomeUpdates, waitForLoading); 2177 } 2178 var dimensionLines = this.home.getDimensionLines(); 2179 for (var i = 0; i < dimensionLines.length; i++) { 2180 this.addObject(homeRoot, dimensionLines [i], listenToHomeUpdates, waitForLoading); 2181 } 2182 var polylines = this.home.getPolylines(); 2183 for (var i = 0; i < polylines.length; i++) { 2184 this.addObject(homeRoot, polylines [i], listenToHomeUpdates, waitForLoading); 2185 } 2186 var rooms = this.home.getRooms(); 2187 for (var i = 0; i < rooms.length; i++) { 2188 this.addObject(homeRoot, rooms [i], listenToHomeUpdates, waitForLoading); 2189 } 2190 var walls = this.home.getWalls(); 2191 for (var i = 0; i < walls.length; i++) { 2192 this.addObject(homeRoot, walls [i], listenToHomeUpdates, waitForLoading); 2193 } 2194 var furniture = this.home.getFurniture(); 2195 for (var i = 0; i < furniture.length; i++) { 2196 var piece = furniture [i]; 2197 if (piece instanceof HomeFurnitureGroup) { 2198 var groupFurniture = piece.getAllFurniture(); 2199 for (var j = 0; j < groupFurniture.length; j++) { 2200 var childPiece = groupFurniture [j]; 2201 if (!(childPiece instanceof HomeFurnitureGroup)) { 2202 this.addObject(homeRoot, childPiece, listenToHomeUpdates, waitForLoading); 2203 } 2204 } 2205 } else { 2206 this.addObject(homeRoot, piece, listenToHomeUpdates, waitForLoading); 2207 } 2208 } 2209 if (listenToHomeUpdates) { 2210 // Add level, wall, furniture, room listeners to home for further update 2211 this.addLevelListener(homeRoot); 2212 this.addWallListener(homeRoot); 2213 this.addFurnitureListener(homeRoot); 2214 this.addRoomListener(homeRoot); 2215 this.addPolylineListener(homeRoot); 2216 this.addDimensionLineListener(homeRoot); 2217 this.addLabelListener(homeRoot); 2218 this.addEnvironmentListeners(); 2219 component3D = this; 2220 this.selectionListener = { 2221 selectionChanged: function(ev) { 2222 component3D.updateObjectsAndFurnitureGroups(ev.getOldSelectedItems()); 2223 component3D.updateObjectsAndFurnitureGroups(ev.getSelectedItems()); 2224 } 2225 }; 2226 this.home.addSelectionListener(this.selectionListener); 2227 } 2228 return homeRoot; 2229 } 2230 2231 /** 2232 * Adds a level listener to home levels that updates the children of the given 2233 * <code>group</code>, each time a level is added, updated or deleted. 2234 * @param {Group3D} group 2235 * @private 2236 */ 2237 HomeComponent3D.prototype.addLevelListener = function(group) { 2238 var component3D = this; 2239 this.levelChangeListener = function(ev) { 2240 var propertyName = ev.getPropertyName(); 2241 if ("VISIBLE" == propertyName 2242 || "VIEWABLE" == propertyName) { 2243 var objects = component3D.homeObjects; 2244 var updatedItems = []; 2245 for (var i = 0; i < objects.length; i++) { 2246 var item = objects [i]; 2247 if (item instanceof Room // 3D rooms depend on rooms at other levels 2248 || item.isAtLevel !== undefined // item instanceof Elevetable 2249 || item.isAtLevel(ev.getSource())) { 2250 updatedItems.push(item); 2251 } 2252 } 2253 component3D.updateObjects(updatedItems); 2254 component3D.groundChangeListener(null); 2255 } else if ("ELEVATION" == propertyName 2256 || "ELEVATION_INDEX" == propertyName) { 2257 component3D.updateObjects(component3D.homeObjects.slice(0)); 2258 component3D.groundChangeListener(null); 2259 } else if ("BACKGROUND_IMAGE" == propertyName) { 2260 component3D.groundChangeListener(null); 2261 } else if ("FLOOR_THICKNESS" == propertyName) { 2262 component3D.updateObjects(component3D.home.getWalls()); 2263 component3D.updateObjects(component3D.home.getRooms()); 2264 } else if ("HEIGHT" == propertyName) { 2265 component3D.updateObjects(component3D.home.getRooms()); 2266 } 2267 }; 2268 var levels = this.home.getLevels(); 2269 for (var i = 0; i < levels.length; i++) { 2270 levels[i].addPropertyChangeListener(this.levelChangeListener); 2271 } 2272 2273 this.levelListener = function(ev) { 2274 var level = ev.getItem(); 2275 switch ((ev.getType())) { 2276 case CollectionEvent.Type.ADD : 2277 level.addPropertyChangeListener(component3D.levelChangeListener); 2278 break; 2279 case CollectionEvent.Type.DELETE : 2280 level.removePropertyChangeListener(component3D.levelChangeListener); 2281 break; 2282 } 2283 component3D.updateObjects(component3D.home.getRooms()); 2284 }; 2285 this.home.addLevelsListener(this.levelListener); 2286 } 2287 2288 /** 2289 * Adds a wall listener to home walls that updates the children of the given 2290 * <code>group</code>, each time a wall is added, updated or deleted. 2291 * @param {Group3D} group 2292 * @private 2293 */ 2294 HomeComponent3D.prototype.addWallListener = function(group) { 2295 var component3D = this; 2296 this.wallChangeListener = function(ev) { 2297 var propertyName = ev.getPropertyName(); 2298 if ("PATTERN" != propertyName) { 2299 var updatedWall = ev.getSource(); 2300 component3D.updateWall(updatedWall); 2301 var levels = component3D.home.getLevels(); 2302 if (updatedWall.getLevel() === null 2303 || updatedWall.isAtLevel(levels [levels.length - 1])) { 2304 component3D.updateObjects(component3D.home.getRooms()); 2305 } 2306 if (updatedWall.getLevel() != null && updatedWall.getLevel().getElevation() < 0) { 2307 component3D.groundChangeListener(null); 2308 } 2309 } 2310 }; 2311 var walls = this.home.getWalls(); 2312 for (var i = 0; i < walls.length; i++) { 2313 walls[i].addPropertyChangeListener(this.wallChangeListener); 2314 } 2315 this.wallListener = function(ev) { 2316 var wall = ev.getItem(); 2317 switch ((ev.getType())) { 2318 case CollectionEvent.Type.ADD : 2319 component3D.addObject(group, wall, true, false); 2320 wall.addPropertyChangeListener(component3D.wallChangeListener); 2321 break; 2322 case CollectionEvent.Type.DELETE : 2323 component3D.deleteObject(wall); 2324 wall.removePropertyChangeListener(component3D.wallChangeListener); 2325 break; 2326 } 2327 component3D.updateObjects(component3D.home.getRooms()); 2328 component3D.groundChangeListener(null); 2329 }; 2330 this.home.addWallsListener(this.wallListener); 2331 } 2332 2333 /** 2334 * Adds a furniture listener to home that updates the children of the given <code>group</code>, 2335 * each time a piece of furniture is added, updated or deleted. 2336 * @private 2337 */ 2338 HomeComponent3D.prototype.addFurnitureListener = function(group) { 2339 var component3D = this; 2340 var updatePieceOfFurnitureGeometry = function(piece, propertyName, oldValue) { 2341 component3D.updateObjects([piece]); 2342 if (component3D.containsDoorsAndWindows(piece)) { 2343 if (oldValue !== null) { 2344 var oldPiece = piece.clone(); 2345 if ("X" == propertyName) { 2346 oldPiece.setX(oldValue); 2347 } else if ("Y" == propertyName) { 2348 oldPiece.setY(oldValue); 2349 } else if ("ANGLE" == propertyName) { 2350 oldPiece.setAngle(oldValue); 2351 } else if ("WIDTH" == propertyName) { 2352 oldPiece.setWidth(oldValue); 2353 } else if ("DEPTH" == propertyName) { 2354 oldPiece.setDepth(oldValue); 2355 } 2356 // For doors and windows, propertyName can't be equal to ROLL or PITCH 2357 2358 component3D.updateIntersectingWalls([oldPiece, piece]); 2359 } else { 2360 component3D.updateIntersectingWalls([piece]); 2361 } 2362 2363 component3D.updateObjects(component3D.home.getWalls()); 2364 } else if (component3D.containsStaircases(piece)) { 2365 component3D.updateObjects(component3D.home.getRooms()); 2366 } 2367 if (piece.getLevel() !== null && piece.getLevel().getElevation() < 0) { 2368 component3D.groundChangeListener(null); 2369 } 2370 }; 2371 this.furnitureChangeListener = function(ev) { 2372 var updatedPiece = ev.getSource(); 2373 var propertyName = ev.getPropertyName(); 2374 if ("X" == propertyName 2375 || "Y" == propertyName 2376 || "ANGLE" == propertyName 2377 || "ROLL" == propertyName 2378 || "PITCH" == propertyName 2379 || "WIDTH" == propertyName 2380 || "DEPTH" == propertyName) { 2381 updatePieceOfFurnitureGeometry(updatedPiece, propertyName, ev.getOldValue()); 2382 } else if ("HEIGHT" == propertyName 2383 || "ELEVATION" == propertyName 2384 || "MODEL" == propertyName 2385 || "MODEL_ROTATION" == propertyName 2386 || "MODEL_MIRRORED" == propertyName 2387 || "MODEL_FLAGS" == propertyName 2388 || "MODEL_TRANSFORMATIONS" == propertyName 2389 || "STAIRCASE_CUT_OUT_SHAPE" == propertyName 2390 || "VISIBLE" == propertyName 2391 || "LEVEL" == propertyName) { 2392 updatePieceOfFurnitureGeometry(updatedPiece, null, null); 2393 } else if ("CUT_OUT_SHAPE" == propertyName 2394 || "WALL_CUT_OUT_ON_BOTH_SIDES" == propertyName 2395 || "WALL_WIDTH" == propertyName 2396 || "WALL_LEFT" == propertyName 2397 || "WALL_HEIGHT" == propertyName 2398 || "WALL_TOP" == propertyName) { 2399 if (component3D.containsDoorsAndWindows(updatedPiece)) { 2400 component3D.updateIntersectingWalls([updatedPiece]); 2401 } 2402 } else if ("COLOR" == propertyName 2403 || "TEXTURE" == propertyName 2404 || "MODEL_MATERIALS" == propertyName 2405 || "SHININESS" == propertyName 2406 || ("POWER" == propertyName 2407 && component3D.home.getEnvironment().getSubpartSizeUnderLight() > 0)) { 2408 component3D.updateObjects([updatedPiece]); 2409 } 2410 }; 2411 2412 var furniture = this.home.getFurniture(); 2413 for (var i = 0; i < furniture.length; i++) { 2414 this.addPropertyChangeListener(furniture [i], this.furnitureChangeListener); 2415 } 2416 this.furnitureListener = function(ev) { 2417 var piece = ev.getItem(); 2418 switch (ev.getType()) { 2419 case CollectionEvent.Type.ADD : 2420 component3D.addPieceOfFurniture(group, piece, true, false); 2421 component3D.addPropertyChangeListener(piece, component3D.furnitureChangeListener); 2422 break; 2423 case CollectionEvent.Type.DELETE : 2424 component3D.deletePieceOfFurniture(piece); 2425 component3D.removePropertyChangeListener(piece, component3D.furnitureChangeListener); 2426 break; 2427 } 2428 // If piece is or contains a door or a window, update walls that intersect with piece 2429 if (component3D.containsDoorsAndWindows(piece)) { 2430 component3D.updateIntersectingWalls([piece]); 2431 } else if (component3D.containsStaircases(piece)) { 2432 component3D.updateObjects(component3D.home.getRooms()); 2433 } else { 2434 component3D.approximateHomeBoundsCache = null; 2435 component3D.homeHeightCache = null; 2436 } 2437 component3D.groundChangeListener(null); 2438 }; 2439 this.home.addFurnitureListener(this.furnitureListener); 2440 } 2441 2442 /** 2443 * Adds the given <code>listener</code> to <code>piece</code> and its children. 2444 * @param {HomePieceOfFurniture} piece 2445 * @param {PropertyChangeListener} listener 2446 * @private 2447 */ 2448 HomeComponent3D.prototype.addPropertyChangeListener = function(piece, listener) { 2449 if (piece instanceof HomeFurnitureGroup) { 2450 var furniture = piece.getFurniture(); 2451 for (var i = 0; i < furniture.length; i++) { 2452 this.addPropertyChangeListener(furniture [i], listener); 2453 } 2454 } else { 2455 piece.addPropertyChangeListener(listener); 2456 } 2457 } 2458 2459 /** 2460 * Removes the given <code>listener</code> from <code>piece</code> and its children. 2461 * @param {HomePieceOfFurniture} piece 2462 * @param {PropertyChangeListener} listener 2463 * @private 2464 */ 2465 HomeComponent3D.prototype.removePropertyChangeListener = function(piece, listener) { 2466 if (piece instanceof HomeFurnitureGroup) { 2467 var furniture = piece.getFurniture(); 2468 for (var i = 0; i < furniture.length; i++) { 2469 this.removePropertyChangeListener(furniture [i], listener); 2470 } 2471 } else { 2472 piece.removePropertyChangeListener(listener); 2473 } 2474 } 2475 2476 /** 2477 * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window. 2478 * @param {HomePieceOfFurniture} piece 2479 * @return {boolean} 2480 * @private 2481 */ 2482 HomeComponent3D.prototype.containsDoorsAndWindows = function(piece) { 2483 if (piece instanceof HomeFurnitureGroup) { 2484 var furniture = piece.getFurniture(); 2485 for (var i = 0; i < furniture.length; i++) { 2486 if (this.containsDoorsAndWindows(furniture[i])) { 2487 return true; 2488 } 2489 } 2490 return false; 2491 } else { 2492 return piece.isDoorOrWindow(); 2493 } 2494 } 2495 2496 /** 2497 * Returns <code>true</code> if the given <code>piece</code> is or contains a staircase 2498 * with a top cut out shape. 2499 * @param {HomePieceOfFurniture} piece 2500 * @return {boolean} 2501 * @private 2502 */ 2503 HomeComponent3D.prototype.containsStaircases = function(piece) { 2504 if (piece instanceof HomeFurnitureGroup) { 2505 var furniture = piece.getFurniture(); 2506 for (var i = 0; i < furniture.length; i++) { 2507 if (this.containsStaircases(furniture[i])) { 2508 return true; 2509 } 2510 } 2511 return false; 2512 } else { 2513 return piece.getStaircaseCutOutShape() !== null; 2514 } 2515 } 2516 2517 /** 2518 * Adds a room listener to home rooms that updates the children of the given 2519 * <code>group</code>, each time a room is added, updated or deleted. 2520 * @param {Group3D} group 2521 * @private 2522 */ 2523 HomeComponent3D.prototype.addRoomListener = function(group) { 2524 var component3D = this; 2525 this.roomChangeListener = function(ev) { 2526 var updatedRoom = ev.getSource(); 2527 var propertyName = ev.getPropertyName(); 2528 if ("FLOOR_COLOR" == propertyName 2529 || "FLOOR_TEXTURE" == propertyName 2530 || "FLOOR_SHININESS" == propertyName 2531 || "CEILING_COLOR" == propertyName 2532 || "CEILING_TEXTURE" == propertyName 2533 || "CEILING_SHININESS" == propertyName 2534 || "CEILING_FLAT" == propertyName) { 2535 component3D.updateObjects([updatedRoom]); 2536 } else if ("FLOOR_VISIBLE" == propertyName 2537 || "CEILING_VISIBLE" == propertyName 2538 || "LEVEL" == propertyName) { 2539 component3D.updateObjects(component3D.home.getRooms()); 2540 component3D.groundChangeListener(null); 2541 } else if ("POINTS" == propertyName) { 2542 if (component3D.homeObjectsToUpdate) { 2543 // Don't try to optimize if more than one room to update 2544 component3D.updateObjects(component3D.home.getRooms()); 2545 } else { 2546 component3D.updateObjects([updatedRoom]); 2547 // Search the rooms that overlap the updated one 2548 var oldArea = new java.awt.geom.Area(component3D.getShape(ev.getOldValue())); 2549 var newArea = new java.awt.geom.Area(component3D.getShape(ev.getNewValue())); 2550 var updatedRoomLevel = updatedRoom.getLevel(); 2551 var rooms = component3D.home.getRooms(); 2552 for (var i = 0; i < rooms.length; i++) { 2553 var room = rooms[i]; 2554 var roomLevel = room.getLevel(); 2555 if (room != updatedRoom 2556 && (roomLevel == null 2557 || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() + roomLevel.getHeight())) < 1E-5 2558 || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() - roomLevel.getFloorThickness())) < 1E-5)) { 2559 var roomAreaIntersectionWithOldArea = new java.awt.geom.Area(component3D.getShape(room.getPoints())); 2560 var roomAreaIntersectionWithNewArea = new java.awt.geom.Area(roomAreaIntersectionWithOldArea); 2561 roomAreaIntersectionWithNewArea.intersect(newArea); 2562 if (!roomAreaIntersectionWithNewArea.isEmpty()) { 2563 component3D.updateObjects([room]); 2564 } else { 2565 roomAreaIntersectionWithOldArea.intersect(oldArea); 2566 if (!roomAreaIntersectionWithOldArea.isEmpty()) { 2567 component3D.updateObjects([room]); 2568 } 2569 } 2570 } 2571 } 2572 } 2573 component3D.groundChangeListener(null); 2574 } 2575 }; 2576 var rooms = this.home.getRooms(); 2577 for (var i = 0; i < rooms.length; i++) { 2578 rooms[i].addPropertyChangeListener(this.roomChangeListener); 2579 } 2580 this.roomListener = function(ev) { 2581 var room = ev.getItem(); 2582 switch (ev.getType()) { 2583 case CollectionEvent.Type.ADD : 2584 component3D.addObject(group, room, ev.getIndex(), true, false); 2585 room.addPropertyChangeListener(component3D.roomChangeListener); 2586 break; 2587 case CollectionEvent.Type.DELETE : 2588 component3D.deleteObject(room); 2589 room.removePropertyChangeListener(component3D.roomChangeListener); 2590 break; 2591 } 2592 component3D.updateObjects(component3D.home.getRooms()); 2593 component3D.groundChangeListener(null); 2594 }; 2595 this.home.addRoomsListener(this.roomListener); 2596 } 2597 2598 /** 2599 * Returns the path matching points. 2600 * @param {Array} points 2601 * @return {GeneralPath} 2602 * @private 2603 */ 2604 HomeComponent3D.prototype.getShape = function(points) { 2605 var path = new java.awt.geom.GeneralPath(); 2606 path.moveTo(points[0][0], points[0][1]); 2607 for (var i = 1; i < points.length; i++) { 2608 path.lineTo(points[i][0], points[i][1]); 2609 } 2610 path.closePath(); 2611 return path; 2612 } 2613 2614 /** 2615 * Adds a polyline listener to home polylines that updates the children of the given 2616 * <code>group</code>, each time a polyline is added, updated or deleted. 2617 * @param {Group} group 2618 * @private 2619 */ 2620 HomeComponent3D.prototype.addPolylineListener = function(group) { 2621 var component3D = this; 2622 this.polylineChangeListener = function(ev) { 2623 var polyline = ev.getSource(); 2624 component3D.updateObjects([polyline]); 2625 }; 2626 var polylines = this.home.getPolylines(); 2627 for (var i = 0; i < polylines.length; i++) { 2628 polylines[i].addPropertyChangeListener(this.polylineChangeListener); 2629 } 2630 this.polylineListener = function(ev) { 2631 var polyline = ev.getItem(); 2632 switch (ev.getType()) { 2633 case CollectionEvent.Type.ADD : 2634 component3D.addObject(group, polyline, true, false); 2635 polyline.addPropertyChangeListener(component3D.polylineChangeListener); 2636 break; 2637 case CollectionEvent.Type.DELETE : 2638 component3D.deleteObject(polyline); 2639 polyline.removePropertyChangeListener(component3D.polylineChangeListener); 2640 break; 2641 } 2642 }; 2643 this.home.addPolylinesListener(this.polylineListener); 2644 } 2645 2646 /** 2647 * Adds a dimension line listener to home dimension lines that updates the children of the given 2648 * <code>group</code>, each time a dimension line is added, updated or deleted. 2649 * @param {Group3D} group 2650 * @private 2651 */ 2652 HomeComponent3D.prototype.addDimensionLineListener = function(group) { 2653 var component3D = this; 2654 this.dimensionLineChangeListener = function(ev) { 2655 var updatedDimensionLine = ev.getSource(); 2656 component3D.updateObjects([updatedDimensionLine]); 2657 }; 2658 var dimensionLines = this.home.getDimensionLines(); 2659 for (var i = 0; i < dimensionLines.length; i++) { 2660 dimensionLines [i].addPropertyChangeListener(this.dimensionLineChangeListener); 2661 } 2662 this.dimensionLineListener = function(ev) { 2663 var dimensionLine = ev.getItem(); 2664 switch (ev.getType()) { 2665 case CollectionEvent.Type.ADD : 2666 component3D.addObject(group, dimensionLine, true, false); 2667 dimensionLine.addPropertyChangeListener(component3D.dimensionLineChangeListener); 2668 break; 2669 case CollectionEvent.Type.DELETE : 2670 component3D.deleteObject(dimensionLine); 2671 dimensionLine.removePropertyChangeListener(component3D.dimensionLineChangeListener); 2672 break; 2673 } 2674 }; 2675 this.home.addDimensionLinesListener(this.dimensionLineListener); 2676 } 2677 2678 /** 2679 * Adds a label listener to home labels that updates the children of the given 2680 * <code>group</code>, each time a label is added, updated or deleted. 2681 * @param {Group3D} group 2682 * @private 2683 */ 2684 HomeComponent3D.prototype.addLabelListener = function(group) { 2685 var component3D = this; 2686 this.labelChangeListener = function(ev) { 2687 var label = ev.getSource(); 2688 component3D.updateObjects([label]); 2689 }; 2690 var labels = this.home.getLabels(); 2691 for (var i = 0; i < labels.length; i++) { 2692 labels[i].addPropertyChangeListener(this.labelChangeListener); 2693 } 2694 this.labelListener = function(ev) { 2695 var label = ev.getItem(); 2696 switch (ev.getType()) { 2697 case CollectionEvent.Type.ADD : 2698 component3D.addObject(group, label, true, false); 2699 label.addPropertyChangeListener(component3D.labelChangeListener); 2700 break; 2701 case CollectionEvent.Type.DELETE : 2702 component3D.deleteObject(label); 2703 label.removePropertyChangeListener(component3D.labelChangeListener); 2704 break; 2705 } 2706 }; 2707 this.home.addLabelsListener(this.labelListener); 2708 } 2709 2710 /** 2711 * Adds a walls alpha change listener and drawing mode change listener to home 2712 * environment that updates the home scene objects appearance. 2713 * @private 2714 */ 2715 HomeComponent3D.prototype.addEnvironmentListeners = function() { 2716 var component3D = this; 2717 this.wallsAlphaListener = function(ev) { 2718 component3D.updateObjects(component3D.home.getWalls()); 2719 component3D.updateObjects(component3D.home.getRooms()); 2720 }; 2721 this.home.getEnvironment().addPropertyChangeListener("WALLS_ALPHA", this.wallsAlphaListener); 2722 } 2723 2724 /** 2725 * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>. 2726 * If <code>index</code> is missing or equal to -1, <code>homeObject</code> will be added at the end of the group. 2727 * @param {Group3D} group 2728 * @param {Object} homeObject 2729 * @param {number} [index] 2730 * @param {boolean} listenToHomeUpdates 2731 * @param {boolean} waitForLoading 2732 * @private 2733 */ 2734 HomeComponent3D.prototype.addObject = function(group, homeObject, index, 2735 listenToHomeUpdates, waitForLoading) { 2736 if (waitForLoading === undefined) { 2737 waitForLoading = listenToHomeUpdates; 2738 listenToHomeUpdates = index; 2739 index = -1; 2740 } 2741 var object3D = this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading); 2742 if (listenToHomeUpdates) { 2743 homeObject.object3D = object3D; 2744 this.homeObjects.push(homeObject); 2745 this.homeObjects3D.push(object3D); 2746 } 2747 if (index === -1) { 2748 group.addChild(object3D); 2749 } else { 2750 group.insertChild(object3D, index); 2751 } 2752 return object3D; 2753 } 2754 2755 /** 2756 * Adds to <code>group</code> a branch matching <code>homeObject</code> or its children if the piece is a group of furniture. 2757 * @param {Group3D} group 2758 * @param {HomePieceOfFurniture} piece 2759 * @param {boolean} listenToHomeUpdates 2760 * @param {boolean} waitForLoading 2761 * @private 2762 */ 2763 HomeComponent3D.prototype.addPieceOfFurniture = function(group, piece, listenToHomeUpdates, waitForLoading) { 2764 if (piece instanceof HomeFurnitureGroup) { 2765 var furniture = piece.getFurniture(); 2766 for (var i = 0; i < furniture.length; i++) { 2767 this.addPieceOfFurniture(group, furniture [i], listenToHomeUpdates, waitForLoading); 2768 } 2769 } else { 2770 this.addObject(group, piece, listenToHomeUpdates, waitForLoading); 2771 } 2772 } 2773 2774 /** 2775 * Detaches from the scene the branch matching <code>homeObject</code>. 2776 * @param {Object} homeObject 2777 * @private 2778 */ 2779 HomeComponent3D.prototype.deleteObject = function(homeObject) { 2780 if (homeObject.object3D) { 2781 homeObject.object3D.detach(); 2782 delete homeObject.object3D; 2783 var objectIndex = this.homeObjects.indexOf(homeObject); 2784 this.homeObjects.splice(objectIndex, 1); 2785 this.homeObjects3D.splice(objectIndex, 1); 2786 if (this.homeObjectsToUpdate) { 2787 var index = this.homeObjectsToUpdate.indexOf(homeObject); 2788 if (index >= 0) { 2789 this.homeObjectsToUpdate.splice(index, 1); 2790 } 2791 } 2792 } 2793 } 2794 2795 /** 2796 * Detaches from the scene the branches matching <code>piece</code> or its children if it's a group. 2797 * @param {HomePieceOfFurniture} piece 2798 * @private 2799 */ 2800 HomeComponent3D.prototype.deletePieceOfFurniture = function(piece) { 2801 if (piece instanceof HomeFurnitureGroup) { 2802 var furniture = piece.getFurniture(); 2803 for (var i = 0; i < furniture.length; i++) { 2804 this.deletePieceOfFurniture(furniture [i]); 2805 } 2806 } else { 2807 this.deleteObject(piece); 2808 } 2809 } 2810 2811 /** 2812 * Updates 3D <code>objects</code> later. 2813 * @param {Array} objects 2814 * @private 2815 */ 2816 HomeComponent3D.prototype.updateObjects = function(objects) { 2817 if (this.homeObjectsToUpdate) { 2818 for (var i = 0; i < objects.length; i++) { 2819 var object = objects [i]; 2820 if (this.homeObjectsToUpdate.indexOf(object) <= -1) { 2821 this.homeObjectsToUpdate.push(object); 2822 } 2823 } 2824 } else { 2825 this.homeObjectsToUpdate = objects.slice(0); 2826 // Invoke later the update of objects of homeObjectsToUpdate 2827 setTimeout( 2828 function(component3D) { 2829 for (var i = 0; i < component3D.homeObjectsToUpdate.length; i++) { 2830 var homeObject = component3D.homeObjectsToUpdate [i]; 2831 // Check object wasn't deleted since updateObjects call 2832 if (homeObject.object3D) { 2833 homeObject.object3D.update(); 2834 } 2835 } 2836 delete component3D.homeObjectsToUpdate; 2837 }, 0, this); 2838 } 2839 this.approximateHomeBoundsCache = null; 2840 this.homeHeightCache = null; 2841 } 2842 2843 /** 2844 * Updates 3D objects and furniture groups children, if <code>objects</code> contains some groups. 2845 * @param {Array} objects 2846 * @private 2847 */ 2848 HomeComponent3D.prototype.updateObjectsAndFurnitureGroups = function(objects) { 2849 this.updateObjects(objects); 2850 for (var i = 0; i < objects.length; i++) { 2851 var item = objects [i]; 2852 if (item instanceof HomeFurnitureGroup) { 2853 this.updateObjects(item.getAllFurniture()); 2854 } 2855 } 2856 } 2857 2858 /** 2859 * Updates walls that may intersect from the given doors or window. 2860 * @param {Array} doorOrWindows 2861 * @private 2862 */ 2863 HomeComponent3D.prototype.updateIntersectingWalls = function(doorOrWindows) { 2864 var walls = this.home.getWalls(); 2865 var wallCount = 0; 2866 if (this.homeObjectsToUpdate) { 2867 for (var i = 0; i < this.homeObjectsToUpdate.length; i++) { 2868 if (this.homeObjectsToUpdate [i] instanceof Wall) { 2869 wallCount++; 2870 } 2871 } 2872 } 2873 2874 if (wallCount !== walls.length) { 2875 var updatedWalls = []; 2876 var doorOrWindowBounds = null; 2877 for (var i = 0; i < doorOrWindows.length; i++) { 2878 var doorOrWindow = doorOrWindows [i]; 2879 var points = doorOrWindow.getPoints(); 2880 if (doorOrWindowBounds === null) { 2881 doorOrWindowBounds = new java.awt.geom.Rectangle2D.Float(points [0][0], points [0][1], 0, 0); 2882 } else { 2883 doorOrWindowBounds.add(points [0][0], points [0][1]); 2884 } 2885 for (var j = 1; j < points.length; j++) { 2886 doorOrWindowBounds.add(points [j][0], points [j][1]); 2887 } 2888 } 2889 // Search walls that intersect approximative bounds 2890 for (var i = 0; i < walls.length; i++) { 2891 var wall = walls [i]; 2892 if (wall.intersectsRectangle(doorOrWindowBounds.getX(), doorOrWindowBounds.getY(), 2893 doorOrWindowBounds.getX() + doorOrWindowBounds.getWidth(), 2894 doorOrWindowBounds.getY() + doorOrWindowBounds.getHeight())) { 2895 updatedWalls.push(wall); 2896 } 2897 } 2898 this.updateObjects(updatedWalls); 2899 } 2900 } 2901 2902 /** 2903 * Updates <code>wall</code> geometry, 2904 * and the walls at its end or start. 2905 * @param {Wall} wall 2906 * @private 2907 */ 2908 HomeComponent3D.prototype.updateWall = function(wall) { 2909 var wallsToUpdate = []; 2910 wallsToUpdate.push(wall); 2911 if (wall.getWallAtStart() != null) { 2912 wallsToUpdate.push(wall.getWallAtStart()); 2913 } 2914 if (wall.getWallAtEnd() != null) { 2915 wallsToUpdate.push(wall.getWallAtEnd()); 2916 } 2917 this.updateObjects(wallsToUpdate); 2918 } 2919 2920 2921 /** 2922 * A factory able to create instances of {@link Object3DBranch} class. 2923 * @param {UserPreferences} [preferences] 2924 * @constructor 2925 * @author Emmanuel Puybaret 2926 */ 2927 function Object3DBranchFactory(preferences) { 2928 if (preferences !== undefined) { 2929 this.preferences = preferences; 2930 } 2931 } 2932 2933 /** 2934 * Returns the 3D object matching a given <code>item</code>. 2935 * @param {Home} home 2936 * @param {Object} item 2937 * @param {boolean|function} waitForLoading 2938 * @return {Object3DBranch} an instance of a subclass of {@link Object3DBranch} 2939 */ 2940 Object3DBranchFactory.prototype.createObject3D = function(home, item, waitForLoading) { 2941 if (item instanceof HomePieceOfFurniture) { 2942 return new HomePieceOfFurniture3D(item, home, this.preferences, waitForLoading); 2943 } else if (item instanceof Wall) { 2944 return new Wall3D(item, home, this.preferences, waitForLoading); 2945 } else if (item instanceof Room) { 2946 return new Room3D(item, home, this.preferences, false, waitForLoading); 2947 } else if (item instanceof Polyline) { 2948 return new Polyline3D(item, home, this.preferences, waitForLoading); 2949 } else if (item instanceof DimensionLine) { 2950 return new DimensionLine3D(item, home, this.preferences, waitForLoading); 2951 } else if (item instanceof Label) { 2952 return new Label3D(item, home, this.preferences, waitForLoading); 2953 } else { 2954 return new Group3D(); 2955 } 2956 } 2957