angular-three 1.10.2 → 2.0.0-beta.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.
Files changed (62) hide show
  1. package/README.md +89 -5
  2. package/esm2022/index.mjs +5 -7
  3. package/esm2022/lib/canvas.mjs +138 -99
  4. package/esm2022/lib/di/before-render.mjs +7 -10
  5. package/esm2022/lib/di/ref.mjs +38 -59
  6. package/esm2022/lib/directives/args.mjs +5 -8
  7. package/esm2022/lib/directives/common.mjs +20 -13
  8. package/esm2022/lib/directives/parent.mjs +5 -8
  9. package/esm2022/lib/{web → dom}/events.mjs +1 -1
  10. package/esm2022/lib/events.mjs +2 -2
  11. package/esm2022/lib/loader.mjs +58 -44
  12. package/esm2022/lib/loop.mjs +6 -8
  13. package/esm2022/lib/portal.mjs +76 -63
  14. package/esm2022/lib/renderer/provider.mjs +3 -2
  15. package/esm2022/lib/renderer/renderer.mjs +53 -52
  16. package/esm2022/lib/renderer/store.mjs +14 -19
  17. package/esm2022/lib/renderer/utils.mjs +27 -18
  18. package/esm2022/lib/routed-scene.mjs +12 -10
  19. package/esm2022/lib/stores/signal.store.mjs +60 -0
  20. package/esm2022/lib/stores/store.mjs +69 -48
  21. package/esm2022/lib/three-types.mjs +2 -0
  22. package/esm2022/lib/types.mjs +1 -1
  23. package/esm2022/lib/utils/apply-props.mjs +11 -7
  24. package/esm2022/lib/utils/attach.mjs +1 -1
  25. package/esm2022/lib/utils/instance.mjs +14 -14
  26. package/esm2022/lib/utils/safe-detect-changes.mjs +1 -1
  27. package/fesm2022/angular-three.mjs +1673 -1744
  28. package/fesm2022/angular-three.mjs.map +1 -1
  29. package/index.d.ts +4 -6
  30. package/lib/canvas.d.ts +11 -20
  31. package/lib/di/before-render.d.ts +5 -1
  32. package/lib/di/ref.d.ts +5 -11
  33. package/lib/directives/args.d.ts +6 -6
  34. package/lib/directives/common.d.ts +1 -4
  35. package/lib/directives/parent.d.ts +1 -1
  36. package/lib/{web → dom}/events.d.ts +2 -2
  37. package/lib/events.d.ts +3 -3
  38. package/lib/loader.d.ts +6 -2
  39. package/lib/loop.d.ts +4 -4
  40. package/lib/portal.d.ts +11 -18
  41. package/lib/renderer/renderer.d.ts +7 -9
  42. package/lib/renderer/store.d.ts +3 -5
  43. package/lib/renderer/utils.d.ts +6 -4
  44. package/lib/routed-scene.d.ts +4 -2
  45. package/lib/stores/signal.store.d.ts +19 -0
  46. package/lib/stores/store.d.ts +4 -7
  47. package/lib/three-types.d.ts +306 -0
  48. package/lib/types.d.ts +22 -25
  49. package/lib/utils/attach.d.ts +2 -2
  50. package/lib/utils/instance.d.ts +1 -1
  51. package/package.json +6 -7
  52. package/plugin/package.json +1 -1
  53. package/plugin/src/generators/init/init.d.ts +1 -1
  54. package/plugin/src/generators/init/init.js +1 -1
  55. package/esm2022/lib/di/destroy.mjs +0 -23
  56. package/esm2022/lib/di/run-in-context.mjs +0 -40
  57. package/esm2022/lib/pipes/push.mjs +0 -50
  58. package/esm2022/lib/stores/rx-store.mjs +0 -108
  59. package/lib/di/destroy.d.ts +0 -9
  60. package/lib/di/run-in-context.d.ts +0 -6
  61. package/lib/pipes/push.d.ts +0 -16
  62. package/lib/stores/rx-store.d.ts +0 -42
@@ -1,264 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ChangeDetectorRef, ElementRef, Injectable, InjectionToken, ViewContainerRef, TemplateRef, Directive, Input, EventEmitter, getDebugNode, RendererFactory2, makeEnvironmentProviders, EnvironmentInjector, createEnvironmentInjector, Component, HostBinding, Output, ViewChild, Injector, Pipe, SkipSelf, ContentChild } from '@angular/core';
2
+ import { ElementRef, signal, untracked, computed, assertInInjectionContext, runInInjectionContext, inject, ChangeDetectorRef, effect, Injector, Injectable, Optional, Inject, InjectionToken, EventEmitter, ViewContainerRef, NgZone, TemplateRef, Directive, Input, getDebugNode, RendererFactory2, makeEnvironmentProviders, provideZoneChangeDetection, EnvironmentInjector, DestroyRef, createEnvironmentInjector, Component, ChangeDetectionStrategy, Output, ViewChild, SkipSelf, ContentChild } from '@angular/core';
3
3
  import { provideNgxResizeOptions, NgxResize } from 'ngx-resize';
4
- import { isObservable, of, map, from, tap, retry, catchError, share, ReplaySubject, switchMap, forkJoin, take, BehaviorSubject, startWith, combineLatest, filter, distinctUntilChanged, takeUntil, merge } from 'rxjs';
5
4
  import * as THREE from 'three';
6
5
  import { DOCUMENT, NgForOf, NgIf } from '@angular/common';
7
- import { RxState } from '@rx-angular/state';
6
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
7
  import * as i1 from '@angular/router';
9
8
  import { ActivationEnd, RouterOutlet } from '@angular/router';
10
-
11
- const idCache = {};
12
- function makeId(event) {
13
- if (event) {
14
- return (event.eventObject || event.object).uuid + '/' + event.index + event.instanceId;
15
- }
16
- const newId = THREE.MathUtils.generateUUID();
17
- // ensure not already used
18
- if (!idCache[newId]) {
19
- idCache[newId] = true;
20
- return newId;
21
- }
22
- return makeId();
23
- }
24
- function makeDpr(dpr, window) {
25
- const target = window?.devicePixelRatio || 1;
26
- return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], target), dpr[1]) : dpr;
27
- }
28
- function makeDefaultCamera(isOrthographic, size) {
29
- if (isOrthographic)
30
- return new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000);
31
- return new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000);
32
- }
33
- function makeDefaultRenderer(glOptions, canvasElement) {
34
- const customRenderer = (typeof glOptions === 'function' ? glOptions(canvasElement) : glOptions);
35
- if (customRenderer?.render != null)
36
- return customRenderer;
37
- return new THREE.WebGLRenderer({
38
- powerPreference: 'high-performance',
39
- canvas: canvasElement,
40
- antialias: true,
41
- alpha: true,
42
- ...(glOptions || {}),
43
- });
44
- }
45
- function makeObjectGraph(object) {
46
- const data = { nodes: {}, materials: {} };
47
- if (object) {
48
- object.traverse((child) => {
49
- if (child.name)
50
- data.nodes[child.name] = child;
51
- if ('material' in child && !data.materials[child.material.name]) {
52
- data.materials[child.material.name] = child
53
- .material;
54
- }
55
- });
56
- }
57
- return data;
58
- }
59
-
60
- function safeDetectChanges(cdr) {
61
- if (!cdr)
62
- return;
63
- try {
64
- // dynamic created component with ViewContainerRef#createComponent does not have Context
65
- // but it has _attachedToViewContainer
66
- if (cdr['context'] || cdr['_attachedToViewContainer']) {
67
- cdr.detectChanges();
68
- }
69
- }
70
- catch (e) {
71
- cdr.markForCheck();
72
- }
73
- }
74
-
75
- const cached = new Map();
76
- function load(loaderConstructorFactory, input, extensions, onProgress) {
77
- const urls$ = isObservable(input) ? input : of(input);
78
- return urls$.pipe(map((inputs) => {
79
- const loaderConstructor = loaderConstructorFactory(inputs);
80
- const loader = new loaderConstructor();
81
- if (extensions)
82
- extensions(loader);
83
- const urls = Array.isArray(inputs) ? inputs : typeof inputs === 'string' ? [inputs] : Object.values(inputs);
84
- return [
85
- urls.map((url) => {
86
- if (!cached.has(url)) {
87
- cached.set(url, from(loader.loadAsync(url, onProgress)).pipe(tap((data) => {
88
- if (data['scene'])
89
- Object.assign(data, makeObjectGraph(data['scene']));
90
- }), retry(2), catchError((err) => {
91
- console.error(`[NGT] Error loading ${url}: ${err.message}`);
92
- return of([]);
93
- }), share({ connector: () => new ReplaySubject(1) })));
94
- }
95
- return cached.get(url);
96
- }),
97
- inputs,
98
- ];
99
- }));
100
- }
101
- function injectNgtLoader(loaderConstructorFactory, input, extensions, onProgress) {
102
- const cdr = inject(ChangeDetectorRef);
103
- return load(loaderConstructorFactory, input, extensions, onProgress).pipe(switchMap(([observables$, inputs]) => {
104
- return forkJoin(observables$).pipe(map((results) => {
105
- if (Array.isArray(inputs))
106
- return results;
107
- if (typeof inputs === 'string')
108
- return results[0];
109
- const keys = Object.keys(inputs);
110
- return keys.reduce((result, key) => {
111
- result[key] = results[keys.indexOf(key)];
112
- return result;
113
- }, {});
114
- }), tap(() => {
115
- requestAnimationFrame(() => void safeDetectChanges(cdr));
116
- }));
117
- }));
118
- }
119
- injectNgtLoader['destroy'] = () => {
120
- cached.clear();
121
- };
122
- injectNgtLoader['preLoad'] = (loaderConstructorFactory, inputs, extensions) => {
123
- load(loaderConstructorFactory, inputs, extensions).pipe(take(1)).subscribe();
124
- };
125
-
126
- function createSubs(callback, subs) {
127
- const sub = { callback };
128
- subs.add(sub);
129
- return () => void subs.delete(sub);
130
- }
131
- const globalEffects = new Set();
132
- const globalAfterEffects = new Set();
133
- const globalTailEffects = new Set();
134
- /**
135
- * Adds a global render callback which is called each frame.
136
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect
137
- */
138
- const addEffect = (callback) => createSubs(callback, globalEffects);
139
- /**
140
- * Adds a global after-render callback which is called each frame.
141
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect
142
- */
143
- const addAfterEffect = (callback) => createSubs(callback, globalAfterEffects);
144
- /**
145
- * Adds a global callback which is called when rendering stops.
146
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail
147
- */
148
- const addTail = (callback) => createSubs(callback, globalTailEffects);
149
- function run(effects, timestamp) {
150
- if (!effects.size)
151
- return;
152
- for (const { callback } of effects.values()) {
153
- callback(timestamp);
154
- }
155
- }
156
- function flushGlobalEffects(type, timestamp) {
157
- switch (type) {
158
- case 'before':
159
- return run(globalEffects, timestamp);
160
- case 'after':
161
- return run(globalAfterEffects, timestamp);
162
- case 'tail':
163
- return run(globalTailEffects, timestamp);
164
- }
165
- }
166
- function render(timestamp, store, frame) {
167
- const state = store.get();
168
- // Run local effects
169
- let delta = state.clock.getDelta();
170
- // In frameloop='never' mode, clock times are updated using the provided timestamp
171
- if (state.frameloop === 'never' && typeof timestamp === 'number') {
172
- delta = timestamp - state.clock.elapsedTime;
173
- state.clock.oldTime = state.clock.elapsedTime;
174
- state.clock.elapsedTime = timestamp;
175
- }
176
- // Call subscribers (useFrame)
177
- // subscribers = state.internal.subscribers;
178
- for (let i = 0; i < state.internal.subscribers.length; i++) {
179
- const subscriber = state.internal.subscribers[i];
180
- subscriber.callback({ ...state, delta, frame });
181
- }
182
- // Render content
183
- if (!state.internal.priority && state.gl.render) {
184
- state.gl.render(state.scene, state.camera);
185
- }
186
- // Decrease frame count
187
- state.internal.frames = Math.max(0, state.internal.frames - 1);
188
- return state.frameloop === 'always' ? 1 : state.internal.frames;
189
- }
190
- function createLoop(roots) {
191
- let running = false;
192
- let repeat;
193
- let frame;
194
- function loop(timestamp) {
195
- frame = requestAnimationFrame(loop);
196
- running = true;
197
- repeat = 0;
198
- // Run effects
199
- flushGlobalEffects('before', timestamp);
200
- // Render all roots
201
- for (const root of roots.values()) {
202
- const state = root.get();
203
- // If the frameloop is invalidated, do not run another frame
204
- if (state.internal.active &&
205
- (state.frameloop === 'always' || state.internal.frames > 0) &&
206
- !state.gl.xr?.isPresenting) {
207
- repeat += render(timestamp, root);
208
- }
209
- }
210
- // Run after-effects
211
- flushGlobalEffects('after', timestamp);
212
- // Stop the loop if nothing invalidates it
213
- if (repeat === 0) {
214
- // Tail call effects, they are called when rendering stops
215
- flushGlobalEffects('tail', timestamp);
216
- // Flag end of operation
217
- running = false;
218
- return cancelAnimationFrame(frame);
219
- }
220
- }
221
- function invalidate(store, frames = 1) {
222
- const state = store?.get();
223
- if (!state)
224
- return roots.forEach((root) => invalidate(root, frames));
225
- if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never')
226
- return;
227
- // Increase frames, do not go higher than 60
228
- state.internal.frames = Math.min(60, state.internal.frames + frames);
229
- // If the render-loop isn't active, start it
230
- if (!running) {
231
- running = true;
232
- requestAnimationFrame(loop);
233
- }
234
- }
235
- function advance(timestamp, runGlobalEffects = true, store, frame) {
236
- const state = store?.get();
237
- if (runGlobalEffects)
238
- flushGlobalEffects('before', timestamp);
239
- if (!state)
240
- for (const root of roots.values())
241
- render(timestamp, root);
242
- // safe to assume store is available here
243
- else
244
- render(timestamp, store, frame);
245
- if (runGlobalEffects)
246
- flushGlobalEffects('after', timestamp);
247
- }
248
- return {
249
- loop,
250
- /**
251
- * Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state.
252
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate
253
- */
254
- invalidate,
255
- /**
256
- * Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`.
257
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance
258
- */
259
- advance,
260
- };
261
- }
9
+ import { filter } from 'rxjs';
262
10
 
