jsxgraph 1.9.1 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/distrib/docs.zip +0 -0
  3. package/distrib/geogebra.min.js +2 -2
  4. package/distrib/geonext.min.js +2 -2
  5. package/distrib/intergeo.min.js +2 -2
  6. package/distrib/jsxgraph.css +1 -1
  7. package/distrib/jsxgraph.zip +0 -0
  8. package/distrib/jsxgraphcore.js +3 -3
  9. package/distrib/jsxgraphcore.mjs +3 -3
  10. package/distrib/jsxgraphcore.mjs.map +1 -1
  11. package/distrib/jsxgraphsrc.js +2328 -1508
  12. package/distrib/jsxgraphsrc.mjs +2328 -1508
  13. package/distrib/jsxgraphsrc.mjs.map +1 -1
  14. package/distrib/sketch.min.js +3 -3
  15. package/package.json +4 -4
  16. package/src/3d/box3d.js +4 -1
  17. package/src/3d/circle3d.js +10 -9
  18. package/src/3d/curve3d.js +2 -0
  19. package/src/3d/linspace3d.js +18 -55
  20. package/src/3d/point3d.js +6 -0
  21. package/src/3d/polygon3d.js +1 -0
  22. package/src/3d/sphere3d.js +47 -43
  23. package/src/3d/surface3d.js +1 -0
  24. package/src/3d/view3d.js +1504 -1067
  25. package/src/base/board.js +176 -55
  26. package/src/base/circle.js +3 -1
  27. package/src/base/constants.js +1 -1
  28. package/src/base/curve.js +1 -0
  29. package/src/base/element.js +38 -30
  30. package/src/base/line.js +93 -105
  31. package/src/base/polygon.js +1 -1
  32. package/src/base/text.js +5 -1
  33. package/src/element/button.js +62 -12
  34. package/src/element/checkbox.js +6 -2
  35. package/src/element/grid.js +6 -6
  36. package/src/element/input.js +9 -6
  37. package/src/element/slider.js +42 -39
  38. package/src/jsxgraph.js +5 -3
  39. package/src/math/geometry.js +86 -64
  40. package/src/math/math.js +47 -5
  41. package/src/options.js +45 -0
  42. package/src/options3d.js +82 -5
  43. package/src/parser/jessiecode.js +2 -1
  44. package/src/parser/prefix.js +8 -1
  45. package/src/reader/sketch.js +106 -0
  46. package/src/renderer/svg.js +16 -6
  47. package/src/utils/env.js +2 -2
  48. package/src/utils/type.js +27 -15
