      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 import Geometry from '../math/geometry.js';
     37
     38 /**
     39  * In 3D space, a circle consists of all points on a given plane with a given distance from a given point. The given point is called the center, and the given distance is called the radius.
     40  * A circle can be constructed by providing a center, a normal vector, and a radius (given as a number or function).
     41  * @class Creates a new 3D circle object. Do not use this constructor to create a 3D circle. Use {@link JXG.View3D#create} with
     42  * type {@link Circle3D} instead.
     43  * @constructor
     44  * @augments JXG.Curve3D
     45  * @augments JXG.GeometryElement
     46  * @param {JXG.View3D} view The 3D view the circle is drawn on.
     47  * @param {JXG.Point} center The center of the circle.
     48  * @param {Array} normal A normal vector of the plane the circle lies in. Must be either an array of three numbers or an array of three functions returning numbers.
     49  * @param {Number|Function} radius The radius of the circle.
     50  * @param {Object} attributes
     51  * @see JXG.Board#generateName
     52  */
     53 JXG.Circle3D = function (view, center, normal, radius, attributes) {
     54     var altFrame1;
     55
     56     this.constructor(view.board, attributes, Const.OBJECT_TYPE_CIRCLE3D, Const.OBJECT_CLASS_3D);
     57     this.constructor3D(view, "circle3d");
     58
     59     /**
     60      * The circle's center. Do not set this parameter directly, as that will break JSXGraph's update system.
     61      * @type JXG.Point3D
     62      */
     63     this.center = this.board.select(center);
     64
     65     /**
     66      * A normal vector of the plane the circle lies in. Do not set this parameter directly, as that will break JSXGraph's update system.
     67      * @type Array
     68      * @private
     69      *
     70      * @see updateNormal
     71      */
     72     this.normal = [0, 0, 0];
     73
     74     /**
     75      * The circle's underlying Curve3D.
     76      */
     77     this.curve;
     78
     79     /**
     80      * The first vector in an orthonormal frame for the plane the circle lies in.
     81      * Do not set this parameter directly, as that will break JSXGraph's update system.
     82      * @type Array
     83      * @private
     84      *
     85      * @see updateFrame
     86      */
     87     this.frame1;
     88
     89     /**
     90      * The second vector in an orthonormal frame for the plane the circle lies in.
     91      * Do not set this parameter directly, as that will break JSXGraph's update system.
     92      * @type Array
     93      * @private
     94      *
     95      * @see updateFrame
     96      */
     97     this.frame2;
     98
     99     this.updateNormal = function () {
    100         // evaluate normal direction
    101         var i, len;
    102         for (i = 0; i < 3; i++) {
    103             this.normal[i] = Type.evaluate(normal[i]);
    104         }
    105
    106         // scale normal to unit length
    107         len = Mat.norm(this.normal);
    108         if (Math.abs(len) > Mat.eps) {
    109             for (i = 0; i < 3; i++) {
    110                 this.normal[i] /= len;
    111             }
    112         }
    113     };
    114
    115     // place the circle or its center---whichever is newer---in the scene tree
    116     if (Type.exists(this.center._is_new)) {
    117         this.addChild(this.center);
    118         delete this.center._is_new;
    119     } else {
    120         this.center.addChild(this);
    121     }
    122
    123     // Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function
    124     this.updateRadius = Type.createFunction(radius, this.board);
    125     this.addParentsFromJCFunctions([this.updateRadius]);
    126
    127     // initialize normal
    128     this.updateNormal();
    129
    130     // initialize the first frame vector by taking the cross product with
    131     // [1, 0, 0] or [-0.5, sqrt(3)/2, 0]---whichever is further away on the unit
    132     // sphere. every vector is at least 60 degrees from one of these, which
    133     // should be good enough to make the frame vector numerically accurate
    134     this.frame1 = Mat.crossProduct(this.normal, [1, 0, 0]);
    135     altFrame1 = Mat.crossProduct(this.normal, [-0.5, 0.8660254037844386, 0]); // [1/2, sqrt(3)/2, 0]
    136     if (Mat.norm(altFrame1) > Mat.norm(this.frame1)) {
    137         this.frame1 = altFrame1;
    138     }
    139
    140     // initialize the second frame vector
    141     this.frame2 = Mat.crossProduct(this.normal, this.frame1);
    142
    143     // scale both frame vectors to unit length
    144     this.normalizeFrame();
    145
    146     // create the underlying curve
    147     this.curve = view.create(
    148         'curve3d',
    149         [
    150             (t) => this.center.X() + this.Radius() * (Math.cos(t) * this.frame1[0] + Math.sin(t) * this.frame2[0]),
    151             (t) => this.center.Y() + this.Radius() * (Math.cos(t) * this.frame1[1] + Math.sin(t) * this.frame2[1]),
    152             (t) => this.center.Z() + this.Radius() * (Math.cos(t) * this.frame1[2] + Math.sin(t) * this.frame2[2]),
    153             [0, 2 * Math.PI] // parameter range
    154         ],
    155         attributes
    156     );
    157 };
    158 JXG.Circle3D.prototype = new JXG.GeometryElement();
    159 Type.copyPrototypeMethods(JXG.Circle3D, JXG.GeometryElement3D, "constructor3D");
    160
    161 JXG.extend(
    162     JXG.Circle3D.prototype,
    163     /** @lends JXG.Circle3D.prototype */ {
    164         update: function () {
    165             this.updateNormal();
    166             this.updateFrame();
    167             this.curve.visProp.visible = !isNaN(this.Radius());
    168             return this;
    169         },
    170
    171         updateRenderer: function () {
    172             this.needsUpdate = false;
    173             return this;
    174         },
    175
    176         /**
    177          * Set a new radius, then update the board.
    178          * @param {String|Number|function} r A string, function or number describing the new radius
    179          * @returns {JXG.Circle3D} Reference to this sphere
    180          */
    181         setRadius: function (r) {
    182             this.updateRadius = Type.createFunction(r, this.board);
    183             this.addParentsFromJCFunctions([this.updateRadius]);
    184             this.board.update();
    185
    186             return this;
    187         },
    188
    189         /**
    190          * Calculates the radius of the circle.
    191          * @param {String|Number|function} [value] Set new radius
    192          * @returns {Number} The radius of the circle
    193          */
    194         Radius: function (value) {
    195             if (Type.exists(value)) {
    196                 this.setRadius(value);
    197                 return this.Radius();
    198             }
    199
    200             return Math.abs(this.updateRadius());
    201         },
    202
    203         normalizeFrame: function () {
    204             // normalize frame
    205             var len1 = Mat.norm(this.frame1),
    206                 len2 = Mat.norm(this.frame2),
    207                 i;
    208             for (i = 0; i < 3; i++) {
    209                 this.frame1[i] /= len1;
    210                 this.frame2[i] /= len2;
    211             }
    212         },
    213
    214         updateFrame: function () {
    215             this.frame1 = Mat.crossProduct(this.frame2, this.normal);
    216             this.frame2 = Mat.crossProduct(this.normal, this.frame1);
    217             this.normalizeFrame();
    218         },
    219
    220         projectCoords: function (p, params) {
    221             // we have to call `this.curve.projectCoords` from the curve, rather
    222             // than the circle, to make `this` refer to the curve within the
    223             // call
    224             return this.curve.projectCoords(p, params);
    225         },
    226
    227         projectScreenCoords: function (pScr, params) {
    228             // we have to call `this.curve.projectScreenCoords` from the curve,
    229             // rather than the circle, to make `this` refer to the curve within
    230             // the call
    231             return this.curve.projectScreenCoords(pScr, params);
    232         }
    233     }
    234 );
    235
    236 /**
    237  * @class This element is used to provide a constructor for a circle.
    238  * @pseudo
    239  * @description In 3D space, a circle consists of all points on a given plane with a given distance from a given point. The given point is called the center, and the given distance is called the radius.
    240  * A circle can be constructed by providing a center, a normal vector, and a radius (given as a number or function).
    241  * <p>
    242  * If the radius has a negative value, its absolute value is taken. If the radius evaluates to NaN,
    243  * the circle is not displayed. This is convenient for constructing an intersection circle, which is empty when its parents do not intersect.
    244  * @name Circle3D
    245  * @augments JXG.Circle3D
    246  * @constructor
    247  * @type JXG.Circle3D
    248  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    249  * @param {JXG.Point_Array_number} center,normal,radius The center must be given as a {@link JXG.Point} (see {@link JXG.providePoints}).
    250  * The normal vector can be given as an array of three numbers or an array of three functions returning numbers,
    251  * and the radius can be given as a number (which will create a circle with a fixed radius) or a function.
    252  * <p>
    253  * If the radius is supplied as a number or the output of a function, its absolute value is taken. When the radius evaluates to NaN, the circle does not display.
    254  */
    255 JXG.createCircle3D = function (board, parents, attributes) {
    256     var view = parents[0],
    257         attr = Type.copyAttributes(attributes, board.options, 'circle3d'),
    258         center = Type.providePoints3D(view, [parents[1]], attributes, 'circle3d', ['point'])[0],
    259         normal = parents[2],
    260         radius = parents[3],
    261         el;
    262
    263     // Create element
    264     el = new JXG.Circle3D(view, center, normal, radius, attr);
    265
    266     // Update scene tree
    267     el.curve.addParents([el]);
    268     el.addChild(el.curve);
    269
    270     el.update();
    271     return el;
    272 };
    273
    274 JXG.registerElement("circle3d", JXG.createCircle3D);
    275
    276 /**
    277  * @class An intersection circle is a circle which lives on two JSXGraph elements.
    278  * The following element types can be (mutually) intersected: sphere, plane.
    279  *
    280  * @pseudo
    281  * @name IntersectionCircle3D
    282  * @augments JXG.Circle3D
    283  * @constructor
    284  * @type JXG.Circle3D
    285  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
    286  * @param {JXG.Sphere3D_JXG.Sphere3D|JXG.Plane3D} el1,el2 The result will be the intersection of el1 and el2.
    287  * @example
    288  * // Create the intersection circle of two spheres
    289  * var view = board.create(
    290  *     'view3d',
    291  *     [[-6, -3], [8, 8],
    292  *     [[0, 3], [0, 3], [0, 3]]],
    293  *     {
    294  *         xPlaneRear: {fillOpacity: 0.2, gradient: null},
    295  *         yPlaneRear: {fillOpacity: 0.2, gradient: null},
    296  *         zPlaneRear: {fillOpacity: 0.2, gradient: null}
    297  *     }
    298  * );
    299  * var a1 = view.create('point3d', [-1, 0, 0]);
    300  * var a2 = view.create('point3d', [1, 0, 0]);
    301  *
    302  * var s1 = view.create(
    303  *    'sphere3d',
    304  *     [a1, 2],
    305  *     {fillColor: '#00ff80'}
    306  * );
    307  * var s2 = view.create(
    308  *    'sphere3d',
    309  *     [a2, 2],
    310  *     {fillColor: '#ff0000'}
    311  * );
    312  *
    313  * var i = view.create('intersectioncircle3d', [s1, s2]);
    314  *
    315  * </pre><div id="JXG64ede949-8dd6-44d0-b2a9-248a479d3a5d" class="jxgbox" style="width: 300px; height: 300px;"></div>
    316  * <script type="text/javascript">
    317  *     (function() {
    318  *         var board = JXG.JSXGraph.initBoard('JXG64ede949-8dd6-44d0-b2a9-248a479d3a5d',
    319  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
    320  *         var view = board.create(
    321  *            'view3d',
    322  *            [[-6, -3], [8, 8],
    323  *            [[0, 3], [0, 3], [0, 3]]],
    324  *            {
    325  *                xPlaneRear: {fillOpacity: 0.2, gradient: null},
    326  *                yPlaneRear: {fillOpacity: 0.2, gradient: null},
    327  *                zPlaneRear: {fillOpacity: 0.2, gradient: null}
    328  *            }
    329  *        );
    330  *        var a1 = view.create('point3d', [-1, 0, 0]);
    331  *        var a2 = view.create('point3d', [1, 0, 0]);
    332  *
    333  *        var s1 = view.create(
    334  *           'sphere3d',
    335  *            [a1, 2],
    336  *            {fillColor: '#00ff80'}
    337  *        );
    338  *        var p2 = view.create(
    339  *           'sphere3d',
    340  *            [a2, 2],
    341  *            {fillColor: '#ff0000'}
    342  *        );
    343  *
    344  *     })();
    345  *
    346  * </script><pre>
    347  *
    348  */
    349 JXG.createIntersectionCircle3D = function (board, parents, attributes) {
    350     var view = parents[0],
    351         el1 = parents[1],
    352         el2 = parents[2],
    353         ixnCircle, center, func,
    354         attr = Type.copyAttributes(attributes, board.options, "intersectioncircle3d");
    355
    356     func = Geometry.intersectionFunction3D(view, el1, el2);
    357     center = view.create('point3d', func[0], { visible: false });
    358     ixnCircle = view.create('circle3d', [center, func[1], func[2]], attr);
    359
    360     try {
    361         el1.addChild(ixnCircle);
    362         el2.addChild(ixnCircle);
    363     } catch (e) {
    364         throw new Error(
    365             "JSXGraph: Can't create 'intersection' with parent types '" +
    366             typeof parents[0] +
    367             "' and '" +
    368             typeof parents[1] +
    369             "'."
    370         );
    371     }
    372
    373     ixnCircle.type = Const.OBJECT_TYPE_INTERSECTION_CIRCLE3D;
    374     ixnCircle.elType = 'intersectioncircle3d';
    375     ixnCircle.setParents([el1.id, el2.id]);
    376
    377     return ixnCircle;
    378 };
    379
    380 JXG.registerElement('intersectioncircle3d', JXG.createIntersectionCircle3D);
    381