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