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