package/src/3d/view3d.js CHANGED
@@ -71,6 +71,13 @@ JXG.View3D = function (board, parents, attributes) {
71
71
  */
72
72
  this.objects = {};
73
73
 
74
+ /**
75
+ * An array containing all the points in the view.
76
+ * @Type Array
77
+ * @private
78
+ */
79
+ this.points = this.visProp.depthorderpoints ? [] : null;
80
+
74
81
  /**
75
82
  * An array containing all geometric objects in this view in the order of construction.
76
83
  * @type Array
@@ -93,6 +100,26 @@ JXG.View3D = function (board, parents, attributes) {
93
100
  */
94
101
  this.defaultAxes = null;
95
102
 
103
+ /**
104
+ * The Tait-Bryan angles specifying the view box orientation
105
+ */
106
+ this.angles = {
107
+ az: null,
108
+ el: null,
109
+ bank: null
110
+ };
111
+
112
+ /**
113
+ * @type {Array}
114
+ * The view box orientation matrix
115
+ */
116
+ this.matrix3DRot = [
117
+ [1, 0, 0, 0],
118
+ [0, 1, 0, 0],
119
+ [0, 0, 1, 0],
120
+ [0, 0, 0, 1]
121
+ ];
122
+
96
123
  /**
97
124
  * @type {Array}
98
125
  * @private
@@ -105,17 +132,28 @@ JXG.View3D = function (board, parents, attributes) {
105
132
  ];
106
133
 
107
134
  /**
108
- * The 4x4 matrix that maps world-space vectors to zero-focus view-space
109
- * vectors---that is, view space vectors for a camera sitting on the screen.
110
- * The camera is actually set back from the screen by the focal distance. To
111
- * get actual view space vectors, with the focal distance accounted for, use
112
- * the `worldToView` method.
113
- *
114
- * This transformation is exposed to help 3D elements manage their 2D
115
- * representations in central projection mode.
135
+ * The 4×4 matrix that maps box coordinates to camera coordinates. These
136
+ * coordinate systems fit into the View3D coordinate atlas as follows.
137
+ * <ul>
138
+ * <li><b>World coordinates.</b> The coordinates used to specify object
139
+ * positions in a JSXGraph scene.</li>
140
+ * <li><b>Box coordinates.</b> The world coordinates translated to put the
141
+ * center of the view box at the origin.
142
+ * <li><b>Camera coordinates.</b> The coordinate system where the
143
+ * <code>x</code>, <code>y</code> plane is the screen, the origin is the
144
+ * center of the screen, and the <code>z</code> axis points out of the
145
+ * screen, toward the viewer.
146
+ * <li><b>Focal coordinates.</b> The camera coordinates translated to put
147
+ * the origin at the focal point, which is set back from the screen by the
148
+ * focal distance.</li>
149
+ * </ul>
150
+ * The <code>boxToCam</code> transformation is exposed to help 3D elements
151
+ * manage their 2D representations in central projection mode. To map world
152
+ * coordinates to focal coordinates, use the
153
+ * {@link JXG.View3D#worldToFocal} method.
116
154
  * @type {Array}
117
155
  */
118
- this.cameraTransform = [];
156
+ this.boxToCam = [];
119
157
 
120
158
  /**
121
159
  * @type array
@@ -158,6 +196,12 @@ JXG.View3D = function (board, parents, attributes) {
158
196
  // Will be set in update().
159
197
  this.projectionType = 'parallel';
160
198
 
199
+ /**
200
+ * Whether trackball navigation is currently enabled.
201
+ * @type String
202
+ */
203
+ this.trackballEnabled = false;
204
+
161
205
  this.timeoutAzimuth = null;
162
206
 
163
207
  this.id = this.board.setId(this, 'V');
@@ -173,1188 +217,1539 @@ JXG.View3D.prototype = new GeometryElement();
173
217
  JXG.extend(
174
218
  JXG.View3D.prototype, /** @lends JXG.View3D.prototype */ {
175
219
 
176
- /**
177
- * Creates a new 3D element of type elementType.
178
- * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
179
- * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
180
- * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
181
- * methods for a list of possible parameters.
182
- * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
183
- * Common attributes are name, visible, strokeColor.
184
- * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
185
- * two or more elements.
186
- */
187
- create: function (elementType, parents, attributes) {
188
- var prefix = [],
189
- el;
220
+ /**
221
+ * Creates a new 3D element of type elementType.
222
+ * @param {String} elementType Type of the element to be constructed given as a string e.g. 'point3d' or 'surface3d'.
223
+ * @param {Array} parents Array of parent elements needed to construct the element e.g. coordinates for a 3D point or two
224
+ * 3D points to construct a line. This highly depends on the elementType that is constructed. See the corresponding JXG.create*
225
+ * methods for a list of possible parameters.
226
+ * @param {Object} [attributes] An object containing the attributes to be set. This also depends on the elementType.
227
+ * Common attributes are name, visible, strokeColor.
228
+ * @returns {Object} Reference to the created element. This is usually a GeometryElement3D, but can be an array containing
229
+ * two or more elements.
230
+ */
231
+ create: function (elementType, parents, attributes) {
232
+ var prefix = [],
233
+ el;
234
+
235
+ if (elementType.indexOf('3d') > 0) {
236
+ // is3D = true;
237
+ prefix.push(this);
238
+ }
239
+ el = this.board.create(elementType, prefix.concat(parents), attributes);
190
240
 
191
- if (elementType.indexOf('3d') > 0) {
192
- // is3D = true;
193
- prefix.push(this);
241
+ return el;
242
+ },
243
+
244
+ /**
245
+ * Select a single or multiple elements at once.
246
+ * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
247
+ * be used as a filter to return multiple elements at once filtered by the properties of the object.
248
+ * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
249
+ * The advanced filters consisting of objects or functions are ignored.
250
+ * @returns {JXG.GeometryElement3D|JXG.Composition}
251
+ * @example
252
+ * // select the element with name A
253
+ * view.select('A');
254
+ *
255
+ * // select all elements with strokecolor set to 'red' (but not '#ff0000')
256
+ * view.select({
257
+ * strokeColor: 'red'
258
+ * });
259
+ *
260
+ * // select all points on or below the x/y plane and make them black.
261
+ * view.select({
262
+ * elType: 'point3d',
263
+ * Z: function (v) {
264
+ * return v <= 0;
265
+ * }
266
+ * }).setAttribute({color: 'black'});
267
+ *
268
+ * // select all elements
269
+ * view.select(function (el) {
270
+ * return true;
271
+ * });
272
+ */
273
+ select: function (str, onlyByIdOrName) {
274
+ var flist,
275
+ olist,
276
+ i,
277
+ l,
278
+ s = str;
279
+
280
+ if (s === null) {
281
+ return s;
282
+ }
283
+
284
+ // It's a string, most likely an id or a name.
285
+ if (Type.isString(s) && s !== '') {
286
+ // Search by ID
287
+ if (Type.exists(this.objects[s])) {
288
+ s = this.objects[s];
289
+ // Search by name
290
+ } else if (Type.exists(this.elementsByName[s])) {
291
+ s = this.elementsByName[s];
292
+ // // Search by group ID
293
+ // } else if (Type.exists(this.groups[s])) {
294
+ // s = this.groups[s];
194
295
  }
195
- el = this.board.create(elementType, prefix.concat(parents), attributes);
196
296
 
197
- return el;
198
- },
297
+ // It's a function or an object, but not an element
298
+ } else if (
299
+ !onlyByIdOrName &&
300
+ (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
301
+ ) {
302
+ flist = Type.filterElements(this.objectsList, s);
199
303
 
200
- /**
201
- * Select a single or multiple elements at once.
202
- * @param {String|Object|function} str The name, id or a reference to a JSXGraph 3D element in the 3D view. An object will
203
- * be used as a filter to return multiple elements at once filtered by the properties of the object.
204
- * @param {Boolean} onlyByIdOrName If true (default:false) elements are only filtered by their id, name or groupId.
205
- * The advanced filters consisting of objects or functions are ignored.
206
- * @returns {JXG.GeometryElement3D|JXG.Composition}
207
- * @example
208
- * // select the element with name A
209
- * view.select('A');
210
- *
211
- * // select all elements with strokecolor set to 'red' (but not '#ff0000')
212
- * view.select({
213
- * strokeColor: 'red'
214
- * });
215
- *
216
- * // select all points on or below the x/y plane and make them black.
217
- * view.select({
218
- * elType: 'point3d',
219
- * Z: function (v) {
220
- * return v <= 0;
221
- * }
222
- * }).setAttribute({color: 'black'});
223
- *
224
- * // select all elements
225
- * view.select(function (el) {
226
- * return true;
227
- * });
228
- */
229
- select: function (str, onlyByIdOrName) {
230
- var flist,
231
- olist,
232
- i,
233
- l,
234
- s = str;
235
-
236
- if (s === null) {
237
- return s;
304
+ olist = {};
305
+ l = flist.length;
306
+ for (i = 0; i < l; i++) {
307
+ olist[flist[i].id] = flist[i];
238
308
  }
309
+ s = new Composition(olist);
239
310
 
240
- // It's a string, most likely an id or a name.
241
- if (Type.isString(s) && s !== '') {
242
- // Search by ID
243
- if (Type.exists(this.objects[s])) {
244
- s = this.objects[s];
245
- // Search by name
246
- } else if (Type.exists(this.elementsByName[s])) {
247
- s = this.elementsByName[s];
248
- // // Search by group ID
249
- // } else if (Type.exists(this.groups[s])) {
250
- // s = this.groups[s];
251
- }
311
+ // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
312
+ } else if (
313
+ Type.isObject(s) &&
314
+ Type.exists(s.id) &&
315
+ !Type.exists(this.objects[s.id])
316
+ ) {
317
+ s = null;
318
+ }
252
319
 
253
- // It's a function or an object, but not an element
254
- } else if (
255
- !onlyByIdOrName &&
256
- (Type.isFunction(s) || (Type.isObject(s) && !Type.isFunction(s.setAttribute)))
257
- ) {
258
- flist = Type.filterElements(this.objectsList, s);
320
+ return s;
321
+ },
322
+
323
+ // set the Tait-Bryan angles to specify the current view rotation matrix
324
+ setAnglesFromRotation: function () {
325
+ var rem = this.matrix3DRot, // rotation remaining after angle extraction
326
+ rBank, cosBank, sinBank,
327
+ cosEl, sinEl,
328
+ cosAz, sinAz;
329
+
330
+ // extract bank by rotating the view box z axis onto the camera yz plane
331
+ rBank = Math.sqrt(rem[1][3] * rem[1][3] + rem[2][3] * rem[2][3]);
332
+ if (rBank > Mat.eps) {
333
+ cosBank = rem[2][3] / rBank;
334
+ sinBank = rem[1][3] / rBank;
335
+ } else {
336
+ // if the z axis is pointed almost exactly at the screen, we
337
+ // keep the current bank value
338
+ cosBank = Math.cos(this.angles.bank);
339
+ sinBank = Math.sin(this.angles.bank);
340
+ }
341
+ rem = Mat.matMatMult([
342
+ [1, 0, 0, 0],
343
+ [0, cosBank, -sinBank, 0],
344
+ [0, sinBank, cosBank, 0],
345
+ [0, 0, 0, 1]
346
+ ], rem);
347
+ this.angles.bank = Math.atan2(sinBank, cosBank);
348
+
349
+ // extract elevation by rotating the view box z axis onto the camera
350
+ // y axis
351
+ cosEl = rem[2][3];
352
+ sinEl = rem[3][3];
353
+ rem = Mat.matMatMult([
354
+ [1, 0, 0, 0],
355
+ [0, 1, 0, 0],
356
+ [0, 0, cosEl, sinEl],
357
+ [0, 0, -sinEl, cosEl]
358
+ ], rem);
359
+ this.angles.el = Math.atan2(sinEl, cosEl);
360
+
361
+ // extract azimuth
362
+ cosAz = -rem[1][1];
363
+ sinAz = rem[3][1];
364
+ this.angles.az = Math.atan2(sinAz, cosAz);
365
+ if (this.angles.az < 0) this.angles.az += 2 * Math.PI;
366
+
367
+ this.setSlidersFromAngles();
368
+ },
369
+
370
+ anglesHaveMoved: function () {
371
+ return (
372
+ this._hasMoveAz || this._hasMoveEl ||
373
+ Math.abs(this.angles.az - this.az_slide.Value()) > Mat.eps ||
374
+ Math.abs(this.angles.el - this.el_slide.Value()) > Mat.eps ||
375
+ Math.abs(this.angles.bank - this.bank_slide.Value()) > Mat.eps
376
+ );
377
+ },
378
+
379
+ getAnglesFromSliders: function () {
380
+ this.angles.az = this.az_slide.Value();
381
+ this.angles.el = this.el_slide.Value();
382
+ this.angles.bank = this.bank_slide.Value();
383
+ },
384
+
385
+ setSlidersFromAngles: function () {
386
+ this.az_slide.setValue(this.angles.az);
387
+ this.el_slide.setValue(this.angles.el);
388
+ this.bank_slide.setValue(this.angles.bank);
389
+ },
390
+
391
+ // return the rotation matrix specified by the current Tait-Bryan angles
392
+ getRotationFromAngles: function () {
393
+ var a, e, b, f,
394
+ cosBank, sinBank,
395
+ mat = [
396
+ [1, 0, 0, 0],
397
+ [0, 1, 0, 0],
398
+ [0, 0, 1, 0],
399
+ [0, 0, 0, 1]
400
+ ];
259
401
 
260
- olist = {};
261
- l = flist.length;
262
- for (i = 0; i < l; i++) {
263
- olist[flist[i].id] = flist[i];
264
- }
265
- s = new Composition(olist);
402
+ // mat projects homogeneous 3D coords in View3D
403
+ // to homogeneous 2D coordinates in the board
404
+ a = this.angles.az;
405
+ e = this.angles.el;
406
+ b = this.angles.bank;
407
+ f = -Math.sin(e);
408
+
409
+ mat[1][1] = -Math.cos(a);
410
+ mat[1][2] = Math.sin(a);
411
+ mat[1][3] = 0;
412
+
413
+ mat[2][1] = f * Math.sin(a);
414
+ mat[2][2] = f * Math.cos(a);
415
+ mat[2][3] = Math.cos(e);
416
+
417
+ mat[3][1] = Math.cos(e) * Math.sin(a);
418
+ mat[3][2] = Math.cos(e) * Math.cos(a);
419
+ mat[3][3] = Math.sin(e);
420
+
421
+ cosBank = Math.cos(b);
422
+ sinBank = Math.sin(b);
423
+ mat = Mat.matMatMult([
424
+ [1, 0, 0, 0],
425
+ [0, cosBank, sinBank, 0],
426
+ [0, -sinBank, cosBank, 0],
427
+ [0, 0, 0, 1]
428
+ ], mat);
429
+
430
+ return mat;
431
+
432
+ /* this code, originally from `_updateCentralProjection`, is an
433
+ * alternate implementation of the azimuth-elevation matrix
434
+ * computation above. using this implementation instead of the
435
+ * current one might lead to simpler code in a future refactoring
436
+ var a, e, up,
437
+ ax, ay, az, v, nrm,
438
+ eye, d,
439
+ func_sphere;
440
+
441
+ // finds the point on the unit sphere with the given azimuth and
442
+ // elevation, and returns its affine coordinates
443
+ func_sphere = function (az, el) {
444
+ return [
445
+ Math.cos(az) * Math.cos(el),
446
+ -Math.sin(az) * Math.cos(el),
447
+ Math.sin(el)
448
+ ];
449
+ };
266
450
 
267
- // It's an element which has been deleted (and still hangs around, e.g. in an attractor list
268
- } else if (
269
- Type.isObject(s) &&
270
- Type.exists(s.id) &&
271
- !Type.exists(this.objects[s.id])
272
- ) {
273
- s = null;
274
- }
451
+ a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere
452
+ e = this.el_slide.Value();
275
453
 
276
- return s;
277
- },
278
-
279
- updateParallelProjection: function () {
280
- var r, a, e, f,
281
- mat = [
282
- [1, 0, 0, 0],
283
- [0, 1, 0, 0],
284
- [0, 0, 1, 0],
285
- [0, 0, 0, 1]
286
- ];
287
-
288
- // mat projects homogeneous 3D coords in View3D
289
- // to homogeneous 2D coordinates in the board
290
- e = this.el_slide.Value();
291
- r = this.r;
292
- a = this.az_slide.Value();
293
- f = r * Math.sin(e);
294
-
295
- mat[1][1] = r * Math.cos(a);
296
- mat[1][2] = -r * Math.sin(a);
297
- mat[1][3] = 0;
298
-
299
- mat[2][1] = f * Math.sin(a);
300
- mat[2][2] = f * Math.cos(a);
301
- mat[2][3] = Math.cos(e);
302
-
303
- mat[3][1] = r * Math.cos(e) * Math.sin(a);
304
- mat[3][2] = r * Math.cos(e) * Math.cos(a);
305
- mat[3][3] = -Math.sin(e);
306
-
307
- return mat;
308
- },
454
+ // create an up vector and an eye vector which are 90 degrees out of phase
455
+ up = func_sphere(a, e + Math.PI / 2);
456
+ eye = func_sphere(a, e);
457
+ d = [eye[0], eye[1], eye[2]];
309
458
 
310
- /**
311
- * Project 2D point (x,y) to the virtual trackpad sphere,
312
- * see Bell's virtual trackpad, and return z-component of the
313
- * number.
314
- *
315
- * @param {Number} r
316
- * @param {Number} x
317
- * @param {Number} y
318
- * @returns Number
319
- * @private
320
- */
321
- _projectToSphere: function(r, x, y) {
322
- var d = Mat.hypot(x, y),
323
- t, z;
324
-
325
- if (d < r * 0.7071067811865475) { // Inside sphere
326
- z = Math.sqrt(r * r - d * d);
327
- } else { // On hyperbola
328
- t = r / 1.414213562373095;
329
- z = t * t / d;
330
- }
331
- return z;
332
- },
459
+ nrm = Mat.norm(d, 3);
460
+ az = [d[0] / nrm, d[1] / nrm, d[2] / nrm];
333
461
 
334
- /**
335
- * Determine 4x4 rotation matrix with Bell's virtual trackball.
336
- *
337
- * @returns {Array} 4x4 rotation matrix
338
- * @private
462
+ nrm = Mat.norm(up, 3);
463
+ v = [up[0] / nrm, up[1] / nrm, up[2] / nrm];
464
+
465
+ ax = Mat.crossProduct(v, az);
466
+ ay = Mat.crossProduct(az, ax);
467
+
468
+ this.matrix3DRot[1] = [0, ax[0], ax[1], ax[2]];
469
+ this.matrix3DRot[2] = [0, ay[0], ay[1], ay[2]];
470
+ this.matrix3DRot[3] = [0, az[0], az[1], az[2]];
339
471
  */
340
- updateProjectionTrackball: function (Pref) {
341
- var R = 100,
342
- dx, dy, dr2,
343
- p1, p2, x, y, theta, t, d,
344
- c, s, n,
345
- mat = [
346
- [1, 0, 0, 0],
347
- [0, 1, 0, 0],
348
- [0, 0, 1, 0],
349
- [0, 0, 0, 1]
350
- ];
351
-
352
- if (!Type.exists(this._trackball)) {
353
- return this.matrix3DRot;
472
+ },
473
+
474
+ /**
475
+ * Project 2D point (x,y) to the virtual trackpad sphere,
476
+ * see Bell's virtual trackpad, and return z-component of the
477
+ * number.
478
+ *
479
+ * @param {Number} r
480
+ * @param {Number} x
481
+ * @param {Number} y
482
+ * @returns Number
483
+ * @private
484
+ */
485
+ _projectToSphere: function (r, x, y) {
486
+ var d = Mat.hypot(x, y),
487
+ t, z;
488
+
489
+ if (d < r * 0.7071067811865475) { // Inside sphere
490
+ z = Math.sqrt(r * r - d * d);
491
+ } else { // On hyperbola
492
+ t = r / 1.414213562373095;
493
+ z = t * t / d;
494
+ }
495
+ return z;
496
+ },
497
+
498
+ /**
499
+ * Determine 4x4 rotation matrix with Bell's virtual trackball.
500
+ *
501
+ * @returns {Array} 4x4 rotation matrix
502
+ * @private
503
+ */
504
+ updateProjectionTrackball: function (Pref) {
505
+ var R = 100,
506
+ dx, dy, dr2,
507
+ p1, p2, x, y, theta, t, d,
508
+ c, s, n,
509
+ mat = [
510
+ [1, 0, 0, 0],
511
+ [0, 1, 0, 0],
512
+ [0, 0, 1, 0],
513
+ [0, 0, 0, 1]
514
+ ];
515
+
516
+ if (!Type.exists(this._trackball)) {
517
+ return this.matrix3DRot;
518
+ }
519
+
520
+ dx = this._trackball.dx;
521
+ dy = this._trackball.dy;
522
+ dr2 = dx * dx + dy * dy;
523
+ if (dr2 > Mat.eps) {
524
+ // // Method by Hanson, "The rolling ball", Graphics Gems III, p.51
525
+ // // Rotation axis:
526
+ // // n = (-dy/dr, dx/dr, 0)
527
+ // // Rotation angle around n:
528
+ // // theta = atan(dr / R) approx dr / R
529
+ // dr = Math.sqrt(dr2);
530
+ // c = R / Math.hypot(R, dr); // cos(theta)
531
+ // t = 1 - c; // 1 - cos(theta)
532
+ // s = dr / Math.hypot(R, dr); // sin(theta)
533
+ // n = [-dy / dr, dx / dr, 0];
534
+
535
+ // Bell virtual trackpad, see
536
+ // https://opensource.apple.com/source/X11libs/X11libs-60/mesa/Mesa-7.8.2/progs/util/trackball.c.auto.html
537
+ // http://scv.bu.edu/documentation/presentations/visualizationworkshop08/materials/opengl/trackball.c.
538
+ // See also Henriksen, Sporring, Hornaek, "Virtual Trackballs revisited".
539
+ //
540
+ R = (this.size[0] * this.board.unitX + this.size[1] * this.board.unitY) * 0.25;
541
+ x = this._trackball.x;
542
+ y = this._trackball.y;
543
+
544
+ p2 = [x, y, this._projectToSphere(R, x, y)];
545
+ x -= dx;
546
+ y -= dy;
547
+ p1 = [x, y, this._projectToSphere(R, x, y)];
548
+
549
+ n = Mat.crossProduct(p1, p2);
550
+ d = Mat.hypot(n[0], n[1], n[2]);
551
+ n[0] /= d;
552
+ n[1] /= d;
553
+ n[2] /= d;
554
+
555
+ t = Geometry.distance(p2, p1, 3) / (2 * R);
556
+ t = (t > 1.0) ? 1.0 : t;
557
+ t = (t < -1.0) ? -1.0 : t;
558
+ theta = 2.0 * Math.asin(t);
559
+ c = Math.cos(theta);
560
+ t = 1 - c;
561
+ s = Math.sin(theta);
562
+
563
+ // Rotation by theta about the axis n. See equation 9.63 of
564
+ //
565
+ // Ian Richard Cole. "Modeling CPV" (thesis). Loughborough
566
+ // University. https://hdl.handle.net/2134/18050
567
+ //
568
+ mat[1][1] = c + n[0] * n[0] * t;
569
+ mat[2][1] = n[1] * n[0] * t + n[2] * s;
570
+ mat[3][1] = n[2] * n[0] * t - n[1] * s;
571
+
572
+ mat[1][2] = n[0] * n[1] * t - n[2] * s;
573
+ mat[2][2] = c + n[1] * n[1] * t;
574
+ mat[3][2] = n[2] * n[1] * t + n[0] * s;
575
+
576
+ mat[1][3] = n[0] * n[2] * t + n[1] * s;
577
+ mat[2][3] = n[1] * n[2] * t - n[0] * s;
578
+ mat[3][3] = c + n[2] * n[2] * t;
579
+ }
580
+
581
+ mat = Mat.matMatMult(mat, this.matrix3DRot);
582
+ return mat;
583
+ },
584
+
585
+ updateAngleSliderBounds: function () {
586
+ var az_smax, az_smin,
587
+ el_smax, el_smin, el_cover,
588
+ el_smid, el_equiv, el_flip_equiv,
589
+ el_equiv_loss, el_flip_equiv_loss, el_interval_loss,
590
+ bank_smax, bank_smin;
591
+
592
+ // update stored trackball toggle
593
+ this.trackballEnabled = Type.evaluate(this.visProp.trackball.enabled);
594
+
595
+ // set slider bounds
596
+ if (this.trackballEnabled) {
597
+ this.az_slide.setMin(0);
598
+ this.az_slide.setMax(2 * Math.PI);
599
+ this.el_slide.setMin(-0.5 * Math.PI);
600
+ this.el_slide.setMax(0.5 * Math.PI);
601
+ this.bank_slide.setMin(-Math.PI);
602
+ this.bank_slide.setMax(Math.PI);
603
+ } else {
604
+ this.az_slide.setMin(this.visProp.az.slider.min);
605
+ this.az_slide.setMax(this.visProp.az.slider.max);
606
+ this.el_slide.setMin(this.visProp.el.slider.min);
607
+ this.el_slide.setMax(this.visProp.el.slider.max);
608
+ this.bank_slide.setMin(this.visProp.bank.slider.min);
609
+ this.bank_slide.setMax(this.visProp.bank.slider.max);
610
+ }
611
+
612
+ // get new slider bounds
613
+ az_smax = this.az_slide._smax;
614
+ az_smin = this.az_slide._smin;
615
+ el_smax = this.el_slide._smax;
616
+ el_smin = this.el_slide._smin;
617
+ bank_smax = this.bank_slide._smax;
618
+ bank_smin = this.bank_slide._smin;
619
+
620
+ // wrap and restore angle values
621
+ if (this.trackballEnabled) {
622
+ // if we're upside-down, flip the bank angle to reach the same
623
+ // orientation with an elevation between -pi/2 and pi/2
624
+ el_cover = Mat.mod(this.angles.el, 2 * Math.PI);
625
+ if (0.5 * Math.PI < el_cover && el_cover < 1.5 * Math.PI) {
626
+ this.angles.el = Math.PI - el_cover;
627
+ this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
628
+ this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
354
629
  }
355
630
 
356
- dx = this._trackball.dx;
357
- dy = this._trackball.dy;
358
- dr2 = dx * dx + dy * dy;
359
- if (dr2 > Mat.eps) {
360
- // // Method by Hanson, "The rolling ball", Graphics Gems III, p.51
361
- // // Rotation axis:
362
- // // n = (-dy/dr, dx/dr, 0)
363
- // // Rotation angle around n:
364
- // // theta = atan(dr / R) approx dr / R
365
- // dr = Math.sqrt(dr2);
366
- // c = R / Math.hypot(R, dr); // cos(theta)
367
- // t = 1 - c; // 1 - cos(theta)
368
- // s = dr / Math.hypot(R, dr); // sin(theta)
369
- // n = [-dy / dr, dx / dr, 0];
370
-
371
- // Bell virtual trackpad, see
372
- // https://opensource.apple.com/source/X11libs/X11libs-60/mesa/Mesa-7.8.2/progs/util/trackball.c.auto.html
373
- // http://scv.bu.edu/documentation/presentations/visualizationworkshop08/materials/opengl/trackball.c.
374
- // See also Henriksen, Sporring, Hornaek, "Virtual Trackballs revisited".
375
- //
376
- R = (this.size[0] * this.board.unitX + this.size[1] * this.board.unitY) * 0.25;
377
- x = this._trackball.x;
378
- y = this._trackball.y;
379
-
380
- // p2 = [x, y, this._projectToSphere(R, x, y)];
381
- p2 = [this._projectToSphere(R, x, y), x, y];
382
- x -= dx;
383
- y -= dy;
384
- // p1 = [x, y, this._projectToSphere(R, x, y)];
385
- p1 = [this._projectToSphere(R, x, y), x, y];
386
-
387
- n = Mat.crossProduct(p2, p1);
388
- d = Mat.hypot(n[0], n[1], n[2]);
389
- n[0] /= -d;
390
- n[1] /= -d;
391
- n[2] /= d;
392
-
393
- t = Geometry.distance(p2, p1, 3) / (2 * R);
394
- t = (t > 1.0) ? 1.0 : t;
395
- t = (t < -1.0) ? -1.0 : t;
396
- theta = 2.0 * Math.asin(t);
397
- c = Math.cos(theta);
398
- t = 1 - c;
399
- s = Math.sin(theta);
400
-
401
- // General rotation around axis n with angle theta
402
- // See Pique, Graphics Gems I, pp. 466
403
- // Attention: he multiplies (row vector x matrix),
404
- // we multiply (matrix x column vector)
405
- mat[1][1] = c + n[0] * n[0] * t;
406
- mat[2][1] = n[0] * n[1] * t - n[2] * s;
407
- mat[3][1] = n[0] * n[2] * t + n[1] * s;
408
-
409
- mat[1][2] = n[1] * n[0] * t + n[2] * s;
410
- mat[2][2] = c + n[1] * n[1] * t;
411
- mat[3][2] = n[1] * n[2] * t - n[0] * s;
412
-
413
- mat[1][3] = n[2] * n[0] * t - n[1] * s;
414
- mat[2][3] = n[2] * n[1] * t + n[0] * s;
415
- mat[3][3] = c + n[2] * n[2] * t;
416
-
417
- if (Pref !== null) {
418
- // For central projection we have to rotate around Pref.
419
- // Parallel projection: Pref === null
420
- mat[1][0] = Pref[0] - mat[1][1] * Pref[0] - mat[1][2] * Pref[0] - mat[1][3] * Pref[0];
421
- mat[2][0] = Pref[1] - mat[2][1] * Pref[1] - mat[2][2] * Pref[1] - mat[2][3] * Pref[1];
422
- mat[3][0] = Pref[2] - mat[3][1] * Pref[2] - mat[3][2] * Pref[2] - mat[3][3] * Pref[2];
631
+ // wrap the azimuth and bank angle
632
+ this.angles.az = Mat.wrap(this.angles.az, az_smin, az_smax);
633
+ this.angles.el = Mat.wrap(this.angles.el, el_smin, el_smax);
634
+ this.angles.bank = Mat.wrap(this.angles.bank, bank_smin, bank_smax);
635
+ } else {
636
+ // wrap and clamp the elevation into the slider range. if
637
+ // flipping the elevation gets us closer to the slider interval,
638
+ // do that, inverting the azimuth and bank angle to compensate
639
+ el_interval_loss = function (t) {
640
+ if (t < el_smin) {
641
+ return el_smin - t;
642
+ } else if (el_smax < t) {
643
+ return t - el_smax;
644
+ } else {
645
+ return 0;
423
646
  }
647
+ };
648
+ el_smid = 0.5 * (el_smin + el_smax);
649
+ el_equiv = Mat.wrap(
650
+ this.angles.el,
651
+ el_smid - Math.PI,
652
+ el_smid + Math.PI
653
+ );
654
+ el_flip_equiv = Mat.wrap(
655
+ Math.PI - this.angles.el,
656
+ el_smid - Math.PI,
657
+ el_smid + Math.PI
658
+ );
659
+ el_equiv_loss = el_interval_loss(el_equiv);
660
+ el_flip_equiv_loss = el_interval_loss(el_flip_equiv);
661
+ if (el_equiv_loss <= el_flip_equiv_loss) {
662
+ this.angles.el = Mat.clamp(el_equiv, el_smin, el_smax);
663
+ } else {
664
+ this.angles.el = Mat.clamp(el_flip_equiv, el_smin, el_smax);
665
+ this.angles.az = Mat.wrap(this.angles.az + Math.PI, az_smin, az_smax);
666
+ this.angles.bank = Mat.wrap(this.angles.bank + Math.PI, bank_smin, bank_smax);
424
667
  }
425
668
 
426
- mat = Mat.matMatMult(this.matrix3DRot, mat);
427
- return mat;
428
- },
429
-
430
- /**
431
- * @private
432
- * @returns {Array}
433
- */
434
- _updateCentralProjection: function () {
435
- var r, e, a, up,
436
- az, ax, ay, v, nrm,
437
- // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html
438
- // bbox3D is always at the world origin, i.e. T_obj is the unit matrix.
439
- // All vectors contain affine coordinates and have length 3
440
- // The matrices are of size 4x4.
441
- eye, d,
442
- zf = 20, // near clip plane
443
- zn = 8, // far clip plane
444
- Pref = [
445
- 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]),
446
- 0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]),
447
- 0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1])
448
- ],
449
- A,
450
- func_sphere;
451
-
452
- /**
453
- * Calculates a spherical parametric surface, which depends on az, el and r.
454
- * @param {Number} a
455
- * @param {Number} e
456
- * @param {Number} r
457
- * @returns {Array} 3-dimensional vector in cartesian coordinates
458
- */
459
- func_sphere = function (az, el, r) {
460
- return [
461
- r * Math.cos(az) * Math.cos(el),
462
- -r * Math.sin(az) * Math.cos(el),
463
- r * Math.sin(el)
464
- ];
465
- };
669
+ // wrap and clamp the azimuth and bank angle into the slider range
670
+ this.angles.az = Mat.wrapAndClamp(this.angles.az, az_smin, az_smax, 2 * Math.PI);
671
+ this.angles.bank = Mat.wrapAndClamp(this.angles.bank, bank_smin, bank_smax, 2 * Math.PI);
466
672
 
467
- a = this.az_slide.Value() + (3 * Math.PI * 0.5); // Sphere
468
- e = this.el_slide.Value();
469
-
470
- r = Type.evaluate(this.visProp.r);
471
- if (r === 'auto') {
472
- r = Mat.hypot(
473
- this.bbox3D[0][0] - this.bbox3D[0][1],
474
- this.bbox3D[1][0] - this.bbox3D[1][1],
475
- this.bbox3D[2][0] - this.bbox3D[2][1]
476
- ) * 1.01;
477
- // console.log(r);
478
- }
673
+ // since we're using `clamp`, angles may have changed
674
+ this.matrix3DRot = this.getRotationFromAngles();
675
+ }
479
676
 
480
- // create an up vector and an eye vector which are 90 degrees out of phase
481
- up = func_sphere(a, e + Math.PI / 2, 1);
482
- eye = func_sphere(a, e, r);
483
- d = [eye[0], eye[1], eye[2]];
677
+ // restore slider positions
678
+ this.setSlidersFromAngles();
679
+ },
484
680
 
485
- eye[0] += Pref[0];
486
- eye[1] += Pref[1];
487
- eye[2] += Pref[2];
681
+ /**
682
+ * @private
683
+ * @returns {Array}
684
+ */
685
+ _updateCentralProjection: function () {
686
+ var zf = 20, // near clip plane
687
+ zn = 8, // far clip plane
688
+
689
+ // See https://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_1_eng_web.html
690
+ // bbox3D is always at the world origin, i.e. T_obj is the unit matrix.
691
+ // All vectors contain affine coordinates and have length 3
692
+ // The matrices are of size 4x4.
693
+ r, A;
694
+
695
+ // set distance from view box center to camera
696
+ r = Type.evaluate(this.visProp.r);
697
+ if (r === 'auto') {
698
+ r = Mat.hypot(
699
+ this.bbox3D[0][0] - this.bbox3D[0][1],
700
+ this.bbox3D[1][0] - this.bbox3D[1][1],
701
+ this.bbox3D[2][0] - this.bbox3D[2][1]
702
+ ) * 1.01;
703
+ }
488
704
 
489
- // d = [eye[0] - Pref[0], eye[1] - Pref[1], eye[2] - Pref[2]];
490
- nrm = Mat.norm(d, 3);
491
- az = [d[0] / nrm, d[1] / nrm, d[2] / nrm];
705
+ // compute camera transformation
706
+ this.boxToCam = this.matrix3DRot.map((row) => row.slice());
707
+ this.boxToCam[3][0] = -r;
492
708
 
493
- nrm = Mat.norm(up, 3);
494
- v = [up[0] / nrm, up[1] / nrm, up[2] / nrm];
709
+ // compute focal distance and clip space transformation
710
+ this.focalDist = 1 / Math.tan(0.5 * Type.evaluate(this.visProp.fov));
711
+ A = [
712
+ [0, 0, 0, -1],
713
+ [0, this.focalDist, 0, 0],
714
+ [0, 0, this.focalDist, 0],
715
+ [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)]
716
+ ];
495
717
 
496
- ax = Mat.crossProduct(v, az);
497
- ay = Mat.crossProduct(az, ax);
718
+ return Mat.matMatMult(A, this.boxToCam);
719
+ },
498
720
 
499
- // compute camera transformation
500
- v = Mat.matVecMult([ax, ay, az], eye);
501
- this.cameraTransform = [
721
+ /**
722
+ * Comparison function for 3D points. It is used to sort points according to their z-index.
723
+ * @param {Point3D} a
724
+ * @param {Point3D} b
725
+ * @returns Integer
726
+ */
727
+ compareDepth: function (a, b) {
728
+ var worldDiff = [0,
729
+ a.coords[1] - b.coords[1],
730
+ a.coords[2] - b.coords[2],
731
+ a.coords[3] - b.coords[3]],
732
+ oriBoxDiff = Mat.matVecMult(this.matrix3DRot, Mat.matVecMult(this.shift, worldDiff));
733
+ return oriBoxDiff[3];
734
+ },
735
+
736
+ // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles.
737
+ update: function () {
738
+ var r = this.r,
739
+ stretch = [
502
740
  [1, 0, 0, 0],
503
- [-v[0], ax[0], ax[1], ax[2]],
504
- [-v[1], ay[0], ay[1], ay[2]],
505
- [-v[2], az[0], az[1], az[2]]
506
- ];
741
+ [0, -r, 0, 0],
742
+ [0, 0, -r, 0],
743
+ [0, 0, 0, 1]
744
+ ],
745
+ mat2D, objectToClip, size,
746
+ dx, dy,
747
+ objectsList;
748
+
749
+ if (
750
+ !Type.exists(this.el_slide) ||
751
+ !Type.exists(this.az_slide) ||
752
+ !Type.exists(this.bank_slide) ||
753
+ !this.needsUpdate
754
+ ) {
755
+ return this;
756
+ }
507
757
 
508
- // compute focal distance and clip space transformation
509
- this.focalDist = 1 / Math.tan(0.5 * Type.evaluate(this.visProp.fov));
510
- A = [
511
- [0, 0, 0, -1],
512
- [0, this.focalDist, 0, 0],
513
- [0, 0, this.focalDist, 0],
514
- [2 * zf * zn / (zn - zf), 0, 0, (zf + zn) / (zn - zf)]
515
- ];
758
+ mat2D = [
759
+ [1, 0, 0],
760
+ [0, 1, 0],
761
+ [0, 0, 1]
762
+ ];
516
763
 
517
- return Mat.matMatMult(A, this.cameraTransform);
518
- },
764
+ this.projectionType = Type.evaluate(this.visProp.projection).toLowerCase();
519
765
 
520
- // Update 3D-to-2D transformation matrix with the actual azimuth and elevation angles.
521
- update: function () {
522
- var mat2D, shift, size,
523
- dx, dy,
524
- Pref = null,
525
- useTrackball = false;
766
+ // override angle slider bounds when trackball navigation is enabled
767
+ if (this.trackballEnabled !== Type.evaluate(this.visProp.trackball.enabled)) {
768
+ this.updateAngleSliderBounds();
769
+ }
526
770
 
527
- if (
528
- !Type.exists(this.el_slide) ||
529
- !Type.exists(this.az_slide) ||
530
- !this.needsUpdate
531
- ) {
532
- return this;
533
- }
771
+ if (this._hasMoveTrackball) {
772
+ // The trackball has been moved since the last update, so we do
773
+ // trackball navigation. When the trackball is enabled, a drag
774
+ // event is interpreted as a trackball movement unless it's
775
+ // caught by something else, like point dragging. When the
776
+ // trackball is disabled, the trackball movement flag should
777
+ // never be set
778
+ this.matrix3DRot = this.updateProjectionTrackball();
779
+ this.setAnglesFromRotation();
780
+ } else if (this.anglesHaveMoved()) {
781
+ // The trackball hasn't been moved since the last up date, but
782
+ // the Tait-Bryan angles have been, so we do angle navigation
783
+ this.getAnglesFromSliders();
784
+ this.matrix3DRot = this.getRotationFromAngles();
785
+ }
534
786
 
535
- mat2D = [
536
- [1, 0, 0],
537
- [0, 1, 0],
538
- [0, 0, 1]
539
- ];
787
+ /**
788
+ * The translation that moves the center of the view box to the origin.
789
+ */
790
+ this.shift = [
791
+ [1, 0, 0, 0],
792
+ [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0],
793
+ [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0],
794
+ [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1]
795
+ ];
796
+
797
+ switch (this.projectionType) {
798
+ case 'central': // Central projection
799
+
800
+ // Add a final transformation to scale and shift the projection
801
+ // on the board, usually called viewport.
802
+ size = 2 * 0.4;
803
+ mat2D[1][1] = this.size[0] / size; // w / d_x
804
+ mat2D[2][2] = this.size[1] / size; // h / d_y
805
+ mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x
806
+ mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y
807
+ // The transformations this.matrix3D and mat2D can not be combined at this point,
808
+ // since the projected vectors have to be normalized in between in project3DTo2D
809
+ this.viewPortTransform = mat2D;
810
+
811
+ objectToClip = this._updateCentralProjection();
812
+ // this.matrix3D is a 4x4 matrix
813
+ this.matrix3D = Mat.matMatMult(objectToClip, this.shift);
814
+ break;
540
815
 
541
- this.projectionType = Type.evaluate(this.visProp.projection).toLowerCase();
542
-
543
- if (Type.evaluate(this.visProp.trackball.enabled) && Type.exists(this.matrix3DRot)) {
544
- // If trackball is enabled and this.matrix3DRot has been initialized,
545
- // do trackball navigation
546
- if (this._hasMoveTrackball) {
547
- // If this._hasMoveTrackball is false, the drag event has been
548
- // caught by e.g. point dragging
549
-
550
- if (this.projectionType === 'central') {
551
- // Get center of the trackball.
552
- // In case of parallel projection, this is not necessary,
553
- // since we translate the whole scene with "shift".
554
- Pref = [
555
- 0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]),
556
- 0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]),
557
- 0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1])
558
- ];
559
- }
560
- this.matrix3DRot = this.updateProjectionTrackball(Pref);
561
- }
562
- useTrackball = true;
563
- }
816
+ case 'parallel': // Parallel projection
817
+ default:
818
+ // Add a final transformation to scale and shift the projection
819
+ // on the board, usually called viewport.
820
+ dx = this.bbox3D[0][1] - this.bbox3D[0][0];
821
+ dy = this.bbox3D[1][1] - this.bbox3D[1][0];
822
+ mat2D[1][1] = this.size[0] / dx; // w / d_x
823
+ mat2D[2][2] = this.size[1] / dy; // h / d_y
824
+ mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x
825
+ mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y
826
+
827
+ // Combine all transformations, this.matrix3D is a 3x4 matrix
828
+ this.matrix3D = Mat.matMatMult(
829
+ mat2D,
830
+ Mat.matMatMult(Mat.matMatMult(this.matrix3DRot, stretch), this.shift).slice(0, 3)
831
+ );
832
+ }
564
833
 
