1 /* 2 Copyright 2008-2023 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 /** 32 * Create linear spaces of dimension at least one, 33 * i.e. lines and planes. 34 */ 35 import JXG from "../jxg"; 36 import Const from "../base/constants"; 37 import Type from "../utils/type"; 38 import Mat from "../math/math"; 39 import Geometry from "../math/geometry"; 40 41 // ----------------------- 42 // Lines 43 // ----------------------- 44 45 /** 46 * Constructor for 3D lines. 47 * @class Creates a new 3D line object. Do not use this constructor to create a 3D line. Use {@link JXG.View3D#create} with type {@link Line3D} instead. 48 * 49 * @augments JXG.GeometryElement3D 50 * @augments JXG.GeometryElement 51 * @param {View3D} view 52 * @param {Point3D|Array} point 53 * @param {Array} direction 54 * @param {Array} range 55 * @param {Object} attributes 56 * @see JXG.Board#generateName 57 */ 58 JXG.Line3D = function (view, point, direction, range, attributes) { 59 this.constructor(view.board, attributes, Const.OBJECT_TYPE_LINE3D, Const.OBJECT_CLASS_3D); 60 this.constructor3D(view, "line3d"); 61 62 this.id = this.view.board.setId(this, "L3D"); 63 this.board.finalizeAdding(this); 64 65 /** 66 * 3D point which - together with a direction - defines the line. 67 * @type JXG.Point3D 68 * 69 * @see JXG.Line3D#direction 70 */ 71 this.point = point; 72 73 /** 74 * Direction which - together with a point - defines the line. Array of numbers or functions (of length 3) or function 75 * returning array of length 3. 76 * 77 * @type Array,Function 78 * @see JXG.Line3D#point 79 */ 80 this.direction = direction; 81 82 /** 83 * Range [r1, r2] of the line. r1, r2 can be numbers or functions. 84 * The 3D line goes from (point + r1 * direction) to (point + r2 * direction) 85 * @type Array 86 */ 87 this.range = range || [-Infinity, Infinity]; 88 89 /** 90 * Starting point of the 3D line 91 * @type JXG.Point3D 92 * @private 93 */ 94 this.point1 = null; 95 96 /** 97 * End point of the 3D line 98 * @type JXG.Point3D 99 * @private 100 */ 101 this.point2 = null; 102 103 this.methodMap = Type.deepCopy(this.methodMap, { 104 // TODO 105 }); 106 }; 107 JXG.Line3D.prototype = new JXG.GeometryElement(); 108 Type.copyPrototypeMethods(JXG.Line3D, JXG.GeometryElement3D, "constructor3D"); 109 110 JXG.extend( 111 JXG.Line3D.prototype, 112 /** @lends JXG.Line3D.prototype */ { 113 /** 114 * Determine one end point of a 3D line from point, direction and range. 115 * 116 * @param {Number|function} r 117 * @private 118 * @returns Array 119 */ 120 getPointCoords: function (r) { 121 var p = [], 122 d = [], 123 i, 124 r0; 125 126 p = [this.point.X(), this.point.Y(), this.point.Z()]; 127 128 if (Type.isFunction(this.direction)) { 129 d = this.direction(); 130 } else { 131 for (i = 1; i < 4; i++) { 132 d.push(Type.evaluate(this.direction[i])); 133 } 134 } 135 136 r0 = Type.evaluate(r); 137 // TODO: test also in the finite case 138 if (Math.abs(r0) === Infinity) { 139 r = this.view.intersectionLineCube(p, d, r0); 140 } 141 142 return [p[0] + d[0] * r0, p[1] + d[1] * r0, p[2] + d[2] * r0]; 143 }, 144 145 update: function () { 146 return this; 147 }, 148 149 updateRenderer: function () { 150 this.needsUpdate = false; 151 return this; 152 } 153 } 154 ); 155 156 /** 157 * @class This element is used to provide a constructor for a 3D line. 158 * @pseudo 159 * @description There are two possibilities to create a Line3D object. 160 * <p> 161 * First: the line in 3D is defined by two points in 3D (Point3D). 162 * The points can be either existing points or coordinate arrays of 163 * the form [x, y, z]. 164 * <p>Second: the line in 3D is defined by a point (or coordinate array [x, y, z]) 165 * a direction given as array [x, y, z] and an optional range 166 * given as array [s, e]. The default value for the range is [-Infinity, Infinity]. 167 * <p> 168 * All numbers can also be provided as functions returning a number. 169 * 170 * @name Line3D 171 * @augments JXG.GeometryElement3D 172 * @constructor 173 * @type JXG.Line3D 174 * @throws {Exception} If the element cannot be constructed with the given parent 175 * objects an exception is thrown. 176 * @param {JXG.Point3D,array,function_JXG.Point3D,array,function} point1,point2 First and second defining point. 177 * @param {JXG.Point3D,array,function_array,function_array,function} point,direction,range Alternatively, point, direction and range can be supplied. 178 * <ul> 179 * <li> point: Point3D or array of length 3 180 * <li> direction: array of length 3 or function returning an array of numbers or function returning an array 181 * <li> range: array of length 2, elements can also be functions. 182 * </ul> 183 * 184 * @example 185 * var bound = [-5, 5]; 186 * var view = board.create('view3d', 187 * [[-6, -3], [8, 8], 188 * [bound, bound, bound]], 189 * {}); 190 * var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 }); 191 * // Lines through 2 points 192 * var l1 = view.create('line3d', [[1, 3, 3], [-3, -3, -3]], {point1: {visible: true}, point2: {visible: true} }); 193 * var l2 = view.create('line3d', [p, l1.point1]); 194 * 195 * // Line by point, direction, range 196 * var l3 = view.create('line3d', [p, [0, 0, 1], [-2, 4]]); 197 * 198 * </pre><div id="JXG05f9baa4-6059-4502-8911-6a934f823b3d" class="jxgbox" style="width: 300px; height: 300px;"></div> 199 * <script type="text/javascript"> 200 * (function() { 201 * var board = JXG.JSXGraph.initBoard('JXG05f9baa4-6059-4502-8911-6a934f823b3d', 202 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 203 * var bound = [-5, 5]; 204 * var view = board.create('view3d', 205 * [[-6, -3], [8, 8], 206 * [bound, bound, bound]], 207 * {}); 208 * var p = view.create('point3d', [1, 2, 2], { name:'A', size: 5 }); 209 * // Lines through 2 points 210 * var l1 = view.create('line3d', [[1, 3, 3], [-3, -3, -3]], {name: 'll1', point1: {visible: true}, point2: {visible: true} }); 211 * var l2 = view.create('line3d', [p, l1.point1]); 212 * // Line by point, direction, range 213 * var l3 = view.create('line3d', [p, [0, 0, 1], [-2, 4]]); 214 * })(); 215 * 216 * </script><pre> 217 * 218 */ 219 JXG.createLine3D = function (board, parents, attributes) { 220 var view = parents[0], 221 attr, 222 points, 223 point, 224 direction, 225 range, 226 point1, 227 point2, 228 el; 229 230 attr = Type.copyAttributes(attributes, board.options, "line3d"); 231 232 // In any case, parents[1] contains a point or point coordinates 233 point = Type.providePoints3D(view, [parents[1]], attributes, "line3d", ["point"])[0]; 234 235 if ( 236 Type.isPoint3D(parents[2]) || 237 (parents.length === 3 && (Type.isArray(parents[2]) || Type.isFunction(parents[2]))) 238 ) { 239 // Line defined by two points; [view, point1, point2] 240 241 point1 = point; 242 point2 = Type.providePoints3D(view, [parents[2]], attributes, "line3d", ["point2"])[0]; 243 direction = function () { 244 return [point2.X() - point.X(), point2.Y() - point.Y(), point2.Z() - point.Z()]; 245 }; 246 range = [0, 1]; 247 el = new JXG.Line3D(view, point, direction, range, attr); 248 } else { 249 // Line defined by point, direction and range 250 251 // Directions are handled as arrays of length 4, 252 // i.e. with homogeneous coordinates. 253 if (Type.isFunction(parents[2])) { 254 direction = parents[2]; 255 } else if (parents[2].length === 3) { 256 direction = [1].concat(parents[2]); 257 } else if (parents[2].length === 4) { 258 direction = parents[2]; 259 } else { 260 // TODO Throw error 261 } 262 range = parents[3]; 263 264 points = Type.providePoints3D( 265 view, 266 [ 267 [0, 0, 0], 268 [0, 0, 0] 269 ], 270 attributes, 271 "line3d", 272 ["point1", "point2"] 273 ); 274 275 // Create a line3d with two dummy points 276 el = new JXG.Line3D(view, point, direction, range, attr); 277 278 // Now set the real points which define the line 279 /** @ignore */ 280 points[0].F = function () { 281 return el.getPointCoords(Type.evaluate(el.range[0])); 282 }; 283 points[0].prepareUpdate().update(); 284 point1 = points[0]; 285 286 /** @ignore */ 287 points[1].F = function () { 288 return el.getPointCoords(Type.evaluate(el.range[1])); 289 }; 290 points[1].prepareUpdate().update(); 291 point2 = points[1]; 292 } 293 // TODO Throw error 294 295 el.element2D = view.create("segment", [point1.element2D, point2.element2D], attr); 296 el.addChild(el.element2D); 297 el.inherits.push(el.element2D); 298 el.element2D.setParents(el); 299 300 point1.addChild(el); 301 point2.addChild(el); 302 el.point1 = point1; 303 el.point2 = point2; 304 305 el.update(); 306 el.element2D.prepareUpdate().update().updateRenderer(); 307 return el; 308 }; 309 JXG.registerElement("line3d", JXG.createLine3D); 310 311 // ----------------------- 312 // Planes 313 // ----------------------- 314 315 /** 316 * Constructor for 3D planes. 317 * @class Creates a new 3D plane object. Do not use this constructor to create a 3D plane. Use {@link JXG.Board#create} with type {@link Plane3D} instead. 318 * 319 * @augments JXG.GeometryElement3D 320 * @augments JXG.GeometryElement 321 * @param {View3D} view 322 * @param {Point3D,Array} point 323 * @param {Array} direction1 324 * @param {Array} range1 325 * @param {Array} direction2 326 * @param {Array} range2 327 * @param {Object} attributes 328 * @see JXG.Board#generateName 329 */ 330 JXG.Plane3D = function (view, point, dir1, range1, dir2, range2, attributes) { 331 this.constructor(view.board, attributes, Const.OBJECT_TYPE_PLANE3D, Const.OBJECT_CLASS_3D); 332 this.constructor3D(view, "plane3d"); 333 334 this.id = this.view.board.setId(this, "PL3D"); 335 this.board.finalizeAdding(this); 336 337 /** 338 * 3D point which - together with two direction vectors - defines the plane. 339 * 340 * @type JXG.Point3D 341 * 342 * @see JXG.3D#direction1 343 * @see JXG.3D#direction2 344 */ 345 this.point = point; 346 347 /** 348 * Two linearly independent vectors - together with a point - define the plane. Each of these direction vectors is an 349 * array of numbers or functions (of length 3) or function returning array of length 3. 350 * 351 * @type Array,Function 352 * 353 * @see JXG.Plane3D#point 354 * @see JXG.Plane3D#direction2 355 */ 356 this.direction1 = dir1; 357 358 /** 359 * Two linearly independent vectors - together with a point - define the plane. Each of these direction vectors is an 360 * array of numbers or functions (of length 3) or function returning array of length 3. 361 * 362 * @type Array,Function 363 * @see JXG.Plane3D#point 364 * @see JXG.Plane3D#direction1 365 */ 366 this.direction2 = dir2; 367 368 /** 369 * Range [r1, r2] of {@link direction1}. The 3D line goes from (point + r1 * direction1) to (point + r2 * direction1) 370 * @type {Array} 371 */ 372 this.range1 = range1 || [-Infinity, Infinity]; 373 374 /** 375 * Range [r1, r2] of {@link direction2}. The 3D line goes from (point + r1 * direction2) to (point + r2 * direction2) 376 * @type {Array} 377 */ 378 this.range2 = range2 || [-Infinity, Infinity]; 379 380 /** 381 * Direction vector 1 of the 3D plane. Contains the evaluated coordinates from {@link direction1} and {@link range1}. 382 * @type Array 383 * @private 384 * 385 * @see updateNormal 386 */ 387 this.vec1 = [0, 0, 0]; 388 389 /** 390 * Direction vector 2 of the 3D plane. Contains the evaluated coordinates from {@link direction2} and {@link range2}. 391 * 392 * @type Array 393 * @private 394 * 395 * @see updateNormal 396 */ 397 this.vec2 = [0, 0, 0]; 398 399 this.grid = null; 400 401 /** 402 * Normal vector of the plane. Left hand side of the Hesse normal form. 403 404 * @type Array 405 * @private 406 * 407 * @see updateNormal 408 * 409 */ 410 this.normal = [0, 0, 0]; 411 412 /** 413 * Right hand side of the Hesse normal form. 414 415 * @type Array 416 * @private 417 * 418 * @see updateNormal 419 * 420 */ 421 this.d = 0; 422 423 this.updateNormal(); 424 425 this.methodMap = Type.deepCopy(this.methodMap, { 426 // TODO 427 }); 428 }; 429 JXG.Plane3D.prototype = new JXG.GeometryElement(); 430 Type.copyPrototypeMethods(JXG.Plane3D, JXG.GeometryElement3D, "constructor3D"); 431 432 JXG.extend( 433 JXG.Plane3D.prototype, 434 /** @lends JXG.Plane3D.prototype */ { 435 /** 436 * Update the Hesse normal form of the plane, i.e. update normal vector and right hand side. 437 * Updates also {@link vec1} and {@link vec2}. 438 * 439 * @name JXG.Plane3D#updateNormal 440 * @function 441 * @returns {Object} Reference to the Plane3D object 442 * @private 443 * @example 444 * plane.updateNormal(); 445 * 446 */ 447 updateNormal: function () { 448 var i, len; 449 for (i = 0; i < 3; i++) { 450 this.vec1[i] = Type.evaluate(this.direction1[i]); 451 this.vec2[i] = Type.evaluate(this.direction2[i]); 452 } 453 454 this.normal = Mat.crossProduct(this.vec1, this.vec2); 455 456 len = Mat.norm(this.normal); 457 if (Math.abs(len) > Mat.eps) { 458 for (i = 0; i < 3; i++) { 459 this.normal[i] /= len; 460 } 461 } 462 this.d = Mat.innerProduct(this.point.coords.slice(1), this.normal, 3); 463 464 return this; 465 }, 466 467 updateDataArray: function () { 468 var s1, 469 e1, 470 s2, 471 e2, 472 c2d, 473 l1, 474 l2, 475 planes = ["xPlaneRear", "yPlaneRear", "zPlaneRear"], 476 points = [], 477 v1 = [0, 0, 0], 478 v2 = [0, 0, 0], 479 q = [0, 0, 0], 480 p = [0, 0, 0], 481 d, 482 i, 483 j, 484 a, 485 b, 486 first, 487 pos, 488 pos_akt, 489 view = this.view; 490 491 this.dataX = []; 492 this.dataY = []; 493 494 this.updateNormal(); 495 496 // Infinite plane 497 if ( 498 this.elType !== "axisplane3d" && 499 view.defaultAxes && 500 Type.evaluate(this.range1[0]) === -Infinity && 501 Type.evaluate(this.range1[1]) === Infinity && 502 Type.evaluate(this.range2[0]) === -Infinity && 503 Type.evaluate(this.range2[1]) === Infinity 504 ) { 505 // Start with the rear plane. 506 // Determine the intersections with the view bbox3d 507 // For each face of the bbox3d we determine two points 508 // which are the ends of the intersection line. 509 // We start with the three rear planes. 510 for (j = 0; j < planes.length; j++) { 511 p = view.intersectionPlanePlane(this, view.defaultAxes[planes[j]]); 512 513 if (p[0].length === 3 && p[1].length === 3) { 514 // This test is necessary to filter out intersection lines which are 515 // identical to intersections of axis planes (they would occur twice). 516 for (i = 0; i < points.length; i++) { 517 if ( 518 (Geometry.distance(p[0], points[i][0], 3) < Mat.eps && 519 Geometry.distance(p[1], points[i][1], 3) < Mat.eps) || 520 (Geometry.distance(p[0], points[i][1], 3) < Mat.eps && 521 Geometry.distance(p[1], points[i][0], 3) < Mat.eps) 522 ) { 523 break; 524 } 525 } 526 if (i === points.length) { 527 points.push(p.slice()); 528 } 529 } 530 531 // Point on the front plane of the bbox3d 532 p = [0, 0, 0]; 533 p[j] = view.bbox3D[j][1]; 534 535 // d is the rhs of the Hesse normal form of the front plane. 536 d = Mat.innerProduct(p, view.defaultAxes[planes[j]].normal, 3); 537 p = view.intersectionPlanePlane(this, view.defaultAxes[planes[j]], d); 538 539 if (p[0].length === 3 && p[1].length === 3) { 540 // Do the same test as above 541 for (i = 0; i < points.length; i++) { 542 if ( 543 (Geometry.distance(p[0], points[i][0], 3) < Mat.eps && 544 Geometry.distance(p[1], points[i][1], 3) < Mat.eps) || 545 (Geometry.distance(p[0], points[i][1], 3) < Mat.eps && 546 Geometry.distance(p[1], points[i][0], 3) < Mat.eps) 547 ) { 548 break; 549 } 550 } 551 if (i === points.length) { 552 points.push(p.slice()); 553 } 554 } 555 } 556 557 // Concatenate the intersection points to a polygon. 558 // If all wents well, each intersection should appear 559 // twice in the list. 560 first = 0; 561 pos = first; 562 i = 0; 563 do { 564 p = points[pos][i]; 565 if (p.length === 3) { 566 c2d = view.project3DTo2D(p); 567 this.dataX.push(c2d[1]); 568 this.dataY.push(c2d[2]); 569 } 570 i = (i + 1) % 2; 571 p = points[pos][i]; 572 573 pos_akt = pos; 574 for (j = 0; j < points.length; j++) { 575 if (j !== pos && Geometry.distance(p, points[j][0]) < Mat.eps) { 576 pos = j; 577 i = 0; 578 break; 579 } 580 if (j !== pos && Geometry.distance(p, points[j][1]) < Mat.eps) { 581 pos = j; 582 i = 1; 583 break; 584 } 585 } 586 if (pos === pos_akt) { 587 console.log("Error: update plane3d: did not find next", pos); 588 break; 589 } 590 } while (pos !== first); 591 592 c2d = view.project3DTo2D(points[first][0]); 593 this.dataX.push(c2d[1]); 594 this.dataY.push(c2d[2]); 595 } else { 596 // 3D bounded flat 597 s1 = Type.evaluate(this.range1[0]); 598 e1 = Type.evaluate(this.range1[1]); 599 s2 = Type.evaluate(this.range2[0]); 600 e2 = Type.evaluate(this.range2[1]); 601 602 q = this.point.coords.slice(1); 603 604 v1 = this.vec1.slice(); 605 v2 = this.vec2.slice(); 606 l1 = Mat.norm(v1, 3); 607 l2 = Mat.norm(v2, 3); 608 for (i = 0; i < 3; i++) { 609 v1[i] /= l1; 610 v2[i] /= l2; 611 } 612 613 for (j = 0; j < 4; j++) { 614 switch (j) { 615 case 0: 616 a = s1; 617 b = s2; 618 break; 619 case 1: 620 a = e1; 621 b = s2; 622 break; 623 case 2: 624 a = e1; 625 b = e2; 626 break; 627 case 3: 628 a = s1; 629 b = e2; 630 } 631 for (i = 0; i < 3; i++) { 632 p[i] = q[i] + a * v1[i] + b * v2[i]; 633 } 634 c2d = view.project3DTo2D(p); 635 this.dataX.push(c2d[1]); 636 this.dataY.push(c2d[2]); 637 } 638 // Close the curve 639 this.dataX.push(this.dataX[0]); 640 this.dataY.push(this.dataY[0]); 641 } 642 return { X: this.dataX, Y: this.dataY }; 643 }, 644 645 update: function () { 646 return this; 647 }, 648 649 updateRenderer: function () { 650 this.needsUpdate = false; 651 return this; 652 } 653 } 654 ); 655 656 // TODO docs 657 JXG.createPlane3D = function (board, parents, attributes) { 658 var view = parents[0], 659 attr, 660 point, 661 dir1 = parents[2], 662 dir2 = parents[3], 663 range1 = parents[4] || [-Infinity, Infinity], 664 range2 = parents[5] || [-Infinity, Infinity], 665 el, 666 grid; 667 668 point = Type.providePoints3D(view, [parents[1]], attributes, "plane3d", ["point"])[0]; 669 if (point === false) { 670 // TODO Throw error 671 } 672 673 attr = Type.copyAttributes(attributes, board.options, "plane3d"); 674 el = new JXG.Plane3D(view, point, dir1, range1, dir2, range2, attr); 675 point.addChild(el); 676 677 el.element2D = view.create("curve", [[], []], attr); 678 el.element2D.updateDataArray = function () { 679 var ret = el.updateDataArray(); 680 this.dataX = ret.X; 681 this.dataY = ret.Y; 682 }; 683 el.addChild(el.element2D); 684 el.inherits.push(el.element2D); 685 el.element2D.setParents(el); 686 687 attr = Type.copyAttributes(attributes.mesh3d, board.options, "mesh3d"); 688 if ( 689 Math.abs(el.range1[0]) !== Infinity && 690 Math.abs(el.range1[1]) !== Infinity && 691 Math.abs(el.range2[0]) !== Infinity && 692 Math.abs(el.range2[1]) !== Infinity 693 ) { 694 grid = view.create( 695 "mesh3d", 696 [ 697 function () { 698 return point.coords; 699 }, 700 dir1, 701 range1, 702 dir2, 703 range2 704 ], 705 attr 706 ); 707 el.grid = grid; 708 el.addChild(grid); 709 el.inherits.push(grid); 710 grid.setParents(el); 711 } 712 713 el.element2D.prepareUpdate().update(); 714 if (!board.isSuspendedUpdate) { 715 el.element2D.updateVisibility().updateRenderer(); 716 } 717 718 return el; 719 }; 720 JXG.registerElement("plane3d", JXG.createPlane3D); 721