1 /* 2 Copyright 2008-2025 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 this.methodMap = Type.deepCopy(this.methodMap, { 122 // TODO 123 }); 124 }; 125 JXG.Curve3D.prototype = new JXG.GeometryElement(); 126 Type.copyPrototypeMethods(JXG.Curve3D, JXG.GeometryElement3D, "constructor3D"); 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 /** 277 * 278 * @returns {JXG.Curve3D} Reference to itself 279 */ 280 updateTransform: function () { 281 var t, c, i, j, len; 282 283 if (this.transformations.length === 0 || this.baseElement === null || 284 Type.exists(this._F) // Transformations have only to be applied here 285 // if the curve is defined by arrays 286 ) { 287 return this; 288 } 289 290 t = this.transformations; 291 for (i = 0; i < t.length; i++) { 292 t[i].update(); 293 } 294 len = this.baseElement.numberPoints; 295 for (i = 0; i < len; i++) { 296 if (this === this.baseElement) { 297 c = this.points[i]; 298 } else { 299 c = this.baseElement.points[i]; 300 } 301 for (j = 0; j < t.length; j++) { 302 c = Mat.matVecMult(t[j].matrix, c); 303 } 304 this.points[i] = c; 305 } 306 this.numberPoints = len; 307 308 return this; 309 }, 310 311 // Already documented in GeometryElement 312 updateDataArray: function() { /* stub */ }, 313 314 // Already documented in GeometryElement 315 update: function () { 316 if (this.needsUpdate) { 317 this.updateDataArray(); 318 this.updateCoords() 319 .updateTransform(); 320 } 321 return this; 322 }, 323 324 // Already documented in GeometryElement 325 updateRenderer: function () { 326 this.needsUpdate = false; 327 return this; 328 }, 329 330 // Already documented in element3d.js 331 projectCoords: function (p, params) { 332 return Geometry.projectCoordsToParametric(p, this, 1, params); 333 } 334 335 // projectScreenCoords: function (pScr, params) { 336 // this.initParamsIfNeeded(params); 337 // return Geometry.projectScreenCoordsToParametric(pScr, this, params); 338 // } 339 } 340 ); 341 342 /** 343 * @class 3D Curves can be defined by mappings or by discrete data sets. 344 * In general, a 3D curve is a mapping from R to R^3, where t maps to (x(t),y(t),z(t)). 345 * The graph is drawn for t in the interval [a,b]. 346 * @pseudo 347 * @description A 3D parametric curve is defined by a function 348 * <i>F: R<sup>1</sup> → R<sup>3</sup></i>. 349 * 350 * @name Curve3D 351 * @augments Curve 352 * @constructor 353 * @type Object 354 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 355 * @param {Function_Function_Function_Array,Function} F<sub>X</sub>,F<sub>Y</sub>,F<sub>Z</sub>,range 356 * 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 357 * lower and upper bound for the range of the parameter u. range may also be a function returning an array of length two. 358 * @param {Function_Array,Function} F,range Alternatively: F<sub>[X,Y,Z]</sub>(u) a function returning an array [x,y,z] of 359 * numbers, range as above. 360 * @param {Array_Array_Array} X,Y,Z Three arrays containing the coordinate points which define the curve. 361 * @example 362 * // create a simple curve in 3d 363 * var bound = [-1.5, 1.5]; 364 * var view=board.create('view3d', 365 * [[-4, -4],[8, 8], 366 * [bound, bound, bound]], 367 * {}); 368 * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]); 369 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 370 * <script type="text/javascript"> 371 * (function() { 372 * var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3', 373 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 374 * // create a simple curve in 3d 375 * var bound = [-1.5, 1.5]; 376 * var view=board.create('view3d', 377 * [[-4, -4],[8, 8], 378 * [bound, bound, bound]], 379 * {}); 380 * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]); 381 * })(); 382 * </script><pre> 383 */ 384 JXG.createCurve3D = function (board, parents, attributes) { 385 var view = parents[0], 386 F, X, Y, Z, range, attr, el, 387 mat, 388 base = null, 389 transform = null; 390 391 if (parents.length === 3) { 392 if (Type.isTransformationOrArray(parents[2]) && parents[1].type === Const.OBJECT_TYPE_CURVE3D) { 393 // [curve, transformation(s)] 394 // This might be adopted to the type of the base element (data plot or function) 395 base = parents[1]; 396 transform = parents[2]; 397 F = null; 398 X = []; 399 Y = []; 400 Z = []; 401 } else { 402 // [F, range] 403 F = parents[1]; 404 range = parents[2]; 405 X = null; 406 Y = null; 407 Z = null; 408 } 409 } else if (parents.length === 2 && Type.isArray(parents[1])) { 410 mat = Mat.transpose(parents[1]); 411 X = mat[0]; 412 Y = mat[1]; 413 Z = mat[2]; 414 F = null; 415 } else { 416 // [X, Y, Z, range] 417 X = parents[1]; 418 Y = parents[2]; 419 Z = parents[3]; 420 range = parents[4]; 421 F = null; 422 } 423 // TODO Throw new Error 424 425 attr = Type.copyAttributes(attributes, board.options, "curve3d"); 426 el = new JXG.Curve3D(view, F, X, Y, Z, range, attr); 427 428 attr = el.setAttr2D(attr); 429 el.element2D = view.create("curve", [[], []], attr); 430 el.element2D.view = view; 431 if (base !== null) { 432 el.addTransform(base, transform); 433 el.addParents(base); 434 } 435 436 /** 437 * @class 438 * @ignore 439 */ 440 el.element2D.updateDataArray = function () { 441 var ret = el.updateDataArray2D(); 442 this.dataX = ret.X; 443 this.dataY = ret.Y; 444 }; 445 el.addChild(el.element2D); 446 el.inherits.push(el.element2D); 447 el.element2D.setParents(el); 448 449 el.element2D.prepareUpdate().update(); 450 if (!board.isSuspendedUpdate) { 451 el.element2D.updateVisibility().updateRenderer(); 452 } 453 454 return el; 455 }; 456 457 JXG.registerElement("curve3d", JXG.createCurve3D); 458 459 /** 460 * @class A vector field is an assignment of a vector to each point in 3D space. 461 * <p> 462 * Plot a vector field either given by three functions 463 * f1(x, y, z), f2(x, y, z), and f3(x, y, z) or by a function f(x, y, z) 464 * returning an array of size 3. 465 * 466 * @pseudo 467 * @name Vectorfield3D 468 * @augments JXG.Curve3D 469 * @constructor 470 * @type JXG.Curve3D 471 * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown. 472 * Parameter options: 473 * @param {Array|Function|String} F Either an array containing three functions f1(x, y, z), f2(x, y, z), 474 * and f3(x, y) or function f(x, y, z) returning an array of length 3. 475 * @param {Array} xData Array of length 3 containing start value for x, number of steps, 476 * end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x. 477 * @param {Array} yData Array of length 3 containing start value for y, number of steps, 478 * end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y. 479 * @param {Array} zData Array of length 3 containing start value for z, number of steps, 480 * end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z. 481 * 482 * @example 483 * const view = board.create('view3d', 484 * [ 485 * [-6, -3], 486 * [8, 8], 487 * [[-3, 3], [-3, 3], [-3, 3]] 488 * ], {}); 489 * 490 * var vf = view.create('vectorfield3d', [ 491 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 492 * [-2, 5, 2], // x from -2 to 2 in 5 steps 493 * [-2, 5, 2], // y 494 * [-2, 5, 2] // z 495 * ], { 496 * strokeColor: 'red', 497 * scale: 0.5 498 * }); 499 * 500 * </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div> 501 * <script type="text/javascript"> 502 * (function() { 503 * var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348', 504 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false, 505 * pan: { 506 * needTwoFingers: true 507 * } 508 * }); 509 * const view = board.create('view3d', 510 * [ 511 * [-6, -3], 512 * [8, 8], 513 * [[-3, 3], [-3, 3], [-3, 3]] 514 * ], {}); 515 * var vf = view.create('vectorfield3d', [ 516 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 517 * [-2, 5, 2], // x from -2 to 2 in 5 steps 518 * [-2, 5, 2], // y 519 * [-2, 5, 2] // z 520 * ], { 521 * strokeColor: 'red', 522 * scale: 0.5 523 * }); 524 * 525 * 526 * })(); 527 * 528 * </script><pre> 529 * 530 */ 531 JXG.createVectorfield3D = function (board, parents, attributes) { 532 var view = parents[0], 533 el, attr; 534 535 if (!(parents.length >= 5 && 536 (Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) && 537 (Type.isArray(parents[2]) && parents[1].length === 3) && 538 (Type.isArray(parents[3]) && parents[2].length === 3) && 539 (Type.isArray(parents[4]) && parents[3].length === 3) 540 )) { 541 throw new Error( 542 "JSXGraph: Can't create vector field 3D with parent types " + 543 "'" + typeof parents[1] + "', " + 544 "'" + typeof parents[2] + "', " + 545 "'" + typeof parents[3] + "'." + 546 "'" + typeof parents[4] + "', " 547 ); 548 } 549 550 attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d'); 551 el = view.create('curve3d', [[], [], []], attr); 552 553 /** 554 * Set the defining functions of 3D vector field. 555 * @memberOf Vectorfield3D 556 * @name setF 557 * @function 558 * @param {Array|Function} func Either an array containing three functions f1(x, y, z), 559 * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3. 560 * @returns {Object} Reference to the 3D vector field object. 561 * 562 * @example 563 * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]); 564 * board.update(); 565 * 566 */ 567 el.setF = function (func, varnames) { 568 var f0, f1, f2; 569 if (Type.isArray(func)) { 570 f0 = Type.createFunction(func[0], this.board, varnames); 571 f1 = Type.createFunction(func[1], this.board, varnames); 572 f2 = Type.createFunction(func[2], this.board, varnames); 573 /** 574 * @ignore 575 */ 576 this.F = function (x, y, z) { 577 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)]; 578 }; 579 } else { 580 this.F = Type.createFunction(func, el.board, varnames); 581 } 582 return this; 583 }; 584 585 el.setF(parents[1], 'x, y, z'); 586 el.xData = parents[2]; 587 el.yData = parents[3]; 588 el.zData = parents[4]; 589 590 el.updateDataArray = function () { 591 var k, i, j, 592 v, nrm, 593 x, y, z, 594 scale = this.evalVisProp('scale'), 595 start = [ 596 Type.evaluate(this.xData[0]), 597 Type.evaluate(this.yData[0]), 598 Type.evaluate(this.zData[0]) 599 ], 600 steps = [ 601 Type.evaluate(this.xData[1]), 602 Type.evaluate(this.yData[1]), 603 Type.evaluate(this.zData[1]) 604 ], 605 end = [ 606 Type.evaluate(this.xData[2]), 607 Type.evaluate(this.yData[2]), 608 Type.evaluate(this.zData[2]) 609 ], 610 delta = [ 611 (end[0] - start[0]) / steps[0], 612 (end[1] - start[1]) / steps[1], 613 (end[2] - start[2]) / steps[2] 614 ], 615 phi, theta1, theta2, theta, 616 showArrow = this.evalVisProp('arrowhead.enabled'), 617 leg, leg_x, leg_y, leg_z, alpha; 618 619 if (showArrow) { 620 // Arrow head style 621 // leg = 8; 622 // alpha = Math.PI * 0.125; 623 leg = this.evalVisProp('arrowhead.size'); 624 alpha = this.evalVisProp('arrowhead.angle'); 625 leg_x = leg / board.unitX; 626 leg_y = leg / board.unitY; 627 leg_z = leg / Math.sqrt(board.unitX * board.unitY); 628 } 629 630 this.dataX = []; 631 this.dataY = []; 632 this.dataZ = []; 633 for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) { 634 for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) { 635 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) { 636 v = this.F(x, y, z); 637 nrm = Mat.norm(v); 638 if (nrm < Number.EPSILON) { 639 continue; 640 } 641 642 v[0] *= scale; 643 v[1] *= scale; 644 v[2] *= scale; 645 Type.concat(this.dataX, [x, x + v[0], NaN]); 646 Type.concat(this.dataY, [y, y + v[1], NaN]); 647 Type.concat(this.dataZ, [z, z + v[2], NaN]); 648 649 if (showArrow) { 650 // Arrow head 651 nrm *= scale; 652 phi = Math.atan2(v[1], v[0]); 653 theta = Math.asin(v[2] / nrm); 654 theta1 = theta - alpha; 655 theta2 = theta + alpha; 656 Type.concat(this.dataX, [ 657 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1), 658 x + v[0], 659 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2), 660 NaN]); 661 Type.concat(this.dataY, [ 662 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1), 663 y + v[1], 664 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2), 665 NaN]); 666 Type.concat(this.dataZ, [ 667 z + v[2] - leg_z * Math.sin(theta2), 668 z + v[2], 669 z + v[2] - leg_z * Math.sin(theta1), 670 NaN]); 671 } 672 } 673 } 674 } 675 }; 676 677 el.methodMap = Type.deepCopy(el.methodMap, { 678 setF: "setF" 679 }); 680 681 return el; 682 }; 683 684 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D); 685