565
- switch (this.projectionType) {
566
- case 'central': // Central projection
567
-
568
- // Add a final transformation to scale and shift the projection
569
- // on the board, usually called viewport.
570
- size = 2 * 0.4;
571
- mat2D[1][1] = this.size[0] / size; // w / d_x
572
- mat2D[2][2] = this.size[1] / size; // h / d_y
573
- mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * size; // llft_x
574
- mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * size; // llft_y
575
- // The transformations this.matrix3D and mat2D can not be combined at this point,
576
- // since the projected vectors have to be normalized in between in project3DTo2D
577
- this.viewPortTransform = mat2D;
578
-
579
- if (!useTrackball) {
580
- // Do elevation / azimuth navigation or at least initialize matrix
581
- // this.matrix3DRot
582
- this.matrix3DRot = this._updateCentralProjection();
583
- }
584
- // this.matrix3D is a 4x4 matrix
585
- this.matrix3D = this.matrix3DRot.slice();
586
- break;
587
-
588
- case 'parallel': // Parallel projection
589
- default:
590
- // Rotate the scenery around the center of the box, not around the origin
591
- shift = [
592
- [1, 0, 0, 0],
593
- [-0.5 * (this.bbox3D[0][0] + this.bbox3D[0][1]), 1, 0, 0],
594
- [-0.5 * (this.bbox3D[1][0] + this.bbox3D[1][1]), 0, 1, 0],
595
- [-0.5 * (this.bbox3D[2][0] + this.bbox3D[2][1]), 0, 0, 1]
596
- ];
597
-
598
- // Add a final transformation to scale and shift the projection
599
- // on the board, usually called viewport.
600
- dx = this.bbox3D[0][1] - this.bbox3D[0][0];
601
- dy = this.bbox3D[1][1] - this.bbox3D[1][0];
602
- mat2D[1][1] = this.size[0] / dx; // w / d_x
603
- mat2D[2][2] = this.size[1] / dy; // h / d_y
604
- mat2D[1][0] = this.llftCorner[0] + mat2D[1][1] * 0.5 * dx; // llft_x
605
- mat2D[2][0] = this.llftCorner[1] + mat2D[2][2] * 0.5 * dy; // llft_y
606
-
607
- if (!useTrackball) {
608
- // Do elevation / azimuth navigation or at least initialize matrix this.matrix3DRot
609
- this.matrix3DRot = this.updateParallelProjection();
610
- }
611
- // Combine all transformations, this.matrix3D is a 3x4 matrix
612
- this.matrix3D = Mat.matMatMult(mat2D, Mat.matMatMult(this.matrix3DRot, shift).slice(0, 3));
613
- }
834
+ // if depth-ordering for points was just switched on, initialize the
835
+ // list of points
836
+ if (this.visProp.depthorderpoints && this.points === null) {
837
+ objectsList = Object.values(this.objects);
838
+ this.points = objectsList.filter(
839
+ el => el.type === Const.OBJECT_TYPE_POINT3D
840
+ );
841
+ }
614
842
 
