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