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 Mat from "../math/math.js";
 34 import Geometry from "../math/geometry.js";
 35 import Type from "../utils/type.js";
 36 //, GeometryElement3D) {
 37 
 38 /**
 39  * A 3D point is the basic geometric element.
 40  * @class Creates a new 3D point object. Do not use this constructor to create a 3D point. Use {@link JXG.View3D#create} with
 41  * type {@link Point3D} instead.
 42  * @augments JXG.GeometryElement3D
 43  * @augments JXG.GeometryElement
 44  * @param {JXG.View3D} view The 3D view the point is drawn on.
 45  * @param {Function|Array} F Array of numbers, array of functions or function returning an array with defines the user coordinates of the point.
 46  * @param {JXG.GeometryElement3D} slide Object the 3D point should be bound to. If null, the point is a free point.
 47  * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and
 48  * {@link JXG.Options#elements}, and optional a name and an id.
 49  * @see JXG.Board#generateName
 50  */
 51 JXG.Point3D = function (view, F, slide, attributes) {
 52     this.constructor(view.board, attributes, Const.OBJECT_TYPE_POINT3D, Const.OBJECT_CLASS_3D);
 53     this.constructor3D(view, "point3d");
 54 
 55     this.board.finalizeAdding(this);
 56 
 57     // add the new point to its view's point list
 58     if (view.visProp.depthorderpoints) {
 59         view.points.push(this);
 60     }
 61 
 62     /**
 63      * Homogeneous coordinates of a Point3D, i.e. array of length 4: [w, x, y, z]. Usually, w=1 for finite points and w=0 for points
 64      * which are infinitely far.
 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 
 75     /**
 76      * Function or array of functions or array of numbers defining the coordinates of the point, used in {@link updateCoords}.
 77      *
 78      * @name Point3D#F
 79      * @function
 80      * @private
 81      *
 82      * @see updateCoords
 83      */
 84     this.F = F;
 85 
 86     /**
 87      * Optional slide element, i.e. element the Point3D lives on.
 88      *
 89      * @example
 90      *   p.slide;
 91      *
 92      * @name Point3D#slide
 93      * @type JXG.GeometryElement3D
 94      * @default null
 95      * @private
 96      *
 97      */
 98     this.slide = slide;
 99 
100     /**
101      * Get x-coordinate of a 3D point.
102      *
103      * @name X
104      * @memberOf Point3D
105      * @function
106      * @returns {Number}
107      *
108      * @example
109      *   p.X();
110      */
111     this.X = function () {
112         return this.coords[1];
113     };
114 
115     /**
116      * Get y-coordinate of a 3D point.
117      *
118      * @name Y
119      * @memberOf Point3D
120      * @function
121      * @returns Number
122      *
123      * @example
124      *   p.Y();
125      */
126     this.Y = function () {
127         return this.coords[2];
128     };
129 
130     /**
131      * Get z-coordinate of a 3D point.
132      *
133      * @name Z
134      * @memberOf Point3D
135      * @function
136      * @returns Number
137      *
138      * @example
139      *   p.Z();
140      */
141     this.Z = function () {
142         return this.coords[3];
143     };
144 
145     /**
146      * Store the last position of the 2D point for the optimizer.
147      *
148      * @type Array
149      * @private
150      */
151     this._params = [];
152 
153     this._c2d = null;
154 
155     this.methodMap = Type.deepCopy(this.methodMap, {
156         // TODO
157     });
158 };
159 JXG.Point3D.prototype = new JXG.GeometryElement();
160 Type.copyPrototypeMethods(JXG.Point3D, JXG.GeometryElement3D, "constructor3D");
161 
162 JXG.extend(
163     JXG.Point3D.prototype,
164     /** @lends JXG.Point3D.prototype */ {
165         /**
166          * Update the homogeneous coords array.
167          *
168          * @name updateCoords
169          * @memberOf Point3D
170          * @function
171          * @returns {Object} Reference to the Point3D object
172          * @private
173          * @example
174          *    p.updateCoords();
175          */
176         updateCoords: function () {
177             var i;
178 
179             if (Type.isFunction(this.F)) {
180                 // this.coords = [1].concat(Type.evaluate(this.F));
181                 this.coords = Type.evaluate(this.F);
182                 this.coords.unshift(1);
183             } else {
184                 this.coords[0] = 1;
185                 for (i = 0; i < 3; i++) {
186                     // Attention: if F is array of numbers, coords are not updated.
187                     // Otherwise, dragging will not work anymore.
188                     if (Type.isFunction(this.F[i])) {
189                         this.coords[i + 1] = Type.evaluate(this.F[i]);
190                     }
191                 }
192             }
193             return this;
194         },
195 
196         /**
197          * Initialize the coords array.
198          *
199          * @private
200          * @returns {Object} Reference to the Point3D object
201          */
202         initCoords: function () {
203             var i;
204 
205             if (Type.isFunction(this.F)) {
206                 // this.coords = [1].concat(Type.evaluate(this.F));
207                 this.coords = Type.evaluate(this.F);
208                 this.coords.unshift(1);
209             } else {
210                 this.coords[0] = 1;
211                 for (i = 0; i < 3; i++) {
212                     this.coords[i + 1] = Type.evaluate(this.F[i]);
213                 }
214             }
215             return this;
216         },
217 
218         /**
219          * Normalize homogeneous coordinates such the the first coordinate (the w-coordinate is equal to 1 or 0)-
220          *
221          * @name normalizeCoords
222          * @memberOf Point3D
223          * @function
224          * @returns {Object} Reference to the Point3D object
225          * @private
226          * @example
227          *    p.normalizeCoords();
228          */
229         normalizeCoords: function () {
230             if (Math.abs(this.coords[0]) > Mat.eps) {
231                 this.coords[1] /= this.coords[0];
232                 this.coords[2] /= this.coords[0];
233                 this.coords[3] /= this.coords[0];
234                 this.coords[0] = 1.0;
235             }
236             return this;
237         },
238 
239         /**
240          * Set the position of a 3D point.
241          *
242          * @name setPosition
243          * @memberOf Point3D
244          * @function
245          * @param {Array} coords 3D coordinates. Either of the form [x,y,z] (Euclidean) or [w,x,y,z] (homogeneous).
246          * @param {Boolean} [noevent] If true, no events are triggered.
247          * @returns {Object} Reference to the Point3D object
248          *
249          * @example
250          *    p.setPosition([1, 3, 4]);
251          */
252         setPosition: function (coords, noevent) {
253             var c = this.coords;
254                 // oc = this.coords.slice(); // Copy of original values
255 
256             if (coords.length === 3) {
257                 // Euclidean coordinates
258                 c[0] = 1.0;
259                 c[1] = coords[0];
260                 c[2] = coords[1];
261                 c[3] = coords[2];
262             } else {
263                 // Homogeneous coordinates (normalized)
264                 c[0] = coords[0];
265                 c[1] = coords[1];
266                 c[2] = coords[2];
267                 c[3] = coords[2];
268                 this.normalizeCoords();
269             }
270 
271             // console.log(el.emitter, !noevent, oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3]);
272             // Not yet working TODO
273             // if (el.emitter && !noevent &&
274             //     (oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3])) {
275             //     this.triggerEventHandlers(['update3D'], [oc]);
276             // }
277             return this;
278         },
279 
280         update: function (drag) {
281             var c3d, foot, res;
282 
283             // Update is called from board.updateElements
284             if (
285                 this.element2D.draggable() &&
286                 Geometry.distance(this._c2d, this.element2D.coords.usrCoords) !== 0
287             ) {
288 
289                 //
290                 if (this.slide) {
291                     this.coords = this.slide.projectScreenCoords(
292                         [this.element2D.X(), this.element2D.Y()],
293                         this._params
294                     );
295                     this.element2D.coords.setCoordinates(
296                         Const.COORDS_BY_USER,
297                         this.view.project3DTo2D(this.coords)
298                     );
299                 } else {
300                     if (this.view.isVerticalDrag()) {
301                         // Drag the point in its vertical to the xy plane
302                         // If the point is outside of bbox3d,
303                         // c3d is already corrected.
304                         c3d = this.view.project2DTo3DVertical(this.element2D, this.coords);
305                     } else {
306                         // Drag the point in its xy plane
307                         foot = [1, 0, 0, this.coords[3]];
308                         c3d = this.view.project2DTo3DPlane(this.element2D, [1, 0, 0, 1], foot);
309                     }
310 
311                     if (c3d[0] !== 0) {
312                         // Check if c3d is inside of view.bbox3d
313                         // Otherwise, the coords have to be corrected below.
314                         res = this.view.project3DToCube(c3d);
315                         this.coords = res[0];
316 
317                         if (res[1]) {
318                             // The 3D coordinates have been corrected, now
319                             // also correct the 2D element.
320                             this.element2D.coords.setCoordinates(
321                                 Const.COORDS_BY_USER,
322                                 this.view.project3DTo2D(this.coords)
323                             );
324                         }
325                     }
326                 }
327             } else {
328                 // Update 2D point from its 3D view
329                 this.updateCoords();
330                 if (this.slide) {
331                     this.coords = this.slide.projectCoords(
332                         [this.X(), this.Y(), this.Z()],
333                         this._params
334                     );
335                 }
336                 this.element2D.coords.setCoordinates(
337                     Const.COORDS_BY_USER,
338                     this.view.project3DTo2D([1, this.X(), this.Y(), this.Z()])
339                 );
340             }
341             this._c2d = this.element2D.coords.usrCoords.slice();
342 
343             return this;
344         },
345 
346         updateRenderer: function () {
347             this.needsUpdate = false;
348             return this;
349         },
350 
351         /**
352          * Check whether a point's homogeneous coordinate vector is zero.
353          * @returns {Boolean} True if the coordinate vector is zero; false otherwise.
354          */
355         isIllDefined: function () {
356             return Type.cmpArrays(this.coords, [0, 0, 0, 0]);
357         },
358 
359         /**
360          * Calculate the distance from one point to another. If one of the points is on the plane at infinity, return positive infinity.
361          * @param {JXG.Point3D} pt The point to which the distance is calculated.
362          * @returns {Number} The distance
363          */
364         distance: function (pt) {
365             var eps_sq = Mat.eps * Mat.eps,
366                 c_this = this.coords,
367                 c_pt = pt.coords;
368 
369             if (c_this[0] * c_this[0] > eps_sq && c_pt[0] * c_pt[0] > eps_sq) {
370                 return Mat.hypot(
371                     c_pt[1] - c_this[1],
372                     c_pt[2] - c_this[2],
373                     c_pt[3] - c_this[3]
374                 );
375             } else {
376                 return Number.POSITIVE_INFINITY;
377             }
378         },
379 
380         // Not yet working
381         __evt__update3D: function (oc) {}
382     }
383 );
384 
385 /**
386  * @class This element is used to provide a constructor for a 3D Point.
387  * @pseudo
388  * @description A Point3D object is defined by 3 coordinates [x,y,z].
389  * <p>
390  * All numbers can also be provided as functions returning a number.
391  *
392  * @name Point3D
393  * @augments JXG.Point3D
394  * @constructor
395  * @throws {Exception} If the element cannot be constructed with the given parent
396  * objects an exception is thrown.
397  * @param {number,function_number,function_number,function} x,y,z The coordinates are given as x, y, z consisting of numbers of functions.
398  * @param {array,function} F Alternatively, the coordinates can be supplied as
399  *  <ul>
400  *   <li>array arr=[x,y,z] of length 3 consisting of numbers or
401  *   <li>function returning an array [x,y,z] of length 3 of numbers.
402  * </ul>
403  *
404  * @example
405  *    var bound = [-5, 5];
406  *    var view = board.create('view3d',
407  *        [[-6, -3], [8, 8],
408  *        [bound, bound, bound]],
409  *        {});
410  *    var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 });
411  *    var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 5, fixed: true });
412  *
413  * </pre><div id="JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1" class="jxgbox" style="width: 300px; height: 300px;"></div>
414  * <script type="text/javascript">
415  *     (function() {
416  *         var board = JXG.JSXGraph.initBoard('JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1',
417  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
418  *         var bound = [-5, 5];
419  *         var view = board.create('view3d',
420  *             [[-6, -3], [8, 8],
421  *             [bound, bound, bound]],
422  *             {});
423  *         var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 });
424  *         var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 5 });
425  *     })();
426  *
427  * </script><pre>
428  *
429  */
430 JXG.createPoint3D = function (board, parents, attributes) {
431     //   parents[0]: view
432     // followed by
433     //   parents[1]: function or array
434     // or
435     //   parents[1..3]: coordinates
436 
437     var view = parents[0],
438         attr, F, slide, c2d, el;
439 
440     // If the last element of parents is a 3D object,
441     // the point is a glider on that element.
442     if (parents.length > 2 && Type.exists(parents[parents.length - 1].is3D)) {
443         slide = parents.pop();
444     } else {
445         slide = null;
446     }
447 
448     if (parents.length === 2) {
449         // [view, array|fun] (Array [x, y, z] | function) returning [x, y, z]
450         F = parents[1];
451     } else if (parents.length === 4) {
452         // [view, x, y, z], (3 numbers | functions)
453         F = parents.slice(1);
454     } else {
455         throw new Error(
456             "JSXGraph: Can't create point3d with parent types '" +
457                 typeof parents[0] +
458                 "' and '" +
459                 typeof parents[1] +
460                 "'." +
461                 "\nPossible parent types: [[x,y,z]], [x,y,z]"
462         );
463         //  "\nPossible parent types: [[x,y,z]], [x,y,z], [element,transformation]"); // TODO
464     }
465 
466     attr = Type.copyAttributes(attributes, board.options, 'point3d');
467     el = new JXG.Point3D(view, F, slide, attr);
468     el.initCoords();
469 
470     c2d = view.project3DTo2D(el.coords);
471 
472     attr = el.setAttr2D(attr);
473     el.element2D = view.create('point', c2d, attr);
474     el.element2D.view = view;
475     el.addChild(el.element2D);
476     el.inherits.push(el.element2D);
477     el.element2D.setParents(el);
478 
479     // if this point is a glider, record that in the update tree
480     if (el.slide) {
481         el.slide.addChild(el);
482         el.setParents(el.slide);
483     }
484 
485     el._c2d = el.element2D.coords.usrCoords.slice(); // Store a copy of the coordinates to detect dragging
486 
487     return el;
488 };
489 
490 JXG.registerElement("point3d", JXG.createPoint3D);
491