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, 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