263
11
  const is = {
264
12
  obj: (a) => a === Object(a) && !Array.isArray(a) && typeof a !== 'function',
@@ -353,35 +101,35 @@ function invalidateInstance(instance) {
353
101
  checkUpdate(instance);
354
102
  }
355
103
  function prepare(object, localState) {
356
- const instance = (typeof object === 'function' ? object() : object);
104
+ const instance = object;
357
105
  if (localState?.primitive || !instance.__ngt__) {
358
- const { objects = new BehaviorSubject([]), nonObjects = new BehaviorSubject([]), ...rest } = localState || {};
106
+ const { objects = signal([]), nonObjects = signal([]), ...rest } = localState || {};
359
107
  instance.__ngt__ = {
360
108
  previousAttach: null,
361
109
  store: null,
362
- parent: null,
110
+ parent: signal(null),
363
111
  memoized: {},
364
112
  eventCount: 0,
365
113
  handlers: {},
366
114
  objects,
367
115
  nonObjects,
368
116
  add: (object, type) => {
369
- const current = instance.__ngt__[type].value;
117
+ const current = untracked(instance.__ngt__[type]);
370
118
  const foundIndex = current.indexOf((obj) => obj === object);
371
119
  if (foundIndex > -1) {
372
120
  // if we add an object with the same reference, then we switch it out
373
121
  // and update the BehaviorSubject
374
122
  current.splice(foundIndex, 1, object);
375
- instance.__ngt__[type].next(current);
123
+ instance.__ngt__[type].set(current);
376
124
  }
377
125
  else {
378
- instance.__ngt__[type].next([...instance.__ngt__[type].value, object]);
126
+ instance.__ngt__[type].update((prev) => [...prev, object]);
379
127
  }
380
- notifyAncestors(instance.__ngt__.parent);
128
+ notifyAncestors(untracked(instance.__ngt__.parent));
381
129
  },
382
130
  remove: (object, type) => {
383
- instance.__ngt__[type].next(instance.__ngt__[type].value.filter((o) => o !== object));
384
- notifyAncestors(instance.__ngt__.parent);
131
+ instance.__ngt__[type].update((prev) => prev.filter((o) => o !== object));
132
+ notifyAncestors(untracked(instance.__ngt__.parent));
385
133
  },
386
134
  ...rest,
387
135
  };
@@ -393,1045 +141,1266 @@ function notifyAncestors(instance) {
393
141
  return;
394
142
  const localState = getLocalState(instance);
395
143
  if (localState.objects)
396
- localState.objects.next(localState.objects.value);
144
+ localState.objects.update((prev) => prev);
397
145
  if (localState.nonObjects)
398
- localState.nonObjects.next(localState.nonObjects.value);
399
- notifyAncestors(localState.parent);
146
+ localState.nonObjects.update((prev) => prev);
147
+ notifyAncestors(untracked(localState.parent));
400
148
  }
401
149
 
402
- function diffProps(instance, props) {
403
- const propsEntries = Object.entries(props);
404
- const changes = [];
405
- for (const [propKey, propValue] of propsEntries) {
406
- if (is.equ(propValue, instance[propKey]))
407
- continue;
408
- changes.push([propKey, propValue]);
150
+ const idCache = {};
151
+ function makeId(event) {
152
+ if (event) {
153
+ return (event.eventObject || event.object).uuid + '/' + event.index + event.instanceId;
409
154
  }
410
- return changes;
155
+ const newId = THREE.MathUtils.generateUUID();
156
+ // ensure not already used
157
+ if (!idCache[newId]) {
158
+ idCache[newId] = true;
159
+ return newId;
160
+ }
161
+ return makeId();
411
162
  }
412
- function applyProps(instance, props) {
413
- // if props is empty
414
- if (!Object.keys(props).length)
415
- return instance;
416
- // filter equals, events , and reserved props
417
- const localState = getLocalState(instance);
418
- const rootState = localState.store?.get();
419
- const changes = diffProps(instance, props);
420
- for (let i = 0; i < changes.length; i++) {
421
- let key = changes[i][0];
422
- const currentInstance = instance;
423
- const targetProp = currentInstance[key];
424
- let value = changes[i][1];
425
- if (is.colorSpaceExist(currentInstance)) {
426
- const sRGBEncoding = 3001;
427
- const SRGBColorSpace = 'srgb';
428
- const LinearSRGBColorSpace = 'srgb-linear';
429
- if (key === 'encoding') {
430
- key = 'colorSpace';
431
- value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
432
- }
433
- else if (key === 'outputEncoding') {
434
- key = 'outputColorSpace';
435
- value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
436
- }
437
- }
438
- // special treatmen for objects with support for set/copy, and layers
439
- if (targetProp && targetProp['set'] && (targetProp['copy'] || targetProp instanceof THREE.Layers)) {
440
- const isColor = targetProp instanceof THREE.Color;
441
- // if value is an array
442
- if (Array.isArray(value)) {
443
- if (targetProp['fromArray'])
444
- targetProp['fromArray'](value);
445
- else
446
- targetProp['set'](...value);
447
- }
448
- // test again target.copy
449
- else if (targetProp['copy'] &&
450
- value &&
451
- value.constructor &&
452
- targetProp.constructor.name === value.constructor.name) {
453
- targetProp['copy'](value);
454
- if (!THREE.ColorManagement && !rootState.linear && isColor)
455
- targetProp['convertSRGBToLinear']();
456
- }
457
- // if nothing else fits, just set the single value, ignore undefined
458
- else if (value !== undefined) {
459
- const isColor = targetProp instanceof THREE.Color;
460
- // allow setting array scalars
461
- if (!isColor && targetProp['setScalar'])
462
- targetProp['setScalar'](value);
463
- // layers have no copy function, copy the mask
464
- else if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers)
465
- targetProp.mask = value.mask;
466
- // otherwise just set ...
467
- else
468
- targetProp['set'](value);
469
- // auto-convert srgb
470
- if (!THREE.ColorManagement && !rootState?.linear && isColor)
471
- targetProp.convertSRGBToLinear();
472
- }
473
- }
474
- // else just overwrite the value
475
- else {
476
- currentInstance[key] = value;
477
- // auto-convert srgb textures
478
- if (currentInstance[key] instanceof THREE.Texture &&
479
- currentInstance[key].format === THREE.RGBAFormat &&
480
- currentInstance[key].type === THREE.UnsignedByteType) {
481
- const texture = currentInstance[key];
482
- if (is.colorSpaceExist(texture) && is.colorSpaceExist(rootState.gl))
483
- texture.colorSpace = rootState.gl.outputColorSpace;
484
- else
485
- texture.encoding = rootState.gl.outputEncoding;
486
- }
487
- }
488
- checkUpdate(targetProp);
489
- invalidateInstance(instance);
490
- }
491
- const instanceHandlers = localState.eventCount;
492
- if (localState.parent && rootState.internal && instance['raycast'] && instanceHandlers !== localState.eventCount) {
493
- // pre-emptively remove the interaction from manager
494
- rootState.removeInteraction(instance['uuid']);
495
- // add the instance to the interaction manager only when it has handlers
496
- if (localState.eventCount)
497
- rootState.addInteraction(instance);
498
- }
499
- if (localState.parent && localState.afterUpdate && localState.afterUpdate.observed && changes.length) {
500
- localState.afterUpdate.emit(instance);
501
- }
502
- return instance;
163
+ function makeDpr(dpr, window) {
164
+ const target = window?.devicePixelRatio || 1;
165
+ return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], target), dpr[1]) : dpr;
503
166
  }
504
-
505
- const startWithUndefined = () => startWith(undefined);
506
- /**
507
- * An extended `tap` operator that accepts an `effectFn` which:
508
- * - runs on every `next` notification from `source$`
509
- * - can optionally return a `cleanUp` function that
510
- * invokes from the 2nd `next` notification onward and on `unsubscribe` (destroyed)
511
- *
512
- * @example
513
- * ```typescript
514
- * source$.pipe(
515
- * tapEffect((sourceValue) = {
516
- * const cb = () => {
517
- * doStuff(sourceValue);
518
- * };
519
- * addListener('event', cb);
520
- *
521
- * return () => {
522
- * removeListener('event', cb);
523
- * }
524
- * })
525
- * )
526
- * ```
527
- */
528
- function tapEffect(effectFn) {
529
- let cleanupFn = () => { };
530
- let firstRun = false;
531
- let prev = undefined;
532
- const teardown = (error) => () => {
533
- if (cleanupFn)
534
- cleanupFn({ prev, complete: true, error });
535
- };
536
- return tap({
537
- next: (value) => {
538
- if (cleanupFn && firstRun)
539
- cleanupFn({ prev, complete: false, error: false });
540
- const cleanUpOrVoid = effectFn(value);
541
- if (cleanUpOrVoid)
542
- cleanupFn = cleanUpOrVoid;
543
- prev = value;
544
- if (!firstRun)
545
- firstRun = true;
546
- },
547
- complete: teardown(false),
548
- unsubscribe: teardown(false),
549
- error: teardown(true),
167
+ function makeDefaultCamera(isOrthographic, size) {
168
+ if (isOrthographic)
169
+ return new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000);
170
+ return new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000);
171
+ }
172
+ function makeDefaultRenderer(glOptions, canvasElement) {
173
+ const customRenderer = (typeof glOptions === 'function' ? glOptions(canvasElement) : glOptions);
174
+ if (customRenderer?.render != null)
175
+ return customRenderer;
176
+ return new THREE.WebGLRenderer({
177
+ powerPreference: 'high-performance',
178
+ canvas: canvasElement,
179
+ antialias: true,
180
+ alpha: true,
181
+ ...(glOptions || {}),
550
182
  });
551
183
  }
552
- class NgtRxStore extends RxState {
553
- constructor() {
554
- super();
555
- // set a dummy property so that initial this.get() won't return undefined
556
- this.set({ __ngt_dummy__: '__ngt_dummy__' });
557
- // override set so our consumers don't have to handle undefined for state that already have default values
558
- const originalSet = this.set.bind(this);
559
- Object.defineProperty(this, 'set', {
560
- get: () => {
561
- return (...args) => {
562
- const firstArg = args[0];
563
- if (is.obj(firstArg)) {
564
- const modArgs = Object.entries(firstArg).reduce((modded, [key, value]) => {
565
- modded[key] = value === undefined ? this.get(key) : value;
566
- return modded;
567
- }, {});
568
- return originalSet(modArgs);
569
- }
570
- return originalSet(...args);
571
- };
572
- },
573
- });
574
- // override get to return {} if get() returns undefined
575
- const originalGet = this.get.bind(this);
576
- Object.defineProperty(this, 'get', {
577
- get: () => (...args) => {
578
- if (args.length === 0)
579
- return originalGet() ?? {};
580
- return originalGet(...args);
581
- },
184
+ function makeObjectGraph(object) {
185
+ const data = { nodes: {}, materials: {} };
186
+ if (object) {
187
+ object.traverse((child) => {
188
+ if (child.name)
189
+ data.nodes[child.name] = child;
190
+ if ('material' in child && !data.materials[child.material.name]) {
191
+ data.materials[child.material.name] = child
192
+ .material;
193
+ }
582
194
  });
583
- // call initialize that might be setup by derived Stores
584
- this.initialize();
585
195
  }
586
- initialize() {
587
- return;
588
- }
589
- effect(obs, sideEffectFn) {
590
- return this.hold(obs.pipe(tapEffect(sideEffectFn)));
196
+ return data;
197
+ }
198
+
199
+ function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
200
+ const captureData = captures.get(obj);
201
+ if (captureData) {
202
+ captures.delete(obj);
203
+ // if this was the last captured object for this pointer
204
+ if (captures.size === 0) {
205
+ capturedMap.delete(pointerId);
206
+ captureData.target.releasePointerCapture(pointerId);
207
+ }
591
208
  }
592
- triggerChangeDetection(cdr, keys = []) {
593
- let $ = this.$;
594
- if (keys.length) {
595
- $ = combineLatest(keys.map((key) => this.select(key).pipe(startWith(this.get(key) ?? undefined))));
209
+ }
210
+ function removeInteractivity(store, object) {
211
+ const internal = store.get('internal');
212
+ // removes every trace of an object from data store
213
+ internal.interaction = internal.interaction.filter((o) => o !== object);
214
+ internal.initialHits = internal.initialHits.filter((o) => o !== object);
215
+ internal.hovered.forEach((value, key) => {
216
+ if (value.eventObject === object || value.object === object) {
217
+ // clear out intersects, they are outdated by now
218
+ internal.hovered.delete(key);
596
219
  }
597
- this.hold($, () => void requestAnimationFrame(() => void safeDetectChanges(cdr)));
220
+ });
221
+ internal.capturedMap.forEach((captures, pointerId) => {
222
+ releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
223
+ });
224
+ if (store.get('previousStore')) {
225
+ removeInteractivity(store.get('previousStore'), object);
598
226
  }
599
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtRxStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
600
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtRxStore }); }
601
227
  }
602
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtRxStore, decorators: [{
603
- type: Injectable
604
- }], ctorParameters: function () { return []; } });
605
-
606
- const rootStateMap = new Map();
607
- const { invalidate, advance } = createLoop(rootStateMap);
608
- const shallowLoose = { objects: 'shallow', strict: false };
609
- class NgtStore extends NgtRxStore {
610
- constructor() {
611
- super(...arguments);
612
- this.parentStore = inject(NgtStore, { optional: true, skipSelf: true });
613
- this.window = inject(DOCUMENT).defaultView;
614
- this.isInit = false;
228
+ function createEvents(store) {
229
+ /** calculates delta **/
230
+ function calculateDistance(event) {
231
+ const internal = store.get('internal');
232
+ const dx = event.offsetX - internal.initialClick[0];
233
+ const dy = event.offsetY - internal.initialClick[1];
234
+ return Math.round(Math.sqrt(dx * dx + dy * dy));
615
235
  }
616
- init() {
617
- if (!this.isInit) {
618
- const position = new THREE.Vector3();
619
- const defaultTarget = new THREE.Vector3();
620
- const tempTarget = new THREE.Vector3();
621
- const getCurrentViewport = (camera = this.get('camera'), target = defaultTarget, size = this.get('size')) => {
622
- const { width, height, top, left } = size;
623
- const aspect = width / height;
624
- if (target instanceof THREE.Vector3)
625
- tempTarget.copy(target);
626
- else
627
- tempTarget.set(...target);
628
- const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
629
- if (is.orthographicCamera(camera)) {
630
- return {
631
- width: width / camera.zoom,
632
- height: height / camera.zoom,
633
- top,
634
- left,
635
- factor: 1,
636
- distance,
637
- aspect,
638
- };
236
+ /** returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc... **/
237
+ function filterPointerEvents(objects) {
238
+ return objects.filter((obj) => ['move', 'over', 'enter', 'out', 'leave'].some((name) => {
239
+ const eventName = `pointer${name}`;
240
+ return getLocalState(obj).handlers?.[eventName];
241
+ }));
242
+ }
243
+ function intersect(event, filter) {
244
+ const state = store.get();
245
+ const duplicates = new Set();
246
+ const intersections = [];
247
+ // allow callers to eliminate event objects
248
+ const eventObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
249
+ // reset all raycaster cameras to undefined
250
+ for (let i = 0; i < eventObjects.length; i++) {
251
+ const instanceState = getLocalState(eventObjects[i]).store?.get();
252
+ if (instanceState) {
253
+ instanceState.raycaster.camera = undefined;
254
+ }
255
+ }
256
+ if (!state.previousStore) {
257
+ // make sure root-level pointer and ray are setup
258
+ state.events.compute?.(event, store);
259
+ }
260
+ function handleRaycast(obj) {
261
+ const objLocalState = getLocalState(obj);
262
+ const objStore = objLocalState.store;
263
+ const objState = objStore?.get();
264
+ // skip event handling when noEvents is set, or when raycaster camera is null
265
+ if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
266
+ return [];
267
+ // when the camera is undefined, we have to call the events layers to update function
268
+ if (objState.raycaster.camera === undefined) {
269
+ objState.events.compute?.(event, objStore, objState.previousStore);
270
+ // if the camera is still undefined, we have to skip this layer entirely
271
+ if (objState.raycaster.camera === undefined)
272
+ objState.raycaster.camera = null;
273
+ }
274
+ // intersect object by object
275
+ return objState.raycaster.camera ? objState.raycaster.intersectObject(obj, true) : [];
276
+ }
277
+ // collect events
278
+ let hits = eventObjects
279
+ // intersect objects
280
+ .flatMap(handleRaycast)
281
+ // sort by event priority
282
+ .sort((a, b) => {
283
+ const aState = getLocalState(a.object).store.get();
284
+ const bState = getLocalState(b.object).store.get();
285
+ if (!aState || !bState)
286
+ return a.distance - b.distance;
287
+ return bState.events.priority - aState.events.priority || a.distance - b.distance;
288
+ })
289
+ // filter out duplicates
290
+ .filter((item) => {
291
+ const id = makeId(item);
292
+ if (duplicates.has(id))
293
+ return false;
294
+ duplicates.add(id);
295
+ return true;
296
+ });
297
+ // allow custom userland intersect sort order, this likely only makes sense on the root
298
+ if (state.events.filter)
299
+ hits = state.events.filter(hits, store);
300
+ // bubble up the events, find the event source
301
+ for (const hit of hits) {
302
+ let eventObject = hit.object;
303
+ // bubble event up
304
+ while (eventObject) {
305
+ if (getLocalState(eventObject).eventCount) {
306
+ intersections.push({ ...hit, eventObject });
639
307
  }
640
- const fov = (camera.fov * Math.PI) / 180; // convert vertical fov to radians
641
- const h = 2 * Math.tan(fov / 2) * distance; // visible height
642
- const w = h * aspect;
643
- return { width: w, height: h, top, left, factor: width / w, distance, aspect };
644
- };
645
- let performanceTimeout;
646
- const setPerformanceCurrent = (current) => {
647
- this.set((state) => ({ performance: { ...state.performance, current } }));
648
- };
649
- this.set({
650
- get: this.get.bind(this),
651
- set: this.set.bind(this),
652
- select: this.select.bind(this),
653
- ready: false,
654
- scene: prepare(new THREE.Scene(), { store: this }),
655
- events: { priority: 1, enabled: true, connected: false },
656
- invalidate: (frames = 1) => invalidate(this, frames),
657
- advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, this),
658
- legacy: false,
659
- linear: false,
660
- flat: false,
661
- controls: null,
662
- clock: new THREE.Clock(),
663
- pointer: new THREE.Vector2(),
664
- frameloop: 'always',
665
- performance: {
666
- current: 1,
667
- min: 0.5,
668
- max: 1,
669
- debounce: 200,
670
- regress: () => {
671
- const state = this.get();
672
- // clear timeout
673
- if (performanceTimeout)
674
- clearTimeout(performanceTimeout);
675
- // set lower bound
676
- if (state.performance.current !== state.performance.min) {
677
- setPerformanceCurrent(state.performance.min);
678
- }
679
- // go back to upper bound
680
- performanceTimeout = setTimeout(() => setPerformanceCurrent(this.get('performance', 'max') || 1), state.performance.debounce);
681
- },
682
- },
683
- size: { width: 0, height: 0, top: 0, left: 0 },
684
- viewport: {
685
- initialDpr: 0,
686
- dpr: 0,
687
- width: 0,
688
- height: 0,
689
- top: 0,
690
- left: 0,
691
- aspect: 0,
692
- distance: 0,
693
- factor: 0,
694
- getCurrentViewport,
695
- },
696
- previousStore: this.parentStore,
697
- internal: {
698
- active: false,
699
- priority: 0,
700
- frames: 0,
701
- lastEvent: null,
702
- interaction: [],
703
- hovered: new Map(),
704
- subscribers: [],
705
- initialClick: [0, 0],
706
- initialHits: [],
707
- capturedMap: new Map(),
708
- subscribe: (callback, priority = 0, store = this) => {
709
- const internal = this.get('internal');
710
- // If this subscription was given a priority, it takes rendering into its own hands
711
- // For that reason we switch off automatic rendering and increase the manual flag
712
- // As long as this flag is positive there can be no internal rendering at all
713
- // because there could be multiple render subscriptions
714
- internal.priority = internal.priority + (priority > 0 ? 1 : 0);
715
- internal.subscribers.push({ priority, store, callback });
716
- // Register subscriber and sort layers from lowest to highest, meaning,
717
- // highest priority renders last (on top of the other frames)
718
- internal.subscribers.sort((a, b) => (a.priority || 0) - (b.priority || 0));
719
- return () => {
720
- const internal = this.get('internal');
721
- if (internal?.subscribers) {
722
- // Decrease manual flag if this subscription had a priority
723
- internal.priority = internal.priority - (priority > 0 ? 1 : 0);
724
- // Remove subscriber from list
725
- internal.subscribers = internal.subscribers.filter((s) => s.callback !== callback);
308
+ eventObject = eventObject.parent;
309
+ }
310
+ }
311
+ // if the interaction is captured, make all capturing targets part of the intersects
312
+ if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
313
+ for (const capturedData of state.internal.capturedMap.get(event.pointerId).values()) {
314
+ if (!duplicates.has(makeId(capturedData.intersection))) {
315
+ intersections.push(capturedData.intersection);
316
+ }
317
+ }
318
+ }
319
+ return intersections;
320
+ }
321
+ /** handle intersections by forwarding them to handlers */
322
+ function handleIntersects(intersections, event, delta, callback) {
323
+ const rootState = store.get();
324
+ // if anything has been found, forward it to the event listeners
325
+ if (intersections.length) {
326
+ const localState = { stopped: false };
327
+ for (const hit of intersections) {
328
+ const state = getLocalState(hit.object).store?.get() || rootState;
329
+ const { raycaster, pointer, camera, internal } = state;
330
+ const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
331
+ const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
332
+ const setPointerCapture = (id) => {
333
+ const captureData = { intersection: hit, target: event.target };
334
+ if (internal.capturedMap.has(id)) {
335
+ // if the pointerId was previously captured, we add the hit to the event capturedMap
336
+ internal.capturedMap.get(id).set(hit.eventObject, captureData);
337
+ }
338
+ else {
339
+ // if the pointerId was not previously captured, we create a Map containing the hitObject, and the hit. hitObject is used for faster access
340
+ internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]]));
341
+ }
342
+ // call the original event now
343
+ event.target.setPointerCapture(id);
344
+ };
345
+ const releasePointerCapture = (id) => {
346
+ const captures = internal.capturedMap.get(id);
347
+ if (captures) {
348
+ releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
349
+ }
350
+ };
351
+ // add native event props
352
+ const extractEventProps = {};
353
+ // This iterates over the event's properties including the inherited ones. Native PointerEvents have most of their props as getters which are inherited, but polyfilled PointerEvents have them all as their own properties (i.e. not inherited). We can't use Object.keys() or Object.entries() as they only return "own" properties; nor Object.getPrototypeOf(event) as that *doesn't* return "own" properties, only inherited ones.
354
+ for (const prop in event) {
355
+ const property = event[prop];
356
+ // only copy over atomics, leave functions alone as these should be called as event.nativeEvent.fn()
357
+ if (typeof property !== 'function') {
358
+ extractEventProps[prop] = property;
359
+ }
360
+ }
361
+ const raycastEvent = {
362
+ ...hit,
363
+ ...extractEventProps,
364
+ pointer,
365
+ intersections,
366
+ stopped: localState.stopped,
367
+ delta,
368
+ unprojectedPoint,
369
+ ray: raycaster.ray,
370
+ camera: camera,
371
+ // Hijack stopPropagation, which just sets a flag
372
+ stopPropagation() {
373
+ // https://github.com/pmndrs/react-three-fiber/issues/596
374
+ // Events are not allowed to stop propagation if the pointer has been captured
375
+ const capturesForPointer = 'pointerId' in event && internal.capturedMap.get(event.pointerId);
376
+ // We only authorize stopPropagation...
377
+ if (
378
+ // ...if this pointer hasn't been captured
379
+ !capturesForPointer ||
380
+ // ... or if the hit object is capturing the pointer
381
+ capturesForPointer.has(hit.eventObject)) {
382
+ raycastEvent.stopped = localState.stopped = true;
383
+ // Propagation is stopped, remove all other hover records
384
+ // An event handler is only allowed to flush other handlers if it is hovered itself
385
+ if (internal.hovered.size &&
386
+ Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
387
+ // Objects cannot flush out higher up objects that have already caught the event
388
+ const higher = intersections.slice(0, intersections.indexOf(hit));
389
+ cancelPointer([...higher, hit]);
726
390
  }
727
- };
391
+ }
728
392
  },
729
- },
730
- setEvents: (events) => {
731
- this.set((state) => ({ events: { ...state.events, ...events } }));
732
- },
733
- setSize: (width, height, top, left) => {
734
- const camera = this.get('camera');
735
- const size = { width, height, top: top || 0, left: left || 0 };
736
- this.set((state) => ({
737
- size,
738
- viewport: { ...state.viewport, ...getCurrentViewport(camera, defaultTarget, size) },
739
- }));
740
- },
741
- setDpr: (dpr) => {
742
- const resolved = makeDpr(dpr, this.window);
743
- this.set((state) => ({
744
- viewport: {
745
- ...state.viewport,
746
- dpr: resolved,
747
- initialDpr: state.viewport.initialDpr || resolved,
748
- },
749
- }));
750
- },
751
- setFrameloop: (frameloop = 'always') => {
752
- const clock = this.get('clock');
753
- clock.stop();
754
- clock.elapsedTime = 0;
755
- if (frameloop !== 'never') {
756
- clock.start();
757
- clock.elapsedTime = 0;
393
+ // there should be a distinction between target and currentTarget
394
+ target: { hasPointerCapture, setPointerCapture, releasePointerCapture },
395
+ currentTarget: { hasPointerCapture, setPointerCapture, releasePointerCapture },
396
+ nativeEvent: event,
397
+ };
398
+ // call subscribers
399
+ callback(raycastEvent);
400
+ // event bubbling may be interupted by stopPropagation
401
+ if (localState.stopped === true)
402
+ break;
403
+ }
404
+ }
405
+ return intersections;
406
+ }
407
+ function cancelPointer(intersections) {
408
+ const { internal } = store.get();
409
+ for (const hoveredObj of internal.hovered.values()) {
410
+ // When no objects were hit or the hovered object wasn't found underneath the cursor
411
+ // we call onPointerOut and delete the object from the hovered-elements map
412
+ if (!intersections.length ||
413
+ !intersections.find((hit) => hit.object === hoveredObj.object &&
414
+ hit.index === hoveredObj.index &&
415
+ hit.instanceId === hoveredObj.instanceId)) {
416
+ const eventObject = hoveredObj.eventObject;
417
+ const instance = getLocalState(eventObject);
418
+ const handlers = instance?.handlers;
419
+ internal.hovered.delete(makeId(hoveredObj));
420
+ if (instance?.eventCount) {
421
+ // Clear out intersects, they are outdated by now
422
+ const data = { ...hoveredObj, intersections };
423
+ handlers?.pointerout?.(data);
424
+ handlers?.pointerleave?.(data);
425
+ }
426
+ }
427
+ }
428
+ }
429
+ function pointerMissed(event, objects) {
430
+ for (let i = 0; i < objects.length; i++) {
431
+ const instance = getLocalState(objects[i]);
432
+ instance?.handlers.pointermissed?.(event);
433
+ }
434
+ }
435
+ function handlePointer(name) {
436
+ // Deal with cancelation
437
+ switch (name) {
438
+ case 'pointerleave':
439
+ case 'pointercancel':
440
+ return () => cancelPointer([]);
441
+ case 'lostpointercapture':
442
+ return (event) => {
443
+ const { internal } = store.get();
444
+ if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
445
+ // If the object event interface had onLostPointerCapture, we'd call it here on every
446
+ // object that's getting removed.
447
+ internal.capturedMap.delete(event.pointerId);
448
+ cancelPointer([]);
758
449
  }
759
- this.set({ frameloop });
760
- },
761
- addInteraction: (interaction) => {
762
- this.set((state) => ({
763
- internal: { ...state.internal, interaction: [...state.internal.interaction, interaction] },
764
- }));
765
- },
766
- removeInteraction: (uuid) => {
767
- this.set((state) => ({
768
- internal: {
769
- ...state.internal,
770
- interaction: state.internal.interaction.filter((interaction) => interaction.uuid !== uuid),
771
- },
772
- }));
773
- },
450
+ };
451
+ }
452
+ // Any other pointer goes here ...
453
+ return function handleEvent(event) {
454
+ const { onPointerMissed, internal } = store.get();
455
+ // prepareRay(event)
456
+ internal.lastEvent.nativeElement = event;
457
+ // Get fresh intersects
458
+ const isPointerMove = name === 'pointermove';
459
+ const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick';
460
+ const filter = isPointerMove ? filterPointerEvents : undefined;
461
+ // const hits = patchIntersects(intersect(filter), event)
462
+ const hits = intersect(event, filter);
463
+ const delta = isClickEvent ? calculateDistance(event) : 0;
464
+ // Save initial coordinates on pointer-down
465
+ if (name === 'pointerdown') {
466
+ internal.initialClick = [event.offsetX, event.offsetY];
467
+ internal.initialHits = hits.map((hit) => hit.eventObject);
468
+ }
469
+ // If a click yields no results, pass it back to the user as a miss
470
+ // Missed events have to come first in order to establish user-land side-effect clean up
471
+ if (isClickEvent && !hits.length) {
472
+ if (delta <= 2) {
473
+ pointerMissed(event, internal.interaction);
474
+ if (onPointerMissed)
475
+ onPointerMissed(event);
476
+ }
477
+ }
478
+ // Take care of unhover
479
+ if (isPointerMove)
480
+ cancelPointer(hits);
481
+ function onIntersect(data) {
482
+ const eventObject = data.eventObject;
483
+ const instance = getLocalState(eventObject);
484
+ const handlers = instance?.handlers;
485
+ // Check presence of handlers
486
+ if (!instance?.eventCount)
487
+ return;
488
+ if (isPointerMove) {
489
+ // Move event ...
490
+ if (handlers?.pointerover ||
491
+ handlers?.pointerenter ||
492
+ handlers?.pointerout ||
493
+ handlers?.pointerleave) {
494
+ // When enter or out is present take care of hover-state
495
+ const id = makeId(data);
496
+ const hoveredItem = internal.hovered.get(id);
497
+ if (!hoveredItem) {
498
+ // If the object wasn't previously hovered, book it and call its handler
499
+ internal.hovered.set(id, data);
500
+ handlers.pointerover?.(data);
501
+ handlers.pointerenter?.(data);
502
+ }
503
+ else if (hoveredItem.stopped) {
504
+ // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
505
+ data.stopPropagation();
506
+ }
507
+ }
508
+ // Call mouse move
509
+ handlers?.pointermove?.(data);
510
+ }
511
+ else {
512
+ // All other events ...
513
+ const handler = handlers?.[name];
514
+ if (handler) {
515
+ // Forward all events back to their respective handlers with the exception of click events,
516
+ // which must use the initial target
517
+ if (!isClickEvent || internal.initialHits.includes(eventObject)) {
518
+ // Missed events have to come first
519
+ pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
520
+ // Now call the handler
521
+ handler(data);
522
+ }
523
+ }
524
+ else {
525
+ // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
526
+ if (isClickEvent && internal.initialHits.includes(eventObject)) {
527
+ pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
528
+ }
529
+ }
530
+ }
531
+ }
532
+ handleIntersects(hits, event, delta, onIntersect);
533
+ };
534
+ }
535
+ return { handlePointer };
536
+ }
537
+
538
+ const DOM_EVENTS = {
539
+ click: false,
540
+ contextmenu: false,
541
+ dblclick: false,
542
+ wheel: false,
543
+ pointerdown: true,
544
+ pointerup: true,
545
+ pointerleave: true,
546
+ pointermove: true,
547
+ pointercancel: true,
548
+ lostpointercapture: true,
549
+ };
550
+ const supportedEvents = [
551
+ 'click',
552
+ 'contextmenu',
553
+ 'dblclick',
554
+ 'pointerup',
555
+ 'pointerdown',
556
+ 'pointerover',
557
+ 'pointerout',
558
+ 'pointerenter',
559
+ 'pointerleave',
560
+ 'pointermove',
561
+ 'pointermissed',
562
+ 'pointercancel',
563
+ 'wheel',
564
+ ];
565
+ function createPointerEvents(store) {
566
+ const { handlePointer } = createEvents(store);
567
+ return {
568
+ priority: 1,
569
+ enabled: true,
570
+ compute: (event, root) => {
571
+ const state = root.get();
572
+ // https://github.com/pmndrs/react-three-fiber/pull/782
573
+ // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides
574
+ state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
575
+ state.raycaster.setFromCamera(state.pointer, state.camera);
576
+ },
577
+ connected: undefined,
578
+ handlers: Object.keys(DOM_EVENTS).reduce((handlers, supportedEventName) => {
579
+ handlers[supportedEventName] = handlePointer(supportedEventName);
580
+ return handlers;
581
+ }, {}),
582
+ connect: (target) => {
583
+ const state = store.get();
584
+ state.events.disconnect?.();
585
+ state.setEvents({ connected: target });
586
+ Object.entries(state.events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
587
+ const passive = DOM_EVENTS[eventName];
588
+ target.addEventListener(eventName, eventHandler, { passive });
774
589
  });
775
- this.isInit = true;
776
- this.resize();
777
- }
778
- }
779
- configure(inputs, canvasElement) {
780
- const { gl: glOptions, size: sizeOptions, camera: cameraOptions, raycaster: raycasterOptions, events, orthographic, lookAt, shadows, linear, legacy, flat, dpr, frameloop, performance, } = inputs;
781
- const state = this.get();
782
- const stateToUpdate = {};
783
- // setup renderer
784
- let gl = state.gl;
785
- if (!state.gl)
786
- stateToUpdate.gl = gl = makeDefaultRenderer(glOptions, canvasElement);
787
- // setup raycaster
788
- let raycaster = state.raycaster;
789
- if (!raycaster)
790
- stateToUpdate.raycaster = raycaster = new THREE.Raycaster();
791
- // set raycaster options
792
- const { params, ...options } = raycasterOptions || {};
793
- if (!is.equ(options, raycaster, shallowLoose))
794
- applyProps(raycaster, { ...options });
795
- if (!is.equ(params, raycaster.params, shallowLoose)) {
796
- applyProps(raycaster, { params: { ...raycaster.params, ...(params || {}) } });
797
- }
798
- // create default camera
799
- if (!state.camera) {
800
- const isCamera = is.camera(cameraOptions);
801
- let camera = isCamera ? cameraOptions : makeDefaultCamera(orthographic || false, state.size);
802
- if (!isCamera) {
803
- if (cameraOptions)
804
- applyProps(camera, cameraOptions);
805
- // set position.z
806
- if (!cameraOptions?.position)
807
- camera.position.z = 5;
808
- // always look at center or passed-in lookAt by default
809
- if (!cameraOptions?.rotation) {
810
- if (Array.isArray(lookAt))
811
- camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
812
- else if (lookAt instanceof THREE.Vector3)
813
- camera.lookAt(lookAt);
814
- else
815
- camera.lookAt(0, 0, 0);
816
- }
817
- // update projection matrix after applyprops
818
- camera.updateProjectionMatrix?.();
819
- }
820
- if (!is.instance(camera))
821
- camera = prepare(camera, { store: this });
822
- stateToUpdate.camera = camera;
823
- }
824
- // Set up XR (one time only!)
825
- if (!state.xr) {
826
- // Handle frame behavior in WebXR
827
- const handleXRFrame = (timestamp, frame) => {
828
- const state = this.get();
829
- if (state.frameloop === 'never')
830
- return;
831
- advance(timestamp, true, this, frame);
832
- };
833
- // Toggle render switching on session
834
- const handleSessionChange = () => {
835
- const state = this.get();
836
- state.gl.xr.enabled = state.gl.xr.isPresenting;
837
- state.gl.xr.setAnimationLoop(state.gl.xr.isPresenting ? handleXRFrame : null);
838
- if (!state.gl.xr.isPresenting)
839
- invalidate(this);
840
- };
841
- // WebXR session manager
842
- const xr = {
843
- connect: () => {
844
- gl.xr.addEventListener('sessionstart', handleSessionChange);
845
- gl.xr.addEventListener('sessionend', handleSessionChange);
846
- },
847
- disconnect: () => {
848
- gl.xr.removeEventListener('sessionstart', handleSessionChange);
849
- gl.xr.removeEventListener('sessionend', handleSessionChange);
850
- },
851
- };
852
- // Subscribe to WebXR session events
853
- if (gl.xr)
854
- xr.connect();
855
- stateToUpdate.xr = xr;
856
- }
857
- // Set shadowmap
858
- if (gl.shadowMap) {
859
- const isBoolean = typeof shadows === 'boolean';
860
- if ((isBoolean && gl.shadowMap.enabled !== shadows) || !is.equ(shadows, gl.shadowMap, shallowLoose)) {
861
- const old = gl.shadowMap.enabled;
862
- gl.shadowMap.enabled = !!shadows;
863
- if (!isBoolean)
864
- Object.assign(gl.shadowMap, shadows);
865
- else
866
- gl.shadowMap.type = THREE.PCFSoftShadowMap;
867
- if (old !== gl.shadowMap.enabled)
868
- checkNeedsUpdate(gl.shadowMap);
590
+ },
591
+ disconnect: () => {
592
+ const { events, setEvents } = store.get();
593
+ if (events.connected) {
594
+ Object.entries(events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
595
+ if (events.connected instanceof HTMLElement) {
596
+ events.connected.removeEventListener(eventName, eventHandler);
597
+ }
598
+ });
599
+ setEvents({ connected: undefined });
869
600
  }
601
+ },
602
+ };
603
+ }
604
+
605
+ function safeDetectChanges(cdr) {
606
+ if (!cdr)
607
+ return;
608
+ try {
609
+ // dynamic created component with ViewContainerRef#createComponent does not have Context
610
+ // but it has _attachedToViewContainer
611
+ if (cdr['context'] || cdr['_attachedToViewContainer']) {
612
+ cdr.detectChanges();
870
613
  }
871
- // Safely set color management if available.
872
- // Avoid accessing THREE.ColorManagement to play nice with older versions
873
- if (THREE.ColorManagement) {
874
- const ColorManagement = THREE.ColorManagement;
875
- if ('enabled' in ColorManagement)
876
- ColorManagement['enabled'] = !legacy ?? false;
877
- else if ('legacyMode' in ColorManagement)
878
- ColorManagement['legacyMode'] = legacy ?? true;
879
- }
880
- // set color space and tonemapping preferences
881
- const LinearEncoding = 3000;
882
- const sRGBEncoding = 3001;
883
- applyProps(gl, {
884
- outputEncoding: linear ? LinearEncoding : sRGBEncoding,
885
- toneMapping: flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping,
886
- });
887
- // Update color management state
888
- if (state.legacy !== legacy)
889
- stateToUpdate.legacy = legacy;
890
- if (state.linear !== linear)
891
- stateToUpdate.linear = linear;
892
- if (state.flat !== flat)
893
- stateToUpdate.flat = flat;
894
- // Set gl props
895
- gl.setClearAlpha(0);
896
- gl.setPixelRatio(makeDpr(state.viewport.dpr));
897
- gl.setSize(state.size.width, state.size.height);
898
- if (is.obj(glOptions) &&
899
- !(typeof glOptions === 'function') &&
900
- !is.renderer(glOptions) &&
901
- !is.equ(glOptions, gl, shallowLoose)) {
902
- applyProps(gl, glOptions);
903
- }
904
- // Store events internally
905
- if (events && !state.events.handlers)
906
- stateToUpdate.events = events(this);
907
- // Check performance
908
- if (performance && !is.equ(performance, state.performance, shallowLoose)) {
909
- stateToUpdate.performance = { ...state.performance, ...performance };
910
- }
911
- this.set(stateToUpdate);
912
- // Check pixelratio
913
- if (dpr && state.viewport.dpr !== makeDpr(dpr))
914
- state.setDpr(dpr);
915
- // Check size, allow it to take on container bounds initially
916
- const size = computeInitialSize(canvasElement, sizeOptions);
917
- if (!is.equ(size, state.size, shallowLoose))
918
- state.setSize(size.width, size.height, size.top, size.left);
919
- // Check frameloop
920
- if (state.frameloop !== frameloop)
921
- state.setFrameloop(frameloop);
922
- if (!this.get('ready'))
923
- this.set({ ready: true });
924
- this.invalidate();
925
614
  }
926
- destroy(canvas) {
927
- this.set((state) => ({ internal: { ...state.internal, active: false } }));
928
- setTimeout(() => {
929
- const { gl, xr, events } = this.get();
930
- if (gl) {
931
- if (events.disconnect) {
932
- events.disconnect();
933
- }
934
- gl.renderLists.dispose();
935
- gl.forceContextLoss();
936
- if (gl.xr && gl.xr.enabled) {
937
- gl.xr.setAnimationLoop(null);
938
- xr.disconnect();
939
- }
940
- dispose(this.get());
941
- rootStateMap.delete(canvas);
942
- }
943
- }, 500);
615
+ catch (e) {
616
+ cdr.markForCheck();
944
617
  }
945
- resize() {
946
- const state = this.get();
947
- let oldSize = state.size;
948
- let oldDpr = state.viewport.dpr;
949
- let oldCamera = state.camera;
950
- this.hold(combineLatest([this.select('camera'), this.select('size'), this.select('viewport'), this.select('gl')]), ([camera, size, viewport, gl]) => {
951
- // resize camera and renderer on changes to size and dpr
952
- if (size !== oldSize || viewport.dpr !== oldDpr) {
953
- oldSize = size;
954
- oldDpr = viewport.dpr;
955
- // update camera
956
- updateCamera(camera, size);
957
- gl.setPixelRatio(viewport.dpr);
958
- gl.setSize(size.width, size.height);
618
+ }
619
+
620
+ const cached = new Map();
621
+ function load(loaderConstructorFactory, inputs, { extensions, onProgress, } = {}) {
622
+ const computedUrls = computed(() => {
623
+ const input = inputs();
624
+ if (Array.isArray(input))
625
+ return input;
626
+ if (typeof input === 'string')
627
+ return [input];
628
+ return Object.values(input);
629
+ });
630
+ return () => {
631
+ const urls = computedUrls();
632
+ const loaderConstructor = loaderConstructorFactory(urls);
633
+ const loader = new loaderConstructor();
634
+ if (extensions)
635
+ extensions(loader);
636
+ return urls.map((url) => new Promise((resolve, reject) => {
637
+ if (cached.has(url)) {
638
+ resolve(cached.get(url));
959
639
  }
960
- // update viewport when camera changes
961
- if (camera !== oldCamera) {
962
- updateCamera(camera, size);
963
- oldCamera = camera;
964
- this.set((state) => ({
965
- viewport: { ...state.viewport, ...state.viewport.getCurrentViewport(camera) },
966
- }));
640
+ else {
641
+ loader.load(url, (data) => {
642
+ if ('scene' in data)
643
+ Object.assign(data, makeObjectGraph(data['scene']));
644
+ cached.set(url, data);
645
+ resolve(data);
646
+ }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error}`)));
967
647
  }
968
- });
969
- }
970
- invalidate() {
971
- this.hold(this.select(), () => invalidate(this));
648
+ }));
649
+ };
650
+ }
651
+ function injectNgtLoader(loaderConstructorFactory, inputs, { extensions, onProgress, injector = inject(Injector, { optional: true }), } = {}) {
652
+ !injector && assertInInjectionContext(injectNgtLoader);
653
+ return runInInjectionContext(injector, () => {
654
+ const cdr = inject(ChangeDetectorRef);
655
+ const response = signal(null);
656
+ const effector = load(loaderConstructorFactory, inputs, { extensions, onProgress });
657
+ effect(() => {
658
+ const originalUrls = untracked(inputs);
659
+ Promise.all(effector())
660
+ .then((results) => {
661
+ if (Array.isArray(originalUrls))
662
+ return results;
663
+ if (typeof originalUrls === 'string')
664
+ return results[0];
665
+ const keys = Object.keys(originalUrls);
666
+ return keys.reduce((result, key) => {
667
+ result[key] = results[keys.indexOf(key)];
668
+ return result;
669
+ }, {});
670
+ })
671
+ .then((value) => {
672
+ response.set(value);
673
+ safeDetectChanges(cdr);
674
+ });
675
+ }, { injector: injector, allowSignalWrites: true });
676
+ return response.asReadonly();
677
+ });
678
+ }
679
+ injectNgtLoader['preload'] = (loaderConstructorFactory, inputs, extensions) => {
680
+ Promise.all(load(loaderConstructorFactory, inputs, { extensions })());
681
+ };
682
+ injectNgtLoader['destroy'] = () => {
683
+ cached.clear();
684
+ };
685
+
686
+ function createSubs(callback, subs) {
687
+ const sub = { callback };
688
+ subs.add(sub);
689
+ return () => void subs.delete(sub);
690
+ }
691
+ const globalEffects = new Set();
692
+ const globalAfterEffects = new Set();
693
+ const globalTailEffects = new Set();
694
+ /**
695
+ * Adds a global render callback which is called each frame.
696
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect
697
+ */
698
+ const addEffect = (callback) => createSubs(callback, globalEffects);
699
+ /**
700
+ * Adds a global after-render callback which is called each frame.
701
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect
702
+ */
703
+ const addAfterEffect = (callback) => createSubs(callback, globalAfterEffects);
704
+ /**
705
+ * Adds a global callback which is called when rendering stops.
706
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail
707
+ */
708
+ const addTail = (callback) => createSubs(callback, globalTailEffects);
709
+ function run(effects, timestamp) {
710
+ if (!effects.size)
711
+ return;
712
+ for (const { callback } of effects.values()) {
713
+ callback(timestamp);
972
714
  }
973
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtStore, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
974
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtStore }); }
975
715
  }
976
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtStore, decorators: [{
977
- type: Injectable
978
- }] });
979
- function computeInitialSize(canvas, defaultSize) {
980
- if (defaultSize)
981
- return defaultSize;
982
- if (canvas instanceof HTMLCanvasElement && canvas.parentElement) {
983
- return canvas.parentElement.getBoundingClientRect();
716
+ function flushGlobalEffects(type, timestamp) {
717
+ switch (type) {
718
+ case 'before':
719
+ return run(globalEffects, timestamp);
720
+ case 'after':
721
+ return run(globalAfterEffects, timestamp);
722
+ case 'tail':
723
+ return run(globalTailEffects, timestamp);
984
724
  }
985
- return { width: 0, height: 0, top: 0, left: 0 };
986
725
  }
987
- // Disposes an object and all its properties
988
- function dispose(obj) {
989
- if (obj.dispose && !is.scene(obj))
990
- obj.dispose();
991
- for (const p in obj) {
992
- p.dispose?.();
993
- delete obj[p];
726
+ function render(timestamp, store, frame) {
727
+ const state = store.get();
728
+ // Run local effects
729
+ let delta = state.clock.getDelta();
730
+ // In frameloop='never' mode, clock times are updated using the provided timestamp
731
+ if (state.frameloop === 'never' && typeof timestamp === 'number') {
732
+ delta = timestamp - state.clock.elapsedTime;
733
+ state.clock.oldTime = state.clock.elapsedTime;
734
+ state.clock.elapsedTime = timestamp;
994
735
  }
995
- }
996
-
997
- const NGT_COMPOUND_PREFIXES = new InjectionToken('NgtCompoundPrefixes');
998
-
999
- const catalogue = {};
1000
- function extend(objects) {
1001
- Object.assign(catalogue, objects);
1002
- }
1003
- const NGT_CATALOGUE = new InjectionToken('THREE Constructors Catalogue', { factory: () => catalogue });
1004
-
1005
- class NgtCommonDirective {
1006
- constructor() {
1007
- this.vcr = inject(ViewContainerRef);
1008
- this.template = inject(TemplateRef);
1009
- this.injected = false;
1010
- this.shouldCreateView = true;
1011
- const commentNode = this.vcr.element.nativeElement;
1012
- if (commentNode['__ngt_renderer_add_comment__']) {
1013
- commentNode['__ngt_renderer_add_comment__']();
1014
- delete commentNode['__ngt_renderer_add_comment__'];
1015
- }
736
+ // Call subscribers (useFrame)
737
+ const subscribers = state.internal.subscribers;
738
+ for (let i = 0; i < subscribers.length; i++) {
739
+ const subscription = subscribers[i];
740
+ subscription.callback({ ...subscription.store.get(), delta, frame });
1016
741
  }
1017
- createView() {
1018
- if (this.shouldCreateView) {
1019
- if (this.view && !this.view.destroyed) {
1020
- this.view.destroy();
742
+ // Render content
743
+ if (!state.internal.priority && state.gl.render)
744
+ state.gl.render(state.scene, state.camera);
745
+ // Decrease frame count
746
+ state.internal.frames = Math.max(0, state.internal.frames - 1);
747
+ return state.frameloop === 'always' ? 1 : state.internal.frames;
748
+ }
749
+ function createLoop(roots) {
750
+ let running = false;
751
+ let repeat;
752
+ let frame;
753
+ function loop(timestamp) {
754
+ frame = requestAnimationFrame(loop);
755
+ running = true;
756
+ repeat = 0;
757
+ // Run effects
758
+ flushGlobalEffects('before', timestamp);
759
+ // Render all roots
760
+ for (const root of roots.values()) {
761
+ const state = root.get();
762
+ // If the frameloop is invalidated, do not run another frame
763
+ if (state.internal.active &&
764
+ (state.frameloop === 'always' || state.internal.frames > 0) &&
765
+ !state.gl.xr?.isPresenting) {
766
+ repeat += render(timestamp, root);
1021
767
  }
1022
- this.view = this.vcr.createEmbeddedView(this.template);
1023
- safeDetectChanges(this.view);
768
+ }
769
+ // Run after-effects
770
+ flushGlobalEffects('after', timestamp);
771
+ // Stop the loop if nothing invalidates it
772
+ if (repeat === 0) {
773
+ // Tail call effects, they are called when rendering stops
774
+ flushGlobalEffects('tail', timestamp);
775
+ // Flag end of operation
776
+ running = false;
777
+ return cancelAnimationFrame(frame);
1024
778
  }
1025
779
  }
1026
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCommonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1027
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtCommonDirective, ngImport: i0 }); }
1028
- }
1029
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCommonDirective, decorators: [{
1030
- type: Directive
1031
- }], ctorParameters: function () { return []; } });
1032
-
1033
- class NgtArgs extends NgtCommonDirective {
1034
- constructor() {
1035
- super(...arguments);
1036
- this.injectedArgs = [];
1037
- }
1038
- set args(args) {
1039
- if (args == null || !Array.isArray(args) || (args.length === 1 && args[0] === null))
780
+ function invalidate(store, frames = 1) {
781
+ const state = store?.get();
782
+ if (!state)
783
+ return roots.forEach((root) => invalidate(root, frames));
784
+ if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never')
1040
785
  return;
1041
- this.injected = false;
1042
- this.injectedArgs = args;
1043
- this.createView();
1044
- }
1045
- get args() {
1046
- if (this.validate()) {
1047
- this.injected = true;
1048
- return this.injectedArgs;
786
+ // Increase frames, do not go higher than 60
787
+ state.internal.frames = Math.min(60, state.internal.frames + frames);
788
+ // If the render-loop isn't active, start it
789
+ if (!running) {
790
+ running = true;
791
+ requestAnimationFrame(loop);
1049
792
  }
1050
- return null;
1051
793
  }
1052
- validate() {
1053
- return !this.injected && !!this.injectedArgs.length;
794
+ function advance(timestamp, runGlobalEffects = true, store, frame) {
795
+ const state = store?.get();
796
+ if (runGlobalEffects)
797
+ flushGlobalEffects('before', timestamp);
798
+ if (!state)
799
+ for (const root of roots.values())
800
+ render(timestamp, root);
801
+ else
802
+ render(timestamp, store, frame);
803
+ if (runGlobalEffects)
804
+ flushGlobalEffects('after', timestamp);
1054
805
  }
1055
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtArgs, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1056
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtArgs, isStandalone: true, selector: "[args]", inputs: { args: "args" }, usesInheritance: true, ngImport: i0 }); }
806
+ return {
807
+ loop,
808
+ /**
809
+ * Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state.
810
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate
811
+ */
812
+ invalidate,
813
+ /**
814
+ * Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`.
815
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance
816
+ */
817
+ advance,
818
+ };
1057
819
  }
1058
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtArgs, decorators: [{
1059
- type: Directive,
1060
- args: [{ selector: '[args]', standalone: true }]
1061
- }], propDecorators: { args: [{
1062
- type: Input
1063
- }] } });
1064
820
 
1065
- class NgtParent extends NgtCommonDirective {
1066
- constructor() {
1067
- super(...arguments);
1068
- this.injectedParent = null;
1069
- }
1070
- set parent(parent) {
1071
- if (!parent)
1072
- return;
1073
- this.injected = false;
1074
- this.injectedParent = parent;
1075
- this.createView();
821
+ function diffProps(instance, props) {
822
+ const propsEntries = Object.entries(props);
823
+ const changes = [];
824
+ for (const [propKey, propValue] of propsEntries) {
825
+ if (is.equ(propValue, instance[propKey]))
826
+ continue;
827
+ changes.push([propKey, propValue]);
1076
828
  }
1077
- get parent() {
1078
- if (this.validate()) {
1079
- this.injected = true;
1080
- return this.injectedParent;
829
+ return changes;
830
+ }
831
+ function applyProps(instance, props) {
832
+ // if props is empty
833
+ if (!Object.keys(props).length)
834
+ return instance;
835
+ // filter equals, events , and reserved props
836
+ const localState = getLocalState(instance);
837
+ const rootState = localState.store?.get();
838
+ const changes = diffProps(instance, props);
839
+ for (let i = 0; i < changes.length; i++) {
840
+ let key = changes[i][0];
841
+ const currentInstance = instance;
842
+ const targetProp = currentInstance[key];
843
+ let value = changes[i][1];
844
+ if (is.colorSpaceExist(currentInstance)) {
845
+ const sRGBEncoding = 3001;
846
+ const SRGBColorSpace = 'srgb';
847
+ const LinearSRGBColorSpace = 'srgb-linear';
848
+ if (key === 'encoding') {
849
+ key = 'colorSpace';
850
+ value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
851
+ }
852
+ else if (key === 'outputEncoding') {
853
+ key = 'outputColorSpace';
854
+ value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
855
+ }
1081
856
  }
1082
- return null;
857
+ // special treatmen for objects with support for set/copy, and layers
858
+ if (targetProp && targetProp['set'] && (targetProp['copy'] || targetProp instanceof THREE.Layers)) {
859
+ const isColor = targetProp instanceof THREE.Color;
860
+ // if value is an array
861
+ if (Array.isArray(value)) {
862
+ if (targetProp['fromArray'])
863
+ targetProp['fromArray'](value);
864
+ else
865
+ targetProp['set'](...value);
866
+ }
867
+ // test again target.copy
868
+ else if (targetProp['copy'] &&
869
+ value &&
870
+ value.constructor &&
871
+ targetProp.constructor.name === value.constructor.name) {
872
+ targetProp['copy'](value);
873
+ if (!THREE.ColorManagement && !rootState.linear && isColor)
874
+ targetProp['convertSRGBToLinear']();
875
+ }
876
+ // if nothing else fits, just set the single value, ignore undefined
877
+ else if (value !== undefined) {
878
+ const isColor = targetProp instanceof THREE.Color;
879
+ // allow setting array scalars
880
+ if (!isColor && targetProp['setScalar'])
881
+ targetProp['setScalar'](value);
882
+ // layers have no copy function, copy the mask
883
+ else if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers)
884
+ targetProp.mask = value.mask;
885
+ // otherwise just set ...
886
+ else
887
+ targetProp['set'](value);
888
+ // auto-convert srgb
889
+ if (!THREE.ColorManagement && !rootState?.linear && isColor)
890
+ targetProp.convertSRGBToLinear();
891
+ }
892
+ }
893
+ // else just overwrite the value
894
+ else {
895
+ currentInstance[key] = value;
896
+ // auto-convert srgb textures
897
+ if (currentInstance[key] instanceof THREE.Texture &&
898
+ currentInstance[key].format === THREE.RGBAFormat &&
899
+ currentInstance[key].type === THREE.UnsignedByteType) {
900
+ const texture = currentInstance[key];
901
+ if (is.colorSpaceExist(texture) && is.colorSpaceExist(rootState.gl))
902
+ texture.colorSpace = rootState.gl.outputColorSpace;
903
+ else
904
+ texture.encoding = rootState.gl.outputEncoding;
905
+ }
906
+ }
907
+ checkUpdate(targetProp);
908
+ invalidateInstance(instance);
909
+ }
910
+ const instanceHandlers = localState.eventCount;
911
+ const parent = localState.parent ? untracked(localState.parent) : null;
912
+ if (parent && rootState.internal && instance['raycast'] && instanceHandlers !== localState.eventCount) {
913
+ // Pre-emptively remove the instance from the interaction manager
914
+ const index = rootState.internal.interaction.indexOf(instance);
915
+ if (index > -1)
916
+ rootState.internal.interaction.splice(index, 1);
917
+ // Add the instance to the interaction manager only when it has handlers
918
+ if (localState.eventCount)
919
+ rootState.internal.interaction.push(instance);
1083
920
  }
1084
- validate() {
1085
- return !this.injected && !!this.injectedParent;
921
+ if (parent && localState.afterUpdate && localState.afterUpdate.observed && changes.length) {
922
+ localState.afterUpdate.emit(instance);
1086
923
  }
1087
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtParent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1088
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtParent, isStandalone: true, selector: "[parent]", inputs: { parent: "parent" }, usesInheritance: true, ngImport: i0 }); }
924
+ return instance;
1089
925
  }
1090
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtParent, decorators: [{
1091
- type: Directive,
1092
- args: [{ selector: '[parent]', standalone: true }]
1093
- }], propDecorators: { parent: [{
1094
- type: Input
1095
- }] } });
1096
926
 
1097
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
1098
- const captureData = captures.get(obj);
1099
- if (captureData) {
1100
- captures.delete(obj);
1101
- // if this was the last captured object for this pointer
1102
- if (captures.size === 0) {
1103
- capturedMap.delete(pointerId);
1104
- captureData.target.releasePointerCapture(pointerId);
1105
- }
927
+ const STORE_COMPUTED_KEY = '__ngt_store_computed__';
928
+ class NgtSignalStore {
929
+ #state;
930
+ #computedCache = new Map();
931
+ constructor(initialState = {}) {
932
+ initialState ??= {};
933
+ this.#state = signal(Object.assign(initialState, { __ngt_dummy_state__: Date.now() }));
934
+ }
935
+ select(...keysAndOptions) {
936
+ if (keysAndOptions.length === 0)
937
+ return this.#state.asReadonly();
938
+ if (keysAndOptions.length === 1 && typeof keysAndOptions[0] === 'object') {
939
+ if (!this.#computedCache.has(STORE_COMPUTED_KEY)) {
940
+ this.#computedCache.set(STORE_COMPUTED_KEY, computed(() => this.#state(), keysAndOptions));
941
+ return this.#computedCache.get(STORE_COMPUTED_KEY);
942
+ }
943
+ }
944
+ const [keys, options] = parseOptions(keysAndOptions);
945
+ const joinedKeys = keys.join('-');
946
+ if (!this.#computedCache.has(joinedKeys)) {
947
+ this.#computedCache.set(joinedKeys, computed(() => {
948
+ const state = this.#state();
949
+ return keys.reduce((value, key) => value[key], state);
950
+ }, options));
951
+ }
952
+ return this.#computedCache.get(joinedKeys);
953
+ }
954
+ get(...keys) {
955
+ const state = untracked(this.#state);
956
+ if (keys.length === 0)
957
+ return state;
958
+ return keys.reduce((value, key) => value[key], state);
959
+ }
960
+ set(state) {
961
+ this.#state.update((previous) => ({
962
+ ...previous,
963
+ ...(typeof state === 'function' ? state(previous) : state),
964
+ }));
1106
965
  }
966
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtSignalStore, deps: [{ token: 'INITIAL_STATE', optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
967
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtSignalStore }); }
1107
968
  }
1108
- function removeInteractivity(store, object) {
1109
- const internal = store.get('internal');
1110
- // removes every trace of an object from data store
1111
- internal.interaction = internal.interaction.filter((o) => o !== object);
1112
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
1113
- internal.hovered.forEach((value, key) => {
1114
- if (value.eventObject === object || value.object === object) {
1115
- // clear out intersects, they are outdated by now
1116
- internal.hovered.delete(key);
1117
- }
1118
- });
1119
- internal.capturedMap.forEach((captures, pointerId) => {
1120
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
1121
- });
1122
- if (store.get('previousStore')) {
1123
- removeInteractivity(store.get('previousStore'), object);
969
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtSignalStore, decorators: [{
970
+ type: Injectable
971
+ }], ctorParameters: function () { return [{ type: undefined, decorators: [{
972
+ type: Optional
973
+ }, {
974
+ type: Inject,
975
+ args: ['INITIAL_STATE']
976
+ }] }]; } });
977
+ function parseOptions(keysAndOptions) {
978
+ if (typeof keysAndOptions.at(-1) === 'object') {
979
+ return [keysAndOptions.slice(0, -1), keysAndOptions.at(-1)];
1124
980
  }
981
+ return [keysAndOptions];
1125
982
  }
1126
- function createEvents(store) {
1127
- /** calculates delta **/
1128
- function calculateDistance(event) {
1129
- const internal = store.get('internal');
1130
- const dx = event.offsetX - internal.initialClick[0];
1131
- const dy = event.offsetY - internal.initialClick[1];
1132
- return Math.round(Math.sqrt(dx * dx + dy * dy));
1133
- }
1134
- /** returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc... **/
1135
- function filterPointerEvents(objects) {
1136
- return objects.filter((obj) => ['move', 'over', 'enter', 'out', 'leave'].some((name) => {
1137
- const eventName = `pointer${name}`;
1138
- return getLocalState(obj).handlers?.[eventName];
1139
- }));
983
+
984
+ const rootStateMap = new Map();
985
+ const { invalidate, advance } = createLoop(rootStateMap);
986
+ const shallowLoose = { objects: 'shallow', strict: false };
987
+ class NgtStore extends NgtSignalStore {
988
+ constructor() {
989
+ super(...arguments);
990
+ this.#parentStore = inject(NgtStore, { optional: true, skipSelf: true });
991
+ this.#window = inject(DOCUMENT).defaultView;
992
+ this.#injector = inject(Injector);
993
+ this.isInit = false;
1140
994
  }
1141
- function intersect(event, filter) {
1142
- const state = store.get();
1143
- const duplicates = new Set();
1144
- const intersections = [];
1145
- // allow callers to eliminate event objects
1146
- const eventObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
1147
- // reset all raycaster cameras to undefined
1148
- for (let i = 0; i < eventObjects.length; i++) {
1149
- const instanceState = getLocalState(eventObjects[i]).store?.get();
1150
- if (instanceState) {
1151
- instanceState.raycaster.camera = undefined;
1152
- }
1153
- }
1154
- if (!state.previousStore) {
1155
- // make sure root-level pointer and ray are setup
1156
- state.events.compute?.(event, store);
1157
- }
1158
- function handleRaycast(obj) {
1159
- const objLocalState = getLocalState(obj);
1160
- const objStore = objLocalState.store;
1161
- const objState = objStore?.get();
1162
- // skip event handling when noEvents is set, or when raycaster camera is null
1163
- if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
1164
- return [];
1165
- // when the camera is undefined, we have to call the events layers to update function
1166
- if (objState.raycaster.camera === undefined) {
1167
- objState.events.compute?.(event, objStore, objState.previousStore);
1168
- // if the camera is still undefined, we have to skip this layer entirely
1169
- if (objState.raycaster.camera === undefined)
1170
- objState.raycaster.camera = null;
1171
- }
1172
- // intersect object by object
1173
- return objState.raycaster.camera ? objState.raycaster.intersectObject(obj, true) : [];
1174
- }
1175
- // collect events
1176
- let hits = eventObjects
1177
- // intersect objects
1178
- .flatMap(handleRaycast)
1179
- // sort by event priority
1180
- .sort((a, b) => {
1181
- const aState = getLocalState(a.object).store.get();
1182
- const bState = getLocalState(b.object).store.get();
1183
- if (!aState || !bState)
1184
- return a.distance - b.distance;
1185
- return bState.events.priority - aState.events.priority || a.distance - b.distance;
1186
- })
1187
- // filter out duplicates
1188
- .filter((item) => {
1189
- const id = makeId(item);
1190
- if (duplicates.has(id))
1191
- return false;
1192
- duplicates.add(id);
1193
- return true;
1194
- });
1195
- // allow custom userland intersect sort order, this likely only makes sense on the root
1196
- if (state.events.filter)
1197
- hits = state.events.filter(hits, store);
1198
- // bubble up the events, find the event source
1199
- for (const hit of hits) {
1200
- let eventObject = hit.object;
1201
- // bubble event up
1202
- while (eventObject) {
1203
- if (getLocalState(eventObject).eventCount) {
1204
- intersections.push({ ...hit, eventObject });
995
+ #parentStore;
996
+ #window;
997
+ #injector;
998
+ init() {
999
+ if (!this.isInit) {
1000
+ const position = new THREE.Vector3();
1001
+ const defaultTarget = new THREE.Vector3();
1002
+ const tempTarget = new THREE.Vector3();
1003
+ const getCurrentViewport = (camera = this.get('camera'), target = defaultTarget, size = this.get('size')) => {
1004
+ const { width, height, top, left } = size;
1005
+ const aspect = width / height;
1006
+ if (target instanceof THREE.Vector3)
1007
+ tempTarget.copy(target);
1008
+ else
1009
+ tempTarget.set(...target);
1010
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1011
+ if (is.orthographicCamera(camera)) {
1012
+ return {
1013
+ width: width / camera.zoom,
1014
+ height: height / camera.zoom,
1015
+ top,
1016
+ left,
1017
+ factor: 1,
1018
+ distance,
1019
+ aspect,
1020
+ };
1021
+ }
1022
+ const fov = (camera.fov * Math.PI) / 180; // convert vertical fov to radians
1023
+ const h = 2 * Math.tan(fov / 2) * distance; // visible height
1024
+ const w = h * aspect;
1025
+ return { width: w, height: h, top, left, factor: width / w, distance, aspect };
1026
+ };
1027
+ let performanceTimeout;
1028
+ const setPerformanceCurrent = (current) => {
1029
+ this.set((state) => ({ performance: { ...state.performance, current } }));
1030
+ };
1031
+ this.set({
1032
+ get: this.get.bind(this),
1033
+ set: this.set.bind(this),
1034
+ ready: false,
1035
+ events: { priority: 1, enabled: true, connected: false },
1036
+ invalidate: (frames = 1) => invalidate(this, frames),
1037
+ advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, this),
1038
+ legacy: false,
1039
+ linear: false,
1040
+ flat: false,
1041
+ controls: null,
1042
+ clock: new THREE.Clock(),
1043
+ pointer: new THREE.Vector2(),
1044
+ frameloop: 'always',
1045
+ performance: {
1046
+ current: 1,
1047
+ min: 0.5,
1048
+ max: 1,
1049
+ debounce: 200,
1050
+ regress: () => {
1051
+ const state = this.get();
1052
+ // clear timeout
1053
+ if (performanceTimeout)
1054
+ clearTimeout(performanceTimeout);
1055
+ // set lower bound
1056
+ if (state.performance.current !== state.performance.min)
1057
+ setPerformanceCurrent(state.performance.min);
1058
+ // go back to upper bound
1059
+ performanceTimeout = setTimeout(() => setPerformanceCurrent(this.get('performance', 'max') || 1), state.performance.debounce);
1060
+ },
1061
+ },
1062
+ size: { width: 0, height: 0, top: 0, left: 0 },
1063
+ viewport: {
1064
+ initialDpr: 0,
1065
+ dpr: 0,
1066
+ width: 0,
1067
+ height: 0,
1068
+ top: 0,
1069
+ left: 0,
1070
+ aspect: 0,
1071
+ distance: 0,
1072
+ factor: 0,
1073
+ getCurrentViewport,
1074
+ },
1075
+ previousStore: this.#parentStore,
1076
+ internal: {
1077
+ active: false,
1078
+ priority: 0,
1079
+ frames: 0,
1080
+ lastEvent: new ElementRef(null),
1081
+ interaction: [],
1082
+ hovered: new Map(),
1083
+ subscribers: [],
1084
+ initialClick: [0, 0],
1085
+ initialHits: [],
1086
+ capturedMap: new Map(),
1087
+ subscribe: (callback, priority = 0, store = this) => {
1088
+ const internal = this.get('internal');
1089
+ // If this subscription was given a priority, it takes rendering into its own hands
1090
+ // For that reason we switch off automatic rendering and increase the manual flag
1091
+ // As long as this flag is positive there can be no internal rendering at all
1092
+ // because there could be multiple render subscriptions
1093
+ internal.priority = internal.priority + (priority > 0 ? 1 : 0);
1094
+ internal.subscribers.push({ priority, store, callback });
1095
+ // Register subscriber and sort layers from lowest to highest, meaning,
1096
+ // highest priority renders last (on top of the other frames)
1097
+ internal.subscribers.sort((a, b) => (a.priority || 0) - (b.priority || 0));
1098
+ return () => {
1099
+ const internal = this.get('internal');
1100
+ if (internal?.subscribers) {
1101
+ // Decrease manual flag if this subscription had a priority
1102
+ internal.priority = internal.priority - (priority > 0 ? 1 : 0);
1103
+ // Remove subscriber from list
1104
+ internal.subscribers = internal.subscribers.filter((s) => s.callback !== callback);
1105
+ }
1106
+ };
1107
+ },
1108
+ },
1109
+ setEvents: (events) => {
1110
+ this.set((state) => ({ events: { ...state.events, ...events } }));
1111
+ },
1112
+ setSize: (width, height, top, left) => {
1113
+ const camera = this.get('camera');
1114
+ const size = { width, height, top: top || 0, left: left || 0 };
1115
+ this.set((state) => ({
1116
+ size,
1117
+ viewport: { ...state.viewport, ...getCurrentViewport(camera, defaultTarget, size) },
1118
+ }));
1119
+ },
1120
+ setDpr: (dpr) => {
1121
+ const resolved = makeDpr(dpr, this.#window);
1122
+ this.set((state) => ({
1123
+ viewport: {
1124
+ ...state.viewport,
1125
+ dpr: resolved,
1126
+ initialDpr: state.viewport.initialDpr || resolved,
1127
+ },
1128
+ }));
1129
+ },
1130
+ setFrameloop: (frameloop = 'always') => {
1131
+ const clock = this.get('clock');
1132
+ clock.stop();
1133
+ clock.elapsedTime = 0;
1134
+ if (frameloop !== 'never') {
1135
+ clock.start();
1136
+ clock.elapsedTime = 0;
1137
+ }
1138
+ this.set({ frameloop });
1139
+ },
1140
+ });
1141
+ this.isInit = true;
1142
+ this.#resize();
1143
+ }
1144
+ }
1145
+ configure(inputs, canvasElement) {
1146
+ const { gl: glOptions, size: sizeOptions, camera: cameraOptions, raycaster: raycasterOptions, scene: sceneOptions, events, orthographic, lookAt, shadows, linear, legacy, flat, dpr, frameloop, performance, } = inputs;
1147
+ const state = this.get();
1148
+ const stateToUpdate = {};
1149
+ // setup renderer
1150
+ let gl = state.gl;
1151
+ if (!state.gl)
1152
+ stateToUpdate.gl = gl = makeDefaultRenderer(glOptions, canvasElement);
1153
+ // setup raycaster
1154
+ let raycaster = state.raycaster;
1155
+ if (!raycaster)
1156
+ stateToUpdate.raycaster = raycaster = new THREE.Raycaster();
1157
+ // set raycaster options
1158
+ const { params, ...options } = raycasterOptions || {};
1159
+ if (!is.equ(options, raycaster, shallowLoose))
1160
+ applyProps(raycaster, { ...options });
1161
+ if (!is.equ(params, raycaster.params, shallowLoose)) {
1162
+ applyProps(raycaster, { params: { ...raycaster.params, ...(params || {}) } });
1163
+ }
1164
+ // create default camera
1165
+ if (!state.camera) {
1166
+ const isCamera = is.camera(cameraOptions);
1167
+ let camera = isCamera ? cameraOptions : makeDefaultCamera(orthographic || false, state.size);
1168
+ if (!isCamera) {
1169
+ if (cameraOptions)
1170
+ applyProps(camera, cameraOptions);
1171
+ // set position.z
1172
+ if (!cameraOptions?.position)
1173
+ camera.position.z = 5;
1174
+ // always look at center or passed-in lookAt by default
1175
+ if (!cameraOptions?.rotation && !cameraOptions?.quaternion) {
1176
+ if (Array.isArray(lookAt))
1177
+ camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
1178
+ else if (lookAt instanceof THREE.Vector3)
1179
+ camera.lookAt(lookAt);
1180
+ else
1181
+ camera.lookAt(0, 0, 0);
1205
1182
  }
1206
- eventObject = eventObject.parent;
1183
+ // update projection matrix after applyprops
1184
+ camera.updateProjectionMatrix?.();
1207
1185
  }
1186
+ if (!is.instance(camera))
1187
+ camera = prepare(camera, { store: this });
1188
+ stateToUpdate.camera = camera;
1208
1189
  }
1209
- // if the interaction is captured, make all capturing targets part of the intersects
1210
- if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
1211
- for (const capturedData of state.internal.capturedMap.get(event.pointerId).values()) {
1212
- if (!duplicates.has(makeId(capturedData.intersection))) {
1213
- intersections.push(capturedData.intersection);
1214
- }
1190
+ // Set up scene (one time only!)
1191
+ if (!state.scene) {
1192
+ let scene;
1193
+ if (sceneOptions instanceof THREE.Scene) {
1194
+ scene = prepare(sceneOptions, { store: this });
1195
+ }
1196
+ else {
1197
+ scene = prepare(new THREE.Scene(), { store: this });
1198
+ if (sceneOptions)
1199
+ applyProps(scene, sceneOptions);
1215
1200
  }
1201
+ stateToUpdate.scene = scene;
1216
1202
  }
1217
- return intersections;
1218
- }
1219
- /** handle intersections by forwarding them to handlers */
1220
- function handleIntersects(intersections, event, delta, callback) {
1221
- const rootState = store.get();
1222
- // if anything has been found, forward it to the event listeners
1223
- if (intersections.length) {
1224
- const localState = { stopped: false };
1225
- for (const hit of intersections) {
1226
- const state = getLocalState(hit.object).store?.get() || rootState;
1227
- const { raycaster, pointer, camera, internal } = state;
1228
- const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
1229
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1230
- const setPointerCapture = (id) => {
1231
- const captureData = { intersection: hit, target: event.target };
1232
- if (internal.capturedMap.has(id)) {
1233
- // if the pointerId was previously captured, we add the hit to the event capturedMap
1234
- internal.capturedMap.get(id).set(hit.eventObject, captureData);
1235
- }
1236
- else {
1237
- // if the pointerId was not previously captured, we create a Map containing the hitObject, and the hit. hitObject is used for faster access
1238
- internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]]));
1239
- }
1240
- // call the original event now
1241
- event.target.setPointerCapture(id);
1242
- };
1243
- const releasePointerCapture = (id) => {
1244
- const captures = internal.capturedMap.get(id);
1245
- if (captures) {
1246
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
1247
- }
1248
- };
1249
- // add native event props
1250
- const extractEventProps = {};
1251
- // This iterates over the event's properties including the inherited ones. Native PointerEvents have most of their props as getters which are inherited, but polyfilled PointerEvents have them all as their own properties (i.e. not inherited). We can't use Object.keys() or Object.entries() as they only return "own" properties; nor Object.getPrototypeOf(event) as that *doesn't* return "own" properties, only inherited ones.
1252
- for (const prop in event) {
1253
- const property = event[prop];
1254
- // only copy over atomics, leave functions alone as these should be called as event.nativeEvent.fn()
1255
- if (typeof property !== 'function') {
1256
- extractEventProps[prop] = property;
1257
- }
1258
- }
1259
- const raycastEvent = {
1260
- ...hit,
1261
- ...extractEventProps,
1262
- pointer,
1263
- intersections,
1264
- stopped: localState.stopped,
1265
- delta,
1266
- unprojectedPoint,
1267
- ray: raycaster.ray,
1268
- camera: camera,
1269
- // Hijack stopPropagation, which just sets a flag
1270
- stopPropagation() {
1271
- // https://github.com/pmndrs/react-three-fiber/issues/596
1272
- // Events are not allowed to stop propagation if the pointer has been captured
1273
- const capturesForPointer = 'pointerId' in event && internal.capturedMap.get(event.pointerId);
1274
- // We only authorize stopPropagation...
1275
- if (
1276
- // ...if this pointer hasn't been captured
1277
- !capturesForPointer ||
1278
- // ... or if the hit object is capturing the pointer
1279
- capturesForPointer.has(hit.eventObject)) {
1280
- raycastEvent.stopped = localState.stopped = true;
1281
- // Propagation is stopped, remove all other hover records
1282
- // An event handler is only allowed to flush other handlers if it is hovered itself
1283
- if (internal.hovered.size &&
1284
- Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1285
- // Objects cannot flush out higher up objects that have already caught the event
1286
- const higher = intersections.slice(0, intersections.indexOf(hit));
1287
- cancelPointer([...higher, hit]);
1288
- }
1289
- }
1290
- },
1291
- // there should be a distinction between target and currentTarget
1292
- target: { hasPointerCapture, setPointerCapture, releasePointerCapture },
1293
- currentTarget: { hasPointerCapture, setPointerCapture, releasePointerCapture },
1294
- nativeEvent: event,
1203
+ // Set up XR (one time only!)
1204
+ if (!state.xr) {
1205
+ // Handle frame behavior in WebXR
1206
+ const handleXRFrame = (timestamp, frame) => {
1207
+ const state = this.get();
1208
+ if (state.frameloop === 'never')
1209
+ return;
1210
+ advance(timestamp, true, this, frame);
1211
+ };
1212
+ // Toggle render switching on session
1213
+ const handleSessionChange = () => {
1214
+ const state = this.get();
1215
+ state.gl.xr.enabled = state.gl.xr.isPresenting;
1216
+ state.gl.xr.setAnimationLoop(state.gl.xr.isPresenting ? handleXRFrame : null);
1217
+ if (!state.gl.xr.isPresenting)
1218
+ state.invalidate();
1219
+ };
1220
+ // WebXR session manager
1221
+ const xr = {
1222
+ connect: () => {
1223
+ gl.xr.addEventListener('sessionstart', handleSessionChange);
1224
+ gl.xr.addEventListener('sessionend', handleSessionChange);
1225
+ },
1226
+ disconnect: () => {
1227
+ gl.xr.removeEventListener('sessionstart', handleSessionChange);
1228
+ gl.xr.removeEventListener('sessionend', handleSessionChange);
1229
+ },
1230
+ };
1231
+ // Subscribe to WebXR session events
1232
+ if (gl.xr)
1233
+ xr.connect();
1234
+ stateToUpdate.xr = xr;
1235
+ }
1236
+ // Set shadowmap
1237
+ if (gl.shadowMap) {
1238
+ const oldEnabled = gl.shadowMap.enabled;
1239
+ const oldType = gl.shadowMap.type;
1240
+ gl.shadowMap.enabled = !!shadows;
1241
+ if (typeof shadows === 'boolean') {
1242
+ gl.shadowMap.type = THREE.PCFSoftShadowMap;
1243
+ }
1244
+ else if (typeof shadows === 'string') {
1245
+ const types = {
1246
+ basic: THREE.BasicShadowMap,
1247
+ percentage: THREE.PCFShadowMap,
1248
+ soft: THREE.PCFSoftShadowMap,
1249
+ variance: THREE.VSMShadowMap,
1295
1250
  };
1296
- // call subscribers
1297
- callback(raycastEvent);
1298
- // event bubbling may be interupted by stopPropagation
1299
- if (localState.stopped === true)
1300
- break;
1251
+ gl.shadowMap.type = types[shadows] ?? THREE.PCFSoftShadowMap;
1301
1252
  }
1253
+ else if (is.obj(shadows)) {
1254
+ Object.assign(gl.shadowMap, shadows);
1255
+ }
1256
+ if (oldEnabled !== gl.shadowMap.enabled || oldType !== gl.shadowMap.type)
1257
+ checkNeedsUpdate(gl.shadowMap);
1302
1258
  }
1303
- return intersections;
1259
+ // Safely set color management if available.
1260
+ // Avoid accessing THREE.ColorManagement to play nice with older versions
1261
+ if (THREE.ColorManagement) {
1262
+ const ColorManagement = THREE.ColorManagement;
1263
+ if ('enabled' in ColorManagement)
1264
+ ColorManagement['enabled'] = !legacy ?? false;
1265
+ else if ('legacyMode' in ColorManagement)
1266
+ ColorManagement['legacyMode'] = legacy ?? true;
1267
+ }
1268
+ // set color space and tonemapping preferences
1269
+ const LinearEncoding = 3000;
1270
+ const sRGBEncoding = 3001;
1271
+ applyProps(gl, {
1272
+ outputEncoding: linear ? LinearEncoding : sRGBEncoding,
1273
+ toneMapping: flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping,
1274
+ });
1275
+ // Update color management state
1276
+ if (state.legacy !== legacy)
1277
+ stateToUpdate.legacy = legacy;
1278
+ if (state.linear !== linear)
1279
+ stateToUpdate.linear = linear;
1280
+ if (state.flat !== flat)
1281
+ stateToUpdate.flat = flat;
1282
+ // Set gl props
1283
+ gl.setClearAlpha(0);
1284
+ gl.setPixelRatio(makeDpr(state.viewport.dpr));
1285
+ gl.setSize(state.size.width, state.size.height);
1286
+ if (is.obj(glOptions) &&
1287
+ !(typeof glOptions === 'function') &&
1288
+ !is.renderer(glOptions) &&
1289
+ !is.equ(glOptions, gl, shallowLoose)) {
1290
+ applyProps(gl, glOptions);
1291
+ }
1292
+ // Store events internally
1293
+ if (events && !state.events.handlers)
1294
+ stateToUpdate.events = events(this);
1295
+ // Check performance
1296
+ if (performance && !is.equ(performance, state.performance, shallowLoose)) {
1297
+ stateToUpdate.performance = { ...state.performance, ...performance };
1298
+ }
1299
+ this.set(stateToUpdate);
1300
+ // Check pixelratio
1301
+ if (dpr && state.viewport.dpr !== makeDpr(dpr))
1302
+ state.setDpr(dpr);
1303
+ // Check size, allow it to take on container bounds initially
1304
+ const size = computeInitialSize(canvasElement, sizeOptions);
1305
+ if (!is.equ(size, state.size, shallowLoose))
1306
+ state.setSize(size.width, size.height, size.top, size.left);
1307
+ // Check frameloop
1308
+ if (state.frameloop !== frameloop)
1309
+ state.setFrameloop(frameloop);
1310
+ if (!this.get('ready'))
1311
+ this.set({ ready: true });
1312
+ this.#invalidate();
1304
1313
  }
1305
- function cancelPointer(intersections) {
1306
- const { internal } = store.get();
1307
- for (const hoveredObj of internal.hovered.values()) {
1308
- // When no objects were hit or the hovered object wasn't found underneath the cursor
1309
- // we call onPointerOut and delete the object from the hovered-elements map
1310
- if (!intersections.length ||
1311
- !intersections.find((hit) => hit.object === hoveredObj.object &&
1312
- hit.index === hoveredObj.index &&
1313
- hit.instanceId === hoveredObj.instanceId)) {
1314
- const eventObject = hoveredObj.eventObject;
1315
- const instance = getLocalState(eventObject);
1316
- const handlers = instance?.handlers;
1317
- internal.hovered.delete(makeId(hoveredObj));
1318
- if (instance?.eventCount) {
1319
- // Clear out intersects, they are outdated by now
1320
- const data = { ...hoveredObj, intersections };
1321
- handlers?.pointerout?.(data);
1322
- handlers?.pointerleave?.(data);
1314
+ destroy(canvas) {
1315
+ this.set((state) => ({ internal: { ...state.internal, active: false } }));
1316
+ setTimeout(() => {
1317
+ const { gl, xr, events } = this.get();
1318
+ if (gl) {
1319
+ if (events.disconnect) {
1320
+ events.disconnect();
1323
1321
  }
1322
+ gl.renderLists.dispose();
1323
+ gl.forceContextLoss();
1324
+ if (gl.xr && gl.xr.enabled) {
1325
+ gl.xr.setAnimationLoop(null);
1326
+ xr.disconnect();
1327
+ }
1328
+ dispose(this.get());
1329
+ rootStateMap.delete(canvas);
1324
1330
  }
1325
- }
1326
- }
1327
- function pointerMissed(event, objects) {
1328
- for (let i = 0; i < objects.length; i++) {
1329
- const instance = getLocalState(objects[i]);
1330
- instance?.handlers.pointermissed?.(event);
1331
- }
1331
+ }, 500);
1332
1332
  }
1333
- function handlePointer(name) {
1334
- // Deal with cancelation
1335
- switch (name) {
1336
- case 'pointerleave':
1337
- case 'pointercancel':
1338
- return () => cancelPointer([]);
1339
- case 'lostpointercapture':
1340
- return (event) => {
1341
- const { internal } = store.get();
1342
- if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
1343
- // If the object event interface had onLostPointerCapture, we'd call it here on every
1344
- // object that's getting removed.
1345
- internal.capturedMap.delete(event.pointerId);
1346
- cancelPointer([]);
1347
- }
1348
- };
1349
- }
1350
- // Any other pointer goes here ...
1351
- return function handleEvent(event) {
1352
- const { onPointerMissed, internal } = store.get();
1353
- // prepareRay(event)
1354
- internal.lastEvent = event;
1355
- // Get fresh intersects
1356
- const isPointerMove = name === 'pointermove';
1357
- const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick';
1358
- const filter = isPointerMove ? filterPointerEvents : undefined;
1359
- // const hits = patchIntersects(intersect(filter), event)
1360
- const hits = intersect(event, filter);
1361
- const delta = isClickEvent ? calculateDistance(event) : 0;
1362
- // Save initial coordinates on pointer-down
1363
- if (name === 'pointerdown') {
1364
- internal.initialClick = [event.offsetX, event.offsetY];
1365
- internal.initialHits = hits.map((hit) => hit.eventObject);
1366
- }
1367
- // If a click yields no results, pass it back to the user as a miss
1368
- // Missed events have to come first in order to establish user-land side-effect clean up
1369
- if (isClickEvent && !hits.length) {
1370
- if (delta <= 2) {
1371
- pointerMissed(event, internal.interaction);
1372
- if (onPointerMissed)
1373
- onPointerMissed(event);
1374
- }
1333
+ #resize() {
1334
+ const state = this.get();
1335
+ let oldSize = state.size;
1336
+ let oldDpr = state.viewport.dpr;
1337
+ let oldCamera = state.camera;
1338
+ const triggers = computed(() => {
1339
+ return {
1340
+ camera: this.select('camera')(),
1341
+ size: this.select('size')(),
1342
+ viewport: this.select('viewport')(),
1343
+ gl: this.get('gl'),
1344
+ };
1345
+ });
1346
+ effect(() => {
1347
+ const { camera, size, viewport, gl } = triggers();
1348
+ // resize camera and renderer on changes to size and dpr
1349
+ if (size !== oldSize || viewport.dpr !== oldDpr) {
1350
+ oldSize = size;
1351
+ oldDpr = viewport.dpr;
1352
+ // update camera
1353
+ updateCamera(camera, size);
1354
+ gl.setPixelRatio(viewport.dpr);
1355
+ gl.setSize(size.width, size.height);
1375
1356
  }
1376
- // Take care of unhover
1377
- if (isPointerMove)
1378
- cancelPointer(hits);
1379
- function onIntersect(data) {
1380
- const eventObject = data.eventObject;
1381
- const instance = getLocalState(eventObject);
1382
- const handlers = instance?.handlers;
1383
- // Check presence of handlers
1384
- if (!instance?.eventCount)
1385
- return;
1386
- if (isPointerMove) {
1387
- // Move event ...
1388
- if (handlers?.pointerover ||
1389
- handlers?.pointerenter ||
1390
- handlers?.pointerout ||
1391
- handlers?.pointerleave) {
1392
- // When enter or out is present take care of hover-state
1393
- const id = makeId(data);
1394
- const hoveredItem = internal.hovered.get(id);
1395
- if (!hoveredItem) {
1396
- // If the object wasn't previously hovered, book it and call its handler
1397
- internal.hovered.set(id, data);
1398
- handlers.pointerover?.(data);
1399
- handlers.pointerenter?.(data);
1400
- }
1401
- else if (hoveredItem.stopped) {
1402
- // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
1403
- data.stopPropagation();
1404
- }
1405
- }
1406
- // Call mouse move
1407
- handlers?.pointermove?.(data);
1408
- }
1409
- else {
1410
- // All other events ...
1411
- const handler = handlers?.[name];
1412
- if (handler) {
1413
- // Forward all events back to their respective handlers with the exception of click events,
1414
- // which must use the initial target
1415
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1416
- // Missed events have to come first
1417
- pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1418
- // Now call the handler
1419
- handler(data);
1420
- }
1421
- }
1422
- else {
1423
- // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
1424
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1425
- pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1426
- }
1427
- }
1428
- }
1357
+ // update viewport when camera changes
1358
+ if (camera !== oldCamera) {
1359
+ oldCamera = camera;
1360
+ updateCamera(camera, size);
1361
+ this.set((state) => ({
1362
+ viewport: { ...state.viewport, ...state.viewport.getCurrentViewport(camera) },
1363
+ }));
1429
1364
  }
1430
- handleIntersects(hits, event, delta, onIntersect);
1431
- };
1365
+ }, { injector: this.#injector, allowSignalWrites: true });
1432
1366
  }
1433
- return { handlePointer };
1367
+ #invalidate() {
1368
+ const state = this.select();
1369
+ effect(() => {
1370
+ state().invalidate();
1371
+ }, { injector: this.#injector });
1372
+ }
1373
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtStore, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
1374
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtStore }); }
1375
+ }
1376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtStore, decorators: [{
1377
+ type: Injectable
1378
+ }] });
1379
+ function computeInitialSize(canvas, defaultSize) {
1380
+ if (defaultSize)
1381
+ return defaultSize;
1382
+ if (canvas instanceof HTMLCanvasElement && canvas.parentElement) {
1383
+ return canvas.parentElement.getBoundingClientRect();
1384
+ }
1385
+ return { width: 0, height: 0, top: 0, left: 0 };
1386
+ }
1387
+ // Disposes an object and all its properties
1388
+ function dispose(obj) {
1389
+ if (obj.dispose && !is.scene(obj))
1390
+ obj.dispose();
1391
+ for (const p in obj) {
1392
+ p.dispose?.();
1393
+ delete obj[p];
1394
+ }
1395
+ }
1396
+
1397
+ const NGT_COMPOUND_PREFIXES = new InjectionToken('NgtCompoundPrefixes');
1398
+
1399
+ const catalogue = {};
1400
+ function extend(objects) {
1401
+ Object.assign(catalogue, objects);
1434
1402
  }
1403
+ const NGT_CATALOGUE = new InjectionToken('THREE Constructors Catalogue', { factory: () => catalogue });
1435
1404
 
1436
1405
  function attach(object, value, paths = []) {
1437
1406
  const [base, ...remaining] = paths;
@@ -1463,6 +1432,8 @@ function createAttachFunction(cb) {
1463
1432
  return (parent, child, store) => cb({ parent, child, store });
1464
1433
  }
1465
1434
 
1435
+ const ROUTED_SCENE = '__ngt_renderer_is_routed_scene__';
1436
+ const SPECIAL_INTERNAL_ADD_COMMENT = '__ngt_renderer_add_comment__';
1466
1437
  const SPECIAL_DOM_TAG = {
1467
1438
  NGT_PORTAL: 'ngt-portal',
1468
1439
  NGT_PRIMITIVE: 'ngt-primitive',
@@ -1516,7 +1487,7 @@ function attachThreeChild(parent, child) {
1516
1487
  }
1517
1488
  // attach
1518
1489
  if (cLS.isRaw) {
1519
- cLS.parent = parent;
1490
+ cLS.parent.set(parent);
1520
1491
  // at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
1521
1492
  if (child.__ngt_renderer__[11 /* NgtRendererClassId.rawValue */] === undefined)
1522
1493
  return;
@@ -1534,7 +1505,7 @@ function attachThreeChild(parent, child) {
1534
1505
  added = true;
1535
1506
  }
1536
1507
  pLS.add(child, added ? 'objects' : 'nonObjects');
1537
- cLS.parent = parent;
1508
+ cLS.parent.set(parent);
1538
1509
  if (cLS.afterAttach)
1539
1510
  cLS.afterAttach.emit({ parent, node: child });
1540
1511
  invalidateInstance(child);
@@ -1544,11 +1515,11 @@ function removeThreeChild(parent, child, dispose) {
1544
1515
  const pLS = getLocalState(parent);
1545
1516
  const cLS = getLocalState(child);
1546
1517
  // clear parent ref
1547
- cLS.parent = null;
1518
+ cLS.parent?.set(null);
1548
1519
  // remove child from parent
1549
- if (pLS.objects)
1520
+ if (untracked(pLS.objects))
1550
1521
  pLS.remove(child, 'objects');
1551
- if (pLS.nonObjects)
1522
+ if (untracked(pLS.nonObjects))
1552
1523
  pLS.remove(child, 'nonObjects');
1553
1524
  if (cLS.attach) {
1554
1525
  detach(parent, child, cLS.attach);
@@ -1559,8 +1530,8 @@ function removeThreeChild(parent, child, dispose) {
1559
1530
  }
1560
1531
  const isPrimitive = cLS.primitive;
1561
1532
  if (!isPrimitive) {
1562
- removeThreeRecursive(cLS.objects?.value || [], child, !!dispose);
1563
- removeThreeRecursive(child.childre, child, !!dispose);
1533
+ removeThreeRecursive(cLS.objects ? untracked(cLS.objects) : [], child, !!dispose);
1534
+ removeThreeRecursive(child.children, child, !!dispose);
1564
1535
  }
1565
1536
  // dispose
1566
1537
  if (!isPrimitive && child['dispose'] && !is.scene(child)) {
@@ -1572,64 +1543,164 @@ function removeThreeRecursive(array, parent, dispose) {
1572
1543
  if (array)
1573
1544
  [...array].forEach((child) => removeThreeChild(parent, child, dispose));
1574
1545
  }
1575
- function processThreeEvent(instance, priority, eventName, callback, cdr, targetCdr) {
1546
+ function processThreeEvent(instance, priority, eventName, callback, zone, cdr) {
1576
1547
  const lS = getLocalState(instance);
1577
1548
  if (eventName === SPECIAL_EVENTS.BEFORE_RENDER) {
1578
1549
  return lS.store
1579
1550
  .get('internal')
1580
1551
  .subscribe((state) => callback({ state, object: instance }), priority || lS.priority || 0);
1581
1552
  }
1582
- if (eventName === SPECIAL_EVENTS.AFTER_UPDATE || eventName === SPECIAL_EVENTS.AFTER_ATTACH) {
1583
- let emitter = lS[eventName];
1584
- if (!emitter)
1585
- emitter = lS[eventName] = new EventEmitter();
1586
- const sub = emitter.subscribe(callback);
1587
- return sub.unsubscribe.bind(sub);
1553
+ if (eventName === SPECIAL_EVENTS.AFTER_UPDATE || eventName === SPECIAL_EVENTS.AFTER_ATTACH) {
1554
+ let emitter = lS[eventName];
1555
+ if (!emitter)
1556
+ emitter = lS[eventName] = new EventEmitter();
1557
+ const sub = emitter.subscribe(callback);
1558
+ return sub.unsubscribe.bind(sub);
1559
+ }
1560
+ if (!lS.handlers)
1561
+ lS.handlers = {};
1562
+ // try to get the previous handler. compound might have one, the THREE object might also have one with the same name
1563
+ const previousHandler = lS.handlers[eventName];
1564
+ // readjust the callback
1565
+ const updatedCallback = (event) => {
1566
+ if (previousHandler)
1567
+ previousHandler(event);
1568
+ zone.run(() => {
1569
+ callback(event);
1570
+ safeDetectChanges(cdr);
1571
+ // cdr.detectChanges();
1572
+ });
1573
+ };
1574
+ Object.assign(lS.handlers, { [eventName]: eventToHandler(updatedCallback) });
1575
+ // increment the count everytime
1576
+ lS.eventCount += 1;
1577
+ // but only add the instance (target) to the interaction array (so that it is handled by the EventManager with Raycast)
1578
+ // the first time eventCount is incremented
1579
+ if (lS.eventCount === 1 && instance['raycast'])
1580
+ lS.store.get('internal', 'interaction').push(instance);
1581
+ // clean up the event listener by removing the target from the interaction array
1582
+ return () => {
1583
+ const localState = getLocalState(instance);
1584
+ if (localState && localState.eventCount) {
1585
+ const index = localState.store
1586
+ .get('internal', 'interaction')
1587
+ .findIndex((obj) => obj.uuid === instance.uuid);
1588
+ if (index >= 0)
1589
+ localState.store.get('internal', 'interaction').splice(index, 1);
1590
+ }
1591
+ };
1592
+ }
1593
+ function eventToHandler(callback) {
1594
+ return (event) => {
1595
+ callback(event);
1596
+ };
1597
+ }
1598
+ function kebabToPascal(str) {
1599
+ // split the string at each hyphen
1600
+ const parts = str.split('-');
1601
+ // map over the parts, capitalizing the first letter of each part
1602
+ const pascalParts = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1));
1603
+ // join the parts together to create the final PascalCase string
1604
+ return pascalParts.join('');
1605
+ }
1606
+
1607
+ class NgtCommonDirective {
1608
+ #vcr;
1609
+ #zone;
1610
+ #template;
1611
+ #view;
1612
+ constructor() {
1613
+ this.#vcr = inject(ViewContainerRef);
1614
+ this.#zone = inject(NgZone);
1615
+ this.#template = inject(TemplateRef);
1616
+ this.injected = false;
1617
+ this.shouldCreateView = true;
1618
+ const commentNode = this.#vcr.element.nativeElement;
1619
+ if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT]) {
1620
+ commentNode[SPECIAL_INTERNAL_ADD_COMMENT]();
1621
+ delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT];
1622
+ }
1623
+ }
1624
+ createView() {
1625
+ if (this.shouldCreateView) {
1626
+ if (this.#view && !this.#view.destroyed) {
1627
+ this.#view.destroy();
1628
+ }
1629
+ this.#zone.runOutsideAngular(() => {
1630
+ this.#view = this.#vcr.createEmbeddedView(this.#template);
1631
+ this.#view.detectChanges();
1632
+ });
1633
+ }
1634
+ }
1635
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCommonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1636
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtCommonDirective, ngImport: i0 }); }
1637
+ }
1638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCommonDirective, decorators: [{
1639
+ type: Directive
1640
+ }], ctorParameters: function () { return []; } });
1641
+
1642
+ class NgtArgs extends NgtCommonDirective {
1643
+ #injectedArgs = [];
1644
+ set args(args) {
1645
+ if (args == null || !Array.isArray(args) || (args.length === 1 && args[0] === null))
1646
+ return;
1647
+ this.injected = false;
1648
+ this.#injectedArgs = args;
1649
+ this.createView();
1650
+ }
1651
+ get args() {
1652
+ if (this.validate()) {
1653
+ this.injected = true;
1654
+ return this.#injectedArgs;
1655
+ }
1656
+ return null;
1657
+ }
1658
+ validate() {
1659
+ return !this.injected && !!this.#injectedArgs.length;
1660
+ }
1661
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtArgs, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1662
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtArgs, isStandalone: true, selector: "[args]", inputs: { args: "args" }, usesInheritance: true, ngImport: i0 }); }
1663
+ }
1664
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtArgs, decorators: [{
1665
+ type: Directive,
1666
+ args: [{ selector: '[args]', standalone: true }]
1667
+ }], propDecorators: { args: [{
1668
+ type: Input
1669
+ }] } });
1670
+
1671
+ class NgtParent extends NgtCommonDirective {
1672
+ #injectedParent = null;
1673
+ set parent(parent) {
1674
+ if (!parent)
1675
+ return;
1676
+ this.injected = false;
1677
+ this.#injectedParent = parent;
1678
+ this.createView();
1679
+ }
1680
+ get parent() {
1681
+ if (this.validate()) {
1682
+ this.injected = true;
1683
+ return this.#injectedParent;
1684
+ }
1685
+ return null;
1588
1686
  }
1589
- if (!lS.handlers)
1590
- lS.handlers = {};
1591
- // try to get the previous handler. compound might have one, the THREE object might also have one with the same name
1592
- const previousHandler = lS.handlers[eventName];
1593
- // readjust the callback
1594
- const updatedCallback = (event) => {
1595
- if (previousHandler)
1596
- previousHandler(event);
1597
- callback(event);
1598
- };
1599
- Object.assign(lS.handlers, { [eventName]: eventToHandler(updatedCallback, cdr, targetCdr) });
1600
- // increment the count everytime
1601
- lS.eventCount += 1;
1602
- // but only add the instance (target) to the interaction array (so that it is handled by the EventManager with Raycast)
1603
- // the first time eventCount is incremented
1604
- if (lS.eventCount === 1 && instance['raycast'])
1605
- lS.store.get('addInteraction')(instance);
1606
- // clean up the event listener by removing the target from the interaction array
1607
- return () => {
1608
- const localState = getLocalState(instance);
1609
- if (localState && localState.eventCount)
1610
- localState.store.get('removeInteraction')(instance['uuid']);
1611
- };
1612
- }
1613
- function eventToHandler(callback, cdr, targetCdr) {
1614
- return (event) => {
1615
- callback(event);
1616
- safeDetectChanges(targetCdr);
1617
- safeDetectChanges(cdr);
1618
- };
1619
- }
1620
- function kebabToPascal(str) {
1621
- // split the string at each hyphen
1622
- const parts = str.split('-');
1623
- // map over the parts, capitalizing the first letter of each part
1624
- const pascalParts = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1));
1625
- // join the parts together to create the final PascalCase string
1626
- return pascalParts.join('');
1687
+ validate() {
1688
+ return !this.injected && !!this.#injectedParent;
1689
+ }
1690
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtParent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1691
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtParent, isStandalone: true, selector: "[parent]", inputs: { parent: "parent" }, usesInheritance: true, ngImport: i0 }); }
1627
1692
  }
