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