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