1 /* 2 Copyright 2008-2024 3 Matthias Ehmann, 4 Carsten Miller, 5 Andreas Walter, 6 Alfred Wassermann 7 8 This file is part of JSXGraph. 9 10 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 11 12 You can redistribute it and/or modify it under the terms of the 13 14 * GNU Lesser General Public License as published by 15 the Free Software Foundation, either version 3 of the License, or 16 (at your option) any later version 17 OR 18 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 19 20 JSXGraph is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 GNU Lesser General Public License for more details. 24 25 You should have received a copy of the GNU Lesser General Public License and 26 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 27 and <https://opensource.org/licenses/MIT/>. 28 */ 29 /*global JXG:true, define: true*/ 30 31 import JXG from "../jxg.js"; 32 import Const from "../base/constants.js"; 33 import Type from "../utils/type.js"; 34 import Mat from "../math/math.js"; 35 import Geometry from "../math/geometry.js"; 36 37 /** 38 * A 3D point is the basic geometric element. 39 * @class Creates a new 3D point object. Do not use this constructor to create a 3D point. Use {@link JXG.View3D#create} with 40 * type {@link Point3D} instead. 41 * @augments JXG.GeometryElement3D 42 * @augments JXG.GeometryElement 43 * @param {JXG.View3D} view The 3D view the point is drawn on. 44 * @param {Function|Array} F Array of numbers, array of functions or function returning an array with defines the user coordinates of the point. 45 * @param {JXG.GeometryElement3D} slide Object the 3D point should be bound to. If null, the point is a free point. 46 * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and 47 * {@link JXG.Options#elements}, and optional a name and an id. 48 * @see JXG.Board#generateName 49 */ 50 JXG.Point3D = function (view, F, slide, attributes) { 51 this.constructor(view.board, attributes, Const.OBJECT_TYPE_POINT3D, Const.OBJECT_CLASS_3D); 52 this.constructor3D(view, "point3d"); 53 54 this.board.finalizeAdding(this); 55 56 // add the new point to its view's point list 57 // if (view.visProp.depthorderpoints) { 58 // view.points.push(this); 59 // } 60 61 /** 62 * Homogeneous coordinates of a Point3D, i.e. array of length 4 containing numbers: [w, x, y, z]. 63 * Usually, w=1 for finite points and w=0 for points which are infinitely far. 64 * If coordinates of the point are supplied as functions, they are resolved in {@link Point3D#updateCoords} into numbers. 65 * 66 * @example 67 * p.coords; 68 * 69 * @name Point3D#coords 70 * @type Array 71 * @private 72 */ 73 this.coords = [0, 0, 0, 0]; 74 this.initialCoords = [0, 0, 0, 0]; 75 76 /** 77 * Function or array of functions or array of numbers defining the coordinates of the point, used in {@link updateCoords}. 78 * 79 * @name Point3D#F 80 * @function 81 * @private 82 * 83 * @see updateCoords 84 */ 85 this.F = F; 86 87 /** 88 * Optional slide element, i.e. element the Point3D lives on. 89 * 90 * @example 91 * p.slide; 92 * 93 * @name Point3D#slide 94 * @type JXG.GeometryElement3D 95 * @default null 96 * @private 97 * 98 */ 99 this.slide = slide; 100 101 /** 102 * In case, the point is a glider, store the preimage of the coordinates in terms of the parametric definition of the host element. 103 * That is, if the host element `slide` is a curve, and the coordinates of the point are equal to `p` and `u = this.position[0]`, then 104 * `p = [slide.X(u), slide.Y(u), slide.Z(u)]`. 105 * 106 * @type Array 107 * @private 108 */ 109 this.position = []; 110 111 this._c2d = null; 112 113 this.methodMap = Type.deepCopy(this.methodMap, { 114 // TODO 115 }); 116 }; 117 118 JXG.Point3D.prototype = new JXG.GeometryElement(); 119 Type.copyPrototypeMethods(JXG.Point3D, JXG.GeometryElement3D, "constructor3D"); 120 121 JXG.extend( 122 JXG.Point3D.prototype, 123 /** @lends JXG.Point3D.prototype */ { 124 125 /** 126 * Get x-coordinate of a 3D point. 127 * 128 * @name X 129 * @memberOf Point3D 130 * @function 131 * @returns {Number} 132 * 133 * @example 134 * p.X(); 135 */ 136 X: function () { 137 return this.coords[1]; 138 }, 139 140 /** 141 * Get y-coordinate of a 3D point. 142 * 143 * @name Y 144 * @memberOf Point3D 145 * @function 146 * @returns Number 147 * 148 * @example 149 * p.Y(); 150 */ 151 Y: function () { 152 return this.coords[2]; 153 }, 154 155 /** 156 * Get z-coordinate of a 3D point. 157 * 158 * @name Z 159 * @memberOf Point3D 160 * @function 161 * @returns Number 162 * 163 * @example 164 * p.Z(); 165 */ 166 Z: function () { 167 return this.coords[3]; 168 }, 169 170 /** 171 * Get w-coordinate of a 3D point. 172 * 173 * @name W 174 * @memberOf Point3D 175 * @function 176 * @returns Number 177 * 178 * @example 179 * p.W(); 180 */ 181 W: function () { 182 return this.coords[0]; 183 }, 184 185 /** 186 * Update the array {@link JXG.Point3D#coords} containing the homogeneous coords. 187 * 188 * @name updateCoords 189 * @memberOf Point3D 190 * @function 191 * @returns {Object} Reference to the Point3D object 192 * @private 193 * @see GeometryElement3D#update() 194 * @example 195 * p.updateCoords(); 196 */ 197 updateCoords: function () { 198 var i, 199 s = 0; 200 201 if (Type.isFunction(this.F)) { 202 this.coords = Type.evaluate(this.F); 203 if (this.coords.length === 3) { 204 this.coords.unshift(1); 205 } 206 } else { 207 if (this.F.length === 3) { 208 this.coords[0] = 1; 209 s = 1; 210 } 211 for (i = 0; i < this.F.length; i++) { 212 // Attention: if F is array of numbers, coords may not be updated. 213 // Otherwise, dragging will not work anymore. 214 if (Type.isFunction(this.F[i])) { 215 this.coords[s + i] = Type.evaluate(this.F[i]); 216 } 217 } 218 } 219 220 return this; 221 }, 222 223 /** 224 * Initialize the coords array. 225 * 226 * @private 227 * @returns {Object} Reference to the Point3D object 228 */ 229 initCoords: function () { 230 var i, 231 s = 0; 232 233 234 if (Type.isFunction(this.F)) { 235 this.coords = Type.evaluate(this.F); 236 if (this.coords.length === 3) { 237 this.coords.unshift(1); 238 } 239 } else { 240 if (this.F.length === 3) { 241 this.coords[0] = 1; 242 s = 1; 243 } 244 for (i = 0; i < this.F.length; i++) { 245 this.coords[s + i] = Type.evaluate(this.F[i]); 246 } 247 } 248 this.initialCoords = this.coords.slice(); 249 250 return this; 251 }, 252 253 /** 254 * Normalize homogeneous coordinates such the the first coordinate (the w-coordinate is equal to 1 or 0)- 255 * 256 * @name normalizeCoords 257 * @memberOf Point3D 258 * @function 259 * @returns {Object} Reference to the Point3D object 260 * @private 261 * @example 262 * p.normalizeCoords(); 263 */ 264 normalizeCoords: function () { 265 if (Math.abs(this.coords[0]) > 1.e-14) { 266 this.coords[1] /= this.coords[0]; 267 this.coords[2] /= this.coords[0]; 268 this.coords[3] /= this.coords[0]; 269 this.coords[0] = 1.0; 270 } 271 return this; 272 }, 273 274 /** 275 * Set the position of a 3D point. 276 * 277 * @name setPosition 278 * @memberOf Point3D 279 * @function 280 * @param {Array} coords 3D coordinates. Either of the form [x,y,z] (Euclidean) or [w,x,y,z] (homogeneous). 281 * @param {Boolean} [noevent] If true, no events are triggered (TODO) 282 * @returns {Object} Reference to the Point3D object 283 * 284 * @example 285 * p.setPosition([1, 3, 4]); 286 */ 287 setPosition: function (coords, noevent) { 288 var c = this.coords; 289 // oc = this.coords.slice(); // Copy of original values 290 291 if (coords.length === 3) { 292 // Euclidean coordinates 293 c[0] = 1.0; 294 c[1] = coords[0]; 295 c[2] = coords[1]; 296 c[3] = coords[2]; 297 } else { 298 // Homogeneous coordinates (normalized) 299 c[0] = coords[0]; 300 c[1] = coords[1]; 301 c[2] = coords[2]; 302 c[3] = coords[2]; 303 this.normalizeCoords(); 304 } 305 306 // console.log(el.emitter, !noevent, oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3]); 307 // Not yet working TODO 308 // if (el.emitter && !noevent && 309 // (oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3])) { 310 // this.triggerEventHandlers(['update3D'], [oc]); 311 // } 312 return this; 313 }, 314 315 // /** 316 // * Add transformations to this element. 317 // * @param {JXG.GeometryElement} el 318 // * @param {JXG.Transformation|Array} transform Either one {@link JXG.Transformation} 319 // * or an array of {@link JXG.Transformation}s. 320 // * @returns {JXG.CoordsElement} Reference to itself. 321 // */ 322 addTransform: function (el, transform) { 323 this.addTransformGeneric(el, transform); 324 return this; 325 }, 326 327 updateTransform: function () { 328 var c, i; 329 330 if (this.transformations.length === 0 || this.baseElement === null) { 331 return this; 332 } 333 334 if (this === this.baseElement) { 335 c = this.initialCoords; 336 } else { 337 c = this.baseElement.coords; 338 } 339 for (i = 0; i < this.transformations.length; i++) { 340 this.transformations[i].update(); 341 c = Mat.matVecMult(this.transformations[i].matrix, c); 342 } 343 this.coords = c; 344 345 return this; 346 }, 347 348 // Already documented in JXG.GeometryElement 349 update: function (drag) { 350 var c3d, // Homogeneous 3D coordinates 351 foot, res; 352 353 if ( 354 this.element2D.draggable() && 355 Geometry.distance(this._c2d, this.element2D.coords.usrCoords) !== 0 356 ) { 357 // Update is called from board.updateElements, e.g. after manipulating a 358 // a slider or dragging a point. 359 // Usually this followed by an update call using the other branch below. 360 if (this.view.isVerticalDrag()) { 361 // Drag the point in its vertical to the xy plane 362 // If the point is outside of bbox3d, 363 // c3d is already corrected. 364 c3d = this.view.project2DTo3DVertical(this.element2D, this.coords); 365 } else { 366 // Drag the point in its xy plane 367 foot = [1, 0, 0, this.coords[3]]; 368 c3d = this.view.project2DTo3DPlane(this.element2D, [1, 0, 0, 1], foot); 369 } 370 371 if (c3d[0] !== 0) { 372 // Check if c3d is inside of view.bbox3d 373 // Otherwise, the coords are now corrected. 374 res = this.view.project3DToCube(c3d); 375 this.coords = res[0]; 376 377 if (res[1]) { 378 // The 3D coordinates have been corrected, now 379 // also correct the 2D element. 380 this.element2D.coords.setCoordinates( 381 Const.COORDS_BY_USER, 382 this.view.project3DTo2D(this.coords) 383 ); 384 } 385 if (this.slide) { 386 this.coords = this.slide.projectCoords([this.X(), this.Y(), this.Z()], this.position); 387 this.element2D.coords.setCoordinates( 388 Const.COORDS_BY_USER, 389 this.view.project3DTo2D(this.coords) 390 ); 391 } 392 } 393 394 } else { 395 // Update 2D point from its 3D view, e.g. when rotating the view 396 this.updateCoords() 397 .updateTransform(); 398 399 if (this.slide) { 400 this.coords = this.slide.projectCoords([this.X(), this.Y(), this.Z()], this.position); 401 } 402 c3d = this.coords; 403 this.element2D.coords.setCoordinates( 404 Const.COORDS_BY_USER, 405 this.view.project3DTo2D(c3d) 406 ); 407 this.zIndex = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3]; 408 } 409 this._c2d = this.element2D.coords.usrCoords.slice(); 410 411 return this; 412 }, 413 414 // Already documented in JXG.GeometryElement 415 updateRenderer: function () { 416 this.needsUpdate = false; 417 return this; 418 }, 419 420 /** 421 * Check whether a point's position is finite, i.e. the first entry is not zero. 422 * @returns {Boolean} True if the first entry of the coordinate vector is not zero; false otherwise. 423 */ 424 testIfFinite: function () { 425 return Math.abs(this.coords[0]) > 1.e-12 ? true : false; 426 // return Type.cmpArrays(this.coords, [0, 0, 0, 0]); 427 }, 428 429 /** 430 * Calculate the distance from one point to another. If one of the points is on the plane at infinity, return positive infinity. 431 * @param {JXG.Point3D} pt The point to which the distance is calculated. 432 * @returns {Number} The distance 433 */ 434 distance: function (pt) { 435 var eps_sq = 1e-12, 436 c_this = this.coords, 437 c_pt = pt.coords; 438 439 if (c_this[0] * c_this[0] > eps_sq && c_pt[0] * c_pt[0] > eps_sq) { 440 return Mat.hypot( 441 c_pt[1] - c_this[1], 442 c_pt[2] - c_this[2], 443 c_pt[3] - c_this[3] 444 ); 445 } else { 446 return Number.POSITIVE_INFINITY; 447 } 448 }, 449 450 // Not yet working 451 __evt__update3D: function (oc) {} 452 } 453 ); 454 455 /** 456 * @class A Point3D object is defined by three coordinates [x,y,z], or a function returning an array with three numbers. 457 * Alternatively, all numbers can also be provided as functions returning a number. 458 * 459 * @pseudo 460 * @name Point3D 461 * @augments JXG.Point3D 462 * @constructor 463 * @throws {Exception} If the element cannot be constructed with the given parent 464 * objects an exception is thrown. 465 * @param {number,function_number,function_number,function_JXG.GeometryElement3D} x,y,z,[slide=undefined] The coordinates are given as x, y, z consisting of numbers or functions. If an optional 3D element "slide" is supplied, the point is a glider on that element. 466 * @param {array,function_JXG.GeometryElement3D} F,[slide=null] Alternatively, the coordinates can be supplied as 467 * <ul> 468 * <li>function returning an array [x,y,z] of length 3 of numbers or 469 * <li>array arr=[x,y,z] of length 3 consisting of numbers 470 * </ul> 471 * If an optional 3D element "slide" is supplied, the point is a glider on that element. 472 * 473 * @example 474 * var bound = [-5, 5]; 475 * var view = board.create('view3d', 476 * [[-6, -3], [8, 8], 477 * [bound, bound, bound]], 478 * {}); 479 * var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 }); 480 * var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 3, fixed: true }); 481 * var w = view.create('point3d', [ () => p.X() + 3, () => p.Y(), () => p.Z() - 2], { name:'C', size: 3, fixed: true }); 482 * 483 * </pre><div id="JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1" class="jxgbox" style="width: 300px; height: 300px;"></div> 484 * <script type="text/javascript"> 485 * (function() { 486 * var board = JXG.JSXGraph.initBoard('JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1', 487 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 488 * var bound = [-5, 5]; 489 * var view = board.create('view3d', 490 * [[-6, -3], [8, 8], 491 * [bound, bound, bound]], 492 * {}); 493 * var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 }); 494 * var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 3 }); 495 * var w = view.create('point3d', [ () => p.X() + 3, () => p.Y(), () => p.Z() - 2], { name:'C', size: 3, fixed: true }); 496 * })(); 497 * 498 * </script><pre> 499 * 500 * @example 501 * // Glider on sphere 502 * var view = board.create( 503 * 'view3d', 504 * [[-6, -3], [8, 8], 505 * [[-3, 3], [-3, 3], [-3, 3]]], 506 * { 507 * depthOrder: { 508 * enabled: true 509 * }, 510 * projection: 'central', 511 * xPlaneRear: {fillOpacity: 0.2, gradient: null}, 512 * yPlaneRear: {fillOpacity: 0.2, gradient: null}, 513 * zPlaneRear: {fillOpacity: 0.2, gradient: null} 514 * } 515 * ); 516 * 517 * // Two points 518 * var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2}); 519 * var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2}); 520 * 521 * // Sphere 522 * var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8}); 523 * 524 * // Glider on sphere 525 * var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4}); 526 * var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 }); 527 * 528 * </pre><div id="JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71" class="jxgbox" style="width: 300px; height: 300px;"></div> 529 * <script type="text/javascript"> 530 * (function() { 531 * var board = JXG.JSXGraph.initBoard('JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71', 532 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 533 * var view = board.create( 534 * 'view3d', 535 * [[-6, -3], [8, 8], 536 * [[-3, 3], [-3, 3], [-3, 3]]], 537 * { 538 * depthOrder: { 539 * enabled: true 540 * }, 541 * projection: 'central', 542 * xPlaneRear: {fillOpacity: 0.2, gradient: null}, 543 * yPlaneRear: {fillOpacity: 0.2, gradient: null}, 544 * zPlaneRear: {fillOpacity: 0.2, gradient: null} 545 * } 546 * ); 547 * 548 * // Two points 549 * var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2}); 550 * var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2}); 551 * 552 * // Sphere 553 * var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8}); 554 * 555 * // Glider on sphere 556 * var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4}); 557 * var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 }); 558 * 559 * })(); 560 * 561 * </script><pre> 562 * 563 */ 564 JXG.createPoint3D = function (board, parents, attributes) { 565 // parents[0]: view 566 // followed by 567 // parents[1]: function or array 568 // or 569 // parents[1..3]: coordinates 570 571 var view = parents[0], 572 attr, F, slide, c2d, el, 573 base = null, 574 transform = null; 575 576 // If the last element of `parents` is a 3D object, 577 // the point is a glider on that element. 578 if (parents.length > 2 && 579 Type.exists(parents[parents.length - 1].is3D) && 580 !Type.isTransformationOrArray(parents[parents.length - 1]) 581 ) { 582 slide = parents.pop(); 583 } else { 584 slide = null; 585 } 586 587 if (parents.length === 2) { 588 // [view, array|fun] (Array [x, y, z] | function) returning [x, y, z] 589 F = parents[1]; 590 } else if (parents.length === 3 && 591 Type.isPoint3D(parents[1]) && 592 Type.isTransformationOrArray(parents[2]) 593 ) { 594 F = [0, 0, 0]; 595 base = parents[1]; 596 transform = parents[2]; 597 } else if (parents.length === 4) { 598 // [view, x, y, z], (3 numbers | functions) 599 F = parents.slice(1); 600 } else if (parents.length === 5) { 601 // [view, w, x, y, z], (4 numbers | functions) 602 F = parents.slice(1); 603 } else { 604 throw new Error( 605 "JSXGraph: Can't create point3d with parent types '" + 606 typeof parents[1] + 607 "' and '" + 608 typeof parents[2] + 609 "'." + 610 "\nPossible parent types: [[x,y,z]], [x,y,z], or [[x,y,z], slide], () => [x, y, z], or [point, transformation(s)]" 611 ); 612 // "\nPossible parent types: [[x,y,z]], [x,y,z], [element,transformation]"); // TODO 613 } 614 615 attr = Type.copyAttributes(attributes, board.options, 'point3d'); 616 el = new JXG.Point3D(view, F, slide, attr); 617 el.initCoords(); 618 if (base !== null && transform !== null) { 619 el.addTransform(base, transform); 620 } 621 622 c2d = view.project3DTo2D(el.coords); 623 624 attr = el.setAttr2D(attr); 625 el.element2D = view.create('point', c2d, attr); 626 el.element2D.view = view; 627 el.addChild(el.element2D); 628 el.inherits.push(el.element2D); 629 el.element2D.setParents(el); 630 631 // If this point is a glider, record that in the update tree 632 if (el.slide) { 633 el.slide.addChild(el); 634 el.setParents(el.slide); 635 } 636 if (base) { 637 el.setParents(base); 638 } 639 640 el._c2d = el.element2D.coords.usrCoords.slice(); // Store a copy of the coordinates to detect dragging 641 642 return el; 643 }; 644 645 JXG.registerElement("point3d", JXG.createPoint3D); 646