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