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