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 
 37 /**
 38  * A sphere consists of all points with a given distance from a given point.
 39  * The given point is called the center, and the given distance is called the radius.
 40  * 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).
 41  * @class Creates a new 3D sphere object. Do not use this constructor to create a 3D sphere. Use {@link JXG.View3D#create} with
 42  * type {@link Sphere3D} instead.
 43  * @augments JXG.GeometryElement3D
 44  * @augments JXG.GeometryElement
 45  * @param {JXG.View3D} view The 3D view the sphere is drawn on.
 46  * @param {String} method Can be:
 47  * <ul><li> <b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
 48  * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
 49  * The parameters <code>p1</code>, <code>p2</code> and <code>radius</code> must be set according to this method parameter.
 50  * @param {JXG.Point3D} par1 The center of the sphere.
 51  * @param {JXG.Point3D} par2 Can be:
 52  * <ul><li>A point on the sphere (if the construction method is <code>'twoPoints'</code>)</li>
 53  * <ul><li>A number or function (if the construction method is <code>'pointRadius'</code>)</li>
 54  * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and
 55  * {@link JXG.Options#elements}, and optional a name and an id.
 56  * @see JXG.Board#generateName
 57  */
 58 JXG.Sphere3D = function (view, method, par1, par2, attributes) {
 59     this.constructor(view.board, attributes, Const.OBJECT_TYPE_SPHERE3D, Const.OBJECT_CLASS_3D);
 60     this.constructor3D(view, "sphere3d");
 61 
 62     this.board.finalizeAdding(this);
 63 
 64     /**
 65      * The construction method.
 66      * Can be:
 67      * <ul><li><b><code>'twoPoints'</code></b> – The sphere is defined by its center and a point on the sphere.</li>
 68      * <li><b><code>'pointRadius'</code></b> – The sphere is defined by its center and its radius in user units.</li></ul>
 69      * @type String
 70      * @see JXG.Sphere3D#center
 71      * @see JXG.Sphere3D#point2
 72      */
 73     this.method = method;
 74 
 75     /**
 76      * The sphere's center. Do not set this parameter directly, as that will break JSXGraph's update system.
 77      * @type JXG.Point3D
 78      */
 79     this.center = this.board.select(par1);
 80 
 81     /**
 82      * 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.
 83      * @type JXG.Point3D
 84      * @see JXG.Sphere3D#method
 85      */
 86     this.point2 = null;
 87 
 88     this.points = [];
 89 
 90     /**
 91      * The 2D representation of the element.
 92      * @type GeometryElement
 93      */
 94     this.element2D = null;
 95 
 96     /**
 97      * Elements supporting the 2D representation.
 98      * @type Array
 99      * @private
100      */
101     this.aux2D = [];
102 
103     /**
104      * The type of projection (<code>'parallel'</code> or <code>'central'</code>) that the sphere is currently drawn in.
105      * @type String
106      */
107     this.projectionType = view.projectionType;
108 
109     if (method === "twoPoints") {
110         this.point2 = this.board.select(par2);
111         this.radius = this.Radius();
112     } else if (method === "pointRadius") {
113         // Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function
114         this.updateRadius = Type.createFunction(par2, this.board);
115         // First evaluation of the radius function
116         this.updateRadius();
117         this.addParentsFromJCFunctions([this.updateRadius]);
118     }
119 
120     if (Type.exists(this.center._is_new)) {
121         this.addChild(this.center);
122         delete this.center._is_new;
123     } else {
124         this.center.addChild(this);
125     }
126 
127     if (method === "twoPoints") {
128         if (Type.exists(this.point2._is_new)) {
129             this.addChild(this.point2);
130             delete this.point2._is_new;
131         } else {
132             this.point2.addChild(this);
133         }
134     }
135 
136     this.methodMap = Type.deepCopy(this.methodMap, {
137         center: "center",
138         point2: "point2",
139         Radius: "Radius"
140     });
141 };
142 JXG.Sphere3D.prototype = new JXG.GeometryElement();
143 Type.copyPrototypeMethods(JXG.Sphere3D, JXG.GeometryElement3D, "constructor3D");
144 
145 JXG.extend(
146     JXG.Sphere3D.prototype,
147     /** @lends JXG.Sphere3D.prototype */ {
148         update: function () {
149             if (this.projectionType !== this.view.projectionType) {
150                 this.rebuildProjection();
151             }
152             return this;
153         },
154 
155         updateRenderer: function () {
156             this.needsUpdate = false;
157             return this;
158         },
159 
160         /**
161          * Set a new radius, then update the board.
162          * @param {String|Number|function} r A string, function or number describing the new radius
163          * @returns {JXG.Sphere3D} Reference to this sphere
164          */
165         setRadius: function (r) {
166             this.updateRadius = Type.createFunction(r, this.board);
167             this.addParentsFromJCFunctions([this.updateRadius]);
168             this.board.update();
169 
170             return this;
171         },
172 
173         /**
174          * Calculates the radius of the circle.
175          * @param {String|Number|function} [value] Set new radius
176          * @returns {Number} The radius of the circle
177          */
178         Radius: function (value) {
179             if (Type.exists(value)) {
180                 this.setRadius(value);
181                 return this.Radius();
182             }
183 
184             if (this.method === "twoPoints") {
185                 if (this.center.isIllDefined() || this.point2.isIllDefined()) {
186                     return NaN;
187                 }
188 
189                 return this.center.distance(this.point2);
190             }
191 
192             if (this.method === "pointRadius") {
193                 return Math.abs(this.updateRadius());
194             }
195 
196             return NaN;
197         },
198 
199         // The central projection of a sphere is an ellipse. The front and back
200         // points of the sphere---that is, the points closest to and furthest
201         // from the screen---project to the foci of the ellipse.
202         //
203         // To see this, look at the cone tangent to the sphere whose tip is at
204         // the camera. The image of the sphere is the ellipse where this cone
205         // intersects the screen. By acting on the sphere with scalings centered
206         // on the camera, you can send it to either of the Dandelin spheres that
207         // touch the screen at the foci of the image ellipse.
208         //
209         // This factory method produces two functions, `focusFn(-1)` and
210         // `focusFn(1)`, that evaluate to the projections of the front and back
211         // points of the sphere, respectively.
212         focusFn: function (sgn) {
213             var that = this;
214 
215             return function () {
216                 var camDir = that.view.boxToCam[3],
217                     r = that.Radius();
218 
219                 return that.view.project3DTo2D([
220                     that.center.X() + sgn * r * camDir[1],
221                     that.center.Y() + sgn * r * camDir[2],
222                     that.center.Z() + sgn * r * camDir[3]
223                 ]).slice(1, 3);
224             };
225         },
226 
227         innerVertexFn: function () {
228             var that = this;
229 
230             return function () {
231                 var view = that.view,
232                     p = view.worldToFocal(that.center.coords, false),
233                     distOffAxis = Mat.hypot(p[0], p[1]),
234                     cam = view.boxToCam,
235                     inward = [
236                         -(p[0] * cam[1][1] + p[1] * cam[2][1]) / distOffAxis,
237                         -(p[0] * cam[1][2] + p[1] * cam[2][2]) / distOffAxis,
238                         -(p[0] * cam[1][3] + p[1] * cam[2][3]) / distOffAxis
239                     ],
240                     r = that.Radius(),
241                     angleOffAxis = Math.atan(-distOffAxis / p[2]),
242                     steepness = Math.acos(r / Mat.norm(p)),
243                     lean = angleOffAxis + steepness,
244                     cos_lean = Math.cos(lean),
245                     sin_lean = Math.sin(lean);
246 
247                 return view.project3DTo2D([
248                     that.center.X() + r * (sin_lean * inward[0] + cos_lean * cam[3][1]),
249                     that.center.Y() + r * (sin_lean * inward[1] + cos_lean * cam[3][2]),
250                     that.center.Z() + r * (sin_lean * inward[2] + cos_lean * cam[3][3])
251                 ]);
252             };
253         },
254 
255         buildCentralProjection: function () {
256             var view = this.view,
257                 auxStyle = { visible: false, withLabel: false },
258                 frontFocus = view.create('point', this.focusFn(-1), auxStyle),
259                 backFocus = view.create('point', this.focusFn(1), auxStyle),
260                 innerVertex = view.create('point', this.innerVertexFn(view), auxStyle);
261 
262             this.aux2D = [frontFocus, backFocus, innerVertex];
263             this.element2D = view.create('ellipse', this.aux2D, this.visProp);
264         },
265 
266         buildParallelProjection: function () {
267             // The parallel projection of a sphere is a circle
268             var that = this,
269                 center2d = function () {
270                     var c3d = [1, that.center.X(), that.center.Y(), that.center.Z()];
271                     return that.view.project3DTo2D(c3d);
272                 },
273                 radius2d = function () {
274                     var boxSize = that.view.bbox3D[0][1] - that.view.bbox3D[0][0];
275                     return that.Radius() * that.view.size[0] / boxSize;
276                 };
277 
278             this.aux2D = [];
279             this.element2D = this.view.create(
280                 'circle',
281                 [center2d, radius2d],
282                 this.visProp
283             );
284         },
285 
286         // replace our 2D representation with a new one that's consistent with
287         // the view's current projection type
288         rebuildProjection: function () {
289             var i;
290 
291             // remove the old 2D representation from the scene tree
292             if (this.element2D) {
293                 this.view.board.removeObject(this.element2D);
294                 for (i in this.aux2D) {
295                     if (this.aux2D.hasOwnProperty(i)) {
296                         this.view.board.removeObject(this.aux2D[i]);
297                     }
298                 }
299             }
300 
301             // build a new 2D representation. the representation is stored in
302             // `this.element2D`, and any auxiliary elements are stored in
303             // `this.aux2D`
304             this.projectionType = this.view.projectionType;
305             if (this.projectionType === 'central') {
306                 this.buildCentralProjection();
307             } else {
308                 this.buildParallelProjection();
309             }
310 
311             // attach the new 2D representation to the scene tree
312             this.addChild(this.element2D);
313             this.inherits.push(this.element2D);
314             this.element2D.view = this.view;
315         }
316     }
317 );
318 
319 /**
320  * @class This element is used to provide a constructor for a sphere.
321  *
322  * @pseudo
323  * @description
324  * A sphere consists of all points with a given distance from a given point.
325  * The given point is called the center, and the given distance is called the radius.
326  * 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).
327  * If the radius is a negative value, its absolute value is taken.
328  *
329  * @name Sphere3D
330  * @augments JXG.Sphere3D
331  * @constructor
332  * @type JXG.Sphere3D
333  * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown.
334  * @param {JXG.Point3D_number,JXG.Point3D} center,radius The center must be given as a {@link JXG.Point3D} (see {@link JXG.providePoints3D}),
335  * but the radius can be given as a number (which will create a sphere with a fixed radius) or another {@link JXG.Point3D}.
336  * <p>
337  * If the radius is supplied as number or the output of a function, its absolute value is taken.
338  *
339  * @example
340  * var view = board.create(
341  *     'view3d',
342  *     [[-6, -3], [8, 8],
343  *     [[0, 3], [0, 3], [0, 3]]],
344  *     {
345  *         xPlaneRear: {fillOpacity: 0.2, gradient: null},
346  *         yPlaneRear: {fillOpacity: 0.2, gradient: null},
347  *         zPlaneRear: {fillOpacity: 0.2, gradient: null}
348  *     }
349  * );
350  *
351  * // Two points
352  * var center = view.create(
353  *     'point3d',
354  *     [1.5, 1.5, 1.5],
355  *     {
356  *         withLabel: false,
357  *         size: 5,
358  *    }
359  * );
360  * var point = view.create(
361  *     'point3d',
362  *     [2, 1.5, 1.5],
363  *     {
364  *         withLabel: false,
365  *         size: 5
366  *    }
367  * );
368  *
369  * // Sphere
370  * var sphere = view.create(
371  *     'sphere3d',
372  *     [center, point],
373  *     {}
374  * );
375  *
376  * </pre><div id="JXG5969b83c-db67-4e62-9702-d0440e5fe2c1" class="jxgbox" style="width: 300px; height: 300px;"></div>
377  * <script type="text/javascript">
378  *     (function() {
379  *         var board = JXG.JSXGraph.initBoard('JXG5969b83c-db67-4e62-9702-d0440e5fe2c1',
380  *             {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false});
381  *         var view = board.create(
382  *             'view3d',
383  *             [[-6, -3], [8, 8],
384  *             [[0, 3], [0, 3], [0, 3]]],
385  *             {
386  *                 xPlaneRear: {fillOpacity: 0.2, gradient: null},
387  *                 yPlaneRear: {fillOpacity: 0.2, gradient: null},
388  *                 zPlaneRear: {fillOpacity: 0.2, gradient: null}
389  *             }
390  *         );
391  *
392  *         // Two points
393  *         var center = view.create(
394  *             'point3d',
395  *             [1.5, 1.5, 1.5],
396  *             {
397  *                 withLabel: false,
398  *                 size: 5,
399  *            }
400  *         );
401  *         var point = view.create(
402  *             'point3d',
403  *             [2, 1.5, 1.5],
404  *             {
405  *                 withLabel: false,
406  *                 size: 5
407  *            }
408  *         );
409  *
410  *         // Sphere
411  *         var sphere = view.create(
412  *             'sphere3d',
413  *             [center, point],
414  *             {}
415  *         );
416  *
417  *     })();
418  *
419  * </script><pre>
420  *
421  */
422 JXG.createSphere3D = function (board, parents, attributes) {
423     //   parents[0]: view
424     //   parents[1]: point,
425     //   parents[2]: point or radius
426 
427     var view = parents[0],
428         attr, p, point_style, provided,
429         el, i;
430 
431     attr = Type.copyAttributes(attributes, board.options, 'sphere3d');
432 
433     p = [];
434     for (i = 1; i < parents.length; i++) {
435         if (Type.isPointType3D(board, parents[i])) {
436             if (p.length === 0) {
437                 point_style = 'center';
438             } else {
439                 point_style = 'point';
440             }
441             provided = Type.providePoints3D(view, [parents[i]], attributes, 'sphere3d', [point_style])[0];
442             if (provided === false) {
443                 throw new Error(
444                     "JSXGraph: Can't create sphere3d from this type. Please provide a point type."
445                 );
446             }
447             p.push(provided);
448         } else {
449             p.push(parents[i]);
450         }
451     }
452 
453     if (Type.isPoint3D(p[0]) && Type.isPoint3D(p[1])) {
454         // Point/Point
455         el = new JXG.Sphere3D(view, "twoPoints", p[0], p[1], attr);
456     } else if (
457         (Type.isNumber(p[0]) || Type.isFunction(p[0]) || Type.isString(p[0])) &&
458         Type.isPoint3D(p[1])
459     ) {
460         // Number/Point
461         el = new JXG.Sphere3D(view, "pointRadius", p[1], p[0], attr);
462     } else if (
463         (Type.isNumber(p[1]) || Type.isFunction(p[1]) || Type.isString(p[1])) &&
464         Type.isPoint3D(p[0])
465     ) {
466         // Point/Number
467         el = new JXG.Sphere3D(view, "pointRadius", p[0], p[1], attr);
468     } else {
469         throw new Error(
470             "JSXGraph: Can't create sphere3d with parent types '" +
471             typeof parents[1] +
472             "' and '" +
473             typeof parents[2] +
474             "'." +
475             "\nPossible parent types: [point,point], [point,number], [point,function]"
476         );
477     }
478 
479     // build a 2D representation, and attach it to the scene tree, and update it
480     // to the correct initial state
481     el.rebuildProjection();
482     el.element2D.prepareUpdate().update().updateRenderer();
483 
484     return el;
485 };
486 
487 JXG.registerElement("sphere3d", JXG.createSphere3D);
488