1 /*
  2     Copyright 2008-2023
  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";
 32 import Const from "../base/constants";
 33 import Type from "../utils/type";
 34 import Mat from "../math/math";
 35 import GeometryElement from "../base/element";
 36 import Composition from "../base/composition";
 37 
 38 /**
 39  * 3D view inside of a JXGraph board.
 40  *
 41  * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with
 42  * type {@link View3D} instead.
 43  *
 44  * @augments JXG.GeometryElement
 45  * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view
 46  * and box size [[x1, x2], [y1,y2], [z1,z2]]. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner
 47  * [x,y] and side lengths [w, h] of the board.
 48  */
 49 JXG.View3D = function (board, parents, attributes) {
 50     this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D);
 51 
 52     /**
 53      * An associative array containing all geometric objects belonging to the view.
 54      * Key is the id of the object and value is a reference to the object.
 55      * @type Object
 56      * @private
 57      */
 58     this.objects = {};
 59 
 60     /**
 61      * An array containing all geometric objects in this view in the order of construction.
 62      * @type Array
 63      * @private
 64      */
 65     this.objectsList = [];
 66 
 67     /**
 68      * An associative array / dictionary to store the objects of the board by name. The name of the object is the key and value is a reference to the object.
 69      * @type Object
 70      * @private
 71      */
 72     this.elementsByName = {};
 73 
 74     /**
 75      * Default axes of the 3D view, contains the axes of the view or null.
 76      *
 77      * @type {Object}
 78      * @default null
 79      */
 80     this.defaultAxes = null;
 81 
 82     /**
 83      * 3D-to-2D transformation matrix
 84      * @type  {Array} 3 x 4 matrix
 85      * @private
 86      */
 87     this.matrix3D = [
 88         [1, 0, 0, 0],
 89         [0, 1, 0, 0],
 90         [0, 0, 1, 0]
 91     ];
 92 
 93     /**
 94      * Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0.
 95      * @type array
 96      * @private
 97      */
 98     this.llftCorner = parents[0];
 99 
