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