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