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 elements in the view that are sorted due to their depth order. 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 = this.evalVisProp('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 = this.evalVisProp('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 = this.matrix3DRot.map(function (row) { return row.slice(); }); 708 this.boxToCam[3][0] = -r; 709 710 // compute focal distance and clip space transformation 711 this.focalDist = 1 / Math.tan(0.5 * this.evalVisProp('fov')); 712 A = [ 713 [0, 0, 0, -1], 714 [0, this.focalDist, 0, 0], 715 [0, 0, this.focalDist, 0], 716 [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)] 717 ]; 718 719 return Mat.matMatMult(A, this.boxToCam); 720 }, 721 722 /** 723 * Comparison function for 3D points. It is used to sort points according to their z-index. 724 * @param {Point3D} a 725 * @param {Point3D} b 726 * @returns Integer 727 */ 728 compareDepth: function (a, b) { 729 var worldDiff = [0, 730 a.coords[1] - b.coords[1], 731 a.coords[2] - b.coords[2], 732 a.coords[3] - b.coords[3]], 733 oriBoxDiff = Mat.matVecMult(this.matrix3DRot, Mat.matVecMult(this.shift, worldDiff)); 734 return oriBoxDiff[3]; 735 }, 736 737 // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles. 738 update: function () { 739 var r = this.r, 740 stretch = [ 741 [1, 0, 0, 0], 742 [0, -r, 0, 0], 743 [0, 0, -r, 0], 744 [0, 0, 0, 1] 745 ], 746 mat2D, objectToClip, size, 747 dx, dy, 748 id, el; 749 // objectsList; 750 751 if ( 752 !Type.exists(this.el_slide) || 753 !Type.exists(this.az_slide) || 754 !Type.exists(this.bank_slide) || 755 !this.needsUpdate 756 ) { 757 return this; 758 } 759 760 mat2D = [ 761 [1, 0, 0], 762 [0, 1, 0], 763 [0, 0, 1] 764 ]; 765 766 this.projectionType = this.evalVisProp('projection').toLowerCase(); 767 768 // override angle slider bounds when trackball navigation is enabled 769 if (this.trackballEnabled !== this.evalVisProp('trackball.enabled')) { 770 this.updateAngleSliderBounds(); 771 } 772 773 if (this._hasMoveTrackball) { 774 // The trackball has been moved since the last update, so we do 775 // trackball navigation. When the trackball is enabled, a drag 776 // event is interpreted as a trackball movement unless it's 777 // caught by something else, like point dragging. When the 778 // trackball is disabled, the trackball movement flag should 779 // never be set 780 this.matrix3DRot = this.updateProjectionTrackball(); 781 this.setAnglesFromRotation(); 782 } else if (this.anglesHaveMoved()) { 783 // The trackball hasn't been moved since the last up date, but 784 // the Tait-Bryan angles have been, so we do angle navigation 785 this.getAnglesFromSliders(); 786 this.matrix3DRot = this.getRotationFromAngles(); 787 } 788 789 /** 790 * The translation that moves the center of the view box to the origin. 791 */ 792 this.shift = [ 793 [1, 0, 0, 0], 794 [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0], 795 [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0], 796 [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1] 797 ]; 798 799 switch (this.projectionType) { 800 case 'central': // Central projection 801 802 // Add a final transformation to scale and shift the projection 803 // on the board, usually called viewport. 804 size = 2 * 0.4; 805 mat2D[1][1] = this.size[0] / size; // w / d_x 806 mat2D[2][2] = this.size[1] / size; // h / d_y 807 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x 808 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y 809 // The transformations this.matrix3D and mat2D can not be combined at this point, 810 // since the projected vectors have to be normalized in between in project3DTo2D 811 this.viewPortTransform = mat2D; 812 813 objectToClip = this._updateCentralProjection(); 814 // this.matrix3D is a 4x4 matrix 815 this.matrix3D = Mat.matMatMult(objectToClip, this.shift); 816 break; 817 818 case 'parallel': // Parallel projection 819 default: 820 // Add a final transformation to scale and shift the projection 821 // on the board, usually called viewport. 822 dx = this.bbox3D[0][1] - this.bbox3D[0][0]; 823 dy = this.bbox3D[1][1] - this.bbox3D[1][0]; 824 mat2D[1][1] = this.size[0] / dx; // w / d_x 825 mat2D[2][2] = this.size[1] / dy; // h / d_y 826 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x 827 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y 828 829 // Combine all transformations, this.matrix3D is a 3x4 matrix 830 this.matrix3D = Mat.matMatMult( 831 mat2D, 832 Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3) 833 ); 834 } 835 836 // if depth-ordering for points was just switched on, initialize the 837 // list of points 838 if (this.visProp.depthorderpoints && this.points === null) { 839 // objectsList = Object.values(this.objects); 840 // this.points = objectsList.filter( 841 // el => el.type === Const.OBJECT_TYPE_POINT3D 842 // ); 843 this.points = []; 844 for (id in this.objects) { 845 if (this.objects.hasOwnProperty(id)) { 846 el = this.objects[id]; 847 if (el.type === Const.OBJECT_TYPE_POINT3D) { 848 this.points.push(el); 849 } 850 } 851 } 852 } 853 854 // if depth-ordering for points was just switched off, throw away the 855 // list of points 856 if (!this.visProp.depthorderpoints && this.points !== null) { 857 this.points = null; 858 } 859 860 // depth-order visible points. the `setLayer` method is used here to 861 // re-order the points within each layer: it has the side effect of 862 // moving the target element to the end of the layer's child list 863 if (this.visProp.depthorderpoints && this.board.renderer && this.board.renderer.type === 'svg') { 864 this.points 865 // .filter((pt) => pt.element2D.evalVisProp('visible')) 866 // .sort(this.compareDepth.bind(this)) 867 // .forEach((pt) => this.board.renderer.setLayer(pt.element2D, pt.element2D.visProp.layer)); 868 .filter(function (pt) { return pt.element2D.evalVisProp('visible'); }) 869 .sort(this.compareDepth.bind(this)) 870 .forEach(function (pt) { return this.board.renderer.setLayer(pt.element2D, pt.element2D.visProp.layer); }); 871 872 /* [DEBUG] list oriented box coordinates in depth order */ 873 // console.log('depth-ordered points in oriented box coordinates'); 874 // this.points 875 // .filter((pt) => pt.element2D.visProp.visible) 876 // .sort(compareDepth) 877 // .forEach(function (pt) { 878 // console.log(Mat.matVecMult(that.matrix3DRot, Mat.matVecMult(that.shift, pt.coords))); 879 // }); 880 } 881 882 return this; 883 }, 884 885 updateRenderer: function () { 886 this.needsUpdate = false; 887 return this; 888 }, 889 890 removeObject: function (object, saveMethod) { 891 var i; 892 893 // this.board.removeObject(object, saveMethod); 894 if (Type.isArray(object)) { 895 for (i = 0; i < object.length; i++) { 896 this.removeObject(object[i]); 897 } 898 return this; 899 } 900 901 object = this.select(object); 902 903 // // If the object which is about to be removed unknown or a string, do nothing. 904 // // it is a string if a string was given and could not be resolved to an element. 905 if (!Type.exists(object) || Type.isString(object)) { 906 return this; 907 } 908 909 try { 910 // // remove all children. 911 // for (el in object.childElements) { 912 // if (object.childElements.hasOwnProperty(el)) { 913 // object.childElements[el].board.removeObject(object.childElements[el]); 914 // } 915 // } 916 917 delete this.objects[object.id]; 918 } catch (e) { 919 JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e); 920 } 921 922 // this.update(); 923 924 this.board.removeObject(object, saveMethod); 925 926 return this; 927 }, 928 929 /** 930 * Map world coordinates to focal coordinates. These coordinate systems 931 * are explained in the {@link JXG.View3D#boxToCam} matrix 932 * documentation. 933 * 934 * @param {Array} pWorld A world space point, in homogeneous coordinates. 935 * @param {Boolean} [homog=true] Whether to return homogeneous coordinates. 936 * If false, projects down to ordinary coordinates. 937 */ 938 worldToFocal: function (pWorld, homog = true) { 939 var k, 940 pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld)); 941 pView[3] -= pView[0] * this.focalDist; 942 if (homog) { 943 return pView; 944 } else { 945 for (k = 1; k < 4; k++) { 946 pView[k] /= pView[0]; 947 } 948 return pView.slice(1, 4); 949 } 950 }, 951 952 /** 953 * Project 3D coordinates to 2D board coordinates 954 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 955 * 956 * @param {Number|Array} x 957 * @param {Number[]} y 958 * @param {Number[]} z 959 * @returns {Array} Array of length 3 containing the projection on to the board 960 * in homogeneous user coordinates. 961 */ 962 project3DTo2D: function (x, y, z) { 963 var vec, w; 964 if (arguments.length === 3) { 965 vec = [1, x, y, z]; 966 } else { 967 // Argument is an array 968 if (x.length === 3) { 969 // vec = [1].concat(x); 970 vec = x.slice(); 971 vec.unshift(1); 972 } else { 973 vec = x; 974 } 975 } 976 977 w = Mat.matVecMult(this.matrix3D, vec); 978 979 switch (this.projectionType) { 980 case 'central': 981 w[1] /= w[0]; 982 w[2] /= w[0]; 983 w[3] /= w[0]; 984 w[0] /= w[0]; 985 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3)); 986 987 case 'parallel': 988 default: 989 return w; 990 } 991 }, 992 993 /** 994 * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y. 995 * Setting R = mat^(-1) gives 996 * 1/ w0 * (1, x, y, d)^T = R * v2d. 997 * The first and the last row of this equation allows to determine 1/w0 and h. 998 * 999 * @param {Array} mat 1000 * @param {Array} v2d 1001 * @param {Number} d 1002 * @returns Array 1003 * @private 1004 */ 1005 _getW0: function (mat, v2d, d) { 1006 var R = Mat.inverse(mat), 1007 R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2], 1008 R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2], 1009 w, h, det; 1010 1011 det = d * R[0][3] - R[3][3]; 1012 w = (R2 * R[0][3] - R1 * R[3][3]) / det; 1013 h = (R2 - R1 * d) / det; 1014 return [1 / w, h]; 1015 }, 1016 1017 /** 1018 * Project a 2D coordinate to the plane defined by point "foot" 1019 * and the normal vector `normal`. 1020 * 1021 * @param {JXG.Point} point2d 1022 * @param {Array} normal 1023 * @param {Array} foot 1024 * @returns {Array} of length 4 containing the projected 1025 * point in homogeneous coordinates. 1026 */ 1027 project2DTo3DPlane: function (point2d, normal, foot) { 1028 var mat, rhs, d, le, sol, 1029 n = normal.slice(1), 1030 v2d, w0, res; 1031 1032 foot = foot || [1, 0, 0, 0]; 1033 le = Mat.norm(n, 3); 1034 d = Mat.innerProduct(foot.slice(1), n, 3) / le; 1035 1036 if (this.projectionType === 'parallel') { 1037 mat = this.matrix3D.slice(0, 3); // Copy each row by reference 1038 mat.push([0, n[0], n[1], n[2]]); 1039 1040 // 2D coordinates of point: 1041 rhs = point2d.coords.usrCoords.slice(); 1042 rhs.push(d); 1043 try { 1044 // Prevent singularity in case elevation angle is zero 1045 if (mat[2][3] === 1.0) { 1046 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 1047 } 1048 sol = Mat.Numerics.Gauss(mat, rhs); 1049 } catch (e) { 1050 sol = [0, NaN, NaN, NaN]; 1051 } 1052 } else { 1053 mat = this.matrix3D; 1054 1055 // 2D coordinates of point: 1056 rhs = point2d.coords.usrCoords.slice(); 1057 1058 v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs); 1059 res = this._getW0(mat, v2d, d); 1060 w0 = res[0]; 1061 rhs = [ 1062 v2d[0] * w0, 1063 v2d[1] * w0, 1064 v2d[2] * w0, 1065 res[1] * w0 1066 ]; 1067 try { 1068 // Prevent singularity in case elevation angle is zero 1069 if (mat[2][3] === 1.0) { 1070 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 1071 } 1072 1073 sol = Mat.Numerics.Gauss(mat, rhs); 1074 sol[1] /= sol[0]; 1075 sol[2] /= sol[0]; 1076 sol[3] /= sol[0]; 1077 // sol[3] = d; 1078 sol[0] /= sol[0]; 1079 } catch (err) { 1080 sol = [0, NaN, NaN, NaN]; 1081 } 1082 } 1083 1084 return sol; 1085 }, 1086 1087 /** 1088 * Project a point on the screen to the nearest point, in screen 1089 * distance, on a line segment in 3d space. The inputs must be in 1090 * ordinary coordinates, but the output is in homogeneous coordinates. 1091 * 1092 * @param {Array} pScr The screen coordinates of the point to project. 1093 * @param {Array} end0 The world space coordinates of one end of the 1094 * line segment. 1095 * @param {Array} end1 The world space coordinates of the other end of 1096 * the line segment. 1097 */ 1098 projectScreenToSegment: function (pScr, end0, end1) { 1099 var end0_2d = this.project3DTo2D(end0).slice(1, 3), 1100 end1_2d = this.project3DTo2D(end1).slice(1, 3), 1101 dir_2d = [ 1102 end1_2d[0] - end0_2d[0], 1103 end1_2d[1] - end0_2d[1] 1104 ], 1105 dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d), 1106 diff = [ 1107 pScr[0] - end0_2d[0], 1108 pScr[1] - end0_2d[1] 1109 ], 1110 s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter 1111 mid, mid_2d, mid_diff, m, 1112 1113 t, // view-space affine parameter 1114 t_clamped, // affine parameter clamped to range 1115 t_clamped_co; 1116 1117 if (this.projectionType === 'central') { 1118 mid = [ 1119 0.5 * (end0[0] + end1[0]), 1120 0.5 * (end0[1] + end1[1]), 1121 0.5 * (end0[2] + end1[2]) 1122 ]; 1123 mid_2d = this.project3DTo2D(mid).slice(1, 3); 1124 mid_diff = [ 1125 mid_2d[0] - end0_2d[0], 1126 mid_2d[1] - end0_2d[1] 1127 ]; 1128 m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq; 1129 1130 // the view-space affine parameter s is related to the 1131 // screen-space affine parameter t by a Möbius transformation, 1132 // which is determined by the following relations: 1133 // 1134 // s | t 1135 // ----- 1136 // 0 | 0 1137 // m | 1/2 1138 // 1 | 1 1139 // 1140 t = (1 - m) * s / ((1 - 2 * m) * s + m); 1141 } else { 1142 t = s; 1143 } 1144 1145 t_clamped = Math.min(Math.max(t, 0), 1); 1146 t_clamped_co = 1 - t_clamped; 1147 return [ 1148 1, 1149 t_clamped_co * end0[0] + t_clamped * end1[0], 1150 t_clamped_co * end0[1] + t_clamped * end1[1], 1151 t_clamped_co * end0[2] + t_clamped * end1[2] 1152 ]; 1153 }, 1154 1155 /** 1156 * Project a 2D coordinate to a new 3D position by keeping 1157 * the 3D x, y coordinates and changing only the z coordinate. 1158 * All horizontal moves of the 2D point are ignored. 1159 * 1160 * @param {JXG.Point} point2d 1161 * @param {Array} base_c3d 1162 * @returns {Array} of length 4 containing the projected 1163 * point in homogeneous coordinates. 1164 */ 1165 project2DTo3DVertical: function (point2d, base_c3d) { 1166 var pScr = point2d.coords.usrCoords.slice(1, 3), 1167 end0 = [base_c3d[1], base_c3d[2], this.bbox3D[2][0]], 1168 end1 = [base_c3d[1], base_c3d[2], this.bbox3D[2][1]]; 1169 1170 return this.projectScreenToSegment(pScr, end0, end1); 1171 }, 1172 1173 /** 1174 * Limit 3D coordinates to the bounding cube. 1175 * 1176 * @param {Array} c3d 3D coordinates [x,y,z] 1177 * @returns Array [Array, Boolean] containing [coords, corrected]. coords contains the updated 3D coordinates, 1178 * correct is true if the coords have been changed. 1179 */ 1180 project3DToCube: function (c3d) { 1181 var cube = this.bbox3D, 1182 isOut = false; 1183 1184 if (c3d[1] < cube[0][0]) { 1185 c3d[1] = cube[0][0]; 1186 isOut = true; 1187 } 1188 if (c3d[1] > cube[0][1]) { 1189 c3d[1] = cube[0][1]; 1190 isOut = true; 1191 } 1192 if (c3d[2] < cube[1][0]) { 1193 c3d[2] = cube[1][0]; 1194 isOut = true; 1195 } 1196 if (c3d[2] > cube[1][1]) { 1197 c3d[2] = cube[1][1]; 1198 isOut = true; 1199 } 1200 if (c3d[3] <= cube[2][0]) { 1201 c3d[3] = cube[2][0]; 1202 isOut = true; 1203 } 1204 if (c3d[3] >= cube[2][1]) { 1205 c3d[3] = cube[2][1]; 1206 isOut = true; 1207 } 1208 1209 return [c3d, isOut]; 1210 }, 1211 1212 /** 1213 * Intersect a ray with the bounding cube of the 3D view. 1214 * @param {Array} p 3D coordinates [x,y,z] 1215 * @param {Array} d 3D direction vector of the line (array of length 3) 1216 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 1217 * @returns Affine ratio of the intersection of the line with the cube. 1218 */ 1219 intersectionLineCube: function (p, d, r) { 1220 var r_n, i, r0, r1; 1221 1222 r_n = r; 1223 for (i = 0; i < 3; i++) { 1224 if (d[i] !== 0) { 1225 r0 = (this.bbox3D[i][0] - p[i]) / d[i]; 1226 r1 = (this.bbox3D[i][1] - p[i]) / d[i]; 1227 if (r < 0) { 1228 r_n = Math.max(r_n, Math.min(r0, r1)); 1229 } else { 1230 r_n = Math.min(r_n, Math.max(r0, r1)); 1231 } 1232 } 1233 } 1234 return r_n; 1235 }, 1236 1237 /** 1238 * Test if coordinates are inside of the bounding cube. 1239 * @param {array} q 3D coordinates [x,y,z] of a point. 1240 * @returns Boolean 1241 */ 1242 isInCube: function (q) { 1243 return ( 1244 q[0] > this.bbox3D[0][0] - Mat.eps && 1245 q[0] < this.bbox3D[0][1] + Mat.eps && 1246 q[1] > this.bbox3D[1][0] - Mat.eps && 1247 q[1] < this.bbox3D[1][1] + Mat.eps && 1248 q[2] > this.bbox3D[2][0] - Mat.eps && 1249 q[2] < this.bbox3D[2][1] + Mat.eps 1250 ); 1251 }, 1252 1253 /** 1254 * 1255 * @param {JXG.Plane3D} plane1 1256 * @param {JXG.Plane3D} plane2 1257 * @param {JXG.Plane3D} d 1258 * @returns {Array} of length 2 containing the coordinates of the defining points of 1259 * of the intersection segment. 1260 */ 1261 intersectionPlanePlane: function (plane1, plane2, d) { 1262 var ret = [[], []], 1263 p, 1264 dir, 1265 r, 1266 q; 1267 1268 d = d || plane2.d; 1269 1270 p = Mat.Geometry.meet3Planes( 1271 plane1.normal, 1272 plane1.d, 1273 plane2.normal, 1274 d, 1275 Mat.crossProduct(plane1.normal, plane2.normal), 1276 0 1277 ); 1278 dir = Mat.Geometry.meetPlanePlane( 1279 plane1.vec1, 1280 plane1.vec2, 1281 plane2.vec1, 1282 plane2.vec2 1283 ); 1284 r = this.intersectionLineCube(p, dir, Infinity); 1285 q = Mat.axpy(r, dir, p); 1286 if (this.isInCube(q)) { 1287 ret[0] = q; 1288 } 1289 r = this.intersectionLineCube(p, dir, -Infinity); 1290 q = Mat.axpy(r, dir, p); 1291 if (this.isInCube(q)) { 1292 ret[1] = q; 1293 } 1294 return ret; 1295 }, 1296 1297 /** 1298 * Generate mesh for a surface / plane. 1299 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 1300 * @param {Array|Function} func 1301 * @param {Array} interval_u 1302 * @param {Array} interval_v 1303 * @returns Array 1304 * @private 1305 * 1306 * @example 1307 * var el = view.create('curve', [[], []]); 1308 * el.updateDataArray = function () { 1309 * var steps_u = this.evalVisProp('stepsu'), 1310 * steps_v = this.evalVisProp('stepsv'), 1311 * r_u = Type.evaluate(this.range_u), 1312 * r_v = Type.evaluate(this.range_v), 1313 * func, ret; 1314 * 1315 * if (this.F !== null) { 1316 * func = this.F; 1317 * } else { 1318 * func = [this.X, this.Y, this.Z]; 1319 * } 1320 * ret = this.view.getMesh(func, 1321 * r_u.concat([steps_u]), 1322 * r_v.concat([steps_v])); 1323 * 1324 * this.dataX = ret[0]; 1325 * this.dataY = ret[1]; 1326 * }; 1327 * 1328 */ 1329 getMesh: function (func, interval_u, interval_v) { 1330 var i_u, i_v, u, v, 1331 c2d, delta_u, delta_v, 1332 p = [0, 0, 0], 1333 steps_u = interval_u[2], 1334 steps_v = interval_v[2], 1335 dataX = [], 1336 dataY = []; 1337 1338 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u; 1339 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v; 1340 1341 for (i_u = 0; i_u <= steps_u; i_u++) { 1342 u = interval_u[0] + delta_u * i_u; 1343 for (i_v = 0; i_v <= steps_v; i_v++) { 1344 v = interval_v[0] + delta_v * i_v; 1345 if (Type.isFunction(func)) { 1346 p = func(u, v); 1347 } else { 1348 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1349 } 1350 c2d = this.project3DTo2D(p); 1351 dataX.push(c2d[1]); 1352 dataY.push(c2d[2]); 1353 } 1354 dataX.push(NaN); 1355 dataY.push(NaN); 1356 } 1357 1358 for (i_v = 0; i_v <= steps_v; i_v++) { 1359 v = interval_v[0] + delta_v * i_v; 1360 for (i_u = 0; i_u <= steps_u; i_u++) { 1361 u = interval_u[0] + delta_u * i_u; 1362 if (Type.isFunction(func)) { 1363 p = func(u, v); 1364 } else { 1365 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 1366 } 1367 c2d = this.project3DTo2D(p); 1368 dataX.push(c2d[1]); 1369 dataY.push(c2d[2]); 1370 } 1371 dataX.push(NaN); 1372 dataY.push(NaN); 1373 } 1374 1375 return [dataX, dataY]; 1376 }, 1377 1378 /** 1379 * 1380 */ 1381 animateAzimuth: function () { 1382 var s = this.az_slide._smin, 1383 e = this.az_slide._smax, 1384 sdiff = e - s, 1385 newVal = this.az_slide.Value() + 0.1; 1386 1387 this.az_slide.position = (newVal - s) / sdiff; 1388 if (this.az_slide.position > 1) { 1389 this.az_slide.position = 0.0; 1390 } 1391 this.board._change3DView = true; 1392 this.board.update(); 1393 this.board._change3DView = false; 1394 1395 this.timeoutAzimuth = setTimeout(function () { 1396 this.animateAzimuth(); 1397 }.bind(this), 200); 1398 }, 1399 1400 /** 1401 * 1402 */ 1403 stopAzimuth: function () { 1404 clearTimeout(this.timeoutAzimuth); 1405 this.timeoutAzimuth = null; 1406 }, 1407 1408 /** 1409 * Check if vertical dragging is enabled and which action is needed. 1410 * Default is shiftKey. 1411 * 1412 * @returns Boolean 1413 * @private 1414 */ 1415 isVerticalDrag: function () { 1416 var b = this.board, 1417 key; 1418 if (!this.evalVisProp('verticaldrag.enabled')) { 1419 return false; 1420 } 1421 key = '_' + this.evalVisProp('verticaldrag.key') + 'Key'; 1422 return b[key]; 1423 }, 1424 1425 /** 1426 * Sets camera view to the given values. 1427 * 1428 * @param {Number} az Value of azimuth. 1429 * @param {Number} el Value of elevation. 1430 * @param {Number} [r] Value of radius. 1431 * 1432 * @returns {Object} Reference to the view. 1433 */ 1434 setView: function (az, el, r) { 1435 r = r || this.r; 1436 1437 this.az_slide.setValue(az); 1438 this.el_slide.setValue(el); 1439 this.r = r; 1440 this.board.update(); 1441 1442 return this; 1443 }, 1444 1445 /** 1446 * Changes view to the next view stored in the attribute `values`. 1447 * 1448 * @see View3D#values 1449 * 1450 * @returns {Object} Reference to the view. 1451 */ 1452 nextView: function () { 1453 var views = this.evalVisProp('values'), 1454 n = this.visProp._currentview; 1455 1456 n = (n + 1) % views.length; 1457 this.setCurrentView(n); 1458 1459 return this; 1460 }, 1461 1462 /** 1463 * Changes view to the previous view stored in the attribute `values`. 1464 * 1465 * @see View3D#values 1466 * 1467 * @returns {Object} Reference to the view. 1468 */ 1469 previousView: function () { 1470 var views = this.evalVisProp('values'), 1471 n = this.visProp._currentview; 1472 1473 n = (n + views.length - 1) % views.length; 1474 this.setCurrentView(n); 1475 1476 return this; 1477 }, 1478 1479 /** 1480 * Changes view to the determined view stored in the attribute `values`. 1481 * 1482 * @see View3D#values 1483 * 1484 * @param {Number} n Index of view in attribute `values`. 1485 * @returns {Object} Reference to the view. 1486 */ 1487 setCurrentView: function (n) { 1488 var views = this.evalVisProp('values'); 1489 1490 if (n < 0 || n >= views.length) { 1491 n = ((n % views.length) + views.length) % views.length; 1492 } 1493 1494 this.setView(views[n][0], views[n][1], views[n][2]); 1495 this.visProp._currentview = n; 1496 1497 return this; 1498 }, 1499 1500 /** 1501 * Controls the navigation in az direction using either the keyboard or a pointer. 1502 * 1503 * @private 1504 * 1505 * @param {event} evt either the keydown or the pointer event 1506 * @returns view 1507 */ 1508 _azEventHandler: function (evt) { 1509 var smax = this.az_slide._smax, 1510 smin = this.az_slide._smin, 1511 speed = (smax - smin) / this.board.canvasWidth * (this.evalVisProp('az.pointer.speed')), 1512 delta = evt.movementX, 1513 az = this.az_slide.Value(), 1514 el = this.el_slide.Value(); 1515 1516 // Doesn't allow navigation if another moving event is triggered 1517 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1518 return this; 1519 } 1520 1521 // Calculate new az value if keyboard events are triggered 1522 // Plus if right-button, minus if left-button 1523 if (this.evalVisProp('az.keyboard.enabled')) { 1524 if (evt.key === 'ArrowRight') { 1525 az = az + this.evalVisProp('az.keyboard.step') * Math.PI / 180; 1526 } else if (evt.key === 'ArrowLeft') { 1527 az = az - this.evalVisProp('az.keyboard.step') * Math.PI / 180; 1528 } 1529 } 1530 1531 if (this.evalVisProp('az.pointer.enabled') && (delta !== 0) && evt.key == null) { 1532 az += delta * speed; 1533 } 1534 1535 // Project the calculated az value to a usable value in the interval [smin,smax] 1536 // Use modulo if continuous is true 1537 if (this.evalVisProp('az.continuous')) { 1538 az = Mat.wrap(az, smin, smax); 1539 } else { 1540 if (az > 0) { 1541 az = Math.min(smax, az); 1542 } else if (az < 0) { 1543 az = Math.max(smin, az); 1544 } 1545 } 1546 1547 this.setView(az, el); 1548 return this; 1549 }, 1550 1551 /** 1552 * Controls the navigation in el direction using either the keyboard or a pointer. 1553 * 1554 * @private 1555 * 1556 * @param {event} evt either the keydown or the pointer event 1557 * @returns view 1558 */ 1559 _elEventHandler: function (evt) { 1560 var smax = this.el_slide._smax, 1561 smin = this.el_slide._smin, 1562 speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('el.pointer.speed'), 1563 delta = evt.movementY, 1564 az = this.az_slide.Value(), 1565 el = this.el_slide.Value(); 1566 1567 // Doesn't allow navigation if another moving event is triggered 1568 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1569 return this; 1570 } 1571 1572 // Calculate new az value if keyboard events are triggered 1573 // Plus if down-button, minus if up-button 1574 if (this.evalVisProp('el.keyboard.enabled')) { 1575 if (evt.key === 'ArrowUp') { 1576 el = el - this.evalVisProp('el.keyboard.step') * Math.PI / 180; 1577 } else if (evt.key === 'ArrowDown') { 1578 el = el + this.evalVisProp('el.keyboard.step') * Math.PI / 180; 1579 } 1580 } 1581 1582 if (this.evalVisProp('el.pointer.enabled') && (delta !== 0) && evt.key == null) { 1583 el += delta * speed; 1584 } 1585 1586 // Project the calculated el value to a usable value in the interval [smin,smax] 1587 // Use modulo if continuous is true and the trackball is disabled 1588 if (this.evalVisProp('el.continuous') && !this.trackballEnabled) { 1589 el = Mat.wrap(el, smin, smax); 1590 } else { 1591 if (el > 0) { 1592 el = Math.min(smax, el); 1593 } else if (el < 0) { 1594 el = Math.max(smin, el); 1595 } 1596 } 1597 1598 this.setView(az, el); 1599 1600 return this; 1601 }, 1602 1603 /** 1604 * Controls the navigation in bank direction using either the keyboard or a pointer. 1605 * 1606 * @private 1607 * 1608 * @param {event} evt either the keydown or the pointer event 1609 * @returns view 1610 */ 1611 _bankEventHandler: function (evt) { 1612 var smax = this.bank_slide._smax, 1613 smin = this.bank_slide._smin, 1614 step, speed, 1615 delta = evt.deltaY, 1616 bank = this.bank_slide.Value(); 1617 1618 // Doesn't allow navigation if another moving event is triggered 1619 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 1620 return this; 1621 } 1622 1623 // Calculate new bank value if keyboard events are triggered 1624 // Plus if down-button, minus if up-button 1625 if (this.evalVisProp('bank.keyboard.enabled')) { 1626 step = this.evalVisProp('bank.keyboard.step') * Math.PI / 180; 1627 if (evt.key === '.' || evt.key === '<') { 1628 bank -= step; 1629 } else if (evt.key === ',' || evt.key === '>') { 1630 bank += step; 1631 } 1632 } 1633 1634 if (this.evalVisProp('bank.pointer.enabled') && (delta !== 0) && evt.key == null) { 1635 speed = (smax - smin) / this.board.canvasHeight * this.evalVisProp('bank.pointer.speed'); 1636 bank += delta * speed; 1637 1638 // prevent the pointer wheel from scrolling the page 1639 evt.preventDefault(); 1640 } 1641 1642 // Project the calculated bank value to a usable value in the interval [smin,smax] 1643 if (this.evalVisProp('bank.continuous')) { 1644 // in continuous mode, wrap value around slider range 1645 bank = Mat.wrap(bank, smin, smax); 1646 } else { 1647 // in non-continuous mode, clamp value to slider range 1648 bank = Mat.clamp(bank, smin, smax); 1649 } 1650 1651 this.bank_slide.setValue(bank); 1652 this.board.update(); 1653 return this; 1654 }, 1655 1656 /** 1657 * Controls the navigation using either virtual trackball. 1658 * 1659 * @private 1660 * 1661 * @param {event} evt either the keydown or the pointer event 1662 * @returns view 1663 */ 1664 _trackballHandler: function (evt) { 1665 var pos = this.board.getMousePosition(evt), 1666 x, y, center; 1667 1668 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); 1669 x = pos[0] - center.scrCoords[1]; 1670 y = pos[1] - center.scrCoords[2]; 1671 this._trackball = { 1672 dx: evt.movementX, 1673 dy: -evt.movementY, 1674 x: x, 1675 y: -y 1676 }; 1677 this.board.update(); 1678 return this; 1679 }, 1680 1681 /** 1682 * Event handler for pointer down event. Triggers handling of all 3D navigation. 1683 * 1684 * @private 1685 * @param {event} evt 1686 * @returns view 1687 */ 1688 pointerDownHandler: function (evt) { 1689 var neededButton, neededKey, target; 1690 1691 this._hasMoveAz = false; 1692 this._hasMoveEl = false; 1693 this._hasMoveBank = false; 1694 this._hasMoveTrackball = false; 1695 1696 if (this.board.mode !== this.board.BOARD_MODE_NONE) { 1697 return; 1698 } 1699 1700 this.board._change3DView = true; 1701 1702 if (this.evalVisProp('trackball.enabled')) { 1703 neededButton = this.evalVisProp('trackball.button'); 1704 neededKey = this.evalVisProp('trackball.key'); 1705 1706 // Move events for virtual trackball 1707 if ( 1708 (neededButton === -1 || neededButton === evt.button) && 1709 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1710 ) { 1711 // If outside is true then the event listener is bound to the document, otherwise to the div 1712 target = (this.evalVisProp('trackball.outside')) ? document : this.board.containerObj; 1713 Env.addEvent(target, 'pointermove', this._trackballHandler, this); 1714 this._hasMoveTrackball = true; 1715 } 1716 } else { 1717 if (this.evalVisProp('az.pointer.enabled')) { 1718 neededButton = this.evalVisProp('az.pointer.button'); 1719 neededKey = this.evalVisProp('az.pointer.key'); 1720 1721 // Move events for azimuth 1722 if ( 1723 (neededButton === -1 || neededButton === evt.button) && 1724 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1725 ) { 1726 // If outside is true then the event listener is bound to the document, otherwise to the div 1727 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 1728 Env.addEvent(target, 'pointermove', this._azEventHandler, this); 1729 this._hasMoveAz = true; 1730 } 1731 } 1732 1733 if (this.evalVisProp('el.pointer.enabled')) { 1734 neededButton = this.evalVisProp('el.pointer.button'); 1735 neededKey = this.evalVisProp('el.pointer.key'); 1736 1737 // Events for elevation 1738 if ( 1739 (neededButton === -1 || neededButton === evt.button) && 1740 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1741 ) { 1742 // If outside is true then the event listener is bound to the document, otherwise to the div 1743 target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj; 1744 Env.addEvent(target, 'pointermove', this._elEventHandler, this); 1745 this._hasMoveEl = true; 1746 } 1747 } 1748 1749 if (this.evalVisProp('bank.pointer.enabled')) { 1750 neededButton = this.evalVisProp('bank.pointer.button'); 1751 neededKey = this.evalVisProp('bank.pointer.key'); 1752 1753 // Events for bank 1754 if ( 1755 (neededButton === -1 || neededButton === evt.button) && 1756 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey)) 1757 ) { 1758 // If `outside` is true, we bind the event listener to 1759 // the document. otherwise, we bind it to the div. we 1760 // register the event listener as active so it can 1761 // prevent the pointer wheel from scrolling the page 1762 target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj; 1763 Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false }); 1764 this._hasMoveBank = true; 1765 } 1766 } 1767 } 1768 Env.addEvent(document, 'pointerup', this.pointerUpHandler, this); 1769 }, 1770 1771 /** 1772 * Event handler for pointer up event. Triggers handling of all 3D navigation. 1773 * 1774 * @private 1775 * @param {event} evt 1776 * @returns view 1777 */ 1778 pointerUpHandler: function (evt) { 1779 var target; 1780 if (this._hasMoveAz) { 1781 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 1782 Env.removeEvent(target, 'pointermove', this._azEventHandler, this); 1783 this._hasMoveAz = false; 1784 } 1785 if (this._hasMoveEl) { 1786 target = (this.evalVisProp('el.pointer.outside')) ? document : this.board.containerObj; 1787 Env.removeEvent(target, 'pointermove', this._elEventHandler, this); 1788 this._hasMoveEl = false; 1789 } 1790 if (this._hasMoveBank) { 1791 target = (this.evalVisProp('bank.pointer.outside')) ? document : this.board.containerObj; 1792 Env.removeEvent(target, 'wheel', this._bankEventHandler, this); 1793 this._hasMoveBank = false; 1794 } 1795 if (this._hasMoveTrackball) { 1796 target = (this.evalVisProp('az.pointer.outside')) ? document : this.board.containerObj; 1797 Env.removeEvent(target, 'pointermove', this._trackballHandler, this); 1798 this._hasMoveTrackball = false; 1799 } 1800 Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this); 1801 this.board._change3DView = false; 1802 1803 } 1804 }); 1805 1806 /** 1807 * @class This element creates a 3D view. 1808 * @pseudo 1809 * @description A View3D element provides the container and the methods to create and display 3D elements. 1810 * It is contained in a JSXGraph board. 1811 * <p> 1812 * It is advisable to disable panning of the board by setting the board attribute "pan": 1813 * <pre> 1814 * pan: {anabled: fasle} 1815 * </pre> 1816 * Otherwise users will not be able to rotate the scene with their fingers on a touch device. 1817 * 1818 * @name View3D 1819 * @augments JXG.View3D 1820 * @constructor 1821 * @type Object 1822 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 1823 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 1824 * dim is an array of the form [w, h]. 1825 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 1826 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 1827 * [x,y] and side lengths [w, h] of the board. 1828 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 1829 * which determines the coordinate ranges of the 3D cube. 1830 * 1831 * @example 1832 * var bound = [-4, 6]; 1833 * var view = board.create('view3d', 1834 * [[-4, -3], [8, 8], 1835 * [bound, bound, bound]], 1836 * { 1837 * projection: 'parallel', 1838 * trackball: {enabled:true}, 1839 * }); 1840 * 1841 * var curve = view.create('curve3d', [ 1842 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1843 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1844 * (t) => Math.sin(3 * t), 1845 * [-Math.PI, Math.PI] 1846 * ], { strokeWidth: 4 }); 1847 * 1848 * </pre><div id="JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b" class="jxgbox" style="width: 300px; height: 300px;"></div> 1849 * <script type="text/javascript"> 1850 * (function() { 1851 * var board = JXG.JSXGraph.initBoard('JXG9b327a6c-1bd6-4e40-a502-59d024dbfd1b', 1852 * {boundingbox: [-8, 8, 8,-8], pan: {enabled: false}, axis: false, showcopyright: false, shownavigation: false}); 1853 * var bound = [-4, 6]; 1854 * var view = board.create('view3d', 1855 * [[-4, -3], [8, 8], 1856 * [bound, bound, bound]], 1857 * { 1858 * projection: 'parallel', 1859 * trackball: {enabled:true}, 1860 * }); 1861 * 1862 * var curve = view.create('curve3d', [ 1863 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1864 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1865 * (t) => Math.sin(3 * t), 1866 * [-Math.PI, Math.PI] 1867 * ], { strokeWidth: 4 }); 1868 * 1869 * })(); 1870 * 1871 * </script><pre> 1872 * 1873 * @example 1874 * var bound = [-4, 6]; 1875 * var view = board.create('view3d', 1876 * [[-4, -3], [8, 8], 1877 * [bound, bound, bound]], 1878 * { 1879 * projection: 'central', 1880 * trackball: {enabled:true}, 1881 * 1882 * xPlaneRear: { visible: false }, 1883 * yPlaneRear: { visible: false } 1884 * 1885 * }); 1886 * 1887 * var curve = view.create('curve3d', [ 1888 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1889 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1890 * (t) => Math.sin(3 * t), 1891 * [-Math.PI, Math.PI] 1892 * ], { strokeWidth: 4 }); 1893 * 1894 * </pre><div id="JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007" class="jxgbox" style="width: 300px; height: 300px;"></div> 1895 * <script type="text/javascript"> 1896 * (function() { 1897 * var board = JXG.JSXGraph.initBoard('JXG0dc2493d-fb2f-40d5-bdb8-762ba0ad2007', 1898 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 1899 * var bound = [-4, 6]; 1900 * var view = board.create('view3d', 1901 * [[-4, -3], [8, 8], 1902 * [bound, bound, bound]], 1903 * { 1904 * projection: 'central', 1905 * trackball: {enabled:true}, 1906 * 1907 * xPlaneRear: { visible: false }, 1908 * yPlaneRear: { visible: false } 1909 * 1910 * }); 1911 * 1912 * var curve = view.create('curve3d', [ 1913 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1914 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1915 * (t) => Math.sin(3 * t), 1916 * [-Math.PI, Math.PI] 1917 * ], { strokeWidth: 4 }); 1918 * 1919 * })(); 1920 * 1921 * </script><pre> 1922 * 1923 * @example 1924 * var bound = [-4, 6]; 1925 * var view = board.create('view3d', 1926 * [[-4, -3], [8, 8], 1927 * [bound, bound, bound]], 1928 * { 1929 * projection: 'central', 1930 * trackball: {enabled:true}, 1931 * 1932 * // Main axes 1933 * axesPosition: 'border', 1934 * 1935 * // Axes at the border 1936 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 1937 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 1938 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 1939 * 1940 * // No axes on planes 1941 * xPlaneRearYAxis: {visible: false}, 1942 * xPlaneRearZAxis: {visible: false}, 1943 * yPlaneRearXAxis: {visible: false}, 1944 * yPlaneRearZAxis: {visible: false}, 1945 * zPlaneRearXAxis: {visible: false}, 1946 * zPlaneRearYAxis: {visible: false} 1947 * }); 1948 * 1949 * var curve = view.create('curve3d', [ 1950 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1951 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1952 * (t) => Math.sin(3 * t), 1953 * [-Math.PI, Math.PI] 1954 * ], { strokeWidth: 4 }); 1955 * 1956 * </pre><div id="JXG586f3551-335c-47e9-8d72-835409f6a103" class="jxgbox" style="width: 300px; height: 300px;"></div> 1957 * <script type="text/javascript"> 1958 * (function() { 1959 * var board = JXG.JSXGraph.initBoard('JXG586f3551-335c-47e9-8d72-835409f6a103', 1960 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 1961 * var bound = [-4, 6]; 1962 * var view = board.create('view3d', 1963 * [[-4, -3], [8, 8], 1964 * [bound, bound, bound]], 1965 * { 1966 * projection: 'central', 1967 * trackball: {enabled:true}, 1968 * 1969 * // Main axes 1970 * axesPosition: 'border', 1971 * 1972 * // Axes at the border 1973 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 1974 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 1975 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 1976 * 1977 * // No axes on planes 1978 * xPlaneRearYAxis: {visible: false}, 1979 * xPlaneRearZAxis: {visible: false}, 1980 * yPlaneRearXAxis: {visible: false}, 1981 * yPlaneRearZAxis: {visible: false}, 1982 * zPlaneRearXAxis: {visible: false}, 1983 * zPlaneRearYAxis: {visible: false} 1984 * }); 1985 * 1986 * var curve = view.create('curve3d', [ 1987 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 1988 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 1989 * (t) => Math.sin(3 * t), 1990 * [-Math.PI, Math.PI] 1991 * ], { strokeWidth: 4 }); 1992 * 1993 * })(); 1994 * 1995 * </script><pre> 1996 * 1997 * @example 1998 * var bound = [-4, 6]; 1999 * var view = board.create('view3d', 2000 * [[-4, -3], [8, 8], 2001 * [bound, bound, bound]], 2002 * { 2003 * projection: 'central', 2004 * trackball: {enabled:true}, 2005 * 2006 * axesPosition: 'none' 2007 * }); 2008 * 2009 * var curve = view.create('curve3d', [ 2010 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2011 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2012 * (t) => Math.sin(3 * t), 2013 * [-Math.PI, Math.PI] 2014 * ], { strokeWidth: 4 }); 2015 * 2016 * </pre><div id="JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26" class="jxgbox" style="width: 300px; height: 300px;"></div> 2017 * <script type="text/javascript"> 2018 * (function() { 2019 * var board = JXG.JSXGraph.initBoard('JXG9a9467e1-f189-4c8c-adb2-d4f49bc7fa26', 2020 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2021 * var bound = [-4, 6]; 2022 * var view = board.create('view3d', 2023 * [[-4, -3], [8, 8], 2024 * [bound, bound, bound]], 2025 * { 2026 * projection: 'central', 2027 * trackball: {enabled:true}, 2028 * 2029 * axesPosition: 'none' 2030 * }); 2031 * 2032 * var curve = view.create('curve3d', [ 2033 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2034 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2035 * (t) => Math.sin(3 * t), 2036 * [-Math.PI, Math.PI] 2037 * ], { strokeWidth: 4 }); 2038 * 2039 * })(); 2040 * 2041 * </script><pre> 2042 * 2043 * @example 2044 * var bound = [-4, 6]; 2045 * var view = board.create('view3d', 2046 * [[-4, -3], [8, 8], 2047 * [bound, bound, bound]], 2048 * { 2049 * projection: 'central', 2050 * trackball: {enabled:true}, 2051 * 2052 * // Main axes 2053 * axesPosition: 'border', 2054 * 2055 * // Axes at the border 2056 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2057 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2058 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2059 * 2060 * xPlaneRear: { 2061 * fillColor: '#fff', 2062 * mesh3d: {visible: false} 2063 * }, 2064 * yPlaneRear: { 2065 * fillColor: '#fff', 2066 * mesh3d: {visible: false} 2067 * }, 2068 * zPlaneRear: { 2069 * fillColor: '#fff', 2070 * mesh3d: {visible: false} 2071 * }, 2072 * xPlaneFront: { 2073 * visible: true, 2074 * fillColor: '#fff', 2075 * mesh3d: {visible: false} 2076 * }, 2077 * yPlaneFront: { 2078 * visible: true, 2079 * fillColor: '#fff', 2080 * mesh3d: {visible: false} 2081 * }, 2082 * zPlaneFront: { 2083 * visible: true, 2084 * fillColor: '#fff', 2085 * mesh3d: {visible: false} 2086 * }, 2087 * 2088 * // No axes on planes 2089 * xPlaneRearYAxis: {visible: false}, 2090 * xPlaneRearZAxis: {visible: false}, 2091 * yPlaneRearXAxis: {visible: false}, 2092 * yPlaneRearZAxis: {visible: false}, 2093 * zPlaneRearXAxis: {visible: false}, 2094 * zPlaneRearYAxis: {visible: false}, 2095 * xPlaneFrontYAxis: {visible: false}, 2096 * xPlaneFrontZAxis: {visible: false}, 2097 * yPlaneFrontXAxis: {visible: false}, 2098 * yPlaneFrontZAxis: {visible: false}, 2099 * zPlaneFrontXAxis: {visible: false}, 2100 * zPlaneFrontYAxis: {visible: false} 2101 * 2102 * }); 2103 * 2104 * var curve = view.create('curve3d', [ 2105 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2106 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2107 * (t) => Math.sin(3 * t), 2108 * [-Math.PI, Math.PI] 2109 * ], { strokeWidth: 4 }); 2110 * 2111 * </pre><div id="JXGbd41a4e3-1bf7-4764-b675-98b01667103b" class="jxgbox" style="width: 300px; height: 300px;"></div> 2112 * <script type="text/javascript"> 2113 * (function() { 2114 * var board = JXG.JSXGraph.initBoard('JXGbd41a4e3-1bf7-4764-b675-98b01667103b', 2115 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2116 * var bound = [-4, 6]; 2117 * var view = board.create('view3d', 2118 * [[-4, -3], [8, 8], 2119 * [bound, bound, bound]], 2120 * { 2121 * projection: 'central', 2122 * trackball: {enabled:true}, 2123 * 2124 * // Main axes 2125 * axesPosition: 'border', 2126 * 2127 * // Axes at the border 2128 * xAxisBorder: { ticks3d: { ticksDistance: 2} }, 2129 * yAxisBorder: { ticks3d: { ticksDistance: 2} }, 2130 * zAxisBorder: { ticks3d: { ticksDistance: 2} }, 2131 * 2132 * xPlaneRear: { 2133 * fillColor: '#fff', 2134 * mesh3d: {visible: false} 2135 * }, 2136 * yPlaneRear: { 2137 * fillColor: '#fff', 2138 * mesh3d: {visible: false} 2139 * }, 2140 * zPlaneRear: { 2141 * fillColor: '#fff', 2142 * mesh3d: {visible: false} 2143 * }, 2144 * xPlaneFront: { 2145 * visible: true, 2146 * fillColor: '#fff', 2147 * mesh3d: {visible: false} 2148 * }, 2149 * yPlaneFront: { 2150 * visible: true, 2151 * fillColor: '#fff', 2152 * mesh3d: {visible: false} 2153 * }, 2154 * zPlaneFront: { 2155 * visible: true, 2156 * fillColor: '#fff', 2157 * mesh3d: {visible: false} 2158 * }, 2159 * 2160 * // No axes on planes 2161 * xPlaneRearYAxis: {visible: false}, 2162 * xPlaneRearZAxis: {visible: false}, 2163 * yPlaneRearXAxis: {visible: false}, 2164 * yPlaneRearZAxis: {visible: false}, 2165 * zPlaneRearXAxis: {visible: false}, 2166 * zPlaneRearYAxis: {visible: false}, 2167 * xPlaneFrontYAxis: {visible: false}, 2168 * xPlaneFrontZAxis: {visible: false}, 2169 * yPlaneFrontXAxis: {visible: false}, 2170 * yPlaneFrontZAxis: {visible: false}, 2171 * zPlaneFrontXAxis: {visible: false}, 2172 * zPlaneFrontYAxis: {visible: false} 2173 * 2174 * }); 2175 * 2176 * var curve = view.create('curve3d', [ 2177 * (t) => (2 + Math.cos(3 * t)) * Math.cos(2 * t), 2178 * (t) => (2 + Math.cos(3 * t)) * Math.sin(2 * t), 2179 * (t) => Math.sin(3 * t), 2180 * [-Math.PI, Math.PI] 2181 * ], { strokeWidth: 4 }); 2182 * })(); 2183 * 2184 * </script><pre> 2185 * 2186 * @example 2187 * var bound = [-5, 5]; 2188 * var view = board.create('view3d', 2189 * [[-6, -3], 2190 * [8, 8], 2191 * [bound, bound, bound]], 2192 * { 2193 * // Main axes 2194 * axesPosition: 'center', 2195 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 2196 * 2197 * // Planes 2198 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 2199 * yPlaneFront: { visible: true, fillColor: 'blue'}, 2200 * 2201 * // Axes on planes 2202 * xPlaneRearYAxis: {strokeColor: 'red'}, 2203 * xPlaneRearZAxis: {strokeColor: 'red'}, 2204 * 2205 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 2206 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 2207 * 2208 * zPlaneFrontXAxis: {visible: false}, 2209 * zPlaneFrontYAxis: {visible: false} 2210 * }); 2211 * 2212 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 2213 * <script type="text/javascript"> 2214 * (function() { 2215 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 2216 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 2217 * var bound = [-5, 5]; 2218 * var view = board.create('view3d', 2219 * [[-6, -3], [8, 8], 2220 * [bound, bound, bound]], 2221 * { 2222 * // Main axes 2223 * axesPosition: 'center', 2224 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 2225 * // Planes 2226 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 2227 * yPlaneFront: { visible: true, fillColor: 'blue'}, 2228 * // Axes on planes 2229 * xPlaneRearYAxis: {strokeColor: 'red'}, 2230 * xPlaneRearZAxis: {strokeColor: 'red'}, 2231 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 2232 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 2233 * zPlaneFrontXAxis: {visible: false}, 2234 * zPlaneFrontYAxis: {visible: false} 2235 * }); 2236 * })(); 2237 * 2238 * </script><pre> 2239 * 2240 */ 2241 JXG.createView3D = function (board, parents, attributes) { 2242 var view, attr, attr_az, attr_el, attr_bank, 2243 x, y, w, h, 2244 coords = parents[0], // llft corner 2245 size = parents[1]; // [w, h] 2246 2247 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 2248 view = new JXG.View3D(board, parents, attr); 2249 view.defaultAxes = view.create('axes3d', [], attr); 2250 2251 x = coords[0]; 2252 y = coords[1]; 2253 w = size[0]; 2254 h = size[1]; 2255 2256 attr_az = Type.copyAttributes(attr, board.options, 'view3d', 'az', 'slider'); 2257 attr_az.name = 'az'; 2258 2259 attr_el = Type.copyAttributes(attr, board.options, 'view3d', 'el', 'slider'); 2260 attr_el.name = 'el'; 2261 2262 attr_bank = Type.copyAttributes(attr, board.options, 'view3d', 'bank', 'slider'); 2263 attr_bank.name = 'bank'; 2264 2265 /** 2266 * Slider to adapt azimuth angle 2267 * @name JXG.View3D#az_slide 2268 * @type {Slider} 2269 */ 2270 view.az_slide = board.create( 2271 'slider', 2272 [ 2273 [x - 1, y - 2], 2274 [x + w + 1, y - 2], 2275 [ 2276 Type.evaluate(attr_az.min), 2277 Type.evaluate(attr_az.start), 2278 Type.evaluate(attr_az.max) 2279 ] 2280 ], 2281 attr_az 2282 ); 2283 view.inherits.push(view.az_slide); 2284 view.az_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate() 2285 2286 /** 2287 * Slider to adapt elevation angle 2288 * 2289 * @name JXG.View3D#el_slide 2290 * @type {Slider} 2291 */ 2292 view.el_slide = board.create( 2293 'slider', 2294 [ 2295 [x - 1, y], 2296 [x - 1, y + h], 2297 [ 2298 Type.evaluate(attr_el.min), 2299 Type.evaluate(attr_el.start), 2300 Type.evaluate(attr_el.max)] 2301 ], 2302 attr_el 2303 ); 2304 view.inherits.push(view.el_slide); 2305 view.el_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate() 2306 2307 /** 2308 * Slider to adjust bank angle 2309 * 2310 * @name JXG.View3D#bank_slide 2311 * @type {Slider} 2312 */ 2313 view.bank_slide = board.create( 2314 'slider', 2315 [ 2316 [x - 1, y + h + 2], 2317 [x + w + 1, y + h + 2], 2318 [ 2319 Type.evaluate(attr_bank.min), 2320 Type.evaluate(attr_bank.start), 2321 Type.evaluate(attr_bank.max) 2322 ] 2323 ], 2324 attr_bank 2325 ); 2326 view.inherits.push(view.bank_slide); 2327 view.bank_slide.elType = 'view3d_slider'; // Used in board.prepareUpdate() 2328 2329 // Set special infobox attributes of view3d.infobox 2330 // Using setAttribute() is not possible here, since we have to 2331 // avoid a call of board.update(). 2332 view.board.infobox.visProp = Type.merge(view.board.infobox.visProp, attr.infobox); 2333 2334 // 3d infobox: drag direction and coordinates 2335 view.board.highlightInfobox = function (x, y, el) { 2336 var d, i, c3d, foot, 2337 pre = '', 2338 brd = el.board, 2339 arr, infobox, 2340 p = null; 2341 2342 if (this.mode === this.BOARD_MODE_DRAG) { 2343 // Drag direction is only shown during dragging 2344 if (view.isVerticalDrag()) { 2345 pre = '<span style="color:black; font-size:200%">\u21C5 </span>'; 2346 } else { 2347 pre = '<span style="color:black; font-size:200%">\u21C4 </span>'; 2348 } 2349 } 2350 2351 // Search 3D parent 2352 for (i = 0; i < el.parents.length; i++) { 2353 p = brd.objects[el.parents[i]]; 2354 if (p.is3D) { 2355 break; 2356 } 2357 } 2358 if (p) { 2359 foot = [1, 0, 0, p.coords[3]]; 2360 view._w0 = Mat.innerProduct(view.matrix3D[0], foot, 4); 2361 2362 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 2363 if (!view.isInCube(c3d)) { 2364 view.board.highlightCustomInfobox('', p); 2365 return; 2366 } 2367 d = p.evalVisProp('infoboxdigits'); 2368 infobox = view.board.infobox; 2369 if (d === 'auto') { 2370 if (infobox.useLocale()) { 2371 arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')']; 2372 } else { 2373 arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')']; 2374 } 2375 2376 } else { 2377 if (infobox.useLocale()) { 2378 arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')']; 2379 } else { 2380 arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')']; 2381 } 2382 } 2383 view.board.highlightCustomInfobox(arr.join(''), p); 2384 } else { 2385 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 2386 } 2387 }; 2388 2389 // Hack needed to enable addEvent for view3D: 2390 view.BOARD_MODE_NONE = 0x0000; 2391 2392 // Add events for the keyboard navigation 2393 Env.addEvent(board.containerObj, 'keydown', function (event) { 2394 var neededKey, 2395 catchEvt = false; 2396 2397 // this.board._change3DView = true; 2398 if (view.evalVisProp('el.keyboard.enabled') && 2399 (event.key === 'ArrowUp' || event.key === 'ArrowDown') 2400 ) { 2401 neededKey = view.evalVisProp('el.keyboard.key'); 2402 if (neededKey === 'none' || 2403 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 2404 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 2405 view._elEventHandler(event); 2406 catchEvt = true; 2407 } 2408 2409 } 2410 if (view.evalVisProp('el.keyboard.enabled') && 2411 (event.key === 'ArrowLeft' || event.key === 'ArrowRight') 2412 ) { 2413 neededKey = view.evalVisProp('az.keyboard.key'); 2414 if (neededKey === 'none' || 2415 (neededKey.indexOf('shift') > -1 && event.shiftKey) || 2416 (neededKey.indexOf('ctrl') > -1 && event.ctrlKey) 2417 ) { 2418 view._azEventHandler(event); 2419 catchEvt = true; 2420 } 2421 } 2422 if (view.evalVisProp('bank.keyboard.enabled') && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) { 2423 neededKey = view.evalVisProp('bank.keyboard.key'); 2424 if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 2425 view._bankEventHandler(event); 2426 catchEvt = true; 2427 } 2428 } 2429 if (event.key === 'PageUp') { 2430 view.nextView(); 2431 catchEvt = true; 2432 } else if (event.key === 'PageDown') { 2433 view.previousView(); 2434 catchEvt = true; 2435 } 2436 2437 if (catchEvt) { 2438 // We stop event handling only in the case if the keypress could be 2439 // used for the 3D view. If this is not done, input fields et al 2440 // can not be used any more. 2441 event.preventDefault(); 2442 } 2443 // this.board._change3DView = false; 2444 2445 }, view); 2446 2447 // Add events for the pointer navigation 2448 Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view); 2449 2450 // Initialize view rotation matrix 2451 view.getAnglesFromSliders(); 2452 view.matrix3DRot = view.getRotationFromAngles(); 2453 2454 // override angle slider bounds when trackball navigation is enabled 2455 view.updateAngleSliderBounds(); 2456 2457 view.board.update(); 2458 2459 return view; 2460 }; 2461 2462 JXG.registerElement("view3d", JXG.createView3D); 2463 2464 export default JXG.View3D; 2465