      1 /*
      2     Copyright 2008-2025
      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 /**
     32  * Create axes and rear and front walls of the
     33  * view3d bounding box bbox3D.
     34  */
     35 import JXG from "../jxg.js";
     36 import Type from "../utils/type.js";
     37
     38 /**
     39  * @class A container element that creates the axes and rear and front planes of a 3D view.
     40  * @pseudo
     41  * @description This element "axes3d" is used to create
     42  *  <ul>
     43  *   <li> 3D coordinate axes (either "axesPosition:'border'" or "axesPosition:'center'")
     44  *   <li> A point3d "O" (origin) if "axesPosition:'center'"
     45  *   <li> Rear and front planes in all three directions of the view3d element.
     46  *   <li> Coordinate axes on the rear and front planes
     47  *  </ul>
     48  *
     49  * @name Axes3D
     50  * @constructor
     51  * @type Object
     52  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
     53  *
     54  */
     55 JXG.createAxes3D = function (board, parents, attributes) {
     56     var view = parents[0],
     57     directions = ["x", "y", "z"],
     58     suffixAxis = "Axis",
     59     sides = ["Rear", "Front"],
     60     rear = [0, 0, 0],           // x, y, z
     61     front = [0, 0, 0],          // x, y, z
     62     i, j, k, i1, i2, attr, pos,
     63     dir, dir1, len,
     64     from, to, vec1, vec2,
     65     range1, range2,
     66     na, na_parent,
     67     ticks_attr,
     68     axes = {};
     69
     70     if (Type.exists(view.bbox3D)) {
     71         for (i = 0; i < directions.length; i++) {
     72             rear[i] = view.bbox3D[i][0];
     73             front[i] = view.bbox3D[i][1];
     74         }
     75     } else {
     76         for (i = 0; i < directions.length; i++) {
     77             rear[i] = parents[1][i];
     78             front[i] = parents[2][1];
     79         }
     80     }
     81
     82     // Main 3D axes
     83     attr = Type.copyAttributes(attributes, board.options, "axes3d");
     84     // Position of the main axes can not be changed during run time
     85     pos = attr.axesposition;
     86
     87     for (i = 0; i < directions.length; i++) {
     88         // Run through ['x', 'y', 'z']
     89         dir = directions[i];
     90         na = dir + suffixAxis;
     91
     92         if (pos === "center") {
     93             // Axes centered
     94             from = [0, 0, 0];
     95             to = [0, 0, 0];
     96             to[i] = front[i];
     97             axes[na] = view.create("axis3d", [from, to], attr[na.toLowerCase()]);
     98             axes[na].view = view;
     99         } else if (pos === 'border') {
    100             // Axes bordered
    101             na += "Border";
    102             from = rear.slice();
    103             to = front.slice();
    104             if (dir === 'z') {
    105                 from[1] = front[1];
    106                 to[0] = rear[0];
    107             } else if (dir === 'x') {
    108                 from = [rear[0], front[1], rear[2]];
    109                 to = [front[0], front[1], rear[2]];
    110             } else {
    111                 from = [front[0], rear[1], rear[2]];
    112                 to = [front[0], front[1], rear[2]];
    113             }
    114             to[i] = front[i];
    115             // attr[na.toLowerCase()].lastArrow = false;
    116             axes[na] = view.create("axis3d", [from, to], attr[na.toLowerCase()]);
    117             axes[na].view = view;
    118
    119             ticks_attr = attr[na.toLowerCase()].ticks3d;
    120             ticks_attr.element3d = true;  // Needed to avoid update during change of view
    121             len = front[i] - rear[i];
    122             if (dir === 'x') {
    123                 axes[na + "Ticks"] = view.create("ticks3d", [from, [1, 0, 0], len, [0, 1, 0]], ticks_attr);
    124             } else if (dir === 'y') {
    125                 axes[na + "Ticks"] = view.create("ticks3d", [from, [0, 1, 0], len, [1, 0, 0]], ticks_attr);
    126             } else {
    127                 axes[na + "Ticks"] = view.create("ticks3d", [from, [0, 0, 1], len, [0, 1, 0]], ticks_attr);
    128             }
    129             axes[na + "Ticks"].view = view;
    130         }
    131     }
    132
    133     if (pos === 'center') {
    134         // Origin (2D point)
    135         axes.O = view.create(
    136             "intersection",
    137             [axes[directions[0] + suffixAxis], axes[directions[1] + suffixAxis]],
    138             {
    139                 name: "",
    140                 visible: false,
    141                 withLabel: false
    142             }
    143         );
    144         axes.O.view = view;
    145     } else {
    146         axes.O = null;
    147     }
    148
    149     // Front and rear planes
    150     for (i = 0; i < directions.length; i++) {
    151         // Run through ['x', 'y', 'z']
    152         i1 = (i + 1) % 3;
    153         i2 = (i + 2) % 3;
    154
    155         dir = directions[i];
    156         for (j = 0; j < sides.length; j++) {
    157             // Run through ['Rear', 'Front']
    158             // attr = Type.copyAttributes(attributes, board.options, "axes3d");
    159
    160             na = dir + "Plane" + sides[j];
    161
    162             from = [0, 0, 0];
    163             from[i] = j === 0 ? rear[i] : front[i];
    164             vec1 = [0, 0, 0];
    165             vec2 = [0, 0, 0];
    166             vec1[i1] = 1;
    167             vec2[i2] = 1;
    168             range1 = [rear[i1], front[i1]];
    169             range2 = [rear[i2], front[i2]];
    170
    171             attr = Type.copyAttributes(attributes, board.options, "axes3d", na);
    172             axes[na] = view.create("plane3d", [from, vec1, vec2, range1, range2], attr);
    173             axes[na].elType = "axisplane3d";
    174         }
    175     }
    176
    177     // Axes on front and rear planes
    178     for (i = 0; i < directions.length; i++) {
    179         // Run through ['x', 'y', 'z']
    180         dir = directions[i];
    181         for (j = 0; j < sides.length; j++) {
    182             for (k = 1; k <= 2; k++) {
    183                 i1 = (i + k) % 3;
    184                 dir1 = directions[i1];
    185                 na = dir + "Plane" + sides[j] + dir1.toUpperCase() + "Axis";
    186                 na_parent = dir + "Plane" + sides[j];
    187
    188                 from = [0, 0, 0];
    189                 to = [0, 0, 0];
    190                 from[i] = to[i] = j === 0 ? rear[i] : front[i];
    191
    192                 from[i1] = rear[i1];
    193                 to[i1] = front[i1];
    194
    195                 attr = Type.copyAttributes(attributes, board.options, "axes3d", na);
    196                 axes[na] = view.create("axis3d", [from, to], attr);
    197                 axes[na].view = view;
    198                 axes[na_parent].addChild(axes[na]);
    199                 axes[na_parent].element2D.inherits.push(axes[na]); // TODO: Access of element2D is not nice
    200             }
    201         }
    202     }
    203
    204     return axes;
    205 };
    206 JXG.registerElement("axes3d", JXG.createAxes3D);
    207
    208 /**
    209  * @class A 3D axis element is a line together with optional ticks and labels.
    210  * @pseudo
    211  * @description Simple element 3d axis as used with "axesPosition:center". No ticks and no label (yet).
    212  * <p>
    213  * At the time being, the input arrays are NOT dynamic, i.e. can not be given as functions.
    214  *
    215  * @name Axis3D
    216  * @augments Arrow
    217  * @constructor
    218  * @type Object
    219  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    220  * @param {Array_Array} start,end Two arrays of length 3 for the start point and the end point of the axis.
    221  *
    222  */
    223 JXG.createAxis3D = function (board, parents, attributes) {
    224     var view = parents[0],
    225         attr,
    226         start = parents[1],
    227         end = parents[2],
    228         el_start,
    229         el_end,
    230         el;
    231
    232     // Use 2D points to create axis
    233     attr = Type.copyAttributes(attributes.point1, board.options, "axis3d", "point1");
    234     attr.element3d = true;  // Needed to avoid update during change of view
    235     el_start = view.create(
    236         "point",
    237         [
    238             (function (xx, yy, zz) {
    239                 return function () {
    240                     return view.project3DTo2D(xx, yy, zz)[1];
    241                 };
    242             })(start[0], start[1], start[2]),
    243             (function (xx, yy, zz) {
    244                 return function () {
    245                     return view.project3DTo2D(xx, yy, zz)[2];
    246                 };
    247             })(start[0], start[1], start[2])
    248         ],
    249         attr
    250     );
    251
    252     attr = Type.copyAttributes(attributes.point2, board.options, "axis3d", "point2");
    253     attr.element3d = true;  // Needed to avoid update during change of view
    254     el_end = view.create(
    255         "point",
    256         [
    257             (function (xx, yy, zz) {
    258                 return function () {
    259                     return view.project3DTo2D(xx, yy, zz)[1];
    260                 };
    261             })(end[0], end[1], end[2]),
    262             (function (xx, yy, zz) {
    263                 return function () {
    264                     return view.project3DTo2D(xx, yy, zz)[2];
    265                 };
    266             })(end[0], end[1], end[2])
    267         ],
    268         attr
    269     );
    270
    271     attr = Type.copyAttributes(attributes, board.options, "axis3d");
    272     attr.element3d = true;  // Needed to avoid update during change of view
    273     el = view.create("arrow", [el_start, el_end], attr);
    274
    275     return el;
    276 };
    277 JXG.registerElement("axis3d", JXG.createAxis3D);
    278
    279 /**
    280  * @class Display a rectangular mesh on a 3D plane element.
    281  * @pseudo
    282  * @description Create a (rectangular) mesh - i.e. grid lines - on a plane3D element.
    283  * <p>
    284  * At the time being, the mesh is not connected to the plane. The connecting element is simply the
    285  * parameter point.
    286  *
    287  * @name Mesh3D
    288  * @augments Curve
    289  * @constructor
    290  * @type Object
    291  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    292  * @param {Array_Array_Array_Array_Array} point,direction1,direction2,range1,range2 point is an array of length 3
    293  * determining the starting point of the grid. direction1 and direction2 are arrays of length 3 for the directions of the grid.
    294  * range1 and range2 (arrays of length 2) give the respective ranges.
    295  * All parameters can be supplied as functions returning an appropriate data type.
    296  *
    297  */
    298 JXG.createMesh3D = function (board, parents, attributes) {
    299     var view = parents[0],
    300         attr, el;
    301
    302     attr = Type.copyAttributes(attributes, board.options, 'mesh3d');
    303     attr.element3d = true;  // Needed to avoid update during change of view
    304     el = view.create("curve", [[], []], attr);
    305
    306     el.point = parents[1];
    307     el.direction1 = parents[2];
    308     el.direction2 = parents[3];
    309     el.range1 = parents[4];
    310     el.range2 = parents[5];
    311
    312     /**
    313      * @ignore
    314      */
    315     el.updateDataArray = function () {
    316         var range1 = Type.evaluate(this.range1),
    317             range2 = Type.evaluate(this.range2),
    318             s1 = range1[0],
    319             e1 = range1[1],
    320             s2 = range2[0],
    321             e2 = range2[1],
    322             l1, l2, res, i,
    323             v1 = [0, 0, 0],
    324             v2 = [0, 0, 0],
    325             step_u = this.evalVisProp('stepwidthu'),
    326             step_v = this.evalVisProp('stepwidthv'),
    327             q = [0, 0, 0];
    328
    329         this.dataX = [];
    330         this.dataY = [];
    331
    332         if (Type.isFunction(this.point)) {
    333             q = this.point();
    334         } else {
    335             if (Type.isPoint3D(this.point)) {
    336                 q = this.point.coords;
    337             } else {
    338                 for (i = 0; i < this.point.length; i++) {
    339                     q[i] = Type.evaluate(this.point[i]);
    340                 }
    341             }
    342         }
    343         if (Type.isFunction(this.direction1)) {
    344             v1 = Type.evaluate(this.direction1);
    345         } else {
    346             for (i = 0; i < this.direction1.length; i++) {
    347                 v1[i] = Type.evaluate(this.direction1[i]);
    348             }
    349         }
    350         if (Type.isFunction(this.direction2)) {
    351             v2 = Type.evaluate(this.direction2);
    352         } else {
    353             for (i = 0; i < this.direction2.length; i++) {
    354                 v2[i] = Type.evaluate(this.direction2[i]);
    355             }
    356         }
    357         if (q.length === 4) {
    358             q = q.slice(1);
    359         }
    360         if (v1.length === 4) {
    361             v1 = v1.slice(1);
    362         }
    363         if (v2.length === 4) {
    364             v2 = v2.slice(1);
    365         }
    366
    367         l1 = JXG.Math.norm(v1, 3);
    368         l2 = JXG.Math.norm(v2, 3);
    369         for (i = 0; i < 3; i++) {
    370             v1[i] /= l1;
    371             v2[i] /= l2;
    372         }
    373
    374         // sol = Mat.Geometry.getPlaneBounds(v1, v2, q, s1, e1);
    375         // if (sol !== null) {
    376         //     s1 = sol[0];
    377         //     e1 = sol[1];
    378         //     s2 = sol[2];
    379         //     e2 = sol[3];
    380         // }
    381
    382         res = view.getMesh(
    383             [
    384                 function (u, v) {
    385                     return q[0] + u * v1[0] + v * v2[0];
    386                 },
    387                 function (u, v) {
    388                     return q[1] + u * v1[1] + v * v2[1];
    389                 },
    390                 function (u, v) {
    391                     return q[2] + u * v1[2] + v * v2[2];
    392                 }
    393             ],
    394             [Math.ceil(s1), Math.floor(e1), (Math.ceil(e1) - Math.floor(s1)) / step_u],
    395             [Math.ceil(s2), Math.floor(e2), (Math.ceil(e2) - Math.floor(s2)) / step_v]
    396         );
    397         this.dataX = res[0];
    398         this.dataY = res[1];
    399     };
    400
    401     return el;
    402 };
    403
    404 JXG.registerElement("mesh3d", JXG.createMesh3D);
    405