1 /* 2 Copyright 2008-2022 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 <http://www.gnu.org/licenses/> 27 and <http://opensource.org/licenses/MIT/>. 28 */ 29 /*global JXG:true, define: true*/ 30 31 define(['jxg', 'options', 'base/constants', 'utils/type', 'math/math', 'base/element','base/composition'], 32 function (JXG, Options, Const, Type, Mat, GeometryElement, Composition) { 33 "use strict"; 34 35 /** 36 * 3D view inside of a JXGraph board. 37 * 38 * @class Creates a new 3D view. Do not use this constructor to create a 3D view. Use {@link JXG.Board#create} with 39 * type {@link View3D} instead. 40 * 41 * @augments JXG.GeometryElement 42 * @param {Array} parents Array consisting of lower left corner [x, y] of the view inside the board, [width, height] of the view 43 * 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 44 * [x,y] and side lengths [w, h] of the board. 45 */ 46 JXG.View3D = function (board, parents, attributes) { 47 this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_3D); 48 49 /** 50 * An associative array containing all geometric objects belonging to the view. 51 * Key is the id of the object and value is a reference to the object. 52 * @type Object 53 * @private 54 */ 55 this.objects = {}; 56 57 /** 58 * An array containing all geometric objects in this view in the order of construction. 59 * @type Array 60 * @private 61 */ 62 this.objectsList = []; 63 64 /** 65 * 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. 66 * @type Object 67 * @private 68 */ 69 this.elementsByName = {}; 70 71 /** 72 * Default axes of the 3D view, contains the axes of the view or null. 73 * 74 * @type {Object} 75 * @default null 76 */ 77 this.defaultAxes = null; 78 79 /** 80 * 3D-to-2D transformation matrix 81 * @type {Array} 3 x 4 matrix 82 * @private 83 */ 84 this.matrix3D = [ 85 [1, 0, 0, 0], 86 [0, 1, 0, 0], 87 [0, 0, 1, 0] 88 ]; 89 90 /** 91 * Lower left corner [x, y] of the 3D view if elevation and azimuth are set to 0. 92 * @type array 93 * @private 94 */ 95 this.llftCorner = parents[0]; 96 97 /** 98 * Width and height [w, h] of the 3D view if elevation and azimuth are set to 0. 99 * @type array 100 * @private 101 */ 102 this.size = parents[1]; 103 104 /** 105 * Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]] of the 3D view 106 * @type array 107 */ 108 this.bbox3D = parents[2]; 109 110 /** 111 * Distance of the view to the origin. In other words, its 112 * the radius of the sphere where the camera sits.view.board.update 113 * @type Number 114 */ 115 this.r = -1; 116 117 this.timeoutAzimuth = null; 118 119 this.id = this.board.setId(this, 'V'); 120 this.board.finalizeAdding(this); 121 this.elType = 'view3d'; 122 123 this.methodMap = Type.deepCopy(this.methodMap, { 124 // TODO 125 }); 126 }; 127 JXG.View3D.prototype = new GeometryElement(); 128 129 JXG.extend(JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ { 130 131 /** 132 * Creates a new 3D element of type elementType. 133 * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'. 134 * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two 135 * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create* 136 * methods for a list of possible parameters. 137 * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType. 138 * Common attributes are name, visible, strokeColor. 139 * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing 140 * two or more elements. 141 */ 142 create: function (elementType, parents, attributes) { 143 var prefix = [], 144 is3D = false, 145 el; 146 147 if (elementType.indexOf('3d') > 0) { 148 is3D = true; 149 prefix.push(this); 150 } 151 el = this.board.create(elementType, prefix.concat(parents), attributes); 152 153 return el; 154 }, 155 156 /** 157 * Select a single or multiple elements at once. 158 * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will 159 * be used as a filter to return multiple elements at once filtered by the properties of the object. 160 * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId. 161 * The advanced filters consisting of objects or functions are ignored. 162 * @returns {JXG.GeometryElement3D|JXG.Composition} 163 * @example 164 * // select the element with name A 165 * view.select('A'); 166 * 167 * // select all elements with strokecolor set to 'red' (but not '#ff0000') 168 * view.select({ 169 * strokeColor: 'red' 170 * }); 171 * 172 * // select all points on or below the x/y plane and make them black. 173 * view.select({ 174 * elType: 'point3d', 175 * Z: function (v) { 176 * return v <= 0; 177 * } 178 * }).setAttribute({color: 'black'}); 179 * 180 * // select all elements 181 * view.select(function (el) { 182 * return true; 183 * }); 184 */ 185 select: function (str, onlyByIdOrName) { 186 var flist, olist, i, l, 187 s = str; 188 189 if (s === null) { 190 return s; 191 } 192 193 // It's a string, most likely an id or a name. 194 if (Type.isString(s) && s !== '') { 195 // Search by ID 196 if (Type.exists(this.objects[s])) { 197 s = this.objects[s]; 198 // Search by name 199 } else if (Type.exists(this.elementsByName[s])) { 200 s = this.elementsByName[s]; 201 // // Search by group ID 202 // } else if (Type.exists(this.groups[s])) { 203 // s = this.groups[s]; 204 } 205 206 // It's a function or an object, but not an element 207 } else if (!onlyByIdOrName && 208 (Type.isFunction(s) || 209 (Type.isObject(s) && !Type.isFunction(s.setAttribute)) 210 )) { 211 flist = Type.filterElements(this.objectsList, s); 212 213 olist = {}; 214 l = flist.length; 215 for (i = 0; i < l; i++) { 216 olist[flist[i].id] = flist[i]; 217 } 218 s = new Composition(olist); 219 220 // It's an element which has been deleted (and still hangs around, e.g. in an attractor list 221 } else if (Type.isObject(s) && Type.exists(s.id) && !Type.exists(this.objects[s.id])) { 222 s = null; 223 } 224 225 return s; 226 }, 227 228 update: function () { 229 // Update 3D-to-2D transformation matrix with the actual 230 // elevation and azimuth angles. 231 232 var e, r, a, f, mat; 233 234 if (!Type.exists(this.el_slide) || 235 !Type.exists(this.az_slide) || 236 !this.needsUpdate) { 237 return this; 238 } 239 240 e = this.el_slide.Value(); 241 r = this.r; 242 a = this.az_slide.Value(); 243 f = r * Math.sin(e); 244 mat = [[1, 0, 0,], [0, 1, 0], [0, 0, 1]]; 245 246 this.matrix3D = [ 247 [1, 0, 0, 0], 248 [0, 1, 0, 0], 249 [0, 0, 1, 0] 250 ]; 251 252 this.matrix3D[1][1] = r * Math.cos(a); 253 this.matrix3D[1][2] = -r * Math.sin(a); 254 this.matrix3D[2][1] = f * Math.sin(a); 255 this.matrix3D[2][2] = f * Math.cos(a); 256 this.matrix3D[2][3] = Math.cos(e); 257 258 mat[1][1] = this.size[0] / (this.bbox3D[0][1] - this.bbox3D[0][0]); // w / d_x 259 mat[2][2] = this.size[1] / (this.bbox3D[1][1] - this.bbox3D[1][0]); // h / d_y 260 mat[1][0] = this.llftCorner[0] - mat[1][1] * this.bbox3D[0][0]; // llft_x 261 mat[2][0] = this.llftCorner[1] - mat[2][2] * this.bbox3D[1][0]; // llft_y 262 this.matrix3D = Mat.matMatMult(mat, this.matrix3D); 263 264 return this; 265 }, 266 267 updateRenderer: function () { 268 this.needsUpdate = false; 269 return this; 270 }, 271 272 /** 273 * Project 3D coordinates to 2D board coordinates 274 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 275 * 276 * @param {Number|Array} x 277 * @param {[Number]} y 278 * @param {[Number]} z 279 * @returns {Array} Array of length 3 containing the projection on to the board 280 * in homogeneous user coordinates. 281 */ 282 project3DTo2D: function (x, y, z) { 283 var vec; 284 if (arguments.length === 3) { 285 vec = [1, x, y, z]; 286 } else { 287 // Argument is an array 288 if (x.length === 3) { 289 vec = [1].concat(x); 290 } else { 291 vec = x; 292 } 293 } 294 return Mat.matVecMult(this.matrix3D, vec); 295 }, 296 297 /** 298 * Project a 2D coordinate to the plane defined by the point foot 299 * and the normal vector `normal`. 300 * 301 * @param {JXG.Point} point 302 * @param {Array} normal 303 * @param {Array} foot 304 * @returns {Array} of length 4 containing the projected 305 * point in homogeneous coordinates. 306 */ 307 project2DTo3DPlane: function (point2d, normal, foot) { 308 var mat, rhs, d, le, 309 n = normal.slice(1), 310 sol = [1, 0, 0, 0]; 311 312 foot = foot || [1, 0, 0, 0]; 313 le = Mat.norm(n, 3); 314 d = Mat.innerProduct(foot.slice(1), n, 3) / le; 315 316 mat = this.matrix3D.slice(0, 3); // True copy 317 mat.push([0].concat(n)); 318 319 // 2D coordinates of point: 320 rhs = point2d.coords.usrCoords.concat([d]); 321 try { 322 // Prevent singularity in case elevation angle is zero 323 if (mat[2][3] === 1.0) { 324 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 325 } 326 sol = Mat.Numerics.Gauss(mat, rhs); 327 } catch (err) { 328 sol = [0, NaN, NaN, NaN]; 329 } 330 331 return sol; 332 }, 333 334 /** 335 * Limit 3D coordinates to the bounding cube. 336 * 337 * @param {Array} c3d 3D coordinates [x,y,z] 338 * @returns Array with updated 3D coordinates. 339 */ 340 project3DToCube: function (c3d) { 341 var cube = this.bbox3D; 342 if (c3d[1] < cube[0][0]) { c3d[1] = cube[0][0]; } 343 if (c3d[1] > cube[0][1]) { c3d[1] = cube[0][1]; } 344 if (c3d[2] < cube[1][0]) { c3d[2] = cube[1][0]; } 345 if (c3d[2] > cube[1][1]) { c3d[2] = cube[1][1]; } 346 if (c3d[3] < cube[2][0]) { c3d[3] = cube[2][0]; } 347 if (c3d[3] > cube[2][1]) { c3d[3] = cube[2][1]; } 348 349 return c3d; 350 }, 351 352 /** 353 * Intersect a ray with the bounding cube of the 3D view. 354 * @param {Array} p 3D coordinates [x,y,z] 355 * @param {Array} d 3D direction vector of the line (array of length 3) 356 * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0). 357 * @returns Affine ratio of the intersection of the line with the cube. 358 */ 359 intersectionLineCube: function (p, d, r) { 360 var rnew, i, r0, r1; 361 362 rnew = r; 363 for (i = 0; i < 3; i++) { 364 if (d[i] !== 0) { 365 r0 = (this.bbox3D[i][0] - p[i]) / d[i]; 366 r1 = (this.bbox3D[i][1] - p[i]) / d[i]; 367 if (r < 0) { 368 rnew = Math.max(rnew, Math.min(r0, r1)); 369 } else { 370 rnew = Math.min(rnew, Math.max(r0, r1)); 371 } 372 } 373 } 374 return rnew; 375 }, 376 377 /** 378 * Test if coordinates are inside of the bounding cube. 379 * @param {array} q 3D coordinates [x,y,z] of a point. 380 * @returns Boolean 381 */ 382 isInCube: function (q) { 383 return q[0] > this.bbox3D[0][0] - Mat.eps && q[0] < this.bbox3D[0][1] + Mat.eps && 384 q[1] > this.bbox3D[1][0] - Mat.eps && q[1] < this.bbox3D[1][1] + Mat.eps && 385 q[2] > this.bbox3D[2][0] - Mat.eps && q[2] < this.bbox3D[2][1] + Mat.eps; 386 }, 387 388 /** 389 * 390 * @param {JXG.Plane3D} plane1 391 * @param {JXG.Plane3D} plane2 392 * @param {JXG.Plane3D} d 393 * @returns {Array} of length 2 containing the coordinates of the defining points of 394 * of the intersection segment. 395 */ 396 intersectionPlanePlane: function(plane1, plane2, d) { 397 var ret = [[], []], 398 p, dir, r, q; 399 400 d = d || plane2.d; 401 402 p = Mat.Geometry.meet3Planes( 403 plane1.normal, plane1.d, plane2.normal, d, Mat.crossProduct(plane1.normal, plane2.normal 404 ), 0); 405 dir = Mat.Geometry.meetPlanePlane(plane1.vec1, plane1.vec2, plane2.vec1, plane2.vec2); 406 r = this.intersectionLineCube(p, dir, Infinity); 407 q = Mat.axpy(r, dir, p); 408 if (this.isInCube(q)) { 409 ret[0] = q; 410 } 411 r = this.intersectionLineCube(p, dir, -Infinity); 412 q = Mat.axpy(r, dir, p); 413 if (this.isInCube(q) ) { 414 ret[1] = q; 415 } 416 return ret; 417 }, 418 419 /** 420 * Generate mesh for a surface / plane. 421 * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function. 422 * @param {Array,Function} func 423 * @param {Array} interval_u 424 * @param {Array} interval_v 425 * @returns Array 426 * @private 427 * 428 * @example 429 * var el = view.create('curve', [[], []]); 430 * el.updateDataArray = function () { 431 * var steps_u = Type.evaluate(this.visProp.stepsu), 432 * steps_v = Type.evaluate(this.visProp.stepsv), 433 * r_u = Type.evaluate(this.range_u), 434 * r_v = Type.evaluate(this.range_v), 435 * func, ret; 436 * 437 * if (this.F !== null) { 438 * func = this.F; 439 * } else { 440 * func = [this.X, this.Y, this.Z]; 441 * } 442 * ret = this.view.getMesh(func, 443 * r_u.concat([steps_u]), 444 * r_v.concat([steps_v])); 445 * 446 * this.dataX = ret[0]; 447 * this.dataY = ret[1]; 448 * }; 449 * 450 */ 451 getMesh: function (func, interval_u, interval_v) { 452 var i_u, i_v, u, v, c2d, 453 delta_u, delta_v, 454 p = [0, 0, 0], 455 steps_u = interval_u[2], 456 steps_v = interval_v[2], 457 458 dataX = [], 459 dataY = []; 460 461 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / (steps_u); 462 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / (steps_v); 463 464 for (i_u = 0; i_u <= steps_u; i_u++) { 465 u = interval_u[0] + delta_u * i_u; 466 for (i_v = 0; i_v <= steps_v; i_v++) { 467 v = interval_v[0] + delta_v * i_v; 468 if (Type.isFunction(func)) { 469 p = func(u, v); 470 } else { 471 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 472 } 473 c2d = this.project3DTo2D(p); 474 dataX.push(c2d[1]); 475 dataY.push(c2d[2]); 476 } 477 dataX.push(NaN); 478 dataY.push(NaN); 479 } 480 481 for (i_v = 0; i_v <= steps_v; i_v++) { 482 v = interval_v[0] + delta_v * i_v; 483 for (i_u = 0; i_u <= steps_u; i_u++) { 484 u = interval_u[0] + delta_u * i_u; 485 if (Type.isFunction(func)) { 486 p = func(u, v); 487 } else { 488 p = [func[0](u, v), func[1](u, v), func[2](u, v)]; 489 } 490 c2d = this.project3DTo2D(p); 491 dataX.push(c2d[1]); 492 dataY.push(c2d[2]); 493 } 494 dataX.push(NaN); 495 dataY.push(NaN); 496 } 497 498 return [dataX, dataY]; 499 }, 500 501 /** 502 * 503 */ 504 animateAzimuth: function () { 505 var s = this.az_slide._smin, 506 e = this.az_slide._smax, 507 sdiff = e - s, 508 newVal = this.az_slide.Value() + 0.1; 509 510 this.az_slide.position = ((newVal - s) / sdiff); 511 if (this.az_slide.position > 1) { 512 this.az_slide.position = 0.0; 513 } 514 this.board.update(); 515 516 this.timeoutAzimuth = setTimeout(function () { this.animateAzimuth(); }.bind(this), 200); 517 }, 518 519 /** 520 * 521 */ 522 stopAzimuth: function () { 523 clearTimeout(this.timeoutAzimuth); 524 this.timeoutAzimuth = null; 525 } 526 }); 527 528 /** 529 * @class This element creates a 3D view. 530 * @pseudo 531 * @description A View3D element provides the container and the methods to create and display 3D elements. 532 * It is contained in a JSXGraph board. 533 * @name View3D 534 * @augments JXG.View3D 535 * @constructor 536 * @type Object 537 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 538 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 539 * dim is an array of the form [w, h]. 540 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 541 * (roughly) projected. If the view's azimuth=0 and elevation=0, the 3D view will cover a rectangle with lower left corner 542 * [x,y] and side lengths [w, h] of the board. 543 * The array 'cube' is of the form [[x1, x2], [y1, y2], [z1, z2]] 544 * which determines the coordinate ranges of the 3D cube. 545 * 546 * @example 547 * var bound = [-5, 5]; 548 * var view = board.create('view3d', 549 * [[-6, -3], 550 * [8, 8], 551 * [bound, bound, bound]], 552 * { 553 * // Main axes 554 * axesPosition: 'center', 555 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 556 * 557 * // Planes 558 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 559 * yPlaneFront: { visible: true, fillColor: 'blue'}, 560 * 561 * // Axes on planes 562 * xPlaneRearYAxis: {strokeColor: 'red'}, 563 * xPlaneRearZAxis: {strokeColor: 'red'}, 564 * 565 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 566 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 567 * 568 * zPlaneFrontXAxis: {visible: false}, 569 * zPlaneFrontYAxis: {visible: false} 570 * }); 571 * 572 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 573 * <script type="text/javascript"> 574 * (function() { 575 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 576 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 577 * var bound = [-5, 5]; 578 * var view = board.create('view3d', 579 * [[-6, -3], [8, 8], 580 * [bound, bound, bound]], 581 * { 582 * // Main axes 583 * axesPosition: 'center', 584 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 585 * // Planes 586 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 587 * yPlaneFront: { visible: true, fillColor: 'blue'}, 588 * // Axes on planes 589 * xPlaneRearYAxis: {strokeColor: 'red'}, 590 * xPlaneRearZAxis: {strokeColor: 'red'}, 591 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 592 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 593 * zPlaneFrontXAxis: {visible: false}, 594 * zPlaneFrontYAxis: {visible: false} 595 * }); 596 * })(); 597 * 598 * </script><pre> 599 * 600 */ 601 JXG.createView3D = function (board, parents, attributes) { 602 var view, attr, 603 x, y, w, h, 604 coords = parents[0], // llft corner 605 size = parents[1]; // [w, h] 606 607 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 608 view = new JXG.View3D(board, parents, attr); 609 view.defaultAxes = view.create('axes3d', parents, attributes); 610 611 x = coords[0]; 612 y = coords[1]; 613 w = size[0]; 614 h = size[1]; 615 616 /** 617 * Slider to adapt azimuth angle 618 * @name JXG.View3D#az_slide 619 * @type {Slider} 620 */ 621 view.az_slide = board.create('slider', [[x - 1, y - 2], [x + w + 1, y - 2], [0, 1.0, 2 * Math.PI]], { 622 style: 6, name: 'az', 623 point1: { frozen: true }, 624 point2: { frozen: true } 625 }); 626 627 /** 628 * Slider to adapt elevation angle 629 * 630 * @name JXG.View3D#el_slide 631 * @type {Slider} 632 */ 633 view.el_slide = board.create('slider', [[x - 1, y], [x - 1, y + h], [0, 0.30, Math.PI / 2]], { 634 style: 6, name: 'el', 635 point1: { frozen: true }, 636 point2: { frozen: true } 637 }); 638 639 view.board.highlightInfobox = function (x, y, el) { 640 var d, i, 641 c3d, foot, 642 brd = el.board, 643 p = null; 644 645 // Search 3D parent 646 for (i = 0; i < el.parents.length; i++) { 647 p = brd.objects[el.parents[i]]; 648 if (p.is3D) { 649 break; 650 } 651 } 652 if (p) { 653 foot = [1, 0, 0, p.coords[3]]; 654 c3d = view.project2DTo3DPlane(p.element2D, [1, 0, 0, 1], foot); 655 if (!view.isInCube(c3d)) { 656 view.board.highlightCustomInfobox('', p); 657 return; 658 } 659 d = Type.evaluate(p.visProp.infoboxdigits); 660 if (d === 'auto') { 661 view.board.highlightCustomInfobox('(' + 662 Type.autoDigits(p.X()) + ' | ' + 663 Type.autoDigits(p.Y()) + ' | ' + 664 Type.autoDigits(p.Z()) + ')', p); 665 } else { 666 view.board.highlightCustomInfobox('(' + 667 Type.toFixed(p.X(), d) + ' | ' + 668 Type.toFixed(p.Y(), d) + ' | ' + 669 Type.toFixed(p.Z(), d) + ')', p); 670 } 671 } else { 672 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 673 } 674 }; 675 676 view.board.update(); 677 678 return view; 679 }; 680 JXG.registerElement('view3d', JXG.createView3D); 681 682 return JXG.View3D; 683 }); 684 685