615
- return this;
616
- },
843
+ // if depth-ordering for points was just switched off, throw away the
844
+ // list of points
845
+ if (!this.visProp.depthorderpoints && this.points !== null) {
846
+ this.points = null;
847
+ }
617
848
 
618
- updateRenderer: function () {
619
- this.needsUpdate = false;
620
- return this;
621
- },
849
+ // depth-order visible points. the `setLayer` method is used here to
850
+ // re-order the points within each layer: it has the side effect of
851
+ // moving the target element to the end of the layer's child list
852
+ if (this.visProp.depthorderpoints && this.board.renderer && this.board.renderer.type === 'svg') {
853
+ this.points
854
+ .filter((pt) => Type.evaluate(pt.element2D.visProp.visible))
855
+ .sort(this.compareDepth.bind(this))
856
+ .forEach((pt) => this.board.renderer.setLayer(pt.element2D, pt.element2D.visProp.layer));
857
+
858
+ /* [DEBUG] list oriented box coordinates in depth order */
859
+ // console.log('depth-ordered points in oriented box coordinates');
860
+ // this.points
861
+ // .filter((pt) => pt.element2D.visProp.visible)
862
+ // .sort(compareDepth)
863
+ // .forEach(function (pt) {
864
+ // console.log(Mat.matVecMult(that.matrix3DRot, Mat.matVecMult(that.shift, pt.coords)));
865
+ // });
866
+ }
622
867
 
623
- removeObject: function (object, saveMethod) {
624
- var i;
868
+ return this;
869
+ },
625
870
 
626
- // this.board.removeObject(object, saveMethod);
627
- if (Type.isArray(object)) {
628
- for (i = 0; i < object.length; i++) {
629
- this.removeObject(object[i]);
630
- }
631
- return this;
632
- }
871
+ updateRenderer: function () {
872
+ this.needsUpdate = false;
873
+ return this;
874
+ },
633
875
 
634
- object = this.select(object);
876
+ removeObject: function (object, saveMethod) {
877
+ var i;
635
878
 
636
- // // If the object which is about to be removed unknown or a string, do nothing.
637
- // // it is a string if a string was given and could not be resolved to an element.
638
- if (!Type.exists(object) || Type.isString(object)) {
639
- return this;
879
+ // this.board.removeObject(object, saveMethod);
880
+ if (Type.isArray(object)) {
881
+ for (i = 0; i < object.length; i++) {
882
+ this.removeObject(object[i]);
640
883
  }
884
+ return this;
885
+ }
641
886
 
642
- try {
643
- // // remove all children.
644
- // for (el in object.childElements) {
645
- // if (object.childElements.hasOwnProperty(el)) {
646
- // object.childElements[el].board.removeObject(object.childElements[el]);
647
- // }
648
- // }
649
-
650
- delete this.objects[object.id];
651
- } catch (e) {
652
- JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e);
653
- }
887
+ object = this.select(object);
654
888
 
655
- // this.update();
889
+ // // If the object which is about to be removed unknown or a string, do nothing.
890
+ // // it is a string if a string was given and could not be resolved to an element.
891
+ if (!Type.exists(object) || Type.isString(object)) {
892
+ return this;
893
+ }
656
894
 
657
- this.board.removeObject(object, saveMethod);
895
+ try {
896
+ // // remove all children.
897
+ // for (el in object.childElements) {
898
+ // if (object.childElements.hasOwnProperty(el)) {
899
+ // object.childElements[el].board.removeObject(object.childElements[el]);
900
+ // }
901
+ // }
902
+
903
+ delete this.objects[object.id];
904
+ } catch (e) {
905
+ JXG.debug('View3D ' + object.id + ': Could not be removed: ' + e);
906
+ }
658
907
 
659
- return this;
660
- },
908
+ // this.update();
661
909
 
662
- /**
663
- * Map world coordinates to view coordinates.
664
- *
665
- * @param {Array} pWorld A world space point, in homogeneous coordinates.
666
- * @param {Boolean} [homog=true] Whether to return homogeneous coordinates.
667
- * If false, projects down to ordinary coordinates.
668
- */
669
- worldToView: function (pWorld, homog=true) {
670
- var k,
671
- pView = Mat.matVecMult(this.cameraTransform, pWorld);
910
+ this.board.removeObject(object, saveMethod);
672
911
 
673
- pView[3] -= pView[0] * this.focalDist;
674
- if (homog) {
675
- return pView;
676
- } else {
677
- for (k = 1; k < 4; k++) {
678
- pView[k] /= pView[0];
679
- }
680
- return pView.slice(1, 4);
912
+ return this;
913
+ },
914
+
915
+ /**
916
+ * Map world coordinates to focal coordinates. These coordinate systems
917
+ * are explained in the {@link JXG.View3D#boxToCam} matrix
918
+ * documentation.
919
+ *
920
+ * @param {Array} pWorld A world space point, in homogeneous coordinates.
921
+ * @param {Boolean} [homog=true] Whether to return homogeneous coordinates.
922
+ * If false, projects down to ordinary coordinates.
923
+ */
924
+ worldToFocal: function (pWorld, homog = true) {
925
+ var k,
926
+ pView = Mat.matVecMult(this.boxToCam, Mat.matVecMult(this.shift, pWorld));
927
+ pView[3] -= pView[0] * this.focalDist;
928
+ if (homog) {
929
+ return pView;
930
+ } else {
931
+ for (k = 1; k < 4; k++) {
932
+ pView[k] /= pView[0];
681
933
  }
682
- },
934
+ return pView.slice(1, 4);
935
+ }
936
+ },
683
937
 
684
- /**
685
- * Project 3D coordinates to 2D board coordinates
686
- * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
687
- *
688
- * @param {Number|Array} x
689
- * @param {Number[]} y
690
- * @param {Number[]} z
691
- * @returns {Array} Array of length 3 containing the projection on to the board
692
- * in homogeneous user coordinates.
693
- */
694
- project3DTo2D: function (x, y, z) {
695
- var vec, w;
696
- if (arguments.length === 3) {
697
- vec = [1, x, y, z];
938
+ /**
939
+ * Project 3D coordinates to 2D board coordinates
940
+ * The 3D coordinates are provides as three numbers x, y, z or one array of length 3.
941
+ *
942
+ * @param {Number|Array} x
943
+ * @param {Number[]} y
944
+ * @param {Number[]} z
945
+ * @returns {Array} Array of length 3 containing the projection on to the board
946
+ * in homogeneous user coordinates.
947
+ */
948
+ project3DTo2D: function (x, y, z) {
949
+ var vec, w;
950
+ if (arguments.length === 3) {
951
+ vec = [1, x, y, z];
952
+ } else {
953
+ // Argument is an array
954
+ if (x.length === 3) {
955
+ // vec = [1].concat(x);
956
+ vec = x.slice();
957
+ vec.unshift(1);
698
958
  } else {
699
- // Argument is an array
700
- if (x.length === 3) {
701
- // vec = [1].concat(x);
702
- vec = x.slice();
703
- vec.unshift(1);
704
- } else {
705
- vec = x;
706
- }
959
+ vec = x;
707
960
  }
961
+ }
708
962
 
709
- w = Mat.matVecMult(this.matrix3D, vec);
963
+ w = Mat.matVecMult(this.matrix3D, vec);
710
964
 
711
- switch (this.projectionType) {
712
- case 'central':
713
- w[1] /= w[0];
714
- w[2] /= w[0];
715
- w[3] /= w[0];
716
- w[0] /= w[0];
717
- return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3));
965
+ switch (this.projectionType) {
966
+ case 'central':
967
+ w[1] /= w[0];
968
+ w[2] /= w[0];
969
+ w[3] /= w[0];
970
+ w[0] /= w[0];
971
+ return Mat.matVecMult(this.viewPortTransform, w.slice(0, 3));
718
972
 
719
- case 'parallel':
720
- default:
721
- return w;
722
- }
723
- },
973
+ case 'parallel':
974
+ default:
975
+ return w;
976
+ }
977
+ },
724
978
 
