1 /*
  2     Copyright 2008-2024
  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 /*
 30     Some functionalities in this file were developed as part of a software project
 31     with students. We would like to thank all contributors for their help:
 32 
 33     Winter semester 2023/2024:
 34         Lars Hofmann
 35         Leonhard Iser
 36         Vincent Kulicke
 37         Laura Rinas
 38  */
 39 
 40 /*global JXG:true, define: true*/
 41 
 42 import JXG from "../jxg.js";
 43 import Const from "../base/constants.js";
 44 import Coords from "../base/coords.js";
 45 import Type from "../utils/type.js";
 46 import Mat from "../math/math.js";
 47 import Geometry from "../math/geometry.js";
 48 import Env from "../utils/env.js";
 49 import GeometryElement from "../base/element.js";
 50 import Composition from "../base/composition.js";
 51 
 52 /**
 53  * 3D view inside a JXGraph board.
 54  *
 55  * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with
 56  * type {@link View3D} instead.
 57  *
 58  * @augments JXG.GeometryElement
 59  * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view
 60  * 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
 61  * [x,y] and side lengths [w, h] of the board.
 62  */
 63 JXG.View3D = function (board, parents, attributes) {
 64     this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D);
 65 
 66     /**
 67      * An associative array containing all geometric objects belonging to the view.
 68      * Key is the id of the object and value is a reference to the object.
 69      * @type Object
 70      * @private
 71      */
 72     this.objects = {};
 73 
 74     /**
 75      * An array containing all the points in the view.
 76      * @Type Array
 77      * @private
 78      */
 79     this.points = this.visProp.depthorderpoints ? [] : null;
 80 
 81     /**
 82      * An array containing all geometric objects in this view in the order of construction.
 83      * @type Array
 84      * @private
 85      */
 86     // this.objectsList = [];
 87 
 88     /**
 89      * 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.
 90      * @type Object
 91      * @private
 92      */
 93     this.elementsByName = {};
 94 
 95     /**
 96      * Default axes of the 3D view, contains the axes of the view or null.
 97      *
 98      * @type {Object}
 99      * @default null
100      */
101     this.defaultAxes = null;
102 
103     /**
104      * The Tait-Bryan angles specifying the view box orientation
105      */
106     this.angles = {
107         az: null,
108         el: null,
109         bank: null
110     };
111 
112     /**
113      * @type {Array}
114      * The view box orientation matrix
115      */
116     this.matrix3DRot = [
117         [1, 0, 0, 0],
118         [0, 1, 0, 0],
119         [0, 0, 1, 0],
120         [0, 0, 0, 1]
121     ];
122 
123     /**
124      * @type  {Array}
125      * @private
126      */
127     // 3D-to-2D transformation matrix
128     this.matrix3D = [
129         [1, 0, 0, 0],
130         [0, 1, 0, 0],
131         [0, 0, 1, 0]
132     ];
133 
134     /**
135      * The 4×4 matrix that maps box coordinates to camera coordinates. These
136      * coordinate systems fit into the View3D coordinate atlas as follows.
137      * <ul>
138      * <li><b>World coordinates.</b> The coordinates used to specify object
139      * positions in a JSXGraph scene.</li>
140      * <li><b>Box coordinates.</b> The world coordinates translated to put the
141      * center of the view box at the origin.
142      * <li><b>Camera coordinates.</b> The coordinate system where the
143      * <code>x</code>, <code>y</code> plane is the screen, the origin is the
144      * center of the screen, and the <code>z</code> axis points out of the
145      * screen, toward the viewer.
146      * <li><b>Focal coordinates.</b> The camera coordinates translated to put
147      * the origin at the focal point, which is set back from the screen by the
148      * focal distance.</li>
149      * </ul>
150      * The <code>boxToCam</code> transformation is exposed to help 3D elements
151      * manage their 2D representations in central projection mode. To map world
152      * coordinates to focal coordinates, use the
153      * {@link JXG.View3D#worldToFocal} method.
154      * @type {Array}
155      */
156     this.boxToCam = [];
157 
158     /**
159      * @type array
160      * @private
161      */
162     // Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0.
163     this.llftCorner = parents[0];
164 
165     /**
166      * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0.
167      * @type array
168      * @private
169      */
170     this.size = parents[1];
171 
172     /**
173      * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view
174      * @type array
175      */
176     this.bbox3D = parents[2];
177 
178     /**
179      * The distance from the camera to the origin. In other words, the
180      * radius of the sphere where the camera sits.
181      * @type Number
182      */
183     this.r = -1;
184 
185     /**
186      * The distance from the camera to the screen. Computed automatically from
187      * the `fov` property.
188      * @type Number
189      */
190     this.focalDist = -1;
191 
192     /**
193      * Type of projection.
194      * @type String
195      */
196     // Will be set in update().
197     this.projectionType = 'parallel';
198 
199     /**
200      * Whether trackball navigation is currently enabled.
201      * @type String
202      */
203     this.trackballEnabled = false;
204 
205     this.timeoutAzimuth = null;
206 
207     this.id = this.board.setId(this, 'V');
208     this.board.finalizeAdding(this);
209     this.elType = 'view3d';
210 
211     this.methodMap = Type.deepCopy(this.methodMap, {
212         // TODO
213     });
214 };
215 JXG.View3D.prototype = new GeometryElement();
216 
217 JXG.extend(
218     JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ {
219 
220     /**
221      * Creates a new 3D element of type elementType.
222      * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
223      * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
224      * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
225      * methods for a list of possible parameters.
226      * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
227      * Common attributes are name, visible, strokeColor.
228      * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
229      * two or more elements.
230      */
231     create: function (elementType, parents, attributes) {
232         var prefix = [],
233             el;
234 
235         if (elementType.indexOf('3d') > 0) {
236             // is3D = true;
237             prefix.push(this);
238         }
239         el = this.board.create(elementType, prefix.concat(parents), attributes);
240 
241         return el;
242     },
243 
244     /**
245      * Select a single or multiple elements at once.
246      * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
247      * be used as a filter to return multiple elements at once filtered by the properties of the object.
248      * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
249      * The advanced filters consisting of objects or functions are ignored.
250      * @returns {JXG.GeometryElement3D|JXG.Composition}
251      * @example
252      * // select the element with name A
253      * view.select('A');
254      *
255      * // select all elements with strokecolor set to 'red' (but not '#ff0000')
256      * view.select({
257      *   strokeColor: 'red'
258      * });
259      *
260      * // select all points on or below the x/y plane and make them black.
261      * view.select({
262      *   elType: 'point3d',
263      *   Z: function (v) {
264      *     return v <= 0;
265      *   }
266      * }).setAttribute({color: 'black'});
267      *
268      * // select all elements
269      * view.select(function (el) {
270      *   return true;
271      * });
272      */
273     select: function (str, onlyByIdOrName) {
274         var flist,
275             olist,
276             i,
277             l,
278             s = str;
279 
280         if (s === null) {
281             return s;
282         }
283 
284         // It's a string, most likely an id or a name.
285         if (Type.isString(s) && s !== '') {
286             // Search by ID
287             if (Type.exists(this.objects[s])) {
288                 s = this.objects[s];
289                 // Search by name
290             } else if (Type.exists(this.elementsByName[s])) {
291                 s = this.elementsByName[s];
292                 // // Search by group ID
293                 // } else if (Type.exists(this.groups[s])) {
294                 //     s = this.groups[s];
295             }
296 
297             // It's a function or an object, but not an element
298         } else if (
299             !onlyByIdOrName &&
300             (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
301         ) {
302             flist = Type.filterElements(this.objectsList, s);
303 
304             olist = {};
305             l = flist.length;
306             for (i = 0; i < l; i++) {
307                 olist[flist[i].id] = flist[i];
308             }
309             s = new Composition(olist);
310 
311             // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
312         } else if (
313             Type.isObject(s) &&
314             Type.exists(s.id) &&
315             !Type.exists(this.objects[s.id])
316         ) {
317             s = null;
318         }
319 
320         return s;
321     },
322 
323     // set the Tait-Bryan angles to specify the current view rotation matrix
324     setAnglesFromRotation: function () {
325         var rem = this.matrix3DRot, // rotation remaining after angle extraction
326             rBank, cosBank, sinBank,
327             cosEl, sinEl,
328             cosAz, sinAz;
329 
330         // extract bank by rotating the view box z axis onto the camera yz plane
331         rBank = Math.sqrt(rem[1][3] * rem[1][3] + rem[2][3] * rem[2][3]);
332         if (rBank > Mat.eps) {
333             cosBank = rem[2][3] / rBank;
334             sinBank = rem[1][3] / rBank;
335         } else {
336             // if the z axis is pointed almost exactly at the screen, we
337             // keep the current bank value
338             cosBank = Math.cos(this.angles.bank);
339             sinBank = Math.sin(this.angles.bank);
340         }
341         rem = Mat.matMatMult([
342             [1, 0, 0, 0],
343             [0, cosBank, -sinBank, 0],
344             [0, sinBank, cosBank, 0],
345             [0, 0, 0, 1]
346         ], rem);
347         this.angles.bank = Math.atan2(sinBank, cosBank);
348 
349         // extract elevation by rotating the view box z axis onto the camera
350         // y axis
351         cosEl = rem[2][3];
352         sinEl = rem[3][3];
353         rem = Mat.matMatMult([
354             [1, 0, 0, 0],
355             [0, 1, 0, 0],
356             [0, 0, cosEl, sinEl],
357             [0, 0, -sinEl, cosEl]
358         ], rem);
359         this.angles.el = Math.atan2(sinEl, cosEl);
360 
361         // extract azimuth
362         cosAz = -rem[1][1];
363         sinAz = rem[3][1];
364         this.angles.az = Math.atan2(sinAz, cosAz);
365         if (this.angles.az < 0) this.angles.az += 2 * Math.PI;
366 
367         this.setSlidersFromAngles();
368     },
369 
370     anglesHaveMoved: function () {
371         return (
372             this._hasMoveAz || this._hasMoveEl ||
373             Math.abs(this.angles.az - this.az_slide.Value()) > Mat.eps ||
374             Math.abs(this.angles.el - this.el_slide.Value()) > Mat.eps ||
375             Math.abs(this.angles.bank - this.bank_slide.Value()) > Mat.eps
376         );
377     },
378 
379     getAnglesFromSliders: function () {
380         this.angles.az = this.az_slide.Value();
381         this.angles.el = this.el_slide.Value();
382         this.angles.bank = this.bank_slide.Value();
383     },
384 
385     setSlidersFromAngles: function () {
386         this.az_slide.setValue(this.angles.az);
387         this.el_slide.setValue(this.angles.el);
388         this.bank_slide.setValue(this.angles.bank);
389     },
390 
391     // return the rotation matrix specified by the current Tait-Bryan angles
392     getRotationFromAngles: function () {
393         var a, e, b, f,
394             cosBank, sinBank,
395             mat = [
396                 [1, 0, 0, 0],
397                 [0, 1, 0, 0],
398                 [0, 0, 1, 0],
399                 [0, 0, 0, 1]
400             ];
401 
402         // mat projects homogeneous 3D coords in View3D
403         // to homogeneous 2D coordinates in the board
404         a = this.angles.az;
405         e = this.angles.el;
406         b = this.angles.bank;
407         f = -Math.sin(e);
408 
409         mat[1][1] = -Math.cos(a);
410         mat[1][2] = Math.sin(a);
411         mat[1][3] = 0;
412 
413         mat[2][1] = f * Math.sin(a);
414         mat[2][2] = f * Math.cos(a);
415         mat[2][3] = Math.cos(e);
416 
417         mat[3][1] = Math.cos(e) * Math.sin(a);
418         mat[3][2] = Math.cos(e) * Math.cos(a);
419         mat[3][3] = Math.sin(e);
420 
421         cosBank = Math.cos(b);
422         sinBank = Math.sin(b);
423         mat = Mat.matMatMult([
424             [1, 0, 0, 0],
425             [0, cosBank, sinBank, 0],
426             [0, -sinBank, cosBank, 0],
427             [0, 0, 0, 1]
428         ], mat);
429 
430         return mat;
431 
432         /* this code, originally from `_updateCentralProjection`, is an
433          * alternate implementation of the azimuth-elevation matrix
434          * computation above. using this implementation instead of the
435          * current one might lead to simpler code in a future refactoring
436         var a, e, up,
437             ax, ay, az, v, nrm,
438             eye, d,
439             func_sphere;
440 
441         // finds the point on the unit sphere with the given azimuth and
442         // elevation, and returns its affine coordinates
443         func_sphere = function (az, el) {
444             return [
445                 Math.cos(az) * Math.cos(el),
446                 -Math.sin(az) * Math.cos(el),
447                 Math.sin(el)
448             ];
449         };
450 
451         a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere
452         e = this.el_slide.Value();
453 
454         // create an up vector and an eye vector which are 90 degrees out of phase
455         up = func_sphere(a, e + Math.PI / 2);
456         eye = func_sphere(a, e);
457         d = [eye[0], eye[1], eye[2]];
458 
459         nrm = Mat.norm(d, 3);
460         az = [d[0] / nrm, d[1] / nrm, d[2] / nrm];
461 
462         nrm = Mat.norm(up, 3);
463         v = [up[0] / nrm, up[1] / nrm, up[2] / nrm];
464 
465         ax = Mat.crossProduct(v, az);
466         ay = Mat.crossProduct(az, ax);
467 
468         this.matrix3DRot[1] = [0, ax[0], ax[1], ax[2]];
469         this.matrix3DRot[2] = [0, ay[0], ay[1], ay[2]];
470         this.matrix3DRot[3] = [0, az[0], az[1], az[2]];
471          */
472     },
473 
474     /**
475      * Project 2D point (x,y) to the virtual trackpad sphere,
476      * see Bell's virtual trackpad, and return z-component of the
477      * number.
478      *
479      * @param {Number} r
480      * @param {Number} x
481      * @param {Number} y
482      * @returns Number
483      * @private
484      */
485     _projectToSphere: function (r, x, y) {
486         var d = Mat.hypot(x, y),
487             t, z;
488 
489         if (d < r * 0.7071067811865475) { // Inside sphere
490             z = Math.sqrt(r * r - d * d);
491         } else {                          // On hyperbola
492             t = r / 1.414213562373095;
493             z = t * t / d;
494         }
495         return z;
496     },
497 
498     /**
499      * Determine 4x4 rotation matrix with Bell's virtual trackball.
500      *
501      * @returns {Array} 4x4 rotation matrix
502      * @private
503      */
504     updateProjectionTrackball: function (Pref) {
505         var R = 100,
506             dx, dy, dr2,
507             p1, p2, x, y, theta, t, d,
508             c, s, n,
509             mat = [
510                 [1, 0, 0, 0],
511                 [0, 1, 0, 0],
512                 [0, 0, 1, 0],
513                 [0, 0, 0, 1]
514             ];
515 
516         if (!Type.exists(this._trackball)) {
517             return this.matrix3DRot;
518         }
519 
520         dx = this._trackball.dx;
521         dy = this._trackball.dy;
522         dr2 = dx * dx + dy * dy;
523         if (dr2 > Mat.eps) {
524             // // Method by Hanson, "The rolling ball", Graphics Gems III, p.51
525             // // Rotation axis:
526             // //     n = (-dy/dr, dx/dr, 0)
527             // // Rotation angle around n:
528             // //     theta = atan(dr / R) approx dr / R
529             // dr = Math.sqrt(dr2);
530             // c = R / Math.hypot(R, dr);  // cos(theta)
531             // t = 1 - c;                  // 1 - cos(theta)
532             // s = dr / Math.hypot(R, dr); // sin(theta)
533             // n = [-dy / dr, dx / dr, 0];
534 
535             // Bell virtual trackpad, see
536             // https://opensource.apple.com/source/X11libs/X11libs-60/mesa/Mesa-7.8.2/progs/util/trackball.c.auto.html
537             // http://scv.bu.edu/documentation/presentations/visualizationworkshop08/materials/opengl/trackball.c.
538             // See also Henriksen, Sporring, Hornaek, "Virtual Trackballs revisited".
539             //
540             R = (this.size[0] * this.board.unitX + this.size[1] * this.board.unitY) * 0.25;
541             x = this._trackball.x;
542             y = this._trackball.y;
543 
544             p2 = [x, y, this._projectToSphere(R, x, y)];
545             x -= dx;
546             y -= dy;
547             p1 = [x, y, this._projectToSphere(R, x, y)];
548 
549             n = Mat.crossProduct(p1, p2);
550             d = Mat.hypot(n[0], n[1], n[2]);
551             n[0] /= d;
552             n[1] /= d;
553             n[2] /= d;
554 
555             t = Geometry.distance(p2, p1, 3) / (2 * R);
556             t = (t > 1.0) ? 1.0 : t;
557             t = (t < -1.0) ? -1.0 : t;
558             theta = 2.0 * Math.asin(t);
559             c = Math.cos(theta);
560             t = 1 - c;
561             s = Math.sin(theta);
562 
563             // Rotation by theta about the axis n. See equation 9.63 of
564             //
565             //   Ian Richard Cole. "Modeling CPV" (thesis). Loughborough
566             //   University. https://hdl.handle.net/2134/18050
567             //
568             mat[1][1] = c + n[0] * n[0] * t;
569             mat[2][1] = n[1] * n[0] * t + n[2] * s;
570             mat[3][1] = n[2] * n[0] * t - n[1] * s;
571 
572             mat[1][2] = n[0] * n[1] * t - n[2] * s;
573             mat[2][2] = c + n[1] * n[1] * t;
574             mat[3][2] = n[2] * n[1] * t + n[0] * s;
575 
576             mat[1][3] = n[0] * n[2] * t + n[1] * s;
577             mat[2][3] = n[1] * n[2] * t - n[0] * s;
578             mat[3][3] = c + n[2] * n[2] * t;
579         }
580 
581         mat = Mat.matMatMult(mat, this.matrix3DRot);
582         return mat;
583     },
584 
585     updateAngleSliderBounds: function () {
586         var az_smax, az_smin,
587             el_smax, el_smin, el_cover,
588             el_smid, el_equiv, el_flip_equiv,
589             el_equiv_loss, el_flip_equiv_loss, el_interval_loss,
590             bank_smax, bank_smin;
591 
592         // update stored trackball toggle
593         this.trackballEnabled = Type.evaluate(this.visProp.trackball.enabled);
594 
595         // set slider bounds
596         if (this.trackballEnabled) {
597             this.az_slide.setMin(0);
598             this.az_slide.setMax(2 * Math.PI);
599             this.el_slide.setMin(-0.5 * Math.PI);
600             this.el_slide.setMax(0.5 * Math.PI);
601             this.bank_slide.setMin(-Math.PI);
602             this.bank_slide.setMax(Math.PI);
603         } else {
604             this.az_slide.setMin(this.visProp.az.slider.min);
605             this.az_slide.setMax(this.visProp.az.slider.max);
606             this.el_slide.setMin(this.visProp.el.slider.min);
607             this.el_slide.setMax(this.visProp.el.slider.max);
608             this.bank_slide.setMin(this.visProp.bank.slider.min);
609             this.bank_slide.setMax(this.visProp.bank.slider.max);
610         }
611 
612         // get new slider bounds
613         az_smax = this.az_slide._smax;
614         az_smin = this.az_slide._smin;
615         el_smax = this.el_slide._smax;
616         el_smin = this.el_slide._smin;
617         bank_smax = this.bank_slide._smax;
618         bank_smin = this.bank_slide._smin;
619 
620         // wrap and restore angle values
621         if (this.trackballEnabled) {
622             // if we're upside-down, flip the bank angle to reach the same
623             // orientation with an elevation between -pi/2 and pi/2
624             el_cover = Mat.mod(this.angles.el, 2 * Math.PI);
625             if (0.5 * Math.PI < el_cover && el_cover < 1.5 * Math.PI) {
626                 this.angles.el = Math.PI - el_cover;
627                 this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
628                 this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
629             }
630 
631             // wrap the azimuth and bank angle
632             this.angles.az = Mat.wrap(this.angles.az, az_smin, az_smax);
633             this.angles.el = Mat.wrap(this.angles.el, el_smin, el_smax);
634             this.angles.bank = Mat.wrap(this.angles.bank, bank_smin, bank_smax);
635         } else {
636             // wrap and clamp the elevation into the slider range. if
637             // flipping the elevation gets us closer to the slider interval,
638             // do that, inverting the azimuth and bank angle to compensate
639             el_interval_loss = function (t) {
640                 if (t < el_smin) {
641                     return el_smin - t;
642                 } else if (el_smax < t) {
643                     return t - el_smax;
644                 } else {
645                     return 0;
646                 }
647             };
648             el_smid = 0.5 * (el_smin + el_smax);
649             el_equiv = Mat.wrap(
650                 this.angles.el,
651                 el_smid - Math.PI,
652                 el_smid + Math.PI
653             );
654             el_flip_equiv = Mat.wrap(
655                 Math.PI - this.angles.el,
656                 el_smid - Math.PI,
657                 el_smid + Math.PI
658             );
659             el_equiv_loss = el_interval_loss(el_equiv);
660             el_flip_equiv_loss = el_interval_loss(el_flip_equiv);
661             if (el_equiv_loss <= el_flip_equiv_loss) {
662                 this.angles.el = Mat.clamp(el_equiv, el_smin, el_smax);
663             } else {
664                 this.angles.el = Mat.clamp(el_flip_equiv, el_smin, el_smax);
665                 this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
666                 this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
667             }
668 
669             // wrap and clamp the azimuth and bank angle into the slider range
670             this.angles.az = Mat.wrapAndClamp(this.angles.az, az_smin, az_smax, 2 * Math.PI);
671             this.angles.bank = Mat.wrapAndClamp(this.angles.bank, bank_smin, bank_smax, 2 * Math.PI);
672 
673             // since we're using `clamp`, angles may have changed
674             this.matrix3DRot = this.getRotationFromAngles();
675         }
676 
677         // restore slider positions
678         this.setSlidersFromAngles();
679     },
680 
681     /**
682      * @private
683      * @returns {Array}
684      */
685     _updateCentralProjection: function () {
686         var zf = 20, // near clip plane
687             zn = 8, // far clip plane
688 
689             // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html
690             // bbox3D is always at the world origin, i.e. T_obj is the unit matrix.
691             // All vectors contain affine coordinates and have length 3
692             // The matrices are of size 4x4.
693             r, A;
694 
695         // set distance from view box center to camera
696         r = Type.evaluate(this.visProp.r);
697         if (r === 'auto') {
698             r = Mat.hypot(
699                 this.bbox3D[0][0] - this.bbox3D[0][1],
700                 this.bbox3D[1][0] - this.bbox3D[1][1],
701                 this.bbox3D[2][0] - this.bbox3D[2][1]
702             ) * 1.01;
703         }
704 
705         // compute camera transformation
706         this.boxToCam = this.matrix3DRot.map((row) => row.slice());
707         this.boxToCam[3][0] = -r;
708 
709         // compute focal distance and clip space transformation
710         this.focalDist = 1 / Math.tan(0.5 * Type.evaluate(this.visProp.fov));
711         A = [
712             [0, 0, 0, -1],
713             [0, this.focalDist, 0, 0],
714             [0, 0, this.focalDist, 0],
715             [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)]
716         ];
717 
718         return Mat.matMatMult(A, this.boxToCam);
719     },
720 
721     /**
722      * Comparison function for 3D points. It is used to sort points according to their z-index.
723      * @param {Point3D} a
724      * @param {Point3D} b
725      * @returns Integer
726      */
727     compareDepth: function (a, b) {
728         var worldDiff = [0,
729                          a.coords[1] - b.coords[1],
730                          a.coords[2] - b.coords[2],
731                          a.coords[3] - b.coords[3]],
732             oriBoxDiff = Mat.matVecMult(this.matrix3DRot, Mat.matVecMult(this.shift, worldDiff));
733         return oriBoxDiff[3];
734     },
735 
736     // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles.
737     update: function () {
738         var r = this.r,
739             stretch = [
740                 [1, 0, 0, 0],
741                 [0, -r, 0, 0],
742                 [0, 0, -r, 0],
743                 [0, 0, 0, 1]
744             ],
745             mat2D, objectToClip, size,
746             dx, dy,
747             objectsList;
748 
749         if (
750             !Type.exists(this.el_slide) ||
751             !Type.exists(this.az_slide) ||
752             !Type.exists(this.bank_slide) ||
753             !this.needsUpdate
754         ) {
755             return this;
756         }
757 
758         mat2D = [
759             [1, 0, 0],
760             [0, 1, 0],
761             [0, 0, 1]
762         ];
763 
764         this.projectionType = Type.evaluate(this.visProp.projection).toLowerCase();
765 
766         // override angle slider bounds when trackball navigation is enabled
767         if (this.trackballEnabled !== Type.evaluate(this.visProp.trackball.enabled)) {
768             this.updateAngleSliderBounds();
769         }
770 
771         if (this._hasMoveTrackball) {
772             // The trackball has been moved since the last update, so we do
773             // trackball navigation. When the trackball is enabled, a drag
774             // event is interpreted as a trackball movement unless it's
775             // caught by something else, like point dragging. When the
776             // trackball is disabled, the trackball movement flag should
777             // never be set
778             this.matrix3DRot = this.updateProjectionTrackball();
779             this.setAnglesFromRotation();
780         } else if (this.anglesHaveMoved()) {
781             // The trackball hasn't been moved since the last up date, but
782             // the Tait-Bryan angles have been, so we do angle navigation
783             this.getAnglesFromSliders();
784             this.matrix3DRot = this.getRotationFromAngles();
785         }
786 
787         /**
788          * The translation that moves the center of the view box to the origin.
789          */
790         this.shift = [
791             [1, 0, 0, 0],
792             [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0],
793             [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0],
794             [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1]
795         ];
796 
797         switch (this.projectionType) {
798             case 'central': // Central projection
799 
800                 // Add a final transformation to scale and shift the projection
801                 // on the board, usually called viewport.
802                 size = 2 * 0.4;
803                 mat2D[1][1] = this.size[0] / size; // w / d_x
804                 mat2D[2][2] = this.size[1] / size; // h / d_y
805                 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x
806                 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y
807                 // The transformations this.matrix3D and mat2D can not be combined at this point,
808                 // since the projected vectors have to be normalized in between in project3DTo2D
809                 this.viewPortTransform = mat2D;
810 
811                 objectToClip = this._updateCentralProjection();
812                 // this.matrix3D is a 4x4 matrix
813                 this.matrix3D = Mat.matMatMult(objectToClip, this.shift);
814                 break;
815 
816             case 'parallel': // Parallel projection
817             default:
818                 // Add a final transformation to scale and shift the projection
819                 // on the board, usually called viewport.
820                 dx = this.bbox3D[0][1] - this.bbox3D[0][0];
821                 dy = this.bbox3D[1][1] - this.bbox3D[1][0];
822                 mat2D[1][1] = this.size[0] / dx; // w / d_x
823                 mat2D[2][2] = this.size[1] / dy; // h / d_y
824                 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x
825                 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y
826 
827                 // Combine all transformations, this.matrix3D is a 3x4 matrix
828                 this.matrix3D = Mat.matMatMult(
829                     mat2D,
830                     Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3)
831                 );
832         }
833 
834         // if depth-ordering for points was just switched on, initialize the
835         // list of points
836         if (this.visProp.depthorderpoints && this.points === null) {
837             objectsList = Object.values(this.objects);
838             this.points = objectsList.filter(
839                 el => el.type === Const.OBJECT_TYPE_POINT3D
840             );
841         }
842 
843         // if depth-ordering for points was just switched off, throw away the
844         // list of points
845         if (!this.visProp.depthorderpoints && this.points !== null) {
846             this.points = null;
847         }
848 
849         // depth-order visible points. the `setLayer` method is used here to
850         // re-order the points within each layer: it has the side effect of
851         // moving the target element to the end of the layer's child list
852         if (this.visProp.depthorderpoints && this.board.renderer && this.board.renderer.type === 'svg') {
853             this.points
854                 .filter((pt) => Type.evaluate(pt.element2D.visProp.visible))
855                 .sort(this.compareDepth.bind(this))
856                 .forEach((pt) => this.board.renderer.setLayer(pt.element2D, pt.element2D.visProp.layer));
857 
858             /* [DEBUG] list oriented box coordinates in depth order */
859             // console.log('depth-ordered points in oriented box coordinates');
860             // this.points
861             //     .filter((pt) => pt.element2D.visProp.visible)
862             //     .sort(compareDepth)
863             //     .forEach(function (pt) {
864             //         console.log(Mat.matVecMult(that.matrix3DRot, Mat.matVecMult(that.shift, pt.coords)));
865             //     });
866         }
867 
868         return this;
869     },
870 
871     updateRenderer: function () {
872         this.needsUpdate = false;
873         return this;
874     },
875 
876     removeObject: function (object, saveMethod) {
877         var i;
878 
879         // this.board.removeObject(object, saveMethod);
880         if (Type.isArray(object)) {
881             for (i = 0; i < object.length; i++) {
882                 this.removeObject(object[i]);
883             }
884             return this;
885         }
886 
887         object = this.select(object);
888 
889         // // If the object which is about to be removed unknown or a string, do nothing.
890         // // it is a string if a string was given and could not be resolved to an element.
891         if (!Type.exists(object) || Type.isString(object)) {
892             return this;
893         }
894 
895         try {
896             //     // remove all children.
897             //     for (el in object.childElements) {
898             //         if (object.childElements.hasOwnProperty(el)) {
899             //             object.childElements[el].board.removeObject(object.childElements[el]);
900             //         }
901             //     }
902 
903             delete this.objects[object.id];
904         } catch (e) {
905             JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e);
906         }
907 
908         // this.update();
909 
910         this.board.removeObject(object, saveMethod);
911 
912         return this;
913     },
914 
915     /**
916      * Map world coordinates to focal coordinates. These coordinate systems
917      * are explained in the {@link JXG.View3D#boxToCam} matrix
918      * documentation.
919      *
920      * @param {Array} pWorld A world space point, in homogeneous coordinates.
921      * @param {Boolean} [homog=true] Whether to return homogeneous coordinates.
922      * If false, projects down to ordinary coordinates.
923      */
924     worldToFocal: function (pWorld, homog = true) {
925         var k,
926             pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld));
927         pView[3] -= pView[0] * this.focalDist;
928         if (homog) {
929             return pView;
930         } else {
931             for (k = 1; k < 4; k++) {
932                 pView[k] /= pView[0];
933             }
934             return pView.slice(1, 4);
935         }
936     },
937 
938     /**
939      * Project 3D coordinates to 2D board coordinates
940      * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
941      *
942      * @param  {Number|Array} x
943      * @param  {Number[]} y
944      * @param  {Number[]} z
945      * @returns {Array} Array of length 3 containing the projection on to the board
946      * in homogeneous user coordinates.
947      */
948     project3DTo2D: function (x, y, z) {
949         var vec, w;
950         if (arguments.length === 3) {
951             vec = [1, x, y, z];
952         } else {
953             // Argument is an array
954             if (x.length === 3) {
955                 // vec = [1].concat(x);
956                 vec = x.slice();
957                 vec.unshift(1);
958             } else {
959                 vec = x;
960             }
961         }
962 
963         w = Mat.matVecMult(this.matrix3D, vec);
964 
965         switch (this.projectionType) {
966             case 'central':
967                 w[1] /= w[0];
968                 w[2] /= w[0];
969                 w[3] /= w[0];
970                 w[0] /= w[0];
971                 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3));
972 
973             case 'parallel':
974             default:
975                 return w;
976         }
977     },
978 
979     /**
980      * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y.
981      * Setting R = mat^(-1) gives
982      *   1/ w0 * (1, x, y, d)^T = R * v2d.
983      * The first and the last row of this equation allows to determine 1/w0 and h.
984      *
985      * @param {Array} mat
986      * @param {Array} v2d
987      * @param {Number} d
988      * @returns Array
989      * @private
990      */
991     _getW0: function (mat, v2d, d) {
992         var R = Mat.inverse(mat),
993             R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2],
994             R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2],
995             w, h, det;
996 
997         det = d * R[0][3] - R[3][3];
998         w = (R2 * R[0][3] - R1 * R[3][3]) / det;
999         h = (R2 - R1 * d) / det;
1000         return [1 / w, h];
1001     },
1002 
1003     /**
1004      * Project a 2D coordinate to the plane defined by point "foot"
1005      * and the normal vector `normal`.
1006      *
1007      * @param  {JXG.Point} point2d
1008      * @param  {Array} normal
1009      * @param  {Array} foot
1010      * @returns {Array} of length 4 containing the projected
1011      * point in homogeneous coordinates.
1012      */
1013     project2DTo3DPlane: function (point2d, normal, foot) {
1014         var mat, rhs, d, le, sol,
1015             n = normal.slice(1),
1016             v2d, w0, res;
1017 
1018         foot = foot || [1, 0, 0, 0];
1019         le = Mat.norm(n, 3);
1020         d = Mat.innerProduct(foot.slice(1), n, 3) / le;
1021 
1022         if (this.projectionType === 'parallel') {
1023             mat = this.matrix3D.slice(0, 3); // Copy each row by reference
1024             mat.push([0, n[0], n[1], n[2]]);
1025 
1026             // 2D coordinates of point:
1027             rhs = point2d.coords.usrCoords.slice();
1028             rhs.push(d);
1029             try {
1030                 // Prevent singularity in case elevation angle is zero
1031                 if (mat[2][3] === 1.0) {
1032                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
1033                 }
1034                 sol = Mat.Numerics.Gauss(mat, rhs);
1035             } catch (e) {
1036                 sol = [0, NaN, NaN, NaN];
1037             }
1038         } else {
1039             mat = this.matrix3D;
1040 
1041             // 2D coordinates of point:
1042             rhs = point2d.coords.usrCoords.slice();
1043 
1044             v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs);
1045             res = this._getW0(mat, v2d, d);
1046             w0 = res[0];
1047             rhs = [
1048                 v2d[0] * w0,
1049                 v2d[1] * w0,
1050                 v2d[2] * w0,
1051                 res[1] * w0
1052             ];
1053             try {
1054                 // Prevent singularity in case elevation angle is zero
1055                 if (mat[2][3] === 1.0) {
1056                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
1057                 }
1058 
1059                 sol = Mat.Numerics.Gauss(mat, rhs);
1060                 sol[1] /= sol[0];
1061                 sol[2] /= sol[0];
1062                 sol[3] /= sol[0];
1063                 // sol[3] = d;
1064                 sol[0] /= sol[0];
1065             } catch (err) {
1066                 sol = [0, NaN, NaN, NaN];
1067             }
1068         }
1069 
1070         return sol;
1071     },
1072 
1073     /**
1074      * Project a point on the screen to the nearest point, in screen
1075      * distance, on a line segment in 3d space. The inputs must be in
1076      * ordinary coordinates, but the output is in homogeneous coordinates.
1077      *
1078      * @param {Array} pScr The screen coordinates of the point to project.
1079      * @param {Array} end0 The world space coordinates of one end of the
1080      * line segment.
1081      * @param {Array} end1 The world space coordinates of the other end of
1082      * the line segment.
1083      */
1084     projectScreenToSegment: function (pScr, end0, end1) {
1085         var end0_2d = this.project3DTo2D(end0).slice(1, 3),
1086             end1_2d = this.project3DTo2D(end1).slice(1, 3),
1087             dir_2d = [
1088                 end1_2d[0] - end0_2d[0],
1089                 end1_2d[1] - end0_2d[1]
1090             ],
1091             dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d),
1092             diff = [
1093                 pScr[0] - end0_2d[0],
1094                 pScr[1] - end0_2d[1]
1095             ],
1096             s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter
1097             mid, mid_2d, mid_diff, m,
1098 
1099             t, // view-space affine parameter
1100             t_clamped, // affine parameter clamped to range
1101             t_clamped_co;
1102 
1103         if (this.projectionType === 'central') {
1104             mid = [
1105                 0.5 * (end0[0] + end1[0]),
1106                 0.5 * (end0[1] + end1[1]),
1107                 0.5 * (end0[2] + end1[2])
1108             ];
1109             mid_2d = this.project3DTo2D(mid).slice(1, 3);
1110             mid_diff = [
1111                 mid_2d[0] - end0_2d[0],
1112                 mid_2d[1] - end0_2d[1]
1113             ];
1114             m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq;
1115 
1116             // the view-space affine parameter s is related to the
1117             // screen-space affine parameter t by a Möbius transformation,
1118             // which is determined by the following relations:
1119             //
1120             // s | t
1121             // -----
1122             // 0 | 0
1123             // m | 1/2
1124             // 1 | 1
1125             //
1126             t = (1 - m) * s / ((1 - 2 * m) * s + m);
1127         } else {
1128             t = s;
1129         }
1130 
1131         t_clamped = Math.min(Math.max(t, 0), 1);
1132         t_clamped_co = 1 - t_clamped;
1133         return [
1134             1,
1135             t_clamped_co * end0[0] + t_clamped * end1[0],
1136             t_clamped_co * end0[1] + t_clamped * end1[1],
1137             t_clamped_co * end0[2] + t_clamped * end1[2]
1138         ];
1139     },
1140 
1141     /**
1142      * Project a 2D coordinate to a new 3D position by keeping
1143      * the 3D x, y coordinates and changing only the z coordinate.
1144      * All horizontal moves of the 2D point are ignored.
1145      *
1146      * @param {JXG.Point} point2d
1147      * @param {Array} base_c3d
1148      * @returns {Array} of length 4 containing the projected
1149      * point in homogeneous coordinates.
1150      */
1151     project2DTo3DVertical: function (point2d, base_c3d) {
1152         var pScr = point2d.coords.usrCoords.slice(1, 3),
1153             end0 = [base_c3d[1], base_c3d[2], this.bbox3D[2][0]],
1154             end1 = [base_c3d[1], base_c3d[2], this.bbox3D[2][1]];
1155 
1156         return this.projectScreenToSegment(pScr, end0, end1);
1157     },
1158 
1159     /**
1160      * Limit 3D coordinates to the bounding cube.
1161      *
1162      * @param {Array} c3d 3D coordinates [x,y,z]
1163      * @returns Array with updated 3D coordinates.
1164      */
1165     project3DToCube: function (c3d) {
1166         var cube = this.bbox3D;
1167         if (c3d[1] < cube[0][0]) {
1168             c3d[1] = cube[0][0];
1169         }
1170         if (c3d[1] > cube[0][1]) {
1171             c3d[1] = cube[0][1];
1172         }
1173         if (c3d[2] < cube[1][0]) {
1174             c3d[2] = cube[1][0];
1175         }
1176         if (c3d[2] > cube[1][1]) {
1177             c3d[2] = cube[1][1];
1178         }
1179         if (c3d[3] < cube[2][0]) {
1180             c3d[3] = cube[2][0];
1181         }
1182         if (c3d[3] > cube[2][1]) {
1183             c3d[3] = cube[2][1];
1184         }
1185 
1186         return c3d;
1187     },
1188 
1189     /**
1190      * Intersect a ray with the bounding cube of the 3D view.
1191      * @param {Array} p 3D coordinates [x,y,z]
1192      * @param {Array} d 3D direction vector of the line (array of length 3)
1193      * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
1194      * @returns Affine ratio of the intersection of the line with the cube.
1195      */
1196     intersectionLineCube: function (p, d, r) {
1197         var r_n, i, r0, r1;
1198 
1199         r_n = r;
1200         for (i = 0; i < 3; i++) {
1201             if (d[i] !== 0) {
1202                 r0 = (this.bbox3D[i][0] - p[i]) / d[i];
1203                 r1 = (this.bbox3D[i][1] - p[i]) / d[i];
1204                 if (r < 0) {
1205                     r_n = Math.max(r_n, Math.min(r0, r1));
1206                 } else {
1207                     r_n = Math.min(r_n, Math.max(r0, r1));
1208                 }
1209             }
1210         }
1211         return r_n;
1212     },
1213 
1214     /**
1215      * Test if coordinates are inside of the bounding cube.
1216      * @param {array} q 3D coordinates [x,y,z] of a point.
1217      * @returns Boolean
1218      */
1219     isInCube: function (q) {
1220         return (
1221             q[0] > this.bbox3D[0][0] - Mat.eps &&
1222             q[0] < this.bbox3D[0][1] + Mat.eps &&
1223             q[1] > this.bbox3D[1][0] - Mat.eps &&
1224             q[1] < this.bbox3D[1][1] + Mat.eps &&
1225             q[2] > this.bbox3D[2][0] - Mat.eps &&
1226             q[2] < this.bbox3D[2][1] + Mat.eps
1227         );
1228     },
1229 
1230     /**
1231      *
1232      * @param {JXG.Plane3D} plane1
1233      * @param {JXG.Plane3D} plane2
1234      * @param {JXG.Plane3D} d
1235      * @returns {Array} of length 2 containing the coordinates of the defining points of
1236      * of the intersection segment.
1237      */
1238     intersectionPlanePlane: function (plane1, plane2, d) {
1239         var ret = [[], []],
1240             p,
1241             dir,
1242             r,
1243             q;
1244 
1245         d = d || plane2.d;
1246 
1247         p = Mat.Geometry.meet3Planes(
1248             plane1.normal,
1249             plane1.d,
1250             plane2.normal,
1251             d,
1252             Mat.crossProduct(plane1.normal, plane2.normal),
1253             0
1254         );
1255         dir = Mat.Geometry.meetPlanePlane(
1256             plane1.vec1,
1257             plane1.vec2,
1258             plane2.vec1,
1259             plane2.vec2
1260         );
1261         r = this.intersectionLineCube(p, dir, Infinity);
1262         q = Mat.axpy(r, dir, p);
1263         if (this.isInCube(q)) {
1264             ret[0] = q;
1265         }
1266         r = this.intersectionLineCube(p, dir, -Infinity);
1267         q = Mat.axpy(r, dir, p);
1268         if (this.isInCube(q)) {
1269             ret[1] = q;
1270         }
1271         return ret;
1272     },
1273 
1274     /**
1275      * Generate mesh for a surface / plane.
1276      * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
1277      * @param {Array|Function} func
1278      * @param {Array} interval_u
1279      * @param {Array} interval_v
1280      * @returns Array
1281      * @private
1282      *
1283      * @example
1284      *  var el = view.create('curve', [[], []]);
1285      *  el.updateDataArray = function () {
1286      *      var steps_u = Type.evaluate(this.visProp.stepsu),
1287      *           steps_v = Type.evaluate(this.visProp.stepsv),
1288      *           r_u = Type.evaluate(this.range_u),
1289      *           r_v = Type.evaluate(this.range_v),
1290      *           func, ret;
1291      *
1292      *      if (this.F !== null) {
1293      *          func = this.F;
1294      *      } else {
1295      *          func = [this.X, this.Y, this.Z];
1296      *      }
1297      *      ret = this.view.getMesh(func,
1298      *          r_u.concat([steps_u]),
1299      *          r_v.concat([steps_v]));
1300      *
1301      *      this.dataX = ret[0];
1302      *      this.dataY = ret[1];
1303      *  };
1304      *
1305      */
1306     getMesh: function (func, interval_u, interval_v) {
1307         var i_u, i_v, u, v,
1308             c2d, delta_u, delta_v,
1309             p = [0, 0, 0],
1310             steps_u = interval_u[2],
1311             steps_v = interval_v[2],
1312             dataX = [],
1313             dataY = [];
1314 
1315         delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
1316         delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
1317 
1318         for (i_u = 0; i_u <= steps_u; i_u++) {
1319             u = interval_u[0] + delta_u * i_u;
1320             for (i_v = 0; i_v <= steps_v; i_v++) {
1321                 v = interval_v[0] + delta_v * i_v;
1322                 if (Type.isFunction(func)) {
1323                     p = func(u, v);
1324                 } else {
1325                     p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1326                 }
1327                 c2d = this.project3DTo2D(p);
1328                 dataX.push(c2d[1]);
1329                 dataY.push(c2d[2]);
1330             }
1331             dataX.push(NaN);
1332             dataY.push(NaN);
1333         }
1334 
1335         for (i_v = 0; i_v <= steps_v; i_v++) {
1336             v = interval_v[0] + delta_v * i_v;
1337             for (i_u = 0; i_u <= steps_u; i_u++) {
1338                 u = interval_u[0] + delta_u * i_u;
1339                 if (Type.isFunction(func)) {
1340                     p = func(u, v);
1341                 } else {
1342                     p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1343                 }
1344                 c2d = this.project3DTo2D(p);
1345                 dataX.push(c2d[1]);
1346                 dataY.push(c2d[2]);
1347             }
1348             dataX.push(NaN);
1349             dataY.push(NaN);
1350         }
1351 
1352         return [dataX, dataY];
1353     },
1354 
1355     /**
1356      *
1357      */
1358     animateAzimuth: function () {
1359         var s = this.az_slide._smin,
1360             e = this.az_slide._smax,
1361             sdiff = e - s,
1362             newVal = this.az_slide.Value() + 0.1;
1363 
1364         this.az_slide.position = (newVal - s) / sdiff;
1365         if (this.az_slide.position > 1) {
1366             this.az_slide.position = 0.0;
1367         }
1368         this.board.update();
1369 
1370         this.timeoutAzimuth = setTimeout(function () {
1371             this.animateAzimuth();
1372         }.bind(this), 200);
1373     },
1374 
1375     /**
1376      *
1377      */
1378     stopAzimuth: function () {
1379         clearTimeout(this.timeoutAzimuth);
1380         this.timeoutAzimuth = null;
1381     },
1382 
1383     /**
1384      * Check if vertical dragging is enabled and which action is needed.
1385      * Default is shiftKey.
1386      *
1387      * @returns Boolean
1388      * @private
1389      */
1390     isVerticalDrag: function () {
1391         var b = this.board,
1392             key;
1393         if (!Type.evaluate(this.visProp.verticaldrag.enabled)) {
1394             return false;
1395         }
1396         key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key';
1397         return b[key];
1398     },
1399 
1400     /**
1401      * Sets camera view to the given values.
1402      *
1403      * @param {Number} az Value of azimuth.
1404      * @param {Number} el Value of elevation.
1405      * @param {Number} [r] Value of radius.
1406      *
1407      * @returns {Object} Reference to the view.
1408      */
1409     setView: function (az, el, r) {
1410         r = r || this.r;
1411 
1412         this.az_slide.setValue(az);
1413         this.el_slide.setValue(el);
1414         this.r = r;
1415         this.board.update();
1416 
1417         return this;
1418     },
1419 
1420     /**
1421      * Changes view to the next view stored in the attribute `values`.
1422      *
1423      * @see View3D#values
1424      *
1425      * @returns {Object} Reference to the view.
1426      */
1427     nextView: function () {
1428         var views = Type.evaluate(this.visProp.values),
1429             n = this.visProp._currentview;
1430 
1431         n = (n + 1) % views.length;
1432         this.setCurrentView(n);
1433 
1434         return this;
1435     },
1436 
1437     /**
1438      * Changes view to the previous view stored in the attribute `values`.
1439      *
1440      * @see View3D#values
1441      *
1442      * @returns {Object} Reference to the view.
1443      */
1444     previousView: function () {
1445         var views = Type.evaluate(this.visProp.values),
1446             n = this.visProp._currentview;
1447 
1448         n = (n + views.length - 1) % views.length;
1449         this.setCurrentView(n);
1450 
1451         return this;
1452     },
1453 
1454     /**
1455      * Changes view to the determined view stored in the attribute `values`.
1456      *
1457      * @see View3D#values
1458      *
1459      * @param {Number} n Index of view in attribute `values`.
1460      * @returns {Object} Reference to the view.
1461      */
1462     setCurrentView: function (n) {
1463         var views = Type.evaluate(this.visProp.values);
1464 
1465         if (n < 0 || n >= views.length) {
1466             n = ((n % views.length) + views.length) % views.length;
1467         }
1468 
1469         this.setView(views[n][0], views[n][1], views[n][2]);
1470         this.visProp._currentview = n;
1471 
1472         return this;
1473     },
1474 
1475     /**
1476      * Controls the navigation in az direction using either the keyboard or a pointer.
1477      *
1478      * @private
1479      *
1480      * @param {event} evt either the keydown or the pointer event
1481      * @returns view
1482      */
1483     _azEventHandler: function (evt) {
1484         var smax = this.az_slide._smax,
1485             smin = this.az_slide._smin,
1486             speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)),
1487             delta = evt.movementX,
1488             az = this.az_slide.Value(),
1489             el = this.el_slide.Value();
1490 
1491         // Doesn't allow navigation if another moving event is triggered
1492         if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1493             return this;
1494         }
1495 
1496         // Calculate new az value if keyboard events are triggered
1497         // Plus if right-button, minus if left-button
1498         if (Type.evaluate(this.visProp.az.keyboard.enabled)) {
1499             if (evt.key === 'ArrowRight') {
1500                 az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
1501             } else if (evt.key === 'ArrowLeft') {
1502                 az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
1503             }
1504         }
1505 
1506         if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && evt.key == null) {
1507             az += delta * speed;
1508         }
1509 
1510         // Project the calculated az value to a usable value in the interval [smin,smax]
1511         // Use modulo if continuous is true
1512         if (Type.evaluate(this.visProp.az.continuous)) {
1513             az = Mat.wrap(az, smin, smax);
1514         } else {
1515             if (az > 0) {
1516                 az = Math.min(smax, az);
1517             } else if (az < 0) {
1518                 az = Math.max(smin, az);
1519             }
1520         }
1521 
1522         this.setView(az, el);
1523         return this;
1524     },
1525 
1526     /**
1527      * Controls the navigation in el direction using either the keyboard or a pointer.
1528      *
1529      * @private
1530      *
1531      * @param {event} evt either the keydown or the pointer event
1532      * @returns view
1533      */
1534     _elEventHandler: function (evt) {
1535         var smax = this.el_slide._smax,
1536             smin = this.el_slide._smin,
1537             speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed),
1538             delta = evt.movementY,
1539             az = this.az_slide.Value(),
1540             el = this.el_slide.Value();
1541 
1542         // Doesn't allow navigation if another moving event is triggered
1543         if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1544             return this;
1545         }
1546 
1547         // Calculate new az value if keyboard events are triggered
1548         // Plus if down-button, minus if up-button
1549         if (Type.evaluate(this.visProp.el.keyboard.enabled)) {
1550             if (evt.key === 'ArrowUp') {
1551                 el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
1552             } else if (evt.key === 'ArrowDown') {
1553                 el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
1554             }
1555         }
1556 
1557         if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && evt.key == null) {
1558             el += delta * speed;
1559         }
1560 
1561         // Project the calculated el value to a usable value in the interval [smin,smax]
1562         // Use modulo if continuous is true and the trackball is disabled
1563         if (Type.evaluate(this.visProp.el.continuous) && !this.trackballEnabled) {
1564             el = Mat.wrap(el, smin, smax);
1565         } else {
1566             if (el > 0) {
1567                 el = Math.min(smax, el);
1568             } else if (el < 0) {
1569                 el = Math.max(smin, el);
1570             }
1571         }
1572 
1573         this.setView(az, el);
1574         return this;
1575     },
1576 
1577     /**
1578      * Controls the navigation in bank direction using either the keyboard or a pointer.
1579      *
1580      * @private
1581      *
1582      * @param {event} evt either the keydown or the pointer event
1583      * @returns view
1584      */
1585     _bankEventHandler: function (evt) {
1586         var smax = this.bank_slide._smax,
1587             smin = this.bank_slide._smin,
1588             step, speed,
1589             delta = evt.deltaY,
1590             bank = this.bank_slide.Value();
1591 
1592         // Doesn't allow navigation if another moving event is triggered
1593         if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1594             return this;
1595         }
1596 
1597         // Calculate new bank value if keyboard events are triggered
1598         // Plus if down-button, minus if up-button
1599         if (Type.evaluate(this.visProp.bank.keyboard.enabled)) {
1600             step = Type.evaluate(this.visProp.bank.keyboard.step) * Math.PI / 180;
1601             if (evt.key === '.' || evt.key === '<') {
1602                 bank -= step;
1603             } else if (evt.key === ',' || evt.key === '>') {
1604                 bank += step;
1605             }
1606         }
1607 
1608         if (Type.evaluate(this.visProp.bank.pointer.enabled) && (delta !== 0) && evt.key == null) {
1609             speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.bank.pointer.speed);
1610             bank += delta * speed;
1611 
1612             // prevent the pointer wheel from scrolling the page
1613             evt.preventDefault();
1614         }
1615 
1616         // Project the calculated bank value to a usable value in the interval [smin,smax]
1617         if (Type.evaluate(this.visProp.bank.continuous)) {
1618             // in continuous mode, wrap value around slider range
1619             bank = Mat.wrap(bank, smin, smax);
1620         } else {
1621             // in non-continuous mode, clamp value to slider range
1622             bank = Mat.clamp(bank, smin, smax);
1623         }
1624 
1625         this.bank_slide.setValue(bank);
1626         this.board.update();
1627         return this;
1628     },
1629 
1630     _trackballHandler: function (evt) {
1631         var pos = this.board.getMousePosition(evt),
1632             x, y, center;
1633 
1634         center = new Coords(Const.COORDS_BY_USER, [this.llftCorner[0] + this.size[0] * 0.5, this.llftCorner[1] + this.size[1] * 0.5], this.board);
1635         x = pos[0] - center.scrCoords[1];
1636         y = pos[1] - center.scrCoords[2];
1637         this._trackball = {
1638             dx: evt.movementX,
1639             dy: -evt.movementY,
1640             x: x,
1641             y: -y
1642         };
1643         this.board.update();
1644         return this;
1645     },
1646 
1647     pointerDownHandler: function (evt) {
1648         var neededButton, neededKey, target;
1649 
1650         this._hasMoveAz = false;
1651         this._hasMoveEl = false;
1652         this._hasMoveBank = false;
1653         this._hasMoveTrackball = false;
1654 
1655         if (this.board.mode !== this.board.BOARD_MODE_NONE) {
1656             return;
1657         }
1658 
1659         if (Type.evaluate(this.visProp.trackball.enabled)) {
1660             neededButton = Type.evaluate(this.visProp.trackball.button);
1661             neededKey = Type.evaluate(this.visProp.trackball.key);
1662 
1663             // Move events for virtual trackball
1664             if (
1665                 (neededButton === -1 || neededButton === evt.button) &&
1666                 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1667             ) {
1668                 // If outside is true then the event listener is bound to the document, otherwise to the div
1669                 target = (Type.evaluate(this.visProp.trackball.outside)) ? document : this.board.containerObj;
1670                 Env.addEvent(target, 'pointermove', this._trackballHandler, this);
1671                 this._hasMoveTrackball = true;
1672             }
1673         } else {
1674             if (Type.evaluate(this.visProp.az.pointer.enabled)) {
1675                 neededButton = Type.evaluate(this.visProp.az.pointer.button);
1676                 neededKey = Type.evaluate(this.visProp.az.pointer.key);
1677 
1678                 // Move events for azimuth
1679                 if (
1680                     (neededButton === -1 || neededButton === evt.button) &&
1681                     (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1682                 ) {
1683                     // If outside is true then the event listener is bound to the document, otherwise to the div
1684                     target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1685                     Env.addEvent(target, 'pointermove', this._azEventHandler, this);
1686                     this._hasMoveAz = true;
1687                 }
1688             }
1689 
1690             if (Type.evaluate(this.visProp.el.pointer.enabled)) {
1691                 neededButton = Type.evaluate(this.visProp.el.pointer.button);
1692                 neededKey = Type.evaluate(this.visProp.el.pointer.key);
1693 
1694                 // Events for elevation
1695                 if (
1696                     (neededButton === -1 || neededButton === evt.button) &&
1697                     (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1698                 ) {
1699                     // If outside is true then the event listener is bound to the document, otherwise to the div
1700                     target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj;
1701                     Env.addEvent(target, 'pointermove', this._elEventHandler, this);
1702                     this._hasMoveEl = true;
1703                 }
1704             }
1705 
1706             if (Type.evaluate(this.visProp.bank.pointer.enabled)) {
1707                 neededButton = Type.evaluate(this.visProp.bank.pointer.button);
1708                 neededKey = Type.evaluate(this.visProp.bank.pointer.key);
1709 
1710                 // Events for bank
1711                 if (
1712                     (neededButton === -1 || neededButton === evt.button) &&
1713                     (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1714                 ) {
1715                     // If `outside` is true, we bind the event listener to
1716                     // the document. otherwise, we bind it to the div. we
1717                     // register the event listener as active so it can
1718                     // prevent the pointer wheel from scrolling the page
1719                     target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj;
1720                     Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false });
1721                     this._hasMoveBank = true;
1722                 }
1723             }
1724         }
1725         Env.addEvent(document, 'pointerup', this.pointerUpHandler, this);
1726     },
1727 
1728     pointerUpHandler: function (evt) {
1729         var target;
1730         if (this._hasMoveAz) {
1731             target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1732             Env.removeEvent(target, 'pointermove', this._azEventHandler, this);
1733             this._hasMoveAz = false;
1734         }
1735         if (this._hasMoveEl) {
1736             target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj;
1737             Env.removeEvent(target, 'pointermove', this._elEventHandler, this);
1738             this._hasMoveEl = false;
1739         }
1740         if (this._hasMoveBank) {
1741             target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj;
1742             Env.removeEvent(target, 'wheel', this._bankEventHandler, this);
1743             this._hasMoveBank = false;
1744         }
1745         if (this._hasMoveTrackball) {
1746             target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1747             Env.removeEvent(target, 'pointermove', this._trackballHandler, this);
1748             this._hasMoveTrackball = false;
1749         }
1750         Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this);
1751     }
1752 });
1753 
1754 /**
1755  * @class This element creates a 3D view.
1756  * @pseudo
1757  * @description  A View3D element provides the container and the methods to create and display 3D elements.
1758  * It is contained in a JSXGraph board.
1759  * @name View3D
1760  * @augments JXG.View3D
1761  * @constructor
1762  * @type Object
1763  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
1764  * @param {Array_Array_Array} lower,dim,cube  Here, lower is an array of the form [x, y] and
1765  * dim is an array of the form [w, h].
1766  * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is
1767  * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner
1768  * [x,y] and side lengths [w, h] of the board.
1769  * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]]
1770  * which determines the coordinate ranges of the 3D cube.
1771  *
1772  * @example
1773  *  var bound = [-5, 5];
1774  *  var view = board.create('view3d',
1775  *      [[-6, -3],
1776  *       [8, 8],
1777  *       [bound, bound, bound]],
1778  *      {
1779  *          // Main axes
1780  *          axesPosition: 'center',
1781  *          xAxis: { strokeColor: 'blue', strokeWidth: 3},
1782  *
1783  *          // Planes
1784  *          xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
1785  *          yPlaneFront: { visible: true, fillColor: 'blue'},
1786  *
1787  *          // Axes on planes
1788  *          xPlaneRearYAxis: {strokeColor: 'red'},
1789  *          xPlaneRearZAxis: {strokeColor: 'red'},
1790  *
1791  *          yPlaneFrontXAxis: {strokeColor: 'blue'},
1792  *          yPlaneFrontZAxis: {strokeColor: 'blue'},
1793  *
1794  *          zPlaneFrontXAxis: {visible: false},
1795  *          zPlaneFrontYAxis: {visible: false}
1796  *      });
1797  *
1798  * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div>
1799  * <script type="text/javascript">
1800  *     (function() {
1801  *         var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7',
1802  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
1803  *         var bound = [-5, 5];
1804  *         var view = board.create('view3d',
1805  *             [[-6, -3], [8, 8],
1806  *             [bound, bound, bound]],
1807  *             {
1808  *                 // Main axes
1809  *                 axesPosition: 'center',
1810  *                 xAxis: { strokeColor: 'blue', strokeWidth: 3},
1811  *                 // Planes
1812  *                 xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
1813  *                 yPlaneFront: { visible: true, fillColor: 'blue'},
1814  *                 // Axes on planes
1815  *                 xPlaneRearYAxis: {strokeColor: 'red'},
1816  *                 xPlaneRearZAxis: {strokeColor: 'red'},
1817  *                 yPlaneFrontXAxis: {strokeColor: 'blue'},
1818  *                 yPlaneFrontZAxis: {strokeColor: 'blue'},
1819  *                 zPlaneFrontXAxis: {visible: false},
1820  *                 zPlaneFrontYAxis: {visible: false}
1821  *             });
1822  *     })();
1823  *
1824  * </script><pre>
1825  *
1826  */
1827 JXG.createView3D = function (board, parents, attributes) {
1828     var view, attr, attr_az, attr_el, attr_bank,
1829         x, y, w, h,
1830         coords = parents[0], // llft corner
1831         size = parents[1]; // [w, h]
1832 
1833     attr = Type.copyAttributes(attributes, board.options, 'view3d');
1834     view = new JXG.View3D(board, parents, attr);
1835     view.defaultAxes = view.create('axes3d', parents, attributes);
1836 
1837     x = coords[0];
1838     y = coords[1];
1839     w = size[0];
1840     h = size[1];
1841 
1842     attr_az = Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider');
1843     attr_az.name = 'az';
1844 
1845     attr_el = Type.copyAttributes(attributes, board.options, 'view3d', 'el', 'slider');
1846     attr_el.name = 'el';
1847 
1848     attr_bank = Type.copyAttributes(attributes, board.options, 'view3d', 'bank', 'slider');
1849     attr_bank.name = 'bank';
1850 
1851     /**
1852      * Slider to adapt azimuth angle
1853      * @name JXG.View3D#az_slide
1854      * @type {Slider}
1855      */
1856     view.az_slide = board.create(
1857         'slider',
1858         [
1859             [x - 1, y - 2],
1860             [x + w + 1, y - 2],
1861             [
1862                 Type.evaluate(attr_az.min),
1863                 Type.evaluate(attr_az.start),
1864                 Type.evaluate(attr_az.max)
1865             ]
1866         ],
1867         attr_az
1868     );
1869     // view.az_slide.inherits.push(view);
1870     view.inherits.push(view.az_slide);
1871 
1872     /**
1873      * Slider to adapt elevation angle
1874      *
1875      * @name JXG.View3D#el_slide
1876      * @type {Slider}
1877      */
1878     view.el_slide = board.create(
1879         'slider',
1880         [
1881             [x - 1, y],
1882             [x - 1, y + h],
1883             [
1884                 Type.evaluate(attr_el.min),
1885                 Type.evaluate(attr_el.start),
1886                 Type.evaluate(attr_el.max)]
1887         ],
1888         attr_el
1889     );
1890     view.inherits.push(view.el_slide);
1891 
1892     /**
1893      * Slider to adjust bank angle
1894      *
1895      * @name JXG.View3D#bank_slide
1896      * @type {Slider}
1897      */
1898     view.bank_slide = board.create(
1899         'slider',
1900         [
1901             [x - 1, y + h + 2],
1902             [x + w + 1, y + h + 2],
1903             [
1904                 Type.evaluate(attr_bank.min),
1905                 Type.evaluate(attr_bank.start),
1906                 Type.evaluate(attr_bank.max)
1907             ]
1908         ],
1909         attr_bank
1910     );
1911     view.inherits.push(view.bank_slide);
1912 
1913     view.board.highlightInfobox = function (x, y, el) {
1914         var d, i, c3d, foot,
1915             pre = '<span style="color:black; font-size:200%">\u21C4  </span>',
1916             brd = el.board,
1917             arr, infobox,
1918             p = null;
1919 
1920         if (view.isVerticalDrag()) {
1921             pre = '<span style="color:black; font-size:200%">\u21C5  </span>';
1922         }
1923         // Search 3D parent
1924         for (i = 0; i < el.parents.length; i++) {
1925             p = brd.objects[el.parents[i]];
1926             if (p.is3D) {
1927                 break;
1928             }
1929         }
1930         if (p) {
1931             foot = [1, 0, 0, p.coords[3]];
1932             view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4);
1933 
1934             c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot);
1935             if (!view.isInCube(c3d)) {
1936                 view.board.highlightCustomInfobox('', p);
1937                 return;
1938             }
1939             d = Type.evaluate(p.visProp.infoboxdigits);
1940             infobox = view.board.infobox;
1941             if (d === 'auto') {
1942                 if (infobox.useLocale()) {
1943                     arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')'];
1944                 } else {
1945                     arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')'];
1946                 }
1947 
1948             } else {
1949                 if (infobox.useLocale()) {
1950                     arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')'];
1951                 } else {
1952                     arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')'];
1953                 }
1954             }
1955             view.board.highlightCustomInfobox(arr.join(''), p);
1956         } else {
1957             view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
1958         }
1959     };
1960 
1961 
1962     // Hack needed to enable addEvent for view3D:
1963     view.BOARD_MODE_NONE = 0x0000;
1964 
1965     // Add events for the keyboard navigation
1966     Env.addEvent(board.containerObj, 'keydown', function (event) {
1967         var neededKey,
1968             catchEvt = false;
1969 
1970         if (Type.evaluate(view.visProp.el.keyboard.enabled) &&
1971             (event.key === 'ArrowUp' || event.key === 'ArrowDown')
1972         ) {
1973             neededKey = Type.evaluate(view.visProp.el.keyboard.key);
1974             if (neededKey === 'none' ||
1975                 (neededKey.indexOf('shift') > -1 && event.shiftKey) ||
1976                 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
1977                 view._elEventHandler(event);
1978                 catchEvt = true;
1979             }
1980 
1981         }
1982         if (Type.evaluate(view.visProp.el.keyboard.enabled) &&
1983             (event.key === 'ArrowLeft' || event.key === 'ArrowRight')
1984         ) {
1985             neededKey = Type.evaluate(view.visProp.az.keyboard.key);
1986             if (neededKey === 'none' ||
1987                 (neededKey.indexOf('shift') > -1 && event.shiftKey) ||
1988                 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)
1989             ) {
1990                 view._azEventHandler(event);
1991                 catchEvt = true;
1992             }
1993         }
1994         if (Type.evaluate(view.visProp.bank.keyboard.enabled) && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) {
1995             neededKey = Type.evaluate(view.visProp.bank.keyboard.key);
1996             if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
1997                 view._bankEventHandler(event);
1998                 catchEvt = true;
1999             }
2000         }
2001         if (event.key === 'PageUp') {
2002             view.nextView();
2003             catchEvt = true;
2004         } else if (event.key === 'PageDown') {
2005             view.previousView();
2006             catchEvt = true;
2007         }
2008 
2009         if (catchEvt) {
2010             // We stop event handling only in the case if the keypress could be
2011             // used for the 3D view. If this is not done, input fields et al
2012             // can not be used any more.
2013             event.preventDefault();
2014         }
2015     }, view);
2016 
2017     // Add events for the pointer navigation
2018     Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view);
2019 
2020     // Initialize view rotation matrix
2021     view.getAnglesFromSliders();
2022     view.matrix3DRot = view.getRotationFromAngles();
2023 
2024     // override angle slider bounds when trackball navigation is enabled
2025     view.updateAngleSliderBounds();
2026 
2027     view.board.update();
2028 
2029     return view;
2030 };
2031 
2032 JXG.registerElement("view3d", JXG.createView3D);
2033 
2034 export default JXG.View3D;