1 /*
  2     Copyright 2008-2026
  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      * Internal function defining the surface without applying any transformations.
 60      * Does only exist if it or X are supplied as a function. Otherwise it is null.
 61      *
 62      * @function
 63      * @private
 64      */
 65     this._F = F;
 66 
 67     /**
 68      * Function or array which maps u to x; i.e. it defines the x-coordinate of the curve
 69      * @function
 70      * @returns Number
 71      * @private
 72      */
 73     this._X = X;
 74 
 75     /**
 76      * Function or array  which maps u to y; i.e. it defines the y-coordinate of the curve
 77      * @function
 78      * @returns Number
 79      * @private
 80      */
 81     this._Y = Y;
 82 
 83     /**
 84      * Function or array  which maps u to z; i.e. it defines the z-coordinate of the curve
 85      * @function
 86      * @returns Number
 87      * @private
 88      */
 89     this._Z = Z;
 90 
 91     this.points = [];
 92 
 93     this.numberPoints = 0;
 94 
 95     this.dataX = null;
 96     this.dataY = null;
 97     this.dataZ = null;
 98 
 99     if (this._F !== null) {
100         this._X = function (u) {
101             return this._F(u)[0];
102         };
103         this._Y = function (u) {
104             return this._F(u)[1];
105         };
106         this._Z = function (u) {
107             return this._F(u)[2];
108         };
109     } else {
110         if (Type.isFunction(this._X)) {
111             this._F = function(u) {
112                 return [this._X(u), this._Y(u), this._Z(u)];
113             };
114         } else {
115             this._F = null;
116         }
117     }
118 
119     this.range = range;
120 
121     this.methodMap = Type.deepCopy(this.methodMap, {
122         // TODO
123     });
124 };
125 JXG.Curve3D.prototype = new JXG.GeometryElement();
126 Type.copyPrototypeMethods(JXG.Curve3D, JXG.GeometryElement3D, 'constructor3D');
127 
128 JXG.extend(
129     JXG.Curve3D.prototype,
130     /** @lends JXG.Curve3D.prototype */ {
131 
132         /**
133          * Simple curve plotting algorithm.
134          *
135          * @returns {JXG.Curve3D} Reference to itself
136          */
137         updateCoords: function() {
138             var steps = this.evalVisProp('numberpointshigh'),
139                 r, s, e, delta,
140                 u, i,
141                 c3d = [1, 0, 0, 0];
142 
143             this.points = [];
144 
145             if (Type.exists(this.dataX)) {
146                 steps = this.dataX.length;
147                 for (u = 0; u < steps; u++) {
148                     this.points.push([1, this.dataX[u], this.dataY[u], this.dataZ[u]]);
149                 }
150             } else if (Type.isArray(this._X)) {
151                 steps = this._X.length;
152                 for (u = 0; u < steps; u++) {
153                     this.points.push([1, this._X[u], this._Y[u], this._Z[u]]);
154                 }
155             } else {
156                 r = Type.evaluate(this.range);
157                 s = Type.evaluate(r[0]);
158                 e = Type.evaluate(r[1]);
159                 delta = (e - s) / (steps - 1);
160                 for (i = 0, u = s; i < steps && u <= e; i++, u += delta) {
161                     c3d = this.F(u);
162                     c3d.unshift(1);
163                     this.points.push(c3d);
164                 }
165             }
166             this.numberPoints = this.points.length;
167 
168             return this;
169         },
170 
171         /**
172          * Generic function which evaluates the function term of the curve
173          * and applies its transformations.
174          * @param {Number} u
175          * @returns
176          */
177         evalF: function(u) {
178             var t, i,
179                 c3d = [0, 0, 0, 0];
180 
181             if (this.transformations.length === 0 || !Type.exists(this.baseElement)) {
182                 if (Type.exists(this._F)) {
183                     c3d = this._F(u);
184                 } else {
185                     c3d = [this._X[u], this._Y[u], this._Z[u]];
186                 }
187                 return c3d;
188             }
189 
190             t = this.transformations;
191             for (i = 0; i < t.length; i++) {
192                 t[i].update();
193             }
194             if (c3d.length === 3) {
195                 c3d.unshift(1);
196             }
197 
198             if (this === this.baseElement) {
199                 if (Type.exists(this._F)) {
200                     c3d = this._F(u);
201                 } else {
202                     c3d = [this._X[u], this._Y[u], this._Z[u]];
203                 }
204             } else {
205                 c3d = this.baseElement.evalF(u);
206             }
207             c3d.unshift(1);
208             c3d = Mat.matVecMult(t[0].matrix, c3d);
209             for (i = 1; i < t.length; i++) {
210                 c3d = Mat.matVecMult(t[i].matrix, c3d);
211             }
212 
213             return c3d.slice(1);
214         },
215 
216         /**
217          * Function defining the curve plus applying transformations.
218          * @param {Number} u
219          * @returns Array [x, y, z] of length 3
220          */
221         F: function(u) {
222             return this.evalF(u);
223         },
224 
225         /**
226         * Function which maps (u) to z; i.e. it defines the x-coordinate of the curve
227         * plus applying transformations.
228         * @param {Number} u
229         * @returns Number
230         */
231         X: function(u) {
232             return this.evalF(u)[0];
233         },
234 
235         /**
236         * Function which maps (u) to y; i.e. it defines the y-coordinate of the curve
237         * plus applying transformations.
238         * @param {Number} u
239         * @returns Number
240         */
241         Y: function(u) {
242             return this.evalF(u)[1];
243         },
244 
245         /**
246         * Function which maps (u) to z; i.e. it defines the z-coordinate of the curve
247         * plus applying transformations.
248         * @param {Number} u
249         * @returns Number
250         */
251         Z: function(u) {
252             return this.evalF(u)[2];
253         },
254 
255         updateDataArray2D: function () {
256             var i, c2d,
257                 dataX = [],
258                 dataY = [],
259                 len = this.points.length;
260 
261             for (i = 0; i < len; i++) {
262                 c2d = this.view.project3DTo2D(this.points[i]);
263                 dataX.push(c2d[1]);
264                 dataY.push(c2d[2]);
265             }
266 
267             return { X: dataX, Y: dataY };
268         },
269 
270         // Already documented in GeometryElement
271         addTransform: function (el, transform) {
272             this.addTransformGeneric(el, transform);
273             return this;
274         },
275 
276         /**
277          *
278          * @returns {JXG.Curve3D} Reference to itself
279          */
280         updateTransform: function () {
281             var t, c, i, j, len;
282 
283             if (this.transformations.length === 0 || this.baseElement === null ||
284                 Type.exists(this._F) // Transformations have only to be applied here
285                                      // if the curve is defined by arrays
286             ) {
287                 return this;
288             }
289 
290             t = this.transformations;
291             for (i = 0; i < t.length; i++) {
292                 t[i].update();
293             }
294             len = this.baseElement.numberPoints;
295             for (i = 0; i < len; i++) {
296                 if (this === this.baseElement) {
297                     c = this.points[i];
298                 } else {
299                     c = this.baseElement.points[i];
300                 }
301                 for (j = 0; j < t.length; j++) {
302                     c = Mat.matVecMult(t[j].matrix, c);
303                 }
304                 this.points[i] = c;
305             }
306             this.numberPoints = len;
307 
308             return this;
309         },
310 
311         // Already documented in GeometryElement
312         updateDataArray: function() { /* stub */ },
313 
314         // Already documented in GeometryElement
315         update: function () {
316             if (this.needsUpdate) {
317                 this.updateDataArray();
318                 this.updateCoords()
319                     .updateTransform();
320             }
321             return this;
322         },
323 
324         // Already documented in GeometryElement
325         updateRenderer: function () {
326             this.needsUpdate = false;
327             return this;
328         },
329 
330         // Already documented in element3d.js
331         projectCoords: function (p, params) {
332             return Geometry.projectCoordsToParametric(p, this, 1, params);
333         }
334 
335         // Use method from element3d.js
336         // projectScreenCoords: function (pScr, params, cyclic) {
337         //     this.initParamsIfNeeded(params);
338         //     return Geometry.projectScreenCoordsToParametric(pScr, this, params, cyclic);
339         // }
340     }
341 );
342 
343 /**
344  * @class 3D Curves can be defined by mappings or by discrete data sets.
345  * In general, a 3D curve is a mapping from R to R^3, where t maps to (x(t),y(t),z(t)).
346  * The graph is drawn for t in the interval [a,b].
347  * @pseudo
348  * @description A 3D parametric curve is defined by a function
349  *    <i>F: R<sup>1</sup> → R<sup>3</sup></i>.
350  *
351  * @name Curve3D
352  * @augments Curve
353  * @constructor
354  * @type Object
355  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
356  * @param {Function_Function_Function_Array,Function} F<sub>X</sub>,F<sub>Y</sub>,F<sub>Z</sub>,range
357  * 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
358  * lower and upper bound for the range of the parameter u. range may also be a function returning an array of length two.
359  * @param {Function_Array,Function} F,range Alternatively: F<sub>[X,Y,Z]</sub>(u) a function returning an array [x,y,z] of
360  * numbers, range as above.
361  * @param {Array_Array_Array} X,Y,Z Three arrays containing the coordinate points which define the curve.
362  * @example
363  * // create a simple curve in 3d
364  * var bound = [-1.5, 1.5];
365  * var view=board.create('view3d',
366  *     [[-4, -4],[8, 8],
367  *     [bound, bound, bound]],
368  *     {});
369  * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]);
370  * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div>
371  * <script type="text/javascript">
372  *     (function() {
373  *         var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3',
374  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
375  *         // create a simple curve in 3d
376  *         var bound = [-1.5, 1.5];
377  *         var view=board.create('view3d',
378  *             [[-4, -4],[8, 8],
379  *             [bound, bound, bound]],
380  *             {});
381  *         var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]);
382  *     })();
383  * </script><pre>
384   */
385 JXG.createCurve3D = function (board, parents, attributes) {
386     var view = parents[0],
387         F, X, Y, Z, range, attr, el,
388         mat,
389         base = null,
390         transform = null;
391 
392     if (parents.length === 3) {
393         if (Type.isTransformationOrArray(parents[2]) && parents[1].type === Const.OBJECT_TYPE_CURVE3D) {
394             // [curve, transformation(s)]
395             // This might be adopted to the type of the base element (data plot or function)
396             base = parents[1];
397             transform = parents[2];
398             F = null;
399             X = [];
400             Y = [];
401             Z = [];
402         } else {
403             // [F, range]
404             F = parents[1];
405             range = parents[2];
406             X = null;
407             Y = null;
408             Z = null;
409         }
410     } else if (parents.length === 2 && Type.isArray(parents[1])) {
411         mat = Mat.transpose(parents[1]);
412         X = mat[0];
413         Y = mat[1];
414         Z = mat[2];
415         F = null;
416     } else {
417         // [X, Y, Z, range]
418         X = parents[1];
419         Y = parents[2];
420         Z = parents[3];
421         range = parents[4];
422         F = null;
423     }
424     // TODO Throw new Error
425 
426     attr = Type.copyAttributes(attributes, board.options, 'curve3d');
427     el = new JXG.Curve3D(view, F, X, Y, Z, range, attr);
428 
429     attr = el.setAttr2D(attr);
430     el.element2D = view.create("curve", [[], []], attr);
431     el.element2D.view = view;
432     if (base !== null) {
433         el.addTransform(base, transform);
434         el.addParents(base);
435     }
436 
437     /**
438      * @class
439      * @ignore
440      */
441     el.element2D.updateDataArray = function () {
442         var ret = el.updateDataArray2D();
443         this.dataX = ret.X;
444         this.dataY = ret.Y;
445     };
446     el.addChild(el.element2D);
447     el.inherits.push(el.element2D);
448     el.element2D.setParents(el);
449 
450     el.element2D.prepareUpdate().update();
451     if (!board.isSuspendedUpdate) {
452         el.element2D.updateVisibility().updateRenderer();
453     }
454 
455     return el;
456 };
457 
458 JXG.registerElement("curve3d", JXG.createCurve3D);
459 
460 /**
461  * @class A vector field is an assignment of a vector to each point in 3D space.
462  * <p>
463  * Plot a vector field either given by three functions
464  * f1(x, y, z), f2(x, y, z), and f3(x, y, z) or by a function f(x, y, z)
465  * returning an array of size 3.
466  *
467  * @pseudo
468  * @name Vectorfield3D
469  * @augments JXG.Curve3D
470  * @constructor
471  * @type JXG.Curve3D
472  * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown.
473  * Parameter options:
474  * @param {Array|Function|String} F Either an array containing three functions f1(x, y, z), f2(x, y, z),
475  * and f3(x, y) or function f(x, y, z) returning an array of length 3.
476  * @param {Array} xData Array of length 3 containing start value for x, number of steps,
477  * end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x.
478  * @param {Array} yData Array of length 3 containing start value for y, number of steps,
479  * end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y.
480  * @param {Array} zData Array of length 3 containing start value for z, number of steps,
481  * end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z.
482  *
483  * @example
484  * const view = board.create('view3d',
485  *     [
486  *         [-6, -3],
487  *         [8, 8],
488  *         [[-3, 3], [-3, 3], [-3, 3]]
489  *     ], {});
490  *
491  * var vf = view.create('vectorfield3d', [
492  *     [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z],
493  *     [-2, 5, 2], // x from -2 to 2 in 5 steps
494  *     [-2, 5, 2], // y
495  *     [-2, 5, 2] // z
496  * ], {
497  *     strokeColor: 'red',
498  *     scale: 0.5
499  * });
500  *
501  * </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div>
502  * <script type="text/javascript">
503  *     (function() {
504  *         var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348',
505  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false,
506  *          pan: {
507  *                needTwoFingers: true
508  *           }
509  *          });
510  *     const view = board.create('view3d',
511  *         [
512  *             [-6, -3],
513  *             [8, 8],
514  *             [[-3, 3], [-3, 3], [-3, 3]]
515  *         ], {});
516  *     var vf = view.create('vectorfield3d', [
517  *         [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z],
518  *         [-2, 5, 2], // x from -2 to 2 in 5 steps
519  *         [-2, 5, 2], // y
520  *         [-2, 5, 2] // z
521  *     ], {
522  *         strokeColor: 'red',
523  *         scale: 0.5
524  *     });
525  *
526  *
527  *     })();
528  *
529  * </script><pre>
530  *
531  */
532 JXG.createVectorfield3D = function (board, parents, attributes) {
533     var view = parents[0],
534         el, attr;
535 
536     if (!(parents.length >= 5 &&
537         (Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) &&
538         (Type.isArray(parents[2]) && parents[1].length === 3) &&
539         (Type.isArray(parents[3]) && parents[2].length === 3) &&
540         (Type.isArray(parents[4]) && parents[3].length === 3)
541     )) {
542         throw new Error(
543             "JSXGraph: Can't create vector field 3D with parent types " +
544             "'" + typeof parents[1] + "', " +
545             "'" + typeof parents[2] + "', " +
546             "'" + typeof parents[3] + "'."  +
547             "'" + typeof parents[4] + "', "
548         );
549     }
550 
551     attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d');
552     el = view.create('curve3d', [[], [], []], attr);
553 
554     /**
555      * Set the defining functions of 3D vector field.
556      * @memberOf Vectorfield3D
557      * @name setF
558      * @function
559      * @param {Array|Function} func Either an array containing three functions f1(x, y, z),
560      * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3.
561      * @returns {Object} Reference to the 3D vector field object.
562      *
563      * @example
564      * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]);
565      * board.update();
566      *
567      */
568     el.setF = function (func, varnames) {
569         var f0, f1, f2;
570         if (Type.isArray(func)) {
571             f0 = Type.createFunction(func[0], this.board, varnames);
572             f1 = Type.createFunction(func[1], this.board, varnames);
573             f2 = Type.createFunction(func[2], this.board, varnames);
574             /**
575              * @ignore
576              */
577             this.F = function (x, y, z) {
578                 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)];
579             };
580         } else {
581             this.F = Type.createFunction(func, el.board, varnames);
582         }
583         return this;
584     };
585 
586     el.setF(parents[1], 'x, y, z');
587     el.xData = parents[2];
588     el.yData = parents[3];
589     el.zData = parents[4];
590 
591     el.updateDataArray = function () {
592         var k, i, j,
593             v, nrm,
594             x, y, z,
595             scale = this.evalVisProp('scale'),
596             start = [
597                 Type.evaluate(this.xData[0]),
598                 Type.evaluate(this.yData[0]),
599                 Type.evaluate(this.zData[0])
600             ],
601             steps = [
602                 Type.evaluate(this.xData[1]),
603                 Type.evaluate(this.yData[1]),
604                 Type.evaluate(this.zData[1])
605             ],
606             end = [
607                 Type.evaluate(this.xData[2]),
608                 Type.evaluate(this.yData[2]),
609                 Type.evaluate(this.zData[2])
610             ],
611             delta = [
612                 (end[0] - start[0]) / steps[0],
613                 (end[1] - start[1]) / steps[1],
614                 (end[2] - start[2]) / steps[2]
615             ],
616             phi, theta1, theta2, theta,
617             showArrow = this.evalVisProp('arrowhead.enabled'),
618             leg, leg_x, leg_y, leg_z, alpha;
619 
620         if (showArrow) {
621             // Arrow head style
622             // leg = 8;
623             // alpha = Math.PI * 0.125;
624             leg = this.evalVisProp('arrowhead.size');
625             alpha = this.evalVisProp('arrowhead.angle');
626             leg_x = leg / board.unitX;
627             leg_y = leg / board.unitY;
628             leg_z = leg / Math.sqrt(board.unitX * board.unitY);
629         }
630 
631         this.dataX = [];
632         this.dataY = [];
633         this.dataZ = [];
634         for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) {
635             for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) {
636                 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) {
637                     v = this.F(x, y, z);
638                     nrm = Mat.norm(v);
639                     if (nrm < Number.EPSILON) {
640                         continue;
641                     }
642 
643                     v[0] *= scale;
644                     v[1] *= scale;
645                     v[2] *= scale;
646                     Type.concat(this.dataX, [x, x + v[0], NaN]);
647                     Type.concat(this.dataY, [y, y + v[1], NaN]);
648                     Type.concat(this.dataZ, [z, z + v[2], NaN]);
649 
650                     if (showArrow) {
651                         // Arrow head
652                         nrm *= scale;
653                         phi = Math.atan2(v[1], v[0]);
654                         theta = Math.asin(v[2] / nrm);
655                         theta1 = theta - alpha;
656                         theta2 = theta + alpha;
657                         Type.concat(this.dataX, [
658                             x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1),
659                             x + v[0],
660                             x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2),
661                             NaN]);
662                         Type.concat(this.dataY, [
663                             y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1),
664                             y + v[1],
665                             y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2),
666                             NaN]);
667                         Type.concat(this.dataZ, [
668                             z + v[2] - leg_z * Math.sin(theta2),
669                             z + v[2],
670                             z + v[2] - leg_z * Math.sin(theta1),
671                             NaN]);
672                     }
673                 }
674             }
675         }
676     };
677 
678     el.methodMap = Type.deepCopy(el.methodMap, {
679         setF: "setF"
680     });
681 
682     return el;
683 };
684 
685 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D);
686