725
- /**
726
- * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y.
727
- * Setting R = mat^(-1) gives
728
- * 1/ w0 * (1, x, y, d)^T = R * v2d.
729
- * The first and the last row of this equation allows to determine 1/w0 and h.
730
- *
731
- * @param {Array} mat
732
- * @param {Array} v2d
733
- * @param {Number} d
734
- * @returns Array
735
- * @private
736
- */
737
- _getW0: function(mat, v2d, d) {
738
- var R = Mat.inverse(mat),
739
- R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2],
740
- R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2],
741
- w, h, det;
742
-
743
- det = d * R[0][3] - R[3][3];
744
- w = (R2 * R[0][3] - R1 * R[3][3]) / det;
745
- h = (R2 - R1 * d) / det;
746
- return [1 / w, h];
747
- },
979
+ /**
980
+ * We know that v2d * w0 = mat * (1, x, y, d)^T where v2d = (1, b, c, h)^T with unknowns w0, h, x, y.
981
+ * Setting R = mat^(-1) gives
982
+ * 1/ w0 * (1, x, y, d)^T = R * v2d.
983
+ * The first and the last row of this equation allows to determine 1/w0 and h.
984
+ *
985
+ * @param {Array} mat
986
+ * @param {Array} v2d
987
+ * @param {Number} d
988
+ * @returns Array
989
+ * @private
990
+ */
991
+ _getW0: function (mat, v2d, d) {
992
+ var R = Mat.inverse(mat),
993
+ R1 = R[0][0] + v2d[1] * R[0][1] + v2d[2] * R[0][2],
994
+ R2 = R[3][0] + v2d[1] * R[3][1] + v2d[2] * R[3][2],
995
+ w, h, det;
996
+
997
+ det = d * R[0][3] - R[3][3];
998
+ w = (R2 * R[0][3] - R1 * R[3][3]) / det;
999
+ h = (R2 - R1 * d) / det;
1000
+ return [1 / w, h];
1001
+ },
748
1002
 
