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