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