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