1 /*
  2     Copyright 2008-2022
  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 <http://www.gnu.org/licenses/>
 27     and <http://opensource.org/licenses/MIT/>.
 28  */
 29 /*global JXG:true, define: true*/
 30 
 31 define(['jxg', 'options', 'base/constants', 'utils/type', 'math/math', 'base/element', '3d/threed',
 32 ], function (JXG, Options, Const, Type, Mat, GeometryElement, ThreeD) {
 33     "use strict";
 34 
 35     ThreeD.View3D = function (board, parents, attributes) {
 36         var bbox3d, coords, size;
 37         this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_CURVE);
 38 
 39         bbox3d = parents[2];  // [[x1, x2], [y1,y2], [z1,z2]]
 40         coords = parents[0]; // llft corner
 41         size = parents[1];   // [w, h]
 42 
 43         /**
 44          * "Namespace" for all 3D handling
 45          */
 46         this.D3 = {};
 47 
 48         /**
 49          * An associative array containing all geometric objects belonging to the view.
 50          * Key is the id of the object and value is a reference to the object.
 51          * @type Object
 52          */
 53         this.D3.objects = {};
 54 
 55         /**
 56          * An array containing all geometric objects in this view in the order of construction.
 57          * @type Array
 58          */
 59         this.D3.objectsList = [];
 60 
 61         /**
 62          * @type {Object} contains the axes of the view or null
 63          * @default null
 64          */
 65         this.D3.defaultAxes = null;
 66 
 67         /**
 68          * 3D-to-2D transformation matrix
 69          * @type  {Array} 3 x 4 mattrix
 70          */
 71         this.D3.matrix = [
 72             [1, 0, 0, 0],
 73             [0, 1, 0, 0],
 74             [0, 0, 1, 0]
 75         ];
 76 
 77         // Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]]:
 78         this.D3.bbox3d = bbox3d;
 79         this.D3.coords = coords;
 80         this.D3.size = size;
 81 
 82         /**
 83          * Distance of the view to the origin. In other words, its
 84          * the radius of the sphere where the camera sits.
 85          */
 86         this.D3.r = -1;
 87 
 88         this.timeoutAzimuth = null;
 89 
 90         this.id = this.board.setId(this, 'V');
 91         this.board.finalizeAdding(this);
 92         this.elType = 'view3d';
 93         this.methodMap = Type.deepCopy(this.methodMap, {
 94         });
 95     };
 96     ThreeD.View3D.prototype = new GeometryElement();
 97 
 98     JXG.extend(ThreeD.View3D.prototype, /** @lends ThreeD.View3D.prototype */ {
 99         create: function (elementType, parents, attributes) {
100             var prefix = [],
101                 is3D = false,
102                 el;
103 
104             if (elementType.indexOf('3d') > 0) {
105                 is3D = true;
106                 prefix.push(this);
107             }
108             el = this.board.create(elementType, prefix.concat(parents), attributes);
109             if (true || is3D) {
110                 this.add(el);
111             }
112             return el;
113         },
114 
115         add: function (el) {
116             this.D3.objects[el.id] = el;
117             this.D3.objectsList.push(el);
118         },
119 
120         /**
121          * Update 3D-to-2D transformation matrix with the actual
122          * elevation and azimuth angles.
123          *
124          * @private
125          */
126         update: function () {
127             var D3 = this.D3,
128                 e, r, a, f, mat;
129 
130             if (!Type.exists(D3.el_slide) ||
131                 !Type.exists(D3.az_slide) ||
132                 !this.needsUpdate) {
133                 return this;
134             }
135 
136             e = D3.el_slide.Value();
137             r = D3.r;
138             a = D3.az_slide.Value();
139             f = r * Math.sin(e);
140             mat = [[1, 0, 0,], [0, 1, 0], [0, 0, 1]];
141 
142             D3.matrix = [
143                 [1, 0, 0, 0],
144                 [0, 1, 0, 0],
145                 [0, 0, 1, 0]
146             ];
147 
148             D3.matrix[1][1] = r * Math.cos(a);
149             D3.matrix[1][2] = -r * Math.sin(a);
150             D3.matrix[2][1] = f * Math.sin(a);
151             D3.matrix[2][2] = f * Math.cos(a);
152             D3.matrix[2][3] = Math.cos(e);
153 
154             if (true) {
155                 mat[1][1] = D3.size[0] / (D3.bbox3d[0][1] - D3.bbox3d[0][0]); // w / d_x
156                 mat[2][2] = D3.size[1] / (D3.bbox3d[1][1] - D3.bbox3d[1][0]); // h / d_y
157                 mat[1][0] = D3.coords[0] - mat[1][1] * D3.bbox3d[0][0];     // llft_x
158                 mat[2][0] = D3.coords[1] - mat[2][2] * D3.bbox3d[1][0];     // llft_y
159 
160                 D3.matrix = Mat.matMatMult(mat, D3.matrix);
161             }
162 
163             return this;
164         },
165 
166         updateRenderer: function () {
167             this.needsUpdate = false;
168             return this;
169         },
170 
171         /**
172          * Project 3D coordinates to 2D board coordinates
173          * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
174          *
175          * @param  {Number|Array} x
176          * @param  {[Number]} y
177          * @param  {[Number]} z
178          * @returns {Array} Array of length 3 containing the projection on to the board
179          * in homogeneous user coordinates.
180          */
181         project3DTo2D: function (x, y, z) {
182             var vec;
183             if (arguments.length === 3) {
184                 vec = [1, x, y, z];
185             } else {
186                 // Argument is an array
187                 if (x.length === 3) {
188                     vec = [1].concat(x);
189                 } else {
190                     vec = x;
191                 }
192             }
193             return Mat.matVecMult(this.D3.matrix, vec);
194         },
195 
196         /**
197          * Project a 2D coordinate to the plane through the origin
198          * defined by its normal vector `normal`.
199          *
200          * @param  {JXG.Point} point
201          * @param  {Array} normal
202          * @returns Array of length 4 containing the projected
203          * point in homogeneous coordinates.
204          */
205         project2DTo3DPlane: function (point, normal, foot) {
206             var mat, rhs, d, le,
207                 n = normal.slice(1),
208                 sol = [1, 0, 0, 0];
209 
210             foot = foot || [1, 0, 0, 0];
211             le = Mat.norm(n, 3);
212             d = Mat.innerProduct(foot.slice(1), n, 3) / le;
213 
214             mat = this.D3.matrix.slice(0, 3); // True copy
215             mat.push([0].concat(n));
216 
217             // 2D coordinates of point:
218             rhs = point.coords.usrCoords.concat([d]);
219             try {
220                 // Prevent singularity in case elevation angle is zero
221                 if (mat[2][3] === 1.0) {
222                     mat[2][1] = mat[2][2] = Mat.eps * 0.001;
223                 }
224                 sol = Mat.Numerics.Gauss(mat, rhs);
225             } catch (err) {
226                 sol = [0, NaN, NaN, NaN];
227             }
228 
229             return sol;
230         },
231 
232         project3DToCube: function (c3d) {
233             var cube = this.D3.bbox3d;
234             if (c3d[1] < cube[0][0]) { c3d[1] = cube[0][0]; }
235             if (c3d[1] > cube[0][1]) { c3d[1] = cube[0][1]; }
236             if (c3d[2] < cube[1][0]) { c3d[2] = cube[1][0]; }
237             if (c3d[2] > cube[1][1]) { c3d[2] = cube[1][1]; }
238             if (c3d[3] < cube[2][0]) { c3d[3] = cube[2][0]; }
239             if (c3d[3] > cube[2][1]) { c3d[3] = cube[2][1]; }
240 
241             return c3d;
242         },
243 
244         intersectionLineCube: function (p, d, r) {
245             var rnew, i, r0, r1;
246 
247             rnew = r;
248             for (i = 0; i < 3; i++) {
249                 if (d[i] !== 0) {
250                     r0 = (this.D3.bbox3d[i][0] - p[i]) / d[i];
251                     r1 = (this.D3.bbox3d[i][1] - p[i]) / d[i];
252                     if (r < 0) {
253                         rnew = Math.max(rnew, Math.min(r0, r1));
254                     } else {
255                         rnew = Math.min(rnew, Math.max(r0, r1));
256                     }
257                 }
258             }
259             return rnew;
260         },
261 
262         isInCube: function (q) {
263             return q[0] > this.D3.bbox3d[0][0] - Mat.eps && q[0] < this.D3.bbox3d[0][1] + Mat.eps &&
264                 q[1] > this.D3.bbox3d[1][0] - Mat.eps && q[1] < this.D3.bbox3d[1][1] + Mat.eps &&
265                 q[2] > this.D3.bbox3d[2][0] - Mat.eps && q[2] < this.D3.bbox3d[2][1] + Mat.eps;
266         },
267 
268         /**
269          *
270          * @param {*} plane1
271          * @param {*} plane2
272          * @param {*} d
273          * @returns Array of length 2 containing the coordinates of the defining points of
274          * of the intersection segment.
275          */
276         intersectionPlanePlane: function(plane1, plane2, d) {
277             var ret = [[], []],
278                 p, dir, r, q;
279 
280             d = d || plane2.D3.d;
281 
282             p = Mat.Geometry.meet3Planes(plane1.D3.normal, plane1.D3.d, plane2.D3.normal, d,
283                      Mat.crossProduct(plane1.D3.normal, plane2.D3.normal), 0);
284             dir = Mat.Geometry.meetPlanePlane(plane1.D3.dir1, plane1.D3.dir2, plane2.D3.dir1, plane2.D3.dir2);
285             r = this.intersectionLineCube(p, dir, Infinity);
286             q = Mat.axpy(r, dir, p);
287             if (this.isInCube(q)) {
288                 ret[0] = q;
289             }
290             r = this.intersectionLineCube(p, dir, -Infinity);
291             q = Mat.axpy(r, dir, p);
292             if (this.isInCube(q) ) {
293                 ret[1] = q;
294             }
295             return ret;
296         },
297 
298         getMesh: function (X, Y, Z, interval_u, interval_v) {
299             var i_u, i_v, u, v, c2d,
300                 delta_u, delta_v,
301                 p = [0, 0, 0],
302                 steps_u = interval_u[2],
303                 steps_v = interval_v[2],
304 
305                 dataX = [],
306                 dataY = [];
307 
308             delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / (steps_u);
309             delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / (steps_v);
310 
311             for (i_u = 0; i_u <= steps_u; i_u++) {
312                 u = interval_u[0] + delta_u * i_u;
313                 for (i_v = 0; i_v <= steps_v; i_v++) {
314                     v = interval_v[0] + delta_v * i_v;
315                     p[0] = X(u, v);
316                     p[1] = Y(u, v);
317                     p[2] = Z(u, v);
318                     c2d = this.project3DTo2D(p);
319                     dataX.push(c2d[1]);
320                     dataY.push(c2d[2]);
321                 }
322                 dataX.push(NaN);
323                 dataY.push(NaN);
324             }
325 
326             for (i_v = 0; i_v <= steps_v; i_v++) {
327                 v = interval_v[0] + delta_v * i_v;
328                 for (i_u = 0; i_u <= steps_u; i_u++) {
329                     u = interval_u[0] + delta_u * i_u;
330                     p[0] = X(u, v);
331                     p[1] = Y(u, v);
332                     p[2] = Z(u, v);
333                     c2d = this.project3DTo2D(p);
334                     dataX.push(c2d[1]);
335                     dataY.push(c2d[2]);
336                 }
337                 dataX.push(NaN);
338                 dataY.push(NaN);
339             }
340 
341             return [dataX, dataY];
342         },
343 
344         animateAzimuth: function () {
345             var s = this.D3.az_slide._smin,
346                 e = this.D3.az_slide._smax,
347                 sdiff = e - s,
348                 newVal = this.D3.az_slide.Value() + 0.1;
349 
350             this.D3.az_slide.position = ((newVal - s) / sdiff);
351             if (this.D3.az_slide.position > 1) {
352                 this.D3.az_slide.position = 0.0;
353             }
354             this.board.update();
355 
356             this.timeoutAzimuth = setTimeout(function () { this.animateAzimuth(); }.bind(this), 200);
357         },
358 
359         stopAzimuth: function () {
360             clearTimeout(this.timeoutAzimuth);
361             this.timeoutAzimuth = null;
362         }
363     });
364 
365     /**
366      * @class This element creates a 3D view.
367      * @pseudo
368      * @description  A View3D element provides the container and the methods to create and display 3D elements.
369      * It is contained in a JSXGraph board.
370      * @name View3D
371      * @augments JXG.View3D
372      * @constructor
373      * @type Object
374      * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
375      * @param {Array_Array_Array} lower,dim,cube  Here, lower is an array of the form [x, y] and
376      * dim is an array of the form [w, h].
377      * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is
378      * (roughly) projected.
379      * cube is an array of the form [[x1, x2], [y1, y2], [z1, z2]]
380      * which determines the coordinate ranges of the 3D cube.
381      *
382      * @example
383      *  var bound = [-5, 5];
384      *  var view = board.create('view3d',
385      *      [[-6, -3],
386      *       [8, 8],
387      *       [bound, bound, bound]],
388      *      {
389      *          // Main axes
390      *          axesPosition: 'center',
391      *          xAxis: { strokeColor: 'blue', strokeWidth: 3},
392      *
393      *          // Planes
394      *          xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
395      *          yPlaneFront: { visible: true, fillColor: 'blue'},
396      *
397      *          // Axes on planes
398      *          xPlaneRearYAxis: {strokeColor: 'red'},
399      *          xPlaneRearZAxis: {strokeColor: 'red'},
400      *
401      *          yPlaneFrontXAxis: {strokeColor: 'blue'},
402      *          yPlaneFrontZAxis: {strokeColor: 'blue'},
403      *
404      *          zPlaneFrontXAxis: {visible: false},
405      *          zPlaneFrontYAxis: {visible: false}
406      *      });
407      *
408      * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div>
409      * <script type="text/javascript">
410      *     (function() {
411      *         var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7',
412      *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
413      *                             var bound = [-5, 5];
414      *                             var view = board.create('view3d',
415      *                                 [[-6, -3], [8, 8],
416      *                                 [bound, bound, bound]],
417      *                                 {
418      *                                     // Main axes
419      *                                     axesPosition: 'center',
420      *                                     xAxis: { strokeColor: 'blue', strokeWidth: 3},
421      *
422      *                                     // Planes
423      *                                     xPlaneRear: { fillColor: 'yellow',  mesh3d: {visible: false}},
424      *                                     yPlaneFront: { visible: true, fillColor: 'blue'},
425      *
426      *                                     // Axes on planes
427      *                                     xPlaneRearYAxis: {strokeColor: 'red'},
428      *                                     xPlaneRearZAxis: {strokeColor: 'red'},
429      *
430      *                                     yPlaneFrontXAxis: {strokeColor: 'blue'},
431      *                                     yPlaneFrontZAxis: {strokeColor: 'blue'},
432      *
433      *                                     zPlaneFrontXAxis: {visible: false},
434      *                                     zPlaneFrontYAxis: {visible: false}
435      *                                 });
436      *
437      *     })();
438      *
439      * </script><pre>
440      *
441      */
442     ThreeD.createView3D = function (board, parents, attributes) {
443         var view, frame, attr,
444             x, y, w, h,
445             coords = parents[0], // llft corner
446             size = parents[1];   // [w, h]
447 
448         attr = Type.copyAttributes(attributes, board.options, 'view3d');
449         view = new ThreeD.View3D(board, parents, attr);
450         view.defaultAxes = view.create('axes3d', parents, attributes);
451 
452         x = coords[0];
453         y = coords[1];
454         w = size[0];
455         h = size[1];
456 
457         /*
458          * Frame around the view object
459          */
460         if (false) {
461             frame = board.create('polygon', [
462                 [coords[0], coords[1] + size[1]],           // ulft
463                 [coords[0], coords[1]],                     // llft
464                 [coords[0] + size[0], coords[1]],           // lrt
465                 [coords[0] + size[0], coords[1] + size[1]], // urt
466             ], {
467                 fillColor: 'none',
468                 highlightFillColor: 'none',
469                 highlight: false,
470                 vertices: {
471                     fixed: true,
472                     visible: false
473                 },
474                 borders: {
475                     strokeColor: 'black',
476                     highlight: false,
477                     strokeWidth: 0.5,
478                     dash: 4
479                 }
480             });
481             //view.add(frame);
482         }
483 
484         /**
485          * Slider to adapt azimuth angle
486          */
487         view.D3.az_slide = board.create('slider', [[x - 1, y - 2], [x + w + 1, y - 2], [0, 1.0, 2 * Math.PI]], {
488             style: 6, name: 'az',
489             point1: { frozen: true },
490             point2: { frozen: true }
491         });
492 
493         /**
494          * Slider to adapt elevation angle
495          */
496         view.D3.el_slide = board.create('slider', [[x - 1, y], [x - 1, y + h], [0, 0.30, Math.PI / 2]], {
497             style: 6, name: 'el',
498             point1: { frozen: true },
499             point2: { frozen: true }
500         });
501 
502         view.board.highlightInfobox = function (x, y, el) {
503             var d;
504 
505             if (Type.exists(el.D3)) {
506                 d = Type.evaluate(el.visProp.infoboxdigits);
507                 if (d === 'auto') {
508                     view.board.highlightCustomInfobox('(' +
509                         Type.autoDigits(el.D3.X()) + ' | ' +
510                         Type.autoDigits(el.D3.Y()) + ' | ' +
511                         Type.autoDigits(el.D3.Z()) + ')', el);
512                 } else {
513                     view.board.highlightCustomInfobox('(' +
514                         Type.toFixed(el.D3.X(), d) + ' | ' +
515                         Type.toFixed(el.D3.Y(), d) + ' | ' +
516                         Type.toFixed(el.D3.Z(), d) + ')', el);
517                 }
518             } else {
519                 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el);
520             }
521         };
522 
523         return view;
524     };
525     JXG.registerElement('view3d', ThreeD.createView3D);
526 
527     return ThreeD.View3D;
528 });
529 
530