1 /*
  2     Copyright 2008-2024
  3         Matthias Ehmann,
  4         Carsten Miller,
  5         Andreas Walter,
  6         Alfred Wassermann
  7 
  8     This file is part of JSXGraph.
  9 
 10     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 11 
 12     You can redistribute it and/or modify it under the terms of the
 13 
 14       * GNU Lesser General Public License as published by
 15         the Free Software Foundation, either version 3 of the License, or
 16         (at your option) any later version
 17       OR
 18       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 19 
 20     JSXGraph is distributed in the hope that it will be useful,
 21     but WITHOUT ANY WARRANTY; without even the implied warranty of
 22     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 23     GNU Lesser General Public License for more details.
 24 
 25     You should have received a copy of the GNU Lesser General Public License and
 26     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 27     and <https://opensource.org/licenses/MIT/>.
 28  */
 29 /*global JXG:true, define: true*/
 30 
 31 import JXG from "../jxg.js";
 32 import Const from "../base/constants.js";
 33 import Type from "../utils/type.js";
 34 import Mat from "../math/math.js";
 35 import Geometry from "../math/geometry.js";
 36 
 37 /**
 38  * A 3D point is the basic geometric element.
 39  * @class Creates a new 3D point object. Do not use this constructor to create a 3D point. Use {@link JXG.View3D#create} with
 40  * type {@link Point3D} instead.
 41  * @augments JXG.GeometryElement3D
 42  * @augments JXG.GeometryElement
 43  * @param {JXG.View3D} view The 3D view the point is drawn on.
 44  * @param {Function|Array} F Array of numbers, array of functions or function returning an array with defines the user coordinates of the point.
 45  * @param {JXG.GeometryElement3D} slide Object the 3D point should be bound to. If null, the point is a free point.
 46  * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and
 47  * {@link JXG.Options#elements}, and optional a name and an id.
 48  * @see JXG.Board#generateName
 49  */
 50 JXG.Point3D = function (view, F, slide, attributes) {
 51     this.constructor(view.board, attributes, Const.OBJECT_TYPE_POINT3D, Const.OBJECT_CLASS_3D);
 52     this.constructor3D(view, "point3d");
 53 
 54     this.board.finalizeAdding(this);
 55 
 56     // add the new point to its view's point list
 57     // if (view.visProp.depthorderpoints) {
 58     //     view.points.push(this);
 59     // }
 60 
 61     /**
 62      * Homogeneous coordinates of a Point3D, i.e. array of length 4 containing numbers: [w, x, y, z].
 63      * Usually, w=1 for finite points and w=0 for points which are infinitely far.
 64      * If coordinates of the point are supplied as functions, they are resolved in {@link Point3D#updateCoords} into numbers.
 65      *
 66      * @example
 67      *   p.coords;
 68      *
 69      * @name Point3D#coords
 70      * @type Array
 71      * @private
 72      */
 73     this.coords = [0, 0, 0, 0];
 74     this.initialCoords = [0, 0, 0, 0];
 75 
 76     /**
 77      * Function or array of functions or array of numbers defining the coordinates of the point, used in {@link updateCoords}.
 78      *
 79      * @name Point3D#F
 80      * @function
 81      * @private
 82      *
 83      * @see updateCoords
 84      */
 85     this.F = F;
 86 
 87     /**
 88      * Optional slide element, i.e. element the Point3D lives on.
 89      *
 90      * @example
 91      *   p.slide;
 92      *
 93      * @name Point3D#slide
 94      * @type JXG.GeometryElement3D
 95      * @default null
 96      * @private
 97      *
 98      */
 99     this.slide = slide;
100 
101     /**
102      * In case, the point is a glider, store the preimage of the coordinates in terms of the parametric definition of the host element.
103      * That is, if the host element `slide` is a curve, and the coordinates of the point are equal to `p` and `u = this.position[0]`, then
104      * `p = [slide.X(u), slide.Y(u), slide.Z(u)]`.
105      *
106      * @type Array
107      * @private
108      */
109     this.position = [];
110 
111     this._c2d = null;
112 
113     this.methodMap = Type.deepCopy(this.methodMap, {
114         // TODO
115     });
116 };
117 
118 JXG.Point3D.prototype = new JXG.GeometryElement();
119 Type.copyPrototypeMethods(JXG.Point3D, JXG.GeometryElement3D, "constructor3D");
120 
121 JXG.extend(
122     JXG.Point3D.prototype,
123     /** @lends JXG.Point3D.prototype */ {
124 
125         /**
126          * Get x-coordinate of a 3D point.
127          *
128          * @name X
129          * @memberOf Point3D
130          * @function
131          * @returns {Number}
132          *
133          * @example
134          *   p.X();
135          */
136         X: function () {
137             return this.coords[1];
138         },
139 
140         /**
141          * Get y-coordinate of a 3D point.
142          *
143          * @name Y
144          * @memberOf Point3D
145          * @function
146          * @returns Number
147          *
148          * @example
149          *   p.Y();
150          */
151         Y: function () {
152             return this.coords[2];
153         },
154 
155         /**
156          * Get z-coordinate of a 3D point.
157          *
158          * @name Z
159          * @memberOf Point3D
160          * @function
161          * @returns Number
162          *
163          * @example
164          *   p.Z();
165          */
166         Z: function () {
167             return this.coords[3];
168         },
169 
170         /**
171          * Get w-coordinate of a 3D point.
172          *
173          * @name W
174          * @memberOf Point3D
175          * @function
176          * @returns Number
177          *
178          * @example
179          *   p.W();
180          */
181         W: function () {
182             return this.coords[0];
183         },
184 
185         /**
186          * Update the array {@link JXG.Point3D#coords} containing the homogeneous coords.
187          *
188          * @name updateCoords
189          * @memberOf Point3D
190          * @function
191          * @returns {Object} Reference to the Point3D object
192          * @private
193          * @see GeometryElement3D#update()
194          * @example
195          *    p.updateCoords();
196          */
197         updateCoords: function () {
198             var i,
199                 s = 0;
200 
201             if (Type.isFunction(this.F)) {
202                 this.coords = Type.evaluate(this.F);
203                 if (this.coords.length === 3) {
204                     this.coords.unshift(1);
205                 }
206             } else {
207                 if (this.F.length === 3) {
208                     this.coords[0] = 1;
209                     s = 1;
210                 }
211                 for (i = 0; i < this.F.length; i++) {
212                     // Attention: if F is array of numbers, coords may not be updated.
213                     // Otherwise, dragging will not work anymore.
214                     if (Type.isFunction(this.F[i])) {
215                         this.coords[s + i] = Type.evaluate(this.F[i]);
216                     }
217                 }
218             }
219 
220             return this;
221         },
222 
223         /**
224          * Initialize the coords array.
225          *
226          * @private
227          * @returns {Object} Reference to the Point3D object
228          */
229         initCoords: function () {
230             var i,
231                 s = 0;
232 
233 
234             if (Type.isFunction(this.F)) {
235                 this.coords = Type.evaluate(this.F);
236                 if (this.coords.length === 3) {
237                     this.coords.unshift(1);
238                 }
239             } else {
240                 if (this.F.length === 3) {
241                     this.coords[0] = 1;
242                     s = 1;
243                 }
244                 for (i = 0; i < this.F.length; i++) {
245                     this.coords[s + i] = Type.evaluate(this.F[i]);
246                 }
247             }
248             this.initialCoords = this.coords.slice();
249 
250             return this;
251         },
252 
253         /**
254          * Normalize homogeneous coordinates such the the first coordinate (the w-coordinate is equal to 1 or 0)-
255          *
256          * @name normalizeCoords
257          * @memberOf Point3D
258          * @function
259          * @returns {Object} Reference to the Point3D object
260          * @private
261          * @example
262          *    p.normalizeCoords();
263          */
264         normalizeCoords: function () {
265             if (Math.abs(this.coords[0]) > 1.e-14) {
266                 this.coords[1] /= this.coords[0];
267                 this.coords[2] /= this.coords[0];
268                 this.coords[3] /= this.coords[0];
269                 this.coords[0] = 1.0;
270             }
271             return this;
272         },
273 
274         /**
275          * Set the position of a 3D point.
276          *
277          * @name setPosition
278          * @memberOf Point3D
279          * @function
280          * @param {Array} coords 3D coordinates. Either of the form [x,y,z] (Euclidean) or [w,x,y,z] (homogeneous).
281          * @param {Boolean} [noevent] If true, no events are triggered (TODO)
282          * @returns {Object} Reference to the Point3D object
283          *
284          * @example
285          *    p.setPosition([1, 3, 4]);
286          */
287         setPosition: function (coords, noevent) {
288             var c = this.coords;
289                 // oc = this.coords.slice(); // Copy of original values
290 
291             if (coords.length === 3) {
292                 // Euclidean coordinates
293                 c[0] = 1.0;
294                 c[1] = coords[0];
295                 c[2] = coords[1];
296                 c[3] = coords[2];
297             } else {
298                 // Homogeneous coordinates (normalized)
299                 c[0] = coords[0];
300                 c[1] = coords[1];
301                 c[2] = coords[2];
302                 c[3] = coords[2];
303                 this.normalizeCoords();
304             }
305 
306             // console.log(el.emitter, !noevent, oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3]);
307             // Not yet working TODO
308             // if (el.emitter && !noevent &&
309             //     (oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3])) {
310             //     this.triggerEventHandlers(['update3D'], [oc]);
311             // }
312             return this;
313         },
314 
315         // /**
316         //  * Add transformations to this element.
317         //  * @param {JXG.GeometryElement} el
318         //  * @param {JXG.Transformation|Array} transform Either one {@link JXG.Transformation}
319         //  * or an array of {@link JXG.Transformation}s.
320         //  * @returns {JXG.CoordsElement} Reference to itself.
321         //  */
322         addTransform: function (el, transform) {
323             this.addTransformGeneric(el, transform);
324             return this;
325         },
326 
327         updateTransform: function () {
328             var c, i;
329 
330             if (this.transformations.length === 0 || this.baseElement === null) {
331                 return this;
332             }
333 
334             if (this === this.baseElement) {
335                 c = this.initialCoords;
336             } else {
337                 c = this.baseElement.coords;
338             }
339             for (i = 0; i < this.transformations.length; i++) {
340                 this.transformations[i].update();
341                 c = Mat.matVecMult(this.transformations[i].matrix, c);
342             }
343             this.coords = c;
344 
345             return this;
346         },
347 
348         // Already documented in JXG.GeometryElement
349         update: function (drag) {
350             var c3d,         // Homogeneous 3D coordinates
351                 foot, res;
352 
353             if (
354                 this.element2D.draggable() &&
355                 Geometry.distance(this._c2d, this.element2D.coords.usrCoords) !== 0
356             ) {
357                 // Update is called from board.updateElements, e.g. after manipulating a
358                 // a slider or dragging a point.
359                 // Usually this followed by an update call using the other branch below.
360                 if (this.view.isVerticalDrag()) {
361                     // Drag the point in its vertical to the xy plane
362                     // If the point is outside of bbox3d,
363                     // c3d is already corrected.
364                     c3d = this.view.project2DTo3DVertical(this.element2D, this.coords);
365                 } else {
366                     // Drag the point in its xy plane
367                     foot = [1, 0, 0, this.coords[3]];
368                     c3d = this.view.project2DTo3DPlane(this.element2D, [1, 0, 0, 1], foot);
369                 }
370 
371                 if (c3d[0] !== 0) {
372                     // Check if c3d is inside of view.bbox3d
373                     // Otherwise, the coords are now corrected.
374                     res = this.view.project3DToCube(c3d);
375                     this.coords = res[0];
376 
377                     if (res[1]) {
378                         // The 3D coordinates have been corrected, now
379                         // also correct the 2D element.
380                         this.element2D.coords.setCoordinates(
381                             Const.COORDS_BY_USER,
382                             this.view.project3DTo2D(this.coords)
383                         );
384                     }
385                     if (this.slide) {
386                         this.coords = this.slide.projectCoords([this.X(), this.Y(), this.Z()], this.position);
387                         this.element2D.coords.setCoordinates(
388                             Const.COORDS_BY_USER,
389                             this.view.project3DTo2D(this.coords)
390                         );
391                     }
392                 }
393 
394             } else {
395                 // Update 2D point from its 3D view, e.g. when rotating the view
396                 this.updateCoords()
397                     .updateTransform();
398 
399                 if (this.slide) {
400                     this.coords = this.slide.projectCoords([this.X(), this.Y(), this.Z()], this.position);
401                 }
402                 c3d = this.coords;
403                 this.element2D.coords.setCoordinates(
404                     Const.COORDS_BY_USER,
405                     this.view.project3DTo2D(c3d)
406                 );
407                 this.zIndex = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3];
408             }
409             this._c2d = this.element2D.coords.usrCoords.slice();
410 
411             return this;
412         },
413 
414         // Already documented in JXG.GeometryElement
415         updateRenderer: function () {
416             this.needsUpdate = false;
417             return this;
418         },
419 
420         /**
421          * Check whether a point's position is finite, i.e. the first entry is not zero.
422          * @returns {Boolean} True if the first entry of the coordinate vector is not zero; false otherwise.
423          */
424         testIfFinite: function () {
425             return Math.abs(this.coords[0]) > 1.e-12 ? true : false;
426             // return Type.cmpArrays(this.coords, [0, 0, 0, 0]);
427         },
428 
429         /**
430          * Calculate the distance from one point to another. If one of the points is on the plane at infinity, return positive infinity.
431          * @param {JXG.Point3D} pt The point to which the distance is calculated.
432          * @returns {Number} The distance
433          */
434         distance: function (pt) {
435             var eps_sq = 1e-12,
436                 c_this = this.coords,
437                 c_pt = pt.coords;
438 
439             if (c_this[0] * c_this[0] > eps_sq && c_pt[0] * c_pt[0] > eps_sq) {
440                 return Mat.hypot(
441                     c_pt[1] - c_this[1],
442                     c_pt[2] - c_this[2],
443                     c_pt[3] - c_this[3]
444                 );
445             } else {
446                 return Number.POSITIVE_INFINITY;
447             }
448         },
449 
450         // Not yet working
451         __evt__update3D: function (oc) {}
452     }
453 );
454 
455 /**
456  * @class A Point3D object is defined by three coordinates [x,y,z], or a function returning an array with three numbers.
457  * Alternatively, all numbers can also be provided as functions returning a number.
458  *
459  * @pseudo
460  * @name Point3D
461  * @augments JXG.Point3D
462  * @constructor
463  * @throws {Exception} If the element cannot be constructed with the given parent
464  * objects an exception is thrown.
465  * @param {number,function_number,function_number,function_JXG.GeometryElement3D} x,y,z,[slide=undefined] The coordinates are given as x, y, z consisting of numbers or functions. If an optional 3D element "slide" is supplied, the point is a glider on that element.
466  * @param {array,function_JXG.GeometryElement3D} F,[slide=null] Alternatively, the coordinates can be supplied as
467  *  <ul>
468  *   <li>function returning an array [x,y,z] of length 3 of numbers or
469  *   <li>array arr=[x,y,z] of length 3 consisting of numbers
470  * </ul>
471  * If an optional 3D element "slide" is supplied, the point is a glider on that element.
472  *
473  * @example
474  *    var bound = [-5, 5];
475  *    var view = board.create('view3d',
476  *        [[-6, -3], [8, 8],
477  *        [bound, bound, bound]],
478  *        {});
479  *    var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 });
480  *    var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 3, fixed: true });
481  *    var w = view.create('point3d', [ () => p.X() + 3, () => p.Y(), () => p.Z() - 2], { name:'C', size: 3, fixed: true });
482  *
483  * </pre><div id="JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1" class="jxgbox" style="width: 300px; height: 300px;"></div>
484  * <script type="text/javascript">
485  *     (function() {
486  *         var board = JXG.JSXGraph.initBoard('JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1',
487  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
488  *         var bound = [-5, 5];
489  *         var view = board.create('view3d',
490  *             [[-6, -3], [8, 8],
491  *             [bound, bound, bound]],
492  *             {});
493  *         var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 });
494  *         var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 3 });
495  *         var w = view.create('point3d', [ () => p.X() + 3, () => p.Y(), () => p.Z() - 2], { name:'C', size: 3, fixed: true });
496  *     })();
497  *
498  * </script><pre>
499  *
500  * @example
501  *     // Glider on sphere
502  *     var view = board.create(
503  *         'view3d',
504  *         [[-6, -3], [8, 8],
505  *         [[-3, 3], [-3, 3], [-3, 3]]],
506  *         {
507  *             depthOrder: {
508  *                 enabled: true
509  *             },
510  *             projection: 'central',
511  *             xPlaneRear: {fillOpacity: 0.2, gradient: null},
512  *             yPlaneRear: {fillOpacity: 0.2, gradient: null},
513  *             zPlaneRear: {fillOpacity: 0.2, gradient: null}
514  *         }
515  *     );
516  *
517  *     // Two points
518  *     var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2});
519  *     var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2});
520  *
521  *     // Sphere
522  *     var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8});
523  *
524  *     // Glider on sphere
525  *     var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4});
526  *     var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 });
527  *
528  * </pre><div id="JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71" class="jxgbox" style="width: 300px; height: 300px;"></div>
529  * <script type="text/javascript">
530  *     (function() {
531  *         var board = JXG.JSXGraph.initBoard('JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71',
532  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
533  *         var view = board.create(
534  *             'view3d',
535  *             [[-6, -3], [8, 8],
536  *             [[-3, 3], [-3, 3], [-3, 3]]],
537  *             {
538  *                 depthOrder: {
539  *                     enabled: true
540  *                 },
541  *                 projection: 'central',
542  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
543  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
544  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
545  *             }
546  *         );
547  *
548  *         // Two points
549  *         var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2});
550  *         var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2});
551  *
552  *         // Sphere
553  *         var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8});
554  *
555  *         // Glider on sphere
556  *         var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4});
557  *         var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 });
558  *
559  *     })();
560  *
561  * </script><pre>
562  *
563  */
564 JXG.createPoint3D = function (board, parents, attributes) {
565     //   parents[0]: view
566     // followed by
567     //   parents[1]: function or array
568     // or
569     //   parents[1..3]: coordinates
570 
571     var view = parents[0],
572         attr, F, slide, c2d, el,
573         base = null,
574         transform = null;
575 
576     // If the last element of `parents` is a 3D object,
577     // the point is a glider on that element.
578     if (parents.length > 2 &&
579         Type.exists(parents[parents.length - 1].is3D) &&
580         !Type.isTransformationOrArray(parents[parents.length - 1])
581     ) {
582         slide = parents.pop();
583     } else {
584         slide = null;
585     }
586 
587     if (parents.length === 2) {
588         // [view, array|fun] (Array [x, y, z] | function) returning [x, y, z]
589         F = parents[1];
590     } else if (parents.length === 3 &&
591         Type.isPoint3D(parents[1]) &&
592         Type.isTransformationOrArray(parents[2])
593     ) {
594         F = [0, 0, 0];
595         base = parents[1];
596         transform = parents[2];
597     } else if (parents.length === 4) {
598         // [view, x, y, z], (3 numbers | functions)
599         F = parents.slice(1);
600     } else if (parents.length === 5) {
601         // [view, w, x, y, z], (4 numbers | functions)
602         F = parents.slice(1);
603     } else {
604         throw new Error(
605             "JSXGraph: Can't create point3d with parent types '" +
606                 typeof parents[1] +
607                 "' and '" +
608                 typeof parents[2] +
609                 "'." +
610                 "\nPossible parent types: [[x,y,z]], [x,y,z], or [[x,y,z], slide], () => [x, y, z], or [point, transformation(s)]"
611         );
612         //  "\nPossible parent types: [[x,y,z]], [x,y,z], [element,transformation]"); // TODO
613     }
614 
615     attr = Type.copyAttributes(attributes, board.options, 'point3d');
616     el = new JXG.Point3D(view, F, slide, attr);
617     el.initCoords();
618     if (base !== null && transform !== null) {
619         el.addTransform(base, transform);
620     }
621 
622     c2d = view.project3DTo2D(el.coords);
623 
624     attr = el.setAttr2D(attr);
625     el.element2D = view.create('point', c2d, attr);
626     el.element2D.view = view;
627     el.addChild(el.element2D);
628     el.inherits.push(el.element2D);
629     el.element2D.setParents(el);
630 
631     // If this point is a glider, record that in the update tree
632     if (el.slide) {
633         el.slide.addChild(el);
634         el.setParents(el.slide);
635     }
636     if (base) {
637         el.setParents(base);
638     }
639 
640     el._c2d = el.element2D.coords.usrCoords.slice(); // Store a copy of the coordinates to detect dragging
641 
642     return el;
643 };
644 
645 JXG.registerElement("point3d", JXG.createPoint3D);
646