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 /** 274 * @class 275 * @ignore 276 */ 277 points[0].F = function () { 278 return el.getPointCoords(Type.evaluate(el.range[0])); 279 }; 280 points[0].prepareUpdate().update(); 281 point1 = points[0]; 282 283 /** 284 * @class 285 * @ignore 286 */ 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 attr = el.setAttr2D(attr); 296 el.element2D = view.create('segment', [point1.element2D, point2.element2D], attr); 297 el.addChild(el.element2D); 298 el.inherits.push(el.element2D); 299 el.element2D.setParents(el); 300 // el.setParents([point1.id, point2.id]); 301 302 el.point1 = point1; 303 el.point2 = point2; 304 if (el.point1._is_new) { 305 el.addChild(el.point1); 306 delete el.point1._is_new; 307 } else { 308 el.point1.addChild(el); 309 } 310 if (el.point2._is_new) { 311 el.addChild(el.point2); 312 delete el.point2._is_new; 313 } else { 314 el.point2.addChild(el); 315 } 316 if (Type.exists(point)) { 317 if (point._is_new) { 318 el.addChild(point); 319 delete point._is_new; 320 } else { 321 point.addChild(el); 322 } 323 } 324 325 el.update(); 326 el.element2D.prepareUpdate().update().updateRenderer(); 327 return el; 328 }; 329 JXG.registerElement('line3d', JXG.createLine3D); 330 331 // ----------------------- 332 // Planes 333 // ----------------------- 334 335 /** 336 * Constructor for 3D planes. 337 * @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. 338 * 339 * @augments JXG.GeometryElement3D 340 * @augments JXG.GeometryElement 341 * @param {View3D} view 342 * @param {Point3D|Array} point 343 * @param {Array} direction1 344 * @param {Array} range1 345 * @param {Array} direction2 346 * @param {Array} range2 347 * @param {Object} attributes 348 * @see JXG.Board#generateName 349 */ 350 JXG.Plane3D = function (view, point, dir1, range1, dir2, range2, attributes) { 351 this.constructor(view.board, attributes, Const.OBJECT_TYPE_PLANE3D, Const.OBJECT_CLASS_3D); 352 this.constructor3D(view, 'plane3d'); 353 354 this.board.finalizeAdding(this); 355 356 /** 357 * 3D point which - together with two direction vectors - defines the plane. 358 * 359 * @type JXG.Point3D 360 * 361 * @see JXG.3D#direction1 362 * @see JXG.3D#direction2 363 */ 364 this.point = point; 365 366 /** 367 * Two linearly independent vectors - together with a point - define the plane. Each of these direction vectors is an 368 * array of numbers or functions (of length 3) or function returning array of length 3. 369 * 370 * @type Array|Function 371 * 372 * @see JXG.Plane3D#point 373 * @see JXG.Plane3D#direction2 374 */ 375 this.direction1 = dir1; 376 377 /** 378 * Two linearly independent vectors - together with a point - define the plane. Each of these direction vectors is an 379 * array of numbers or functions (of length 3) or function returning array of length 3. 380 * 381 * @type Array|Function 382 * @see JXG.Plane3D#point 383 * @see JXG.Plane3D#direction1 384 */ 385 this.direction2 = dir2; 386 387 /** 388 * Range [r1, r2] of {@link direction1}. The 3D line goes from (point + r1 * direction1) to (point + r2 * direction1) 389 * @type {Array} 390 */ 391 this.range1 = range1 || [-Infinity, Infinity]; 392 393 /** 394 * Range [r1, r2] of {@link direction2}. The 3D line goes from (point + r1 * direction2) to (point + r2 * direction2) 395 * @type {Array} 396 */ 397 this.range2 = range2 || [-Infinity, Infinity]; 398 399 /** 400 * Direction vector 1 of the 3D plane. Contains the evaluated coordinates from {@link direction1} and {@link range1}. 401 * @type Array 402 * @private 403 * 404 * @see updateNormal 405 */ 406 this.vec1 = [0, 0, 0]; 407 408 /** 409 * Direction vector 2 of the 3D plane. Contains the evaluated coordinates from {@link direction2} and {@link range2}. 410 * 411 * @type Array 412 * @private 413 * 414 * @see updateNormal 415 */ 416 this.vec2 = [0, 0, 0]; 417 418 this.grid = null; 419 420 /** 421 * Normal vector of the plane. Left hand side of the Hesse normal form. 422 423 * @type Array 424 * @private 425 * 426 * @see updateNormal 427 * 428 */ 429 this.normal = [0, 0, 0]; 430 431 /** 432 * Right hand side of the Hesse normal form. 433 434 * @type Array 435 * @private 436 * 437 * @see updateNormal 438 * 439 */ 440 this.d = 0; 441 442 this.updateNormal(); 443 444 this.methodMap = Type.deepCopy(this.methodMap, { 445 // TODO 446 }); 447 }; 448 JXG.Plane3D.prototype = new JXG.GeometryElement(); 449 Type.copyPrototypeMethods(JXG.Plane3D, JXG.GeometryElement3D, 'constructor3D'); 450 451 JXG.extend( 452 JXG.Plane3D.prototype, 453 /** @lends JXG.Plane3D.prototype */ { 454 /** 455 * Update the Hesse normal form of the plane, i.e. update normal vector and right hand side. 456 * Updates also {@link vec1} and {@link vec2}. 457 * 458 * @name JXG.Plane3D#updateNormal 459 * @function 460 * @returns {Object} Reference to the Plane3D object 461 * @private 462 * @example 463 * plane.updateNormal(); 464 * 465 */ 466 updateNormal: function () { 467 var i, len; 468 for (i = 0; i < 3; i++) { 469 this.vec1[i] = Type.evaluate(this.direction1[i]); 470 this.vec2[i] = Type.evaluate(this.direction2[i]); 471 } 472 473 this.normal = Mat.crossProduct(this.vec1, this.vec2); 474 475 len = Mat.norm(this.normal); 476 if (Math.abs(len) > Mat.eps) { 477 for (i = 0; i < 3; i++) { 478 this.normal[i] /= len; 479 } 480 } 481 this.d = Mat.innerProduct(this.point.coords.slice(1), this.normal, 3); 482 483 return this; 484 }, 485 486 updateDataArray: function () { 487 var s1, e1, s2, e2, c2d, l1, l2, 488 planes = ['xPlaneRear', 'yPlaneRear', 'zPlaneRear'], 489 points = [], 490 v1 = [0, 0, 0], 491 v2 = [0, 0, 0], 492 q = [0, 0, 0], 493 p = [0, 0, 0], 494 d, i, j, a, b, first, pos, pos_akt, 495 view = this.view; 496 497 this.dataX = []; 498 this.dataY = []; 499 500 this.updateNormal(); 501 502 // Infinite plane 503 if ( 504 this.elType !== 'axisplane3d' && 505 view.defaultAxes && 506 Type.evaluate(this.range1[0]) === -Infinity && 507 Type.evaluate(this.range1[1]) === Infinity && 508 Type.evaluate(this.range2[0]) === -Infinity && 509 Type.evaluate(this.range2[1]) === Infinity 510 ) { 511 // Start with the rear plane. 512 // Determine the intersections with the view bbox3d 513 // For each face of the bbox3d we determine two points 514 // which are the ends of the intersection line. 515 // We start with the three rear planes. 516 for (j = 0; j < planes.length; j++) { 517 p = view.intersectionPlanePlane(this, view.defaultAxes[planes[j]]); 518 519 if (p[0].length === 3 && p[1].length === 3) { 520 // This test is necessary to filter out intersection lines which are 521 // identical to intersections of axis planes (they would occur twice). 522 for (i = 0; i < points.length; i++) { 523 if ( 524 (Geometry.distance(p[0], points[i][0], 3) < Mat.eps && 525 Geometry.distance(p[1], points[i][1], 3) < Mat.eps) || 526 (Geometry.distance(p[0], points[i][1], 3) < Mat.eps && 527 Geometry.distance(p[1], points[i][0], 3) < Mat.eps) 528 ) { 529 break; 530 } 531 } 532 if (i === points.length) { 533 points.push(p.slice()); 534 } 535 } 536 537 // Point on the front plane of the bbox3d 538 p = [0, 0, 0]; 539 p[j] = view.bbox3D[j][1]; 540 541 // d is the rhs of the Hesse normal form of the front plane. 542 d = Mat.innerProduct(p, view.defaultAxes[planes[j]].normal, 3); 543 p = view.intersectionPlanePlane(this, view.defaultAxes[planes[j]], d); 544 545 if (p[0].length === 3 && p[1].length === 3) { 546 // Do the same test as above 547 for (i = 0; i < points.length; i++) { 548 if ( 549 (Geometry.distance(p[0], points[i][0], 3) < Mat.eps && 550 Geometry.distance(p[1], points[i][1], 3) < Mat.eps) || 551 (Geometry.distance(p[0], points[i][1], 3) < Mat.eps && 552 Geometry.distance(p[1], points[i][0], 3) < Mat.eps) 553 ) { 554 break; 555 } 556 } 557 if (i === points.length) { 558 points.push(p.slice()); 559 } 560 } 561 } 562 563 // Concatenate the intersection points to a polygon. 564 // If all wents well, each intersection should appear 565 // twice in the list. 566 first = 0; 567 pos = first; 568 i = 0; 569 do { 570 p = points[pos][i]; 571 if (p.length === 3) { 572 c2d = view.project3DTo2D(p); 573 this.dataX.push(c2d[1]); 574 this.dataY.push(c2d[2]); 575 } 576 i = (i + 1) % 2; 577 p = points[pos][i]; 578 579 pos_akt = pos; 580 for (j = 0; j < points.length; j++) { 581 if (j !== pos && Geometry.distance(p, points[j][0]) < Mat.eps) { 582 pos = j; 583 i = 0; 584 break; 585 } 586 if (j !== pos && Geometry.distance(p, points[j][1]) < Mat.eps) { 587 pos = j; 588 i = 1; 589 break; 590 } 591 } 592 if (pos === pos_akt) { 593 console.log('Error: update plane3d: did not find next', pos); 594 break; 595 } 596 } while (pos !== first); 597 598 c2d = view.project3DTo2D(points[first][0]); 599 this.dataX.push(c2d[1]); 600 this.dataY.push(c2d[2]); 601 } else { 602 // 3D bounded flat 603 s1 = Type.evaluate(this.range1[0]); 604 e1 = Type.evaluate(this.range1[1]); 605 s2 = Type.evaluate(this.range2[0]); 606 e2 = Type.evaluate(this.range2[1]); 607 608 q = this.point.coords.slice(1); 609 610 v1 = this.vec1.slice(); 611 v2 = this.vec2.slice(); 612 l1 = Mat.norm(v1, 3); 613 l2 = Mat.norm(v2, 3); 614 for (i = 0; i < 3; i++) { 615 v1[i] /= l1; 616 v2[i] /= l2; 617 } 618 619 for (j = 0; j < 4; j++) { 620 switch (j) { 621 case 0: 622 a = s1; 623 b = s2; 624 break; 625 case 1: 626 a = e1; 627 b = s2; 628 break; 629 case 2: 630 a = e1; 631 b = e2; 632 break; 633 case 3: 634 a = s1; 635 b = e2; 636 } 637 for (i = 0; i < 3; i++) { 638 p[i] = q[i] + a * v1[i] + b * v2[i]; 639 } 640 c2d = view.project3DTo2D(p); 641 this.dataX.push(c2d[1]); 642 this.dataY.push(c2d[2]); 643 } 644 // Close the curve 645 this.dataX.push(this.dataX[0]); 646 this.dataY.push(this.dataY[0]); 647 } 648 return { X: this.dataX, Y: this.dataY }; 649 }, 650 651 update: function () { 652 return this; 653 }, 654 655 updateRenderer: function () { 656 this.needsUpdate = false; 657 return this; 658 } 659 } 660 ); 661 662 // TODO docs 663 JXG.createPlane3D = function (board, parents, attributes) { 664 var view = parents[0], 665 attr, 666 point, 667 dir1 = parents[2], 668 dir2 = parents[3], 669 range1 = parents[4] || [-Infinity, Infinity], 670 range2 = parents[5] || [-Infinity, Infinity], 671 el, 672 grid; 673 674 point = Type.providePoints3D(view, [parents[1]], attributes, 'plane3d', ['point'])[0]; 675 if (point === false) { 676 // TODO Throw error 677 } 678 679 attr = Type.copyAttributes(attributes, board.options, 'plane3d'); 680 el = new JXG.Plane3D(view, point, dir1, range1, dir2, range2, attr); 681 point.addChild(el); 682 683 attr = el.setAttr2D(attr); 684 el.element2D = view.create('curve', [[], []], attr); 685 686 /** 687 * @class 688 * @ignore 689 */ 690 el.element2D.updateDataArray = function () { 691 var ret = el.updateDataArray(); 692 this.dataX = ret.X; 693 this.dataY = ret.Y; 694 }; 695 el.addChild(el.element2D); 696 el.inherits.push(el.element2D); 697 el.element2D.setParents(el); 698 699 attr = Type.copyAttributes(attributes.mesh3d, board.options, 'mesh3d'); 700 if ( 701 Math.abs(el.range1[0]) !== Infinity && 702 Math.abs(el.range1[1]) !== Infinity && 703 Math.abs(el.range2[0]) !== Infinity && 704 Math.abs(el.range2[1]) !== Infinity 705 ) { 706 grid = view.create('mesh3d', [ 707 function () { 708 return point.coords; 709 }, 710 dir1, range1, dir2, range2 711 ], attr 712 ); 713 el.grid = grid; 714 el.addChild(grid); 715 el.inherits.push(grid); 716 grid.setParents(el); 717 } 718 719 el.element2D.prepareUpdate().update(); 720 if (!board.isSuspendedUpdate) { 721 el.element2D.updateVisibility().updateRenderer(); 722 } 723 724 return el; 725 }; 726 JXG.registerElement('plane3d', JXG.createPlane3D); 727