      1 /*
      2     Copyright 2008-2025
      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([1, 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([1, 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