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