1 /*
  2     Copyright 2008-2023
  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 linear spaces of dimension at least one,
 33  * i.e. lines and planes.
 34  */
 35 import JXG from '../jxg';
 36 import Const from '../base/constants';
 37 import Type from '../utils/type';
 38 import Mat from '../math/math';
 39 import Geometry from '../math/geometry';
 40 
 41 // -----------------------
 42 //  Lines
 43 // -----------------------
 44 
 45 /**
 46  * Constructor for 3D lines.
 47  * @class Creates a new 3D line object. Do not use this constructor to create a 3D line. Use {@link JXG.View3D#create} with type {@link Line3D} instead.
 48  *
 49  * @augments JXG.GeometryElement3D
 50  * @augments JXG.GeometryElement
 51  * @param {View3D} view
 52  * @param {Point3D|Array} point
 53  * @param {Array} direction
 54  * @param {Array} range
 55  * @param {Object} attributes
 56  * @see JXG.Board#generateName
 57  */
 58 JXG.Line3D = function (view, point, direction, range, attributes) {
 59     this.constructor(view.board, attributes, Const.OBJECT_TYPE_LINE3D, Const.OBJECT_CLASS_3D);
 60     this.constructor3D(view, 'line3d');
 61 
 62     this.board.finalizeAdding(this);
 63 
 64     /**
 65      * 3D point which - together with a direction - defines the line.
 66      * @type JXG.Point3D
 67      *
 68      * @see JXG.Line3D#direction
 69      */
 70     this.point = point;
 71 
 72     /**
 73      * Direction which - together with a point - defines the line. Array of numbers or functions (of length 3) or function
 74      * returning array of length 3.
 75      *
 76      * @type Array|Function
 77      * @see JXG.Line3D#point
 78      */
 79     this.direction = direction;
 80 
 81     /**
 82      * Range [r1, r2] of the line. r1, r2 can be numbers or functions.
 83      * The 3D line goes from (point + r1 * direction) to (point + r2 * direction)
 84      * @type Array
 85      */
 86     this.range = range || [-Infinity, Infinity];
 87 
 88     /**
 89      * Starting point of the 3D line
 90      * @type JXG.Point3D
 91      * @private
 92      */
 93     this.point1 = null;
 94 
 95     /**
 96      * End point of the 3D line
 97      * @type JXG.Point3D
 98      * @private
 99      */
100     this.point2 = null;
101 
102     this.methodMap = Type.deepCopy(this.methodMap, {
103         // TODO
104     });
105 };
106 JXG.Line3D.prototype = new JXG.GeometryElement();
107 Type.copyPrototypeMethods(JXG.Line3D, JXG.GeometryElement3D, 'constructor3D');
108 
109 JXG.extend(
110     JXG.Line3D.prototype,
111     /** @lends JXG.Line3D.prototype */ {
112         /**
113          * Determine one end point of a 3D line from point, direction and range.
114          *
115          * @param {Number|function} r
116          * @private
117          * @returns Array
118          */
119         getPointCoords: function (r) {
120             var p = [],
121                 d = [],
122                 i,
123                 r0;
124 
125             p = [this.point.X(), this.point.Y(), this.point.Z()];
126 
127             if (Type.isFunction(this.direction)) {
128                 d = this.direction();
129             } else {
130                 for (i = 1; i < 4; i++) {
131                     d.push(Type.evaluate(this.direction[i]));
132                 }
133             }
134 
135             r0 = Type.evaluate(r);
136             // TODO: test also in the finite case
137             if (Math.abs(r0) === Infinity) {
138                 r = this.view.intersectionLineCube(p, d, r0);
139             }
140 
141             return [p[0] + d[0] * r0, p[1] + d[1] * r0, p[2] + d[2] * r0];
142         },
143 
144         update: function () {
145             return this;
146         },
147 
148         updateRenderer: function () {
149             this.needsUpdate = false;
150             return this;
151         }
152     }
153 );
154 
155 /**
156  * @class This element is used to provide a constructor for a 3D line.
157  * @pseudo
158  * @description There are two possibilities to create a Line3D object.
159  * <p>
160  * First: the line in 3D is defined by two points in 3D (Point3D).
161  * The points can be either existing points or coordinate arrays of
162  * the form [x, y, z].
163  * <p>Second: the line in 3D is defined by a point (or coordinate array [x, y, z])
164  * a direction given as array [x, y, z] and an optional range
165  * given as array [s, e]. The default value for the range is [-Infinity, Infinity].
166  * <p>
167  * All numbers can also be provided as functions returning a number.
168  *
169  * @name Line3D
170  * @augments JXG.GeometryElement3D
171  * @constructor
172  * @type JXG.Line3D
173  * @throws {Exception} If the element cannot be constructed with the given parent
174  * objects an exception is thrown.
175  * @param {JXG.Point3D,array,function_JXG.Point3D,array,function} point1,point2 First and second defining point.
176  * @param {JXG.Point3D,array,function_array,function_array,function} point,direction,range Alternatively, point, direction and range can be supplied.
177  * <ul>
178  * <li> point: Point3D or array of length 3
179  * <li> direction: array of length 3 or function returning an array of numbers or function returning an array
180  * <li> range: array of length 2, elements can also be functions.
181  * </ul>
182  *
183  * @example
184  *     var bound = [-5, 5];
185  *     var view = board.create('view3d',
186  *         [[-6, -3], [8, 8],
187  *         [bound, bound, bound]],
188  *         {});
189  *     var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 });
190  *     // Lines through 2 points
191  *     var l1 = view.create('line3d', [[1, 3, 3], [-3, -3, -3]], {point1: {visible: true}, point2: {visible: true} });
192  *     var l2 = view.create('line3d', [p, l1.point1]);
193  *
194  *     // Line by point, direction, range
195  *     var l3 = view.create('line3d', [p, [0, 0, 1], [-2, 4]]);
196  *
197  * </pre><div id='JXG05f9baa4-6059-4502-8911-6a934f823b3d' class='jxgbox' style='width: 300px; height: 300px;'></div>
198  * <script type='text/javascript'>
199  *     (function() {
200  *         var board = JXG.JSXGraph.initBoard('JXG05f9baa4-6059-4502-8911-6a934f823b3d',
201  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
202  *         var bound = [-5, 5];
203  *         var view = board.create('view3d',
204  *             [[-6, -3], [8, 8],
205  *             [bound, bound, bound]],
206  *             {});
207  *         var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 });
208  *         // Lines through 2 points
209  *         var l1 = view.create('line3d', [[1, 3, 3], [-3, -3, -3]], {name: 'll1', point1: {visible: true}, point2: {visible: true} });
210  *         var l2 = view.create('line3d', [p, l1.point1]);
211  *         // Line by point, direction, range
212  *         var l3 = view.create('line3d', [p, [0, 0, 1], [-2, 4]]);
213  *     })();
214  *
215  * </script><pre>
216  *
217  */
218 JXG.createLine3D = function (board, parents, attributes) {
219     var view = parents[0],
220         attr, points,
221         point, direction, range,
222         point1, point2, el;
223 
224     attr = Type.copyAttributes(attributes, board.options, 'line3d');
225 
226     // In any case, parents[1] contains a point or point coordinates
227 
228     if (
229         Type.isPoint3D(parents[2]) ||
230         (parents.length === 3 && (Type.isArray(parents[2]) || Type.isFunction(parents[2])))
231     ) {
232         // Line defined by two points; [view, point1, point2]
233 
234         point1 = Type.providePoints3D(view, [parents[1]], attributes, 'line3d', ['point1'])[0];
235         point2 = Type.providePoints3D(view, [parents[2]], attributes, 'line3d', ['point2'])[0];
236         direction = function () {
237             return [point2.X() - point1.X(), point2.Y() - point1.Y(), point2.Z() - point1.Z()];
238         };
239         range = [0, 1];
240         el = new JXG.Line3D(view, point1, direction, range, attr);
241     } else {
242         // Line defined by point, direction and range
243         point = Type.providePoints3D(view, [parents[1]], attributes, 'line3d', ['point'])[0];
244 
245         // Directions are handled as arrays of length 4,
246         // i.e. with homogeneous coordinates.
247         if (Type.isFunction(parents[2])) {
248             direction = parents[2];
249         } else if (parents[2].length === 3) {
250             direction = [1].concat(parents[2]);
251         } else if (parents[2].length === 4) {
252             direction = parents[2];
253         } else {
254             // TODO Throw error
255         }
256         range = parents[3];
257 
258         points = Type.providePoints3D(
259             view,
260             [
261                 [0, 0, 0],
262                 [0, 0, 0]
263             ],
264             attributes,
265             'line3d',
266             ['point1', 'point2']
267         );
268 
269         // Create a line3d with two dummy points
270         el = new JXG.Line3D(view, point, direction, range, attr);
271 
272         // Now set the real points which define the line
273         /**
274          * @class
275          * @ignore
276          */
277         points[0].F = function () {
278             return el.getPointCoords(Type.evaluate(el.range[0]));
279         };
280         points[0].prepareUpdate().update();
281         point1 = points[0];
282 
283         /**
284          * @class
285          * @ignore
286          */
287         points[1].F = function () {
288             return el.getPointCoords(Type.evaluate(el.range[1]));
289         };
290         points[1].prepareUpdate().update();
291         point2 = points[1];
292     }
293     // TODO Throw error
294 
295     attr = el.setAttr2D(attr);
296     el.element2D = view.create('segment', [point1.element2D, point2.element2D], attr);
297     el.addChild(el.element2D);
298     el.inherits.push(el.element2D);
299     el.element2D.setParents(el);
300     // el.setParents([point1.id, point2.id]);
301 
302     el.point1 = point1;
303     el.point2 = point2;
304     if (el.point1._is_new) {
305         el.addChild(el.point1);
306         delete el.point1._is_new;
307     } else {
308         el.point1.addChild(el);
309     }
310     if (el.point2._is_new) {
311         el.addChild(el.point2);
312         delete el.point2._is_new;
313     } else {
314         el.point2.addChild(el);
315     }
316     if (Type.exists(point)) {
317         if (point._is_new) {
318             el.addChild(point);
319             delete point._is_new;
320         } else {
321             point.addChild(el);
322         }
323     }
324 
325     el.update();
326     el.element2D.prepareUpdate().update().updateRenderer();
327     return el;
328 };
329 JXG.registerElement('line3d', JXG.createLine3D);
330 
331 // -----------------------
332 //  Planes
333 // -----------------------
334 
335 /**
336  * Constructor for 3D planes.
337  * @class Creates a new 3D plane object. Do not use this constructor to create a 3D plane. Use {@link JXG.Board#create} with type {@link Plane3D} instead.
338  *
339  * @augments JXG.GeometryElement3D
340  * @augments JXG.GeometryElement
341  * @param {View3D} view
342  * @param {Point3D|Array} point
343  * @param {Array} direction1
344  * @param {Array} range1
345  * @param {Array} direction2
346  * @param {Array} range2
347  * @param {Object} attributes
348  * @see JXG.Board#generateName
349  */
350 JXG.Plane3D = function (view, point, dir1, range1, dir2, range2, attributes) {
351     this.constructor(view.board, attributes, Const.OBJECT_TYPE_PLANE3D, Const.OBJECT_CLASS_3D);
352     this.constructor3D(view, 'plane3d');
353 
354     this.board.finalizeAdding(this);
355 
356     /**
357      * 3D point which - together with two direction vectors - defines the plane.
358      *
359      * @type JXG.Point3D
360      *
361      * @see JXG.3D#direction1
362      * @see JXG.3D#direction2
363      */
364     this.point = point;
365 
366     /**
367      * Two linearly independent vectors - together with a point - define the plane. Each of these direction vectors is an
368      * array of numbers or functions (of length 3) or function returning array of length 3.
369      *
370      * @type Array|Function
371      *
372      * @see JXG.Plane3D#point
373      * @see JXG.Plane3D#direction2
374      */
375     this.direction1 = dir1;
376 
377     /**
378      * Two linearly independent vectors - together with a point - define the plane. Each of these direction vectors is an
379      * array of numbers or functions (of length 3) or function returning array of length 3.
380      *
381      * @type Array|Function
382      * @see JXG.Plane3D#point
383      * @see JXG.Plane3D#direction1
384      */
385     this.direction2 = dir2;
386 
387     /**
388      * Range [r1, r2] of {@link direction1}. The 3D line goes from (point + r1 * direction1) to (point + r2 * direction1)
389      * @type {Array}
390      */
391     this.range1 = range1 || [-Infinity, Infinity];
392 
393     /**
394      * Range [r1, r2] of {@link direction2}. The 3D line goes from (point + r1 * direction2) to (point + r2 * direction2)
395      * @type {Array}
396      */
397     this.range2 = range2 || [-Infinity, Infinity];
398 
399     /**
400      * Direction vector 1 of the 3D plane. Contains the evaluated coordinates from {@link direction1} and {@link range1}.
401      * @type Array
402      * @private
403      *
404      * @see updateNormal
405      */
406     this.vec1 = [0, 0, 0];
407 
408     /**
409      * Direction vector 2 of the 3D plane. Contains the evaluated coordinates from {@link direction2} and {@link range2}.
410      *
411      * @type Array
412      * @private
413      *
414      * @see updateNormal
415      */
416     this.vec2 = [0, 0, 0];
417 
418     this.grid = null;
419 
420     /**
421          * Normal vector of the plane. Left hand side of the Hesse normal form.
422 
423         * @type Array
424          * @private
425          *
426          * @see updateNormal
427          *
428          */
429     this.normal = [0, 0, 0];
430 
431     /**
432          * Right hand side of the Hesse normal form.
433 
434         * @type Array
435          * @private
436          *
437          * @see updateNormal
438          *
439          */
440     this.d = 0;
441 
442     this.updateNormal();
443 
444     this.methodMap = Type.deepCopy(this.methodMap, {
445         // TODO
446     });
447 };
448 JXG.Plane3D.prototype = new JXG.GeometryElement();
449 Type.copyPrototypeMethods(JXG.Plane3D, JXG.GeometryElement3D, 'constructor3D');
450 
451 JXG.extend(
452     JXG.Plane3D.prototype,
453     /** @lends JXG.Plane3D.prototype */ {
454         /**
455          * Update the Hesse normal form of the plane, i.e. update normal vector and right hand side.
456          * Updates also {@link vec1} and {@link vec2}.
457          *
458          * @name JXG.Plane3D#updateNormal
459          * @function
460          * @returns {Object} Reference to the Plane3D object
461          * @private
462          * @example
463          *    plane.updateNormal();
464          *
465          */
466         updateNormal: function () {
467             var i, len;
468             for (i = 0; i < 3; i++) {
469                 this.vec1[i] = Type.evaluate(this.direction1[i]);
470                 this.vec2[i] = Type.evaluate(this.direction2[i]);
471             }
472 
473             this.normal = Mat.crossProduct(this.vec1, this.vec2);
474 
475             len = Mat.norm(this.normal);
476             if (Math.abs(len) > Mat.eps) {
477                 for (i = 0; i < 3; i++) {
478                     this.normal[i] /= len;
479                 }
480             }
481             this.d = Mat.innerProduct(this.point.coords.slice(1), this.normal, 3);
482 
483             return this;
484         },
485 
486         updateDataArray: function () {
487             var s1, e1, s2, e2, c2d, l1, l2,
488                 planes = ['xPlaneRear', 'yPlaneRear', 'zPlaneRear'],
489                 points = [],
490                 v1 = [0, 0, 0],
491                 v2 = [0, 0, 0],
492                 q = [0, 0, 0],
493                 p = [0, 0, 0],
494                 d, i, j, a, b, first, pos, pos_akt,
495                 view = this.view;
496 
497             this.dataX = [];
498             this.dataY = [];
499 
500             this.updateNormal();
501 
502             // Infinite plane
503             if (
504                 this.elType !== 'axisplane3d' &&
505                 view.defaultAxes &&
506                 Type.evaluate(this.range1[0]) === -Infinity &&
507                 Type.evaluate(this.range1[1]) === Infinity &&
508                 Type.evaluate(this.range2[0]) === -Infinity &&
509                 Type.evaluate(this.range2[1]) === Infinity
510             ) {
511                 // Start with the rear plane.
512                 // Determine the intersections with the view bbox3d
513                 // For each face of the bbox3d we determine two points
514                 // which are the ends of the intersection line.
515                 // We start with the three rear planes.
516                 for (j = 0; j < planes.length; j++) {
517                     p = view.intersectionPlanePlane(this, view.defaultAxes[planes[j]]);
518 
519                     if (p[0].length === 3 && p[1].length === 3) {
520                         // This test is necessary to filter out intersection lines which are
521                         // identical to intersections of axis planes (they would occur twice).
522                         for (i = 0; i < points.length; i++) {
523                             if (
524                                 (Geometry.distance(p[0], points[i][0], 3) < Mat.eps &&
525                                     Geometry.distance(p[1], points[i][1], 3) < Mat.eps) ||
526                                 (Geometry.distance(p[0], points[i][1], 3) < Mat.eps &&
527                                     Geometry.distance(p[1], points[i][0], 3) < Mat.eps)
528                             ) {
529                                 break;
530                             }
531                         }
532                         if (i === points.length) {
533                             points.push(p.slice());
534                         }
535                     }
536 
537                     // Point on the front plane of the bbox3d
538                     p = [0, 0, 0];
539                     p[j] = view.bbox3D[j][1];
540 
541                     // d is the rhs of the Hesse normal form of the front plane.
542                     d = Mat.innerProduct(p, view.defaultAxes[planes[j]].normal, 3);
543                     p = view.intersectionPlanePlane(this, view.defaultAxes[planes[j]], d);
544 
545                     if (p[0].length === 3 && p[1].length === 3) {
546                         // Do the same test as above
547                         for (i = 0; i < points.length; i++) {
548                             if (
549                                 (Geometry.distance(p[0], points[i][0], 3) < Mat.eps &&
550                                     Geometry.distance(p[1], points[i][1], 3) < Mat.eps) ||
551                                 (Geometry.distance(p[0], points[i][1], 3) < Mat.eps &&
552                                     Geometry.distance(p[1], points[i][0], 3) < Mat.eps)
553                             ) {
554                                 break;
555                             }
556                         }
557                         if (i === points.length) {
558                             points.push(p.slice());
559                         }
560                     }
561                 }
562 
563                 // Concatenate the intersection points to a polygon.
564                 // If all wents well, each intersection should appear
565                 // twice in the list.
566                 first = 0;
567                 pos = first;
568                 i = 0;
569                 do {
570                     p = points[pos][i];
571                     if (p.length === 3) {
572                         c2d = view.project3DTo2D(p);
573                         this.dataX.push(c2d[1]);
574                         this.dataY.push(c2d[2]);
575                     }
576                     i = (i + 1) % 2;
577                     p = points[pos][i];
578 
579                     pos_akt = pos;
580                     for (j = 0; j < points.length; j++) {
581                         if (j !== pos && Geometry.distance(p, points[j][0]) < Mat.eps) {
582                             pos = j;
583                             i = 0;
584                             break;
585                         }
586                         if (j !== pos && Geometry.distance(p, points[j][1]) < Mat.eps) {
587                             pos = j;
588                             i = 1;
589                             break;
590                         }
591                     }
592                     if (pos === pos_akt) {
593                         console.log('Error: update plane3d: did not find next', pos);
594                         break;
595                     }
596                 } while (pos !== first);
597 
598                 c2d = view.project3DTo2D(points[first][0]);
599                 this.dataX.push(c2d[1]);
600                 this.dataY.push(c2d[2]);
601             } else {
602                 // 3D bounded flat
603                 s1 = Type.evaluate(this.range1[0]);
604                 e1 = Type.evaluate(this.range1[1]);
605                 s2 = Type.evaluate(this.range2[0]);
606                 e2 = Type.evaluate(this.range2[1]);
607 
608                 q = this.point.coords.slice(1);
609 
610                 v1 = this.vec1.slice();
611                 v2 = this.vec2.slice();
612                 l1 = Mat.norm(v1, 3);
613                 l2 = Mat.norm(v2, 3);
614                 for (i = 0; i < 3; i++) {
615                     v1[i] /= l1;
616                     v2[i] /= l2;
617                 }
618 
619                 for (j = 0; j < 4; j++) {
620                     switch (j) {
621                         case 0:
622                             a = s1;
623                             b = s2;
624                             break;
625                         case 1:
626                             a = e1;
627                             b = s2;
628                             break;
629                         case 2:
630                             a = e1;
631                             b = e2;
632                             break;
633                         case 3:
634                             a = s1;
635                             b = e2;
636                     }
637                     for (i = 0; i < 3; i++) {
638                         p[i] = q[i] + a * v1[i] + b * v2[i];
639                     }
640                     c2d = view.project3DTo2D(p);
641                     this.dataX.push(c2d[1]);
642                     this.dataY.push(c2d[2]);
643                 }
644                 // Close the curve
645                 this.dataX.push(this.dataX[0]);
646                 this.dataY.push(this.dataY[0]);
647             }
648             return { X: this.dataX, Y: this.dataY };
649         },
650 
651         update: function () {
652             return this;
653         },
654 
655         updateRenderer: function () {
656             this.needsUpdate = false;
657             return this;
658         }
659     }
660 );
661 
662 // TODO docs
663 JXG.createPlane3D = function (board, parents, attributes) {
664     var view = parents[0],
665         attr,
666         point,
667         dir1 = parents[2],
668         dir2 = parents[3],
669         range1 = parents[4] || [-Infinity, Infinity],
670         range2 = parents[5] || [-Infinity, Infinity],
671         el,
672         grid;
673 
674     point = Type.providePoints3D(view, [parents[1]], attributes, 'plane3d', ['point'])[0];
675     if (point === false) {
676         // TODO Throw error
677     }
678 
679     attr = Type.copyAttributes(attributes, board.options, 'plane3d');
680     el = new JXG.Plane3D(view, point, dir1, range1, dir2, range2, attr);
681     point.addChild(el);
682 
683     attr = el.setAttr2D(attr);
684     el.element2D = view.create('curve', [[], []], attr);
685 
686     /**
687      * @class
688      * @ignore
689      */
690     el.element2D.updateDataArray = function () {
691         var ret = el.updateDataArray();
692         this.dataX = ret.X;
693         this.dataY = ret.Y;
694     };
695     el.addChild(el.element2D);
696     el.inherits.push(el.element2D);
697     el.element2D.setParents(el);
698 
699     attr = Type.copyAttributes(attributes.mesh3d, board.options, 'mesh3d');
700     if (
701         Math.abs(el.range1[0]) !== Infinity &&
702         Math.abs(el.range1[1]) !== Infinity &&
703         Math.abs(el.range2[0]) !== Infinity &&
704         Math.abs(el.range2[1]) !== Infinity
705     ) {
706         grid = view.create('mesh3d', [
707                 function () {
708                     return point.coords;
709                 },
710                 dir1, range1, dir2, range2
711             ], attr
712         );
713         el.grid = grid;
714         el.addChild(grid);
715         el.inherits.push(grid);
716         grid.setParents(el);
717     }
718 
719     el.element2D.prepareUpdate().update();
720     if (!board.isSuspendedUpdate) {
721         el.element2D.updateVisibility().updateRenderer();
722     }
723 
724     return el;
725 };
726 JXG.registerElement('plane3d', JXG.createPlane3D);
727