      1 /*
      2     Copyright 2008-2024
      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
     37 /**
     38  * A sphere consists of all points with a given distance from a given point.
     39  * The given point is called the center, and the given distance is called the radius.
     40  * 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).
     41  * @class Creates a new 3D sphere object. Do not use this constructor to create a 3D sphere. Use {@link JXG.View3D#create} with
     42  * type {@link Sphere3D} instead.
     43  * @augments JXG.GeometryElement3D
     44  * @augments JXG.GeometryElement
     45  * @param {JXG.View3D} view The 3D view the sphere is drawn on.
     46  * @param {String} method Can be:
     47  * <ul><li> <b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
     48  * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
     49  * The parameters <code>p1</code>, <code>p2</code> and <code>radius</code> must be set according to this method parameter.
     50  * @param {JXG.Point3D} par1 The center of the sphere.
     51  * @param {JXG.Point3D} par2 Can be:
     52  * <ul><li>A point on the sphere (if the construction method is <code>'twoPoints'</code>)</li>
     53  * <ul><li>A number or function (if the construction method is <code>'pointRadius'</code>)</li>
     54  * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and
     55  * {@link JXG.Options#elements}, and optional a name and an id.
     56  * @see JXG.Board#generateName
     57  */
     58 JXG.Sphere3D = function (view, method, par1, par2, attributes) {
     59     this.constructor(view.board, attributes, Const.OBJECT_TYPE_SPHERE3D, Const.OBJECT_CLASS_3D);
     60     this.constructor3D(view, "sphere3d");
     61
     62     this.board.finalizeAdding(this);
     63
     64     /**
     65      * The construction method.
     66      * Can be:
     67      * <ul><li><b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
     68      * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
     69      * @type String
     70      * @see #center
     71      * @see #point2
     72      */
     73     this.method = method;
     74
     75     /**
     76      * The sphere's center. Do not set this parameter directly, as that will break JSXGraph's update system.
     77      * @type JXG.Point3D
     78      */
     79     this.center = this.board.select(par1);
     80
     81     /**
     82      * 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.
     83      * @type JXG.Point3D
     84      * @see #method
     85      */
     86     this.point2 = null;
     87
     88     this.points = [];
     89
     90     /**
     91      * The 2D representation of the element.
     92      * @type GeometryElement
     93      */
     94     this.element2D = null;
     95
     96     /**
     97      * Elements supporting the 2D representation.
     98      * @type Array
     99      */
    100     this.aux2D = [];
    101
    102     /**
    103      * The type of projection (<code>'parallel'</code> or <code>'central'</code>) that the sphere is currently drawn in.
    104      * @type String
    105      */
    106     this.projectionType = view.projectionType;
    107
    108     if (method === "twoPoints") {
    109         this.point2 = this.board.select(par2);
    110         this.radius = this.Radius();
    111     } else if (method === "pointRadius") {
    112         // Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function
    113         this.updateRadius = Type.createFunction(par2, this.board);
    114         // First evaluation of the radius function
    115         this.updateRadius();
    116         this.addParentsFromJCFunctions([this.updateRadius]);
    117     }
    118
    119     if (Type.exists(this.center._is_new)) {
    120         this.addChild(this.center);
    121         delete this.center._is_new;
    122     } else {
    123         this.center.addChild(this);
    124     }
    125
    126     if (method === "twoPoints") {
    127         if (Type.exists(this.point2._is_new)) {
    128             this.addChild(this.point2);
    129             delete this.point2._is_new;
    130         } else {
    131             this.point2.addChild(this);
    132         }
    133     }
    134
    135     this.methodMap = Type.deepCopy(this.methodMap, {
    136         center: "center",
    137         point2: "point2",
    138         Radius: "Radius"
    139     });
    140 };
    141 JXG.Sphere3D.prototype = new JXG.GeometryElement();
    142 Type.copyPrototypeMethods(JXG.Sphere3D, JXG.GeometryElement3D, "constructor3D");
    143
    144 JXG.extend(
    145     JXG.Sphere3D.prototype,
    146     /** @lends JXG.Sphere3D.prototype */ {
    147         update: function () {
    148             if (this.projectionType !== this.view.projectionType) {
    149                 this.rebuildProjection();
    150             }
    151             return this;
    152         },
    153
    154         updateRenderer: function () {
    155             this.needsUpdate = false;
    156             return this;
    157         },
    158
    159         /**
    160          * Set a new radius, then update the board.
    161          * @param {String|Number|function} r A string, function or number describing the new radius
    162          * @returns {JXG.Sphere3D} Reference to this sphere
    163          */
    164         setRadius: function (r) {
    165             this.updateRadius = Type.createFunction(r, this.board);
    166             this.addParentsFromJCFunctions([this.updateRadius]);
    167             this.board.update();
    168
    169             return this;
    170         },
    171
    172         /**
    173          * Calculates the radius of the circle.
    174          * @param {String|Number|function} [value] Set new radius
    175          * @returns {Number} The radius of the circle
    176          */
    177         Radius: function (value) {
    178             if (Type.exists(value)) {
    179                 this.setRadius(value);
    180                 return this.Radius();
    181             }
    182
    183             if (this.method === "twoPoints") {
    184                 if (this.center.isIllDefined() || this.point2.isIllDefined()) {
    185                     return NaN;
    186                 }
    187
    188                 return this.center.distance(this.point2);
    189             }
    190
    191             if (this.method === "pointRadius") {
    192                 return Math.abs(this.updateRadius());
    193             }
    194
    195             return NaN;
    196         },
    197
    198         // The central projection of a sphere is an ellipse. The front and back
    199         // points of the sphere---that is, the points closest to and furthest
    200         // from the screen---project to the foci of the ellipse.
    201         //
    202         // To see this, look at the cone tangent to the sphere whose tip is at
    203         // the camera. The image of the sphere is the ellipse where this cone
    204         // intersects the screen. By acting on the sphere with scalings centered
    205         // on the camera, you can send it to either of the Dandelin spheres that
    206         // touch the screen at the foci of the image ellipse.
    207         //
    208         // This factory method produces two functions, `focusFn(-1)` and
    209         // `focusFn(1)`, that evaluate to the projections of the front and back
    210         // points of the sphere, respectively.
    211         focusFn: function (sgn) {
    212             var that = this;
    213
    214             return function () {
    215                 var camDir = that.view.boxToCam[3],
    216                     r = that.Radius();
    217
    218                 return that.view.project3DTo2D([
    219                     that.center.X() + sgn * r * camDir[1],
    220                     that.center.Y() + sgn * r * camDir[2],
    221                     that.center.Z() + sgn * r * camDir[3]
    222                 ]).slice(1, 3);
    223             };
    224         },
    225
    226         innerVertexFn: function () {
    227             var that = this;
    228
    229             return function () {
    230                 var view = that.view,
    231                     p = view.worldToFocal(that.center.coords, false),
    232                     distOffAxis = Mat.hypot(p[0], p[1]),
    233                     cam = view.boxToCam,
    234                     inward = [
    235                         -(p[0] * cam[1][1] + p[1] * cam[2][1]) / distOffAxis,
    236                         -(p[0] * cam[1][2] + p[1] * cam[2][2]) / distOffAxis,
    237                         -(p[0] * cam[1][3] + p[1] * cam[2][3]) / distOffAxis
    238                     ],
    239                     r = that.Radius(),
    240                     angleOffAxis = Math.atan(-distOffAxis / p[2]),
    241                     steepness = Math.acos(r / Mat.norm(p)),
    242                     lean = angleOffAxis + steepness,
    243                     cos_lean = Math.cos(lean),
    244                     sin_lean = Math.sin(lean);
    245
    246                 return view.project3DTo2D([
    247                     that.center.X() + r * (sin_lean * inward[0] + cos_lean * cam[3][1]),
    248                     that.center.Y() + r * (sin_lean * inward[1] + cos_lean * cam[3][2]),
    249                     that.center.Z() + r * (sin_lean * inward[2] + cos_lean * cam[3][3])
    250                 ]);
    251             };
    252         },
    253
    254         buildCentralProjection: function () {
    255             var view = this.view,
    256                 auxStyle = { visible: false, withLabel: false },
    257                 frontFocus = view.create('point', this.focusFn(-1), auxStyle),
    258                 backFocus = view.create('point', this.focusFn(1), auxStyle),
    259                 innerVertex = view.create('point', this.innerVertexFn(view), auxStyle);
    260
    261             this.aux2D = [frontFocus, backFocus, innerVertex];
    262             this.element2D = view.create('ellipse', this.aux2D, this.visProp);
    263         },
    264
    265         buildParallelProjection: function () {
    266             // The parallel projection of a sphere is a circle
    267             var that = this,
    268                 center2d = function () {
    269                     var c3d = [1, that.center.X(), that.center.Y(), that.center.Z()];
    270                     return that.view.project3DTo2D(c3d);
    271                 },
    272                 radius2d = function () {
    273                     var boxSize = that.view.bbox3D[0][1] - that.view.bbox3D[0][0];
    274                     return that.Radius() * that.view.size[0] / boxSize;
    275                 };
    276
    277             this.aux2D = [];
    278             this.element2D = this.view.create(
    279                 'circle',
    280                 [center2d, radius2d],
    281                 this.visProp
    282             );
    283         },
    284
    285         // replace our 2D representation with a new one that's consistent with
    286         // the view's current projection type
    287         rebuildProjection: function () {
    288             var i;
    289
    290             // remove the old 2D representation from the scene tree
    291             if (this.element2D) {
    292                 this.view.board.removeObject(this.element2D);
    293                 for (i in this.aux2D) {
    294                     if (this.aux2D.hasOwnProperty(i)) {
    295                         this.view.board.removeObject(this.aux2D[i]);
    296                     }
    297                 }
    298             }
    299
    300             // build a new 2D representation. the representation is stored in
    301             // `this.element2D`, and any auxiliary elements are stored in
    302             // `this.aux2D`
    303             this.projectionType = this.view.projectionType;
    304             if (this.projectionType === 'central') {
    305                 this.buildCentralProjection();
    306             } else {
    307                 this.buildParallelProjection();
    308             }
    309
    310             // attach the new 2D representation to the scene tree
    311             this.addChild(this.element2D);
    312             this.inherits.push(this.element2D);
    313             this.element2D.view = this.view;
    314         }
    315     }
    316 );
    317
    318 /**
    319  * @class This element is used to provide a constructor for a sphere.
    320  *
    321  * @pseudo
    322  * @description
    323  * A sphere consists of all points with a given distance from a given point.
    324  * The given point is called the center, and the given distance is called the radius.
    325  * 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).
    326  * If the radius is a negative value, its absolute value is taken.
    327  *
    328  * @name Sphere3D
    329  * @augments JXG.Sphere3D
    330  * @constructor
    331  * @type JXG.Sphere3D
    332  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    333  * @param {JXG.Point3D_number,JXG.Point3D} center,radius The center must be given as a {@link JXG.Point3D} (see {@link JXG.providePoints3D}),
    334  * but the radius can be given as a number (which will create a sphere with a fixed radius) or another {@link JXG.Point3D}.
    335  * <p>
    336  * If the radius is supplied as number or the output of a function, its absolute value is taken.
    337  *
    338  * @example
    339  * var view = board.create(
    340  *     'view3d',
    341  *     [[-6, -3], [8, 8],
    342  *     [[0, 3], [0, 3], [0, 3]]],
    343  *     {
    344  *         xPlaneRear: {fillOpacity: 0.2, gradient: null},
    345  *         yPlaneRear: {fillOpacity: 0.2, gradient: null},
    346  *         zPlaneRear: {fillOpacity: 0.2, gradient: null}
    347  *     }
    348  * );
    349  *
    350  * // Two points
    351  * var center = view.create(
    352  *     'point3d',
    353  *     [1.5, 1.5, 1.5],
    354  *     {
    355  *         withLabel: false,
    356  *         size: 5,
    357  *    }
    358  * );
    359  * var point = view.create(
    360  *     'point3d',
    361  *     [2, 1.5, 1.5],
    362  *     {
    363  *         withLabel: false,
    364  *         size: 5
    365  *    }
    366  * );
    367  *
    368  * // Sphere
    369  * var sphere = view.create(
    370  *     'sphere3d',
    371  *     [center, point],
    372  *     {}
    373  * );
    374  *
    375  * </pre><div id="JXG5969b83c-db67-4e62-9702-d0440e5fe2c1" class="jxgbox" style="width: 300px; height: 300px;"></div>
    376  * <script type="text/javascript">
    377  *     (function() {
    378  *         var board = JXG.JSXGraph.initBoard('JXG5969b83c-db67-4e62-9702-d0440e5fe2c1',
    379  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
    380  *         var view = board.create(
    381  *             'view3d',
    382  *             [[-6, -3], [8, 8],
    383  *             [[0, 3], [0, 3], [0, 3]]],
    384  *             {
    385  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
    386  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
    387  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
    388  *             }
    389  *         );
    390  *
    391  *         // Two points
    392  *         var center = view.create(
    393  *             'point3d',
    394  *             [1.5, 1.5, 1.5],
    395  *             {
    396  *                 withLabel: false,
    397  *                 size: 5,
    398  *            }
    399  *         );
    400  *         var point = view.create(
    401  *             'point3d',
    402  *             [2, 1.5, 1.5],
    403  *             {
    404  *                 withLabel: false,
    405  *                 size: 5
    406  *            }
    407  *         );
    408  *
    409  *         // Sphere
    410  *         var sphere = view.create(
    411  *             'sphere3d',
    412  *             [center, point],
    413  *             {}
    414  *         );
    415  *
    416  *     })();
    417  *
    418  * </script><pre>
    419  *
    420  */
    421 JXG.createSphere3D = function (board, parents, attributes) {
    422     //   parents[0]: view
    423     //   parents[1]: point,
    424     //   parents[2]: point or radius
    425
    426     var view = parents[0],
    427         attr, p, point_style, provided,
    428         el, i;
    429
    430     attr = Type.copyAttributes(attributes, board.options, 'sphere3d');
    431
    432     p = [];
    433     for (i = 1; i < parents.length; i++) {
    434         if (Type.isPointType3D(board, parents[i])) {
    435             if (p.length === 0) {
    436                 point_style = 'center';
    437             } else {
    438                 point_style = 'point';
    439             }
    440             provided = Type.providePoints3D(view, [parents[i]], attributes, 'sphere3d', [point_style])[0];
    441             if (provided === false) {
    442                 throw new Error(
    443                     "JSXGraph: Can't create sphere3d from this type. Please provide a point type."
    444                 );
    445             }
    446             p.push(provided);
    447         } else {
    448             p.push(parents[i]);
    449         }
    450     }
    451
    452     if (Type.isPoint3D(p[0]) && Type.isPoint3D(p[1])) {
    453         // Point/Point
    454         el = new JXG.Sphere3D(view, "twoPoints", p[0], p[1], attr);
    455     } else if (
    456         (Type.isNumber(p[0]) || Type.isFunction(p[0]) || Type.isString(p[0])) &&
    457         Type.isPoint3D(p[1])
    458     ) {
    459         // Number/Point
    460         el = new JXG.Sphere3D(view, "pointRadius", p[1], p[0], attr);
    461     } else if (
    462         (Type.isNumber(p[1]) || Type.isFunction(p[1]) || Type.isString(p[1])) &&
    463         Type.isPoint3D(p[0])
    464     ) {
    465         // Point/Number
    466         el = new JXG.Sphere3D(view, "pointRadius", p[0], p[1], attr);
    467     } else {
    468         throw new Error(
    469             "JSXGraph: Can't create sphere3d with parent types '" +
    470             typeof parents[1] +
    471             "' and '" +
    472             typeof parents[2] +
    473             "'." +
    474             "\nPossible parent types: [point,point], [point,number], [point,function]"
    475         );
    476     }
    477
    478     // build a 2D representation, and attach it to the scene tree, and update it
    479     // to the correct initial state
    480     el.rebuildProjection();
    481     el.element2D.prepareUpdate().update().updateRenderer();
    482
    483     return el;
    484 };
    485
    486 JXG.registerElement("sphere3d", JXG.createSphere3D);
    487