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 * @default null 193 */ 194 this.r = null; 195 196 /** 197 * The distance from the camera to the screen. Computed automatically from 198 * the `fov` property. 199 * @type Number 200 */ 201 this.focalDist = -1; 202 203 /** 204 * Type of projection. Is set in in update(). 205 * @type String 206 */ 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 236 JXG.View3D.prototype = new GeometryElement(); 237 Type.copyMethodMap(JXG.View3D, { 238 // TODO 239 }); 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 * Get distance from view box center to camera. 707 * In other words, the radius of the sphere where the camera sits. 708 * Distinguishes between projection tpye 'central' and 'parallel'. 709 * Uses the value of attribute 'r'. 710 * 711 * @returns Number 712 * @private 713 * @see View3D#r 714 */ 715 getCameraDistance: function() { 716 var rs, r, rr, diam; 717 718 rr = Type.evaluate(this.r); 719 if (rr === null || rr === 0) { 720 // Use attribute r 721 rs = this.evalVisProp('r'); 722 } else { 723 // Use previously set value in this.r 724 rs = rr; 725 } 726 727 if (rs === 'auto') { 728 r = 1.01; 729 } else { 730 r = (this.projectionType === 'central') ? rs : (1 / rs); 731 } 732 733 if (this.projectionType === 'central') { 734 diam = Mat.hypot( 735 this.bbox3D[0][0] - this.bbox3D[0][1], 736 this.bbox3D[1][0] - this.bbox3D[1][1], 737 this.bbox3D[2][0] - this.bbox3D[2][1] 738 ); 739 r = diam * r; 740 } 741 742 return r; 743 }, 744 745 /** 746 * @private 747 * @returns {Array} 748 */ 749 _updateCentralProjection: function () { 750 var zf = 20, // near clip plane 751 zn = 8, // far clip plane 752 753 // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html 754 // bbox3D is always at the world origin, i.e. T_obj is the unit matrix. 755 // All vectors contain affine coordinates and have length 3 756 // The matrices are of size 4x4. 757 r, A; 758 759 // Set distance from view box center to camera 760 r = this.getCameraDistance(); 761 762 // Compute camera transformation 763 // this.boxToCam = this.matrix3DRot.map((row) => row.slice()); 764 this.boxToCam = this.matrix3DRot.map(function (row) { return row.slice(); }); 765 this.boxToCam[3][0] = -r; 766 767 // compute focal distance and clip space transformation 768 this.focalDist = 1 / Math.tan(0.5 * this.evalVisProp('fov')); 769 A = [ 770 [0, 0, 0, -1], 771 [0, this.focalDist, 0, 0], 772 [0, 0, this.focalDist, 0], 773 [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)] 774 ]; 775 776 return Mat.matMatMult(A, this.boxToCam); 777 }, 778 779 // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles. 780 // Called in board.updateElements() 781 update: function () { 782 var r, stretch, 783 mat2D, objectToClip, size, 784 dx, dy; 785 // objectsList; 786 787 if ( 788 !Type.exists(this.el_slide) || 789 !Type.exists(this.az_slide) || 790 !Type.exists(this.bank_slide) || 791 !this.needsUpdate 792 ) { 793 this.needsUpdate = false; 794 return this; 795 } 796 797 mat2D = [ 798 [1, 0, 0], 799 [0, 1, 0], 800 [0, 0, 1] 801 ]; 802 803 this.projectionType = this.evalVisProp('projection').toLowerCase(); 804 805 // override angle slider bounds when trackball navigation is enabled 806 if (this.trackballEnabled !== this.evalVisProp('trackball.enabled')) { 807 this.updateAngleSliderBounds(); 808 } 809 810 if (this._hasMoveTrackball) { 811 // The trackball has been moved since the last update, so we do 812 // trackball navigation. When the trackball is enabled, a drag 813 // event is interpreted as a trackball movement unless it's 814 // caught by something else, like point dragging. When the 815 // trackball is disabled, the trackball movement flag should 816 // never be set 817 this.matrix3DRot = this.updateProjectionTrackball(); 818 this.setAnglesFromRotation(); 819 } else if (this.anglesHaveMoved()) { 820 // The trackball hasn't been moved since the last up date, but 821 // the Tait-Bryan angles have been, so we do angle navigation 822 this.getAnglesFromSliders(); 823 this.matrix3DRot = this.getRotationFromAngles(); 824 } 825 826 /** 827 * The translation that moves the center of the view box to the origin. 828 */ 829 this.shift = [ 830 [1, 0, 0, 0], 831 [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0], 832 [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0], 833 [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1] 834 ]; 835 836 switch (this.projectionType) { 837 case 'central': // Central projection 838 839 // Add a final transformation to scale and shift the projection 840 // on the board, usually called viewport. 841 size = 2 * 0.4; 842 mat2D[1][1] = this.size[0] / size; // w / d_x 843 mat2D[2][2] = this.size[1] / size; // h / d_y 844 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x 845 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y 846 // The transformations this.matrix3D and mat2D can not be combined at this point, 847 // since the projected vectors have to be normalized in between in project3DTo2D 848 this.viewPortTransform = mat2D; 849 objectToClip = this._updateCentralProjection(); 850 // this.matrix3D is a 4x4 matrix 851 this.matrix3D = Mat.matMatMult(objectToClip, this.shift); 852 break; 853 854 case 'parallel': // Parallel projection 855 default: 856 r = this.getCameraDistance(); 857 stretch = [ 858 [1, 0, 0, 0], 859 [0, r, 0, 0], 860 [0, 0, r, 0], 861 [0, 0, 0, r] 862 ]; 863 864 // Add a final transformation to scale and shift the projection 865 // on the board, usually called viewport. 866 dx = this.bbox3D[0][1] - this.bbox3D[0][0]; 867 dy = this.bbox3D[1][1] - this.bbox3D[1][0]; 868 mat2D[1][1] = this.size[0] / dx; // w / d_x 869 mat2D[2][2] = this.size[1] / dy; // h / d_y 870 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x 871 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y 872 873 // Combine all transformations, this.matrix3D is a 3x4 matrix 874 this.matrix3D = Mat.matMatMult( 875 mat2D, 876 Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3) 877 ); 878 } 879 880 // Used for zIndex in dept ordering in subsequent update methods of the 881 // 3D elements and in view3d.updateRenderer 882 this.matrix3DRotShift = Mat.matMatMult(this.matrix3DRot, this.shift); 883 884 return this; 885 }, 886 887 /** 888 * Compares 3D elements according to their z-Index. 889 * @param {JXG.GeometryElement3D} a 890 * @param {JXG.GeometryElement3D} b 891 * @returns Number 892 */ 893 compareDepth: function (a, b) { 894 // return a.zIndex - b.zIndex; 895 // if (a.type !== Const.OBJECT_TYPE_PLANE3D && b.type !== Const.OBJECT_TYPE_PLANE3D) { 896 // return a.zIndex - b.zIndex; 897 // } else if (a.type === Const.OBJECT_TYPE_PLANE3D) { 898 // let bHesse = Mat.innerProduct(a.point.coords, a.normal, 4); 899 // let po = Mat.innerProduct(b.coords, a.normal, 4); 900 // let pos = Mat.innerProduct(this.boxToCam[3], a.normal, 4); 901 // console.log(this.boxToCam[3]) 902 // return pos - po; 903 // } else if (b.type === Const.OBJECT_TYPE_PLANE3D) { 904 // let bHesse = Mat.innerProduct(b.point.coords, b.normal, 4); 905 // let po = Mat.innerProduct(a.coords, a.normal, 4); 906 // let pos = Mat.innerProduct(this.boxToCam[3], b.normal, 4); 907 // console.log('b', pos, po, bHesse) 908 // return -pos; 909 // } 910 return a.zIndex - b.zIndex; 911 }, 912 913 updateZIndices: function() { 914 var id, el; 915 for (id in this.objects) { 916 if (this.objects.hasOwnProperty(id)) { 917 el = this.objects[id]; 918 // Update zIndex of less frequent objects line3d and polygon3d 919 // The other elements (point3d, face3d) do this in their update method. 920 if (( 921 el.type === Const.OBJECT_TYPE_LINE3D || 922 el.type === Const.OBJECT_TYPE_POLYGON3D 923 ) && 924 Type.exists(el.element2D) && 925 el.element2D.evalVisProp('visible') 926 ) { 927 el.updateZIndex(); 928 } 929 } 930 } 931 }, 932 933 updateShaders: function() { 934 var id, el, v; 935 for (id in this.objects) { 936 if (this.objects.hasOwnProperty(id)) { 937 el = this.objects[id]; 938 939 if (el.visPropCalc.visible && Type.exists(el.shader)) { 940 if (this.board._change3DView && el.evalVisProp('shader.fixed')) { 941 // In case, 3D view is rotated and the shader is fixed 942 // we can avoid the call of shader() 943 v = el.zIndex; 944 } else { 945 v = el.shader(); 946 } 947 if (v < this.zIndexMin) { 948 this.zIndexMin = v; 949 } else if (v > this.zIndexMax) { 950 this.zIndexMax = v; 951 } 952 } 953 } 954 } 955 }, 956 957 updateDepthOrdering: function () { 958 var id, el, 959 i, j, l, layers, lay; 960 961 // Collect elements for depth ordering layer-wise 962 layers = this.evalVisProp('depthorder.layers'); 963 for (i = 0; i < layers.length; i++) { 964 this.depthOrdered[layers[i]] = []; 965 } 966 967 for (id in this.objects) { 968 if (this.objects.hasOwnProperty(id)) { 969 el = this.objects[id]; 970 if ((el.type === Const.OBJECT_TYPE_FACE3D || 971 el.type === Const.OBJECT_TYPE_LINE3D || 972 // el.type === Const.OBJECT_TYPE_PLANE3D || 973 el.type === Const.OBJECT_TYPE_POINT3D || 974 el.type === Const.OBJECT_TYPE_POLYGON3D 975 ) && 976 Type.exists(el.element2D) && 977 el.element2D.visPropCalc.visible 978 // el.element2D.evalVisProp('visible') 979 ) { 980 lay = el.element2D.evalVisProp('layer'); 981 if (layers.indexOf(lay) >= 0) { 982 this.depthOrdered[lay].push(el); 983 } 984 } 985 } 986 } 987 988 if (this.board.renderer && this.board.renderer.type === 'svg') { 989 for (i = 0; i < layers.length; i++) { 990 lay = layers[i]; 991 this.depthOrdered[lay].sort(this.compareDepth.bind(this)); 992 // DEBUG 993 // if (this.depthOrdered[lay].length > 0) { 994 // for (let k = 0; k < this.depthOrdered[lay].length; k++) { 995 // let o = this.depthOrdered[lay][k] 996 // console.log(o.visProp.fillcolor, o.zIndex) 997 // } 998 // } 999 l = this.depthOrdered[lay]; 1000 for (j = 0; j < l.length; j++) { 1001 this.board.renderer.setLayer(l[j].element2D, lay); 1002 } 1003 // this.depthOrdered[lay].forEach((el) => this.board.renderer.setLayer(el.element2D, lay)); 1004 // Attention: forEach prevents deleting an element 1005 } 1006 } 1007 1008 return this; 1009 }, 1010 1011 updateRenderer: function () { 1012 if (!this.needsUpdate) { 1013 return this; 1014 } 1015 1016 // console.time('update') 1017 // Handle depth ordering 1018 this.depthOrdered = {}; 1019 1020 if (this.shift !== undefined && this.evalVisProp('depthorder.enabled')) { 1021 // Update the zIndices of certain element types. 1022 // We do it here in updateRenderer, because the elements' positions 1023 // are meanwhile updated. 1024 this.updateZIndices(); 1025 1026 this.updateShaders(); 1027 1028 if (this.board.renderer && this.board.renderer.type === 'svg') { 1029 // For SVG we update the DOM order here. 1030 // In canvas we sort the elements in board.updateRendererCanvas 1031 this.updateDepthOrdering(); 1032 } 1033 } 1034 // console.timeEnd('update') 1035 1036 this.needsUpdate = false; 1037 return this; 1038 }, 1039 1040 removeObject: function (object, saveMethod) { 1041 var i, el, le, o, fst, face; 1042 1043 // this.board.removeObject(object, saveMethod); 1044 if (Type.isArray(object)) { 1045 for (i = 0; i < object.length; i++) { 1046 this.removeObject(object[i]); 1047 } 1048 return this; 1049 } 1050 1051 object = this.select(object); 1052 1053 // // If the object which is about to be removed unknown or a string, do nothing. 1054 // // it is a string if a string was given and could not be resolved to an element. 1055 if (!Type.exists(object) || Type.isString(object)) { 1056 return this; 1057 } 1058 1059 try { 1060 // Remove all children. 1061 for (el in object.childElements) { 1062 if (object.childElements.hasOwnProperty(el)) { 1063 this.removeObject(object.childElements[el]); 1064 } 1065 } 1066 if (object.type === Const.OBJECT_TYPE_POLYHEDRON3D) { 1067 // Special treatment for polyhedron3d. 1068 // With this we can avoid the time consuming addChild() calls. 1069 le = object.faces.length; 1070 if (le > 0) { 1071 fst = object.faces[0]._pos; 1072 fst = (object.faces[0].element2D._pos < fst) ? object.faces[0].element2D._pos : fst; 1073 } 1074 for (i = 0; i < le; i++) { 1075 face = object.faces[i]; 1076 delete this.objects[face.id]; 1077 1078 // this.board.removeObject(face.element2D, saveMethod); 1079 delete this.board.objects[face.element2D.id]; 1080 delete this.board.elementsByName[face.element2D.name]; 1081 face.element2D.remove(); 1082 this.board.objectsList.splice(face.element2D._pos, 1); 1083 1084 delete this.board.objects[face.id]; 1085 delete this.board.elementsByName[face.name]; 1086 face.remove(); 1087 this.board.objectsList.splice(face._pos, 1); 1088 } 1089 le = this.board.objectsList.length; 1090 // Reindex the positions 1091 for (i = fst; i < this.board.objectsList.length; i++) { 1092 o = this.board.objectsList[i]; 1093 if (o._pos > -1) { o._pos = i; } 1094 } 1095 object.faces = []; 1096 } 1097 1098 delete this.objects[object.id]; 1099 } catch (e) { 1100 JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e); 1101 } 1102 1103 // this.update(); 1104 1105 this.board.removeObject(object, saveMethod); 1106 1107 return this; 1108 }, 1109 1110 /** 1111 * Map world coordinates to focal coordinates. These coordinate systems 1112 * are explained in the {@link JXG.View3D#boxToCam} matrix 1113 * documentation. 1114 * 1115 * @param {Array} pWorld A world space point, in homogeneous coordinates. 1116 * @param {Boolean} [homog=true] Whether to return homogeneous coordinates. 1117 * If false, projects down to ordinary coordinates. 1118 */ 1119 worldToFocal: function (pWorld, homog = true) { 1120 var k, 1121 pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld)); 1122 1123 pView[3] -= pView[0] * this.focalDist; 1124 if (homog) { 1125 return pView; 1126 } else { 1127 for (k = 1; k < 4; k++) { 1128 pView[k] /= pView[0]; 1129 } 1130 return pView.slice(1, 4); 1131 } 1132 }, 1133 1134 /** 1135 * Project 3D coordinates to 2D board coordinates 1136 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 1137 * 1138 * @param {Number|Array} x 1139 * @param {Number[]} y 1140 * @param {Number[]} z 1141 * @returns {Array} Array of length 3 containing the projection on to the board 1142 * in homogeneous user coordinates. 1143 */ 1144 project3DTo2D: function (x, y, z) { 1145 var vec, w; 1146 if (arguments.length === 3) { 1147 vec = [1, x, y, z]; 1148 } else { 1149 // Argument is an array 1150 if (x.length === 3) { 1151 // vec = [1].concat(x); 1152 vec = x.slice(); 1153 vec.unshift(1); 1154 } else { 1155 vec = x; 1156 } 1157 } 1158 1159 w = Mat.matVecMult(this.matrix3D, vec); 1160 1161 switch (this.projectionType) { 1162 case 'central': 1163 w[1] /= w[0]; 1164 w[2] /= w[0]; 1165 w[3] /= w[0]; 1166 w[0] /= w[0]; 1167 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3)); 1168 1169 case 'parallel': 1170 default: 1171 return w; 1172 } 1173 }, 1174 1175 /** 1176 * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y. 1177 * Setting R = mat^(-1) gives 1178 * 1/ w0 * (1, x, y, d)^T = R * v2d. 1179 * The first and the last row of this equation allows to determine 1/w0 and h. 1180 * 1181 * @param {Array} mat 1182 * @param {Array} v2d 1183 * @param {Number} d 1184 * @returns Array 1185 * @private 1186 */ 1187 _getW0: function (mat, v2d, d) { 1188 var R = Mat.inverse(mat), 1189 R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2], 1190 R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2], 1191 w, h, det; 1192 1193 det = d * R[0][3] - R[3][3]; 1194 w = (R2 * R[0][3] - R1 * R[3][3]) / det; 1195 h = (R2 - R1 * d) / det; 1196 return [1 / w, h]; 1197 }, 1198 1199 /** 1200 * Project a 2D coordinate to the plane defined by point "foot" 1201 * and the normal vector `normal`. 1202 * 1203 * @param {JXG.Point} point2d 1204 * @param {Array} normal Normal of plane 1205 * @param {Array} foot Foot point of plane 1206 * @returns {Array} of length 4 containing the projected 1207 * point in homogeneous coordinates. 1208 */ 1209 project2DTo3DPlane: function (point2d, normal, foot) { 1210 var mat, rhs, d, le, sol, 1211 f = foot.slice(1) || [0, 0, 0], 1212 n = normal.slice(1), 1213 v2d, w0, res; 1214 1215 le = Mat.norm(n, 3); 1216 d = Mat.innerProduct(f, n, 3) / le; 1217 1218 if (this.projectionType === 'parallel') { 1219 mat = this.matrix3D.slice(0, 3); // Copy each row by reference 1220 mat.push([0, n[0], n[1], n[2]]); 1221 1222 // 2D coordinates of point 1223 rhs = point2d.coords.usrCoords.slice(); 1224 rhs.push(d); 1225 try { 1226 // Prevent singularity in case elevation angle is zero 1227 if (mat[2][3] === 1.0) { 1228 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 1229 } 1230 sol = Mat.Numerics.Gauss(mat, rhs); 1231 } catch (e) { 1232 sol = [0, NaN, NaN, NaN]; 1233 } 1234 } else { 1235 mat = this.matrix3D; 1236 1237 // 2D coordinates of point: 1238 rhs = point2d.coords.usrCoords.slice(); 1239 1240 v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs); 1241 res = this._getW0(mat, v2d, d); 1242 w0 = res[0]; 1243 rhs = [ 1244 v2d[0] * w0, 1245 v2d[1] * w0, 1246 v2d[2] * w0, 1247 res[1] * w0 1248 ]; 1249 try { 1250 // Prevent singularity in case elevation angle is zero 1251 if (mat[2][3] === 1.0) { 1252 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 1253 } 1254 1255 sol = Mat.Numerics.Gauss(mat, rhs); 1256 sol[1] /= sol[0]; 1257 sol[2] /= sol[0]; 1258 sol[3] /= sol[0]; 1259 // sol[3] = d; 1260 sol[0] /= sol[0]; 1261 } catch (err) { 1262 sol = [0, NaN, NaN, NaN]; 1263 } 1264 } 1265 1266 return sol; 1267 }, 1268 1269 /** 1270 * Project a point on the screen to the nearest point, in screen 1271 * distance, on a line segment in 3d space. The inputs and outputs 1272 * are in homogeneous coordinates. 1273 * <p> 1274 * Used in View3d.project2DTo3DVertical() and 1275 * Line3d.projectScreenCoords(). 1276 * 1277 * @param {Array} pScr The screen coordinates of the point to project. 1278 * @param {Array} end0 The world space coordinates of one end of the 1279 * line segment (array of length 4). 1280 * @param {Array} end1 The world space coordinates of the other end of 1281 * the line segment (array of length 4). 1282 * 1283 * @returns {Array} Homogeneous coordinates of the projection 1284 */ 1285 projectScreenToSegment: function (pScr, end0, end1) { 1286 var end0_2d = this.project3DTo2D(end0).slice(1, 3), 1287 end1_2d = this.project3DTo2D(end1).slice(1, 3), 1288 dir_2d = [ 1289 end1_2d[0] - end0_2d[0], 1290 end1_2d[1] - end0_2d[1] 1291 ], 1292 dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d), 1293 diff = [ 1294 pScr[0] - end0_2d[0], 1295 pScr[1] - end0_2d[1] 1296 ], 1297 s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter 1298 mid, mid_2d, mid_diff, m, 1299 1300 t, // view-space affine parameter 1301 t_clamped, // affine parameter clamped to range 1302 t_clamped_co; 1303 1304 if (this.projectionType === 'central') { 1305 mid = [ 1306 1, 1307 0.5 * (end0[1] + end1[1]), 1308 0.5 * (end0[2] + end1[2]), 1309 0.5 * (end0[3] + end1[3]) 1310 ]; 1311 mid_2d = this.project3DTo2D(mid).slice(1, 3); 1312 mid_diff = [ 1313 mid_2d[0] - end0_2d[0], 1314 mid_2d[1] - end0_2d[1] 1315 ]; 1316 m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq; 1317 1318 // the view-space affine parameter s is related to the 1319 // screen-space affine parameter t by a Möbius transformation, 1320 // which is determined by the following relations: 1321 // 1322 // s | t 1323 // ----- 1324 // 0 | 0 1325 // m | 1/2 1326 // 1 | 1 1327 // 1328 t = (1 - m) * s / ((1 - 2 * m) * s + m); 1329 } else { 1330 t = s; 1331 } 1332 1333 t_clamped = Math.min(Math.max(t, 0), 1); 1334 t_clamped_co = 1 - t_clamped; 1335 return [ 1336 1, 1337 t_clamped_co * end0[1] + t_clamped * end1[1], 1338 t_clamped_co * end0[2] + t_clamped * end1[2], 1339 t_clamped_co * end0[3] + t_clamped * end1[3] 1340 ]; 1341 }, 1342 1343 /** 1344 * Project a 2D coordinate to a new 3D position by keeping 1345 * the 3D x, y coordinates and changing only the z coordinate. 1346 * All horizontal moves of the 2D point are ignored. 1347 * 1348 * @param {JXG.Point} point2d 1349 * @param {Array} base_c3d 1350 * @returns {Array} of length 4 containing the projected 1351 * point in homogeneous coordinates. 1352 */ 1353 project2DTo3DVertical: function (point2d, base_c3d) { 1354 var pScr = point2d.coords.usrCoords.slice(1, 3), 1355 end0 = [1, base_c3d[1], base_c3d[2], this.bbox3D[2][0]], 1356 end1 = [1, base_c3d[1], base_c3d[2], this.bbox3D[2][1]]; 1357 1358 return this.projectScreenToSegment(pScr, end0, end1); 1359 }, 1360 1361 /** 1362 * Limit 3D coordinates to the bounding cube. 1363 * 1364 * @param {Array} c3d 3D coordinates [x,y,z] 1365 * @returns Array [Array, Boolean] containing [coords, corrected]. coords contains the updated 3D coordinates, 1366 * correct is true if the coords have been changed. 1367 */ 1368 project3DToCube: function (c3d) { 1369 var cube = this.bbox3D, 1370 isOut = false; 1371 1372 if (c3d[1] < cube[0][0]) { 1373 c3d[1] = cube[0][0]; 1374 isOut = true; 1375 } 1376 if (c3d[1] > cube[0][1]) { 1377 c3d[1] = cube[0][1]; 1378 isOut = true; 1379 } 1380 if (c3d[2] < cube[1][0]) { 1381 c3d[2] = cube[1][0]; 1382 isOut = true; 1383 } 1384 if (c3d[2] > cube[1][1]) { 1385 c3d[2] = cube[1][1]; 1386 isOut = true; 1387 } 1388 if (c3d[3] <= cube[2][0]) { 1389 c3d[3] = cube[2][0]; 1390 isOut = true; 1391 } 1392 if (c3d[3] >= cube[2][1]) { 1393 c3d[3] = cube[2][1]; 1394 isOut = true; 1395 } 1396 1397 return [c3d, isOut]; 1398 }, 1399 1400 /** 1401 * Intersect a ray with the bounding cube of the 3D view. 1402 * @param {Array} p 3D coordinates [w,x,y,z] 1403 * @param {Array} dir 3D direction vector of the line (array of length 3 or 4) 1404 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 1405 * @returns Affine ratio of the intersection of the line with the cube. 1406 */ 1407 intersectionLineCube: function (p, dir, r) { 1408 var r_n, i, r0, r1, d; 1409 1410 d = (dir.length === 3) ? dir : dir.slice(1); 1411 1412 r_n = r; 1413 for (i = 0; i < 3; i++) { 1414 if (d[i] !== 0) { 1415 r0 = (this.bbox3D[i][0] - p[i + 1]) / d[i]; 1416 r1 = (this.bbox3D[i][1] - p[i + 1]) / d[i]; 1417 if (r < 0) { 1418 r_n = Math.max(r_n, Math.min(r0, r1)); 1419 } else { 1420 r_n = Math.min(r_n, Math.max(r0, r1)); 1421 } 1422 } 1423 } 1424 return r_n; 1425 }, 1426 1427 /** 1428 * Test if coordinates are inside of the bounding cube. 1429 * @param {array} p 3D coordinates [[w],x,y,z] of a point. 1430 * @returns Boolean 1431 */ 1432 isInCube: function (p, polyhedron) { 1433 var q; 1434 if (p.length === 4) { 1435 if (p[0] === 0) { 1436 return false; 1437 } 1438 q = p.slice(1); 1439 } 1440 return ( 1441 q[0] > this.bbox3D[0][0] - Mat.eps && 1442 q[0] < this.bbox3D[0][1] + Mat.eps && 1443 q[1] > this.bbox3D[1][0] - Mat.eps && 1444 q[1] < this.bbox3D[1][1] + Mat.eps && 1445 q[2] > this.bbox3D[2][0] - Mat.eps && 1446 q[2] < this.bbox3D[2][1] + Mat.eps 1447 ); 1448 }, 1449 1450 /** 1451 * 1452 * @param {JXG.Plane3D} plane1 1453 * @param {JXG.Plane3D} plane2 1454 * @param {Number} d Right hand side of Hesse normal for plane2 (it can be adjusted) 1455 * @returns {Array} of length 2 containing the coordinates of the defining points of 1456 * of the intersection segment, or false if there is no intersection 1457 */ 1458 intersectionPlanePlane: function (plane1, plane2, d) { 1459 var ret = [false, false], 1460 p, q, r, w, 1461 dir; 1462 1463 d = d || plane2.d; 1464 1465 // Get one point of the intersection of the two planes 1466 w = Mat.crossProduct(plane1.normal.slice(1), plane2.normal.slice(1)); 1467 w.unshift(0); 1468 1469 p = Mat.Geometry.meet3Planes( 1470 plane1.normal, 1471 plane1.d, 1472 plane2.normal, 1473 d, 1474 w, 1475 0 1476 ); 1477 1478 // Get the direction of the intersecting line of the two planes 1479 dir = Mat.Geometry.meetPlanePlane( 1480 plane1.vec1, 1481 plane1.vec2, 1482 plane2.vec1, 1483 plane2.vec2 1484 ); 1485 1486 // Get the bounding points of the intersecting segment 1487 r = this.intersectionLineCube(p, dir, Infinity); 1488 q = Mat.axpy(r, dir, p); 1489 if (this.isInCube(q)) { 1490 ret[0] = q; 1491 } 1492 r = this.intersectionLineCube(p, dir, -Infinity); 1493 q = Mat.axpy(r, dir, p); 1494 if (this.isInCube(q)) { 1495 ret[1] = q; 1496 } 1497 1498 return ret; 1499 }, 1500 1501 intersectionPlaneFace: function (plane, face) { 1502 var ret = [], 1503 j, t, 1504 p, crds, 1505 p1, p2, c, 1506 f, le, x1, y1, x2, y2, 1507 dir, vec, w, 1508 mat = [], b = [], sol; 1509 1510 w = Mat.crossProduct(plane.normal.slice(1), face.normal.slice(1)); 1511 w.unshift(0); 1512 1513 // Get one point of the intersection of the two planes 1514 p = Geometry.meet3Planes( 1515 plane.normal, 1516 plane.d, 1517 face.normal, 1518 face.d, 1519 w, 1520 0 1521 ); 1522 1523 // Get the direction the intersecting line of the two planes 1524 dir = Geometry.meetPlanePlane( 1525 plane.vec1, 1526 plane.vec2, 1527 face.vec1, 1528 face.vec2 1529 ); 1530 1531 f = face.polyhedron.faces[face.faceNumber]; 1532 crds = face.polyhedron.coords; 1533 le = f.length; 1534 for (j = 1; j <= le; j++) { 1535 p1 = crds[f[j - 1]]; 1536 p2 = crds[f[j % le]]; 1537 vec = [0, p2[1] - p1[1], p2[2] - p1[2], p2[3] - p1[3]]; 1538 1539 x1 = Math.random(); 1540 y1 = Math.random(); 1541 x2 = Math.random(); 1542 y2 = Math.random(); 1543 mat = [ 1544 [x1 * dir[1] + y1 * dir[3], x1 * (-vec[1]) + y1 * (-vec[3])], 1545 [x2 * dir[2] + y2 * dir[3], x2 * (-vec[2]) + y2 * (-vec[3])] 1546 ]; 1547 b = [ 1548 x1 * (p1[1] - p[1]) + y1 * (p1[3] - p[3]), 1549 x2 * (p1[2] - p[2]) + y2 * (p1[3] - p[3]) 1550 ]; 1551 1552 sol = Numerics.Gauss(mat, b); 1553 t = sol[1]; 1554 if (t > -Mat.eps && t < 1 + Mat.eps) { 1555 c = [1, p1[1] + t * vec[1], p1[2] + t * vec[2], p1[3] + t * vec[3]]; 1556 ret.push(c); 1557 } 1558 } 1559 1560 return ret; 1561 }, 1562 1563 // TODO: 1564 // - handle non-closed polyhedra 1565 // - handle intersections in vertex, edge, plane 1566 intersectionPlanePolyhedron: function(plane, phdr) { 1567 var i, j, seg, 1568 p, first, pos, pos_akt, 1569 eps = 1e-12, 1570 points = [], 1571 x = [], 1572 y = [], 1573 z = []; 1574 1575 for (i = 0; i < phdr.numberFaces; i++) { 1576 if (phdr.def.faces[i].length < 3) { 1577 // We skip intersection with points or lines 1578 continue; 1579 } 1580 1581 // seg will be an array consisting of two points 1582 // that span the intersecting segment of the plane 1583 // and the face. 1584 seg = this.intersectionPlaneFace(plane, phdr.faces[i]); 1585 1586 // Plane intersects the face in less than 2 points 1587 if (seg.length < 2) { 1588 continue; 1589 } 1590 1591 if (seg[0].length === 4 && seg[1].length === 4) { 1592 // This test is necessary to filter out intersection lines which are 1593 // identical to intersections of axis planes (they would occur twice), 1594 // i.e. edges of bbox3d. 1595 for (j = 0; j < points.length; j++) { 1596 if ( 1597 (Geometry.distance(seg[0], points[j][0], 4) < eps && 1598 Geometry.distance(seg[1], points[j][1], 4) < eps) || 1599 (Geometry.distance(seg[0], points[j][1], 4) < eps && 1600 Geometry.distance(seg[1], points[j][0], 4) < eps) 1601 ) { 1602 break; 1603 } 1604 } 1605 if (j === points.length) { 1606 points.push(seg.slice()); 1607 } 1608 } 1609 } 1610 1611 // Handle the case that the intersection is the empty set. 1612 if (points.length === 0) { 1613 return { X: x, Y: y, Z: z }; 1614 } 1615 1616 // Concatenate the intersection points to a polygon. 1617 // If all went well, each intersection should appear 1618 // twice in the list. 1619 // __Attention:__ each face has to be planar!!! 1620 // Otherwise the algorithm will fail. 1621 first = 0; 1622 pos = first; 1623 i = 0; 1624 do { 1625 p = points[pos][i]; 1626 if (p.length === 4) { 1627 x.push(p[1]); 1628 y.push(p[2]); 1629 z.push(p[3]); 1630 } 1631 i = (i + 1) % 2; 1632 p = points[pos][i]; 1633 1634 pos_akt = pos; 1635 for (j = 0; j < points.length; j++) { 1636 if (j !== pos && Geometry.distance(p, points[j][0]) < eps) { 1637 pos = j; 1638 i = 0; 1639 break; 1640 } 1641 if (j !== pos && Geometry.distance(p, points[j][1]) < eps) { 1642 pos = j; 1643 i = 1; 1644 break; 1645 } 1646 } 1647 if (pos === pos_akt) { 1648 console.log('Error face3d intersection update: did not find next', pos, i); 1649 break; 1650 } 1651 } while (pos !== first); 1652 x.push(x[0]); 1653 y.push(y[0]); 1654 z.push(z[0]); 1655 1656 return { X: x, Y: y, Z: z }; 1657 }, 1658 1659 /** 1660 * Generate mesh for a surface / plane. 1661 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 1662 * @param {Array|Function} func 1663 * @param {Array} interval_u 1664 * @param {Array} interval_v 1665 * @returns Array 1666 * @private 1667 * 1668 * @example 1669 * var el = view.create('curve', [[], []]); 1670 * el.updateDataArray = function () { 1671 * var steps_u = this.evalVisProp('stepsu'), 1672 * steps_v = this.evalVisProp('stepsv'), 1673 * r_u = Type.evaluate(this.range_u), 1674 * r_v = Type.evaluate(this.range_v), 1675 * func, ret; 1676 * 1677 * if (this.F !== null) { 1678 * func = this.F; 1679 * } else { 1680 * func = [this.X, this.Y, this.Z]; 1681 * } 1682 * ret = this.view.getMesh(func, 1683 * r_u.concat([steps_u]), 1684 * r_v.concat([steps_v])); 1685 * 1686 * this.dataX = ret[0]; 1687 * this.dataY = ret[1]; 1688 * }; 1689 * 1690 */ 1691 getMesh: function (func, interval_u, interval_v) { 1692 var i_u, i_v, u, v, 1693 c2d, delta_u, delta_v, 1694 p = [0, 0, 0], 1695 steps_u = Type.evaluate(interval_u[2]), 1696 steps_v = Type.evaluate(interval_v[2]), 1697 dataX = [], 1698 dataY = []; 1699 1700 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u; 1701 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v; 1702 1703 for (i_u = 0; i_u <= steps_u; i_u++) { 1704 u = interval_u[0] + delta_u * i_u; 1705 for (i_v = 0; i_v <= steps_v; i_v++) { 1706 v = interval_v[0] + delta_v * i_v; 1707 if (Type.isFunction(func)) { 1708 p = func(u, v); 1709 } else { 1710 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1711 } 1712 c2d = this.project3DTo2D(p); 1713 dataX.push(c2d[1]); 1714 dataY.push(c2d[2]); 1715 } 1716 dataX.push(NaN); 1717 dataY.push(NaN); 1718 } 1719 1720 for (i_v = 0; i_v <= steps_v; i_v++) { 1721 v = interval_v[0] + delta_v * i_v; 1722 for (i_u = 0; i_u <= steps_u; i_u++) { 1723 u = interval_u[0] + delta_u * i_u; 1724 if (Type.isFunction(func)) { 1725 p = func(u, v); 1726 } else { 1727 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1728 } 1729 c2d = this.project3DTo2D(p); 1730 dataX.push(c2d[1]); 1731 dataY.push(c2d[2]); 1732 } 1733 dataX.push(NaN); 1734 dataY.push(NaN); 1735 } 1736 1737 return [dataX, dataY]; 1738 }, 1739 1740 /** 1741 * 1742 */ 1743 animateAzimuth: function () { 1744 var s = this.az_slide._smin, 1745 e = this.az_slide._smax, 1746 sdiff = e - s, 1747 newVal = this.az_slide.Value() + 0.1; 1748 1749 this.az_slide.position = (newVal - s) / sdiff; 1750 if (this.az_slide.position > 1) { 1751 this.az_slide.position = 0.0; 1752 } 1753 this.board._change3DView = true; 1754 this.board.update(); 1755 this.board._change3DView = false; 1756 1757 this.timeoutAzimuth = setTimeout(function () { 1758 this.animateAzimuth(); 1759 }.bind(this), 200); 1760 }, 1761 1762 /** 1763 * 1764 */ 1765 stopAzimuth: function () { 1766 clearTimeout(this.timeoutAzimuth); 1767 this.timeoutAzimuth = null; 1768 }, 1769 1770 /** 1771 * Check if vertical dragging is enabled and which action is needed. 1772 * Default is shiftKey. 1773 * 1774 * @returns Boolean 1775 * @private 1776 */ 1777 isVerticalDrag: function () { 1778 var b = this.board, 1779 key; 1780 if (!this.evalVisProp('verticaldrag.enabled')) { 1781 return false; 1782 } 1783 key = '_' + this.evalVisProp('verticaldrag.key') + 'Key'; 1784 return b[key]; 1785 }, 1786 1787 /** 1788 * Stop ignoring attribute r. After a call of view3d.setView, view3d.nextView, 1789 * view3d.previousView, or view3d.setCurrentView this attribute is ignored. 1790 * Call of view3d.freeR() will end this. 1791 * @see View3D#setView 1792 */ 1793 freeR: function() { 1794 this.r = null; 1795 }, 1796 1797 /** 1798 * Sets camera view to the given values. 1799 * If the optional value r is supplied that value has priority until the next call of 1800 * view3d.setView or until a call of view3d.freeR(). 1801 * In particular, the attribute r is ignored until a call of view3d.freeR(). 1802 * @param {Number} az Value of azimuth. 1803 * @param {Number} el Value of elevation. 1804 * @param {Number} [r] Value of radius. 1805 * 1806 * @returns {Object} Reference to the view. 1807 * @see View3D#freeR 1808 * @see View3D#r 1809 * @see View3D#nextView 1810 * @see View3D#previousView 1811 * @see View3D#setCurrentView 1812 */ 1813 setView: function (az, el, r) { 1814 // Set the distance to a fixed value. 1815 if (r !== undefined) { 1816 this.r = r; 1817 } 1818 r = this.getCameraDistance(); 1819 1820 this.az_slide.setValue(az); 1821 this.el_slide.setValue(el); 1822 this.board.update(); 1823 1824 return this; 1825 }, 1826 1827 /** 1828 * Changes view to the next view stored in the attribute `values`. 1829 * 1830 * @see View3D#values 1831 * @see View3D#setView 1832 * @see View3D#previousView 1833 * @see View3D#setCurrentView 1834 * @see View3D#freeR 1835 * 1836 * @returns {Object} Reference to the view. 1837 */ 1838 nextView: function () { 1839 var views = this.evalVisProp('values'), 1840 n = this.visProp._currentview; 1841 1842 n = (n + 1) % views.length; 1843 this.setCurrentView(n); 1844 1845 return this; 1846 }, 1847 1848 /** 1849 * Changes view to the previous view stored in the attribute `values`. 1850 * 1851 * @see View3D#values 1852 * @see View3D#setView 1853 * @see View3D#nextView 1854 * @see View3D#setCurrentView 1855 * @see View3D#freeR 1856 * 1857 * @returns {Object} Reference to the view. 1858 */ 1859 previousView: function () { 1860 var views = this.evalVisProp('values'), 1861 n = this.visProp._currentview; 1862 1863 n = (n + views.length - 1) % views.length; 1864 this.setCurrentView(n); 1865 1866 return this; 1867 }, 1868 1869 /** 1870 * Changes view to the determined view stored in the attribute `values`. 1871 * 1872 * @see View3D#values 1873 * @see View3D#nextView 1874 * @see View3D#previousView 1875 * @see View3D#setCurrentView 1876 * @see View3D#freeR 1877 * 1878 * @param {Number} n Index of view in attribute `values`. 1879 * @returns {Object} Reference to the view. 1880 */ 1881 setCurrentView: function (n) { 1882 var views = this.evalVisProp('values'); 1883 1884 if (n < 0 || n >= views.length) { 1885 n = ((n % views.length) + views.length) % views.length; 1886 } 1887 1888 this.setView(views[n][0], views[n][1], views[n][2]); 1889 this.visProp._currentview = n; 1890 1891 return this; 1892 }, 1893 1894 /** 1895 * Controls 2-degree navigation in az direction using pointer. 1896 * 1897 * @private 1898 * 1899 * @param {event} evt the pointer event 1900 * @returns view 1901 */ 1902 _az_elEventHandler: function (evt) { 1903 var smax = this.az_slide._smax, 1904 smin = this.az_slide._smin, 1905 speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')), 1906 deltaX, // = evt.movementX, 1907 deltaY, // = evt.movementY 1908 az = this.az_slide.Value(), 1909 el = this.el_slide.Value(); 1910 1911 deltaX = evt.screenX - this._lastPos.x; 1912 this._lastPos.x = evt.screenX; 1913 deltaY = evt.screenY - this._lastPos.y; 1914 this._lastPos.y = evt.screenY; 1915 1916 // Doesn't allow navigation if another moving event is triggered 1917 if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) { 1918 return this; 1919 } 1920 1921 if (this.evalVisProp('az.pointer.enabled') && (deltaX !== 0) && evt.key == null) { 1922 // delta *= (Math.abs(delta) > 100) ? 0.03 : 1; 1923 az += deltaX * speed; 1924 } 1925 if (this.evalVisProp('el.pointer.enabled') && (deltaY !== 0) && evt.key == null) { 1926 el += deltaY * speed; 1927 } 1928 1929 // Project the calculated az value to a usable value in the interval [smin,smax] 1930 // Use modulo if continuous is true 1931 if (this.evalVisProp('az.continuous')) { 1932 az = Mat.wrap(az, smin, smax); 1933 } else { 1934 if (az > 0) { 1935 az = Math.min(smax, az); 1936 } else if (az < 0) { 1937 az = Math.max(smin, az); 1938 } 1939 } 1940 // Project the calculated el value to a usable value in the interval [smin,smax] 1941 // Use modulo if continuous is true and the trackball is disabled 1942 smax = this.el_slide._smax; 1943 smin = this.el_slide._smin; 1944 if (this.evalVisProp('el.continuous') && !this.trackballEnabled) { 1945 el = Mat.wrap(el, smin, smax); 1946 } else { 1947 if (el > 0) { 1948 el = Math.min(smax, el); 1949 } else if (el < 0) { 1950 el = Math.max(smin, el); 1951 } 1952 } 1953 1954 this.setView(az, el); 1955 return this; 1956 }, 1957 1958 /** 1959 * Controls the navigation in az direction using either the keyboard or a pointer. 1960 * 1961 * @private 1962 * 1963 * @param {event} evt either the keydown or the pointer event 1964 * @returns view 1965 */ 1966 _azEventHandler: function (evt) { 1967 var smax = this.az_slide._smax, 1968 smin = this.az_slide._smin, 1969 speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')), 1970 delta, // = evt.movementX, 1971 az = this.az_slide.Value(), 1972 el = this.el_slide.Value(); 1973 1974 delta = evt.screenX - this._lastPos.x; 1975 this._lastPos.x = evt.screenX; 1976 1977 // Doesn't allow navigation if another moving event is triggered 1978 if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) { 1979 return this; 1980 } 1981 1982 // Calculate new az value if keyboard events are triggered 1983 // Plus if right-button, minus if left-button 1984 if (this.evalVisProp('az.keyboard.enabled')) { 1985 if (evt.key === 'ArrowRight') { 1986 az = az + this.evalVisProp('az.keyboard.step') * Math.PI / 180; 1987 } else if (evt.key === 'ArrowLeft') { 1988 az = az - this.evalVisProp('az.keyboard.step') * Math.PI / 180; 1989 } 1990 } 1991 1992 if (this.evalVisProp('az.pointer.enabled') && (delta !== 0) && evt.key == null) { 1993 // delta *= (Math.abs(delta) > 100) ? 0.03 : 1; 1994 az += delta * speed; 1995 } 1996 1997 // Project the calculated az value to a usable value in the interval [smin,smax] 1998 // Use modulo if continuous is true 1999 if (this.evalVisProp('az.continuous')) { 2000 az = Mat.wrap(az, smin, smax); 2001 } else { 2002 if (az > 0) { 2003 az = Math.min(smax, az); 2004 } else if (az < 0) { 2005 az = Math.max(smin, az); 2006 } 2007 } 2008 2009 this.setView(az, el); 2010 return this; 2011 }, 2012 2013 /** 2014 * Controls the navigation in el direction using either the keyboard or a pointer. 2015 * 2016 * @private 2017 * 2018 * @param {event} evt either the keydown or the pointer event 2019 * @returns view 2020 */ 2021 _elEventHandler: function (evt) { 2022 var smax = this.el_slide._smax, 2023 smin = this.el_slide._smin, 2024 speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('el.pointer.speed'), 2025 delta, // = evt.movementY, 2026 az = this.az_slide.Value(), 2027 el = this.el_slide.Value(); 2028 2029 delta = evt.screenY - this._lastPos.y; 2030 this._lastPos.y = evt.screenY; 2031 2032 // Doesn't allow navigation if another moving event is triggered 2033 if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) { 2034 return this; 2035 } 2036 2037 // Calculate new az value if keyboard events are triggered 2038 // Plus if down-button, minus if up-button 2039 if (this.evalVisProp('el.keyboard.enabled')) { 2040 if (evt.key === 'ArrowUp') { 2041 el = el - this.evalVisProp('el.keyboard.step') * Math.PI / 180; 2042 } else if (evt.key === 'ArrowDown') { 2043 el = el + this.evalVisProp('el.keyboard.step') * Math.PI / 180; 2044 } 2045 } 2046 2047 if (this.evalVisProp('el.pointer.enabled') && (delta !== 0) && evt.key == null) { 2048 // delta *= (Math.abs(delta) > 100) ? 0.05 : 1; 2049 el += delta * speed; 2050 } 2051 2052 // Project the calculated el value to a usable value in the interval [smin,smax] 2053 // Use modulo if continuous is true and the trackball is disabled 2054 if (this.evalVisProp('el.continuous') && !this.trackballEnabled) { 2055 el = Mat.wrap(el, smin, smax); 2056 } else { 2057 if (el > 0) { 2058 el = Math.min(smax, el); 2059 } else if (el < 0) { 2060 el = Math.max(smin, el); 2061 } 2062 } 2063 2064 this.setView(az, el); 2065 2066 return this; 2067 }, 2068 2069 /** 2070 * Controls the navigation in bank direction using either the keyboard or a pointer. 2071 * 2072 * @private 2073 * 2074 * @param {event} evt either the keydown or the pointer event 2075 * @returns view 2076 */ 2077 _bankEventHandler: function (evt) { 2078 var smax = this.bank_slide._smax, 2079 smin = this.bank_slide._smin, 2080 step, speed, 2081 delta = evt.deltaY, // Wheel event 2082 bank = this.bank_slide.Value(); 2083 2084 // Doesn't allow navigation if another moving event is triggered 2085 if (this.board.mode === this.board.BOARD_MODE_DRAG || !this.board._change3DView) { 2086 return this; 2087 } 2088 2089 // Calculate new bank value if keyboard events are triggered 2090 // Plus if down-button, minus if up-button 2091 if (this.evalVisProp('bank.keyboard.enabled')) { 2092 step = this.evalVisProp('bank.keyboard.step') * Math.PI / 180; 2093 if (evt.key === '.' || evt.key === '<') { 2094 bank -= step; 2095 } else if (evt.key === ',' || evt.key === '>') { 2096 bank += step; 2097 } 2098 } 2099 2100 if (this.evalVisProp('bank.pointer.enabled') && (delta !== 0) && evt.key == null) { 2101 speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('bank.pointer.speed'); 2102 bank += delta * speed; 2103 2104 // prevent the pointer wheel from scrolling the page 2105 evt.preventDefault(); 2106 } 2107 2108 // Project the calculated bank value to a usable value in the interval [smin,smax] 2109 if (this.evalVisProp('bank.continuous')) { 2110 // in continuous mode, wrap value around slider range 2111 bank = Mat.wrap(bank, smin, smax); 2112 } else { 2113 // in non-continuous mode, clamp value to slider range 2114 bank = Mat.clamp(bank, smin, smax); 2115 } 2116 2117 this.bank_slide.setValue(bank); 2118 this.board.update(); 2119 return this; 2120 }, 2121 2122 /** 2123 * Controls the navigation using either virtual trackball. 2124 * 2125 * @private 2126 * 2127 * @param {event} evt either the keydown or the pointer event 2128 * @returns view 2129 */ 2130 _trackballHandler: function (evt) { 2131 var pos = this.board.getMousePosition(evt), 2132 x, y, dx, dy, center; 2133 2134 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); 2135 x = pos[0] - center.scrCoords[1]; 2136 y = pos[1] - center.scrCoords[2]; 2137 2138 dx = evt.screenX - this._lastPos.x; 2139 dy = evt.screenY - this._lastPos.y; 2140 this._lastPos.x = evt.screenX; 2141 this._lastPos.y = evt.screenY; 2142 2143 this._trackball = { 2144 dx: dx, 2145 dy: -dy, 2146 x: x, 2147 y: -y 2148 }; 2149 this.board.update(); 2150 return this; 2151 }, 2152 2153 /** 2154 * Event handler for pointer down event. Triggers handling of all 3D navigation. 2155 * 2156 * @private 2157 * @param {event} evt 2158 * @returns view 2159 */ 2160 pointerDownHandler: function (evt) { 2161 var neededButton, neededKey, target; 2162 2163 this._hasMoveAzEl = false; 2164 this._hasMoveAz = false; 2165 this._hasMoveEl = false; 2166 this._hasMoveBank = false; 2167 this._hasMoveTrackball = false; 2168 2169 if (this.board.mode !== this.board.BOARD_MODE_NONE) { 2170 return; 2171 } 2172 2173 this.board._change3DView = true; 2174 2175 this._lastPos.x = evt.screenX; 2176 this._lastPos.y = evt.screenY; 2177 2178 if (this.evalVisProp('trackball.enabled')) { 2179 neededButton = this.evalVisProp('trackball.button'); 2180 neededKey = this.evalVisProp('trackball.key'); 2181 2182 // Move events for virtual trackball 2183 if ( 2184 (neededButton === -1 || neededButton === evt.button) && 2185 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 2186 ) { 2187 // If outside is true then the event listener is bound to the document, otherwise to the div 2188 target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj; 2189 Env.addEvent(target, 'pointermove', this._trackballHandler, this); 2190 this._hasMoveTrackball = true; 2191 } 2192 } else { 2193 if (this.evalVisProp('az.pointer.enabled') && this.evalVisProp('el.pointer.enabled')) { 2194 neededButton = this.evalVisProp('az.pointer.button'); 2195 neededKey = this.evalVisProp('az.pointer.key'); 2196 if (neededButton === this.evalVisProp('el.pointer.button') && 2197 neededKey === this.evalVisProp('el.pointer.key')) { 2198 2199 // Move events for azimuth and elevation 2200 if ( 2201 (neededButton === -1 || neededButton === evt.button) && 2202 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || 2203 (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 2204 ) { 2205 // If outside is true then the event listener is bound to the document, otherwise to the div 2206 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 2207 2208 if (target === ((this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj)) { 2209 Env.addEvent(target, 'pointermove', this._az_elEventHandler, this); 2210 this._hasMoveAzEl = true; 2211 } 2212 } 2213 } 2214 } 2215 if (!this._hasMoveAzEl) { 2216 if (this.evalVisProp('az.pointer.enabled')) { 2217 neededButton = this.evalVisProp('az.pointer.button'); 2218 neededKey = this.evalVisProp('az.pointer.key'); 2219 2220 // Move events for azimuth 2221 if ( 2222 (neededButton === -1 || neededButton === evt.button) && 2223 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 2224 ) { 2225 // If outside is true then the event listener is bound to the document, otherwise to the div 2226 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 2227 Env.addEvent(target, 'pointermove', this._azEventHandler, this); 2228 this._hasMoveAz = true; 2229 } 2230 } 2231 2232 if (this.evalVisProp('el.pointer.enabled')) { 2233 neededButton = this.evalVisProp('el.pointer.button'); 2234 neededKey = this.evalVisProp('el.pointer.key'); 2235 2236 // Events for elevation 2237 if ( 2238 (neededButton === -1 || neededButton === evt.button) && 2239 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 2240 ) { 2241 // If outside is true then the event listener is bound to the document, otherwise to the div 2242 target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj; 2243 Env.addEvent(target, 'pointermove', this._elEventHandler, this); 2244 this._hasMoveEl = true; 2245 } 2246 } 2247 } 2248 if (this.evalVisProp('bank.pointer.enabled')) { 2249 neededButton = this.evalVisProp('bank.pointer.button'); 2250 neededKey = this.evalVisProp('bank.pointer.key'); 2251 2252 // Events for bank 2253 if ( 2254 (neededButton === -1 || neededButton === evt.button) && 2255 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 2256 ) { 2257 // If `outside` is true, we bind the event listener to 2258 // the document. otherwise, we bind it to the div. we 2259 // register the event listener as active so it can 2260 // prevent the pointer wheel from scrolling the page 2261 target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj; 2262 Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false }); 2263 this._hasMoveBank = true; 2264 } 2265 } 2266 } 2267 Env.addEvent(document, 'pointerup', this.pointerUpHandler, this); 2268 }, 2269 2270 /** 2271 * Event handler for pointer up event. Triggers handling of all 3D navigation. 2272 * 2273 * @private 2274 * @param {event} evt 2275 * @returns view 2276 */ 2277 pointerUpHandler: function (evt) { 2278 var target; 2279 2280 if (this._hasMoveAzEl) { 2281 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 2282 Env.removeEvent(target, 'pointermove', this._az_elEventHandler, this); 2283 this._hasMoveAzEl = false; 2284 } 2285 if (this._hasMoveAz) { 2286 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 2287 Env.removeEvent(target, 'pointermove', this._azEventHandler, this); 2288 this._hasMoveAz = false; 2289 } 2290 if (this._hasMoveEl) { 2291 target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj; 2292 Env.removeEvent(target, 'pointermove', this._elEventHandler, this); 2293 this._hasMoveEl = false; 2294 } 2295 if (this._hasMoveBank) { 2296 target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj; 2297 Env.removeEvent(target, 'wheel', this._bankEventHandler, this); 2298 this._hasMoveBank = false; 2299 } 2300 if (this._hasMoveTrackball) { 2301 target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj; 2302 Env.removeEvent(target, 'pointermove', this._trackballHandler, this); 2303 this._hasMoveTrackball = false; 2304 } 2305 Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this); 2306 this.board._change3DView = false; 2307 this.board.mode = this.board.BOARD_MODE_NONE; 2308 } 2309 }); 2310 2311 /** 2312 * @class A View3D element provides the container and the methods to create and display 3D elements. 2313 * @pseudo 2314 * @description A View3D element provides the container and the methods to create and display 3D elements. 2315 * It is contained in a JSXGraph board. 2316 * <p> 2317 * It is advisable to disable panning of the board by setting the board attribute "pan": 2318 * <pre> 2319 * pan: {enabled: false} 2320 * </pre> 2321 * Otherwise users will not be able to rotate the scene with their fingers on a touch device. 2322 * <p> 2323 * The start position of the camera can be adjusted by the attributes {@link View3D#az}, {@link View3D#el}, and {@link View3D#bank}. 2324 * 2325 * @name View3D 2326 * @augments JXG.View3D 2327 * @constructor 2328 * @type Object 2329 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 2330 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 2331 * dim is an array of the form [w, h]. 2332 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 2333 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 2334 * [x,y] and side lengths [w, h] of the board. 2335 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 2336 * which determines the coordinate ranges of the 3D cube. 2337 * 2338 * @example 2339 * var bound = [-4, 6]; 2340 * var view = board.create('view3d', 2341 * [[-4, -3], [8, 8], 2342 * [bound, bound, bound]], 2343 * { 2344 * projection: 'parallel', 2345 * trackball: {enabled:true}, 2346 * }); 2347 * 2348 * var curve = view.create('curve3d', [ 2349 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2350 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2351 * (t) => Math.sin(3 * t), 2352 * [-Math.PI, Math.PI] 2353 * ], { strokeWidth: 4 }); 2354 * 2355 * </pre><div id="JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b" class="jxgbox" style="width: 300px; height: 300px;"></div> 2356 * <script type="text/javascript"> 2357 * (function() { 2358 * var board = JXG.JSXGraph.initBoard('JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b', 2359 * {boundingbox: [-8, 8, 8,-8], pan: {enabled: false}, axis: false, showcopyright: false, shownavigation: false}); 2360 * var bound = [-4, 6]; 2361 * var view = board.create('view3d', 2362 * [[-4, -3], [8, 8], 2363 * [bound, bound, bound]], 2364 * { 2365 * projection: 'parallel', 2366 * trackball: {enabled:true}, 2367 * }); 2368 * 2369 * var curve = view.create('curve3d', [ 2370 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2371 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2372 * (t) => Math.sin(3 * t), 2373 * [-Math.PI, Math.PI] 2374 * ], { strokeWidth: 4 }); 2375 * 2376 * })(); 2377 * 2378 * </script><pre> 2379 * 2380 * @example 2381 * var bound = [-4, 6]; 2382 * var view = board.create('view3d', 2383 * [[-4, -3], [8, 8], 2384 * [bound, bound, bound]], 2385 * { 2386 * projection: 'central', 2387 * trackball: {enabled:true}, 2388 * 2389 * xPlaneRear: { visible: false }, 2390 * yPlaneRear: { visible: false } 2391 * 2392 * }); 2393 * 2394 * var curve = view.create('curve3d', [ 2395 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2396 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2397 * (t) => Math.sin(3 * t), 2398 * [-Math.PI, Math.PI] 2399 * ], { strokeWidth: 4 }); 2400 * 2401 * </pre><div id="JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007" class="jxgbox" style="width: 300px; height: 300px;"></div> 2402 * <script type="text/javascript"> 2403 * (function() { 2404 * var board = JXG.JSXGraph.initBoard('JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007', 2405 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2406 * var bound = [-4, 6]; 2407 * var view = board.create('view3d', 2408 * [[-4, -3], [8, 8], 2409 * [bound, bound, bound]], 2410 * { 2411 * projection: 'central', 2412 * trackball: {enabled:true}, 2413 * 2414 * xPlaneRear: { visible: false }, 2415 * yPlaneRear: { visible: false } 2416 * 2417 * }); 2418 * 2419 * var curve = view.create('curve3d', [ 2420 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2421 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2422 * (t) => Math.sin(3 * t), 2423 * [-Math.PI, Math.PI] 2424 * ], { strokeWidth: 4 }); 2425 * 2426 * })(); 2427 * 2428 * </script><pre> 2429 * 2430 * @example 2431 * var bound = [-4, 6]; 2432 * var view = board.create('view3d', 2433 * [[-4, -3], [8, 8], 2434 * [bound, bound, bound]], 2435 * { 2436 * projection: 'central', 2437 * trackball: {enabled:true}, 2438 * 2439 * // Main axes 2440 * axesPosition: 'border', 2441 * 2442 * // Axes at the border 2443 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2444 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2445 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2446 * 2447 * // No axes on planes 2448 * xPlaneRearYAxis: {visible: false}, 2449 * xPlaneRearZAxis: {visible: false}, 2450 * yPlaneRearXAxis: {visible: false}, 2451 * yPlaneRearZAxis: {visible: false}, 2452 * zPlaneRearXAxis: {visible: false}, 2453 * zPlaneRearYAxis: {visible: false} 2454 * }); 2455 * 2456 * var curve = view.create('curve3d', [ 2457 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2458 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2459 * (t) => Math.sin(3 * t), 2460 * [-Math.PI, Math.PI] 2461 * ], { strokeWidth: 4 }); 2462 * 2463 * </pre><div id="JXG586f3551-335c-47e9-8d72-835409f6a103" class="jxgbox" style="width: 300px; height: 300px;"></div> 2464 * <script type="text/javascript"> 2465 * (function() { 2466 * var board = JXG.JSXGraph.initBoard('JXG586f3551-335c-47e9-8d72-835409f6a103', 2467 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2468 * var bound = [-4, 6]; 2469 * var view = board.create('view3d', 2470 * [[-4, -3], [8, 8], 2471 * [bound, bound, bound]], 2472 * { 2473 * projection: 'central', 2474 * trackball: {enabled:true}, 2475 * 2476 * // Main axes 2477 * axesPosition: 'border', 2478 * 2479 * // Axes at the border 2480 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2481 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2482 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2483 * 2484 * // No axes on planes 2485 * xPlaneRearYAxis: {visible: false}, 2486 * xPlaneRearZAxis: {visible: false}, 2487 * yPlaneRearXAxis: {visible: false}, 2488 * yPlaneRearZAxis: {visible: false}, 2489 * zPlaneRearXAxis: {visible: false}, 2490 * zPlaneRearYAxis: {visible: false} 2491 * }); 2492 * 2493 * var curve = view.create('curve3d', [ 2494 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2495 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2496 * (t) => Math.sin(3 * t), 2497 * [-Math.PI, Math.PI] 2498 * ], { strokeWidth: 4 }); 2499 * 2500 * })(); 2501 * 2502 * </script><pre> 2503 * 2504 * @example 2505 * var bound = [-4, 6]; 2506 * var view = board.create('view3d', 2507 * [[-4, -3], [8, 8], 2508 * [bound, bound, bound]], 2509 * { 2510 * projection: 'central', 2511 * trackball: {enabled:true}, 2512 * 2513 * axesPosition: 'none' 2514 * }); 2515 * 2516 * var curve = view.create('curve3d', [ 2517 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2518 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2519 * (t) => Math.sin(3 * t), 2520 * [-Math.PI, Math.PI] 2521 * ], { strokeWidth: 4 }); 2522 * 2523 * </pre><div id="JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26" class="jxgbox" style="width: 300px; height: 300px;"></div> 2524 * <script type="text/javascript"> 2525 * (function() { 2526 * var board = JXG.JSXGraph.initBoard('JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26', 2527 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2528 * var bound = [-4, 6]; 2529 * var view = board.create('view3d', 2530 * [[-4, -3], [8, 8], 2531 * [bound, bound, bound]], 2532 * { 2533 * projection: 'central', 2534 * trackball: {enabled:true}, 2535 * 2536 * axesPosition: 'none' 2537 * }); 2538 * 2539 * var curve = view.create('curve3d', [ 2540 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2541 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2542 * (t) => Math.sin(3 * t), 2543 * [-Math.PI, Math.PI] 2544 * ], { strokeWidth: 4 }); 2545 * 2546 * })(); 2547 * 2548 * </script><pre> 2549 * 2550 * @example 2551 * var bound = [-4, 6]; 2552 * var view = board.create('view3d', 2553 * [[-4, -3], [8, 8], 2554 * [bound, bound, bound]], 2555 * { 2556 * projection: 'central', 2557 * trackball: {enabled:true}, 2558 * 2559 * // Main axes 2560 * axesPosition: 'border', 2561 * 2562 * // Axes at the border 2563 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2564 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2565 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2566 * 2567 * xPlaneRear: { 2568 * fillColor: '#fff', 2569 * mesh3d: {visible: false} 2570 * }, 2571 * yPlaneRear: { 2572 * fillColor: '#fff', 2573 * mesh3d: {visible: false} 2574 * }, 2575 * zPlaneRear: { 2576 * fillColor: '#fff', 2577 * mesh3d: {visible: false} 2578 * }, 2579 * xPlaneFront: { 2580 * visible: true, 2581 * fillColor: '#fff', 2582 * mesh3d: {visible: false} 2583 * }, 2584 * yPlaneFront: { 2585 * visible: true, 2586 * fillColor: '#fff', 2587 * mesh3d: {visible: false} 2588 * }, 2589 * zPlaneFront: { 2590 * visible: true, 2591 * fillColor: '#fff', 2592 * mesh3d: {visible: false} 2593 * }, 2594 * 2595 * // No axes on planes 2596 * xPlaneRearYAxis: {visible: false}, 2597 * xPlaneRearZAxis: {visible: false}, 2598 * yPlaneRearXAxis: {visible: false}, 2599 * yPlaneRearZAxis: {visible: false}, 2600 * zPlaneRearXAxis: {visible: false}, 2601 * zPlaneRearYAxis: {visible: false}, 2602 * xPlaneFrontYAxis: {visible: false}, 2603 * xPlaneFrontZAxis: {visible: false}, 2604 * yPlaneFrontXAxis: {visible: false}, 2605 * yPlaneFrontZAxis: {visible: false}, 2606 * zPlaneFrontXAxis: {visible: false}, 2607 * zPlaneFrontYAxis: {visible: false} 2608 * 2609 * }); 2610 * 2611 * var curve = view.create('curve3d', [ 2612 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2613 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2614 * (t) => Math.sin(3 * t), 2615 * [-Math.PI, Math.PI] 2616 * ], { strokeWidth: 4 }); 2617 * 2618 * </pre><div id="JXGbd41a4e3-1bf7-4764-b675-98b01667103b" class="jxgbox" style="width: 300px; height: 300px;"></div> 2619 * <script type="text/javascript"> 2620 * (function() { 2621 * var board = JXG.JSXGraph.initBoard('JXGbd41a4e3-1bf7-4764-b675-98b01667103b', 2622 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2623 * var bound = [-4, 6]; 2624 * var view = board.create('view3d', 2625 * [[-4, -3], [8, 8], 2626 * [bound, bound, bound]], 2627 * { 2628 * projection: 'central', 2629 * trackball: {enabled:true}, 2630 * 2631 * // Main axes 2632 * axesPosition: 'border', 2633 * 2634 * // Axes at the border 2635 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2636 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2637 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2638 * 2639 * xPlaneRear: { 2640 * fillColor: '#fff', 2641 * mesh3d: {visible: false} 2642 * }, 2643 * yPlaneRear: { 2644 * fillColor: '#fff', 2645 * mesh3d: {visible: false} 2646 * }, 2647 * zPlaneRear: { 2648 * fillColor: '#fff', 2649 * mesh3d: {visible: false} 2650 * }, 2651 * xPlaneFront: { 2652 * visible: true, 2653 * fillColor: '#fff', 2654 * mesh3d: {visible: false} 2655 * }, 2656 * yPlaneFront: { 2657 * visible: true, 2658 * fillColor: '#fff', 2659 * mesh3d: {visible: false} 2660 * }, 2661 * zPlaneFront: { 2662 * visible: true, 2663 * fillColor: '#fff', 2664 * mesh3d: {visible: false} 2665 * }, 2666 * 2667 * // No axes on planes 2668 * xPlaneRearYAxis: {visible: false}, 2669 * xPlaneRearZAxis: {visible: false}, 2670 * yPlaneRearXAxis: {visible: false}, 2671 * yPlaneRearZAxis: {visible: false}, 2672 * zPlaneRearXAxis: {visible: false}, 2673 * zPlaneRearYAxis: {visible: false}, 2674 * xPlaneFrontYAxis: {visible: false}, 2675 * xPlaneFrontZAxis: {visible: false}, 2676 * yPlaneFrontXAxis: {visible: false}, 2677 * yPlaneFrontZAxis: {visible: false}, 2678 * zPlaneFrontXAxis: {visible: false}, 2679 * zPlaneFrontYAxis: {visible: false} 2680 * 2681 * }); 2682 * 2683 * var curve = view.create('curve3d', [ 2684 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2685 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2686 * (t) => Math.sin(3 * t), 2687 * [-Math.PI, Math.PI] 2688 * ], { strokeWidth: 4 }); 2689 * })(); 2690 * 2691 * </script><pre> 2692 * 2693 * @example 2694 * var bound = [-5, 5]; 2695 * var view = board.create('view3d', 2696 * [[-6, -3], 2697 * [8, 8], 2698 * [bound, bound, bound]], 2699 * { 2700 * // Main axes 2701 * axesPosition: 'center', 2702 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 2703 * 2704 * // Planes 2705 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 2706 * yPlaneFront: { visible: true, fillColor: 'blue'}, 2707 * 2708 * // Axes on planes 2709 * xPlaneRearYAxis: {strokeColor: 'red'}, 2710 * xPlaneRearZAxis: {strokeColor: 'red'}, 2711 * 2712 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 2713 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 2714 * 2715 * zPlaneFrontXAxis: {visible: false}, 2716 * zPlaneFrontYAxis: {visible: false} 2717 * }); 2718 * 2719 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 2720 * <script type="text/javascript"> 2721 * (function() { 2722 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 2723 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2724 * var bound = [-5, 5]; 2725 * var view = board.create('view3d', 2726 * [[-6, -3], [8, 8], 2727 * [bound, bound, bound]], 2728 * { 2729 * // Main axes 2730 * axesPosition: 'center', 2731 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 2732 * // Planes 2733 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 2734 * yPlaneFront: { visible: true, fillColor: 'blue'}, 2735 * // Axes on planes 2736 * xPlaneRearYAxis: {strokeColor: 'red'}, 2737 * xPlaneRearZAxis: {strokeColor: 'red'}, 2738 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 2739 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 2740 * zPlaneFrontXAxis: {visible: false}, 2741 * zPlaneFrontYAxis: {visible: false} 2742 * }); 2743 * })(); 2744 * 2745 * </script><pre> 2746 * @example 2747 * var bound = [-5, 5]; 2748 * var view = board.create('view3d', 2749 * [[-6, -3], [8, 8], 2750 * [bound, bound, bound]], 2751 * { 2752 * projection: 'central', 2753 * az: { 2754 * slider: { 2755 * visible: true, 2756 * point1: { 2757 * pos: [5, -4] 2758 * }, 2759 * point2: { 2760 * pos: [5, 4] 2761 * }, 2762 * label: {anchorX: 'middle'} 2763 * } 2764 * }, 2765 * el: { 2766 * slider: { 2767 * visible: true, 2768 * point1: { 2769 * pos: [6, -5] 2770 * }, 2771 * point2: { 2772 * pos: [6, 3] 2773 * }, 2774 * label: {anchorX: 'middle'} 2775 * } 2776 * }, 2777 * bank: { 2778 * slider: { 2779 * visible: true, 2780 * point1: { 2781 * pos: [7, -6] 2782 * }, 2783 * point2: { 2784 * pos: [7, 2] 2785 * }, 2786 * label: {anchorX: 'middle'} 2787 * } 2788 * } 2789 * }); 2790 * 2791 * 2792 * </pre><div id="JXGe181cc55-271b-419b-84fd-622326fd1d1a" class="jxgbox" style="width: 300px; height: 300px;"></div> 2793 * <script type="text/javascript"> 2794 * (function() { 2795 * var board = JXG.JSXGraph.initBoard('JXGe181cc55-271b-419b-84fd-622326fd1d1a', 2796 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 2797 * var bound = [-5, 5]; 2798 * var view = board.create('view3d', 2799 * [[-6, -3], [8, 8], 2800 * [bound, bound, bound]], 2801 * { 2802 * projection: 'central', 2803 * az: { 2804 * slider: { 2805 * visible: true, 2806 * point1: { 2807 * pos: [5, -4] 2808 * }, 2809 * point2: { 2810 * pos: [5, 4] 2811 * }, 2812 * label: {anchorX: 'middle'} 2813 * } 2814 * }, 2815 * el: { 2816 * slider: { 2817 * visible: true, 2818 * point1: { 2819 * pos: [6, -5] 2820 * }, 2821 * point2: { 2822 * pos: [6, 3] 2823 * }, 2824 * label: {anchorX: 'middle'} 2825 * } 2826 * }, 2827 * bank: { 2828 * slider: { 2829 * visible: true, 2830 * point1: { 2831 * pos: [7, -6] 2832 * }, 2833 * point2: { 2834 * pos: [7, 2] 2835 * }, 2836 * label: {anchorX: 'middle'} 2837 * } 2838 * } 2839 * }); 2840 * 2841 * 2842 * })(); 2843 * 2844 * </script><pre> 2845 * 2846 * 2847 */ 2848 JXG.createView3D = function (board, parents, attributes) { 2849 var view, attr, attr_az, attr_el, attr_bank, 2850 x, y, w, h, 2851 p1, p2, v, 2852 coords = parents[0], // llft corner 2853 size = parents[1]; // [w, h] 2854 2855 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 2856 view = new JXG.View3D(board, parents, attr); 2857 view.defaultAxes = view.create('axes3d', [], attr); 2858 2859 x = coords[0]; 2860 y = coords[1]; 2861 w = size[0]; 2862 h = size[1]; 2863 2864 attr_az = Type.copyAttributes(attr, board.options, 'view3d', 'az', 'slider'); 2865 attr_az.name = 'az'; 2866 2867 attr_el = Type.copyAttributes(attr, board.options, 'view3d', 'el', 'slider'); 2868 attr_el.name = 'el'; 2869 2870 attr_bank = Type.copyAttributes(attr, board.options, 'view3d', 'bank', 'slider'); 2871 attr_bank.name = 'bank'; 2872 2873 v = Type.evaluate(attr_az.point1.pos); 2874 if (!Type.isArray(v)) { 2875 // 'auto' 2876 p1 = [x - 1, y - 2]; 2877 } else { 2878 p1 = v; 2879 } 2880 v = Type.evaluate(attr_az.point2.pos); 2881 if (!Type.isArray(v)) { 2882 // 'auto' 2883 p2 = [x + w + 1, y - 2]; 2884 } else { 2885 p2 = v; 2886 } 2887 2888 /** 2889 * Slider to adapt azimuth angle 2890 * @name JXG.View3D#az_slide 2891 * @type {Slider} 2892 */ 2893 view.az_slide = board.create( 2894 'slider', 2895 [ 2896 p1, p2, 2897 [ 2898 Type.evaluate(attr_az.min), 2899 Type.evaluate(attr_az.start), 2900 Type.evaluate(attr_az.max) 2901 ] 2902 ], 2903 attr_az 2904 ); 2905 view.inherits.push(view.az_slide); 2906 view.az_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate() 2907 2908 v = Type.evaluate(attr_el.point1.pos); 2909 if (!Type.isArray(v)) { 2910 // 'auto' 2911 p1 = [x - 1, y]; 2912 } else { 2913 p1 = v; 2914 } 2915 v = Type.evaluate(attr_el.point2.pos); 2916 if (!Type.isArray(v)) { 2917 // 'auto' 2918 p2 = [x - 1, y + h]; 2919 } else { 2920 p2 = v; 2921 } 2922 2923 /** 2924 * Slider to adapt elevation angle 2925 * 2926 * @name JXG.View3D#el_slide 2927 * @type {Slider} 2928 */ 2929 view.el_slide = board.create( 2930 'slider', 2931 [ 2932 p1, p2, 2933 [ 2934 Type.evaluate(attr_el.min), 2935 Type.evaluate(attr_el.start), 2936 Type.evaluate(attr_el.max)] 2937 ], 2938 attr_el 2939 ); 2940 view.inherits.push(view.el_slide); 2941 view.el_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate() 2942 2943 v = Type.evaluate(attr_bank.point1.pos); 2944 if (!Type.isArray(v)) { 2945 // 'auto' 2946 p1 = [x - 1, y + h + 2]; 2947 } else { 2948 p1 = v; 2949 } 2950 v = Type.evaluate(attr_bank.point2.pos); 2951 if (!Type.isArray(v)) { 2952 // 'auto' 2953 p2 = [x + w + 1, y + h + 2]; 2954 } else { 2955 p2 = v; 2956 } 2957 2958 /** 2959 * Slider to adjust bank angle 2960 * 2961 * @name JXG.View3D#bank_slide 2962 * @type {Slider} 2963 */ 2964 view.bank_slide = board.create( 2965 'slider', 2966 [ 2967 p1, p2, 2968 [ 2969 Type.evaluate(attr_bank.min), 2970 Type.evaluate(attr_bank.start), 2971 Type.evaluate(attr_bank.max) 2972 ] 2973 ], 2974 attr_bank 2975 ); 2976 view.inherits.push(view.bank_slide); 2977 view.bank_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate() 2978 2979 // Set special infobox attributes of view3d.infobox 2980 // Using setAttribute() is not possible here, since we have to 2981 // avoid a call of board.update(). 2982 // The drawback is that we can not use shortcuts 2983 view.board.infobox.visProp = Type.merge(view.board.infobox.visProp, attr.infobox); 2984 2985 // 3d infobox: drag direction and coordinates 2986 view.board.highlightInfobox = function (x, y, el) { 2987 var d, i, c3d, foot, 2988 pre = '', 2989 brd = el.board, 2990 arr, infobox, 2991 p = null; 2992 2993 if (this.mode === this.BOARD_MODE_DRAG) { 2994 // Drag direction is only shown during dragging 2995 if (view.isVerticalDrag()) { 2996 pre = '<span style="color:black; font-size:200%">\u21C5 </span>'; 2997 } else { 2998 pre = '<span style="color:black; font-size:200%">\u21C4 </span>'; 2999 } 3000 } 3001 3002 // Search 3D parent 3003 for (i = 0; i < el.parents.length; i++) { 3004 p = brd.objects[el.parents[i]]; 3005 if (p.is3D) { 3006 break; 3007 } 3008 } 3009 3010 if (p && Type.exists(p.element2D)) { 3011 foot = [1, 0, 0, p.coords[3]]; 3012 view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4); 3013 3014 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 3015 if (!view.isInCube(c3d)) { 3016 view.board.highlightCustomInfobox('', p); 3017 return; 3018 } 3019 d = p.evalVisProp('infoboxdigits'); 3020 infobox = view.board.infobox; 3021 if (d === 'auto') { 3022 if (infobox.useLocale()) { 3023 arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')']; 3024 } else { 3025 arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')']; 3026 } 3027 3028 } else { 3029 if (infobox.useLocale()) { 3030 arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')']; 3031 } else { 3032 arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')']; 3033 } 3034 } 3035 view.board.highlightCustomInfobox(arr.join(''), p); 3036 } else { 3037 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 3038 } 3039 }; 3040 3041 // Hack needed to enable addEvent for view3D: 3042 view.BOARD_MODE_NONE = 0x0000; 3043 3044 // Add events for the keyboard navigation 3045 Env.addEvent(board.containerObj, 'keydown', function (event) { 3046 var neededKey, 3047 catchEvt = false; 3048 3049 // this.board._change3DView = true; 3050 if (view.evalVisProp('el.keyboard.enabled') && 3051 (event.key === 'ArrowUp' || event.key === 'ArrowDown') 3052 ) { 3053 neededKey = view.evalVisProp('el.keyboard.key'); 3054 if (neededKey === 'none' || 3055 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 3056 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 3057 view._elEventHandler(event); 3058 catchEvt = true; 3059 } 3060 3061 } 3062 3063 if (view.evalVisProp('az.keyboard.enabled') && 3064 (event.key === 'ArrowLeft' || event.key === 'ArrowRight') 3065 ) { 3066 neededKey = view.evalVisProp('az.keyboard.key'); 3067 if (neededKey === 'none' || 3068 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 3069 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey) 3070 ) { 3071 view._azEventHandler(event); 3072 catchEvt = true; 3073 } 3074 } 3075 3076 if (view.evalVisProp('bank.keyboard.enabled') && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) { 3077 neededKey = view.evalVisProp('bank.keyboard.key'); 3078 if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 3079 view._bankEventHandler(event); 3080 catchEvt = true; 3081 } 3082 } 3083 3084 if (event.key === 'PageUp') { 3085 view.nextView(); 3086 catchEvt = true; 3087 } else if (event.key === 'PageDown') { 3088 view.previousView(); 3089 catchEvt = true; 3090 } 3091 3092 if (catchEvt) { 3093 // We stop event handling only in the case if the keypress could be 3094 // used for the 3D view. If this is not done, input fields et al 3095 // can not be used any more. 3096 event.preventDefault(); 3097 } 3098 this.board._change3DView = false; 3099 3100 }, view); 3101 3102 // Add events for the pointer navigation 3103 Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view); 3104 3105 // Initialize view rotation matrix 3106 view.getAnglesFromSliders(); 3107 view.matrix3DRot = view.getRotationFromAngles(); 3108 3109 // override angle slider bounds when trackball navigation is enabled 3110 view.updateAngleSliderBounds(); 3111 3112 view.board.update(); 3113 3114 return view; 3115 }; 3116 3117 JXG.registerElement("view3d", JXG.createView3D); 3118 3119 export default JXG.View3D; 3120