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 GeometryElement from "../base/element"; 36 import Composition from "../base/composition"; 37 38 /** 39 * 3D view inside of a JXGraph board. 40 * 41 * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with 42 * type {@link View3D} instead. 43 * 44 * @augments JXG.GeometryElement 45 * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view 46 * 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 47 * [x,y] and side lengths [w, h] of the board. 48 */ 49 JXG.View3D = function (board, parents, attributes) { 50 this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D); 51 52 /** 53 * An associative array containing all geometric objects belonging to the view. 54 * Key is the id of the object and value is a reference to the object. 55 * @type Object 56 * @private 57 */ 58 this.objects = {}; 59 60 /** 61 * An array containing all geometric objects in this view in the order of construction. 62 * @type Array 63 * @private 64 */ 65 // this.objectsList = []; 66 67 /** 68 * 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. 69 * @type Object 70 * @private 71 */ 72 this.elementsByName = {}; 73 74 /** 75 * Default axes of the 3D view, contains the axes of the view or null. 76 * 77 * @type {Object} 78 * @default null 79 */ 80 this.defaultAxes = null; 81 82 /** 83 * @type {Array} 84 * @private 85 */ 86 // 3 x 4 matrix 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 this.projectionType = 'parallel'; 122 123 this.timeoutAzimuth = null; 124 125 this.id = this.board.setId(this, "V"); 126 this.board.finalizeAdding(this); 127 this.elType = "view3d"; 128 129 this.methodMap = Type.deepCopy(this.methodMap, { 130 // TODO 131 }); 132 }; 133 JXG.View3D.prototype = new GeometryElement(); 134 135 JXG.extend( 136 JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ { 137 138 /** 139 * Creates a new 3D element of type elementType. 140 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'. 141 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two 142 * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 143 * methods for a list of possible parameters. 144 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 145 * Common attributes are name, visible, strokeColor. 146 * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing 147 * two or more elements. 148 */ 149 create: function (elementType, parents, attributes) { 150 var prefix = [], 151 // is3D = false, 152 el; 153 154 if (elementType.indexOf("3d") > 0) { 155 // is3D = true; 156 prefix.push(this); 157 } 158 el = this.board.create(elementType, prefix.concat(parents), attributes); 159 160 return el; 161 }, 162 163 /** 164 * Select a single or multiple elements at once. 165 * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will 166 * be used as a filter to return multiple elements at once filtered by the properties of the object. 167 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 168 * The advanced filters consisting of objects or functions are ignored. 169 * @returns {JXG.GeometryElement3D|JXG.Composition} 170 * @example 171 * // select the element with name A 172 * view.select('A'); 173 * 174 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 175 * view.select({ 176 * strokeColor: 'red' 177 * }); 178 * 179 * // select all points on or below the x/y plane and make them black. 180 * view.select({ 181 * elType: 'point3d', 182 * Z: function (v) { 183 * return v <= 0; 184 * } 185 * }).setAttribute({color: 'black'}); 186 * 187 * // select all elements 188 * view.select(function (el) { 189 * return true; 190 * }); 191 */ 192 select: function (str, onlyByIdOrName) { 193 var flist, 194 olist, 195 i, 196 l, 197 s = str; 198 199 if (s === null) { 200 return s; 201 } 202 203 // It's a string, most likely an id or a name. 204 if (Type.isString(s) && s !== "") { 205 // Search by ID 206 if (Type.exists(this.objects[s])) { 207 s = this.objects[s]; 208 // Search by name 209 } else if (Type.exists(this.elementsByName[s])) { 210 s = this.elementsByName[s]; 211 // // Search by group ID 212 // } else if (Type.exists(this.groups[s])) { 213 // s = this.groups[s]; 214 } 215 216 // It's a function or an object, but not an element 217 } else if ( 218 !onlyByIdOrName && 219 (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute))) 220 ) { 221 flist = Type.filterElements(this.objectsList, s); 222 223 olist = {}; 224 l = flist.length; 225 for (i = 0; i < l; i++) { 226 olist[flist[i].id] = flist[i]; 227 } 228 s = new Composition(olist); 229 230 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 231 } else if ( 232 Type.isObject(s) && 233 Type.exists(s.id) && 234 !Type.exists(this.objects[s.id]) 235 ) { 236 s = null; 237 } 238 239 return s; 240 }, 241 242 updateParallelProjection: function () { 243 var r, a, e, f, 244 mat = [ 245 [1, 0, 0, 0], 246 [0, 1, 0, 0], 247 [0, 0, 1, 0] 248 ]; 249 250 // mat projects homogeneous 3D coords in View3D 251 // to homogeneous 2D coordinates in the board 252 e = this.el_slide.Value(); 253 r = this.r; 254 a = this.az_slide.Value(); 255 f = r * Math.sin(e); 256 257 mat[1][1] = r * Math.cos(a); 258 mat[1][2] = -r * Math.sin(a); 259 mat[2][1] = f * Math.sin(a); 260 mat[2][2] = f * Math.cos(a); 261 mat[2][3] = Math.cos(e); 262 263 return mat; 264 }, 265 266 updateCentralProjection: function () { 267 var r, e, a, 268 az, ax, ay, v, nrm, 269 // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html 270 // bbox3D is always at the world origin, i.e. T_obj is the unit matrix. 271 // All vectors contain affine coordinates and have length 3 272 // The matrices are of size 4x4. 273 Tcam1, // The inverse camera transformation 274 eye, d, 275 foc = 1 / Math.tan(0.5 * Math.PI / 2), 276 zf = 20, 277 zn = 8, 278 Pref = [ 279 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 280 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 281 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]) 282 ], 283 up = [0, 0, 1], 284 A = [ 285 [0, 0, 0, -1], 286 [0, foc, 0, 0], 287 [0, 0, foc, 0], 288 [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)] 289 ]; 290 291 a = this.az_slide.Value(); 292 e = this.el_slide.Value() * 2; 293 // r = this.r; 294 295 // Sphere 296 r = 12; 297 a += 3 * Math.PI * 0.5; 298 eye = [ 299 r * Math.cos(a) * Math.cos(e), 300 -r * Math.sin(a) * Math.cos(e), 301 r * Math.sin(e) 302 ]; 303 304 // Circle 305 // r = 8; 306 // // up = [0, Math.cos(e), Math.sin(e)]; 307 // up = [0, 0, 1]; 308 // eye = [ 309 // -r * Math.cos(a), 310 // -r * Math.sin(a), 311 // 1. * r 312 // ]; 313 314 d = [eye[0] - Pref[0], eye[1] - Pref[1], eye[2] - Pref[2]]; 315 nrm = Mat.norm(d, 3); 316 az = [d[0] / nrm, d[1] / nrm, d[2] / nrm]; 317 318 nrm = Mat.norm(up, 3); 319 v = [up[0] / nrm, up[1] / nrm, up[2] / nrm]; 320 321 ax = Mat.crossProduct(v, az); 322 ay = Mat.crossProduct(az, ax); 323 324 v = Mat.matVecMult([ax, ay, az], eye); 325 Tcam1 = [ 326 [1, 0, 0, 0], 327 [-v[0], ax[0], ax[1], ax[2]], 328 [-v[1], ay[0], ay[1], ay[2]], 329 [-v[2], az[0], az[1], az[2]] 330 ]; 331 A = Mat.matMatMult(A, Tcam1); 332 333 return A; 334 }, 335 336 update: function () { 337 // Update 3D-to-2D transformation matrix with the actual 338 // elevation and azimuth angles. 339 340 var mat2D, shift, size; 341 342 if ( 343 !Type.exists(this.el_slide) || 344 !Type.exists(this.az_slide) || 345 !this.needsUpdate 346 ) { 347 return this; 348 } 349 350 mat2D = [ 351 [1, 0, 0], 352 [0, 1, 0], 353 [0, 0, 1] 354 ]; 355 356 this.projectionType = Type.evaluate(this.visProp.projection); 357 358 if (this.projectionType === 'parallel') { 359 // Parallel projection 360 // Rotate the scenery around the center of the box, 361 // not around the origin 362 shift = [ 363 [1, 0, 0, 0], 364 [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0], 365 [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0], 366 [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1] 367 ]; 368 369 // Add a second transformation to scale and shift the projection 370 // on the board, usually called viewport. 371 mat2D[1][1] = this.size[0] / (this.bbox3D[0][1] - this.bbox3D[0][0]); // w / d_x 372 mat2D[2][2] = this.size[1] / (this.bbox3D[1][1] - this.bbox3D[1][0]); // h / d_y 373 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * (this.bbox3D[0][1] - this.bbox3D[0][0]); // llft_x 374 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * (this.bbox3D[1][1] - this.bbox3D[1][0]); // llft_y 375 376 // this.matrix3D is a 3x4 matrix 377 this.matrix3D = this.updateParallelProjection(); 378 // Combine the projections 379 this.matrix3D = Mat.matMatMult(mat2D, Mat.matMatMult(this.matrix3D, shift)); 380 381 } else { 382 // Central projection 383 // this.matrix3D is a 4x4 matrix 384 this.matrix3D = this.updateCentralProjection(); 385 386 size = 0.4; 387 mat2D[1][1] = this.size[0] / (2 * size); // w / d_x 388 mat2D[2][2] = this.size[1] / (2 * size); // h / d_y 389 mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * (2 * size); // llft_x 390 mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * (2 * size); // llft_y 391 392 // The transformations this.matrix3D and mat2D can not be combined yet, since 393 // the projected vector has to be normalized in between in 394 // project3DTo2D 395 this.viewPortTransform = mat2D; 396 } 397 398 return this; 399 }, 400 401 updateRenderer: function () { 402 this.needsUpdate = false; 403 return this; 404 }, 405 406 removeObject: function (object, saveMethod) { 407 var i; 408 409 // this.board.removeObject(object, saveMethod); 410 if (Type.isArray(object)) { 411 for (i = 0; i < object.length; i++) { 412 this.removeObject(object[i]); 413 } 414 return this; 415 } 416 417 object = this.select(object); 418 419 // // If the object which is about to be removed unknown or a string, do nothing. 420 // // it is a string if a string was given and could not be resolved to an element. 421 if (!Type.exists(object) || Type.isString(object)) { 422 return this; 423 } 424 425 try { 426 // // remove all children. 427 // for (el in object.childElements) { 428 // if (object.childElements.hasOwnProperty(el)) { 429 // object.childElements[el].board.removeObject(object.childElements[el]); 430 // } 431 // } 432 433 delete this.objects[object.id]; 434 } catch (e) { 435 JXG.debug("View3D " + object.id + ": Could not be removed: " + e); 436 } 437 438 // this.update(); 439 440 this.board.removeObject(object, saveMethod); 441 442 return this; 443 }, 444 445 /** 446 * Project 3D coordinates to 2D board coordinates 447 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 448 * 449 * @param {Number|Array} x 450 * @param {Number[]} y 451 * @param {Number[]} z 452 * @returns {Array} Array of length 3 containing the projection on to the board 453 * in homogeneous user coordinates. 454 */ 455 project3DTo2D: function (x, y, z) { 456 var vec, w; 457 if (arguments.length === 3) { 458 vec = [1, x, y, z]; 459 } else { 460 // Argument is an array 461 if (x.length === 3) { 462 vec = [1].concat(x); 463 } else { 464 vec = x; 465 } 466 } 467 468 w = Mat.matVecMult(this.matrix3D, vec); 469 470 if (this.projectionType === 'parallel') { 471 return w; 472 } 473 474 // Central projection 475 w[1] /= w[0]; 476 w[2] /= w[0]; 477 w[3] /= w[0]; 478 w[0] /= w[0]; 479 480 return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3)); 481 }, 482 483 /** 484 * Project a 2D coordinate to the plane defined by point "foot" 485 * and the normal vector `normal`. 486 * 487 * @param {JXG.Point} point2d 488 * @param {Array} normal 489 * @param {Array} foot 490 * @returns {Array} of length 4 containing the projected 491 * point in homogeneous coordinates. 492 */ 493 project2DTo3DPlane: function (point2d, normal, foot) { 494 var mat, rhs, d, le, 495 n = normal.slice(1), 496 sol = [1, 0, 0, 0]; 497 498 foot = foot || [1, 0, 0, 0]; 499 le = Mat.norm(n, 3); 500 d = Mat.innerProduct(foot.slice(1), n, 3) / le; 501 502 mat = this.matrix3D.slice(0, 3); // True copy 503 mat.push([0].concat(n)); 504 505 // 2D coordinates of point: 506 rhs = point2d.coords.usrCoords.concat([d]); 507 try { 508 // Prevent singularity in case elevation angle is zero 509 if (mat[2][3] === 1.0) { 510 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 511 } 512 sol = Mat.Numerics.Gauss(mat, rhs); 513 } catch (err) { 514 sol = [0, NaN, NaN, NaN]; 515 } 516 517 return sol; 518 }, 519 520 /** 521 * Project a 2D coordinate to a new 3D position by keeping 522 * the 3D x, y coordinates and changing only the z coordinate. 523 * All horizontal moves of the 2D point are ignored. 524 * 525 * @param {JXG.Point} point2d 526 * @param {Array} coords3D 527 * @returns {Array} of length 4 containing the projected 528 * point in homogeneous coordinates. 529 */ 530 project2DTo3DVertical: function (point2d, coords3D) { 531 var m3D = this.matrix3D[2], 532 b = m3D[3], 533 rhs = point2d.coords.usrCoords[2]; // y in 2D 534 535 rhs -= m3D[0] * m3D[0] + m3D[1] * coords3D[1] + m3D[2] * coords3D[2]; 536 if (Math.abs(b) < Mat.eps) { 537 return coords3D; // No changes 538 } else { 539 return coords3D.slice(0, 3).concat([rhs / b]); 540 } 541 }, 542 543 /** 544 * Limit 3D coordinates to the bounding cube. 545 * 546 * @param {Array} c3d 3D coordinates [x,y,z] 547 * @returns Array with updated 3D coordinates. 548 */ 549 project3DToCube: function (c3d) { 550 var cube = this.bbox3D; 551 if (c3d[1] < cube[0][0]) { 552 c3d[1] = cube[0][0]; 553 } 554 if (c3d[1] > cube[0][1]) { 555 c3d[1] = cube[0][1]; 556 } 557 if (c3d[2] < cube[1][0]) { 558 c3d[2] = cube[1][0]; 559 } 560 if (c3d[2] > cube[1][1]) { 561 c3d[2] = cube[1][1]; 562 } 563 if (c3d[3] < cube[2][0]) { 564 c3d[3] = cube[2][0]; 565 } 566 if (c3d[3] > cube[2][1]) { 567 c3d[3] = cube[2][1]; 568 } 569 570 return c3d; 571 }, 572 573 /** 574 * Intersect a ray with the bounding cube of the 3D view. 575 * @param {Array} p 3D coordinates [x,y,z] 576 * @param {Array} d 3D direction vector of the line (array of length 3) 577 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 578 * @returns Affine ratio of the intersection of the line with the cube. 579 */ 580 intersectionLineCube: function (p, d, r) { 581 var rnew, i, r0, r1; 582 583 rnew = r; 584 for (i = 0; i < 3; i++) { 585 if (d[i] !== 0) { 586 r0 = (this.bbox3D[i][0] - p[i]) / d[i]; 587 r1 = (this.bbox3D[i][1] - p[i]) / d[i]; 588 if (r < 0) { 589 rnew = Math.max(rnew, Math.min(r0, r1)); 590 } else { 591 rnew = Math.min(rnew, Math.max(r0, r1)); 592 } 593 } 594 } 595 return rnew; 596 }, 597 598 /** 599 * Test if coordinates are inside of the bounding cube. 600 * @param {array} q 3D coordinates [x,y,z] of a point. 601 * @returns Boolean 602 */ 603 isInCube: function (q) { 604 return ( 605 q[0] > this.bbox3D[0][0] - Mat.eps && 606 q[0] < this.bbox3D[0][1] + Mat.eps && 607 q[1] > this.bbox3D[1][0] - Mat.eps && 608 q[1] < this.bbox3D[1][1] + Mat.eps && 609 q[2] > this.bbox3D[2][0] - Mat.eps && 610 q[2] < this.bbox3D[2][1] + Mat.eps 611 ); 612 }, 613 614 /** 615 * 616 * @param {JXG.Plane3D} plane1 617 * @param {JXG.Plane3D} plane2 618 * @param {JXG.Plane3D} d 619 * @returns {Array} of length 2 containing the coordinates of the defining points of 620 * of the intersection segment. 621 */ 622 intersectionPlanePlane: function (plane1, plane2, d) { 623 var ret = [[], []], 624 p, 625 dir, 626 r, 627 q; 628 629 d = d || plane2.d; 630 631 p = Mat.Geometry.meet3Planes( 632 plane1.normal, 633 plane1.d, 634 plane2.normal, 635 d, 636 Mat.crossProduct(plane1.normal, plane2.normal), 637 0 638 ); 639 dir = Mat.Geometry.meetPlanePlane( 640 plane1.vec1, 641 plane1.vec2, 642 plane2.vec1, 643 plane2.vec2 644 ); 645 r = this.intersectionLineCube(p, dir, Infinity); 646 q = Mat.axpy(r, dir, p); 647 if (this.isInCube(q)) { 648 ret[0] = q; 649 } 650 r = this.intersectionLineCube(p, dir, -Infinity); 651 q = Mat.axpy(r, dir, p); 652 if (this.isInCube(q)) { 653 ret[1] = q; 654 } 655 return ret; 656 }, 657 658 /** 659 * Generate mesh for a surface / plane. 660 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 661 * @param {Array|Function} func 662 * @param {Array} interval_u 663 * @param {Array} interval_v 664 * @returns Array 665 * @private 666 * 667 * @example 668 * var el = view.create('curve', [[], []]); 669 * el.updateDataArray = function () { 670 * var steps_u = Type.evaluate(this.visProp.stepsu), 671 * steps_v = Type.evaluate(this.visProp.stepsv), 672 * r_u = Type.evaluate(this.range_u), 673 * r_v = Type.evaluate(this.range_v), 674 * func, ret; 675 * 676 * if (this.F !== null) { 677 * func = this.F; 678 * } else { 679 * func = [this.X, this.Y, this.Z]; 680 * } 681 * ret = this.view.getMesh(func, 682 * r_u.concat([steps_u]), 683 * r_v.concat([steps_v])); 684 * 685 * this.dataX = ret[0]; 686 * this.dataY = ret[1]; 687 * }; 688 * 689 */ 690 getMesh: function (func, interval_u, interval_v) { 691 var i_u, i_v, u, v, 692 c2d, delta_u, delta_v, 693 p = [0, 0, 0], 694 steps_u = interval_u[2], 695 steps_v = interval_v[2], 696 dataX = [], 697 dataY = []; 698 699 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u; 700 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v; 701 702 for (i_u = 0; i_u <= steps_u; i_u++) { 703 u = interval_u[0] + delta_u * i_u; 704 for (i_v = 0; i_v <= steps_v; i_v++) { 705 v = interval_v[0] + delta_v * i_v; 706 if (Type.isFunction(func)) { 707 p = func(u, v); 708 } else { 709 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 710 } 711 c2d = this.project3DTo2D(p); 712 dataX.push(c2d[1]); 713 dataY.push(c2d[2]); 714 } 715 dataX.push(NaN); 716 dataY.push(NaN); 717 } 718 719 for (i_v = 0; i_v <= steps_v; i_v++) { 720 v = interval_v[0] + delta_v * i_v; 721 for (i_u = 0; i_u <= steps_u; i_u++) { 722 u = interval_u[0] + delta_u * i_u; 723 if (Type.isFunction(func)) { 724 p = func(u, v); 725 } else { 726 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 727 } 728 c2d = this.project3DTo2D(p); 729 dataX.push(c2d[1]); 730 dataY.push(c2d[2]); 731 } 732 dataX.push(NaN); 733 dataY.push(NaN); 734 } 735 736 return [dataX, dataY]; 737 }, 738 739 /** 740 * 741 */ 742 animateAzimuth: function () { 743 var s = this.az_slide._smin, 744 e = this.az_slide._smax, 745 sdiff = e - s, 746 newVal = this.az_slide.Value() + 0.1; 747 748 this.az_slide.position = (newVal - s) / sdiff; 749 if (this.az_slide.position > 1) { 750 this.az_slide.position = 0.0; 751 } 752 this.board.update(); 753 754 this.timeoutAzimuth = setTimeout( 755 function () { 756 this.animateAzimuth(); 757 }.bind(this), 758 200 759 ); 760 }, 761 762 /** 763 * 764 */ 765 stopAzimuth: function () { 766 clearTimeout(this.timeoutAzimuth); 767 this.timeoutAzimuth = null; 768 }, 769 770 /** 771 * Check if vertical dragging is enabled and which action is needed. 772 * Default is shiftKey. 773 * 774 * @returns Boolean 775 * @private 776 */ 777 isVerticalDrag: function () { 778 var b = this.board, 779 key; 780 if (!Type.evaluate(this.visProp.verticaldrag.enabled)) { 781 return false; 782 } 783 key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key'; 784 return b[key]; 785 } 786 }); 787 788 /** 789 * @class This element creates a 3D view. 790 * @pseudo 791 * @description A View3D element provides the container and the methods to create and display 3D elements. 792 * It is contained in a JSXGraph board. 793 * @name View3D 794 * @augments JXG.View3D 795 * @constructor 796 * @type Object 797 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 798 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 799 * dim is an array of the form [w, h]. 800 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 801 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 802 * [x,y] and side lengths [w, h] of the board. 803 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 804 * which determines the coordinate ranges of the 3D cube. 805 * 806 * @example 807 * var bound = [-5, 5]; 808 * var view = board.create('view3d', 809 * [[-6, -3], 810 * [8, 8], 811 * [bound, bound, bound]], 812 * { 813 * // Main axes 814 * axesPosition: 'center', 815 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 816 * 817 * // Planes 818 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 819 * yPlaneFront: { visible: true, fillColor: 'blue'}, 820 * 821 * // Axes on planes 822 * xPlaneRearYAxis: {strokeColor: 'red'}, 823 * xPlaneRearZAxis: {strokeColor: 'red'}, 824 * 825 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 826 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 827 * 828 * zPlaneFrontXAxis: {visible: false}, 829 * zPlaneFrontYAxis: {visible: false} 830 * }); 831 * 832 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 833 * <script type="text/javascript"> 834 * (function() { 835 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 836 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 837 * var bound = [-5, 5]; 838 * var view = board.create('view3d', 839 * [[-6, -3], [8, 8], 840 * [bound, bound, bound]], 841 * { 842 * // Main axes 843 * axesPosition: 'center', 844 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 845 * // Planes 846 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 847 * yPlaneFront: { visible: true, fillColor: 'blue'}, 848 * // Axes on planes 849 * xPlaneRearYAxis: {strokeColor: 'red'}, 850 * xPlaneRearZAxis: {strokeColor: 'red'}, 851 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 852 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 853 * zPlaneFrontXAxis: {visible: false}, 854 * zPlaneFrontYAxis: {visible: false} 855 * }); 856 * })(); 857 * 858 * </script><pre> 859 * 860 */ 861 JXG.createView3D = function (board, parents, attributes) { 862 var view, attr, 863 x, y, w, h, 864 coords = parents[0], // llft corner 865 size = parents[1]; // [w, h] 866 867 attr = Type.copyAttributes(attributes, board.options, "view3d"); 868 view = new JXG.View3D(board, parents, attr); 869 view.defaultAxes = view.create("axes3d", parents, attributes); 870 871 x = coords[0]; 872 y = coords[1]; 873 w = size[0]; 874 h = size[1]; 875 876 /** 877 * Slider to adapt azimuth angle 878 * @name JXG.View3D#az_slide 879 * @type {Slider} 880 */ 881 view.az_slide = board.create( 882 "slider", 883 [ 884 [x - 1, y - 2], 885 [x + w + 1, y - 2], 886 [0, 1.0, 2 * Math.PI] 887 ], 888 { 889 style: 6, 890 name: "az", 891 point1: { frozen: true }, 892 point2: { frozen: true } 893 } 894 ); 895 896 /** 897 * Slider to adapt elevation angle 898 * 899 * @name JXG.View3D#el_slide 900 * @type {Slider} 901 */ 902 view.el_slide = board.create( 903 "slider", 904 [ 905 [x - 1, y], 906 [x - 1, y + h], 907 [0, 0.3, Math.PI / 2] 908 ], 909 { 910 style: 6, 911 name: "el", 912 point1: { frozen: true }, 913 point2: { frozen: true } 914 } 915 ); 916 917 view.board.highlightInfobox = function (x, y, el) { 918 var d, i, c3d, foot, 919 pre = '<span style="color:black; font-size:200%">\u21C4 </span>', 920 brd = el.board, 921 arr, infobox, 922 p = null; 923 924 if (view.isVerticalDrag()) { 925 pre = '<span style="color:black; font-size:200%">\u21C5 </span>'; 926 } 927 // Search 3D parent 928 for (i = 0; i < el.parents.length; i++) { 929 p = brd.objects[el.parents[i]]; 930 if (p.is3D) { 931 break; 932 } 933 } 934 if (p) { 935 foot = [1, 0, 0, p.coords[3]]; 936 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 937 if (!view.isInCube(c3d)) { 938 view.board.highlightCustomInfobox('', p); 939 return; 940 } 941 d = Type.evaluate(p.visProp.infoboxdigits); 942 infobox = view.board.infobox; 943 if (d === 'auto') { 944 if (infobox.useLocale()) { 945 arr = [pre, '(', infobox.formatNumberLocale(p.X()), ' | ', infobox.formatNumberLocale(p.Y()), ' | ', infobox.formatNumberLocale(p.Z()), ')']; 946 } else { 947 arr = [pre, '(', Type.autoDigits(p.X()), ' | ', Type.autoDigits(p.Y()), ' | ', Type.autoDigits(p.Z()), ')']; 948 } 949 950 } else { 951 if (infobox.useLocale()) { 952 arr = [pre, '(', infobox.formatNumberLocale(p.X(), d), ' | ', infobox.formatNumberLocale(p.Y(), d), ' | ', infobox.formatNumberLocale(p.Z(), d), ')']; 953 } else { 954 arr = [pre, '(', Type.toFixed(p.X(), d), ' | ', Type.toFixed(p.Y(), d), ' | ', Type.toFixed(p.Z(), d), ')']; 955 } 956 } 957 view.board.highlightCustomInfobox(arr.join(''), p); 958 } else { 959 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 960 } 961 }; 962 963 view.board.update(); 964 965 return view; 966 }; 967 968 JXG.registerElement("view3d", JXG.createView3D); 969 970 export default JXG.View3D; 971