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