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 * @see Polyhedron3D#def 122 */ 123 updateCoords: function() { 124 var i, j, le, p, 125 def = this.polyhedron; 126 127 for (i in def.vertices) { 128 p = def.vertices[i]; 129 if (Type.isFunction(p)) { 130 def.coords[i] = Type.evaluate(p); 131 } else if (Type.isArray(p)) { 132 def.coords[i] = []; 133 le = p.length; 134 for (j = 0; j < le; j++) { 135 def.coords[i][j] = Type.evaluate(p[j]); 136 } 137 } else { 138 p = def.view.select(p); 139 if (Type.isPoint3D(p)) { 140 def.coords[i] = p.coords; 141 } else { 142 throw new Error('Polyhedron3D.updateCoords: unknown vertices type!'); 143 } 144 } 145 if (def.coords[i].length === 3) { 146 def.coords[i].unshift(1); 147 } 148 } 149 150 return this; 151 }, 152 153 /** 154 * Update the 2D coordinates of the face and determine it's z-index. 155 * @function 156 * @name Face3D#updateDataArray2D 157 * @returns {Object} {X:[], Y:[]} 158 */ 159 updateDataArray2D: function () { 160 var j, le, 161 c3d, c2d, 162 x = [], 163 y = [], 164 p = this.polyhedron, 165 face = p.faces[this.faceNumber]; 166 167 if (this.faceNumber === 0) { 168 // coords2D equal to [] means, projection is needed down below. 169 // Thus, every vertex is projected only once. 170 for (j in p.vertices) { 171 p.coords2D[j] = []; 172 } 173 } 174 175 // Add the projected coordinates of the vertices of this face 176 // to the 2D curve. 177 // If not done yet, project the 3D vertices of this face to 2D. 178 le = face.length; 179 this.zIndex = 0.0; 180 for (j = 0; j < le; j++) { 181 c2d = p.coords2D[face[j]]; 182 if (c2d.length === 0) { 183 // if coords2D.length > 0, it has already be projected 184 // in another face3d. 185 c3d = p.coords[face[j]]; 186 c2d = this.view.project3DTo2D(c3d); 187 p.coords2D[face[j]] = c2d; 188 // p.zIndex[face[j]] = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3]; 189 p.zIndex[face[j]] = Mat.innerProduct(this.view.matrix3DRotShift[3], c3d); 190 } 191 x.push(c2d[1]); 192 y.push(c2d[2]); 193 194 this.zIndex += p.zIndex[face[j]]; 195 } 196 if (le > 0) { 197 this.zIndex /= le; 198 } 199 if (le !== 2) { 200 // 2D faces and points are a closed loop 201 x.push(x[0]); 202 y.push(y[0]); 203 } 204 205 return { X: x, Y: y }; 206 }, 207 208 addTransform: function (el, transform) { 209 if (this.faceNumber === 0) { 210 this.addTransformGeneric(el, transform); 211 } 212 return this; 213 }, 214 215 removeTransform: function (transform) { 216 if (this.faceNumber === 0) { 217 this.removeTransformGeneric(transform); 218 } 219 return this; 220 }, 221 222 clearTransforms: function () { 223 if (this.faceNumber === 0) { 224 this.clearTransformsGeneric(); 225 } 226 return this; 227 }, 228 229 updateTransform: function () { 230 var t, c, i, j, b; 231 232 if (this.faceNumber !== 0) { 233 return this; 234 } 235 236 if (this.transformations.length === 0 || this.baseElement === null) { 237 return this; 238 } 239 240 t = this.transformations; 241 for (i = 0; i < t.length; i++) { 242 t[i].update(); 243 } 244 245 if (this === this.baseElement) { 246 b = this.polyhedron; 247 } else { 248 b = this.baseElement.polyhedron; 249 } 250 for (i in b.coords) { 251 if (b.coords.hasOwnProperty(i)) { 252 c = b.coords[i]; 253 for (j = 0; j < t.length; j++) { 254 c = Mat.matVecMult(t[j].matrix, c); 255 } 256 this.polyhedron.coords[i] = c; 257 } 258 } 259 260 return this; 261 }, 262 263 update: function () { 264 var i, le, 265 phdr, nrm, 266 p1, p2, 267 face; 268 269 if (this.needsUpdate && !this.view.board._change3DView) { 270 // Do not update face coordinates and normal during view rotation 271 phdr = this.polyhedron; 272 273 if (this.faceNumber === 0) { 274 // Update coordinates of all vertices 275 this.updateCoords() 276 .updateTransform(); 277 } 278 279 face = phdr.faces[this.faceNumber]; 280 le = face.length; 281 if (le < 3) { 282 // Get out of here if face is point or segment 283 return this; 284 } 285 286 // Update spanning vectors 287 p1 = phdr.coords[face[0]]; 288 p2 = phdr.coords[face[1]]; 289 this.vec1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]]; 290 291 p2 = phdr.coords[face[2]]; 292 this.vec2 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]]; 293 294 // Update Hesse form, i.e. normal and d 295 this.normal = Mat.crossProduct(this.vec1.slice(1), this.vec2.slice(1)); 296 nrm = Mat.norm(this.normal); 297 this.normal.unshift(0); 298 299 if (Math.abs(nrm) > 1.e-12) { 300 for (i = 1; i < 4; i++) { 301 this.normal[i] /= nrm; 302 } 303 } 304 this.d = Mat.innerProduct(p1, this.normal, 4); 305 } 306 return this; 307 }, 308 309 updateRenderer: function () { 310 if (this.needsUpdate) { 311 this.needsUpdate = false; 312 } 313 return this; 314 }, 315 316 // To be merged with View3d.getRotationFromAngles 317 getRotationFromAngles: function (angles) { 318 var a, e, b, f, 319 cosBank, sinBank, 320 mat = [ 321 [1, 0, 0, 0], 322 [0, 1, 0, 0], 323 [0, 0, 1, 0], 324 [0, 0, 0, 1] 325 ]; 326 327 // mat projects homogeneous 3D coords in View3D 328 // to homogeneous 2D coordinates in the board 329 a = angles.az; 330 e = angles.el; 331 b = angles.bank; 332 f = -Math.sin(e); 333 334 mat[1][1] = -Math.cos(a); 335 mat[1][2] = Math.sin(a); 336 mat[1][3] = 0; 337 338 mat[2][1] = f * Math.sin(a); 339 mat[2][2] = f * Math.cos(a); 340 mat[2][3] = Math.cos(e); 341 342 mat[3][1] = Math.cos(e) * Math.sin(a); 343 mat[3][2] = Math.cos(e) * Math.cos(a); 344 mat[3][3] = Math.sin(e); 345 346 cosBank = Math.cos(b); 347 sinBank = Math.sin(b); 348 mat = Mat.matMatMult([ 349 [1, 0, 0, 0], 350 [0, cosBank, sinBank, 0], 351 [0, -sinBank, cosBank, 0], 352 [0, 0, 0, 1] 353 ], mat); 354 355 return mat; 356 }, 357 358 /** 359 * Determines the lightness of the face (in the HSL color scheme). 360 * <p> 361 * Sets the fillColor of the adjoint 2D curve. 362 * @name shader 363 * @memberOf Face3D 364 * @function 365 * @returns {Number} zIndex of the face 366 */ 367 shader: function() { 368 var hue, sat, light, angle, hsl, 369 sun, angles, 370 abs, 371 lightObj, 372 minFace, maxFace, 373 minLight, maxLight; 374 375 if (this.evalVisProp('shader.enabled')) { 376 hue = this.evalVisProp('shader.hue'); 377 sat = this.evalVisProp('shader.saturation'); 378 minLight = this.evalVisProp('shader.minlightness'); 379 maxLight = this.evalVisProp('shader.maxlightness'); 380 381 if (this.evalVisProp('shader.type').toLowerCase() === 'angle') { 382 lightObj = this.evalVisProp('shader.light'); 383 384 switch (lightObj.type) { 385 // 1: lighting==camera (default), 386 // 2: Fixed: angle(object, camera), 387 // 3: Fixed: angle(lighting, camera) 388 case 2: // Fixed: angle(object, camera) 389 angles = { 390 az: lightObj.az * Math.PI / 180, 391 el: lightObj.el * Math.PI / 180, 392 bank: lightObj.bank * Math.PI / 180 393 }; 394 sun = this.getRotationFromAngles(angles)[3]; 395 abs = lightObj.dir; 396 break; 397 case 3: // Fixed: angle(lighting, camera) 398 angles = { 399 az: this.view.angles.az + lightObj.az * Math.PI / 180, 400 el: this.view.angles.el + lightObj.el * Math.PI / 180, 401 bank: this.view.angles.bank 402 }; 403 sun = this.getRotationFromAngles(angles)[3]; 404 abs = lightObj.dir; 405 break; 406 default: // Fixed: angle(lighting, camera) = 0 407 sun = this.view.matrix3DRotShift[3]; 408 abs = lightObj.dir; 409 } 410 411 // angle = Mat.innerProduct(this.view.matrix3DRotShift[3], this.normal); 412 angle = Mat.innerProduct(sun, this.normal); 413 angle = (abs === 0) ? Math.abs(angle) : ((abs < 0) ? -angle : angle); 414 light = minLight + (maxLight - minLight) * angle; 415 } else { 416 // zIndex 417 maxFace = this.view.zIndexMax; 418 minFace = this.view.zIndexMin; 419 light = minLight + (maxLight - minLight) * ((this.zIndex - minFace) / (maxFace - minFace)); 420 } 421 422 // hsl = `hsl(${hue}, ${sat}%, ${light}%)`; 423 hsl = 'hsl(' + hue + ',' + sat +'%,' + light + '%)'; 424 425 this.element2D.visProp.fillcolor = hsl; 426 this._shadingDone = true; 427 return this.zIndex; 428 } 429 } 430 } 431 ); 432 433 /** 434 * @class This element creates a 3D face. 435 * @pseudo 436 * @description A 3D faces is TODO 437 * 438 * @name Face3D 439 * @augments Curve 440 * @constructor 441 * @type Object 442 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 443 */ 444 JXG.createFace3D = function (board, parents, attributes) { 445 var view = parents[0], 446 polyhedron = parents[1], 447 faceNumber = parents[2], 448 attr, el; 449 450 // TODO Throw new Error 451 attr = Type.copyAttributes(attributes, board.options, 'face3d'); 452 el = new JXG.Face3D(view, polyhedron, faceNumber, attr); 453 454 attr = el.setAttr2D(attr); 455 el.element2D = view.create("curve", [[], []], attr); 456 el.element2D.view = view; 457 el.element2D.dump = false; 458 459 /** 460 * @class 461 * @ignore 462 */ 463 el.element2D.updateDataArray = function () { 464 var ret = el.updateDataArray2D(); 465 this.dataX = ret.X; 466 this.dataY = ret.Y; 467 }; 468 // Deactivate hasPoint to increase speed. 469 // This is be too agressive, e.g. for the upcoming 470 // rotation3D circles (SoftwarePraktikum) 471 // We may enable have to enable hasPoint() for 472 // click events. 473 el.element2D.hasPoint = function() {}; 474 475 el.addChild(el.element2D); 476 el.inherits.push(el.element2D); 477 el.element2D.setParents(el); 478 479 el.element2D.prepareUpdate().update(); 480 if (!board.isSuspendedUpdate) { 481 el.element2D.updateVisibility().updateRenderer(); 482 } 483 484 return el; 485 }; 486 487 JXG.registerElement("face3d", JXG.createFace3D); 488 489