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 Geometry from "../math/geometry.js"; 34 import Type from "../utils/type.js"; 35 import Mat from "../math/math.js"; 36 37 /** 38 * Constructor for 3D curves. 39 * @class Creates a new 3D curve object. Do not use this constructor to create a 3D curve. Use {@link JXG.View3D#create} with type {@link Curve3D} instead. 40 * 41 * @augments JXG.GeometryElement3D 42 * @augments JXG.GeometryElement 43 * @param {View3D} view 44 * @param {Function} F 45 * @param {Function} X 46 * @param {Function} Y 47 * @param {Function} Z 48 * @param {Array} range 49 * @param {Object} attributes 50 * @see JXG.Board#generateName 51 */ 52 JXG.Curve3D = function (view, F, X, Y, Z, range, attributes) { 53 this.constructor(view.board, attributes, Const.OBJECT_TYPE_CURVE3D, Const.OBJECT_CLASS_3D); 54 this.constructor3D(view, 'curve3d'); 55 56 this.board.finalizeAdding(this); 57 58 /** 59 * Internal function defining the surface without applying any transformations. 60 * Does only exist if it or X are supplied as a function. Otherwise it is null. 61 * 62 * @function 63 * @private 64 */ 65 this._F = F; 66 67 /** 68 * Function or array which maps u to x; i.e. it defines the x-coordinate of the curve 69 * @function 70 * @returns Number 71 * @private 72 */ 73 this._X = X; 74 75 /** 76 * Function or array which maps u to y; i.e. it defines the y-coordinate of the curve 77 * @function 78 * @returns Number 79 * @private 80 */ 81 this._Y = Y; 82 83 /** 84 * Function or array which maps u to z; i.e. it defines the z-coordinate of the curve 85 * @function 86 * @returns Number 87 * @private 88 */ 89 this._Z = Z; 90 91 this.points = []; 92 93 this.numberPoints = 0; 94 95 this.dataX = null; 96 this.dataY = null; 97 this.dataZ = null; 98 99 if (this._F !== null) { 100 this._X = function (u) { 101 return this._F(u)[0]; 102 }; 103 this._Y = function (u) { 104 return this._F(u)[1]; 105 }; 106 this._Z = function (u) { 107 return this._F(u)[2]; 108 }; 109 } else { 110 if (Type.isFunction(this._X)) { 111 this._F = function(u) { 112 return [this._X(u), this._Y(u), this._Z(u)]; 113 }; 114 } else { 115 this._F = null; 116 } 117 } 118 119 this.range = range; 120 }; 121 JXG.Curve3D.prototype = new JXG.GeometryElement(); 122 123 Type.copyPrototypeMethods(JXG.Curve3D, JXG.GeometryElement3D, 'constructor3D'); 124 Type.copyMethodMap(JXG.Curve3D, { 125 // TODO 126 }); 127 128 JXG.extend( 129 JXG.Curve3D.prototype, 130 /** @lends JXG.Curve3D.prototype */ { 131 132 /** 133 * Simple curve plotting algorithm. 134 * 135 * @returns {JXG.Curve3D} Reference to itself 136 */ 137 updateCoords: function() { 138 var steps = this.evalVisProp('numberpointshigh'), 139 r, s, e, delta, 140 u, i, 141 c3d = [1, 0, 0, 0]; 142 143 this.points = []; 144 145 if (Type.exists(this.dataX)) { 146 steps = this.dataX.length; 147 for (u = 0; u < steps; u++) { 148 this.points.push([1, this.dataX[u], this.dataY[u], this.dataZ[u]]); 149 } 150 } else if (Type.isArray(this._X)) { 151 steps = this._X.length; 152 for (u = 0; u < steps; u++) { 153 this.points.push([1, this._X[u], this._Y[u], this._Z[u]]); 154 } 155 } else { 156 r = Type.evaluate(this.range); 157 s = Type.evaluate(r[0]); 158 e = Type.evaluate(r[1]); 159 delta = (e - s) / (steps - 1); 160 for (i = 0, u = s; i < steps && u <= e; i++, u += delta) { 161 c3d = this.F(u); 162 c3d.unshift(1); 163 this.points.push(c3d); 164 } 165 } 166 this.numberPoints = this.points.length; 167 168 return this; 169 }, 170 171 /** 172 * Generic function which evaluates the function term of the curve 173 * and applies its transformations. 174 * @param {Number} u 175 * @returns 176 */ 177 evalF: function(u) { 178 var t, i, 179 c3d = [0, 0, 0, 0]; 180 181 if (this.transformations.length === 0 || !Type.exists(this.baseElement)) { 182 if (Type.exists(this._F)) { 183 c3d = this._F(u); 184 } else { 185 c3d = [this._X[u], this._Y[u], this._Z[u]]; 186 } 187 return c3d; 188 } 189 190 t = this.transformations; 191 for (i = 0; i < t.length; i++) { 192 t[i].update(); 193 } 194 if (c3d.length === 3) { 195 c3d.unshift(1); 196 } 197 198 if (this === this.baseElement) { 199 if (Type.exists(this._F)) { 200 c3d = this._F(u); 201 } else { 202 c3d = [this._X[u], this._Y[u], this._Z[u]]; 203 } 204 } else { 205 c3d = this.baseElement.evalF(u); 206 } 207 c3d.unshift(1); 208 c3d = Mat.matVecMult(t[0].matrix, c3d); 209 for (i = 1; i < t.length; i++) { 210 c3d = Mat.matVecMult(t[i].matrix, c3d); 211 } 212 213 return c3d.slice(1); 214 }, 215 216 /** 217 * Function defining the curve plus applying transformations. 218 * @param {Number} u 219 * @returns Array [x, y, z] of length 3 220 */ 221 F: function(u) { 222 return this.evalF(u); 223 }, 224 225 /** 226 * Function which maps (u) to z; i.e. it defines the x-coordinate of the curve 227 * plus applying transformations. 228 * @param {Number} u 229 * @returns Number 230 */ 231 X: function(u) { 232 return this.evalF(u)[0]; 233 }, 234 235 /** 236 * Function which maps (u) to y; i.e. it defines the y-coordinate of the curve 237 * plus applying transformations. 238 * @param {Number} u 239 * @returns Number 240 */ 241 Y: function(u) { 242 return this.evalF(u)[1]; 243 }, 244 245 /** 246 * Function which maps (u) to z; i.e. it defines the z-coordinate of the curve 247 * plus applying transformations. 248 * @param {Number} u 249 * @returns Number 250 */ 251 Z: function(u) { 252 return this.evalF(u)[2]; 253 }, 254 255 updateDataArray2D: function () { 256 var i, c2d, 257 dataX = [], 258 dataY = [], 259 len = this.points.length; 260 261 for (i = 0; i < len; i++) { 262 c2d = this.view.project3DTo2D(this.points[i]); 263 dataX.push(c2d[1]); 264 dataY.push(c2d[2]); 265 } 266 267 return { X: dataX, Y: dataY }; 268 }, 269 270 // Already documented in GeometryElement 271 addTransform: function (el, transform) { 272 this.addTransformGeneric(el, transform); 273 return this; 274 }, 275 276 // Already documented in GeometryElement 277 removeTransform: function (transform) { 278 this.removeTransformGeneric(transform); 279 return this; 280 }, 281 282 // Already documented in GeometryElement 283 clearTransforms: function () { 284 this.clearTransformsGeneric(); 285 return this; 286 }, 287 288 /** 289 * 290 * @returns {JXG.Curve3D} Reference to itself 291 */ 292 updateTransform: function () { 293 var t, c, i, j, len; 294 295 if (this.transformations.length === 0 || this.baseElement === null || 296 Type.exists(this._F) // Transformations have only to be applied here 297 // if the curve is defined by arrays 298 ) { 299 return this; 300 } 301 302 t = this.transformations; 303 for (i = 0; i < t.length; i++) { 304 t[i].update(); 305 } 306 len = this.baseElement.numberPoints; 307 for (i = 0; i < len; i++) { 308 if (this === this.baseElement) { 309 c = this.points[i]; 310 } else { 311 c = this.baseElement.points[i]; 312 } 313 for (j = 0; j < t.length; j++) { 314 c = Mat.matVecMult(t[j].matrix, c); 315 } 316 this.points[i] = c; 317 } 318 this.numberPoints = len; 319 320 return this; 321 }, 322 323 // Already documented in GeometryElement 324 updateDataArray: function() { /* stub */ }, 325 326 // Already documented in GeometryElement 327 update: function () { 328 if (this.needsUpdate) { 329 this.updateDataArray(); 330 this.updateCoords() 331 .updateTransform(); 332 } 333 return this; 334 }, 335 336 // Already documented in GeometryElement 337 updateRenderer: function () { 338 this.needsUpdate = false; 339 return this; 340 }, 341 342 // Already documented in element3d.js 343 projectCoords: function (p, params) { 344 return Geometry.projectCoordsToParametric(p, this, 1, params); 345 } 346 347 // Use method from element3d.js 348 // projectScreenCoords: function (pScr, params, cyclic) { 349 // this.initParamsIfNeeded(params); 350 // return Geometry.projectScreenCoordsToParametric(pScr, this, params, cyclic); 351 // } 352 } 353 ); 354 355 /** 356 * @class 3D Curves can be defined by mappings or by discrete data sets. 357 * In general, a 3D curve is a mapping from R to R^3, where t maps to (x(t),y(t),z(t)). 358 * The graph is drawn for t in the interval [a,b]. 359 * @pseudo 360 * @description A 3D parametric curve is defined by a function 361 * <i>F: R<sup>1</sup> → R<sup>3</sup></i>. 362 * 363 * @name Curve3D 364 * @augments Curve 365 * @constructor 366 * @type Object 367 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 368 * @param {Function_Function_Function_Array,Function} F<sub>X</sub>,F<sub>Y</sub>,F<sub>Z</sub>,range 369 * F<sub>X</sub>(u), F<sub>Y</sub>(u), F<sub>Z</sub>(u) are functions returning a number, range is the array containing 370 * lower and upper bound for the range of the parameter u. range may also be a function returning an array of length two. 371 * @param {Function_Array,Function} F,range Alternatively: F<sub>[X,Y,Z]</sub>(u) a function returning an array [x,y,z] of 372 * numbers, range as above. 373 * @param {Array_Array_Array} X,Y,Z Three arrays containing the coordinate points which define the curve. 374 * @example 375 * // create a simple curve in 3d 376 * var bound = [-1.5, 1.5]; 377 * var view=board.create('view3d', 378 * [[-4, -4],[8, 8], 379 * [bound, bound, bound]], 380 * {}); 381 * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]); 382 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 383 * <script type="text/javascript"> 384 * (function() { 385 * var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3', 386 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 387 * // create a simple curve in 3d 388 * var bound = [-1.5, 1.5]; 389 * var view=board.create('view3d', 390 * [[-4, -4],[8, 8], 391 * [bound, bound, bound]], 392 * {}); 393 * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]); 394 * })(); 395 * </script><pre> 396 */ 397 JXG.createCurve3D = function (board, parents, attributes) { 398 var view = parents[0], 399 F, X, Y, Z, range, attr, el, 400 mat, 401 base = null, 402 transform = null; 403 404 if (parents.length === 3) { 405 if (Type.isTransformationOrArray(parents[2]) && parents[1].type === Const.OBJECT_TYPE_CURVE3D) { 406 // [curve, transformation(s)] 407 // This might be adopted to the type of the base element (data plot or function) 408 base = parents[1]; 409 transform = parents[2]; 410 F = null; 411 X = []; 412 Y = []; 413 Z = []; 414 } else { 415 // [F, range] 416 F = parents[1]; 417 range = parents[2]; 418 X = null; 419 Y = null; 420 Z = null; 421 } 422 } else if (parents.length === 2 && Type.isArray(parents[1])) { 423 mat = Mat.transpose(parents[1]); 424 X = mat[0]; 425 Y = mat[1]; 426 Z = mat[2]; 427 F = null; 428 } else { 429 // [X, Y, Z, range] 430 X = parents[1]; 431 Y = parents[2]; 432 Z = parents[3]; 433 range = parents[4]; 434 F = null; 435 } 436 // TODO Throw new Error 437 438 attr = Type.copyAttributes(attributes, board.options, 'curve3d'); 439 el = new JXG.Curve3D(view, F, X, Y, Z, range, attr); 440 441 attr = el.setAttr2D(attr); 442 el.element2D = view.create("curve", [[], []], attr); 443 el.element2D.view = view; 444 el.element2D.dump = false; 445 if (base !== null) { 446 el.addTransform(base, transform); 447 el.addParents(base); 448 } 449 450 /** 451 * @class 452 * @ignore 453 */ 454 el.element2D.updateDataArray = function () { 455 var ret = el.updateDataArray2D(); 456 this.dataX = ret.X; 457 this.dataY = ret.Y; 458 }; 459 el.addChild(el.element2D); 460 el.inherits.push(el.element2D); 461 el.element2D.setParents(el); 462 463 el.element2D.prepareUpdate().update(); 464 if (!board.isSuspendedUpdate) { 465 el.element2D.updateVisibility().updateRenderer(); 466 } 467 468 return el; 469 }; 470 471 JXG.registerElement("curve3d", JXG.createCurve3D); 472 473 /** 474 * @class A vector field is an assignment of a vector to each point in 3D space. 475 * <p> 476 * Plot a vector field either given by three functions 477 * f1(x, y, z), f2(x, y, z), and f3(x, y, z) or by a function f(x, y, z) 478 * returning an array of size 3. 479 * 480 * @pseudo 481 * @name Vectorfield3D 482 * @augments JXG.Curve3D 483 * @constructor 484 * @type JXG.Curve3D 485 * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown. 486 * Parameter options: 487 * @param {Array|Function|String} F Either an array containing three functions f1(x, y, z), f2(x, y, z), 488 * and f3(x, y) or function f(x, y, z) returning an array of length 3. 489 * @param {Array} xData Array of length 3 containing start value for x, number of steps, 490 * end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x. 491 * @param {Array} yData Array of length 3 containing start value for y, number of steps, 492 * end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y. 493 * @param {Array} zData Array of length 3 containing start value for z, number of steps, 494 * end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z. 495 * 496 * @example 497 * const view = board.create('view3d', 498 * [ 499 * [-6, -3], 500 * [8, 8], 501 * [[-3, 3], [-3, 3], [-3, 3]] 502 * ], {}); 503 * 504 * var vf = view.create('vectorfield3d', [ 505 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 506 * [-2, 5, 2], // x from -2 to 2 in 5 steps 507 * [-2, 5, 2], // y 508 * [-2, 5, 2] // z 509 * ], { 510 * strokeColor: 'red', 511 * scale: 0.5 512 * }); 513 * 514 * </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div> 515 * <script type="text/javascript"> 516 * (function() { 517 * var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348', 518 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false, 519 * pan: { 520 * needTwoFingers: true 521 * } 522 * }); 523 * const view = board.create('view3d', 524 * [ 525 * [-6, -3], 526 * [8, 8], 527 * [[-3, 3], [-3, 3], [-3, 3]] 528 * ], {}); 529 * var vf = view.create('vectorfield3d', [ 530 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 531 * [-2, 5, 2], // x from -2 to 2 in 5 steps 532 * [-2, 5, 2], // y 533 * [-2, 5, 2] // z 534 * ], { 535 * strokeColor: 'red', 536 * scale: 0.5 537 * }); 538 * 539 * 540 * })(); 541 * 542 * </script><pre> 543 * 544 */ 545 JXG.createVectorfield3D = function (board, parents, attributes) { 546 var view = parents[0], 547 el, attr; 548 549 if (!(parents.length >= 5 && 550 (Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) && 551 (Type.isArray(parents[2]) && parents[1].length === 3) && 552 (Type.isArray(parents[3]) && parents[2].length === 3) && 553 (Type.isArray(parents[4]) && parents[3].length === 3) 554 )) { 555 throw new Error( 556 "JSXGraph: Can't create vector field 3D with parent types " + 557 "'" + typeof parents[1] + "', " + 558 "'" + typeof parents[2] + "', " + 559 "'" + typeof parents[3] + "'." + 560 "'" + typeof parents[4] + "', " 561 ); 562 } 563 564 attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d'); 565 el = view.create('curve3d', [[], [], []], attr); 566 567 /** 568 * Set the defining functions of 3D vector field. 569 * @memberOf Vectorfield3D 570 * @name setF 571 * @function 572 * @param {Array|Function} func Either an array containing three functions f1(x, y, z), 573 * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3. 574 * @returns {Object} Reference to the 3D vector field object. 575 * 576 * @example 577 * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]); 578 * board.update(); 579 * 580 */ 581 el.setF = function (func, varnames) { 582 var f0, f1, f2; 583 if (Type.isArray(func)) { 584 f0 = Type.createFunction(func[0], this.board, varnames); 585 f1 = Type.createFunction(func[1], this.board, varnames); 586 f2 = Type.createFunction(func[2], this.board, varnames); 587 /** 588 * @ignore 589 */ 590 this.F = function (x, y, z) { 591 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)]; 592 }; 593 } else { 594 this.F = Type.createFunction(func, el.board, varnames); 595 } 596 return this; 597 }; 598 599 el.setF(parents[1], 'x, y, z'); 600 el.xData = parents[2]; 601 el.yData = parents[3]; 602 el.zData = parents[4]; 603 604 el.updateDataArray = function () { 605 var k, i, j, 606 v, nrm, 607 x, y, z, 608 scale = this.evalVisProp('scale'), 609 start = [ 610 Type.evaluate(this.xData[0]), 611 Type.evaluate(this.yData[0]), 612 Type.evaluate(this.zData[0]) 613 ], 614 steps = [ 615 Type.evaluate(this.xData[1]), 616 Type.evaluate(this.yData[1]), 617 Type.evaluate(this.zData[1]) 618 ], 619 end = [ 620 Type.evaluate(this.xData[2]), 621 Type.evaluate(this.yData[2]), 622 Type.evaluate(this.zData[2]) 623 ], 624 delta = [ 625 (end[0] - start[0]) / steps[0], 626 (end[1] - start[1]) / steps[1], 627 (end[2] - start[2]) / steps[2] 628 ], 629 phi, theta1, theta2, theta, 630 showArrow = this.evalVisProp('arrowhead.enabled'), 631 leg, leg_x, leg_y, leg_z, alpha; 632 633 if (showArrow) { 634 // Arrow head style 635 // leg = 8; 636 // alpha = Math.PI * 0.125; 637 leg = this.evalVisProp('arrowhead.size'); 638 alpha = this.evalVisProp('arrowhead.angle'); 639 leg_x = leg / board.unitX; 640 leg_y = leg / board.unitY; 641 leg_z = leg / Math.sqrt(board.unitX * board.unitY); 642 } 643 644 this.dataX = []; 645 this.dataY = []; 646 this.dataZ = []; 647 for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) { 648 for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) { 649 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) { 650 v = this.F(x, y, z); 651 nrm = Mat.norm(v); 652 if (nrm < Number.EPSILON) { 653 continue; 654 } 655 656 v[0] *= scale; 657 v[1] *= scale; 658 v[2] *= scale; 659 Type.concat(this.dataX, [x, x + v[0], NaN]); 660 Type.concat(this.dataY, [y, y + v[1], NaN]); 661 Type.concat(this.dataZ, [z, z + v[2], NaN]); 662 663 if (showArrow) { 664 // Arrow head 665 nrm *= scale; 666 phi = Math.atan2(v[1], v[0]); 667 theta = Math.asin(v[2] / nrm); 668 theta1 = theta - alpha; 669 theta2 = theta + alpha; 670 Type.concat(this.dataX, [ 671 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1), 672 x + v[0], 673 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2), 674 NaN]); 675 Type.concat(this.dataY, [ 676 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1), 677 y + v[1], 678 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2), 679 NaN]); 680 Type.concat(this.dataZ, [ 681 z + v[2] - leg_z * Math.sin(theta2), 682 z + v[2], 683 z + v[2] - leg_z * Math.sin(theta1), 684 NaN]); 685 } 686 } 687 } 688 } 689 }; 690 691 Type.extendInstanceMethodMap(el, { 692 setF: "setF" 693 }); 694 695 return el; 696 }; 697 698 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D); 699