      1 /*
      2     Copyright 2008-2025
      3         Matthias Ehmann,
      4         Aaron Fenyes,
      5         Carsten Miller,
      6         Andreas Walter,
      7         Alfred Wassermann
      8
      9     This file is part of JSXGraph.
     10
     11     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     12
     13     You can redistribute it and/or modify it under the terms of the
     14
     15       * GNU Lesser General Public License as published by
     16         the Free Software Foundation, either version 3 of the License, or
     17         (at your option) any later version
     18       OR
     19       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     20
     21     JSXGraph is distributed in the hope that it will be useful,
     22     but WITHOUT ANY WARRANTY; without even the implied warranty of
     23     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     24     GNU Lesser General Public License for more details.
     25
     26     You should have received a copy of the GNU Lesser General Public License and
     27     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
     28     and <https://opensource.org/licenses/MIT/>.
     29  */
     30 /*global JXG:true, define: true*/
     31
     32 import JXG from "../jxg.js";
     33 import Const from "../base/constants.js";
     34 import Type from "../utils/type.js";
     35 import Mat from "../math/math.js";
     36 import Stat from "../math/statistics.js";
     37 import Geometry from "../math/geometry.js";
     38
     39 /**
     40  * A sphere consists of all points with a given distance from a given point.
     41  * The given point is called the center, and the given distance is called the radius.
     42  * A sphere can be constructed by providing a center and a point on the sphere or a center and a radius (given as a number or function).
     43  * @class Creates a new 3D sphere object. Do not use this constructor to create a 3D sphere. Use {@link JXG.View3D#create} with
     44  * type {@link Sphere3D} instead.
     45  * @augments JXG.GeometryElement3D
     46  * @augments JXG.GeometryElement
     47  * @param {JXG.View3D} view The 3D view the sphere is drawn on.
     48  * @param {String} method Can be:
     49  * <ul><li> <b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
     50  * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
     51  * The parameters <code>p1</code>, <code>p2</code> and <code>radius</code> must be set according to this method parameter.
     52  * @param {JXG.Point3D} par1 The center of the sphere.
     53  * @param {JXG.Point3D} par2 Can be:
     54  * <ul><li>A point on the sphere (if the construction method is <code>'twoPoints'</code>)</li>
     55  * <ul><li>A number or function (if the construction method is <code>'pointRadius'</code>)</li>
     56  * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and
     57  * {@link JXG.Options#elements}, and optional a name and an id.
     58  * @see JXG.Board#generateName
     59  */
     60 JXG.Sphere3D = function (view, method, par1, par2, attributes) {
     61     this.constructor(view.board, attributes, Const.OBJECT_TYPE_SPHERE3D, Const.OBJECT_CLASS_3D);
     62     this.constructor3D(view, "sphere3d");
     63
     64     this.board.finalizeAdding(this);
     65
     66     /**
     67      * The construction method.
     68      * Can be:
     69      * <ul><li><b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
     70      * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
     71      * @type String
     72      * @see JXG.Sphere3D#center
     73      * @see JXG.Sphere3D#point2
     74      */
     75     this.method = method;
     76
     77     /**
     78      * The sphere's center. Do not set this parameter directly, as that will break JSXGraph's update system.
     79      * @type JXG.Point3D
     80      */
     81     this.center = this.board.select(par1);
     82
     83     /**
     84      * A point on the sphere; only set if the construction method is 'twoPoints'. Do not set this parameter directly, as that will break JSXGraph's update system.
     85      * @type JXG.Point3D
     86      * @see JXG.Sphere3D#method
     87      */
     88     this.point2 = null;
     89
     90     this.points = [];
     91
     92     /**
     93      * The 2D representation of the element.
     94      * @type GeometryElement
     95      */
     96     this.element2D = null;
     97
     98     /**
     99      * Elements supporting the 2D representation.
    100      * @type Array
    101      * @private
    102      */
    103     this.aux2D = [];
    104
    105     /**
    106      * The type of projection (<code>'parallel'</code> or <code>'central'</code>) that the sphere is currently drawn in.
    107      * @type String
    108      */
    109     this.projectionType = view.projectionType;
    110
    111     if (method === "twoPoints") {
    112         this.point2 = this.board.select(par2);
    113         this.radius = this.Radius();
    114     } else if (method === "pointRadius") {
    115         // Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function
    116         this.updateRadius = Type.createFunction(par2, this.board);
    117         // First evaluation of the radius function
    118         this.updateRadius();
    119         this.addParentsFromJCFunctions([this.updateRadius]);
    120     }
    121
    122     if (Type.exists(this.center._is_new)) {
    123         this.addChild(this.center);
    124         delete this.center._is_new;
    125     } else {
    126         this.center.addChild(this);
    127     }
    128
    129     if (method === "twoPoints") {
    130         if (Type.exists(this.point2._is_new)) {
    131             this.addChild(this.point2);
    132             delete this.point2._is_new;
    133         } else {
    134             this.point2.addChild(this);
    135         }
    136     }
    137
    138     this.methodMap = Type.deepCopy(this.methodMap, {
    139         center: "center",
    140         point2: "point2",
    141         Radius: "Radius"
    142     });
    143 };
    144 JXG.Sphere3D.prototype = new JXG.GeometryElement();
    145 Type.copyPrototypeMethods(JXG.Sphere3D, JXG.GeometryElement3D, "constructor3D");
    146
    147 JXG.extend(
    148     JXG.Sphere3D.prototype,
    149     /** @lends JXG.Sphere3D.prototype */ {
    150
    151         X: function(u, v) {
    152             var r = this.Radius();
    153             return r * Math.sin(u) * Math.cos(v);
    154         },
    155
    156         Y: function(u, v) {
    157             var r = this.Radius();
    158             return r * Math.sin(u) * Math.sin(v);
    159         },
    160
    161         Z: function(u, v) {
    162             var r = this.Radius();
    163             return r * Math.cos(u);
    164         },
    165
    166         range_u: [0, 2 * Math.PI],
    167         range_v: [0, Math.PI],
    168
    169         update: function () {
    170             if (this.projectionType !== this.view.projectionType) {
    171                 this.rebuildProjection();
    172             }
    173             return this;
    174         },
    175
    176         updateRenderer: function () {
    177             this.needsUpdate = false;
    178             return this;
    179         },
    180
    181         /**
    182          * Set a new radius, then update the board.
    183          * @param {String|Number|function} r A string, function or number describing the new radius
    184          * @returns {JXG.Sphere3D} Reference to this sphere
    185          */
    186         setRadius: function (r) {
    187             this.updateRadius = Type.createFunction(r, this.board);
    188             this.addParentsFromJCFunctions([this.updateRadius]);
    189             this.board.update();
    190
    191             return this;
    192         },
    193
    194         /**
    195          * Calculates the radius of the circle.
    196          * @param {String|Number|function} [value] Set new radius
    197          * @returns {Number} The radius of the circle
    198          */
    199         Radius: function (value) {
    200             if (Type.exists(value)) {
    201                 this.setRadius(value);
    202                 return this.Radius();
    203             }
    204
    205             if (this.method === "twoPoints") {
    206                 if (!this.center.testIfFinite() || !this.point2.testIfFinite()) {
    207                     return NaN;
    208                 }
    209
    210                 return this.center.distance(this.point2);
    211             }
    212
    213             if (this.method === "pointRadius") {
    214                 return Math.abs(this.updateRadius());
    215             }
    216
    217             return NaN;
    218         },
    219
    220         // The central projection of a sphere is an ellipse. The front and back
    221         // points of the sphere---that is, the points closest to and furthest
    222         // from the screen---project to the foci of the ellipse.
    223         //
    224         // To see this, look at the cone tangent to the sphere whose tip is at
    225         // the camera. The image of the sphere is the ellipse where this cone
    226         // intersects the screen. By acting on the sphere with scalings centered
    227         // on the camera, you can send it to either of the Dandelin spheres that
    228         // touch the screen at the foci of the image ellipse.
    229         //
    230         // This factory method produces two functions, `focusFn(-1)` and
    231         // `focusFn(1)`, that evaluate to the projections of the front and back
    232         // points of the sphere, respectively.
    233         focusFn: function (sgn) {
    234             var that = this;
    235
    236             return function () {
    237                 var camDir = that.view.boxToCam[3],
    238                     r = that.Radius();
    239
    240                 return that.view.project3DTo2D([
    241                     that.center.X() + sgn * r * camDir[1],
    242                     that.center.Y() + sgn * r * camDir[2],
    243                     that.center.Z() + sgn * r * camDir[3]
    244                 ]).slice(1, 3);
    245             };
    246         },
    247
    248         innerVertexFn: function () {
    249             var that = this;
    250
    251             return function () {
    252                 var view = that.view,
    253                     p = view.worldToFocal(that.center.coords, false),
    254                     distOffAxis = Mat.hypot(p[0], p[1]),
    255                     cam = view.boxToCam,
    256                     r = that.Radius(),
    257                     angleOffAxis = Math.atan(-distOffAxis / p[2]),
    258                     steepness = Math.acos(r / Mat.norm(p)),
    259                     lean = angleOffAxis + steepness,
    260                     cos_lean = Math.cos(lean),
    261                     sin_lean = Math.sin(lean),
    262                     inward;
    263
    264                 if (distOffAxis > 1e-8) {
    265                     // if the center of the sphere isn't too close to the camera
    266                     // axis, find the direction in plane of the screen that
    267                     // points from the center of the sphere toward the camera
    268                     // axis
    269                     inward = [
    270                         -(p[0] * cam[1][1] + p[1] * cam[2][1]) / distOffAxis,
    271                         -(p[0] * cam[1][2] + p[1] * cam[2][2]) / distOffAxis,
    272                         -(p[0] * cam[1][3] + p[1] * cam[2][3]) / distOffAxis
    273                     ];
    274                 } else {
    275                     // if the center of the sphere is very close to the camera
    276                     // axis, choose an arbitrary unit vector in the plane of the
    277                     // screen
    278                     inward = [cam[1][1], cam[1][2], cam[1][3]];
    279                 }
    280                 return view.project3DTo2D([
    281                     that.center.X() + r * (sin_lean * inward[0] + cos_lean * cam[3][1]),
    282                     that.center.Y() + r * (sin_lean * inward[1] + cos_lean * cam[3][2]),
    283                     that.center.Z() + r * (sin_lean * inward[2] + cos_lean * cam[3][3])
    284                 ]);
    285             };
    286         },
    287
    288         buildCentralProjection: function (attr) {
    289             var view = this.view,
    290                 auxStyle = { visible: false, withLabel: false },
    291                 frontFocus = view.create('point', this.focusFn(-1), auxStyle),
    292                 backFocus = view.create('point', this.focusFn(1), auxStyle),
    293                 innerVertex = view.create('point', this.innerVertexFn(view), auxStyle);
    294
    295             this.aux2D = [frontFocus, backFocus, innerVertex];
    296             this.element2D = view.create('ellipse', this.aux2D, attr === undefined ? this.visProp : attr);
    297         },
    298
    299         buildParallelProjection: function (attr) {
    300             // The parallel projection of a sphere is a circle
    301             var that = this,
    302                 // center2d = function () {
    303                 //     var c3d = [1, that.center.X(), that.center.Y(), that.center.Z()];
    304                 //     return that.view.project3DTo2D(c3d);
    305                 // },
    306                 radius2d = function () {
    307                     var boxSize = that.view.bbox3D[0][1] - that.view.bbox3D[0][0];
    308                     return that.Radius() * that.view.size[0] / boxSize;
    309                 };
    310
    311             this.aux2D = [];
    312             this.element2D = this.view.create(
    313                 'circle',
    314                 // [center2d, radius2d],
    315                 [that.center.element2D, radius2d],
    316                 attr === undefined ? this.visProp : attr
    317             );
    318         },
    319
    320         // replace our 2D representation with a new one that's consistent with
    321         // the view's current projection type
    322         rebuildProjection: function (attr) {
    323             var i;
    324
    325             // remove the old 2D representation from the scene tree
    326             if (this.element2D) {
    327                 this.view.board.removeObject(this.element2D);
    328                 for (i in this.aux2D) {
    329                     if (this.aux2D.hasOwnProperty(i)) {
    330                         this.view.board.removeObject(this.aux2D[i]);
    331                     }
    332                 }
    333             }
    334
    335             // build a new 2D representation. the representation is stored in
    336             // `this.element2D`, and any auxiliary elements are stored in
    337             // `this.aux2D`
    338             this.projectionType = this.view.projectionType;
    339             if (this.projectionType === 'central') {
    340                 this.buildCentralProjection(attr);
    341             } else {
    342                 this.buildParallelProjection(attr);
    343             }
    344
    345             // attach the new 2D representation to the scene tree
    346             this.addChild(this.element2D);
    347             this.inherits.push(this.element2D);
    348             this.element2D.view = this.view;
    349         },
    350
    351         // Already documented in element3d.js
    352         projectCoords: function(p, params) {
    353             var r = this.Radius(),
    354                 pp = [1].concat(p),
    355                 c = this.center.coords,
    356                 d = Geometry.distance(c, pp, 4),
    357                 v = Stat.subtract(pp, c);
    358
    359             if (d === 0) {
    360                 // p is at the center, take an arbitrary point on sphere
    361                 params[0] = 0;
    362                 params[1] = 0;
    363                 return [1, r, 0, 0];
    364             }
    365             if (r === 0) {
    366                 params[0] = 0;
    367                 params[1] = 0;
    368                 return this.center.coords;
    369             }
    370
    371             d = r / d;
    372             v[0] = 1;
    373             v[1] *= d;
    374             v[2] *= d;
    375             v[3] *= d;
    376
    377             // Preimage of the new position
    378             params[1] = Math.atan2(v[2], v[1]);
    379             params[1] += (params[1] < 0) ? Math.PI : 0;
    380             if (params[1] !== 0) {
    381                 params[0] = Math.atan2(v[2], v[3] * Math.sin(params[1]));
    382             } else {
    383                 params[0] = Math.atan2(v[1], v[3] * Math.cos(params[1]));
    384             }
    385             params[0] += (params[0] < 0) ? 2 * Math.PI : 0;
    386
    387             return v;
    388         }
    389
    390         // projectScreenCoords: function (pScr, params) {
    391         //     if (params.length === 0) {
    392         //         params.unshift(
    393         //             0.5 * (this.range_u[0] + this.range_u[1]),
    394         //             0.5 * (this.range_v[0] + this.range_v[1])
    395         //         );
    396         //     }
    397         //     return Geometry.projectScreenCoordsToParametric(pScr, this, params);
    398         // }
    399     }
    400 );
    401
    402 /**
    403  * @class A sphere in a 3D view.
    404  * A sphere consists of all points with a given distance from a given point.
    405  * The given point is called the center, and the given distance is called the radius.
    406  * A sphere can be constructed by providing a center and a point on the sphere or a center and a radius (given as a number or function).
    407  * If the radius is a negative value, its absolute value is taken.
    408  *
    409  * @pseudo
    410  * @name Sphere3D
    411  * @augments JXG.Sphere3D
    412  * @constructor
    413  * @type JXG.Sphere3D
    414  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    415  * @param {JXG.Point3D_number,JXG.Point3D} center,radius The center must be given as a {@link JXG.Point3D} (see {@link JXG.providePoints3D}),
    416  * but the radius can be given as a number (which will create a sphere with a fixed radius) or another {@link JXG.Point3D}.
    417  * <p>
    418  * If the radius is supplied as number or the output of a function, its absolute value is taken.
    419  *
    420  * @example
    421  * var view = board.create(
    422  *     'view3d',
    423  *     [[-6, -3], [8, 8],
    424  *     [[0, 3], [0, 3], [0, 3]]],
    425  *     {
    426  *         xPlaneRear: {fillOpacity: 0.2, gradient: null},
    427  *         yPlaneRear: {fillOpacity: 0.2, gradient: null},
    428  *         zPlaneRear: {fillOpacity: 0.2, gradient: null}
    429  *     }
    430  * );
    431  *
    432  * // Two points
    433  * var center = view.create(
    434  *     'point3d',
    435  *     [1.5, 1.5, 1.5],
    436  *     {
    437  *         withLabel: false,
    438  *         size: 5,
    439  *    }
    440  * );
    441  * var point = view.create(
    442  *     'point3d',
    443  *     [2, 1.5, 1.5],
    444  *     {
    445  *         withLabel: false,
    446  *         size: 5
    447  *    }
    448  * );
    449  *
    450  * // Sphere
    451  * var sphere = view.create(
    452  *     'sphere3d',
    453  *     [center, point],
    454  *     {}
    455  * );
    456  *
    457  * </pre><div id="JXG5969b83c-db67-4e62-9702-d0440e5fe2c1" class="jxgbox" style="width: 300px; height: 300px;"></div>
    458  * <script type="text/javascript">
    459  *     (function() {
    460  *         var board = JXG.JSXGraph.initBoard('JXG5969b83c-db67-4e62-9702-d0440e5fe2c1',
    461  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
    462  *         var view = board.create(
    463  *             'view3d',
    464  *             [[-6, -3], [8, 8],
    465  *             [[0, 3], [0, 3], [0, 3]]],
    466  *             {
    467  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
    468  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
    469  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
    470  *             }
    471  *         );
    472  *
    473  *         // Two points
    474  *         var center = view.create(
    475  *             'point3d',
    476  *             [1.5, 1.5, 1.5],
    477  *             {
    478  *                 withLabel: false,
    479  *                 size: 5,
    480  *            }
    481  *         );
    482  *         var point = view.create(
    483  *             'point3d',
    484  *             [2, 1.5, 1.5],
    485  *             {
    486  *                 withLabel: false,
    487  *                 size: 5
    488  *            }
    489  *         );
    490  *
    491  *         // Sphere
    492  *         var sphere = view.create(
    493  *             'sphere3d',
    494  *             [center, point],
    495  *             {}
    496  *         );
    497  *
    498  *     })();
    499  *
    500  * </script><pre>
    501  *
    502  * @example
    503  *     // Glider on sphere
    504  *     var view = board.create(
    505  *         'view3d',
    506  *         [[-6, -3], [8, 8],
    507  *         [[-3, 3], [-3, 3], [-3, 3]]],
    508  *         {
    509  *             depthOrder: {
    510  *                 enabled: true
    511  *             },
    512  *             projection: 'central',
    513  *             xPlaneRear: {fillOpacity: 0.2, gradient: null},
    514  *             yPlaneRear: {fillOpacity: 0.2, gradient: null},
    515  *             zPlaneRear: {fillOpacity: 0.2, gradient: null}
    516  *         }
    517  *     );
    518  *
    519  *     // Two points
    520  *     var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2});
    521  *     var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2});
    522  *
    523  *     // Sphere
    524  *     var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8});
    525  *
    526  *     // Glider on sphere
    527  *     var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4});
    528  *     var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 });
    529  *
    530  * </pre><div id="JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71" class="jxgbox" style="width: 300px; height: 300px;"></div>
    531  * <script type="text/javascript">
    532  *     (function() {
    533  *         var board = JXG.JSXGraph.initBoard('JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71',
    534  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
    535  *         var view = board.create(
    536  *             'view3d',
    537  *             [[-6, -3], [8, 8],
    538  *             [[-3, 3], [-3, 3], [-3, 3]]],
    539  *             {
    540  *                 depthOrder: {
    541  *                     enabled: true
    542  *                 },
    543  *                 projection: 'central',
    544  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
    545  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
    546  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
    547  *             }
    548  *         );
    549  *
    550  *         // Two points
    551  *         var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2});
    552  *         var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2});
    553  *
    554  *         // Sphere
    555  *         var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8});
    556  *
    557  *         // Glider on sphere
    558  *         var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4});
    559  *         var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 });
    560  *
    561  *     })();
    562  *
    563  * </script><pre>
    564  *
    565  */
    566 JXG.createSphere3D = function (board, parents, attributes) {
    567     //   parents[0]: view
    568     //   parents[1]: point,
    569     //   parents[2]: point or radius
    570
    571     var view = parents[0],
    572         attr, p, point_style, provided,
    573         el, i;
    574
    575     attr = Type.copyAttributes(attributes, board.options, 'sphere3d');
    576     p = [];
    577     for (i = 1; i < parents.length; i++) {
    578         if (Type.isPointType3D(board, parents[i])) {
    579             if (p.length === 0) {
    580                 point_style = 'center';
    581             } else {
    582                 point_style = 'point';
    583             }
    584             provided = Type.providePoints3D(view, [parents[i]], attributes, 'sphere3d', [point_style])[0];
    585             if (provided === false) {
    586                 throw new Error(
    587                     "JSXGraph: Can't create sphere3d from this type. Please provide a point type."
    588                 );
    589             }
    590             p.push(provided);
    591         } else {
    592             p.push(parents[i]);
    593         }
    594     }
    595
    596     if (Type.isPoint3D(p[0]) && Type.isPoint3D(p[1])) {
    597         // Point/Point
    598         el = new JXG.Sphere3D(view, "twoPoints", p[0], p[1], attr);
    599
    600         /////////////// nothing in docs suggest you can use [number, pointType]
    601         // } else if (
    602         //     (Type.isNumber(p[0]) || Type.isFunction(p[0]) || Type.isString(p[0])) &&
    603         //     Type.isPoint3D(p[1])
    604         // ) {
    605         //     // Number/Point
    606         //     el = new JXG.Sphere3D(view, "pointRadius", p[1], p[0], attr);
    607
    608     } else if (
    609         Type.isPoint3D(p[0]) &&
    610         (Type.isNumber(p[1]) || Type.isFunction(p[1]) || Type.isString(p[1]))
    611     ) {
    612         // Point/Number
    613         el = new JXG.Sphere3D(view, "pointRadius", p[0], p[1], attr);
    614     } else {
    615         throw new Error(
    616             "JSXGraph: Can't create sphere3d with parent types '" +
    617             typeof parents[1] +
    618             "' and '" +
    619             typeof parents[2] +
    620             "'." +
    621             "\nPossible parent types: [point,point], [point,number], [point,function]"
    622         );
    623     }
    624
    625     // Build a 2D representation, and attach it to the scene tree, and update it
    626     // to the correct initial state
    627     // Here, element2D is created.
    628     attr = el.setAttr2D(attr);
    629     el.rebuildProjection(attr);
    630
    631     el.element2D.prepareUpdate().update();
    632     if (!board.isSuspendedUpdate) {
    633         el.element2D.updateVisibility().updateRenderer();
    634     }
    635
    636     return el;
    637 };
    638
    639 JXG.registerElement("sphere3d", JXG.createSphere3D);
    640