100     /**
101      * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0.
102      * @type array
103      * @private
104      */
105     this.size = parents[1];
106 
107     /**
108      * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view
109      * @type array
110      */
111     this.bbox3D = parents[2];
112 
113     /**
114      * Distance of the view to the origin. In other words, its
115      * the radius of the sphere where the camera sits.view.board.update
116      * @type Number
117      */
118     this.r = -1;
119 
120     this.timeoutAzimuth = null;
121 
122     this.id = this.board.setId(this, "V");
123     this.board.finalizeAdding(this);
124     this.elType = "view3d";
125 
126     this.methodMap = Type.deepCopy(this.methodMap, {
127         // TODO
128     });
129 };
130 JXG.View3D.prototype = new GeometryElement();
131 
132 JXG.extend(
133     JXG.View3D.prototype,
134     /** @lends JXG.View3D.prototype */ {
135         /**
136          * Creates a new 3D element of type elementType.
137          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
138          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
139          * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
140          * methods for a list of possible parameters.
141          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
142          * Common attributes are name, visible, strokeColor.
143          * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
144          * two or more elements.
145          */
146         create: function (elementType, parents, attributes) {
147             var prefix = [],
148                 is3D = false,
149                 el;
150 
151             if (elementType.indexOf("3d") > 0) {
152                 is3D = true;
153                 prefix.push(this);
154             }
155             el = this.board.create(elementType, prefix.concat(parents), attributes);
156 
157             return el;
158         },
159 
160         /**
161          * Select a single or multiple elements at once.
162          * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
163          * be used as a filter to return multiple elements at once filtered by the properties of the object.
164          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
165          * The advanced filters consisting of objects or functions are ignored.
166          * @returns {JXG.GeometryElement3D|JXG.Composition}
167          * @example
168          * // select the element with name A
169          * view.select('A');
170          *
171          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
172          * view.select({
173          *   strokeColor: 'red'
174          * });
175          *
176          * // select all points on or below the x/y plane and make them black.
177          * view.select({
178          *   elType: 'point3d',
179          *   Z: function (v) {
180          *     return v <= 0;
181          *   }
182          * }).setAttribute({color: 'black'});
183          *
184          * // select all elements
185          * view.select(function (el) {
186          *   return true;
187          * });
188          */
189         select: function (str, onlyByIdOrName) {
190             var flist,
191                 olist,
192                 i,
193                 l,
194                 s = str;
195 
196             if (s === null) {
197                 return s;
198             }
199 
200             // It's a string, most likely an id or a name.
201             if (Type.isString(s) && s !== "") {
202                 // Search by ID
203                 if (Type.exists(this.objects[s])) {
204                     s = this.objects[s];
205                     // Search by name
206                 } else if (Type.exists(this.elementsByName[s])) {
207                     s = this.elementsByName[s];
208                     // // Search by group ID
209                     // } else if (Type.exists(this.groups[s])) {
210                     //     s = this.groups[s];
211                 }
212 
213                 // It's a function or an object, but not an element
214             } else if (
215                 !onlyByIdOrName &&
216                 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
217             ) {
218                 flist = Type.filterElements(this.objectsList, s);
219 
220                 olist = {};
221                 l = flist.length;
222                 for (i = 0; i < l; i++) {
223                     olist[flist[i].id] = flist[i];
224                 }
225                 s = new Composition(olist);
226 
227                 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
228             } else if (
229                 Type.isObject(s) &&
230                 Type.exists(s.id) &&
231                 !Type.exists(this.objects[s.id])
232             ) {
233                 s = null;
234             }
235 
236             return s;
237         },
238 
239         update: function () {
240             // Update 3D-to-2D transformation matrix with the actual
241             // elevation and azimuth angles.
242 
243             var e, r, a, f, mat;
244 
245             if (
246                 !Type.exists(this.el_slide) ||
247                 !Type.exists(this.az_slide) ||
248                 !this.needsUpdate
249             ) {
250                 return this;
251             }
252 
253             e = this.el_slide.Value();
254             r = this.r;
255             a = this.az_slide.Value();
256             f = r * Math.sin(e);
257             mat = [
258                 [1, 0, 0],
259                 [0, 1, 0],
260                 [0, 0, 1]
261             ];
262 
263             this.matrix3D = [
264                 [1, 0, 0, 0],
265                 [0, 1, 0, 0],
266                 [0, 0, 1, 0]
267             ];
268 
269             this.matrix3D[1][1] = r * Math.cos(a);
270             this.matrix3D[1][2] = -r * Math.sin(a);
271             this.matrix3D[2][1] = f * Math.sin(a);
272             this.matrix3D[2][2] = f * Math.cos(a);
273             this.matrix3D[2][3] = Math.cos(e);
274 
275             mat[1][1] = this.size[0] / (this.bbox3D[0][1] - this.bbox3D[0][0]); // w / d_x
276             mat[2][2] = this.size[1] / (this.bbox3D[1][1] - this.bbox3D[1][0]); // h / d_y
277             mat[1][0] = this.llftCorner[0] - mat[1][1] * this.bbox3D[0][0]; // llft_x
278             mat[2][0] = this.llftCorner[1] - mat[2][2] * this.bbox3D[1][0]; // llft_y
279             this.matrix3D = Mat.matMatMult(mat, this.matrix3D);
280 
281             return this;
282         },
283 
284         updateRenderer: function () {
285             this.needsUpdate = false;
286             return this;
287         },
288 
289         /**
290          * Project 3D coordinates to 2D board coordinates
291          * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
292          *
293          * @param  {Number|Array} x
294          * @param  {[Number]} y
295          * @param  {[Number]} z
296          * @returns {Array} Array of length 3 containing the projection on to the board
297          * in homogeneous user coordinates.
298          */
299         project3DTo2D: function (x, y, z) {
300             var vec;
301             if (arguments.length === 3) {
302                 vec = [1, x, y, z];
303             } else {
304                 // Argument is an array
305                 if (x.length === 3) {
306                     vec = [1].concat(x);
307                 } else {
308                     vec = x;
309                 }
310             }
311             return Mat.matVecMult(this.matrix3D, vec);
312         },
313 
314         /**
315          * Project a 2D coordinate to the plane defined by the point foot
316          * and the normal vector `normal`.
317          *
318          * @param  {JXG.Point} point
319          * @param  {Array} normal
320          * @param  {Array} foot
321          * @returns {Array} of length 4 containing the projected
322          * point in homogeneous coordinates.
323          */
324         project2DTo3DPlane: function (point2d, normal, foot) {
325             var mat,
326                 rhs,
327                 d,
328                 le,
329                 n = normal.slice(1),
330                 sol = [1, 0, 0, 0];
331 
332             foot = foot || [1, 0, 0, 0];
333             le = Mat.norm(n, 3);
334             d = Mat.innerProduct(foot.slice(1), n, 3) / le;
335 
336             mat = this.matrix3D.slice(0, 3); // True copy
337             mat.push([0].concat(n));
338 
339             // 2D coordinates of point:
340             rhs = point2d.coords.usrCoords.concat([d]);
341             try {
342                 // Prevent singularity in case elevation angle is zero
343                 if (mat[2][3] === 1.0) {
344                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
345                 }
346                 sol = Mat.Numerics.Gauss(mat, rhs);
347             } catch (err) {
348                 sol = [0, NaN, NaN, NaN];
349             }
350 
351             return sol;
352         },
353 
354         /**
355          * Limit 3D coordinates to the bounding cube.
356          *
357          * @param {Array} c3d 3D coordinates [x,y,z]
358          * @returns Array with updated 3D coordinates.
359          */
360         project3DToCube: function (c3d) {
361             var cube = this.bbox3D;
362             if (c3d[1] < cube[0][0]) {
363                 c3d[1] = cube[0][0];
364             }
365             if (c3d[1] > cube[0][1]) {
366                 c3d[1] = cube[0][1];
367             }
368             if (c3d[2] < cube[1][0]) {
369                 c3d[2] = cube[1][0];
370             }
371             if (c3d[2] > cube[1][1]) {
372                 c3d[2] = cube[1][1];
373             }
374             if (c3d[3] < cube[2][0]) {
375                 c3d[3] = cube[2][0];
376             }
377             if (c3d[3] > cube[2][1]) {
378                 c3d[3] = cube[2][1];
379             }
380 
381             return c3d;
382         },
383 
384         /**
385          * Intersect a ray with the bounding cube of the 3D view.
386          * @param {Array} p 3D coordinates [x,y,z]
387          * @param {Array} d 3D direction vector of the line (array of length 3)
388          * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
389          * @returns Affine ratio of the intersection of the line with the cube.
390          */
391         intersectionLineCube: function (p, d, r) {
392             var rnew, i, r0, r1;
393 
394             rnew = r;
395             for (i = 0; i < 3; i++) {
396                 if (d[i] !== 0) {
397                     r0 = (this.bbox3D[i][0] - p[i]) / d[i];
398                     r1 = (this.bbox3D[i][1] - p[i]) / d[i];
399                     if (r < 0) {
400                         rnew = Math.max(rnew, Math.min(r0, r1));
401                     } else {
402                         rnew = Math.min(rnew, Math.max(r0, r1));
403                     }
404                 }
405             }
406             return rnew;
407         },
408 
409         /**
410          * Test if coordinates are inside of the bounding cube.
411          * @param {array} q 3D coordinates [x,y,z] of a point.
412          * @returns Boolean
413          */
414         isInCube: function (q) {
415             return (
416                 q[0] > this.bbox3D[0][0] - Mat.eps &&
417                 q[0] < this.bbox3D[0][1] + Mat.eps &&
418                 q[1] > this.bbox3D[1][0] - Mat.eps &&
419                 q[1] < this.bbox3D[1][1] + Mat.eps &&
420                 q[2] > this.bbox3D[2][0] - Mat.eps &&
421                 q[2] < this.bbox3D[2][1] + Mat.eps
422             );
423         },
424 
425         /**
426          *
427          * @param {JXG.Plane3D} plane1
428          * @param {JXG.Plane3D} plane2
429          * @param {JXG.Plane3D} d
430          * @returns {Array} of length 2 containing the coordinates of the defining points of
431          * of the intersection segment.
432          */
433         intersectionPlanePlane: function (plane1, plane2, d) {
434             var ret = [[], []],
435                 p,
436                 dir,
437                 r,
438                 q;
439 
440             d = d || plane2.d;
441 
442             p = Mat.Geometry.meet3Planes(
443                 plane1.normal,
444                 plane1.d,
445                 plane2.normal,
446                 d,
447                 Mat.crossProduct(plane1.normal, plane2.normal),
448                 0
449             );
450             dir = Mat.Geometry.meetPlanePlane(
451                 plane1.vec1,
452                 plane1.vec2,
453                 plane2.vec1,
454                 plane2.vec2
455             );
456             r = this.intersectionLineCube(p, dir, Infinity);
457             q = Mat.axpy(r, dir, p);
458             if (this.isInCube(q)) {
459                 ret[0] = q;
460             }
461             r = this.intersectionLineCube(p, dir, -Infinity);
462             q = Mat.axpy(r, dir, p);
463             if (this.isInCube(q)) {
464                 ret[1] = q;
465             }
466             return ret;
467         },
468 
469         /**
470          * Generate mesh for a surface / plane.
471          * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
472          * @param {Array,Function} func
473          * @param {Array} interval_u
474          * @param {Array} interval_v
475          * @returns Array
476          * @private
477          *
478          * @example
479          *  var el = view.create('curve', [[], []]);
480          *  el.updateDataArray = function () {
481          *      var steps_u = Type.evaluate(this.visProp.stepsu),
482          *           steps_v = Type.evaluate(this.visProp.stepsv),
483          *           r_u = Type.evaluate(this.range_u),
484          *           r_v = Type.evaluate(this.range_v),
485          *           func, ret;
486          *
487          *      if (this.F !== null) {
488          *          func = this.F;
489          *      } else {
490          *          func = [this.X, this.Y, this.Z];
491          *      }
492          *      ret = this.view.getMesh(func,
493          *          r_u.concat([steps_u]),
494          *          r_v.concat([steps_v]));
495          *
496          *      this.dataX = ret[0];
497          *      this.dataY = ret[1];
498          *  };
499          *
500          */
501         getMesh: function (func, interval_u, interval_v) {
502             var i_u,
503                 i_v,
504                 u,
505                 v,
506                 c2d,
507                 delta_u,
508                 delta_v,
509                 p = [0, 0, 0],
510                 steps_u = interval_u[2],
511                 steps_v = interval_v[2],
512                 dataX = [],
513                 dataY = [];
514 
515             delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
516             delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
517 
518             for (i_u = 0; i_u <= steps_u; i_u++) {
519                 u = interval_u[0] + delta_u * i_u;
520                 for (i_v = 0; i_v <= steps_v; i_v++) {
521                     v = interval_v[0] + delta_v * i_v;
522                     if (Type.isFunction(func)) {
523                         p = func(u, v);
524                     } else {
525                         p = [func[0](u, v), func[1](u, v), func[2](u, v)];
526                     }
527                     c2d = this.project3DTo2D(p);
528                     dataX.push(c2d[1]);
529                     dataY.push(c2d[2]);
530                 }
531                 dataX.push(NaN);
532                 dataY.push(NaN);
533             }
534 
535             for (i_v = 0; i_v <= steps_v; i_v++) {
536                 v = interval_v[0] + delta_v * i_v;
537                 for (i_u = 0; i_u <= steps_u; i_u++) {
538                     u = interval_u[0] + delta_u * i_u;
539                     if (Type.isFunction(func)) {
540                         p = func(u, v);
541                     } else {
542                         p = [func[0](u, v), func[1](u, v), func[2](u, v)];
543                     }
544                     c2d = this.project3DTo2D(p);
545                     dataX.push(c2d[1]);
546                     dataY.push(c2d[2]);
547                 }
548                 dataX.push(NaN);
549                 dataY.push(NaN);
550             }
551 
552             return [dataX, dataY];
553         },
554 
555         /**
556          *
557          */
558         animateAzimuth: function () {
559             var s = this.az_slide._smin,
560                 e = this.az_slide._smax,
561                 sdiff = e - s,
562                 newVal = this.az_slide.Value() + 0.1;
563 
564             this.az_slide.position = (newVal - s) / sdiff;
565             if (this.az_slide.position > 1) {
566                 this.az_slide.position = 0.0;
567             }
568             this.board.update();
569 
570             this.timeoutAzimuth = setTimeout(
571                 function () {
572                     this.animateAzimuth();
573                 }.bind(this),
574                 200
575             );
576         },
577 
578         /**
579          *
580          */
581         stopAzimuth: function () {
582             clearTimeout(this.timeoutAzimuth);
583             this.timeoutAzimuth = null;
584         }
585     }
586 );
587 
588 /**
589  * @class This element creates a 3D view.
590  * @pseudo
591  * @description  A View3D element provides the container and the methods to create and display 3D elements.
592  * It is contained in a JSXGraph board.
593  * @name View3D
594  * @augments JXG.View3D
595  * @constructor
596  * @type Object
597  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
598  * @param {Array_Array_Array} lower,dim,cube  Here, lower is an array of the form [x, y] and
599  * dim is an array of the form [w, h].
600  * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is
601  * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner
602  * [x,y] and side lengths [w, h] of the board.
603  * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]]
604  * which determines the coordinate ranges of the 3D cube.
605  *
606  * @example
607  *  var bound = [-5, 5];
608  *  var view = board.create('view3d',
609  *      [[-6, -3],
610  *       [8, 8],
611  *       [bound, bound, bound]],
612  *      {
613  *          // Main axes
614  *          axesPosition: 'center',
615  *          xAxis: { strokeColor: 'blue', strokeWidth: 3},
616  *
617  *          // Planes
618  *          xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
619  *          yPlaneFront: { visible: true, fillColor: 'blue'},
620  *
621  *          // Axes on planes
622  *          xPlaneRearYAxis: {strokeColor: 'red'},
623  *          xPlaneRearZAxis: {strokeColor: 'red'},
624  *
625  *          yPlaneFrontXAxis: {strokeColor: 'blue'},
626  *          yPlaneFrontZAxis: {strokeColor: 'blue'},
627  *
628  *          zPlaneFrontXAxis: {visible: false},
629  *          zPlaneFrontYAxis: {visible: false}
630  *      });
631  *
632  * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div>
633  * <script type="text/javascript">
634  *     (function() {
635  *         var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7',
636  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
637  *         var bound = [-5, 5];
638  *         var view = board.create('view3d',
639  *             [[-6, -3], [8, 8],
640  *             [bound, bound, bound]],
641  *             {
642  *                 // Main axes
643  *                 axesPosition: 'center',
644  *                 xAxis: { strokeColor: 'blue', strokeWidth: 3},
645  *                 // Planes
646  *                 xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
647  *                 yPlaneFront: { visible: true, fillColor: 'blue'},
648  *                 // Axes on planes
649  *                 xPlaneRearYAxis: {strokeColor: 'red'},
650  *                 xPlaneRearZAxis: {strokeColor: 'red'},
651  *                 yPlaneFrontXAxis: {strokeColor: 'blue'},
652  *                 yPlaneFrontZAxis: {strokeColor: 'blue'},
653  *                 zPlaneFrontXAxis: {visible: false},
654  *                 zPlaneFrontYAxis: {visible: false}
655  *             });
656  *     })();
657  *
658  * </script><pre>
659  *
660  */
661 JXG.createView3D = function (board, parents, attributes) {
662     var view,
663         attr,
664         x,
665         y,
666         w,
667         h,
668         coords = parents[0], // llft corner
669         size = parents[1]; // [w, h]
670 
671     attr = Type.copyAttributes(attributes, board.options, "view3d");
672     view = new JXG.View3D(board, parents, attr);
673     view.defaultAxes = view.create("axes3d", parents, attributes);
674 
675     x = coords[0];
676     y = coords[1];
677     w = size[0];
678     h = size[1];
679 
680     /**
681      * Slider to adapt azimuth angle
682      * @name JXG.View3D#az_slide
683      * @type {Slider}
684      */
685     view.az_slide = board.create(
686         "slider",
687         [
688             [x - 1, y - 2],
689             [x + w + 1, y - 2],
690             [0, 1.0, 2 * Math.PI]
691         ],
692         {
693             style: 6,
694             name: "az",
695             point1: { frozen: true },
696             point2: { frozen: true }
697         }
698     );
699 
700     /**
701      * Slider to adapt elevation angle
702      *
703      * @name JXG.View3D#el_slide
704      * @type {Slider}
705      */
706     view.el_slide = board.create(
707         "slider",
708         [
709             [x - 1, y],
710             [x - 1, y + h],
711             [0, 0.3, Math.PI / 2]
712         ],
713         {
714             style: 6,
715             name: "el",
716             point1: { frozen: true },
717             point2: { frozen: true }
718         }
719     );
720 
721     view.board.highlightInfobox = function (x, y, el) {
722         var d,
723             i,
724             c3d,
725             foot,
726             brd = el.board,
727             p = null;
728 
729         // Search 3D parent
730         for (i = 0; i < el.parents.length; i++) {
731             p = brd.objects[el.parents[i]];
732             if (p.is3D) {
733                 break;
734             }
735         }
736         if (p) {
737             foot = [1, 0, 0, p.coords[3]];
738             c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot);
739             if (!view.isInCube(c3d)) {
740                 view.board.highlightCustomInfobox("", p);
741                 return;
742             }
743             d = Type.evaluate(p.visProp.infoboxdigits);
744             if (d === "auto") {
745                 view.board.highlightCustomInfobox(
746                     "(" +
747                         Type.autoDigits(p.X()) +
748                         " | " +
749                         Type.autoDigits(p.Y()) +
750                         " | " +
751                         Type.autoDigits(p.Z()) +
752                         ")",
753                     p
754                 );
755             } else {
756                 view.board.highlightCustomInfobox(
757                     "(" +
758                         Type.toFixed(p.X(), d) +
759                         " | " +
760                         Type.toFixed(p.Y(), d) +
761                         " | " +
762                         Type.toFixed(p.Z(), d) +
763                         ")",
764                     p
765                 );
766             }
767         } else {
768             view.board.highlightCustomInfobox("(" + x + ", " + y + ")", el);
769         }
770     };
771 
772     view.board.update();
773 
774     return view;
775 };
776 JXG.registerElement("view3d", JXG.createView3D);
777 
778 export default JXG.View3D;
779