p5.tree 0.0.1

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.
@@ -0,0 +1,2751 @@
1
+ import p5 from 'p5';
2
+
3
+ /**
4
+ * @file Adds Tree rendering functions to the p5 prototype.
5
+ * @version 0.0.1
6
+ * @author JP Charalambos
7
+ * @license GPL-3.0-only
8
+ *
9
+ * @description
10
+ * A p5.js WEBGL addon for shader development and space transformations.
11
+ *
12
+ * Camera path recording/playback section.
13
+ *
14
+ * Requires WEBGL (p5.Camera).
15
+ *
16
+ * Camera API:
17
+ * camera.path : p5.Camera[]
18
+ * camera.addPath(...)
19
+ * camera.playPath(...)
20
+ * camera.stopPath(...)
21
+ * camera.resetPath(...)
22
+ * camera.seekPath(...)
23
+ *
24
+ * p5 wrappers (same names) forward to current active camera.
25
+ *
26
+ * Uses p5 lifecicle predraw hook to tick playback automatically.
27
+ *
28
+ * Projection safety:
29
+ * p5.Camera.slerp requires that all cameras use the same projection.
30
+ * We enforce this by comparing projMatrix.mat4 signatures.
31
+ */
32
+
33
+
34
+ p5.registerAddon((p5, fn, lifecycles) => {
35
+ // --- namespace (module shelf) ---
36
+ p5.Tree ||= {};
37
+
38
+ const CONST = value => ({ value, writable: false, enumerable: true, configurable: false });
39
+
40
+ Object.defineProperties(p5.Tree, {
41
+ VERSION: CONST('0.0.1'),
42
+
43
+ NONE: CONST(0),
44
+
45
+ // Spaces
46
+ WORLD: CONST('WORLD'),
47
+ EYE: CONST('EYE'),
48
+ NDC: CONST('NDC'),
49
+ SCREEN: CONST('SCREEN'),
50
+ MODEL: CONST('MODEL'),
51
+
52
+ // Points and vectors
53
+ ORIGIN: CONST(Object.freeze([0, 0, 0])),
54
+
55
+ i: CONST(Object.freeze([1, 0, 0])),
56
+ j: CONST(Object.freeze([0, 1, 0])),
57
+ k: CONST(Object.freeze([0, 0, 1])),
58
+
59
+ _i: CONST(Object.freeze([-1, 0, 0])),
60
+ _j: CONST(Object.freeze([0, -1, 0])),
61
+ _k: CONST(Object.freeze([0, 0, -1])),
62
+
63
+ // Axes / grid bits & styles
64
+ X: CONST(1 << 0),
65
+ _X: CONST(1 << 1),
66
+ Y: CONST(1 << 2),
67
+ _Y: CONST(1 << 3),
68
+ Z: CONST(1 << 4),
69
+ _Z: CONST(1 << 5),
70
+ LABELS: CONST(1 << 6),
71
+
72
+ DOTS: CONST(0),
73
+ SOLID: CONST(1),
74
+
75
+ // bullsEye
76
+ CIRCLE: CONST(0),
77
+ SQUARE: CONST(1),
78
+
79
+ // View frustum bits
80
+ NEAR: CONST(1 << 0),
81
+ FAR: CONST(1 << 1),
82
+ LEFT: CONST(1 << 2),
83
+ RIGHT: CONST(1 << 3),
84
+ BOTTOM: CONST(1 << 4),
85
+ TOP: CONST(1 << 5),
86
+ BODY: CONST(1 << 6),
87
+ APEX: CONST(1 << 7),
88
+
89
+ // Visibility
90
+ INVISIBLE: CONST(0),
91
+ VISIBLE: CONST(1),
92
+ SEMIVISIBLE: CONST(2),
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Matrix queries (p5.treegl -> p5.tree, p5-v2)
97
+ // Rely on p5-v2, minimal safeties, cache-friendly.
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * @private
102
+ * Returns the WEBGL renderer or undefined.
103
+ * @param {p5} pInst
104
+ * @returns {p5.RendererGL|undefined}
105
+ */
106
+ const _rendererGL = function (pInst) {
107
+ const r = pInst._renderer;
108
+ return r instanceof p5.RendererGL ? r : undefined;
109
+ };
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // p5.Matrix operations (immutable)
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * @private
117
+ * Returns the inverse of a matrix (immutable).
118
+ * p5-v2: invert(a) inverts 'a' into 'this' (gl-matrix style).
119
+ * @param {p5.Matrix} matrix
120
+ * @returns {p5.Matrix}
121
+ */
122
+ const _invert = function (matrix) {
123
+ const out = matrix.clone();
124
+ out.invert(matrix);
125
+ return out;
126
+ };
127
+
128
+ /**
129
+ * @private
130
+ * Returns the transpose of a matrix (immutable).
131
+ * Fast-path for mat4 / mat3 to match treegl semantics.
132
+ * @param {p5.Matrix} matrix
133
+ * @returns {p5.Matrix}
134
+ */
135
+ const _transpose = function (matrix) {
136
+ const m4 = matrix.mat4;
137
+ if (m4) {
138
+ return new p5.Matrix([
139
+ m4[0], m4[4], m4[8], m4[12],
140
+ m4[1], m4[5], m4[9], m4[13],
141
+ m4[2], m4[6], m4[10], m4[14],
142
+ m4[3], m4[7], m4[11], m4[15]
143
+ ]);
144
+ }
145
+ const m3 = matrix.mat3;
146
+ if (m3) {
147
+ return new p5.Matrix([
148
+ m3[0], m3[3], m3[6],
149
+ m3[1], m3[4], m3[7],
150
+ m3[2], m3[5], m3[8]
151
+ ]);
152
+ }
153
+ };
154
+
155
+ p5.Matrix.prototype.mult4 = function (vector) {
156
+ return new p5.Vector(...this._mult4([vector.x, vector.y, vector.z, 1]));
157
+ };
158
+
159
+ p5.Matrix.prototype._mult4 = function (vec4) {
160
+ if (this.mat4 === undefined) {
161
+ console.error('_mult4 only works with mat4');
162
+ return;
163
+ }
164
+ return [
165
+ this.mat4[0] * vec4[0] + this.mat4[4] * vec4[1] + this.mat4[8] * vec4[2] + this.mat4[12] * vec4[3],
166
+ this.mat4[1] * vec4[0] + this.mat4[5] * vec4[1] + this.mat4[9] * vec4[2] + this.mat4[13] * vec4[3],
167
+ this.mat4[2] * vec4[0] + this.mat4[6] * vec4[1] + this.mat4[10] * vec4[2] + this.mat4[14] * vec4[3],
168
+ this.mat4[3] * vec4[0] + this.mat4[7] * vec4[1] + this.mat4[11] * vec4[2] + this.mat4[15] * vec4[3]
169
+ ];
170
+ };
171
+
172
+ /**
173
+ * Returns the transpose of a matrix (immutable).
174
+ * @param {p5.Matrix} matrix
175
+ * @returns {p5.Matrix}
176
+ */
177
+ fn.tMatrix = function (matrix) {
178
+ return _transpose(matrix);
179
+ };
180
+
181
+ /**
182
+ * Returns the inverse of a matrix.
183
+ * @param {p5.Matrix} matrix
184
+ * @returns {p5.Matrix}
185
+ */
186
+ fn.iMatrix = function (matrix) {
187
+ return _invert(matrix);
188
+ };
189
+
190
+ /**
191
+ * Returns A * B without mutating A (immutable).
192
+ * @param {p5.Matrix} a
193
+ * @param {p5.Matrix} b
194
+ * @returns {p5.Matrix}
195
+ */
196
+ fn.axbMatrix = function (a, b) {
197
+ return a.clone().mult(b);
198
+ };
199
+
200
+ /**
201
+ * Creates a new p5.Matrix.
202
+ * (Wrapper for `new p5.Matrix(...args)`.)
203
+ *
204
+ * - `createMatrix()` → identity 4×4
205
+ * - `createMatrix(n)` → identity n×n (typically 3 or 4)
206
+ * - `createMatrix(coeffs)` → matrix from coefficients (length 9 or 16)
207
+ *
208
+ * @param {...(number|Array<number>)} [args] Arguments forwarded to the p5.Matrix constructor.
209
+ * @returns {p5.Matrix}
210
+ */
211
+ fn.createMatrix = (...args) => new p5.Matrix(...args);
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Matrix queries (immutable, cache-friendly)
215
+ // ---------------------------------------------------------------------------
216
+
217
+ /**
218
+ * Returns the current projection matrix (immutable copy).
219
+ * @returns {p5.Matrix}
220
+ */
221
+ p5.RendererGL.prototype.pMatrix = function () {
222
+ return this.states.uPMatrix.clone();
223
+ };
224
+
225
+ /**
226
+ * Returns the current projection matrix (immutable copy).
227
+ * Requires WEBGL.
228
+ * @returns {p5.Matrix}
229
+ */
230
+ fn.pMatrix = function () {
231
+ return _rendererGL(this).pMatrix();
232
+ };
233
+
234
+ /**
235
+ * Returns the current model matrix (immutable copy).
236
+ * @returns {p5.Matrix}
237
+ */
238
+ p5.RendererGL.prototype.mMatrix = function () {
239
+ return this.states.uModelMatrix.clone();
240
+ };
241
+
242
+ /**
243
+ * Returns the current model matrix (immutable copy).
244
+ * Requires WEBGL.
245
+ * @returns {p5.Matrix}
246
+ */
247
+ fn.mMatrix = function () {
248
+ return _rendererGL(this).mMatrix();
249
+ };
250
+
251
+ /**
252
+ * Returns the view matrix (world -> camera) for this camera (immutable copy).
253
+ * @returns {p5.Matrix}
254
+ */
255
+ p5.Camera.prototype.vMatrix = function () {
256
+ return this.cameraMatrix.clone();
257
+ };
258
+
259
+ /**
260
+ * Returns the eye matrix (camera -> world) for this camera (immutable).
261
+ * @returns {p5.Matrix}
262
+ */
263
+ p5.Camera.prototype.eMatrix = function () {
264
+ return _invert(this.cameraMatrix);
265
+ };
266
+
267
+ /**
268
+ * Returns the current view matrix (world -> camera) (immutable copy).
269
+ * Prefers the renderer cached view matrix when available.
270
+ * @returns {p5.Matrix}
271
+ */
272
+ p5.RendererGL.prototype.vMatrix = function () {
273
+ return (this.states.uViewMatrix || this.states.curCamera.cameraMatrix).clone();
274
+ };
275
+
276
+ /**
277
+ * Returns the current view matrix (world -> camera) (immutable copy).
278
+ * Requires WEBGL.
279
+ * @returns {p5.Matrix}
280
+ */
281
+ fn.vMatrix = function () {
282
+ return _rendererGL(this).vMatrix();
283
+ };
284
+
285
+ /**
286
+ * Returns the current eye matrix (camera -> world) (immutable).
287
+ * @returns {p5.Matrix}
288
+ */
289
+ p5.RendererGL.prototype.eMatrix = function () {
290
+ return _invert(this.states.uViewMatrix || this.states.curCamera.cameraMatrix);
291
+ };
292
+
293
+ /**
294
+ * Returns the current eye matrix (camera -> world) (immutable).
295
+ * Requires WEBGL.
296
+ * @returns {p5.Matrix}
297
+ */
298
+ fn.eMatrix = function () {
299
+ return _rendererGL(this).eMatrix();
300
+ };
301
+
302
+ /**
303
+ * lMatrix({ from, to }):
304
+ * Location transform (mat4) mapping points from `from` space to `to` space.
305
+ * treegl semantics: to^-1 * from.
306
+ * @param {object} [opts]
307
+ * @param {p5.Matrix} [opts.from=new p5.Matrix()] Source frame matrix.
308
+ * @param {p5.Matrix} [opts.to=this.eMatrix()] Target frame matrix.
309
+ * @returns {p5.Matrix}
310
+ */
311
+ p5.RendererGL.prototype.lMatrix = function ({
312
+ from = new p5.Matrix(4),
313
+ to = this.eMatrix()
314
+ } = {}) {
315
+ return _invert(to).mult(from);
316
+ };
317
+
318
+ /**
319
+ * lMatrix({ from, to }):
320
+ * Location transform (mat4) mapping points from `from` space to `to` space.
321
+ * Requires WEBGL.
322
+ * @param {object} [opts]
323
+ * @param {p5.Matrix} [opts.from]
324
+ * @param {p5.Matrix} [opts.to]
325
+ * @returns {p5.Matrix}
326
+ */
327
+ fn.lMatrix = function (opts = {}) {
328
+ return _rendererGL(this).lMatrix(opts);
329
+ };
330
+
331
+ /**
332
+ * dMatrix({ from, to, matrix }):
333
+ * Direction transform (mat3) mapping vectors from `from` space to `to` space.
334
+ * Translation ignored. treegl semantics: linear_part(from^-1 * to).
335
+ * If `matrix` (mat4) is provided, uses linear_part(matrix).
336
+ * @param {object} [opts]
337
+ * @param {p5.Matrix} [opts.from=new p5.Matrix()] Source frame matrix.
338
+ * @param {p5.Matrix} [opts.to=this.eMatrix()] Target frame matrix.
339
+ * @param {p5.Matrix} [opts.matrix] Precomputed mat4 override.
340
+ * @returns {p5.Matrix} mat3
341
+ */
342
+ p5.RendererGL.prototype.dMatrix = function ({
343
+ from = new p5.Matrix(4),
344
+ to = this.eMatrix(),
345
+ matrix
346
+ } = {}) {
347
+ const m = (matrix || _invert(from).mult(to));
348
+ const a = m.mat4 || m.matrix; // v2: mat4 getter if 4x4, else fallback
349
+ // Note: this is the same "mat4 -> mat3 transpose" as treegl (baked into indices)
350
+ return new p5.Matrix([
351
+ a[0], a[4], a[8],
352
+ a[1], a[5], a[9],
353
+ a[2], a[6], a[10]
354
+ ]);
355
+ };
356
+
357
+ /**
358
+ * dMatrix({ from, to, matrix }):
359
+ * Direction transform (mat3) mapping vectors from `from` space to `to` space.
360
+ * Requires WEBGL.
361
+ * @param {object} [opts]
362
+ * @param {p5.Matrix} [opts.from]
363
+ * @param {p5.Matrix} [opts.to]
364
+ * @param {p5.Matrix} [opts.matrix]
365
+ * @returns {p5.Matrix} mat3
366
+ */
367
+ fn.dMatrix = function (opts = {}) {
368
+ return _rendererGL(this).dMatrix(opts);
369
+ };
370
+
371
+ /**
372
+ * mvMatrix({ vMatrix, mMatrix }):
373
+ * ModelView matrix (mat4) = M * V (p5-v2 convention).
374
+ * @param {object} [opts]
375
+ * @param {p5.Matrix} [opts.vMatrix=this.vMatrix()] View matrix.
376
+ * @param {p5.Matrix} [opts.mMatrix=this.mMatrix()] Model matrix.
377
+ * @returns {p5.Matrix}
378
+ */
379
+ p5.RendererGL.prototype.mvMatrix = function ({
380
+ vMatrix = this.vMatrix(),
381
+ mMatrix
382
+ } = {}) {
383
+ return (mMatrix || this.states.uModelMatrix).clone().mult(vMatrix);
384
+ };
385
+
386
+ /**
387
+ * mvMatrix({ vMatrix, mMatrix }):
388
+ * ModelView matrix (mat4) = M * V (p5-v2 convention).
389
+ * Requires WEBGL.
390
+ * @param {object} [opts]
391
+ * @param {p5.Matrix} [opts.vMatrix]
392
+ * @param {p5.Matrix} [opts.mMatrix]
393
+ * @returns {p5.Matrix}
394
+ */
395
+ fn.mvMatrix = function (opts = {}) {
396
+ return _rendererGL(this).mvMatrix(opts);
397
+ };
398
+
399
+ /**
400
+ * nMatrix({ vMatrix, mMatrix, mvMatrix }):
401
+ * Normal matrix (mat3) = inverseTranspose(linear_part(MV)).
402
+ * @param {object} [opts]
403
+ * @param {p5.Matrix} [opts.vMatrix] Optional view matrix.
404
+ * @param {p5.Matrix} [opts.mMatrix] Optional model matrix.
405
+ * @param {p5.Matrix} [opts.mvMatrix=this.mvMatrix({ mMatrix, vMatrix })] Optional MV matrix override.
406
+ * @returns {p5.Matrix} mat3
407
+ */
408
+ p5.RendererGL.prototype.nMatrix = function ({
409
+ vMatrix,
410
+ mMatrix,
411
+ mvMatrix = this.mvMatrix({ mMatrix, vMatrix })
412
+ } = {}) {
413
+ return _transpose(_invert(mvMatrix.createSubMatrix3x3()));
414
+ };
415
+
416
+ /**
417
+ * nMatrix({ vMatrix, mMatrix, mvMatrix }):
418
+ * Normal matrix (mat3) = inverseTranspose(linear_part(MV)).
419
+ * Requires WEBGL.
420
+ * @param {object} [opts]
421
+ * @param {p5.Matrix} [opts.vMatrix]
422
+ * @param {p5.Matrix} [opts.mMatrix]
423
+ * @param {p5.Matrix} [opts.mvMatrix]
424
+ * @returns {p5.Matrix} mat3
425
+ */
426
+ fn.nMatrix = function (opts = {}) {
427
+ return _rendererGL(this).nMatrix(opts);
428
+ };
429
+
430
+ /**
431
+ * pmvMatrix({ pMatrix, vMatrix, mMatrix, mvMatrix }):
432
+ * PMV (mat4) = M * V * P (p5-v2 convention).
433
+ * @param {object} [opts]
434
+ * @param {p5.Matrix} [opts.pMatrix=this.pMatrix()] Projection matrix.
435
+ * @param {p5.Matrix} [opts.vMatrix] Optional view matrix (used if mvMatrix is computed).
436
+ * @param {p5.Matrix} [opts.mMatrix] Optional model matrix (used if mvMatrix is computed).
437
+ * @param {p5.Matrix} [opts.mvMatrix=this.mvMatrix({ mMatrix, vMatrix })] Optional MV matrix override.
438
+ * @returns {p5.Matrix}
439
+ */
440
+ p5.RendererGL.prototype.pmvMatrix = function ({
441
+ pMatrix = this.pMatrix(),
442
+ vMatrix,
443
+ mMatrix,
444
+ mvMatrix
445
+ } = {}) {
446
+ return (mvMatrix ? mvMatrix.clone() : this.mvMatrix({ mMatrix, vMatrix })).mult(pMatrix);
447
+ };
448
+
449
+ /**
450
+ * pmvMatrix({ pMatrix, vMatrix, mMatrix, mvMatrix }):
451
+ * PMV (mat4) = M * V * P (p5-v2 convention).
452
+ * Requires WEBGL.
453
+ * @param {object} [opts]
454
+ * @param {p5.Matrix} [opts.pMatrix]
455
+ * @param {p5.Matrix} [opts.vMatrix]
456
+ * @param {p5.Matrix} [opts.mMatrix]
457
+ * @param {p5.Matrix} [opts.mvMatrix]
458
+ * @returns {p5.Matrix}
459
+ */
460
+ fn.pmvMatrix = function (opts = {}) {
461
+ return _rendererGL(this).pmvMatrix(opts);
462
+ };
463
+
464
+ /**
465
+ * pvMatrix({ pMatrix, vMatrix }):
466
+ * PV (mat4) = V * P (p5-v2 convention).
467
+ * @param {object} [opts]
468
+ * @param {p5.Matrix} [opts.pMatrix=this.pMatrix()] Projection matrix.
469
+ * @param {p5.Matrix} [opts.vMatrix=this.vMatrix()] View matrix.
470
+ * @returns {p5.Matrix}
471
+ */
472
+ p5.RendererGL.prototype.pvMatrix = function ({
473
+ pMatrix = this.pMatrix(),
474
+ vMatrix
475
+ } = {}) {
476
+ return (vMatrix || (this.states.uViewMatrix || this.states.curCamera.cameraMatrix)).clone().mult(pMatrix);
477
+ };
478
+
479
+ /**
480
+ * pvMatrix({ pMatrix, vMatrix }):
481
+ * PV (mat4) = V * P (p5-v2 convention).
482
+ * Requires WEBGL.
483
+ * @param {object} [opts]
484
+ * @param {p5.Matrix} [opts.pMatrix]
485
+ * @param {p5.Matrix} [opts.vMatrix]
486
+ * @returns {p5.Matrix}
487
+ */
488
+ fn.pvMatrix = function (opts = {}) {
489
+ return _rendererGL(this).pvMatrix(opts);
490
+ };
491
+
492
+ /**
493
+ * ipvMatrix({ pMatrix, vMatrix, pvMatrix }):
494
+ * Inverse(PV) (mat4).
495
+ * @param {object} [opts]
496
+ * @param {p5.Matrix} [opts.pMatrix] Optional projection matrix (used if pvMatrix is computed).
497
+ * @param {p5.Matrix} [opts.vMatrix] Optional view matrix (used if pvMatrix is computed).
498
+ * @param {p5.Matrix} [opts.pvMatrix=this.pvMatrix({ pMatrix, vMatrix })] Optional PV matrix override.
499
+ * @returns {p5.Matrix}
500
+ */
501
+ p5.RendererGL.prototype.ipvMatrix = function ({
502
+ pMatrix,
503
+ vMatrix,
504
+ pvMatrix = this.pvMatrix({ pMatrix, vMatrix })
505
+ } = {}) {
506
+ return _invert(pvMatrix);
507
+ };
508
+
509
+ /**
510
+ * ipvMatrix({ pMatrix, vMatrix, pvMatrix }):
511
+ * Inverse(PV) (mat4). Requires WEBGL.
512
+ * @param {object} [opts]
513
+ * @returns {p5.Matrix}
514
+ */
515
+ fn.ipvMatrix = function (opts = {}) {
516
+ return _rendererGL(this).ipvMatrix(opts);
517
+ };
518
+
519
+ // ---------------------------------------------------------------------------
520
+ // Projection matrix queries (isOrtho, planes, fov, hfov)
521
+ // ---------------------------------------------------------------------------
522
+
523
+ /**
524
+ * Returns true if this projection matrix is orthographic.
525
+ * @returns {boolean}
526
+ */
527
+ p5.Matrix.prototype.isOrtho = function () {
528
+ return this.mat4[15] !== 0;
529
+ };
530
+
531
+ /**
532
+ * Returns true if the current projection is orthographic.
533
+ * @returns {boolean}
534
+ */
535
+ p5.RendererGL.prototype.isOrtho = function () {
536
+ return this.pMatrix().isOrtho();
537
+ };
538
+
539
+ /**
540
+ * Returns true if the current projection is orthographic.
541
+ * Requires WEBGL.
542
+ * @returns {boolean}
543
+ */
544
+ fn.isOrtho = function () {
545
+ return _rendererGL(this).isOrtho();
546
+ };
547
+
548
+ /**
549
+ * Near plane distance.
550
+ * @returns {number}
551
+ */
552
+ p5.Matrix.prototype.nPlane = function () {
553
+ const m = this.mat4;
554
+ return m[15] === 0 ? m[14] / (m[10] - 1) : (1 + m[14]) / m[10];
555
+ };
556
+
557
+ /**
558
+ * Far plane distance.
559
+ * @returns {number}
560
+ */
561
+ p5.Matrix.prototype.fPlane = function () {
562
+ const m = this.mat4;
563
+ return m[15] === 0 ? m[14] / (1 + m[10]) : (m[14] - 1) / m[10];
564
+ };
565
+
566
+ /**
567
+ * Left plane at the near plane.
568
+ * @returns {number}
569
+ */
570
+ p5.Matrix.prototype.lPlane = function () {
571
+ const m = this.mat4;
572
+ return m[15] === 1 ? -(1 + m[12]) / m[0] : this.nPlane() * (m[8] - 1) / m[0];
573
+ };
574
+
575
+ /**
576
+ * Right plane at the near plane.
577
+ * @returns {number}
578
+ */
579
+ p5.Matrix.prototype.rPlane = function () {
580
+ const m = this.mat4;
581
+ return m[15] === 1 ? (1 - m[12]) / m[0] : this.nPlane() * (1 + m[8]) / m[0];
582
+ };
583
+
584
+ /**
585
+ * Top plane at the near plane.
586
+ * @returns {number}
587
+ */
588
+ p5.Matrix.prototype.tPlane = function () {
589
+ const m = this.mat4;
590
+ return m[15] === 1 ? (m[13] - 1) / m[5] : this.nPlane() * (m[9] - 1) / m[5];
591
+ };
592
+
593
+ /**
594
+ * Bottom plane at the near plane.
595
+ * @returns {number}
596
+ */
597
+ p5.Matrix.prototype.bPlane = function () {
598
+ const m = this.mat4;
599
+ return m[15] === 1 ? (1 + m[13]) / m[5] : this.nPlane() * (1 + m[9]) / m[5];
600
+ };
601
+
602
+ /**
603
+ * Near plane distance for the current projection.
604
+ * @returns {number}
605
+ */
606
+ p5.RendererGL.prototype.nPlane = function () {
607
+ return this.pMatrix().nPlane();
608
+ };
609
+
610
+ /**
611
+ * Far plane distance for the current projection.
612
+ * @returns {number}
613
+ */
614
+ p5.RendererGL.prototype.fPlane = function () {
615
+ return this.pMatrix().fPlane();
616
+ };
617
+
618
+ /**
619
+ * Left plane for the current projection.
620
+ * @returns {number}
621
+ */
622
+ p5.RendererGL.prototype.lPlane = function () {
623
+ return this.pMatrix().lPlane();
624
+ };
625
+
626
+ /**
627
+ * Right plane for the current projection.
628
+ * @returns {number}
629
+ */
630
+ p5.RendererGL.prototype.rPlane = function () {
631
+ return this.pMatrix().rPlane();
632
+ };
633
+
634
+ /**
635
+ * Top plane for the current projection.
636
+ * @returns {number}
637
+ */
638
+ p5.RendererGL.prototype.tPlane = function () {
639
+ return this.pMatrix().tPlane();
640
+ };
641
+
642
+ /**
643
+ * Bottom plane for the current projection.
644
+ * @returns {number}
645
+ */
646
+ p5.RendererGL.prototype.bPlane = function () {
647
+ return this.pMatrix().bPlane();
648
+ };
649
+
650
+ /**
651
+ * Near plane distance for the current projection.
652
+ * Requires WEBGL.
653
+ * @returns {number}
654
+ */
655
+ fn.nPlane = function () {
656
+ return _rendererGL(this).nPlane();
657
+ };
658
+
659
+ /**
660
+ * Far plane distance for the current projection.
661
+ * Requires WEBGL.
662
+ * @returns {number}
663
+ */
664
+ fn.fPlane = function () {
665
+ return _rendererGL(this).fPlane();
666
+ };
667
+
668
+ /**
669
+ * Left plane for the current projection.
670
+ * Requires WEBGL.
671
+ * @returns {number}
672
+ */
673
+ fn.lPlane = function () {
674
+ return _rendererGL(this).lPlane();
675
+ };
676
+
677
+ /**
678
+ * Right plane for the current projection.
679
+ * Requires WEBGL.
680
+ * @returns {number}
681
+ */
682
+ fn.rPlane = function () {
683
+ return _rendererGL(this).rPlane();
684
+ };
685
+
686
+ /**
687
+ * Top plane for the current projection.
688
+ * Requires WEBGL.
689
+ * @returns {number}
690
+ */
691
+ fn.tPlane = function () {
692
+ return _rendererGL(this).tPlane();
693
+ };
694
+
695
+ /**
696
+ * Bottom plane for the current projection.
697
+ * Requires WEBGL.
698
+ * @returns {number}
699
+ */
700
+ fn.bPlane = function () {
701
+ return _rendererGL(this).bPlane();
702
+ };
703
+
704
+ /**
705
+ * Vertical field of view (radians), perspective only.
706
+ * @returns {number|undefined}
707
+ */
708
+ p5.Matrix.prototype.fov = function () {
709
+ if (this.mat4[15] !== 0) {
710
+ console.error('[tree.matrix] fov only works for a perspective projection.');
711
+ return;
712
+ }
713
+ return Math.abs(2 * Math.atan(1 / this.mat4[5]));
714
+ };
715
+
716
+ /**
717
+ * Horizontal field of view (radians), perspective only.
718
+ * @returns {number|undefined}
719
+ */
720
+ p5.Matrix.prototype.hfov = function () {
721
+ if (this.mat4[15] !== 0) {
722
+ console.error('[tree.matrix] hfov only works for a perspective projection.');
723
+ return;
724
+ }
725
+ return Math.abs(2 * Math.atan(1 / this.mat4[0]));
726
+ };
727
+
728
+ /**
729
+ * Vertical field of view (radians) of the current projection.
730
+ * @returns {number|undefined}
731
+ */
732
+ p5.RendererGL.prototype.fov = function () {
733
+ return this.pMatrix().fov();
734
+ };
735
+
736
+ /**
737
+ * Horizontal field of view (radians) of the current projection.
738
+ * @returns {number|undefined}
739
+ */
740
+ p5.RendererGL.prototype.hfov = function () {
741
+ return this.pMatrix().hfov();
742
+ };
743
+
744
+ /**
745
+ * Vertical field of view (radians) of the current projection.
746
+ * Requires WEBGL.
747
+ * @returns {number|undefined}
748
+ */
749
+ fn.fov = function () {
750
+ return _rendererGL(this).fov();
751
+ };
752
+
753
+ /**
754
+ * Horizontal field of view (radians) of the current projection.
755
+ * Requires WEBGL.
756
+ * @returns {number|undefined}
757
+ */
758
+ fn.hfov = function () {
759
+ return _rendererGL(this).hfov();
760
+ };
761
+
762
+ // --- private keys (shared internal state across protos) ---
763
+ const STATE_KEY = Symbol.for('tree.camera.path.state');
764
+ const PLAYERS_KEY = Symbol.for('tree.camera.path.players');
765
+
766
+ const clamp01 = function (x) {
767
+ return x < 0 ? 0 : (x > 1 ? 1 : x);
768
+ };
769
+
770
+ const isFiniteNumber = function (x) {
771
+ return typeof x === 'number' && Number.isFinite(x);
772
+ };
773
+
774
+ const warn = function (msg) {
775
+ console.warn('[tree.camera.path] ' + msg);
776
+ };
777
+
778
+ const ensurePath = function (cam) {
779
+ cam.path || (cam.path = []);
780
+ return cam.path;
781
+ };
782
+
783
+ const segmentCount = function (path) {
784
+ return Math.max(0, path.length - 1);
785
+ };
786
+
787
+ const getState = function (cam) {
788
+ cam[STATE_KEY] || (cam[STATE_KEY] = {
789
+ playing: false,
790
+ loop: false,
791
+ pingPong: false,
792
+ onEnd: undefined,
793
+ rate: 1,
794
+ duration: 30, // frames per segment
795
+ seg: 0,
796
+ f: 0,
797
+ pathIsOrtho: undefined
798
+ });
799
+ return cam[STATE_KEY];
800
+ };
801
+
802
+ const getPlayers = function (pInst) {
803
+ pInst[PLAYERS_KEY] || (pInst[PLAYERS_KEY] = new Set());
804
+ return pInst[PLAYERS_KEY];
805
+ };
806
+
807
+ const getActiveCamera = function (pInst) {
808
+ const r = pInst && pInst._renderer;
809
+ return (
810
+ (r && r.states && r.states.curCamera) || // p5-v2 canonical
811
+ (r && (r._curCamera || r.curCamera || r._camera)) || // fallbacks
812
+ undefined
813
+ );
814
+ };
815
+
816
+ /**
817
+ * Interpolate camera pose at normalized global t in [0..1] along the whole path.
818
+ * Also updates internal seg/f so playPath resumes from that location.
819
+ */
820
+ const seekGlobal = function (cam, t) {
821
+ const path = ensurePath(cam);
822
+ const nSeg = segmentCount(path);
823
+ if (nSeg === 0) return;
824
+ const st = getState(cam);
825
+ const tt = clamp01(t);
826
+ const x = tt * nSeg;
827
+ const seg = Math.min(nSeg - 1, Math.floor(x));
828
+ const amt = x - seg;
829
+ cam.slerp(path[seg], path[seg + 1], amt);
830
+ st.seg = seg;
831
+ st.f = Math.round(amt * Math.max(1, st.duration | 0));
832
+ };
833
+
834
+ /**
835
+ * Interpolate camera pose at amt in [0..1] within a specific segment index.
836
+ */
837
+ const seekSegment = function (cam, amt, segIndex) {
838
+ const path = ensurePath(cam);
839
+ const nSeg = segmentCount(path);
840
+ if (nSeg === 0) return;
841
+ const st = getState(cam);
842
+ const seg = Math.max(0, Math.min(segIndex | 0, nSeg - 1));
843
+ const a = clamp01(amt);
844
+ cam.slerp(path[seg], path[seg + 1], a);
845
+ st.seg = seg;
846
+ st.f = Math.round(a * Math.max(1, st.duration | 0));
847
+ };
848
+
849
+ /**
850
+ * Playback tick.
851
+ *
852
+ * Playback runs in "frames per segment" (`duration`), and `rate` is interpreted
853
+ * as a speed multiplier applied per frame.
854
+ *
855
+ * Rate semantics:
856
+ * - rate > 0 : forward playback
857
+ * - rate < 0 : reverse playback
858
+ * - rate === 0 : stopped
859
+ *
860
+ * The absolute value of `rate` is used as a per-frame advance amount.
861
+ * Fractional rates are supported (e.g. 0.5 plays at half speed).
862
+ *
863
+ * Segment boundaries are handled according to playback mode:
864
+ * - pingPong: bounce at the ends and reverse direction
865
+ * - loop: wrap around to the opposite end
866
+ * - otherwise: stop at the end and optionally invoke `onEnd`
867
+ */
868
+ const tick = function (cam) {
869
+ const st = getState(cam);
870
+ if (!st.playing) return;
871
+ const path = ensurePath(cam);
872
+ const nSeg = segmentCount(path);
873
+ if (nSeg === 0) {
874
+ st.playing = false;
875
+ return;
876
+ }
877
+ const dur = Math.max(1, st.duration | 0);
878
+ const speed = Math.abs(st.rate);
879
+ if (speed === 0) {
880
+ st.playing = false;
881
+ return;
882
+ }
883
+ let dir = st.rate >= 0 ? 1 : -1;
884
+ st.f += speed;
885
+ while (st.f >= dur) {
886
+ st.f -= dur;
887
+ st.seg += dir;
888
+ if (st.seg >= nSeg || st.seg < 0) {
889
+ if (st.pingPong) {
890
+ // Bounce at endpoints and flip direction.
891
+ if (dir > 0) {
892
+ st.seg = nSeg - 1;
893
+ st.f = 0;
894
+ st.rate = -speed;
895
+ } else {
896
+ st.seg = 0;
897
+ st.f = 0;
898
+ st.rate = speed;
899
+ }
900
+ dir = st.rate >= 0 ? 1 : -1;
901
+ } else if (st.loop) {
902
+ st.seg = dir > 0 ? 0 : (nSeg - 1);
903
+ } else {
904
+ st.playing = false;
905
+ seekGlobal(cam, dir > 0 ? 1 : 0);
906
+ const cb = st.onEnd;
907
+ if (typeof cb === 'function') {
908
+ try { cb(cam); } catch (e) { /* ignore user callback errors */ }
909
+ }
910
+ return;
911
+ }
912
+ }
913
+ }
914
+ const local = st.f / dur;
915
+ const amt = dir > 0 ? local : (1 - local);
916
+ cam.slerp(path[st.seg], path[st.seg + 1], amt);
917
+ };
918
+
919
+ // -----------------------
920
+ // v2 addon lifecycle hook
921
+ // -----------------------
922
+
923
+ lifecycles.predraw = function () {
924
+ const players = getPlayers(this);
925
+ players.forEach(cam => {
926
+ tick(cam);
927
+ getState(cam).playing || players.delete(cam);
928
+ });
929
+ };
930
+
931
+ lifecycles.remove = function () {
932
+ const players = this[PLAYERS_KEY];
933
+ players && players.clear();
934
+ };
935
+
936
+ // ----------
937
+ // Camera API
938
+ // ----------
939
+
940
+ /**
941
+ * addPath overloads (opts, if present, must be last; opts is the only allowed plain object):
942
+ *
943
+ * camera.addPath(); // snapshot this camera
944
+ * camera.addPath(opts); // snapshot this camera (with opts)
945
+ *
946
+ * camera.addPath(otherCam, opts); // snapshot otherCam
947
+ * camera.addPath([camA, camB, ...], opts); // bulk add (cameras)
948
+ *
949
+ * camera.addPath(eye, center, up, opts); // eye/center/up: p5.Vector or [x, y, z]
950
+ *
951
+ * camera.addPath(view, opts); // view: p5.Matrix (4x4) or mat4[16]
952
+ * // (world -> camera), like p5.Camera.cameraMatrix
953
+ * camera.addPath([viewA, viewB, ...], opts); // bulk add (views)
954
+ *
955
+ * Options:
956
+ * - reset: boolean (default false) Clears the current path before adding.
957
+ *
958
+ * Notes:
959
+ * - Keyframes are stored as camera snapshots (p5.Camera.copy()) so Camera.slerp() works.
960
+ * - Projection compatibility is enforced (Camera.slerp requires same projection).
961
+ *
962
+ * @param {...any} args
963
+ * @returns {p5.Camera} this
964
+ */
965
+ p5.Camera.prototype.addPath = function (...args) {
966
+ const st = getState(this);
967
+ const path = ensurePath(this);
968
+ const isPlainObject = v => {
969
+ if (!v || typeof v !== 'object') return false;
970
+ if (Array.isArray(v)) return false;
971
+ if (ArrayBuffer.isView(v)) return false;
972
+ return Object.getPrototypeOf(v) === Object.prototype;
973
+ };
974
+ const isVec3 = v =>
975
+ v instanceof p5.Vector ||
976
+ (Array.isArray(v) && v.length === 3 && v.every(n => typeof n === 'number' && Number.isFinite(n)));
977
+ const toVec3 = v => v instanceof p5.Vector ? [v.x, v.y, v.z] : [v[0], v[1], v[2]];
978
+ const sameKeyframe = function (a, b) {
979
+ if (!a || !b) return false;
980
+ const aCM = a.cameraMatrix && a.cameraMatrix.mat4;
981
+ const bCM = b.cameraMatrix && b.cameraMatrix.mat4;
982
+ if (!aCM || !bCM) return false;
983
+ for (let i = 0; i < 16; i++) if (aCM[i] !== bCM[i]) return false;
984
+ return true;
985
+ };
986
+ const addSnapshot = c => {
987
+ const last = path.length ? path[path.length - 1] : undefined;
988
+ last && sameKeyframe(last, c) || path.push(c.copy());
989
+ };
990
+ const isOrthoCam = c => {
991
+ const m = c && c.projMatrix && c.projMatrix.mat4;
992
+ return m && m.length === 16 ? (m[15] !== 0) : undefined;
993
+ };
994
+ const initProjBaseline = c => {
995
+ if (st.pathIsOrtho !== undefined) return;
996
+ const v = isOrthoCam(c);
997
+ st.pathIsOrtho = v;
998
+ v === undefined && warn('addPath: unable to verify projection type (projMatrix.mat4 unavailable).');
999
+ };
1000
+ const checkProjCompat = c => {
1001
+ initProjBaseline(c);
1002
+ const v = isOrthoCam(c);
1003
+ if (st.pathIsOrtho === undefined || v === undefined) {
1004
+ v === undefined && warn('addPath: unable to verify projection type (projMatrix.mat4 unavailable).');
1005
+ return true;
1006
+ }
1007
+ if (v !== st.pathIsOrtho) {
1008
+ warn('addPath rejected: keyframe has different projection type (ortho vs perspective).');
1009
+ return false;
1010
+ }
1011
+ return true;
1012
+ };
1013
+ const isMat4Array = v =>
1014
+ (Array.isArray(v) || ArrayBuffer.isView(v)) &&
1015
+ v.length === 16 &&
1016
+ Array.prototype.every.call(v, n => typeof n === 'number' && Number.isFinite(n));
1017
+ const isView = v => v instanceof p5.Matrix || isMat4Array(v);
1018
+ const toMat4 = v => v instanceof p5.Matrix ? v.mat4 : v;
1019
+ const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1020
+ const len3 = v => Math.sqrt(dot3(v, v));
1021
+ const norm3 = v => {
1022
+ const l = len3(v) || 1;
1023
+ return [v[0] / l, v[1] / l, v[2] / l];
1024
+ };
1025
+ const importViewToCamera = view => {
1026
+ // view is a column-major mat4, world -> camera (p5.Camera.cameraMatrix).
1027
+ const m = toMat4(view);
1028
+ // Rows of rotation part (world->camera):
1029
+ const right = norm3([m[0], m[4], m[8]]);
1030
+ const up = norm3([m[1], m[5], m[9]]);
1031
+ const negFwd = norm3([m[2], m[6], m[10]]);
1032
+ const fwd = [-negFwd[0], -negFwd[1], -negFwd[2]];
1033
+ // Translation column: t = -R^T * eye
1034
+ const t = [m[12], m[13], m[14]];
1035
+ // eye = -(t0*right + t1*up) + t2*forward
1036
+ const eye = [
1037
+ -(t[0] * right[0] + t[1] * up[0]) + t[2] * fwd[0],
1038
+ -(t[0] * right[1] + t[1] * up[1]) + t[2] * fwd[1],
1039
+ -(t[0] * right[2] + t[1] * up[2]) + t[2] * fwd[2]
1040
+ ];
1041
+ // Enforce center using this camera’s current focus distance.
1042
+ const dist = Math.sqrt(
1043
+ (this.centerX - this.eyeX) * (this.centerX - this.eyeX) +
1044
+ (this.centerY - this.eyeY) * (this.centerY - this.eyeY) +
1045
+ (this.centerZ - this.eyeZ) * (this.centerZ - this.eyeZ)
1046
+ ) || 1;
1047
+ const center = [
1048
+ eye[0] + fwd[0] * dist,
1049
+ eye[1] + fwd[1] * dist,
1050
+ eye[2] + fwd[2] * dist
1051
+ ];
1052
+ // Important: use camera(...) so cameraMatrix stays consistent with eye/center/up.
1053
+ const c = this.copy();
1054
+ c.camera(
1055
+ eye[0], eye[1], eye[2],
1056
+ center[0], center[1], center[2],
1057
+ up[0], up[1], up[2]
1058
+ );
1059
+ return c;
1060
+ };
1061
+ // --- opts extraction (opts always last; only plain object is opts) ---
1062
+ const o = args.length && isPlainObject(args[args.length - 1]) ? args.pop() : {};
1063
+ if (o.reset) {
1064
+ path.length = 0;
1065
+ st.seg = 0;
1066
+ st.f = 0;
1067
+ st.pathIsOrtho = undefined;
1068
+ }
1069
+ initProjBaseline(this);
1070
+ // addPath() -> snapshot this
1071
+ if (args.length === 0) {
1072
+ addSnapshot(this);
1073
+ return this;
1074
+ }
1075
+ // addPath(view) OR addPath(camera) OR addPath([cameras]) OR addPath([views])
1076
+ if (args.length === 1) {
1077
+ const override = args[0];
1078
+ // single view
1079
+ if (isView(override)) {
1080
+ const c = importViewToCamera(override);
1081
+ checkProjCompat(c) && addSnapshot(c);
1082
+ return this;
1083
+ }
1084
+ // bulk: views or cameras
1085
+ if (Array.isArray(override)) {
1086
+ const list = override;
1087
+ // bulk views
1088
+ if (list.length && list.every(isView)) {
1089
+ for (let i = 0; i < list.length; i++) {
1090
+ const c = importViewToCamera(list[i]);
1091
+ checkProjCompat(c) && addSnapshot(c);
1092
+ }
1093
+ return this;
1094
+ }
1095
+ // bulk cameras (existing behavior)
1096
+ for (let i = 0; i < list.length; i++) {
1097
+ const c = list[i];
1098
+ if (!(c instanceof p5.Camera)) {
1099
+ warn('addPath: ignored non-camera value.');
1100
+ continue;
1101
+ }
1102
+ checkProjCompat(c) && addSnapshot(c);
1103
+ }
1104
+ return this;
1105
+ }
1106
+ // single camera
1107
+ if (override instanceof p5.Camera) {
1108
+ checkProjCompat(override) && addSnapshot(override);
1109
+ return this;
1110
+ }
1111
+ warn('addPath: ignored unsupported arguments.');
1112
+ return this;
1113
+ }
1114
+ // addPath(eye, center, up, opts)
1115
+ if (args.length === 3 && args.every(isVec3)) {
1116
+ const eye = toVec3(args[0]);
1117
+ const center = toVec3(args[1]);
1118
+ const up = norm3(toVec3(args[2]));
1119
+ // Important: use camera(...) so cameraMatrix stays consistent with eye/center/up.
1120
+ const c = this.copy();
1121
+ c.camera(
1122
+ eye[0], eye[1], eye[2],
1123
+ center[0], center[1], center[2],
1124
+ up[0], up[1], up[2]
1125
+ );
1126
+ checkProjCompat(c) && addSnapshot(c);
1127
+ return this;
1128
+ }
1129
+ warn('addPath: ignored unsupported arguments.');
1130
+ return this;
1131
+ };
1132
+
1133
+ /**
1134
+ * playPath overloads:
1135
+ * camera.playPath(rate)
1136
+ * camera.playPath({ duration, loop, pingPong, onEnd, rate })
1137
+ *
1138
+ * Playback runs in "frames per segment" (duration), and rate is interpreted as a
1139
+ * simple speed multiplier:
1140
+ * - rate > 0 : forward
1141
+ * - rate < 0 : reverse
1142
+ * - rate === 0 : stopped (does not register for ticking)
1143
+ *
1144
+ * duration: frames per segment (default 30).
1145
+ * loop: wraps at ends (default false).
1146
+ * pingPong: bounces at ends (default false).
1147
+ * onEnd: called when playback naturally ends (non-looping, non-pingpong).
1148
+ *
1149
+ * Special case (single keyframe):
1150
+ * If camera.path has exactly 1 keyframe, playPath does not start playback.
1151
+ * It restores this camera pose to that keyframe (via p5.Camera.camera())
1152
+ * and ensures playback is stopped/unregistered.
1153
+ *
1154
+ * If both pingPong and loop are true, pingPong takes precedence.
1155
+ */
1156
+ p5.Camera.prototype.playPath = function (rateOrOpts) {
1157
+ const st = getState(this);
1158
+ const path = ensurePath(this);
1159
+ const pInst = this._renderer && this._renderer._pInst;
1160
+ const unregister = () => pInst && getPlayers(pInst).delete(this);
1161
+ const register = () => pInst && getPlayers(pInst).add(this);
1162
+ // 0 keyframes: nothing to do
1163
+ if (path.length === 0) {
1164
+ warn('playPath ignored: need at least 1 keyframe in camera.path.');
1165
+ st.playing = false;
1166
+ unregister();
1167
+ return this;
1168
+ }
1169
+ // 1 keyframe: restore pose only (no playback)
1170
+ if (path.length === 1) {
1171
+ const kf = path[0];
1172
+ st.playing = false;
1173
+ unregister();
1174
+ return this.camera(
1175
+ kf.eyeX, kf.eyeY, kf.eyeZ,
1176
+ kf.centerX, kf.centerY, kf.centerZ,
1177
+ kf.upX, kf.upY, kf.upZ
1178
+ );
1179
+ }
1180
+ const nSeg = segmentCount(path);
1181
+ if (nSeg === 0) {
1182
+ warn('playPath ignored: need at least 2 keyframes in camera.path.');
1183
+ st.playing = false;
1184
+ unregister();
1185
+ return this;
1186
+ }
1187
+ if (isFiniteNumber(rateOrOpts)) {
1188
+ st.rate = rateOrOpts;
1189
+ } else {
1190
+ const o = rateOrOpts || {};
1191
+ st.duration = isFiniteNumber(o.duration) ? o.duration : st.duration;
1192
+ st.loop = !!o.loop;
1193
+ st.pingPong = !!o.pingPong;
1194
+ st.onEnd = typeof o.onEnd === 'function' ? o.onEnd : st.onEnd;
1195
+ st.rate = isFiniteNumber(o.rate) ? o.rate : st.rate;
1196
+ }
1197
+ // rate === 0 means "stop" (don’t register for ticking)
1198
+ if (st.rate === 0) {
1199
+ st.playing = false;
1200
+ unregister();
1201
+ return this;
1202
+ }
1203
+ // If starting from stopped state, default to an endpoint depending on direction.
1204
+ // Important: do NOT use seekGlobal(cam, 1) for reverse start, because seekGlobal sets st.f = dur
1205
+ // and the next tick will immediately underflow the segment and stop (snap).
1206
+ if (!st.playing) {
1207
+ const forward = st.rate >= 0;
1208
+ st.seg = forward ? 0 : (nSeg - 1);
1209
+ st.f = 0;
1210
+ // Snap pose to the start/end of the current segment, but keep st.f = 0.
1211
+ this.slerp(path[st.seg], path[st.seg + 1], forward ? 0 : 1);
1212
+ }
1213
+ st.playing = true;
1214
+ register();
1215
+ return this;
1216
+ };
1217
+
1218
+ /**
1219
+ * stopPath({ reset=false })
1220
+ * - Stops playback.
1221
+ * - If reset:true, seeks to start (forward) or end (reverse).
1222
+ */
1223
+ p5.Camera.prototype.stopPath = function (opts) {
1224
+ const st = getState(this);
1225
+ const o = opts || {};
1226
+ st.playing = false;
1227
+ const pInst = this._renderer && this._renderer._pInst;
1228
+ pInst && getPlayers(pInst).delete(this);
1229
+ if (o.reset) {
1230
+ const forward = st.rate >= 0;
1231
+ seekGlobal(this, forward ? 0 : 1);
1232
+ st.seg = forward ? 0 : Math.max(0, segmentCount(ensurePath(this)) - 1);
1233
+ st.f = 0;
1234
+ }
1235
+ return this;
1236
+ };
1237
+
1238
+ /**
1239
+ * resetPath(n?)
1240
+ * - resetPath() clears all keyframes and stops.
1241
+ * - resetPath(n) keeps first n keyframes (truncate) and stops.
1242
+ */
1243
+ p5.Camera.prototype.resetPath = function (n) {
1244
+ const st = getState(this);
1245
+ const path = ensurePath(this);
1246
+ st.playing = false;
1247
+ st.seg = 0;
1248
+ st.f = 0;
1249
+ const pInst = this._renderer && this._renderer._pInst;
1250
+ pInst && getPlayers(pInst).delete(this);
1251
+ if (!isFiniteNumber(n)) {
1252
+ path.length = 0;
1253
+ st.pathIsOrtho = undefined;
1254
+ return this;
1255
+ }
1256
+ const nInt = n | 0;
1257
+ const keep = Math.max(0, Math.abs(nInt));
1258
+ if (keep === 0) {
1259
+ path.length = 0;
1260
+ st.pathIsOrtho = undefined;
1261
+ return this;
1262
+ }
1263
+ if (nInt >= 0) {
1264
+ path.length = Math.min(path.length, keep);
1265
+ } else if (path.length > keep) {
1266
+ path.splice(0, path.length - keep);
1267
+ }
1268
+ if (segmentCount(path) === 0) {
1269
+ st.pathIsOrtho = undefined;
1270
+ }
1271
+ return this;
1272
+ };
1273
+
1274
+ /**
1275
+ * seekPath overloads:
1276
+ * camera.seekPath(t) // global t in [0..1]
1277
+ * camera.seekPath(amt, segIndex) // segment-local
1278
+ *
1279
+ * Seeking always stops playback.
1280
+ */
1281
+ p5.Camera.prototype.seekPath = function (t, segIndex) {
1282
+ const st = getState(this);
1283
+ st.playing = false;
1284
+ const pInst = this._renderer && this._renderer._pInst;
1285
+ pInst && getPlayers(pInst).delete(this);
1286
+ if (isFiniteNumber(segIndex)) {
1287
+ seekSegment(this, t, segIndex);
1288
+ return this;
1289
+ }
1290
+ seekGlobal(this, t);
1291
+ return this;
1292
+ };
1293
+
1294
+ // ------------------------------------------------------------
1295
+ // p5 wrappers (same names, forward to active camera)
1296
+ // ------------------------------------------------------------
1297
+
1298
+ fn.addPath = function (...args) {
1299
+ const cam = getActiveCamera(this);
1300
+ cam && cam.addPath(...args);
1301
+ return this;
1302
+ };
1303
+
1304
+ fn.playPath = function (...args) {
1305
+ const cam = getActiveCamera(this);
1306
+ cam && cam.playPath(...args);
1307
+ return this;
1308
+ };
1309
+
1310
+ fn.seekPath = function (...args) {
1311
+ const cam = getActiveCamera(this);
1312
+ cam && cam.seekPath(...args);
1313
+ return this;
1314
+ };
1315
+
1316
+ fn.resetPath = function (...args) {
1317
+ const cam = getActiveCamera(this);
1318
+ cam && cam.resetPath(...args);
1319
+ return this;
1320
+ };
1321
+
1322
+ fn.stopPath = function (...args) {
1323
+ const cam = getActiveCamera(this);
1324
+ cam && cam.stopPath(...args);
1325
+ return this;
1326
+ };
1327
+
1328
+ // HUD
1329
+
1330
+ fn.beginHUD = function (...args) {
1331
+ this._renderer?.beginHUD?.(...args);
1332
+ return this;
1333
+ };
1334
+
1335
+ fn.endHUD = function (...args) {
1336
+ this._renderer?.endHUD?.(...args);
1337
+ return this;
1338
+ };
1339
+
1340
+ /*
1341
+ // treegl approach:
1342
+ p5.RendererGL.prototype.beginHUD = function () {
1343
+ if (this._hudActive === true) return;
1344
+ const p = this._pInst;
1345
+ const gl = this.drawingContext;
1346
+ const states = this.states;
1347
+ if (!p || !gl || !states) return;
1348
+ // ------------------------------------------------------------------
1349
+ // Save world state (treegl: m, v, p)
1350
+ // ------------------------------------------------------------------
1351
+ this._hudPrevCamera = states.curCamera;
1352
+ // push isolates renderer state (tree-style equivalent of treegl push)
1353
+ p.push();
1354
+ p.resetShader();
1355
+ // Ensure HUD does not inherit model transforms
1356
+ p.resetMatrix();
1357
+ // ------------------------------------------------------------------
1358
+ // Depth state
1359
+ // ------------------------------------------------------------------
1360
+ this._hudDepthWasEnabled = gl.isEnabled(gl.DEPTH_TEST);
1361
+ gl.flush();
1362
+ gl.disable(gl.DEPTH_TEST);
1363
+ // ------------------------------------------------------------------
1364
+ // HUD camera
1365
+ // ------------------------------------------------------------------
1366
+ if (this._hudCam === undefined) {
1367
+ this._hudCam = p.createCamera();
1368
+ }
1369
+ const z = Number.MAX_VALUE;
1370
+ // HUD coordinates:
1371
+ // x ∈ [0, width], y ∈ [0, height], origin at top-left
1372
+ this._hudCam.ortho(0, p.width, -p.height, 0, -z, z);
1373
+ // this._hudCam.ortho(0, p.width, 0, -p.height, -z, z); // flipped variant
1374
+ this._hudCam.camera(0, 0, 1, 0, 0, 0, 0, 1, 0);
1375
+ p.setCamera(this._hudCam);
1376
+ this._hudActive = true;
1377
+ };
1378
+
1379
+ p5.RendererGL.prototype.endHUD = function () {
1380
+ if (this._hudActive !== true) return;
1381
+ const p = this._pInst;
1382
+ const gl = this.drawingContext;
1383
+ const states = this.states;
1384
+ if (!p || !gl || !states) return;
1385
+ gl.flush();
1386
+ // Restore depth test
1387
+ this._hudDepthWasEnabled ? gl.enable(gl.DEPTH_TEST) : gl.disable(gl.DEPTH_TEST);
1388
+ // Restore renderer state
1389
+ p.pop();
1390
+ // Restore camera (tree equivalent of restoring u* matrices)
1391
+ if (this._hudPrevCamera !== undefined) {
1392
+ p.setCamera(this._hudPrevCamera);
1393
+ }
1394
+ this._hudPrevCamera = undefined;
1395
+ this._hudDepthWasEnabled = undefined;
1396
+ this._hudActive = false;
1397
+ };
1398
+ */
1399
+
1400
+ p5.RendererGL.prototype.beginHUD = function () {
1401
+ if (this._hudActive === true) return;
1402
+ const p = this._pInst;
1403
+ if (!p) return;
1404
+ const gl = this.drawingContext;
1405
+ const states = this.states;
1406
+ if (!gl || !states) return;
1407
+ p.push(); // isolate all subsequent HUD drawing from caller state
1408
+ p.resetShader();
1409
+ // Ensure HUD space does NOT inherit the user's current model transforms
1410
+ // (e.g. push()/translate()
1411
+ p.resetMatrix();
1412
+ // ---------------------
1413
+ // --- HUD setup ---
1414
+ this._hudPrevCam = states.curCamera;
1415
+ this._hudDepthWasEnabled = gl.isEnabled(gl.DEPTH_TEST);
1416
+ gl.flush();
1417
+ gl.disable(gl.DEPTH_TEST);
1418
+ if (this._hudCam === undefined) this._hudCam = p.createCamera();
1419
+ const z = 1e6;
1420
+ // HUD coordinates: x in [0, width], y in [0, height]
1421
+ this._hudCam.ortho(0, p.width, -p.height, 0, -z, z);
1422
+ // this._hudCam.ortho(0, p.width, 0, -p.height, -z, z); // <- flipped
1423
+ this._hudCam.camera(0, 0, 1, 0, 0, 0, 0, 1, 0);
1424
+ p.setCamera(this._hudCam);
1425
+ this._hudActive = true;
1426
+ };
1427
+
1428
+ p5.RendererGL.prototype.endHUD = function () {
1429
+ if (this._hudActive !== true) return;
1430
+ const p = this._pInst;
1431
+ const gl = this.drawingContext;
1432
+ const states = this.states;
1433
+ if (p === undefined || gl === undefined || states === undefined) return;
1434
+ gl.flush();
1435
+ this._hudDepthWasEnabled ? gl.enable(gl.DEPTH_TEST) : gl.disable(gl.DEPTH_TEST);
1436
+ p.pop(); // calls: this.pop(this._rendererState);
1437
+ this._hudPrevCam !== undefined && p.setCamera(this._hudPrevCam);
1438
+ this._hudPrevCam = undefined;
1439
+ this._hudDepthWasEnabled = undefined;
1440
+ this._hudActive = false;
1441
+ };
1442
+
1443
+ // ---------------------------------------------------------------------------
1444
+ // Space transforms: transformPosition / transformDirection
1445
+ // ---------------------------------------------------------------------------
1446
+
1447
+ p5.RendererGL.prototype._parseTransformArgs = function (defaultMainArg, ...args) {
1448
+ let mainArg = defaultMainArg;
1449
+ const options = {};
1450
+ for (const arg of args) {
1451
+ if (arg instanceof p5.Vector || Array.isArray(arg)) {
1452
+ mainArg = arg;
1453
+ } else if (arg && typeof arg === 'object') {
1454
+ Object.assign(options, arg);
1455
+ }
1456
+ }
1457
+ return { mainArg, options };
1458
+ };
1459
+
1460
+ // ---------------------------------------------------------------------------
1461
+ // Points (positions)
1462
+ // ---------------------------------------------------------------------------
1463
+
1464
+ fn.transformPosition = function (...args) {
1465
+ return _rendererGL(this)?.transformPosition(...args);
1466
+ };
1467
+
1468
+ /**
1469
+ * Converts a point (location) from one space into another.
1470
+ *
1471
+ * @param {p5.Vector|number[]} [point=p5.Tree.ORIGIN]
1472
+ * @param {Object} [opts]
1473
+ * @param {p5.Matrix|string} [opts.from=p5.Tree.EYE]
1474
+ * @param {p5.Matrix|string} [opts.to=p5.Tree.WORLD]
1475
+ * @param {p5.Matrix} [opts.pMatrix]
1476
+ * @param {p5.Matrix} [opts.vMatrix]
1477
+ * @param {p5.Matrix} [opts.eMatrix]
1478
+ * @param {p5.Matrix} [opts.pvMatrix]
1479
+ * @param {p5.Matrix} [opts.ipvMatrix]
1480
+ * @returns {p5.Vector}
1481
+ */
1482
+ p5.RendererGL.prototype.transformPosition = function (...args) {
1483
+ const { mainArg, options } = this._parseTransformArgs(p5.Tree.ORIGIN, ...args);
1484
+ return this._position(mainArg, options);
1485
+ };
1486
+
1487
+ p5.RendererGL.prototype._position = function (
1488
+ point = p5.Tree.ORIGIN,
1489
+ {
1490
+ from = p5.Tree.EYE,
1491
+ to = p5.Tree.WORLD,
1492
+ pMatrix,
1493
+ vMatrix,
1494
+ eMatrix,
1495
+ pvMatrix,
1496
+ ipvMatrix
1497
+ } = {}
1498
+ ) {
1499
+ if (Array.isArray(point)) {
1500
+ point = new p5.Vector(point[0] ?? 0, point[1] ?? 0, point[2] ?? 0);
1501
+ }
1502
+ if (from == p5.Tree.MODEL) {
1503
+ from = this.mMatrix({ eMatrix });
1504
+ }
1505
+ if (to == p5.Tree.MODEL) {
1506
+ to = this.mMatrix({ eMatrix });
1507
+ }
1508
+ if ((from == p5.Tree.WORLD) && (to == p5.Tree.SCREEN)) {
1509
+ return this._worldToScreenPosition({ point, pMatrix, vMatrix, pvMatrix });
1510
+ }
1511
+ if ((from == p5.Tree.SCREEN) && (to == p5.Tree.WORLD)) {
1512
+ return this._screenToWorldPosition({ point, pMatrix, vMatrix, pvMatrix, ipvMatrix });
1513
+ }
1514
+ if (from == p5.Tree.SCREEN && to == p5.Tree.NDC) {
1515
+ return this._screenToNDCPosition(point);
1516
+ }
1517
+ if (from == p5.Tree.NDC && to == p5.Tree.SCREEN) {
1518
+ return this._ndcToScreenPosition(point);
1519
+ }
1520
+ if (from == p5.Tree.WORLD && to == p5.Tree.NDC) {
1521
+ return this._screenToNDCPosition(
1522
+ this._worldToScreenPosition({ point, pMatrix, vMatrix, pvMatrix })
1523
+ );
1524
+ }
1525
+ if (from == p5.Tree.NDC && to == p5.Tree.WORLD) {
1526
+ return this._screenToWorldPosition({
1527
+ point: this._ndcToScreenPosition(point),
1528
+ pMatrix,
1529
+ vMatrix,
1530
+ pvMatrix,
1531
+ ipvMatrix
1532
+ });
1533
+ }
1534
+ if (from == p5.Tree.NDC && (to instanceof p5.Matrix || to == p5.Tree.EYE)) {
1535
+ return (to == p5.Tree.EYE
1536
+ ? (vMatrix ?? this.vMatrix())
1537
+ : to.copy().invert(to)
1538
+ ).mult4(
1539
+ this._screenToWorldPosition({
1540
+ point: this._ndcToScreenPosition(point),
1541
+ pMatrix,
1542
+ vMatrix,
1543
+ pvMatrix,
1544
+ ipvMatrix
1545
+ })
1546
+ );
1547
+ }
1548
+ if ((from instanceof p5.Matrix || from == p5.Tree.EYE) && to == p5.Tree.NDC) {
1549
+ return this._screenToNDCPosition(
1550
+ this._worldToScreenPosition({
1551
+ point: (from == p5.Tree.EYE
1552
+ ? (eMatrix ?? this.eMatrix())
1553
+ : from
1554
+ ).mult4(point),
1555
+ pMatrix,
1556
+ vMatrix,
1557
+ pvMatrix
1558
+ })
1559
+ );
1560
+ }
1561
+ if (from == p5.Tree.WORLD && (to instanceof p5.Matrix || to == p5.Tree.EYE)) {
1562
+ return (to == p5.Tree.EYE
1563
+ ? (vMatrix ?? this.vMatrix())
1564
+ : to.copy().invert(to)
1565
+ ).mult4(point);
1566
+ }
1567
+ if ((from instanceof p5.Matrix || from == p5.Tree.EYE) && to == p5.Tree.WORLD) {
1568
+ return (from == p5.Tree.EYE
1569
+ ? (eMatrix ?? this.eMatrix())
1570
+ : from
1571
+ ).mult4(point);
1572
+ }
1573
+ if (from instanceof p5.Matrix && to instanceof p5.Matrix) {
1574
+ return this.lMatrix({ from: from, to: to }).mult4(point);
1575
+ }
1576
+ if (from == p5.Tree.SCREEN && (to instanceof p5.Matrix || to == p5.Tree.EYE)) {
1577
+ return (to == p5.Tree.EYE
1578
+ ? (vMatrix ?? this.vMatrix())
1579
+ : to.copy().invert(to)
1580
+ ).mult4(
1581
+ this._screenToWorldPosition({ point, pMatrix, vMatrix, pvMatrix, ipvMatrix })
1582
+ );
1583
+ }
1584
+ if ((from instanceof p5.Matrix || from == p5.Tree.EYE) && to == p5.Tree.SCREEN) {
1585
+ return this._worldToScreenPosition({
1586
+ point: (from == p5.Tree.EYE
1587
+ ? (eMatrix ?? this.eMatrix())
1588
+ : from
1589
+ ).mult4(point),
1590
+ pMatrix,
1591
+ vMatrix,
1592
+ pvMatrix
1593
+ });
1594
+ }
1595
+ if (from instanceof p5.Matrix && to == p5.Tree.EYE) {
1596
+ return (vMatrix ?? this.vMatrix()).mult4(from.mult4(point));
1597
+ }
1598
+ if (from == p5.Tree.EYE && to instanceof p5.Matrix) {
1599
+ return to.copy().invert(to).mult4((eMatrix ?? this.eMatrix()).mult4(point));
1600
+ }
1601
+ console.error('couldn\'t parse your transformPosition query!');
1602
+ return point;
1603
+ };
1604
+
1605
+ p5.RendererGL.prototype._ndcToScreenPosition = function (point) {
1606
+ return new p5.Vector(
1607
+ p5.prototype.map(point.x, -1, 1, 0, this.width),
1608
+ p5.prototype.map(point.y, -1, 1, 0, this.height),
1609
+ p5.prototype.map(point.z, -1, 1, 0, 1)
1610
+ );
1611
+ };
1612
+
1613
+ p5.RendererGL.prototype._screenToNDCPosition = function (point) {
1614
+ return new p5.Vector(
1615
+ p5.prototype.map(point.x, 0, this.width, -1, 1),
1616
+ p5.prototype.map(point.y, 0, this.height, -1, 1),
1617
+ p5.prototype.map(point.z, 0, 1, -1, 1)
1618
+ );
1619
+ };
1620
+
1621
+ p5.RendererGL.prototype._worldToScreenPosition = function ({
1622
+ point = new p5.Vector(0, 0, 0.5),
1623
+ pMatrix,
1624
+ vMatrix,
1625
+ pvMatrix = this.pvMatrix({ pMatrix, vMatrix })
1626
+ } = {}) {
1627
+ let target = pvMatrix._mult4([point.x, point.y, point.z, 1]);
1628
+ if (target[3] === 0) {
1629
+ console.error('[p5.tree] World->Screen broken: check pvMatrix.');
1630
+ return point.copy();
1631
+ }
1632
+ const viewport = [0, this.height, this.width, -this.height];
1633
+ target[0] /= target[3];
1634
+ target[1] /= target[3];
1635
+ target[2] /= target[3];
1636
+ target[0] = target[0] * 0.5 + 0.5;
1637
+ target[1] = target[1] * 0.5 + 0.5;
1638
+ target[2] = target[2] * 0.5 + 0.5;
1639
+ target[0] = target[0] * viewport[2] + viewport[0];
1640
+ target[1] = target[1] * viewport[3] + viewport[1];
1641
+ return new p5.Vector(target[0], target[1], target[2]);
1642
+ };
1643
+
1644
+ p5.RendererGL.prototype._screenToWorldPosition = function ({
1645
+ point = new p5.Vector(this.width / 2, this.height / 2, 0.5),
1646
+ pMatrix,
1647
+ vMatrix,
1648
+ pvMatrix,
1649
+ ipvMatrix = this.ipvMatrix({ pMatrix, vMatrix, pvMatrix })
1650
+ } = {}) {
1651
+ const viewport = [0, this.height, this.width, -this.height];
1652
+ const source = [point.x, point.y, point.z, 1];
1653
+ source[0] = (source[0] - viewport[0]) / viewport[2];
1654
+ source[1] = (source[1] - viewport[1]) / viewport[3];
1655
+ source[0] = source[0] * 2 - 1;
1656
+ source[1] = source[1] * 2 - 1;
1657
+ source[2] = source[2] * 2 - 1;
1658
+ let target = ipvMatrix._mult4(source);
1659
+ if (target[3] === 0) {
1660
+ console.error('[p5.tree] Screen->World broken: check ipvMatrix.');
1661
+ return point.copy();
1662
+ }
1663
+ target[0] /= target[3];
1664
+ target[1] /= target[3];
1665
+ target[2] /= target[3];
1666
+ return new p5.Vector(target[0], target[1], target[2]);
1667
+ };
1668
+
1669
+ // ---------------------------------------------------------------------------
1670
+ // Directions (vector displacements)
1671
+ // ---------------------------------------------------------------------------
1672
+
1673
+ fn.transformDirection = function (...args) {
1674
+ return _rendererGL(this)?.transformDirection(...args);
1675
+ };
1676
+
1677
+ /**
1678
+ * Converts a vector displacement from one space into another.
1679
+ *
1680
+ * @param {p5.Vector|number[]} [vector=p5.Tree._k]
1681
+ * @param {Object} [opts]
1682
+ * @param {p5.Matrix|string} [opts.from=p5.Tree.EYE]
1683
+ * @param {p5.Matrix|string} [opts.to=p5.Tree.WORLD]
1684
+ * @param {p5.Matrix} [opts.vMatrix]
1685
+ * @param {p5.Matrix} [opts.eMatrix]
1686
+ * @param {p5.Matrix} [opts.pMatrix]
1687
+ * @returns {p5.Vector}
1688
+ */
1689
+ p5.RendererGL.prototype.transformDirection = function (...args) {
1690
+ const { mainArg, options } = this._parseTransformArgs(p5.Tree._k, ...args);
1691
+ return this._direction(mainArg, options);
1692
+ };
1693
+
1694
+ p5.RendererGL.prototype._direction = function (
1695
+ vector = p5.Tree._k,
1696
+ {
1697
+ from = p5.Tree.EYE,
1698
+ to = p5.Tree.WORLD,
1699
+ vMatrix,
1700
+ eMatrix,
1701
+ pMatrix
1702
+ } = {}
1703
+ ) {
1704
+ if (Array.isArray(vector)) {
1705
+ vector = new p5.Vector(vector[0] ?? 0, vector[1] ?? 0, vector[2] ?? 0);
1706
+ }
1707
+ if (from === p5.Tree.MODEL) from = this.mMatrix({ eMatrix });
1708
+ if (to === p5.Tree.MODEL) to = this.mMatrix({ eMatrix });
1709
+ if (from === p5.Tree.WORLD && to === p5.Tree.SCREEN) return this._worldToScreenDirection(vector, pMatrix);
1710
+ if (from === p5.Tree.SCREEN && to === p5.Tree.WORLD) return this._screenToWorldDirection(vector, pMatrix);
1711
+ if (from === p5.Tree.SCREEN && to === p5.Tree.NDC) return this._screenToNDCDirection(vector);
1712
+ if (from === p5.Tree.NDC && to === p5.Tree.SCREEN) return this._ndcToScreenDirection(vector);
1713
+ if (from === p5.Tree.WORLD && to === p5.Tree.NDC) {
1714
+ return this._screenToNDCDirection(this._worldToScreenDirection(vector, pMatrix));
1715
+ }
1716
+ if (from === p5.Tree.NDC && to === p5.Tree.WORLD) {
1717
+ return this._screenToWorldDirection(this._ndcToScreenDirection(vector), pMatrix);
1718
+ }
1719
+ if (from === p5.Tree.NDC && to === p5.Tree.EYE) {
1720
+ const m = this.dMatrix({ matrix: eMatrix ?? this.eMatrix() }); // mat3
1721
+ return m.multiplyVec3(
1722
+ this._screenToWorldDirection(this._ndcToScreenDirection(vector), pMatrix)
1723
+ );
1724
+ }
1725
+ if (from === p5.Tree.EYE && to === p5.Tree.NDC) {
1726
+ const m = this.dMatrix({ matrix: vMatrix ?? this.vMatrix() }); // mat3
1727
+ return this._screenToNDCDirection(
1728
+ this._worldToScreenDirection(m.multiplyVec3(vector), pMatrix)
1729
+ );
1730
+ }
1731
+ if (from === p5.Tree.SCREEN && to instanceof p5.Matrix) {
1732
+ const m = this.dMatrix({ matrix: to }); // mat3
1733
+ return m.multiplyVec3(this._screenToWorldDirection(vector, pMatrix));
1734
+ }
1735
+ if (from instanceof p5.Matrix && to === p5.Tree.SCREEN) {
1736
+ const m = this.dMatrix({ matrix: _invert(from) }); // mat3
1737
+ return this._worldToScreenDirection(m.multiplyVec3(vector), pMatrix);
1738
+ }
1739
+ if (from instanceof p5.Matrix && to instanceof p5.Matrix) {
1740
+ return this.dMatrix({ from, to }).multiplyVec3(vector); // mat3
1741
+ }
1742
+ if (from === p5.Tree.EYE && to === p5.Tree.WORLD) {
1743
+ return this.dMatrix({ matrix: vMatrix ?? this.vMatrix() }).multiplyVec3(vector); // mat3
1744
+ }
1745
+ if (from === p5.Tree.WORLD && to === p5.Tree.EYE) {
1746
+ return this.dMatrix({ matrix: eMatrix ?? this.eMatrix() }).multiplyVec3(vector); // mat3
1747
+ }
1748
+ if (from === p5.Tree.EYE && to === p5.Tree.SCREEN) {
1749
+ return this._worldToScreenDirection(
1750
+ this.dMatrix({ matrix: vMatrix ?? this.vMatrix() }).multiplyVec3(vector),
1751
+ pMatrix
1752
+ );
1753
+ }
1754
+ if (from === p5.Tree.SCREEN && to === p5.Tree.EYE) {
1755
+ return this.dMatrix({ matrix: eMatrix ?? this.eMatrix() }).multiplyVec3(
1756
+ this._screenToWorldDirection(vector, pMatrix)
1757
+ );
1758
+ }
1759
+ if (from === p5.Tree.EYE && to instanceof p5.Matrix) {
1760
+ const m = this.dMatrix({ matrix: (vMatrix ?? this.vMatrix()).apply(to) }); // mat3
1761
+ return m.multiplyVec3(vector);
1762
+ }
1763
+ if (from instanceof p5.Matrix && to === p5.Tree.EYE) {
1764
+ const m = this.dMatrix({ matrix: _invert(from).apply(eMatrix ?? this.eMatrix()) }); // mat3
1765
+ return m.multiplyVec3(vector);
1766
+ }
1767
+ if (from === p5.Tree.WORLD && to instanceof p5.Matrix) {
1768
+ return this.dMatrix({ matrix: to }).multiplyVec3(vector); // mat3
1769
+ }
1770
+ if (from instanceof p5.Matrix && to === p5.Tree.WORLD) {
1771
+ return this.dMatrix({ matrix: _invert(from) }).multiplyVec3(vector); // mat3
1772
+ }
1773
+
1774
+ if (from instanceof p5.Matrix && to === p5.Tree.NDC) {
1775
+ const m = this.dMatrix({ matrix: _invert(from) }); // mat3
1776
+ return this._screenToNDCDirection(this._worldToScreenDirection(m.multiplyVec3(vector), pMatrix));
1777
+ }
1778
+ if (from === p5.Tree.NDC && to instanceof p5.Matrix) {
1779
+ const m = this.dMatrix({ matrix: to }); // mat3
1780
+ return m.multiplyVec3(
1781
+ this._screenToWorldDirection(this._ndcToScreenDirection(vector), pMatrix)
1782
+ );
1783
+ }
1784
+ console.error('[p5.tree] transformDirection: could not parse query.');
1785
+ return vector;
1786
+ };
1787
+
1788
+ p5.RendererGL.prototype._worldToScreenDirection = function (vector, pMatrix) {
1789
+ pMatrix = pMatrix ?? this.pMatrix();
1790
+ const eyeVector = this._direction(vector, { from: p5.Tree.WORLD, to: p5.Tree.EYE });
1791
+ let dx = eyeVector.x;
1792
+ let dy = eyeVector.y;
1793
+ const perspective = pMatrix.mat4[15] === 0;
1794
+ if (perspective) {
1795
+ const zEye = this._position(p5.Tree.ORIGIN, { from: p5.Tree.WORLD, to: p5.Tree.EYE }).z;
1796
+ const k = Math.abs(zEye * Math.tan(pMatrix.fov() / 2));
1797
+ dx /= 2 * k / this.height;
1798
+ dy /= 2 * k / this.height;
1799
+ }
1800
+ let dz = eyeVector.z;
1801
+ dz /= (pMatrix.nPlane() - pMatrix.fPlane()) / (
1802
+ perspective
1803
+ ? Math.tan(pMatrix.fov() / 2)
1804
+ : Math.abs(pMatrix.rPlane() - pMatrix.lPlane()) / this.width
1805
+ );
1806
+ return new p5.Vector(dx, dy, dz);
1807
+ };
1808
+
1809
+ p5.RendererGL.prototype._screenToWorldDirection = function (vector, pMatrix) {
1810
+ pMatrix = pMatrix ?? this.pMatrix();
1811
+
1812
+ let dx = vector.x;
1813
+ let dy = vector.y;
1814
+
1815
+ const perspective = pMatrix.mat4[15] === 0;
1816
+ if (perspective) {
1817
+ const zEye = this._position(p5.Tree.ORIGIN, { from: p5.Tree.WORLD, to: p5.Tree.EYE }).z;
1818
+ const k = Math.abs(zEye * Math.tan(pMatrix.fov() / 2));
1819
+ dx *= 2 * k / this.height;
1820
+ dy *= 2 * k / this.height;
1821
+ }
1822
+
1823
+ let dz = vector.z;
1824
+ dz *= (pMatrix.nPlane() - pMatrix.fPlane()) / (
1825
+ perspective
1826
+ ? Math.tan(pMatrix.fov() / 2)
1827
+ : Math.abs(pMatrix.rPlane() - pMatrix.lPlane()) / this.width
1828
+ );
1829
+
1830
+ return this._direction(new p5.Vector(dx, dy, dz), { from: p5.Tree.EYE, to: p5.Tree.WORLD });
1831
+ };
1832
+
1833
+ p5.RendererGL.prototype._ndcToScreenDirection = function (vector) {
1834
+ return new p5.Vector(this.width * vector.x / 2, this.height * vector.y / 2, vector.z / 2);
1835
+ };
1836
+
1837
+ p5.RendererGL.prototype._screenToNDCDirection = function (vector) {
1838
+ return new p5.Vector(2 * vector.x / this.width, 2 * vector.y / this.height, 2 * vector.z);
1839
+ };
1840
+
1841
+ // ---------------------------------------------------------------------------
1842
+ // Utilities
1843
+ // ---------------------------------------------------------------------------
1844
+
1845
+ /**
1846
+ * Returns the world-to-pixel ratio units at a given world point position.
1847
+ *
1848
+ * A line of `n * pixelRatio(point)` world units will be projected with a
1849
+ * length of `n` pixels on screen (locally around that point).
1850
+ *
1851
+ * - In orthographic projection, the ratio is constant.
1852
+ * - In perspective projection, the ratio depends on eye-space depth.
1853
+ *
1854
+ * Requires WEBGL.
1855
+ *
1856
+ * @param {p5.Vector|number[]} [point=p5.Tree.ORIGIN] World-space point.
1857
+ * @returns {number|undefined} World units per pixel at the given point.
1858
+ */
1859
+ fn.pixelRatio = function (point) {
1860
+ return _rendererGL(this)?.pixelRatio(point);
1861
+ };
1862
+
1863
+ /**
1864
+ * Returns the world-to-pixel ratio units at a given world point position.
1865
+ * @param {p5.Vector|number[]} [point=p5.Tree.ORIGIN]
1866
+ * @returns {number}
1867
+ */
1868
+ p5.RendererGL.prototype.pixelRatio = function (point = p5.Tree.ORIGIN) {
1869
+ return this.isOrtho()
1870
+ ? Math.abs(this.tPlane() - this.bPlane()) / this.height
1871
+ : 2 * Math.abs(
1872
+ this.transformPosition(point, { from: p5.Tree.WORLD, to: p5.Tree.EYE }).z
1873
+ ) * Math.tan(this.fov() / 2) / this.height;
1874
+ };
1875
+
1876
+ /**
1877
+ * Returns the normalized texel size for an image/texture.
1878
+ * Useful for offsetting UVs by exactly one pixel.
1879
+ *
1880
+ * @param {p5.Image|p5.Framebuffer|Object} image Any object exposing `width` and `height`.
1881
+ * @returns {number[]} `[1 / width, 1 / height]`
1882
+ */
1883
+ fn.texOffset = function (image) {
1884
+ return [1 / image.width, 1 / image.height];
1885
+ };
1886
+
1887
+ /**
1888
+ * Returns the current mouse position in *pixel* coordinates.
1889
+ * By default the y-axis is flipped so y=0 is at the bottom (HUD-style).
1890
+ *
1891
+ * @param {boolean} [flip=true] Whether to flip the y coordinate.
1892
+ * @returns {number[]} `[x, y]` in pixels (includes pixelDensity scaling).
1893
+ */
1894
+ fn.mousePosition = function (flip = true) {
1895
+ const pd = this.pixelDensity();
1896
+ return [pd * this.mouseX, pd * (flip ? this.height - this.mouseY : this.mouseY)];
1897
+ };
1898
+
1899
+ /**
1900
+ * Returns a pointer position in *pixel* coordinates from an arbitrary (x, y) pair.
1901
+ * Delegates to the WEBGL renderer.
1902
+ *
1903
+ * Accepts parameters in any order:
1904
+ * - `number, number` → pointerX, pointerY
1905
+ * - optional `boolean` → `flip`
1906
+ *
1907
+ * @param {...(number|boolean)} args
1908
+ * @returns {number[]|undefined} `[x, y]` in pixels, or undefined if not in WEBGL.
1909
+ */
1910
+ fn.pointerPosition = function (...args) {
1911
+ return _rendererGL(this)?.pointerPosition(...args);
1912
+ };
1913
+
1914
+ /**
1915
+ * Returns the canvas resolution in *pixel* coordinates.
1916
+ * Delegates to the WEBGL renderer.
1917
+ *
1918
+ * @returns {number[]|undefined} `[width, height]` in pixels, or undefined if not in WEBGL.
1919
+ */
1920
+ fn.resolution = function () {
1921
+ return _rendererGL(this)?.resolution();
1922
+ };
1923
+
1924
+ /**
1925
+ * Returns a pointer position in *pixel* coordinates from an arbitrary (x, y) pair.
1926
+ *
1927
+ * Accepts parameters in any order:
1928
+ * - `number, number` → pointerX, pointerY
1929
+ * - optional `boolean` → `flip`
1930
+ *
1931
+ * @param {...(number|boolean)} args
1932
+ * @returns {number[]} `[x, y]` in pixels (includes pixelDensity scaling).
1933
+ */
1934
+ p5.RendererGL.prototype.pointerPosition = function (...args) {
1935
+ let pointerX;
1936
+ let pointerY;
1937
+ let flip = true;
1938
+ for (const arg of args) {
1939
+ if (typeof arg === 'number') {
1940
+ // First number is x, second is y.
1941
+ pointerX === undefined ? (pointerX = arg) : (pointerY = arg);
1942
+ } else if (typeof arg === 'boolean') {
1943
+ flip = arg;
1944
+ }
1945
+ }
1946
+ const pd = this.pixelDensity();
1947
+ return [pd * pointerX, pd * (flip ? this.height - pointerY : pointerY)];
1948
+ };
1949
+
1950
+ /**
1951
+ * Returns the canvas resolution in *pixel* coordinates.
1952
+ * @returns {number[]} `[width, height]` in pixels (includes pixelDensity scaling).
1953
+ */
1954
+ p5.RendererGL.prototype.resolution = function () {
1955
+ const pd = this.pixelDensity();
1956
+ return [pd * this.width, pd * this.height];
1957
+ };
1958
+
1959
+ // -------------------------------------------------------------------------
1960
+ // Drawing helpers (axes / grid)
1961
+ // -------------------------------------------------------------------------
1962
+
1963
+ fn.axes = function (opts) {
1964
+ _rendererGL(this)?.axes(opts);
1965
+ return this;
1966
+ };
1967
+
1968
+ p5.RendererGL.prototype.axes = function ({
1969
+ size = 100,
1970
+ colors = ['Red', 'Lime', 'DodgerBlue'],
1971
+ bits = p5.Tree.LABELS | p5.Tree.X | p5.Tree.Y | p5.Tree.Z
1972
+ } = {}) {
1973
+ const p = this._pInst;
1974
+ if (!p) return;
1975
+ p.push();
1976
+ if ((bits & p5.Tree.LABELS) !== 0) {
1977
+ const charWidth = size / 40.0;
1978
+ const charHeight = size / 30.0;
1979
+ const charShift = 1.04 * size;
1980
+ // The X
1981
+ p.stroke(colors[0 % colors.length]);
1982
+ p.line(charShift, charWidth, -charHeight, charShift, -charWidth, charHeight);
1983
+ p.line(charShift, -charWidth, -charHeight, charShift, charWidth, charHeight);
1984
+ // The Y
1985
+ p.stroke(colors[1 % colors.length]);
1986
+ p.line(charWidth, charShift, charHeight, 0.0, charShift, 0.0);
1987
+ p.line(0.0, charShift, 0.0, -charWidth, charShift, charHeight);
1988
+ p.line(-charWidth, charShift, charHeight, 0.0, charShift, 0.0);
1989
+ p.line(0.0, charShift, 0.0, 0.0, charShift, -charHeight);
1990
+ // The Z
1991
+ p.stroke(colors[2 % colors.length]);
1992
+ p.line(-charWidth, -charHeight, charShift, charWidth, -charHeight, charShift);
1993
+ p.line(charWidth, -charHeight, charShift, -charWidth, charHeight, charShift);
1994
+ p.line(-charWidth, charHeight, charShift, charWidth, charHeight, charShift);
1995
+ }
1996
+ // X Axis
1997
+ p.stroke(colors[0 % colors.length]);
1998
+ (bits & p5.Tree.X) !== 0 && p.line(0, 0, 0, size, 0, 0);
1999
+ (bits & p5.Tree._X) !== 0 && p.line(0, 0, 0, -size, 0, 0);
2000
+ // Y Axis
2001
+ p.stroke(colors[1 % colors.length]);
2002
+ (bits & p5.Tree.Y) !== 0 && p.line(0, 0, 0, 0, size, 0);
2003
+ (bits & p5.Tree._Y) !== 0 && p.line(0, 0, 0, 0, -size, 0);
2004
+ // Z Axis
2005
+ p.stroke(colors[2 % colors.length]);
2006
+ (bits & p5.Tree.Z) !== 0 && p.line(0, 0, 0, 0, 0, size);
2007
+ (bits & p5.Tree._Z) !== 0 && p.line(0, 0, 0, 0, 0, -size);
2008
+
2009
+ p.pop();
2010
+ };
2011
+
2012
+ fn.grid = function (opts) {
2013
+ _rendererGL(this)?.grid(opts);
2014
+ return this;
2015
+ };
2016
+
2017
+ p5.RendererGL.prototype.grid = function ({
2018
+ size = 100,
2019
+ subdivisions = 10,
2020
+ style = p5.Tree.SOLID,
2021
+ weight = 1,
2022
+ minorSubdivisions = 5
2023
+ } = {}) {
2024
+ const p = this._pInst;
2025
+ if (!p) return;
2026
+ p.push();
2027
+ if (style === p5.Tree.DOTS) {
2028
+ let posi = 0;
2029
+ let posj = 0;
2030
+ p.strokeWeight(weight * 2);
2031
+ p.beginShape(p.POINTS);
2032
+ for (let i = 0; i <= subdivisions; ++i) {
2033
+ posi = size * (2.0 * i / subdivisions - 1.0);
2034
+ for (let j = 0; j <= subdivisions; ++j) {
2035
+ posj = size * (2.0 * j / subdivisions - 1.0);
2036
+ p.vertex(posi, posj, 0);
2037
+ }
2038
+ }
2039
+ p.endShape();
2040
+ const internalSub = Math.max(1, minorSubdivisions | 0);
2041
+ const subSubdivisions = subdivisions * internalSub;
2042
+ p.strokeWeight(weight);
2043
+ p.beginShape(p.POINTS);
2044
+ for (let i = 0; i <= subSubdivisions; ++i) {
2045
+ posi = size * (2.0 * i / subSubdivisions - 1.0);
2046
+ for (let j = 0; j <= subSubdivisions; ++j) {
2047
+ posj = size * (2.0 * j / subSubdivisions - 1.0);
2048
+ ((i % internalSub) !== 0 || (j % internalSub) !== 0) && p.vertex(posi, posj, 0);
2049
+ }
2050
+ }
2051
+ p.endShape();
2052
+ } else {
2053
+ for (let i = 0; i <= subdivisions; ++i) {
2054
+ const pos = size * (2.0 * i / subdivisions - 1.0);
2055
+ p.line(pos, -size, 0, pos, +size, 0);
2056
+ p.line(-size, pos, 0, size, pos, 0);
2057
+ }
2058
+ }
2059
+ p.pop();
2060
+ };
2061
+
2062
+ // ---------------------------------------------------------------------------
2063
+ // Picking
2064
+ // ---------------------------------------------------------------------------
2065
+
2066
+ /**
2067
+ * Returns `true` if the mouse is close enough to a target screen position.
2068
+ *
2069
+ * If `x`/`y` are not provided, they are derived by projecting `mMatrix` to
2070
+ * `p5.Tree.SCREEN`. In that case, `size` is interpreted in *world units* and
2071
+ * converted to pixels using `pixelRatio()` at the corresponding world point.
2072
+ *
2073
+ * Requires WEBGL.
2074
+ *
2075
+ * @param {object} [opts]
2076
+ * @param {p5.Matrix} [opts.mMatrix] Model-space matrix origin to compute (x, y) from.
2077
+ * @param {number} [opts.x] Screen x coordinate in HUD space (pixels).
2078
+ * @param {number} [opts.y] Screen y coordinate in HUD space (pixels).
2079
+ * @param {number} [opts.size=50] Picking diameter (pixels in HUD space, or world units when deriving x/y).
2080
+ * @param {number} [opts.shape=p5.Tree.CIRCLE] Either `p5.Tree.CIRCLE` or `p5.Tree.SQUARE`.
2081
+ * @param {p5.Matrix} [opts.eMatrix] Eye matrix override.
2082
+ * @param {p5.Matrix} [opts.pMatrix] Projection matrix override.
2083
+ * @param {p5.Matrix} [opts.vMatrix] View (camera) matrix override.
2084
+ * @param {p5.Matrix} [opts.pvMatrix] Projection-view matrix override.
2085
+ * @returns {boolean|undefined}
2086
+ */
2087
+ fn.mousePicking = function (opts) {
2088
+ return _rendererGL(this)?.mousePicking(opts);
2089
+ };
2090
+
2091
+ /**
2092
+ * Returns `true` if a pointer is close enough to a target screen position.
2093
+ *
2094
+ * If `x`/`y` are not provided, they are derived by projecting `mMatrix` to
2095
+ * `p5.Tree.SCREEN`. In that case, `size` is interpreted in *world units* and
2096
+ * converted to pixels using `pixelRatio()` at the corresponding world point.
2097
+ *
2098
+ * Requires WEBGL.
2099
+ *
2100
+ * @param {...any} args
2101
+ * @returns {boolean|undefined}
2102
+ */
2103
+ fn.pointerPicking = function (...args) {
2104
+ return _rendererGL(this)?.pointerPicking(...args);
2105
+ };
2106
+
2107
+ p5.RendererGL.prototype.mousePicking = function ({
2108
+ mMatrix = this.mMatrix(),
2109
+ x,
2110
+ y,
2111
+ size = 50,
2112
+ shape = p5.Tree.CIRCLE,
2113
+ eMatrix,
2114
+ pMatrix,
2115
+ vMatrix,
2116
+ pvMatrix
2117
+ } = {}) {
2118
+ const p = this._pInst;
2119
+ if (!p) return false;
2120
+ return this.pointerPicking(p.mouseX, p.mouseY, { mMatrix, x, y, size, shape, eMatrix, pMatrix, vMatrix, pvMatrix });
2121
+ };
2122
+
2123
+ /**
2124
+ * Returns `true` if pointer is close enough to a target screen position.
2125
+ *
2126
+ * Supported call patterns:
2127
+ * - `pointerPicking(pointerX, pointerY, opts)`
2128
+ * - `pointerPicking(opts)` (pointer defaults to current mouse if available)
2129
+ *
2130
+ * @param {...any} args
2131
+ * @returns {boolean}
2132
+ */
2133
+ p5.RendererGL.prototype.pointerPicking = function (...args) {
2134
+ let pointerX;
2135
+ let pointerY;
2136
+ const config = {};
2137
+ for (const arg of args) {
2138
+ if (typeof arg === 'number' && Number.isFinite(arg)) {
2139
+ pointerX == null ? pointerX = arg : pointerY = arg;
2140
+ } else if (arg && typeof arg === 'object') {
2141
+ Object.assign(config, arg);
2142
+ }
2143
+ }
2144
+ const p = this._pInst;
2145
+ if (pointerX == null) pointerX = p ? p.mouseX : this.width / 2;
2146
+ if (pointerY == null) pointerY = p ? p.mouseY : this.height / 2;
2147
+ let {
2148
+ mMatrix = this.mMatrix(),
2149
+ x,
2150
+ y,
2151
+ size = 50,
2152
+ shape = p5.Tree.CIRCLE,
2153
+ eMatrix,
2154
+ pMatrix,
2155
+ vMatrix,
2156
+ pvMatrix
2157
+ } = config;
2158
+ // If target screen position not provided, derive it from mMatrix.
2159
+ // In that case, treat `size` as world units and convert to pixels locally.
2160
+ if (x == null || y == null) {
2161
+ const screen = this.transformPosition({ from: mMatrix, to: p5.Tree.SCREEN, pMatrix, vMatrix, pvMatrix });
2162
+ x = screen.x;
2163
+ y = screen.y;
2164
+ const world = this.transformPosition({ from: mMatrix, to: p5.Tree.WORLD, eMatrix });
2165
+ size = size / this.pixelRatio(world);
2166
+ }
2167
+ const r = size / 2.0;
2168
+ const dx = x - pointerX;
2169
+ const dy = y - pointerY;
2170
+ return shape === p5.Tree.CIRCLE
2171
+ ? Math.sqrt(dx * dx + dy * dy) < r
2172
+ : (Math.abs(dx) < r && Math.abs(dy) < r);
2173
+ };
2174
+
2175
+ // -------------------------------------------------------------------------
2176
+ // Drawing helpers (bullsEye / cross)
2177
+ // -------------------------------------------------------------------------
2178
+
2179
+ /**
2180
+ * @private
2181
+ * Draws a circle primitive in the *current* renderer space.
2182
+ *
2183
+ * This is a geometry primitive (lines / triangles in the XY plane at z=0),
2184
+ * so it can be used in 3D *or* in HUD/screen space depending on the caller:
2185
+ * - Call inside `beginHUD()/endHUD()` to interpret `x,y,radius` in screen pixels.
2186
+ * - Call outside HUD to interpret them in the current 3D space units.
2187
+ *
2188
+ * @param {object} [opts]
2189
+ * @param {boolean} [opts.filled=false] Whether to fill the circle.
2190
+ * @param {number} [opts.x=width/2] Center x in current space.
2191
+ * @param {number} [opts.y=height/2] Center y in current space.
2192
+ * @param {number} [opts.radius=100] Radius in current space.
2193
+ * @param {number} [opts.detail=50] Segment count.
2194
+ */
2195
+ p5.RendererGL.prototype._circle = function ({
2196
+ filled = false,
2197
+ x = this.width / 2,
2198
+ y = this.height / 2,
2199
+ radius = 100,
2200
+ detail = 50
2201
+ } = {}) {
2202
+ const p = this._pInst;
2203
+ if (!p) return;
2204
+ p.push();
2205
+ p.translate(x, y);
2206
+ if (filled) {
2207
+ p.beginShape(p.TRIANGLE_STRIP);
2208
+ for (let t = 0; t <= detail; t++) {
2209
+ const cx = Math.cos(t * (2 * Math.PI) / detail);
2210
+ const cy = Math.sin(t * (2 * Math.PI) / detail);
2211
+ p.vertex(0, 0, 0, 0.5, 0.5);
2212
+ p.vertex(radius * cx, radius * cy, 0, (cx * 0.5) + 0.5, (cy * 0.5) + 0.5);
2213
+ }
2214
+ p.endShape();
2215
+ } else {
2216
+ const angle = (2 * Math.PI) / detail;
2217
+ let last = { x: radius, y: 0 };
2218
+ for (let i = 1; i <= detail; i++) {
2219
+ const pos = { x: Math.cos(i * angle) * radius, y: Math.sin(i * angle) * radius };
2220
+ p.line(last.x, last.y, pos.x, pos.y);
2221
+ last = pos;
2222
+ }
2223
+ }
2224
+ p.pop();
2225
+ };
2226
+
2227
+ /**
2228
+ * Draws a cross in HUD space (`x,y` in screen coordinates).
2229
+ *
2230
+ * If `x` and `y` are not provided, the cross is placed at the screen position
2231
+ * corresponding to the origin of `mMatrix`.
2232
+ *
2233
+ * If `mMatrix` is used (x/y omitted), `size` is interpreted in world units
2234
+ * and converted to pixels using `pixelRatio()` at the corresponding world point.
2235
+ *
2236
+ * @param {object} [opts]
2237
+ * @param {p5.Matrix} [opts.mMatrix] Model-space matrix origin to compute (x, y) from.
2238
+ * @param {number} [opts.x] Screen x coordinate in HUD space (pixels).
2239
+ * @param {number} [opts.y] Screen y coordinate in HUD space (pixels).
2240
+ * @param {number} [opts.size=50] Cross size (pixels in HUD space, or world units when deriving x/y).
2241
+ * @param {p5.Matrix} [opts.eMatrix] Eye matrix override.
2242
+ * @param {p5.Matrix} [opts.pMatrix] Projection matrix override.
2243
+ * @param {p5.Matrix} [opts.vMatrix] View (camera) matrix override.
2244
+ * @param {p5.Matrix} [opts.pvMatrix] Projection-view matrix override.
2245
+ */
2246
+ fn.cross = function (opts) {
2247
+ _rendererGL(this)?.cross(opts);
2248
+ return this;
2249
+ };
2250
+
2251
+ p5.RendererGL.prototype.cross = function ({
2252
+ mMatrix = this.mMatrix(),
2253
+ x,
2254
+ y,
2255
+ size = 50,
2256
+ eMatrix,
2257
+ pMatrix,
2258
+ vMatrix,
2259
+ pvMatrix
2260
+ } = {}) {
2261
+ const p = this._pInst;
2262
+ if (!p) return;
2263
+
2264
+ if (x == null || y == null) {
2265
+ const screen = this.transformPosition({ from: mMatrix, to: p5.Tree.SCREEN, pMatrix, vMatrix, pvMatrix });
2266
+ x = screen.x;
2267
+ y = screen.y;
2268
+ const world = this.transformPosition({ from: mMatrix, to: p5.Tree.WORLD, eMatrix });
2269
+ size = size / this.pixelRatio(world);
2270
+ }
2271
+ const half = size / 2.0;
2272
+ this.beginHUD();
2273
+ p.line(x - half, y, x + half, y);
2274
+ p.line(x, y - half, x, y + half);
2275
+ this.endHUD();
2276
+ };
2277
+
2278
+ /**
2279
+ * Draws a bulls-eye on the screen (HUD space): either a circle or square-corners,
2280
+ * plus a center cross.
2281
+ *
2282
+ * If `x` and `y` are not provided, the bulls-eye is placed at the screen position
2283
+ * corresponding to the origin of `mMatrix`.
2284
+ *
2285
+ * If `mMatrix` is used (x/y omitted), `size` is interpreted in world units
2286
+ * and converted to pixels using `pixelRatio()` at the corresponding world point.
2287
+ *
2288
+ * @param {object} [opts]
2289
+ * @param {p5.Matrix} [opts.mMatrix] Model-space matrix origin to compute (x, y) from.
2290
+ * @param {number} [opts.x] Screen x coordinate in HUD space (pixels).
2291
+ * @param {number} [opts.y] Screen y coordinate in HUD space (pixels).
2292
+ * @param {number} [opts.size=50] Bulls-eye diameter (pixels in HUD space, or world units when deriving x/y).
2293
+ * @param {number} [opts.shape=p5.Tree.CIRCLE] Either `p5.Tree.CIRCLE` or `p5.Tree.SQUARE`.
2294
+ * @param {p5.Matrix} [opts.eMatrix] Eye matrix override.
2295
+ * @param {p5.Matrix} [opts.pMatrix] Projection matrix override.
2296
+ * @param {p5.Matrix} [opts.vMatrix] View (camera) matrix override.
2297
+ * @param {p5.Matrix} [opts.pvMatrix] Projection-view matrix override.
2298
+ */
2299
+ fn.bullsEye = function (opts) {
2300
+ _rendererGL(this)?.bullsEye(opts);
2301
+ return this;
2302
+ };
2303
+
2304
+ p5.RendererGL.prototype.bullsEye = function ({
2305
+ mMatrix = this.mMatrix(),
2306
+ x,
2307
+ y,
2308
+ size = 50,
2309
+ shape = p5.Tree.CIRCLE,
2310
+ eMatrix,
2311
+ pMatrix,
2312
+ vMatrix,
2313
+ pvMatrix
2314
+ } = {}) {
2315
+ const p = this._pInst;
2316
+ if (!p) return;
2317
+ if (x == null || y == null) {
2318
+ const screen = this.transformPosition({ from: mMatrix, to: p5.Tree.SCREEN, pMatrix, vMatrix, pvMatrix });
2319
+ x = screen.x;
2320
+ y = screen.y;
2321
+ const world = this.transformPosition({ from: mMatrix, to: p5.Tree.WORLD, eMatrix });
2322
+ size = size / this.pixelRatio(world);
2323
+ }
2324
+ const half = size / 2.0;
2325
+ const corner = 0.6 * half;
2326
+ this.beginHUD();
2327
+ if (shape === p5.Tree.CIRCLE) {
2328
+ this._circle({ x, y, radius: half });
2329
+ } else {
2330
+ p.line(x - half, y - half + corner, x - half, y - half);
2331
+ p.line(x - half, y - half, x - half + corner, y - half);
2332
+ p.line(x + half - corner, y - half, x + half, y - half);
2333
+ p.line(x + half, y - half, x + half, y - half + corner);
2334
+ p.line(x + half, y + half - corner, x + half, y + half);
2335
+ p.line(x + half, y + half, x + half - corner, y + half);
2336
+ p.line(x - half + corner, y + half, x - half, y + half);
2337
+ p.line(x - half, y + half, x - half, y + half - corner);
2338
+ }
2339
+ // Center cross (0.6 * size), in HUD space.
2340
+ const crossHalf = 0.6 * half;
2341
+ p.line(x - crossHalf, y, x + crossHalf, y);
2342
+ p.line(x, y - crossHalf, x, y + crossHalf);
2343
+ this.endHUD();
2344
+ };
2345
+
2346
+ // ---------------------------------------------------------------------------
2347
+ // View frustum (pg frustum display)
2348
+ // ---------------------------------------------------------------------------
2349
+
2350
+ fn.viewFrustum = function (opts) {
2351
+ _rendererGL(this)?.viewFrustum(opts);
2352
+ return this;
2353
+ };
2354
+
2355
+ /**
2356
+ * Displays a view frustum, either from a pg (p5.Graphics / p5.RendererGL) or from eMatrix/pMatrix.
2357
+ *
2358
+ * @param {Object} [opts]
2359
+ * @param {p5.Matrix} [opts.vMatrix=this.vMatrix()] desired view matrix (world -> this eye) for drawing the frustum.
2360
+ * @param {p5.RendererGL|p5.Graphics} [opts.pg] renderer/pg whose frustum is to be displayed.
2361
+ * @param {p5.Matrix} [opts.eMatrix=pg?.eMatrix()] eye matrix defining frustum pose (eye -> world).
2362
+ * @param {p5.Matrix} [opts.pMatrix=pg?.pMatrix()] projection matrix defining frustum projection.
2363
+ * @param {number} [opts.bits=p5.Tree.NEAR|p5.Tree.FAR] bitmask (NEAR/FAR/BODY/APEX).
2364
+ * @param {Function|false|null} [opts.viewer=...] callback drawn at the frustum origin (in frustum space).
2365
+ */
2366
+ p5.RendererGL.prototype.viewFrustum = function ({
2367
+ vMatrix = this.vMatrix(),
2368
+ pg,
2369
+ eMatrix = pg?.eMatrix(),
2370
+ pMatrix = pg?.pMatrix(),
2371
+ bits = p5.Tree.NEAR | p5.Tree.FAR,
2372
+ viewer = () => this.axes({ size: 50, bits: p5.Tree.X | p5.Tree._X | p5.Tree.Y | p5.Tree._Y | p5.Tree.Z | p5.Tree._Z })
2373
+ } = {}) {
2374
+ const p = this._pInst;
2375
+ if (!p) return;
2376
+ if (this === pg) {
2377
+ console.error('displaying viewFrustum requires a pg different than this');
2378
+ return;
2379
+ }
2380
+ if (!pMatrix || !eMatrix) {
2381
+ console.error('displaying viewFrustum requires either a pg or projection and eye matrices');
2382
+ return;
2383
+ }
2384
+ const states = this.states;
2385
+ const uView = states?.uViewMatrix;
2386
+ if (!uView) return;
2387
+ p.push();
2388
+ p.resetMatrix();
2389
+ // Override view matrix in-place (fast path: no inversion).
2390
+ // Save previous values so we can restore them after drawing.
2391
+ const prevView = uView.copy();
2392
+ uView.set(vMatrix);
2393
+ // Apply frustum camera pose (eye -> world) as a model transform.
2394
+ this.applyMatrix(...eMatrix.mat4);
2395
+ // Optional viewer at frustum origin
2396
+ typeof viewer === 'function' && viewer();
2397
+ const isOrtho = pMatrix.isOrtho();
2398
+ const apex = !isOrtho && ((bits & p5.Tree.APEX) !== 0);
2399
+ const n = -pMatrix.nPlane();
2400
+ const f = -pMatrix.fPlane();
2401
+ const l = pMatrix.lPlane();
2402
+ const r = pMatrix.rPlane();
2403
+ // hack preserved (sign handling for t/b differs in ortho vs persp)
2404
+ const t = isOrtho ? -pMatrix.tPlane() : pMatrix.tPlane();
2405
+ const b = isOrtho ? -pMatrix.bPlane() : pMatrix.bPlane();
2406
+ // far plane corners
2407
+ const ratio = isOrtho ? 1 : f / n;
2408
+ const _l = ratio * l;
2409
+ const _r = ratio * r;
2410
+ const _b = ratio * b;
2411
+ const _t = ratio * t;
2412
+ // FAR plane
2413
+ if ((bits & p5.Tree.FAR) !== 0) {
2414
+ this.beginShape();
2415
+ this.vertex(_l, _t, f);
2416
+ this.vertex(_r, _t, f);
2417
+ this.vertex(_r, _b, f);
2418
+ this.vertex(_l, _b, f);
2419
+ this.endShape(p5.prototype.CLOSE);
2420
+ } else {
2421
+ this.line(_l, _t, f, _r, _t, f);
2422
+ this.line(_r, _t, f, _r, _b, f);
2423
+ this.line(_r, _b, f, _l, _b, f);
2424
+ this.line(_l, _b, f, _l, _t, f);
2425
+ }
2426
+ // BODY
2427
+ if ((bits & p5.Tree.BODY) !== 0) {
2428
+ this.beginShape();
2429
+ this.vertex(_l, _t, f);
2430
+ this.vertex(l, t, n);
2431
+ this.vertex(r, t, n);
2432
+ this.vertex(_r, _t, f);
2433
+ this.endShape();
2434
+ this.beginShape();
2435
+ this.vertex(_r, _t, f);
2436
+ this.vertex(r, t, n);
2437
+ this.vertex(r, b, n);
2438
+ this.vertex(_r, _b, f);
2439
+ this.endShape();
2440
+ this.beginShape();
2441
+ this.vertex(_r, _b, f);
2442
+ this.vertex(r, b, n);
2443
+ this.vertex(l, b, n);
2444
+ this.vertex(_l, _b, f);
2445
+ this.endShape();
2446
+ this.beginShape();
2447
+ this.vertex(l, t, n);
2448
+ this.vertex(_l, _t, f);
2449
+ this.vertex(_l, _b, f);
2450
+ this.vertex(l, b, n);
2451
+ this.endShape();
2452
+ if (apex) {
2453
+ this.line(0, 0, 0, r, t, n);
2454
+ this.line(0, 0, 0, l, t, n);
2455
+ this.line(0, 0, 0, l, b, n);
2456
+ this.line(0, 0, 0, r, b, n);
2457
+ }
2458
+ } else {
2459
+ this.line(apex ? 0 : r, apex ? 0 : t, apex ? 0 : n, _r, _t, f);
2460
+ this.line(apex ? 0 : l, apex ? 0 : t, apex ? 0 : n, _l, _t, f);
2461
+ this.line(apex ? 0 : l, apex ? 0 : b, apex ? 0 : n, _l, _b, f);
2462
+ this.line(apex ? 0 : r, apex ? 0 : b, apex ? 0 : n, _r, _b, f);
2463
+ }
2464
+ // NEAR plane
2465
+ if ((bits & p5.Tree.NEAR) !== 0) {
2466
+ this.beginShape();
2467
+ this.vertex(l, t, n);
2468
+ this.vertex(r, t, n);
2469
+ this.vertex(r, b, n);
2470
+ this.vertex(l, b, n);
2471
+ this.endShape(p5.prototype.CLOSE);
2472
+ } else {
2473
+ this.line(l, t, n, r, t, n);
2474
+ this.line(r, t, n, r, b, n);
2475
+ this.line(r, b, n, l, b, n);
2476
+ this.line(l, b, n, l, t, n);
2477
+ }
2478
+ // Restore previous view matrix (no try/finally as requested).
2479
+ uView.set(prevView);
2480
+ p.pop();
2481
+ };
2482
+
2483
+ // ---------------------------------------------------------------------------
2484
+ // Visibility (frustum culling queries)
2485
+ // ---------------------------------------------------------------------------
2486
+
2487
+ /**
2488
+ * Returns object visibility with respect to the current view frustum.
2489
+ * Object may be either:
2490
+ * - a point (center),
2491
+ * - a sphere (center + radius),
2492
+ * - or an axis-aligned box (corner1 + corner2).
2493
+ *
2494
+ * @returns {number} One of p5.Tree.VISIBLE, p5.Tree.INVISIBLE, p5.Tree.SEMIVISIBLE.
2495
+ */
2496
+ fn.visibility = function (...args) {
2497
+ return _rendererGL(this).visibility(...args);
2498
+ };
2499
+
2500
+ /**
2501
+ * Returns the 6 plane equations of the view frustum in world space.
2502
+ * @returns {Object}
2503
+ */
2504
+ fn.bounds = function (opts = {}) {
2505
+ return _rendererGL(this).bounds(opts);
2506
+ };
2507
+
2508
+ /**
2509
+ * Returns signed distance from a point to a frustum plane.
2510
+ * @returns {number}
2511
+ */
2512
+ fn.distanceToBound = function (...args) {
2513
+ return _rendererGL(this).distanceToBound(...args);
2514
+ };
2515
+
2516
+ /**
2517
+ * Parses visibility query arguments.
2518
+ * Supports:
2519
+ * - visibility({ corner1, corner2, center, radius, bounds })
2520
+ * - visibility(center[, radius][, bounds])
2521
+ * - visibility(corner1, corner2[, bounds])
2522
+ *
2523
+ * @private
2524
+ */
2525
+ p5.RendererGL.prototype._parseVisibilityArgs = function (...args) {
2526
+ let corner1;
2527
+ let corner2;
2528
+ let center;
2529
+ let radius;
2530
+ let pendingRadius;
2531
+ let bounds;
2532
+ const vecs = [];
2533
+ const isPlainObject = v => {
2534
+ if (!v || typeof v !== 'object') return false;
2535
+ if (Array.isArray(v)) return false;
2536
+ if (ArrayBuffer.isView(v)) return false;
2537
+ return Object.getPrototypeOf(v) === Object.prototype;
2538
+ };
2539
+ for (const arg of args) {
2540
+ if (arg instanceof p5.Vector || Array.isArray(arg)) {
2541
+ vecs.push(arg);
2542
+ continue;
2543
+ }
2544
+ if (typeof arg === 'number' && Number.isFinite(arg) && radius === undefined) {
2545
+ // Only accept a radius if we already have (or will infer) a center.
2546
+ center ? (radius = arg) : (pendingRadius = arg);
2547
+ continue;
2548
+ }
2549
+ if (isPlainObject(arg)) {
2550
+ if ('corner1' in arg || 'corner2' in arg || 'center' in arg || 'radius' in arg || 'bounds' in arg) {
2551
+ corner1 = arg.corner1 ?? corner1;
2552
+ corner2 = arg.corner2 ?? corner2;
2553
+ center = arg.center ?? center;
2554
+ radius = arg.radius ?? radius;
2555
+ bounds = arg.bounds ?? bounds;
2556
+ } else {
2557
+ bounds = arg;
2558
+ }
2559
+ }
2560
+ }
2561
+ // Ordering rule: if 2 vectors are provided, first is corner1, second is corner2.
2562
+ if (!corner1 && !corner2) {
2563
+ if (!center && vecs.length === 1) {
2564
+ center = vecs[0];
2565
+ } else if (!corner1 && !corner2 && vecs.length >= 2) {
2566
+ corner1 = vecs[0];
2567
+ corner2 = vecs[1];
2568
+ }
2569
+ }
2570
+ // Commit leading radius only if we ended up with a center (supports visibility(radius, center[, bounds])).
2571
+ if (radius === undefined && pendingRadius !== undefined && center) {
2572
+ radius = pendingRadius;
2573
+ }
2574
+ return { corner1, corner2, center, radius, bounds };
2575
+ };
2576
+
2577
+ /**
2578
+ * Returns object visibility with respect to the current view frustum.
2579
+ *
2580
+ * Supported forms:
2581
+ * - visibility(center[, radius][, bounds])
2582
+ * - visibility(radius, center[, bounds])
2583
+ * - visibility(corner1, corner2[, bounds])
2584
+ * - visibility({ corner1, corner2, center, radius, bounds })
2585
+ *
2586
+ * @param {Object} [opts]
2587
+ * @param {p5.Vector|number[]} [opts.corner1] First box corner (use with corner2).
2588
+ * @param {p5.Vector|number[]} [opts.corner2] Second box corner (use with corner1).
2589
+ * @param {p5.Vector|number[]} [opts.center] Sphere (or point) center.
2590
+ * @param {number} [opts.radius] Sphere radius (if omitted, center is treated as point).
2591
+ * @param {Object} [opts.bounds] Frustum plane equations (defaults to this.bounds()).
2592
+ * @returns {number} One of p5.Tree.VISIBLE, p5.Tree.INVISIBLE, p5.Tree.SEMIVISIBLE.
2593
+ */
2594
+ p5.RendererGL.prototype.visibility = function (...args) {
2595
+ const { corner1, corner2, center, radius, bounds } = this._parseVisibilityArgs(...args);
2596
+ const b = bounds ?? this.bounds();
2597
+ return center ? (radius ? this._ballVisibility(center, radius, b) : this._pointVisibility(center, b))
2598
+ : (corner1 && corner2 ? this._boxVisibility(corner1, corner2, b)
2599
+ : (console.error('[p5.tree] visibility: could not parse query.'), p5.Tree.INVISIBLE));
2600
+ };
2601
+
2602
+ p5.RendererGL.prototype._pointVisibility = function (point, bounds = this.bounds()) {
2603
+ for (const key in bounds) {
2604
+ const d = this.distanceToBound(point, key, bounds);
2605
+ if (d > 0) return p5.Tree.INVISIBLE;
2606
+ if (d === 0) return p5.Tree.SEMIVISIBLE;
2607
+ }
2608
+ return p5.Tree.VISIBLE;
2609
+ };
2610
+
2611
+ p5.RendererGL.prototype._ballVisibility = function (center, radius, bounds = this.bounds()) {
2612
+ let allInForAllPlanes = true;
2613
+ for (const key in bounds) {
2614
+ const d = this.distanceToBound(center, key, bounds);
2615
+ if (d > radius) return p5.Tree.INVISIBLE;
2616
+ if (d > 0 || -d < radius) allInForAllPlanes = false;
2617
+ }
2618
+ return allInForAllPlanes ? p5.Tree.VISIBLE : p5.Tree.SEMIVISIBLE;
2619
+ };
2620
+
2621
+ p5.RendererGL.prototype._boxVisibility = function (corner1, corner2, bounds = this.bounds()) {
2622
+ const asVec3 = v =>
2623
+ v instanceof p5.Vector ? v : new p5.Vector(v?.[0] ?? 0, v?.[1] ?? 0, v?.[2] ?? 0);
2624
+ corner1 = asVec3(corner1);
2625
+ corner2 = asVec3(corner2);
2626
+ let allInForAllPlanes = true;
2627
+ for (const key in bounds) {
2628
+ let allOut = true;
2629
+ for (let c = 0; c < 8; ++c) {
2630
+ const pos = new p5.Vector(
2631
+ (c & 4) !== 0 ? corner1.x : corner2.x,
2632
+ (c & 2) !== 0 ? corner1.y : corner2.y,
2633
+ (c & 1) !== 0 ? corner1.z : corner2.z
2634
+ );
2635
+ if (this.distanceToBound(pos, key, bounds) > 0) {
2636
+ allInForAllPlanes = false;
2637
+ } else {
2638
+ allOut = false;
2639
+ }
2640
+ }
2641
+ if (allOut) return p5.Tree.INVISIBLE;
2642
+ }
2643
+ return allInForAllPlanes ? p5.Tree.VISIBLE : p5.Tree.SEMIVISIBLE;
2644
+ };
2645
+
2646
+ /**
2647
+ * Returns the 6 plane equations of the view frustum bounds defined in world space.
2648
+ * Each plane equation is of the form:
2649
+ * a*x + b*y + c*z + d = 0
2650
+ *
2651
+ * @param {Object} [opts]
2652
+ * @param {p5.Matrix} [opts.vMatrix] View matrix (world -> eye).
2653
+ * @param {p5.Matrix} [opts.eMatrix] Eye matrix (eye -> world).
2654
+ * @returns {Object} Object keyed by p5.Tree.LEFT/RIGHT/NEAR/FAR/TOP/BOTTOM.
2655
+ */
2656
+ p5.RendererGL.prototype.bounds = function ({
2657
+ vMatrix,
2658
+ eMatrix
2659
+ } = {}) {
2660
+ const n = this.nPlane();
2661
+ const f = this.fPlane();
2662
+ const l = this.lPlane();
2663
+ const r = this.rPlane();
2664
+ const b = this.bPlane();
2665
+ const t = this.tPlane();
2666
+ const normals = Array(6);
2667
+ const distances = Array(6);
2668
+ // Camera position and basis in world space.
2669
+ const pos = this._position([0, 0, 0], { from: p5.Tree.EYE, to: p5.Tree.WORLD, eMatrix });
2670
+ const viewDir = this._direction([0, 0, -1], { from: p5.Tree.EYE, to: p5.Tree.WORLD, vMatrix });
2671
+ const up = this._direction([0, 1, 0], { from: p5.Tree.EYE, to: p5.Tree.WORLD, vMatrix });
2672
+ const right = this._direction([1, 0, 0], { from: p5.Tree.EYE, to: p5.Tree.WORLD, vMatrix });
2673
+ const posViewDir = p5.Vector.dot(pos, viewDir);
2674
+ if (this.isOrtho()) {
2675
+ normals[0] = p5.Vector.mult(right, -1);
2676
+ normals[1] = right;
2677
+ normals[4] = up;
2678
+ normals[5] = p5.Vector.mult(up, -1);
2679
+ distances[0] = p5.Vector.dot(p5.Vector.sub(pos, p5.Vector.mult(right, -l)), normals[0]);
2680
+ distances[1] = p5.Vector.dot(p5.Vector.add(pos, p5.Vector.mult(right, r)), normals[1]);
2681
+ distances[4] = p5.Vector.dot(p5.Vector.add(pos, p5.Vector.mult(up, -b)), normals[4]);
2682
+ distances[5] = p5.Vector.dot(p5.Vector.sub(pos, p5.Vector.mult(up, t)), normals[5]);
2683
+ } else {
2684
+ const hfovr = Math.atan2(r, n);
2685
+ const shfovr = Math.sin(hfovr);
2686
+ const chfovr = Math.cos(hfovr);
2687
+ const hfovl = Math.atan2(l, n);
2688
+ const shfovl = Math.sin(hfovl);
2689
+ const chfovl = Math.cos(hfovl);
2690
+ normals[0] = p5.Vector.add(p5.Vector.mult(viewDir, shfovl), p5.Vector.mult(right, -chfovl));
2691
+ normals[1] = p5.Vector.add(p5.Vector.mult(viewDir, -shfovr), p5.Vector.mult(right, chfovr));
2692
+ const fovt = Math.atan2(t, n);
2693
+ const sfovt = Math.sin(fovt);
2694
+ const cfovt = Math.cos(fovt);
2695
+ const fovb = Math.atan2(b, n);
2696
+ const sfovb = Math.sin(fovb);
2697
+ const cfovb = Math.cos(fovb);
2698
+ normals[4] = p5.Vector.add(p5.Vector.mult(viewDir, -sfovt), p5.Vector.mult(up, cfovt));
2699
+ normals[5] = p5.Vector.add(p5.Vector.mult(viewDir, sfovb), p5.Vector.mult(up, -cfovb));
2700
+ distances[0] = shfovl * posViewDir - chfovl * p5.Vector.dot(pos, right);
2701
+ distances[1] = -shfovr * posViewDir + chfovr * p5.Vector.dot(pos, right);
2702
+ distances[4] = -sfovt * posViewDir + cfovt * p5.Vector.dot(pos, up);
2703
+ distances[5] = sfovb * posViewDir - cfovb * p5.Vector.dot(pos, up);
2704
+ }
2705
+ // Near/far planes (common to ortho and perspective).
2706
+ normals[2] = p5.Vector.mult(viewDir, -1);
2707
+ normals[3] = viewDir;
2708
+ distances[2] = -posViewDir - n;
2709
+ distances[3] = posViewDir + f;
2710
+ const bounds = {};
2711
+ bounds[p5.Tree.LEFT] = { a: normals[0].x, b: normals[0].y, c: normals[0].z, d: distances[0] };
2712
+ bounds[p5.Tree.RIGHT] = { a: normals[1].x, b: normals[1].y, c: normals[1].z, d: distances[1] };
2713
+ bounds[p5.Tree.NEAR] = { a: normals[2].x, b: normals[2].y, c: normals[2].z, d: distances[2] };
2714
+ bounds[p5.Tree.FAR] = { a: normals[3].x, b: normals[3].y, c: normals[3].z, d: distances[3] };
2715
+ bounds[p5.Tree.TOP] = { a: normals[4].x, b: normals[4].y, c: normals[4].z, d: distances[4] };
2716
+ bounds[p5.Tree.BOTTOM] = { a: normals[5].x, b: normals[5].y, c: normals[5].z, d: distances[5] };
2717
+ return bounds;
2718
+ };
2719
+
2720
+ /**
2721
+ * Returns signed distance between a point and a frustum plane.
2722
+ *
2723
+ * @param {p5.Vector|number[]} point
2724
+ * @param {number|string} key One of p5.Tree.LEFT/RIGHT/BOTTOM/TOP/NEAR/FAR.
2725
+ * @param {Object} [bounds] Plane equations (defaults to this.bounds()).
2726
+ * @returns {number}
2727
+ */
2728
+ p5.RendererGL.prototype.distanceToBound = function (...args) {
2729
+ let point;
2730
+ let key;
2731
+ let bounds = this.bounds();
2732
+ const asVec3 = v =>
2733
+ v instanceof p5.Vector ? v : new p5.Vector(v?.[0] ?? 0, v?.[1] ?? 0, v?.[2] ?? 0);
2734
+ for (const arg of args) {
2735
+ if (arg instanceof p5.Vector || Array.isArray(arg)) {
2736
+ point = asVec3(arg);
2737
+ } else if (typeof arg === 'string' || typeof arg === 'number') {
2738
+ key = arg;
2739
+ } else if (arg && typeof arg === 'object' && !Array.isArray(arg)) {
2740
+ bounds = arg;
2741
+ }
2742
+ }
2743
+ if (!point || key === undefined) {
2744
+ console.error('[p5.tree] distanceToBound: could not parse query.');
2745
+ return 0;
2746
+ }
2747
+ const eq = bounds[key];
2748
+ return p5.Vector.dot(point, new p5.Vector(eq.a, eq.b, eq.c)) - eq.d;
2749
+ };
2750
+ });
2751
+ //# sourceMappingURL=p5.tree.esm.js.map