749
- /**
750
- * Project a 2D coordinate to the plane defined by point "foot"
751
- * and the normal vector `normal`.
752
- *
753
- * @param {JXG.Point} point2d
754
- * @param {Array} normal
755
- * @param {Array} foot
756
- * @returns {Array} of length 4 containing the projected
757
- * point in homogeneous coordinates.
758
- */
759
- project2DTo3DPlane: function (point2d, normal, foot) {
760
- var mat, rhs, d, le, sol,
761
- n = normal.slice(1),
762
- v2d, w0, res;
763
-
764
- foot = foot || [1, 0, 0, 0];
765
- le = Mat.norm(n, 3);
766
- d = Mat.innerProduct(foot.slice(1), n, 3) / le;
767
-
768
- if (this.projectionType === 'parallel') {
769
- mat = this.matrix3D.slice(0, 3); // True copy
770
- mat.push([0, n[0], n[1], n[2]]);
771
-
772
- // 2D coordinates of point:
773
- rhs = point2d.coords.usrCoords.slice();
774
- rhs.push(d);
775
- try {
776
- // Prevent singularity in case elevation angle is zero
777
- if (mat[2][3] === 1.0) {
778
- mat[2][1] = mat[2][2] = Mat.eps * 0.001;
779
- }
780
- sol = Mat.Numerics.Gauss(mat, rhs);
781
- } catch (e) {
782
- sol = [0, NaN, NaN, NaN];
1003
+ /**
1004
+ * Project a 2D coordinate to the plane defined by point "foot"
1005
+ * and the normal vector `normal`.
1006
+ *
1007
+ * @param {JXG.Point} point2d
1008
+ * @param {Array} normal
1009
+ * @param {Array} foot
1010
+ * @returns {Array} of length 4 containing the projected
1011
+ * point in homogeneous coordinates.
1012
+ */
1013
+ project2DTo3DPlane: function (point2d, normal, foot) {
1014
+ var mat, rhs, d, le, sol,
1015
+ n = normal.slice(1),
1016
+ v2d, w0, res;
1017
+
1018
+ foot = foot || [1, 0, 0, 0];
1019
+ le = Mat.norm(n, 3);
1020
+ d = Mat.innerProduct(foot.slice(1), n, 3) / le;
1021
+
1022
+ if (this.projectionType === 'parallel') {
1023
+ mat = this.matrix3D.slice(0, 3); // Copy each row by reference
1024
+ mat.push([0, n[0], n[1], n[2]]);
1025
+
1026
+ // 2D coordinates of point:
1027
+ rhs = point2d.coords.usrCoords.slice();
1028
+ rhs.push(d);
1029
+ try {
1030
+ // Prevent singularity in case elevation angle is zero
1031
+ if (mat[2][3] === 1.0) {
1032
+ mat[2][1] = mat[2][2] = Mat.eps * 0.001;
783
1033
  }
784
- } else {
785
- mat = this.matrix3DRot; // True copy
786
-
787
- // 2D coordinates of point:
788
- rhs = point2d.coords.usrCoords.slice();
789
-
790
- v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs);
791
- res = this._getW0(mat, v2d, d);
792
- w0 = res[0];
793
- rhs = [
794
- v2d[0] * w0,
795
- v2d[1] * w0,
796
- v2d[2] * w0,
797
- res[1] * w0
798
- ];
799
- try {
800
- // Prevent singularity in case elevation angle is zero
801
- if (mat[2][3] === 1.0) {
802
- mat[2][1] = mat[2][2] = Mat.eps * 0.001;
803
- }
804
-
805
- sol = Mat.Numerics.Gauss(mat, rhs);
806
- sol[1] /= sol[0];
807
- sol[2] /= sol[0];
808
- sol[3] /= sol[0];
809
- // sol[3] = d;
810
- sol[0] /= sol[0];
811
- } catch (err) {
812
- sol = [0, NaN, NaN, NaN];
1034
+ sol = Mat.Numerics.Gauss(mat, rhs);
1035
+ } catch (e) {
1036
+ sol = [0, NaN, NaN, NaN];
1037
+ }
1038
+ } else {
1039
+ mat = this.matrix3D;
1040
+
1041
+ // 2D coordinates of point:
1042
+ rhs = point2d.coords.usrCoords.slice();
1043
+
1044
+ v2d = Mat.Numerics.Gauss(this.viewPortTransform, rhs);
1045
+ res = this._getW0(mat, v2d, d);
1046
+ w0 = res[0];
1047
+ rhs = [
1048
+ v2d[0] * w0,
1049
+ v2d[1] * w0,
1050
+ v2d[2] * w0,
1051
+ res[1] * w0
1052
+ ];
1053
+ try {
1054
+ // Prevent singularity in case elevation angle is zero
1055
+ if (mat[2][3] === 1.0) {
1056
+ mat[2][1] = mat[2][2] = Mat.eps * 0.001;
813
1057
  }
1058
+
1059
+ sol = Mat.Numerics.Gauss(mat, rhs);
1060
+ sol[1] /= sol[0];
1061
+ sol[2] /= sol[0];
1062
+ sol[3] /= sol[0];
1063
+ // sol[3] = d;
1064
+ sol[0] /= sol[0];
1065
+ } catch (err) {
1066
+ sol = [0, NaN, NaN, NaN];
814
1067
  }
1068
+ }
815
1069
 
816
- return sol;
817
- },
1070
+ return sol;
1071
+ },
818
1072
 
819
- /**
820
- * Project a 2D coordinate to a new 3D position by keeping
821
- * the 3D x, y coordinates and changing only the z coordinate.
822
- * All horizontal moves of the 2D point are ignored.
823
- *
824
- * @param {JXG.Point} point2d
825
- * @param {Array} coords3D
826
- * @returns {Array} of length 4 containing the projected
827
- * point in homogeneous coordinates.
828
- */
829
- project2DTo3DVertical: function (point2d, coords3D) {
830
- var m3D = this.matrix3D[2],
831
- b = m3D[3],
832
- rhs = point2d.coords.usrCoords[2]; // y in 2D
833
-
834
- rhs -= m3D[0] * coords3D[0] + m3D[1] * coords3D[1] + m3D[2] * coords3D[2];
835
- if (Math.abs(b) < Mat.eps) {
836
- return coords3D; // No changes
837
- } else {
838
- return coords3D.slice(0, 3).concat([rhs / b]);
839
- }
840
- },
1073
+ /**
1074
+ * Project a point on the screen to the nearest point, in screen
1075
+ * distance, on a line segment in 3d space. The inputs must be in
1076
+ * ordinary coordinates, but the output is in homogeneous coordinates.
1077
+ *
1078
+ * @param {Array} pScr The screen coordinates of the point to project.
1079
+ * @param {Array} end0 The world space coordinates of one end of the
1080
+ * line segment.
1081
+ * @param {Array} end1 The world space coordinates of the other end of
1082
+ * the line segment.
1083
+ */
1084
+ projectScreenToSegment: function (pScr, end0, end1) {
1085
+ var end0_2d = this.project3DTo2D(end0).slice(1, 3),
1086
+ end1_2d = this.project3DTo2D(end1).slice(1, 3),
1087
+ dir_2d = [
1088
+ end1_2d[0] - end0_2d[0],
1089
+ end1_2d[1] - end0_2d[1]
1090
+ ],
1091
+ dir_2d_norm_sq = Mat.innerProduct(dir_2d, dir_2d),
1092
+ diff = [
1093
+ pScr[0] - end0_2d[0],
1094
+ pScr[1] - end0_2d[1]
1095
+ ],
1096
+ s = Mat.innerProduct(diff, dir_2d) / dir_2d_norm_sq, // screen-space affine parameter
1097
+ mid, mid_2d, mid_diff, m,
1098
+
1099
+ t, // view-space affine parameter
1100
+ t_clamped, // affine parameter clamped to range
1101
+ t_clamped_co;
1102
+
1103
+ if (this.projectionType === 'central') {
1104
+ mid = [
1105
+ 0.5 * (end0[0] + end1[0]),
1106
+ 0.5 * (end0[1] + end1[1]),
1107
+ 0.5 * (end0[2] + end1[2])
1108
+ ];
1109
+ mid_2d = this.project3DTo2D(mid).slice(1, 3);
1110
+ mid_diff = [
1111
+ mid_2d[0] - end0_2d[0],
1112
+ mid_2d[1] - end0_2d[1]
1113
+ ];
1114
+ m = Mat.innerProduct(mid_diff, dir_2d) / dir_2d_norm_sq;
1115
+
1116
+ // the view-space affine parameter s is related to the
1117
+ // screen-space affine parameter t by a Möbius transformation,
1118
+ // which is determined by the following relations:
1119
+ //
1120
+ // s | t
1121
+ // -----
1122
+ // 0 | 0
1123
+ // m | 1/2
1124
+ // 1 | 1
1125
+ //
1126
+ t = (1 - m) * s / ((1 - 2 * m) * s + m);
1127
+ } else {
1128
+ t = s;
1129
+ }
841
1130
 
842
- /**
843
- * Limit 3D coordinates to the bounding cube.
844
- *
845
- * @param {Array} c3d 3D coordinates [x,y,z]
846
- * @returns Array with updated 3D coordinates.
847
- */
848
- project3DToCube: function (c3d) {
849
- var cube = this.bbox3D;
850
- if (c3d[1] < cube[0][0]) {
851
- c3d[1] = cube[0][0];
852
- }
853
- if (c3d[1] > cube[0][1]) {
854
- c3d[1] = cube[0][1];
855
- }
856
- if (c3d[2] < cube[1][0]) {
857
- c3d[2] = cube[1][0];
858
- }
859
- if (c3d[2] > cube[1][1]) {
860
- c3d[2] = cube[1][1];
861
- }
862
- if (c3d[3] < cube[2][0]) {
863
- c3d[3] = cube[2][0];
864
- }
865
- if (c3d[3] > cube[2][1]) {
866
- c3d[3] = cube[2][1];
867
- }
1131
+ t_clamped = Math.min(Math.max(t, 0), 1);
1132
+ t_clamped_co = 1 - t_clamped;
1133
+ return [
1134
+ 1,
1135
+ t_clamped_co * end0[0] + t_clamped * end1[0],
1136
+ t_clamped_co * end0[1] + t_clamped * end1[1],
1137
+ t_clamped_co * end0[2] + t_clamped * end1[2]
1138
+ ];
1139
+ },
868
1140
 
869
- return c3d;
870
- },
1141
+ /**
1142
+ * Project a 2D coordinate to a new 3D position by keeping
1143
+ * the 3D x, y coordinates and changing only the z coordinate.
1144
+ * All horizontal moves of the 2D point are ignored.
1145
+ *
1146
+ * @param {JXG.Point} point2d
1147
+ * @param {Array} base_c3d
1148
+ * @returns {Array} of length 4 containing the projected
1149
+ * point in homogeneous coordinates.
1150
+ */
1151
+ project2DTo3DVertical: function (point2d, base_c3d) {
1152
+ var pScr = point2d.coords.usrCoords.slice(1, 3),
1153
+ end0 = [base_c3d[1], base_c3d[2], this.bbox3D[2][0]],
1154
+ end1 = [base_c3d[1], base_c3d[2], this.bbox3D[2][1]];
871
1155
 
872
- /**
873
- * Intersect a ray with the bounding cube of the 3D view.
874
- * @param {Array} p 3D coordinates [x,y,z]
875
- * @param {Array} d 3D direction vector of the line (array of length 3)
876
- * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
877
- * @returns Affine ratio of the intersection of the line with the cube.
878
- */
879
- intersectionLineCube: function (p, d, r) {
880
- var r_n, i, r0, r1;
881
-
882
- r_n = r;
883
- for (i = 0; i < 3; i++) {
884
- if (d[i] !== 0) {
885
- r0 = (this.bbox3D[i][0] - p[i]) / d[i];
886
- r1 = (this.bbox3D[i][1] - p[i]) / d[i];
887
- if (r < 0) {
888
- r_n = Math.max(r_n, Math.min(r0, r1));
889
- } else {
890
- r_n = Math.min(r_n, Math.max(r0, r1));
891
- }
1156
+ return this.projectScreenToSegment(pScr, end0, end1);
1157
+ },
1158
+
1159
+ /**
1160
+ * Limit 3D coordinates to the bounding cube.
1161
+ *
1162
+ * @param {Array} c3d 3D coordinates [x,y,z]
1163
+ * @returns Array with updated 3D coordinates.
1164
+ */
1165
+ project3DToCube: function (c3d) {
1166
+ var cube = this.bbox3D;
1167
+ if (c3d[1] < cube[0][0]) {
1168
+ c3d[1] = cube[0][0];
1169
+ }
1170
+ if (c3d[1] > cube[0][1]) {
1171
+ c3d[1] = cube[0][1];
1172
+ }
1173
+ if (c3d[2] < cube[1][0]) {
1174
+ c3d[2] = cube[1][0];
1175
+ }
1176
+ if (c3d[2] > cube[1][1]) {
1177
+ c3d[2] = cube[1][1];
1178
+ }
1179
+ if (c3d[3] < cube[2][0]) {
1180
+ c3d[3] = cube[2][0];
1181
+ }
1182
+ if (c3d[3] > cube[2][1]) {
1183
+ c3d[3] = cube[2][1];
1184
+ }
1185
+
1186
+ return c3d;
1187
+ },
1188
+
1189
+ /**
1190
+ * Intersect a ray with the bounding cube of the 3D view.
1191
+ * @param {Array} p 3D coordinates [x,y,z]
1192
+ * @param {Array} d 3D direction vector of the line (array of length 3)
1193
+ * @param {Number} r direction of the ray (positive if r > 0, negative if r < 0).
1194
+ * @returns Affine ratio of the intersection of the line with the cube.
1195
+ */
1196
+ intersectionLineCube: function (p, d, r) {
1197
+ var r_n, i, r0, r1;
1198
+
1199
+ r_n = r;
1200
+ for (i = 0; i < 3; i++) {
1201
+ if (d[i] !== 0) {
1202
+ r0 = (this.bbox3D[i][0] - p[i]) / d[i];
1203
+ r1 = (this.bbox3D[i][1] - p[i]) / d[i];
1204
+ if (r < 0) {
1205
+ r_n = Math.max(r_n, Math.min(r0, r1));
1206
+ } else {
1207
+ r_n = Math.min(r_n, Math.max(r0, r1));
892
1208
  }
893
1209
  }
894
- return r_n;
895
- },
1210
+ }
1211
+ return r_n;
1212
+ },
896
1213
 
897
- /**
898
- * Test if coordinates are inside of the bounding cube.
899
- * @param {array} q 3D coordinates [x,y,z] of a point.
900
- * @returns Boolean
901
- */
902
- isInCube: function (q) {
903
- return (
904
- q[0] > this.bbox3D[0][0] - Mat.eps &&
905
- q[0] < this.bbox3D[0][1] + Mat.eps &&
906
- q[1] > this.bbox3D[1][0] - Mat.eps &&
907
- q[1] < this.bbox3D[1][1] + Mat.eps &&
908
- q[2] > this.bbox3D[2][0] - Mat.eps &&
909
- q[2] < this.bbox3D[2][1] + Mat.eps
910
- );
911
- },
1214
+ /**
1215
+ * Test if coordinates are inside of the bounding cube.
1216
+ * @param {array} q 3D coordinates [x,y,z] of a point.
1217
+ * @returns Boolean
1218
+ */
1219
+ isInCube: function (q) {
1220
+ return (
1221
+ q[0] > this.bbox3D[0][0] - Mat.eps &&
1222
+ q[0] < this.bbox3D[0][1] + Mat.eps &&
1223
+ q[1] > this.bbox3D[1][0] - Mat.eps &&
1224
+ q[1] < this.bbox3D[1][1] + Mat.eps &&
1225
+ q[2] > this.bbox3D[2][0] - Mat.eps &&
1226
+ q[2] < this.bbox3D[2][1] + Mat.eps
1227
+ );
1228
+ },
912
1229
 
913
- /**
914
- *
915
- * @param {JXG.Plane3D} plane1
916
- * @param {JXG.Plane3D} plane2
917
- * @param {JXG.Plane3D} d
918
- * @returns {Array} of length 2 containing the coordinates of the defining points of
919
- * of the intersection segment.
920
- */
921
- intersectionPlanePlane: function (plane1, plane2, d) {
922
- var ret = [[], []],
923
- p,
924
- dir,
925
- r,
926
- q;
927
-
928
- d = d || plane2.d;
929
-
930
- p = Mat.Geometry.meet3Planes(
931
- plane1.normal,
932
- plane1.d,
933
- plane2.normal,
934
- d,
935
- Mat.crossProduct(plane1.normal, plane2.normal),
936
- 0
937
- );
938
- dir = Mat.Geometry.meetPlanePlane(
939
- plane1.vec1,
940
- plane1.vec2,
941
- plane2.vec1,
942
- plane2.vec2
943
- );
944
- r = this.intersectionLineCube(p, dir, Infinity);
945
- q = Mat.axpy(r, dir, p);
946
- if (this.isInCube(q)) {
947
- ret[0] = q;
948
- }
949
- r = this.intersectionLineCube(p, dir, -Infinity);
950
- q = Mat.axpy(r, dir, p);
951
- if (this.isInCube(q)) {
952
- ret[1] = q;
953
- }
954
- return ret;
955
- },
1230
+ /**
1231
+ *
1232
+ * @param {JXG.Plane3D} plane1
1233
+ * @param {JXG.Plane3D} plane2
1234
+ * @param {JXG.Plane3D} d
1235
+ * @returns {Array} of length 2 containing the coordinates of the defining points of
1236
+ * of the intersection segment.
1237
+ */
1238
+ intersectionPlanePlane: function (plane1, plane2, d) {
1239
+ var ret = [[], []],
1240
+ p,
1241
+ dir,
1242
+ r,
1243
+ q;
1244
+
1245
+ d = d || plane2.d;
1246
+
1247
+ p = Mat.Geometry.meet3Planes(
1248
+ plane1.normal,
1249
+ plane1.d,
1250
+ plane2.normal,
1251
+ d,
1252
+ Mat.crossProduct(plane1.normal, plane2.normal),
1253
+ 0
1254
+ );
1255
+ dir = Mat.Geometry.meetPlanePlane(
1256
+ plane1.vec1,
1257
+ plane1.vec2,
1258
+ plane2.vec1,
1259
+ plane2.vec2
1260
+ );
1261
+ r = this.intersectionLineCube(p, dir, Infinity);
1262
+ q = Mat.axpy(r, dir, p);
1263
+ if (this.isInCube(q)) {
1264
+ ret[0] = q;
1265
+ }
1266
+ r = this.intersectionLineCube(p, dir, -Infinity);
1267
+ q = Mat.axpy(r, dir, p);
1268
+ if (this.isInCube(q)) {
1269
+ ret[1] = q;
1270
+ }
1271
+ return ret;
1272
+ },
956
1273
 
957
- /**
958
- * Generate mesh for a surface / plane.
959
- * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
960
- * @param {Array|Function} func
961
- * @param {Array} interval_u
962
- * @param {Array} interval_v
963
- * @returns Array
964
- * @private
965
- *
966
- * @example
967
- * var el = view.create('curve', [[], []]);
968
- * el.updateDataArray = function () {
969
- * var steps_u = Type.evaluate(this.visProp.stepsu),
970
- * steps_v = Type.evaluate(this.visProp.stepsv),
971
- * r_u = Type.evaluate(this.range_u),
972
- * r_v = Type.evaluate(this.range_v),
973
- * func, ret;
974
- *
975
- * if (this.F !== null) {
976
- * func = this.F;
977
- * } else {
978
- * func = [this.X, this.Y, this.Z];
979
- * }
980
- * ret = this.view.getMesh(func,
981
- * r_u.concat([steps_u]),
982
- * r_v.concat([steps_v]));
983
- *
984
- * this.dataX = ret[0];
985
- * this.dataY = ret[1];
986
- * };
987
- *
988
- */
989
- getMesh: function (func, interval_u, interval_v) {
990
- var i_u, i_v, u, v,
991
- c2d, delta_u, delta_v,
992
- p = [0, 0, 0],
993
- steps_u = interval_u[2],
994
- steps_v = interval_v[2],
995
- dataX = [],
996
- dataY = [];
997
-
998
- delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
999
- delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
1274
+ /**
1275
+ * Generate mesh for a surface / plane.
1276
+ * Returns array [dataX, dataY] for a JSXGraph curve's updateDataArray function.
1277
+ * @param {Array|Function} func
1278
+ * @param {Array} interval_u
1279
+ * @param {Array} interval_v
1280
+ * @returns Array
1281
+ * @private
1282
+ *
1283
+ * @example
1284
+ * var el = view.create('curve', [[], []]);
1285
+ * el.updateDataArray = function () {
1286
+ * var steps_u = Type.evaluate(this.visProp.stepsu),
1287
+ * steps_v = Type.evaluate(this.visProp.stepsv),
1288
+ * r_u = Type.evaluate(this.range_u),
1289
+ * r_v = Type.evaluate(this.range_v),
1290
+ * func, ret;
1291
+ *
1292
+ * if (this.F !== null) {
1293
+ * func = this.F;
1294
+ * } else {
1295
+ * func = [this.X, this.Y, this.Z];
1296
+ * }
1297
+ * ret = this.view.getMesh(func,
1298
+ * r_u.concat([steps_u]),
1299
+ * r_v.concat([steps_v]));
1300
+ *
1301
+ * this.dataX = ret[0];
1302
+ * this.dataY = ret[1];
1303
+ * };
1304
+ *
1305
+ */
1306
+ getMesh: function (func, interval_u, interval_v) {
1307
+ var i_u, i_v, u, v,
1308
+ c2d, delta_u, delta_v,
1309
+ p = [0, 0, 0],
1310
+ steps_u = interval_u[2],
1311
+ steps_v = interval_v[2],
1312
+ dataX = [],
1313
+ dataY = [];
1314
+
1315
+ delta_u = (Type.evaluate(interval_u[1]) - Type.evaluate(interval_u[0])) / steps_u;
1316
+ delta_v = (Type.evaluate(interval_v[1]) - Type.evaluate(interval_v[0])) / steps_v;
1317
+
1318
+ for (i_u = 0; i_u <= steps_u; i_u++) {
1319
+ u = interval_u[0] + delta_u * i_u;
1320
+ for (i_v = 0; i_v <= steps_v; i_v++) {
1321
+ v = interval_v[0] + delta_v * i_v;
1322
+ if (Type.isFunction(func)) {
1323
+ p = func(u, v);
1324
+ } else {
1325
+ p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1326
+ }
1327
+ c2d = this.project3DTo2D(p);
1328
+ dataX.push(c2d[1]);
1329
+ dataY.push(c2d[2]);
1330
+ }
1331
+ dataX.push(NaN);
1332
+ dataY.push(NaN);
1333
+ }
1000
1334
 
1335
+ for (i_v = 0; i_v <= steps_v; i_v++) {
1336
+ v = interval_v[0] + delta_v * i_v;
1001
1337
  for (i_u = 0; i_u <= steps_u; i_u++) {
1002
1338
  u = interval_u[0] + delta_u * i_u;
1003
- for (i_v = 0; i_v <= steps_v; i_v++) {
1004
- v = interval_v[0] + delta_v * i_v;
1005
- if (Type.isFunction(func)) {
1006
- p = func(u, v);
1007
- } else {
1008
- p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1009
- }
1010
- c2d = this.project3DTo2D(p);
1011
- dataX.push(c2d[1]);
1012
- dataY.push(c2d[2]);
1339
+ if (Type.isFunction(func)) {
1340
+ p = func(u, v);
1341
+ } else {
1342
+ p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1013
1343
  }
1014
- dataX.push(NaN);
1015
- dataY.push(NaN);
1344
+ c2d = this.project3DTo2D(p);
1345
+ dataX.push(c2d[1]);
1346
+ dataY.push(c2d[2]);
1016
1347
  }
1348
+ dataX.push(NaN);
1349
+ dataY.push(NaN);
1350
+ }
1017
1351
 
1018
- for (i_v = 0; i_v <= steps_v; i_v++) {
1019
- v = interval_v[0] + delta_v * i_v;
1020
- for (i_u = 0; i_u <= steps_u; i_u++) {
1021
- u = interval_u[0] + delta_u * i_u;
1022
- if (Type.isFunction(func)) {
1023
- p = func(u, v);
1024
- } else {
1025
- p = [func[0](u, v), func[1](u, v), func[2](u, v)];
1026
- }
1027
- c2d = this.project3DTo2D(p);
1028
- dataX.push(c2d[1]);
1029
- dataY.push(c2d[2]);
1030
- }
1031
- dataX.push(NaN);
1032
- dataY.push(NaN);
1033
- }
1352
+ return [dataX, dataY];
1353
+ },
1034
1354
 
1035
- return [dataX, dataY];
1036
- },
1355
+ /**
1356
+ *
1357
+ */
1358
+ animateAzimuth: function () {
1359
+ var s = this.az_slide._smin,
1360
+ e = this.az_slide._smax,
1361
+ sdiff = e - s,
1362
+ newVal = this.az_slide.Value() + 0.1;
1363
+
1364
+ this.az_slide.position = (newVal - s) / sdiff;
1365
+ if (this.az_slide.position > 1) {
1366
+ this.az_slide.position = 0.0;
1367
+ }
1368
+ this.board.update();
1037
1369
 
1038
- /**
1039
- *
1040
- */
1041
- animateAzimuth: function () {
1042
- var s = this.az_slide._smin,
1043
- e = this.az_slide._smax,
1044
- sdiff = e - s,
1045
- newVal = this.az_slide.Value() + 0.1;
1046
-
1047
- this.az_slide.position = (newVal - s) / sdiff;
1048
- if (this.az_slide.position > 1) {
1049
- this.az_slide.position = 0.0;
1050
- }
1051
- this.board.update();
1370
+ this.timeoutAzimuth = setTimeout(function () {
1371
+ this.animateAzimuth();
1372
+ }.bind(this), 200);
1373
+ },
1052
1374
 
1053
- this.timeoutAzimuth = setTimeout(function () {
1054
- this.animateAzimuth();
1055
- }.bind(this), 200);
1056
- },
1375
+ /**
1376
+ *
1377
+ */
1378
+ stopAzimuth: function () {
1379
+ clearTimeout(this.timeoutAzimuth);
1380
+ this.timeoutAzimuth = null;
1381
+ },
1057
1382
 
1058
- /**
1059
- *
1060
- */
1061
- stopAzimuth: function () {
1062
- clearTimeout(this.timeoutAzimuth);
1063
- this.timeoutAzimuth = null;
1064
- },
1383
+ /**
1384
+ * Check if vertical dragging is enabled and which action is needed.
1385
+ * Default is shiftKey.
1386
+ *
1387
+ * @returns Boolean
1388
+ * @private
1389
+ */
1390
+ isVerticalDrag: function () {
1391
+ var b = this.board,
1392
+ key;
1393
+ if (!Type.evaluate(this.visProp.verticaldrag.enabled)) {
1394
+ return false;
1395
+ }
1396
+ key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key';
1397
+ return b[key];
1398
+ },
1065
1399
 
1066
- /**
1067
- * Check if vertical dragging is enabled and which action is needed.
1068
- * Default is shiftKey.
1069
- *
1070
- * @returns Boolean
1071
- * @private
1072
- */
1073
- isVerticalDrag: function () {
1074
- var b = this.board,
1075
- key;
1076
- if (!Type.evaluate(this.visProp.verticaldrag.enabled)) {
1077
- return false;
1078
- }
1079
- key = '_' + Type.evaluate(this.visProp.verticaldrag.key) + 'Key';
1080
- return b[key];
1081
- },
1400
+ /**
1401
+ * Sets camera view to the given values.
1402
+ *
1403
+ * @param {Number} az Value of azimuth.
1404
+ * @param {Number} el Value of elevation.
1405
+ * @param {Number} [r] Value of radius.
1406
+ *
1407
+ * @returns {Object} Reference to the view.
1408
+ */
1409
+ setView: function (az, el, r) {
1410
+ r = r || this.r;
1082
1411
 
1083
- /**
1084
- * Sets camera view to the given values.
1085
- *
1086
- * @param {Number} az Value of azimuth.
1087
- * @param {Number} el Value of elevation.
1088
- * @param {Number} [r] Value of radius.
1089
- *
1090
- * @returns {Object} Reference to the view.
1091
- */
1092
- setView: function (az, el, r) {
1093
- r = r || this.r;
1412
+ this.az_slide.setValue(az);
1413
+ this.el_slide.setValue(el);
1414
+ this.r = r;
1415
+ this.board.update();
1094
1416
 
1095
- this.az_slide.setValue(az);
1096
- this.el_slide.setValue(el);
1097
- this.r = r;
1098
- this.board.update();
1417
+ return this;
1418
+ },
1099
1419
 
1100
- return this;
1101
- },
1420
+ /**
1421
+ * Changes view to the next view stored in the attribute `values`.
1422
+ *
1423
+ * @see View3D#values
1424
+ *
1425
+ * @returns {Object} Reference to the view.
1426
+ */
1427
+ nextView: function () {
1428
+ var views = Type.evaluate(this.visProp.values),
1429
+ n = this.visProp._currentview;
1102
1430
 
1103
- /**
1104
- * Changes view to the next view stored in the attribute `values`.
1105
- *
1106
- * @see View3D#values
1107
- *
1108
- * @returns {Object} Reference to the view.
1109
- */
1110
- nextView: function () {
1111
- var views = Type.evaluate(this.visProp.values),
1112
- n = this.visProp._currentview;
1431
+ n = (n + 1) % views.length;
1432
+ this.setCurrentView(n);
1113
1433
 
1114
- n = (n + 1) % views.length;
1115
- this.setCurrentView(n);
1434
+ return this;
1435
+ },
1116
1436
 
1117
- return this;
1118
- },
1437
+ /**
1438
+ * Changes view to the previous view stored in the attribute `values`.
1439
+ *
1440
+ * @see View3D#values
1441
+ *
1442
+ * @returns {Object} Reference to the view.
1443
+ */
1444
+ previousView: function () {
1445
+ var views = Type.evaluate(this.visProp.values),
1446
+ n = this.visProp._currentview;
1119
1447
 
1120
- /**
1121
- * Changes view to the previous view stored in the attribute `values`.
1122
- *
1123
- * @see View3D#values
1124
- *
1125
- * @returns {Object} Reference to the view.
1126
- */
1127
- previousView: function () {
1128
- var views = Type.evaluate(this.visProp.values),
1129
- n = this.visProp._currentview;
1448
+ n = (n + views.length - 1) % views.length;
1449
+ this.setCurrentView(n);
1130
1450
 
1131
- n = (n + views.length - 1) % views.length;
1132
- this.setCurrentView(n);
1451
+ return this;
1452
+ },
1133
1453
 
1134
- return this;
1135
- },
1454
+ /**
1455
+ * Changes view to the determined view stored in the attribute `values`.
1456
+ *
1457
+ * @see View3D#values
1458
+ *
1459
+ * @param {Number} n Index of view in attribute `values`.
1460
+ * @returns {Object} Reference to the view.
1461
+ */
1462
+ setCurrentView: function (n) {
1463
+ var views = Type.evaluate(this.visProp.values);
1136
1464
 
1137
- /**
1138
- * Changes view to the determined view stored in the attribute `values`.
1139
- *
1140
- * @see View3D#values
1141
- *
1142
- * @param {Number} n Index of view in attribute `values`.
1143
- * @returns {Object} Reference to the view.
1144
- */
1145
- setCurrentView: function (n) {
1146
- var views = Type.evaluate(this.visProp.values);
1465
+ if (n < 0 || n >= views.length) {
1466
+ n = ((n % views.length) + views.length) % views.length;
1467
+ }
1147
1468
 
1148
- if (n < 0 || n >= views.length) {
1149
- n = ((n % views.length) + views.length) % views.length;
1150
- }
1469
+ this.setView(views[n][0], views[n][1], views[n][2]);
1470
+ this.visProp._currentview = n;
1151
1471
 
1152
- this.setView(views[n][0], views[n][1], views[n][2]);
1153
- this.visProp._currentview = n;
1472
+ return this;
1473
+ },
1154
1474
 
1475
+ /**
1476
+ * Controls the navigation in az direction using either the keyboard or a pointer.
1477
+ *
1478
+ * @private
1479
+ *
1480
+ * @param {event} evt either the keydown or the pointer event
1481
+ * @returns view
1482
+ */
1483
+ _azEventHandler: function (evt) {
1484
+ var smax = this.az_slide._smax,
1485
+ smin = this.az_slide._smin,
1486
+ speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)),
1487
+ delta = evt.movementX,
1488
+ az = this.az_slide.Value(),
1489
+ el = this.el_slide.Value();
1490
+
1491
+ // Doesn't allow navigation if another moving event is triggered
1492
+ if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1155
1493
  return this;
1156
- },
1494
+ }
1157
1495
 
1158
- /**
1159
- * Controls the navigation in az direction using either the keyboard or a pointer.
1160
- *
1161
- * @private
1162
- *
1163
- * @param {event} evt either the keydown or the pointer event
1164
- * @returns view
1165
- */
1166
- _azEventHandler: function (evt) {
1167
- var smax = this.az_slide._smax,
1168
- smin = this.az_slide._smin,
1169
- speed = (smax - smin) / this.board.canvasWidth * (Type.evaluate(this.visProp.az.pointer.speed)),
1170
- delta = evt.movementX,
1171
- az = this.az_slide.Value(),
1172
- el = this.el_slide.Value();
1173
-
1174
- // Doesn't allow navigation if another moving event is triggered
1175
- if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1176
- return this;
1496
+ // Calculate new az value if keyboard events are triggered
1497
+ // Plus if right-button, minus if left-button
1498
+ if (Type.evaluate(this.visProp.az.keyboard.enabled)) {
1499
+ if (evt.key === 'ArrowRight') {
1500
+ az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
1501
+ } else if (evt.key === 'ArrowLeft') {
1502
+ az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
1177
1503
  }
1504
+ }
1178
1505
 
1179
- // Calculate new az value if keyboard events are triggered
1180
- // Plus if right-button, minus if left-button
1181
- if (Type.evaluate(this.visProp.az.keyboard.enabled)) {
1182
- if (evt.key === 'ArrowRight') {
1183
- az = az + Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
1184
- } else if (evt.key === 'ArrowLeft') {
1185
- az = az - Type.evaluate(this.visProp.az.keyboard.step) * Math.PI / 180;
1186
- }
1187
- }
1506
+ if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && evt.key == null) {
1507
+ az += delta * speed;
1508
+ }
1188
1509
 
1189
- if (Type.evaluate(this.visProp.az.pointer.enabled) && (delta !== 0) && evt.key == null) {
1190
- az += delta * speed;
1510
+ // Project the calculated az value to a usable value in the interval [smin,smax]
1511
+ // Use modulo if continuous is true
1512
+ if (Type.evaluate(this.visProp.az.continuous)) {
1513
+ az = Mat.wrap(az, smin, smax);
1514
+ } else {
1515
+ if (az > 0) {
1516
+ az = Math.min(smax, az);
1517
+ } else if (az < 0) {
1518
+ az = Math.max(smin, az);
1191
1519
  }
1520
+ }
1192
1521
 
1193
- // Project the calculated az value to a usable value in the interval [smin,smax]
1194
- // Use modulo if continuous is true
1195
- if (Type.evaluate(this.visProp.az.continuous)) {
1196
- az = (az + smax) % smax;
1197
- } else {
1198
- if (az > 0) {
1199
- az = Math.min(smax, az);
1200
- } else if (az < 0) {
1201
- az = Math.max(smin, az);
1202
- }
1203
- }
1522
+ this.setView(az, el);
1523
+ return this;
1524
+ },
1204
1525
 
1205
- this.setView(az, el);
1526
+ /**
1527
+ * Controls the navigation in el direction using either the keyboard or a pointer.
1528
+ *
1529
+ * @private
1530
+ *
1531
+ * @param {event} evt either the keydown or the pointer event
1532
+ * @returns view
1533
+ */
1534
+ _elEventHandler: function (evt) {
1535
+ var smax = this.el_slide._smax,
1536
+ smin = this.el_slide._smin,
1537
+ speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed),
1538
+ delta = evt.movementY,
1539
+ az = this.az_slide.Value(),
1540
+ el = this.el_slide.Value();
1541
+
1542
+ // Doesn't allow navigation if another moving event is triggered
1543
+ if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1206
1544
  return this;
1207
- },
1545
+ }
1208
1546
 
1209
- /**
1210
- * Controls the navigation in el direction using either the keyboard or a pointer.
1211
- *
1212
- * @private
1213
- *
1214
- * @param {event} evt either the keydown or the pointer event
1215
- * @returns view
1216
- */
1217
- _elEventHandler: function (evt) {
1218
- var smax = this.el_slide._smax,
1219
- smin = this.el_slide._smin,
1220
- speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.el.pointer.speed),
1221
- delta = evt.movementY,
1222
- az = this.az_slide.Value(),
1223
- el = this.el_slide.Value();
1224
-
1225
- // Doesn't allow navigation if another moving event is triggered
1226
- if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1227
- return this;
1547
+ // Calculate new az value if keyboard events are triggered
1548
+ // Plus if down-button, minus if up-button
1549
+ if (Type.evaluate(this.visProp.el.keyboard.enabled)) {
1550
+ if (evt.key === 'ArrowUp') {
1551
+ el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
1552
+ } else if (evt.key === 'ArrowDown') {
1553
+ el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
1228
1554
  }
1555
+ }
1229
1556
 
1230
- // Calculate new az value if keyboard events are triggered
1231
- // Plus if down-button, minus if up-button
1232
- if (Type.evaluate(this.visProp.el.keyboard.enabled)) {
1233
- if (evt.key === 'ArrowUp') {
1234
- el = el - Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
1235
- } else if (evt.key === 'ArrowDown') {
1236
- el = el + Type.evaluate(this.visProp.el.keyboard.step) * Math.PI / 180;
1237
- }
1238
- }
1557
+ if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && evt.key == null) {
1558
+ el += delta * speed;
1559
+ }
1239
1560
 
