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