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', '3d/threed', 32 ], function (JXG, Options, Const, Type, Mat, GeometryElement, ThreeD) { 33 "use strict"; 34 35 ThreeD.View3D = function (board, parents, attributes) { 36 var bbox3d, coords, size; 37 this.constructor(board, attributes, Const.OBJECT_TYPE_VIEW3D, Const.OBJECT_CLASS_CURVE); 38 39 bbox3d = parents[2]; // [[x1, x2], [y1,y2], [z1,z2]] 40 coords = parents[0]; // llft corner 41 size = parents[1]; // [w, h] 42 43 /** 44 * "Namespace" for all 3D handling 45 */ 46 this.D3 = {}; 47 48 /** 49 * An associative array containing all geometric objects belonging to the view. 50 * Key is the id of the object and value is a reference to the object. 51 * @type Object 52 */ 53 this.D3.objects = {}; 54 55 /** 56 * An array containing all geometric objects in this view in the order of construction. 57 * @type Array 58 */ 59 this.D3.objectsList = []; 60 61 /** 62 * @type {Object} contains the axes of the view or null 63 * @default null 64 */ 65 this.D3.defaultAxes = null; 66 67 /** 68 * 3D-to-2D transformation matrix 69 * @type {Array} 3 x 4 mattrix 70 */ 71 this.D3.matrix = [ 72 [1, 0, 0, 0], 73 [0, 1, 0, 0], 74 [0, 0, 1, 0] 75 ]; 76 77 // Bounding box (cube) [[x1, x2], [y1,y2], [z1,z2]]: 78 this.D3.bbox3d = bbox3d; 79 this.D3.coords = coords; 80 this.D3.size = size; 81 82 /** 83 * Distance of the view to the origin. In other words, its 84 * the radius of the sphere where the camera sits. 85 */ 86 this.D3.r = -1; 87 88 this.timeoutAzimuth = null; 89 90 this.id = this.board.setId(this, 'V'); 91 this.board.finalizeAdding(this); 92 this.elType = 'view3d'; 93 this.methodMap = Type.deepCopy(this.methodMap, { 94 }); 95 }; 96 ThreeD.View3D.prototype = new GeometryElement(); 97 98 JXG.extend(ThreeD.View3D.prototype, /** @lends ThreeD.View3D.prototype */ { 99 create: function (elementType, parents, attributes) { 100 var prefix = [], 101 is3D = false, 102 el; 103 104 if (elementType.indexOf('3d') > 0) { 105 is3D = true; 106 prefix.push(this); 107 } 108 el = this.board.create(elementType, prefix.concat(parents), attributes); 109 if (true || is3D) { 110 this.add(el); 111 } 112 return el; 113 }, 114 115 add: function (el) { 116 this.D3.objects[el.id] = el; 117 this.D3.objectsList.push(el); 118 }, 119 120 /** 121 * Update 3D-to-2D transformation matrix with the actual 122 * elevation and azimuth angles. 123 * 124 * @private 125 */ 126 update: function () { 127 var D3 = this.D3, 128 e, r, a, f, mat; 129 130 if (!Type.exists(D3.el_slide) || 131 !Type.exists(D3.az_slide) || 132 !this.needsUpdate) { 133 return this; 134 } 135 136 e = D3.el_slide.Value(); 137 r = D3.r; 138 a = D3.az_slide.Value(); 139 f = r * Math.sin(e); 140 mat = [[1, 0, 0,], [0, 1, 0], [0, 0, 1]]; 141 142 D3.matrix = [ 143 [1, 0, 0, 0], 144 [0, 1, 0, 0], 145 [0, 0, 1, 0] 146 ]; 147 148 D3.matrix[1][1] = r * Math.cos(a); 149 D3.matrix[1][2] = -r * Math.sin(a); 150 D3.matrix[2][1] = f * Math.sin(a); 151 D3.matrix[2][2] = f * Math.cos(a); 152 D3.matrix[2][3] = Math.cos(e); 153 154 if (true) { 155 mat[1][1] = D3.size[0] / (D3.bbox3d[0][1] - D3.bbox3d[0][0]); // w / d_x 156 mat[2][2] = D3.size[1] / (D3.bbox3d[1][1] - D3.bbox3d[1][0]); // h / d_y 157 mat[1][0] = D3.coords[0] - mat[1][1] * D3.bbox3d[0][0]; // llft_x 158 mat[2][0] = D3.coords[1] - mat[2][2] * D3.bbox3d[1][0]; // llft_y 159 160 D3.matrix = Mat.matMatMult(mat, D3.matrix); 161 } 162 163 return this; 164 }, 165 166 updateRenderer: function () { 167 this.needsUpdate = false; 168 return this; 169 }, 170 171 /** 172 * Project 3D coordinates to 2D board coordinates 173 * The 3D coordinates are provides as three numbers x, y, z or one array of length 3. 174 * 175 * @param {Number|Array} x 176 * @param {[Number]} y 177 * @param {[Number]} z 178 * @returns {Array} Array of length 3 containing the projection on to the board 179 * in homogeneous user coordinates. 180 */ 181 project3DTo2D: function (x, y, z) { 182 var vec; 183 if (arguments.length === 3) { 184 vec = [1, x, y, z]; 185 } else { 186 // Argument is an array 187 if (x.length === 3) { 188 vec = [1].concat(x); 189 } else { 190 vec = x; 191 } 192 } 193 return Mat.matVecMult(this.D3.matrix, vec); 194 }, 195 196 /** 197 * Project a 2D coordinate to the plane through the origin 198 * defined by its normal vector `normal`. 199 * 200 * @param {JXG.Point} point 201 * @param {Array} normal 202 * @returns Array of length 4 containing the projected 203 * point in homogeneous coordinates. 204 */ 205 project2DTo3DPlane: function (point, normal, foot) { 206 var mat, rhs, d, le, 207 n = normal.slice(1), 208 sol = [1, 0, 0, 0]; 209 210 foot = foot || [1, 0, 0, 0]; 211 le = Mat.norm(n, 3); 212 d = Mat.innerProduct(foot.slice(1), n, 3) / le; 213 214 mat = this.D3.matrix.slice(0, 3); // True copy 215 mat.push([0].concat(n)); 216 217 // 2D coordinates of point: 218 rhs = point.coords.usrCoords.concat([d]); 219 try { 220 // Prevent singularity in case elevation angle is zero 221 if (mat[2][3] === 1.0) { 222 mat[2][1] = mat[2][2] = Mat.eps * 0.001; 223 } 224 sol = Mat.Numerics.Gauss(mat, rhs); 225 } catch (err) { 226 sol = [0, NaN, NaN, NaN]; 227 } 228 229 return sol; 230 }, 231 232 project3DToCube: function (c3d) { 233 var cube = this.D3.bbox3d; 234 if (c3d[1] < cube[0][0]) { c3d[1] = cube[0][0]; } 235 if (c3d[1] > cube[0][1]) { c3d[1] = cube[0][1]; } 236 if (c3d[2] < cube[1][0]) { c3d[2] = cube[1][0]; } 237 if (c3d[2] > cube[1][1]) { c3d[2] = cube[1][1]; } 238 if (c3d[3] < cube[2][0]) { c3d[3] = cube[2][0]; } 239 if (c3d[3] > cube[2][1]) { c3d[3] = cube[2][1]; } 240 241 return c3d; 242 }, 243 244 intersectionLineCube: function (p, d, r) { 245 var rnew, i, r0, r1; 246 247 rnew = r; 248 for (i = 0; i < 3; i++) { 249 if (d[i] !== 0) { 250 r0 = (this.D3.bbox3d[i][0] - p[i]) / d[i]; 251 r1 = (this.D3.bbox3d[i][1] - p[i]) / d[i]; 252 if (r < 0) { 253 rnew = Math.max(rnew, Math.min(r0, r1)); 254 } else { 255 rnew = Math.min(rnew, Math.max(r0, r1)); 256 } 257 } 258 } 259 return rnew; 260 }, 261 262 isInCube: function (q) { 263 return q[0] > this.D3.bbox3d[0][0] - Mat.eps && q[0] < this.D3.bbox3d[0][1] + Mat.eps && 264 q[1] > this.D3.bbox3d[1][0] - Mat.eps && q[1] < this.D3.bbox3d[1][1] + Mat.eps && 265 q[2] > this.D3.bbox3d[2][0] - Mat.eps && q[2] < this.D3.bbox3d[2][1] + Mat.eps; 266 }, 267 268 /** 269 * 270 * @param {*} plane1 271 * @param {*} plane2 272 * @param {*} d 273 * @returns Array of length 2 containing the coordinates of the defining points of 274 * of the intersection segment. 275 */ 276 intersectionPlanePlane: function(plane1, plane2, d) { 277 var ret = [[], []], 278 p, dir, r, q; 279 280 d = d || plane2.D3.d; 281 282 p = Mat.Geometry.meet3Planes(plane1.D3.normal, plane1.D3.d, plane2.D3.normal, d, 283 Mat.crossProduct(plane1.D3.normal, plane2.D3.normal), 0); 284 dir = Mat.Geometry.meetPlanePlane(plane1.D3.dir1, plane1.D3.dir2, plane2.D3.dir1, plane2.D3.dir2); 285 r = this.intersectionLineCube(p, dir, Infinity); 286 q = Mat.axpy(r, dir, p); 287 if (this.isInCube(q)) { 288 ret[0] = q; 289 } 290 r = this.intersectionLineCube(p, dir, -Infinity); 291 q = Mat.axpy(r, dir, p); 292 if (this.isInCube(q) ) { 293 ret[1] = q; 294 } 295 return ret; 296 }, 297 298 getMesh: function (X, Y, Z, interval_u, interval_v) { 299 var i_u, i_v, u, v, c2d, 300 delta_u, delta_v, 301 p = [0, 0, 0], 302 steps_u = interval_u[2], 303 steps_v = interval_v[2], 304 305 dataX = [], 306 dataY = []; 307 308 delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / (steps_u); 309 delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / (steps_v); 310 311 for (i_u = 0; i_u <= steps_u; i_u++) { 312 u = interval_u[0] + delta_u * i_u; 313 for (i_v = 0; i_v <= steps_v; i_v++) { 314 v = interval_v[0] + delta_v * i_v; 315 p[0] = X(u, v); 316 p[1] = Y(u, v); 317 p[2] = Z(u, v); 318 c2d = this.project3DTo2D(p); 319 dataX.push(c2d[1]); 320 dataY.push(c2d[2]); 321 } 322 dataX.push(NaN); 323 dataY.push(NaN); 324 } 325 326 for (i_v = 0; i_v <= steps_v; i_v++) { 327 v = interval_v[0] + delta_v * i_v; 328 for (i_u = 0; i_u <= steps_u; i_u++) { 329 u = interval_u[0] + delta_u * i_u; 330 p[0] = X(u, v); 331 p[1] = Y(u, v); 332 p[2] = Z(u, v); 333 c2d = this.project3DTo2D(p); 334 dataX.push(c2d[1]); 335 dataY.push(c2d[2]); 336 } 337 dataX.push(NaN); 338 dataY.push(NaN); 339 } 340 341 return [dataX, dataY]; 342 }, 343 344 animateAzimuth: function () { 345 var s = this.D3.az_slide._smin, 346 e = this.D3.az_slide._smax, 347 sdiff = e - s, 348 newVal = this.D3.az_slide.Value() + 0.1; 349 350 this.D3.az_slide.position = ((newVal - s) / sdiff); 351 if (this.D3.az_slide.position > 1) { 352 this.D3.az_slide.position = 0.0; 353 } 354 this.board.update(); 355 356 this.timeoutAzimuth = setTimeout(function () { this.animateAzimuth(); }.bind(this), 200); 357 }, 358 359 stopAzimuth: function () { 360 clearTimeout(this.timeoutAzimuth); 361 this.timeoutAzimuth = null; 362 } 363 }); 364 365 /** 366 * @class This element creates a 3D view. 367 * @pseudo 368 * @description A View3D element provides the container and the methods to create and display 3D elements. 369 * It is contained in a JSXGraph board. 370 * @name View3D 371 * @augments JXG.View3D 372 * @constructor 373 * @type Object 374 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 375 * @param {Array_Array_Array} lower,dim,cube Here, lower is an array of the form [x, y] and 376 * dim is an array of the form [w, h]. 377 * The arrays [x, y] and [w, h] define the 2D frame into which the 3D cube is 378 * (roughly) projected. 379 * cube is an array of the form [[x1, x2], [y1, y2], [z1, z2]] 380 * which determines the coordinate ranges of the 3D cube. 381 * 382 * @example 383 * var bound = [-5, 5]; 384 * var view = board.create('view3d', 385 * [[-6, -3], 386 * [8, 8], 387 * [bound, bound, bound]], 388 * { 389 * // Main axes 390 * axesPosition: 'center', 391 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 392 * 393 * // Planes 394 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 395 * yPlaneFront: { visible: true, fillColor: 'blue'}, 396 * 397 * // Axes on planes 398 * xPlaneRearYAxis: {strokeColor: 'red'}, 399 * xPlaneRearZAxis: {strokeColor: 'red'}, 400 * 401 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 402 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 403 * 404 * zPlaneFrontXAxis: {visible: false}, 405 * zPlaneFrontYAxis: {visible: false} 406 * }); 407 * 408 * </pre><div id="JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7" class="jxgbox" style="width: 500px; height: 500px;"></div> 409 * <script type="text/javascript"> 410 * (function() { 411 * var board = JXG.JSXGraph.initBoard('JXGdd06d90e-be5d-4531-8f0b-65fc30b1a7c7', 412 * {boundingbox: [-8, 8, 8,-8], axis: false, showcopyright: false, shownavigation: false}); 413 * var bound = [-5, 5]; 414 * var view = board.create('view3d', 415 * [[-6, -3], [8, 8], 416 * [bound, bound, bound]], 417 * { 418 * // Main axes 419 * axesPosition: 'center', 420 * xAxis: { strokeColor: 'blue', strokeWidth: 3}, 421 * 422 * // Planes 423 * xPlaneRear: { fillColor: 'yellow', mesh3d: {visible: false}}, 424 * yPlaneFront: { visible: true, fillColor: 'blue'}, 425 * 426 * // Axes on planes 427 * xPlaneRearYAxis: {strokeColor: 'red'}, 428 * xPlaneRearZAxis: {strokeColor: 'red'}, 429 * 430 * yPlaneFrontXAxis: {strokeColor: 'blue'}, 431 * yPlaneFrontZAxis: {strokeColor: 'blue'}, 432 * 433 * zPlaneFrontXAxis: {visible: false}, 434 * zPlaneFrontYAxis: {visible: false} 435 * }); 436 * 437 * })(); 438 * 439 * </script><pre> 440 * 441 */ 442 ThreeD.createView3D = function (board, parents, attributes) { 443 var view, frame, attr, 444 x, y, w, h, 445 coords = parents[0], // llft corner 446 size = parents[1]; // [w, h] 447 448 attr = Type.copyAttributes(attributes, board.options, 'view3d'); 449 view = new ThreeD.View3D(board, parents, attr); 450 view.defaultAxes = view.create('axes3d', parents, attributes); 451 452 x = coords[0]; 453 y = coords[1]; 454 w = size[0]; 455 h = size[1]; 456 457 /* 458 * Frame around the view object 459 */ 460 if (false) { 461 frame = board.create('polygon', [ 462 [coords[0], coords[1] + size[1]], // ulft 463 [coords[0], coords[1]], // llft 464 [coords[0] + size[0], coords[1]], // lrt 465 [coords[0] + size[0], coords[1] + size[1]], // urt 466 ], { 467 fillColor: 'none', 468 highlightFillColor: 'none', 469 highlight: false, 470 vertices: { 471 fixed: true, 472 visible: false 473 }, 474 borders: { 475 strokeColor: 'black', 476 highlight: false, 477 strokeWidth: 0.5, 478 dash: 4 479 } 480 }); 481 //view.add(frame); 482 } 483 484 /** 485 * Slider to adapt azimuth angle 486 */ 487 view.D3.az_slide = board.create('slider', [[x - 1, y - 2], [x + w + 1, y - 2], [0, 1.0, 2 * Math.PI]], { 488 style: 6, name: 'az', 489 point1: { frozen: true }, 490 point2: { frozen: true } 491 }); 492 493 /** 494 * Slider to adapt elevation angle 495 */ 496 view.D3.el_slide = board.create('slider', [[x - 1, y], [x - 1, y + h], [0, 0.30, Math.PI / 2]], { 497 style: 6, name: 'el', 498 point1: { frozen: true }, 499 point2: { frozen: true } 500 }); 501 502 view.board.highlightInfobox = function (x, y, el) { 503 var d; 504 505 if (Type.exists(el.D3)) { 506 d = Type.evaluate(el.visProp.infoboxdigits); 507 if (d === 'auto') { 508 view.board.highlightCustomInfobox('(' + 509 Type.autoDigits(el.D3.X()) + ' | ' + 510 Type.autoDigits(el.D3.Y()) + ' | ' + 511 Type.autoDigits(el.D3.Z()) + ')', el); 512 } else { 513 view.board.highlightCustomInfobox('(' + 514 Type.toFixed(el.D3.X(), d) + ' | ' + 515 Type.toFixed(el.D3.Y(), d) + ' | ' + 516 Type.toFixed(el.D3.Z(), d) + ')', el); 517 } 518 } else { 519 view.board.highlightCustomInfobox('(' + x + ', ' + y + ')', el); 520 } 521 }; 522 523 return view; 524 }; 525 JXG.registerElement('view3d', ThreeD.createView3D); 526 527 return ThreeD.View3D; 528 }); 529 530