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