1 /*
  2     Copyright 2008-2023
  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 /*global JXG:true, define: true*/
 30 
 31 import JXG from "../jxg";
 32 import Const from "../base/constants";
 33 import Type from "../utils/type";
 34 import Mat from "../math/math";
 35 import Env from "../utils/env";
 36 import GeometryElement from "../base/element";
 37 import Composition from "../base/composition";
 38 
 39 /**
 40  * 3D view inside a JXGraph board.
 41  *
 42  * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with
 43  * type {@link View3D} instead.
 44  *
 45  * @augments JXG.GeometryElement
 46  * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view
 47  * 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
 48  * [x,y] and side lengths [w, h] of the board.
 49  */
 50 JXG.View3D = function (board, parents, attributes) {
 51     this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D);
 52 
 53     /**
 54      * An associative array containing all geometric objects belonging to the view.
 55      * Key is the id of the object and value is a reference to the object.
 56      * @type Object
 57      * @private
 58      */
 59     this.objects = {};
 60 
 61     /**
 62      * An array containing all geometric objects in this view in the order of construction.
 63      * @type Array
 64      * @private
 65      */
 66     // this.objectsList = [];
 67 
 68     /**
 69      * 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.
 70      * @type Object
 71      * @private
 72      */
 73     this.elementsByName = {};
 74 
 75     /**
 76      * Default axes of the 3D view, contains the axes of the view or null.
 77      *
 78      * @type {Object}
 79      * @default null
 80      */
 81     this.defaultAxes = null;
 82 
 83     /**
 84      * @type  {Array}
 85      * @private
 86      */
 87     // 3D-to-2D transformation matrix
 88     this.matrix3D = [
 89         [1, 0, 0, 0],
 90         [0, 1, 0, 0],
 91         [0, 0, 1, 0]
 92     ];
 93 
 94     /**
 95      * @type array
 96      * @private
 97      */
 98     // Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0.
 99     this.llftCorner = parents[0];
