1 /* 2 * Ground3D.js 3 * 4 * Sweet Home 3D, Copyright (c) 2017 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 scene3d.js 22 // Object3DBranch.js 23 // TextureManager.js 24 25 26 /** 27 * Creates a 3D ground for the given <code>home</code>. 28 * @param {Home} home 29 * @param {number} originX 30 * @param {number} originY 31 * @param {number} width 32 * @param {number} depth 33 * @param {boolean} waitTextureLoadingEnd 34 * @constructor 35 * @extends Object3DBranch 36 * @author Emmanuel Puybaret 37 */ 38 function Ground3D(home, originX, originY, width, depth, waitTextureLoadingEnd) { 39 Object3DBranch.call(this); 40 this.setUserData(home); 41 this.originX = originX; 42 this.originY = originY; 43 this.width = width; 44 this.depth = depth; 45 46 var groundAppearance = new Appearance3D(); 47 var groundShape = new Shape3D(); 48 groundShape.setCapability(Shape3D.ALLOW_GEOMETRY_WRITE); 49 groundShape.setAppearance(groundAppearance); 50 51 this.addChild(groundShape); 52 53 var backgroundImageAppearance = new Appearance3D(); 54 this.updateAppearanceMaterial(backgroundImageAppearance, Object3DBranch.DEFAULT_COLOR, Object3DBranch.DEFAULT_COLOR, 0); 55 backgroundImageAppearance.setCullFace(Appearance3D.CULL_NONE); 56 57 var transformGroup = new TransformGroup3D(); 58 // Allow the change of the transformation that sets background image size and position 59 transformGroup.setCapability(TransformGroup3D.ALLOW_TRANSFORM_WRITE); 60 var backgroundImageShape = new Shape3D( 61 new IndexedTriangleArray3D( 62 [vec3.fromValues(-0.5, 0, -0.5), 63 vec3.fromValues(-0.5, 0, 0.5), 64 vec3.fromValues(0.5, 0, 0.5), 65 vec3.fromValues(0.5, 0, -0.5)], 66 [0, 1, 2, 0, 2, 3], 67 [vec2.fromValues(0., 0.), 68 vec2.fromValues(1., 0.), 69 vec2.fromValues(1., 1.), 70 vec2.fromValues(0., 1.)], 71 [3, 0, 1, 3, 1, 2], 72 [vec3.fromValues(0., 1., 0.)], 73 [0, 0, 0, 0, 0, 0]), backgroundImageAppearance); 74 transformGroup.addChild(backgroundImageShape); 75 this.addChild(transformGroup); 76 77 this.update(waitTextureLoadingEnd); 78 } 79 Ground3D.prototype = Object.create(Object3DBranch.prototype); 80 Ground3D.prototype.constructor = Ground3D; 81 82 /** 83 * Updates the geometry and attributes of ground and sublevels. 84 * @param {boolean} [waitTextureLoadingEnd] 85 */ 86 Ground3D.prototype.update = function(waitTextureLoadingEnd) { 87 if (waitTextureLoadingEnd === undefined) { 88 waitTextureLoadingEnd = false; 89 } 90 var home = this.getUserData(); 91 92 // Update background image viewed on ground 93 var backgroundImageGroup = this.getChild(1); 94 var backgroundImageShape = backgroundImageGroup.getChild(0); 95 var backgroundImageAppearance = backgroundImageShape.getAppearance(); 96 var backgroundImage = null; 97 if (home.getEnvironment().isBackgroundImageVisibleOnGround3D()) { 98 var levels = home.getLevels(); 99 if (levels.length > 0) { 100 for (var i = levels.length - 1; i >= 0; i--) { 101 var level = levels [i]; 102 if (level.getElevation() == 0 103 && level.isViewableAndVisible() 104 && level.getBackgroundImage() !== null 105 && level.getBackgroundImage().isVisible()) { 106 backgroundImage = level.getBackgroundImage(); 107 break; 108 } 109 } 110 } else if (home.getBackgroundImage() !== null 111 && home.getBackgroundImage().isVisible()) { 112 backgroundImage = home.getBackgroundImage(); 113 } 114 } 115 if (backgroundImage !== null) { 116 var ground3d = this; 117 TextureManager.getInstance().loadTexture(backgroundImage.getImage(), waitTextureLoadingEnd, { 118 textureUpdated : function(texture) { 119 // Update image location and size 120 var backgroundImageScale = backgroundImage.getScale(); 121 var imageWidth = backgroundImageScale * texture.width; 122 var imageHeight = backgroundImageScale * texture.height; 123 var backgroundImageTransform = mat4.create(); 124 mat4.scale(backgroundImageTransform, backgroundImageTransform, vec3.fromValues(imageWidth, 1, imageHeight)); 125 var backgroundImageTranslation = mat4.create(); 126 mat4.fromTranslation(backgroundImageTranslation, vec3.fromValues(imageWidth / 2 - backgroundImage.getXOrigin(), 0., 127 imageHeight / 2 - backgroundImage.getYOrigin())); 128 mat4.mul(backgroundImageTransform, backgroundImageTranslation, backgroundImageTransform); 129 backgroundImageAppearance.setTextureImage(texture); 130 backgroundImageGroup.setTransform(backgroundImageTransform); 131 ground3d.updateGround(waitTextureLoadingEnd, 132 new java.awt.geom.Rectangle2D.Float(-backgroundImage.getXOrigin(), -backgroundImage.getYOrigin(), imageWidth, imageHeight)); 133 }, 134 textureError : function(error) { 135 return this.textureUpdated(TextureManager.getInstance().getErrorImage()); 136 }, 137 progression : function(part, info, percentage) { 138 } 139 }); 140 backgroundImageAppearance.setVisible(true); 141 } else { 142 backgroundImageAppearance.setVisible(false); 143 this.updateGround(waitTextureLoadingEnd, null); 144 } 145 } 146 147 /** 148 * @param {boolean} waitTextureLoadingEnd 149 * @param {java.awt.geom.Rectangle2D} backgroundImageRectangle 150 * @private 151 */ 152 Ground3D.prototype.updateGround = function(waitTextureLoadingEnd, backgroundImageRectangle) { 153 var home = this.getUserData(); 154 var groundShape = this.getChild(0); 155 var currentGeometriesCount = groundShape.getGeometries().length; 156 var groundAppearance = groundShape.getAppearance(); 157 var groundTexture = home.getEnvironment().getGroundTexture(); 158 if (groundTexture === null) { 159 var groundColor = home.getEnvironment().getGroundColor(); 160 this.updateAppearanceMaterial(groundAppearance, groundColor, groundColor, 0); 161 groundAppearance.setTextureImage(null); 162 } else { 163 this.updateAppearanceMaterial(groundAppearance, Object3DBranch.DEFAULT_COLOR, Object3DBranch.DEFAULT_COLOR, 0); 164 this.updateTextureTransform(groundAppearance, groundTexture, true); 165 TextureManager.getInstance().loadTexture(groundTexture.getImage(), waitTextureLoadingEnd, { 166 textureUpdated : function(texture) { 167 groundAppearance.setTextureImage(texture); 168 }, 169 textureError : function(error) { 170 return this.textureUpdated(TextureManager.getInstance().getErrorImage()); 171 } 172 }); 173 } 174 175 var areaRemovedFromGround = new java.awt.geom.Area(); 176 if (backgroundImageRectangle !== null) { 177 areaRemovedFromGround.add(new java.awt.geom.Area(backgroundImageRectangle)); 178 } 179 var undergroundLevelAreas = []; 180 var rooms = home.getRooms(); 181 for (var i = 0; i < rooms.length; i++) { 182 var room = rooms[i]; 183 var roomLevel = room.getLevel(); 184 if ((roomLevel === null || roomLevel.isViewable()) 185 && room.isFloorVisible()) { 186 var roomPoints = room.getPoints(); 187 if (roomPoints.length > 2) { 188 var roomArea = new java.awt.geom.Area(this.getShape(roomPoints)); 189 var levelAreas = roomLevel !== null && roomLevel.getElevation() < 0 190 ? this.getUndergroundAreas(undergroundLevelAreas, roomLevel) 191 : null; 192 if (roomLevel === null 193 || (roomLevel.getElevation() <= 0 194 && roomLevel.isViewableAndVisible())) { 195 areaRemovedFromGround.add(roomArea); 196 if (levelAreas !== null) { 197 levelAreas.roomArea.add(roomArea); 198 } 199 } 200 if (levelAreas !== null) { 201 levelAreas.undergroundArea.add(roomArea); 202 } 203 } 204 } 205 } 206 207 this.updateUndergroundAreasDugByFurniture(undergroundLevelAreas, home.getFurniture()); 208 209 var walls = home.getWalls(); 210 for (var i = 0; i < walls.length; i++) { 211 var wall = walls[i]; 212 var wallLevel = wall.getLevel(); 213 if (wallLevel !== null 214 && wallLevel.isViewable() 215 && wallLevel.getElevation() < 0) { 216 var levelAreas = this.getUndergroundAreas(undergroundLevelAreas, wallLevel); 217 levelAreas.wallArea.add(new java.awt.geom.Area(this.getShape(wall.getPoints()))); 218 } 219 } 220 var undergroundAreas = undergroundLevelAreas; 221 for (var i = 0; i < undergroundAreas.length; i++) { 222 var levelAreas = undergroundAreas[i]; 223 var areaPoints = this.getPoints(levelAreas.wallArea); 224 for (var j = 0; j < areaPoints.length; j++) { 225 var points = areaPoints[j]; 226 if (!new Room(points).isClockwise()) { 227 levelAreas.undergroundArea.add(new java.awt.geom.Area(this.getShape(points))); 228 } 229 } 230 } 231 232 undergroundAreas.sort(function (levelAreas1, levelAreas2) { 233 return -(levelAreas1.level.getElevation() - levelAreas2.level.getElevation()); 234 }); 235 for (var i = 0; i < undergroundAreas.length; i++) { 236 var levelAreas = undergroundAreas[i]; 237 var level = levelAreas.level; 238 var area = levelAreas.undergroundArea; 239 var areaAtStart = area.clone(); 240 levelAreas.undergroundSideArea.add(area.clone()); 241 for (var j = 0; j < undergroundAreas.length; j++) { 242 var otherLevelAreas = undergroundAreas[j]; 243 if (otherLevelAreas.level.getElevation() < level.getElevation()) { 244 var areaPoints = this.getPoints(otherLevelAreas.undergroundArea); 245 for (var k = 0; k < areaPoints.length; k++) { 246 var points = areaPoints[k]; 247 if (!new Room(points).isClockwise()) { 248 var pointsArea = new java.awt.geom.Area(this.getShape(points)); 249 area.subtract(pointsArea); 250 levelAreas.undergroundSideArea.add(pointsArea); 251 } 252 } 253 } 254 } 255 var areaPoints = this.getPoints(area); 256 for (var j = 0; j < areaPoints.length; j++) { 257 var points = areaPoints[j]; 258 if (new Room(points).isClockwise()) { 259 var coveredHole = new java.awt.geom.Area(this.getShape(points)); 260 coveredHole.exclusiveOr(areaAtStart); 261 coveredHole.subtract(areaAtStart); 262 levelAreas.upperLevelArea.add(coveredHole); 263 } else { 264 areaRemovedFromGround.add(new java.awt.geom.Area(this.getShape(points))); 265 } 266 } 267 } 268 for (var i = 0; i < undergroundAreas.length; i++) { 269 var levelAreas = undergroundAreas[i]; 270 var roomArea = levelAreas.roomArea; 271 if (roomArea !== null) { 272 levelAreas.undergroundArea.subtract(roomArea); 273 } 274 } 275 276 var groundArea = new java.awt.geom.Area(this.getShape( 277 [[this.originX, this.originY], 278 [this.originX, this.originY + this.depth], 279 [this.originX + this.width, this.originY + this.depth], 280 [this.originX + this.width, this.originY]])); 281 var removedAreaBounds = areaRemovedFromGround.getBounds2D(); 282 if (!groundArea.getBounds2D().equals(removedAreaBounds)) { 283 var outsideGroundArea = groundArea; 284 if (areaRemovedFromGround.isEmpty()) { 285 removedAreaBounds = new java.awt.geom.Rectangle2D.Float(Math.max(-5000.0, this.originX), Math.max(-5000.0, this.originY), 0, 0); 286 removedAreaBounds.add(Math.min(5000.0, this.originX + this.width), 287 Math.min(5000.0, this.originY + this.depth)); 288 } else { 289 removedAreaBounds.add(Math.max(removedAreaBounds.getMinX() - 5000.0, this.originX), 290 Math.max(removedAreaBounds.getMinY() - 5000.0, this.originY)); 291 removedAreaBounds.add(Math.min(removedAreaBounds.getMaxX() + 5000.0, this.originX + this.width), 292 Math.min(removedAreaBounds.getMaxY() + 5000.0, this.originY + this.depth)); 293 } 294 groundArea = new java.awt.geom.Area(removedAreaBounds); 295 outsideGroundArea.subtract(groundArea); 296 this.addAreaGeometry(groundShape, groundTexture, outsideGroundArea, 0); 297 } 298 groundArea.subtract(areaRemovedFromGround); 299 300 undergroundAreas.splice(0, 0, new Ground3D.LevelAreas(new Level("Ground", 0, 0, 0), groundArea)); 301 var previousLevelElevation = 0; 302 for (var i = 0; i < undergroundAreas.length; i++) { 303 var levelAreas = undergroundAreas[i]; 304 var elevation = levelAreas.level.getElevation(); 305 this.addAreaGeometry(groundShape, groundTexture, levelAreas.undergroundArea, elevation); 306 if (previousLevelElevation - elevation > 0) { 307 var areaPoints = this.getPoints(levelAreas.undergroundSideArea); 308 for (var j = 0; j < areaPoints.length; j++) { 309 var points = areaPoints[j]; 310 this.addAreaSidesGeometry(groundShape, groundTexture, points, elevation, previousLevelElevation - elevation); 311 } 312 this.addAreaGeometry(groundShape, groundTexture, levelAreas.upperLevelArea, previousLevelElevation); 313 } 314 previousLevelElevation = elevation; 315 } 316 317 for (var i = currentGeometriesCount - 1; i >= 0; i--) { 318 groundShape.removeGeometry(i); 319 } 320 } 321 322 /** 323 * Returns the list of points that defines the given area. 324 * @param {Area} area 325 * @return {Array} 326 * @private 327 */ 328 Ground3D.prototype.getPoints = function(area) { 329 var areaPoints = []; 330 var areaPartPoints = []; 331 var previousRoomPoint = null; 332 for (var it = area.getPathIterator(null, 1); !it.isDone(); it.next()) { 333 var roomPoint = [0, 0]; 334 if (it.currentSegment(roomPoint) === java.awt.geom.PathIterator.SEG_CLOSE) { 335 if (areaPartPoints[0][0] === previousRoomPoint[0] 336 && areaPartPoints[0][1] === previousRoomPoint[1]) { 337 areaPartPoints.splice(areaPartPoints.length - 1, 1); 338 } 339 if (areaPartPoints.length > 2) { 340 areaPoints.push(areaPartPoints.slice(0)); 341 } 342 areaPartPoints.length = 0; 343 previousRoomPoint = null; 344 } else { 345 if (previousRoomPoint === null 346 || roomPoint[0] !== previousRoomPoint[0] 347 || roomPoint[1] !== previousRoomPoint[1]) { 348 areaPartPoints.push(roomPoint); 349 } 350 previousRoomPoint = roomPoint; 351 } 352 } 353 return areaPoints; 354 } 355 356 /** 357 * Returns the {@link LevelAreas} instance matching the given level. 358 * @param {Object} undergroundAreas 359 * @param {Level} level 360 * @return {Ground3D.LevelAreas} 361 * @private 362 */ 363 Ground3D.prototype.getUndergroundAreas = function(undergroundAreas, level) { 364 var levelAreas = null; 365 for (var i = 0; i < undergroundAreas.length; i++) { 366 if (undergroundAreas[i].level === level) { 367 levelAreas = undergroundAreas[i]; 368 break; 369 } 370 } 371 if (levelAreas === null) { 372 undergroundAreas.push(levelAreas = new Ground3D.LevelAreas(level)); 373 } 374 return levelAreas; 375 }; 376 377 378 /** 379 * Updates underground level areas dug by the visible furniture placed at underground levels. 380 * @param {Object} undergroundLevelAreas 381 * @param {Level} level 382 * @return {Array} furniture 383 * @private 384 */ 385 Ground3D.prototype.updateUndergroundAreasDugByFurniture = function(undergroundLevelAreas, furniture) { 386 for (var i = 0; i < furniture.length; i++) { 387 var piece = furniture[i]; 388 var pieceLevel = piece.getLevel(); 389 if (piece.getGroundElevation() < 0 390 && piece.isVisible() 391 && pieceLevel !== null 392 && pieceLevel.isViewable() 393 && pieceLevel.getElevation() < 0) { 394 if (piece instanceof HomeFurnitureGroup) { 395 this.updateUndergroundAreasDugByFurniture(undergroundLevelAreas, piece.getFurniture()); 396 } else { 397 var levelAreas = this.getUndergroundAreas(undergroundLevelAreas, pieceLevel); 398 if (piece.getStaircaseCutOutShape() === null) { 399 levelAreas.undergroundArea.add(new java.awt.geom.Area(this.getShape(piece.getPoints()))); 400 } else { 401 levelAreas.undergroundArea.add(ModelManager.getInstance().getAreaOnFloor(piece)); 402 } 403 } 404 } 405 } 406 } 407 408 /** 409 * Adds to ground shape the geometry matching the given area. 410 * @param {Shape3D} groundShape 411 * @param {HomeTexture} groundTexture 412 * @param {Area} area 413 * @param {number} elevation 414 * @private 415 */ 416 Ground3D.prototype.addAreaGeometry = function(groundShape, groundTexture, area, elevation) { 417 var areaPoints = this.getAreaPoints(area, 1, false); 418 if (areaPoints.length !== 0) { 419 var vertexCount = 0; 420 var stripCounts = new Array(areaPoints.length); 421 for (var i = 0; i < stripCounts.length; i++) { 422 stripCounts[i] = areaPoints[i].length; 423 vertexCount += stripCounts[i]; 424 } 425 var geometryCoords = new Array(vertexCount); 426 var geometryTextureCoords = groundTexture !== null 427 ? new Array(vertexCount) 428 : null; 429 430 var j = 0; 431 for (var index = 0; index < areaPoints.length; index++) { 432 var areaPartPoints = areaPoints[index]; 433 for (var i = 0; i < areaPartPoints.length; i++, j++) { 434 var point = areaPartPoints[i]; 435 geometryCoords[j] = vec3.fromValues(point[0], elevation, point[1]); 436 if (groundTexture !== null) { 437 geometryTextureCoords[j] = vec2.fromValues(point[0] - this.originX, this.originY - point[1]); 438 } 439 } 440 } 441 442 var geometryInfo = new GeometryInfo3D(GeometryInfo3D.POLYGON_ARRAY); 443 geometryInfo.setCoordinates(geometryCoords); 444 if (groundTexture !== null) { 445 geometryInfo.setTextureCoordinates(geometryTextureCoords); 446 } 447 geometryInfo.setStripCounts(stripCounts); 448 geometryInfo.setCreaseAngle(0); 449 geometryInfo.setGeneratedNormals(true); 450 groundShape.addGeometry(geometryInfo.getIndexedGeometryArray()); 451 } 452 } 453 454 /** 455 * Adds to ground shape the geometry matching the given area sides. 456 * @param {Shape3D} groundShape 457 * @param {HomeTexture} groundTexture 458 * @param {Array} areaPoints 459 * @param {number} elevation 460 * @param {number} sideHeight 461 * @private 462 */ 463 Ground3D.prototype.addAreaSidesGeometry = function(groundShape, groundTexture, areaPoints, elevation, sideHeight) { 464 var geometryCoords = new Array(areaPoints.length * 4); 465 var geometryTextureCoords = groundTexture !== null 466 ? new Array(geometryCoords.length) 467 : null; 468 for (var i = 0, j = 0; i < areaPoints.length; i++) { 469 var point = areaPoints[i]; 470 var nextPoint = areaPoints[i < areaPoints.length - 1 ? i + 1 : 0]; 471 geometryCoords[j++] = vec3.fromValues(point[0], elevation, point[1]); 472 geometryCoords[j++] = vec3.fromValues(point[0], elevation + sideHeight, point[1]); 473 geometryCoords[j++] = vec3.fromValues(nextPoint[0], elevation + sideHeight, nextPoint[1]); 474 geometryCoords[j++] = vec3.fromValues(nextPoint[0], elevation, nextPoint[1]); 475 if (groundTexture !== null) { 476 var distance = java.awt.geom.Point2D.distance(point[0], point[1], nextPoint[0], nextPoint[1]); 477 geometryTextureCoords[j - 4] = vec2.fromValues(point[0], elevation); 478 geometryTextureCoords[j - 3] = vec2.fromValues(point[0], elevation + sideHeight); 479 geometryTextureCoords[j - 2] = vec2.fromValues(point[0] - distance, elevation + sideHeight); 480 geometryTextureCoords[j - 1] = vec2.fromValues(point[0] - distance, elevation); 481 } 482 } 483 484 var geometryInfo = new GeometryInfo3D(GeometryInfo3D.QUAD_ARRAY); 485 geometryInfo.setCoordinates(geometryCoords); 486 if (groundTexture !== null) { 487 geometryInfo.setTextureCoordinates(geometryTextureCoords); 488 } 489 geometryInfo.setCreaseAngle(0); 490 geometryInfo.setGeneratedNormals(true); 491 groundShape.addGeometry(geometryInfo.getIndexedGeometryArray()); 492 } 493 494 /** 495 * Areas of underground levels. 496 * @constructor 497 * @private 498 */ 499 Ground3D.LevelAreas = function(level, undergroundArea) { 500 if (undergroundArea === undefined) { 501 undergroundArea = new java.awt.geom.Area(); 502 } 503 this.level = level; 504 this.undergroundArea = undergroundArea; 505 this.roomArea = new java.awt.geom.Area(); 506 this.wallArea = new java.awt.geom.Area(); 507 this.undergroundSideArea = new java.awt.geom.Area(); 508 this.upperLevelArea = new java.awt.geom.Area(); 509 }