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 }