1 /* 2 Copyright 2008-2026 3 Matthias Ehmann, 4 Carsten Miller, 5 Andreas Walter, 6 Alfred Wassermann 7 8 This file is part of JSXGraph. 9 10 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 11 12 You can redistribute it and/or modify it under the terms of the 13 14 * GNU Lesser General Public License as published by 15 the Free Software Foundation, either version 3 of the License, or 16 (at your option) any later version 17 OR 18 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 19 20 JSXGraph is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU Lesser General Public License for more details. 24 25 You should have received a copy of the GNU Lesser General Public License and 26 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 27 and <https://opensource.org/licenses/MIT/>. 28 */ 29 /*global JXG:true, define: true*/ 30 31 import JXG from "../jxg.js"; 32 import Const from "../base/constants.js"; 33 import Type from "../utils/type.js"; 34 import Mat from "../math/math.js"; 35 36 /** 37 * 3D faces 38 * @class Creates a new 3D face object. Do not use this constructor to create a 3D curve. Use {@link JXG.View3D#create} with type {@link Face3D} instead. 39 * 40 * @augments JXG.GeometryElement3D 41 * @augments JXG.GeometryElement 42 * @param {View3D} view 43 * @param {Function} F 44 * @param {Function} X 45 * @param {Function} Y 46 * @param {Function} Z 47 * @param {Array} range 48 * @param {Object} attributes 49 * @see JXG.Board#generateName 50 */ 51 JXG.Face3D = function (view, polyhedron, faceNumber, attributes) { 52 this.constructor(view.board, attributes, Const.OBJECT_TYPE_FACE3D, Const.OBJECT_CLASS_3D); 53 this.constructor3D(view, 'face3d'); 54 55 this.board.finalizeAdding(this); 56 57 /** 58 * Link to the defining data of the parent polyhedron3d. 59 * @name Face3D#polyhedron 60 * @type Object 61 * @see Polyhedron3D#def 62 */ 63 this.polyhedron = polyhedron; 64 65 /** 66 * Index of the face in the list of faces of the polyhedron 67 * @name Face3D#faceNumber 68 * @type Number 69 */ 70 this.faceNumber = faceNumber; 71 72 /** 73 * Normal vector for the face. Array of length 4. 74 * @name Face3D#normal 75 * @type array 76 */ 77 this.normal = [0, 0, 0, 0]; 78 79 /** 80 * Hesse right hand side of the plane that contains the face. 81 * @name Face3D#d 82 * @type Number 83 */ 84 this.d = 0; 85 86 /** 87 * First basis vector of the face. Vector of length 4. 88 * @name Face3D#vec1 89 * @type Array 90 */ 91 this.vec1 = [0, 0, 0, 0]; 92 93 /** 94 * Second basis vector of the face. Vector of length 4. 95 * @name Face3D#vec2 96 * @type Array 97 */ 98 this.vec2 = [0, 0, 0, 0]; 99 100 if (this.faceNumber === 0) { 101 this.updateCoords(); 102 } 103 }; 104 105 JXG.Face3D.prototype = new JXG.GeometryElement(); 106 107 Type.copyPrototypeMethods(JXG.Face3D, JXG.GeometryElement3D, 'constructor3D'); 108 Type.copyMethodMap(JXG.Face3D, { 109 // TODO 110 }); 111 112 JXG.extend( 113 JXG.Face3D.prototype, 114 /** @lends JXG.Face3D.prototype */ { 115 116 /** 117 * Update the coordinates of all vertices of the polyhedron 118 * @function 119 * @name Face3D#updateCoords 120 * @returns {Face3D} reference to itself 121 */ 122 updateCoords: function() { 123 var i, j, le, p, 124 def = this.polyhedron; 125 126 for (i in def.vertices) { 127 p = def.vertices[i]; 128 if (Type.isFunction(p)) { 129 def.coords[i] = Type.evaluate(p); 130 } else if (Type.isArray(p)) { 131 def.coords[i] = []; 132 le = p.length; 133 for (j = 0; j < le; j++) { 134 def.coords[i][j] = Type.evaluate(p[j]); 135 } 136 } else { 137 p = def.view.select(p); 138 if (Type.isPoint3D(p)) { 139 def.coords[i] = p.coords; 140 } else { 141 throw new Error('Polyhedron3D.updateCoords: unknown vertices type!'); 142 } 143 } 144 if (def.coords[i].length === 3) { 145 def.coords[i].unshift(1); 146 } 147 } 148 149 return this; 150 }, 151 152 /** 153 * Update the 2D coordinates of the face and determine it's z-index. 154 * @function 155 * @name Face3D#updateDataArray2D 156 * @returns {Object} {X:[], Y:[]} 157 */ 158 updateDataArray2D: function () { 159 var j, le, 160 c3d, c2d, 161 x = [], 162 y = [], 163 p = this.polyhedron, 164 face = p.faces[this.faceNumber]; 165 166 if (this.faceNumber === 0) { 167 // coords2D equal to [] means, projection is needed down below. 168 // Thus, every vertex is projected only once. 169 for (j in p.vertices) { 170 p.coords2D[j] = []; 171 } 172 } 173 174 // Add the projected coordinates of the vertices of this face 175 // to the 2D curve. 176 // If not done yet, project the 3D vertices of this face to 2D. 177 le = face.length; 178 this.zIndex = 0.0; 179 for (j = 0; j < le; j++) { 180 c2d = p.coords2D[face[j]]; 181 if (c2d.length === 0) { 182 // if coords2D.length > 0, it has already be projected 183 // in another face3d. 184 c3d = p.coords[face[j]]; 185 c2d = this.view.project3DTo2D(c3d); 186 p.coords2D[face[j]] = c2d; 187 // p.zIndex[face[j]] = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3]; 188 p.zIndex[face[j]] = Mat.innerProduct(this.view.matrix3DRotShift[3], c3d); 189 } 190 x.push(c2d[1]); 191 y.push(c2d[2]); 192 193 this.zIndex += p.zIndex[face[j]]; 194 } 195 if (le > 0) { 196 this.zIndex /= le; 197 } 198 if (le !== 2) { 199 // 2D faces and points are a closed loop 200 x.push(x[0]); 201 y.push(y[0]); 202 } 203 204 return { X: x, Y: y }; 205 }, 206 207 addTransform: function (el, transform) { 208 if (this.faceNumber === 0) { 209 this.addTransformGeneric(el, transform); 210 } 211 return this; 212 }, 213 214 removeTransform: function (transform) { 215 if (this.faceNumber === 0) { 216 this.removeTransformGeneric(transform); 217 } 218 return this; 219 }, 220 221 clearTransforms: function () { 222 if (this.faceNumber === 0) { 223 this.clearTransformsGeneric(); 224 } 225 return this; 226 }, 227 228 updateTransform: function () { 229 var t, c, i, j, b; 230 231 if (this.faceNumber !== 0) { 232 return this; 233 } 234 235 if (this.transformations.length === 0 || this.baseElement === null) { 236 return this; 237 } 238 239 t = this.transformations; 240 for (i = 0; i < t.length; i++) { 241 t[i].update(); 242 } 243 244 if (this === this.baseElement) { 245 b = this.polyhedron; 246 } else { 247 b = this.baseElement.polyhedron; 248 } 249 for (i in b.coords) { 250 if (b.coords.hasOwnProperty(i)) { 251 c = b.coords[i]; 252 for (j = 0; j < t.length; j++) { 253 c = Mat.matVecMult(t[j].matrix, c); 254 } 255 this.polyhedron.coords[i] = c; 256 } 257 } 258 259 return this; 260 }, 261 262 update: function () { 263 var i, le, 264 phdr, nrm, 265 p1, p2, 266 face; 267 268 if (this.needsUpdate && !this.view.board._change3DView) { 269 // Do not update face coordinates and normal during view rotation 270 phdr = this.polyhedron; 271 272 if (this.faceNumber === 0) { 273 // Update coordinates of all vertices 274 this.updateCoords() 275 .updateTransform(); 276 } 277 278 face = phdr.faces[this.faceNumber]; 279 le = face.length; 280 if (le < 3) { 281 // Get out of here if face is point or segment 282 return this; 283 } 284 285 // Update spanning vectors 286 p1 = phdr.coords[face[0]]; 287 p2 = phdr.coords[face[1]]; 288 this.vec1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]]; 289 290 p2 = phdr.coords[face[2]]; 291 this.vec2 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]]; 292 293 // Update Hesse form, i.e. normal and d 294 this.normal = Mat.crossProduct(this.vec1.slice(1), this.vec2.slice(1)); 295 nrm = Mat.norm(this.normal); 296 this.normal.unshift(0); 297 298 if (Math.abs(nrm) > 1.e-12) { 299 for (i = 1; i < 4; i++) { 300 this.normal[i] /= nrm; 301 } 302 } 303 this.d = Mat.innerProduct(p1, this.normal, 4); 304 } 305 return this; 306 }, 307 308 updateRenderer: function () { 309 if (this.needsUpdate) { 310 this.needsUpdate = false; 311 } 312 return this; 313 }, 314 315 // To be merged with View3d.getRotationFromAngles 316 getRotationFromAngles: function (angles) { 317 var a, e, b, f, 318 cosBank, sinBank, 319 mat = [ 320 [1, 0, 0, 0], 321 [0, 1, 0, 0], 322 [0, 0, 1, 0], 323 [0, 0, 0, 1] 324 ]; 325 326 // mat projects homogeneous 3D coords in View3D 327 // to homogeneous 2D coordinates in the board 328 a = angles.az; 329 e = angles.el; 330 b = angles.bank; 331 f = -Math.sin(e); 332 333 mat[1][1] = -Math.cos(a); 334 mat[1][2] = Math.sin(a); 335 mat[1][3] = 0; 336 337 mat[2][1] = f * Math.sin(a); 338 mat[2][2] = f * Math.cos(a); 339 mat[2][3] = Math.cos(e); 340 341 mat[3][1] = Math.cos(e) * Math.sin(a); 342 mat[3][2] = Math.cos(e) * Math.cos(a); 343 mat[3][3] = Math.sin(e); 344 345 cosBank = Math.cos(b); 346 sinBank = Math.sin(b); 347 mat = Mat.matMatMult([ 348 [1, 0, 0, 0], 349 [0, cosBank, sinBank, 0], 350 [0, -sinBank, cosBank, 0], 351 [0, 0, 0, 1] 352 ], mat); 353 354 return mat; 355 }, 356 357 /** 358 * Determines the lightness of the face (in the HSL color scheme). 359 * <p> 360 * Sets the fillColor of the adjoint 2D curve. 361 * @name shader 362 * @memberOf Face3D 363 * @function 364 * @returns {Number} zIndex of the face 365 */ 366 shader: function() { 367 var hue, sat, light, angle, hsl, 368 sun, angles, 369 abs, 370 lightObj, 371 minFace, maxFace, 372 minLight, maxLight; 373 374 if (this.evalVisProp('shader.enabled')) { 375 hue = this.evalVisProp('shader.hue'); 376 sat = this.evalVisProp('shader.saturation'); 377 minLight = this.evalVisProp('shader.minlightness'); 378 maxLight = this.evalVisProp('shader.maxlightness'); 379 380 if (this.evalVisProp('shader.type').toLowerCase() === 'angle') { 381 lightObj = this.evalVisProp('shader.light'); 382 383 switch (lightObj.type) { 384 // 1: lighting==camera (default), 385 // 2: Fixed: angle(object, camera), 386 // 3: Fixed: angle(lighting, camera) 387 case 2: // Fixed: angle(object, camera) 388 angles = { 389 az: lightObj.az * Math.PI / 180, 390 el: lightObj.el * Math.PI / 180, 391 bank: lightObj.bank * Math.PI / 180 392 }; 393 sun = this.getRotationFromAngles(angles)[3]; 394 abs = lightObj.dir; 395 break; 396 case 3: // Fixed: angle(lighting, camera) 397 angles = { 398 az: this.view.angles.az + lightObj.az * Math.PI / 180, 399 el: this.view.angles.el + lightObj.el * Math.PI / 180, 400 bank: this.view.angles.bank 401 }; 402 sun = this.getRotationFromAngles(angles)[3]; 403 abs = lightObj.dir; 404 break; 405 default: // Fixed: angle(lighting, camera) = 0 406 sun = this.view.matrix3DRotShift[3]; 407 abs = lightObj.dir; 408 } 409 410 // angle = Mat.innerProduct(this.view.matrix3DRotShift[3], this.normal); 411 angle = Mat.innerProduct(sun, this.normal); 412 angle = (abs === 0) ? Math.abs(angle) : ((abs < 0) ? -angle : angle); 413 light = minLight + (maxLight - minLight) * angle; 414 } else { 415 // zIndex 416 maxFace = this.view.zIndexMax; 417 minFace = this.view.zIndexMin; 418 light = minLight + (maxLight - minLight) * ((this.zIndex - minFace) / (maxFace - minFace)); 419 } 420 421 // hsl = `hsl(${hue}, ${sat}%, ${light}%)`; 422 hsl = 'hsl(' + hue + ',' + sat +'%,' + light + '%)'; 423 424 this.element2D.visProp.fillcolor = hsl; 425 this._shadingDone = true; 426 return this.zIndex; 427 } 428 } 429 } 430 ); 431 432 /** 433 * @class This element creates a 3D face. 434 * @pseudo 435 * @description A 3D faces is TODO 436 * 437 * @name Face3D 438 * @augments Curve 439 * @constructor 440 * @type Object 441 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 442 */ 443 JXG.createFace3D = function (board, parents, attributes) { 444 var view = parents[0], 445 polyhedron = parents[1], 446 faceNumber = parents[2], 447 attr, el; 448 449 // TODO Throw new Error 450 attr = Type.copyAttributes(attributes, board.options, 'face3d'); 451 el = new JXG.Face3D(view, polyhedron, faceNumber, attr); 452 453 attr = el.setAttr2D(attr); 454 el.element2D = view.create("curve", [[], []], attr); 455 el.element2D.view = view; 456 el.element2D.dump = false; 457 458 /** 459 * @class 460 * @ignore 461 */ 462 el.element2D.updateDataArray = function () { 463 var ret = el.updateDataArray2D(); 464 this.dataX = ret.X; 465 this.dataY = ret.Y; 466 }; 467 // Deactivate hasPoint to increase speed. 468 // This is be too agressive, e.g. for the upcoming 469 // rotation3D circles (SoftwarePraktikum) 470 // We may enable have to enable hasPoint() for 471 // click events. 472 el.element2D.hasPoint = function() {}; 473 474 el.addChild(el.element2D); 475 el.inherits.push(el.element2D); 476 el.element2D.setParents(el); 477 478 el.element2D.prepareUpdate().update(); 479 if (!board.isSuspendedUpdate) { 480 el.element2D.updateVisibility().updateRenderer(); 481 } 482 483 return el; 484 }; 485 486 JXG.registerElement("face3d", JXG.createFace3D); 487 488