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