1 /* 2 * Ground3D.js 3 * 4 * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <info@sweethome3d.com> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 */ 20 21 // Requires 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 var elevationComparison = -(levelAreas1.level.getElevation() - levelAreas2.level.getElevation()); 234 if (elevationComparison !== 0) { 235 return elevationComparison; 236 } else { 237 return levelAreas1.level.getElevationIndex() - levelAreas2.level.getElevationIndex(); 238 } 239 }); 240 for (var i = 0; i < undergroundAreas.length; i++) { 241 var levelAreas = undergroundAreas[i]; 242 var level = levelAreas.level; 243 var area = levelAreas.undergroundArea; 244 var areaAtStart = area.clone(); 245 levelAreas.undergroundSideArea.add(area.clone()); 246 for (var j = 0; j < undergroundAreas.length; j++) { 247 var otherLevelAreas = undergroundAreas[j]; 248 if (otherLevelAreas.level.getElevation() < level.getElevation()) { 249 var areaPoints = this.getPoints(otherLevelAreas.undergroundArea); 250 for (var k = 0; k < areaPoints.length; k++) { 251 var points = areaPoints[k]; 252 if (!new Room(points).isClockwise()) { 253 var pointsArea = new java.awt.geom.Area(this.getShape(points)); 254 area.subtract(pointsArea); 255 levelAreas.undergroundSideArea.add(pointsArea); 256 } 257 } 258 } 259 } 260 var areaPoints = this.getPoints(area); 261 for (var j = 0; j < areaPoints.length; j++) { 262 var points = areaPoints[j]; 263 if (new Room(points).isClockwise()) { 264 var coveredHole = new java.awt.geom.Area(this.getShape(points)); 265 coveredHole.exclusiveOr(areaAtStart); 266 coveredHole.subtract(areaAtStart); 267 levelAreas.upperLevelArea.add(coveredHole); 268 } else { 269 areaRemovedFromGround.add(new java.awt.geom.Area(this.getShape(points))); 270 } 271 } 272 } 273 for (var i = 0; i < undergroundAreas.length; i++) { 274 var levelAreas = undergroundAreas[i]; 275 var roomArea = levelAreas.roomArea; 276 if (roomArea !== null) { 277 levelAreas.undergroundArea.subtract(roomArea); 278 } 279 } 280 281 var groundArea = new java.awt.geom.Area(this.getShape( 282 [[this.originX, this.originY], 283 [this.originX, this.originY + this.depth], 284 [this.originX + this.width, this.originY + this.depth], 285 [this.originX + this.width, this.originY]])); 286 var removedAreaBounds = areaRemovedFromGround.getBounds2D(); 287 if (!groundArea.getBounds2D().equals(removedAreaBounds)) { 288 var outsideGroundArea = groundArea; 289 if (areaRemovedFromGround.isEmpty()) { 290 removedAreaBounds = new java.awt.geom.Rectangle2D.Float(Math.max(-5000.0, this.originX), Math.max(-5000.0, this.originY), 0, 0); 291 removedAreaBounds.add(Math.min(5000.0, this.originX + this.width), 292 Math.min(5000.0, this.originY + this.depth)); 293 } else { 294 removedAreaBounds.add(Math.max(removedAreaBounds.getMinX() - 5000.0, this.originX), 295 Math.max(removedAreaBounds.getMinY() - 5000.0, this.originY)); 296 removedAreaBounds.add(Math.min(removedAreaBounds.getMaxX() + 5000.0, this.originX + this.width), 297 Math.min(removedAreaBounds.getMaxY() + 5000.0, this.originY + this.depth)); 298 } 299 groundArea = new java.awt.geom.Area(removedAreaBounds); 300 outsideGroundArea.subtract(groundArea); 301 this.addAreaGeometry(groundShape, groundTexture, outsideGroundArea, 0); 302 } 303 groundArea.subtract(areaRemovedFromGround); 304 305 undergroundAreas.splice(0, 0, new Ground3D.LevelAreas(new Level("Ground", 0, 0, 0), groundArea)); 306 var previousLevelElevation = 0; 307 for (var i = 0; i < undergroundAreas.length; i++) { 308 var levelAreas = undergroundAreas[i]; 309 var elevation = levelAreas.level.getElevation(); 310 this.addAreaGeometry(groundShape, groundTexture, levelAreas.undergroundArea, elevation); 311 if (previousLevelElevation - elevation > 0) { 312 var areaPoints = this.getPoints(levelAreas.undergroundSideArea); 313 for (var j = 0; j < areaPoints.length; j++) { 314 var points = areaPoints[j]; 315 this.addAreaSidesGeometry(groundShape, groundTexture, points, elevation, previousLevelElevation - elevation); 316 } 317 this.addAreaGeometry(groundShape, groundTexture, levelAreas.upperLevelArea, previousLevelElevation); 318 } 319 previousLevelElevation = elevation; 320 } 321 322 for (var i = currentGeometriesCount - 1; i >= 0; i--) { 323 groundShape.removeGeometry(i); 324 } 325 } 326 327 /** 328 * Returns the list of points that defines the given area. 329 * @param {Area} area 330 * @return {Array} 331 * @private 332 */ 333 Ground3D.prototype.getPoints = function(area) { 334 var areaPoints = []; 335 var areaPartPoints = []; 336 var previousRoomPoint = null; 337 for (var it = area.getPathIterator(null, 1); !it.isDone(); it.next()) { 338 var roomPoint = [0, 0]; 339 if (it.currentSegment(roomPoint) === java.awt.geom.PathIterator.SEG_CLOSE) { 340 if (areaPartPoints[0][0] === previousRoomPoint[0] 341 && areaPartPoints[0][1] === previousRoomPoint[1]) { 342 areaPartPoints.splice(areaPartPoints.length - 1, 1); 343 } 344 if (areaPartPoints.length > 2) { 345 areaPoints.push(areaPartPoints.slice(0)); 346 } 347 areaPartPoints.length = 0; 348 previousRoomPoint = null; 349 } else { 350 if (previousRoomPoint === null 351 || roomPoint[0] !== previousRoomPoint[0] 352 || roomPoint[1] !== previousRoomPoint[1]) { 353 areaPartPoints.push(roomPoint); 354 } 355 previousRoomPoint = roomPoint; 356 } 357 } 358 return areaPoints; 359 } 360 361 /** 362 * Returns the {@link LevelAreas} instance matching the given level. 363 * @param {Object} undergroundAreas 364 * @param {Level} level 365 * @return {Ground3D.LevelAreas} 366 * @private 367 */ 368 Ground3D.prototype.getUndergroundAreas = function(undergroundAreas, level) { 369 var levelAreas = null; 370 for (var i = 0; i < undergroundAreas.length; i++) { 371 if (undergroundAreas[i].level === level) { 372 levelAreas = undergroundAreas[i]; 373 break; 374 } 375 } 376 if (levelAreas === null) { 377 undergroundAreas.push(levelAreas = new Ground3D.LevelAreas(level)); 378 } 379 return levelAreas; 380 }; 381 382 383 /** 384 * Updates underground level areas dug by the visible furniture placed at underground levels. 385 * @param {Object} undergroundLevelAreas 386 * @param {Level} level 387 * @return {Array} furniture 388 * @private 389 */ 390 Ground3D.prototype.updateUndergroundAreasDugByFurniture = function(undergroundLevelAreas, furniture) { 391 for (var i = 0; i < furniture.length; i++) { 392 var piece = furniture[i]; 393 var pieceLevel = piece.getLevel(); 394 if (piece.getGroundElevation() < 0 395 && piece.isVisible() 396 && pieceLevel !== null 397 && pieceLevel.isViewable() 398 && pieceLevel.getElevation() < 0) { 399 if (piece instanceof HomeFurnitureGroup) { 400 this.updateUndergroundAreasDugByFurniture(undergroundLevelAreas, piece.getFurniture()); 401 } else { 402 var levelAreas = this.getUndergroundAreas(undergroundLevelAreas, pieceLevel); 403 if (piece.getStaircaseCutOutShape() === null) { 404 levelAreas.undergroundArea.add(new java.awt.geom.Area(this.getShape(piece.getPoints()))); 405 } else { 406 levelAreas.undergroundArea.add(ModelManager.getInstance().getAreaOnFloor(piece)); 407 } 408 } 409 } 410 } 411 } 412 413 /** 414 * Adds to ground shape the geometry matching the given area. 415 * @param {Shape3D} groundShape 416 * @param {HomeTexture} groundTexture 417 * @param {Area} area 418 * @param {number} elevation 419 * @private 420 */ 421 Ground3D.prototype.addAreaGeometry = function(groundShape, groundTexture, area, elevation) { 422 var areaPoints = this.getAreaPoints(area, 1, false); 423 if (areaPoints.length !== 0) { 424 var vertexCount = 0; 425 var stripCounts = new Array(areaPoints.length); 426 for (var i = 0; i < stripCounts.length; i++) { 427 stripCounts[i] = areaPoints[i].length; 428 vertexCount += stripCounts[i]; 429 } 430 var geometryCoords = new Array(vertexCount); 431 var geometryTextureCoords = groundTexture !== null 432 ? new Array(vertexCount) 433 : null; 434 435 var j = 0; 436 for (var index = 0; index < areaPoints.length; index++) { 437 var areaPartPoints = areaPoints[index]; 438 for (var i = 0; i < areaPartPoints.length; i++, j++) { 439 var point = areaPartPoints[i]; 440 geometryCoords[j] = vec3.fromValues(point[0], elevation, point[1]); 441 if (groundTexture !== null) { 442 geometryTextureCoords[j] = vec2.fromValues(point[0] - this.originX, this.originY - point[1]); 443 } 444 } 445 } 446 447 var geometryInfo = new GeometryInfo3D(GeometryInfo3D.POLYGON_ARRAY); 448 geometryInfo.setCoordinates(geometryCoords); 449 if (groundTexture !== null) { 450 geometryInfo.setTextureCoordinates(geometryTextureCoords); 451 } 452 geometryInfo.setStripCounts(stripCounts); 453 geometryInfo.setCreaseAngle(0); 454 geometryInfo.setGeneratedNormals(true); 455 groundShape.addGeometry(geometryInfo.getIndexedGeometryArray()); 456 } 457 } 458 459 /** 460 * Adds to ground shape the geometry matching the given area sides. 461 * @param {Shape3D} groundShape 462 * @param {HomeTexture} groundTexture 463 * @param {Array} areaPoints 464 * @param {number} elevation 465 * @param {number} sideHeight 466 * @private 467 */ 468 Ground3D.prototype.addAreaSidesGeometry = function(groundShape, groundTexture, areaPoints, elevation, sideHeight) { 469 var geometryCoords = new Array(areaPoints.length * 4); 470 var geometryTextureCoords = groundTexture !== null 471 ? new Array(geometryCoords.length) 472 : null; 473 for (var i = 0, j = 0; i < areaPoints.length; i++) { 474 var point = areaPoints[i]; 475 var nextPoint = areaPoints[i < areaPoints.length - 1 ? i + 1 : 0]; 476 geometryCoords[j++] = vec3.fromValues(point[0], elevation, point[1]); 477 geometryCoords[j++] = vec3.fromValues(point[0], elevation + sideHeight, point[1]); 478 geometryCoords[j++] = vec3.fromValues(nextPoint[0], elevation + sideHeight, nextPoint[1]); 479 geometryCoords[j++] = vec3.fromValues(nextPoint[0], elevation, nextPoint[1]); 480 if (groundTexture !== null) { 481 var distance = java.awt.geom.Point2D.distance(point[0], point[1], nextPoint[0], nextPoint[1]); 482 geometryTextureCoords[j - 4] = vec2.fromValues(point[0], elevation); 483 geometryTextureCoords[j - 3] = vec2.fromValues(point[0], elevation + sideHeight); 484 geometryTextureCoords[j - 2] = vec2.fromValues(point[0] - distance, elevation + sideHeight); 485 geometryTextureCoords[j - 1] = vec2.fromValues(point[0] - distance, elevation); 486 } 487 } 488 489 var geometryInfo = new GeometryInfo3D(GeometryInfo3D.QUAD_ARRAY); 490 geometryInfo.setCoordinates(geometryCoords); 491 if (groundTexture !== null) { 492 geometryInfo.setTextureCoordinates(geometryTextureCoords); 493 } 494 geometryInfo.setCreaseAngle(0); 495 geometryInfo.setGeneratedNormals(true); 496 groundShape.addGeometry(geometryInfo.getIndexedGeometryArray()); 497 } 498 499 /** 500 * Areas of underground levels. 501 * @constructor 502 * @private 503 */ 504 Ground3D.LevelAreas = function(level, undergroundArea) { 505 if (undergroundArea === undefined) { 506 undergroundArea = new java.awt.geom.Area(); 507 } 508 this.level = level; 509 this.undergroundArea = undergroundArea; 510 this.roomArea = new java.awt.geom.Area(); 511 this.wallArea = new java.awt.geom.Area(); 512 this.undergroundSideArea = new java.awt.geom.Area(); 513 this.upperLevelArea = new java.awt.geom.Area(); 514 }