1 /*
  2     Copyright 2008-2026
  3         Matthias Ehmann,
  4         Carsten Miller,
  5         Andreas Walter,
  6         Alfred Wassermann
  7 
  8     This file is part of JSXGraph.
  9 
 10     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 11 
 12     You can redistribute it and/or modify it under the terms of the
 13 
 14       * GNU Lesser General Public License as published by
 15         the Free Software Foundation, either version 3 of the License, or
 16         (at your option) any later version
 17       OR
 18       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 19 
 20     JSXGraph is distributed in the hope that it will be useful,
 21     but WITHOUT ANY WARRANTY; without even the implied warranty of
 22     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 23     GNU Lesser General Public License for more details.
 24 
 25     You should have received a copy of the GNU Lesser General Public License and
 26     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 27     and <https://opensource.org/licenses/MIT/>.
 28  */
 29 /*
 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 Numerics from "../math/numerics.js";
 49 import Env from "../utils/env.js";
 50 import GeometryElement from "../base/element.js";
 51 import Composition from "../base/composition.js";
 52 
 53 /**
 54  * 3D view inside a JXGraph board.
 55  *
 56  * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with
 57  * type {@link View3D} instead.
 58  *
 59  * @augments JXG.GeometryElement
 60  * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view
 61  * 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
 62  * [x,y] and side lengths [w, h] of the board.
 63  */
 64 JXG.View3D = function (board, parents, attributes) {
 65     this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D);
 66 
 67     /**
 68      * An associative array containing all geometric objects belonging to the view.
 69      * Key is the id of the object and value is a reference to the object.
 70      * @type Object
 71      * @private
 72      */
 73     this.objects = {};
 74 
 75     /**
 76      * An array containing all the elements in the view that are sorted due to their depth order.
 77      * @Type Object
 78      * @private
 79      */
 80     this.depthOrdered = {};
 81 
 82     /**
 83      * TODO: why deleted?
 84      * An array containing all geometric objects in this view in the order of construction.
 85      * @type Array
 86      * @private
 87      */
 88     // this.objectsList = [];
 89 
 90     /**
 91      * 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.
 92      * @type Object
 93      * @private
 94      */
 95     this.elementsByName = {};
 96 
 97     /**
 98      * Default axes of the 3D view, contains the axes of the view or null.
 99      *
100      * @type {Object}
101      * @default null
102      */
103     this.defaultAxes = null;
104 
105     /**
106      * The Tait-Bryan angles specifying the view box orientation
107      */
108     this.angles = {
109         az: null,
110         el: null,
111         bank: null
112     };
113 
114     /**
115      * @type {Array}
116      * The view box orientation matrix
117      */
118     this.matrix3DRot = [
119         [1, 0, 0, 0],
120         [0, 1, 0, 0],
121         [0, 0, 1, 0],
122         [0, 0, 0, 1]
123     ];
124 
125     // Used for z-index computation
126     this.matrix3DRotShift = [
127         [1, 0, 0, 0],
128         [0, 1, 0, 0],
129         [0, 0, 1, 0],
130         [0, 0, 0, 1]
131     ];
132 
133     /**
134      * @type  {Array}
135      * @private
136      */
137     // 3D-to-2D transformation matrix
138     this.matrix3D = [
139         [1, 0, 0, 0],
140         [0, 1, 0, 0],
141         [0, 0, 1, 0]
142     ];
143 
144     /**
145      * The 4×4 matrix that maps box coordinates to camera coordinates. These
146      * coordinate systems fit into the View3D coordinate atlas as follows.
147      * <ul>
148      * <li><b>World coordinates.</b> The coordinates used to specify object
149      * positions in a JSXGraph scene.</li>
150      * <li><b>Box coordinates.</b> The world coordinates translated to put the
151      * center of the view box at the origin.
152      * <li><b>Camera coordinates.</b> The coordinate system where the
153      * <code>x</code>, <code>y</code> plane is the screen, the origin is the
154      * center of the screen, and the <code>z</code> axis points out of the
155      * screen, toward the viewer.
156      * <li><b>Focal coordinates.</b> The camera coordinates translated to put
157      * the origin at the focal point, which is set back from the screen by the
158      * focal distance.</li>
159      * </ul>
160      * The <code>boxToCam</code> transformation is exposed to help 3D elements
161      * manage their 2D representations in central projection mode. To map world
162      * coordinates to focal coordinates, use the
163      * {@link JXG.View3D#worldToFocal} method.
164      * @type {Array}
165      */
166     this.boxToCam = [];
167 
168     /**
169      * @type array
170      * @private
171      */
172     // Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0.
173     this.llftCorner = parents[0];
174 
175     /**
176      * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0.
177      * @type array
178      * @private
179      */
180     this.size = parents[1];
181 
182     /**
183      * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view
184      * @type array
185      */
186     this.bbox3D = parents[2];
187 
188     /**
189      * The distance from the camera to the origin. In other words, the
190      * radius of the sphere where the camera sits.
191      * @type Number
192      * @default null
193      */
194     this.r = null;
195 
196     /**
197      * The distance from the camera to the screen. Computed automatically from
198      * the `fov` property.
199      * @type Number
200      */
201     this.focalDist = -1;
202 
203     /**
204      * Type of projection. Is set in in update().
205      * @type String
206      */
207     this.projectionType = 'parallel';
208 
209     /**
210      * Whether trackball navigation is currently enabled.
211      * @type String
212      */
213     this.trackballEnabled = false;
214 
215     /**
216      * Store last position of pointer.
217      * This is the successor to use evt.movementX/Y which caused problems on firefox
218      * @type Object
219      * @private
220      */
221     this._lastPos = {
222         x: 0,
223         y: 0
224     };
225 
226     this.timeoutAzimuth = null;
227 
228     this.zIndexMin = Infinity;
229     this.zIndexMax = -Infinity;
230 
231     this.id = this.board.setId(this, 'V');
232     this.board.finalizeAdding(this);
233     this.elType = 'view3d';
234 };
235 
236 JXG.View3D.prototype = new GeometryElement();
237 Type.copyMethodMap(JXG.View3D, {
238     // TODO
239 });
240 
241 JXG.extend(
242     JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ {
243 
244     /**
245      * Creates a new 3D element of type elementType.
246      * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
247      * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
248      * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
249      * methods for a list of possible parameters.
250      * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
251      * Common attributes are name, visible, strokeColor.
252      * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
253      * two or more elements.
254      */
255     create: function (elementType, parents, attributes) {
256         var prefix = [],
257             el;
258 
259         if (elementType.indexOf('3d') > 0) {
260             // is3D = true;
261             prefix.push(this);
262         }
263         el = this.board.create(elementType, prefix.concat(parents), attributes);
264 
265         return el;
266     },
267 
268     /**
269      * Select a single or multiple elements at once.
270      * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
271      * be used as a filter to return multiple elements at once filtered by the properties of the object.
272      * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
273      * The advanced filters consisting of objects or functions are ignored.
274      * @returns {JXG.GeometryElement3D|JXG.Composition}
275      * @example
276      * // select the element with name A
277      * view.select('A');
278      *
279      * // select all elements with strokecolor set to 'red' (but not '#ff0000')
280      * view.select({
281      *   strokeColor: 'red'
282      * });
283      *
284      * // select all points on or below the x/y plane and make them black.
285      * view.select({
286      *   elType: 'point3d',
287      *   Z: function (v) {
288      *     return v <= 0;
289      *   }
290      * }).setAttribute({color: 'black'});
291      *
292      * // select all elements
293      * view.select(function (el) {
294      *   return true;
295      * });
296      */
297     select: function (str, onlyByIdOrName) {
298         var flist,
299             olist,
300             i,
301             l,
302             s = str;
303 
304         if (s === null) {
305             return s;
306         }
307 
308         if (Type.isString(s) && s !== '') {
309             // It's a string, most likely an id or a name.
310             // Search by ID
311             if (Type.exists(this.objects[s])) {
312                 s = this.objects[s];
313                 // Search by name
314             } else if (Type.exists(this.elementsByName[s])) {
315                 s = this.elementsByName[s];
316                 // // Search by group ID
317                 // } else if (Type.exists(this.groups[s])) {
318                 //     s = this.groups[s];
319             }
320 
321         } else if (
322             !onlyByIdOrName &&
323             (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
324         ) {
325             // It's a function or an object, but not an element
326             flist = Type.filterElements(this.objectsList, s);
327 
328             olist = {};
329             l = flist.length;
330             for (i = 0; i < l; i++) {
331                 olist[flist[i].id] = flist[i];
332             }
333             s = new Composition(olist);
334 
335         } else if (
336             Type.isObject(s) &&
337             Type.exists(s.id) &&
338             !Type.exists(this.objects[s.id])
339         ) {
340             // It's an element which has been deleted (and still hangs around, e.g. in an attractor list)
341             s = null;
342         }
343 
344         return s;
345     },
346 
347     // set the Tait-Bryan angles to specify the current view rotation matrix
348     setAnglesFromRotation: function () {
349         var rem = this.matrix3DRot, // rotation remaining after angle extraction
350             rBank, cosBank, sinBank,
351             cosEl, sinEl,
352             cosAz, sinAz;
353 
354         // extract bank by rotating the view box z axis onto the camera yz plane
355         rBank = Math.sqrt(rem[1][3] * rem[1][3] + rem[2][3] * rem[2][3]);
356         if (rBank > Mat.eps) {
357             cosBank = rem[2][3] / rBank;
358             sinBank = rem[1][3] / rBank;
359         } else {
360             // if the z axis is pointed almost exactly at the screen, we
361             // keep the current bank value
362             cosBank = Math.cos(this.angles.bank);
363             sinBank = Math.sin(this.angles.bank);
364         }
365         rem = Mat.matMatMult([
366             [1, 0, 0, 0],
367             [0, cosBank, -sinBank, 0],
368             [0, sinBank, cosBank, 0],
369             [0, 0, 0, 1]
370         ], rem);
371         this.angles.bank = Math.atan2(sinBank, cosBank);
372 
373         // extract elevation by rotating the view box z axis onto the camera
374         // y axis
375         cosEl = rem[2][3];
376         sinEl = rem[3][3];
377         rem = Mat.matMatMult([
378             [1, 0, 0, 0],
379             [0, 1, 0, 0],
380             [0, 0, cosEl, sinEl],
381             [0, 0, -sinEl, cosEl]
382         ], rem);
383         this.angles.el = Math.atan2(sinEl, cosEl);
384 
385         // extract azimuth
386         cosAz = -rem[1][1];
387         sinAz = rem[3][1];
388         this.angles.az = Math.atan2(sinAz, cosAz);
389         if (this.angles.az < 0) this.angles.az += 2 * Math.PI;
390 
391         this.setSlidersFromAngles();
392     },
393 
394     anglesHaveMoved: function () {
395         return (
396             this._hasMoveAz || this._hasMoveEl ||
397             Math.abs(this.angles.az - this.az_slide.Value()) > Mat.eps ||
398             Math.abs(this.angles.el - this.el_slide.Value()) > Mat.eps ||
399             Math.abs(this.angles.bank - this.bank_slide.Value()) > Mat.eps
400         );
401     },
402 
403     getAnglesFromSliders: function () {
404         this.angles.az = this.az_slide.Value();
405         this.angles.el = this.el_slide.Value();
406         this.angles.bank = this.bank_slide.Value();
407     },
408 
409     setSlidersFromAngles: function () {
410         this.az_slide.setValue(this.angles.az);
411         this.el_slide.setValue(this.angles.el);
412         this.bank_slide.setValue(this.angles.bank);
413     },
414 
415     // return the rotation matrix specified by the current Tait-Bryan angles
416     getRotationFromAngles: function () {
417         var a, e, b, f,
418             cosBank, sinBank,
419             mat = [
420                 [1, 0, 0, 0],
421                 [0, 1, 0, 0],
422                 [0, 0, 1, 0],
423                 [0, 0, 0, 1]
424             ];
425 
426         // mat projects homogeneous 3D coords in View3D
427         // to homogeneous 2D coordinates in the board
428         a = this.angles.az;
429         e = this.angles.el;
430         b = this.angles.bank;
431         f = -Math.sin(e);
432 
433         mat[1][1] = -Math.cos(a);
434         mat[1][2] = Math.sin(a);
435         mat[1][3] = 0;
436 
437         mat[2][1] = f * Math.sin(a);
438         mat[2][2] = f * Math.cos(a);
439         mat[2][3] = Math.cos(e);
440 
441         mat[3][1] = Math.cos(e) * Math.sin(a);
442         mat[3][2] = Math.cos(e) * Math.cos(a);
443         mat[3][3] = Math.sin(e);
444 
445         cosBank = Math.cos(b);
446         sinBank = Math.sin(b);
447         mat = Mat.matMatMult([
448             [1, 0, 0, 0],
449             [0, cosBank, sinBank, 0],
450             [0, -sinBank, cosBank, 0],
451             [0, 0, 0, 1]
452         ], mat);
453 
454         return mat;
455 
456         /* this code, originally from `_updateCentralProjection`, is an
457          * alternate implementation of the azimuth-elevation matrix
458          * computation above. using this implementation instead of the
459          * current one might lead to simpler code in a future refactoring
460         var a, e, up,
461             ax, ay, az, v, nrm,
462             eye, d,
463             func_sphere;
464 
465         // finds the point on the unit sphere with the given azimuth and
466         // elevation, and returns its affine coordinates
467         func_sphere = function (az, el) {
468             return [
469                 Math.cos(az) * Math.cos(el),
470                 -Math.sin(az) * Math.cos(el),
471                 Math.sin(el)
472             ];
473         };
474 
475         a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere
476         e = this.el_slide.Value();
477 
478         // create an up vector and an eye vector which are 90 degrees out of phase
479         up = func_sphere(a, e + Math.PI / 2);
480         eye = func_sphere(a, e);
481         d = [eye[0], eye[1], eye[2]];
482 
483         nrm = Mat.norm(d, 3);
484         az = [d[0] / nrm, d[1] / nrm, d[2] / nrm];
485 
486         nrm = Mat.norm(up, 3);
487         v = [up[0] / nrm, up[1] / nrm, up[2] / nrm];
488 
489         ax = Mat.crossProduct(v, az);
490         ay = Mat.crossProduct(az, ax);
491 
492         this.matrix3DRot[1] = [0, ax[0], ax[1], ax[2]];
493         this.matrix3DRot[2] = [0, ay[0], ay[1], ay[2]];
494         this.matrix3DRot[3] = [0, az[0], az[1], az[2]];
495          */
496     },
497 
498     /**
499      * Project 2D point (x,y) to the virtual trackpad sphere,
500      * see Bell's virtual trackpad, and return z-component of the
501      * number.
502      *
503      * @param {Number} r
504      * @param {Number} x
505      * @param {Number} y
506      * @returns Number
507      * @private
508      */
509     _projectToSphere: function (r, x, y) {
510         var d = Mat.hypot(x, y),
511             t, z;
512 
513         if (d < r * 0.7071067811865475) { // Inside sphere
514             z = Math.sqrt(r * r - d * d);
515         } else {                          // On hyperbola
516             t = r / 1.414213562373095;
517             z = t * t / d;
518         }
519         return z;
520     },
521 
522     /**
523      * Determine 4x4 rotation matrix with Bell's virtual trackball.
524      *
525      * @returns {Array} 4x4 rotation matrix
526      * @private
527      */
528     updateProjectionTrackball: function (Pref) {
529         var R = 100,
530             dx, dy, dr2,
531             p1, p2, x, y, theta, t, d,
532             c, s, n,
533             mat = [
534                 [1, 0, 0, 0],
535                 [0, 1, 0, 0],
536                 [0, 0, 1, 0],
537                 [0, 0, 0, 1]
538             ];
539 
540         if (!Type.exists(this._trackball)) {
541             return this.matrix3DRot;
542         }
543 
544         dx = this._trackball.dx;
545         dy = this._trackball.dy;
546         dr2 = dx * dx + dy * dy;
547         if (dr2 > Mat.eps) {
548             // // Method by Hanson, "The rolling ball", Graphics Gems III, p.51
549             // // Rotation axis:
550             // //     n = (-dy/dr, dx/dr, 0)
551             // // Rotation angle around n:
552             // //     theta = atan(dr / R) approx dr / R
553             // dr = Math.sqrt(dr2);
554             // c = R / Math.hypot(R, dr);  // cos(theta)
555             // t = 1 - c;                  // 1 - cos(theta)
556             // s = dr / Math.hypot(R, dr); // sin(theta)
557             // n = [-dy / dr, dx / dr, 0];
558 
559             // Bell virtual trackpad, see
560             // https://opensource.apple.com/source/X11libs/X11libs-60/mesa/Mesa-7.8.2/progs/util/trackball.c.auto.html
561             // http://scv.bu.edu/documentation/presentations/visualizationworkshop08/materials/opengl/trackball.c.
562             // See also Henriksen, Sporring, Hornaek, "Virtual Trackballs revisited".
563             //
564             R = (this.size[0] * this.board.unitX + this.size[1] * this.board.unitY) * 0.25;
565             x = this._trackball.x;
566             y = this._trackball.y;
567 
568             p2 = [x, y, this._projectToSphere(R, x, y)];
569             x -= dx;
570             y -= dy;
571             p1 = [x, y, this._projectToSphere(R, x, y)];
572 
573             n = Mat.crossProduct(p1, p2);
574             d = Mat.hypot(n[0], n[1], n[2]);
575             n[0] /= d;
576             n[1] /= d;
577             n[2] /= d;
578 
579             t = Geometry.distance(p2, p1, 3) / (2 * R);
580             t = (t > 1.0) ? 1.0 : t;
581             t = (t < -1.0) ? -1.0 : t;
582             theta = 2.0 * Math.asin(t);
583             c = Math.cos(theta);
584             t = 1 - c;
585             s = Math.sin(theta);
586 
587             // Rotation by theta about the axis n. See equation 9.63 of
588             //
589             //   Ian Richard Cole. "Modeling CPV" (thesis). Loughborough
590             //   University. https://hdl.handle.net/2134/18050
591             //
592             mat[1][1] = c + n[0] * n[0] * t;
593             mat[2][1] = n[1] * n[0] * t + n[2] * s;
594             mat[3][1] = n[2] * n[0] * t - n[1] * s;
595 
596             mat[1][2] = n[0] * n[1] * t - n[2] * s;
597             mat[2][2] = c + n[1] * n[1] * t;
598             mat[3][2] = n[2] * n[1] * t + n[0] * s;
599 
600             mat[1][3] = n[0] * n[2] * t + n[1] * s;
601             mat[2][3] = n[1] * n[2] * t - n[0] * s;
602             mat[3][3] = c + n[2] * n[2] * t;
603         }
604 
605         mat = Mat.matMatMult(mat, this.matrix3DRot);
606         return mat;
607     },
608 
609     updateAngleSliderBounds: function () {
610         var az_smax, az_smin,
611             el_smax, el_smin, el_cover,
612             el_smid, el_equiv, el_flip_equiv,
613             el_equiv_loss, el_flip_equiv_loss, el_interval_loss,
614             bank_smax, bank_smin;
615 
616         // update stored trackball toggle
617         this.trackballEnabled = this.evalVisProp('trackball.enabled');
618 
619         // set slider bounds
620         if (this.trackballEnabled) {
621             this.az_slide.setMin(0);
622             this.az_slide.setMax(2 * Math.PI);
623             this.el_slide.setMin(-0.5 * Math.PI);
624             this.el_slide.setMax(0.5 * Math.PI);
625             this.bank_slide.setMin(-Math.PI);
626             this.bank_slide.setMax(Math.PI);
627         } else {
628             this.az_slide.setMin(this.visProp.az.slider.min);
629             this.az_slide.setMax(this.visProp.az.slider.max);
630             this.el_slide.setMin(this.visProp.el.slider.min);
631             this.el_slide.setMax(this.visProp.el.slider.max);
632             this.bank_slide.setMin(this.visProp.bank.slider.min);
633             this.bank_slide.setMax(this.visProp.bank.slider.max);
634         }
635 
636         // get new slider bounds
637         az_smax = this.az_slide._smax;
638         az_smin = this.az_slide._smin;
639         el_smax = this.el_slide._smax;
640         el_smin = this.el_slide._smin;
641         bank_smax = this.bank_slide._smax;
642         bank_smin = this.bank_slide._smin;
643 
644         // wrap and restore angle values
645         if (this.trackballEnabled) {
646             // if we're upside-down, flip the bank angle to reach the same
647             // orientation with an elevation between -pi/2 and pi/2
648             el_cover = Mat.mod(this.angles.el, 2 * Math.PI);
649             if (0.5 * Math.PI < el_cover && el_cover < 1.5 * Math.PI) {
650                 this.angles.el = Math.PI - el_cover;
651                 this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
652                 this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
653             }
654 
655             // wrap the azimuth and bank angle
656             this.angles.az = Mat.wrap(this.angles.az, az_smin, az_smax);
657             this.angles.el = Mat.wrap(this.angles.el, el_smin, el_smax);
658             this.angles.bank = Mat.wrap(this.angles.bank, bank_smin, bank_smax);
659         } else {
660             // wrap and clamp the elevation into the slider range. if
661             // flipping the elevation gets us closer to the slider interval,
662             // do that, inverting the azimuth and bank angle to compensate
663             el_interval_loss = function (t) {
664                 if (t < el_smin) {
665                     return el_smin - t;
666                 } else if (el_smax < t) {
667                     return t - el_smax;
668                 } else {
669                     return 0;
670                 }
671             };
672             el_smid = 0.5 * (el_smin + el_smax);
673             el_equiv = Mat.wrap(
674                 this.angles.el,
675                 el_smid - Math.PI,
676                 el_smid + Math.PI
677             );
678             el_flip_equiv = Mat.wrap(
679                 Math.PI - this.angles.el,
680                 el_smid - Math.PI,
681                 el_smid + Math.PI
682             );
683             el_equiv_loss = el_interval_loss(el_equiv);
684             el_flip_equiv_loss = el_interval_loss(el_flip_equiv);
685             if (el_equiv_loss <= el_flip_equiv_loss) {
686                 this.angles.el = Mat.clamp(el_equiv, el_smin, el_smax);
687             } else {
688                 this.angles.el = Mat.clamp(el_flip_equiv, el_smin, el_smax);
689                 this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
690                 this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
691             }
692 
693             // wrap and clamp the azimuth and bank angle into the slider range
694             this.angles.az = Mat.wrapAndClamp(this.angles.az, az_smin, az_smax, 2 * Math.PI);
695             this.angles.bank = Mat.wrapAndClamp(this.angles.bank, bank_smin, bank_smax, 2 * Math.PI);
696 
697             // since we're using `clamp`, angles may have changed
698             this.matrix3DRot = this.getRotationFromAngles();
699         }
700 
701         // restore slider positions
702         this.setSlidersFromAngles();
703     },
704 
705     /**
706      * Get distance from view box center to camera.
707      * In other words, the radius of the sphere where the camera sits.
708      * Distinguishes between projection tpye 'central' and 'parallel'.
709      * Uses the value of attribute 'r'.
710      *
711      * @returns Number
712      * @private
713      * @see View3D#r
714      */
715     getCameraDistance: function() {
716         var rs, r, rr, diam;
717 
718         rr = Type.evaluate(this.r);
719         if (rr === null || rr === 0) {
720             // Use attribute r
721             rs = this.evalVisProp('r');
722         } else {
723             // Use previously set value in this.r
724             rs = rr;
725         }
726 
727         if (rs === 'auto') {
728             r = 1.01;
729         } else {
730             r = (this.projectionType === 'central') ? rs : (1 / rs);
731         }
732 
733         if (this.projectionType === 'central') {
734             diam = Mat.hypot(
735                 this.bbox3D[0][0] - this.bbox3D[0][1],
736                 this.bbox3D[1][0] - this.bbox3D[1][1],
737                 this.bbox3D[2][0] - this.bbox3D[2][1]
738             );
739             r = diam * r;
740         }
741 
742         return r;
743     },
744 
745     /**
746      * @private
747      * @returns {Array}
748      */
749     _updateCentralProjection: function () {
750         var zf = 20, // near clip plane
751             zn = 8,  // far clip plane
752 
753             // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html
754             // bbox3D is always at the world origin, i.e. T_obj is the unit matrix.
755             // All vectors contain affine coordinates and have length 3
756             // The matrices are of size 4x4.
757             r, A;
758 
759         // Set distance from view box center to camera
760         r = this.getCameraDistance();
761 
762         // Compute camera transformation
763         // this.boxToCam = this.matrix3DRot.map((row) => row.slice());
764         this.boxToCam = this.matrix3DRot.map(function (row) { return row.slice(); });
765         this.boxToCam[3][0] = -r;
766 
767         // compute focal distance and clip space transformation
768         this.focalDist = 1 / Math.tan(0.5 * this.evalVisProp('fov'));
769         A = [
770             [0, 0, 0, -1],
771             [0, this.focalDist, 0, 0],
772             [0, 0, this.focalDist, 0],
773             [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)]
774         ];
775 
776         return Mat.matMatMult(A, this.boxToCam);
777     },
778 
779     // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles.
780     // Called in board.updateElements()
781     update: function () {
782         var r, stretch,
783             mat2D, objectToClip, size,
784             dx, dy;
785             // objectsList;
786 
787         if (
788             !Type.exists(this.el_slide) ||
789             !Type.exists(this.az_slide) ||
790             !Type.exists(this.bank_slide) ||
791             !this.needsUpdate
792         ) {
793             this.needsUpdate = false;
794             return this;
795         }
796 
797         mat2D = [
798             [1, 0, 0],
799             [0, 1, 0],
800             [0, 0, 1]
801         ];
802 
803         this.projectionType = this.evalVisProp('projection').toLowerCase();
804 
805         // override angle slider bounds when trackball navigation is enabled
806         if (this.trackballEnabled !== this.evalVisProp('trackball.enabled')) {
807             this.updateAngleSliderBounds();
808         }
809 
810         if (this._hasMoveTrackball) {
811             // The trackball has been moved since the last update, so we do
812             // trackball navigation. When the trackball is enabled, a drag
813             // event is interpreted as a trackball movement unless it's
814             // caught by something else, like point dragging. When the
815             // trackball is disabled, the trackball movement flag should
816             // never be set
817             this.matrix3DRot = this.updateProjectionTrackball();
818             this.setAnglesFromRotation();
819         } else if (this.anglesHaveMoved()) {
820             // The trackball hasn't been moved since the last up date, but
821             // the Tait-Bryan angles have been, so we do angle navigation
822             this.getAnglesFromSliders();
823             this.matrix3DRot = this.getRotationFromAngles();
824         }
825 
826         /**
827          * The translation that moves the center of the view box to the origin.
828          */
829         this.shift = [
830             [1, 0, 0, 0],
831             [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0],
832             [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0],
833             [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1]
834         ];
835 
836         switch (this.projectionType) {
837             case 'central': // Central projection
838 
839                 // Add a final transformation to scale and shift the projection
840                 // on the board, usually called viewport.
841                 size = 2 * 0.4;
842                 mat2D[1][1] = this.size[0] / size; // w / d_x
843                 mat2D[2][2] = this.size[1] / size; // h / d_y
844                 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x
845                 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y
846                 // The transformations this.matrix3D and mat2D can not be combined at this point,
847                 // since the projected vectors have to be normalized in between in project3DTo2D
848                 this.viewPortTransform = mat2D;
849                 objectToClip = this._updateCentralProjection();
850                 // this.matrix3D is a 4x4 matrix
851                 this.matrix3D = Mat.matMatMult(objectToClip, this.shift);
852                 break;
853 
854             case 'parallel': // Parallel projection
855             default:
856                 r = this.getCameraDistance();
857                 stretch = [
858                     [1, 0, 0, 0],
859                     [0, r, 0, 0],
860                     [0, 0, r, 0],
861                     [0, 0, 0, r]
862                 ];
863 
864                 // Add a final transformation to scale and shift the projection
865                 // on the board, usually called viewport.
866                 dx = this.bbox3D[0][1] - this.bbox3D[0][0];
867                 dy = this.bbox3D[1][1] - this.bbox3D[1][0];
868                 mat2D[1][1] = this.size[0] / dx; // w / d_x
869                 mat2D[2][2] = this.size[1] / dy; // h / d_y
870                 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x
871                 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y
872 
873                 // Combine all transformations, this.matrix3D is a 3x4 matrix
874                 this.matrix3D = Mat.matMatMult(
875                     mat2D,
876                     Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3)
877                 );
878         }
879 
880         // Used for zIndex in dept ordering in subsequent update methods of the
881         // 3D elements and in view3d.updateRenderer
882         this.matrix3DRotShift = Mat.matMatMult(this.matrix3DRot, this.shift);
883 
884         return this;
885     },
886 
887     /**
888      * Compares 3D elements according to their z-Index.
889      * @param {JXG.GeometryElement3D} a
890      * @param {JXG.GeometryElement3D} b
891      * @returns Number
892      */
893     compareDepth: function (a, b) {
894         // return a.zIndex - b.zIndex;
895         // if (a.type !== Const.OBJECT_TYPE_PLANE3D && b.type !== Const.OBJECT_TYPE_PLANE3D) {
896         //     return a.zIndex - b.zIndex;
897         // } else if (a.type === Const.OBJECT_TYPE_PLANE3D) {
898         //     let bHesse = Mat.innerProduct(a.point.coords, a.normal, 4);
899         //     let po = Mat.innerProduct(b.coords, a.normal, 4);
900         //     let pos = Mat.innerProduct(this.boxToCam[3], a.normal, 4);
901         // console.log(this.boxToCam[3])
902         //     return pos - po;
903         // } else if (b.type === Const.OBJECT_TYPE_PLANE3D) {
904         //     let bHesse = Mat.innerProduct(b.point.coords, b.normal, 4);
905         //     let po = Mat.innerProduct(a.coords, a.normal, 4);
906         //     let pos = Mat.innerProduct(this.boxToCam[3], b.normal, 4);
907         //     console.log('b', pos, po, bHesse)
908         //     return -pos;
909         // }
910         return a.zIndex - b.zIndex;
911     },
912 
913     updateZIndices: function() {
914         var id, el;
915         for (id in this.objects) {
916             if (this.objects.hasOwnProperty(id)) {
917                 el = this.objects[id];
918                 // Update zIndex of less frequent objects line3d and polygon3d
919                 // The other elements (point3d, face3d) do this in their update method.
920                 if ((
921                         el.type === Const.OBJECT_TYPE_LINE3D ||
922                         el.type === Const.OBJECT_TYPE_POLYGON3D
923                     ) &&
924                     Type.exists(el.element2D) &&
925                     el.element2D.evalVisProp('visible')
926                 ) {
927                     el.updateZIndex();
928                 }
929             }
930         }
931     },
932 
933     updateShaders: function() {
934         var id, el, v;
935         for (id in this.objects) {
936             if (this.objects.hasOwnProperty(id)) {
937                 el = this.objects[id];
938 
939                 if (el.visPropCalc.visible && Type.exists(el.shader)) {
940                     if (this.board._change3DView && el.evalVisProp('shader.fixed')) {
941                         // In case, 3D view is rotated and the shader is fixed
942                         // we can avoid the call of shader()
943                         v = el.zIndex;
944                     } else {
945                         v = el.shader();
946                     }
947                     if (v < this.zIndexMin) {
948                         this.zIndexMin = v;
949                     } else if (v > this.zIndexMax) {
950                         this.zIndexMax = v;
951                     }
952                 }
953             }
954         }
955     },
956 
957     updateDepthOrdering: function () {
958         var id, el,
959             i, j, l, layers, lay;
960 
961         // Collect elements for depth ordering layer-wise
962         layers = this.evalVisProp('depthorder.layers');
963         for (i = 0; i < layers.length; i++) {
964             this.depthOrdered[layers[i]] = [];
965         }
966 
967         for (id in this.objects) {
968             if (this.objects.hasOwnProperty(id)) {
969                 el = this.objects[id];
970                 if ((el.type === Const.OBJECT_TYPE_FACE3D ||
971                     el.type === Const.OBJECT_TYPE_LINE3D ||
972                     // el.type === Const.OBJECT_TYPE_PLANE3D ||
973                     el.type === Const.OBJECT_TYPE_POINT3D ||
974                     el.type === Const.OBJECT_TYPE_POLYGON3D
975                     ) &&
976                     Type.exists(el.element2D) &&
977                     el.element2D.visPropCalc.visible
978                     // el.element2D.evalVisProp('visible')
979                 ) {
980                     lay = el.element2D.evalVisProp('layer');
981                     if (layers.indexOf(lay) >= 0) {
982                         this.depthOrdered[lay].push(el);
983                     }
984                 }
985             }
986         }
987 
988         if (this.board.renderer && this.board.renderer.type === 'svg') {
989             for (i = 0; i < layers.length; i++) {
990                 lay = layers[i];
991                 this.depthOrdered[lay].sort(this.compareDepth.bind(this));
992                 // DEBUG
993                 // if (this.depthOrdered[lay].length > 0) {
994                 //     for (let k = 0; k < this.depthOrdered[lay].length; k++) {
995                 //         let o = this.depthOrdered[lay][k]
996                 //         console.log(o.visProp.fillcolor, o.zIndex)
997                 //     }
998                 // }
999                 l = this.depthOrdered[lay];
1000                 for (j = 0; j < l.length; j++) {
1001                     this.board.renderer.setLayer(l[j].element2D, lay);
1002                 }
1003                 // this.depthOrdered[lay].forEach((el) => this.board.renderer.setLayer(el.element2D, lay));
1004                 // Attention: forEach prevents deleting an element
1005             }
1006         }
1007 
1008         return this;
1009     },
1010 
1011     updateRenderer: function () {
1012         if (!this.needsUpdate) {
1013             return this;
1014         }
1015 
1016         // console.time('update')
1017         // Handle depth ordering
1018         this.depthOrdered = {};
1019 
1020         if (this.shift !== undefined && this.evalVisProp('depthorder.enabled')) {
1021             // Update the zIndices of certain element types.
1022             // We do it here in updateRenderer, because the elements' positions
1023             // are meanwhile updated.
1024             this.updateZIndices();
1025 
1026             this.updateShaders();
1027 
1028             if (this.board.renderer && this.board.renderer.type === 'svg') {
1029                 // For SVG we update the DOM order here.
1030                 // In canvas we sort the elements in board.updateRendererCanvas
1031                 this.updateDepthOrdering();
1032             }
1033         }
1034         // console.timeEnd('update')
1035 
1036         this.needsUpdate = false;
1037         return this;
1038     },
1039 
1040     removeObject: function (object, saveMethod) {
1041         var i, el, le, o, fst, face;
1042 
1043         // this.board.removeObject(object, saveMethod);
1044         if (Type.isArray(object)) {
1045             for (i = 0; i < object.length; i++) {
1046                 this.removeObject(object[i]);
1047             }
1048             return this;
1049         }
1050 
1051         object = this.select(object);
1052 
1053         // // If the object which is about to be removed unknown or a string, do nothing.
1054         // // it is a string if a string was given and could not be resolved to an element.
1055         if (!Type.exists(object) || Type.isString(object)) {
1056             return this;
1057         }
1058 
1059         try {
1060             // Remove all children.
1061             for (el in object.childElements) {
1062                 if (object.childElements.hasOwnProperty(el)) {
1063                     this.removeObject(object.childElements[el]);
1064                 }
1065             }
1066             if (object.type === Const.OBJECT_TYPE_POLYHEDRON3D) {
1067                 // Special treatment for polyhedron3d.
1068                 // With this we can avoid the time consuming addChild() calls.
1069                 le = object.faces.length;
1070                 if (le > 0) {
1071                     fst = object.faces[0]._pos;
1072                     fst = (object.faces[0].element2D._pos < fst) ? object.faces[0].element2D._pos : fst;
1073                 }
1074                 for (i = 0; i < le; i++) {
1075                     face = object.faces[i];
1076                     delete this.objects[face.id];
1077 
1078                     // this.board.removeObject(face.element2D, saveMethod);
1079                     delete this.board.objects[face.element2D.id];
1080                     delete this.board.elementsByName[face.element2D.name];
1081                     face.element2D.remove();
1082                     this.board.objectsList.splice(face.element2D._pos, 1);
1083 
1084                     delete this.board.objects[face.id];
1085                     delete this.board.elementsByName[face.name];
1086                     face.remove();
1087                     this.board.objectsList.splice(face._pos, 1);
1088                 }
1089                 le = this.board.objectsList.length;
1090                 // Reindex the positions
1091                 for (i = fst; i < this.board.objectsList.length; i++) {
1092                     o = this.board.objectsList[i];
1093                     if (o._pos > -1) { o._pos = i; }
1094                 }
1095                 object.faces = [];
1096             }
1097 
1098             delete this.objects[object.id];
1099         } catch (e) {
1100             JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e);
1101         }
1102 
1103         // this.update();
1104 
1105         this.board.removeObject(object, saveMethod);
1106 
1107         return this;
1108     },
1109 
1110     /**
1111      * Map world coordinates to focal coordinates. These coordinate systems
1112      * are explained in the {@link JXG.View3D#boxToCam} matrix
1113      * documentation.
1114      *
1115      * @param {Array} pWorld A world space point, in homogeneous coordinates.
1116      * @param {Boolean} [homog=true] Whether to return homogeneous coordinates.
1117      * If false, projects down to ordinary coordinates.
1118      */
1119     worldToFocal: function (pWorld, homog = true) {
1120         var k,
1121             pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld));
1122 
1123         pView[3] -= pView[0] * this.focalDist;
1124         if (homog) {
1125             return pView;
1126         } else {
1127             for (k = 1; k < 4; k++) {
1128                 pView[k] /= pView[0];
1129             }
1130             return pView.slice(1, 4);
1131         }
1132     },
1133 
1134     /**
1135      * Project 3D coordinates to 2D board coordinates
1136      * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
1137      *
1138      * @param  {Number|Array} x
1139      * @param  {Number[]} y
1140      * @param  {Number[]} z
1141      * @returns {Array} Array of length 3 containing the projection on to the board
1142      * in homogeneous user coordinates.
1143      */
1144     project3DTo2D: function (x, y, z) {
1145         var vec, w;
1146         if (arguments.length === 3) {
1147             vec = [1, x, y, z];
1148         } else {
1149             // Argument is an array
1150             if (x.length === 3) {
1151                 // vec = [1].concat(x);
1152                 vec = x.slice();
1153                 vec.unshift(1);
1154             } else {
1155                 vec = x;
1156             }
1157         }
1158 
1159         w = Mat.matVecMult(this.matrix3D, vec);
1160 
1161         switch (this.projectionType) {
1162             case 'central':
1163                 w[1] /= w[0];
1164                 w[2] /= w[0];
1165                 w[3] /= w[0];
1166                 w[0] /= w[0];
1167                 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3));
1168 
1169             case 'parallel':
1170             default:
1171                 return w;
1172         }
1173     },
1174 
1175     /**
1176      * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y.
1177      * Setting R = mat^(-1) gives
1178      *   1/ w0 * (1, x, y, d)^T = R * v2d.
1179      * The first and the last row of this equation allows to determine 1/w0 and h.
1180      *
1181      * @param {Array} mat
1182      * @param {Array} v2d
1183      * @param {Number} d
1184      * @returns Array
1185      * @private
1186      */
1187     _getW0: function (mat, v2d, d) {
1188         var R = Mat.inverse(mat),
1189             R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2],
1190             R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2],
1191             w, h, det;
1192 
1193         det = d * R[0][3] - R[3][3];
1194         w = (R2 * R[0][3] - R1 * R[3][3]) / det;
1195         h = (R2 - R1 * d) / det;
1196         return [1 / w, h];
1197     },
1198 
1199     /**
1200      * Project a 2D coordinate to the plane defined by point "foot"
1201      * and the normal vector `normal`.
1202      *
1203      * @param  {JXG.Point} point2d
1204      * @param  {Array} normal Normal of plane
1205      * @param  {Array} foot Foot point of plane
1206      * @returns {Array} of length 4 containing the projected
1207      * point in homogeneous coordinates.
1208      */
1209     project2DTo3DPlane: function (point2d, normal, foot) {
1210         var mat, rhs, d, le, sol,
1211             f = foot.slice(1) || [0, 0, 0],
1212             n = normal.slice(1),
1213             v2d, w0, res;
1214 
1215         le = Mat.norm(n, 3);
1216         d = Mat.innerProduct(f, n, 3) / le;
1217 
1218         if (this.projectionType === 'parallel') {
1219             mat = this.matrix3D.slice(0, 3);     // Copy each row by reference
1220             mat.push([0, n[0], n[1], n[2]]);
1221 
1222             // 2D coordinates of point
1223             rhs = point2d.coords.usrCoords.slice();
1224             rhs.push(d);
1225             try {
1226                 // Prevent singularity in case elevation angle is zero
1227                 if (mat[2][3] === 1.0) {
1228                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
1229                 }
1230                 sol = Mat.Numerics.Gauss(mat, rhs);
1231             } catch (e) {
1232                 sol = [0, NaN, NaN, NaN];
1233             }
1234         } else {
1235             mat = this.matrix3D;
1236 
1237             // 2D coordinates of point:
1238             rhs = point2d.coords.usrCoords.slice();
1239 
1240             v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs);
1241             res = this._getW0(mat, v2d, d);
1242             w0 = res[0];
1243             rhs = [
1244                 v2d[0] * w0,
1245                 v2d[1] * w0,
1246                 v2d[2] * w0,
1247                 res[1] * w0
1248             ];
1249             try {
1250                 // Prevent singularity in case elevation angle is zero
1251                 if (mat[2][3] === 1.0) {
1252                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
1253                 }
1254 
1255                 sol = Mat.Numerics.Gauss(mat, rhs);
1256                 sol[1] /= sol[0];
1257                 sol[2] /= sol[0];
1258                 sol[3] /= sol[0];
1259                 // sol[3] = d;
1260                 sol[0] /= sol[0];
1261             } catch (err) {
1262                 sol = [0, NaN, NaN, NaN];
1263             }
1264         }
1265 
1266         return sol;
1267     },
1268 
1269     /**
1270      * Project a point on the screen to the nearest point, in screen
1271      * distance, on a line segment in 3d space. The inputs and outputs
1272      * are in homogeneous coordinates.
1273      * <p>
1274      * Used in View3d.project2DTo3DVertical() and
1275      * Line3d.projectScreenCoords().
1276      *
1277      * @param {Array} pScr The screen coordinates of the point to project.
1278      * @param {Array} end0 The world space coordinates of one end of the
1279      * line segment (array of length 4).
1280      * @param {Array} end1 The world space coordinates of the other end of
1281      * the line segment (array of length 4).
1282      *
1283      * @returns {Array} Homogeneous coordinates of the projection
1284      */
1285     projectScreenToSegment: function (pScr, end0, end1) {
1286         var end0_2d = this.project3DTo2D(end0).slice(1, 3),
1287             end1_2d = this.project3DTo2D(end1).slice(1, 3),
1288             dir_2d = [
1289                 end1_2d[0] - end0_2d[0],
1290                 end1_2d[1] - end0_2d[1]
1291             ],
1292             dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d),
1293             diff = [
1294                 pScr[0] - end0_2d[0],
1295                 pScr[1] - end0_2d[1]
1296             ],
1297             s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter
1298             mid, mid_2d, mid_diff, m,
1299 
1300             t, // view-space affine parameter
1301             t_clamped, // affine parameter clamped to range
1302             t_clamped_co;
1303 
1304         if (this.projectionType === 'central') {
1305             mid = [
1306                 1,
1307                 0.5 * (end0[1] + end1[1]),
1308                 0.5 * (end0[2] + end1[2]),
1309                 0.5 * (end0[3] + end1[3])
1310             ];
1311             mid_2d = this.project3DTo2D(mid).slice(1, 3);
1312             mid_diff = [
1313                 mid_2d[0] - end0_2d[0],
1314                 mid_2d[1] - end0_2d[1]
1315             ];
1316             m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq;
1317 
1318             // the view-space affine parameter s is related to the
1319             // screen-space affine parameter t by a Möbius transformation,
1320             // which is determined by the following relations:
1321             //
1322             // s | t
1323             // -----
1324             // 0 | 0
1325             // m | 1/2
1326             // 1 | 1
1327             //
1328             t = (1 - m) * s / ((1 - 2 * m) * s + m);
1329         } else {
1330             t = s;
1331         }
1332 
1333         t_clamped = Math.min(Math.max(t, 0), 1);
1334         t_clamped_co = 1 - t_clamped;
1335         return [
1336             1,
1337             t_clamped_co * end0[1] + t_clamped * end1[1],
1338             t_clamped_co * end0[2] + t_clamped * end1[2],
1339             t_clamped_co * end0[3] + t_clamped * end1[3]
1340         ];
1341     },
1342 
1343     /**
1344      * Project a 2D coordinate to a new 3D position by keeping
1345      * the 3D x, y coordinates and changing only the z coordinate.
1346      * All horizontal moves of the 2D point are ignored.
1347      *
1348      * @param {JXG.Point} point2d
1349      * @param {Array} base_c3d
1350      * @returns {Array} of length 4 containing the projected
1351      * point in homogeneous coordinates.
1352      */
1353     project2DTo3DVertical: function (point2d, base_c3d) {
1354         var pScr = point2d.coords.usrCoords.slice(1, 3),
1355             end0 = [1, base_c3d[1], base_c3d[2], this.bbox3D[2][0]],
1356             end1 = [1, base_c3d[1], base_c3d[2], this.bbox3D[2][1]];
1357 
1358         return this.projectScreenToSegment(pScr, end0, end1);
1359     },
1360 
1361     /**
1362      * Limit 3D coordinates to the bounding cube.
1363      *
1364      * @param {Array} c3d 3D coordinates [x,y,z]
1365      * @returns Array [Array, Boolean] containing [coords, corrected]. coords contains the updated 3D coordinates,
1366      * correct is true if the coords have been changed.
1367      */
1368     project3DToCube: function (c3d) {
1369         var cube = this.bbox3D,
1370             isOut = false;
1371 
1372         if (c3d[1] < cube[0][0]) {
1373             c3d[1] = cube[0][0];
1374             isOut = true;
1375         }
1376         if (c3d[1] > cube[0][1]) {
1377             c3d[1] = cube[0][1];
1378             isOut = true;
1379         }
1380         if (c3d[2] < cube[1][0]) {
1381             c3d[2] = cube[1][0];
1382             isOut = true;
1383         }
1384         if (c3d[2] > cube[1][1]) {
1385             c3d[2] = cube[1][1];
1386             isOut = true;
1387         }
1388         if (c3d[3] <= cube[2][0]) {
1389             c3d[3] = cube[2][0];
1390             isOut = true;
1391         }
1392         if (c3d[3] >= cube[2][1]) {
1393             c3d[3] = cube[2][1];
1394             isOut = true;
1395         }
1396 
1397         return [c3d, isOut];
1398     },
1399 
1400     /**
1401      * Intersect a ray with the bounding cube of the 3D view.
1402      * @param {Array} p 3D coordinates [w,x,y,z]
1403      * @param {Array} dir 3D direction vector of the line (array of length 3 or 4)
1404      * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
1405      * @returns Affine ratio of the intersection of the line with the cube.
1406      */
1407     intersectionLineCube: function (p, dir, r) {
1408         var r_n, i, r0, r1, d;
1409 
1410         d = (dir.length === 3) ? dir : dir.slice(1);
1411 
1412         r_n = r;
1413         for (i = 0; i < 3; i++) {
1414             if (d[i] !== 0) {
1415                 r0 = (this.bbox3D[i][0] - p[i + 1]) / d[i];
1416                 r1 = (this.bbox3D[i][1] - p[i + 1]) / d[i];
1417                 if (r < 0) {
1418                     r_n = Math.max(r_n, Math.min(r0, r1));
1419                 } else {
1420                     r_n = Math.min(r_n, Math.max(r0, r1));
1421                 }
1422             }
1423         }
1424         return r_n;
1425     },
1426 
1427     /**
1428      * Test if coordinates are inside of the bounding cube.
1429      * @param {array} p 3D coordinates [[w],x,y,z] of a point.
1430      * @returns Boolean
1431      */
1432     isInCube: function (p, polyhedron) {
1433         var q;
1434         if (p.length === 4) {
1435             if (p[0] === 0) {
1436                 return false;
1437             }
1438             q = p.slice(1);
1439         }
1440         return (
1441             q[0] > this.bbox3D[0][0] - Mat.eps &&
1442             q[0] < this.bbox3D[0][1] + Mat.eps &&
1443             q[1] > this.bbox3D[1][0] - Mat.eps &&
1444             q[1] < this.bbox3D[1][1] + Mat.eps &&
1445             q[2] > this.bbox3D[2][0] - Mat.eps &&
1446             q[2] < this.bbox3D[2][1] + Mat.eps
1447         );
1448     },
1449 
1450     /**
1451      *
1452      * @param {JXG.Plane3D} plane1
1453      * @param {JXG.Plane3D} plane2
1454      * @param {Number} d Right hand side of Hesse normal for plane2 (it can be adjusted)
1455      * @returns {Array} of length 2 containing the coordinates of the defining points of
1456      * of the intersection segment, or false if there is no intersection
1457      */
1458     intersectionPlanePlane: function (plane1, plane2, d) {
1459         var ret = [false, false],
1460             p, q, r, w,
1461             dir;
1462 
1463         d = d || plane2.d;
1464 
1465         // Get one point of the intersection of the two planes
1466         w = Mat.crossProduct(plane1.normal.slice(1), plane2.normal.slice(1));
1467         w.unshift(0);
1468 
1469         p = Mat.Geometry.meet3Planes(
1470             plane1.normal,
1471             plane1.d,
1472             plane2.normal,
1473             d,
1474             w,
1475             0
1476         );
1477 
1478         // Get the direction of the intersecting line of the two planes
1479         dir = Mat.Geometry.meetPlanePlane(
1480             plane1.vec1,
1481             plane1.vec2,
1482             plane2.vec1,
1483             plane2.vec2
1484         );
1485 
1486         // Get the bounding points of the intersecting segment
1487         r = this.intersectionLineCube(p, dir, Infinity);
1488         q = Mat.axpy(r, dir, p);
1489         if (this.isInCube(q)) {
1490             ret[0] = q;
1491         }
1492         r = this.intersectionLineCube(p, dir, -Infinity);
1493         q = Mat.axpy(r, dir, p);
1494         if (this.isInCube(q)) {
1495             ret[1] = q;
1496         }
1497 
1498         return ret;
1499     },
1500 
1501     intersectionPlaneFace: function (plane, face) {
1502         var ret = [],
1503             j, t,
1504             p, crds,
1505             p1, p2, c,
1506             f, le, x1, y1, x2, y2,
1507             dir, vec, w,
1508             mat = [], b = [], sol;
1509 
1510         w = Mat.crossProduct(plane.normal.slice(1), face.normal.slice(1));
1511         w.unshift(0);
1512 
1513         // Get one point of the intersection of the two planes
1514         p = Geometry.meet3Planes(
1515             plane.normal,
1516             plane.d,
1517             face.normal,
1518             face.d,
1519             w,
1520             0
1521         );
1522 
1523         // Get the direction the intersecting line of the two planes
1524         dir = Geometry.meetPlanePlane(
1525             plane.vec1,
1526             plane.vec2,
1527             face.vec1,
1528             face.vec2
1529         );
1530 
1531         f = face.polyhedron.faces[face.faceNumber];
1532         crds = face.polyhedron.coords;
1533         le = f.length;
1534         for (j = 1; j <= le; j++) {
1535             p1 = crds[f[j - 1]];
1536             p2 = crds[f[j % le]];
1537             vec = [0, p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]];
1538 
1539             x1 = Math.random();
1540             y1 = Math.random();
1541             x2 = Math.random();
1542             y2 = Math.random();
1543             mat = [
1544                 [x1 * dir[1] + y1 * dir[3], x1 * (-vec[1]) + y1 * (-vec[3])],
1545                 [x2 * dir[2] + y2 * dir[3], x2 * (-vec[2]) + y2 * (-vec[3])]
1546             ];
1547             b = [
1548                 x1 * (p1[1] - p[1]) + y1 * (p1[3] - p[3]),
1549                 x2 * (p1[2] - p[2]) + y2 * (p1[3] - p[3])
1550             ];
1551 
1552             sol = Numerics.Gauss(mat, b);
1553             t = sol[1];
1554             if (t > -Mat.eps && t < 1 + Mat.eps) {
1555                 c = [1, p1[1] + t * vec[1], p1[2] + t * vec[2], p1[3] + t * vec[3]];
1556                 ret.push(c);
1557             }
1558         }
1559 
1560         return ret;
1561     },
1562 
1563     // TODO:
1564     // - handle non-closed polyhedra
1565     // - handle intersections in vertex, edge, plane
1566     intersectionPlanePolyhedron: function(plane, phdr) {
1567         var i, j, seg,
1568             p, first, pos, pos_akt,
1569             eps = 1e-12,
1570             points = [],
1571             x = [],
1572             y = [],
1573             z = [];
1574 
1575         for (i = 0; i < phdr.numberFaces; i++) {
1576             if (phdr.def.faces[i].length < 3) {
1577                 // We skip intersection with points or lines
1578                 continue;
1579             }
1580 
1581             // seg will be an array consisting of two points
1582             // that span the intersecting segment of the plane
1583             // and the face.
1584             seg = this.intersectionPlaneFace(plane, phdr.faces[i]);
1585 
1586             // Plane intersects the face in less than 2 points
1587             if (seg.length < 2) {
1588                 continue;
1589             }
1590 
1591             if (seg[0].length === 4 && seg[1].length === 4) {
1592                 // This test is necessary to filter out intersection lines which are
1593                 // identical to intersections of axis planes (they would occur twice),
1594                 // i.e. edges of bbox3d.
1595                 for (j = 0; j < points.length; j++) {
1596                     if (
1597                         (Geometry.distance(seg[0], points[j][0], 4) < eps &&
1598                             Geometry.distance(seg[1], points[j][1], 4) < eps) ||
1599                         (Geometry.distance(seg[0], points[j][1], 4) < eps &&
1600                             Geometry.distance(seg[1], points[j][0], 4) < eps)
1601                     ) {
1602                         break;
1603                     }
1604                 }
1605                 if (j === points.length) {
1606                     points.push(seg.slice());
1607                 }
1608             }
1609         }
1610 
1611         // Handle the case that the intersection is the empty set.
1612         if (points.length === 0) {
1613             return { X: x, Y: y, Z: z };
1614         }
1615 
1616         // Concatenate the intersection points to a polygon.
1617         // If all went well, each intersection should appear
1618         // twice in the list.
1619         // __Attention:__ each face has to be planar!!!
1620         // Otherwise the algorithm will fail.
1621         first = 0;
1622         pos = first;
1623         i = 0;
1624         do {
1625             p = points[pos][i];
1626             if (p.length === 4) {
1627                 x.push(p[1]);
1628                 y.push(p[2]);
1629                 z.push(p[3]);
1630             }
1631             i = (i + 1) % 2;
1632             p = points[pos][i];
1633 
1634             pos_akt = pos;
1635             for (j = 0; j < points.length; j++) {
1636                 if (j !== pos && Geometry.distance(p, points[j][0]) < eps) {
1637                     pos = j;
1638                     i = 0;
1639                     break;
1640                 }
1641                 if (j !== pos && Geometry.distance(p, points[j][1]) < eps) {
1642                     pos = j;
1643                     i = 1;
1644                     break;
1645                 }
1646             }
1647             if (pos === pos_akt) {
1648                 console.log('Error face3d intersection update: did not find next', pos, i);
1649                 break;
1650             }
1651         } while (pos !== first);
1652         x.push(x[0]);
1653         y.push(y[0]);
1654         z.push(z[0]);
1655 
1656         return { X: x, Y: y, Z: z };
1657     },
1658 
1659     /**
1660      * Generate mesh for a surface / plane.
1661      * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
1662      * @param {Array|Function} func
1663      * @param {Array} interval_u
1664      * @param {Array} interval_v
1665      * @returns Array
1666      * @private
1667      *
1668      * @example
1669      *  var el = view.create('curve', [[], []]);
1670      *  el.updateDataArray = function () {
1671      *      var steps_u = this.evalVisProp('stepsu'),
1672      *           steps_v = this.evalVisProp('stepsv'),
1673      *           r_u = Type.evaluate(this.range_u),
1674      *           r_v = Type.evaluate(this.range_v),
1675      *           func, ret;
1676      *
1677      *      if (this.F !== null) {
1678      *          func = this.F;
1679      *      } else {
1680      *          func = [this.X, this.Y, this.Z];
1681      *      }
1682      *      ret = this.view.getMesh(func,
1683      *          r_u.concat([steps_u]),
1684      *          r_v.concat([steps_v]));
1685      *
1686      *      this.dataX = ret[0];
1687      *      this.dataY = ret[1];
1688      *  };
1689      *
1690      */
1691     getMesh: function (func, interval_u, interval_v) {
1692         var i_u, i_v, u, v,
1693             c2d, delta_u, delta_v,
1694             p = [0, 0, 0],
1695             steps_u = Type.evaluate(interval_u[2]),
1696             steps_v = Type.evaluate(interval_v[2]),
1697             dataX = [],
1698             dataY = [];
1699 
1700         delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
1701         delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
1702 
1703         for (i_u = 0; i_u <= steps_u; i_u++) {
1704             u = interval_u[0] + delta_u * i_u;
1705             for (i_v = 0; i_v <= steps_v; i_v++) {
1706                 v = interval_v[0] + delta_v * i_v;
1707                 if (Type.isFunction(func)) {
1708                     p = func(u, v);
1709                 } else {
1710                     p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1711                 }
1712                 c2d = this.project3DTo2D(p);
1713                 dataX.push(c2d[1]);
1714                 dataY.push(c2d[2]);
1715             }
1716             dataX.push(NaN);
1717             dataY.push(NaN);
1718         }
1719 
1720         for (i_v = 0; i_v <= steps_v; i_v++) {
1721             v = interval_v[0] + delta_v * i_v;
1722             for (i_u = 0; i_u <= steps_u; i_u++) {
1723                 u = interval_u[0] + delta_u * i_u;
1724                 if (Type.isFunction(func)) {
1725                     p = func(u, v);
1726                 } else {
1727                     p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1728                 }
1729                 c2d = this.project3DTo2D(p);
1730                 dataX.push(c2d[1]);
1731                 dataY.push(c2d[2]);
1732             }
1733             dataX.push(NaN);
1734             dataY.push(NaN);
1735         }
1736 
1737         return [dataX, dataY];
1738     },
1739 
1740     /**
1741      *
1742      */
1743     animateAzimuth: function () {
1744         var s = this.az_slide._smin,
1745             e = this.az_slide._smax,
1746             sdiff = e - s,
1747             newVal = this.az_slide.Value() + 0.1;
1748 
1749         this.az_slide.position = (newVal - s) / sdiff;
1750         if (this.az_slide.position > 1) {
1751             this.az_slide.position = 0.0;
1752         }
1753         this.board._change3DView = true;
1754         this.board.update();
1755         this.board._change3DView = false;
1756 
1757         this.timeoutAzimuth = setTimeout(function () {
1758             this.animateAzimuth();
1759         }.bind(this), 200);
1760     },
1761 
1762     /**
1763      *
1764      */
1765     stopAzimuth: function () {
1766         clearTimeout(this.timeoutAzimuth);
1767         this.timeoutAzimuth = null;
1768     },
1769 
1770     /**
1771      * Check if vertical dragging is enabled and which action is needed.
1772      * Default is shiftKey.
1773      *
1774      * @returns Boolean
1775      * @private
1776      */
1777     isVerticalDrag: function () {
1778         var b = this.board,
1779             key;
1780         if (!this.evalVisProp('verticaldrag.enabled')) {
1781             return false;
1782         }
1783         key = '_' + this.evalVisProp('verticaldrag.key') + 'Key';
1784         return b[key];
1785     },
1786 
1787     /**
1788      * Stop ignoring attribute r. After a call of view3d.setView, view3d.nextView,
1789      * view3d.previousView, or view3d.setCurrentView this attribute is ignored.
1790      * Call of view3d.freeR() will end this.
1791      * @see View3D#setView
1792      */
1793     freeR: function() {
1794         this.r = null;
1795     },
1796 
1797     /**
1798      * Sets camera view to the given values.
1799      * If the optional value r is supplied that value has priority until the next call of
1800      * view3d.setView or until a call of view3d.freeR().
1801      * In particular, the attribute r is ignored until a call of view3d.freeR().
1802      * @param {Number} az Value of azimuth.
1803      * @param {Number} el Value of elevation.
1804      * @param {Number} [r] Value of radius.
1805      *
1806      * @returns {Object} Reference to the view.
1807      * @see View3D#freeR
1808      * @see View3D#r
1809      * @see View3D#nextView
1810      * @see View3D#previousView
1811      * @see View3D#setCurrentView
1812      */
1813     setView: function (az, el, r) {
1814         // Set the distance to a fixed value.
1815         if (r !== undefined) {
1816             this.r = r;
1817         }
1818         r = this.getCameraDistance();
1819 
1820         this.az_slide.setValue(az);
1821         this.el_slide.setValue(el);
1822         this.board.update();
1823 
1824         return this;
1825     },
1826 
1827     /**
1828      * Changes view to the next view stored in the attribute `values`.
1829      *
1830      * @see View3D#values
1831      * @see View3D#setView
1832      * @see View3D#previousView
1833      * @see View3D#setCurrentView
1834      * @see View3D#freeR
1835      *
1836      * @returns {Object} Reference to the view.
1837      */
1838     nextView: function () {
1839         var views = this.evalVisProp('values'),
1840             n = this.visProp._currentview;
1841 
1842         n = (n + 1) % views.length;
1843         this.setCurrentView(n);
1844 
1845         return this;
1846     },
1847 
1848     /**
1849      * Changes view to the previous view stored in the attribute `values`.
1850      *
1851      * @see View3D#values
1852      * @see View3D#setView
1853      * @see View3D#nextView
1854      * @see View3D#setCurrentView
1855      * @see View3D#freeR
1856      *
1857      * @returns {Object} Reference to the view.
1858      */
1859     previousView: function () {
1860         var views = this.evalVisProp('values'),
1861             n = this.visProp._currentview;
1862 
1863         n = (n + views.length - 1) % views.length;
1864         this.setCurrentView(n);
1865 
1866         return this;
1867     },
1868 
1869     /**
1870      * Changes view to the determined view stored in the attribute `values`.
1871      *
1872      * @see View3D#values
1873      * @see View3D#nextView
1874      * @see View3D#previousView
1875      * @see View3D#setCurrentView
1876      * @see View3D#freeR
1877      *
1878      * @param {Number} n Index of view in attribute `values`.
1879      * @returns {Object} Reference to the view.
1880      */
1881     setCurrentView: function (n) {
1882         var views = this.evalVisProp('values');
1883 
1884         if (n < 0 || n >= views.length) {
1885             n = ((n % views.length) + views.length) % views.length;
1886         }
1887 
1888         this.setView(views[n][0], views[n][1], views[n][2]);
1889         this.visProp._currentview = n;
1890 
1891         return this;
1892     },
1893 
1894     /**
1895      * Controls 2-degree navigation in az direction using  pointer.
1896      *
1897      * @private
1898      *
1899      * @param {event} evt the pointer event
1900      * @returns view
1901      */
1902     _az_elEventHandler: function (evt) {
1903         var smax = this.az_slide._smax,
1904             smin = this.az_slide._smin,
1905             speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')),
1906             deltaX, // = evt.movementX,
1907             deltaY, // = evt.movementY
1908             az = this.az_slide.Value(),
1909             el = this.el_slide.Value();
1910 
1911         deltaX = evt.screenX - this._lastPos.x;
1912         this._lastPos.x = evt.screenX;
1913         deltaY = evt.screenY - this._lastPos.y;
1914         this._lastPos.y = evt.screenY;
1915 
1916         // Doesn't allow navigation if another moving event is triggered
1917         if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
1918             return this;
1919         }
1920 
1921         if (this.evalVisProp('az.pointer.enabled') && (deltaX !== 0) && evt.key == null) {
1922             // delta *= (Math.abs(delta) > 100) ? 0.03 : 1;
1923             az += deltaX * speed;
1924         }
1925         if (this.evalVisProp('el.pointer.enabled') && (deltaY !== 0) && evt.key == null) {
1926             el += deltaY * speed;
1927         }
1928 
1929         // Project the calculated az value to a usable value in the interval [smin,smax]
1930         // Use modulo if continuous is true
1931         if (this.evalVisProp('az.continuous')) {
1932             az = Mat.wrap(az, smin, smax);
1933         } else {
1934             if (az > 0) {
1935                 az = Math.min(smax, az);
1936             } else if (az < 0) {
1937                 az = Math.max(smin, az);
1938             }
1939         }
1940         // Project the calculated el value to a usable value in the interval [smin,smax]
1941         // Use modulo if continuous is true and the trackball is disabled
1942         smax = this.el_slide._smax;
1943         smin = this.el_slide._smin;
1944         if (this.evalVisProp('el.continuous') && !this.trackballEnabled) {
1945             el = Mat.wrap(el, smin, smax);
1946         } else {
1947             if (el > 0) {
1948                 el = Math.min(smax, el);
1949             } else if (el < 0) {
1950                 el = Math.max(smin, el);
1951             }
1952         }
1953 
1954         this.setView(az, el);
1955         return this;
1956     },
1957 
1958     /**
1959      * Controls the navigation in az direction using either the keyboard or a pointer.
1960      *
1961      * @private
1962      *
1963      * @param {event} evt either the keydown or the pointer event
1964      * @returns view
1965      */
1966     _azEventHandler: function (evt) {
1967         var smax = this.az_slide._smax,
1968             smin = this.az_slide._smin,
1969             speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')),
1970             delta, // = evt.movementX,
1971             az = this.az_slide.Value(),
1972             el = this.el_slide.Value();
1973 
1974         delta = evt.screenX - this._lastPos.x;
1975         this._lastPos.x = evt.screenX;
1976 
1977         // Doesn't allow navigation if another moving event is triggered
1978         if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
1979             return this;
1980         }
1981 
1982         // Calculate new az value if keyboard events are triggered
1983         // Plus if right-button, minus if left-button
1984         if (this.evalVisProp('az.keyboard.enabled')) {
1985             if (evt.key === 'ArrowRight') {
1986                 az = az + this.evalVisProp('az.keyboard.step') * Math.PI / 180;
1987             } else if (evt.key === 'ArrowLeft') {
1988                 az = az - this.evalVisProp('az.keyboard.step') * Math.PI / 180;
1989             }
1990         }
1991 
1992         if (this.evalVisProp('az.pointer.enabled') && (delta !== 0) && evt.key == null) {
1993             // delta *= (Math.abs(delta) > 100) ? 0.03 : 1;
1994             az += delta * speed;
1995         }
1996 
1997         // Project the calculated az value to a usable value in the interval [smin,smax]
1998         // Use modulo if continuous is true
1999         if (this.evalVisProp('az.continuous')) {
2000             az = Mat.wrap(az, smin, smax);
2001         } else {
2002             if (az > 0) {
2003                 az = Math.min(smax, az);
2004             } else if (az < 0) {
2005                 az = Math.max(smin, az);
2006             }
2007         }
2008 
2009         this.setView(az, el);
2010         return this;
2011     },
2012 
2013     /**
2014      * Controls the navigation in el direction using either the keyboard or a pointer.
2015      *
2016      * @private
2017      *
2018      * @param {event} evt either the keydown or the pointer event
2019      * @returns view
2020      */
2021     _elEventHandler: function (evt) {
2022         var smax = this.el_slide._smax,
2023             smin = this.el_slide._smin,
2024             speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('el.pointer.speed'),
2025             delta, // = evt.movementY,
2026             az = this.az_slide.Value(),
2027             el = this.el_slide.Value();
2028 
2029         delta = evt.screenY - this._lastPos.y;
2030         this._lastPos.y = evt.screenY;
2031 
2032         // Doesn't allow navigation if another moving event is triggered
2033         if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
2034             return this;
2035         }
2036 
2037         // Calculate new az value if keyboard events are triggered
2038         // Plus if down-button, minus if up-button
2039         if (this.evalVisProp('el.keyboard.enabled')) {
2040             if (evt.key === 'ArrowUp') {
2041                 el = el - this.evalVisProp('el.keyboard.step') * Math.PI / 180;
2042             } else if (evt.key === 'ArrowDown') {
2043                 el = el + this.evalVisProp('el.keyboard.step') * Math.PI / 180;
2044             }
2045         }
2046 
2047         if (this.evalVisProp('el.pointer.enabled') && (delta !== 0) && evt.key == null) {
2048             // delta *= (Math.abs(delta) > 100) ? 0.05 : 1;
2049             el += delta * speed;
2050         }
2051 
2052         // Project the calculated el value to a usable value in the interval [smin,smax]
2053         // Use modulo if continuous is true and the trackball is disabled
2054         if (this.evalVisProp('el.continuous') && !this.trackballEnabled) {
2055             el = Mat.wrap(el, smin, smax);
2056         } else {
2057             if (el > 0) {
2058                 el = Math.min(smax, el);
2059             } else if (el < 0) {
2060                 el = Math.max(smin, el);
2061             }
2062         }
2063 
2064         this.setView(az, el);
2065 
2066         return this;
2067     },
2068 
2069     /**
2070      * Controls the navigation in bank direction using either the keyboard or a pointer.
2071      *
2072      * @private
2073      *
2074      * @param {event} evt either the keydown or the pointer event
2075      * @returns view
2076      */
2077     _bankEventHandler: function (evt) {
2078         var smax = this.bank_slide._smax,
2079             smin = this.bank_slide._smin,
2080             step, speed,
2081             delta = evt.deltaY, // Wheel event
2082             bank = this.bank_slide.Value();
2083 
2084         // Doesn't allow navigation if another moving event is triggered
2085         if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) {
2086             return this;
2087         }
2088 
2089         // Calculate new bank value if keyboard events are triggered
2090         // Plus if down-button, minus if up-button
2091         if (this.evalVisProp('bank.keyboard.enabled')) {
2092             step = this.evalVisProp('bank.keyboard.step') * Math.PI / 180;
2093             if (evt.key === '.' || evt.key === '<') {
2094                 bank -= step;
2095             } else if (evt.key === ',' || evt.key === '>') {
2096                 bank += step;
2097             }
2098         }
2099 
2100         if (this.evalVisProp('bank.pointer.enabled') && (delta !== 0) && evt.key == null) {
2101             speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('bank.pointer.speed');
2102             bank += delta * speed;
2103 
2104             // prevent the pointer wheel from scrolling the page
2105             evt.preventDefault();
2106         }
2107 
2108         // Project the calculated bank value to a usable value in the interval [smin,smax]
2109         if (this.evalVisProp('bank.continuous')) {
2110             // in continuous mode, wrap value around slider range
2111             bank = Mat.wrap(bank, smin, smax);
2112         } else {
2113             // in non-continuous mode, clamp value to slider range
2114             bank = Mat.clamp(bank, smin, smax);
2115         }
2116 
2117         this.bank_slide.setValue(bank);
2118         this.board.update();
2119         return this;
2120     },
2121 
2122     /**
2123      * Controls the navigation using either virtual trackball.
2124      *
2125      * @private
2126      *
2127      * @param {event} evt either the keydown or the pointer event
2128      * @returns view
2129      */
2130     _trackballHandler: function (evt) {
2131         var pos = this.board.getMousePosition(evt),
2132             x, y, dx, dy, center;
2133 
2134         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);
2135         x = pos[0] - center.scrCoords[1];
2136         y = pos[1] - center.scrCoords[2];
2137 
2138         dx = evt.screenX - this._lastPos.x;
2139         dy = evt.screenY - this._lastPos.y;
2140         this._lastPos.x = evt.screenX;
2141         this._lastPos.y = evt.screenY;
2142 
2143         this._trackball = {
2144             dx: dx,
2145             dy: -dy,
2146             x: x,
2147             y: -y
2148         };
2149         this.board.update();
2150         return this;
2151     },
2152 
2153     /**
2154      * Event handler for pointer down event. Triggers handling of all 3D navigation.
2155      *
2156      * @private
2157      * @param {event} evt
2158      * @returns view
2159      */
2160     pointerDownHandler: function (evt) {
2161         var neededButton, neededKey, target;
2162 
2163         this._hasMoveAzEl = false;
2164         this._hasMoveAz = false;
2165         this._hasMoveEl = false;
2166         this._hasMoveBank = false;
2167         this._hasMoveTrackball = false;
2168 
2169         if (this.board.mode !== this.board.BOARD_MODE_NONE) {
2170             return;
2171         }
2172 
2173         this.board._change3DView = true;
2174 
2175         this._lastPos.x = evt.screenX;
2176         this._lastPos.y = evt.screenY;
2177 
2178         if (this.evalVisProp('trackball.enabled')) {
2179             neededButton = this.evalVisProp('trackball.button');
2180             neededKey = this.evalVisProp('trackball.key');
2181 
2182             // Move events for virtual trackball
2183             if (
2184                 (neededButton === -1 || neededButton === evt.button) &&
2185                 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
2186             ) {
2187                 // If outside is true then the event listener is bound to the document, otherwise to the div
2188                 target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj;
2189                 Env.addEvent(target, 'pointermove', this._trackballHandler, this);
2190                 this._hasMoveTrackball = true;
2191             }
2192         } else {
2193             if (this.evalVisProp('az.pointer.enabled') && this.evalVisProp('el.pointer.enabled')) {
2194                 neededButton = this.evalVisProp('az.pointer.button');
2195                 neededKey = this.evalVisProp('az.pointer.key');
2196                 if (neededButton === this.evalVisProp('el.pointer.button') &&
2197                     neededKey === this.evalVisProp('el.pointer.key')) {
2198 
2199                     // Move events for azimuth and elevation
2200                     if (
2201                         (neededButton === -1 || neededButton === evt.button) &&
2202                         (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) ||
2203                             (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
2204                     ) {
2205                         // If outside is true then the event listener is bound to the document, otherwise to the div
2206                         target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
2207 
2208                         if (target === ((this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj)) {
2209                             Env.addEvent(target, 'pointermove', this._az_elEventHandler, this);
2210                             this._hasMoveAzEl = true;
2211                         }
2212                     }
2213                 }
2214             }
2215             if (!this._hasMoveAzEl) {
2216                 if (this.evalVisProp('az.pointer.enabled')) {
2217                     neededButton = this.evalVisProp('az.pointer.button');
2218                     neededKey = this.evalVisProp('az.pointer.key');
2219 
2220                     // Move events for azimuth
2221                     if (
2222                         (neededButton === -1 || neededButton === evt.button) &&
2223                         (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
2224                     ) {
2225                         // If outside is true then the event listener is bound to the document, otherwise to the div
2226                         target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
2227                         Env.addEvent(target, 'pointermove', this._azEventHandler, this);
2228                         this._hasMoveAz = true;
2229                     }
2230                 }
2231 
2232                 if (this.evalVisProp('el.pointer.enabled')) {
2233                     neededButton = this.evalVisProp('el.pointer.button');
2234                     neededKey = this.evalVisProp('el.pointer.key');
2235 
2236                     // Events for elevation
2237                     if (
2238                         (neededButton === -1 || neededButton === evt.button) &&
2239                         (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
2240                     ) {
2241                         // If outside is true then the event listener is bound to the document, otherwise to the div
2242                         target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj;
2243                         Env.addEvent(target, 'pointermove', this._elEventHandler, this);
2244                         this._hasMoveEl = true;
2245                     }
2246                 }
2247             }
2248             if (this.evalVisProp('bank.pointer.enabled')) {
2249                 neededButton = this.evalVisProp('bank.pointer.button');
2250                 neededKey = this.evalVisProp('bank.pointer.key');
2251 
2252                 // Events for bank
2253                 if (
2254                     (neededButton === -1 || neededButton === evt.button) &&
2255                     (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
2256                 ) {
2257                     // If `outside` is true, we bind the event listener to
2258                     // the document. otherwise, we bind it to the div. we
2259                     // register the event listener as active so it can
2260                     // prevent the pointer wheel from scrolling the page
2261                     target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj;
2262                     Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false });
2263                     this._hasMoveBank = true;
2264                 }
2265             }
2266         }
2267         Env.addEvent(document, 'pointerup', this.pointerUpHandler, this);
2268     },
2269 
2270     /**
2271      * Event handler for pointer up event. Triggers handling of all 3D navigation.
2272      *
2273      * @private
2274      * @param {event} evt
2275      * @returns view
2276      */
2277     pointerUpHandler: function (evt) {
2278         var target;
2279 
2280         if (this._hasMoveAzEl) {
2281             target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
2282             Env.removeEvent(target, 'pointermove', this._az_elEventHandler, this);
2283             this._hasMoveAzEl = false;
2284         }
2285         if (this._hasMoveAz) {
2286             target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj;
2287             Env.removeEvent(target, 'pointermove', this._azEventHandler, this);
2288             this._hasMoveAz = false;
2289         }
2290         if (this._hasMoveEl) {
2291             target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj;
2292             Env.removeEvent(target, 'pointermove', this._elEventHandler, this);
2293             this._hasMoveEl = false;
2294         }
2295         if (this._hasMoveBank) {
2296             target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj;
2297             Env.removeEvent(target, 'wheel', this._bankEventHandler, this);
2298             this._hasMoveBank = false;
2299         }
2300         if (this._hasMoveTrackball) {
2301             target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj;
2302             Env.removeEvent(target, 'pointermove', this._trackballHandler, this);
2303             this._hasMoveTrackball = false;
2304         }
2305         Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this);
2306         this.board._change3DView = false;
2307         this.board.mode = this.board.BOARD_MODE_NONE;
2308     }
2309 });
2310 
2311 /**
2312  * @class A View3D element provides the container and the methods to create and display 3D elements.
2313  * @pseudo
2314  * @description  A View3D element provides the container and the methods to create and display 3D elements.
2315  * It is contained in a JSXGraph board.
2316  * <p>
2317  * It is advisable to disable panning of the board by setting the board attribute "pan":
2318  * <pre>
2319  *   pan: {enabled: false}
2320  * </pre>
2321  * Otherwise users will not be able to rotate the scene with their fingers on a touch device.
2322  * <p>
2323  * The start position of the camera can be adjusted by the attributes {@link View3D#az}, {@link View3D#el}, and {@link View3D#bank}.
2324  *
2325  * @name View3D
2326  * @augments JXG.View3D
2327  * @constructor
2328  * @type Object
2329  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
2330  * @param {Array_Array_Array} lower,dim,cube  Here, lower is an array of the form [x, y] and
2331  * dim is an array of the form [w, h].
2332  * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is
2333  * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner
2334  * [x,y] and side lengths [w, h] of the board.
2335  * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]]
2336  * which determines the coordinate ranges of the 3D cube.
2337  *
2338  * @example
2339  *     var bound = [-4, 6];
2340  *     var view = board.create('view3d',
2341  *         [[-4, -3], [8, 8],
2342  *         [bound, bound, bound]],
2343  *         {
2344  *             projection: 'parallel',
2345  *             trackball: {enabled:true},
2346  *         });
2347  *
2348  *     var curve = view.create('curve3d', [
2349  *         (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2350  *         (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2351  *         (t) => Math.sin(3 * t),
2352  *         [-Math.PI, Math.PI]
2353  *     ], { strokeWidth: 4 });
2354  *
2355  * </pre><div id="JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b" class="jxgbox" style="width: 300px; height: 300px;"></div>
2356  * <script type="text/javascript">
2357  *     (function() {
2358  *         var board = JXG.JSXGraph.initBoard('JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b',
2359  *             {boundingbox: [-8, 8, 8,-8], pan: {enabled: false}, axis: false, showcopyright: false, shownavigation: false});
2360  *         var bound = [-4, 6];
2361  *         var view = board.create('view3d',
2362  *             [[-4, -3], [8, 8],
2363  *             [bound, bound, bound]],
2364  *             {
2365  *                 projection: 'parallel',
2366  *                 trackball: {enabled:true},
2367  *             });
2368  *
2369  *         var curve = view.create('curve3d', [
2370  *             (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2371  *             (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2372  *             (t) => Math.sin(3 * t),
2373  *             [-Math.PI, Math.PI]
2374  *         ], { strokeWidth: 4 });
2375  *
2376  *     })();
2377  *
2378  * </script><pre>
2379  *
2380  * @example
2381  *     var bound = [-4, 6];
2382  *     var view = board.create('view3d',
2383  *         [[-4, -3], [8, 8],
2384  *         [bound, bound, bound]],
2385  *         {
2386  *             projection: 'central',
2387  *             trackball: {enabled:true},
2388  *
2389  *             xPlaneRear: { visible: false },
2390  *             yPlaneRear: { visible: false }
2391  *
2392  *         });
2393  *
2394  *     var curve = view.create('curve3d', [
2395  *         (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2396  *         (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2397  *         (t) => Math.sin(3 * t),
2398  *         [-Math.PI, Math.PI]
2399  *     ], { strokeWidth: 4 });
2400  *
2401  * </pre><div id="JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007" class="jxgbox" style="width: 300px; height: 300px;"></div>
2402  * <script type="text/javascript">
2403  *     (function() {
2404  *         var board = JXG.JSXGraph.initBoard('JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007',
2405  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
2406  *         var bound = [-4, 6];
2407  *         var view = board.create('view3d',
2408  *             [[-4, -3], [8, 8],
2409  *             [bound, bound, bound]],
2410  *             {
2411  *                 projection: 'central',
2412  *                 trackball: {enabled:true},
2413  *
2414  *                 xPlaneRear: { visible: false },
2415  *                 yPlaneRear: { visible: false }
2416  *
2417  *             });
2418  *
2419  *         var curve = view.create('curve3d', [
2420  *             (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2421  *             (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2422  *             (t) => Math.sin(3 * t),
2423  *             [-Math.PI, Math.PI]
2424  *         ], { strokeWidth: 4 });
2425  *
2426  *     })();
2427  *
2428  * </script><pre>
2429  *
2430 * @example
2431  *     var bound = [-4, 6];
2432  *     var view = board.create('view3d',
2433  *         [[-4, -3], [8, 8],
2434  *         [bound, bound, bound]],
2435  *         {
2436  *             projection: 'central',
2437  *             trackball: {enabled:true},
2438  *
2439  *             // Main axes
2440  *             axesPosition: 'border',
2441  *
2442  *             // Axes at the border
2443  *             xAxisBorder: { ticks3d: { ticksDistance: 2} },
2444  *             yAxisBorder: { ticks3d: { ticksDistance: 2} },
2445  *             zAxisBorder: { ticks3d: { ticksDistance: 2} },
2446  *
2447  *             // No axes on planes
2448  *             xPlaneRearYAxis: {visible: false},
2449  *             xPlaneRearZAxis: {visible: false},
2450  *             yPlaneRearXAxis: {visible: false},
2451  *             yPlaneRearZAxis: {visible: false},
2452  *             zPlaneRearXAxis: {visible: false},
2453  *             zPlaneRearYAxis: {visible: false}
2454  *         });
2455  *
2456  *     var curve = view.create('curve3d', [
2457  *         (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2458  *         (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2459  *         (t) => Math.sin(3 * t),
2460  *         [-Math.PI, Math.PI]
2461  *     ], { strokeWidth: 4 });
2462  *
2463  * </pre><div id="JXG586f3551-335c-47e9-8d72-835409f6a103" class="jxgbox" style="width: 300px; height: 300px;"></div>
2464  * <script type="text/javascript">
2465  *     (function() {
2466  *         var board = JXG.JSXGraph.initBoard('JXG586f3551-335c-47e9-8d72-835409f6a103',
2467  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
2468  *         var bound = [-4, 6];
2469  *         var view = board.create('view3d',
2470  *             [[-4, -3], [8, 8],
2471  *             [bound, bound, bound]],
2472  *             {
2473  *                 projection: 'central',
2474  *                 trackball: {enabled:true},
2475  *
2476  *                 // Main axes
2477  *                 axesPosition: 'border',
2478  *
2479  *                 // Axes at the border
2480  *                 xAxisBorder: { ticks3d: { ticksDistance: 2} },
2481  *                 yAxisBorder: { ticks3d: { ticksDistance: 2} },
2482  *                 zAxisBorder: { ticks3d: { ticksDistance: 2} },
2483  *
2484  *                 // No axes on planes
2485  *                 xPlaneRearYAxis: {visible: false},
2486  *                 xPlaneRearZAxis: {visible: false},
2487  *                 yPlaneRearXAxis: {visible: false},
2488  *                 yPlaneRearZAxis: {visible: false},
2489  *                 zPlaneRearXAxis: {visible: false},
2490  *                 zPlaneRearYAxis: {visible: false}
2491  *             });
2492  *
2493  *         var curve = view.create('curve3d', [
2494  *             (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2495  *             (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2496  *             (t) => Math.sin(3 * t),
2497  *             [-Math.PI, Math.PI]
2498  *         ], { strokeWidth: 4 });
2499  *
2500  *     })();
2501  *
2502  * </script><pre>
2503  *
2504  * @example
2505  *     var bound = [-4, 6];
2506  *     var view = board.create('view3d',
2507  *         [[-4, -3], [8, 8],
2508  *         [bound, bound, bound]],
2509  *         {
2510  *             projection: 'central',
2511  *             trackball: {enabled:true},
2512  *
2513  *             axesPosition: 'none'
2514  *         });
2515  *
2516  *     var curve = view.create('curve3d', [
2517  *         (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2518  *         (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2519  *         (t) => Math.sin(3 * t),
2520  *         [-Math.PI, Math.PI]
2521  *     ], { strokeWidth: 4 });
2522  *
2523  * </pre><div id="JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26" class="jxgbox" style="width: 300px; height: 300px;"></div>
2524  * <script type="text/javascript">
2525  *     (function() {
2526  *         var board = JXG.JSXGraph.initBoard('JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26',
2527  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
2528  *         var bound = [-4, 6];
2529  *         var view = board.create('view3d',
2530  *             [[-4, -3], [8, 8],
2531  *             [bound, bound, bound]],
2532  *             {
2533  *                 projection: 'central',
2534  *                 trackball: {enabled:true},
2535  *
2536  *                 axesPosition: 'none'
2537  *             });
2538  *
2539  *         var curve = view.create('curve3d', [
2540  *             (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2541  *             (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2542  *             (t) => Math.sin(3 * t),
2543  *             [-Math.PI, Math.PI]
2544  *         ], { strokeWidth: 4 });
2545  *
2546  *     })();
2547  *
2548  * </script><pre>
2549  *
2550  * @example
2551  *     var bound = [-4, 6];
2552  *     var view = board.create('view3d',
2553  *         [[-4, -3], [8, 8],
2554  *         [bound, bound, bound]],
2555  *         {
2556  *             projection: 'central',
2557  *             trackball: {enabled:true},
2558  *
2559  *             // Main axes
2560  *             axesPosition: 'border',
2561  *
2562  *             // Axes at the border
2563  *             xAxisBorder: { ticks3d: { ticksDistance: 2} },
2564  *             yAxisBorder: { ticks3d: { ticksDistance: 2} },
2565  *             zAxisBorder: { ticks3d: { ticksDistance: 2} },
2566  *
2567  *             xPlaneRear: {
2568  *                 fillColor: '#fff',
2569  *                 mesh3d: {visible: false}
2570  *             },
2571  *             yPlaneRear: {
2572  *                 fillColor: '#fff',
2573  *                 mesh3d: {visible: false}
2574  *             },
2575  *             zPlaneRear: {
2576  *                 fillColor: '#fff',
2577  *                 mesh3d: {visible: false}
2578  *             },
2579  *             xPlaneFront: {
2580  *                 visible: true,
2581  *                 fillColor: '#fff',
2582  *                 mesh3d: {visible: false}
2583  *             },
2584  *             yPlaneFront: {
2585  *                 visible: true,
2586  *                 fillColor: '#fff',
2587  *                 mesh3d: {visible: false}
2588  *             },
2589  *             zPlaneFront: {
2590  *                 visible: true,
2591  *                 fillColor: '#fff',
2592  *                 mesh3d: {visible: false}
2593  *             },
2594  *
2595  *             // No axes on planes
2596  *             xPlaneRearYAxis: {visible: false},
2597  *             xPlaneRearZAxis: {visible: false},
2598  *             yPlaneRearXAxis: {visible: false},
2599  *             yPlaneRearZAxis: {visible: false},
2600  *             zPlaneRearXAxis: {visible: false},
2601  *             zPlaneRearYAxis: {visible: false},
2602  *             xPlaneFrontYAxis: {visible: false},
2603  *             xPlaneFrontZAxis: {visible: false},
2604  *             yPlaneFrontXAxis: {visible: false},
2605  *             yPlaneFrontZAxis: {visible: false},
2606  *             zPlaneFrontXAxis: {visible: false},
2607  *             zPlaneFrontYAxis: {visible: false}
2608  *
2609  *         });
2610  *
2611  *     var curve = view.create('curve3d', [
2612  *         (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2613  *         (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2614  *         (t) => Math.sin(3 * t),
2615  *         [-Math.PI, Math.PI]
2616  *     ], { strokeWidth: 4 });
2617  *
2618  * </pre><div id="JXGbd41a4e3-1bf7-4764-b675-98b01667103b" class="jxgbox" style="width: 300px; height: 300px;"></div>
2619  * <script type="text/javascript">
2620  *     (function() {
2621  *         var board = JXG.JSXGraph.initBoard('JXGbd41a4e3-1bf7-4764-b675-98b01667103b',
2622  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
2623  *         var bound = [-4, 6];
2624  *         var view = board.create('view3d',
2625  *             [[-4, -3], [8, 8],
2626  *             [bound, bound, bound]],
2627  *             {
2628  *                 projection: 'central',
2629  *                 trackball: {enabled:true},
2630  *
2631  *                 // Main axes
2632  *                 axesPosition: 'border',
2633  *
2634  *                 // Axes at the border
2635  *                 xAxisBorder: { ticks3d: { ticksDistance: 2} },
2636  *                 yAxisBorder: { ticks3d: { ticksDistance: 2} },
2637  *                 zAxisBorder: { ticks3d: { ticksDistance: 2} },
2638  *
2639  *                 xPlaneRear: {
2640  *                     fillColor: '#fff',
2641  *                     mesh3d: {visible: false}
2642  *                 },
2643  *                 yPlaneRear: {
2644  *                     fillColor: '#fff',
2645  *                     mesh3d: {visible: false}
2646  *                 },
2647  *                 zPlaneRear: {
2648  *                     fillColor: '#fff',
2649  *                     mesh3d: {visible: false}
2650  *                 },
2651  *                 xPlaneFront: {
2652  *                     visible: true,
2653  *                     fillColor: '#fff',
2654  *                     mesh3d: {visible: false}
2655  *                 },
2656  *                 yPlaneFront: {
2657  *                     visible: true,
2658  *                     fillColor: '#fff',
2659  *                     mesh3d: {visible: false}
2660  *                 },
2661  *                 zPlaneFront: {
2662  *                     visible: true,
2663  *                     fillColor: '#fff',
2664  *                     mesh3d: {visible: false}
2665  *                 },
2666  *
2667  *                 // No axes on planes
2668  *                 xPlaneRearYAxis: {visible: false},
2669  *                 xPlaneRearZAxis: {visible: false},
2670  *                 yPlaneRearXAxis: {visible: false},
2671  *                 yPlaneRearZAxis: {visible: false},
2672  *                 zPlaneRearXAxis: {visible: false},
2673  *                 zPlaneRearYAxis: {visible: false},
2674  *                 xPlaneFrontYAxis: {visible: false},
2675  *                 xPlaneFrontZAxis: {visible: false},
2676  *                 yPlaneFrontXAxis: {visible: false},
2677  *                 yPlaneFrontZAxis: {visible: false},
2678  *                 zPlaneFrontXAxis: {visible: false},
2679  *                 zPlaneFrontYAxis: {visible: false}
2680  *
2681  *             });
2682  *
2683  *         var curve = view.create('curve3d', [
2684  *             (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t),
2685  *             (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t),
2686  *             (t) => Math.sin(3 * t),
2687  *             [-Math.PI, Math.PI]
2688  *         ], { strokeWidth: 4 });
2689  *     })();
2690  *
2691  * </script><pre>
2692  *
2693  * @example
2694  *  var bound = [-5, 5];
2695  *  var view = board.create('view3d',
2696  *      [[-6, -3],
2697  *       [8, 8],
2698  *       [bound, bound, bound]],
2699  *      {
2700  *          // Main axes
2701  *          axesPosition: 'center',
2702  *          xAxis: { strokeColor: 'blue', strokeWidth: 3},
2703  *
2704  *          // Planes
2705  *          xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
2706  *          yPlaneFront: { visible: true, fillColor: 'blue'},
2707  *
2708  *          // Axes on planes
2709  *          xPlaneRearYAxis: {strokeColor: 'red'},
2710  *          xPlaneRearZAxis: {strokeColor: 'red'},
2711  *
2712  *          yPlaneFrontXAxis: {strokeColor: 'blue'},
2713  *          yPlaneFrontZAxis: {strokeColor: 'blue'},
2714  *
2715  *          zPlaneFrontXAxis: {visible: false},
2716  *          zPlaneFrontYAxis: {visible: false}
2717  *      });
2718  *
2719  * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div>
2720  * <script type="text/javascript">
2721  *     (function() {
2722  *         var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7',
2723  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
2724  *         var bound = [-5, 5];
2725  *         var view = board.create('view3d',
2726  *             [[-6, -3], [8, 8],
2727  *             [bound, bound, bound]],
2728  *             {
2729  *                 // Main axes
2730  *                 axesPosition: 'center',
2731  *                 xAxis: { strokeColor: 'blue', strokeWidth: 3},
2732  *                 // Planes
2733  *                 xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
2734  *                 yPlaneFront: { visible: true, fillColor: 'blue'},
2735  *                 // Axes on planes
2736  *                 xPlaneRearYAxis: {strokeColor: 'red'},
2737  *                 xPlaneRearZAxis: {strokeColor: 'red'},
2738  *                 yPlaneFrontXAxis: {strokeColor: 'blue'},
2739  *                 yPlaneFrontZAxis: {strokeColor: 'blue'},
2740  *                 zPlaneFrontXAxis: {visible: false},
2741  *                 zPlaneFrontYAxis: {visible: false}
2742  *             });
2743  *     })();
2744  *
2745  * </script><pre>
2746  * @example
2747  * var bound = [-5, 5];
2748  * var view = board.create('view3d',
2749  *     [[-6, -3], [8, 8],
2750  *     [bound, bound, bound]],
2751  *     {
2752  *         projection: 'central',
2753  *         az: {
2754  *             slider: {
2755  *                 visible: true,
2756  *                 point1: {
2757  *                     pos: [5, -4]
2758  *                 },
2759  *                 point2: {
2760  *                     pos: [5, 4]
2761  *                 },
2762  *                 label: {anchorX: 'middle'}
2763  *             }
2764  *         },
2765  *         el: {
2766  *             slider: {
2767  *                 visible: true,
2768  *                 point1: {
2769  *                     pos: [6, -5]
2770  *                 },
2771  *                 point2: {
2772  *                     pos: [6, 3]
2773  *                 },
2774  *                 label: {anchorX: 'middle'}
2775  *             }
2776  *         },
2777  *         bank: {
2778  *             slider: {
2779  *                 visible: true,
2780  *                 point1: {
2781  *                     pos: [7, -6]
2782  *                 },
2783  *                 point2: {
2784  *                     pos: [7, 2]
2785  *                 },
2786  *                 label: {anchorX: 'middle'}
2787  *             }
2788  *         }
2789  *     });
2790  *
2791  *
2792  * </pre><div id="JXGe181cc55-271b-419b-84fd-622326fd1d1a" class="jxgbox" style="width: 300px; height: 300px;"></div>
2793  * <script type="text/javascript">
2794  *     (function() {
2795  *         var board = JXG.JSXGraph.initBoard('JXGe181cc55-271b-419b-84fd-622326fd1d1a',
2796  *             {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false});
2797  *     var bound = [-5, 5];
2798  *     var view = board.create('view3d',
2799  *         [[-6, -3], [8, 8],
2800  *         [bound, bound, bound]],
2801  *         {
2802  *             projection: 'central',
2803  *             az: {
2804  *                 slider: {
2805  *                     visible: true,
2806  *                     point1: {
2807  *                         pos: [5, -4]
2808  *                     },
2809  *                     point2: {
2810  *                         pos: [5, 4]
2811  *                     },
2812  *                     label: {anchorX: 'middle'}
2813  *                 }
2814  *             },
2815  *             el: {
2816  *                 slider: {
2817  *                     visible: true,
2818  *                     point1: {
2819  *                         pos: [6, -5]
2820  *                     },
2821  *                     point2: {
2822  *                         pos: [6, 3]
2823  *                     },
2824  *                     label: {anchorX: 'middle'}
2825  *                 }
2826  *             },
2827  *             bank: {
2828  *                 slider: {
2829  *                     visible: true,
2830  *                     point1: {
2831  *                         pos: [7, -6]
2832  *                     },
2833  *                     point2: {
2834  *                         pos: [7, 2]
2835  *                     },
2836  *                     label: {anchorX: 'middle'}
2837  *                 }
2838  *             }
2839  *         });
2840  *
2841  *
2842  *     })();
2843  *
2844  * </script><pre>
2845  *
2846  *
2847  */
2848 JXG.createView3D = function (board, parents, attributes) {
2849     var view, attr, attr_az, attr_el, attr_bank,
2850         x, y, w, h,
2851         p1, p2, v,
2852         coords = parents[0], // llft corner
2853         size = parents[1]; // [w, h]
2854 
2855     attr = Type.copyAttributes(attributes, board.options, 'view3d');
2856     view = new JXG.View3D(board, parents, attr);
2857     view.defaultAxes = view.create('axes3d', [], attr);
2858 
2859     x = coords[0];
2860     y = coords[1];
2861     w = size[0];
2862     h = size[1];
2863 
2864     attr_az = Type.copyAttributes(attr, board.options, 'view3d', 'az', 'slider');
2865     attr_az.name = 'az';
2866 
2867     attr_el = Type.copyAttributes(attr, board.options, 'view3d', 'el', 'slider');
2868     attr_el.name = 'el';
2869 
2870     attr_bank = Type.copyAttributes(attr, board.options, 'view3d', 'bank', 'slider');
2871     attr_bank.name = 'bank';
2872 
2873     v = Type.evaluate(attr_az.point1.pos);
2874     if (!Type.isArray(v)) {
2875         // 'auto'
2876         p1 = [x - 1, y - 2];
2877     } else {
2878         p1 = v;
2879     }
2880     v = Type.evaluate(attr_az.point2.pos);
2881     if (!Type.isArray(v)) {
2882         // 'auto'
2883         p2 = [x + w + 1, y - 2];
2884     } else {
2885         p2 = v;
2886     }
2887 
2888     /**
2889      * Slider to adapt azimuth angle
2890      * @name JXG.View3D#az_slide
2891      * @type {Slider}
2892      */
2893     view.az_slide = board.create(
2894         'slider',
2895         [
2896             p1, p2,
2897             [
2898                 Type.evaluate(attr_az.min),
2899                 Type.evaluate(attr_az.start),
2900                 Type.evaluate(attr_az.max)
2901             ]
2902         ],
2903         attr_az
2904     );
2905     view.inherits.push(view.az_slide);
2906     view.az_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate()
2907 
2908     v = Type.evaluate(attr_el.point1.pos);
2909     if (!Type.isArray(v)) {
2910         // 'auto'
2911         p1 = [x - 1, y];
2912     } else {
2913         p1 = v;
2914     }
2915     v = Type.evaluate(attr_el.point2.pos);
2916     if (!Type.isArray(v)) {
2917         // 'auto'
2918         p2 = [x - 1, y + h];
2919     } else {
2920         p2 = v;
2921     }
2922 
2923     /**
2924      * Slider to adapt elevation angle
2925      *
2926      * @name JXG.View3D#el_slide
2927      * @type {Slider}
2928      */
2929     view.el_slide = board.create(
2930         'slider',
2931         [
2932             p1, p2,
2933             [
2934                 Type.evaluate(attr_el.min),
2935                 Type.evaluate(attr_el.start),
2936                 Type.evaluate(attr_el.max)]
2937         ],
2938         attr_el
2939     );
2940     view.inherits.push(view.el_slide);
2941     view.el_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate()
2942 
2943     v = Type.evaluate(attr_bank.point1.pos);
2944     if (!Type.isArray(v)) {
2945         // 'auto'
2946         p1 = [x - 1, y + h + 2];
2947     } else {
2948         p1 = v;
2949     }
2950     v = Type.evaluate(attr_bank.point2.pos);
2951     if (!Type.isArray(v)) {
2952         // 'auto'
2953         p2 = [x + w + 1, y + h + 2];
2954     } else {
2955         p2 = v;
2956     }
2957 
2958     /**
2959      * Slider to adjust bank angle
2960      *
2961      * @name JXG.View3D#bank_slide
2962      * @type {Slider}
2963      */
2964     view.bank_slide = board.create(
2965         'slider',
2966         [
2967             p1, p2,
2968             [
2969                 Type.evaluate(attr_bank.min),
2970                 Type.evaluate(attr_bank.start),
2971                 Type.evaluate(attr_bank.max)
2972             ]
2973         ],
2974         attr_bank
2975     );
2976     view.inherits.push(view.bank_slide);
2977     view.bank_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate()
2978 
2979     // Set special infobox attributes of view3d.infobox
2980     // Using setAttribute() is not possible here, since we have to
2981     // avoid a call of board.update().
2982     // The drawback is that we can not use shortcuts
2983     view.board.infobox.visProp = Type.merge(view.board.infobox.visProp, attr.infobox);
2984 
2985     // 3d infobox: drag direction and coordinates
2986     view.board.highlightInfobox = function (x, y, el) {
2987         var d, i, c3d, foot,
2988             pre = '',
2989             brd = el.board,
2990             arr, infobox,
2991             p = null;
2992 
2993         if (this.mode === this.BOARD_MODE_DRAG) {
2994             // Drag direction is only shown during dragging
2995             if (view.isVerticalDrag()) {
2996                 pre = '<span style="color:black; font-size:200%">\u21C5  </span>';
2997             } else {
2998                 pre = '<span style="color:black; font-size:200%">\u21C4  </span>';
2999             }
3000         }
3001 
3002         // Search 3D parent
3003         for (i = 0; i < el.parents.length; i++) {
3004             p = brd.objects[el.parents[i]];
3005             if (p.is3D) {
3006                 break;
3007             }
3008         }
3009 
3010         if (p && Type.exists(p.element2D)) {
3011             foot = [1, 0, 0, p.coords[3]];
3012             view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4);
3013 
3014             c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot);
3015             if (!view.isInCube(c3d)) {
3016                 view.board.highlightCustomInfobox('', p);
3017                 return;
3018             }
3019             d = p.evalVisProp('infoboxdigits');
3020             infobox = view.board.infobox;
3021             if (d === 'auto') {
3022                 if (infobox.useLocale()) {
3023                     arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')'];
3024                 } else {
3025                     arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')'];
3026                 }
3027 
3028             } else {
3029                 if (infobox.useLocale()) {
3030                     arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')'];
3031                 } else {
3032                     arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')'];
3033                 }
3034             }
3035             view.board.highlightCustomInfobox(arr.join(''), p);
3036         } else {
3037             view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
3038         }
3039     };
3040 
3041     // Hack needed to enable addEvent for view3D:
3042     view.BOARD_MODE_NONE = 0x0000;
3043 
3044     // Add events for the keyboard navigation
3045     Env.addEvent(board.containerObj, 'keydown', function (event) {
3046         var neededKey,
3047             catchEvt = false;
3048 
3049         // this.board._change3DView = true;
3050         if (view.evalVisProp('el.keyboard.enabled') &&
3051             (event.key === 'ArrowUp' || event.key === 'ArrowDown')
3052         ) {
3053             neededKey = view.evalVisProp('el.keyboard.key');
3054             if (neededKey === 'none' ||
3055                 (neededKey.indexOf('shift') > -1 && event.shiftKey) ||
3056                 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
3057                 view._elEventHandler(event);
3058                 catchEvt = true;
3059             }
3060 
3061         }
3062 
3063         if (view.evalVisProp('az.keyboard.enabled') &&
3064             (event.key === 'ArrowLeft' || event.key === 'ArrowRight')
3065         ) {
3066             neededKey = view.evalVisProp('az.keyboard.key');
3067             if (neededKey === 'none' ||
3068                 (neededKey.indexOf('shift') > -1 && event.shiftKey) ||
3069                 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)
3070             ) {
3071                 view._azEventHandler(event);
3072                 catchEvt = true;
3073             }
3074         }
3075 
3076         if (view.evalVisProp('bank.keyboard.enabled') && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) {
3077             neededKey = view.evalVisProp('bank.keyboard.key');
3078             if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
3079                 view._bankEventHandler(event);
3080                 catchEvt = true;
3081             }
3082         }
3083 
3084         if (event.key === 'PageUp') {
3085             view.nextView();
3086             catchEvt = true;
3087         } else if (event.key === 'PageDown') {
3088             view.previousView();
3089             catchEvt = true;
3090         }
3091 
3092         if (catchEvt) {
3093             // We stop event handling only in the case if the keypress could be
3094             // used for the 3D view. If this is not done, input fields et al
3095             // can not be used any more.
3096             event.preventDefault();
3097         }
3098         this.board._change3DView = false;
3099 
3100     }, view);
3101 
3102     // Add events for the pointer navigation
3103     Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view);
3104 
3105     // Initialize view rotation matrix
3106     view.getAnglesFromSliders();
3107     view.matrix3DRot = view.getRotationFromAngles();
3108 
3109     // override angle slider bounds when trackball navigation is enabled
3110     view.updateAngleSliderBounds();
3111 
3112     view.board.update();
3113 
3114     return view;
3115 };
3116 
3117 JXG.registerElement("view3d", JXG.createView3D);
3118 
3119 export default JXG.View3D;
3120