1 /* 2 Copyright 2008-2025 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.js"; 32 import Const from "../base/constants.js"; 33 import Mat from "../math/math.js"; 34 import Geometry from "../math/geometry.js"; 35 import Type from "../utils/type.js"; 36 //, GeometryElement3D) { 37 38 /** 39 * A 3D text is a basic geometric element. 40 * @class Creates a new 3D point object. Do not use this constructor to create a 3D point. Use {@link JXG.View3D#create} with 41 * type {@link Point3D} instead. 42 * @augments JXG.GeometryElement3D 43 * @augments JXG.GeometryElement 44 * @param {JXG.View3D} view The 3D view the point is drawn on. 45 * @param {Function|Array} F Array of numbers, array of functions or function returning an array with defines the user coordinates of the point. 46 * @param {JXG.GeometryElement3D} slide Object the 3D point should be bound to. If null, the point is a free point. 47 * @param {Object} attributes An object containing visual properties like in {@link JXG.Options#point3d} and 48 * {@link JXG.Options#elements}, and optional a name and an id. 49 * @see JXG.Board#generateName 50 */ 51 JXG.Text3D = function (view, F, text, slide, attributes) { 52 this.constructor(view.board, attributes, Const.OBJECT_TYPE_TEXT3D, Const.OBJECT_CLASS_3D); 53 this.constructor3D(view, "text3d"); 54 55 this.board.finalizeAdding(this); 56 57 /** 58 * Homogeneous coordinates of a Point3D, i.e. array of length 4: [w, x, y, z]. Usually, w=1 for finite points and w=0 for points 59 * which are infinitely far. 60 * 61 * @example 62 * p.coords; 63 * 64 * @name Point3D#coords 65 * @type Array 66 * @private 67 */ 68 this.coords = [0, 0, 0, 0]; 69 70 /** 71 * Function or array of functions or array of numbers defining the coordinates of the point, used in {@link updateCoords}. 72 * 73 * @name Point3D#F 74 * @function 75 * @private 76 * 77 * @see updateCoords 78 */ 79 this.F = F; 80 81 /** 82 * Optional slide element, i.e. element the Point3D lives on. 83 * 84 * @example 85 * p.slide; 86 * 87 * @name Point3D#slide 88 * @type JXG.GeometryElement3D 89 * @default null 90 * @private 91 * 92 */ 93 this.slide = slide; 94 95 /** 96 * Get x-coordinate of a 3D point. 97 * 98 * @name X 99 * @memberOf Point3D 100 * @function 101 * @returns {Number} 102 * 103 * @example 104 * p.X(); 105 */ 106 this.X = function () { 107 return this.coords[1]; 108 }; 109 110 /** 111 * Get y-coordinate of a 3D point. 112 * 113 * @name Y 114 * @memberOf Point3D 115 * @function 116 * @returns Number 117 * 118 * @example 119 * p.Y(); 120 */ 121 this.Y = function () { 122 return this.coords[2]; 123 }; 124 125 /** 126 * Get z-coordinate of a 3D point. 127 * 128 * @name Z 129 * @memberOf Point3D 130 * @function 131 * @returns Number 132 * 133 * @example 134 * p.Z(); 135 */ 136 this.Z = function () { 137 return this.coords[3]; 138 }; 139 140 /** 141 * Store the last position of the 2D point for the optimizer. 142 * 143 * @type Array 144 * @private 145 */ 146 this.position = []; 147 148 this._c2d = null; 149 150 this.methodMap = Type.deepCopy(this.methodMap, { 151 // TODO 152 }); 153 }; 154 JXG.Text3D.prototype = new JXG.GeometryElement(); 155 Type.copyPrototypeMethods(JXG.Text3D, JXG.GeometryElement3D, "constructor3D"); 156 157 JXG.extend( 158 JXG.Text3D.prototype, 159 /** @lends JXG.Text3D.prototype */ { 160 /** 161 * Update the homogeneous coords array. 162 * 163 * @name updateCoords 164 * @memberOf Text3D 165 * @function 166 * @returns {Object} Reference to the Text3D object 167 * @private 168 * @example 169 * p.updateCoords(); 170 */ 171 updateCoords: function () { 172 var i; 173 174 if (Type.isFunction(this.F)) { 175 // this.coords = [1].concat(Type.evaluate(this.F)); 176 this.coords = Type.evaluate(this.F); 177 this.coords.unshift(1); 178 } else { 179 this.coords[0] = 1; 180 for (i = 0; i < 3; i++) { 181 // Attention: if F is array of numbers, coords are not updated. 182 // Otherwise, dragging will not work anymore. 183 if (Type.isFunction(this.F[i])) { 184 this.coords[i + 1] = Type.evaluate(this.F[i]); 185 } 186 } 187 } 188 return this; 189 }, 190 191 /** 192 * Initialize the coords array. 193 * 194 * @private 195 * @returns {Object} Reference to the Text3D object 196 */ 197 initCoords: function () { 198 var i; 199 200 if (Type.isFunction(this.F)) { 201 // this.coords = [1].concat(Type.evaluate(this.F)); 202 this.coords = Type.evaluate(this.F); 203 this.coords.unshift(1); 204 } else { 205 this.coords[0] = 1; 206 for (i = 0; i < 3; i++) { 207 this.coords[i + 1] = Type.evaluate(this.F[i]); 208 } 209 } 210 return this; 211 }, 212 213 /** 214 * Normalize homogeneous coordinates such the the first coordinate (the w-coordinate is equal to 1 or 0)- 215 * 216 * @name normalizeCoords 217 * @memberOf Text3D 218 * @function 219 * @returns {Object} Reference to the Text3D object 220 * @private 221 * @example 222 * p.normalizeCoords(); 223 */ 224 normalizeCoords: function () { 225 if (Math.abs(this.coords[0]) > Mat.eps) { 226 this.coords[1] /= this.coords[0]; 227 this.coords[2] /= this.coords[0]; 228 this.coords[3] /= this.coords[0]; 229 this.coords[0] = 1.0; 230 } 231 return this; 232 }, 233 234 /** 235 * Set the position of a 3D point. 236 * 237 * @name setPosition 238 * @memberOf Text3D 239 * @function 240 * @param {Array} coords 3D coordinates. Either of the form [x,y,z] (Euclidean) or [w,x,y,z] (homogeneous). 241 * @param {Boolean} [noevent] If true, no events are triggered. 242 * @returns {Object} Reference to the Text3D object 243 * 244 * @example 245 * p.setPosition([1, 3, 4]); 246 */ 247 setPosition: function (coords, noevent) { 248 var c = this.coords; 249 // oc = this.coords.slice(); // Copy of original values 250 251 if (coords.length === 3) { 252 // Euclidean coordinates 253 c[0] = 1.0; 254 c[1] = coords[0]; 255 c[2] = coords[1]; 256 c[3] = coords[2]; 257 } else { 258 // Homogeneous coordinates (normalized) 259 c[0] = coords[0]; 260 c[1] = coords[1]; 261 c[2] = coords[2]; 262 c[3] = coords[2]; 263 this.normalizeCoords(); 264 } 265 266 // console.log(el.emitter, !noevent, oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3]); 267 // Not yet working TODO 268 // if (el.emitter && !noevent && 269 // (oc[0] !== c[0] || oc[1] !== c[1] || oc[2] !== c[2] || oc[3] !== c[3])) { 270 // this.triggerEventHandlers(['update3D'], [oc]); 271 // } 272 return this; 273 }, 274 275 update: function (drag) { 276 var c3d, foot, res; 277 278 // Update is called from board.updateElements. 279 // See Point3D.update() for the logic. 280 if ( 281 this.element2D.draggable() && 282 Geometry.distance(this._c2d, this.element2D.coords.usrCoords) !== 0 283 ) { 284 if (this.view.isVerticalDrag()) { 285 // Drag the text in its vertical to the xy plane 286 // If the text is outside of bbox3d, 287 // c3d is already corrected. 288 c3d = this.view.project2DTo3DVertical(this.element2D, this.coords); 289 } else { 290 // Drag the text in its xy plane 291 foot = [1, 0, 0, this.coords[3]]; 292 c3d = this.view.project2DTo3DPlane(this.element2D, [1, 0, 0, 1], foot); 293 } 294 295 if (c3d[0] !== 0) { 296 // Check if c3d is inside of view.bbox3d 297 // Otherwise, the coords are now corrected. 298 res = this.view.project3DToCube(c3d); 299 this.coords = res[0]; 300 301 if (res[1]) { 302 // The 3D coordinates have been corrected, now 303 // also correct the 2D element. 304 this.element2D.coords.setCoordinates( 305 Const.COORDS_BY_USER, 306 this.view.project3DTo2D(this.coords) 307 ); 308 } 309 310 if (this.slide) { 311 this.coords = this.slide.projectCoords([this.X(), this.Y(), this.Z()], this.position); 312 this.element2D.coords.setCoordinates( 313 Const.COORDS_BY_USER, 314 this.view.project3DTo2D(this.coords) 315 ); 316 } 317 } 318 } else { 319 this.updateCoords(); 320 if (this.slide) { 321 this.coords = this.slide.projectCoords([this.X(), this.Y(), this.Z()], this.position); 322 } 323 // Update 2D text from its 3D view 324 c3d = this.coords; 325 this.element2D.coords.setCoordinates( 326 Const.COORDS_BY_USER, 327 this.view.project3DTo2D(c3d) 328 ); 329 // this.zIndex = Mat.matVecMult(this.view.matrix3DRotShift, c3d)[3]; 330 this.zIndex = Mat.innerProduct(this.view.matrix3DRotShift[3], c3d); 331 this.element2D.prepareUpdate().update(); 332 } 333 this._c2d = this.element2D.coords.usrCoords.slice(); 334 335 return this; 336 }, 337 338 updateRenderer: function () { 339 this.needsUpdate = false; 340 return this; 341 }, 342 343 /** 344 * Check whether a text's position is finite, i.e. the first entry is not zero. 345 * @returns {Boolean} True if the first entry of the coordinate vector is not zero; false otherwise. 346 */ 347 testIfFinite: function () { 348 return Math.abs(this.coords[0]) > Mat.eps ? true : false; 349 // return Type.cmpArrays(this.coords, [0, 0, 0, 0]); 350 }, 351 352 // Not yet working 353 __evt__update3D: function (oc) {} 354 } 355 ); 356 357 /** 358 * @class Construct a text element in a 3D view. 359 * @pseudo 360 * @description A Text3D object is defined by 3 coordinates [x, y, z, text] or an array / function for the position of the text 361 * and a string or function defining the text. 362 * <p> 363 * That is, all numbers can also be provided as functions returning a number. 364 * <p> 365 * At the time being, text display is independent from the camera view. 366 * 367 * @name Text3D 368 * @augments JXG.Text3D 369 * @augments Text 370 * @constructor 371 * @throws {Exception} If the element cannot be constructed with the given parent 372 * objects an exception is thrown. 373 * @param {number,function_number,function_number,function_String,function_JXG.GeometryElement3D} x,y,z,txt,[slide=undefined] 374 * The coordinates are given as x, y, z consisting of numbers of functions and the text. 375 * If an optional 3D element "slide" is supplied, the point is a glider on that element. 376 * @param {array,function_string_JXG.GeometryElement3D}} F,txt,[slide=undefined] Alternatively, the coordinates can be supplied as array or function returning an array. 377 * If an optional 3D element "slide" is supplied, the point is a glider on that element. 378 * 379 * @example 380 * var bound = [-4, 6]; 381 * var view = board.create('view3d', 382 * [[-4, -3], [8, 8], 383 * [bound, bound, bound]], 384 * { 385 * projection: 'central' 386 * }); 387 * 388 * var txt1 = view.create('text3d', [[1, 2, 1], 'hello'], { 389 * fontSize: 20, 390 * }); 391 * 392 * </pre><div id="JXGb61d7c50-617a-4bed-9a45-13c949f90e94" class="jxgbox" style="width: 300px; height: 300px;"></div> 393 * <script type="text/javascript"> 394 * (function() { 395 * var board = JXG.JSXGraph.initBoard('JXGb61d7c50-617a-4bed-9a45-13c949f90e94', 396 * {boundingbox: [-8, 8, 8,-8], axis: false, pan: {enabled: false}, showcopyright: false, shownavigation: false}); 397 * var bound = [-4, 6]; 398 * var view = board.create('view3d', 399 * [[-4, -3], [8, 8], 400 * [bound, bound, bound]], 401 * { 402 * projection: 'central' 403 * }); 404 * 405 * var txt1 = view.create('text3d', [[1, 2, 1], 'hello'], { 406 * fontSize: 20, 407 * }); 408 * 409 * })(); 410 * 411 * </script><pre> 412 * 413 */ 414 JXG.createText3D = function (board, parents, attributes) { 415 var view = parents[0], 416 attr, F, slide, 417 text, 418 c2d, el; 419 420 // If the last element of parents is a 3D object, 421 // the point is a glider on that element. 422 if (parents.length > 2 && Type.exists(parents[parents.length - 1].is3D)) { 423 slide = parents.pop(); 424 } else { 425 slide = null; 426 } 427 428 if (parents.length === 3) { 429 // [view, array|fun, text] (Array [x, y, z] | function) returning [x, y, z] and string | function 430 F = parents[1]; 431 text = parents[2]; 432 } else if (parents.length === 5) { 433 // [view, x, y, z, text], (3 numbers | functions) sand string | function 434 F = parents.slice(1, 4); 435 text = parents[4]; 436 } else { 437 throw new Error( 438 "JSXGraph: Can't create text3d with parent types '" + 439 typeof parents[1] + 440 "' and '" + 441 typeof parents[2] + 442 "'." + 443 "\nPossible parent types: [[x,y,z], text], [x,y,z, text]" 444 ); 445 // "\nPossible parent types: [[x,y,z]], [x,y,z], [element,transformation]"); // TODO 446 } 447 448 attr = Type.copyAttributes(attributes, board.options, 'text3d'); 449 el = new JXG.Text3D(view, F, text, slide, attr); 450 el.initCoords(); 451 452 c2d = view.project3DTo2D(el.coords); 453 454 attr = el.setAttr2D(attr); 455 el.element2D = view.create('text', [c2d[1], c2d[2], text], attr); 456 457 el.element2D.view = view; 458 el.addChild(el.element2D); 459 el.inherits.push(el.element2D); 460 el.element2D.setParents(el); 461 462 // If this point is a glider, record that in the update tree 463 if (el.slide) { 464 el.slide.addChild(el); 465 el.setParents(el.slide); 466 } 467 468 el._c2d = el.element2D.coords.usrCoords.slice(); // Store a copy of the coordinates to detect dragging 469 470 return el; 471 }; 472 473 JXG.registerElement("text3d", JXG.createText3D); 474