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