@plasius/gpu-camera 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,654 @@
1
+ // src/index.js
2
+ var EPSILON = 1e-6;
3
+ var DEFAULT_VIEWPORT = Object.freeze({
4
+ x: 0,
5
+ y: 0,
6
+ width: 1,
7
+ height: 1
8
+ });
9
+ var DEFAULT_UP = Object.freeze([0, 1, 0]);
10
+ var cameraProjectionKinds = Object.freeze([
11
+ "perspective",
12
+ "orthographic"
13
+ ]);
14
+ var cameraControlKinds = Object.freeze([
15
+ "set-look-at",
16
+ "orbit",
17
+ "pan",
18
+ "truck",
19
+ "dolly"
20
+ ]);
21
+ function nowMs(timeSource) {
22
+ if (typeof timeSource === "function") {
23
+ return Number(timeSource()) || Date.now();
24
+ }
25
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
26
+ return performance.now();
27
+ }
28
+ return Date.now();
29
+ }
30
+ function clamp(value, min, max) {
31
+ return Math.min(max, Math.max(min, value));
32
+ }
33
+ function finiteNumber(value, fallback = 0) {
34
+ const number = Number(value);
35
+ return Number.isFinite(number) ? number : fallback;
36
+ }
37
+ function cloneVec3(value, fallback = [0, 0, 0]) {
38
+ if (!Array.isArray(value) || value.length < 3) {
39
+ return [...fallback];
40
+ }
41
+ return [
42
+ finiteNumber(value[0], fallback[0]),
43
+ finiteNumber(value[1], fallback[1]),
44
+ finiteNumber(value[2], fallback[2])
45
+ ];
46
+ }
47
+ function addVec3(a, b) {
48
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
49
+ }
50
+ function subVec3(a, b) {
51
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
52
+ }
53
+ function scaleVec3(vector, scalar) {
54
+ return [vector[0] * scalar, vector[1] * scalar, vector[2] * scalar];
55
+ }
56
+ function dotVec3(a, b) {
57
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
58
+ }
59
+ function crossVec3(a, b) {
60
+ return [
61
+ a[1] * b[2] - a[2] * b[1],
62
+ a[2] * b[0] - a[0] * b[2],
63
+ a[0] * b[1] - a[1] * b[0]
64
+ ];
65
+ }
66
+ function lengthVec3(vector) {
67
+ return Math.hypot(vector[0], vector[1], vector[2]);
68
+ }
69
+ function normalizeVec3(vector, fallback = [0, 0, 1]) {
70
+ const length = lengthVec3(vector);
71
+ if (length <= EPSILON) {
72
+ return [...fallback];
73
+ }
74
+ return [vector[0] / length, vector[1] / length, vector[2] / length];
75
+ }
76
+ function cloneViewport(viewport = DEFAULT_VIEWPORT) {
77
+ return {
78
+ x: clamp(finiteNumber(viewport.x, DEFAULT_VIEWPORT.x), 0, 1),
79
+ y: clamp(finiteNumber(viewport.y, DEFAULT_VIEWPORT.y), 0, 1),
80
+ width: clamp(finiteNumber(viewport.width, DEFAULT_VIEWPORT.width), EPSILON, 1),
81
+ height: clamp(finiteNumber(viewport.height, DEFAULT_VIEWPORT.height), EPSILON, 1)
82
+ };
83
+ }
84
+ function normalizeProjection(projection = {}) {
85
+ const kind = cameraProjectionKinds.includes(projection.kind) ? projection.kind : "perspective";
86
+ if (kind === "orthographic") {
87
+ const left = finiteNumber(projection.left, -1);
88
+ const right = finiteNumber(projection.right, 1);
89
+ const bottom = finiteNumber(projection.bottom, -1);
90
+ const top = finiteNumber(projection.top, 1);
91
+ const near2 = Math.max(EPSILON, finiteNumber(projection.near, 0.1));
92
+ const far2 = Math.max(near2 + EPSILON, finiteNumber(projection.far, 2e3));
93
+ return {
94
+ kind,
95
+ left,
96
+ right,
97
+ bottom,
98
+ top,
99
+ near: near2,
100
+ far: far2,
101
+ aspect: finiteNumber(projection.aspect, 1)
102
+ };
103
+ }
104
+ const fovY = clamp(finiteNumber(projection.fovY, 60), 1, 179);
105
+ const near = Math.max(EPSILON, finiteNumber(projection.near, 0.1));
106
+ const far = Math.max(near + EPSILON, finiteNumber(projection.far, 2e3));
107
+ const aspect = Math.max(EPSILON, finiteNumber(projection.aspect, 1));
108
+ return {
109
+ kind,
110
+ fovY,
111
+ near,
112
+ far,
113
+ aspect
114
+ };
115
+ }
116
+ function normalizeTransform(transform = {}) {
117
+ const position = cloneVec3(transform.position, [0, 0, 5]);
118
+ const target = cloneVec3(transform.target, [0, 0, 0]);
119
+ const up = normalizeVec3(cloneVec3(transform.up, DEFAULT_UP), DEFAULT_UP);
120
+ return {
121
+ position,
122
+ target,
123
+ up
124
+ };
125
+ }
126
+ function cloneCamera(camera) {
127
+ return {
128
+ id: camera.id,
129
+ enabled: camera.enabled,
130
+ priority: camera.priority,
131
+ revision: camera.revision,
132
+ touchedAt: camera.touchedAt,
133
+ viewport: cloneViewport(camera.viewport),
134
+ transform: normalizeTransform(camera.transform),
135
+ projection: normalizeProjection(camera.projection),
136
+ metadata: camera.metadata ? { ...camera.metadata } : void 0
137
+ };
138
+ }
139
+ function assertCameraId(cameras, cameraId) {
140
+ const camera = cameras.get(cameraId);
141
+ if (!camera) {
142
+ throw new Error(`Unknown camera "${cameraId}".`);
143
+ }
144
+ return camera;
145
+ }
146
+ function sortedCameras(cameras) {
147
+ return [...cameras].sort((a, b) => {
148
+ if (b.priority !== a.priority) {
149
+ return b.priority - a.priority;
150
+ }
151
+ return a.id.localeCompare(b.id);
152
+ });
153
+ }
154
+ function promoteActive(cameras, activeCameraId) {
155
+ if (!activeCameraId) {
156
+ return cameras;
157
+ }
158
+ const index = cameras.findIndex((camera) => camera.id === activeCameraId);
159
+ if (index <= 0) {
160
+ return cameras;
161
+ }
162
+ const promoted = [...cameras];
163
+ const [active] = promoted.splice(index, 1);
164
+ promoted.unshift(active);
165
+ return promoted;
166
+ }
167
+ function ensureArray(value) {
168
+ return Array.isArray(value) ? value : [];
169
+ }
170
+ function buildPerspectiveMatrix(projection, overrideAspect) {
171
+ const aspect = Math.max(EPSILON, finiteNumber(overrideAspect, projection.aspect));
172
+ const fovRad = projection.fovY * Math.PI / 180;
173
+ const f = 1 / Math.tan(fovRad / 2);
174
+ const near = projection.near;
175
+ const far = projection.far;
176
+ return new Float32Array([
177
+ f / aspect,
178
+ 0,
179
+ 0,
180
+ 0,
181
+ 0,
182
+ f,
183
+ 0,
184
+ 0,
185
+ 0,
186
+ 0,
187
+ (far + near) / (near - far),
188
+ -1,
189
+ 0,
190
+ 0,
191
+ 2 * far * near / (near - far),
192
+ 0
193
+ ]);
194
+ }
195
+ function buildOrthographicMatrix(projection, overrideAspect) {
196
+ const near = projection.near;
197
+ const far = projection.far;
198
+ const aspect = Math.max(EPSILON, finiteNumber(overrideAspect, projection.aspect || 1));
199
+ let left = projection.left;
200
+ let right = projection.right;
201
+ let top = projection.top;
202
+ let bottom = projection.bottom;
203
+ if (finiteNumber(projection.aspect, 0) > EPSILON) {
204
+ const scale = aspect / projection.aspect;
205
+ left *= scale;
206
+ right *= scale;
207
+ }
208
+ const width = Math.max(EPSILON, right - left);
209
+ const height = Math.max(EPSILON, top - bottom);
210
+ const depth = Math.max(EPSILON, far - near);
211
+ return new Float32Array([
212
+ 2 / width,
213
+ 0,
214
+ 0,
215
+ 0,
216
+ 0,
217
+ 2 / height,
218
+ 0,
219
+ 0,
220
+ 0,
221
+ 0,
222
+ -2 / depth,
223
+ 0,
224
+ -(right + left) / width,
225
+ -(top + bottom) / height,
226
+ -(far + near) / depth,
227
+ 1
228
+ ]);
229
+ }
230
+ function buildProjectionMatrix(camera, overrideAspect) {
231
+ const projection = normalizeProjection(camera?.projection);
232
+ if (projection.kind === "orthographic") {
233
+ return buildOrthographicMatrix(projection, overrideAspect);
234
+ }
235
+ return buildPerspectiveMatrix(projection, overrideAspect);
236
+ }
237
+ function buildViewMatrix(camera) {
238
+ const transform = normalizeTransform(camera?.transform);
239
+ const eye = transform.position;
240
+ const target = transform.target;
241
+ const up = normalizeVec3(transform.up, DEFAULT_UP);
242
+ let zAxis = normalizeVec3(subVec3(eye, target), [0, 0, 1]);
243
+ let xAxis = normalizeVec3(crossVec3(up, zAxis), [1, 0, 0]);
244
+ let yAxis = crossVec3(zAxis, xAxis);
245
+ if (lengthVec3(xAxis) <= EPSILON) {
246
+ zAxis = [0, 0, 1];
247
+ xAxis = [1, 0, 0];
248
+ yAxis = [0, 1, 0];
249
+ }
250
+ return new Float32Array([
251
+ xAxis[0],
252
+ yAxis[0],
253
+ zAxis[0],
254
+ 0,
255
+ xAxis[1],
256
+ yAxis[1],
257
+ zAxis[1],
258
+ 0,
259
+ xAxis[2],
260
+ yAxis[2],
261
+ zAxis[2],
262
+ 0,
263
+ -dotVec3(xAxis, eye),
264
+ -dotVec3(yAxis, eye),
265
+ -dotVec3(zAxis, eye),
266
+ 1
267
+ ]);
268
+ }
269
+ function toCameraUniform(camera, overrideAspect) {
270
+ const normalized = {
271
+ transform: normalizeTransform(camera?.transform),
272
+ projection: normalizeProjection(camera?.projection)
273
+ };
274
+ return {
275
+ id: String(camera?.id ?? ""),
276
+ viewMatrix: buildViewMatrix(normalized),
277
+ projectionMatrix: buildProjectionMatrix(normalized, overrideAspect),
278
+ position: new Float32Array(normalized.transform.position),
279
+ target: new Float32Array(normalized.transform.target),
280
+ near: normalized.projection.near,
281
+ far: normalized.projection.far,
282
+ projectionKind: normalized.projection.kind
283
+ };
284
+ }
285
+ function normalizeCameraDefinition(definition, generatedId) {
286
+ const id = String(definition?.id ?? generatedId ?? "camera").trim();
287
+ if (!id) {
288
+ throw new Error("Camera id cannot be empty.");
289
+ }
290
+ return {
291
+ id,
292
+ enabled: definition?.enabled !== false,
293
+ priority: finiteNumber(definition?.priority, 0),
294
+ revision: Math.max(0, Math.floor(finiteNumber(definition?.revision, 0))),
295
+ touchedAt: finiteNumber(definition?.touchedAt, 0),
296
+ viewport: cloneViewport(definition?.viewport),
297
+ transform: normalizeTransform(definition?.transform),
298
+ projection: normalizeProjection(definition?.projection),
299
+ metadata: definition?.metadata ? { ...definition.metadata } : void 0
300
+ };
301
+ }
302
+ function applyCameraControl(camera, control, options = {}) {
303
+ const base = normalizeCameraDefinition(camera, camera?.id ?? "camera");
304
+ if (!control || typeof control !== "object") {
305
+ return base;
306
+ }
307
+ const kind = String(control.type || "").trim();
308
+ if (!cameraControlKinds.includes(kind)) {
309
+ throw new Error(`Unknown camera control "${kind}".`);
310
+ }
311
+ const minDistance = Math.max(EPSILON, finiteNumber(options.minDistance, 0.05));
312
+ const maxDistance = Math.max(minDistance + EPSILON, finiteNumber(options.maxDistance, 1e5));
313
+ const minPolarAngle = clamp(
314
+ finiteNumber(options.minPolarAngle, EPSILON),
315
+ EPSILON,
316
+ Math.PI - EPSILON
317
+ );
318
+ const maxPolarAngle = clamp(
319
+ finiteNumber(options.maxPolarAngle, Math.PI - EPSILON),
320
+ minPolarAngle,
321
+ Math.PI - EPSILON
322
+ );
323
+ let next = cloneCamera(base);
324
+ if (kind === "set-look-at") {
325
+ next.transform.position = cloneVec3(control.position, next.transform.position);
326
+ next.transform.target = cloneVec3(control.target, next.transform.target);
327
+ next.transform.up = normalizeVec3(cloneVec3(control.up, next.transform.up), DEFAULT_UP);
328
+ }
329
+ if (kind === "pan" || kind === "truck") {
330
+ const delta = cloneVec3(control.delta, [0, 0, 0]);
331
+ next.transform.position = addVec3(next.transform.position, delta);
332
+ next.transform.target = addVec3(next.transform.target, delta);
333
+ }
334
+ if (kind === "dolly") {
335
+ const distance = finiteNumber(control.distance, 0);
336
+ const eye = next.transform.position;
337
+ const target = next.transform.target;
338
+ const direction = normalizeVec3(subVec3(target, eye), [0, 0, -1]);
339
+ const offset = subVec3(eye, target);
340
+ const currentRadius = Math.max(EPSILON, lengthVec3(offset));
341
+ const nextRadius = clamp(currentRadius - distance, minDistance, maxDistance);
342
+ const moved = subVec3(target, scaleVec3(direction, nextRadius));
343
+ next.transform.position = moved;
344
+ }
345
+ if (kind === "orbit") {
346
+ const deltaAzimuth = finiteNumber(control.deltaAzimuth, 0);
347
+ const deltaPolar = finiteNumber(control.deltaPolar, 0);
348
+ const radiusDelta = finiteNumber(control.radiusDelta, 0);
349
+ const eye = next.transform.position;
350
+ const target = next.transform.target;
351
+ const offset = subVec3(eye, target);
352
+ const radius = Math.max(EPSILON, lengthVec3(offset));
353
+ const nextRadius = clamp(radius + radiusDelta, minDistance, maxDistance);
354
+ const azimuth = Math.atan2(offset[0], offset[2]) + deltaAzimuth;
355
+ const polarCurrent = Math.acos(clamp(offset[1] / radius, -1, 1));
356
+ const polar = clamp(
357
+ polarCurrent + deltaPolar,
358
+ minPolarAngle,
359
+ maxPolarAngle
360
+ );
361
+ const sinPolar = Math.sin(polar);
362
+ const position = [
363
+ target[0] + nextRadius * sinPolar * Math.sin(azimuth),
364
+ target[1] + nextRadius * Math.cos(polar),
365
+ target[2] + nextRadius * sinPolar * Math.cos(azimuth)
366
+ ];
367
+ next.transform.position = position;
368
+ }
369
+ next.revision = Math.max(base.revision + 1, next.revision);
370
+ next.touchedAt = finiteNumber(options.touchedAt, base.touchedAt);
371
+ return next;
372
+ }
373
+ function createRenderPlan(snapshot, options = {}) {
374
+ const mode = options.mode === "multiview" ? "multiview" : "single";
375
+ const enabledOnly = options.enabledOnly !== false;
376
+ const includeMatrices = options.includeMatrices !== false;
377
+ const maxParallelViews = Math.max(
378
+ 1,
379
+ Math.floor(
380
+ finiteNumber(
381
+ options.maxParallelViews,
382
+ finiteNumber(snapshot?.maxParallelViews, 1)
383
+ )
384
+ )
385
+ );
386
+ const byId = /* @__PURE__ */ new Map();
387
+ const inputCameras = ensureArray(snapshot?.cameras).map(
388
+ (camera) => normalizeCameraDefinition(camera, camera?.id)
389
+ );
390
+ for (const camera of inputCameras) {
391
+ byId.set(camera.id, camera);
392
+ }
393
+ let selected;
394
+ if (Array.isArray(options.cameraIds) && options.cameraIds.length > 0) {
395
+ selected = options.cameraIds.map((cameraId) => byId.get(String(cameraId))).filter(Boolean);
396
+ } else {
397
+ selected = sortedCameras(inputCameras);
398
+ }
399
+ if (enabledOnly) {
400
+ selected = selected.filter((camera) => camera.enabled);
401
+ }
402
+ selected = promoteActive(selected, snapshot?.activeCameraId ?? null);
403
+ if (mode === "single") {
404
+ selected = selected.slice(0, 1);
405
+ }
406
+ const hotSet = new Set(ensureArray(snapshot?.hotCameraIds).map((id) => String(id)));
407
+ const batches = [];
408
+ for (let index = 0; index < selected.length; index += maxParallelViews) {
409
+ const chunk = selected.slice(index, index + maxParallelViews);
410
+ batches.push({
411
+ index: batches.length,
412
+ parallel: chunk.length > 1,
413
+ views: chunk.map((camera, order) => {
414
+ const aspect = camera.viewport.width / camera.viewport.height;
415
+ const view = {
416
+ cameraId: camera.id,
417
+ order,
418
+ priority: camera.priority,
419
+ revision: camera.revision,
420
+ hot: hotSet.has(camera.id),
421
+ viewport: cloneViewport(camera.viewport)
422
+ };
423
+ if (includeMatrices) {
424
+ view.viewMatrix = buildViewMatrix(camera);
425
+ view.projectionMatrix = buildProjectionMatrix(camera, aspect);
426
+ }
427
+ return view;
428
+ })
429
+ });
430
+ }
431
+ return {
432
+ mode,
433
+ generatedAt: finiteNumber(options.generatedAt, Date.now()),
434
+ activeCameraId: snapshot?.activeCameraId ?? null,
435
+ hotCameraIds: [...hotSet],
436
+ maxParallelViews,
437
+ totalViews: selected.length,
438
+ canRenderInParallel: mode === "multiview" && batches.some((batch) => batch.parallel),
439
+ batches
440
+ };
441
+ }
442
+ function createCameraManager(options = {}) {
443
+ const listeners = /* @__PURE__ */ new Set();
444
+ const cameras = /* @__PURE__ */ new Map();
445
+ const maxParallelViews = Math.max(1, Math.floor(finiteNumber(options.maxParallelViews, 2)));
446
+ const maxHotCameras = Math.max(1, Math.floor(finiteNumber(options.maxHotCameras, 3)));
447
+ const timeSource = options.timeSource;
448
+ let sequence = 0;
449
+ let activeCameraId = null;
450
+ let version = 0;
451
+ let hotCameraIds = [];
452
+ let updatedAt = nowMs(timeSource);
453
+ function bumpVersion() {
454
+ version += 1;
455
+ updatedAt = nowMs(timeSource);
456
+ const snapshot = getSnapshot();
457
+ for (const listener of listeners) {
458
+ listener(snapshot);
459
+ }
460
+ }
461
+ function markHot(cameraId) {
462
+ hotCameraIds = [cameraId, ...hotCameraIds.filter((id) => id !== cameraId)].slice(
463
+ 0,
464
+ maxHotCameras
465
+ );
466
+ }
467
+ function selectFallbackActive() {
468
+ const enabled = sortedCameras([...cameras.values()]).filter((camera) => camera.enabled);
469
+ if (enabled.length > 0) {
470
+ return enabled[0].id;
471
+ }
472
+ const sorted = sortedCameras([...cameras.values()]);
473
+ return sorted.length > 0 ? sorted[0].id : null;
474
+ }
475
+ function getSnapshot() {
476
+ const snapshot = {
477
+ activeCameraId,
478
+ version,
479
+ updatedAt,
480
+ maxParallelViews,
481
+ maxHotCameras,
482
+ hotCameraIds: [...hotCameraIds],
483
+ cameras: sortedCameras([...cameras.values()]).map((camera) => cloneCamera(camera))
484
+ };
485
+ return snapshot;
486
+ }
487
+ function registerCamera(definition = {}) {
488
+ sequence += 1;
489
+ const generatedId = `camera-${sequence}`;
490
+ const camera = normalizeCameraDefinition(definition, generatedId);
491
+ if (cameras.has(camera.id)) {
492
+ throw new Error(`Camera "${camera.id}" is already registered.`);
493
+ }
494
+ camera.touchedAt = nowMs(timeSource);
495
+ cameras.set(camera.id, camera);
496
+ if (!activeCameraId) {
497
+ activeCameraId = camera.id;
498
+ }
499
+ markHot(camera.id);
500
+ bumpVersion();
501
+ return cloneCamera(camera);
502
+ }
503
+ function updateCamera(cameraId, patch = {}) {
504
+ const current = assertCameraId(cameras, cameraId);
505
+ const next = normalizeCameraDefinition(
506
+ {
507
+ ...current,
508
+ ...patch,
509
+ id: current.id,
510
+ transform: {
511
+ ...current.transform,
512
+ ...patch.transform
513
+ },
514
+ projection: {
515
+ ...current.projection,
516
+ ...patch.projection
517
+ },
518
+ viewport: {
519
+ ...current.viewport,
520
+ ...patch.viewport
521
+ }
522
+ },
523
+ current.id
524
+ );
525
+ next.revision = current.revision + 1;
526
+ next.touchedAt = nowMs(timeSource);
527
+ cameras.set(current.id, next);
528
+ markHot(current.id);
529
+ if (patch.makeActive === true) {
530
+ activeCameraId = current.id;
531
+ }
532
+ bumpVersion();
533
+ return cloneCamera(next);
534
+ }
535
+ function upsertCamera(definition = {}) {
536
+ if (definition?.id && cameras.has(definition.id)) {
537
+ return updateCamera(definition.id, definition);
538
+ }
539
+ return registerCamera(definition);
540
+ }
541
+ function removeCamera(cameraId) {
542
+ if (!cameras.has(cameraId)) {
543
+ return false;
544
+ }
545
+ cameras.delete(cameraId);
546
+ hotCameraIds = hotCameraIds.filter((id) => id !== cameraId);
547
+ if (activeCameraId === cameraId) {
548
+ activeCameraId = selectFallbackActive();
549
+ }
550
+ bumpVersion();
551
+ return true;
552
+ }
553
+ function activateCamera(cameraId) {
554
+ const current = assertCameraId(cameras, cameraId);
555
+ activeCameraId = current.id;
556
+ const updated = cloneCamera(current);
557
+ updated.touchedAt = nowMs(timeSource);
558
+ cameras.set(current.id, updated);
559
+ markHot(current.id);
560
+ bumpVersion();
561
+ return cloneCamera(updated);
562
+ }
563
+ function listCameras(listOptions = {}) {
564
+ let next = sortedCameras([...cameras.values()]);
565
+ if (listOptions.enabledOnly) {
566
+ next = next.filter((camera) => camera.enabled);
567
+ }
568
+ return next.map((camera) => cloneCamera(camera));
569
+ }
570
+ function switchCamera(direction = 1, switchOptions = {}) {
571
+ let list = sortedCameras([...cameras.values()]);
572
+ if (switchOptions.enabledOnly !== false) {
573
+ list = list.filter((camera) => camera.enabled);
574
+ }
575
+ if (list.length === 0) {
576
+ return null;
577
+ }
578
+ const currentIndex = list.findIndex((camera) => camera.id === activeCameraId);
579
+ const step = direction >= 0 ? 1 : -1;
580
+ const nextIndex = currentIndex < 0 ? 0 : (currentIndex + step + list.length) % list.length;
581
+ return activateCamera(list[nextIndex].id);
582
+ }
583
+ function applyControl(cameraId, control, controlOptions = {}) {
584
+ const current = assertCameraId(cameras, cameraId);
585
+ const touchedAt = nowMs(timeSource);
586
+ const next = applyCameraControl(current, control, {
587
+ ...controlOptions,
588
+ touchedAt
589
+ });
590
+ cameras.set(cameraId, next);
591
+ markHot(cameraId);
592
+ if (controlOptions.makeActive === true) {
593
+ activeCameraId = cameraId;
594
+ }
595
+ bumpVersion();
596
+ return cloneCamera(next);
597
+ }
598
+ function getCamera(cameraId) {
599
+ const camera = cameras.get(cameraId);
600
+ return camera ? cloneCamera(camera) : null;
601
+ }
602
+ function hasCamera(cameraId) {
603
+ return cameras.has(cameraId);
604
+ }
605
+ function createPlan(planOptions = {}) {
606
+ return createRenderPlan(getSnapshot(), {
607
+ ...planOptions,
608
+ maxParallelViews: planOptions.maxParallelViews ?? maxParallelViews,
609
+ generatedAt: nowMs(timeSource)
610
+ });
611
+ }
612
+ function subscribe(listener) {
613
+ if (typeof listener !== "function") {
614
+ throw new Error("Listener must be a function.");
615
+ }
616
+ listeners.add(listener);
617
+ return () => {
618
+ listeners.delete(listener);
619
+ };
620
+ }
621
+ function clear() {
622
+ cameras.clear();
623
+ activeCameraId = null;
624
+ hotCameraIds = [];
625
+ bumpVersion();
626
+ }
627
+ return {
628
+ registerCamera,
629
+ updateCamera,
630
+ upsertCamera,
631
+ removeCamera,
632
+ activateCamera,
633
+ switchCamera,
634
+ applyControl,
635
+ hasCamera,
636
+ getCamera,
637
+ listCameras,
638
+ getSnapshot,
639
+ createRenderPlan: createPlan,
640
+ subscribe,
641
+ clear
642
+ };
643
+ }
644
+ export {
645
+ applyCameraControl,
646
+ buildProjectionMatrix,
647
+ buildViewMatrix,
648
+ cameraControlKinds,
649
+ cameraProjectionKinds,
650
+ createCameraManager,
651
+ createRenderPlan,
652
+ toCameraUniform
653
+ };
654
+ //# sourceMappingURL=index.js.map