1240
- if (Type.evaluate(this.visProp.el.pointer.enabled) && (delta !== 0) && evt.key == null) {
1241
- el += delta * speed;
1561
+ // Project the calculated el value to a usable value in the interval [smin,smax]
1562
+ // Use modulo if continuous is true and the trackball is disabled
1563
+ if (Type.evaluate(this.visProp.el.continuous) && !this.trackballEnabled) {
1564
+ el = Mat.wrap(el, smin, smax);
1565
+ } else {
1566
+ if (el > 0) {
1567
+ el = Math.min(smax, el);
1568
+ } else if (el < 0) {
1569
+ el = Math.max(smin, el);
1242
1570
  }
1571
+ }
1243
1572
 
1244
- // Project the calculated el value to a usable value in the interval [smin,smax]
1245
- // Use modulo if continuous is true
1246
- if (Type.evaluate(this.visProp.el.continuous)) {
1247
- el = (el + smax) % smax;
1248
- } else {
1249
- if (el > 0) {
1250
- el = Math.min(smax, el);
1251
- } else if (el < 0) {
1252
- el = Math.max(smin, el);
1253
- }
1254
- }
1573
+ this.setView(az, el);
1574
+ return this;
1575
+ },
1255
1576
 
1256
- this.setView(az, el);
1257
- return this;
1258
- },
1259
-
1260
- _trackballHandler: function(evt) {
1261
- var pos = this.board.getMousePosition(evt),
1262
- x, y, center;
1263
-
1264
- center = new Coords(Const.COORDS_BY_USER, [this.llftCorner[0] + this.size[0] * 0.5, this.llftCorner[1] + this.size[1] * 0.5], this.board);
1265
- x = pos[0] - center.scrCoords[1];
1266
- y = pos[1] - center.scrCoords[2];
1267
- this._trackball = {
1268
- dx: evt.movementX,
1269
- dy: evt.movementY,
1270
- x: x,
1271
- y: y
1272
- };
1273
- this.board.update();
1577
+ /**
1578
+ * Controls the navigation in bank direction using either the keyboard or a pointer.
1579
+ *
1580
+ * @private
1581
+ *
1582
+ * @param {event} evt either the keydown or the pointer event
1583
+ * @returns view
1584
+ */
1585
+ _bankEventHandler: function (evt) {
1586
+ var smax = this.bank_slide._smax,
1587
+ smin = this.bank_slide._smin,
1588
+ step, speed,
1589
+ delta = evt.deltaY,
1590
+ bank = this.bank_slide.Value();
1591
+
1592
+ // Doesn't allow navigation if another moving event is triggered
1593
+ if (this.board.mode === this.board.BOARD_MODE_DRAG) {
1274
1594
  return this;
1275
- },
1595
+ }
1276
1596
 
1277
- pointerDownHandler: function (evt) {
1278
- var neededButton, neededKey, target;
1597
+ // Calculate new bank value if keyboard events are triggered
1598
+ // Plus if down-button, minus if up-button
1599
+ if (Type.evaluate(this.visProp.bank.keyboard.enabled)) {
1600
+ step = Type.evaluate(this.visProp.bank.keyboard.step) * Math.PI / 180;
1601
+ if (evt.key === '.' || evt.key === '<') {
1602
+ bank -= step;
1603
+ } else if (evt.key === ',' || evt.key === '>') {
1604
+ bank += step;
1605
+ }
1606
+ }
1279
1607
 
1280
- this._hasMoveAz = false;
1281
- this._hasMoveEl = false;
1282
- this._hasMoveTrackball = false;
1608
+ if (Type.evaluate(this.visProp.bank.pointer.enabled) && (delta !== 0) && evt.key == null) {
1609
+ speed = (smax - smin) / this.board.canvasHeight * Type.evaluate(this.visProp.bank.pointer.speed);
1610
+ bank += delta * speed;
1283
1611
 
1284
- if (this.board.mode !== this.board.BOARD_MODE_NONE) {
1285
- return;
1286
- }
1612
+ // prevent the pointer wheel from scrolling the page
1613
+ evt.preventDefault();
1614
+ }
1615
+
1616
+ // Project the calculated bank value to a usable value in the interval [smin,smax]
1617
+ if (Type.evaluate(this.visProp.bank.continuous)) {
1618
+ // in continuous mode, wrap value around slider range
1619
+ bank = Mat.wrap(bank, smin, smax);
1620
+ } else {
1621
+ // in non-continuous mode, clamp value to slider range
1622
+ bank = Mat.clamp(bank, smin, smax);
1623
+ }
1624
+
1625
+ this.bank_slide.setValue(bank);
1626
+ this.board.update();
1627
+ return this;
1628
+ },
1629
+
1630
+ _trackballHandler: function (evt) {
1631
+ var pos = this.board.getMousePosition(evt),
1632
+ x, y, center;
1633
+
1634
+ center = new Coords(Const.COORDS_BY_USER, [this.llftCorner[0] + this.size[0] * 0.5, this.llftCorner[1] + this.size[1] * 0.5], this.board);
1635
+ x = pos[0] - center.scrCoords[1];
1636
+ y = pos[1] - center.scrCoords[2];
1637
+ this._trackball = {
1638
+ dx: evt.movementX,
1639
+ dy: -evt.movementY,
1640
+ x: x,
1641
+ y: -y
1642
+ };
1643
+ this.board.update();
1644
+ return this;
1645
+ },
1646
+
1647
+ pointerDownHandler: function (evt) {
1648
+ var neededButton, neededKey, target;
1649
+
1650
+ this._hasMoveAz = false;
1651
+ this._hasMoveEl = false;
1652
+ this._hasMoveBank = false;
1653
+ this._hasMoveTrackball = false;
1654
+
1655
+ if (this.board.mode !== this.board.BOARD_MODE_NONE) {
1656
+ return;
1657
+ }
1287
1658
 
1288
- if (Type.evaluate(this.visProp.trackball.enabled)) {
1289
- neededButton = Type.evaluate(this.visProp.trackball.button);
1290
- neededKey = Type.evaluate(this.visProp.trackball.key);
1659
+ if (Type.evaluate(this.visProp.trackball.enabled)) {
1660
+ neededButton = Type.evaluate(this.visProp.trackball.button);
1661
+ neededKey = Type.evaluate(this.visProp.trackball.key);
1291
1662
 
1292
- // Move events for virtual trackball
1663
+ // Move events for virtual trackball
1664
+ if (
1665
+ (neededButton === -1 || neededButton === evt.button) &&
1666
+ (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1667
+ ) {
1668
+ // If outside is true then the event listener is bound to the document, otherwise to the div
1669
+ target = (Type.evaluate(this.visProp.trackball.outside)) ? document : this.board.containerObj;
1670
+ Env.addEvent(target, 'pointermove', this._trackballHandler, this);
1671
+ this._hasMoveTrackball = true;
1672
+ }
1673
+ } else {
1674
+ if (Type.evaluate(this.visProp.az.pointer.enabled)) {
1675
+ neededButton = Type.evaluate(this.visProp.az.pointer.button);
1676
+ neededKey = Type.evaluate(this.visProp.az.pointer.key);
1677
+
1678
+ // Move events for azimuth
1293
1679
  if (
1294
1680
  (neededButton === -1 || neededButton === evt.button) &&
1295
1681
  (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1296
1682
  ) {
1297
1683
  // If outside is true then the event listener is bound to the document, otherwise to the div
1298
- target = (Type.evaluate(this.visProp.trackball.outside)) ? document : this.board.containerObj;
1299
- Env.addEvent(target, 'pointermove', this._trackballHandler, this);
1300
- this._hasMoveTrackball = true;
1301
- }
1302
- } else {
1303
- if (Type.evaluate(this.visProp.az.pointer.enabled)) {
1304
- neededButton = Type.evaluate(this.visProp.az.pointer.button);
1305
- neededKey = Type.evaluate(this.visProp.az.pointer.key);
1306
-
1307
- // Move events for azimuth
1308
- if (
1309
- (neededButton === -1 || neededButton === evt.button) &&
1310
- (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1311
- ) {
1312
- // If outside is true then the event listener is bound to the document, otherwise to the div
1313
- target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1314
- Env.addEvent(target, 'pointermove', this._azEventHandler, this);
1315
- this._hasMoveAz = true;
1316
- }
1684
+ target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1685
+ Env.addEvent(target, 'pointermove', this._azEventHandler, this);
1686
+ this._hasMoveAz = true;
1317
1687
  }
1688
+ }
1689
+
1690
+ if (Type.evaluate(this.visProp.el.pointer.enabled)) {
1691
+ neededButton = Type.evaluate(this.visProp.el.pointer.button);
1692
+ neededKey = Type.evaluate(this.visProp.el.pointer.key);
1318
1693
 
1319
- if (Type.evaluate(this.visProp.el.pointer.enabled)) {
1320
- neededButton = Type.evaluate(this.visProp.el.pointer.button);
1321
- neededKey = Type.evaluate(this.visProp.el.pointer.key);
1322
-
1323
- // Events for elevation
1324
- if (
1325
- (neededButton === -1 || neededButton === evt.button) &&
1326
- (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1327
- ) {
1328
- // If outside is true then the event listener is bound to the document, otherwise to the div
1329
- target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj;
1330
- Env.addEvent(target, 'pointermove', this._elEventHandler, this);
1331
- this._hasMoveEl = true;
1332
- }
1694
+ // Events for elevation
1695
+ if (
1696
+ (neededButton === -1 || neededButton === evt.button) &&
1697
+ (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1698
+ ) {
1699
+ // If outside is true then the event listener is bound to the document, otherwise to the div
1700
+ target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj;
1701
+ Env.addEvent(target, 'pointermove', this._elEventHandler, this);
1702
+ this._hasMoveEl = true;
1333
1703
  }
1334
1704
  }
1335
- Env.addEvent(document, 'pointerup', this.pointerUpHandler, this);
1336
- },
1337
-
1338
- pointerUpHandler: function(evt) {
1339
- var target;
1340
- if (this._hasMoveAz) {
1341
- target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1342
- Env.removeEvent(target, 'pointermove', this._azEventHandler, this);
1343
- this._hasMoveAz = false;
1344
- }
1345
- if (this._hasMoveEl) {
1346
- target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj;
1347
- Env.removeEvent(target, 'pointermove', this._elEventHandler, this);
1348
- this._hasMoveEl = false;
1349
- }
1350
- if (this._hasMoveTrackball) {
1351
- target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1352
- Env.removeEvent(target, 'pointermove', this._trackballHandler, this);
1353
- this._hasMoveTrackball = false;
1705
+
1706
+ if (Type.evaluate(this.visProp.bank.pointer.enabled)) {
1707
+ neededButton = Type.evaluate(this.visProp.bank.pointer.button);
1708
+ neededKey = Type.evaluate(this.visProp.bank.pointer.key);
1709
+
1710
+ // Events for bank
1711
+ if (
1712
+ (neededButton === -1 || neededButton === evt.button) &&
1713
+ (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && evt.shiftKey) || (neededKey.indexOf('ctrl') > -1 && evt.ctrlKey))
1714
+ ) {
1715
+ // If `outside` is true, we bind the event listener to
1716
+ // the document. otherwise, we bind it to the div. we
1717
+ // register the event listener as active so it can
1718
+ // prevent the pointer wheel from scrolling the page
1719
+ target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj;
1720
+ Env.addEvent(target, 'wheel', this._bankEventHandler, this, { passive: false });
1721
+ this._hasMoveBank = true;
1722
+ }
1354
1723
  }
1355
- Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this);
1356
1724
  }
1357
- });
1725
+ Env.addEvent(document, 'pointerup', this.pointerUpHandler, this);
1726
+ },
1727
+
1728
+ pointerUpHandler: function (evt) {
1729
+ var target;
1730
+ if (this._hasMoveAz) {
1731
+ target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1732
+ Env.removeEvent(target, 'pointermove', this._azEventHandler, this);
1733
+ this._hasMoveAz = false;
1734
+ }
1735
+ if (this._hasMoveEl) {
1736
+ target = (Type.evaluate(this.visProp.el.pointer.outside)) ? document : this.board.containerObj;
1737
+ Env.removeEvent(target, 'pointermove', this._elEventHandler, this);
1738
+ this._hasMoveEl = false;
1739
+ }
1740
+ if (this._hasMoveBank) {
1741
+ target = (Type.evaluate(this.visProp.bank.pointer.outside)) ? document : this.board.containerObj;
1742
+ Env.removeEvent(target, 'wheel', this._bankEventHandler, this);
1743
+ this._hasMoveBank = false;
1744
+ }
1745
+ if (this._hasMoveTrackball) {
1746
+ target = (Type.evaluate(this.visProp.az.pointer.outside)) ? document : this.board.containerObj;
1747
+ Env.removeEvent(target, 'pointermove', this._trackballHandler, this);
1748
+ this._hasMoveTrackball = false;
1749
+ }
1750
+ Env.removeEvent(document, 'pointerup', this.pointerUpHandler, this);
1751
+ }
1752
+ });
1358
1753
 
1359
1754
  /**
1360
1755
  * @class This element creates a 3D view.
@@ -1430,7 +1825,7 @@ JXG.extend(
1430
1825
  *
1431
1826
  */
1432
1827
  JXG.createView3D = function (board, parents, attributes) {
1433
- var view, attr, attr_az, attr_el,
1828
+ var view, attr, attr_az, attr_el, attr_bank,
1434
1829
  x, y, w, h,
1435
1830
  coords = parents[0], // llft corner
1436
1831
  size = parents[1]; // [w, h]
@@ -1450,6 +1845,9 @@ JXG.createView3D = function (board, parents, attributes) {
1450
1845
  attr_el = Type.copyAttributes(attributes, board.options, 'view3d', 'el', 'slider');
1451
1846
  attr_el.name = 'el';
1452
1847
 
1848
+ attr_bank = Type.copyAttributes(attributes, board.options, 'view3d', 'bank', 'slider');
1849
+ attr_bank.name = 'bank';
1850
+
1453
1851
  /**
1454
1852
  * Slider to adapt azimuth angle
1455
1853
  * @name JXG.View3D#az_slide
@@ -1468,6 +1866,8 @@ JXG.createView3D = function (board, parents, attributes) {
1468
1866
  ],
1469
1867
  attr_az
1470
1868
  );
1869
+ // view.az_slide.inherits.push(view);
1870
+ view.inherits.push(view.az_slide);
1471
1871
 
1472
1872
  /**
1473
1873
  * Slider to adapt elevation angle
@@ -1487,6 +1887,28 @@ JXG.createView3D = function (board, parents, attributes) {
1487
1887
  ],
1488
1888
  attr_el
1489
1889
  );
1890
+ view.inherits.push(view.el_slide);
1891
+
1892
+ /**
1893
+ * Slider to adjust bank angle
1894
+ *
1895
+ * @name JXG.View3D#bank_slide
1896
+ * @type {Slider}
1897
+ */
1898
+ view.bank_slide = board.create(
1899
+ 'slider',
1900
+ [
1901
+ [x - 1, y + h + 2],
1902
+ [x + w + 1, y + h + 2],
1903
+ [
1904
+ Type.evaluate(attr_bank.min),
1905
+ Type.evaluate(attr_bank.start),
1906
+ Type.evaluate(attr_bank.max)
1907
+ ]
1908
+ ],
1909
+ attr_bank
1910
+ );
1911
+ view.inherits.push(view.bank_slide);
1490
1912
 
1491
1913
  view.board.highlightInfobox = function (x, y, el) {
1492
1914
  var d, i, c3d, foot,
@@ -1536,6 +1958,7 @@ JXG.createView3D = function (board, parents, attributes) {
1536
1958
  }
1537
1959
  };
1538
1960
 
1961
+
1539
1962
  // Hack needed to enable addEvent for view3D:
1540
1963
  view.BOARD_MODE_NONE = 0x0000;
1541
1964
 
@@ -1568,6 +1991,13 @@ JXG.createView3D = function (board, parents, attributes) {
1568
1991
  catchEvt = true;
1569
1992
  }
1570
1993
  }
1994
+ if (Type.evaluate(view.visProp.bank.keyboard.enabled) && (event.key === ',' || event.key === '<' || event.key === '.' || event.key === '>')) {
1995
+ neededKey = Type.evaluate(view.visProp.bank.keyboard.key);
1996
+ if (neededKey === 'none' || (neededKey.indexOf('shift') > -1 && event.shiftKey) || (neededKey.indexOf('ctrl') > -1 && event.ctrlKey)) {
1997
+ view._bankEventHandler(event);
1998
+ catchEvt = true;
1999
+ }
2000
+ }
1571
2001
  if (event.key === 'PageUp') {
1572
2002
  view.nextView();
1573
2003
  catchEvt = true;
@@ -1587,6 +2017,13 @@ JXG.createView3D = function (board, parents, attributes) {
1587
2017
  // Add events for the pointer navigation
1588
2018
  Env.addEvent(board.containerObj, 'pointerdown', view.pointerDownHandler, view);
1589
2019
 
2020
+ // Initialize view rotation matrix
2021
+ view.getAnglesFromSliders();
2022
+ view.matrix3DRot = view.getRotationFromAngles();
2023
+
2024
+ // override angle slider bounds when trackball navigation is enabled
2025
+ view.updateAngleSliderBounds();
2026
+
1590
2027
  view.board.update();
1591
2028
 
1592
2029
  return view;