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.GeometryElement3D
 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, 'line3d', ['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.center.addChild(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="JXGdb931076-b29a-4eff-b97e-4251aaf24943" class="jxgbox" style="width: 300px; height: 300px;"></div>
316  * <script type="text/javascript">
317  *     (function() {
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 p2 = view.create(
337  *           'sphere3d',
338  *            [a2, 2],
339  *            {fillColor: '#ff0000'}
340  *        );
341  *
342  *     })();
343  *
344  * </script><pre>
345  *
346  */
347 JXG.createIntersectionCircle3D = function (board, parents, attributes) {
348     var view = parents[0],
349         el1 = parents[1],
350         el2 = parents[2],
351         ixnCircle, center, func,
352         attr = Type.copyAttributes(attributes, board.options, "intersectioncircle3d");
353 
354     func = Geometry.intersectionFunction3D(view, el1, el2);
355     center = view.create('point3d', func[0], { visible: false });
356     ixnCircle = view.create('circle3d', [center, func[1], func[2]], attr);
357 
358     try {
359         el1.addChild(ixnCircle);
360         el2.addChild(ixnCircle);
361     } catch (e) {
362         throw new Error(
363             "JSXGraph: Can't create 'intersection' with parent types '" +
364             typeof parents[0] +
365             "' and '" +
366             typeof parents[1] +
367             "'."
368         );
369     }
370 
371     ixnCircle.type = Const.OBJECT_TYPE_INTERSECTION_CIRCLE3D;
372     ixnCircle.elType = 'intersectioncircle3d';
373     ixnCircle.setParents([el1.id, el2.id]);
374 
375     return ixnCircle;
376 };
377 
378 JXG.registerElement('intersectioncircle3d', JXG.createIntersectionCircle3D);
379