1 /*
  2  * Object3DBranch.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 //          ShapeTools.js
 23 //          geom.js
 24 
 25 /**
 26  * Root of a 3D branch that matches a home object. 
 27  * @param {Object} [item] 
 28  * @param {Home}   [home]
 29  * @param {UserPreferences} [userPreferences]
 30  * @constructor
 31  * @extends BranchGroup3D
 32  * @author Emmanuel Puybaret
 33  */
 34 function Object3DBranch(item, home, userPreferences) {
 35   BranchGroup3D.call(this);
 36   if (item !== undefined) {
 37     this.setUserData(item);
 38     this.home = home;
 39     this.userPreferences = userPreferences
 40   }
 41 }
 42 Object3DBranch.prototype = Object.create(BranchGroup3D.prototype);
 43 Object3DBranch.prototype.constructor = Object3DBranch;
 44 
 45 Object3DBranch.DEFAULT_COLOR         = 0xFFFFFF;
 46 Object3DBranch.DEFAULT_AMBIENT_COLOR = 0x333333;
 47 
 48 /**
 49  * Returns home instance or <code>null</code>.
 50  * @return {Home}
 51  */
 52 Object3DBranch.prototype.getHome = function() {
 53   return this.home !== undefined ? this.home : null;
 54 }
 55 
 56 /**
 57  * Returns user preferences.
 58  * @return {UserPreferences}
 59  */
 60 Object3DBranch.prototype.getUserPreferences = function() {
 61   return this.userPreferences !== undefined ? this.userPreferences : null;
 62 }
 63 
 64 /**
 65  * Returns the shape matching the coordinates in <code>points</code> array.
 66  * @param {Array} points
 67  * @return {Shape}
 68  * @protected
 69  * @ignore
 70  */
 71 Object3DBranch.prototype.getShape = function(points) {
 72   return ShapeTools.getShape(points, true, null);
 73 }
 74 
 75 /**
 76  * Updates an appearance with the given colors.
 77  * @protected
 78  * @ignore
 79  */
 80 Object3DBranch.prototype.updateAppearanceMaterial = function(appearance, diffuseColor, ambientColor, shininess) {
 81   if (diffuseColor !== null) {
 82     appearance.setAmbientColor(vec3.fromValues(((ambientColor >>> 16) & 0xFF) / 255.,
 83                                                ((ambientColor >>> 8) & 0xFF) / 255.,
 84                                                 (ambientColor & 0xFF) / 255.));
 85     appearance.setDiffuseColor(vec3.fromValues(((diffuseColor >>> 16) & 0xFF) / 255.,
 86                                                ((diffuseColor >>> 8) & 0xFF) / 255.,
 87                                                 (diffuseColor & 0xFF) / 255.));
 88     appearance.setSpecularColor(vec3.fromValues(shininess, shininess, shininess));
 89     appearance.setShininess(Math.max(1, shininess * 128));
 90   } else {
 91     appearance.setAmbientColor(vec3.fromValues(.2, .2, .2));
 92     appearance.setDiffuseColor(vec3.fromValues(1, 1, 1));
 93     appearance.setSpecularColor(vec3.fromValues(shininess, shininess, shininess));
 94     appearance.setShininess(Math.max(1, shininess * 128));
 95   }
 96 }
 97 
 98 /**
 99  * Updates the texture transformation of an appearance.
100  * and scaled if required.
101  * @param {Appearance3D} appearance
102  * @param {HomeTexture} texture
103  * @param {boolean} [scaled]
104  */
105 Object3DBranch.prototype.updateTextureTransform = function(appearance, texture, scaled) {
106   var textureWidth = texture.getWidth();
107   var textureHeight = texture.getHeight();
108   if (textureWidth === -1 || textureHeight === -1) {
109     // Set a default value of 1m for textures with width and height equal to -1
110     // (this may happen for textures retrieved from 3D models)
111     textureWidth = 100.;
112     textureHeight = 100.;
113   }
114   var textureXOffset = texture.getXOffset();
115   var textureYOffset = texture.getYOffset();
116   var textureScale = 1 / texture.getScale();
117   var rotation = mat3.create();
118   mat3.rotate(rotation, rotation, texture.getAngle());
119   var translation = mat3.create();
120   var transform = mat3.create();
121   // Change scale if required
122   if (scaled) {
123     mat3.fromTranslation(translation, vec2.fromValues(-textureXOffset / textureScale * textureWidth, -textureYOffset / textureScale * textureHeight));
124     mat3.scale(transform, transform, vec2.fromValues(textureScale / textureWidth, textureScale / textureHeight));
125   } else {
126     mat3.fromTranslation(translation, vec2.fromValues(-textureXOffset / textureScale, -textureYOffset / textureScale));
127     mat3.multiplyScalar(transform, transform, textureScale);
128   }
129   mat3.mul(rotation, rotation, translation);
130   mat3.mul(transform, transform, rotation);
131   appearance.setTextureTransform(transform);
132 }
133 
134 /**
135  * Updates the texture transformation of an appearance to fit the surface matching <code>areaPoints</code>.
136  * @param {Appearance3D} appearance
137  * @param {HomeTexture} texture
138  * @param {Array} areaPoints
139  * @param {boolean} invertY
140  */
141 Object3DBranch.prototype.updateTextureTransformFittingArea = function(appearance, texture, areaPoints, invertY) {
142   var minX = Number.POSITIVE_INFINITY;
143   var minY = Number.POSITIVE_INFINITY;
144   var maxX = Number.NEGATIVE_INFINITY;
145   var maxY = Number.NEGATIVE_INFINITY;
146   for (var i = 0; i < areaPoints.length; i++) {
147     minX = Math.min(minX, areaPoints [i][0]);
148     minY = Math.min(minY, areaPoints [i][1]);
149     maxX = Math.max(maxX, areaPoints [i][0]);
150     maxY = Math.max(maxY, areaPoints [i][1]);
151   }
152   if (maxX - minX <= 0 || maxY - minY <= 0) {
153     this.updateTextureTransform(appearance, texture, true);
154   }
155 
156   var translation = mat3.create();
157   mat3.fromTranslation(translation, vec2.fromValues(-minX, invertY ? minY : -minY));
158   var transform = mat3.create();
159   mat3.scale(transform, transform, vec2.fromValues(1 / (maxX - minX),  1 / (maxY - minY)));
160   mat3.mul(transform, transform, translation);
161   appearance.setTextureTransform(transform);
162 }
163 
164 /**
165  * Returns an appearance for selection shapes.
166  * @return {Appearance}
167  * @ignore
168  */
169 Object3DBranch.prototype.getSelectionAppearance = function() {
170   var selectionAppearance = new Appearance3D();
171   selectionAppearance.setCullFace(Appearance3D.CULL_NONE);
172   selectionAppearance.setIllumination(0);
173   selectionAppearance.setDiffuseColor(vec3.fromValues(0, 0, 0.7102));
174   return selectionAppearance;
175 }
176 
177 /**
178  * Returns the list of polygons points matching the given <code>area</code> with detailed information in
179  * <code>areaPoints</code> and <code>areaHoles</code> if they exists.
180  * @param {Area} area
181  * @param {Array} [areaPoints]
182  * @param {Array} [areaHoles]
183  * @param {number} flatness
184  * @param {boolean} reversed
185  * @return {Array}
186  * @protected
187  * @ignore
188  */
189 Object3DBranch.prototype.getAreaPoints = function (area, areaPoints, areaHoles, flatness, reversed) {
190   if (flatness === undefined && reversed === undefined) {
191     // 3 parameters
192     flatness = areaPoints;
193     reversed = areaHoles;
194     areaPoints = null; 
195     areaHoles = null;
196   }
197   var areaPointsLists = [];
198   var areaHolesLists = [];
199   var currentPathPoints = null;
200   var previousPoint = null;
201   for (var it = area.getPathIterator(null, flatness); !it.isDone(); it.next()) {
202     var point = [0, 0];
203     switch ((it.currentSegment(point))) {
204       case java.awt.geom.PathIterator.SEG_MOVETO :
205         currentPathPoints = [];
206         currentPathPoints.push(point);
207         previousPoint = point;
208         break;
209       case java.awt.geom.PathIterator.SEG_LINETO :
210         if (point[0] !== previousPoint[0] || point[1] !== previousPoint[1]) {
211           currentPathPoints.push(point);
212         }
213         previousPoint = point;
214         break;
215       case java.awt.geom.PathIterator.SEG_CLOSE :
216         var firstPoint = currentPathPoints[0];
217         if (firstPoint[0] === previousPoint[0]
218             && firstPoint[1] === previousPoint[1]) {
219           currentPathPoints.splice(currentPathPoints.length - 1, 1);
220         }
221         if (currentPathPoints.length > 2) {
222           var areaPartPoints = currentPathPoints;
223           var subRoom = new Room(areaPartPoints);
224           if (subRoom.getArea() > 0) {
225             var pathPointsClockwise = subRoom.isClockwise();
226             if (pathPointsClockwise) {
227               areaHolesLists.push(currentPathPoints);
228             } else {
229               areaPointsLists.push(currentPathPoints);
230             }
231             if (areaPoints !== null || areaHoles !== null) {
232               if (pathPointsClockwise !== reversed) {
233                 areaPartPoints = currentPathPoints.slice(0);
234                 areaPartPoints.reverse();
235               }
236               if (pathPointsClockwise) {
237                 if (areaHoles != null) {
238                   areaHoles.push(areaPartPoints);
239                 }
240               } else {
241                 if (areaPoints != null) {
242                   areaPoints.push(areaPartPoints);
243                 }
244               }
245             }
246           }
247         }
248         break;
249     }
250   }
251   
252   var areaPointsWithoutHoles = [];
253   if ((areaHolesLists.length === 0) && areaPoints !== null) {
254     areaPointsWithoutHoles.push.apply(areaPointsWithoutHoles, areaPoints);
255   } else if ((areaPointsLists.length === 0) && !(areaHolesLists.length === 0)) {
256     if (areaHoles !== null) {
257       areaHoles.length = 0;
258     }
259   } else {
260     var sortedAreaPoints;
261     var subAreas = [];
262     if (areaPointsLists.length > 1) {
263       sortedAreaPoints = [];
264       for (var i = 0; areaPointsLists.length !== 0; ) {
265         var testedArea = areaPointsLists[i];
266         var j = 0;
267         for ( ; j < areaPointsLists.length; j++) {
268           if (i !== j) {
269             var testedAreaPoints = areaPointsLists[j];
270             var subArea = null;
271             for (var k = 0; k < subAreas.length; k++) {
272               if (subAreas [k].key === testedAreaPoints) {
273                 subArea = subAreas [k].value;
274                 break;
275               }
276             }
277             if (subArea == null) {
278               subArea = new java.awt.geom.Area(this.getShape(testedAreaPoints.slice(0)));
279               subAreas.push({key : testedAreaPoints, value : subArea});
280             }
281             if (subArea.contains(testedArea[0][0], testedArea[0][1])) {
282               break;
283             }
284           }
285         }
286         if (j === areaPointsLists.length) {
287           areaPointsLists.splice(i, 1);
288           sortedAreaPoints.push(testedArea);
289           i = 0;
290         } else if (i < areaPointsLists.length) {
291           i++;
292         } else {
293           i = 0;
294         }
295       }
296     } else {
297       sortedAreaPoints = areaPointsLists;
298     }
299     for (var i = sortedAreaPoints.length - 1; i >= 0; i--) {
300       var enclosingAreaPartPoints = sortedAreaPoints[i];
301       var subArea = null;
302       for (var k = 0; k < subAreas.length; k++) {
303         if (subAreas [k].key === enclosingAreaPartPoints) {
304           subArea = subAreas [k].value;
305           break;
306         }
307       }
308       if (subArea === null) {
309         subArea = new java.awt.geom.Area(this.getShape(enclosingAreaPartPoints.slice(0)));
310       }
311       var holesInArea = [];
312       for (var k = 0; k < areaHolesLists.length; k++) {
313         var holePoints = areaHolesLists[k];
314         if (subArea.contains(holePoints[0][0], holePoints[0][1])) {
315           holesInArea.push(holePoints);
316         }
317       }
318       
319       var lastEnclosingAreaPointJoiningHoles = null;
320       while (holesInArea.length !== 0) {
321         var minDistance = Number.MAX_VALUE;
322         var closestHolePointsIndex = 0;
323         var closestPointIndex = 0;
324         var areaClosestPointIndex = 0;
325         for (var j = 0; j < holesInArea.length && minDistance > 0; j++) {
326           var holePoints = holesInArea[j];
327           for (var k = 0; k < holePoints.length && minDistance > 0; k++) {
328             for (var l = 0; l < enclosingAreaPartPoints.length && minDistance > 0; l++) {
329               var enclosingAreaPartPoint = enclosingAreaPartPoints[l];
330               var distance = java.awt.geom.Point2D.distanceSq(holePoints[k][0], holePoints[k][1], 
331                   enclosingAreaPartPoint[0], enclosingAreaPartPoint[1]);
332               if (distance < minDistance
333                   && lastEnclosingAreaPointJoiningHoles !== enclosingAreaPartPoint) {
334                 minDistance = distance;
335                 closestHolePointsIndex = j;
336                 closestPointIndex = k;
337                 areaClosestPointIndex = l;
338               }
339             }
340           }
341         }
342         var closestHolePoints = holesInArea[closestHolePointsIndex];
343         if (minDistance !== 0) {
344           lastEnclosingAreaPointJoiningHoles = enclosingAreaPartPoints[areaClosestPointIndex];
345           enclosingAreaPartPoints.splice(areaClosestPointIndex, 0, lastEnclosingAreaPointJoiningHoles);
346           enclosingAreaPartPoints.splice(++areaClosestPointIndex, 0, closestHolePoints[closestPointIndex]);
347         }
348         var lastPartPoints = closestHolePoints.slice(closestPointIndex, closestHolePoints.length);
349         for (var k = 0; k < lastPartPoints.length; k++, areaClosestPointIndex++) {
350           enclosingAreaPartPoints.splice(areaClosestPointIndex, 0, lastPartPoints[k]);
351         }
352         var points = closestHolePoints.slice(0, closestPointIndex);
353         for (var k = 0; k < points.length; k++, areaClosestPointIndex++) {
354           enclosingAreaPartPoints.splice(areaClosestPointIndex, 0, points[k]);
355         }
356         
357         holesInArea.splice(closestHolePointsIndex, 1);
358         areaHolesLists.splice(closestHolePoints, 1);
359       }
360     }
361     for (var k = 0; k < sortedAreaPoints.length; k++) {
362       var pathPoints = sortedAreaPoints[k];
363       if (reversed) {
364         pathPoints.reverse();
365       }
366       areaPointsWithoutHoles.push(pathPoints.slice(0));
367     }
368   }
369   return areaPointsWithoutHoles;
370 }
371 
372 /**
373  * Returns <code>true</code> if the given arrays contain the same values. 
374  * @private
375  */
376 Object3DBranch.areModelRotationsEqual = function(rotation1, rotation2) {
377   if (rotation1 === rotation2) {
378     return true;
379   } else if (rotation1 == null || rotation2 == null) {
380     return false;
381   } else {
382     for (var i = 0; i < rotation1.length; i++) {
383       for (var j = 0; j < rotation2.length; j++) {
384         if (rotation1[i][j] !== rotation2 [i][j]) {
385           return false;
386         }
387       }
388     }
389     return true;
390   }
391 }
392