1 /* 2 Copyright 2008-2024 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 base = null, 388 transform = null; 389 390 if (parents.length === 3) { 391 if (Type.isTransformationOrArray(parents[2]) && parents[1].type === Const.OBJECT_TYPE_CURVE3D) { 392 // [curve, transformation(s)] 393 // This might be adopted to the type of the base element (data plot or function) 394 base = parents[1]; 395 transform = parents[2]; 396 F = null; 397 X = []; 398 Y = []; 399 Z = []; 400 } else { 401 // [F, range] 402 F = parents[1]; 403 range = parents[2]; 404 X = null; 405 Y = null; 406 Z = null; 407 } 408 } else { 409 // [X, Y, Z, range] 410 X = parents[1]; 411 Y = parents[2]; 412 Z = parents[3]; 413 range = parents[4]; 414 F = null; 415 } 416 // TODO Throw new Error 417 418 attr = Type.copyAttributes(attributes, board.options, "curve3d"); 419 el = new JXG.Curve3D(view, F, X, Y, Z, range, attr); 420 421 attr = el.setAttr2D(attr); 422 el.element2D = view.create("curve", [[], []], attr); 423 el.element2D.view = view; 424 if (base !== null) { 425 el.addTransform(base, transform); 426 el.addParents(base); 427 } 428 429 /** 430 * @class 431 * @ignore 432 */ 433 el.element2D.updateDataArray = function () { 434 var ret = el.updateDataArray2D(); 435 this.dataX = ret.X; 436 this.dataY = ret.Y; 437 }; 438 el.addChild(el.element2D); 439 el.inherits.push(el.element2D); 440 el.element2D.setParents(el); 441 442 el.element2D.prepareUpdate().update(); 443 if (!board.isSuspendedUpdate) { 444 el.element2D.updateVisibility().updateRenderer(); 445 } 446 447 return el; 448 }; 449 450 JXG.registerElement("curve3d", JXG.createCurve3D); 451 452 /** 453 * @class A vector field is an assignment of a vector to each point in 3D space. 454 * <p> 455 * Plot a vector field either given by three functions 456 * f1(x, y, z), f2(x, y, z), and f3(x, y, z) or by a function f(x, y, z) 457 * returning an array of size 3. 458 * 459 * @pseudo 460 * @name Vectorfield3D 461 * @augments JXG.Curve3D 462 * @constructor 463 * @type JXG.Curve3D 464 * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown. 465 * Parameter options: 466 * @param {Array|Function|String} F Either an array containing three functions f1(x, y, z), f2(x, y, z), 467 * and f3(x, y) or function f(x, y, z) returning an array of length 3. 468 * @param {Array} xData Array of length 3 containing start value for x, number of steps, 469 * end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x. 470 * @param {Array} yData Array of length 3 containing start value for y, number of steps, 471 * end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y. 472 * @param {Array} zData Array of length 3 containing start value for z, number of steps, 473 * end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z. 474 * 475 * @example 476 * const view = board.create('view3d', 477 * [ 478 * [-6, -3], 479 * [8, 8], 480 * [[-3, 3], [-3, 3], [-3, 3]] 481 * ], {}); 482 * 483 * var vf = view.create('vectorfield3d', [ 484 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 485 * [-2, 5, 2], // x from -2 to 2 in 5 steps 486 * [-2, 5, 2], // y 487 * [-2, 5, 2] // z 488 * ], { 489 * strokeColor: 'red', 490 * scale: 0.5 491 * }); 492 * 493 * </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div> 494 * <script type="text/javascript"> 495 * (function() { 496 * var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348', 497 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false, 498 * pan: { 499 * needTwoFingers: true 500 * } 501 * }); 502 * const view = board.create('view3d', 503 * [ 504 * [-6, -3], 505 * [8, 8], 506 * [[-3, 3], [-3, 3], [-3, 3]] 507 * ], {}); 508 * var vf = view.create('vectorfield3d', [ 509 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 510 * [-2, 5, 2], // x from -2 to 2 in 5 steps 511 * [-2, 5, 2], // y 512 * [-2, 5, 2] // z 513 * ], { 514 * strokeColor: 'red', 515 * scale: 0.5 516 * }); 517 * 518 * 519 * })(); 520 * 521 * </script><pre> 522 * 523 */ 524 JXG.createVectorfield3D = function (board, parents, attributes) { 525 var view = parents[0], 526 el, attr; 527 528 if (!(parents.length >= 5 && 529 (Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) && 530 (Type.isArray(parents[2]) && parents[1].length === 3) && 531 (Type.isArray(parents[3]) && parents[2].length === 3) && 532 (Type.isArray(parents[4]) && parents[3].length === 3) 533 )) { 534 throw new Error( 535 "JSXGraph: Can't create vector field 3D with parent types " + 536 "'" + typeof parents[1] + "', " + 537 "'" + typeof parents[2] + "', " + 538 "'" + typeof parents[3] + "'." + 539 "'" + typeof parents[4] + "', " 540 ); 541 } 542 543 attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d'); 544 el = view.create('curve3d', [[], [], []], attr); 545 546 /** 547 * Set the defining functions of 3D vector field. 548 * @memberOf Vectorfield3D 549 * @name setF 550 * @function 551 * @param {Array|Function} func Either an array containing three functions f1(x, y, z), 552 * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3. 553 * @returns {Object} Reference to the 3D vector field object. 554 * 555 * @example 556 * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]); 557 * board.update(); 558 * 559 */ 560 el.setF = function (func, varnames) { 561 var f0, f1, f2; 562 if (Type.isArray(func)) { 563 f0 = Type.createFunction(func[0], this.board, varnames); 564 f1 = Type.createFunction(func[1], this.board, varnames); 565 f2 = Type.createFunction(func[2], this.board, varnames); 566 /** 567 * @ignore 568 */ 569 this.F = function (x, y, z) { 570 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)]; 571 }; 572 } else { 573 this.F = Type.createFunction(func, el.board, varnames); 574 } 575 return this; 576 }; 577 578 el.setF(parents[1], 'x, y, z'); 579 el.xData = parents[2]; 580 el.yData = parents[3]; 581 el.zData = parents[4]; 582 583 el.updateDataArray = function () { 584 var k, i, j, 585 v, nrm, 586 x, y, z, 587 scale = this.evalVisProp('scale'), 588 start = [ 589 Type.evaluate(this.xData[0]), 590 Type.evaluate(this.yData[0]), 591 Type.evaluate(this.zData[0]) 592 ], 593 steps = [ 594 Type.evaluate(this.xData[1]), 595 Type.evaluate(this.yData[1]), 596 Type.evaluate(this.zData[1]) 597 ], 598 end = [ 599 Type.evaluate(this.xData[2]), 600 Type.evaluate(this.yData[2]), 601 Type.evaluate(this.zData[2]) 602 ], 603 delta = [ 604 (end[0] - start[0]) / steps[0], 605 (end[1] - start[1]) / steps[1], 606 (end[2] - start[2]) / steps[2] 607 ], 608 phi, theta1, theta2, theta, 609 showArrow = this.evalVisProp('arrowhead.enabled'), 610 leg, leg_x, leg_y, leg_z, alpha; 611 612 if (showArrow) { 613 // Arrow head style 614 // leg = 8; 615 // alpha = Math.PI * 0.125; 616 leg = this.evalVisProp('arrowhead.size'); 617 alpha = this.evalVisProp('arrowhead.angle'); 618 leg_x = leg / board.unitX; 619 leg_y = leg / board.unitY; 620 leg_z = leg / Math.sqrt(board.unitX * board.unitY); 621 } 622 623 this.dataX = []; 624 this.dataY = []; 625 this.dataZ = []; 626 for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) { 627 for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) { 628 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) { 629 v = this.F(x, y, z); 630 nrm = Mat.norm(v); 631 if (nrm < Number.EPSILON) { 632 continue; 633 } 634 635 v[0] *= scale; 636 v[1] *= scale; 637 v[2] *= scale; 638 Type.concat(this.dataX, [x, x + v[0], NaN]); 639 Type.concat(this.dataY, [y, y + v[1], NaN]); 640 Type.concat(this.dataZ, [z, z + v[2], NaN]); 641 642 if (showArrow) { 643 // Arrow head 644 nrm *= scale; 645 phi = Math.atan2(v[1], v[0]); 646 theta = Math.asin(v[2] / nrm); 647 theta1 = theta - alpha; 648 theta2 = theta + alpha; 649 Type.concat(this.dataX, [ 650 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1), 651 x + v[0], 652 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2), 653 NaN]); 654 Type.concat(this.dataY, [ 655 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1), 656 y + v[1], 657 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2), 658 NaN]); 659 Type.concat(this.dataZ, [ 660 z + v[2] - leg_z * Math.sin(theta2), 661 z + v[2], 662 z + v[2] - leg_z * Math.sin(theta1), 663 NaN]); 664 } 665 } 666 } 667 } 668 }; 669 670 el.methodMap = Type.deepCopy(el.methodMap, { 671 setF: "setF" 672 }); 673 674 return el; 675 }; 676 677 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D); 678