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 import Geometry from '../math/geometry.js'; 37 38 /** 39 * In 3D space, a circle consists of all points on a given plane with a given distance from a given point. The given point is called the center, and the given distance is called the radius. 40 * A circle can be constructed by providing a center, a normal vector, and a radius (given as a number or function). 41 * @class Creates a new 3D circle object. Do not use this constructor to create a 3D circle. Use {@link JXG.View3D#create} with 42 * type {@link Circle3D} instead. 43 * @constructor 44 * @augments JXG.Curve3D 45 * @augments JXG.GeometryElement 46 * @param {JXG.View3D} view The 3D view the circle is drawn on. 47 * @param {JXG.Point} center The center of the circle. 48 * @param {Array} normal A normal vector of the plane the circle lies in. Must be either an array of three numbers or an array of three functions returning numbers. 49 * @param {Number|Function} radius The radius of the circle. 50 * @param {Object} attributes 51 * @see JXG.Board#generateName 52 */ 53 JXG.Circle3D = function (view, center, normal, radius, attributes) { 54 var altFrame1, that; 55 56 this.constructor(view.board, attributes, Const.OBJECT_TYPE_CIRCLE3D, Const.OBJECT_CLASS_3D); 57 this.constructor3D(view, "circle3d"); 58 59 /** 60 * The circle's center. Do not set this parameter directly, as that will break JSXGraph's update system. 61 * @type JXG.Point3D 62 */ 63 this.center = this.board.select(center); 64 65 this.normalFunc = normal; 66 67 /** 68 * A normal vector of the plane the circle lies in. Do not set this parameter directly, as that will break JSXGraph's update system. 69 * @type Array 70 * @private 71 * 72 * @see updateNormal 73 */ 74 this.normal = [0, 0, 0, 0]; 75 76 /** 77 * The circle's underlying Curve3D. 78 */ 79 this.curve; 80 81 /** 82 * The first vector in an orthonormal frame for the plane the circle lies in. 83 * Do not set this parameter directly, as that will break JSXGraph's update system. 84 * @type Array 85 * @private 86 * 87 * @see updateFrame 88 */ 89 this.frame1; 90 91 /** 92 * The second vector in an orthonormal frame for the plane the circle lies in. 93 * Do not set this parameter directly, as that will break JSXGraph's update system. 94 * @type Array 95 * @private 96 * 97 * @see updateFrame 98 */ 99 this.frame2; 100 101 // place the circle or its center---whichever is newer---in the scene tree 102 if (Type.exists(this.center._is_new)) { 103 this.addChild(this.center); 104 delete this.center._is_new; 105 } else { 106 this.center.addChild(this); 107 } 108 109 // Converts JessieCode syntax into JavaScript syntax and generally ensures that the radius is a function 110 this.updateRadius = Type.createFunction(radius, this.board); 111 this.addParentsFromJCFunctions([this.updateRadius]); 112 113 // initialize normal 114 this.updateNormal(); 115 116 // initialize the first frame vector by taking the cross product with 117 // [1, 0, 0] or [-0.5, sqrt(3)/2, 0]---whichever is further away on the unit 118 // sphere. every vector is at least 60 degrees from one of these, which 119 // should be good enough to make the frame vector numerically accurate 120 this.frame1 = Mat.crossProduct(this.normal.slice(1), [1, 0, 0]); 121 this.frame1.unshift(0); 122 altFrame1 = Mat.crossProduct(this.normal.slice(1), [-0.5, 0.8660254037844386, 0]); // [1/2, sqrt(3)/2, 0] 123 altFrame1.unshift(0); 124 if (Mat.norm(altFrame1) > Mat.norm(this.frame1)) { 125 this.frame1 = altFrame1; 126 } 127 128 // initialize the second frame vector 129 this.frame2 = Mat.crossProduct(this.normal.slice(1), this.frame1.slice(1)); 130 this.frame2.unshift(0); 131 132 // scale both frame vectors to unit length 133 this.normalizeFrame(); 134 135 // create the underlying curve 136 that = this; 137 this.curve = view.create( 138 'curve3d', 139 [ 140 function(t) { 141 var r = that.Radius(), 142 s = Math.sin(t), 143 c = Math.cos(t); 144 145 return [ 146 that.center.coords[1] + r * (c * that.frame1[1] + s * that.frame2[1]), 147 that.center.coords[2] + r * (c * that.frame1[2] + s * that.frame2[2]), 148 that.center.coords[3] + r * (c * that.frame1[3] + s * that.frame2[3]) 149 ]; 150 }, 151 [0, 2 * Math.PI] // parameter range 152 ], 153 attributes 154 ); 155 }; 156 JXG.Circle3D.prototype = new JXG.GeometryElement(); 157 Type.copyPrototypeMethods(JXG.Circle3D, JXG.GeometryElement3D, "constructor3D"); 158 159 JXG.extend( 160 JXG.Circle3D.prototype, 161 /** @lends JXG.Circle3D.prototype */ { 162 163 // Already documented in element3d.js 164 update: function () { 165 if (this.needsUpdate) { 166 this.updateNormal() 167 .updateFrame(); 168 169 this.curve.visProp.visible = !isNaN(this.Radius()); // TODO 170 } 171 return this; 172 }, 173 174 // Already documented in element3d.js 175 updateRenderer: function () { 176 this.needsUpdate = false; 177 return this; 178 }, 179 180 /** 181 * Set a new radius, then update the board. 182 * @param {String|Number|function} r A string, function or number describing the new radius 183 * @returns {JXG.Circle3D} Reference to this sphere 184 */ 185 setRadius: function (r) { 186 this.updateRadius = Type.createFunction(r, this.board); 187 this.addParentsFromJCFunctions([this.updateRadius]); 188 this.board.update(); 189 190 return this; 191 }, 192 193 /** 194 * Calculates the radius of the circle. 195 * @param {String|Number|function} [value] Set new radius 196 * @returns {Number} The radius of the circle 197 */ 198 Radius: function (value) { 199 if (Type.exists(value)) { 200 this.setRadius(value); 201 return this.Radius(); 202 } 203 204 return Math.abs(this.updateRadius()); 205 }, 206 207 normalizeFrame: function () { 208 // normalize frame 209 var len1 = Mat.norm(this.frame1), 210 len2 = Mat.norm(this.frame2), 211 i; 212 213 for (i = 0; i < 4; i++) { 214 this.frame1[i] /= len1; 215 this.frame2[i] /= len2; 216 } 217 218 return this; 219 }, 220 221 updateNormal: function () { 222 // evaluate normal direction 223 var i, len, 224 eps = 1.e-12; 225 226 this.normal = Type.evaluate(this.normalFunc); 227 228 // scale normal to unit length 229 len = Mat.norm(this.normal); 230 if (Math.abs(len) > eps) { 231 for (i = 0; i < 4; i++) { 232 this.normal[i] /= len; 233 } 234 } 235 236 return this; 237 }, 238 239 updateFrame: function () { 240 this.frame1 = Mat.crossProduct(this.frame2.slice(1), this.normal.slice(1)); 241 this.frame1.unshift(0); 242 this.frame2 = Mat.crossProduct(this.normal.slice(1), this.frame1.slice(1)); 243 this.frame2.unshift(0); 244 this.normalizeFrame(); 245 246 return this; 247 }, 248 249 // Already documented in element3d.js 250 projectCoords: function (p, params) { 251 // we have to call `this.curve.projectCoords`, i.e. the curve's projectCoords rather 252 // than the circle's, to make `this` refer to the curve within the 253 // call. 254 return this.curve.projectCoords(p, params); 255 } 256 257 // projectScreenCoords: function (pScr, params) { 258 // // we have to call `this.curve.projectScreenCoords` from the curve, 259 // // rather than the circle, to make `this` refer to the curve within 260 // // the call 261 // return this.curve.projectScreenCoords(pScr, params); 262 // } 263 } 264 ); 265 266 /** 267 * @class A circle in 3D can be defined by various combinations of points and numbers. 268 * @pseudo 269 * @description In 3D space, a circle consists of all points on a given plane with a given distance from a given point. The given point is called the center, and the given distance is called the radius. 270 * A circle can be constructed by providing a center, a normal vector, and a radius (given as a number or function). 271 * <p> 272 * If the radius has a negative value, its absolute value is taken. If the radius evaluates to NaN, 273 * the circle is not displayed. This is convenient for constructing an intersection circle, which is empty when its parents do not intersect. 274 * @name Circle3D 275 * @augments JXG.Circle3D 276 * @constructor 277 * @type JXG.Circle3D 278 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 279 * @param {JXG.Point,Array,Function_Array,Function_Number,Function} center,normal,radius The center must be given as a {@link JXG.Point}, array or function (see {@link JXG.providePoints}). 280 * The normal vector can be given as an array of four numbers (i.e. homogeneous coordinates [0, x, y, z]) or a function returning an array of length 4 281 * and the radius can be given as a number (which will create a circle with a fixed radius) or a function. 282 * <p> 283 * If the radius is supplied as a number or the output of a function, its absolute value is taken. When the radius evaluates to NaN, the circle does not display. 284 */ 285 JXG.createCircle3D = function (board, parents, attributes) { 286 var view = parents[0], 287 attr = Type.copyAttributes(attributes, board.options, 'circle3d'), 288 center = Type.providePoints3D(view, [parents[1]], attributes, 'circle3d', ['point'])[0], 289 normal = parents[2], 290 radius = parents[3], 291 el; 292 293 // Create element 294 el = new JXG.Circle3D(view, center, normal, radius, attr); 295 296 // Update scene tree 297 el.curve.addParents([el]); 298 el.addChild(el.curve); 299 300 el.update(); 301 return el; 302 }; 303 304 JXG.registerElement("circle3d", JXG.createCircle3D); 305 306 /** 307 * @class The circle that is the intersection of two elements (plane3d or sphere3d) in 3D. 308 * 309 * @pseudo 310 * @name IntersectionCircle3D 311 * @augments JXG.Circle3D 312 * @constructor 313 * @type JXG.Circle3D 314 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 315 * @param {JXG.Sphere3D_JXG.Sphere3D|JXG.Plane3D} el1,el2 The result will be the intersection of el1 and el2. 316 * @example 317 * // Create the intersection circle of two spheres 318 * var view = board.create( 319 * 'view3d', 320 * [[-6, -3], [8, 8], 321 * [[0, 3], [0, 3], [0, 3]]], 322 * { 323 * xPlaneRear: {fillOpacity: 0.2, gradient: null}, 324 * yPlaneRear: {fillOpacity: 0.2, gradient: null}, 325 * zPlaneRear: {fillOpacity: 0.2, gradient: null} 326 * } 327 * ); 328 * var a1 = view.create('point3d', [-1, 0, 0]); 329 * var a2 = view.create('point3d', [1, 0, 0]); 330 * 331 * var s1 = view.create( 332 * 'sphere3d', 333 * [a1, 2], 334 * {fillColor: '#00ff80'} 335 * ); 336 * var s2 = view.create( 337 * 'sphere3d', 338 * [a2, 2], 339 * {fillColor: '#ff0000'} 340 * ); 341 * 342 * var i = view.create('intersectioncircle3d', [s1, s2]); 343 * 344 * </pre><div id="JXG64ede949-8dd6-44d0-b2a9-248a479d3a5d" class="jxgbox" style="width: 300px; height: 300px;"></div> 345 * <script type="text/javascript"> 346 * (function() { 347 * var board = JXG.JSXGraph.initBoard('JXG64ede949-8dd6-44d0-b2a9-248a479d3a5d', 348 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 349 * var view = board.create( 350 * 'view3d', 351 * [[-6, -3], [8, 8], 352 * [[0, 3], [0, 3], [0, 3]]], 353 * { 354 * xPlaneRear: {fillOpacity: 0.2, gradient: null}, 355 * yPlaneRear: {fillOpacity: 0.2, gradient: null}, 356 * zPlaneRear: {fillOpacity: 0.2, gradient: null} 357 * } 358 * ); 359 * var a1 = view.create('point3d', [-1, 0, 0]); 360 * var a2 = view.create('point3d', [1, 0, 0]); 361 * 362 * var s1 = view.create( 363 * 'sphere3d', 364 * [a1, 2], 365 * {fillColor: '#00ff80'} 366 * ); 367 * var p2 = view.create( 368 * 'sphere3d', 369 * [a2, 2], 370 * {fillColor: '#ff0000'} 371 * ); 372 * 373 * })(); 374 * 375 * </script><pre> 376 * 377 */ 378 JXG.createIntersectionCircle3D = function (board, parents, attributes) { 379 var view = parents[0], 380 el1 = parents[1], 381 el2 = parents[2], 382 ixnCircle, center, func, 383 attr = Type.copyAttributes(attributes, board.options, "intersectioncircle3d"); 384 385 func = Geometry.intersectionFunction3D(view, el1, el2); 386 center = view.create('point3d', func[0], { visible: false }); 387 ixnCircle = view.create('circle3d', [center, func[1], func[2]], attr); 388 389 try { 390 el1.addChild(ixnCircle); 391 el2.addChild(ixnCircle); 392 } catch (e) { 393 throw new Error( 394 "JSXGraph: Can't create 'intersection' with parent types '" + 395 typeof parents[1] + 396 "' and '" + 397 typeof parents[2] + 398 "'." 399 ); 400 } 401 402 ixnCircle.type = Const.OBJECT_TYPE_INTERSECTION_CIRCLE3D; 403 ixnCircle.elType = 'intersectioncircle3d'; 404 ixnCircle.setParents([el1.id, el2.id]); 405 406 return ixnCircle; 407 }; 408 409 JXG.registerElement('intersectioncircle3d', JXG.createIntersectionCircle3D); 410