      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 JXG.createVectorfield3D = function (board, parents, attributes) {
    302     var view = parents[0],
    303         el, attr;
    304
    305     if (!(parents.length >= 5 &&
    306         (Type.isArray(parents[1]) || Type.isFunction(parents[0]) || Type.isString(parents[0])) &&
    307         (Type.isArray(parents[2]) && parents[1].length === 3) &&
    308         (Type.isArray(parents[3]) && parents[2].length === 3) &&
    309         (Type.isArray(parents[4]) && parents[3].length === 3)
    310     )) {
    311         throw new Error(
    312             "JSXGraph: Can't create vector field 3D with parent types " +
    313             "'" + typeof parents[0] + "', " +
    314             "'" + typeof parents[1] + "', " +
    315             "'" + typeof parents[2] + "'."  +
    316             "'" + typeof parents[1] + "', "
    317         );
    318     }
    319
    320     attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d');
    321     el = view.create('curve3d', [[], [], []], attr);
    322
    323     /**
    324      * Set the defining functions of 3D vector field.
    325      * @memberOf Vectorfield3D
    326      * @name setF
    327      * @function
    328      * @param {Array|Function} func Either an array containing three functions f1(x, y, z),
    329      * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3.
    330      * @returns {Object} Reference to the 3D vector field object.
    331      *
    332      * @example
    333      * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]);
    334      * board.update();
    335      *
    336      */
    337     el.setF = function (func, varnames) {
    338         var f0, f1, f2;
    339         if (Type.isArray(func)) {
    340             f0 = Type.createFunction(func[0], this.board, varnames);
    341             f1 = Type.createFunction(func[1], this.board, varnames);
    342             f2 = Type.createFunction(func[2], this.board, varnames);
    343             /**
    344              * @ignore
    345              */
    346             this.F = function (x, y, z) {
    347                 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)];
    348             };
    349         } else {
    350             this.F = Type.createFunction(func, el.board, varnames);
    351         }
    352         return this;
    353     };
    354
    355     el.setF(parents[1], 'x, y, z');
    356     el.xData = parents[2];
    357     el.yData = parents[3];
    358     el.zData = parents[4];
    359
    360     el.updateDataArray = function () {
    361         var k, i, j,
    362             v, nrm,
    363             x, y, z,
    364             scale = Type.evaluate(this.visProp.scale),
    365             start = [
    366                 Type.evaluate(this.xData[0]),
    367                 Type.evaluate(this.yData[0]),
    368                 Type.evaluate(this.zData[0])
    369             ],
    370             steps = [
    371                 Type.evaluate(this.xData[1]),
    372                 Type.evaluate(this.yData[1]),
    373                 Type.evaluate(this.zData[1])
    374             ],
    375             end = [
    376                 Type.evaluate(this.xData[2]),
    377                 Type.evaluate(this.yData[2]),
    378                 Type.evaluate(this.zData[2])
    379             ],
    380             delta = [
    381                 (end[0] - start[0]) / steps[0],
    382                 (end[1] - start[1]) / steps[1],
    383                 (end[2] - start[2]) / steps[2]
    384             ],
    385             phi, theta1, theta2, theta,
    386             showArrow = Type.evaluate(this.visProp.arrowhead.enabled),
    387             leg, leg_x, leg_y, leg_z, alpha;
    388
    389         if (showArrow) {
    390             // Arrow head style
    391             // leg = 8;
    392             // alpha = Math.PI * 0.125;
    393             leg = Type.evaluate(this.visProp.arrowhead.size);
    394             alpha = Type.evaluate(this.visProp.arrowhead.angle);
    395             leg_x = leg / board.unitX;
    396             leg_y = leg / board.unitY;
    397             leg_z = leg / Math.sqrt(board.unitX * board.unitY);
    398         }
    399
    400         this.dataX = [];
    401         this.dataY = [];
    402         this.dataZ = [];
    403         for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) {
    404             for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) {
    405                 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) {
    406                     v = this.F(x, y, z);
    407                     nrm = Mat.norm(v);
    408                     if (nrm < Number.EPSILON) {
    409                         continue;
    410                     }
    411
    412                     v[0] *= scale;
    413                     v[1] *= scale;
    414                     v[2] *= scale;
    415                     Type.concat(this.dataX, [x, x + v[0], NaN]);
    416                     Type.concat(this.dataY, [y, y + v[1], NaN]);
    417                     Type.concat(this.dataZ, [z, z + v[2], NaN]);
    418
    419                     if (showArrow) {
    420                         // Arrow head
    421                         nrm *= scale;
    422                         phi = Math.atan2(v[1], v[0]);
    423                         theta = Math.asin(v[2] / nrm);
    424                         theta1 = theta - alpha;
    425                         theta2 = theta + alpha;
    426                         Type.concat(this.dataX, [
    427                             x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1),
    428                             x + v[0],
    429                             x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2),
    430                             NaN]);
    431                         Type.concat(this.dataY, [
    432                             y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1),
    433                             y + v[1],
    434                             y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2),
    435                             NaN]);
    436                         Type.concat(this.dataZ, [
    437                             z + v[2] - leg_z * Math.sin(theta2),
    438                             z + v[2],
    439                             z + v[2] - leg_z * Math.sin(theta1),
    440                             NaN]);
    441                     }
    442                 }
    443             }
    444         }
    445     };
    446
    447     el.methodMap = Type.deepCopy(el.methodMap, {
    448         setF: "setF"
    449     });
    450
    451     return el;
    452 };
    453
    454 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D);
    455