1 /*
  2     Copyright 2008-2024
  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
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                 }
188                 x.push(c2d[1]);
189                 y.push(c2d[2]);
190 
191                 this.zIndex += p.zIndex[face[j]];
192             }
193             if (le > 0) {
194                 this.zIndex /= le;
195             }
196             if (le !== 2) {
197                 // 2D faces and points are a closed loop
198                 x.push(x[0]);
199                 y.push(y[0]);
200             }
201 
202             return { X: x, Y: y };
203         },
204 
205         addTransform: function (el, transform) {
206             if (this.faceNumber === 0) {
207                 this.addTransformGeneric(el, transform);
208             }
209             return this;
210         },
211 
212         updateTransform: function () {
213             var t, c, i, j, b;
214 
215             if (this.faceNumber !== 0) {
216                 return this;
217             }
218 
219             if (this.transformations.length === 0 || this.baseElement === null) {
220                 return this;
221             }
222 
223             t = this.transformations;
224             for (i = 0; i < t.length; i++) {
225                 t[i].update();
226             }
227 
228             if (this === this.baseElement) {
229                 b = this.polyhedron;
230             } else {
231                 b = this.baseElement.polyhedron;
232             }
233             for (i in b.coords) {
234                 if (b.coords.hasOwnProperty(i)) {
235                     c = b.coords[i];
236                     for (j = 0; j < t.length; j++) {
237                         c = Mat.matVecMult(t[j].matrix, c);
238                     }
239                     this.polyhedron.coords[i] = c;
240                 }
241             }
242 
243             return this;
244         },
245 
246         update: function () {
247             var i, le,
248                 phdr, nrm,
249                 p1, p2,
250                 face;
251 
252             if (this.needsUpdate && !this.view.board._change3DView) {
253                 phdr = this.polyhedron;
254 
255                 if (this.faceNumber === 0) {
256                     // Update coordinates of all vertices
257                     this.updateCoords()
258                         .updateTransform();
259                 }
260 
261                 face = phdr.faces[this.faceNumber];
262                 le = face.length;
263                 if (le < 3) {
264                     // Get out of here if face is point or segment
265                     return this;
266                 }
267 
268                 // Update spanning vectors
269                 p1 = phdr.coords[face[0]];
270                 p2 = phdr.coords[face[1]];
271                 this.vec1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]];
272 
273                 p2 = phdr.coords[face[2]];
274                 this.vec2 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]];
275 
276                 // Update Hesse form, i.e. normal and d
277                 this.normal = Mat.crossProduct(this.vec1.slice(1), this.vec2.slice(1));
278                 nrm = Mat.norm(this.normal);
279                 this.normal.unshift(0);
280 
281                 if (Math.abs(nrm) > 1.e-12) {
282                     for (i = 1; i < 4; i++) {
283                         this.normal[i] /= nrm;
284                     }
285                 }
286                 this.d = Mat.innerProduct(p1, this.normal, 4);
287             }
288             return this;
289         },
290 
291         updateRenderer: function () {
292             if (this.needsUpdate) {
293                 this.needsUpdate = false;
294                 this.shader();
295             }
296             return this;
297         },
298 
299         /**
300          * Determines the lightness of the face (in the HSL color scheme).
301          * <p>
302          * Sets the fillColor of the adjoint 2D curve.
303          * @name shader
304          * @memberOf Face3D
305          * @function
306          * @returns {Number} zIndex of the face
307          */
308         shader: function() {
309             var hue, sat, light, angle, hsl,
310                 // bb = this.view.bbox3D,
311                 minFace, maxFace,
312                 minLight, maxLight;
313 
314 
315             if (this.evalVisProp('shader.enabled')) {
316                 hue = this.evalVisProp('shader.hue');
317                 sat = this.evalVisProp('shader.saturation');
318                 minLight = this.evalVisProp('shader.minlightness');
319                 maxLight = this.evalVisProp('shader.maxlightness');
320 
321                 if (this.evalVisProp('shader.type').toLowerCase() === 'angle') {
322                     // Angle normal / eye
323                     angle = Mat.innerProduct(this.view.matrix3DRotShift[3], this.normal);
324                     angle = Math.abs(angle);
325                     light = minLight + (maxLight - minLight) * angle;
326                 } else {
327                     // zIndex
328                     maxFace = this.view.zIndexMax;
329                     minFace = this.view.zIndexMin;
330                     light = minLight + (maxLight - minLight) * ((this.zIndex - minFace) / (maxFace - minFace));
331                 }
332 
333                 // hsl = `hsl(${hue}, ${sat}%, ${light}%)`;
334                 hsl = 'hsl(' + hue + ',' + sat +'%,' + light + '%)';
335 
336                 this.element2D.visProp.fillcolor = hsl;
337                 return this.zIndex;
338             }
339         }
340     }
341 );
342 
343 /**
344  * @class This element creates a 3D face.
345  * @pseudo
346  * @description A 3D faces is TODO
347  *
348  * @name Face3D
349  * @augments Curve
350  * @constructor
351  * @type Object
352  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
353   */
354 JXG.createFace3D = function (board, parents, attributes) {
355     var view = parents[0],
356         polyhedron = parents[1],
357         faceNumber = parents[2],
358         attr, el;
359 
360     // TODO Throw new Error
361     attr = Type.copyAttributes(attributes, board.options, "face3d");
362     el = new JXG.Face3D(view, polyhedron, faceNumber, attr);
363 
364     attr = el.setAttr2D(attr);
365     el.element2D = view.create("curve", [[], []], attr);
366     el.element2D.view = view;
367 
368     /**
369      * @class
370      * @ignore
371      */
372     el.element2D.updateDataArray = function () {
373         var ret = el.updateDataArray2D();
374         this.dataX = ret.X;
375         this.dataY = ret.Y;
376     };
377     el.addChild(el.element2D);
378     el.inherits.push(el.element2D);
379     el.element2D.setParents(el);
380 
381     el.element2D.prepareUpdate().update();
382     if (!board.isSuspendedUpdate) {
383         el.element2D.updateVisibility().updateRenderer();
384     }
385 
386     return el;
387 };
388 
389 JXG.registerElement("face3d", JXG.createFace3D);
390 
391