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