100 
101     /**
102      * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0.
103      * @type array
104      * @private
105      */
106     this.size = parents[1];
107 
108     /**
109      * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view
110      * @type array
111      */
112     this.bbox3D = parents[2];
113 
114     /**
115      * Distance of the view to the origin. In other words, its
116      * the radius of the sphere where the camera sits.view.board.update
117      * @type Number
118      */
119     this.r = -1;
120 
121     /**
122      * Type of projection.
123      * @type String
124      */
125     // Will be set in update().
126     this.projectionType = 'parallel';
127 
128     this.timeoutAzimuth = null;
129 
130     this.id = this.board.setId(this, 'V');
131     this.board.finalizeAdding(this);
132     this.elType = 'view3d';
133 
134     this.methodMap = Type.deepCopy(this.methodMap, {
135         // TODO
136     });
137 };
138 JXG.View3D.prototype = new GeometryElement();
139 
140 JXG.extend(
141     JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ {
142 
143         /**
144          * Creates a new 3D element of type elementType.
145          * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
146          * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
147          * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
148          * methods for a list of possible parameters.
149          * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
150          * Common attributes are name, visible, strokeColor.
151          * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
152          * two or more elements.
153          */
154         create: function (elementType, parents, attributes) {
155             var prefix = [],
156                 el;
157 
158             if (elementType.indexOf('3d') > 0) {
159                 // is3D = true;
160                 prefix.push(this);
161             }
162             el = this.board.create(elementType, prefix.concat(parents), attributes);
163 
164             return el;
165         },
166 
167         /**
168          * Select a single or multiple elements at once.
169          * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
170          * be used as a filter to return multiple elements at once filtered by the properties of the object.
171          * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
172          * The advanced filters consisting of objects or functions are ignored.
173          * @returns {JXG.GeometryElement3D|JXG.Composition}
174          * @example
175          * // select the element with name A
176          * view.select('A');
177          *
178          * // select all elements with strokecolor set to 'red' (but not '#ff0000')
179          * view.select({
180          *   strokeColor: 'red'
181          * });
182          *
183          * // select all points on or below the x/y plane and make them black.
184          * view.select({
185          *   elType: 'point3d',
186          *   Z: function (v) {
187          *     return v <= 0;
188          *   }
189          * }).setAttribute({color: 'black'});
190          *
191          * // select all elements
192          * view.select(function (el) {
193          *   return true;
194          * });
195          */
196         select: function (str, onlyByIdOrName) {
197             var flist,
198                 olist,
199                 i,
200                 l,
201                 s = str;
202 
203             if (s === null) {
204                 return s;
205             }
206 
207             // It's a string, most likely an id or a name.
208             if (Type.isString(s) && s !== '') {
209                 // Search by ID
210                 if (Type.exists(this.objects[s])) {
211                     s = this.objects[s];
212                     // Search by name
213                 } else if (Type.exists(this.elementsByName[s])) {
214                     s = this.elementsByName[s];
215                     // // Search by group ID
216                     // } else if (Type.exists(this.groups[s])) {
217                     //     s = this.groups[s];
218                 }
219 
220                 // It's a function or an object, but not an element
221             } else if (
222                 !onlyByIdOrName &&
223                 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
224             ) {
225                 flist = Type.filterElements(this.objectsList, s);
226 
227                 olist = {};
228                 l = flist.length;
229                 for (i = 0; i < l; i++) {
230                     olist[flist[i].id] = flist[i];
231                 }
232                 s = new Composition(olist);
233 
234                 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
235             } else if (
236                 Type.isObject(s) &&
237                 Type.exists(s.id) &&
238                 !Type.exists(this.objects[s.id])
239             ) {
240                 s = null;
241             }
242 
243             return s;
244         },
245 
246         updateParallelProjection: function () {
247             var r, a, e, f,
248                 mat = [
249                     [1, 0, 0, 0],
250                     [0, 1, 0, 0],
251                     [0, 0, 1, 0]
252                 ];
253 
254             // mat projects homogeneous 3D coords in View3D
255             // to homogeneous 2D coordinates in the board
256             e = this.el_slide.Value();
257             r = this.r;
258             a = this.az_slide.Value();
259             f = r * Math.sin(e);
260 
261             mat[1][1] = r * Math.cos(a);
262             mat[1][2] = -r * Math.sin(a);
263             mat[2][1] = f * Math.sin(a);
264             mat[2][2] = f * Math.cos(a);
265             mat[2][3] = Math.cos(e);
266 
267             return mat;
268         },
269 
270         /**
271          * @private
272          * @returns {Array}
273          */
274         _updateCentralProjection: function () {
275             var r, e, a, up,
276                 az, ax, ay, v, nrm,
277                 // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html
278                 // bbox3D is always at the world origin, i.e. T_obj is the unit matrix.
279                 // All vectors contain affine coordinates and have length 3
280                 // The matrices are of size 4x4.
281                 Tcam1, // The inverse camera transformation
282                 eye, d,
283                 foc = 1 / Math.tan(0.5 * Type.evaluate(this.visProp.fov)),
284                 zf = 20,
285                 zn = 8,
286                 Pref = [
287                     0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]),
288                     0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]),
289                     0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1])
290                 ],
291 
292                 A = [
293                     [0, 0, 0, -1],
294                     [0, foc, 0, 0],
295                     [0, 0, foc, 0],
296                     [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)]
297                 ],
298 
299                 func_sphere;
300 
301             /**
302              * Calculates a spherical parametric surface, which depends on az, el and r.
303              * @param {Number} a
304              * @param {Number} e
305              * @param {Number} r
306              * @returns {Array} 3-dimensional vector in cartesian coordinates
307              */
308             func_sphere = function (az, el, r) {
309                 return [
310                     r * Math.cos(az) * Math.cos(el),
311                     -r * Math.sin(az) * Math.cos(el),
312                     r * Math.sin(el)
313                 ];
314             };
315 
316             a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere
317             e = this.el_slide.Value() * 2;
318 
319             r = Type.evaluate(this.visProp.r);
320             if (r === 'auto') {
321                 r = Math.sqrt(
322                     Math.pow(this.bbox3D[0][0] - this.bbox3D[0][1], 2) +
323                     Math.pow(this.bbox3D[1][0] - this.bbox3D[1][1], 2) +
324                     Math.pow(this.bbox3D[2][0] - this.bbox3D[2][1], 2)
325                 ) * 1.01;
326             }
327 
328             // create an up vector and an eye vector which are 90 degrees out of phase
329             up = func_sphere(a, e + Math.PI / 2, 1);
330             eye = func_sphere(a, e, r);
331 
332             d = [eye[0] - Pref[0], eye[1] - Pref[1], eye[2] - Pref[2]];
333             nrm = Mat.norm(d, 3);
334             az = [d[0] / nrm, d[1] / nrm, d[2] / nrm];
335 
336             nrm = Mat.norm(up, 3);
337             v = [up[0] / nrm, up[1] / nrm, up[2] / nrm];
338 
339             ax = Mat.crossProduct(v, az);
340             ay = Mat.crossProduct(az, ax);
341 
342             v = Mat.matVecMult([ax, ay, az], eye);
343             Tcam1 = [
344                 [1, 0, 0, 0],
345                 [-v[0], ax[0], ax[1], ax[2]],
346                 [-v[1], ay[0], ay[1], ay[2]],
347                 [-v[2], az[0], az[1], az[2]]
348             ];
349             A = Mat.matMatMult(A, Tcam1);
350 
351             return A;
352         },
353 
354         // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles.
355         update: function () {
356             var mat2D, shift, size;
357 
358             if (
359                 !Type.exists(this.el_slide) ||
360                 !Type.exists(this.az_slide) ||
361                 !this.needsUpdate
362             ) {
363                 return this;
364             }
365 
366             mat2D = [
367                 [1, 0, 0],
368                 [0, 1, 0],
369                 [0, 0, 1]
370             ];
371 
372             this.projectionType = Type.evaluate(this.visProp.projection).toLowerCase();
373 
374             switch (this.projectionType) {
375                 case 'central': // Central projection
376 
377                     this.matrix3D = this._updateCentralProjection();
378                     // this.matrix3D is a 4x4 matrix
379 
380                     size = 0.4;
381                     mat2D[1][1] = this.size[0] / (2 * size); // w / d_x
382                     mat2D[2][2] = this.size[1] / (2 * size); // h / d_y
383                     mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * (2 * size); // llft_x
384                     mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * (2 * size); // llft_y
385 
386                     // The transformations this.matrix3D and mat2D can not be combined yet, since
387                     // the projected vector has to be normalized in between in
388                     // project3DTo2D
389                     this.viewPortTransform = mat2D;
390                     break;
391 
392                 case 'parallel': // Parallel projection
393                 default:
394                     // Rotate the scenery around the center of the box, not around the origin
395                     shift = [
396                         [1, 0, 0, 0],
397                         [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0],
398                         [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0],
399                         [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1]
400                     ];
401 
402                     // Add a second transformation to scale and shift the projection
403                     // on the board, usually called viewport.
404                     mat2D[1][1] = this.size[0] / (this.bbox3D[0][1] - this.bbox3D[0][0]); // w / d_x
405                     mat2D[2][2] = this.size[1] / (this.bbox3D[1][1] - this.bbox3D[1][0]); // h / d_y
406                     mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * (this.bbox3D[0][1] - this.bbox3D[0][0]); // llft_x
407                     mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * (this.bbox3D[1][1] - this.bbox3D[1][0]); // llft_y
408 
409                     // this.matrix3D is a 3x4 matrix
410                     this.matrix3D = this.updateParallelProjection();
411                     // Combine the projections
412                     this.matrix3D = Mat.matMatMult(mat2D, Mat.matMatMult(this.matrix3D, shift));
413             }
414 
415             return this;
416         },
417 
418         updateRenderer: function () {
419             this.needsUpdate = false;
420             return this;
421         },
422 
423         removeObject: function (object, saveMethod) {
424             var i;
425 
426             // this.board.removeObject(object, saveMethod);
427             if (Type.isArray(object)) {
428                 for (i = 0; i < object.length; i++) {
429                     this.removeObject(object[i]);
430                 }
431                 return this;
432             }
433 
434             object = this.select(object);
435 
436             // // If the object which is about to be removed unknown or a string, do nothing.
437             // // it is a string if a string was given and could not be resolved to an element.
438             if (!Type.exists(object) || Type.isString(object)) {
439                 return this;
440             }
441 
442             try {
443                 //     // remove all children.
444                 //     for (el in object.childElements) {
445                 //         if (object.childElements.hasOwnProperty(el)) {
446                 //             object.childElements[el].board.removeObject(object.childElements[el]);
447                 //         }
448                 //     }
449 
450                 delete this.objects[object.id];
451             } catch (e) {
452                 JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e);
453             }
454 
455             // this.update();
456 
457             this.board.removeObject(object, saveMethod);
458 
459             return this;
460         },
461 
462         /**
463          * Project 3D coordinates to 2D board coordinates
464          * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
465          *
466          * @param  {Number|Array} x
467          * @param  {Number[]} y
468          * @param  {Number[]} z
469          * @returns {Array} Array of length 3 containing the projection on to the board
470          * in homogeneous user coordinates.
471          */
472         project3DTo2D: function (x, y, z) {
473             var vec, w;
474             if (arguments.length === 3) {
475                 vec = [1, x, y, z];
476             } else {
477                 // Argument is an array
478                 if (x.length === 3) {
479                     vec = [1].concat(x);
480                 } else {
481                     vec = x;
482                 }
483             }
484 
485             w = Mat.matVecMult(this.matrix3D, vec);
486 
487             switch (this.projectionType) {
488                 case 'central':
489                     w[1] /= w[0];
490                     w[2] /= w[0];
491                     w[3] /= w[0];
492                     w[0] /= w[0];
493                     return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3));
494 
495                 case 'parallel':
496                 default:
497                     return w;
498             }
499         },
500 
501         /**
502          * Project a 2D coordinate to the plane defined by point "foot"
503          * and the normal vector `normal`.
504          *
505          * @param  {JXG.Point} point2d
506          * @param  {Array} normal
507          * @param  {Array} foot
508          * @returns {Array} of length 4 containing the projected
509          * point in homogeneous coordinates.
510          */
511         project2DTo3DPlane: function (point2d, normal, foot) {
512             var mat, rhs, d, le,
513                 n = normal.slice(1),
514                 sol;
515 
516             foot = foot || [1, 0, 0, 0];
517             le = Mat.norm(n, 3);
518             d = Mat.innerProduct(foot.slice(1), n, 3) / le;
519 
520             mat = this.matrix3D.slice(0, 3); // True copy
521             mat.push([0].concat(n));
522 
523             // 2D coordinates of point:
524             rhs = point2d.coords.usrCoords.concat([d]);
525             try {
526                 // Prevent singularity in case elevation angle is zero
527                 if (mat[2][3] === 1.0) {
528                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
529                 }
530                 sol = Mat.Numerics.Gauss(mat, rhs);
531             } catch (err) {
532                 sol = [0, NaN, NaN, NaN];
533             }
534 
535             return sol;
536         },
537 
538         /**
539          * Project a 2D coordinate to a new 3D position by keeping
540          * the 3D x, y coordinates and changing only the z coordinate.
541          * All horizontal moves of the 2D point are ignored.
542          *
543          * @param {JXG.Point} point2d
544          * @param {Array} coords3D
545          * @returns {Array} of length 4 containing the projected
546          * point in homogeneous coordinates.
547          */
548         project2DTo3DVertical: function (point2d, coords3D) {
549             var m3D = this.matrix3D[2],
550                 b = m3D[3],
551                 rhs = point2d.coords.usrCoords[2]; // y in 2D
552 
553             rhs -= m3D[0] * coords3D[0] + m3D[1] * coords3D[1] + m3D[2] * coords3D[2];
554             if (Math.abs(b) < Mat.eps) {
555                 return coords3D; // No changes
556             } else {
557                 return coords3D.slice(0, 3).concat([rhs / b]);
558             }
559         },
560 
561         /**
562          * Limit 3D coordinates to the bounding cube.
563          *
564          * @param {Array} c3d 3D coordinates [x,y,z]
565          * @returns Array with updated 3D coordinates.
566          */
567         project3DToCube: function (c3d) {
568             var cube = this.bbox3D;
569             if (c3d[1] < cube[0][0]) {
570                 c3d[1] = cube[0][0];
571             }
572             if (c3d[1] > cube[0][1]) {
573                 c3d[1] = cube[0][1];
574             }
575             if (c3d[2] < cube[1][0]) {
576                 c3d[2] = cube[1][0];
577             }
578             if (c3d[2] > cube[1][1]) {
579                 c3d[2] = cube[1][1];
580             }
581             if (c3d[3] < cube[2][0]) {
582                 c3d[3] = cube[2][0];
583             }
584             if (c3d[3] > cube[2][1]) {
585                 c3d[3] = cube[2][1];
586             }
587 
588             return c3d;
589         },
590 
591         /**
592          * Intersect a ray with the bounding cube of the 3D view.
593          * @param {Array} p 3D coordinates [x,y,z]
594          * @param {Array} d 3D direction vector of the line (array of length 3)
595          * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
596          * @returns Affine ratio of the intersection of the line with the cube.
597          */
598         intersectionLineCube: function (p, d, r) {
599             var rnew, i, r0, r1;
600 
601             rnew = r;
602             for (i = 0; i < 3; i++) {
603                 if (d[i] !== 0) {
604                     r0 = (this.bbox3D[i][0] - p[i]) / d[i];
605                     r1 = (this.bbox3D[i][1] - p[i]) / d[i];
606                     if (r < 0) {
607                         rnew = Math.max(rnew, Math.min(r0, r1));
608                     } else {
609                         rnew = Math.min(rnew, Math.max(r0, r1));
610                     }
611                 }
612             }
613             return rnew;
614         },
615 
616         /**
617          * Test if coordinates are inside of the bounding cube.
618          * @param {array} q 3D coordinates [x,y,z] of a point.
619          * @returns Boolean
620          */
621         isInCube: function (q) {
622             return (
623                 q[0] > this.bbox3D[0][0] - Mat.eps &&
624                 q[0] < this.bbox3D[0][1] + Mat.eps &&
625                 q[1] > this.bbox3D[1][0] - Mat.eps &&
626                 q[1] < this.bbox3D[1][1] + Mat.eps &&
627                 q[2] > this.bbox3D[2][0] - Mat.eps &&
628                 q[2] < this.bbox3D[2][1] + Mat.eps
629             );
630         },
631 
632         /**
633          *
634          * @param {JXG.Plane3D} plane1
635          * @param {JXG.Plane3D} plane2
636          * @param {JXG.Plane3D} d
637          * @returns {Array} of length 2 containing the coordinates of the defining points of
638          * of the intersection segment.
639          */
640         intersectionPlanePlane: function (plane1, plane2, d) {
641             var ret = [[], []],
642                 p,
643                 dir,
644                 r,
645                 q;
646 
647             d = d || plane2.d;
648 
649             p = Mat.Geometry.meet3Planes(
650                 plane1.normal,
651                 plane1.d,
652                 plane2.normal,
653                 d,
654                 Mat.crossProduct(plane1.normal, plane2.normal),
655                 0
656             );
657             dir = Mat.Geometry.meetPlanePlane(
658                 plane1.vec1,
659                 plane1.vec2,
660                 plane2.vec1,
661                 plane2.vec2
662             );
663             r = this.intersectionLineCube(p, dir, Infinity);
664             q = Mat.axpy(r, dir, p);
665             if (this.isInCube(q)) {
666                 ret[0] = q;
667             }
668             r = this.intersectionLineCube(p, dir, -Infinity);
669             q = Mat.axpy(r, dir, p);
670             if (this.isInCube(q)) {
671                 ret[1] = q;
672             }
673             return ret;
674         },
675 
676         /**
677          * Generate mesh for a surface / plane.
678          * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
679          * @param {Array|Function} func
680          * @param {Array} interval_u
681          * @param {Array} interval_v
682          * @returns Array
683          * @private
684          *
685          * @example
686          *  var el = view.create('curve', [[], []]);
687          *  el.updateDataArray = function () {
688          *      var steps_u = Type.evaluate(this.visProp.stepsu),
689          *           steps_v = Type.evaluate(this.visProp.stepsv),
690          *           r_u = Type.evaluate(this.range_u),
691          *           r_v = Type.evaluate(this.range_v),
692          *           func, ret;
693          *
694          *      if (this.F !== null) {
695          *          func = this.F;
696          *      } else {
697          *          func = [this.X, this.Y, this.Z];
698          *      }
699          *      ret = this.view.getMesh(func,
700          *          r_u.concat([steps_u]),
701          *          r_v.concat([steps_v]));
702          *
703          *      this.dataX = ret[0];
704          *      this.dataY = ret[1];
705          *  };
706          *
707          */
708         getMesh: function (func, interval_u, interval_v) {
709             var i_u, i_v, u, v,
710                 c2d, delta_u, delta_v,
711                 p = [0, 0, 0],
712                 steps_u = interval_u[2],
713                 steps_v = interval_v[2],
714                 dataX = [],
715                 dataY = [];
716 
717             delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
718             delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
719 
720             for (i_u = 0; i_u <= steps_u; i_u++) {
721                 u = interval_u[0] + delta_u * i_u;
722                 for (i_v = 0; i_v <= steps_v; i_v++) {
723                     v = interval_v[0] + delta_v * i_v;
724                     if (Type.isFunction(func)) {
725                         p = func(u, v);
726                     } else {
727                         p = [func[0](u, v), func[1](u, v), func[2](u, v)];
728                     }
729                     c2d = this.project3DTo2D(p);
730                     dataX.push(c2d[1]);
731                     dataY.push(c2d[2]);
732                 }
733                 dataX.push(NaN);
734                 dataY.push(NaN);
735             }
736 
737             for (i_v = 0; i_v <= steps_v; i_v++) {
738                 v = interval_v[0] + delta_v * i_v;
739                 for (i_u = 0; i_u <= steps_u; i_u++) {
740                     u = interval_u[0] + delta_u * i_u;
741                     if (Type.isFunction(func)) {
742                         p = func(u, v);
743                     } else {
744                         p = [func[0](u, v), func[1](u, v), func[2](u, v)];
745                     }
746                     c2d = this.project3DTo2D(p);
747                     dataX.push(c2d[1]);
748                     dataY.push(c2d[2]);
749                 }
750                 dataX.push(NaN);
751                 dataY.push(NaN);
752             }
753 
754             return [dataX, dataY];
755         },
756 
757         /**
758          *
759          */
760         animateAzimuth: function () {
761             var s = this.az_slide._smin,
762                 e = this.az_slide._smax,
763                 sdiff = e - s,
764                 newVal = this.az_slide.Value() + 0.1;
765 
766             this.az_slide.position = (newVal - s) / sdiff;
767             if (this.az_slide.position > 1) {
768                 this.az_slide.position = 0.0;
769             }
770             this.board.update();
771 
772             this.timeoutAzimuth = setTimeout(function () {
773                 this.animateAzimuth();
774             }.bind(this), 200);
775         },
776 
777         /**
778          *
779          */
780         stopAzimuth: function () {
781             clearTimeout(this.timeoutAzimuth);
782             this.timeoutAzimuth = null;
783         },
784 
785         /**
786          * Check if vertical dragging is enabled and which action is needed.
787          * Default is shiftKey.
788          *
789          * @returns Boolean
790          * @private
791          */
792         isVerticalDrag: function () {
793             var b = this.board,
794                 key;
795             if (!Type.evaluate(this.visProp.verticaldrag.enabled)) {
796                 return false;
797             }
798             key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key';
799             return b[key];
800         },
801 
802         /**
803          * Sets camera view to the given values.
804          *
805          * @param {Number} az Value of azimuth.
806          * @param {Number} el Value of elevation.
807          * @param {Number} [r] Value of radius.
808          *
809          * @returns {Object} Reference to the view.
810          */
811         setView: function (az, el, r) {
812             r = r || this.r;
813 
814             this.az_slide.setValue(az);
815             this.el_slide.setValue(el);
816             this.r = r;
817             this.board.update();
818 
819             return this;
820         },
821 
822         /**
823          * Changes view to the next view stored in the attribute `values`.
824          *
825          * @see View3D#values
826          *
827          * @returns {Object} Reference to the view.
828          */
829         nextView: function () {
830             var views = Type.evaluate(this.visProp.values),
831                 n = this.visProp._currentview;
832 
833             n = (n + 1) % views.length;
834             this.setCurrentView(n);
835 
836             return this;
837         },
838 
839         /**
840          * Changes view to the previous view stored in the attribute `values`.
841          *
842          * @see View3D#values
843          *
844          * @returns {Object} Reference to the view.
845          */
846         previousView: function () {
847             var views = Type.evaluate(this.visProp.values),
848                 n = this.visProp._currentview;
849 
850             n = (n + views.length - 1) % views.length;
851             this.setCurrentView(n);
852 
853             return this;
854         },
855 
856         /**
857          * Changes view to the determined view stored in the attribute `values`.
858          *
859          * @see View3D#values
860          *
861          * @param {Number} n Index of view in attribute `values`.
862          * @returns {Object} Reference to the view.
863          */
864         setCurrentView: function (n) {
865             var views = Type.evaluate(this.visProp.values);
866 
867             if (n < 0 || n >= views.length) {
868                 n = ((n % views.length) + views.length) % views.length;
869             }
870 
871             this.setView(views[n][0], views[n][1], views[n][2]);
872             this.visProp._currentview = n;
873 
874             return this;
875         },
876 
877         /**
878          * Controls the navigation in az direction using either the keyboard or a pointer.
879          *
880          * @private
881          *
882          * @param {event} event either the keydown or the pointer event
883          * @returns view
884          */
885         _azEventHandler: function (event) {
886             var smax = this.az_slide._smax,
887                 smin = this.az_slide._smin,
888                 speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)),
889                 delta = event.movementX,
890                 az = this.az_slide.Value(),
891                 el = this.el_slide.Value();
892 
893             // Doesn't allow navigation if another moving event is triggered
894             if (this.board.mode === this.board.BOARD_MODE_DRAG) {
895                 return this;
896             }
897 
898             // Calculate new az value if keyboard events are triggered
899             // Plus if right-button, minus if left-button
900             if (Type.evaluate(this.visProp.az.keyboard.enabled)) {
901                 if (event.key === 'ArrowRight') {
902                     az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
903                 } else if (event.key === 'ArrowLeft') {
904                     az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
905                 }
906             }
907 
908             if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && event.key == null) {
909                 az += delta * speed;
910             }
911 
912             // Project the calculated az value to a usable value in the interval [smin,smax]
913             // Use modulo if continuous is true
914             if (Type.evaluate(this.visProp.az.continuous)) {
915                 az = (((az % smax) + smax) % smax);
916             } else {
917                 if (az > 0) {
918                     az = Math.min(smax, az);
919                 } else if (az < 0) {
920                     az = Math.max(smin, az);
921                 }
922             }
923 
924             this.setView(az, el);
925             return this;
926         },
927 
928         /**
929          * Controls the navigation in el direction using either the keyboard or a pointer.
930          *
931          * @private
932          *
933          * @param {event} event either the keydown or the pointer event
934          * @returns view
935          */
936         _elEventHandler: function (event) {
937             var smax = this.el_slide._smax,
938                 smin = this.el_slide._smin,
939                 speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed),
940                 delta = event.movementY,
941                 az = this.az_slide.Value(),
942                 el = this.el_slide.Value();
943 
944             // Doesn't allow navigation if another moving event is triggered
945             if (this.board.mode === this.board.BOARD_MODE_DRAG) {
946                 return this;
947             }
948 
949             // Calculate new az value if keyboard events are triggered
950             // Plus if right-button, minus if left-button
951             if (Type.evaluate(this.visProp.el.keyboard.enabled)) {
952                 if (event.key === 'ArrowUp') {
953                     el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
954                 } else if (event.key === 'ArrowDown') {
955                     el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
956                 }
957             }
958 
959             // Calculate new az value if keyboard events are triggered
960             // Plus if right-button, minus if left-button
961             if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && event.key == null) {
962                 el += delta * speed;
963             }
964 
965             // Project the calculated az value to a usable value in the interval [smin,smax]
966             // Use modulo if continuous is true
967             if (Type.evaluate(this.visProp.el.continuous)) {
968                 el = (((el % smax) + smax) % smax);
969             } else {
970                 if (el > 0) {
971                     el = Math.min(smax, el);
972                 } else if (el < 0) {
973                     el = Math.max(smin, el);
974                 }
975             }
976 
977             this.setView(az, el);
978             return this;
979         }
980     });
981 
982 /**
983  * @class This element creates a 3D view.
984  * @pseudo
985  * @description  A View3D element provides the container and the methods to create and display 3D elements.
986  * It is contained in a JSXGraph board.
987  * @name View3D
988  * @augments JXG.View3D
989  * @constructor
990  * @type Object
991  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
992  * @param {Array_Array_Array} lower,dim,cube  Here, lower is an array of the form [x, y] and
993  * dim is an array of the form [w, h].
994  * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is
995  * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner
996  * [x,y] and side lengths [w, h] of the board.
997  * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]]
998  * which determines the coordinate ranges of the 3D cube.
999  *
1000  * @example
1001  *  var bound = [-5, 5];
1002  *  var view = board.create('view3d',
1003  *      [[-6, -3],
1004  *       [8, 8],
1005  *       [bound, bound, bound]],
1006  *      {
1007  *          // Main axes
1008  *          axesPosition: 'center',
1009  *          xAxis: { strokeColor: 'blue', strokeWidth: 3},
1010  *
1011  *          // Planes
1012  *          xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
1013  *          yPlaneFront: { visible: true, fillColor: 'blue'},
1014  *
1015  *          // Axes on planes
1016  *          xPlaneRearYAxis: {strokeColor: 'red'},
1017  *          xPlaneRearZAxis: {strokeColor: 'red'},
1018  *
1019  *          yPlaneFrontXAxis: {strokeColor: 'blue'},
1020  *          yPlaneFrontZAxis: {strokeColor: 'blue'},
1021  *
1022  *          zPlaneFrontXAxis: {visible: false},
1023  *          zPlaneFrontYAxis: {visible: false}
1024  *      });
1025  *
1026  * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div>
1027  * <script type="text/javascript">
1028  *     (function() {
1029  *         var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7',
1030  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
1031  *         var bound = [-5, 5];
1032  *         var view = board.create('view3d',
1033  *             [[-6, -3], [8, 8],
1034  *             [bound, bound, bound]],
1035  *             {
1036  *                 // Main axes
1037  *                 axesPosition: 'center',
1038  *                 xAxis: { strokeColor: 'blue', strokeWidth: 3},
1039  *                 // Planes
1040  *                 xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
1041  *                 yPlaneFront: { visible: true, fillColor: 'blue'},
1042  *                 // Axes on planes
1043  *                 xPlaneRearYAxis: {strokeColor: 'red'},
1044  *                 xPlaneRearZAxis: {strokeColor: 'red'},
1045  *                 yPlaneFrontXAxis: {strokeColor: 'blue'},
1046  *                 yPlaneFrontZAxis: {strokeColor: 'blue'},
1047  *                 zPlaneFrontXAxis: {visible: false},
1048  *                 zPlaneFrontYAxis: {visible: false}
1049  *             });
1050  *     })();
1051  *
1052  * </script><pre>
1053  *
1054  */
1055 JXG.createView3D = function (board, parents, attributes) {
1056     var view, attr, attr_az, attr_el,
1057         x, y, w, h,
1058         coords = parents[0], // llft corner
1059         size = parents[1]; // [w, h]
1060 
1061     attr = Type.copyAttributes(attributes, board.options, 'view3d');
1062     view = new JXG.View3D(board, parents, attr);
1063     view.defaultAxes = view.create('axes3d', parents, attributes);
1064 
1065     x = coords[0];
1066     y = coords[1];
1067     w = size[0];
1068     h = size[1];
1069 
1070     attr_az = Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider');
1071     attr_az.name = 'az';
1072 
1073     attr_el = Type.copyAttributes(attributes, board.options, 'view3d', 'el', 'slider');
1074     attr_el.name = 'el';
1075 
1076     /**
1077      * Slider to adapt azimuth angle
1078      * @name JXG.View3D#az_slide
1079      * @type {Slider}
1080      */
1081     view.az_slide = board.create(
1082         'slider',
1083         [
1084             [x - 1, y - 2],
1085             [x + w + 1, y - 2],
1086             [
1087                 Type.evaluate(attr_az.min),
1088                 Type.evaluate(attr_az.start),
1089                 Type.evaluate(attr_az.max)
1090             ]
1091         ],
1092         attr_az
1093     );
1094 
1095     /**
1096      * Slider to adapt elevation angle
1097      *
1098      * @name JXG.View3D#el_slide
1099      * @type {Slider}
1100      */
1101     view.el_slide = board.create(
1102         'slider',
1103         [
1104             [x - 1, y],
1105             [x - 1, y + h],
1106             [
1107                 Type.evaluate(attr_el.min),
1108                 Type.evaluate(attr_el.start),
1109                 Type.evaluate(attr_el.max)]
1110         ],
1111         attr_el
1112     );
1113 
1114     view.board.highlightInfobox = function (x, y, el) {
1115         var d, i, c3d, foot,
1116             pre = '<span style="color:black; font-size:200%">\u21C4  </span>',
1117             brd = el.board,
1118             arr, infobox,
1119             p = null;
1120 
1121         if (view.isVerticalDrag()) {
1122             pre = '<span style="color:black; font-size:200%">\u21C5  </span>';
1123         }
1124         // Search 3D parent
1125         for (i = 0; i < el.parents.length; i++) {
1126             p = brd.objects[el.parents[i]];
1127             if (p.is3D) {
1128                 break;
1129             }
1130         }
1131         if (p) {
1132             foot = [1, 0, 0, p.coords[3]];
1133             c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot);
1134             if (!view.isInCube(c3d)) {
1135                 view.board.highlightCustomInfobox('', p);
1136                 return;
1137             }
1138             d = Type.evaluate(p.visProp.infoboxdigits);
1139             infobox = view.board.infobox;
1140             if (d === 'auto') {
1141                 if (infobox.useLocale()) {
1142                     arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')'];
1143                 } else {
1144                     arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')'];
1145                 }
1146 
1147             } else {
1148                 if (infobox.useLocale()) {
1149                     arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')'];
1150                 } else {
1151                     arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')'];
1152                 }
1153             }
1154             view.board.highlightCustomInfobox(arr.join(''), p);
1155         } else {
1156             view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
1157         }
1158     };
1159 
1160     // Hack needed to enable addEvent for view3D:
1161     view.BOARD_MODE_NONE = 0x0000;
1162 
1163     // Add events for the keyboard navigation
1164     Env.addEvent(board.containerObj, 'keydown', function (event) {
1165         var neededKey;
1166 
1167         if (Type.evaluate(view.visProp.el.keyboard.enabled) && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
1168             neededKey = Type.evaluate(view.visProp.el.keyboard.key);
1169             if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
1170                 view._elEventHandler(event);
1171             }
1172 
1173         }
1174         if (Type.evaluate(view.visProp.el.keyboard.enabled) && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
1175             neededKey = Type.evaluate(view.visProp.az.keyboard.key);
1176             if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
1177                 view._azEventHandler(event);
1178             }
1179         }
1180         if (event.key === 'PageUp') {
1181             view.nextView();
1182         } else if (event.key === 'PageDown') {
1183             view.previousView();
1184         }
1185 
1186         event.preventDefault();
1187     }, view);
1188 
1189     // Add events for the pointer navigation
1190     board.containerObj.addEventListener('pointerdown', function (event) {
1191         var neededButton, neededKey,
1192             target;
1193 
1194         if (Type.evaluate(view.visProp.az.pointer.enabled)) {
1195             neededButton = Type.evaluate(view.visProp.az.pointer.button);
1196             neededKey = Type.evaluate(view.visProp.az.pointer.key);
1197 
1198             // Events for azimuth
1199             if (
1200                 (neededButton === -1 || neededButton === event.button) &&
1201                 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey))
1202             ) {
1203                 // If outside is true then the event listener is bound to the document, otherwise to the div
1204                 if (Type.evaluate(view.visProp.az.pointer.outside)) {
1205                     target = document;
1206                 } else {
1207                     target = board.containerObj;
1208                 }
1209                 Env.addEvent(target, 'pointermove', view._azEventHandler, view);
1210                 view._hasMoveAz = true;
1211             }
1212         }
1213 
1214         if (Type.evaluate(view.visProp.el.pointer.enabled)) {
1215             neededButton = Type.evaluate(view.visProp.el.pointer.button);
1216             neededKey = Type.evaluate(view.visProp.el.pointer.key);
1217 
1218             // Events for elevation
1219             if (
1220                 (neededButton === -1 || neededButton === event.button) &&
1221                 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey))
1222             ) {
1223                 // If outside is true then the event listener is bound to the document, otherwise to the div
1224                 if (Type.evaluate(view.visProp.el.pointer.outside)) {
1225                     target = document;
1226                 } else {
1227                     target = board.containerObj;
1228                 }
1229                 Env.addEvent(target, 'pointermove', view._elEventHandler, view);
1230                 view._hasMoveEl = true;
1231             }
1232         }
1233 
1234         // Remove pointerMove and pointerUp event listener as soon as pointer up is triggered
1235         function handlePointerUp() {
1236             var target;
1237             if (view._hasMoveAz) {
1238                 if (Type.evaluate(view.visProp.az.pointer.outside)) {
1239                     target = document;
1240                 } else {
1241                     target = view.board.containerObj;
1242                 }
1243                 Env.removeEvent(target, 'pointermove', view._azEventHandler, view);
1244                 view._hasMoveAz = false;
1245             }
1246             if (view._hasMoveEl) {
1247                 if (Type.evaluate(view.visProp.el.pointer.outside)) {
1248                     target = document;
1249                 } else {
1250                     target = view.board.containerObj;
1251                 }
1252                 Env.removeEvent(target, 'pointermove', view._elEventHandler, view);
1253                 view._hasMoveEl = false;
1254             }
1255             Env.removeEvent(document, 'pointerup', handlePointerUp, view);
1256         }
1257 
1258         Env.addEvent(document, 'pointerup', handlePointerUp, view);
1259     });
1260 
1261     view.board.update();
1262 
1263     return view;
1264 };
1265 
1266 JXG.registerElement("view3d", JXG.createView3D);
1267 
1268 export default JXG.View3D;