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 with updated 3D coordinates. 1164 */ 1165 project3DToCube: function (c3d) { 1166 var cube = this.bbox3D; 1167 if (c3d[1] < cube[0][0]) { 1168 c3d[1] = cube[0][0]; 1169 } 1170 if (c3d[1] > cube[0][1]) { 1171 c3d[1] = cube[0][1]; 1172 } 1173 if (c3d[2] < cube[1][0]) { 1174 c3d[2] = cube[1][0]; 1175 } 1176 if (c3d[2] > cube[1][1]) { 1177 c3d[2] = cube[1][1]; 1178 } 1179 if (c3d[3] < cube[2][0]) { 1180 c3d[3] = cube[2][0]; 1181 } 1182 if (c3d[3] > cube[2][1]) { 1183 c3d[3] = cube[2][1]; 1184 } 1185 1186 return c3d; 1187 }, 1188 1189 /** 1190 * Intersect a ray with the bounding cube of the 3D view. 1191 * @param {Array} p 3D coordinates [x,y,z] 1192 * @param {Array} d 3D direction vector of the line (array of length 3) 1193 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 1194 * @returns Affine ratio of the intersection of the line with the cube. 1195 */ 1196 intersectionLineCube: function (p, d, r) { 1197 var r_n, i, r0, r1; 1198 1199 r_n = r; 1200 for (i = 0; i < 3; i++) { 1201 if (d[i] !== 0) { 1202 r0 = (this.bbox3D[i][0] - p[i]) / d[i]; 1203 r1 = (this.bbox3D[i][1] - p[i]) / d[i]; 1204 if (r < 0) { 1205 r_n = Math.max(r_n, Math.min(r0, r1)); 1206 } else { 1207 r_n = Math.min(r_n, Math.max(r0, r1)); 1208 } 1209 } 1210 } 1211 return r_n; 1212 }, 1213 1214 /** 1215 * Test if coordinates are inside of the bounding cube. 1216 * @param {array} q 3D coordinates [x,y,z] of a point. 1217 * @returns Boolean 1218 */ 1219 isInCube: function (q) { 1220 return ( 1221 q[0] > this.bbox3D[0][0] - Mat.eps && 1222 q[0] < this.bbox3D[0][1] + Mat.eps && 1223 q[1] > this.bbox3D[1][0] - Mat.eps && 1224 q[1] < this.bbox3D[1][1] + Mat.eps && 1225 q[2] > this.bbox3D[2][0] - Mat.eps && 1226 q[2] < this.bbox3D[2][1] + Mat.eps 1227 ); 1228 }, 1229 1230 /** 1231 * 1232 * @param {JXG.Plane3D} plane1 1233 * @param {JXG.Plane3D} plane2 1234 * @param {JXG.Plane3D} d 1235 * @returns {Array} of length 2 containing the coordinates of the defining points of 1236 * of the intersection segment. 1237 */ 1238 intersectionPlanePlane: function (plane1, plane2, d) { 1239 var ret = [[], []], 1240 p, 1241 dir, 1242 r, 1243 q; 1244 1245 d = d || plane2.d; 1246 1247 p = Mat.Geometry.meet3Planes( 1248 plane1.normal, 1249 plane1.d, 1250 plane2.normal, 1251 d, 1252 Mat.crossProduct(plane1.normal, plane2.normal), 1253 0 1254 ); 1255 dir = Mat.Geometry.meetPlanePlane( 1256 plane1.vec1, 1257 plane1.vec2, 1258 plane2.vec1, 1259 plane2.vec2 1260 ); 1261 r = this.intersectionLineCube(p, dir, Infinity); 1262 q = Mat.axpy(r, dir, p); 1263 if (this.isInCube(q)) { 1264 ret[0] = q; 1265 } 1266 r = this.intersectionLineCube(p, dir, -Infinity); 1267 q = Mat.axpy(r, dir, p); 1268 if (this.isInCube(q)) { 1269 ret[1] = q; 1270 } 1271 return ret; 1272 }, 1273 1274 /** 1275 * Generate mesh for a surface / plane. 1276 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 1277 * @param {Array|Function} func 1278 * @param {Array} interval_u 1279 * @param {Array} interval_v 1280 * @returns Array 1281 * @private 1282 * 1283 * @example 1284 * var el = view.create('curve', [[], []]); 1285 * el.updateDataArray = function () { 1286 * var steps_u = Type.evaluate(this.visProp.stepsu), 1287 * steps_v = Type.evaluate(this.visProp.stepsv), 1288 * r_u = Type.evaluate(this.range_u), 1289 * r_v = Type.evaluate(this.range_v), 1290 * func, ret; 1291 * 1292 * if (this.F !== null) { 1293 * func = this.F; 1294 * } else { 1295 * func = [this.X, this.Y, this.Z]; 1296 * } 1297 * ret = this.view.getMesh(func, 1298 * r_u.concat([steps_u]), 1299 * r_v.concat([steps_v])); 1300 * 1301 * this.dataX = ret[0]; 1302 * this.dataY = ret[1]; 1303 * }; 1304 * 1305 */ 1306 getMesh: function (func, interval_u, interval_v) { 1307 var i_u, i_v, u, v, 1308 c2d, delta_u, delta_v, 1309 p = [0, 0, 0], 1310 steps_u = interval_u[2], 1311 steps_v = interval_v[2], 1312 dataX = [], 1313 dataY = []; 1314 1315 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u; 1316 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v; 1317 1318 for (i_u = 0; i_u <= steps_u; i_u++) { 1319 u = interval_u[0] + delta_u * i_u; 1320 for (i_v = 0; i_v <= steps_v; i_v++) { 1321 v = interval_v[0] + delta_v * i_v; 1322 if (Type.isFunction(func)) { 1323 p = func(u, v); 1324 } else { 1325 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1326 } 1327 c2d = this.project3DTo2D(p); 1328 dataX.push(c2d[1]); 1329 dataY.push(c2d[2]); 1330 } 1331 dataX.push(NaN); 1332 dataY.push(NaN); 1333 } 1334 1335 for (i_v = 0; i_v <= steps_v; i_v++) { 1336 v = interval_v[0] + delta_v * i_v; 1337 for (i_u = 0; i_u <= steps_u; i_u++) { 1338 u = interval_u[0] + delta_u * i_u; 1339 if (Type.isFunction(func)) { 1340 p = func(u, v); 1341 } else { 1342 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1343 } 1344 c2d = this.project3DTo2D(p); 1345 dataX.push(c2d[1]); 1346 dataY.push(c2d[2]); 1347 } 1348 dataX.push(NaN); 1349 dataY.push(NaN); 1350 } 1351 1352 return [dataX, dataY]; 1353 }, 1354 1355 /** 1356 * 1357 */ 1358 animateAzimuth: function () { 1359 var s = this.az_slide._smin, 1360 e = this.az_slide._smax, 1361 sdiff = e - s, 1362 newVal = this.az_slide.Value() + 0.1; 1363 1364 this.az_slide.position = (newVal - s) / sdiff; 1365 if (this.az_slide.position > 1) { 1366 this.az_slide.position = 0.0; 1367 } 1368 this.board.update(); 1369 1370 this.timeoutAzimuth = setTimeout(function () { 1371 this.animateAzimuth(); 1372 }.bind(this), 200); 1373 }, 1374 1375 /** 1376 * 1377 */ 1378 stopAzimuth: function () { 1379 clearTimeout(this.timeoutAzimuth); 1380 this.timeoutAzimuth = null; 1381 }, 1382 1383 /** 1384 * Check if vertical dragging is enabled and which action is needed. 1385 * Default is shiftKey. 1386 * 1387 * @returns Boolean 1388 * @private 1389 */ 1390 isVerticalDrag: function () { 1391 var b = this.board, 1392 key; 1393 if (!Type.evaluate(this.visProp.verticaldrag.enabled)) { 1394 return false; 1395 } 1396 key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key'; 1397 return b[key]; 1398 }, 1399 1400 /** 1401 * Sets camera view to the given values. 1402 * 1403 * @param {Number} az Value of azimuth. 1404 * @param {Number} el Value of elevation. 1405 * @param {Number} [r] Value of radius. 1406 * 1407 * @returns {Object} Reference to the view. 1408 */ 1409 setView: function (az, el, r) { 1410 r = r || this.r; 1411 1412 this.az_slide.setValue(az); 1413 this.el_slide.setValue(el); 1414 this.r = r; 1415 this.board.update(); 1416 1417 return this; 1418 }, 1419 1420 /** 1421 * Changes view to the next view stored in the attribute `values`. 1422 * 1423 * @see View3D#values 1424 * 1425 * @returns {Object} Reference to the view. 1426 */ 1427 nextView: function () { 1428 var views = Type.evaluate(this.visProp.values), 1429 n = this.visProp._currentview; 1430 1431 n = (n + 1) % views.length; 1432 this.setCurrentView(n); 1433 1434 return this; 1435 }, 1436 1437 /** 1438 * Changes view to the previous view stored in the attribute `values`. 1439 * 1440 * @see View3D#values 1441 * 1442 * @returns {Object} Reference to the view. 1443 */ 1444 previousView: function () { 1445 var views = Type.evaluate(this.visProp.values), 1446 n = this.visProp._currentview; 1447 1448 n = (n + views.length - 1) % views.length; 1449 this.setCurrentView(n); 1450 1451 return this; 1452 }, 1453 1454 /** 1455 * Changes view to the determined view stored in the attribute `values`. 1456 * 1457 * @see View3D#values 1458 * 1459 * @param {Number} n Index of view in attribute `values`. 1460 * @returns {Object} Reference to the view. 1461 */ 1462 setCurrentView: function (n) { 1463 var views = Type.evaluate(this.visProp.values); 1464 1465 if (n < 0 || n >= views.length) { 1466 n = ((n % views.length) + views.length) % views.length; 1467 } 1468 1469 this.setView(views[n][0], views[n][1], views[n][2]); 1470 this.visProp._currentview = n; 1471 1472 return this; 1473 }, 1474 1475 /** 1476 * Controls the navigation in az direction using either the keyboard or a pointer. 1477 * 1478 * @private 1479 * 1480 * @param {event} evt either the keydown or the pointer event 1481 * @returns view 1482 */ 1483 _azEventHandler: function (evt) { 1484 var smax = this.az_slide._smax, 1485 smin = this.az_slide._smin, 1486 speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)), 1487 delta = evt.movementX, 1488 az = this.az_slide.Value(), 1489 el = this.el_slide.Value(); 1490 1491 // Doesn't allow navigation if another moving event is triggered 1492 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1493 return this; 1494 } 1495 1496 // Calculate new az value if keyboard events are triggered 1497 // Plus if right-button, minus if left-button 1498 if (Type.evaluate(this.visProp.az.keyboard.enabled)) { 1499 if (evt.key === 'ArrowRight') { 1500 az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180; 1501 } else if (evt.key === 'ArrowLeft') { 1502 az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180; 1503 } 1504 } 1505 1506 if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && evt.key == null) { 1507 az += delta * speed; 1508 } 1509 1510 // Project the calculated az value to a usable value in the interval [smin,smax] 1511 // Use modulo if continuous is true 1512 if (Type.evaluate(this.visProp.az.continuous)) { 1513 az = Mat.wrap(az, smin, smax); 1514 } else { 1515 if (az > 0) { 1516 az = Math.min(smax, az); 1517 } else if (az < 0) { 1518 az = Math.max(smin, az); 1519 } 1520 } 1521 1522 this.setView(az, el); 1523 return this; 1524 }, 1525 1526 /** 1527 * Controls the navigation in el direction using either the keyboard or a pointer. 1528 * 1529 * @private 1530 * 1531 * @param {event} evt either the keydown or the pointer event 1532 * @returns view 1533 */ 1534 _elEventHandler: function (evt) { 1535 var smax = this.el_slide._smax, 1536 smin = this.el_slide._smin, 1537 speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed), 1538 delta = evt.movementY, 1539 az = this.az_slide.Value(), 1540 el = this.el_slide.Value(); 1541 1542 // Doesn't allow navigation if another moving event is triggered 1543 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1544 return this; 1545 } 1546 1547 // Calculate new az value if keyboard events are triggered 1548 // Plus if down-button, minus if up-button 1549 if (Type.evaluate(this.visProp.el.keyboard.enabled)) { 1550 if (evt.key === 'ArrowUp') { 1551 el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180; 1552 } else if (evt.key === 'ArrowDown') { 1553 el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180; 1554 } 1555 } 1556 1557 if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && evt.key == null) { 1558 el += delta * speed; 1559 } 1560 1561 // Project the calculated el value to a usable value in the interval [smin,smax] 1562 // Use modulo if continuous is true and the trackball is disabled 1563 if (Type.evaluate(this.visProp.el.continuous) && !this.trackballEnabled) { 1564 el = Mat.wrap(el, smin, smax); 1565 } else { 1566 if (el > 0) { 1567 el = Math.min(smax, el); 1568 } else if (el < 0) { 1569 el = Math.max(smin, el); 1570 } 1571 } 1572 1573 this.setView(az, el); 1574 return this; 1575 }, 1576 1577 /** 1578 * Controls the navigation in bank direction using either the keyboard or a pointer. 1579 * 1580 * @private 1581 * 1582 * @param {event} evt either the keydown or the pointer event 1583 * @returns view 1584 */ 1585 _bankEventHandler: function (evt) { 1586 var smax = this.bank_slide._smax, 1587 smin = this.bank_slide._smin, 1588 step, speed, 1589 delta = evt.deltaY, 1590 bank = this.bank_slide.Value(); 1591 1592 // Doesn't allow navigation if another moving event is triggered 1593 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1594 return this; 1595 } 1596 1597 // Calculate new bank value if keyboard events are triggered 1598 // Plus if down-button, minus if up-button 1599 if (Type.evaluate(this.visProp.bank.keyboard.enabled)) { 1600 step = Type.evaluate(this.visProp.bank.keyboard.step) * Math.PI / 180; 1601 if (evt.key === '.' || evt.key === '<') { 1602 bank -= step; 1603 } else if (evt.key === ',' || evt.key === '>') { 1604 bank += step; 1605 } 1606 } 1607 1608 if (Type.evaluate(this.visProp.bank.pointer.enabled) && (delta !== 0) && evt.key == null) { 1609 speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.bank.pointer.speed); 1610 bank += delta * speed; 1611 1612 // prevent the pointer wheel from scrolling the page 1613 evt.preventDefault(); 1614 } 1615 1616 // Project the calculated bank value to a usable value in the interval [smin,smax] 1617 if (Type.evaluate(this.visProp.bank.continuous)) { 1618 // in continuous mode, wrap value around slider range 1619 bank = Mat.wrap(bank, smin, smax); 1620 } else { 1621 // in non-continuous mode, clamp value to slider range 1622 bank = Mat.clamp(bank, smin, smax); 1623 } 1624 1625 this.bank_slide.setValue(bank); 1626 this.board.update(); 1627 return this; 1628 }, 1629 1630 _trackballHandler: function (evt) { 1631 var pos = this.board.getMousePosition(evt), 1632 x, y, center; 1633 1634 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); 1635 x = pos[0] - center.scrCoords[1]; 1636 y = pos[1] - center.scrCoords[2]; 1637 this._trackball = { 1638 dx: evt.movementX, 1639 dy: -evt.movementY, 1640 x: x, 1641 y: -y 1642 }; 1643 this.board.update(); 1644 return this; 1645 }, 1646 1647 pointerDownHandler: function (evt) { 1648 var neededButton, neededKey, target; 1649 1650 this._hasMoveAz = false; 1651 this._hasMoveEl = false; 1652 this._hasMoveBank = false; 1653 this._hasMoveTrackball = false; 1654 1655 if (this.board.mode !== this.board.BOARD_MODE_NONE) { 1656 return; 1657 } 1658 1659 if (Type.evaluate(this.visProp.trackball.enabled)) { 1660 neededButton = Type.evaluate(this.visProp.trackball.button); 1661 neededKey = Type.evaluate(this.visProp.trackball.key); 1662 1663 // Move events for virtual trackball 1664 if ( 1665 (neededButton === -1 || neededButton === evt.button) && 1666 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1667 ) { 1668 // If outside is true then the event listener is bound to the document, otherwise to the div 1669 target = (Type.evaluate(this.visProp.trackball.outside)) ? document : this.board.containerObj; 1670 Env.addEvent(target, 'pointermove', this._trackballHandler, this); 1671 this._hasMoveTrackball = true; 1672 } 1673 } else { 1674 if (Type.evaluate(this.visProp.az.pointer.enabled)) { 1675 neededButton = Type.evaluate(this.visProp.az.pointer.button); 1676 neededKey = Type.evaluate(this.visProp.az.pointer.key); 1677 1678 // Move events for azimuth 1679 if ( 1680 (neededButton === -1 || neededButton === evt.button) && 1681 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1682 ) { 1683 // If outside is true then the event listener is bound to the document, otherwise to the div 1684 target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj; 1685 Env.addEvent(target, 'pointermove', this._azEventHandler, this); 1686 this._hasMoveAz = true; 1687 } 1688 } 1689 1690 if (Type.evaluate(this.visProp.el.pointer.enabled)) { 1691 neededButton = Type.evaluate(this.visProp.el.pointer.button); 1692 neededKey = Type.evaluate(this.visProp.el.pointer.key); 1693 1694 // Events for elevation 1695 if ( 1696 (neededButton === -1 || neededButton === evt.button) && 1697 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1698 ) { 1699 // If outside is true then the event listener is bound to the document, otherwise to the div 1700 target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj; 1701 Env.addEvent(target, 'pointermove', this._elEventHandler, this); 1702 this._hasMoveEl = true; 1703 } 1704 } 1705 1706 if (Type.evaluate(this.visProp.bank.pointer.enabled)) { 1707 neededButton = Type.evaluate(this.visProp.bank.pointer.button); 1708 neededKey = Type.evaluate(this.visProp.bank.pointer.key); 1709 1710 // Events for bank 1711 if ( 1712 (neededButton === -1 || neededButton === evt.button) && 1713 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1714 ) { 1715 // If `outside` is true, we bind the event listener to 1716 // the document. otherwise, we bind it to the div. we 1717 // register the event listener as active so it can 1718 // prevent the pointer wheel from scrolling the page 1719 target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj; 1720 Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false }); 1721 this._hasMoveBank = true; 1722 } 1723 } 1724 } 1725 Env.addEvent(document, 'pointerup', this.pointerUpHandler, this); 1726 }, 1727 1728 pointerUpHandler: function (evt) { 1729 var target; 1730 if (this._hasMoveAz) { 1731 target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj; 1732 Env.removeEvent(target, 'pointermove', this._azEventHandler, this); 1733 this._hasMoveAz = false; 1734 } 1735 if (this._hasMoveEl) { 1736 target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj; 1737 Env.removeEvent(target, 'pointermove', this._elEventHandler, this); 1738 this._hasMoveEl = false; 1739 } 1740 if (this._hasMoveBank) { 1741 target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj; 1742 Env.removeEvent(target, 'wheel', this._bankEventHandler, this); 1743 this._hasMoveBank = false; 1744 } 1745 if (this._hasMoveTrackball) { 1746 target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj; 1747 Env.removeEvent(target, 'pointermove', this._trackballHandler, this); 1748 this._hasMoveTrackball = false; 1749 } 1750 Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this); 1751 } 1752 }); 1753 1754 /** 1755 * @class This element creates a 3D view. 1756 * @pseudo 1757 * @description A View3D element provides the container and the methods to create and display 3D elements. 1758 * It is contained in a JSXGraph board. 1759 * @name View3D 1760 * @augments JXG.View3D 1761 * @constructor 1762 * @type Object 1763 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1764 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 1765 * dim is an array of the form [w, h]. 1766 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 1767 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 1768 * [x,y] and side lengths [w, h] of the board. 1769 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 1770 * which determines the coordinate ranges of the 3D cube. 1771 * 1772 * @example 1773 * var bound = [-5, 5]; 1774 * var view = board.create('view3d', 1775 * [[-6, -3], 1776 * [8, 8], 1777 * [bound, bound, bound]], 1778 * { 1779 * // Main axes 1780 * axesPosition: 'center', 1781 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 1782 * 1783 * // Planes 1784 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 1785 * yPlaneFront: { visible: true, fillColor: 'blue'}, 1786 * 1787 * // Axes on planes 1788 * xPlaneRearYAxis: {strokeColor: 'red'}, 1789 * xPlaneRearZAxis: {strokeColor: 'red'}, 1790 * 1791 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 1792 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 1793 * 1794 * zPlaneFrontXAxis: {visible: false}, 1795 * zPlaneFrontYAxis: {visible: false} 1796 * }); 1797 * 1798 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 1799 * <script type="text/javascript"> 1800 * (function() { 1801 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 1802 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 1803 * var bound = [-5, 5]; 1804 * var view = board.create('view3d', 1805 * [[-6, -3], [8, 8], 1806 * [bound, bound, bound]], 1807 * { 1808 * // Main axes 1809 * axesPosition: 'center', 1810 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 1811 * // Planes 1812 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 1813 * yPlaneFront: { visible: true, fillColor: 'blue'}, 1814 * // Axes on planes 1815 * xPlaneRearYAxis: {strokeColor: 'red'}, 1816 * xPlaneRearZAxis: {strokeColor: 'red'}, 1817 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 1818 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 1819 * zPlaneFrontXAxis: {visible: false}, 1820 * zPlaneFrontYAxis: {visible: false} 1821 * }); 1822 * })(); 1823 * 1824 * </script><pre> 1825 * 1826 */ 1827 JXG.createView3D = function (board, parents, attributes) { 1828 var view, attr, attr_az, attr_el, attr_bank, 1829 x, y, w, h, 1830 coords = parents[0], // llft corner 1831 size = parents[1]; // [w, h] 1832 1833 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 1834 view = new JXG.View3D(board, parents, attr); 1835 view.defaultAxes = view.create('axes3d', parents, attributes); 1836 1837 x = coords[0]; 1838 y = coords[1]; 1839 w = size[0]; 1840 h = size[1]; 1841 1842 attr_az = Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider'); 1843 attr_az.name = 'az'; 1844 1845 attr_el = Type.copyAttributes(attributes, board.options, 'view3d', 'el', 'slider'); 1846 attr_el.name = 'el'; 1847 1848 attr_bank = Type.copyAttributes(attributes, board.options, 'view3d', 'bank', 'slider'); 1849 attr_bank.name = 'bank'; 1850 1851 /** 1852 * Slider to adapt azimuth angle 1853 * @name JXG.View3D#az_slide 1854 * @type {Slider} 1855 */ 1856 view.az_slide = board.create( 1857 'slider', 1858 [ 1859 [x - 1, y - 2], 1860 [x + w + 1, y - 2], 1861 [ 1862 Type.evaluate(attr_az.min), 1863 Type.evaluate(attr_az.start), 1864 Type.evaluate(attr_az.max) 1865 ] 1866 ], 1867 attr_az 1868 ); 1869 // view.az_slide.inherits.push(view); 1870 view.inherits.push(view.az_slide); 1871 1872 /** 1873 * Slider to adapt elevation angle 1874 * 1875 * @name JXG.View3D#el_slide 1876 * @type {Slider} 1877 */ 1878 view.el_slide = board.create( 1879 'slider', 1880 [ 1881 [x - 1, y], 1882 [x - 1, y + h], 1883 [ 1884 Type.evaluate(attr_el.min), 1885 Type.evaluate(attr_el.start), 1886 Type.evaluate(attr_el.max)] 1887 ], 1888 attr_el 1889 ); 1890 view.inherits.push(view.el_slide); 1891 1892 /** 1893 * Slider to adjust bank angle 1894 * 1895 * @name JXG.View3D#bank_slide 1896 * @type {Slider} 1897 */ 1898 view.bank_slide = board.create( 1899 'slider', 1900 [ 1901 [x - 1, y + h + 2], 1902 [x + w + 1, y + h + 2], 1903 [ 1904 Type.evaluate(attr_bank.min), 1905 Type.evaluate(attr_bank.start), 1906 Type.evaluate(attr_bank.max) 1907 ] 1908 ], 1909 attr_bank 1910 ); 1911 view.inherits.push(view.bank_slide); 1912 1913 view.board.highlightInfobox = function (x, y, el) { 1914 var d, i, c3d, foot, 1915 pre = '<span style="color:black; font-size:200%">\u21C4 </span>', 1916 brd = el.board, 1917 arr, infobox, 1918 p = null; 1919 1920 if (view.isVerticalDrag()) { 1921 pre = '<span style="color:black; font-size:200%">\u21C5 </span>'; 1922 } 1923 // Search 3D parent 1924 for (i = 0; i < el.parents.length; i++) { 1925 p = brd.objects[el.parents[i]]; 1926 if (p.is3D) { 1927 break; 1928 } 1929 } 1930 if (p) { 1931 foot = [1, 0, 0, p.coords[3]]; 1932 view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4); 1933 1934 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 1935 if (!view.isInCube(c3d)) { 1936 view.board.highlightCustomInfobox('', p); 1937 return; 1938 } 1939 d = Type.evaluate(p.visProp.infoboxdigits); 1940 infobox = view.board.infobox; 1941 if (d === 'auto') { 1942 if (infobox.useLocale()) { 1943 arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')']; 1944 } else { 1945 arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')']; 1946 } 1947 1948 } else { 1949 if (infobox.useLocale()) { 1950 arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')']; 1951 } else { 1952 arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')']; 1953 } 1954 } 1955 view.board.highlightCustomInfobox(arr.join(''), p); 1956 } else { 1957 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 1958 } 1959 }; 1960 1961 1962 // Hack needed to enable addEvent for view3D: 1963 view.BOARD_MODE_NONE = 0x0000; 1964 1965 // Add events for the keyboard navigation 1966 Env.addEvent(board.containerObj, 'keydown', function (event) { 1967 var neededKey, 1968 catchEvt = false; 1969 1970 if (Type.evaluate(view.visProp.el.keyboard.enabled) && 1971 (event.key === 'ArrowUp' || event.key === 'ArrowDown') 1972 ) { 1973 neededKey = Type.evaluate(view.visProp.el.keyboard.key); 1974 if (neededKey === 'none' || 1975 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 1976 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 1977 view._elEventHandler(event); 1978 catchEvt = true; 1979 } 1980 1981 } 1982 if (Type.evaluate(view.visProp.el.keyboard.enabled) && 1983 (event.key === 'ArrowLeft' || event.key === 'ArrowRight') 1984 ) { 1985 neededKey = Type.evaluate(view.visProp.az.keyboard.key); 1986 if (neededKey === 'none' || 1987 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 1988 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey) 1989 ) { 1990 view._azEventHandler(event); 1991 catchEvt = true; 1992 } 1993 } 1994 if (Type.evaluate(view.visProp.bank.keyboard.enabled) && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) { 1995 neededKey = Type.evaluate(view.visProp.bank.keyboard.key); 1996 if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 1997 view._bankEventHandler(event); 1998 catchEvt = true; 1999 } 2000 } 2001 if (event.key === 'PageUp') { 2002 view.nextView(); 2003 catchEvt = true; 2004 } else if (event.key === 'PageDown') { 2005 view.previousView(); 2006 catchEvt = true; 2007 } 2008 2009 if (catchEvt) { 2010 // We stop event handling only in the case if the keypress could be 2011 // used for the 3D view. If this is not done, input fields et al 2012 // can not be used any more. 2013 event.preventDefault(); 2014 } 2015 }, view); 2016 2017 // Add events for the pointer navigation 2018 Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view); 2019 2020 // Initialize view rotation matrix 2021 view.getAnglesFromSliders(); 2022 view.matrix3DRot = view.getRotationFromAngles(); 2023 2024 // override angle slider bounds when trackball navigation is enabled 2025 view.updateAngleSliderBounds(); 2026 2027 view.board.update(); 2028 2029 return view; 2030 }; 2031 2032 JXG.registerElement("view3d", JXG.createView3D); 2033 2034 export default JXG.View3D;