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