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