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