1 /* 2 Copyright 2008-2026 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 a 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 optionally 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 /** 112 * An array of coordinates for moveTo(). An in-progress move can be updated or cancelled by updating or clearing this array. Use moveTo() instead of 113 * accessing this array directly. 114 * @type Array 115 * @private 116 */ 117 this.movePath = []; 118 this.moveCallback = null; 119 this.moveInterval = null; 120 121 this._c2d = null; 122 }; 123 124 JXG.Point3D.prototype = new JXG.GeometryElement(); 125 126 Type.copyPrototypeMethods(JXG.Point3D, JXG.GeometryElement3D, 'constructor3D'); 127 Type.copyMethodMap(JXG.Point3D, { 128 // TODO 129 }); 130 131 JXG.extend( 132 JXG.Point3D.prototype, 133 /** @lends JXG.Point3D.prototype */ { 134 135 /** 136 * Get x-coordinate of a 3D point. 137 * 138 * @name X 139 * @memberOf Point3D 140 * @function 141 * @returns {Number} 142 * 143 * @example 144 * p.X(); 145 */ 146 X: function () { 147 return this.coords[1]; 148 }, 149 150 /** 151 * Get y-coordinate of a 3D point. 152 * 153 * @name Y 154 * @memberOf Point3D 155 * @function 156 * @returns Number 157 * 158 * @example 159 * p.Y(); 160 */ 161 Y: function () { 162 return this.coords[2]; 163 }, 164 165 /** 166 * Get z-coordinate of a 3D point. 167 * 168 * @name Z 169 * @memberOf Point3D 170 * @function 171 * @returns Number 172 * 173 * @example 174 * p.Z(); 175 */ 176 Z: function () { 177 return this.coords[3]; 178 }, 179 180 /** 181 * Get w-coordinate of a 3D point. 182 * 183 * @name W 184 * @memberOf Point3D 185 * @function 186 * @returns Number 187 * 188 * @example 189 * p.W(); 190 */ 191 W: function () { 192 return this.coords[0]; 193 }, 194 195 /** 196 * Update the array {@link JXG.Point3D#coords} containing the homogeneous coords. 197 * 198 * @name updateCoords 199 * @memberOf Point3D 200 * @function 201 * @returns {Object} Reference to the Point3D object 202 * @private 203 * @see GeometryElement3D#update() 204 * @example 205 * p.updateCoords(); 206 */ 207 updateCoords: function () { 208 var i, 209 s = 0; 210 211 if (Type.isFunction(this.F)) { 212 this.coords = Type.evaluate(this.F); 213 if (this.coords.length === 3) { 214 this.coords.unshift(1); 215 } 216 } else { 217 if (this.F.length === 3) { 218 this.coords[0] = 1; 219 s = 1; 220 } 221 for (i = 0; i < this.F.length; i++) { 222 // Attention: if F is array of numbers, coords may not be updated. 223 // Otherwise, dragging will not work anymore. 224 if (Type.isFunction(this.F[i])) { 225 this.coords[s + i] = Type.evaluate(this.F[i]); 226 } 227 } 228 } 229 230 return this; 231 }, 232 233 /** 234 * Initialize the coords array. 235 * 236 * @private 237 * @returns {Object} Reference to the Point3D object 238 */ 239 initCoords: function () { 240 var i, 241 s = 0; 242 243 244 if (Type.isFunction(this.F)) { 245 this.coords = Type.evaluate(this.F); 246 if (this.coords.length === 3) { 247 this.coords.unshift(1); 248 } 249 } else { 250 if (this.F.length === 3) { 251 this.coords[0] = 1; 252 s = 1; 253 } 254 for (i = 0; i < this.F.length; i++) { 255 this.coords[s + i] = Type.evaluate(this.F[i]); 256 } 257 } 258 this.initialCoords = this.coords.slice(); 259 260 return this; 261 }, 262 263 /** 264 * Normalize homogeneous coordinates such the the first coordinate (the w-coordinate is equal to 1 or 0)- 265 * 266 * @name normalizeCoords 267 * @memberOf Point3D 268 * @function 269 * @returns {Object} Reference to the Point3D object 270 * @private 271 * @example 272 * p.normalizeCoords(); 273 */ 274 normalizeCoords: function () { 275 if (Math.abs(this.coords[0]) > 1.e-14) { 276 this.coords[1] /= this.coords[0]; 277 this.coords[2] /= this.coords[0]; 278 this.coords[3] /= this.coords[0]; 279 this.coords[0] = 1.0; 280 } 281 return this; 282 }, 283 284 /** 285 * Set the position of a 3D point. 286 * 287 * @name setPosition 288 * @memberOf Point3D 289 * @function 290 * @param {Array} coords 3D coordinates. Either of the form [x,y,z] (Euclidean) or [w,x,y,z] (homogeneous). 291 * @param {Boolean} [noevent] If true, no events are triggered (TODO) 292 * @returns {Object} Reference to the Point3D object 293 * 294 * @example 295 * p.setPosition([1, 3, 4]); 296 */ 297 setPosition: function (coords, noevent) { 298 var c = this.coords; 299 // oc = this.coords.slice(); // Copy of original values 300 301 if (coords.length === 3) { 302 // Euclidean coordinates 303 c[0] = 1.0; 304 c[1] = coords[0]; 305 c[2] = coords[1]; 306 c[3] = coords[2]; 307 } else { 308 // Homogeneous coordinates (normalized) 309 c[0] = coords[0]; 310 c[1] = coords[1]; 311 c[2] = coords[2]; 312 c[3] = coords[3]; 313 this.normalizeCoords(); 314 } 315 316 // console.log(el.emitter, !noevent, oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3]); 317 // Not yet working TODO 318 // if (el.emitter && !noevent && 319 // (oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3])) { 320 // this.triggerEventHandlers(['update3D'], [oc]); 321 // } 322 return this; 323 }, 324 325 // /** 326 // * Add transformations to this element. 327 // * @param {JXG.GeometryElement} el 328 // * @param {JXG.Transformation|Array} transform Either one {@link JXG.Transformation} 329 // * or an array of {@link JXG.Transformation}s. 330 // * @returns {JXG.CoordsElement} Reference to itself. 331 // */ 332 addTransform: function (el, transform) { 333 this.addTransformGeneric(el, transform); 334 return this; 335 }, 336 337 removeTransform: function (transform) { 338 this.removeTransformGeneric(transform); 339 return this; 340 }, 341 342 clearTransforms: function () { 343 this.clearTransformsGeneric(); 344 return this; 345 }, 346 347 updateTransform: function () { 348 var c, i; 349 350 if (this.transformations.length === 0 || this.baseElement === null) { 351 return this; 352 } 353 354 if (this === this.baseElement) { 355 c = this.initialCoords; 356 } else { 357 c = this.baseElement.coords; 358 } 359 for (i = 0; i < this.transformations.length; i++) { 360 this.transformations[i].update(); 361 c = Mat.matVecMult(this.transformations[i].matrix, c); 362 } 363 this.coords = c; 364 365 return this; 366 }, 367 368 // Already documented in JXG.GeometryElement 369 update: function (drag) { 370 var c3d, // Homogeneous 3D coordinates 371 foot, res; 372 373 if ( 374 this.element2D.draggable() && 375 Geometry.distance(this._c2d, this.element2D.coords.usrCoords) !== 0 376 ) { 377 // Update is called from board.updateElements, e.g. after manipulating a 378 // a slider or dragging a point. 379 // Usually this followed by an update call using the other branch below. 380 381 if (this.slide /*&& this.slide.type === Const.OBJECT_TYPE_PLANE3D*/) { 382 // Dragging 3D points with two degrees of freedom on a 3D plane. 383 // On other slide object we still use the shift key, see below. 384 385 this.coords = this.slide.projectScreenCoords([this.element2D.X(), this.element2D.Y()], this.position, this.evalVisProp('cyclic')); 386 this.element2D.coords.setCoordinates( 387 Const.COORDS_BY_USER, 388 this.view.project3DTo2D(this.coords) 389 ); 390 } else { 391 if (this.view.isVerticalDrag()) { 392 // Drag the point in its vertical to the xy plane 393 // If the point is outside of bbox3d, 394 // c3d is already corrected. 395 c3d = this.view.project2DTo3DVertical(this.element2D, this.coords); 396 } else { 397 // Drag the point in its xy plane 398 foot = [1, 0, 0, this.coords[3]]; 399 c3d = this.view.project2DTo3DPlane(this.element2D, [1, 0, 0, 1], foot); 400 } 401 402 if (c3d[0] !== 0) { 403 // Check if c3d is inside of view.bbox3d 404 // Otherwise, the coords are now corrected. 405 res = this.view.project3DToCube(c3d); 406 this.coords = res[0]; 407 408 if (res[1]) { 409 // The 3D coordinates have been corrected, now also correct the 2D element. 410 this.element2D.coords.setCoordinates( 411 Const.COORDS_BY_USER, 412 this.view.project3DTo2D(this.coords) 413 ); 414 } 415 if (this.slide) { 416 this.coords = this.slide.projectCoords([1, this.X(), this.Y(), this.Z()], this.position); 417 this.element2D.coords.setCoordinates( 418 Const.COORDS_BY_USER, 419 this.view.project3DTo2D(this.coords) 420 ); 421 } 422 } 423 } 424 } else { 425 // Update 2D point from its 3D view, e.g. when rotating the view 426 this.updateCoords() 427 .updateTransform(); 428 429 if (this.slide) { 430 this.coords = this.slide.projectCoords([1, this.X(), this.Y(), this.Z()], this.position); 431 } 432 c3d = this.coords; 433 this.element2D.coords.setCoordinates( 434 Const.COORDS_BY_USER, 435 this.view.project3DTo2D(c3d) 436 ); 437 // this.zIndex = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3]; 438 this.zIndex = Mat.innerProduct(this.view.matrix3DRotShift[3], c3d); 439 } 440 this._c2d = this.element2D.coords.usrCoords.slice(); 441 442 return this; 443 }, 444 445 // Already documented in JXG.GeometryElement 446 updateRenderer: function () { 447 this.needsUpdate = false; 448 return this; 449 }, 450 451 /** 452 * Check whether a point's position is finite, i.e. the first entry is not zero. 453 * @returns {Boolean} True if the first entry of the coordinate vector is not zero; false otherwise. 454 */ 455 testIfFinite: function () { 456 return Math.abs(this.coords[0]) > 1.e-12 ? true : false; 457 // return Type.cmpArrays(this.coords, [0, 0, 0, 0]); 458 }, 459 460 /** 461 * Calculate the distance from one point to another. If one of the points is on the plane at infinity, return positive infinity. 462 * @param {JXG.Point3D} pt The point to which the distance is calculated. 463 * @returns {Number} The distance 464 */ 465 distance: function (pt) { 466 var eps_sq = 1e-12, 467 c_this = this.coords, 468 c_pt = pt.coords; 469 470 if (c_this[0] * c_this[0] > eps_sq && c_pt[0] * c_pt[0] > eps_sq) { 471 return Mat.hypot( 472 c_pt[1] - c_this[1], 473 c_pt[2] - c_this[2], 474 c_pt[3] - c_this[3] 475 ); 476 } else { 477 return Number.POSITIVE_INFINITY; 478 } 479 }, 480 481 482 483 /** 484 * Starts an animated point movement towards the given coordinates <tt>where</tt>. 485 * The animation is done after <tt>time</tt> milliseconds. 486 * If the second parameter is not given or is equal to 0, coordinates are changed without animation. 487 * @param {Array} where Array containing the target coordinate in cartesian or homogenous form. 488 * @param {Number} [time] Number of milliseconds the animation should last. 489 * @param {Object} [options] Optional settings for the animation 490 * @param {function} [options.callback] A function that is called as soon as the animation is finished. 491 * @param {String} [options.effect='<>'|'>'|'<'] animation effects like speed fade in and out. possible values are 492 * '<>' for speed increase on start and slow down at the end (default), '<' for speed up, '>' for slow down, and '--' for constant speed during 493 * the whole animation. 494 * @see JXG.Point3D#moveAlong 495 * @see JXG.Point#moveTo 496 * @example 497 * // visit a coordinate, then use callback to visit a second coordinate. 498 * const board = JXG.JSXGraph.initBoard('jxgbox') 499 * var view = board.create( 500 * 'view3d', 501 * [[-6, -3], [8, 8], 502 * [[-3, 3], [-3, 3], [-3, 3]]]); 503 * 504 * let A = view.create('point3d', [0, 0, 0]); 505 * 506 * // move A with callbacks 507 * board.create('button', [-4, 4.3, 'callbacks', () => { 508 * A.moveTo([3, 3, 3], 3000, 509 * { 510 * callback: () => A.moveTo([-3, -3, -3], 3000, { 511 * callback: () => A.moveTo([0, 0, 0],1000), effect: '<' 512 * }), 513 * effect: '>' 514 * }) 515 * }]) 516 * 517 * // move A with async/await 518 * board.create('button', [-3, 4.3, 'async/await', async () => { 519 * await A.moveTo([3, 3, 3], 3000, { effect: '>' }); 520 * await A.moveTo([-3, -3, -3], 3000, { effect: '<' }); 521 * A.moveTo([0, 0, 0],1000) 522 * }]) 523 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-cba3b0c2aad4" class="jxgbox" style="width: 300px; height: 300px;"></div> 524 * <script type="text/javascript"> 525 * { 526 * const board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-cba3b0c2aad4') 527 * var view = board.create( 528 * 'view3d', 529 * [[-6, -3], [8, 8], 530 * [[-3, 3], [-3, 3], [-3, 3]]]); 531 * 532 * let A = view.create('point3d', [0, 0, 0]); 533 * // move A with callbacks 534 * board.create('button', [-4, 4.3, 'callbacks', () => { 535 * A.moveTo([3, 3, 3], 3000, 536 * { 537 * callback: () => A.moveTo([-3, -3, -3], 3000, { 538 * callback: () => A.moveTo([0, 0, 0],1000), effect: '<' 539 * }), 540 * effect: '>' 541 * }) 542 * }]) 543 * 544 * // move A with async/await 545 * board.create('button', [-1, 4.3, 'async/await', async () => { 546 * await A.moveTo([3, 3, 3], 3000, { effect: '>' }); 547 * await A.moveTo([-3, -3, -3], 3000, { effect: '<' }); 548 * A.moveTo([0, 0, 0],1000) 549 * }]) 550 * } 551 * </script><pre> 552 */ 553 moveTo: function (where, time, options) { 554 options = options || {}; 555 556 var i, 557 steps = Math.ceil(time / this.board.attr.animationdelay), 558 X = where[0], 559 Y = where[1], 560 Z = where[2], 561 dX = this.coords[1] - X, 562 dY = this.coords[2] - Y, 563 dZ = this.coords[3] - Z, 564 doneCallback = () => { }, 565 stepFun; 566 567 if (options.callback) 568 doneCallback = options.callback; // unload 569 570 571 /** @ignore */ 572 stepFun = function (i) { 573 var x = i / steps; // absolute progress of the animatin 574 575 if (options.effect) { 576 if (options.effect === "<>") { 577 return Math.pow(Math.sin((x * Math.PI) / 2), 2); 578 } 579 if (options.effect === "<") { // cubic ease in 580 return x * x * x; 581 } 582 if (options.effect === ">") { // cubic ease out 583 return 1 - Math.pow(1 - x, 3); 584 } 585 if (options.effect === "==") { 586 return i / steps; // linear 587 } 588 throw new Error("valid effects are '==', '<>', '>', and '<'."); 589 } 590 return i / steps; // default 591 }; 592 593 // immediate move, no time 594 if ( 595 !Type.exists(time) || 596 time === 0 597 // check for tiny move, is this necessary? 598 // Math.abs(where.usrCoords[0] - this.coords.usrCoords[0]) > Mat.eps 599 ) { 600 this.setPosition([X, Y, Z], true); // no event here 601 return this.board.update(this); 602 } 603 604 // In case there is no callback and we are already at the endpoint we can stop here 605 if ( 606 !Type.exists(options.callback) && 607 Math.abs(dX) < Mat.eps && 608 Math.abs(dY) < Mat.eps && 609 Math.abs(dZ) < Mat.eps 610 ) { 611 return this; 612 } 613 614 this.animationPath = []; 615 for (i = steps; i >= 0; i--) { 616 this.animationPath[steps - i] = [ 617 X + dX * stepFun(i), 618 Y + dY * stepFun(i), 619 Z + dZ * stepFun(i) 620 ]; 621 } 622 623 return this.moveAlong(this.animationPath, time, 624 { callback: doneCallback }); 625 626 }, 627 628 /** 629 * Move along a path defined by an array of coordinates 630 * @param {number[][]} [traversePath] Array of path coordinates (either cartesian or homogenous). 631 * @param {number} [time] Number of milliseconds the animation should last. 632 * @param {Object} [options] 'callback' and 'interpolate'. see {@link JXG.CoordsElement#moveAlong}, 633 * @example 634 *const board = JXG.JSXGraph.initBoard('jxgbox') 635 *var view = board.create( 636 * 'view3d', 637 * [[-6, -3], [8, 8], 638 * [[-3, 3], [-3, 3], [-3, 3]]]); 639 * 640 * board.create('button', [-4, 4.5, 'start', () => { 641 * let A = view.create('point3d', [0, 0, 0]); 642 * A.moveAlong([[3, 3, 3], [-2, -1, -2], [-1, -1, -1], [-1, -2, 1]], 3000, 643 * { callback: () => board.create('text', [-4, 4, 'done!']) }) 644 *}]) 645 * 646 * </pre><div id="JXGa45032e5-a517-4f1d-868a-abc698d344cf" class="jxgbox" style="width: 300px; height: 300px;"></div> 647 * <script type="text/javascript"> 648 * (function() { 649 * const board = JXG.JSXGraph.initBoard("JXGa45032e5-a517-4f1d-868a-abc698d344cf") 650 * var view = board.create( 651 * 'view3d', 652 * [[-6, -3], [8, 8], 653 * [[-3, 3], [-3, 3], [-3, 3]]]); 654 * 655 * board.create('button', [-4, 4.5, 'start', () => { 656 * let A = view.create('point3d', [0, 0, 0]); 657 * A.moveAlong([[3, 3, 3], [-2, -1, -2], [-1, -1, -1], [-1, -2, 1]], 3000, 658 * { callback: () => board.create('text', [-4, 4, 'done!']) }) 659 * }]) 660 * 661 * })(); 662 * 663 * </script><pre> 664 * 665 */ 666 moveAlong: function (traversePath, time, options) { 667 let stepTime = time/traversePath.length; // will be same as this.board.attr.animationdelay if called by MoveTo 668 669 670 // unload the options 671 if (Type.isObject(options)) { 672 if ('callback' in options) 673 this.moveCallback = options.callback; 674 // TODO:add interpolation using Neville. How? easiest is add interpolation to path before start 675 // if ('interpolate' in options) interpolate = options.interpolate; 676 } 677 678 679 if (this.movePath.length > 0) { // existing move in progress 680 this.movePath = traversePath; // set the new path and return ?? 681 return; // promise is still outstanding 682 } 683 684 // no move currently in progress 685 this.movePath = traversePath; // set the new path and return a promise 686 return new Promise((resolve, reject) => { 687 this.moveInterval = setInterval(() => { 688 if (this.movePath.length > 0) { 689 let coord = this.movePath.shift(); 690 this.setPosition(coord, true); // no events during transit 691 this.board.update(this); 692 } 693 if (this.movePath.length === 0) { // now shorter than previous test 694 clearInterval(this.moveInterval); 695 resolve(); 696 if (Type.isFunction(this.moveCallback)) { 697 this.moveCallback(); // invoke the callback 698 } 699 } 700 }, stepTime); 701 }); 702 }, 703 704 705 706 707 // Not yet working 708 __evt__update3D: function (oc) { } 709 } 710 ); 711 712 /** 713 * @class A Point3D object is defined by three coordinates [x,y,z], or a function returning an array with three numbers. 714 * Alternatively, all numbers can also be provided as functions returning a number. 715 * 716 * @pseudo 717 * @name Point3D 718 * @augments JXG.Point3D 719 * @constructor 720 * @throws {Exception} If the element cannot be constructed with the given parent 721 * objects an exception is thrown. 722 * @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. 723 * If an optional 3D element "slide" is supplied, the point is a glider on that element. At the time of version v1.11, only elements of type line3d are supperted as glider hosts. 724 * @param {array,function_JXG.GeometryElement3D} F,[slide=null] Alternatively, the coordinates can be supplied as 725 * <ul> 726 * <li>function returning an array [x,y,z] of length 3 of numbers or 727 * <li>array arr=[x,y,z] of length 3 consisting of numbers 728 * </ul> 729 * If an optional 3D element "slide" is supplied, the point is a glider on that element. 730 * 731 * @example 732 * var bound = [-5, 5]; 733 * var view = board.create('view3d', 734 * [[-6, -3], [8, 8], 735 * [bound, bound, bound]], 736 * {}); 737 * var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 }); 738 * var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 3, fixed: true }); 739 * var w = view.create('point3d', [ () => p.X() + 3, () => p.Y(), () => p.Z() - 2], { name:'C', size: 3, fixed: true }); 740 * 741 * </pre><div id="JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1" class="jxgbox" style="width: 300px; height: 300px;"></div> 742 * <script type="text/javascript"> 743 * (function() { 744 * var board = JXG.JSXGraph.initBoard('JXGb9ee8f9f-3d2b-4f73-8221-4f82c09933f1', 745 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 746 * var bound = [-5, 5]; 747 * var view = board.create('view3d', 748 * [[-6, -3], [8, 8], 749 * [bound, bound, bound]], 750 * {}); 751 * var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 }); 752 * var q = view.create('point3d', function() { return [p.X(), p.Y(), p.Z() - 3]; }, { name:'B', size: 3 }); 753 * var w = view.create('point3d', [ () => p.X() + 3, () => p.Y(), () => p.Z() - 2], { name:'C', size: 3, fixed: true }); 754 * })(); 755 * 756 * </script><pre> 757 * 758 * @example 759 * // Glider on sphere 760 * var view = board.create( 761 * 'view3d', 762 * [[-6, -3], [8, 8], 763 * [[-3, 3], [-3, 3], [-3, 3]]], 764 * { 765 * depthOrder: { 766 * enabled: true 767 * }, 768 * projection: 'central', 769 * xPlaneRear: {fillOpacity: 0.2, gradient: null}, 770 * yPlaneRear: {fillOpacity: 0.2, gradient: null}, 771 * zPlaneRear: {fillOpacity: 0.2, gradient: null} 772 * } 773 * ); 774 * 775 * // Two points 776 * var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2}); 777 * var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2}); 778 * 779 * // Sphere 780 * var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8}); 781 * 782 * // Glider on sphere 783 * var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4}); 784 * var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 }); 785 * 786 * </pre><div id="JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71" class="jxgbox" style="width: 300px; height: 300px;"></div> 787 * <script type="text/javascript"> 788 * (function() { 789 * var board = JXG.JSXGraph.initBoard('JXG672fe3c7-e6fd-48e0-9a24-22f51f2dfa71', 790 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 791 * var view = board.create( 792 * 'view3d', 793 * [[-6, -3], [8, 8], 794 * [[-3, 3], [-3, 3], [-3, 3]]], 795 * { 796 * depthOrder: { 797 * enabled: true 798 * }, 799 * projection: 'central', 800 * xPlaneRear: {fillOpacity: 0.2, gradient: null}, 801 * yPlaneRear: {fillOpacity: 0.2, gradient: null}, 802 * zPlaneRear: {fillOpacity: 0.2, gradient: null} 803 * } 804 * ); 805 * 806 * // Two points 807 * var center = view.create('point3d', [0, 0, 0], {withLabel: false, size: 2}); 808 * var point = view.create('point3d', [2, 0, 0], {withLabel: false, size: 2}); 809 * 810 * // Sphere 811 * var sphere = view.create('sphere3d', [center, point], {fillOpacity: 0.8}); 812 * 813 * // Glider on sphere 814 * var glide = view.create('point3d', [2, 2, 0, sphere], {withLabel: false, color: 'red', size: 4}); 815 * var l1 = view.create('line3d', [glide, center], { strokeWidth: 2, dash: 2 }); 816 * 817 * })(); 818 * 819 * </script><pre> 820 * 821 */ 822 JXG.createPoint3D = function (board, parents, attributes) { 823 // parents[0]: view 824 // followed by 825 // parents[1]: function or array 826 // or 827 // parents[1..3]: coordinates 828 829 var view = parents[0], 830 attr, F, slide, c2d, el, 831 base = null, 832 transform = null; 833 834 // If the last element of `parents` is a 3D object, 835 // the point is a glider on that element. 836 if (parents.length > 2 && 837 Type.exists(parents[parents.length - 1].is3D) && 838 !Type.isTransformationOrArray(parents[parents.length - 1]) 839 ) { 840 slide = parents.pop(); 841 } else { 842 slide = null; 843 } 844 845 if (parents.length === 2) { 846 // [view, array|fun] (Array [x, y, z] | function) returning [x, y, z] 847 F = parents[1]; 848 } else if (parents.length === 3 && 849 Type.isPoint3D(parents[1]) && 850 Type.isTransformationOrArray(parents[2]) 851 ) { 852 F = [0, 0, 0]; 853 base = parents[1]; 854 transform = parents[2]; 855 } else if (parents.length === 4) { 856 // [view, x, y, z], (3 numbers | functions) 857 F = parents.slice(1); 858 } else if (parents.length === 5) { 859 // [view, w, x, y, z], (4 numbers | functions) 860 F = parents.slice(1); 861 } else { 862 throw new Error( 863 "JSXGraph: Can't create point3d with parent types '" + 864 typeof parents[1] + 865 "' and '" + 866 typeof parents[2] + 867 "'." + 868 "\nPossible parent types: [[x,y,z]], [x,y,z], or [[x,y,z], slide], () => [x, y, z], or [point, transformation(s)]" 869 ); 870 // "\nPossible parent types: [[x,y,z]], [x,y,z], [element,transformation]"); // TODO 871 } 872 873 attr = Type.copyAttributes(attributes, board.options, 'point3d'); 874 el = new JXG.Point3D(view, F, slide, attr); 875 el.initCoords(); 876 if (base !== null && transform !== null) { 877 el.addTransform(base, transform); 878 } 879 880 c2d = view.project3DTo2D(el.coords); 881 882 attr = el.setAttr2D(attr); 883 el.element2D = view.create('point', c2d, attr); 884 el.element2D.view = view; 885 el.element2D.dump = false; 886 el.addChild(el.element2D); 887 el.inherits.push(el.element2D); 888 el.element2D.setParents(el); 889 890 // If this point is a glider, record that in the update tree 891 if (el.slide) { 892 el.slide.addChild(el); 893 el.setParents(el.slide); 894 } 895 if (base) { 896 el.setParents(base); 897 } 898 899 el._c2d = el.element2D.coords.usrCoords.slice(); // Store a copy of the coordinates to detect dragging 900 901 return el; 902 }; 903 904 JXG.registerElement("point3d", JXG.createPoint3D); 905 906