1 /* 2 Copyright 2008-2023 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 /*global JXG:true, define: true*/ 30 31 import JXG from "../jxg"; 32 import Const from "../base/constants"; 33 import Type from "../utils/type"; 34 import Mat from "../math/math"; 35 import Env from "../utils/env"; 36 import GeometryElement from "../base/element"; 37 import Composition from "../base/composition"; 38 39 /** 40 * 3D view inside a JXGraph board. 41 * 42 * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with 43 * type {@link View3D} instead. 44 * 45 * @augments JXG.GeometryElement 46 * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view 47 * 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 48 * [x,y] and side lengths [w, h] of the board. 49 */ 50 JXG.View3D = function (board, parents, attributes) { 51 this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D); 52 53 /** 54 * An associative array containing all geometric objects belonging to the view. 55 * Key is the id of the object and value is a reference to the object. 56 * @type Object 57 * @private 58 */ 59 this.objects = {}; 60 61 /** 62 * An array containing all geometric objects in this view in the order of construction. 63 * @type Array 64 * @private 65 */ 66 // this.objectsList = []; 67 68 /** 69 * 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. 70 * @type Object 71 * @private 72 */ 73 this.elementsByName = {}; 74 75 /** 76 * Default axes of the 3D view, contains the axes of the view or null. 77 * 78 * @type {Object} 79 * @default null 80 */ 81 this.defaultAxes = null; 82 83 /** 84 * @type {Array} 85 * @private 86 */ 87 // 3D-to-2D transformation matrix 88 this.matrix3D = [ 89 [1, 0, 0, 0], 90 [0, 1, 0, 0], 91 [0, 0, 1, 0] 92 ]; 93 94 /** 95 * @type array 96 * @private 97 */ 98 // Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0. 99 this.llftCorner = parents[0]; 100 101 /** 102 * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0. 103 * @type array 104 * @private 105 */ 106 this.size = parents[1]; 107 108 /** 109 * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view 110 * @type array 111 */ 112 this.bbox3D = parents[2]; 113 114 /** 115 * Distance of the view to the origin. In other words, its 116 * the radius of the sphere where the camera sits.view.board.update 117 * @type Number 118 */ 119 this.r = -1; 120 121 /** 122 * Type of projection. 123 * @type String 124 */ 125 // Will be set in update(). 126 this.projectionType = 'parallel'; 127 128 this.timeoutAzimuth = null; 129 130 this.id = this.board.setId(this, 'V'); 131 this.board.finalizeAdding(this); 132 this.elType = 'view3d'; 133 134 this.methodMap = Type.deepCopy(this.methodMap, { 135 // TODO 136 }); 137 }; 138 JXG.View3D.prototype = new GeometryElement(); 139 140 JXG.extend( 141 JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ { 142 143 /** 144 * Creates a new 3D element of type elementType. 145 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'. 146 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two 147 * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 148 * methods for a list of possible parameters. 149 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 150 * Common attributes are name, visible, strokeColor. 151 * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing 152 * two or more elements. 153 */ 154 create: function (elementType, parents, attributes) { 155 var prefix = [], 156 el; 157 158 if (elementType.indexOf('3d') > 0) { 159 // is3D = true; 160 prefix.push(this); 161 } 162 el = this.board.create(elementType, prefix.concat(parents), attributes); 163 164 return el; 165 }, 166 167 /** 168 * Select a single or multiple elements at once. 169 * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will 170 * be used as a filter to return multiple elements at once filtered by the properties of the object. 171 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 172 * The advanced filters consisting of objects or functions are ignored. 173 * @returns {JXG.GeometryElement3D|JXG.Composition} 174 * @example 175 * // select the element with name A 176 * view.select('A'); 177 * 178 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 179 * view.select({ 180 * strokeColor: 'red' 181 * }); 182 * 183 * // select all points on or below the x/y plane and make them black. 184 * view.select({ 185 * elType: 'point3d', 186 * Z: function (v) { 187 * return v <= 0; 188 * } 189 * }).setAttribute({color: 'black'}); 190 * 191 * // select all elements 192 * view.select(function (el) { 193 * return true; 194 * }); 195 */ 196 select: function (str, onlyByIdOrName) { 197 var flist, 198 olist, 199 i, 200 l, 201 s = str; 202 203 if (s === null) { 204 return s; 205 } 206 207 // It's a string, most likely an id or a name. 208 if (Type.isString(s) && s !== '') { 209 // Search by ID 210 if (Type.exists(this.objects[s])) { 211 s = this.objects[s]; 212 // Search by name 213 } else if (Type.exists(this.elementsByName[s])) { 214 s = this.elementsByName[s]; 215 // // Search by group ID 216 // } else if (Type.exists(this.groups[s])) { 217 // s = this.groups[s]; 218 } 219 220 // It's a function or an object, but not an element 221 } else if ( 222 !onlyByIdOrName && 223 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 224 ) { 225 flist = Type.filterElements(this.objectsList, s); 226 227 olist = {}; 228 l = flist.length; 229 for (i = 0; i < l; i++) { 230 olist[flist[i].id] = flist[i]; 231 } 232 s = new Composition(olist); 233 234 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 235 } else if ( 236 Type.isObject(s) && 237 Type.exists(s.id) && 238 !Type.exists(this.objects[s.id]) 239 ) { 240 s = null; 241 } 242 243 return s; 244 }, 245 246 updateParallelProjection: function () { 247 var r, a, e, f, 248 mat = [ 249 [1, 0, 0, 0], 250 [0, 1, 0, 0], 251 [0, 0, 1, 0] 252 ]; 253 254 // mat projects homogeneous 3D coords in View3D 255 // to homogeneous 2D coordinates in the board 256 e = this.el_slide.Value(); 257 r = this.r; 258 a = this.az_slide.Value(); 259 f = r * Math.sin(e); 260 261 mat[1][1] = r * Math.cos(a); 262 mat[1][2] = -r * Math.sin(a); 263 mat[2][1] = f * Math.sin(a); 264 mat[2][2] = f * Math.cos(a); 265 mat[2][3] = Math.cos(e); 266 267 return mat; 268 }, 269 270 /** 271 * @private 272 * @returns {Array} 273 */ 274 _updateCentralProjection: function () { 275 var r, e, a, up, 276 az, ax, ay, v, nrm, 277 // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html 278 // bbox3D is always at the world origin, i.e. T_obj is the unit matrix. 279 // All vectors contain affine coordinates and have length 3 280 // The matrices are of size 4x4. 281 Tcam1, // The inverse camera transformation 282 eye, d, 283 foc = 1 / Math.tan(0.5 * Type.evaluate(this.visProp.fov)), 284 zf = 20, 285 zn = 8, 286 Pref = [ 287 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 288 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 289 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]) 290 ], 291 292 A = [ 293 [0, 0, 0, -1], 294 [0, foc, 0, 0], 295 [0, 0, foc, 0], 296 [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)] 297 ], 298 299 func_sphere; 300 301 /** 302 * Calculates a spherical parametric surface, which depends on az, el and r. 303 * @param {Number} a 304 * @param {Number} e 305 * @param {Number} r 306 * @returns {Array} 3-dimensional vector in cartesian coordinates 307 */ 308 func_sphere = function (az, el, r) { 309 return [ 310 r * Math.cos(az) * Math.cos(el), 311 -r * Math.sin(az) * Math.cos(el), 312 r * Math.sin(el) 313 ]; 314 }; 315 316 a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere 317 e = this.el_slide.Value() * 2; 318 319 r = Type.evaluate(this.visProp.r); 320 if (r === 'auto') { 321 r = Math.sqrt( 322 Math.pow(this.bbox3D[0][0] - this.bbox3D[0][1], 2) + 323 Math.pow(this.bbox3D[1][0] - this.bbox3D[1][1], 2) + 324 Math.pow(this.bbox3D[2][0] - this.bbox3D[2][1], 2) 325 ) * 1.01; 326 } 327 328 // create an up vector and an eye vector which are 90 degrees out of phase 329 up = func_sphere(a, e + Math.PI / 2, 1); 330 eye = func_sphere(a, e, r); 331 332 d = [eye[0] - Pref[0], eye[1] - Pref[1], eye[2] - Pref[2]]; 333 nrm = Mat.norm(d, 3); 334 az = [d[0] / nrm, d[1] / nrm, d[2] / nrm]; 335 336 nrm = Mat.norm(up, 3); 337 v = [up[0] / nrm, up[1] / nrm, up[2] / nrm]; 338 339 ax = Mat.crossProduct(v, az); 340 ay = Mat.crossProduct(az, ax); 341 342 v = Mat.matVecMult([ax, ay, az], eye); 343 Tcam1 = [ 344 [1, 0, 0, 0], 345 [-v[0], ax[0], ax[1], ax[2]], 346 [-v[1], ay[0], ay[1], ay[2]], 347 [-v[2], az[0], az[1], az[2]] 348 ]; 349 A = Mat.matMatMult(A, Tcam1); 350 351 return A; 352 }, 353 354 // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles. 355 update: function () { 356 var mat2D, shift, size; 357 358 if ( 359 !Type.exists(this.el_slide) || 360 !Type.exists(this.az_slide) || 361 !this.needsUpdate 362 ) { 363 return this; 364 } 365 366 mat2D = [ 367 [1, 0, 0], 368 [0, 1, 0], 369 [0, 0, 1] 370 ]; 371 372 this.projectionType = Type.evaluate(this.visProp.projection).toLowerCase(); 373 374 switch (this.projectionType) { 375 case 'central': // Central projection 376 377 this.matrix3D = this._updateCentralProjection(); 378 // this.matrix3D is a 4x4 matrix 379 380 size = 0.4; 381 mat2D[1][1] = this.size[0] / (2 * size); // w / d_x 382 mat2D[2][2] = this.size[1] / (2 * size); // h / d_y 383 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * (2 * size); // llft_x 384 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * (2 * size); // llft_y 385 386 // The transformations this.matrix3D and mat2D can not be combined yet, since 387 // the projected vector has to be normalized in between in 388 // project3DTo2D 389 this.viewPortTransform = mat2D; 390 break; 391 392 case 'parallel': // Parallel projection 393 default: 394 // Rotate the scenery around the center of the box, not around the origin 395 shift = [ 396 [1, 0, 0, 0], 397 [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0], 398 [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0], 399 [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1] 400 ]; 401 402 // Add a second transformation to scale and shift the projection 403 // on the board, usually called viewport. 404 mat2D[1][1] = this.size[0] / (this.bbox3D[0][1] - this.bbox3D[0][0]); // w / d_x 405 mat2D[2][2] = this.size[1] / (this.bbox3D[1][1] - this.bbox3D[1][0]); // h / d_y 406 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * (this.bbox3D[0][1] - this.bbox3D[0][0]); // llft_x 407 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * (this.bbox3D[1][1] - this.bbox3D[1][0]); // llft_y 408 409 // this.matrix3D is a 3x4 matrix 410 this.matrix3D = this.updateParallelProjection(); 411 // Combine the projections 412 this.matrix3D = Mat.matMatMult(mat2D, Mat.matMatMult(this.matrix3D, shift)); 413 } 414 415 return this; 416 }, 417 418 updateRenderer: function () { 419 this.needsUpdate = false; 420 return this; 421 }, 422 423 removeObject: function (object, saveMethod) { 424 var i; 425 426 // this.board.removeObject(object, saveMethod); 427 if (Type.isArray(object)) { 428 for (i = 0; i < object.length; i++) { 429 this.removeObject(object[i]); 430 } 431 return this; 432 } 433 434 object = this.select(object); 435 436 // // If the object which is about to be removed unknown or a string, do nothing. 437 // // it is a string if a string was given and could not be resolved to an element. 438 if (!Type.exists(object) || Type.isString(object)) { 439 return this; 440 } 441 442 try { 443 // // remove all children. 444 // for (el in object.childElements) { 445 // if (object.childElements.hasOwnProperty(el)) { 446 // object.childElements[el].board.removeObject(object.childElements[el]); 447 // } 448 // } 449 450 delete this.objects[object.id]; 451 } catch (e) { 452 JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e); 453 } 454 455 // this.update(); 456 457 this.board.removeObject(object, saveMethod); 458 459 return this; 460 }, 461 462 /** 463 * Project 3D coordinates to 2D board coordinates 464 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 465 * 466 * @param {Number|Array} x 467 * @param {Number[]} y 468 * @param {Number[]} z 469 * @returns {Array} Array of length 3 containing the projection on to the board 470 * in homogeneous user coordinates. 471 */ 472 project3DTo2D: function (x, y, z) { 473 var vec, w; 474 if (arguments.length === 3) { 475 vec = [1, x, y, z]; 476 } else { 477 // Argument is an array 478 if (x.length === 3) { 479 vec = [1].concat(x); 480 } else { 481 vec = x; 482 } 483 } 484 485 w = Mat.matVecMult(this.matrix3D, vec); 486 487 switch (this.projectionType) { 488 case 'central': 489 w[1] /= w[0]; 490 w[2] /= w[0]; 491 w[3] /= w[0]; 492 w[0] /= w[0]; 493 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3)); 494 495 case 'parallel': 496 default: 497 return w; 498 } 499 }, 500 501 /** 502 * Project a 2D coordinate to the plane defined by point "foot" 503 * and the normal vector `normal`. 504 * 505 * @param {JXG.Point} point2d 506 * @param {Array} normal 507 * @param {Array} foot 508 * @returns {Array} of length 4 containing the projected 509 * point in homogeneous coordinates. 510 */ 511 project2DTo3DPlane: function (point2d, normal, foot) { 512 var mat, rhs, d, le, 513 n = normal.slice(1), 514 sol; 515 516 foot = foot || [1, 0, 0, 0]; 517 le = Mat.norm(n, 3); 518 d = Mat.innerProduct(foot.slice(1), n, 3) / le; 519 520 mat = this.matrix3D.slice(0, 3); // True copy 521 mat.push([0].concat(n)); 522 523 // 2D coordinates of point: 524 rhs = point2d.coords.usrCoords.concat([d]); 525 try { 526 // Prevent singularity in case elevation angle is zero 527 if (mat[2][3] === 1.0) { 528 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 529 } 530 sol = Mat.Numerics.Gauss(mat, rhs); 531 } catch (err) { 532 sol = [0, NaN, NaN, NaN]; 533 } 534 535 return sol; 536 }, 537 538 /** 539 * Project a 2D coordinate to a new 3D position by keeping 540 * the 3D x, y coordinates and changing only the z coordinate. 541 * All horizontal moves of the 2D point are ignored. 542 * 543 * @param {JXG.Point} point2d 544 * @param {Array} coords3D 545 * @returns {Array} of length 4 containing the projected 546 * point in homogeneous coordinates. 547 */ 548 project2DTo3DVertical: function (point2d, coords3D) { 549 var m3D = this.matrix3D[2], 550 b = m3D[3], 551 rhs = point2d.coords.usrCoords[2]; // y in 2D 552 553 rhs -= m3D[0] * coords3D[0] + m3D[1] * coords3D[1] + m3D[2] * coords3D[2]; 554 if (Math.abs(b) < Mat.eps) { 555 return coords3D; // No changes 556 } else { 557 return coords3D.slice(0, 3).concat([rhs / b]); 558 } 559 }, 560 561 /** 562 * Limit 3D coordinates to the bounding cube. 563 * 564 * @param {Array} c3d 3D coordinates [x,y,z] 565 * @returns Array with updated 3D coordinates. 566 */ 567 project3DToCube: function (c3d) { 568 var cube = this.bbox3D; 569 if (c3d[1] < cube[0][0]) { 570 c3d[1] = cube[0][0]; 571 } 572 if (c3d[1] > cube[0][1]) { 573 c3d[1] = cube[0][1]; 574 } 575 if (c3d[2] < cube[1][0]) { 576 c3d[2] = cube[1][0]; 577 } 578 if (c3d[2] > cube[1][1]) { 579 c3d[2] = cube[1][1]; 580 } 581 if (c3d[3] < cube[2][0]) { 582 c3d[3] = cube[2][0]; 583 } 584 if (c3d[3] > cube[2][1]) { 585 c3d[3] = cube[2][1]; 586 } 587 588 return c3d; 589 }, 590 591 /** 592 * Intersect a ray with the bounding cube of the 3D view. 593 * @param {Array} p 3D coordinates [x,y,z] 594 * @param {Array} d 3D direction vector of the line (array of length 3) 595 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 596 * @returns Affine ratio of the intersection of the line with the cube. 597 */ 598 intersectionLineCube: function (p, d, r) { 599 var rnew, i, r0, r1; 600 601 rnew = r; 602 for (i = 0; i < 3; i++) { 603 if (d[i] !== 0) { 604 r0 = (this.bbox3D[i][0] - p[i]) / d[i]; 605 r1 = (this.bbox3D[i][1] - p[i]) / d[i]; 606 if (r < 0) { 607 rnew = Math.max(rnew, Math.min(r0, r1)); 608 } else { 609 rnew = Math.min(rnew, Math.max(r0, r1)); 610 } 611 } 612 } 613 return rnew; 614 }, 615 616 /** 617 * Test if coordinates are inside of the bounding cube. 618 * @param {array} q 3D coordinates [x,y,z] of a point. 619 * @returns Boolean 620 */ 621 isInCube: function (q) { 622 return ( 623 q[0] > this.bbox3D[0][0] - Mat.eps && 624 q[0] < this.bbox3D[0][1] + Mat.eps && 625 q[1] > this.bbox3D[1][0] - Mat.eps && 626 q[1] < this.bbox3D[1][1] + Mat.eps && 627 q[2] > this.bbox3D[2][0] - Mat.eps && 628 q[2] < this.bbox3D[2][1] + Mat.eps 629 ); 630 }, 631 632 /** 633 * 634 * @param {JXG.Plane3D} plane1 635 * @param {JXG.Plane3D} plane2 636 * @param {JXG.Plane3D} d 637 * @returns {Array} of length 2 containing the coordinates of the defining points of 638 * of the intersection segment. 639 */ 640 intersectionPlanePlane: function (plane1, plane2, d) { 641 var ret = [[], []], 642 p, 643 dir, 644 r, 645 q; 646 647 d = d || plane2.d; 648 649 p = Mat.Geometry.meet3Planes( 650 plane1.normal, 651 plane1.d, 652 plane2.normal, 653 d, 654 Mat.crossProduct(plane1.normal, plane2.normal), 655 0 656 ); 657 dir = Mat.Geometry.meetPlanePlane( 658 plane1.vec1, 659 plane1.vec2, 660 plane2.vec1, 661 plane2.vec2 662 ); 663 r = this.intersectionLineCube(p, dir, Infinity); 664 q = Mat.axpy(r, dir, p); 665 if (this.isInCube(q)) { 666 ret[0] = q; 667 } 668 r = this.intersectionLineCube(p, dir, -Infinity); 669 q = Mat.axpy(r, dir, p); 670 if (this.isInCube(q)) { 671 ret[1] = q; 672 } 673 return ret; 674 }, 675 676 /** 677 * Generate mesh for a surface / plane. 678 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 679 * @param {Array|Function} func 680 * @param {Array} interval_u 681 * @param {Array} interval_v 682 * @returns Array 683 * @private 684 * 685 * @example 686 * var el = view.create('curve', [[], []]); 687 * el.updateDataArray = function () { 688 * var steps_u = Type.evaluate(this.visProp.stepsu), 689 * steps_v = Type.evaluate(this.visProp.stepsv), 690 * r_u = Type.evaluate(this.range_u), 691 * r_v = Type.evaluate(this.range_v), 692 * func, ret; 693 * 694 * if (this.F !== null) { 695 * func = this.F; 696 * } else { 697 * func = [this.X, this.Y, this.Z]; 698 * } 699 * ret = this.view.getMesh(func, 700 * r_u.concat([steps_u]), 701 * r_v.concat([steps_v])); 702 * 703 * this.dataX = ret[0]; 704 * this.dataY = ret[1]; 705 * }; 706 * 707 */ 708 getMesh: function (func, interval_u, interval_v) { 709 var i_u, i_v, u, v, 710 c2d, delta_u, delta_v, 711 p = [0, 0, 0], 712 steps_u = interval_u[2], 713 steps_v = interval_v[2], 714 dataX = [], 715 dataY = []; 716 717 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u; 718 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v; 719 720 for (i_u = 0; i_u <= steps_u; i_u++) { 721 u = interval_u[0] + delta_u * i_u; 722 for (i_v = 0; i_v <= steps_v; i_v++) { 723 v = interval_v[0] + delta_v * i_v; 724 if (Type.isFunction(func)) { 725 p = func(u, v); 726 } else { 727 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 728 } 729 c2d = this.project3DTo2D(p); 730 dataX.push(c2d[1]); 731 dataY.push(c2d[2]); 732 } 733 dataX.push(NaN); 734 dataY.push(NaN); 735 } 736 737 for (i_v = 0; i_v <= steps_v; i_v++) { 738 v = interval_v[0] + delta_v * i_v; 739 for (i_u = 0; i_u <= steps_u; i_u++) { 740 u = interval_u[0] + delta_u * i_u; 741 if (Type.isFunction(func)) { 742 p = func(u, v); 743 } else { 744 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 745 } 746 c2d = this.project3DTo2D(p); 747 dataX.push(c2d[1]); 748 dataY.push(c2d[2]); 749 } 750 dataX.push(NaN); 751 dataY.push(NaN); 752 } 753 754 return [dataX, dataY]; 755 }, 756 757 /** 758 * 759 */ 760 animateAzimuth: function () { 761 var s = this.az_slide._smin, 762 e = this.az_slide._smax, 763 sdiff = e - s, 764 newVal = this.az_slide.Value() + 0.1; 765 766 this.az_slide.position = (newVal - s) / sdiff; 767 if (this.az_slide.position > 1) { 768 this.az_slide.position = 0.0; 769 } 770 this.board.update(); 771 772 this.timeoutAzimuth = setTimeout(function () { 773 this.animateAzimuth(); 774 }.bind(this), 200); 775 }, 776 777 /** 778 * 779 */ 780 stopAzimuth: function () { 781 clearTimeout(this.timeoutAzimuth); 782 this.timeoutAzimuth = null; 783 }, 784 785 /** 786 * Check if vertical dragging is enabled and which action is needed. 787 * Default is shiftKey. 788 * 789 * @returns Boolean 790 * @private 791 */ 792 isVerticalDrag: function () { 793 var b = this.board, 794 key; 795 if (!Type.evaluate(this.visProp.verticaldrag.enabled)) { 796 return false; 797 } 798 key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key'; 799 return b[key]; 800 }, 801 802 /** 803 * Sets camera view to the given values. 804 * 805 * @param {Number} az Value of azimuth. 806 * @param {Number} el Value of elevation. 807 * @param {Number} [r] Value of radius. 808 * 809 * @returns {Object} Reference to the view. 810 */ 811 setView: function (az, el, r) { 812 r = r || this.r; 813 814 this.az_slide.setValue(az); 815 this.el_slide.setValue(el); 816 this.r = r; 817 this.board.update(); 818 819 return this; 820 }, 821 822 /** 823 * Changes view to the next view stored in the attribute `values`. 824 * 825 * @see View3D#values 826 * 827 * @returns {Object} Reference to the view. 828 */ 829 nextView: function () { 830 var views = Type.evaluate(this.visProp.values), 831 n = this.visProp._currentview; 832 833 n = (n + 1) % views.length; 834 this.setCurrentView(n); 835 836 return this; 837 }, 838 839 /** 840 * Changes view to the previous view stored in the attribute `values`. 841 * 842 * @see View3D#values 843 * 844 * @returns {Object} Reference to the view. 845 */ 846 previousView: function () { 847 var views = Type.evaluate(this.visProp.values), 848 n = this.visProp._currentview; 849 850 n = (n + views.length - 1) % views.length; 851 this.setCurrentView(n); 852 853 return this; 854 }, 855 856 /** 857 * Changes view to the determined view stored in the attribute `values`. 858 * 859 * @see View3D#values 860 * 861 * @param {Number} n Index of view in attribute `values`. 862 * @returns {Object} Reference to the view. 863 */ 864 setCurrentView: function (n) { 865 var views = Type.evaluate(this.visProp.values); 866 867 if (n < 0 || n >= views.length) { 868 n = ((n % views.length) + views.length) % views.length; 869 } 870 871 this.setView(views[n][0], views[n][1], views[n][2]); 872 this.visProp._currentview = n; 873 874 return this; 875 }, 876 877 /** 878 * Controls the navigation in az direction using either the keyboard or a pointer. 879 * 880 * @private 881 * 882 * @param {event} event either the keydown or the pointer event 883 * @returns view 884 */ 885 _azEventHandler: function (event) { 886 var smax = this.az_slide._smax, 887 smin = this.az_slide._smin, 888 speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)), 889 delta = event.movementX, 890 az = this.az_slide.Value(), 891 el = this.el_slide.Value(); 892 893 // Doesn't allow navigation if another moving event is triggered 894 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 895 return this; 896 } 897 898 // Calculate new az value if keyboard events are triggered 899 // Plus if right-button, minus if left-button 900 if (Type.evaluate(this.visProp.az.keyboard.enabled)) { 901 if (event.key === 'ArrowRight') { 902 az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180; 903 } else if (event.key === 'ArrowLeft') { 904 az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180; 905 } 906 } 907 908 if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && event.key == null) { 909 az += delta * speed; 910 } 911 912 // Project the calculated az value to a usable value in the interval [smin,smax] 913 // Use modulo if continuous is true 914 if (Type.evaluate(this.visProp.az.continuous)) { 915 az = (((az % smax) + smax) % smax); 916 } else { 917 if (az > 0) { 918 az = Math.min(smax, az); 919 } else if (az < 0) { 920 az = Math.max(smin, az); 921 } 922 } 923 924 this.setView(az, el); 925 return this; 926 }, 927 928 /** 929 * Controls the navigation in el direction using either the keyboard or a pointer. 930 * 931 * @private 932 * 933 * @param {event} event either the keydown or the pointer event 934 * @returns view 935 */ 936 _elEventHandler: function (event) { 937 var smax = this.el_slide._smax, 938 smin = this.el_slide._smin, 939 speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed), 940 delta = event.movementY, 941 az = this.az_slide.Value(), 942 el = this.el_slide.Value(); 943 944 // Doesn't allow navigation if another moving event is triggered 945 if (this.board.mode === this.board.BOARD_MODE_DRAG) { 946 return this; 947 } 948 949 // Calculate new az value if keyboard events are triggered 950 // Plus if right-button, minus if left-button 951 if (Type.evaluate(this.visProp.el.keyboard.enabled)) { 952 if (event.key === 'ArrowUp') { 953 el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180; 954 } else if (event.key === 'ArrowDown') { 955 el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180; 956 } 957 } 958 959 // Calculate new az value if keyboard events are triggered 960 // Plus if right-button, minus if left-button 961 if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && event.key == null) { 962 el += delta * speed; 963 } 964 965 // Project the calculated az value to a usable value in the interval [smin,smax] 966 // Use modulo if continuous is true 967 if (Type.evaluate(this.visProp.el.continuous)) { 968 el = (((el % smax) + smax) % smax); 969 } else { 970 if (el > 0) { 971 el = Math.min(smax, el); 972 } else if (el < 0) { 973 el = Math.max(smin, el); 974 } 975 } 976 977 this.setView(az, el); 978 return this; 979 } 980 }); 981 982 /** 983 * @class This element creates a 3D view. 984 * @pseudo 985 * @description A View3D element provides the container and the methods to create and display 3D elements. 986 * It is contained in a JSXGraph board. 987 * @name View3D 988 * @augments JXG.View3D 989 * @constructor 990 * @type Object 991 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 992 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 993 * dim is an array of the form [w, h]. 994 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 995 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 996 * [x,y] and side lengths [w, h] of the board. 997 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 998 * which determines the coordinate ranges of the 3D cube. 999 * 1000 * @example 1001 * var bound = [-5, 5]; 1002 * var view = board.create('view3d', 1003 * [[-6, -3], 1004 * [8, 8], 1005 * [bound, bound, bound]], 1006 * { 1007 * // Main axes 1008 * axesPosition: 'center', 1009 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 1010 * 1011 * // Planes 1012 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 1013 * yPlaneFront: { visible: true, fillColor: 'blue'}, 1014 * 1015 * // Axes on planes 1016 * xPlaneRearYAxis: {strokeColor: 'red'}, 1017 * xPlaneRearZAxis: {strokeColor: 'red'}, 1018 * 1019 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 1020 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 1021 * 1022 * zPlaneFrontXAxis: {visible: false}, 1023 * zPlaneFrontYAxis: {visible: false} 1024 * }); 1025 * 1026 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 1027 * <script type="text/javascript"> 1028 * (function() { 1029 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 1030 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 1031 * var bound = [-5, 5]; 1032 * var view = board.create('view3d', 1033 * [[-6, -3], [8, 8], 1034 * [bound, bound, bound]], 1035 * { 1036 * // Main axes 1037 * axesPosition: 'center', 1038 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 1039 * // Planes 1040 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 1041 * yPlaneFront: { visible: true, fillColor: 'blue'}, 1042 * // Axes on planes 1043 * xPlaneRearYAxis: {strokeColor: 'red'}, 1044 * xPlaneRearZAxis: {strokeColor: 'red'}, 1045 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 1046 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 1047 * zPlaneFrontXAxis: {visible: false}, 1048 * zPlaneFrontYAxis: {visible: false} 1049 * }); 1050 * })(); 1051 * 1052 * </script><pre> 1053 * 1054 */ 1055 JXG.createView3D = function (board, parents, attributes) { 1056 var view, attr, attr_az, attr_el, 1057 x, y, w, h, 1058 coords = parents[0], // llft corner 1059 size = parents[1]; // [w, h] 1060 1061 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 1062 view = new JXG.View3D(board, parents, attr); 1063 view.defaultAxes = view.create('axes3d', parents, attributes); 1064 1065 x = coords[0]; 1066 y = coords[1]; 1067 w = size[0]; 1068 h = size[1]; 1069 1070 attr_az = Type.copyAttributes(attributes, board.options, 'view3d', 'az', 'slider'); 1071 attr_az.name = 'az'; 1072 1073 attr_el = Type.copyAttributes(attributes, board.options, 'view3d', 'el', 'slider'); 1074 attr_el.name = 'el'; 1075 1076 /** 1077 * Slider to adapt azimuth angle 1078 * @name JXG.View3D#az_slide 1079 * @type {Slider} 1080 */ 1081 view.az_slide = board.create( 1082 'slider', 1083 [ 1084 [x - 1, y - 2], 1085 [x + w + 1, y - 2], 1086 [ 1087 Type.evaluate(attr_az.min), 1088 Type.evaluate(attr_az.start), 1089 Type.evaluate(attr_az.max) 1090 ] 1091 ], 1092 attr_az 1093 ); 1094 1095 /** 1096 * Slider to adapt elevation angle 1097 * 1098 * @name JXG.View3D#el_slide 1099 * @type {Slider} 1100 */ 1101 view.el_slide = board.create( 1102 'slider', 1103 [ 1104 [x - 1, y], 1105 [x - 1, y + h], 1106 [ 1107 Type.evaluate(attr_el.min), 1108 Type.evaluate(attr_el.start), 1109 Type.evaluate(attr_el.max)] 1110 ], 1111 attr_el 1112 ); 1113 1114 view.board.highlightInfobox = function (x, y, el) { 1115 var d, i, c3d, foot, 1116 pre = '<span style="color:black; font-size:200%">\u21C4 </span>', 1117 brd = el.board, 1118 arr, infobox, 1119 p = null; 1120 1121 if (view.isVerticalDrag()) { 1122 pre = '<span style="color:black; font-size:200%">\u21C5 </span>'; 1123 } 1124 // Search 3D parent 1125 for (i = 0; i < el.parents.length; i++) { 1126 p = brd.objects[el.parents[i]]; 1127 if (p.is3D) { 1128 break; 1129 } 1130 } 1131 if (p) { 1132 foot = [1, 0, 0, p.coords[3]]; 1133 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 1134 if (!view.isInCube(c3d)) { 1135 view.board.highlightCustomInfobox('', p); 1136 return; 1137 } 1138 d = Type.evaluate(p.visProp.infoboxdigits); 1139 infobox = view.board.infobox; 1140 if (d === 'auto') { 1141 if (infobox.useLocale()) { 1142 arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')']; 1143 } else { 1144 arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')']; 1145 } 1146 1147 } else { 1148 if (infobox.useLocale()) { 1149 arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')']; 1150 } else { 1151 arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')']; 1152 } 1153 } 1154 view.board.highlightCustomInfobox(arr.join(''), p); 1155 } else { 1156 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 1157 } 1158 }; 1159 1160 // Hack needed to enable addEvent for view3D: 1161 view.BOARD_MODE_NONE = 0x0000; 1162 1163 // Add events for the keyboard navigation 1164 Env.addEvent(board.containerObj, 'keydown', function (event) { 1165 var neededKey; 1166 1167 if (Type.evaluate(view.visProp.el.keyboard.enabled) && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) { 1168 neededKey = Type.evaluate(view.visProp.el.keyboard.key); 1169 if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 1170 view._elEventHandler(event); 1171 } 1172 1173 } 1174 if (Type.evaluate(view.visProp.el.keyboard.enabled) && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { 1175 neededKey = Type.evaluate(view.visProp.az.keyboard.key); 1176 if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) { 1177 view._azEventHandler(event); 1178 } 1179 } 1180 if (event.key === 'PageUp') { 1181 view.nextView(); 1182 } else if (event.key === 'PageDown') { 1183 view.previousView(); 1184 } 1185 1186 event.preventDefault(); 1187 }, view); 1188 1189 // Add events for the pointer navigation 1190 board.containerObj.addEventListener('pointerdown', function (event) { 1191 var neededButton, neededKey, 1192 target; 1193 1194 if (Type.evaluate(view.visProp.az.pointer.enabled)) { 1195 neededButton = Type.evaluate(view.visProp.az.pointer.button); 1196 neededKey = Type.evaluate(view.visProp.az.pointer.key); 1197 1198 // Events for azimuth 1199 if ( 1200 (neededButton === -1 || neededButton === event.button) && 1201 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) 1202 ) { 1203 // If outside is true then the event listener is bound to the document, otherwise to the div 1204 if (Type.evaluate(view.visProp.az.pointer.outside)) { 1205 target = document; 1206 } else { 1207 target = board.containerObj; 1208 } 1209 Env.addEvent(target, 'pointermove', view._azEventHandler, view); 1210 view._hasMoveAz = true; 1211 } 1212 } 1213 1214 if (Type.evaluate(view.visProp.el.pointer.enabled)) { 1215 neededButton = Type.evaluate(view.visProp.el.pointer.button); 1216 neededKey = Type.evaluate(view.visProp.el.pointer.key); 1217 1218 // Events for elevation 1219 if ( 1220 (neededButton === -1 || neededButton === event.button) && 1221 (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) 1222 ) { 1223 // If outside is true then the event listener is bound to the document, otherwise to the div 1224 if (Type.evaluate(view.visProp.el.pointer.outside)) { 1225 target = document; 1226 } else { 1227 target = board.containerObj; 1228 } 1229 Env.addEvent(target, 'pointermove', view._elEventHandler, view); 1230 view._hasMoveEl = true; 1231 } 1232 } 1233 1234 // Remove pointerMove and pointerUp event listener as soon as pointer up is triggered 1235 function handlePointerUp() { 1236 var target; 1237 if (view._hasMoveAz) { 1238 if (Type.evaluate(view.visProp.az.pointer.outside)) { 1239 target = document; 1240 } else { 1241 target = view.board.containerObj; 1242 } 1243 Env.removeEvent(target, 'pointermove', view._azEventHandler, view); 1244 view._hasMoveAz = false; 1245 } 1246 if (view._hasMoveEl) { 1247 if (Type.evaluate(view.visProp.el.pointer.outside)) { 1248 target = document; 1249 } else { 1250 target = view.board.containerObj; 1251 } 1252 Env.removeEvent(target, 'pointermove', view._elEventHandler, view); 1253 view._hasMoveEl = false; 1254 } 1255 Env.removeEvent(document, 'pointerup', handlePointerUp, view); 1256 } 1257 1258 Env.addEvent(document, 'pointerup', handlePointerUp, view); 1259 }); 1260 1261 view.board.update(); 1262 1263 return view; 1264 }; 1265 1266 JXG.registerElement("view3d", JXG.createView3D); 1267 1268 export default JXG.View3D;