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 * @function 60 * @ignore 61 */ 62 this.F = F; 63 64 /** 65 * Function which maps u to x; i.e. it defines the x-coordinate of the curve 66 * @function 67 * @returns Number 68 */ 69 this.X = X; 70 71 /** 72 * Function which maps u to y; i.e. it defines the y-coordinate of the curve 73 * @function 74 * @returns Number 75 */ 76 this.Y = Y; 77 78 /** 79 * Function which maps u to z; i.e. it defines the x-coordinate of the curve 80 * @function 81 * @returns Number 82 */ 83 this.Z = Z; 84 85 this.dataX = null; 86 this.dataY = null; 87 this.dataZ = null; 88 89 if (this.F !== null) { 90 this.X = function (u) { 91 return this.F(u)[0]; 92 }; 93 this.Y = function (u) { 94 return this.F(u)[1]; 95 }; 96 this.Z = function (u) { 97 return this.F(u)[2]; 98 }; 99 } 100 101 this.range = range; 102 103 this.methodMap = Type.deepCopy(this.methodMap, { 104 // TODO 105 }); 106 }; 107 JXG.Curve3D.prototype = new JXG.GeometryElement(); 108 Type.copyPrototypeMethods(JXG.Curve3D, JXG.GeometryElement3D, "constructor3D"); 109 110 JXG.extend( 111 JXG.Curve3D.prototype, 112 /** @lends JXG.Curve3D.prototype */ { 113 updateDataArray2D: function () { 114 var steps = this.evalVisProp('numberpointshigh'), 115 r, s, e, delta, c2d, u, dataX, dataY, 116 i, 117 p = [0, 0, 0]; 118 119 dataX = []; 120 dataY = []; 121 if (Type.exists(this.dataX)) { 122 steps = this.dataX.length; 123 for (u = 0; u < steps; u++) { 124 p = [this.dataX[u], this.dataY[u], this.dataZ[u]]; 125 c2d = this.view.project3DTo2D(p); 126 dataX.push(c2d[1]); 127 dataY.push(c2d[2]); 128 } 129 } else if (Type.isArray(this.X)) { 130 steps = this.X.length; 131 for (u = 0; u < steps; u++) { 132 p = [this.X[u], this.Y[u], this.Z[u]]; 133 c2d = this.view.project3DTo2D(p); 134 dataX.push(c2d[1]); 135 dataY.push(c2d[2]); 136 } 137 } else { 138 r = Type.evaluate(this.range); 139 s = Type.evaluate(r[0]); 140 e = Type.evaluate(r[1]); 141 delta = (e - s) / (steps - 1); 142 for (i = 0, u = s; i < steps && u <= e; i++, u += delta) { 143 if (this.F !== null) { 144 p = this.F(u); 145 } else { 146 p = [this.X(u), this.Y(u), this.Z(u)]; 147 } 148 c2d = this.view.project3DTo2D(p); 149 dataX.push(c2d[1]); 150 dataY.push(c2d[2]); 151 } 152 } 153 return { X: dataX, Y: dataY }; 154 }, 155 156 updateDataArray: function() { 157 }, 158 159 update: function () { 160 // if (this.needsUpdate) { 161 this.updateDataArray(); 162 // } 163 return this; 164 }, 165 166 updateRenderer: function () { 167 this.needsUpdate = false; 168 return this; 169 }, 170 171 initParamsIfNeeded: function (params) { 172 if (params.length === 0) { 173 params.unshift(0.5*(this.range[0] + this.range[1])); 174 } 175 }, 176 177 projectCoords: function (p, params) { 178 this.initParamsIfNeeded(params); 179 return Geometry.projectCoordsToParametric(p, this, params); 180 }, 181 182 projectScreenCoords: function (pScr, params) { 183 this.initParamsIfNeeded(params); 184 return Geometry.projectScreenCoordsToParametric(pScr, this, params); 185 } 186 } 187 ); 188 189 /** 190 * @class This element creates a 3D parametric curve. 191 * @pseudo 192 * @description A 3D parametric curve is defined by a function 193 * <i>F: R<sup>1</sup> → R<sup>3</sup></i>. 194 * 195 * @name Curve3D 196 * @augments Curve 197 * @constructor 198 * @type Object 199 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 200 * @param {Function_Function_Function_Array,Function} F<sub>X</sub>,F<sub>Y</sub>,F<sub>Z</sub>,range 201 * 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 202 * lower and upper bound for the range of the parameter u. range may also be a function returning an array of length two. 203 * @param {Function_Array,Function} F,range Alternatively: F<sub>[X,Y,Z]</sub>(u) a function returning an array [x,y,z] of 204 * numbers, range as above. 205 * @param {Array_Array_Array} X,Y,Z Three arrays containing the coordinate points which define the curve. 206 * @example 207 * // create a simple curve in 3d 208 * var bound = [-1.5, 1.5]; 209 * var view=board.create('view3d', 210 * [[-4, -4],[8, 8], 211 * [bound, bound, bound]], 212 * {}); 213 * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]); 214 * </pre><div id="JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3" class="jxgbox" style="width: 300px; height: 300px;"></div> 215 * <script type="text/javascript"> 216 * (function() { 217 * var board = JXG.JSXGraph.initBoard('JXG0f35a50e-e99d-11e8-a1ca-04d3b0c2aad3', 218 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 219 * // create a simple curve in 3d 220 * var bound = [-1.5, 1.5]; 221 * var view=board.create('view3d', 222 * [[-4, -4],[8, 8], 223 * [bound, bound, bound]], 224 * {}); 225 * var curve = view.create('curve3d', [(u)=>Math.cos(u), (u)=>Math.sin(u), (u)=>(u/Math.PI)-1,[0,2*Math.PI] ]); 226 * })(); 227 * </script><pre> 228 */ 229 JXG.createCurve3D = function (board, parents, attributes) { 230 var view = parents[0], 231 F, X, Y, Z, range, attr, el; 232 233 if (parents.length === 3) { 234 F = parents[1]; 235 range = parents[2]; 236 X = null; 237 Y = null; 238 Z = null; 239 } else { 240 X = parents[1]; 241 Y = parents[2]; 242 Z = parents[3]; 243 range = parents[4]; 244 F = null; 245 } 246 // TODO Throw error 247 248 attr = Type.copyAttributes(attributes, board.options, "curve3d"); 249 el = new JXG.Curve3D(view, F, X, Y, Z, range, attr); 250 251 attr = el.setAttr2D(attr); 252 el.element2D = view.create("curve", [[], []], attr); 253 el.element2D.view = view; 254 255 /** 256 * @class 257 * @ignore 258 */ 259 el.element2D.updateDataArray = function () { 260 var ret = el.updateDataArray2D(); 261 this.dataX = ret.X; 262 this.dataY = ret.Y; 263 }; 264 el.addChild(el.element2D); 265 el.inherits.push(el.element2D); 266 el.element2D.setParents(el); 267 268 el.element2D.prepareUpdate().update(); 269 if (!board.isSuspendedUpdate) { 270 el.element2D.updateVisibility().updateRenderer(); 271 } 272 273 return el; 274 }; 275 276 JXG.registerElement("curve3d", JXG.createCurve3D); 277 278 /** 279 * @class 3D vector field. 280 * <p> 281 * Plot a vector field either given by three functions 282 * f1(x, y, z), f2(x, y, z), and f3(x, y, z) or by a function f(x, y, z) 283 * returning an array of size 3. 284 * 285 * @pseudo 286 * @name Vectorfield3D 287 * @augments JXG.Curve3D 288 * @constructor 289 * @type JXG.Curve3D 290 * @throws {Error} If the element cannot be constructed with the given parent objects an exception is thrown. 291 * Parameter options: 292 * @param {Array|Function|String} F Either an array containing three functions f1(x, y, z), f2(x, y, z), 293 * and f3(x, y) or function f(x, y, z) returning an array of length 3. 294 * @param {Array} xData Array of length 3 containing start value for x, number of steps, 295 * end value of x. The vector field will contain (number of steps) + 1 vectors in direction of x. 296 * @param {Array} yData Array of length 3 containing start value for y, number of steps, 297 * end value of y. The vector field will contain (number of steps) + 1 vectors in direction of y. 298 * @param {Array} zData Array of length 3 containing start value for z, number of steps, 299 * end value of z. The vector field will contain (number of steps) + 1 vectors in direction of z. 300 * 301 * @example 302 * const view = board.create('view3d', 303 * [ 304 * [-6, -3], 305 * [8, 8], 306 * [[-3, 3], [-3, 3], [-3, 3]] 307 * ], {}); 308 * 309 * var vf = view.create('vectorfield3d', [ 310 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 311 * [-2, 5, 2], // x from -2 to 2 in 5 steps 312 * [-2, 5, 2], // y 313 * [-2, 5, 2] // z 314 * ], { 315 * strokeColor: 'red', 316 * scale: 0.5 317 * }); 318 * 319 * </pre><div id="JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348" class="jxgbox" style="width: 300px; height: 300px;"></div> 320 * <script type="text/javascript"> 321 * (function() { 322 * var board = JXG.JSXGraph.initBoard('JXG8e41c67b-3338-4428-bd0f-c69d8f6fb348', 323 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false, 324 * pan: { 325 * needTwoFingers: true 326 * } 327 * }); 328 * const view = board.create('view3d', 329 * [ 330 * [-6, -3], 331 * [8, 8], 332 * [[-3, 3], [-3, 3], [-3, 3]] 333 * ], {}); 334 * var vf = view.create('vectorfield3d', [ 335 * [(x, y, z) => Math.cos(y), (x, y, z) => Math.sin(x), (x, y, z) => z], 336 * [-2, 5, 2], // x from -2 to 2 in 5 steps 337 * [-2, 5, 2], // y 338 * [-2, 5, 2] // z 339 * ], { 340 * strokeColor: 'red', 341 * scale: 0.5 342 * }); 343 * 344 * 345 * })(); 346 * 347 * </script><pre> 348 * 349 */ 350 JXG.createVectorfield3D = function (board, parents, attributes) { 351 var view = parents[0], 352 el, attr; 353 354 if (!(parents.length >= 5 && 355 (Type.isArray(parents[1]) || Type.isFunction(parents[1]) || Type.isString(parents[1])) && 356 (Type.isArray(parents[2]) && parents[1].length === 3) && 357 (Type.isArray(parents[3]) && parents[2].length === 3) && 358 (Type.isArray(parents[4]) && parents[3].length === 3) 359 )) { 360 throw new Error( 361 "JSXGraph: Can't create vector field 3D with parent types " + 362 "'" + typeof parents[1] + "', " + 363 "'" + typeof parents[2] + "', " + 364 "'" + typeof parents[3] + "'." + 365 "'" + typeof parents[4] + "', " 366 ); 367 } 368 369 attr = Type.copyAttributes(attributes, board.options, 'vectorfield3d'); 370 el = view.create('curve3d', [[], [], []], attr); 371 372 /** 373 * Set the defining functions of 3D vector field. 374 * @memberOf Vectorfield3D 375 * @name setF 376 * @function 377 * @param {Array|Function} func Either an array containing three functions f1(x, y, z), 378 * f2(x, y, z), and f3(x, y, z) or function f(x, y, z) returning an array of length 3. 379 * @returns {Object} Reference to the 3D vector field object. 380 * 381 * @example 382 * field.setF([(x, y, z) => Math.sin(y), (x, y, z) => Math.cos(x), (x, y, z) => z]); 383 * board.update(); 384 * 385 */ 386 el.setF = function (func, varnames) { 387 var f0, f1, f2; 388 if (Type.isArray(func)) { 389 f0 = Type.createFunction(func[0], this.board, varnames); 390 f1 = Type.createFunction(func[1], this.board, varnames); 391 f2 = Type.createFunction(func[2], this.board, varnames); 392 /** 393 * @ignore 394 */ 395 this.F = function (x, y, z) { 396 return [f0(x, y, z), f1(x, y, z), f2(x, y, z)]; 397 }; 398 } else { 399 this.F = Type.createFunction(func, el.board, varnames); 400 } 401 return this; 402 }; 403 404 el.setF(parents[1], 'x, y, z'); 405 el.xData = parents[2]; 406 el.yData = parents[3]; 407 el.zData = parents[4]; 408 409 el.updateDataArray = function () { 410 var k, i, j, 411 v, nrm, 412 x, y, z, 413 scale = this.evalVisProp('scale'), 414 start = [ 415 Type.evaluate(this.xData[0]), 416 Type.evaluate(this.yData[0]), 417 Type.evaluate(this.zData[0]) 418 ], 419 steps = [ 420 Type.evaluate(this.xData[1]), 421 Type.evaluate(this.yData[1]), 422 Type.evaluate(this.zData[1]) 423 ], 424 end = [ 425 Type.evaluate(this.xData[2]), 426 Type.evaluate(this.yData[2]), 427 Type.evaluate(this.zData[2]) 428 ], 429 delta = [ 430 (end[0] - start[0]) / steps[0], 431 (end[1] - start[1]) / steps[1], 432 (end[2] - start[2]) / steps[2] 433 ], 434 phi, theta1, theta2, theta, 435 showArrow = this.evalVisProp('arrowhead.enabled'), 436 leg, leg_x, leg_y, leg_z, alpha; 437 438 if (showArrow) { 439 // Arrow head style 440 // leg = 8; 441 // alpha = Math.PI * 0.125; 442 leg = this.evalVisProp('arrowhead.size'); 443 alpha = this.evalVisProp('arrowhead.angle'); 444 leg_x = leg / board.unitX; 445 leg_y = leg / board.unitY; 446 leg_z = leg / Math.sqrt(board.unitX * board.unitY); 447 } 448 449 this.dataX = []; 450 this.dataY = []; 451 this.dataZ = []; 452 for (i = 0, x = start[0]; i <= steps[0]; x += delta[0], i++) { 453 for (j = 0, y = start[1]; j <= steps[1]; y += delta[1], j++) { 454 for (k = 0, z = start[2]; k <= steps[2]; z += delta[2], k++) { 455 v = this.F(x, y, z); 456 nrm = Mat.norm(v); 457 if (nrm < Number.EPSILON) { 458 continue; 459 } 460 461 v[0] *= scale; 462 v[1] *= scale; 463 v[2] *= scale; 464 Type.concat(this.dataX, [x, x + v[0], NaN]); 465 Type.concat(this.dataY, [y, y + v[1], NaN]); 466 Type.concat(this.dataZ, [z, z + v[2], NaN]); 467 468 if (showArrow) { 469 // Arrow head 470 nrm *= scale; 471 phi = Math.atan2(v[1], v[0]); 472 theta = Math.asin(v[2] / nrm); 473 theta1 = theta - alpha; 474 theta2 = theta + alpha; 475 Type.concat(this.dataX, [ 476 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta1), 477 x + v[0], 478 x + v[0] - leg_x * Math.cos(phi) * Math.cos(theta2), 479 NaN]); 480 Type.concat(this.dataY, [ 481 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta1), 482 y + v[1], 483 y + v[1] - leg_y * Math.sin(phi) * Math.cos(theta2), 484 NaN]); 485 Type.concat(this.dataZ, [ 486 z + v[2] - leg_z * Math.sin(theta2), 487 z + v[2], 488 z + v[2] - leg_z * Math.sin(theta1), 489 NaN]); 490 } 491 } 492 } 493 } 494 }; 495 496 el.methodMap = Type.deepCopy(el.methodMap, { 497 setF: "setF" 498 }); 499 500 return el; 501 }; 502 503 JXG.registerElement("vectorfield3D", JXG.createVectorfield3D); 504