1 /* 2 * HomeComponent3D.js 3 * 4 * Sweet Home 3D, Copyright (c) 2015 Emmanuel PUYBARET / eTeks <info@eteks.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 21 // Requires 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 component3D.updateObjects(component3D.homeObjects.slice(0)); 2257 component3D.groundChangeListener(null); 2258 } else if ("BACKGROUND_IMAGE" == propertyName) { 2259 component3D.groundChangeListener(null); 2260 } else if ("FLOOR_THICKNESS" == propertyName) { 2261 component3D.updateObjects(component3D.home.getWalls()); 2262 component3D.updateObjects(component3D.home.getRooms()); 2263 } else if ("HEIGHT" == propertyName) { 2264 component3D.updateObjects(component3D.home.getRooms()); 2265 } 2266 }; 2267 var levels = this.home.getLevels(); 2268 for (var i = 0; i < levels.length; i++) { 2269 levels[i].addPropertyChangeListener(this.levelChangeListener); 2270 } 2271 2272 this.levelListener = function(ev) { 2273 var level = ev.getItem(); 2274 switch ((ev.getType())) { 2275 case CollectionEvent.Type.ADD : 2276 level.addPropertyChangeListener(component3D.levelChangeListener); 2277 break; 2278 case CollectionEvent.Type.DELETE : 2279 level.removePropertyChangeListener(component3D.levelChangeListener); 2280 break; 2281 } 2282 component3D.updateObjects(component3D.home.getRooms()); 2283 }; 2284 this.home.addLevelsListener(this.levelListener); 2285 } 2286 2287 /** 2288 * Adds a wall listener to home walls that updates the children of the given 2289 * <code>group</code>, each time a wall is added, updated or deleted. 2290 * @param {Group3D} group 2291 * @private 2292 */ 2293 HomeComponent3D.prototype.addWallListener = function(group) { 2294 var component3D = this; 2295 this.wallChangeListener = function(ev) { 2296 var propertyName = ev.getPropertyName(); 2297 if ("PATTERN" != propertyName) { 2298 var updatedWall = ev.getSource(); 2299 component3D.updateWall(updatedWall); 2300 var levels = component3D.home.getLevels(); 2301 if (updatedWall.getLevel() === null 2302 || updatedWall.isAtLevel(levels [levels.length - 1])) { 2303 component3D.updateObjects(component3D.home.getRooms()); 2304 } 2305 if (updatedWall.getLevel() != null && updatedWall.getLevel().getElevation() < 0) { 2306 component3D.groundChangeListener(null); 2307 } 2308 } 2309 }; 2310 var walls = this.home.getWalls(); 2311 for (var i = 0; i < walls.length; i++) { 2312 walls[i].addPropertyChangeListener(this.wallChangeListener); 2313 } 2314 this.wallListener = function(ev) { 2315 var wall = ev.getItem(); 2316 switch ((ev.getType())) { 2317 case CollectionEvent.Type.ADD : 2318 component3D.addObject(group, wall, true, false); 2319 wall.addPropertyChangeListener(component3D.wallChangeListener); 2320 break; 2321 case CollectionEvent.Type.DELETE : 2322 component3D.deleteObject(wall); 2323 wall.removePropertyChangeListener(component3D.wallChangeListener); 2324 break; 2325 } 2326 component3D.updateObjects(component3D.home.getRooms()); 2327 component3D.groundChangeListener(null); 2328 }; 2329 this.home.addWallsListener(this.wallListener); 2330 } 2331 2332 /** 2333 * Adds a furniture listener to home that updates the children of the given <code>group</code>, 2334 * each time a piece of furniture is added, updated or deleted. 2335 * @private 2336 */ 2337 HomeComponent3D.prototype.addFurnitureListener = function(group) { 2338 var component3D = this; 2339 var updatePieceOfFurnitureGeometry = function(piece, propertyName, oldValue) { 2340 component3D.updateObjects([piece]); 2341 if (component3D.containsDoorsAndWindows(piece)) { 2342 if (oldValue !== null) { 2343 var oldPiece = piece.clone(); 2344 if ("X" == propertyName) { 2345 oldPiece.setX(oldValue); 2346 } else if ("Y" == propertyName) { 2347 oldPiece.setY(oldValue); 2348 } else if ("ANGLE" == propertyName) { 2349 oldPiece.setAngle(oldValue); 2350 } else if ("WIDTH" == propertyName) { 2351 oldPiece.setWidth(oldValue); 2352 } else if ("DEPTH" == propertyName) { 2353 oldPiece.setDepth(oldValue); 2354 } 2355 // For doors and windows, propertyName can't be equal to ROLL or PITCH 2356 2357 component3D.updateIntersectingWalls([oldPiece, piece]); 2358 } else { 2359 component3D.updateIntersectingWalls([piece]); 2360 } 2361 2362 component3D.updateObjects(component3D.home.getWalls()); 2363 } else if (component3D.containsStaircases(piece)) { 2364 component3D.updateObjects(component3D.home.getRooms()); 2365 } 2366 if (piece.getLevel() !== null && piece.getLevel().getElevation() < 0) { 2367 component3D.groundChangeListener(null); 2368 } 2369 }; 2370 this.furnitureChangeListener = function(ev) { 2371 var updatedPiece = ev.getSource(); 2372 var propertyName = ev.getPropertyName(); 2373 if ("X" == propertyName 2374 || "Y" == propertyName 2375 || "ANGLE" == propertyName 2376 || "ROLL" == propertyName 2377 || "PITCH" == propertyName 2378 || "WIDTH" == propertyName 2379 || "DEPTH" == propertyName) { 2380 updatePieceOfFurnitureGeometry(updatedPiece, propertyName, ev.getOldValue()); 2381 } else if ("HEIGHT" == propertyName 2382 || "ELEVATION" == propertyName 2383 || "MODEL" == propertyName 2384 || "MODEL_ROTATION" == propertyName 2385 || "MODEL_MIRRORED" == propertyName 2386 || "MODEL_FLAGS" == propertyName 2387 || "MODEL_TRANSFORMATIONS" == propertyName 2388 || "STAIRCASE_CUT_OUT_SHAPE" == propertyName 2389 || "VISIBLE" == propertyName 2390 || "LEVEL" == propertyName) { 2391 updatePieceOfFurnitureGeometry(updatedPiece, null, null); 2392 } else if ("CUT_OUT_SHAPE" == propertyName 2393 || "WALL_CUT_OUT_ON_BOTH_SIDES" == propertyName 2394 || "WALL_WIDTH" == propertyName 2395 || "WALL_LEFT" == propertyName 2396 || "WALL_HEIGHT" == propertyName 2397 || "WALL_TOP" == propertyName) { 2398 if (component3D.containsDoorsAndWindows(updatedPiece)) { 2399 component3D.updateIntersectingWalls([updatedPiece]); 2400 } 2401 } else if ("COLOR" == propertyName 2402 || "TEXTURE" == propertyName 2403 || "MODEL_MATERIALS" == propertyName 2404 || "SHININESS" == propertyName 2405 || ("POWER" == propertyName 2406 && component3D.home.getEnvironment().getSubpartSizeUnderLight() > 0)) { 2407 component3D.updateObjects([updatedPiece]); 2408 } 2409 }; 2410 2411 var furniture = this.home.getFurniture(); 2412 for (var i = 0; i < furniture.length; i++) { 2413 this.addPropertyChangeListener(furniture [i], this.furnitureChangeListener); 2414 } 2415 this.furnitureListener = function(ev) { 2416 var piece = ev.getItem(); 2417 switch (ev.getType()) { 2418 case CollectionEvent.Type.ADD : 2419 component3D.addPieceOfFurniture(group, piece, true, false); 2420 component3D.addPropertyChangeListener(piece, component3D.furnitureChangeListener); 2421 break; 2422 case CollectionEvent.Type.DELETE : 2423 component3D.deletePieceOfFurniture(piece); 2424 component3D.removePropertyChangeListener(piece, component3D.furnitureChangeListener); 2425 break; 2426 } 2427 // If piece is or contains a door or a window, update walls that intersect with piece 2428 if (component3D.containsDoorsAndWindows(piece)) { 2429 component3D.updateIntersectingWalls([piece]); 2430 } else if (component3D.containsStaircases(piece)) { 2431 component3D.updateObjects(component3D.home.getRooms()); 2432 } else { 2433 component3D.approximateHomeBoundsCache = null; 2434 component3D.homeHeightCache = null; 2435 } 2436 component3D.groundChangeListener(null); 2437 }; 2438 this.home.addFurnitureListener(this.furnitureListener); 2439 } 2440 2441 /** 2442 * Adds the given <code>listener</code> to <code>piece</code> and its children. 2443 * @param {HomePieceOfFurniture} piece 2444 * @param {PropertyChangeListener} listener 2445 * @private 2446 */ 2447 HomeComponent3D.prototype.addPropertyChangeListener = function(piece, listener) { 2448 if (piece instanceof HomeFurnitureGroup) { 2449 var furniture = piece.getFurniture(); 2450 for (var i = 0; i < furniture.length; i++) { 2451 this.addPropertyChangeListener(furniture [i], listener); 2452 } 2453 } else { 2454 piece.addPropertyChangeListener(listener); 2455 } 2456 } 2457 2458 /** 2459 * Removes the given <code>listener</code> from <code>piece</code> and its children. 2460 * @param {HomePieceOfFurniture} piece 2461 * @param {PropertyChangeListener} listener 2462 * @private 2463 */ 2464 HomeComponent3D.prototype.removePropertyChangeListener = function(piece, listener) { 2465 if (piece instanceof HomeFurnitureGroup) { 2466 var furniture = piece.getFurniture(); 2467 for (var i = 0; i < furniture.length; i++) { 2468 this.removePropertyChangeListener(furniture [i], listener); 2469 } 2470 } else { 2471 piece.removePropertyChangeListener(listener); 2472 } 2473 } 2474 2475 /** 2476 * Returns <code>true</code> if the given <code>piece</code> is or contains a door or window. 2477 * @param {HomePieceOfFurniture} piece 2478 * @return {boolean} 2479 * @private 2480 */ 2481 HomeComponent3D.prototype.containsDoorsAndWindows = function(piece) { 2482 if (piece instanceof HomeFurnitureGroup) { 2483 var furniture = piece.getFurniture(); 2484 for (var i = 0; i < furniture.length; i++) { 2485 if (this.containsDoorsAndWindows(furniture[i])) { 2486 return true; 2487 } 2488 } 2489 return false; 2490 } else { 2491 return piece.isDoorOrWindow(); 2492 } 2493 } 2494 2495 /** 2496 * Returns <code>true</code> if the given <code>piece</code> is or contains a staircase 2497 * with a top cut out shape. 2498 * @param {HomePieceOfFurniture} piece 2499 * @return {boolean} 2500 * @private 2501 */ 2502 HomeComponent3D.prototype.containsStaircases = function(piece) { 2503 if (piece instanceof HomeFurnitureGroup) { 2504 var furniture = piece.getFurniture(); 2505 for (var i = 0; i < furniture.length; i++) { 2506 if (this.containsStaircases(furniture[i])) { 2507 return true; 2508 } 2509 } 2510 return false; 2511 } else { 2512 return piece.getStaircaseCutOutShape() !== null; 2513 } 2514 } 2515 2516 /** 2517 * Adds a room listener to home rooms that updates the children of the given 2518 * <code>group</code>, each time a room is added, updated or deleted. 2519 * @param {Group3D} group 2520 * @private 2521 */ 2522 HomeComponent3D.prototype.addRoomListener = function(group) { 2523 var component3D = this; 2524 this.roomChangeListener = function(ev) { 2525 var updatedRoom = ev.getSource(); 2526 var propertyName = ev.getPropertyName(); 2527 if ("FLOOR_COLOR" == propertyName 2528 || "FLOOR_TEXTURE" == propertyName 2529 || "FLOOR_SHININESS" == propertyName 2530 || "CEILING_COLOR" == propertyName 2531 || "CEILING_TEXTURE" == propertyName 2532 || "CEILING_SHININESS" == propertyName 2533 || "CEILING_FLAT" == propertyName) { 2534 component3D.updateObjects([updatedRoom]); 2535 } else if ("FLOOR_VISIBLE" == propertyName 2536 || "CEILING_VISIBLE" == propertyName 2537 || "LEVEL" == propertyName) { 2538 component3D.updateObjects(component3D.home.getRooms()); 2539 component3D.groundChangeListener(null); 2540 } else if ("POINTS" == propertyName) { 2541 if (component3D.homeObjectsToUpdate) { 2542 // Don't try to optimize if more than one room to update 2543 component3D.updateObjects(component3D.home.getRooms()); 2544 } else { 2545 component3D.updateObjects([updatedRoom]); 2546 // Search the rooms that overlap the updated one 2547 var oldArea = new java.awt.geom.Area(component3D.getShape(ev.getOldValue())); 2548 var newArea = new java.awt.geom.Area(component3D.getShape(ev.getNewValue())); 2549 var updatedRoomLevel = updatedRoom.getLevel(); 2550 var rooms = component3D.home.getRooms(); 2551 for (var i = 0; i < rooms.length; i++) { 2552 var room = rooms[i]; 2553 var roomLevel = room.getLevel(); 2554 if (room != updatedRoom 2555 && (roomLevel == null 2556 || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() + roomLevel.getHeight())) < 1E-5 2557 || Math.abs(updatedRoomLevel.getElevation() + updatedRoomLevel.getHeight() - (roomLevel.getElevation() - roomLevel.getFloorThickness())) < 1E-5)) { 2558 var roomAreaIntersectionWithOldArea = new java.awt.geom.Area(component3D.getShape(room.getPoints())); 2559 var roomAreaIntersectionWithNewArea = new java.awt.geom.Area(roomAreaIntersectionWithOldArea); 2560 roomAreaIntersectionWithNewArea.intersect(newArea); 2561 if (!roomAreaIntersectionWithNewArea.isEmpty()) { 2562 component3D.updateObjects([room]); 2563 } else { 2564 roomAreaIntersectionWithOldArea.intersect(oldArea); 2565 if (!roomAreaIntersectionWithOldArea.isEmpty()) { 2566 component3D.updateObjects([room]); 2567 } 2568 } 2569 } 2570 } 2571 } 2572 component3D.groundChangeListener(null); 2573 } 2574 }; 2575 var rooms = this.home.getRooms(); 2576 for (var i = 0; i < rooms.length; i++) { 2577 rooms[i].addPropertyChangeListener(this.roomChangeListener); 2578 } 2579 this.roomListener = function(ev) { 2580 var room = ev.getItem(); 2581 switch (ev.getType()) { 2582 case CollectionEvent.Type.ADD : 2583 component3D.addObject(group, room, ev.getIndex(), true, false); 2584 room.addPropertyChangeListener(component3D.roomChangeListener); 2585 break; 2586 case CollectionEvent.Type.DELETE : 2587 component3D.deleteObject(room); 2588 room.removePropertyChangeListener(component3D.roomChangeListener); 2589 break; 2590 } 2591 component3D.updateObjects(component3D.home.getRooms()); 2592 component3D.groundChangeListener(null); 2593 }; 2594 this.home.addRoomsListener(this.roomListener); 2595 } 2596 2597 /** 2598 * Returns the path matching points. 2599 * @param {Array} points 2600 * @return {GeneralPath} 2601 * @private 2602 */ 2603 HomeComponent3D.prototype.getShape = function(points) { 2604 var path = new java.awt.geom.GeneralPath(); 2605 path.moveTo(points[0][0], points[0][1]); 2606 for (var i = 1; i < points.length; i++) { 2607 path.lineTo(points[i][0], points[i][1]); 2608 } 2609 path.closePath(); 2610 return path; 2611 } 2612 2613 /** 2614 * Adds a polyline listener to home polylines that updates the children of the given 2615 * <code>group</code>, each time a polyline is added, updated or deleted. 2616 * @param {Group} group 2617 * @private 2618 */ 2619 HomeComponent3D.prototype.addPolylineListener = function(group) { 2620 var component3D = this; 2621 this.polylineChangeListener = function(ev) { 2622 var polyline = ev.getSource(); 2623 component3D.updateObjects([polyline]); 2624 }; 2625 var polylines = this.home.getPolylines(); 2626 for (var i = 0; i < polylines.length; i++) { 2627 polylines[i].addPropertyChangeListener(this.polylineChangeListener); 2628 } 2629 this.polylineListener = function(ev) { 2630 var polyline = ev.getItem(); 2631 switch (ev.getType()) { 2632 case CollectionEvent.Type.ADD : 2633 component3D.addObject(group, polyline, true, false); 2634 polyline.addPropertyChangeListener(component3D.polylineChangeListener); 2635 break; 2636 case CollectionEvent.Type.DELETE : 2637 component3D.deleteObject(polyline); 2638 polyline.removePropertyChangeListener(component3D.polylineChangeListener); 2639 break; 2640 } 2641 }; 2642 this.home.addPolylinesListener(this.polylineListener); 2643 } 2644 2645 /** 2646 * Adds a dimension line listener to home dimension lines that updates the children of the given 2647 * <code>group</code>, each time a dimension line is added, updated or deleted. 2648 * @param {Group3D} group 2649 * @private 2650 */ 2651 HomeComponent3D.prototype.addDimensionLineListener = function(group) { 2652 var component3D = this; 2653 this.dimensionLineChangeListener = function(ev) { 2654 var updatedDimensionLine = ev.getSource(); 2655 component3D.updateObjects([updatedDimensionLine]); 2656 }; 2657 var dimensionLines = this.home.getDimensionLines(); 2658 for (var i = 0; i < dimensionLines.length; i++) { 2659 dimensionLines [i].addPropertyChangeListener(this.dimensionLineChangeListener); 2660 } 2661 this.dimensionLineListener = function(ev) { 2662 var dimensionLine = ev.getItem(); 2663 switch (ev.getType()) { 2664 case CollectionEvent.Type.ADD : 2665 component3D.addObject(group, dimensionLine, true, false); 2666 dimensionLine.addPropertyChangeListener(component3D.dimensionLineChangeListener); 2667 break; 2668 case CollectionEvent.Type.DELETE : 2669 component3D.deleteObject(dimensionLine); 2670 dimensionLine.removePropertyChangeListener(component3D.dimensionLineChangeListener); 2671 break; 2672 } 2673 }; 2674 this.home.addDimensionLinesListener(this.dimensionLineListener); 2675 } 2676 2677 /** 2678 * Adds a label listener to home labels that updates the children of the given 2679 * <code>group</code>, each time a label is added, updated or deleted. 2680 * @param {Group3D} group 2681 * @private 2682 */ 2683 HomeComponent3D.prototype.addLabelListener = function(group) { 2684 var component3D = this; 2685 this.labelChangeListener = function(ev) { 2686 var label = ev.getSource(); 2687 component3D.updateObjects([label]); 2688 }; 2689 var labels = this.home.getLabels(); 2690 for (var i = 0; i < labels.length; i++) { 2691 labels[i].addPropertyChangeListener(this.labelChangeListener); 2692 } 2693 this.labelListener = function(ev) { 2694 var label = ev.getItem(); 2695 switch (ev.getType()) { 2696 case CollectionEvent.Type.ADD : 2697 component3D.addObject(group, label, true, false); 2698 label.addPropertyChangeListener(component3D.labelChangeListener); 2699 break; 2700 case CollectionEvent.Type.DELETE : 2701 component3D.deleteObject(label); 2702 label.removePropertyChangeListener(component3D.labelChangeListener); 2703 break; 2704 } 2705 }; 2706 this.home.addLabelsListener(this.labelListener); 2707 } 2708 2709 /** 2710 * Adds a walls alpha change listener and drawing mode change listener to home 2711 * environment that updates the home scene objects appearance. 2712 * @private 2713 */ 2714 HomeComponent3D.prototype.addEnvironmentListeners = function() { 2715 var component3D = this; 2716 this.wallsAlphaListener = function(ev) { 2717 component3D.updateObjects(component3D.home.getWalls()); 2718 component3D.updateObjects(component3D.home.getRooms()); 2719 }; 2720 this.home.getEnvironment().addPropertyChangeListener("WALLS_ALPHA", this.wallsAlphaListener); 2721 } 2722 2723 /** 2724 * Adds to <code>group</code> a branch matching <code>homeObject</code> at a given <code>index</code>. 2725 * If <code>index</code> is missing or equal to -1, <code>homeObject</code> will be added at the end of the group. 2726 * @param {Group3D} group 2727 * @param {Object} homeObject 2728 * @param {number} [index] 2729 * @param {boolean} listenToHomeUpdates 2730 * @param {boolean} waitForLoading 2731 * @private 2732 */ 2733 HomeComponent3D.prototype.addObject = function(group, homeObject, index, 2734 listenToHomeUpdates, waitForLoading) { 2735 if (waitForLoading === undefined) { 2736 waitForLoading = listenToHomeUpdates; 2737 listenToHomeUpdates = index; 2738 index = -1; 2739 } 2740 var object3D = this.object3dFactory.createObject3D(this.home, homeObject, waitForLoading); 2741 if (listenToHomeUpdates) { 2742 homeObject.object3D = object3D; 2743 this.homeObjects.push(homeObject); 2744 this.homeObjects3D.push(object3D); 2745 } 2746 if (index === -1) { 2747 group.addChild(object3D); 2748 } else { 2749 group.insertChild(object3D, index); 2750 } 2751 return object3D; 2752 } 2753 2754 /** 2755 * Adds to <code>group</code> a branch matching <code>homeObject</code> or its children if the piece is a group of furniture. 2756 * @param {Group3D} group 2757 * @param {HomePieceOfFurniture} piece 2758 * @param {boolean} listenToHomeUpdates 2759 * @param {boolean} waitForLoading 2760 * @private 2761 */ 2762 HomeComponent3D.prototype.addPieceOfFurniture = function(group, piece, listenToHomeUpdates, waitForLoading) { 2763 if (piece instanceof HomeFurnitureGroup) { 2764 var furniture = piece.getFurniture(); 2765 for (var i = 0; i < furniture.length; i++) { 2766 this.addPieceOfFurniture(group, furniture [i], listenToHomeUpdates, waitForLoading); 2767 } 2768 } else { 2769 this.addObject(group, piece, listenToHomeUpdates, waitForLoading); 2770 } 2771 } 2772 2773 /** 2774 * Detaches from the scene the branch matching <code>homeObject</code>. 2775 * @param {Object} homeObject 2776 * @private 2777 */ 2778 HomeComponent3D.prototype.deleteObject = function(homeObject) { 2779 if (homeObject.object3D) { 2780 homeObject.object3D.detach(); 2781 delete homeObject.object3D; 2782 var objectIndex = this.homeObjects.indexOf(homeObject); 2783 this.homeObjects.splice(objectIndex, 1); 2784 this.homeObjects3D.splice(objectIndex, 1); 2785 if (this.homeObjectsToUpdate) { 2786 var index = this.homeObjectsToUpdate.indexOf(homeObject); 2787 if (index >= 0) { 2788 this.homeObjectsToUpdate.splice(index, 1); 2789 } 2790 } 2791 } 2792 } 2793 2794 /** 2795 * Detaches from the scene the branches matching <code>piece</code> or its children if it's a group. 2796 * @param {HomePieceOfFurniture} piece 2797 * @private 2798 */ 2799 HomeComponent3D.prototype.deletePieceOfFurniture = function(piece) { 2800 if (piece instanceof HomeFurnitureGroup) { 2801 var furniture = piece.getFurniture(); 2802 for (var i = 0; i < furniture.length; i++) { 2803 this.deletePieceOfFurniture(furniture [i]); 2804 } 2805 } else { 2806 this.deleteObject(piece); 2807 } 2808 } 2809 2810 /** 2811 * Updates 3D <code>objects</code> later. 2812 * @param {Array} objects 2813 * @private 2814 */ 2815 HomeComponent3D.prototype.updateObjects = function(objects) { 2816 if (this.homeObjectsToUpdate) { 2817 for (var i = 0; i < objects.length; i++) { 2818 var object = objects [i]; 2819 if (this.homeObjectsToUpdate.indexOf(object) <= -1) { 2820 this.homeObjectsToUpdate.push(object); 2821 } 2822 } 2823 } else { 2824 this.homeObjectsToUpdate = objects.slice(0); 2825 // Invoke later the update of objects of homeObjectsToUpdate 2826 setTimeout( 2827 function(component3D) { 2828 for (var i = 0; i < component3D.homeObjectsToUpdate.length; i++) { 2829 var homeObject = component3D.homeObjectsToUpdate [i]; 2830 // Check object wasn't deleted since updateObjects call 2831 if (homeObject.object3D) { 2832 homeObject.object3D.update(); 2833 } 2834 } 2835 delete component3D.homeObjectsToUpdate; 2836 }, 0, this); 2837 } 2838 this.approximateHomeBoundsCache = null; 2839 this.homeHeightCache = null; 2840 } 2841 2842 /** 2843 * Updates 3D objects and furniture groups children, if <code>objects</code> contains some groups. 2844 * @param {Array} objects 2845 * @private 2846 */ 2847 HomeComponent3D.prototype.updateObjectsAndFurnitureGroups = function(objects) { 2848 this.updateObjects(objects); 2849 for (var i = 0; i < objects.length; i++) { 2850 var item = objects [i]; 2851 if (item instanceof HomeFurnitureGroup) { 2852 this.updateObjects(item.getAllFurniture()); 2853 } 2854 } 2855 } 2856 2857 /** 2858 * Updates walls that may intersect from the given doors or window. 2859 * @param {Array} doorOrWindows 2860 * @private 2861 */ 2862 HomeComponent3D.prototype.updateIntersectingWalls = function(doorOrWindows) { 2863 var walls = this.home.getWalls(); 2864 var wallCount = 0; 2865 if (this.homeObjectsToUpdate) { 2866 for (var i = 0; i < this.homeObjectsToUpdate.length; i++) { 2867 if (this.homeObjectsToUpdate [i] instanceof Wall) { 2868 wallCount++; 2869 } 2870 } 2871 } 2872 2873 if (wallCount !== walls.length) { 2874 var updatedWalls = []; 2875 var doorOrWindowBounds = null; 2876 for (var i = 0; i < doorOrWindows.length; i++) { 2877 var doorOrWindow = doorOrWindows [i]; 2878 var points = doorOrWindow.getPoints(); 2879 if (doorOrWindowBounds === null) { 2880 doorOrWindowBounds = new java.awt.geom.Rectangle2D.Float(points [0][0], points [0][1], 0, 0); 2881 } else { 2882 doorOrWindowBounds.add(points [0][0], points [0][1]); 2883 } 2884 for (var j = 1; j < points.length; j++) { 2885 doorOrWindowBounds.add(points [j][0], points [j][1]); 2886 } 2887 } 2888 // Search walls that intersect approximative bounds 2889 for (var i = 0; i < walls.length; i++) { 2890 var wall = walls [i]; 2891 if (wall.intersectsRectangle(doorOrWindowBounds.getX(), doorOrWindowBounds.getY(), 2892 doorOrWindowBounds.getX() + doorOrWindowBounds.getWidth(), 2893 doorOrWindowBounds.getY() + doorOrWindowBounds.getHeight())) { 2894 updatedWalls.push(wall); 2895 } 2896 } 2897 this.updateObjects(updatedWalls); 2898 } 2899 } 2900 2901 /** 2902 * Updates <code>wall</code> geometry, 2903 * and the walls at its end or start. 2904 * @param {Wall} wall 2905 * @private 2906 */ 2907 HomeComponent3D.prototype.updateWall = function(wall) { 2908 var wallsToUpdate = []; 2909 wallsToUpdate.push(wall); 2910 if (wall.getWallAtStart() != null) { 2911 wallsToUpdate.push(wall.getWallAtStart()); 2912 } 2913 if (wall.getWallAtEnd() != null) { 2914 wallsToUpdate.push(wall.getWallAtEnd()); 2915 } 2916 this.updateObjects(wallsToUpdate); 2917 } 2918 2919 2920 /** 2921 * A factory able to create instances of {@link Object3DBranch} class. 2922 * @param {UserPreferences} [preferences] 2923 * @constructor 2924 * @author Emmanuel Puybaret 2925 */ 2926 function Object3DBranchFactory(preferences) { 2927 if (preferences !== undefined) { 2928 this.preferences = preferences; 2929 } 2930 } 2931 2932 /** 2933 * Returns the 3D object matching a given <code>item</code>. 2934 * @param {Home} home 2935 * @param {Object} item 2936 * @param {boolean|function} waitForLoading 2937 * @return {Object3DBranch} an instance of a subclass of {@link Object3DBranch} 2938 */ 2939 Object3DBranchFactory.prototype.createObject3D = function(home, item, waitForLoading) { 2940 if (item instanceof HomePieceOfFurniture) { 2941 return new HomePieceOfFurniture3D(item, home, this.preferences, waitForLoading); 2942 } else if (item instanceof Wall) { 2943 return new Wall3D(item, home, this.preferences, waitForLoading); 2944 } else if (item instanceof Room) { 2945 return new Room3D(item, home, this.preferences, false, waitForLoading); 2946 } else if (item instanceof Polyline) { 2947 return new Polyline3D(item, home, this.preferences, waitForLoading); 2948 } else if (item instanceof DimensionLine) { 2949 return new DimensionLine3D(item, home, this.preferences, waitForLoading); 2950 } else if (item instanceof Label) { 2951 return new Label3D(item, home, this.preferences, waitForLoading); 2952 } else { 2953 return new Group3D(); 2954 } 2955 } 2956