1 /*
  2  * Ground3D.js
  3  *
  4  * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]>
  5  *
  6  * This program is free software; you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation; either version 2 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * This program is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with this program; if not, write to the Free Software
 18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19  */
 20 
 21 // 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 }