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