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