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 /**
 32  * Create axes and rear and front walls of the
 33  * view3d bounding box bbox3D.
 34  */
 35 import JXG from "../jxg.js";
 36 import Const from "../base/constants.js";
 37 import Type from "../utils/type.js";
 38 
 39 JXG.Polyhedron3D = function (view, polyhedron, faces, attributes) {
 40     var e,
 41         genericMethods,
 42         generateMethod,
 43         that = this;
 44 
 45     this.constructor(view.board, attributes, Const.OBJECT_TYPE_POLYHEDRON3D, Const.OBJECT_CLASS_3D);
 46     this.constructor3D(view, "polyhedron3d");
 47 
 48     this.board.finalizeAdding(this);
 49 
 50     this.elType = 'polyhedron3d';
 51 
 52     /**
 53      * List of Face3D objects.
 54      * @name Polyhedron3D#faces
 55      * @type Array
 56      */
 57     this.faces = faces;
 58 
 59     /**
 60      * Number of faces
 61      * @name Polyhedron3D#numberFaces
 62      * @type Number
 63      */
 64     this.numberFaces = faces.length;
 65 
 66     /**
 67      * Contains the defining data of the polyhedron:
 68      * Definitions of vertices and a list of vertices for each face. This is pretty much the input given
 69      * in the construction of the polyhedron plus internal data.
 70      * @name Polyhedron3D#def
 71      * @type Object
 72      * @example
 73      *  polyhedron = {
 74      *      view: view,
 75      *      vertices: {},
 76      *      coords: {},
 77      *      coords2D: {},
 78      *      zIndex: {},
 79      *      faces: []
 80      *  };
 81      */
 82     this.def = polyhedron;
 83 
 84     // Simultaneous methods for all faces
 85     genericMethods = [
 86         "setAttribute",
 87         "setParents",
 88         "prepareUpdate",
 89         "updateRenderer",
 90         "update",
 91         "fullUpdate",
 92         "highlight",
 93         "noHighlight"
 94     ];
 95 
 96     generateMethod = function (what) {
 97         return function () {
 98             var i;
 99 
100             for (i in that.faces) {
101                 if (that.faces.hasOwnProperty(i)) {
102                     if (Type.exists(that.faces[i][what])) {
103                         that.faces[i][what].apply(that.faces[i], arguments);
104                     }
105                 }
106             }
107             return that;
108         };
109     };
110 
111     for (e = 0; e < genericMethods.length; e++) {
112         this[genericMethods[e]] = generateMethod(genericMethods[e]);
113     }
114 
115     this.methodMap = Type.deepCopy(this.methodMap, {
116         setAttribute: "setAttribute",
117         setParents: "setParents"
118     });
119 };
120 JXG.Polyhedron3D.prototype = new JXG.GeometryElement();
121 Type.copyPrototypeMethods(JXG.Polyhedron3D, JXG.GeometryElement3D, "constructor3D");
122 
123 JXG.extend(
124     JXG.Polyhedron3D.prototype,
125     /** @lends JXG.Polyhedron3D.prototype */ {
126 
127         addTransform: function (el, transform) {
128             if (this.faces.length > 0 && el.faces.length > 0) {
129                 this.faces[0].addTransform(el.faces[0], transform);
130             } else {
131                 throw new Error("Adding transformation failed. At least one of the two polyhedra has no faces.");
132             }
133             return this;
134         }
135 
136     }
137 );
138 
139 /**
140  * @class A polyhedron in a 3D view consists of faces.
141  * @pseudo
142  * @description Create a polyhedron in a 3D view consisting of faces. Faces can
143  * be 0-, 1- or 2-dimensional.
144  *
145  * @name Polyhedron3D
146  * @augments JXG.GeometryElement3D
147  * @constructor
148  * @type Object
149  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
150  * @param {} TODO
151  *
152  * @example
153  * var box = [-4, 4];
154  * var view = board.create(
155  *     'view3d',
156  *     [[-5, -3], [8, 8],
157  *     [box, box, box]],
158  *     {
159  *         projection: 'parallel',
160  *         trackball: { enabled: false },
161  *         depthOrder: {
162  *             enabled: true
163  *         },
164  *         xPlaneRear: { visible: false },
165  *         yPlaneRear: { visible: false },
166  *         zPlaneRear: { fillOpacity: 0.2 }
167  *     }
168  * );
169  * var cube = view.create('polyhedron3d', [
170  * [
171  *     [-3, -3, -3],
172  *     [3, -3, -3],
173  *     [3, 3, -3],
174  *     [-3, 3, -3],
175  *
176  *     [-3, -3, 3],
177  *     [3, -3, 3],
178  *     [3, 3, 3],
179  *     [-3, 3, 3]
180  * ],
181  * [
182  *     [0, 1, 2, 3],
183  *     [0, 1, 5, 4],
184  *     [[1, 2, 6, 5], { fillColor: 'black', fillOpacity: 0.5, strokeWidth: 5 }],
185  *     [2, 3, 7, 6],
186  *     [3, 0, 4, 7],
187  *     [4, 5, 6, 7]
188  * ]
189  * ], {
190  * fillColorArray: ['blue', 'red', 'yellow']
191  * });
192  *
193  * </pre><div id="JXG2ab3325b-4171-4a00-9896-a1b886969e18" class="jxgbox" style="width: 300px; height: 300px;"></div>
194  * <script type="text/javascript">
195  *     (function() {
196  *         var board = JXG.JSXGraph.initBoard('JXG2ab3325b-4171-4a00-9896-a1b886969e18',
197  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
198  *     var box = [-4, 4];
199  *     var view = board.create(
200  *         'view3d',
201  *         [[-5, -3], [8, 8],
202  *         [box, box, box]],
203  *         {
204  *             projection: 'parallel',
205  *             trackball: { enabled: false },
206  *             depthOrder: {
207  *                 enabled: true
208  *             },
209  *             xPlaneRear: { visible: false },
210  *             yPlaneRear: { visible: false },
211  *             zPlaneRear: { fillOpacity: 0.2 }
212  *         }
213  *     );
214  *     var cube = view.create('polyhedron3d', [
215  *     [
216  *         [-3, -3, -3],
217  *         [3, -3, -3],
218  *         [3, 3, -3],
219  *         [-3, 3, -3],
220  *
221  *         [-3, -3, 3],
222  *         [3, -3, 3],
223  *         [3, 3, 3],
224  *         [-3, 3, 3]
225  *     ],
226  *     [
227  *         [0, 1, 2, 3],
228  *         [0, 1, 5, 4],
229  *         [[1, 2, 6, 5], { fillColor: 'black', fillOpacity: 0.5, strokeWidth: 5 }],
230  *         [2, 3, 7, 6],
231  *         [3, 0, 4, 7],
232  *         [4, 5, 6, 7]
233  *     ]
234  *     ], {
235  *     fillColorArray: ['blue', 'red', 'yellow']
236  *     });
237  *
238  *     })();
239  *
240  * </script><pre>
241  *
242  * @example
243  * var box = [-4, 4];
244  * var view = board.create(
245  *     'view3d',
246  *     [[-5, -3], [8, 8],
247  *     [box, box, box]],
248  *     {
249  *         projection: 'parallel',
250  *         trackball: { enabled: false },
251  *         depthOrder: {
252  *             enabled: true
253  *         },
254  *         xPlaneRear: { visible: false },
255  *         yPlaneRear: { visible: false },
256  *         zPlaneRear: { fillOpacity: 0.2 }
257  *     }
258  * );
259  * var aa = view.create('point3d', [-3, -3, -3], { name: 'A', layer: 12});
260  * var bb = view.create('point3d', [() => aa.X(), () => aa.Y(), 3], { name: 'B', fixed: true, layer: 12});
261  * var cube = view.create('polyhedron3d', [
262  *     {
263  *         a: 'A',
264  *         b: [3, -3, -3],
265  *         c: [3, 3, -3],
266  *         d: [-3, 3, -3],
267  *
268  *         e: bb,
269  *         f: [3, -3, 3],
270  *         g: [3, 3, 3],
271  *         h: [-3, 3, 3]
272  *     },
273  *     [
274  *         ['a', 'b', 'c', 'd'],
275  *         ['a', 'b', 'f', 'e'],
276  *         ['b', 'c', 'g', 'f'],
277  *         ['c', 'd', 'h', 'g'],
278  *         ['d', 'a', 'e', 'h'],
279  *         ['e', 'f', 'g', 'h'],
280  *
281  *         ['a', 'g'], // Edge
282  *         ['f']       // Vertex
283  *     ]
284  * ], {
285  *     fillColorArray: ['blue', 'red', 'yellow'],
286  *     fillOpacity: 0.4,
287  *     layer: 12
288  * });
289  * cube.faces[6].setAttribute({ strokeWidth: 5 });
290  * cube.faces[7].setAttribute({ strokeWidth: 10 });
291  *
292  * </pre><div id="JXG1e862f44-3e38-424b-98d5-f972338a8b7f" class="jxgbox" style="width: 300px; height: 300px;"></div>
293  * <script type="text/javascript">
294  *     (function() {
295  *         var board = JXG.JSXGraph.initBoard('JXG1e862f44-3e38-424b-98d5-f972338a8b7f',
296  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
297  *     var box = [-4, 4];
298  *     var view = board.create(
299  *         'view3d',
300  *         [[-5, -3], [8, 8],
301  *         [box, box, box]],
302  *         {
303  *             projection: 'parallel',
304  *             trackball: { enabled: false },
305  *             depthOrder: {
306  *                 enabled: true
307  *             },
308  *             xPlaneRear: { visible: false },
309  *             yPlaneRear: { visible: false },
310  *             zPlaneRear: { fillOpacity: 0.2 }
311  *         }
312  *     );
313  *     var aa = view.create('point3d', [-3, -3, -3], { name: 'A', layer: 12});
314  *     var bb = view.create('point3d', [() => aa.X(), () => aa.Y(), 3], { name: 'B', fixed: true, layer: 12});
315  *     var cube = view.create('polyhedron3d', [
316  *         {
317  *             a: 'A',
318  *             b: [3, -3, -3],
319  *             c: [3, 3, -3],
320  *             d: [-3, 3, -3],
321  *
322  *             e: bb,
323  *             f: [3, -3, 3],
324  *             g: [3, 3, 3],
325  *             h: [-3, 3, 3]
326  *         },
327  *         [
328  *             ['a', 'b', 'c', 'd'],
329  *             ['a', 'b', 'f', 'e'],
330  *             ['b', 'c', 'g', 'f'],
331  *             ['c', 'd', 'h', 'g'],
332  *             ['d', 'a', 'e', 'h'],
333  *             ['e', 'f', 'g', 'h'],
334  *
335  *             ['a', 'g'], // Edge
336  *             ['f']       // Vertex
337  *         ]
338  *     ], {
339  *         fillColorArray: ['blue', 'red', 'yellow'],
340  *         fillOpacity: 0.4,
341  *         layer: 12
342  *     });
343  *     cube.faces[6].setAttribute({ strokeWidth: 5 });
344  *     cube.faces[7].setAttribute({ strokeWidth: 10 });
345  *
346  *     })();
347  *
348  * </script><pre>
349  *
350  * @example
351  * var box = [-4, 4];
352  * var view = board.create(
353  *     'view3d',
354  *     [[-5, -3], [8, 8],
355  *     [box, box, box]],
356  *     {
357  *         projection: 'parallel',
358  *         trackball: { enabled: false },
359  *         depthOrder: {
360  *             enabled: true
361  *         },
362  *         xPlaneRear: { visible: false },
363  *         yPlaneRear: { visible: false },
364  *         zPlaneRear: { fillOpacity: 0.2 }
365  *     }
366  * );
367  * var s = board.create('slider', [[-4, -6], [4, -6], [0, 2, 4]], { name: 's' });
368  * var cube = view.create('polyhedron3d', [
369  *     [
370  *         () => { let f = s.Value(); return [-f, -f, -f]; },
371  *         () => { let f = s.Value(); return [f, -f, -f]; },
372  *         () => { let f = s.Value(); return [f, f, -f]; },
373  *         () => { let f = s.Value(); return [-f, f, -f]; },
374  *
375  *         () => { let f = s.Value(); return [-f, -f, f]; },
376  *         () => { let f = s.Value(); return [f, -f, f]; },
377  *         () => { let f = s.Value(); return [f, f, f]; },
378  *         // () => { let f = s.Value(); return [-f, f, f]; }
379  *         [ () => -s.Value(),  () => s.Value(), () => s.Value() ]
380  *     ],
381  *     [
382  *         [0, 1, 2, 3],
383  *         [0, 1, 5, 4],
384  *         [1, 2, 6, 5],
385  *         [2, 3, 7, 6],
386  *         [3, 0, 4, 7],
387  *         [4, 5, 6, 7],
388  *     ]
389  * ], {
390  *     strokeWidth: 3,
391  *     fillOpacity: 0.6,
392  *     fillColorArray: ['blue', 'red', 'yellow'],
393  *     shader: {
394  *         enabled:false
395  *     }
396  * });
397  *
398  * </pre><div id="JXG6f27584b-b648-4743-a864-a6c559ead00e" class="jxgbox" style="width: 300px; height: 300px;"></div>
399  * <script type="text/javascript">
400  *     (function() {
401  *         var board = JXG.JSXGraph.initBoard('JXG6f27584b-b648-4743-a864-a6c559ead00e',
402  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
403  *     var box = [-4, 4];
404  *     var view = board.create(
405  *         'view3d',
406  *         [[-5, -3], [8, 8],
407  *         [box, box, box]],
408  *         {
409  *             projection: 'parallel',
410  *             trackball: { enabled: false },
411  *             depthOrder: {
412  *                 enabled: true
413  *             },
414  *             xPlaneRear: { visible: false },
415  *             yPlaneRear: { visible: false },
416  *             zPlaneRear: { fillOpacity: 0.2 }
417  *         }
418  *     );
419  *     var s = board.create('slider', [[-4, -6], [4, -6], [0, 2, 4]], { name: 's' });
420  *     var cube = view.create('polyhedron3d', [
421  *         [
422  *             () => { let f = s.Value(); return [-f, -f, -f]; },
423  *             () => { let f = s.Value(); return [f, -f, -f]; },
424  *             () => { let f = s.Value(); return [f, f, -f]; },
425  *             () => { let f = s.Value(); return [-f, f, -f]; },
426  *
427  *             () => { let f = s.Value(); return [-f, -f, f]; },
428  *             () => { let f = s.Value(); return [f, -f, f]; },
429  *             () => { let f = s.Value(); return [f, f, f]; },
430  *             // () => { let f = s.Value(); return [-f, f, f]; }
431  *             [ () => -s.Value(),  () => s.Value(), () => s.Value() ]
432  *         ],
433  *         [
434  *             [0, 1, 2, 3],
435  *             [0, 1, 5, 4],
436  *             [1, 2, 6, 5],
437  *             [2, 3, 7, 6],
438  *             [3, 0, 4, 7],
439  *             [4, 5, 6, 7],
440  *         ]
441  *     ], {
442  *         strokeWidth: 3,
443  *         fillOpacity: 0.6,
444  *         fillColorArray: ['blue', 'red', 'yellow'],
445  *         shader: {
446  *             enabled:false
447  *         }
448  *     });
449  *
450  *     })();
451  *
452  * </script><pre>
453  *
454  * @example
455  * var box = [-4, 4];
456  * var view = board.create(
457  *     'view3d',
458  *     [[-5, -3], [8, 8],
459  *     [box, box, box]],
460  *     {
461  *         projection: 'parallel',
462  *         trackball: { enabled: false },
463  *         depthOrder: {
464  *             enabled: true
465  *         },
466  *         xPlaneRear: { visible: false },
467  *         yPlaneRear: { visible: false },
468  *         zPlaneRear: { fillOpacity: 0.2 }
469  *     }
470  * );
471  * let rho = 1.6180339887;
472  * let vertexList = [
473  *     [0, -1, -rho], [0, +1, -rho], [0, -1, rho], [0, +1, rho],
474  *     [1, rho, 0], [-1, rho, 0], [1, -rho, 0], [-1, -rho, 0],
475  *     [-rho, 0, 1], [-rho, 0, -1], [rho, 0, 1], [rho, 0, -1]
476  * ];
477  * let faceArray = [
478  *     [4, 1, 11],
479  *     [11, 1, 0],
480  *     [6, 11, 0],
481  *     [0, 1, 9],
482  *     [11, 10, 4],
483  *     [9, 1, 5],
484  *     [8, 9, 5],
485  *     [5, 3, 8],
486  *     [6, 10, 11],
487  *     [2, 3, 10],
488  *     [2, 10, 6],
489  *     [8, 3, 2],
490  *     [3, 4, 10],
491  *     [7, 8, 2],
492  *     [9, 8, 7],
493  *     [0, 9, 7],
494  *     [4, 3, 5],
495  *     [5, 1, 4],
496  *     [0, 7, 6],
497  *     [7, 2, 6]
498  * ];
499  * var ico = view.create('polyhedron3d', [vertexList, faceArray], {
500  * fillColorArray: [],
501  * fillOpacity: 1,
502  * strokeWidth: 0.1,
503  * layer: 12,
504  * shader: {
505  *     enabled: true,
506  *     type: 'angle',
507  *     hue: 60,
508  *     saturation: 90,
509  *     minlightness: 60,
510  *     maxLightness: 80
511  * }
512  * });
513  *
514  * </pre><div id="JXGfea93484-96e9-4eb5-9e45-bb53d612aead" class="jxgbox" style="width: 300px; height: 300px;"></div>
515  * <script type="text/javascript">
516  *     (function() {
517  *         var board = JXG.JSXGraph.initBoard('JXGfea93484-96e9-4eb5-9e45-bb53d612aead',
518  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
519  *     var box = [-4, 4];
520  *     var view = board.create(
521  *         'view3d',
522  *         [[-5, -3], [8, 8],
523  *         [box, box, box]],
524  *         {
525  *             projection: 'parallel',
526  *             trackball: { enabled: false },
527  *             depthOrder: {
528  *                 enabled: true
529  *             },
530  *             xPlaneRear: { visible: false },
531  *             yPlaneRear: { visible: false },
532  *             zPlaneRear: { fillOpacity: 0.2 }
533  *         }
534  *     );
535  *     let rho = 1.6180339887;
536  *     let vertexList = [
537  *     [0, -1, -rho], [0, +1, -rho], [0, -1, rho], [0, +1, rho],
538  *     [1, rho, 0], [-1, rho, 0], [1, -rho, 0], [-1, -rho, 0],
539  *     [-rho, 0, 1], [-rho, 0, -1], [rho, 0, 1], [rho, 0, -1]
540  *     ];
541  *     let faceArray = [
542  *     [4, 1, 11],
543  *     [11, 1, 0],
544  *     [6, 11, 0],
545  *     [0, 1, 9],
546  *     [11, 10, 4],
547  *     [9, 1, 5],
548  *     [8, 9, 5],
549  *     [5, 3, 8],
550  *     [6, 10, 11],
551  *     [2, 3, 10],
552  *     [2, 10, 6],
553  *     [8, 3, 2],
554  *     [3, 4, 10],
555  *     [7, 8, 2],
556  *     [9, 8, 7],
557  *     [0, 9, 7],
558  *     [4, 3, 5],
559  *     [5, 1, 4],
560  *     [0, 7, 6],
561  *     [7, 2, 6]
562  *     ];
563  *     var ico = view.create('polyhedron3d', [vertexList, faceArray], {
564  *     fillColorArray: [],
565  *     fillOpacity: 1,
566  *     strokeWidth: 0.1,
567  *     layer: 12,
568  *     shader: {
569  *         enabled: true,
570  *         type: 'angle',
571  *         hue: 60,
572  *         saturation: 90,
573  *         minlightness: 60,
574  *         maxLightness: 80
575  *     }
576  *     });
577  *
578  *     })();
579  *
580  * </script><pre>
581  *
582  */
583 JXG.createPolyhedron3D = function (board, parents, attributes) {
584     var view = parents[0],
585         i, le,
586         face, f,
587         el,
588         attr, attr_polyhedron,
589         faceList = [],
590         base = null,
591         transform = null,
592 
593         polyhedron = {
594             view: view,
595             vertices: {},
596             coords: {},
597             coords2D: {},
598             zIndex: {},
599             faces: []
600         };
601 
602     if (Type.exists(parents[1].type) && parents[1].type === Const.OBJECT_TYPE_POLYHEDRON3D) {
603         // Polyhedron from baseElement and transformations
604         base = parents[1];
605         transform = parents[2];
606         polyhedron.vertices = base.def.vertices;
607         polyhedron.faces = base.def.faces;
608     } else {
609         // Copy vertices into a dict
610         if (Type.isArray(parents[1])) {
611             le = parents[1].length;
612             for (i = 0; i < le; i++) {
613                 polyhedron.vertices[i] = parents[1][i];
614             }
615         } else if (Type.isObject(parents[1])) {
616             for (i in parents[1]) {
617                 if (parents[1].hasOwnProperty(i)) {
618                     polyhedron.vertices[i] = parents[1][i];
619                 }
620             }
621         }
622         polyhedron.faces = parents[2];
623     }
624 
625     attr_polyhedron = Type.copyAttributes(attributes, board.options, "polyhedron3d");
626 
627     console.time('polyhedron');
628 
629     view.board.suspendUpdate();
630     // Create face3d elements
631     le = polyhedron.faces.length;
632     for (i = 0; i < le; i++) {
633         attr = Type.copyAttributes(attributes, board.options, "face3d");
634         if (attr_polyhedron.fillcolorarray.length > 0) {
635             attr.fillcolor = attr_polyhedron.fillcolorarray[i % attr_polyhedron.fillcolorarray.length];
636         }
637         f = polyhedron.faces[i];
638 
639         if (Type.isArray(f) && f.length === 2 && Type.isObject(f[1]) && Type.isArray(f[0])) {
640             // Handle case that face is of type [[points], {attr}]
641             Type.mergeAttr(attr, f[1]);
642             // Normalize face array, i.e. don't store attributes of that face in polyhedron
643             polyhedron.faces[i] = f[0];
644         }
645 
646         face = view.create('face3d', [polyhedron, i], attr);
647         faceList.push(face);
648     }
649     el = new JXG.Polyhedron3D(view, polyhedron, faceList, attr_polyhedron);
650     el.setParents(el); // Sets el as parent to all faces.
651     for (i = 0; i < le; i++) {
652         el.inherits.push(el.faces[i]);
653         el.addChild(el.faces[i]);
654     }
655     if (base !== null) {
656         el.addTransform(base, transform);
657         el.addParents(base);
658     }
659     view.board.unsuspendUpdate();
660 
661     console.timeEnd('polyhedron');
662 
663     return el;
664 };
665 
666 JXG.registerElement("polyhedron3d", JXG.createPolyhedron3D);
667