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 Stat from "../math/statistics.js";
 37 import Geometry from "../math/geometry.js";
 38 
 39 /**
 40  * A sphere consists of all points with a given distance from a given point.
 41  * The given point is called the center, and the given distance is called the radius.
 42  * 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).
 43  * @class Creates a new 3D sphere object. Do not use this constructor to create a 3D sphere. Use {@link JXG.View3D#create} with
 44  * type {@link Sphere3D} instead.
 45  * @augments JXG.GeometryElement3D
 46  * @augments JXG.GeometryElement
 47  * @param {JXG.View3D} view The 3D view the sphere is drawn on.
 48  * @param {String} method Can be:
 49  * <ul><li> <b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
 50  * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
 51  * The parameters <code>p1</code>, <code>p2</code> and <code>radius</code> must be set according to this method parameter.
 52  * @param {JXG.Point3D} par1 The center of the sphere.
 53  * @param {JXG.Point3D} par2 Can be:
 54  * <ul><li>A point on the sphere (if the construction method is <code>'twoPoints'</code>)</li>
 55  * <ul><li>A number or function (if the construction method is <code>'pointRadius'</code>)</li>
 56  * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and
 57  * {@link JXG.Options#elements}, and optional a name and an id.
 58  * @see JXG.Board#generateName
 59  */
 60 JXG.Sphere3D = function (view, method, par1, par2, attributes) {
 61     this.constructor(view.board, attributes, Const.OBJECT_TYPE_SPHERE3D, Const.OBJECT_CLASS_3D);
 62     this.constructor3D(view, "sphere3d");
 63 
 64     this.board.finalizeAdding(this);
 65 
 66     /**
 67      * The construction method.
 68      * Can be:
 69      * <ul><li><b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
 70      * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
 71      * @type String
 72      * @see JXG.Sphere3D#center
 73      * @see JXG.Sphere3D#point2
 74      */
 75     this.method = method;
 76 
 77     /**
 78      * The sphere's center. Do not set this parameter directly, as that will break JSXGraph's update system.
 79      * @type JXG.Point3D
 80      */
 81     this.center = this.board.select(par1);
 82 
 83     /**
 84      * 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.
 85      * @type JXG.Point3D
 86      * @see JXG.Sphere3D#method
 87      */
 88     this.point2 = null;
 89 
 90     this.points = [];
 91 
 92     /**
 93      * The 2D representation of the element.
 94      * @type GeometryElement
 95      */
 96     this.element2D = null;
 97 
 98     /**
 99      * Elements supporting the 2D representation.
100      * @type Array
101      * @private
102      */
103     this.aux2D = [];
104 
105     /**
106      * The type of projection (<code>'parallel'</code> or <code>'central'</code>) that the sphere is currently drawn in.
107      * @type String
108      */
109     this.projectionType = view.projectionType;
110 
111     if (method === "twoPoints") {
112         this.point2 = this.board.select(par2);
113         this.radius = this.Radius();
114     } else if (method === "pointRadius") {
115         // Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function
116         this.updateRadius = Type.createFunction(par2, this.board);
117         // First evaluation of the radius function
118         this.updateRadius();
119         this.addParentsFromJCFunctions([this.updateRadius]);
120     }
121 
122     if (Type.exists(this.center._is_new)) {
123         this.addChild(this.center);
124         delete this.center._is_new;
125     } else {
126         this.center.addChild(this);
127     }
128 
129     if (method === "twoPoints") {
130         if (Type.exists(this.point2._is_new)) {
131             this.addChild(this.point2);
132             delete this.point2._is_new;
133         } else {
134             this.point2.addChild(this);
135         }
136     }
137 
138     this.methodMap = Type.deepCopy(this.methodMap, {
139         center: "center",
140         point2: "point2",
141         Radius: "Radius"
142     });
143 };
144 JXG.Sphere3D.prototype = new JXG.GeometryElement();
145 Type.copyPrototypeMethods(JXG.Sphere3D, JXG.GeometryElement3D, "constructor3D");
146 
147 JXG.extend(
148     JXG.Sphere3D.prototype,
149     /** @lends JXG.Sphere3D.prototype */ {
150 
151         X: function(u, v) {
152             var r = this.Radius();
153             return r * Math.sin(u) * Math.cos(v);
154         },
155 
156         Y: function(u, v) {
157             var r = this.Radius();
158             return r * Math.sin(u) * Math.sin(v);
159         },
160 
161         Z: function(u, v) {
162             var r = this.Radius();
163             return r * Math.cos(u);
164         },
165 
166         range_u: [0, 2 * Math.PI],
167         range_v: [0, Math.PI],
168 
169         update: function () {
170             if (this.projectionType !== this.view.projectionType) {
171                 this.rebuildProjection();
172             }
173             return this;
174         },
175 
176         updateRenderer: function () {
177             this.needsUpdate = false;
178             return this;
179         },
180 
181         /**
182          * Set a new radius, then update the board.
183          * @param {String|Number|function} r A string, function or number describing the new radius
184          * @returns {JXG.Sphere3D} Reference to this sphere
185          */
186         setRadius: function (r) {
187             this.updateRadius = Type.createFunction(r, this.board);
188             this.addParentsFromJCFunctions([this.updateRadius]);
189             this.board.update();
190 
191             return this;
192         },
193 
194         /**
195          * Calculates the radius of the circle.
196          * @param {String|Number|function} [value] Set new radius
197          * @returns {Number} The radius of the circle
198          */
199         Radius: function (value) {
200             if (Type.exists(value)) {
201                 this.setRadius(value);
202                 return this.Radius();
203             }
204 
205             if (this.method === "twoPoints") {
206                 if (!this.center.testIfFinite() || !this.point2.testIfFinite()) {
207                     return NaN;
208                 }
209 
210                 return this.center.distance(this.point2);
211             }
212 
213             if (this.method === "pointRadius") {
214                 return Math.abs(this.updateRadius());
215             }
216 
217             return NaN;
218         },
219 
220         // The central projection of a sphere is an ellipse. The front and back
221         // points of the sphere---that is, the points closest to and furthest
222         // from the screen---project to the foci of the ellipse.
223         //
224         // To see this, look at the cone tangent to the sphere whose tip is at
225         // the camera. The image of the sphere is the ellipse where this cone
226         // intersects the screen. By acting on the sphere with scalings centered
227         // on the camera, you can send it to either of the Dandelin spheres that
228         // touch the screen at the foci of the image ellipse.
229         //
230         // This factory method produces two functions, `focusFn(-1)` and
231         // `focusFn(1)`, that evaluate to the projections of the front and back
232         // points of the sphere, respectively.
233         focusFn: function (sgn) {
234             var that = this;
235 
236             return function () {
237                 var camDir = that.view.boxToCam[3],
238                     r = that.Radius();
239 
240                 return that.view.project3DTo2D([
241                     that.center.X() + sgn * r * camDir[1],
242                     that.center.Y() + sgn * r * camDir[2],
243                     that.center.Z() + sgn * r * camDir[3]
244                 ]).slice(1, 3);
245             };
246         },
247 
248         innerVertexFn: function () {
249             var that = this;
250 
251             return function () {
252                 var view = that.view,
253                     p = view.worldToFocal(that.center.coords, false),
254                     distOffAxis = Mat.hypot(p[0], p[1]),
255                     cam = view.boxToCam,
256                     r = that.Radius(),
257                     angleOffAxis = Math.atan(-distOffAxis / p[2]),
258                     steepness = Math.acos(r / Mat.norm(p)),
259                     lean = angleOffAxis + steepness,
260                     cos_lean = Math.cos(lean),
261                     sin_lean = Math.sin(lean),
262                     inward;
263 
264                 if (distOffAxis > 1e-8) {
265                     // if the center of the sphere isn't too close to the camera
266                     // axis, find the direction in plane of the screen that
267                     // points from the center of the sphere toward the camera
268                     // axis
269                     inward = [
270                         -(p[0] * cam[1][1] + p[1] * cam[2][1]) / distOffAxis,
271                         -(p[0] * cam[1][2] + p[1] * cam[2][2]) / distOffAxis,
272                         -(p[0] * cam[1][3] + p[1] * cam[2][3]) / distOffAxis
273                     ];
274                 } else {
275                     // if the center of the sphere is very close to the camera
276                     // axis, choose an arbitrary unit vector in the plane of the
277                     // screen
278                     inward = [cam[1][1], cam[1][2], cam[1][3]];
279                 }
280                 return view.project3DTo2D([
281                     that.center.X() + r * (sin_lean * inward[0] + cos_lean * cam[3][1]),
282                     that.center.Y() + r * (sin_lean * inward[1] + cos_lean * cam[3][2]),
283                     that.center.Z() + r * (sin_lean * inward[2] + cos_lean * cam[3][3])
284                 ]);
285             };
286         },
287 
288         buildCentralProjection: function (attr) {
289             var view = this.view,
290                 auxStyle = { visible: false, withLabel: false },
291                 frontFocus = view.create('point', this.focusFn(-1), auxStyle),
292                 backFocus = view.create('point', this.focusFn(1), auxStyle),
293                 innerVertex = view.create('point', this.innerVertexFn(view), auxStyle);
294 
295             this.aux2D = [frontFocus, backFocus, innerVertex];
296             this.element2D = view.create('ellipse', this.aux2D, attr === undefined ? this.visProp : attr);
297         },
298 
299         buildParallelProjection: function (attr) {
300             // The parallel projection of a sphere is a circle
301             var that = this,
302                 // center2d = function () {
303                 //     var c3d = [1, that.center.X(), that.center.Y(), that.center.Z()];
304                 //     return that.view.project3DTo2D(c3d);
305                 // },
306                 radius2d = function () {
307                     var boxSize = that.view.bbox3D[0][1] - that.view.bbox3D[0][0];
308                     return that.Radius() * that.view.size[0] / boxSize;
309                 };
310 
311             this.aux2D = [];
312             this.element2D = this.view.create(
313                 'circle',
314                 // [center2d, radius2d],
315                 [that.center.element2D, radius2d],
316                 attr === undefined ? this.visProp : attr
317             );
318         },
319 
320         // replace our 2D representation with a new one that's consistent with
321         // the view's current projection type
322         rebuildProjection: function (attr) {
323             var i;
324 
325             // remove the old 2D representation from the scene tree
326             if (this.element2D) {
327                 this.view.board.removeObject(this.element2D);
328                 for (i in this.aux2D) {
329                     if (this.aux2D.hasOwnProperty(i)) {
330                         this.view.board.removeObject(this.aux2D[i]);
331                     }
332                 }
333             }
334 
335             // build a new 2D representation. the representation is stored in
336             // `this.element2D`, and any auxiliary elements are stored in
337             // `this.aux2D`
338             this.projectionType = this.view.projectionType;
339             if (this.projectionType === 'central') {
340                 this.buildCentralProjection(attr);
341             } else {
342                 this.buildParallelProjection(attr);
343             }
344 
345             // attach the new 2D representation to the scene tree
346             this.addChild(this.element2D);
347             this.inherits.push(this.element2D);
348             this.element2D.view = this.view;
349         },
350 
351         // Already documented in element3d.js
352         projectCoords: function(p, params) {
353             var r = this.Radius(),
354                 pp = [1].concat(p),
355                 c = this.center.coords,
356                 d = Geometry.distance(c, pp, 4),
357                 v = Stat.subtract(pp, c);
358 
359             if (d === 0) {
360                 // p is at the center, take an arbitrary point on sphere
361                 params[0] = 0;
362                 params[1] = 0;
363                 return [1, r, 0, 0];
364             }
365             if (r === 0) {
366                 params[0] = 0;
367                 params[1] = 0;
368                 return this.center.coords;
369             }
370 
371             d = r / d;
372             v[0] = 1;
373             v[1] *= d;
374             v[2] *= d;
375             v[3] *= d;
376 
377             // Preimage of the new position
378             params[1] = Math.atan2(v[2], v[1]);
379             params[1] += (params[1] < 0) ? Math.PI : 0;
380             if (params[1] !== 0) {
381                 params[0] = Math.atan2(v[2], v[3] * Math.sin(params[1]));
382             } else {
383                 params[0] = Math.atan2(v[1], v[3] * Math.cos(params[1]));
384             }
385             params[0] += (params[0] < 0) ? 2 * Math.PI : 0;
386 
387             return v;
388         }
389 
390         // projectScreenCoords: function (pScr, params) {
391         //     if (params.length === 0) {
392         //         params.unshift(
393         //             0.5 * (this.range_u[0] + this.range_u[1]),
394         //             0.5 * (this.range_v[0] + this.range_v[1])
395         //         );
396         //     }
397         //     return Geometry.projectScreenCoordsToParametric(pScr, this, params);
398         // }
399     }
400 );
401 
402 /**
403  * @class A sphere in a 3D view.
404  * A sphere consists of all points with a given distance from a given point.
405  * The given point is called the center, and the given distance is called the radius.
406  * 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).
407  * If the radius is a negative value, its absolute value is taken.
408  *
409  * @pseudo
410  * @name Sphere3D
411  * @augments JXG.Sphere3D
412  * @constructor
413  * @type JXG.Sphere3D
414  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
415  * @param {JXG.Point3D_number,JXG.Point3D} center,radius The center must be given as a {@link JXG.Point3D} (see {@link JXG.providePoints3D}),
416  * but the radius can be given as a number (which will create a sphere with a fixed radius) or another {@link JXG.Point3D}.
417  * <p>
418  * If the radius is supplied as number or the output of a function, its absolute value is taken.
419  *
420  * @example
421  * var view = board.create(
422  *     'view3d',
423  *     [[-6, -3], [8, 8],
424  *     [[0, 3], [0, 3], [0, 3]]],
425  *     {
426  *         xPlaneRear: {fillOpacity: 0.2, gradient: null},
427  *         yPlaneRear: {fillOpacity: 0.2, gradient: null},
428  *         zPlaneRear: {fillOpacity: 0.2, gradient: null}
429  *     }
430  * );
431  *
432  * // Two points
433  * var center = view.create(
434  *     'point3d',
435  *     [1.5, 1.5, 1.5],
436  *     {
437  *         withLabel: false,
438  *         size: 5,
439  *    }
440  * );
441  * var point = view.create(
442  *     'point3d',
443  *     [2, 1.5, 1.5],
444  *     {
445  *         withLabel: false,
446  *         size: 5
447  *    }
448  * );
449  *
450  * // Sphere
451  * var sphere = view.create(
452  *     'sphere3d',
453  *     [center, point],
454  *     {}
455  * );
456  *
457  * </pre><div id="JXG5969b83c-db67-4e62-9702-d0440e5fe2c1" class="jxgbox" style="width: 300px; height: 300px;"></div>
458  * <script type="text/javascript">
459  *     (function() {
460  *         var board = JXG.JSXGraph.initBoard('JXG5969b83c-db67-4e62-9702-d0440e5fe2c1',
461  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
462  *         var view = board.create(
463  *             'view3d',
464  *             [[-6, -3], [8, 8],
465  *             [[0, 3], [0, 3], [0, 3]]],
466  *             {
467  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
468  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
469  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
470  *             }
471  *         );
472  *
473  *         // Two points
474  *         var center = view.create(
475  *             'point3d',
476  *             [1.5, 1.5, 1.5],
477  *             {
478  *                 withLabel: false,
479  *                 size: 5,
480  *            }
481  *         );
482  *         var point = view.create(
483  *             'point3d',
484  *             [2, 1.5, 1.5],
485  *             {
486  *                 withLabel: false,
487  *                 size: 5
488  *            }
489  *         );
490  *
491  *         // Sphere
492  *         var sphere = view.create(
493  *             'sphere3d',
494  *             [center, point],
495  *             {}
496  *         );
497  *
498  *     })();
499  *
500  * </script><pre>
501  *
502  * @example
503  *     // Glider on sphere
504  *     var view = board.create(
505  *         'view3d',
506  *         [[-6, -3], [8, 8],
507  *         [[-3, 3], [-3, 3], [-3, 3]]],
508  *         {
509  *             depthOrder: {
510  *                 enabled: true
511  *             },
512  *             projection: 'central',
513  *             xPlaneRear: {fillOpacity: 0.2, gradient: null},
514  *             yPlaneRear: {fillOpacity: 0.2, gradient: null},
515  *             zPlaneRear: {fillOpacity: 0.2, gradient: null}
516  *         }
517  *     );
518  *
519  *     // Two points
520  *     var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2});
521  *     var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2});
522  *
523  *     // Sphere
524  *     var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8});
525  *
526  *     // Glider on sphere
527  *     var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4});
528  *     var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 });
529  *
530  * </pre><div id="JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71" class="jxgbox" style="width: 300px; height: 300px;"></div>
531  * <script type="text/javascript">
532  *     (function() {
533  *         var board = JXG.JSXGraph.initBoard('JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71',
534  *             {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false});
535  *         var view = board.create(
536  *             'view3d',
537  *             [[-6, -3], [8, 8],
538  *             [[-3, 3], [-3, 3], [-3, 3]]],
539  *             {
540  *                 depthOrder: {
541  *                     enabled: true
542  *                 },
543  *                 projection: 'central',
544  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
545  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
546  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
547  *             }
548  *         );
549  *
550  *         // Two points
551  *         var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2});
552  *         var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2});
553  *
554  *         // Sphere
555  *         var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8});
556  *
557  *         // Glider on sphere
558  *         var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4});
559  *         var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 });
560  *
561  *     })();
562  *
563  * </script><pre>
564  *
565  */
566 JXG.createSphere3D = function (board, parents, attributes) {
567     //   parents[0]: view
568     //   parents[1]: point,
569     //   parents[2]: point or radius
570 
571     var view = parents[0],
572         attr, p, point_style, provided,
573         el, i;
574 
575     attr = Type.copyAttributes(attributes, board.options, 'sphere3d');
576     p = [];
577     for (i = 1; i < parents.length; i++) {
578         if (Type.isPointType3D(board, parents[i])) {
579             if (p.length === 0) {
580                 point_style = 'center';
581             } else {
582                 point_style = 'point';
583             }
584             provided = Type.providePoints3D(view, [parents[i]], attributes, 'sphere3d', [point_style])[0];
585             if (provided === false) {
586                 throw new Error(
587                     "JSXGraph: Can't create sphere3d from this type. Please provide a point type."
588                 );
589             }
590             p.push(provided);
591         } else {
592             p.push(parents[i]);
593         }
594     }
595 
596     if (Type.isPoint3D(p[0]) && Type.isPoint3D(p[1])) {
597         // Point/Point
598         el = new JXG.Sphere3D(view, "twoPoints", p[0], p[1], attr);
599 
600         /////////////// nothing in docs suggest you can use [number, pointType]
601         // } else if (
602         //     (Type.isNumber(p[0]) || Type.isFunction(p[0]) || Type.isString(p[0])) &&
603         //     Type.isPoint3D(p[1])
604         // ) {
605         //     // Number/Point
606         //     el = new JXG.Sphere3D(view, "pointRadius", p[1], p[0], attr);
607 
608     } else if (
609         Type.isPoint3D(p[0]) &&
610         (Type.isNumber(p[1]) || Type.isFunction(p[1]) || Type.isString(p[1]))
611     ) {
612         // Point/Number
613         el = new JXG.Sphere3D(view, "pointRadius", p[0], p[1], attr);
614     } else {
615         throw new Error(
616             "JSXGraph: Can't create sphere3d with parent types '" +
617             typeof parents[1] +
618             "' and '" +
619             typeof parents[2] +
620             "'." +
621             "\nPossible parent types: [point,point], [point,number], [point,function]"
622         );
623     }
624 
625     // Build a 2D representation, and attach it to the scene tree, and update it
626     // to the correct initial state
627     // Here, element2D is created.
628     attr = el.setAttr2D(attr);
629     el.rebuildProjection(attr);
630 
631     el.element2D.prepareUpdate().update();
632     if (!board.isSuspendedUpdate) {
633         el.element2D.updateVisibility().updateRenderer();
634     }
635 
636     return el;
637 };
638 
639 JXG.registerElement("sphere3d", JXG.createSphere3D);
640