1693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtParent, decorators: [{
1694
+ type: Directive,
1695
+ args: [{ selector: '[parent]', standalone: true }]
1696
+ }], propDecorators: { parent: [{
1697
+ type: Input
1698
+ }] } });
1628
1699
 
1629
1700
  class NgtRendererStore {
1701
+ #comments = [];
1630
1702
  constructor(root) {
1631
1703
  this.root = root;
1632
- this.comments = [];
1633
1704
  }
1634
1705
  createNode(type, node) {
1635
1706
  const state = [
@@ -1662,12 +1733,12 @@ class NgtRendererStore {
1662
1733
  // we attach an arrow function to the Comment node
1663
1734
  // In our directives, we can call this function to then start tracking the RendererNode
1664
1735
  // this is done to limit the amount of Nodes we need to process for getCreationState
1665
- rendererNode['__ngt_renderer_add_comment__'] = (node) => {
1736
+ rendererNode[SPECIAL_INTERNAL_ADD_COMMENT] = (node) => {
1666
1737
  if (node && node.__ngt_renderer__[0 /* NgtRendererClassId.type */] === 'portal') {
1667
1738
  this.portals.push(node);
1668
1739
  }
1669
1740
  else {
1670
- this.comments.push(rendererNode);
1741
+ this.#comments.push(rendererNode);
1671
1742
  }
1672
1743
  };
1673
1744
  return rendererNode;
@@ -1786,7 +1857,7 @@ class NgtRendererStore {
1786
1857
  value.nativeElement = node;
1787
1858
  return;
1788
1859
  }
1789
- const parent = getLocalState(node).parent || rS[1 /* NgtRendererClassId.parent */];
1860
+ const parent = getLocalState(node).parent() || rS[1 /* NgtRendererClassId.parent */];
1790
1861
  // [rawValue]
1791
1862
  if (getLocalState(node).isRaw && name === SPECIAL_PROPERTIES.VALUE) {
1792
1863
  rS[11 /* NgtRendererClassId.rawValue */] = value;
@@ -1821,9 +1892,6 @@ class NgtRendererStore {
1821
1892
  get rootScene() {
1822
1893
  return this.root.store.get('scene');
1823
1894
  }
1824
- get rootCdr() {
1825
- return this.root.cdr;
1826
- }
1827
1895
  get portals() {
1828
1896
  return this.root.portals;
1829
1897
  }
@@ -1878,12 +1946,10 @@ class NgtRendererStore {
1878
1946
  rS[6 /* NgtRendererClassId.compoundParent */] = undefined;
1879
1947
  const localState = getLocalState(node);
1880
1948
  if (localState.objects) {
1881
- localState.objects.value.forEach((obj) => this.destroy(obj, parent));
1882
- localState.objects.complete();
1949
+ untracked(localState.objects).forEach((obj) => this.destroy(obj, parent));
1883
1950
  }
1884
1951
  if (localState.nonObjects) {
1885
- localState.nonObjects.value.forEach((obj) => this.destroy(obj, parent));
1886
- localState.nonObjects.complete();
1952
+ untracked(localState.nonObjects).forEach((obj) => this.destroy(obj, parent));
1887
1953
  }
1888
1954
  if (localState.afterUpdate)
1889
1955
  localState.afterUpdate.complete();
@@ -1903,10 +1969,10 @@ class NgtRendererStore {
1903
1969
  }
1904
1970
  if (rS[0 /* NgtRendererClassId.type */] === 'comment') {
1905
1971
  rS[14 /* NgtRendererClassId.injectorFactory */] = null;
1906
- delete node['__ngt_renderer_add_comment__'];
1907
- const index = this.comments.findIndex((comment) => comment === node);
1972
+ delete node[SPECIAL_INTERNAL_ADD_COMMENT];
1973
+ const index = this.#comments.findIndex((comment) => comment === node);
1908
1974
  if (index > -1) {
1909
- this.comments.splice(index, 1);
1975
+ this.#comments.splice(index, 1);
1910
1976
  }
1911
1977
  }
1912
1978
  if (rS[0 /* NgtRendererClassId.type */] === 'portal') {
@@ -1945,9 +2011,9 @@ class NgtRendererStore {
1945
2011
  }
1946
2012
  firstNonInjectedDirective(dir) {
1947
2013
  let directive;
1948
- let i = this.comments.length - 1;
2014
+ let i = this.#comments.length - 1;
1949
2015
  while (i >= 0) {
1950
- const comment = this.comments[i];
2016
+ const comment = this.#comments[i];
1951
2017
  if (comment.__ngt_renderer__[4 /* NgtRendererClassId.destroyed */]) {
1952
2018
  i--;
1953
2019
  continue;
@@ -1995,33 +2061,35 @@ class NgtRendererStore {
1995
2061
  }
1996
2062
 
1997
2063
  class NgtRendererFactory {
1998
- constructor() {
1999
- this.delegateRendererFactory = inject(RendererFactory2, { skipSelf: true });
2000
- this.catalogue = inject(NGT_CATALOGUE);
2001
- this.rendererMap = new Map();
2002
- this.routedSet = new Set();
2003
- // all Renderer instances share the same Store
2004
- this.rendererStore = new NgtRendererStore({
2005
- store: inject(NgtStore),
2006
- cdr: inject(ChangeDetectorRef),
2007
- portals: [],
2008
- compoundPrefixes: inject(NGT_COMPOUND_PREFIXES),
2009
- document: inject(DOCUMENT),
2010
- });
2011
- }
2064
+ #delegateRendererFactory = inject(RendererFactory2, { skipSelf: true });
2065
+ #catalogue = inject(NGT_CATALOGUE);
2066
+ #zone = inject(NgZone);
2067
+ #cdr = inject(ChangeDetectorRef);
2068
+ #rendererMap = new Map();
2069
+ #routedSet = new Set();
2070
+ // all Renderer instances share the same Store
2071
+ #rendererStore = new NgtRendererStore({
2072
+ portals: [],
2073
+ store: inject(NgtStore),
2074
+ compoundPrefixes: inject(NGT_COMPOUND_PREFIXES),
2075
+ document: inject(DOCUMENT),
2076
+ });
2012
2077
  createRenderer(hostElement, type) {
2013
- const delegateRenderer = this.delegateRendererFactory.createRenderer(hostElement, type);
2078
+ const delegateRenderer = this.#delegateRendererFactory.createRenderer(hostElement, type);
2014
2079
  if (!type)
2015
2080
  return delegateRenderer;
2016
- if (type['type']['isRoutedScene']) {
2017
- this.routedSet.add(type.id);
2081
+ // if ((type as NgtAnyRecord)['type']['isHtml']) {
2082
+ // return delegateRenderer;
2083
+ // }
2084
+ if (type['type'][ROUTED_SCENE]) {
2085
+ this.#routedSet.add(type.id);
2018
2086
  }
2019
- let renderer = this.rendererMap.get(type.id);
2087
+ let renderer = this.#rendererMap.get(type.id);
2020
2088
  if (!renderer) {
2021
- renderer = new NgtRenderer(delegateRenderer, this.rendererStore, this.catalogue,
2089
+ renderer = new NgtRenderer(delegateRenderer, this.#rendererStore, this.#catalogue, this.#zone, this.#cdr,
2022
2090
  // setting root scene if there's no routed scene OR this component is the routed Scene
2023
- !hostElement && (this.routedSet.size === 0 || this.routedSet.has(type.id)));
2024
- this.rendererMap.set(type.id, renderer);
2091
+ !hostElement && (this.#routedSet.size === 0 || this.#routedSet.has(type.id)));
2092
+ this.#rendererMap.set(type.id, renderer);
2025
2093
  }
2026
2094
  return renderer;
2027
2095
  }
@@ -2035,10 +2103,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
2035
2103
  * Anything abbreviated with rS/RS stands for RendererState
2036
2104
  */
2037
2105
  class NgtRenderer {
2038
- constructor(delegate, store, catalogue, root = true) {
2106
+ constructor(delegate, store, catalogue, zone, cdr, root = true) {
2039
2107
  this.delegate = delegate;
2040
2108
  this.store = store;
2041
2109
  this.catalogue = catalogue;
2110
+ this.zone = zone;
2111
+ this.cdr = cdr;
2042
2112
  this.root = root;
2043
2113
  this.createText = this.delegate.createText.bind(this.delegate);
2044
2114
  this.destroy = this.delegate.destroy.bind(this.delegate);
@@ -2135,11 +2205,20 @@ class NgtRenderer {
2135
2205
  }
2136
2206
  if (cRS[2 /* NgtRendererClassId.injectedParent */]) {
2137
2207
  if (is.ref(cRS[2 /* NgtRendererClassId.injectedParent */])) {
2138
- cRS[2 /* NgtRendererClassId.injectedParent */].$.pipe(take(1)).subscribe((val) => {
2139
- if (val !== parent) {
2140
- this.appendChild(val, newChild);
2208
+ const injector = cRS[14 /* NgtRendererClassId.injectorFactory */]().get(Injector, null);
2209
+ if (!injector) {
2210
+ console.warn(`[NGT] NgtRenderer is attempting to start an effect for injectedParent but no Injector is found.`);
2211
+ return;
2212
+ }
2213
+ const watcher = effect(() => {
2214
+ const injectedParent = cRS[2 /* NgtRendererClassId.injectedParent */].nativeElement;
2215
+ if (injectedParent && injectedParent !== parent) {
2216
+ this.appendChild(injectedParent, newChild);
2217
+ // only run this effect once
2218
+ // as soon as we re-run appendChild with the injectedParent, we stop the effect
2219
+ watcher.destroy();
2141
2220
  }
2142
- });
2221
+ }, { injector, manualCleanup: true });
2143
2222
  return;
2144
2223
  }
2145
2224
  else if (parent !== cRS[2 /* NgtRendererClassId.injectedParent */]) {
@@ -2168,7 +2247,7 @@ class NgtRenderer {
2168
2247
  // if both are three instances, straightforward case
2169
2248
  if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2170
2249
  // if child already attached to a parent, skip
2171
- if (getLocalState(newChild).parent)
2250
+ if (getLocalState(newChild).parent && untracked(getLocalState(newChild).parent))
2172
2251
  return;
2173
2252
  // attach THREE child
2174
2253
  attachThreeChild(parent, newChild);
@@ -2208,7 +2287,7 @@ class NgtRenderer {
2208
2287
  }
2209
2288
  const shouldFindGrandparentInstance =
2210
2289
  // if child is three but haven't been attached to a parent yet
2211
- (cRS[0 /* NgtRendererClassId.type */] === 'three' && !getLocalState(newChild).parent) ||
2290
+ (cRS[0 /* NgtRendererClassId.type */] === 'three' && !untracked(getLocalState(newChild).parent)) ||
2212
2291
  // or both parent and child are DOM elements
2213
2292
  // or they are compound AND haven't had a THREE instance yet
2214
2293
  ((pRS[0 /* NgtRendererClassId.type */] === 'dom' ||
@@ -2223,7 +2302,7 @@ class NgtRenderer {
2223
2302
  }
2224
2303
  }
2225
2304
  insertBefore(parent, newChild
2226
- // TODO we might need these?
2305
+ // TODO: we might need these?
2227
2306
  // refChild: NgtRendererNode
2228
2307
  // isMove?: boolean | undefined
2229
2308
  ) {
@@ -2295,33 +2374,23 @@ class NgtRenderer {
2295
2374
  }
2296
2375
  listen(target, eventName, callback) {
2297
2376
  const rS = target.__ngt_renderer__;
2298
- const targetCdr = rS?.[14 /* NgtRendererClassId.injectorFactory */]?.().get(ChangeDetectorRef, null);
2377
+ // if the target doesn't have __ngt_renderer__, we delegate
2378
+ // if target is DOM node, then we pass that to delegate Renderer
2379
+ if (!rS || this.store.isDOM(target)) {
2380
+ return this.delegate.listen(target, eventName, callback);
2381
+ }
2299
2382
  if (rS[0 /* NgtRendererClassId.type */] === 'three' ||
2300
2383
  (rS[0 /* NgtRendererClassId.type */] === 'compound' && rS[7 /* NgtRendererClassId.compounded */])) {
2301
2384
  const instance = rS[7 /* NgtRendererClassId.compounded */] || target;
2302
2385
  const priority = getLocalState(target).priority;
2303
- return processThreeEvent(instance, priority || 0, eventName, callback, this.store.rootCdr, targetCdr);
2386
+ return processThreeEvent(instance, priority || 0, eventName, callback, this.zone, this.cdr);
2304
2387
  }
2305
2388
  if (rS[0 /* NgtRendererClassId.type */] === 'compound' && !rS[7 /* NgtRendererClassId.compounded */]) {
2306
2389
  this.store.queueOperation(target, [
2307
2390
  'op',
2308
2391
  () => this.store.queueOperation(target, ['cleanUp', this.listen(target, eventName, callback)]),
2309
2392
  ]);
2310
- }
2311
- // setup a new callback with CDR so that it will trigger change detection properly
2312
- const callbackWithCdr = (event) => {
2313
- const value = callback(event);
2314
- safeDetectChanges(targetCdr);
2315
- safeDetectChanges(this.store.rootCdr);
2316
- return value;
2317
- };
2318
- // if the target doesn't have __ngt_renderer__, we delegate
2319
- if (!rS) {
2320
- return this.delegate.listen(target, eventName, callbackWithCdr);
2321
- }
2322
- // if target is DOM node, then we pass that to delegate Renderer
2323
- if (this.store.isDOM(target)) {
2324
- return this.delegate.listen(target, eventName, callbackWithCdr);
2393
+ return () => { };
2325
2394
  }
2326
2395
  // @ts-expect-error - we know that target is not DOM node
2327
2396
  if (target === this.store.rootScene) {
@@ -2333,7 +2402,7 @@ class NgtRenderer {
2333
2402
  const eventTarget = domTarget === 'window'
2334
2403
  ? target['ownerDocument']['defaultView']
2335
2404
  : target['ownerDocument'];
2336
- return this.delegate.listen(eventTarget, event, callbackWithCdr);
2405
+ return this.delegate.listen(eventTarget, event, callback);
2337
2406
  }
2338
2407
  return () => { };
2339
2408
  }
@@ -2352,105 +2421,18 @@ function provideNgtRenderer({ store, changeDetectorRef, compoundPrefixes = [] })
2352
2421
  { provide: NgtStore, useValue: store },
2353
2422
  { provide: ChangeDetectorRef, useValue: changeDetectorRef },
2354
2423
  { provide: NGT_COMPOUND_PREFIXES, useValue: compoundPrefixes },
2424
+ provideZoneChangeDetection({ runCoalescing: true, eventCoalescing: true }),
2355
2425
  ]);
2356
2426
  }
2357
2427
 
2358
- const DOM_EVENTS = {
2359
- click: false,
2360
- contextmenu: false,
2361
- dblclick: false,
2362
- wheel: false,
2363
- pointerdown: true,
2364
- pointerup: true,
2365
- pointerleave: true,
2366
- pointermove: true,
2367
- pointercancel: true,
2368
- lostpointercapture: true,
2369
- };
2370
- const supportedEvents = [
2371
- 'click',
2372
- 'contextmenu',
2373
- 'dblclick',
2374
- 'pointerup',
2375
- 'pointerdown',
2376
- 'pointerover',
2377
- 'pointerout',
2378
- 'pointerenter',
2379
- 'pointerleave',
2380
- 'pointermove',
2381
- 'pointermissed',
2382
- 'pointercancel',
2383
- 'wheel',
2384
- ];
2385
- function createPointerEvents(store) {
2386
- const { handlePointer } = createEvents(store);
2387
- return {
2388
- priority: 1,
2389
- enabled: true,
2390
- compute: (event, root) => {
2391
- const state = root.get();
2392
- // https://github.com/pmndrs/react-three-fiber/pull/782
2393
- // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides
2394
- state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
2395
- state.raycaster.setFromCamera(state.pointer, state.camera);
2396
- },
2397
- connected: undefined,
2398
- handlers: Object.keys(DOM_EVENTS).reduce((handlers, supportedEventName) => {
2399
- handlers[supportedEventName] = handlePointer(supportedEventName);
2400
- return handlers;
2401
- }, {}),
2402
- connect: (target) => {
2403
- const state = store.get();
2404
- state.events.disconnect?.();
2405
- state.setEvents({ connected: target });
2406
- Object.entries(state.events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
2407
- const passive = DOM_EVENTS[eventName];
2408
- target.addEventListener(eventName, eventHandler, { passive });
2409
- });
2410
- },
2411
- disconnect: () => {
2412
- const { events, setEvents } = store.get();
2413
- if (events.connected) {
2414
- Object.entries(events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
2415
- if (events.connected instanceof HTMLElement) {
2416
- events.connected.removeEventListener(eventName, eventHandler);
2417
- }
2418
- });
2419
- setEvents({ connected: undefined });
2420
- }
2421
- },
2422
- };
2423
- }
2424
-
2425
- class NgtCanvas extends NgtRxStore {
2426
- constructor() {
2427
- super(...arguments);
2428
- this.cdr = inject(ChangeDetectorRef);
2429
- this.envInjector = inject(EnvironmentInjector);
2430
- this.host = inject(ElementRef);
2431
- this.store = inject(NgtStore);
2432
- this.hbClass = true;
2433
- this.sceneGraphInputs = {};
2434
- this.compoundPrefixes = [];
2435
- this.created = new EventEmitter();
2436
- this.pointerMissed = new EventEmitter();
2437
- }
2438
- initialize() {
2439
- super.initialize();
2440
- this.set({
2441
- shadows: false,
2442
- linear: false,
2443
- flat: false,
2444
- legacy: false,
2445
- orthographic: false,
2446
- frameloop: 'always',
2447
- dpr: [1, 2],
2448
- events: createPointerEvents,
2449
- });
2450
- }
2451
- get hbPointerEvents() {
2452
- return this.get('eventSource') !== this.host.nativeElement ? 'none' : 'auto';
2453
- }
2428
+ class NgtCanvas extends NgtSignalStore {
2429
+ #envInjector;
2430
+ #injector;
2431
+ #host;
2432
+ #zone;
2433
+ #cdr;
2434
+ #store;
2435
+ #isReady;
2454
2436
  set linear(linear) {
2455
2437
  this.set({ linear });
2456
2438
  }
@@ -2473,13 +2455,14 @@ class NgtCanvas extends NgtRxStore {
2473
2455
  this.set({ raycaster });
2474
2456
  }
2475
2457
  set shadows(shadows) {
2476
- this.set({
2477
- shadows: typeof shadows === 'object' ? shadows : shadows,
2478
- });
2458
+ this.set({ shadows });
2479
2459
  }
2480
2460
  set camera(camera) {
2481
2461
  this.set({ camera });
2482
2462
  }
2463
+ set scene(scene) {
2464
+ this.set({ scene });
2465
+ }
2483
2466
  set gl(gl) {
2484
2467
  this.set({ gl });
2485
2468
  }
@@ -2495,43 +2478,96 @@ class NgtCanvas extends NgtRxStore {
2495
2478
  set performance(performance) {
2496
2479
  this.set({ performance });
2497
2480
  }
2481
+ #glRef;
2482
+ #glEnvInjector;
2483
+ constructor() {
2484
+ super({
2485
+ shadows: false,
2486
+ linear: false,
2487
+ flat: false,
2488
+ legacy: false,
2489
+ orthographic: false,
2490
+ frameloop: 'always',
2491
+ dpr: [1, 2],
2492
+ events: createPointerEvents,
2493
+ });
2494
+ this.#envInjector = inject(EnvironmentInjector);
2495
+ this.#injector = inject(Injector);
2496
+ this.#host = inject(ElementRef);
2497
+ this.#zone = inject(NgZone);
2498
+ this.#cdr = inject(ChangeDetectorRef);
2499
+ this.#store = inject(NgtStore);
2500
+ this.#isReady = this.#store.select('ready');
2501
+ this.sceneGraphInputs = {};
2502
+ this.compoundPrefixes = [];
2503
+ this.created = new EventEmitter();
2504
+ this.pointerMissed = new EventEmitter();
2505
+ inject(DestroyRef).onDestroy(() => {
2506
+ if (this.#glRef)
2507
+ this.#glRef.destroy();
2508
+ if (this.#glEnvInjector)
2509
+ this.#glEnvInjector.destroy();
2510
+ injectNgtLoader.destroy();
2511
+ this.#store.destroy(this.glCanvas.nativeElement);
2512
+ });
2513
+ }
2514
+ get hbPointerEvents() {
2515
+ return this.select('eventSource')() !== this.#host.nativeElement ? 'none' : 'auto';
2516
+ }
2498
2517
  ngOnChanges(changes) {
2499
- if (changes['sceneGraphInputs'] && this.glRef) {
2500
- this.setSceneGraphInputs();
2518
+ if (changes['sceneGraphInputs'] && !changes['sceneGraphInputs'].firstChange && this.#glRef) {
2519
+ this.#setSceneGraphInputs();
2501
2520
  }
2502
2521
  }
2503
2522
  ngOnInit() {
2504
2523
  if (!this.get('eventSource')) {
2505
2524
  // set default event source to the host element
2506
- this.eventSource = this.host.nativeElement;
2525
+ this.set({ eventSource: this.#host.nativeElement });
2507
2526
  }
2508
2527
  if (this.pointerMissed.observed) {
2509
- this.store.set({
2528
+ this.#store.set({
2510
2529
  onPointerMissed: (event) => {
2511
2530
  this.pointerMissed.emit(event);
2512
- safeDetectChanges(this.cdr);
2513
2531
  },
2514
2532
  });
2515
2533
  }
2516
2534
  // setup NgtStore
2517
- this.store.init();
2535
+ this.#store.init();
2518
2536
  // set rootStateMap
2519
- rootStateMap.set(this.glCanvas.nativeElement, this.store);
2537
+ rootStateMap.set(this.glCanvas.nativeElement, this.#store);
2520
2538
  // subscribe to store to listen for ready state
2521
- this.hold(this.store.select('ready').pipe(filter((ready) => ready)), () => this.storeReady());
2539
+ effect(() => {
2540
+ this.#zone.runOutsideAngular(() => {
2541
+ if (this.#isReady())
2542
+ this.#storeReady();
2543
+ });
2544
+ }, { injector: this.#injector, allowSignalWrites: true });
2522
2545
  }
2523
- onResize({ width, height }) {
2546
+ #resizeRef;
2547
+ // NOTE: this is invoked outside of Angular Zone
2548
+ onResize({ width, height, top, left }) {
2549
+ // destroy previous effect
2550
+ if (this.#resizeRef) {
2551
+ this.#resizeRef.destroy();
2552
+ }
2524
2553
  if (width > 0 && height > 0) {
2525
- if (!this.store.isInit)
2526
- this.store.init();
2527
- this.store.configure(this.get(), this.glCanvas.nativeElement);
2554
+ if (!this.#store.isInit)
2555
+ this.#store.init();
2556
+ const inputs = this.select();
2557
+ this.#resizeRef = this.#zone.run(() => effect(() => {
2558
+ const canvasInputs = inputs();
2559
+ this.#zone.runOutsideAngular(() => {
2560
+ this.#store.configure({ ...canvasInputs, size: { width, height, top, left } }, this.glCanvas.nativeElement);
2561
+ });
2562
+ }, { injector: this.#injector, manualCleanup: true, allowSignalWrites: true }));
2528
2563
  }
2529
2564
  }
2530
- storeReady() {
2565
+ // NOTE: This is invoked outside of Angular Zone
2566
+ #storeReady() {
2531
2567
  // canvas is ready, let's activate the loop
2532
- this.store.set((state) => ({ internal: { ...state.internal, active: true } }));
2568
+ this.#store.set((state) => ({ internal: { ...state.internal, active: true } }));
2533
2569
  const inputs = this.get();
2534
- const state = this.store.get();
2570
+ const state = this.#store.get();
2535
2571
  // connect to event source
2536
2572
  state.events.connect?.(is.ref(inputs.eventSource) ? inputs.eventSource.nativeElement : inputs.eventSource);
2537
2573
  // setup compute function for events
@@ -2547,68 +2583,73 @@ class NgtCanvas extends NgtRxStore {
2547
2583
  });
2548
2584
  }
2549
2585
  // emit created event if observed
2550
- if (this.created.observed)
2551
- this.created.emit(this.store.get());
2586
+ if (this.created.observed) {
2587
+ // but go back into zone to run it
2588
+ this.#zone.run(() => {
2589
+ this.created.emit(this.#store.get());
2590
+ });
2591
+ }
2552
2592
  // render
2553
- if (this.glRef)
2554
- this.glRef.destroy();
2593
+ if (this.#glRef)
2594
+ this.#glRef.destroy();
2555
2595
  requestAnimationFrame(() => {
2556
- const { store, cdr: changeDetectorRef, compoundPrefixes } = this;
2557
- this.glEnvInjector = createEnvironmentInjector([provideNgtRenderer({ store, changeDetectorRef, compoundPrefixes })], this.envInjector);
2558
- this.glRef = this.glAnchor.createComponent(this.sceneGraph, { environmentInjector: this.glEnvInjector });
2559
- // detach the Scene graph from Change Detection mechanism
2560
- // everything from this point forward will trigger CD manually with detectChanges
2561
- this.glRef.changeDetectorRef.detach();
2562
- this.setSceneGraphInputs();
2563
- // here, we override the detectChanges to also call detectChanges on the ComponentRef
2564
- this.overrideDetectChanges();
2565
- this.cdr.detectChanges();
2596
+ this.#glEnvInjector = createEnvironmentInjector([
2597
+ provideNgtRenderer({
2598
+ store: this.#store,
2599
+ changeDetectorRef: this.#cdr,
2600
+ compoundPrefixes: this.compoundPrefixes,
2601
+ }),
2602
+ ], this.#envInjector);
2603
+ this.#glRef = this.glAnchor.createComponent(this.sceneGraph, {
2604
+ environmentInjector: this.#glEnvInjector,
2605
+ });
2606
+ this.#setSceneGraphInputs();
2607
+ this.#overrideChangeDetectorRef();
2608
+ safeDetectChanges(this.#cdr);
2566
2609
  });
2567
2610
  }
2568
- ngOnDestroy() {
2569
- if (this.glRef)
2570
- this.glRef.destroy();
2571
- if (this.glEnvInjector)
2572
- this.glEnvInjector.destroy();
2573
- injectNgtLoader.destroy();
2574
- this.store.destroy(this.glCanvas.nativeElement);
2575
- super.ngOnDestroy();
2576
- }
2577
- overrideDetectChanges() {
2578
- const originalDetectChanges = this.cdr.detectChanges.bind(this.cdr);
2579
- this.cdr.detectChanges = () => {
2611
+ #overrideChangeDetectorRef() {
2612
+ const originalDetectChanges = this.#cdr.detectChanges.bind(this.#cdr);
2613
+ this.#cdr.detectChanges = () => {
2580
2614
  originalDetectChanges();
2581
- safeDetectChanges(this.glRef?.changeDetectorRef);
2615
+ safeDetectChanges(this.#glRef?.changeDetectorRef);
2582
2616
  };
2583
2617
  }
2584
- setSceneGraphInputs() {
2585
- for (const key of Object.keys(this.sceneGraphInputs)) {
2586
- this.glRef.setInput(key, this.sceneGraphInputs[key]);
2587
- }
2588
- safeDetectChanges(this.cdr);
2618
+ #setSceneGraphInputs() {
2619
+ this.#zone.run(() => {
2620
+ if (this.#glRef) {
2621
+ for (const [key, value] of Object.entries(this.sceneGraphInputs)) {
2622
+ this.#glRef.setInput(key, value);
2623
+ }
2624
+ safeDetectChanges(this.#glRef.changeDetectorRef);
2625
+ }
2626
+ });
2589
2627
  }
2590
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCanvas, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
2591
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.0", type: NgtCanvas, isStandalone: true, selector: "ngt-canvas", inputs: { sceneGraph: "sceneGraph", sceneGraphInputs: "sceneGraphInputs", compoundPrefixes: "compoundPrefixes", linear: "linear", legacy: "legacy", flat: "flat", orthographic: "orthographic", frameloop: "frameloop", dpr: "dpr", raycaster: "raycaster", shadows: "shadows", camera: "camera", gl: "gl", eventSource: "eventSource", eventPrefix: "eventPrefix", lookAt: "lookAt", performance: "performance" }, outputs: { created: "created", pointerMissed: "pointerMissed" }, host: { properties: { "class.ngt-canvas": "this.hbClass", "style.pointerEvents": "this.hbPointerEvents" } }, providers: [NgtStore, provideNgxResizeOptions({ emitInZone: false })], viewQueries: [{ propertyName: "glCanvas", first: true, predicate: ["glCanvas"], descendants: true, static: true }, { propertyName: "glAnchor", first: true, predicate: ["glCanvas"], descendants: true, read: ViewContainerRef, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
2628
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCanvas, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2629
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.0", type: NgtCanvas, isStandalone: true, selector: "ngt-canvas", inputs: { sceneGraph: "sceneGraph", sceneGraphInputs: "sceneGraphInputs", compoundPrefixes: "compoundPrefixes", linear: "linear", legacy: "legacy", flat: "flat", orthographic: "orthographic", frameloop: "frameloop", dpr: "dpr", raycaster: "raycaster", shadows: "shadows", camera: "camera", scene: "scene", gl: "gl", eventSource: "eventSource", eventPrefix: "eventPrefix", lookAt: "lookAt", performance: "performance" }, outputs: { created: "created", pointerMissed: "pointerMissed" }, host: { styleAttribute: "display: block;position: relative;width: 100%;height: 100%;overflow: hidden;" }, providers: [NgtStore, provideNgxResizeOptions({ emitInZone: false, emitInitialResult: true })], viewQueries: [{ propertyName: "glCanvas", first: true, predicate: ["glCanvas"], descendants: true, static: true }, { propertyName: "glAnchor", first: true, predicate: ["glCanvas"], descendants: true, read: ViewContainerRef, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
2592
2630
  <div (ngxResize)="onResize($event)" style="height: 100%; width: 100%;">
2593
- <canvas #glCanvas style="display: block;"></canvas>
2631
+ <canvas #glCanvas style="display: block;"> </canvas>
2594
2632
  </div>
2595
- `, isInline: true, styles: [":host{display:block;position:relative;width:100%;height:100%;overflow:hidden}\n"], dependencies: [{ kind: "directive", type: NgxResize, selector: "[ngxResize]", inputs: ["ngxResizeOptions"], outputs: ["ngxResize"] }] }); }
2633
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgxResize, selector: "[ngxResize]", inputs: ["ngxResizeOptions"], outputs: ["ngxResize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2596
2634
  }
2597
2635
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtCanvas, decorators: [{
2598
2636
  type: Component,
2599
- args: [{ selector: 'ngt-canvas', standalone: true, template: `
2637
+ args: [{
2638
+ selector: 'ngt-canvas',
2639
+ standalone: true,
2640
+ template: `
2600
2641
  <div (ngxResize)="onResize($event)" style="height: 100%; width: 100%;">
2601
- <canvas #glCanvas style="display: block;"></canvas>
2642
+ <canvas #glCanvas style="display: block;"> </canvas>
2602
2643
  </div>
2603
- `, imports: [NgxResize], providers: [NgtStore, provideNgxResizeOptions({ emitInZone: false })], styles: [":host{display:block;position:relative;width:100%;height:100%;overflow:hidden}\n"] }]
2604
- }], propDecorators: { hbClass: [{
2605
- type: HostBinding,
2606
- args: ['class.ngt-canvas']
2607
- }], hbPointerEvents: [{
2608
- type: HostBinding,
2609
- args: ['style.pointerEvents']
2610
- }], sceneGraph: [{
2611
- type: Input
2644
+ `,
2645
+ imports: [NgxResize],
2646
+ providers: [NgtStore, provideNgxResizeOptions({ emitInZone: false, emitInitialResult: true })],
2647
+ host: { style: 'display: block;position: relative;width: 100%;height: 100%;overflow: hidden;' },
2648
+ changeDetection: ChangeDetectionStrategy.OnPush,
2649
+ }]
2650
+ }], ctorParameters: function () { return []; }, propDecorators: { sceneGraph: [{
2651
+ type: Input,
2652
+ args: [{ required: true }]
2612
2653
  }], sceneGraphInputs: [{
2613
2654
  type: Input
2614
2655
  }], compoundPrefixes: [{
@@ -2631,6 +2672,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
2631
2672
  type: Input
2632
2673
  }], camera: [{
2633
2674
  type: Input
2675
+ }], scene: [{
2676
+ type: Input
2634
2677
  }], gl: [{
2635
2678
  type: Input
2636
2679
  }], eventSource: [{
@@ -2653,135 +2696,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
2653
2696
  args: ['glCanvas', { static: true, read: ViewContainerRef }]
2654
2697
  }] } });
2655
2698
 
2656
- /**
2657
- * A utility injection fn that can be used in other injection fn to provide the destroy capability.
2658
- */
2659
- function injectNgtDestroy(cb) {
2660
- try {
2661
- const cdr = inject(ChangeDetectorRef);
2662
- const destroy$ = new ReplaySubject();
2663
- queueMicrotask(() => {
2664
- cdr.onDestroy(() => {
2665
- destroy$.next();
2666
- destroy$.complete();
2667
- cb?.();
2668
- });
2669
- });
2670
- return { destroy$, cdr };
2671
- }
2672
- catch (e) {
2673
- throw new Error(`[NGT] injectNgtDestroy is being called outside of Constructor Context`);
2674
- }
2675
- }
2676
-
2677
- function injectBeforeRender(cb, priority = 0) {
2678
- try {
2699
+ function injectBeforeRender(cb, { priority = 0, injector = inject(Injector, { optional: true }), } = {}) {
2700
+ !injector && assertInInjectionContext(injectBeforeRender);
2701
+ return runInInjectionContext(injector, () => {
2679
2702
  const store = inject(NgtStore);
2680
2703
  const sub = store.get('internal').subscribe(cb, priority, store);
2681
- injectNgtDestroy(() => void sub());
2704
+ inject(DestroyRef).onDestroy(() => void sub());
2682
2705
  return sub;
2683
- }
2684
- catch (e) {
2685
- throw new Error(`[NGT] "injectBeforeRender" is invoked outside of Constructor Context`);
2686
- }
2687
- }
2688
-
2689
- function injectNgtRef(initialValue = null) {
2690
- const ref = is.ref(initialValue) ? initialValue : new ElementRef(initialValue);
2691
- let lastValue = ref.nativeElement;
2692
- const cdRefs = [];
2693
- const ref$ = new BehaviorSubject(lastValue);
2694
- const { destroy$, cdr } = injectNgtDestroy(() => void ref$.complete());
2695
- cdRefs.push(cdr);
2696
- const obs$ = ref$.asObservable().pipe(distinctUntilChanged(), takeUntil(destroy$));
2697
- const subscribe = (callback) => obs$.subscribe((current) => {
2698
- callback(current, lastValue);
2699
- lastValue = current;
2700
- });
2701
- const useCDR = (cdr) => void cdRefs.push(cdr);
2702
- const $ = obs$.pipe(filter((value, index) => index > 0 || value != null), takeUntil(destroy$));
2703
- const children$ = (type = 'objects') => $.pipe(switchMap((instance) => {
2704
- const localState = getLocalState(instance);
2705
- if (localState.objects && localState.nonObjects) {
2706
- return merge(localState.objects, localState.nonObjects).pipe(map(() => {
2707
- try {
2708
- return type === 'both'
2709
- ? [...localState.objects.value, ...localState.nonObjects.value]
2710
- : localState[type].value;
2711
- }
2712
- catch (e) {
2713
- console.error(`[NGT] Exception in accessing children of ${instance}`);
2714
- return [];
2715
- }
2716
- }));
2717
- }
2718
- return of([]);
2719
- }), filter((children, index) => index > 0 || children.length > 0), takeUntil(destroy$));
2720
- // here, we override nativeElement to add more functionalities to nativeElement
2721
- Object.defineProperty(ref, 'nativeElement', {
2722
- set: (newVal) => {
2723
- if (ref.nativeElement !== newVal) {
2724
- ref$.next(newVal);
2725
- lastValue = ref.nativeElement;
2726
- ref.nativeElement = newVal;
2727
- // clone the cdRefs so we can mutate cdRefs in the loop
2728
- const cds = [...cdRefs];
2729
- for (let i = 0; i < cds.length; i++) {
2730
- const cd = cds[i];
2731
- // if a ChangeDetectorRef is destroyed, we stop tracking it and go to the next one
2732
- if (cd.destroyed) {
2733
- cdRefs.splice(i, 1);
2734
- continue;
2735
- }
2736
- // during creation phase, 'context' on ViewRef will be null
2737
- // we check the "context" to avoid running detectChanges during this phase.
2738
- // because there's nothing to check
2739
- safeDetectChanges(cd);
2740
- }
2741
- }
2742
- },
2743
- get: () => ref$.value,
2744
2706
  });
2745
- return Object.assign(ref, { subscribe, $, children$, useCDR });
2746
2707
  }
2747
2708
 
2748
- /**
2749
- * Please use this sparringly and know what you're doing.
2750
- *
2751
- * Create a runInContext function that has access to the NodeInjector as well
2752
- */
2753
- function createRunInContext() {
2754
- const nodeInjector = inject(Injector);
2755
- const envInjector = inject(EnvironmentInjector);
2756
- const originalGet = envInjector.get.bind(envInjector);
2757
- return (cb) => {
2758
- let tryFromNodeInjector = false;
2759
- envInjector.get = (...args) => {
2760
- try {
2761
- const originalFlags = args[2];
2762
- if (!(originalFlags & 8)) {
2763
- args[2] |= 8;
2764
- }
2765
- const fromEnvInjector = originalGet(...args);
2766
- if (fromEnvInjector)
2767
- return fromEnvInjector;
2768
- if (fromEnvInjector === null && ((args[1] !== undefined && args[1] === null) || originalFlags & 8))
2769
- return fromEnvInjector;
2770
- args[2] = originalFlags;
2771
- if (!tryFromNodeInjector) {
2772
- tryFromNodeInjector = true;
2773
- const fromNodeInjector = nodeInjector.get(...args);
2774
- tryFromNodeInjector = false;
2775
- return fromNodeInjector;
2776
- }
2777
- return null;
2778
- }
2779
- catch (e) {
2780
- return originalGet(...args);
2709
+ function injectNgtRef(initial = null, injector = inject(Injector, { optional: true })) {
2710
+ !injector && assertInInjectionContext(injectNgtRef);
2711
+ return runInInjectionContext(injector, () => {
2712
+ const cdr = inject(ChangeDetectorRef);
2713
+ const ref = is.ref(initial) ? initial : new ElementRef(initial);
2714
+ const signalRef = signal(ref.nativeElement);
2715
+ const readonlySignal = signalRef.asReadonly();
2716
+ const cached = new Map();
2717
+ inject(DestroyRef).onDestroy(() => void cached.clear());
2718
+ const children = (type = 'objects') => {
2719
+ if (!cached.has(type)) {
2720
+ cached.set(type, computed(() => {
2721
+ const instance = readonlySignal();
2722
+ if (!instance)
2723
+ return [];
2724
+ const localState = getLocalState(instance);
2725
+ if (!localState.objects || !localState.nonObjects)
2726
+ return [];
2727
+ if (type === 'objects')
2728
+ return localState.objects();
2729
+ if (type === 'nonObjects')
2730
+ return localState.nonObjects();
2731
+ return [...localState.objects(), ...localState.nonObjects()];
2732
+ }));
2781
2733
  }
2734
+ return cached.get(type);
2782
2735
  };
2783
- return envInjector.runInContext(cb);
2784
- };
2736
+ Object.defineProperty(ref, 'nativeElement', {
2737
+ set: (newElement) => {
2738
+ if (newElement !== untracked(signalRef)) {
2739
+ signalRef.set(newElement);
2740
+ safeDetectChanges(cdr);
2741
+ }
2742
+ },
2743
+ get: () => readonlySignal(),
2744
+ });
2745
+ return Object.assign(ref, { children });
2746
+ });
2785
2747
  }
2786
2748
 
2787
2749
  class NgtRepeat extends NgForOf {
@@ -2798,51 +2760,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
2798
2760
  type: Input
2799
2761
  }] } });
2800
2762
 
2801
- function isPromise(value) {
2802
- return ((value instanceof Promise || Object.prototype.toString.call(value) === '[object Promise]') &&
2803
- typeof value['then'] === 'function');
2804
- }
2805
- class NgtPush {
2806
- constructor() {
2807
- this.cdr = inject(ChangeDetectorRef);
2808
- this.parentCdr = inject(ChangeDetectorRef, { skipSelf: true, optional: true });
2809
- this.envCdr = inject(EnvironmentInjector).get(ChangeDetectorRef, null);
2810
- }
2811
- transform(value, defaultValue = null) {
2812
- if (this.obj === value)
2813
- return this.latestValue;
2814
- this.obj = value;
2815
- this.latestValue = defaultValue;
2816
- if (this.sub)
2817
- this.sub.unsubscribe();
2818
- if (isObservable(this.obj))
2819
- this.sub = this.obj.subscribe(this.updateValue.bind(this));
2820
- else if (isPromise(this.obj))
2821
- this.obj.then(this.updateValue.bind(this));
2822
- else
2823
- throw new Error(`[NGT] Invalid value passed to ngtPush pipe`);
2824
- return this.latestValue;
2825
- }
2826
- updateValue(val) {
2827
- this.latestValue = val;
2828
- safeDetectChanges(this.cdr);
2829
- safeDetectChanges(this.parentCdr);
2830
- safeDetectChanges(this.envCdr);
2831
- }
2832
- ngOnDestroy() {
2833
- if (this.sub)
2834
- this.sub.unsubscribe();
2835
- this.latestValue = undefined;
2836
- this.obj = undefined;
2837
- }
2838
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPush, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
2839
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "16.0.0", ngImport: i0, type: NgtPush, isStandalone: true, name: "ngtPush", pure: false }); }
2840
- }
2841
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPush, decorators: [{
2842
- type: Pipe,
2843
- args: [{ name: 'ngtPush', pure: false, standalone: true }]
2844
- }] });
2845
-
2846
2763
  const privateKeys = [
2847
2764
  'get',
2848
2765
  'set',
@@ -2855,20 +2772,23 @@ const privateKeys = [
2855
2772
  'advance',
2856
2773
  'size',
2857
2774
  'viewport',
2858
- 'addInteraction',
2859
- 'removeInteraction',
2860
2775
  ];
2861
2776
  class NgtPortalBeforeRender {
2777
+ #portalStore;
2778
+ #subscription;
2862
2779
  constructor() {
2863
- this.portalStore = inject(NgtStore);
2780
+ this.#portalStore = inject(NgtStore);
2864
2781
  this.renderPriority = 1;
2865
2782
  this.beforeRender = new EventEmitter();
2783
+ inject(DestroyRef).onDestroy(() => {
2784
+ this.#subscription?.();
2785
+ });
2866
2786
  }
2867
2787
  ngOnInit() {
2868
2788
  let oldClear;
2869
- this.subscription = this.portalStore.get('internal').subscribe(({ delta, frame }) => {
2870
- this.beforeRender.emit({ ...this.portalStore.get(), delta, frame });
2871
- const { gl, scene, camera } = this.portalStore.get();
2789
+ this.#subscription = this.#portalStore.get('internal').subscribe(({ delta, frame }) => {
2790
+ this.beforeRender.emit({ ...this.#portalStore.get(), delta, frame });
2791
+ const { gl, scene, camera } = this.#portalStore.get();
2872
2792
  oldClear = gl.autoClear;
2873
2793
  if (this.renderPriority === 1) {
2874
2794
  // clear scene and render with default
@@ -2881,10 +2801,7 @@ class NgtPortalBeforeRender {
2881
2801
  gl.render(scene, camera);
2882
2802
  // restore
2883
2803
  gl.autoClear = oldClear;
2884
- }, this.renderPriority, this.portalStore);
2885
- }
2886
- ngOnDestroy() {
2887
- this.subscription?.();
2804
+ }, this.renderPriority, this.#portalStore);
2888
2805
  }
2889
2806
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPortalBeforeRender, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2890
2807
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.0.0", type: NgtPortalBeforeRender, isStandalone: true, selector: "[ngtPortalBeforeRender]", inputs: { renderPriority: "renderPriority", parentScene: "parentScene", parentCamera: "parentCamera" }, outputs: { beforeRender: "beforeRender" }, ngImport: i0 }); }
@@ -2892,21 +2809,23 @@ class NgtPortalBeforeRender {
2892
2809
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPortalBeforeRender, decorators: [{
2893
2810
  type: Directive,
2894
2811
  args: [{ selector: '[ngtPortalBeforeRender]', standalone: true }]
2895
- }], propDecorators: { renderPriority: [{
2812
+ }], ctorParameters: function () { return []; }, propDecorators: { renderPriority: [{
2896
2813
  type: Input
2897
2814
  }], parentScene: [{
2898
- type: Input
2815
+ type: Input,
2816
+ args: [{ required: true }]
2899
2817
  }], parentCamera: [{
2900
- type: Input
2818
+ type: Input,
2819
+ args: [{ required: true }]
2901
2820
  }], beforeRender: [{
2902
2821
  type: Output
2903
2822
  }] } });
2904
2823
  class NgtPortalContent {
2905
2824
  constructor(vcr, parentVcr) {
2906
2825
  const commentNode = vcr.element.nativeElement;
2907
- if (commentNode['__ngt_renderer_add_comment__']) {
2908
- commentNode['__ngt_renderer_add_comment__'](parentVcr.element.nativeElement);
2909
- delete commentNode['__ngt_renderer_add_comment__'];
2826
+ if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT]) {
2827
+ commentNode[SPECIAL_INTERNAL_ADD_COMMENT](parentVcr.element.nativeElement);
2828
+ delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT];
2910
2829
  }
2911
2830
  }
2912
2831
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPortalContent, deps: [{ token: i0.ViewContainerRef }, { token: i0.ViewContainerRef, skipSelf: true }], target: i0.ɵɵFactoryTarget.Directive }); }
@@ -2918,32 +2837,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
2918
2837
  }], ctorParameters: function () { return [{ type: i0.ViewContainerRef }, { type: i0.ViewContainerRef, decorators: [{
2919
2838
  type: SkipSelf
2920
2839
  }] }]; } });
2921
- class NgtPortal extends NgtRxStore {
2922
- constructor() {
2923
- super(...arguments);
2924
- this.autoRender = true;
2925
- this.autoRenderPriority = 1;
2926
- this.beforeRender = new EventEmitter();
2927
- this.parentStore = inject(NgtStore, { skipSelf: true });
2928
- this.parentScene = this.parentStore.get('scene');
2929
- this.parentCamera = this.parentStore.get('camera');
2930
- this.portalStore = inject(NgtStore, { self: true });
2931
- this.raycaster = new THREE.Raycaster();
2932
- this.pointer = new THREE.Vector2();
2933
- this.portalContentRendered = false;
2934
- }
2840
+ class NgtPortal extends NgtSignalStore {
2935
2841
  set container(container) {
2936
2842
  this.set({ container });
2937
2843
  }
2938
2844
  set state(state) {
2939
2845
  this.set({ state });
2940
2846
  }
2941
- initialize() {
2942
- super.initialize();
2943
- this.set({ container: injectNgtRef(prepare(new THREE.Scene())) });
2847
+ #parentStore;
2848
+ #portalStore;
2849
+ #injector;
2850
+ #zone;
2851
+ #raycaster;
2852
+ #pointer;
2853
+ #portalContentView;
2854
+ constructor() {
2855
+ super({ container: injectNgtRef(prepare(new THREE.Scene())) });
2856
+ this.autoRender = true;
2857
+ this.autoRenderPriority = 1;
2858
+ this.beforeRender = new EventEmitter();
2859
+ this.#parentStore = inject(NgtStore, { skipSelf: true });
2860
+ this.parentScene = this.#parentStore.get('scene');
2861
+ this.parentCamera = this.#parentStore.get('camera');
2862
+ this.#portalStore = inject(NgtStore, { self: true });
2863
+ this.#injector = inject(Injector);
2864
+ this.#zone = inject(NgZone);
2865
+ this.#raycaster = new THREE.Raycaster();
2866
+ this.#pointer = new THREE.Vector2();
2867
+ this.portalContentRendered = false;
2868
+ inject(DestroyRef).onDestroy(() => {
2869
+ if (this.#portalContentView && !this.#portalContentView.destroyed) {
2870
+ this.#portalContentView.destroy();
2871
+ }
2872
+ });
2944
2873
  }
2945
2874
  ngOnInit() {
2946
- const previousState = this.parentStore.get();
2875
+ const previousState = this.#parentStore.get();
2947
2876
  const inputsState = this.get();
2948
2877
  if (!inputsState.state && this.autoRender) {
2949
2878
  inputsState.state = { events: { priority: this.autoRenderPriority + 1 } };
@@ -2953,43 +2882,42 @@ class NgtPortal extends NgtRxStore {
2953
2882
  const container = is.ref(containerState) ? containerState.nativeElement : containerState;
2954
2883
  const localState = getLocalState(container);
2955
2884
  if (!localState.store) {
2956
- localState.store = this.portalStore;
2885
+ localState.store = this.#portalStore;
2957
2886
  }
2958
- this.portalStore.set({
2887
+ this.#portalStore.set({
2959
2888
  ...previousState,
2960
2889
  scene: container,
2961
- raycaster: this.raycaster,
2962
- pointer: this.pointer,
2963
- previousStore: this.parentStore,
2890
+ raycaster: this.#raycaster,
2891
+ pointer: this.#pointer,
2892
+ previousStore: this.#parentStore,
2964
2893
  events: { ...previousState.events, ...(events || {}) },
2965
2894
  size: { ...previousState.size, ...(size || {}) },
2966
2895
  ...restInputsState,
2967
- get: this.portalStore.get.bind(this.portalStore),
2968
- set: this.portalStore.set.bind(this.portalStore),
2969
- select: this.portalStore.select.bind(this.portalStore),
2970
- setEvents: (events) => this.portalStore.set((state) => ({ ...state, events: { ...state.events, ...events } })),
2896
+ get: this.#portalStore.get.bind(this.#portalStore),
2897
+ set: this.#portalStore.set.bind(this.#portalStore),
2898
+ setEvents: (events) => this.#portalStore.set((state) => ({ ...state, events: { ...state.events, ...events } })),
2971
2899
  });
2972
- this.hold(this.parentStore.select(), (previous) => this.portalStore.set((state) => this.inject(previous, state)));
2900
+ const parentState = this.#parentStore.select();
2901
+ effect(() => {
2902
+ const previous = parentState();
2903
+ this.#zone.runOutsideAngular(() => {
2904
+ this.#portalStore.set((state) => this.#inject(previous, state));
2905
+ });
2906
+ }, { injector: this.#injector, allowSignalWrites: true });
2973
2907
  requestAnimationFrame(() => {
2974
- this.portalStore.set((injectState) => this.inject(this.parentStore.get(), injectState));
2908
+ this.#portalStore.set((injectState) => this.#inject(this.#parentStore.get(), injectState));
2975
2909
  });
2976
- this.portalContentView = this.portalContentAnchor.createEmbeddedView(this.portalContentTemplate);
2977
- this.portalContentView.detectChanges();
2910
+ this.#portalContentView = this.portalContentAnchor.createEmbeddedView(this.portalContentTemplate);
2911
+ safeDetectChanges(this.#portalContentView);
2978
2912
  this.portalContentRendered = true;
2979
2913
  }
2980
2914
  onBeforeRender(portal) {
2981
2915
  this.beforeRender.emit({
2982
- root: { ...this.parentStore.get(), delta: portal.delta, frame: portal.frame },
2916
+ root: { ...this.#parentStore.get(), delta: portal.delta, frame: portal.frame },
2983
2917
  portal,
2984
2918
  });
2985
2919
  }
2986
- ngOnDestroy() {
2987
- if (this.portalContentView && !this.portalContentView.destroyed) {
2988
- this.portalContentView.destroy();
2989
- }
2990
- super.ngOnDestroy();
2991
- }
2992
- inject(rootState, injectState) {
2920
+ #inject(rootState, injectState) {
2993
2921
  const intersect = { ...rootState };
2994
2922
  Object.keys(intersect).forEach((key) => {
2995
2923
  if (privateKeys.includes(key) ||
@@ -3009,16 +2937,16 @@ class NgtPortal extends NgtRxStore {
3009
2937
  return {
3010
2938
  ...intersect,
3011
2939
  scene: is.ref(inputs.container) ? inputs.container.nativeElement : inputs.container,
3012
- raycaster: this.raycaster,
3013
- pointer: this.pointer,
3014
- previousStore: this.parentStore,
2940
+ raycaster: this.#raycaster,
2941
+ pointer: this.#pointer,
2942
+ previousStore: this.#parentStore,
3015
2943
  events: { ...rootState.events, ...(injectState?.events || {}), ...events },
3016
2944
  size: { ...rootState.size, ...size },
3017
2945
  viewport: { ...rootState.viewport, ...(viewport || {}) },
3018
2946
  ...restInputsState,
3019
2947
  };
3020
2948
  }
3021
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPortal, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
2949
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtPortal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3022
2950
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.0", type: NgtPortal, isStandalone: true, selector: "ngt-portal", inputs: { container: "container", state: "state", autoRender: "autoRender", autoRenderPriority: "autoRenderPriority" }, outputs: { beforeRender: "beforeRender" }, providers: [NgtStore], queries: [{ propertyName: "portalContentTemplate", first: true, predicate: NgtPortalContent, descendants: true, read: TemplateRef, static: true }], viewQueries: [{ propertyName: "portalContentAnchor", first: true, predicate: ["portalContentAnchor"], descendants: true, read: ViewContainerRef, static: true }], usesInheritance: true, ngImport: i0, template: `
3023
2951
  <ng-container #portalContentAnchor>
3024
2952
  <ng-container
@@ -3052,7 +2980,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
3052
2980
  imports: [NgIf, NgtPortalBeforeRender],
3053
2981
  providers: [NgtStore],
3054
2982
  }]
3055
- }], propDecorators: { container: [{
2983
+ }], ctorParameters: function () { return []; }, propDecorators: { container: [{
3056
2984
  type: Input
3057
2985
  }], state: [{
3058
2986
  type: Input
@@ -3070,15 +2998,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
3070
2998
  args: ['portalContentAnchor', { read: ViewContainerRef, static: true }]
3071
2999
  }] } });
3072
3000
 
3001
+ var _a;
3073
3002
  class NgtRoutedScene {
3074
- static { this.isRoutedScene = true; }
3075
- constructor(router) {
3076
- const { destroy$, cdr } = injectNgtDestroy();
3003
+ static { _a = ROUTED_SCENE; }
3004
+ static { this[_a] = true; }
3005
+ constructor(router, cdr) {
3077
3006
  router.events
3078
- .pipe(filter((event) => event instanceof ActivationEnd), takeUntil(destroy$))
3007
+ .pipe(filter((event) => event instanceof ActivationEnd), takeUntilDestroyed())
3079
3008
  .subscribe(() => safeDetectChanges(cdr));
3080
3009
  }
3081
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtRoutedScene, deps: [{ token: i1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
3010
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtRoutedScene, deps: [{ token: i1.Router }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
3082
3011
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.0.0", type: NgtRoutedScene, isStandalone: true, selector: "ngt-routed-scene", ngImport: i0, template: `<router-outlet />`, isInline: true, dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }] }); }
3083
3012
  }
3084
3013
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImport: i0, type: NgtRoutedScene, decorators: [{
@@ -3089,11 +3018,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.0.0", ngImpor
3089
3018
  template: `<router-outlet />`,
3090
3019
  imports: [RouterOutlet],
3091
3020
  }]
3092
- }], ctorParameters: function () { return [{ type: i1.Router }]; } });
3021
+ }], ctorParameters: function () { return [{ type: i1.Router }, { type: i0.ChangeDetectorRef }]; } });
3093
3022
 
3094
3023
  /**
3095
3024
  * Generated bundle index. Do not edit.
3096
3025
  */
3097
3026
 
3098
- export { NGT_CATALOGUE, NgtArgs, NgtCanvas, NgtParent, NgtPortal, NgtPortalBeforeRender, NgtPortalContent, NgtPush, NgtRepeat, NgtRoutedScene, NgtRxStore, NgtStore, addAfterEffect, addEffect, addTail, applyProps, checkNeedsUpdate, checkUpdate, createAttachFunction, createLoop, createRunInContext, extend, flushGlobalEffects, getLocalState, injectBeforeRender, injectNgtDestroy, injectNgtLoader, injectNgtRef, invalidateInstance, is, makeDefaultCamera, makeDefaultRenderer, makeDpr, makeId, makeObjectGraph, prepare, rootStateMap, safeDetectChanges, startWithUndefined, tapEffect, updateCamera };
3027
+ export { NGT_CATALOGUE, NgtArgs, NgtCanvas, NgtParent, NgtPortal, NgtPortalContent, NgtRepeat, NgtRoutedScene, NgtSignalStore, NgtStore, addAfterEffect, addEffect, addTail, applyProps, checkNeedsUpdate, checkUpdate, createAttachFunction, extend, getLocalState, injectBeforeRender, injectNgtLoader, injectNgtRef, invalidateInstance, is, makeDefaultCamera, makeDefaultRenderer, makeDpr, makeId, makeObjectGraph, prepare, rootStateMap, safeDetectChanges, updateCamera };
3099
3028
  //# sourceMappingURL=angular-three.mjs.map