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 /* 30 Some functionalities in this file were developed as part of a software project 31 with students. We would like to thank all contributors for their help: 32 33 Winter semester 2023/2024: 34 Lars Hofmann 35 Leonhard Iser 36 Vincent Kulicke 37 Laura Rinas 38 */ 39 40 /*global JXG:true, define: true*/ 41 42 import JXG from "../jxg.js"; 43 import Const from "../base/constants.js"; 44 import Coords from "../base/coords.js"; 45 import Type from "../utils/type.js"; 46 import Mat from "../math/math.js"; 47 import Geometry from "../math/geometry.js"; 48 import Env from "../utils/env.js"; 49 import GeometryElement from "../base/element.js"; 50 import Composition from "../base/composition.js"; 51 52 /** 53 * 3D view inside a JXGraph board. 54 * 55 * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with 56 * type {@link View3D} instead. 57 * 58 * @augments JXG.GeometryElement 59 * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view 60 * and box size [[x1, x2], [y1,y2], [z1,z2]]. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 61 * [x,y] and side lengths [w, h] of the board. 62 */ 63 JXG.View3D = function (board, parents, attributes) { 64 this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D); 65 66 /** 67 * An associative array containing all geometric objects belonging to the view. 68 * Key is the id of the object and value is a reference to the object. 69 * @type Object 70 * @private 71 */ 72 this.objects = {}; 73 74 /** 75 * An array containing all the points in the view. 76 * @Type Array 77 * @private 78 */ 79 this.points = this.visProp.depthorderpoints ? [] : null; 80 81 /** 82 * An array containing all geometric objects in this view in the order of construction. 83 * @type Array 84 * @private 85 */ 86 // this.objectsList = []; 87 88 /** 89 * An associative array / dictionary to store the objects of the board by name. The name of the object is the key and value is a reference to the object. 90 * @type Object 91 * @private 92 */ 93 this.elementsByName = {}; 94 95 /** 96 * Default axes of the 3D view, contains the axes of the view or null. 97 * 98 * @type {Object} 99 * @default null 100 */ 101 this.defaultAxes = null; 102 103 /** 104 * The Tait-Bryan angles specifying the view box orientation 105 */ 106 this.angles = { 107 az: null, 108 el: null, 109 bank: null 110 }; 111 112 /** 113 * @type {Array} 114 * The view box orientation matrix 115 */ 116 this.matrix3DRot = [ 117 [1, 0, 0, 0], 118 [0, 1, 0, 0], 119 [0, 0, 1, 0], 120 [0, 0, 0, 1] 121 ]; 122 123 /** 124 * @type {Array} 125 * @private 126 */ 127 // 3D-to-2D transformation matrix 128 this.matrix3D = [ 129 [1, 0, 0, 0], 130 [0, 1, 0, 0], 131 [0, 0, 1, 0] 132 ]; 133 134 /** 135 * The 4×4 matrix that maps box coordinates to camera coordinates. These 136 * coordinate systems fit into the View3D coordinate atlas as follows. 137 * <ul> 138 * <li><b>World coordinates.</b> The coordinates used to specify object 139 * positions in a JSXGraph scene.</li> 140 * <li><b>Box coordinates.</b> The world coordinates translated to put the 141 * center of the view box at the origin. 142 * <li><b>Camera coordinates.</b> The coordinate system where the 143 * <code>x</code>, <code>y</code> plane is the screen, the origin is the 144 * center of the screen, and the <code>z</code> axis points out of the 145 * screen, toward the viewer. 146 * <li><b>Focal coordinates.</b> The camera coordinates translated to put 147 * the origin at the focal point, which is set back from the screen by the 148 * focal distance.</li> 149 * </ul> 150 * The <code>boxToCam</code> transformation is exposed to help 3D elements 151 * manage their 2D representations in central projection mode. To map world 152 * coordinates to focal coordinates, use the 153 * {@link JXG.View3D#worldToFocal} method. 154 * @type {Array} 155 */ 156 this.boxToCam = []; 157 158 /** 159 * @type array 160 * @private 161 */ 162 // Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0. 163 this.llftCorner = parents[0]; 164 165 /** 166 * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0. 167 * @type array 168 * @private 169 */ 170 this.size = parents[1]; 171 172 /** 173 * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view 174 * @type array 175 */ 176 this.bbox3D = parents[2]; 177 178 /** 179 * The distance from the camera to the origin. In other words, the 180 * radius of the sphere where the camera sits. 181 * @type Number 182 */ 183 this.r = -1; 184 185 /** 186 * The distance from the camera to the screen. Computed automatically from 187 * the `fov` property. 188 * @type Number 189 */ 190 this.focalDist = -1; 191 192 /** 193 * Type of projection. 194 * @type String 195 */ 196 // Will be set in update(). 197 this.projectionType = 'parallel'; 198 199 /** 200 * Whether trackball navigation is currently enabled. 201 * @type String 202 */ 203 this.trackballEnabled = false; 204 205 this.timeoutAzimuth = null; 206 207 this.id = this.board.setId(this, 'V'); 208 this.board.finalizeAdding(this); 209 this.elType = 'view3d'; 210 211 this.methodMap = Type.deepCopy(this.methodMap, { 212 // TODO 213 }); 214 }; 215 JXG.View3D.prototype = new GeometryElement(); 216 217 JXG.extend( 218 JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ { 219 220 /** 221 * Creates a new 3D element of type elementType. 222 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'. 223 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two 224 * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 225 * methods for a list of possible parameters. 226 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 227 * Common attributes are name, visible, strokeColor. 228 * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing 229 * two or more elements. 230 */ 231 create: function (elementType, parents, attributes) { 232 var prefix = [], 233 el; 234 235 if (elementType.indexOf('3d') > 0) { 236 // is3D = true; 237 prefix.push(this); 238 } 239 el = this.board.create(elementType, prefix.concat(parents), attributes); 240 241 return el; 242 }, 243 244 /** 245 * Select a single or multiple elements at once. 246 * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will 247 * be used as a filter to return multiple elements at once filtered by the properties of the object. 248 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 249 * The advanced filters consisting of objects or functions are ignored. 250 * @returns {JXG.GeometryElement3D|JXG.Composition} 251 * @example 252 * // select the element with name A 253 * view.select('A'); 254 * 255 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 256 * view.select({ 257 * strokeColor: 'red' 258 * }); 259 * 260 * // select all points on or below the x/y plane and make them black. 261 * view.select({ 262 * elType: 'point3d', 263 * Z: function (v) { 264 * return v <= 0; 265 * } 266 * }).setAttribute({color: 'black'}); 267 * 268 * // select all elements 269 * view.select(function (el) { 270 * return true; 271 * }); 272 */ 273 select: function (str, onlyByIdOrName) { 274 var flist, 275 olist, 276 i, 277 l, 278 s = str; 279 280 if (s === null) { 281 return s; 282 } 283 284 // It's a string, most likely an id or a name. 285 if (Type.isString(s) && s !== '') { 286 // Search by ID 287 if (Type.exists(this.objects[s])) { 288 s = this.objects[s]; 289 // Search by name 290 } else if (Type.exists(this.elementsByName[s])) { 291 s = this.elementsByName[s]; 292 // // Search by group ID 293 // } else if (Type.exists(this.groups[s])) { 294 // s = this.groups[s]; 295 } 296 297 // It's a function or an object, but not an element 298 } else if ( 299 !onlyByIdOrName && 300 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 301 ) { 302 flist = Type.filterElements(this.objectsList, s); 303 304 olist = {}; 305 l = flist.length; 306 for (i = 0; i < l; i++) { 307 olist[flist[i].id] = flist[i]; 308 } 309 s = new Composition(olist); 310 311 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 312 } else if ( 313 Type.isObject(s) && 314 Type.exists(s.id) && 315 !Type.exists(this.objects[s.id]) 316 ) { 317 s = null; 318 } 319 320 return s; 321 }, 322 323 // set the Tait-Bryan angles to specify the current view rotation matrix 324 setAnglesFromRotation: function () { 325 var rem = this.matrix3DRot, // rotation remaining after angle extraction 326 rBank, cosBank, sinBank, 327 cosEl, sinEl, 328 cosAz, sinAz; 329 330 // extract bank by rotating the view box z axis onto the camera yz plane 331 rBank = Math.sqrt(rem[1][3] * rem[1][3] + rem[2][3] * rem[2][3]); 332 if (rBank > Mat.eps) { 333 cosBank = rem[2][3] / rBank; 334 sinBank = rem[1][3] / rBank; 335 } else { 336 // if the z axis is pointed almost exactly at the screen, we 337 // keep the current bank value 338 cosBank = Math.cos(this.angles.bank); 339 sinBank = Math.sin(this.angles.bank); 340 } 341 rem = Mat.matMatMult([ 342 [1, 0, 0, 0], 343 [0, cosBank, -sinBank, 0], 344 [0, sinBank, cosBank, 0], 345 [0, 0, 0, 1] 346 ], rem); 347 this.angles.bank = Math.atan2(sinBank, cosBank); 348 349 // extract elevation by rotating the view box z axis onto the camera 350 // y axis 351 cosEl = rem[2][3]; 352 sinEl = rem[3][3]; 353 rem = Mat.matMatMult([ 354 [1, 0, 0, 0], 355 [0, 1, 0, 0], 356 [0, 0, cosEl, sinEl], 357 [0, 0, -sinEl, cosEl] 358 ], rem); 359 this.angles.el = Math.atan2(sinEl, cosEl); 360 361 // extract azimuth 362 cosAz = -rem[1][1]; 363 sinAz = rem[3][1]; 364 this.angles.az = Math.atan2(sinAz, cosAz); 365 if (this.angles.az < 0) this.angles.az += 2 * Math.PI; 366 367 this.setSlidersFromAngles(); 368 }, 369 370 anglesHaveMoved: function () { 371 return ( 372 this._hasMoveAz || this._hasMoveEl || 373 Math.abs(this.angles.az - this.az_slide.Value()) > Mat.eps || 374 Math.abs(this.angles.el - this.el_slide.Value()) > Mat.eps || 375 Math.abs(this.angles.bank - this.bank_slide.Value()) > Mat.eps 376 ); 377 }, 378 379 getAnglesFromSliders: function () { 380 this.angles.az = this.az_slide.Value(); 381 this.angles.el = this.el_slide.Value(); 382 this.angles.bank = this.bank_slide.Value(); 383 }, 384 385 setSlidersFromAngles: function () { 386 this.az_slide.setValue(this.angles.az); 387 this.el_slide.setValue(this.angles.el); 388 this.bank_slide.setValue(this.angles.bank); 389 }, 390 391 // return the rotation matrix specified by the current Tait-Bryan angles 392 getRotationFromAngles: function () { 393 var a, e, b, f, 394 cosBank, sinBank, 395 mat = [ 396 [1, 0, 0, 0], 397 [0, 1, 0, 0], 398 [0, 0, 1, 0], 399 [0, 0, 0, 1] 400 ]; 401 402 // mat projects homogeneous 3D coords in View3D 403 // to homogeneous 2D coordinates in the board 404 a = this.angles.az; 405 e = this.angles.el; 406 b = this.angles.bank; 407 f = -Math.sin(e); 408 409 mat[1][1] = -Math.cos(a); 410 mat[1][2] = Math.sin(a); 411 mat[1][3] = 0; 412 413 mat[2][1] = f * Math.sin(a); 414 mat[2][2] = f * Math.cos(a); 415 mat[2][3] = Math.cos(e); 416 417 mat[3][1] = Math.cos(e) * Math.sin(a); 418 mat[3][2] = Math.cos(e) * Math.cos(a); 419 mat[3][3] = Math.sin(e); 420 421 cosBank = Math.cos(b); 422 sinBank = Math.sin(b); 423 mat = Mat.matMatMult([ 424 [1, 0, 0, 0], 425 [0, cosBank, sinBank, 0], 426 [0, -sinBank, cosBank, 0], 427 [0, 0, 0, 1] 428 ], mat); 429 430 return mat; 431 432 /* this code, originally from `_updateCentralProjection`, is an 433 * alternate implementation of the azimuth-elevation matrix 434 * computation above. using this implementation instead of the 435 * current one might lead to simpler code in a future refactoring 436 var a, e, up, 437 ax, ay, az, v, nrm, 438 eye, d, 439 func_sphere; 440 441 // finds the point on the unit sphere with the given azimuth and 442 // elevation, and returns its affine coordinates 443 func_sphere = function (az, el) { 444 return [ 445 Math.cos(az) * Math.cos(el), 446 -Math.sin(az) * Math.cos(el), 447 Math.sin(el) 448 ]; 449 }; 450 451 a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere 452 e = this.el_slide.Value(); 453 454 // create an up vector and an eye vector which are 90 degrees out of phase 455 up = func_sphere(a, e + Math.PI / 2); 456 eye = func_sphere(a, e); 457 d = [eye[0], eye[1], eye[2]]; 458 459 nrm = Mat.norm(d, 3); 460 az = [d[0] / nrm, d[1] / nrm, d[2] / nrm]; 461 462 nrm = Mat.norm(up, 3); 463 v = [up[0] / nrm, up[1] / nrm, up[2] / nrm]; 464 465 ax = Mat.crossProduct(v, az); 466 ay = Mat.crossProduct(az, ax); 467 468 this.matrix3DRot[1] = [0, ax[0], ax[1], ax[2]]; 469 this.matrix3DRot[2] = [0, ay[0], ay[1], ay[2]]; 470 this.matrix3DRot[3] = [0, az[0], az[1], az[2]]; 471 */ 472 }, 473 474 /** 475 * Project 2D point (x,y) to the virtual trackpad sphere, 476 * see Bell's virtual trackpad, and return z-component of the 477 * number. 478 * 479 * @param {Number} r 480 * @param {Number} x 481 * @param {Number} y 482 * @returns Number 483 * @private 484 */ 485 _projectToSphere: function (r, x, y) { 486 var d = Mat.hypot(x, y), 487 t, z; 488 489 if (d < r * 0.7071067811865475) { // Inside sphere 490 z = Math.sqrt(r * r - d * d); 491 } else { // On hyperbola 492 t = r / 1.414213562373095; 493 z = t * t / d; 494 } 495 return z; 496 }, 497 498 /** 499 * Determine 4x4 rotation matrix with Bell's virtual trackball. 500 * 501 * @returns {Array} 4x4 rotation matrix 502 * @private 503 */ 504 updateProjectionTrackball: function (Pref) { 505 var R = 100, 506 dx, dy, dr2, 507 p1, p2, x, y, theta, t, d, 508 c, s, n, 509 mat = [ 510 [1, 0, 0, 0], 511 [0, 1, 0, 0], 512 [0, 0, 1, 0], 513 [0, 0, 0, 1] 514 ]; 515 516 if (!Type.exists(this._trackball)) { 517 return this.matrix3DRot; 518 } 519 520 dx = this._trackball.dx; 521 dy = this._trackball.dy; 522 dr2 = dx * dx + dy * dy; 523 if (dr2 > Mat.eps) { 524 // // Method by Hanson, "The rolling ball", Graphics Gems III, p.51 525 // // Rotation axis: 526 // // n = (-dy/dr, dx/dr, 0) 527 // // Rotation angle around n: 528 // // theta = atan(dr / R) approx dr / R 529 // dr = Math.sqrt(dr2); 530 // c = R / Math.hypot(R, dr); // cos(theta) 531 // t = 1 - c; // 1 - cos(theta) 532 // s = dr / Math.hypot(R, dr); // sin(theta) 533 // n = [-dy / dr, dx / dr, 0]; 534 535 // Bell virtual trackpad, see 536 // https://opensource.apple.com/source/X11libs/X11libs-60/mesa/Mesa-7.8.2/progs/util/trackball.c.auto.html 537 // http://scv.bu.edu/documentation/presentations/visualizationworkshop08/materials/opengl/trackball.c. 538 // See also Henriksen, Sporring, Hornaek, "Virtual Trackballs revisited". 539 // 540 R = (this.size[0] * this.board.unitX + this.size[1] * this.board.unitY) * 0.25; 541 x = this._trackball.x; 542 y = this._trackball.y; 543 544 p2 = [x, y, this._projectToSphere(R, x, y)]; 545 x -= dx; 546 y -= dy; 547 p1 = [x, y, this._projectToSphere(R, x, y)]; 548 549 n = Mat.crossProduct(p1, p2); 550 d = Mat.hypot(n[0], n[1], n[2]); 551 n[0] /= d; 552 n[1] /= d; 553 n[2] /= d; 554 555 t = Geometry.distance(p2, p1, 3) / (2 * R); 556 t = (t > 1.0) ? 1.0 : t; 557 t = (t < -1.0) ? -1.0 : t; 558 theta = 2.0 * Math.asin(t); 559 c = Math.cos(theta); 560 t = 1 - c; 561 s = Math.sin(theta); 562 563 // Rotation by theta about the axis n. See equation 9.63 of 564 // 565 // Ian Richard Cole. "Modeling CPV" (thesis). Loughborough 566 // University. https://hdl.handle.net/2134/18050 567 // 568 mat[1][1] = c + n[0] * n[0] * t; 569 mat[2][1] = n[1] * n[0] * t + n[2] * s; 570 mat[3][1] = n[2] * n[0] * t - n[1] * s; 571 572 mat[1][2] = n[0] * n[1] * t - n[2] * s; 573 mat[2][2] = c + n[1] * n[1] * t; 574 mat[3][2] = n[2] * n[1] * t + n[0] * s; 575 576 mat[1][3] = n[0] * n[2] * t + n[1] * s; 577 mat[2][3] = n[1] * n[2] * t - n[0] * s; 578 mat[3][3] = c + n[2] * n[2] * t; 579 } 580 581 mat = Mat.matMatMult(mat, this.matrix3DRot); 582 return mat; 583 }, 584 585 updateAngleSliderBounds: function () { 586 var az_smax, az_smin, 587 el_smax, el_smin, el_cover, 588 el_smid, el_equiv, el_flip_equiv, 589 el_equiv_loss, el_flip_equiv_loss, el_interval_loss, 590 bank_smax, bank_smin; 591 592 // update stored trackball toggle 593 this.trackballEnabled = Type.evaluate(this.visProp.trackball.enabled); 594 595 // set slider bounds 596 if (this.trackballEnabled) { 597 this.az_slide.setMin(0); 598 this.az_slide.setMax(2 * Math.PI); 599 this.el_slide.setMin(-0.5 * Math.PI); 600 this.el_slide.setMax(0.5 * Math.PI); 601 this.bank_slide.setMin(-Math.PI); 602 this.bank_slide.setMax(Math.PI); 603 } else { 604 this.az_slide.setMin(this.visProp.az.slider.min); 605 this.az_slide.setMax(this.visProp.az.slider.max); 606 this.el_slide.setMin(this.visProp.el.slider.min); 607 this.el_slide.setMax(this.visProp.el.slider.max); 608 this.bank_slide.setMin(this.visProp.bank.slider.min); 609 this.bank_slide.setMax(this.visProp.bank.slider.max); 610 } 611 612 // get new slider bounds 613 az_smax = this.az_slide._smax; 614 az_smin = this.az_slide._smin; 615 el_smax = this.el_slide._smax; 616 el_smin = this.el_slide._smin; 617 bank_smax = this.bank_slide._smax; 618 bank_smin = this.bank_slide._smin; 619 620 // wrap and restore angle values 621 if (this.trackballEnabled) { 622 // if we're upside-down, flip the bank angle to reach the same 623 // orientation with an elevation between -pi/2 and pi/2 624 el_cover = Mat.mod(this.angles.el, 2 * Math.PI); 625 if (0.5 * Math.PI < el_cover && el_cover < 1.5 * Math.PI) { 626 this.angles.el = Math.PI - el_cover; 627 this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax); 628 this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax); 629 } 630 631 // wrap the azimuth and bank angle 632 this.angles.az = Mat.wrap(this.angles.az, az_smin, az_smax); 633 this.angles.el = Mat.wrap(this.angles.el, el_smin, el_smax); 634 this.angles.bank = Mat.wrap(this.angles.bank, bank_smin, bank_smax); 635 } else { 636 // wrap and clamp the elevation into the slider range. if 637 // flipping the elevation gets us closer to the slider interval, 638 // do that, inverting the azimuth and bank angle to compensate 639 el_interval_loss = function (t) { 640 if (t < el_smin) { 641 return el_smin - t; 642 } else if (el_smax < t) { 643 return t - el_smax; 644 } else { 645 return 0; 646 } 647 }; 648 el_smid = 0.5 * (el_smin + el_smax); 649 el_equiv = Mat.wrap( 650 this.angles.el, 651 el_smid - Math.PI, 652 el_smid + Math.PI 653 ); 654 el_flip_equiv = Mat.wrap( 655 Math.PI - this.angles.el, 656 el_smid - Math.PI, 657 el_smid + Math.PI 658 ); 659 el_equiv_loss = el_interval_loss(el_equiv); 660 el_flip_equiv_loss = el_interval_loss(el_flip_equiv); 661 if (el_equiv_loss <= el_flip_equiv_loss) { 662 this.angles.el = Mat.clamp(el_equiv, el_smin, el_smax); 663 } else { 664 this.angles.el = Mat.clamp(el_flip_equiv, el_smin, el_smax); 665 this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax); 666 this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax); 667 } 668 669 // wrap and clamp the azimuth and bank angle into the slider range 670 this.angles.az = Mat.wrapAndClamp(this.angles.az, az_smin, az_smax, 2 * Math.PI); 671 this.angles.bank = Mat.wrapAndClamp(this.angles.bank, bank_smin, bank_smax, 2 * Math.PI); 672 673 // since we're using `clamp`, angles may have changed 674 this.matrix3DRot = this.getRotationFromAngles(); 675 } 676 677 // restore slider positions 678 this.setSlidersFromAngles(); 679 }, 680 681 /** 682 * @private 683 * @returns {Array} 684 */ 685 _updateCentralProjection: function () { 686 var zf = 20, // near clip plane 687 zn = 8, // far clip plane 688 689 // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html 690 // bbox3D is always at the world origin, i.e. T_obj is the unit matrix. 691 // All vectors contain affine coordinates and have length 3 692 // The matrices are of size 4x4. 693 r, A; 694 695 // set distance from view box center to camera 696 r = Type.evaluate(this.visProp.r); 697 if (r === 'auto') { 698 r = Mat.hypot( 699 this.bbox3D[0][0] - this.bbox3D[0][1], 700 this.bbox3D[1][0] - this.bbox3D[1][1], 701 this.bbox3D[2][0] - this.bbox3D[2][1] 702 ) * 1.01; 703 } 704 705 // compute camera transformation 706 this.boxToCam = this.matrix3DRot.map((row) => row.slice()); 707 this.boxToCam[3][0] = -r; 708 709 // compute focal distance and clip space transformation 710 this.focalDist = 1 / Math.tan(0.5 * Type.evaluate(this.visProp.fov)); 711 A = [ 712 [0, 0, 0, -1], 713 [0, this.focalDist, 0, 0], 714 [0, 0, this.focalDist, 0], 715 [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)] 716 ]; 717 718 return Mat.matMatMult(A, this.boxToCam); 719 }, 720 721 /** 722 * Comparison function for 3D points. It is used to sort points according to their z-index. 723 * @param {Point3D} a 724 * @param {Point3D} b 725 * @returns Integer 726 */ 727 compareDepth: function (a, b) { 728 var worldDiff = [0, 729 a.coords[1] - b.coords[1], 730 a.coords[2] - b.coords[2], 731 a.coords[3] - b.coords[3]], 732 oriBoxDiff = Mat.matVecMult(this.matrix3DRot, Mat.matVecMult(this.shift, worldDiff)); 733 return oriBoxDiff[3]; 734 }, 735 736 // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles. 737 update: function () { 738 var r = this.r, 739 stretch = [ 740 [1, 0, 0, 0], 741 [0, -r, 0, 0], 742 [0, 0, -r, 0], 743 [0, 0, 0, 1] 744 ], 745 mat2D, objectToClip, size, 746 dx, dy, 747 objectsList; 748 749 if ( 750 !Type.exists(this.el_slide) || 751 !Type.exists(this.az_slide) || 752 !Type.exists(this.bank_slide) || 753 !this.needsUpdate 754 ) { 755 return this; 756 } 757 758 mat2D = [ 759 [1, 0, 0], 760 [0, 1, 0], 761 [0, 0, 1] 762 ]; 763 764 this.projectionType = Type.evaluate(this.visProp.projection).toLowerCase(); 765 766 // override angle slider bounds when trackball navigation is enabled 767 if (this.trackballEnabled !== Type.evaluate(this.visProp.trackball.enabled)) { 768 this.updateAngleSliderBounds(); 769 } 770 771 if (this._hasMoveTrackball) { 772 // The trackball has been moved since the last update, so we do 773 // trackball navigation. When the trackball is enabled, a drag 774 // event is interpreted as a trackball movement unless it's 775 // caught by something else, like point dragging. When the 776 // trackball is disabled, the trackball movement flag should 777 // never be set 778 this.matrix3DRot = this.updateProjectionTrackball(); 779 this.setAnglesFromRotation(); 780 } else if (this.anglesHaveMoved()) { 781 // The trackball hasn't been moved since the last up date, but 782 // the Tait-Bryan angles have been, so we do angle navigation 783 this.getAnglesFromSliders(); 784 this.matrix3DRot = this.getRotationFromAngles(); 785 } 786 787 /** 788 * The translation that moves the center of the view box to the origin. 789 */ 790 this.shift = [ 791 [1, 0, 0, 0], 792 [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0], 793 [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0], 794 [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1] 795 ]; 796 797 switch (this.projectionType) { 798 case 'central': // Central projection 799 800 // Add a final transformation to scale and shift the projection 801 // on the board, usually called viewport. 802 size = 2 * 0.4; 803 mat2D[1][1] = this.size[0] / size; // w / d_x 804 mat2D[2][2] = this.size[1] / size; // h / d_y 805 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x 806 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y 807 // The transformations this.matrix3D and mat2D can not be combined at this point, 808 // since the projected vectors have to be normalized in between in project3DTo2D 809 this.viewPortTransform = mat2D; 810 811 objectToClip = this._updateCentralProjection(); 812 // this.matrix3D is a 4x4 matrix 813 this.matrix3D = Mat.matMatMult(objectToClip, this.shift); 814 break; 815 816 case 'parallel': // Parallel projection 817 default: 818 // Add a final transformation to scale and shift the projection 819 // on the board, usually called viewport. 820 dx = this.bbox3D[0][1] - this.bbox3D[0][0]; 821 dy = this.bbox3D[1][1] - this.bbox3D[1][0]; 822 mat2D[1][1] = this.size[0] / dx; // w / d_x 823 mat2D[2][2] = this.size[1] / dy; // h / d_y 824 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x 825 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y 826 827 // Combine all transformations, this.matrix3D is a 3x4 matrix 828 this.matrix3D = Mat.matMatMult( 829 mat2D, 830 Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3) 831 ); 832 } 833 834 // if depth-ordering for points was just switched on, initialize the 835 // list of points 836 if (this.visProp.depthorderpoints && this.points === null) { 837 objectsList = Object.values(this.objects); 838 this.points = objectsList.filter( 839 el => el.type === Const.OBJECT_TYPE_POINT3D 840 ); 841 } 842 843 // if depth-ordering for points was just switched off, throw away the 844 // list of points 845 if (!this.visProp.depthorderpoints && this.points !== null) { 846 this.points = null; 847 } 848 849 // depth-order visible points. the `setLayer` method is used here to 850 // re-order the points within each layer: it has the side effect of 851 // moving the target element to the end of the layer's child list 852 if (this.visProp.depthorderpoints && this.board.renderer && this.board.renderer.type === 'svg') { 853 this.points 854 .filter((pt) => Type.evaluate(pt.element2D.visProp.visible)) 855 .sort(this.compareDepth.bind(this)) 856 .forEach((pt) => this.board.renderer.setLayer(pt.element2D, pt.element2D.visProp.layer)); 857 858 /* [DEBUG] list oriented box coordinates in depth order */ 859 // console.log('depth-ordered points in oriented box coordinates'); 860 // this.points 861 // .filter((pt) => pt.element2D.visProp.visible) 862 // .sort(compareDepth) 863 // .forEach(function (pt) { 864 // console.log(Mat.matVecMult(that.matrix3DRot, Mat.matVecMult(that.shift, pt.coords))); 865 // }); 866 } 867 868 return this; 869 }, 870 871 updateRenderer: function () { 872 this.needsUpdate = false; 873 return this; 874 }, 875 876 removeObject: function (object, saveMethod) { 877 var i; 878 879 // this.board.removeObject(object, saveMethod); 880 if (Type.isArray(object)) { 881 for (i = 0; i < object.length; i++) { 882 this.removeObject(object[i]); 883 } 884 return this; 885 } 886 887 object = this.select(object); 888 889 // // If the object which is about to be removed unknown or a string, do nothing. 890 // // it is a string if a string was given and could not be resolved to an element. 891 if (!Type.exists(object) || Type.isString(object)) { 892 return this; 893 } 894 895 try { 896 // // remove all children. 897 // for (el in object.childElements) { 898 // if (object.childElements.hasOwnProperty(el)) { 899 // object.childElements[el].board.removeObject(object.childElements[el]); 900 // } 901 // } 902 903 delete this.objects[object.id]; 904 } catch (e) { 905 JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e); 906 } 907 908 // this.update(); 909 910 this.board.removeObject(object, saveMethod); 911 912 return this; 913 }, 914 915 /** 916 * Map world coordinates to focal coordinates. These coordinate systems 917 * are explained in the {@link JXG.View3D#boxToCam} matrix 918 * documentation. 919 * 920 * @param {Array} pWorld A world space point, in homogeneous coordinates. 921 * @param {Boolean} [homog=true] Whether to return homogeneous coordinates. 922 * If false, projects down to ordinary coordinates. 923 */ 924 worldToFocal: function (pWorld, homog = true) { 925 var k, 926 pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld)); 927 pView[3] -= pView[0] * this.focalDist; 928 if (homog) { 929 return pView; 930 } else { 931 for (k = 1; k < 4; k++) { 932 pView[k] /= pView[0]; 933 } 934 return pView.slice(1, 4); 935 } 936 }, 937 938 /** 939 * Project 3D coordinates to 2D board coordinates 940 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 941 * 942 * @param {Number|Array} x 943 * @param {Number[]} y 944 * @param {Number[]} z 945 * @returns {Array} Array of length 3 containing the projection on to the board 946 * in homogeneous user coordinates. 947 */ 948 project3DTo2D: function (x, y, z) { 949 var vec, w; 950 if (arguments.length === 3) { 951 vec = [1, x, y, z]; 952 } else { 953 // Argument is an array 954 if (x.length === 3) { 955 // vec = [1].concat(x); 956 vec = x.slice(); 957 vec.unshift(1); 958 } else { 959 vec = x; 960 } 961 } 962 963 w = Mat.matVecMult(this.matrix3D, vec); 964 965 switch (this.projectionType) { 966 case 'central': 967 w[1] /= w[0]; 968 w[2] /= w[0]; 969 w[3] /= w[0]; 970 w[0] /= w[0]; 971 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3)); 972 973 case 'parallel': 974 default: 975 return w; 976 } 977 }, 978 979 /** 980 * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y. 981 * Setting R = mat^(-1) gives 982 * 1/ w0 * (1, x, y, d)^T = R * v2d. 983 * The first and the last row of this equation allows to determine 1/w0 and h. 984 * 985 * @param {Array} mat 986 * @param {Array} v2d 987 * @param {Number} d 988 * @returns Array 989 * @private 990 */ 991 _getW0: function (mat, v2d, d) { 992 var R = Mat.inverse(mat), 993 R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2], 994 R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2], 995 w, h, det; 996 997 det = d * R[0][3] - R[3][3]; 998 w = (R2 * R[0][3] - R1 * R[3][3]) / det; 999 h = (R2 - R1 * d) / det; 1000 return [1 / w, h]; 1001 }, 1002 1003 /** 1004 * Project a 2D coordinate to the plane defined by point "foot" 1005 * and the normal vector `normal`. 1006 * 1007 * @param {JXG.Point} point2d 1008 * @param {Array} normal 1009 * @param {Array} foot 1010 * @returns {Array} of length 4 containing the projected 1011 * point in homogeneous coordinates. 1012 */ 1013 project2DTo3DPlane: function (point2d, normal, foot) { 1014 var mat, rhs, d, le, sol, 1015 n = normal.slice(1), 1016 v2d, w0, res; 1017 1018 foot = foot || [1, 0, 0, 0]; 1019 le = Mat.norm(n, 3); 1020 d = Mat.innerProduct(foot.slice(1), n, 3) / le; 1021 1022 if (this.projectionType === 'parallel') { 1023 mat = this.matrix3D.slice(0, 3); // Copy each row by reference 1024 mat.push([0, n[0], n[1], n[2]]); 1025 1026 // 2D coordinates of point: 1027 rhs = point2d.coords.usrCoords.slice(); 1028 rhs.push(d); 1029 try { 1030 // Prevent singularity in case elevation angle is zero 1031 if (mat[2][3] === 1.0) { 1032 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 1033 } 1034 sol = Mat.Numerics.Gauss(mat, rhs); 1035 } catch (e) { 1036 sol = [0, NaN, NaN, NaN]; 1037 } 1038 } else { 1039 mat = this.matrix3D; 1040 1041 // 2D coordinates of point: 1042 rhs = point2d.coords.usrCoords.slice(); 1043 1044 v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs); 1045 res = this._getW0(mat, v2d, d); 1046 w0 = res[0]; 1047 rhs = [ 1048 v2d[0] * w0, 1049 v2d[1] * w0, 1050 v2d[2] * w0, 1051 res[1] * w0 1052 ]; 1053 try { 1054 // Prevent singularity in case elevation angle is zero 1055 if (mat[2][3] === 1.0) { 1056 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 1057 } 1058 1059 sol = Mat.Numerics.Gauss(mat, rhs); 1060 sol[1] /= sol[0]; 1061 sol[2] /= sol[0]; 1062 sol[3] /= sol[0]; 1063 // sol[3] = d; 1064 sol[0] /= sol[0]; 1065 } catch (err) { 1066 sol = [0, NaN, NaN, NaN]; 1067 } 1068 } 1069 1070 return sol; 1071 }, 1072 1073 /** 1074 * Project a point on the screen to the nearest point, in screen 1075 * distance, on a line segment in 3d space. The inputs must be in 1076 * ordinary coordinates, but the output is in homogeneous coordinates. 1077 * 1078 * @param {Array} pScr The screen coordinates of the point to project. 1079 * @param {Array} end0 The world space coordinates of one end of the 1080 * line segment. 1081 * @param {Array} end1 The world space coordinates of the other end of 1082 * the line segment. 1083 */ 1084 projectScreenToSegment: function (pScr, end0, end1) { 1085 var end0_2d = this.project3DTo2D(end0).slice(1, 3), 1086 end1_2d = this.project3DTo2D(end1).slice(1, 3), 1087 dir_2d = [ 1088 end1_2d[0] - end0_2d[0], 1089 end1_2d[1] - end0_2d[1] 1090 ], 1091 dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d), 1092 diff = [ 1093 pScr[0] - end0_2d[0], 1094 pScr[1] - end0_2d[1] 1095 ], 1096 s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter 1097 mid, mid_2d, mid_diff, m, 1098 1099 t, // view-space affine parameter 1100 t_clamped, // affine parameter clamped to range 1101 t_clamped_co; 1102 1103 if (this.projectionType === 'central') { 1104 mid = [ 1105 0.5 * (end0[0] + end1[0]), 1106 0.5 * (end0[1] + end1[1]), 1107 0.5 * (end0[2] + end1[2]) 1108 ]; 1109 mid_2d = this.project3DTo2D(mid).slice(1, 3); 1110 mid_diff = [ 1111 mid_2d[0] - end0_2d[0], 1112 mid_2d[1] - end0_2d[1] 1113 ]; 1114 m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq; 1115 1116 // the view-space affine parameter s is related to the 1117 // screen-space affine parameter t by a Möbius transformation, 1118 // which is determined by the following relations: 1119 // 1120 // s | t 1121 // ----- 1122 // 0 | 0 1123 // m | 1/2 1124 // 1 | 1 1125 // 1126 t = (1 - m) * s / ((1 - 2 * m) * s + m); 1127 } else { 1128 t = s; 1129 } 1130 1131 t_clamped = Math.min(Math.max(t, 0), 1); 1132 t_clamped_co = 1 - t_clamped; 1133 return [ 1134 1, 1135 t_clamped_co * end0[0] + t_clamped * end1[0], 1136 t_clamped_co * end0[1] + t_clamped * end1[1], 1137 t_clamped_co * end0[2] + t_clamped * end1[2] 1138 ]; 1139 }, 1140 1141 /** 1142 * Project a 2D coordinate to a new 3D position by keeping 1143 * the 3D x, y coordinates and changing only the z coordinate. 1144 * All horizontal moves of the 2D point are ignored. 1145 * 1146 * @param {JXG.Point} point2d 1147 * @param {Array} base_c3d 1148 * @returns {Array} of length 4 containing the projected 1149 * point in homogeneous coordinates. 1150 */ 1151 project2DTo3DVertical: function (point2d, base_c3d) { 1152 var pScr = point2d.coords.usrCoords.slice(1, 3), 1153 end0 = [base_c3d[1], base_c3d[2], this.bbox3D[2][0]], 1154 end1 = [base_c3d[1], base_c3d[2], this.bbox3D[2][1]]; 1155 1156 return this.projectScreenToSegment(pScr, end0, end1); 1157 }, 1158 1159 /** 1160 * Limit 3D coordinates to the bounding cube. 1161 * 1162 * @param {Array} c3d 3D coordinates [x,y,z] 1163 * @returns Array [Array, Boolean] containing [coords, corrected]. coords contains the updated 3D coordinates, 1164 * correct is true if the coords have been changed. 1165 */ 1166 project3DToCube: function (c3d) { 1167 var cube = this.bbox3D, 1168 isOut = false; 1169 1170 if (c3d[1] < cube[0][0]) { 1171 c3d[1] = cube[0][0]; 1172 isOut = true; 1173 } 1174 if (c3d[1] > cube[0][1]) { 1175 c3d[1] = cube[0][1]; 1176 isOut = true; 1177 } 1178 if (c3d[2] < cube[1][0]) { 1179 c3d[2] = cube[1][0]; 1180 isOut = true; 1181 } 1182 if (c3d[2] > cube[1][1]) { 1183 c3d[2] = cube[1][1]; 1184 isOut = true; 1185 } 1186 if (c3d[3] <= cube[2][0]) { 1187 c3d[3] = cube[2][0]; 1188 isOut = true; 1189 } 1190 if (c3d[3] >= cube[2][1]) { 1191 c3d[3] = cube[2][1]; 1192 isOut = true; 1193 } 1194 1195 return [c3d, isOut]; 1196 }, 1197 1198 /** 1199 * Intersect a ray with the bounding cube of the 3D view. 1200 * @param {Array} p 3D coordinates [x,y,z] 1201 * @param {Array} d 3D direction vector of the line (array of length 3) 1202 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 1203 * @returns Affine ratio of the intersection of the line with the cube. 1204 */ 1205 intersectionLineCube: function (p, d, r) { 1206 var r_n, i, r0, r1; 1207 1208 r_n = r; 1209 for (i = 0; i < 3; i++) { 1210 if (d[i] !== 0) { 1211 r0 = (this.bbox3D[i][0] - p[i]) / d[i]; 1212 r1 = (this.bbox3D[i][1] - p[i]) / d[i]; 1213 if (r < 0) { 1214 r_n = Math.max(r_n, Math.min(r0, r1)); 1215 } else { 1216 r_n = Math.min(r_n, Math.max(r0, r1)); 1217 } 1218 } 1219 } 1220 return r_n; 1221 }, 1222 1223 /** 1224 * Test if coordinates are inside of the bounding cube. 1225 * @param {array} q 3D coordinates [x,y,z] of a point. 1226 * @returns Boolean 1227 */ 1228 isInCube: function (q) { 1229 return ( 1230 q[0] > this.bbox3D[0][0] - Mat.eps && 1231 q[0] < this.bbox3D[0][1] + Mat.eps && 1232 q[1] > this.bbox3D[1][0] - Mat.eps && 1233 q[1] < this.bbox3D[1][1] + Mat.eps && 1234 q[2] > this.bbox3D[2][0] - Mat.eps && 1235 q[2] < this.bbox3D[2][1] + Mat.eps 1236 ); 1237 }, 1238 1239 /** 1240 * 1241 * @param {JXG.Plane3D} plane1 1242 * @param {JXG.Plane3D} plane2 1243 * @param {JXG.Plane3D} d 1244 * @returns {Array} of length 2 containing the coordinates of the defining points of 1245 * of the intersection segment. 1246 */ 1247 intersectionPlanePlane: function (plane1, plane2, d) { 1248 var ret = [[], []], 1249 p, 1250 dir, 1251 r, 1252 q; 1253 1254 d = d || plane2.d; 1255 1256 p = Mat.Geometry.meet3Planes( 1257 plane1.normal, 1258 plane1.d, 1259 plane2.normal, 1260 d, 1261 Mat.crossProduct(plane1.normal, plane2.normal), 1262 0 1263 ); 1264 dir = Mat.Geometry.meetPlanePlane( 1265 plane1.vec1, 1266 plane1.vec2, 1267 plane2.vec1, 1268 plane2.vec2 1269 ); 1270 r = this.intersectionLineCube(p, dir, Infinity); 1271 q = Mat.axpy(r, dir, p); 1272 if (this.isInCube(q)) { 1273 ret[0] = q; 1274 } 1275 r = this.intersectionLineCube(p, dir, -Infinity); 1276 q = Mat.axpy(r, dir, p); 1277 if (this.isInCube(q)) { 1278 ret[1] = q; 1279 } 1280 return ret; 1281 }, 1282 1283 /** 1284 * Generate mesh for a surface / plane. 1285 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 1286 * @param {Array|Function} func 1287 * @param {Array} interval_u 1288 * @param {Array} interval_v 1289 * @returns Array 1290 * @private 1291 * 1292 * @example 1293 * var el = view.create('curve', [[], []]); 1294 * el.updateDataArray = function () { 1295 * var steps_u = Type.evaluate(this.visProp.stepsu), 1296 * steps_v = Type.evaluate(this.visProp.stepsv), 1297 * r_u = Type.evaluate(this.range_u), 1298 * r_v = Type.evaluate(this.range_v), 1299 * func, ret; 1300 * 1301 * if (this.F !== null) { 1302 * func = this.F; 1303 * } else { 1304 * func = [this.X, this.Y, this.Z]; 1305 * } 1306 * ret = this.view.getMesh(func, 1307 * r_u.concat([steps_u]), 1308 * r_v.concat([steps_v])); 1309 * 1310 * this.dataX = ret[0]; 1311 * this.dataY = ret[1]; 1312 * }; 1313 * 1314 */ 1315 getMesh: function (func, interval_u, interval_v) { 1316 var i_u, i_v, u, v, 1317 c2d, delta_u, delta_v, 1318 p = [0, 0, 0], 1319 steps_u = interval_u[2], 1320 steps_v = interval_v[2], 1321 dataX = [], 1322 dataY = []; 1323 1324 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u; 1325 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v; 1326 1327 for (i_u = 0; i_u <= steps_u; i_u++) { 1328 u = interval_u[0] + delta_u * i_u; 1329 for (i_v = 0; i_v <= steps_v; i_v++) { 1330 v = interval_v[0] + delta_v * i_v; 1331 if (Type.isFunction(func)) { 1332 p = func(u, v); 1333 } else { 1334 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1335 } 1336 c2d = this.project3DTo2D(p); 1337 dataX.push(c2d[1]); 1338 dataY.push(c2d[2]); 1339 } 1340 dataX.push(NaN); 1341 dataY.push(NaN); 1342 } 1343 1344 for (i_v = 0; i_v <= steps_v; i_v++) { 1345 v = interval_v[0] + delta_v * i_v; 1346 for (i_u = 0; i_u <= steps_u; i_u++) { 1347 u = interval_u[0] + delta_u * i_u; 1348 if (Type.isFunction(func)) { 1349 p = func(u, v); 1350 } else { 1351 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1352 } 1353 c2d = this.project3DTo2D(p); 1354 dataX.push(c2d[1]); 1355 dataY.push(c2d[2]); 1356 } 1357 dataX.push(NaN); 1358 dataY.push(NaN); 1359 } 1360 1361 return [dataX, dataY]; 1362 }, 1363 1364 /** 1365 * 1366 */ 1367 animateAzimuth: function () { 1368 var s = this.az_slide._smin, 1369 e = this.az_slide._smax, 1370 sdiff = e - s, 1371 newVal = this.az_slide.Value() + 0.1; 1372 1373 this.az_slide.position = (newVal - s) / sdiff; 1374 if (this.az_slide.position > 1) { 1375 this.az_slide.position = 0.0; 1376 } 1377 this.board.update(); 1378 1379 this.timeoutAzimuth = setTimeout(function () { 1380 this.animateAzimuth(); 1381 }.bind(this), 200); 1382 }, 1383 1384 /** 1385 * 1386 */ 1387 stopAzimuth: function () { 1388 clearTimeout(this.timeoutAzimuth); 1389 this.timeoutAzimuth = null; 1390 }, 1391 1392 /** 1393 * Check if vertical dragging is enabled and which action is needed. 1394 * Default is shiftKey. 1395 * 1396 * @returns Boolean 1397 * @private 1398 */ 1399 isVerticalDrag: function () { 1400 var b = this.board, 1401 key; 1402 if (!Type.evaluate(this.visProp.verticaldrag.enabled)) { 1403 return false; 1404 } 1405 key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key'; 1406 return b[key]; 1407 }, 1408 1409 /** 1410 * Sets camera view to the given values. 1411 * 1412 * @param {Number} az Value of azimuth. 1413 * @param {Number} el Value of elevation. 1414 * @param {Number} [r] Value of radius. 1415 * 1416 * @returns {Object} Reference to the view. 1417 */ 1418 setView: function (az, el, r) { 1419 r = r || this.r; 1420 1421 this.az_slide.setValue(az); 1422 this.el_slide.setValue(el); 1423 this.r = r; 1424 this.board.update(); 1425 1426 return this; 1427 }, 1428 1429 /** 1430 * Changes view to the next view stored in the attribute `values`. 1431 * 1432 * @see View3D#values 1433 * 1434 * @returns {Object} Reference to the view. 1435 */ 1436 nextView: function () { 1437 var views = Type.evaluate(this.visProp.values), 1438 n = this.visProp._currentview; 1439 1440 n = (n + 1) % views.length; 1441 this.setCurrentView(n); 1442 1443 return this; 1444 }, 1445 1446 /** 1447 * Changes view to the previous view stored in the attribute `values`. 1448 * 1449 * @see View3D#values 1450 * 1451 * @returns {Object} Reference to the view. 1452 */ 1453 previousView: function () { 1454 var views = Type.evaluate(this.visProp.values), 1455 n = this.visProp._currentview; 1456 1457 n = (n + views.length - 1) % views.length; 1458 this.setCurrentView(n); 1459 1460 return this; 1461 }, 1462 1463 /** 1464 * Changes view to the determined view stored in the attribute `values`. 1465 * 1466 * @see View3D#values 1467 * 1468 * @param {Number} n Index of view in attribute `values`. 1469 * @returns {Object} Reference to the view. 1470 */ 1471 setCurrentView: function (n) { 1472 var views = Type.evaluate(this.visProp.values); 1473 1474 if (n < 0 || n >= views.length) { 1475 n = ((n % views.length) + views.length) % views.length; 1476 } 1477 1478 this.setView(views[n][0], views[n][1], views[n][2]); 1479 this.visProp._currentview = n; 1480 1481 return this; 1482 }, 1483 1484 /** 1485 * Controls the navigation in az direction using either the keyboard or a pointer. 1486 * 1487 * @private 1488 * 1489 * @param {event} evt either the keydown or the pointer event 1490 * @returns view 1491 */ 1492 _azEventHandler: function (evt) { 1493 var smax = this.az_slide._smax, 1494 smin = this.az_slide._smin, 1495 speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)), 1496 delta = evt.movementX, 1497 az = this.az_slide.Value(), 1498 el = this.el_slide.Value(); 1499 1500 // Doesn't allow navigation if another moving event is triggered 1501 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1502 return this; 1503 } 1504 1505 // Calculate new az value if keyboard events are triggered 1506 // Plus if right-button, minus if left-button 1507 if (Type.evaluate(this.visProp.az.keyboard.enabled)) { 1508 if (evt.key === 'ArrowRight') { 1509 az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180; 1510 } else if (evt.key === 'ArrowLeft') { 1511 az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180; 1512 } 1513 } 1514 1515 if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && evt.key == null) { 1516 az += delta * speed; 1517 } 1518 1519 // Project the calculated az value to a usable value in the interval [smin,smax] 1520 // Use modulo if continuous is true 1521 if (Type.evaluate(this.visProp.az.continuous)) { 1522 az = Mat.wrap(az, smin, smax); 1523 } else { 1524 if (az > 0) { 1525 az = Math.min(smax, az); 1526 } else if (az < 0) { 1527 az = Math.max(smin, az); 1528 } 1529 } 1530 1531 this.setView(az, el); 1532 return this; 1533 }, 1534 1535 /** 1536 * Controls the navigation in el direction using either the keyboard or a pointer. 1537 * 1538 * @private 1539 * 1540 * @param {event} evt either the keydown or the pointer event 1541 * @returns view 1542 */ 1543 _elEventHandler: function (evt) { 1544 var smax = this.el_slide._smax, 1545 smin = this.el_slide._smin, 1546 speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed), 1547 delta = evt.movementY, 1548 az = this.az_slide.Value(), 1549 el = this.el_slide.Value(); 1550 1551 // Doesn't allow navigation if another moving event is triggered 1552 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1553 return this; 1554 } 1555 1556 // Calculate new az value if keyboard events are triggered 1557 // Plus if down-button, minus if up-button 1558 if (Type.evaluate(this.visProp.el.keyboard.enabled)) { 1559 if (evt.key === 'ArrowUp') { 1560 el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180; 1561 } else if (evt.key === 'ArrowDown') { 1562 el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180; 1563 } 1564 } 1565 1566 if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && evt.key == null) { 1567 el += delta * speed; 1568 } 1569 1570 // Project the calculated el value to a usable value in the interval [smin,smax] 1571 // Use modulo if continuous is true and the trackball is disabled 1572 if (Type.evaluate(this.visProp.el.continuous) && !this.trackballEnabled) { 1573 el = Mat.wrap(el, smin, smax); 1574 } else { 1575 if (el > 0) { 1576 el = Math.min(smax, el); 1577 } else if (el < 0) { 1578 el = Math.max(smin, el); 1579 } 1580 } 1581 1582 this.setView(az, el); 1583 return this; 1584 }, 1585 1586 /** 1587 * Controls the navigation in bank direction using either the keyboard or a pointer. 1588 * 1589 * @private 1590 * 1591 * @param {event} evt either the keydown or the pointer event 1592 * @returns view 1593 */ 1594 _bankEventHandler: function (evt) { 1595 var smax = this.bank_slide._smax, 1596 smin = this.bank_slide._smin, 1597 step, speed, 1598 delta = evt.deltaY, 1599 bank = this.bank_slide.Value(); 1600 1601 // Doesn't allow navigation if another moving event is triggered 1602 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1603 return this; 1604 } 1605 1606 // Calculate new bank value if keyboard events are triggered 1607 // Plus if down-button, minus if up-button 1608 if (Type.evaluate(this.visProp.bank.keyboard.enabled)) { 1609 step = Type.evaluate(this.visProp.bank.keyboard.step) * Math.PI / 180; 1610 if (evt.key === '.' || evt.key === '<') { 1611 bank -= step; 1612 } else if (evt.key === ',' || evt.key === '>') { 1613 bank += step; 1614 } 1615 } 1616 1617 if (Type.evaluate(this.visProp.bank.pointer.enabled) && (delta !== 0) && evt.key == null) { 1618 speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.bank.pointer.speed); 1619 bank += delta * speed; 1620 1621 // prevent the pointer wheel from scrolling the page 1622 evt.preventDefault(); 1623 } 1624 1625 // Project the calculated bank value to a usable value in the interval [smin,smax] 1626 if (Type.evaluate(this.visProp.bank.continuous)) { 1627 // in continuous mode, wrap value around slider range 1628 bank = Mat.wrap(bank, smin, smax); 1629 } else { 1630 // in non-continuous mode, clamp value to slider range 1631 bank = Mat.clamp(bank, smin, smax); 1632 } 1633 1634 this.bank_slide.setValue(bank); 1635 this.board.update(); 1636 return this; 1637 }, 1638 1639 /** 1640 * Controls the navigation using either virtual trackball. 1641 * 1642 * @private 1643 * 1644 * @param {event} evt either the keydown or the pointer event 1645 * @returns view 1646 */ 1647 _trackballHandler: function (evt) { 1648 var pos = this.board.getMousePosition(evt), 1649 x, y, center; 1650 1651 center = new Coords(Const.COORDS_BY_USER, [this.llftCorner[0] + this.size[0] * 0.5, this.llftCorner[1] + this.size[1] * 0.5], this.board); 1652 x = pos[0] - center.scrCoords[1]; 1653 y = pos[1] - center.scrCoords[2]; 1654 this._trackball = { 1655 dx: evt.movementX, 1656 dy: -evt.movementY, 1657 x: x, 1658 y: -y 1659 }; 1660 this.board.update(); 1661 return this; 1662 }, 1663 1664 /** 1665 * Event handler for pointer down event. Triggers handling of all 3D navigation. 1666 * 1667 * @private 1668 * @param {event} evt 1669 * @returns view 1670 */ 1671 pointerDownHandler: function (evt) { 1672 var neededButton, neededKey, target; 1673 1674 this._hasMoveAz = false; 1675 this._hasMoveEl = false; 1676 this._hasMoveBank = false; 1677 this._hasMoveTrackball = false; 1678 1679 if (this.board.mode !== this.board.BOARD_MODE_NONE) { 1680 return; 1681 } 1682 1683 if (Type.evaluate(this.visProp.trackball.enabled)) { 1684 neededButton = Type.evaluate(this.visProp.trackball.button); 1685 neededKey = Type.evaluate(this.visProp.trackball.key); 1686 1687 // Move events for virtual trackball 1688 if ( 1689 (neededButton === -1 || neededButton === evt.button) && 1690 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1691 ) { 1692 // If outside is true then the event listener is bound to the document, otherwise to the div 1693 target = (Type.evaluate(this.visProp.trackball.outside)) ? document : this.board.containerObj; 1694 Env.addEvent(target, 'pointermove', this._trackballHandler, this); 1695 this._hasMoveTrackball = true; 1696 } 1697 } else { 1698 if (Type.evaluate(this.visProp.az.pointer.enabled)) { 1699 neededButton = Type.evaluate(this.visProp.az.pointer.button); 1700 neededKey = Type.evaluate(this.visProp.az.pointer.key); 1701 1702 // Move events for azimuth 1703 if ( 1704 (neededButton === -1 || neededButton === evt.button) && 1705 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1706 ) { 1707 // If outside is true then the event listener is bound to the document, otherwise to the div 1708 target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj; 1709 Env.addEvent(target, 'pointermove', this._azEventHandler, this); 1710 this._hasMoveAz = true; 1711 } 1712 } 1713 1714 if (Type.evaluate(this.visProp.el.pointer.enabled)) { 1715 neededButton = Type.evaluate(this.visProp.el.pointer.button); 1716 neededKey = Type.evaluate(this.visProp.el.pointer.key); 1717 1718 // Events for elevation 1719 if ( 1720 (neededButton === -1 || neededButton === evt.button) && 1721 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1722 ) { 1723 // If outside is true then the event listener is bound to the document, otherwise to the div 1724 target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj; 1725 Env.addEvent(target, 'pointermove', this._elEventHandler, this); 1726 this._hasMoveEl = true; 1727 } 1728 } 1729 1730 if (Type.evaluate(this.visProp.bank.pointer.enabled)) { 1731 neededButton = Type.evaluate(this.visProp.bank.pointer.button); 1732 neededKey = Type.evaluate(this.visProp.bank.pointer.key); 1733 1734 // Events for bank 1735 if ( 1736 (neededButton === -1 || neededButton === evt.button) && 1737 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1738 ) { 1739 // If `outside` is true, we bind the event listener to 1740 // the document. otherwise, we bind it to the div. we 1741 // register the event listener as active so it can 1742 // prevent the pointer wheel from scrolling the page 1743 target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj; 1744 Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false }); 1745 this._hasMoveBank = true; 1746 } 1747 } 1748 } 1749 Env.addEvent(document, 'pointerup', this.pointerUpHandler, this); 1750 }, 1751 1752 /** 1753 * Event handler for pointer up event. Triggers handling of all 3D navigation. 1754 * 1755 * @private 1756 * @param {event} evt 1757 * @returns view 1758 */ 1759 pointerUpHandler: function (evt) { 1760 var target; 1761 if (this._hasMoveAz) { 1762 target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj; 1763 Env.removeEvent(target, 'pointermove', this._azEventHandler, this); 1764 this._hasMoveAz = false; 1765 } 1766 if (this._hasMoveEl) { 1767 target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj; 1768 Env.removeEvent(target, 'pointermove', this._elEventHandler, this); 1769 this._hasMoveEl = false; 1770 } 1771 if (this._hasMoveBank) { 1772 target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj; 1773 Env.removeEvent(target, 'wheel', this._bankEventHandler, this); 1774 this._hasMoveBank = false; 1775 } 1776 if (this._hasMoveTrackball) { 1777 target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj; 1778 Env.removeEvent(target, 'pointermove', this._trackballHandler, this); 1779 this._hasMoveTrackball = false; 1780 } 1781 Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this); 1782 } 1783 }); 1784 1785 /** 1786 * @class This element creates a 3D view. 1787 * @pseudo 1788 * @description A View3D element provides the container and the methods to create and display 3D elements. 1789 * It is contained in a JSXGraph board. 1790 * <p> 1791 * It is advisable to disable panning of the board by setting the board attribute "pan": 1792 * <pre> 1793 * pan: {anabled: fasle} 1794 * </pre> 1795 * Otherwise users will not be able to rotate the scene with their fingers on a touch device. 1796 * 1797 * @name View3D 1798 * @augments JXG.View3D 1799 * @constructor 1800 * @type Object 1801 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1802 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 1803 * dim is an array of the form [w, h]. 1804 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 1805 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 1806 * [x,y] and side lengths [w, h] of the board. 1807 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 1808 * which determines the coordinate ranges of the 3D cube. 1809 * 1810 * @example 1811 * var bound = [-4, 6]; 1812 * var view = board.create('view3d', 1813 * [[-4, -3], [8, 8], 1814 * [bound, bound, bound]], 1815 * { 1816 * projection: 'parallel', 1817 * trackball: {enabled:true}, 1818 * }); 1819 * 1820 * var curve = view.create('curve3d', [ 1821 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1822 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1823 * (t) => Math.sin(3 * t), 1824 * [-Math.PI, Math.PI] 1825 * ], { strokeWidth: 4 }); 1826 * 1827 * </pre><div id="JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1828 * <script type="text/javascript"> 1829 * (function() { 1830 * var board = JXG.JSXGraph.initBoard('JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b', 1831 * {boundingbox: [-8, 8, 8,-8], pan: {enabled: false}, axis: false, showcopyright: false, shownavigation: false}); 1832 * var bound = [-4, 6]; 1833 * var view = board.create('view3d', 1834 * [[-4, -3], [8, 8], 1835 * [bound, bound, bound]], 1836 * { 1837 * projection: 'parallel', 1838 * trackball: {enabled:true}, 1839 * }); 1840 * 1841 * var curve = view.create('curve3d', [ 1842 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1843 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1844 * (t) => Math.sin(3 * t), 1845 * [-Math.PI, Math.PI] 1846 * ], { strokeWidth: 4 }); 1847 * 1848 * })(); 1849 * 1850 * </script><pre> 1851 * 1852 * @example 1853 * var bound = [-4, 6]; 1854 * var view = board.create('view3d', 1855 * [[-4, -3], [8, 8], 1856 * [bound, bound, bound]], 1857 * { 1858 * projection: 'central', 1859 * trackball: {enabled:true}, 1860 * 1861 * xPlaneRear: { visible: false }, 1862 * yPlaneRear: { visible: false } 1863 * 1864 * }); 1865 * 1866 * var curve = view.create('curve3d', [ 1867 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1868 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1869 * (t) => Math.sin(3 * t), 1870 * [-Math.PI, Math.PI] 1871 * ], { strokeWidth: 4 }); 1872 * 1873 * </pre><div id="JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007" class="jxgbox" style="width: 300px; height: 300px;"></div> 1874 * <script type="text/javascript"> 1875 * (function() { 1876 * var board = JXG.JSXGraph.initBoard('JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007', 1877 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 1878 * var bound = [-4, 6]; 1879 * var view = board.create('view3d', 1880 * [[-4, -3], [8, 8], 1881 * [bound, bound, bound]], 1882 * { 1883 * projection: 'central', 1884 * trackball: {enabled:true}, 1885 * 1886 * xPlaneRear: { visible: false }, 1887 * yPlaneRear: { visible: false } 1888 * 1889 * }); 1890 * 1891 * var curve = view.create('curve3d', [ 1892 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1893 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1894 * (t) => Math.sin(3 * t), 1895 * [-Math.PI, Math.PI] 1896 * ], { strokeWidth: 4 }); 1897 * 1898 * })(); 1899 * 1900 * </script><pre> 1901 * 1902 * @example 1903 * var bound = [-4, 6]; 1904 * var view = board.create('view3d', 1905 * [[-4, -3], [8, 8], 1906 * [bound, bound, bound]], 1907 * { 1908 * projection: 'central', 1909 * trackball: {enabled:true}, 1910 * 1911 * // Main axes 1912 * axesPosition: 'border', 1913 * 1914 * // Axes at the border 1915 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 1916 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 1917 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 1918 * 1919 * // No axes on planes 1920 * xPlaneRearYAxis: {visible: false}, 1921 * xPlaneRearZAxis: {visible: false}, 1922 * yPlaneRearXAxis: {visible: false}, 1923 * yPlaneRearZAxis: {visible: false}, 1924 * zPlaneRearXAxis: {visible: false}, 1925 * zPlaneRearYAxis: {visible: false} 1926 * }); 1927 * 1928 * var curve = view.create('curve3d', [ 1929 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1930 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1931 * (t) => Math.sin(3 * t), 1932 * [-Math.PI, Math.PI] 1933 * ], { strokeWidth: 4 }); 1934 * 1935 * </pre><div id="JXG586f3551-335c-47e9-8d72-835409f6a103" class="jxgbox" style="width: 300px; height: 300px;"></div> 1936 * <script type="text/javascript"> 1937 * (function() { 1938 * var board = JXG.JSXGraph.initBoard('JXG586f3551-335c-47e9-8d72-835409f6a103', 1939 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 1940 * var bound = [-4, 6]; 1941 * var view = board.create('view3d', 1942 * [[-4, -3], [8, 8], 1943 * [bound, bound, bound]], 1944 * { 1945 * projection: 'central', 1946 * trackball: {enabled:true}, 1947 * 1948 * // Main axes 1949 * axesPosition: 'border', 1950 * 1951 * // Axes at the border 1952 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 1953 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 1954 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 1955 * 1956 * // No axes on planes 1957 * xPlaneRearYAxis: {visible: false}, 1958 * xPlaneRearZAxis: {visible: false}, 1959 * yPlaneRearXAxis: {visible: false}, 1960 * yPlaneRearZAxis: {visible: false}, 1961 * zPlaneRearXAxis: {visible: false}, 1962 * zPlaneRearYAxis: {visible: false} 1963 * }); 1964 * 1965 * var curve = view.create('curve3d', [ 1966 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1967 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1968 * (t) => Math.sin(3 * t), 1969 * [-Math.PI, Math.PI] 1970 * ], { strokeWidth: 4 }); 1971 * 1972 * })(); 1973 * 1974 * </script><pre> 1975 * 1976 * @example 1977 * var bound = [-4, 6]; 1978 * var view = board.create('view3d', 1979 * [[-4, -3], [8, 8], 1980 * [bound, bound, bound]], 1981 * { 1982 * projection: 'central', 1983 * trackball: {enabled:true}, 1984 * 1985 * axesPosition: 'none' 1986 * }); 1987 * 1988 * var curve = view.create('curve3d', [ 1989 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1990 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1991 * (t) => Math.sin(3 * t), 1992 * [-Math.PI, Math.PI] 1993 * ], { strokeWidth: 4 }); 1994 * 1995 * </pre><div id="JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26" class="jxgbox" style="width: 300px; height: 300px;"></div> 1996 * <script type="text/javascript"> 1997 * (function() { 1998 * var board = JXG.JSXGraph.initBoard('JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26', 1999 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2000 * var bound = [-4, 6]; 2001 * var view = board.create('view3d', 2002 * [[-4, -3], [8, 8], 2003 * [bound, bound, bound]], 2004 * { 2005 * projection: 'central', 2006 * trackball: {enabled:true}, 2007 * 2008 * axesPosition: 'none' 2009 * }); 2010 * 2011 * var curve = view.create('curve3d', [ 2012 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2013 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2014 * (t) => Math.sin(3 * t), 2015 * [-Math.PI, Math.PI] 2016 * ], { strokeWidth: 4 }); 2017 * 2018 * })(); 2019 * 2020 * </script><pre> 2021 * 2022 * @example 2023 * var bound = [-4, 6]; 2024 * var view = board.create('view3d', 2025 * [[-4, -3], [8, 8], 2026 * [bound, bound, bound]], 2027 * { 2028 * projection: 'central', 2029 * trackball: {enabled:true}, 2030 * 2031 * // Main axes 2032 * axesPosition: 'border', 2033 * 2034 * // Axes at the border 2035 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2036 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2037 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2038 * 2039 * xPlaneRear: { 2040 * fillColor: '#fff', 2041 * mesh3d: {visible: false} 2042 * }, 2043 * yPlaneRear: { 2044 * fillColor: '#fff', 2045 * mesh3d: {visible: false} 2046 * }, 2047 * zPlaneRear: { 2048 * fillColor: '#fff', 2049 * mesh3d: {visible: false} 2050 * }, 2051 * xPlaneFront: { 2052 * visible: true, 2053 * fillColor: '#fff', 2054 * mesh3d: {visible: false} 2055 * }, 2056 * yPlaneFront: { 2057 * visible: true, 2058 * fillColor: '#fff', 2059 * mesh3d: {visible: false} 2060 * }, 2061 * zPlaneFront: { 2062 * visible: true, 2063 * fillColor: '#fff', 2064 * mesh3d: {visible: false} 2065 * }, 2066 * 2067 * // No axes on planes 2068 * xPlaneRearYAxis: {visible: false}, 2069 * xPlaneRearZAxis: {visible: false}, 2070 * yPlaneRearXAxis: {visible: false}, 2071 * yPlaneRearZAxis: {visible: false}, 2072 * zPlaneRearXAxis: {visible: false}, 2073 * zPlaneRearYAxis: {visible: false}, 2074 * xPlaneFrontYAxis: {visible: false}, 2075 * xPlaneFrontZAxis: {visible: false}, 2076 * yPlaneFrontXAxis: {visible: false}, 2077 * yPlaneFrontZAxis: {visible: false}, 2078 * zPlaneFrontXAxis: {visible: false}, 2079 * zPlaneFrontYAxis: {visible: false} 2080 * 2081 * }); 2082 * 2083 * var curve = view.create('curve3d', [ 2084 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2085 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2086 * (t) => Math.sin(3 * t), 2087 * [-Math.PI, Math.PI] 2088 * ], { strokeWidth: 4 }); 2089 * 2090 * </pre><div id="JXGbd41a4e3-1bf7-4764-b675-98b01667103b" class="jxgbox" style="width: 300px; height: 300px;"></div> 2091 * <script type="text/javascript"> 2092 * (function() { 2093 * var board = JXG.JSXGraph.initBoard('JXGbd41a4e3-1bf7-4764-b675-98b01667103b', 2094 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2095 * var bound = [-4, 6]; 2096 * var view = board.create('view3d', 2097 * [[-4, -3], [8, 8], 2098 * [bound, bound, bound]], 2099 * { 2100 * projection: 'central', 2101 * trackball: {enabled:true}, 2102 * 2103 * // Main axes 2104 * axesPosition: 'border', 2105 * 2106 * // Axes at the border 2107 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2108 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2109 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2110 * 2111 * xPlaneRear: { 2112 * fillColor: '#fff', 2113 * mesh3d: {visible: false} 2114 * }, 2115 * yPlaneRear: { 2116 * fillColor: '#fff', 2117 * mesh3d: {visible: false} 2118 * }, 2119 * zPlaneRear: { 2120 * fillColor: '#fff', 2121 * mesh3d: {visible: false} 2122 * }, 2123 * xPlaneFront: { 2124 * visible: true, 2125 * fillColor: '#fff', 2126 * mesh3d: {visible: false} 2127 * }, 2128 * yPlaneFront: { 2129 * visible: true, 2130 * fillColor: '#fff', 2131 * mesh3d: {visible: false} 2132 * }, 2133 * zPlaneFront: { 2134 * visible: true, 2135 * fillColor: '#fff', 2136 * mesh3d: {visible: false} 2137 * }, 2138 * 2139 * // No axes on planes 2140 * xPlaneRearYAxis: {visible: false}, 2141 * xPlaneRearZAxis: {visible: false}, 2142 * yPlaneRearXAxis: {visible: false}, 2143 * yPlaneRearZAxis: {visible: false}, 2144 * zPlaneRearXAxis: {visible: false}, 2145 * zPlaneRearYAxis: {visible: false}, 2146 * xPlaneFrontYAxis: {visible: false}, 2147 * xPlaneFrontZAxis: {visible: false}, 2148 * yPlaneFrontXAxis: {visible: false}, 2149 * yPlaneFrontZAxis: {visible: false}, 2150 * zPlaneFrontXAxis: {visible: false}, 2151 * zPlaneFrontYAxis: {visible: false} 2152 * 2153 * }); 2154 * 2155 * var curve = view.create('curve3d', [ 2156 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2157 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2158 * (t) => Math.sin(3 * t), 2159 * [-Math.PI, Math.PI] 2160 * ], { strokeWidth: 4 }); 2161 * })(); 2162 * 2163 * </script><pre> 2164 * 2165 * @example 2166 * var bound = [-5, 5]; 2167 * var view = board.create('view3d', 2168 * [[-6, -3], 2169 * [8, 8], 2170 * [bound, bound, bound]], 2171 * { 2172 * // Main axes 2173 * axesPosition: 'center', 2174 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 2175 * 2176 * // Planes 2177 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 2178 * yPlaneFront: { visible: true, fillColor: 'blue'}, 2179 * 2180 * // Axes on planes 2181 * xPlaneRearYAxis: {strokeColor: 'red'}, 2182 * xPlaneRearZAxis: {strokeColor: 'red'}, 2183 * 2184 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 2185 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 2186 * 2187 * zPlaneFrontXAxis: {visible: false}, 2188 * zPlaneFrontYAxis: {visible: false} 2189 * }); 2190 * 2191 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 2192 * <script type="text/javascript"> 2193 * (function() { 2194 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 2195 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2196 * var bound = [-5, 5]; 2197 * var view = board.create('view3d', 2198 * [[-6, -3], [8, 8], 2199 * [bound, bound, bound]], 2200 * { 2201 * // Main axes 2202 * axesPosition: 'center', 2203 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 2204 * // Planes 2205 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 2206 * yPlaneFront: { visible: true, fillColor: 'blue'}, 2207 * // Axes on planes 2208 * xPlaneRearYAxis: {strokeColor: 'red'}, 2209 * xPlaneRearZAxis: {strokeColor: 'red'}, 2210 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 2211 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 2212 * zPlaneFrontXAxis: {visible: false}, 2213 * zPlaneFrontYAxis: {visible: false} 2214 * }); 2215 * })(); 2216 * 2217 * </script><pre> 2218 * 2219 */ 2220 JXG.createView3D = function (board, parents, attributes) { 2221 var view, attr, attr_az, attr_el, attr_bank, 2222 x, y, w, h, 2223 coords = parents[0], // llft corner 2224 size = parents[1]; // [w, h] 2225 2226 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 2227 view = new JXG.View3D(board, parents, attr); 2228 view.defaultAxes = view.create('axes3d', [], attr); 2229 2230 x = coords[0]; 2231 y = coords[1]; 2232 w = size[0]; 2233 h = size[1]; 2234 2235 attr_az = Type.copyAttributes(attr, board.options, 'view3d', 'az', 'slider'); 2236 attr_az.name = 'az'; 2237 2238 attr_el = Type.copyAttributes(attr, board.options, 'view3d', 'el', 'slider'); 2239 attr_el.name = 'el'; 2240 2241 attr_bank = Type.copyAttributes(attr, board.options, 'view3d', 'bank', 'slider'); 2242 attr_bank.name = 'bank'; 2243 2244 /** 2245 * Slider to adapt azimuth angle 2246 * @name JXG.View3D#az_slide 2247 * @type {Slider} 2248 */ 2249 view.az_slide = board.create( 2250 'slider', 2251 [ 2252 [x - 1, y - 2], 2253 [x + w + 1, y - 2], 2254 [ 2255 Type.evaluate(attr_az.min), 2256 Type.evaluate(attr_az.start), 2257 Type.evaluate(attr_az.max) 2258 ] 2259 ], 2260 attr_az 2261 ); 2262 // view.az_slide.inherits.push(view); 2263 view.inherits.push(view.az_slide); 2264 2265 /** 2266 * Slider to adapt elevation angle 2267 * 2268 * @name JXG.View3D#el_slide 2269 * @type {Slider} 2270 */ 2271 view.el_slide = board.create( 2272 'slider', 2273 [ 2274 [x - 1, y], 2275 [x - 1, y + h], 2276 [ 2277 Type.evaluate(attr_el.min), 2278 Type.evaluate(attr_el.start), 2279 Type.evaluate(attr_el.max)] 2280 ], 2281 attr_el 2282 ); 2283 view.inherits.push(view.el_slide); 2284 2285 /** 2286 * Slider to adjust bank angle 2287 * 2288 * @name JXG.View3D#bank_slide 2289 * @type {Slider} 2290 */ 2291 view.bank_slide = board.create( 2292 'slider', 2293 [ 2294 [x - 1, y + h + 2], 2295 [x + w + 1, y + h + 2], 2296 [ 2297 Type.evaluate(attr_bank.min), 2298 Type.evaluate(attr_bank.start), 2299 Type.evaluate(attr_bank.max) 2300 ] 2301 ], 2302 attr_bank 2303 ); 2304 view.inherits.push(view.bank_slide); 2305 2306 // Set special infobox attributes of view3d.infobox 2307 // Using setAttribute() is not possible here, since we have to 2308 // avoid a call of board.update(). 2309 view.board.infobox.visProp = Type.merge(view.board.infobox.visProp, attr.infobox); 2310 2311 // 3d infobox: drag direction and coordinates 2312 view.board.highlightInfobox = function (x, y, el) { 2313 var d, i, c3d, foot, 2314 pre = '', 2315 brd = el.board, 2316 arr, infobox, 2317 p = null; 2318 2319 if (this.mode === this.BOARD_MODE_DRAG) { 2320 // Drag direction is only shown during dragging 2321 if (view.isVerticalDrag()) { 2322 pre = '<span style="color:black; font-size:200%">\u21C5 </span>'; 2323 } else { 2324 pre = '<span style="color:black; font-size:200%">\u21C4 </span>'; 2325 } 2326 } 2327 2328 // Search 3D parent 2329 for (i = 0; i < el.parents.length; i++) { 2330 p = brd.objects[el.parents[i]]; 2331 if (p.is3D) { 2332 break; 2333 } 2334 } 2335 if (p) { 2336 foot = [1, 0, 0, p.coords[3]]; 2337 view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4); 2338 2339 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 2340 if (!view.isInCube(c3d)) { 2341 view.board.highlightCustomInfobox('', p); 2342 return; 2343 } 2344 d = Type.evaluate(p.visProp.infoboxdigits); 2345 infobox = view.board.infobox; 2346 if (d === 'auto') { 2347 if (infobox.useLocale()) { 2348 arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')']; 2349 } else { 2350 arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')']; 2351 } 2352 2353 } else { 2354 if (infobox.useLocale()) { 2355 arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')']; 2356 } else { 2357 arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')']; 2358 } 2359 } 2360 view.board.highlightCustomInfobox(arr.join(''), p); 2361 } else { 2362 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 2363 } 2364 }; 2365 2366 // Hack needed to enable addEvent for view3D: 2367 view.BOARD_MODE_NONE = 0x0000; 2368 2369 // Add events for the keyboard navigation 2370 Env.addEvent(board.containerObj, 'keydown', function (event) { 2371 var neededKey, 2372 catchEvt = false; 2373 2374 if (Type.evaluate(view.visProp.el.keyboard.enabled) && 2375 (event.key === 'ArrowUp' || event.key === 'ArrowDown') 2376 ) { 2377 neededKey = Type.evaluate(view.visProp.el.keyboard.key); 2378 if (neededKey === 'none' || 2379 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 2380 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 2381 view._elEventHandler(event); 2382 catchEvt = true; 2383 } 2384 2385 } 2386 if (Type.evaluate(view.visProp.el.keyboard.enabled) && 2387 (event.key === 'ArrowLeft' || event.key === 'ArrowRight') 2388 ) { 2389 neededKey = Type.evaluate(view.visProp.az.keyboard.key); 2390 if (neededKey === 'none' || 2391 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 2392 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey) 2393 ) { 2394 view._azEventHandler(event); 2395 catchEvt = true; 2396 } 2397 } 2398 if (Type.evaluate(view.visProp.bank.keyboard.enabled) && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) { 2399 neededKey = Type.evaluate(view.visProp.bank.keyboard.key); 2400 if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 2401 view._bankEventHandler(event); 2402 catchEvt = true; 2403 } 2404 } 2405 if (event.key === 'PageUp') { 2406 view.nextView(); 2407 catchEvt = true; 2408 } else if (event.key === 'PageDown') { 2409 view.previousView(); 2410 catchEvt = true; 2411 } 2412 2413 if (catchEvt) { 2414 // We stop event handling only in the case if the keypress could be 2415 // used for the 3D view. If this is not done, input fields et al 2416 // can not be used any more. 2417 event.preventDefault(); 2418 } 2419 }, view); 2420 2421 // Add events for the pointer navigation 2422 Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view); 2423 2424 // Initialize view rotation matrix 2425 view.getAnglesFromSliders(); 2426 view.matrix3DRot = view.getRotationFromAngles(); 2427 2428 // override angle slider bounds when trackball navigation is enabled 2429 view.updateAngleSliderBounds(); 2430 2431 view.board.update(); 2432 2433 return view; 2434 }; 2435 2436 JXG.registerElement("view3d", JXG.createView3D); 2437 2438 export default JXG.View3D; 2439