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