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