1 /*
  2     Copyright 2008-2024
  3         Matthias Ehmann,
  4         Carsten Miller,
  5         Andreas Walter,
  6         Alfred Wassermann
  7 
  8     This file is part of JSXGraph.
  9 
 10     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 11 
 12     You can redistribute it and/or modify it under the terms of the
 13 
 14       * GNU Lesser General Public License as published by
 15         the Free Software Foundation, either version 3 of the License, or
 16         (at your option) any later version
 17       OR
 18       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 19 
 20     JSXGraph is distributed in the hope that it will be useful,
 21     but WITHOUT ANY WARRANTY; without even the implied warranty of
 22     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 23     GNU Lesser General Public License for more details.
 24 
 25     You should have received a copy of the GNU Lesser General Public License and
 26     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 27     and <https://opensource.org/licenses/MIT/>.
 28  */
 29 /*global JXG:true, define: true*/
 30 
 31 import JXG from "../jxg.js";
 32 import Const from "../base/constants.js";
 33 import Geometry from "../math/geometry.js";
 34 import Type from "../utils/type.js";
 35 import Mat from "../math/math.js";
 36 
 37 /**
 38  * Constructor for 3D curves.
 39  * @class Creates a new 3D curve object. Do not use this constructor to create a 3D curve. Use {@link JXG.View3D#create} with type {@link Curve3D} instead.
 40  *
 41  * @augments JXG.GeometryElement3D
 42  * @augments JXG.GeometryElement
 43  * @param {View3D} view
 44  * @param {Function} F
 45  * @param {Function} X
 46  * @param {Function} Y
 47  * @param {Function} Z
 48  * @param {Array} range
 49  * @param {Object} attributes
 50  * @see JXG.Board#generateName
 51  */
 52 JXG.Curve3D = function (view, F, X, Y, Z, range, attributes) {
 53     this.constructor(view.board, attributes, Const.OBJECT_TYPE_CURVE3D, Const.OBJECT_CLASS_3D);
 54     this.constructor3D(view, "curve3d");
 55 
 56     this.board.finalizeAdding(this);
 57 
 58     /**
 59      * @function
 60      * @ignore
 61      */
 62     this.F = F;
 63 
 64     /**
 65      * Function which maps u to x; i.e. it defines the x-coordinate of the curve
 66      * @function
 67      * @returns Number
 68      */
 69     this.X = X;
 70 
 71     /**
 72      * Function which maps u to y; i.e. it defines the y-coordinate of the curve
 73      * @function
 74      * @returns Number
 75      */
 76     this.Y = Y;
 77 
 78     /**
 79      * Function which maps u to z; i.e. it defines the x-coordinate of the curve
 80      * @function
 81      * @returns Number
 82      */
 83     this.Z = Z;
 84 
 85     this.dataX = null;
 86     this.dataY = null;
 87     this.dataZ = null;
 88 
 89     if (this.F !== null) {
 90         this.X = function (u) {
 91             return this.F(u)[0];
 92         };
 93         this.Y = function (u) {
 94             return this.F(u)[1];
 95         };
 96         this.Z = function (u) {
 97             return this.F(u)[2];
 98         };
 99     }
100 
101     this.range = range;
102 
103     this.methodMap = Type.deepCopy(this.methodMap, {
104         // TODO
105     });
106 };
107 JXG.Curve3D.prototype = new JXG.GeometryElement();
108 Type.copyPrototypeMethods(JXG.Curve3D, JXG.GeometryElement3D, "constructor3D");
109 
110 JXG.extend(
111     JXG.Curve3D.prototype,
112     /** @lends JXG.Curve3D.prototype */ {
113         updateDataArray2D: function () {
114             var steps = Type.evaluate(this.visProp.numberpointshigh),
115                 r, s, e, delta, c2d, u, dataX, dataY,
116                 i,
117                 p = [0, 0, 0];
118 
119             dataX = [];
120             dataY = [];
121             if (Type.exists(this.dataX)) {
122                 steps = this.dataX.length;
123                 for (u = 0; u < steps; u++) {
124                     p = [this.dataX[u], this.dataY[u], this.dataZ[u]];
125                     c2d = this.view.project3DTo2D(p);
126                     dataX.push(c2d[1]);
127                     dataY.push(c2d[2]);
128                 }
129             } else if (Type.isArray(this.X)) {
130                 steps = this.X.length;
131                 for (u = 0; u < steps; u++) {
132                     p = [this.X[u], this.Y[u], this.Z[u]];
133                     c2d = this.view.project3DTo2D(p);
134                     dataX.push(c2d[1]);
135                     dataY.push(c2d[2]);
136                 }
137             } else {
138                 r = Type.evaluate(this.range);
139                 s = Type.evaluate(r[0]);
140                 e = Type.evaluate(r[1]);
141                 delta = (e - s) / (steps - 1);
142                 for (i = 0, u = s; i < steps && u <= e; i++, u += delta) {
143                     if (this.F !== null) {
144                         p = this.F(u);
145                     } else {
146                         p = [this.X(u), this.Y(u), this.Z(u)];
147                     }
148                     c2d = this.view.project3DTo2D(p);
149                     dataX.push(c2d[1]);
150                     dataY.push(c2d[2]);
151                 }
152             }
153             return { X: dataX, Y: dataY };
154         },
155 
156         updateDataArray: function() {
157         },
158 
159         update: function () {
160             // if (this.needsUpdate) {
161                 this.updateDataArray();
162             // }
163             return this;
164         },
165 
166         updateRenderer: function () {
167             this.needsUpdate = false;
168             return this;
169         },
170 
171         initParamsIfNeeded: function (params) {
172             if (params.length === 0) {
173                 params.unshift(0.5*(this.range[0] + this.range[1]));
174             }
175         },
176 
177         projectCoords: function (p, params) {
178             this.initParamsIfNeeded(params);
179             return Geometry.projectCoordsToParametric(p, this, params);
180         },
181 
182         projectScreenCoords: function (pScr, params) {
183             this.initParamsIfNeeded(params);
184             return Geometry.projectScreenCoordsToParametric(pScr, this, params);
185         }
186     }
187 );
188 
189 /**
190  * @class This element creates a 3D parametric curve.
191  * @pseudo
192  * @description A 3D parametric curve is defined by a function
193  *    <i>F: R<sup>1</sup> → R<sup>3</sup></i>.
194  *
195  * @name Curve3D
196  * @augments Curve
197  * @constructor
198  * @type Object
199  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
200  * @param {Function_Function_Function_Array,Function} F<sub>X</sub>,F<sub>Y</sub>,F<sub>Z</sub>,range
201  * F<sub>X</sub>(u), F<sub>Y</sub>(u), F<sub>Z</sub>(u) are functions returning a number, range is the array containing
202  * lower and upper bound for the range of the parameter u. range may also be a function returning an array of length two.
203  * @param {Function_Array,Function} F,range Alternatively: F<sub>[X,Y,Z]</sub>(u) a function returning an array [x,y,z] of
204  * numbers, range as above.
205  * @param {Array_Array_Array} X,Y,Z Three arrays containing the coordinate points which define the curve.
206  * @example
207  * // create a simple curve in 3d
208  * var bound = [-1.5, 1.5];
209  * var view=board.create('view3d',
210  *     [[-4, -4],[8, 8],
211  *     [bound, bound, bound]],
212  *     {});
213  * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]);
214  * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div>
215  * <script type="text/javascript">
216  *     (function() {
217  *         var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3',
218  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
219  *         // create a simple curve in 3d
220  *         var bound = [-1.5, 1.5];
221  *         var view=board.create('view3d',
222  *             [[-4, -4],[8, 8],
223  *             [bound, bound, bound]],
224  *             {});
225  *         var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]);
226  *     })();
227  * </script><pre>
228   */
229 JXG.createCurve3D = function (board, parents, attributes) {
230     var view = parents[0],
231         F, X, Y, Z, range, attr, el;
232 
233     if (parents.length === 3) {
234         F = parents[1];
235         range = parents[2];
236         X = null;
237         Y = null;
238         Z = null;
239     } else {
240         X = parents[1];
241         Y = parents[2];
242         Z = parents[3];
243         range = parents[4];
244         F = null;
245     }
246     // TODO Throw error
247 
248     attr = Type.copyAttributes(attributes, board.options, "curve3d");
249     el = new JXG.Curve3D(view, F, X, Y, Z, range, attr);
250 
251     attr = el.setAttr2D(attr);
252     el.element2D = view.create("curve", [[], []], attr);
253     el.element2D.view = view;
254 
255     /**
256      * @class
257      * @ignore
258      */
259     el.element2D.updateDataArray = function () {
260         var ret = el.updateDataArray2D();
261         this.dataX = ret.X;
262         this.dataY = ret.Y;
263     };
264     el.addChild(el.element2D);
265     el.inherits.push(el.element2D);
266     el.element2D.setParents(el);
267 
268     el.element2D.prepareUpdate().update();
269     if (!board.isSuspendedUpdate) {
270         el.element2D.updateVisibility().updateRenderer();
271     }
272 
273     return el;
274 };
275 
276 JXG.registerElement("curve3d", JXG.createCurve3D);
277 
278 /**
279  * @class 3D vector field.
280  * <p>
281  * Plot a vector field either given by three functions
282  * f1(x, y, z), f2(x, y, z), and f3(x, y, z) or by a function f(x, y, z)
283  * returning an array of size 3.
284  *
285  * @pseudo
286  * @name Vectorfield3D
287  * @augments JXG.Curve3D
288  * @constructor
289  * @type JXG.Curve3D
290  * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
291  * Parameter options:
292  * @param {Array|Function|String} F Either an array containing three functions f1(x, y, z), f2(x, y, z),
293  * and f3(x, y) or function f(x, y, z) returning an array of length 3.
294  * @param {Array} xData Array of length 3 containing start value for x, number of steps,
295  * end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x.
296  * @param {Array} yData Array of length 3 containing start value for y, number of steps,
297  * end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y.
298  * @param {Array} zData Array of length 3 containing start value for z, number of steps,
299  * end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z.
300  *
301  * @example
302  * const view = board.create('view3d',
303  *     [
304  *         [-6, -3],
305  *         [8, 8],
306  *         [[-3, 3], [-3, 3], [-3, 3]]
307  *     ], {});
308  *
309  * var vf = view.create('vectorfield3d', [
310  *     [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z],
311  *     [-2, 5, 2], // x from -2 to 2 in 5 steps
312  *     [-2, 5, 2], // y
313  *     [-2, 5, 2] // z
314  * ], {
315  *     strokeColor: 'red',
316  *     scale: 0.5
317  * });
318  *
319  * </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div>
320  * <script type="text/javascript">
321  *     (function() {
322  *         var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348',
323  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false,
324  *          pan: {
325  *                needTwoFingers: true
326  *           }
327  *          });
328  *     const view = board.create('view3d',
329  *         [
330  *             [-6, -3],
331  *             [8, 8],
332  *             [[-3, 3], [-3, 3], [-3, 3]]
333  *         ], {});
334  *     var vf = view.create('vectorfield3d', [
335  *         [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z],
336  *         [-2, 5, 2], // x from -2 to 2 in 5 steps
337  *         [-2, 5, 2], // y
338  *         [-2, 5, 2] // z
339  *     ], {
340  *         strokeColor: 'red',
341  *         scale: 0.5
342  *     });
343  *
344  *
345  *     })();
346  *
347  * </script><pre>
348  *
349  */
350 JXG.createVectorfield3D = function (board, parents, attributes) {
351     var view = parents[0],
352         el, attr;
353 
354     if (!(parents.length >= 5 &&
355         (Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) &&
356         (Type.isArray(parents[2]) && parents[1].length === 3) &&
357         (Type.isArray(parents[3]) && parents[2].length === 3) &&
358         (Type.isArray(parents[4]) && parents[3].length === 3)
359     )) {
360         throw new Error(
361             "JSXGraph: Can't create vector field 3D with parent types " +
362             "'" + typeof parents[1] + "', " +
363             "'" + typeof parents[2] + "', " +
364             "'" + typeof parents[3] + "'."  +
365             "'" + typeof parents[4] + "', "
366         );
367     }
368 
369     attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d');
370     el = view.create('curve3d', [[], [], []], attr);
371 
372     /**
373      * Set the defining functions of 3D vector field.
374      * @memberOf Vectorfield3D
375      * @name setF
376      * @function
377      * @param {Array|Function} func Either an array containing three functions f1(x, y, z),
378      * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3.
379      * @returns {Object} Reference to the 3D vector field object.
380      *
381      * @example
382      * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]);
383      * board.update();
384      *
385      */
386     el.setF = function (func, varnames) {
387         var f0, f1, f2;
388         if (Type.isArray(func)) {
389             f0 = Type.createFunction(func[0], this.board, varnames);
390             f1 = Type.createFunction(func[1], this.board, varnames);
391             f2 = Type.createFunction(func[2], this.board, varnames);
392             /**
393              * @ignore
394              */
395             this.F = function (x, y, z) {
396                 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)];
397             };
398         } else {
399             this.F = Type.createFunction(func, el.board, varnames);
400         }
401         return this;
402     };
403 
404     el.setF(parents[1], 'x, y, z');
405     el.xData = parents[2];
406     el.yData = parents[3];
407     el.zData = parents[4];
408 
409     el.updateDataArray = function () {
410         var k, i, j,
411             v, nrm,
412             x, y, z,
413             scale = Type.evaluate(this.visProp.scale),
414             start = [
415                 Type.evaluate(this.xData[0]),
416                 Type.evaluate(this.yData[0]),
417                 Type.evaluate(this.zData[0])
418             ],
419             steps = [
420                 Type.evaluate(this.xData[1]),
421                 Type.evaluate(this.yData[1]),
422                 Type.evaluate(this.zData[1])
423             ],
424             end = [
425                 Type.evaluate(this.xData[2]),
426                 Type.evaluate(this.yData[2]),
427                 Type.evaluate(this.zData[2])
428             ],
429             delta = [
430                 (end[0] - start[0]) / steps[0],
431                 (end[1] - start[1]) / steps[1],
432                 (end[2] - start[2]) / steps[2]
433             ],
434             phi, theta1, theta2, theta,
435             showArrow = Type.evaluate(this.visProp.arrowhead.enabled),
436             leg, leg_x, leg_y, leg_z, alpha;
437 
438         if (showArrow) {
439             // Arrow head style
440             // leg = 8;
441             // alpha = Math.PI * 0.125;
442             leg = Type.evaluate(this.visProp.arrowhead.size);
443             alpha = Type.evaluate(this.visProp.arrowhead.angle);
444             leg_x = leg / board.unitX;
445             leg_y = leg / board.unitY;
446             leg_z = leg / Math.sqrt(board.unitX * board.unitY);
447         }
448 
449         this.dataX = [];
450         this.dataY = [];
451         this.dataZ = [];
452         for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) {
453             for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) {
454                 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) {
455                     v = this.F(x, y, z);
456                     nrm = Mat.norm(v);
457                     if (nrm < Number.EPSILON) {
458                         continue;
459                     }
460 
461                     v[0] *= scale;
462                     v[1] *= scale;
463                     v[2] *= scale;
464                     Type.concat(this.dataX, [x, x + v[0], NaN]);
465                     Type.concat(this.dataY, [y, y + v[1], NaN]);
466                     Type.concat(this.dataZ, [z, z + v[2], NaN]);
467 
468                     if (showArrow) {
469                         // Arrow head
470                         nrm *= scale;
471                         phi = Math.atan2(v[1], v[0]);
472                         theta = Math.asin(v[2] / nrm);
473                         theta1 = theta - alpha;
474                         theta2 = theta + alpha;
475                         Type.concat(this.dataX, [
476                             x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1),
477                             x + v[0],
478                             x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2),
479                             NaN]);
480                         Type.concat(this.dataY, [
481                             y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1),
482                             y + v[1],
483                             y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2),
484                             NaN]);
485                         Type.concat(this.dataZ, [
486                             z + v[2] - leg_z * Math.sin(theta2),
487                             z + v[2],
488                             z + v[2] - leg_z * Math.sin(theta1),
489                             NaN]);
490                     }
491                 }
492             }
493         }
494     };
495 
496     el.methodMap = Type.deepCopy(el.methodMap, {
497         setF: "setF"
498     });
499 
500     return el;
501 };
502 
503 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D);
504