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