angular-three 2.0.0-beta.21 → 2.0.0-beta.223

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 (109) hide show
  1. package/esm2022/index.mjs +4 -11
  2. package/esm2022/lib/canvas.mjs +81 -156
  3. package/esm2022/lib/directives/args.mjs +6 -6
  4. package/esm2022/lib/directives/common.mjs +15 -14
  5. package/esm2022/lib/directives/parent.mjs +6 -6
  6. package/esm2022/lib/dom/events.mjs +2 -2
  7. package/esm2022/lib/events.mjs +28 -25
  8. package/esm2022/lib/instance.mjs +39 -34
  9. package/esm2022/lib/loader.mjs +12 -14
  10. package/esm2022/lib/loop.mjs +9 -10
  11. package/esm2022/lib/portal.mjs +122 -135
  12. package/esm2022/lib/ref.mjs +18 -20
  13. package/esm2022/lib/renderer/catalogue.mjs +2 -2
  14. package/esm2022/lib/renderer/constants.mjs +2 -2
  15. package/esm2022/lib/renderer/index.mjs +58 -62
  16. package/esm2022/lib/renderer/store.mjs +129 -120
  17. package/esm2022/lib/renderer/utils.mjs +35 -42
  18. package/esm2022/lib/roots.mjs +41 -38
  19. package/esm2022/lib/routed-scene.mjs +6 -7
  20. package/esm2022/lib/store.mjs +163 -189
  21. package/esm2022/lib/utils/apply-props.mjs +12 -17
  22. package/esm2022/lib/utils/attach.mjs +6 -6
  23. package/esm2022/lib/utils/before-render.mjs +12 -0
  24. package/esm2022/lib/utils/cd-aware-signal.mjs +24 -0
  25. package/esm2022/lib/utils/create-api-token.mjs +13 -0
  26. package/esm2022/lib/utils/is.mjs +6 -5
  27. package/esm2022/lib/utils/make.mjs +15 -12
  28. package/esm2022/lib/utils/signal-store.mjs +67 -57
  29. package/esm2022/lib/utils/update.mjs +3 -2
  30. package/fesm2022/angular-three.mjs +1621 -1795
  31. package/fesm2022/angular-three.mjs.map +1 -1
  32. package/index.d.ts +6 -10
  33. package/lib/canvas.d.ts +24 -37
  34. package/lib/directives/common.d.ts +12 -1
  35. package/lib/events.d.ts +2 -2
  36. package/lib/instance.d.ts +19 -10
  37. package/lib/loader.d.ts +13 -4
  38. package/lib/loop.d.ts +6 -29
  39. package/lib/portal.d.ts +18 -26
  40. package/lib/ref.d.ts +0 -1
  41. package/lib/renderer/catalogue.d.ts +5 -1
  42. package/lib/renderer/constants.d.ts +1 -1
  43. package/lib/renderer/index.d.ts +55 -4
  44. package/lib/renderer/store.d.ts +18 -21
  45. package/lib/renderer/utils.d.ts +2 -3
  46. package/lib/roots.d.ts +4 -3
  47. package/lib/store.d.ts +9 -11
  48. package/lib/utils/apply-props.d.ts +0 -1
  49. package/lib/{before-render.d.ts → utils/before-render.d.ts} +1 -1
  50. package/lib/utils/cd-aware-signal.d.ts +4 -0
  51. package/lib/utils/create-api-token.d.ts +23 -0
  52. package/lib/utils/is.d.ts +11 -12
  53. package/lib/utils/make.d.ts +3 -2
  54. package/lib/utils/signal-store.d.ts +16 -3
  55. package/metadata.json +1 -1
  56. package/package.json +30 -11
  57. package/plugin/generators.json +0 -32
  58. package/plugin/src/generators/init/compat.d.ts +1 -3
  59. package/plugin/src/generators/init/files/experience/{experience.component.ts.__tmpl__ → experience.component.ts__tmpl__} +1 -0
  60. package/plugin/src/generators/init/generator.d.ts +2 -5
  61. package/plugin/src/generators/init/generator.js +94 -95
  62. package/plugin/src/generators/init/generator.js.map +1 -1
  63. package/plugin/src/generators/init/schema.json +1 -12
  64. package/plugin/src/generators/utils.js.map +1 -1
  65. package/plugin/src/generators/{versions.d.ts → version.d.ts} +5 -3
  66. package/plugin/src/generators/version.js +18 -0
  67. package/plugin/src/generators/version.js.map +1 -0
  68. package/plugin/src/index.d.ts +0 -3
  69. package/plugin/src/index.js +0 -9
  70. package/plugin/src/index.js.map +1 -1
  71. package/web-types.json +1 -1
  72. package/esm2022/lib/before-render.mjs +0 -13
  73. package/esm2022/lib/directives/key.mjs +0 -29
  74. package/esm2022/lib/directives/repeat.mjs +0 -17
  75. package/esm2022/lib/three-types.mjs +0 -2
  76. package/esm2022/lib/utils/assert-injection-context.mjs +0 -14
  77. package/esm2022/lib/utils/create-injection-token.mjs +0 -47
  78. package/esm2022/lib/utils/safe-detect-changes.mjs +0 -17
  79. package/lib/directives/key.d.ts +0 -10
  80. package/lib/directives/repeat.d.ts +0 -7
  81. package/lib/three-types.d.ts +0 -306
  82. package/lib/utils/assert-injection-context.d.ts +0 -2
  83. package/lib/utils/create-injection-token.d.ts +0 -27
  84. package/lib/utils/safe-detect-changes.d.ts +0 -2
  85. package/plugin/package.json +0 -6
  86. package/plugin/src/generators/init-cannon/compat.d.ts +0 -2
  87. package/plugin/src/generators/init-cannon/compat.js +0 -6
  88. package/plugin/src/generators/init-cannon/compat.js.map +0 -1
  89. package/plugin/src/generators/init-cannon/generator.d.ts +0 -2
  90. package/plugin/src/generators/init-cannon/generator.js +0 -22
  91. package/plugin/src/generators/init-cannon/generator.js.map +0 -1
  92. package/plugin/src/generators/init-cannon/schema.json +0 -6
  93. package/plugin/src/generators/init-postprocessing/compat.d.ts +0 -2
  94. package/plugin/src/generators/init-postprocessing/compat.js +0 -6
  95. package/plugin/src/generators/init-postprocessing/compat.js.map +0 -1
  96. package/plugin/src/generators/init-postprocessing/generator.d.ts +0 -2
  97. package/plugin/src/generators/init-postprocessing/generator.js +0 -20
  98. package/plugin/src/generators/init-postprocessing/generator.js.map +0 -1
  99. package/plugin/src/generators/init-postprocessing/schema.json +0 -6
  100. package/plugin/src/generators/init-soba/compat.d.ts +0 -2
  101. package/plugin/src/generators/init-soba/compat.js +0 -6
  102. package/plugin/src/generators/init-soba/compat.js.map +0 -1
  103. package/plugin/src/generators/init-soba/generator.d.ts +0 -2
  104. package/plugin/src/generators/init-soba/generator.js +0 -26
  105. package/plugin/src/generators/init-soba/generator.js.map +0 -1
  106. package/plugin/src/generators/init-soba/schema.json +0 -6
  107. package/plugin/src/generators/versions.js +0 -16
  108. package/plugin/src/generators/versions.js.map +0 -1
  109. /package/plugin/src/generators/init/files/experience/{experience.component.html.__tmpl__ → experience.component.html__tmpl__} +0 -0
@@ -1,16 +1,19 @@
1
1
  import * as i0 from '@angular/core';
2
- import { untracked, computed, signal, ElementRef, inject, Injector, assertInInjectionContext, runInInjectionContext, DestroyRef, effect, InjectionToken, ChangeDetectorRef, Optional, SkipSelf, ViewContainerRef, NgZone, TemplateRef, Directive, Input, EventEmitter, getDebugNode, RendererFactory2, Injectable, makeEnvironmentProviders, provideZoneChangeDetection, EnvironmentInjector, createEnvironmentInjector, Component, ChangeDetectionStrategy, Output, ViewChild, ContentChild } from '@angular/core';
3
- import { DOCUMENT, NgForOf, NgIf } from '@angular/common';
4
- import { Subject, filter } from 'rxjs';
2
+ import { untracked, computed, signal, ElementRef, inject, effect, InjectionToken, Optional, SkipSelf, ViewContainerRef, NgZone, TemplateRef, DestroyRef, Directive, Input, EventEmitter, getDebugNode, RendererFactory2, Injectable, Injector, provideZoneChangeDetection, makeEnvironmentProviders, EnvironmentInjector, afterNextRender, createEnvironmentInjector, Component, ChangeDetectionStrategy, Output, ViewChild, CUSTOM_ELEMENTS_SCHEMA, ContentChild, ChangeDetectorRef, forwardRef } from '@angular/core';
3
+ import { injectAutoEffect } from 'ngxtension/auto-effect';
4
+ import { provideResizeOptions, NgxResize } from 'ngxtension/resize';
5
5
  import * as THREE from 'three';
6
- import { provideNgxResizeOptions, NgxResize } from 'ngx-resize';
6
+ import { DOCUMENT } from '@angular/common';
7
+ import { createInjectionToken } from 'ngxtension/create-injection-token';
8
+ import { Subject, filter } from 'rxjs';
9
+ import { assertInjector } from 'ngxtension/assert-injector';
7
10
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
11
  import * as i1 from '@angular/router';
9
12
  import { ActivationEnd, RouterOutlet } from '@angular/router';
10
13
 
11
14
  const STORE_COMPUTED_KEY = '__ngt_signal_store_computed__';
12
- const setter = (_source) => (state) => {
13
- const updater = (previous) => {
15
+ function reducer(state) {
16
+ return (previous) => {
14
17
  const partial = typeof state === 'function' ? state(previous) : state;
15
18
  Object.keys(partial).forEach((key) => {
16
19
  const typedKey = key;
@@ -20,83 +23,93 @@ const setter = (_source) => (state) => {
20
23
  });
21
24
  return partial;
22
25
  };
23
- untracked(() => {
24
- _source.update((previous) => ({ ...previous, ...updater(previous) }));
25
- });
26
- };
27
- const patcher = (_source) => (state) => {
28
- const updater = (previous) => {
29
- Object.keys(state).forEach((key) => {
30
- const typedKey = key;
31
- if (state[typedKey] === undefined && previous[typedKey] != null) {
32
- state[typedKey] = previous[typedKey];
33
- }
26
+ }
27
+ function updater(_source) {
28
+ return (state) => {
29
+ const updater = reducer(state);
30
+ untracked(() => {
31
+ _source.update((previous) => ({ ...previous, ...updater(previous) }));
34
32
  });
35
- return state;
36
33
  };
37
- untracked(() => {
38
- _source.update((previous) => ({ ...updater(previous), ...previous }));
39
- });
40
- };
41
- const getter = (_source) => (...keys) => {
42
- const root = untracked(_source);
43
- if (keys.length === 0)
44
- return root;
45
- return keys.reduce((value, key) => value[key], root);
46
- };
47
- const selector = (_state, computedCache) => (...keysAndOptions) => {
48
- if (keysAndOptions.length === 0)
49
- return _state;
50
- if (keysAndOptions.length === 1 && typeof keysAndOptions[0] === 'object') {
51
- const cachedKey = STORE_COMPUTED_KEY.concat(JSON.stringify(keysAndOptions[0]));
52
- if (!computedCache.has(cachedKey)) {
53
- computedCache.set(cachedKey, computed(_state, keysAndOptions));
54
- }
55
- return computedCache.get(cachedKey);
56
- }
57
- const [keys, options] = parseStoreOptions(keysAndOptions);
58
- const joinedKeys = keys.join('-');
59
- const cachedKeys = joinedKeys.concat(options ? JSON.stringify(options) : '');
60
- if (!computedCache.has(cachedKeys)) {
61
- computedCache.set(cachedKeys, computed(() => keys.reduce((value, key) => value[key], _state()), options));
62
- }
63
- return computedCache.get(cachedKeys);
64
- };
34
+ }
35
+ function patcher(_source) {
36
+ return (state) => {
37
+ const updater = reducer(state);
38
+ untracked(() => {
39
+ _source.update((previous) => ({ ...updater(previous), ...previous }));
40
+ });
41
+ };
42
+ }
43
+ function getter(_source) {
44
+ return (...keys) => {
45
+ const root = untracked(_source);
46
+ if (keys.length === 0)
47
+ return root;
48
+ return keys.reduce((value, key) => value[key], root);
49
+ };
50
+ }
51
+ function selector(_state, computedCache) {
52
+ return (...keysAndOptions) => {
53
+ if (keysAndOptions.length === 0)
54
+ return _state;
55
+ if (keysAndOptions.length === 1 && typeof keysAndOptions[0] === 'object') {
56
+ const cachedKey = STORE_COMPUTED_KEY.concat(JSON.stringify(keysAndOptions[0]));
57
+ if (!computedCache.has(cachedKey)) {
58
+ computedCache.set(cachedKey, computed(_state, keysAndOptions));
59
+ }
60
+ return computedCache.get(cachedKey);
61
+ }
62
+ const [keys, options] = parseStoreOptions(keysAndOptions);
63
+ const joinedKeys = keys.join('-');
64
+ const cachedKeys = joinedKeys.concat(JSON.stringify(options));
65
+ if (!computedCache.has(cachedKeys)) {
66
+ computedCache.set(cachedKeys, computed(() => keys.reduce((value, key) => value[key], _state()), options));
67
+ }
68
+ return computedCache.get(cachedKeys);
69
+ };
70
+ }
71
+ function parseStoreOptions(keysAndOptions) {
72
+ if (typeof keysAndOptions.at(-1) === 'object') {
73
+ return [keysAndOptions.slice(0, -1), keysAndOptions.at(-1)];
74
+ }
75
+ return [keysAndOptions, { equal: Object.is }];
76
+ }
65
77
  function signalStore(initialState = {}, options) {
66
78
  let source;
67
- let set;
79
+ let update;
68
80
  let get;
69
81
  let patch;
82
+ let select;
83
+ let state;
84
+ const computedCache = new Map();
85
+ if (!options) {
86
+ options = { equal: Object.is };
87
+ }
70
88
  if (typeof initialState === 'function') {
71
89
  source = signal({}, options);
90
+ state = source.asReadonly();
72
91
  get = getter(source);
73
- set = setter(source);
92
+ update = updater(source);
74
93
  patch = patcher(source);
75
- source.set(initialState({ set, get, patch }));
94
+ select = selector(state, computedCache);
95
+ source.set(initialState({ update, get, patch, select }));
76
96
  }
77
97
  else {
78
98
  source = signal(initialState, options);
99
+ state = source.asReadonly();
79
100
  get = getter(source);
80
- set = setter(source);
101
+ update = updater(source);
81
102
  patch = patcher(source);
103
+ select = selector(state, computedCache);
82
104
  }
83
- const state = source.asReadonly();
84
- const computedCache = new Map();
85
- const store = { select: selector(state, computedCache), get, set, patch, state };
86
- // NOTE: internal _snapshot to debug current state
87
- Object.defineProperty(store, '_snapshot', {
88
- get: state,
105
+ const store = { select, get, update, patch, state };
106
+ Object.defineProperty(store, 'snapshot', {
107
+ get: untracked.bind({}, state),
89
108
  configurable: false,
90
109
  enumerable: false,
91
110
  });
92
111
  return store;
93
112
  }
94
- function parseStoreOptions(keysAndOptions) {
95
- if (typeof keysAndOptions.at(-1) === 'object') {
96
- return [keysAndOptions.slice(0, -1), keysAndOptions.at(-1)];
97
- }
98
- return [keysAndOptions];
99
- }
100
113
 
101
114
  const is = {
102
115
  obj: (a) => a === Object(a) && !Array.isArray(a) && typeof a !== 'function',
@@ -105,11 +118,13 @@ const is = {
105
118
  orthographicCamera: (a) => !!a && a.isOrthographicCamera,
106
119
  perspectiveCamera: (a) => !!a && a.isPerspectiveCamera,
107
120
  camera: (a) => !!a && a.isCamera,
108
- renderer: (a) => !!a && a instanceof THREE.WebGLRenderer,
121
+ renderer: (a) => !!a && typeof a === 'object' && 'render' in a && typeof a['render'] === 'function',
109
122
  scene: (a) => !!a && a.isScene,
110
- object3D: (a) => !!a && a.isObject3D,
111
- instance: (a) => !!a && !!a['__ngt__'],
112
123
  ref: (a) => a instanceof ElementRef,
124
+ instance: (a) => !!a && !!a['__ngt__'],
125
+ object3D: (a) => !!a && a.isObject3D,
126
+ // instance: (a: unknown): a is NgtInstanceNode => !!a && !!(a as NgtAnyRecord)['__ngt__'],
127
+ // ref: (a: unknown): a is ElementRef => a instanceof ElementRef,
113
128
  colorSpaceExist: (object) => 'colorSpace' in object || 'outputColorSpace' in object,
114
129
  equ(a, b, { arrays = 'shallow', objects = 'reference', strict = true } = {}) {
115
130
  // Wrong type or one of the two undefined, doesn't match
@@ -172,8 +187,9 @@ function updateCamera(camera, size) {
172
187
  camera.top = size.height / 2;
173
188
  camera.bottom = size.height / -2;
174
189
  }
175
- else
190
+ else {
176
191
  camera.aspect = size.width / size.height;
192
+ }
177
193
  camera.updateProjectionMatrix();
178
194
  camera.updateMatrixWorld();
179
195
  }
@@ -181,11 +197,11 @@ function updateCamera(camera, size) {
181
197
 
182
198
  function getLocalState(obj) {
183
199
  if (!obj)
184
- return {};
185
- return obj['__ngt__'] || {};
200
+ return undefined;
201
+ return obj['__ngt__'];
186
202
  }
187
203
  function invalidateInstance(instance) {
188
- const state = getLocalState(instance).store?.get();
204
+ const state = getLocalState(instance)?.store.snapshot;
189
205
  if (state && state.internal.frames === 0)
190
206
  state.invalidate();
191
207
  checkUpdate(instance);
@@ -193,37 +209,44 @@ function invalidateInstance(instance) {
193
209
  function prepare(object, localState) {
194
210
  const instance = object;
195
211
  if (localState?.primitive || !instance.__ngt__) {
196
- const { objects = signal([]), nonObjects = signal([]), ...rest } = localState || {};
212
+ const { instanceStore = signalStore({
213
+ nativeProps: {},
214
+ parent: null,
215
+ objects: [],
216
+ nonObjects: [],
217
+ }), ...rest } = localState || {};
197
218
  instance.__ngt__ = {
198
219
  previousAttach: null,
199
220
  store: null,
200
- parent: signal(null),
201
221
  memoized: {},
202
222
  eventCount: 0,
203
223
  handlers: {},
204
- objects,
205
- nonObjects,
206
- nativeProps: signalStore(),
207
- add: (object, type) => {
208
- untracked(() => {
209
- const current = instance.__ngt__[type]();
210
- const foundIndex = current.indexOf((obj) => obj === object);
211
- if (foundIndex > -1) {
212
- // if we add an object with the same reference, then we switch it out
213
- current.splice(foundIndex, 1, object);
214
- instance.__ngt__[type].set(current);
215
- }
216
- else {
217
- instance.__ngt__[type].update((prev) => [...prev, object]);
218
- }
219
- notifyAncestors(instance.__ngt__.parent());
220
- });
224
+ instanceStore,
225
+ parent: instanceStore.select('parent'),
226
+ objects: instanceStore.select('objects'),
227
+ nonObjects: instanceStore.select('nonObjects'),
228
+ nativeProps: instanceStore.select('nativeProps'),
229
+ add(object, type) {
230
+ const current = instance.__ngt__.instanceStore.get(type);
231
+ const foundIndex = current.indexOf((node) => object === node);
232
+ if (foundIndex > -1) {
233
+ current.splice(foundIndex, 1, object);
234
+ instance.__ngt__.instanceStore.update({ [type]: current });
235
+ }
236
+ else {
237
+ instance.__ngt__.instanceStore.update((prev) => ({ [type]: [...prev[type], object] }));
238
+ }
239
+ notifyAncestors(instance.__ngt__.instanceStore.get('parent'));
221
240
  },
222
- remove: (object, type) => {
223
- untracked(() => {
224
- instance.__ngt__[type].update((prev) => prev.filter((o) => o !== object));
225
- notifyAncestors(instance.__ngt__.parent());
226
- });
241
+ remove(object, type) {
242
+ instance.__ngt__.instanceStore.update((prev) => ({ [type]: prev[type].filter((node) => node !== object) }));
243
+ notifyAncestors(instance.__ngt__.instanceStore.get('parent'));
244
+ },
245
+ setNativeProps(key, value) {
246
+ instance.__ngt__.instanceStore.update((prev) => ({ nativeProps: { ...prev.nativeProps, [key]: value } }));
247
+ },
248
+ setParent(parent) {
249
+ instance.__ngt__.instanceStore.update({ parent });
227
250
  },
228
251
  ...rest,
229
252
  };
@@ -234,146 +257,10 @@ function notifyAncestors(instance) {
234
257
  if (!instance)
235
258
  return;
236
259
  const localState = getLocalState(instance);
237
- if (localState.objects)
238
- localState.objects.update((prev) => prev);
239
- if (localState.nonObjects)
240
- localState.nonObjects.update((prev) => prev);
241
- notifyAncestors(localState.parent());
242
- }
243
-
244
- // This function prepares a set of changes to be applied to the instance
245
- function diffProps(instance, props) {
246
- const propsEntries = Object.entries(props);
247
- const changes = [];
248
- for (const [propKey, propValue] of propsEntries) {
249
- let key = propKey;
250
- if (is.colorSpaceExist(instance)) {
251
- if (propKey === 'encoding') {
252
- key = 'colorSpace';
253
- }
254
- else if (propKey === 'outputEncoding') {
255
- key = 'outputColorSpace';
256
- }
257
- }
258
- if (is.equ(propValue, instance[key]))
259
- continue;
260
- changes.push([propKey, propValue]);
261
- }
262
- return changes;
263
- }
264
- // This function applies a set of changes to the instance
265
- function applyProps(instance, props) {
266
- // if props is empty
267
- if (!Object.keys(props).length)
268
- return instance;
269
- // Filter equals, events and reserved props
270
- // filter equals, events , and reserved props
271
- const localState = getLocalState(instance);
272
- const rootState = localState.store?.get();
273
- const changes = diffProps(instance, props);
274
- for (let i = 0; i < changes.length; i++) {
275
- let [key, value] = changes[i];
276
- // Alias (output)encoding => (output)colorSpace (since r152)
277
- // https://github.com/pmndrs/react-three-fiber/pull/2829
278
- if (is.colorSpaceExist(instance)) {
279
- const sRGBEncoding = 3001;
280
- const SRGBColorSpace = 'srgb';
281
- const LinearSRGBColorSpace = 'srgb-linear';
282
- if (key === 'encoding') {
283
- key = 'colorSpace';
284
- value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
285
- }
286
- else if (key === 'outputEncoding') {
287
- key = 'outputColorSpace';
288
- value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
289
- }
290
- }
291
- const currentInstance = instance;
292
- const targetProp = currentInstance[key];
293
- // special treatmen for objects with support for set/copy, and layers
294
- if (targetProp && targetProp['set'] && (targetProp['copy'] || targetProp instanceof THREE.Layers)) {
295
- const isColor = targetProp instanceof THREE.Color;
296
- // if value is an array
297
- if (Array.isArray(value)) {
298
- if (targetProp['fromArray'])
299
- targetProp['fromArray'](value);
300
- else
301
- targetProp['set'](...value);
302
- }
303
- // test again target.copy
304
- else if (targetProp['copy'] &&
305
- value &&
306
- value.constructor &&
307
- targetProp.constructor.name === value.constructor.name) {
308
- targetProp['copy'](value);
309
- if (!THREE.ColorManagement && !rootState.linear && isColor)
310
- targetProp['convertSRGBToLinear']();
311
- }
312
- // if nothing else fits, just set the single value, ignore undefined
313
- else if (value !== undefined) {
314
- const isColor = targetProp instanceof THREE.Color;
315
- // allow setting array scalars
316
- if (!isColor && targetProp['setScalar'])
317
- targetProp['setScalar'](value);
318
- // layers have no copy function, copy the mask
319
- else if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers)
320
- targetProp.mask = value.mask;
321
- // otherwise just set ...
322
- else
323
- targetProp['set'](value);
324
- // auto-convert srgb
325
- if (!THREE.ColorManagement && !rootState?.linear && isColor)
326
- targetProp.convertSRGBToLinear();
327
- }
328
- }
329
- // else just overwrite the value
330
- else {
331
- currentInstance[key] = value;
332
- // auto-convert srgb textures
333
- if (currentInstance[key] instanceof THREE.Texture &&
334
- currentInstance[key].format === THREE.RGBAFormat &&
335
- currentInstance[key].type === THREE.UnsignedByteType) {
336
- const texture = currentInstance[key];
337
- if (rootState?.gl) {
338
- if (is.colorSpaceExist(texture) && is.colorSpaceExist(rootState.gl))
339
- texture.colorSpace = rootState.gl.outputColorSpace;
340
- else
341
- texture.encoding = rootState.gl.outputEncoding;
342
- }
343
- }
344
- }
345
- checkUpdate(currentInstance[key]);
346
- checkUpdate(targetProp);
347
- invalidateInstance(instance);
348
- }
349
- const instanceHandlers = localState.eventCount;
350
- const parent = localState.parent ? untracked(localState.parent) : null;
351
- if (parent && rootState.internal && instance['raycast'] && instanceHandlers !== localState.eventCount) {
352
- // Pre-emptively remove the instance from the interaction manager
353
- const index = rootState.internal.interaction.indexOf(instance);
354
- if (index > -1)
355
- rootState.internal.interaction.splice(index, 1);
356
- // Add the instance to the interaction manager only when it has handlers
357
- if (localState.eventCount)
358
- rootState.internal.interaction.push(instance);
359
- }
360
- if (parent && localState.afterUpdate && localState.afterUpdate.observed && changes.length) {
361
- localState.afterUpdate.emit(instance);
362
- }
363
- return instance;
364
- }
365
-
366
- function assertInjectionContext(fn, injector) {
367
- try {
368
- if (!injector) {
369
- return inject(Injector);
370
- }
371
- return injector;
372
- }
373
- catch {
374
- !injector && assertInInjectionContext(fn);
375
- return null;
376
- }
260
+ if (!localState)
261
+ return;
262
+ localState.instanceStore.update((prev) => ({ objects: prev.objects, nonObjects: prev.nonObjects }));
263
+ notifyAncestors(localState.instanceStore.get('parent'));
377
264
  }
378
265
 
379
266
  const idCache = {};
@@ -390,26 +277,28 @@ function makeId(event) {
390
277
  return makeId();
391
278
  }
392
279
  function makeDpr(dpr, window) {
393
- const target = window?.devicePixelRatio || 1;
280
+ // Err on the side of progress by assuming 2x dpr if we can't detect it
281
+ // This will happen in workers where window is defined but dpr isn't.
282
+ const target = typeof window !== 'undefined' ? window.devicePixelRatio ?? 2 : 1;
394
283
  return Array.isArray(dpr) ? Math.min(Math.max(dpr[0], target), dpr[1]) : dpr;
395
284
  }
396
- function makeDefaultCamera(isOrthographic, size) {
397
- if (isOrthographic)
398
- return new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000);
399
- return new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000);
400
- }
401
- function makeDefaultRenderer(glOptions, canvasElement) {
402
- const customRenderer = (typeof glOptions === 'function' ? glOptions(canvasElement) : glOptions);
403
- if (customRenderer?.render != null)
285
+ function makeRendererInstance(glOptions, canvas) {
286
+ const customRenderer = (typeof glOptions === 'function' ? glOptions(canvas) : glOptions);
287
+ if (is.renderer(customRenderer))
404
288
  return customRenderer;
405
289
  return new THREE.WebGLRenderer({
406
290
  powerPreference: 'high-performance',
407
- canvas: canvasElement,
291
+ canvas: canvas,
408
292
  antialias: true,
409
293
  alpha: true,
410
- ...(glOptions || {}),
294
+ ...glOptions,
411
295
  });
412
296
  }
297
+ function makeCameraInstance(isOrthographic, size) {
298
+ if (isOrthographic)
299
+ return new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000);
300
+ return new THREE.PerspectiveCamera(75, size.width / size.height, 0.1, 1000);
301
+ }
413
302
  function makeObjectGraph(object) {
414
303
  const data = { nodes: {}, materials: {} };
415
304
  if (object) {
@@ -425,706 +314,62 @@ function makeObjectGraph(object) {
425
314
  return data;
426
315
  }
427
316
 
428
- const shallowLoose = { objects: 'shallow', strict: false };
429
- const roots = new Map();
430
- function injectCanvasRootInitializer(injector) {
431
- injector = assertInjectionContext(injectCanvasRootInitializer, injector);
432
- return runInInjectionContext(injector, () => {
433
- const injectedStore = injectNgtStore();
434
- const loop = injectNgtLoop();
435
- const destroyRef = inject(DestroyRef);
436
- return (canvas) => {
437
- const exist = roots.has(canvas);
438
- let store = roots.get(canvas);
439
- if (store) {
440
- console.warn('[NGT] Same canvas root is being created twice');
441
- }
442
- store ||= injectedStore;
443
- if (!store) {
444
- throw new Error('[NGT] No store initialized');
445
- }
446
- if (!exist) {
447
- roots.set(canvas, store);
448
- }
449
- let isConfigured = false;
450
- let invalidateRef;
451
- destroyRef.onDestroy(() => invalidateRef?.destroy());
452
- return {
453
- isConfigured,
454
- destroy: (timeout = 500) => {
455
- const root = roots.get(canvas);
456
- if (root) {
457
- root.set((state) => ({ internal: { ...state.internal, active: false } }));
458
- setTimeout(() => {
459
- try {
460
- const state = root.get();
461
- state.events.disconnect?.();
462
- state.gl?.renderLists?.dispose?.();
463
- state.gl?.forceContextLoss?.();
464
- if (state.gl?.xr)
465
- state.xr.disconnect();
466
- dispose(state);
467
- roots.delete(canvas);
468
- }
469
- catch (e) {
470
- console.error('[NGT] Unexpected error while destroying Canvas Root', e);
471
- }
472
- }, timeout);
473
- }
474
- },
475
- configure: (inputs) => {
476
- const { gl: glOptions, size: sizeOptions, camera: cameraOptions, raycaster: raycasterOptions, scene: sceneOptions, events, orthographic, lookAt, shadows, linear, legacy, flat, dpr, frameloop, performance, } = inputs;
477
- const state = store.get();
478
- const stateToUpdate = {};
479
- // setup renderer
480
- let gl = state.gl;
481
- if (!state.gl)
482
- stateToUpdate.gl = gl = makeDefaultRenderer(glOptions, canvas);
483
- // setup raycaster
484
- let raycaster = state.raycaster;
485
- if (!raycaster)
486
- stateToUpdate.raycaster = raycaster = new THREE.Raycaster();
487
- // set raycaster options
488
- const { params, ...options } = raycasterOptions || {};
489
- if (!is.equ(options, raycaster, shallowLoose))
490
- applyProps(raycaster, { ...options });
491
- if (!is.equ(params, raycaster.params, shallowLoose)) {
492
- applyProps(raycaster, { params: { ...raycaster.params, ...(params || {}) } });
493
- }
494
- // create default camera
495
- if (!state.camera) {
496
- const isCamera = is.camera(cameraOptions);
497
- let camera = isCamera ? cameraOptions : makeDefaultCamera(orthographic || false, state.size);
498
- if (!isCamera) {
499
- if (cameraOptions)
500
- applyProps(camera, cameraOptions);
501
- // set position.z
502
- if (!cameraOptions?.position)
503
- camera.position.z = 5;
504
- // always look at center or passed-in lookAt by default
505
- if (!cameraOptions?.rotation && !cameraOptions?.quaternion) {
506
- if (Array.isArray(lookAt))
507
- camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
508
- else if (lookAt instanceof THREE.Vector3)
509
- camera.lookAt(lookAt);
510
- else
511
- camera.lookAt(0, 0, 0);
512
- }
513
- // update projection matrix after applyprops
514
- camera.updateProjectionMatrix?.();
515
- }
516
- if (!is.instance(camera))
517
- camera = prepare(camera, { store });
518
- stateToUpdate.camera = camera;
519
- }
520
- // Set up scene (one time only!)
521
- if (!state.scene) {
522
- let scene;
523
- if (sceneOptions instanceof THREE.Scene) {
524
- scene = prepare(sceneOptions, { store });
525
- }
526
- else {
527
- scene = prepare(new THREE.Scene(), { store });
528
- if (sceneOptions)
529
- applyProps(scene, sceneOptions);
530
- }
531
- stateToUpdate.scene = scene;
532
- }
533
- // Set up XR (one time only!)
534
- if (!state.xr) {
535
- // Handle frame behavior in WebXR
536
- const handleXRFrame = (timestamp, frame) => {
537
- const state = store.get();
538
- if (state.frameloop === 'never')
539
- return;
540
- loop.advance(timestamp, true, store, frame);
541
- };
542
- // Toggle render switching on session
543
- const handleSessionChange = () => {
544
- const state = store.get();
545
- state.gl.xr.enabled = state.gl.xr.isPresenting;
546
- state.gl.xr.setAnimationLoop(state.gl.xr.isPresenting ? handleXRFrame : null);
547
- if (!state.gl.xr.isPresenting)
548
- loop.invalidate(store);
549
- };
550
- // WebXR session manager
551
- const xr = {
552
- connect: () => {
553
- gl.xr.addEventListener('sessionstart', handleSessionChange);
554
- gl.xr.addEventListener('sessionend', handleSessionChange);
555
- },
556
- disconnect: () => {
557
- gl.xr.removeEventListener('sessionstart', handleSessionChange);
558
- gl.xr.removeEventListener('sessionend', handleSessionChange);
559
- },
560
- };
561
- // Subscribe to WebXR session events
562
- if (gl.xr && typeof gl.xr.addEventListener === 'function')
563
- xr.connect();
564
- stateToUpdate.xr = xr;
565
- }
566
- // Set shadowmap
567
- if (gl.shadowMap) {
568
- const oldEnabled = gl.shadowMap.enabled;
569
- const oldType = gl.shadowMap.type;
570
- gl.shadowMap.enabled = !!shadows;
571
- if (typeof shadows === 'boolean') {
572
- gl.shadowMap.type = THREE.PCFSoftShadowMap;
573
- }
574
- else if (typeof shadows === 'string') {
575
- const types = {
576
- basic: THREE.BasicShadowMap,
577
- percentage: THREE.PCFShadowMap,
578
- soft: THREE.PCFSoftShadowMap,
579
- variance: THREE.VSMShadowMap,
580
- };
581
- gl.shadowMap.type = types[shadows] ?? THREE.PCFSoftShadowMap;
582
- }
583
- else if (is.obj(shadows)) {
584
- Object.assign(gl.shadowMap, shadows);
585
- }
586
- if (oldEnabled !== gl.shadowMap.enabled || oldType !== gl.shadowMap.type)
587
- checkNeedsUpdate(gl.shadowMap);
588
- }
589
- // Safely set color management if available.
590
- // Avoid accessing THREE.ColorManagement to play nice with older versions
591
- if (THREE.ColorManagement) {
592
- const ColorManagement = THREE.ColorManagement;
593
- if ('enabled' in ColorManagement)
594
- ColorManagement['enabled'] = !legacy ?? false;
595
- else if ('legacyMode' in ColorManagement)
596
- ColorManagement['legacyMode'] = legacy ?? true;
597
- }
598
- // set color space and tonemapping preferences
599
- const LinearEncoding = 3000;
600
- const sRGBEncoding = 3001;
601
- applyProps(gl, {
602
- outputEncoding: linear ? LinearEncoding : sRGBEncoding,
603
- toneMapping: flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping,
604
- });
605
- // Update color management state
606
- if (state.legacy !== legacy)
607
- stateToUpdate.legacy = legacy;
608
- if (state.linear !== linear)
609
- stateToUpdate.linear = linear;
610
- if (state.flat !== flat)
611
- stateToUpdate.flat = flat;
612
- // Set gl props
613
- gl.setClearAlpha(0);
614
- gl.setPixelRatio(makeDpr(state.viewport.dpr));
615
- gl.setSize(state.size.width, state.size.height);
616
- if (is.obj(glOptions) &&
617
- !(typeof glOptions === 'function') &&
618
- !is.renderer(glOptions) &&
619
- !is.equ(glOptions, gl, shallowLoose)) {
620
- applyProps(gl, glOptions);
621
- }
622
- // Store events internally
623
- if (events && !state.events.handlers)
624
- stateToUpdate.events = events(store);
625
- // Check performance
626
- if (performance && !is.equ(performance, state.performance, shallowLoose)) {
627
- stateToUpdate.performance = { ...state.performance, ...performance };
628
- }
629
- store.set(stateToUpdate);
630
- // Check size, allow it to take on container bounds initially
631
- const size = computeInitialSize(canvas, sizeOptions);
632
- if (!is.equ(size, state.size, shallowLoose)) {
633
- state.setSize(size.width, size.height, size.top, size.left);
634
- }
635
- // Check pixelratio
636
- if (dpr && state.viewport.dpr !== makeDpr(dpr))
637
- state.setDpr(dpr);
638
- // Check frameloop
639
- if (state.frameloop !== frameloop)
640
- state.setFrameloop(frameloop);
641
- isConfigured = true;
642
- invalidateRef?.destroy();
643
- invalidateRef = effect(() => void store.state().invalidate(), { manualCleanup: true, injector });
644
- },
645
- };
646
- };
647
- });
648
- }
649
- function computeInitialSize(canvas, defaultSize) {
650
- if (defaultSize)
651
- return defaultSize;
652
- if (canvas instanceof HTMLCanvasElement && canvas.parentElement) {
653
- return canvas.parentElement.getBoundingClientRect();
654
- }
655
- return { width: 0, height: 0, top: 0, left: 0 };
656
- }
657
- // Disposes an object and all its properties
658
- function dispose(obj) {
659
- if (obj.dispose && obj.type !== 'Scene')
660
- obj.dispose();
661
- for (const p in obj) {
662
- p.dispose?.();
663
- delete obj[p];
664
- }
665
- }
666
-
667
- function createInjectFn(token) {
668
- return (injectOptions) => inject(token, injectOptions);
669
- }
670
- function createProvideFn(token, factory, deps, extraProviders) {
671
- return (value) => {
672
- let provider;
673
- if (value) {
674
- provider = { provide: token, useValue: value };
675
- }
676
- else {
677
- provider = { provide: token, useFactory: factory, deps: (deps ?? []) };
678
- }
679
- return extraProviders ? [extraProviders, provider] : provider;
680
- };
681
- }
682
- function createInjectionToken(factory, options) {
683
- const opts = options ?? { isRoot: true };
684
- opts.isRoot ??= true;
685
- if (opts.isRoot) {
686
- if (opts.token) {
687
- throw new Error(`\
688
- createInjectionToken is creating a root InjectionToken but an external token is passed in.
689
- `);
690
- }
691
- const token = new InjectionToken(`Token for ${factory.name}`, {
692
- factory: () => {
693
- if (opts.deps && Array.isArray(opts.deps)) {
694
- return factory(...opts.deps.map((dep) => inject(dep)));
695
- }
696
- return factory();
697
- },
698
- });
699
- return [
700
- createInjectFn(token),
701
- createProvideFn(token, factory, opts.deps),
702
- token,
703
- ];
704
- }
705
- const token = opts.token || new InjectionToken(`Token for ${factory.name}`);
706
- return [
707
- createInjectFn(token),
708
- createProvideFn(token, factory, opts.deps, opts.extraProviders),
709
- token,
710
- ];
711
- }
712
-
713
- function createSubs(callback, subs) {
714
- const sub = { callback };
715
- subs.add(sub);
716
- return () => void subs.delete(sub);
717
- }
718
- const globalEffects = new Set();
719
- const globalAfterEffects = new Set();
720
- const globalTailEffects = new Set();
721
- /**
722
- * Adds a global render callback which is called each frame.
723
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect
724
- */
725
- const addEffect = (callback) => createSubs(callback, globalEffects);
726
- /**
727
- * Adds a global after-render callback which is called each frame.
728
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect
729
- */
730
- const addAfterEffect = (callback) => createSubs(callback, globalAfterEffects);
731
- /**
732
- * Adds a global callback which is called when rendering stops.
733
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail
734
- */
735
- const addTail = (callback) => createSubs(callback, globalTailEffects);
736
- function run(effects, timestamp) {
737
- if (!effects.size)
738
- return;
739
- for (const { callback } of effects.values()) {
740
- callback(timestamp);
741
- }
742
- }
743
- function flushGlobalEffects(type, timestamp) {
744
- switch (type) {
745
- case 'before':
746
- return run(globalEffects, timestamp);
747
- case 'after':
748
- return run(globalAfterEffects, timestamp);
749
- case 'tail':
750
- return run(globalTailEffects, timestamp);
751
- }
752
- }
753
- function render(timestamp, store, frame) {
754
- const state = store.get();
755
- // Run local effects
756
- let delta = state.clock.getDelta();
757
- // In frameloop='never' mode, clock times are updated using the provided timestamp
758
- if (state.frameloop === 'never' && typeof timestamp === 'number') {
759
- delta = timestamp - state.clock.elapsedTime;
760
- state.clock.oldTime = state.clock.elapsedTime;
761
- state.clock.elapsedTime = timestamp;
762
- }
763
- // Call subscribers (useFrame)
764
- const subscribers = state.internal.subscribers;
765
- for (let i = 0; i < subscribers.length; i++) {
766
- const subscription = subscribers[i];
767
- subscription.callback({ ...subscription.store.get(), delta, frame });
768
- }
769
- // Render content
770
- if (!state.internal.priority && state.gl.render)
771
- state.gl.render(state.scene, state.camera);
772
- // Decrease frame count
773
- state.internal.frames = Math.max(0, state.internal.frames - 1);
774
- return state.frameloop === 'always' ? 1 : state.internal.frames;
775
- }
776
- function createLoop(roots) {
777
- let running = false;
778
- let repeat;
779
- let frame;
780
- function loop(timestamp) {
781
- frame = requestAnimationFrame(loop);
782
- running = true;
783
- repeat = 0;
784
- // Run effects
785
- flushGlobalEffects('before', timestamp);
786
- // Render all roots
787
- for (const root of roots.values()) {
788
- const state = root.get();
789
- // If the frameloop is invalidated, do not run another frame
790
- if (state.internal.active &&
791
- (state.frameloop === 'always' || state.internal.frames > 0) &&
792
- !state.gl.xr?.isPresenting) {
793
- repeat += render(timestamp, root);
794
- }
795
- }
796
- // Run after-effects
797
- flushGlobalEffects('after', timestamp);
798
- // Stop the loop if nothing invalidates it
799
- if (repeat === 0) {
800
- // Tail call effects, they are called when rendering stops
801
- flushGlobalEffects('tail', timestamp);
802
- // Flag end of operation
803
- running = false;
804
- return cancelAnimationFrame(frame);
805
- }
806
- }
807
- function invalidate(store, frames = 1) {
808
- const state = store?.get();
809
- if (!state)
810
- return roots.forEach((root) => invalidate(root, frames));
811
- if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never')
812
- return;
813
- // Increase frames, do not go higher than 60
814
- state.internal.frames = Math.min(60, state.internal.frames + frames);
815
- // If the render-loop isn't active, start it
816
- if (!running) {
817
- running = true;
818
- requestAnimationFrame(loop);
819
- }
820
- }
821
- function advance(timestamp, runGlobalEffects = true, store, frame) {
822
- if (runGlobalEffects)
823
- flushGlobalEffects('before', timestamp);
824
- const state = store?.get();
825
- if (!state)
826
- for (const root of roots.values())
827
- render(timestamp, root);
828
- else
829
- render(timestamp, store, frame);
830
- if (runGlobalEffects)
831
- flushGlobalEffects('after', timestamp);
832
- }
833
- return {
834
- loop,
835
- /**
836
- * Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state.
837
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate
838
- */
839
- invalidate,
840
- /**
841
- * Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`.
842
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance
843
- */
844
- advance,
845
- };
846
- }
847
- const [injectNgtLoop, , NGT_LOOP] = createInjectionToken(() => createLoop(roots));
848
-
849
- function safeDetectChanges(...cdrs) {
850
- cdrs.forEach((cdr) => {
851
- if (!cdr)
852
- return;
853
- try {
854
- // dynamic created component with ViewContainerRef#createComponent does not have Context
855
- // but it has _attachedToViewContainer
856
- if (cdr['_attachedToViewContainer'] || !!cdr['context']) {
857
- cdr.detectChanges();
858
- }
859
- }
860
- catch (e) {
861
- cdr.markForCheck();
862
- }
863
- });
864
- }
865
-
866
- function storeFactory(loop, document, injector, parent) {
867
- return runInInjectionContext(injector, () => {
868
- const window = document.defaultView;
869
- if (!window) {
870
- // TODO: revisit this when we need to support multiple platforms
871
- throw new Error(`[NGT] Window is not available.`);
872
- }
873
- const cdr = inject(ChangeDetectorRef);
874
- // NOTE: using Subject because we do not care about late-subscribers
875
- const pointerMissed$ = new Subject();
876
- const store = signalStore(({ get, set }) => {
877
- const { invalidate, advance } = loop;
878
- const position = new THREE.Vector3();
879
- const defaultTarget = new THREE.Vector3();
880
- const tempTarget = new THREE.Vector3();
881
- function getCurrentViewport(camera = get('camera'), target = defaultTarget, size = get('size')) {
882
- const { width, height, top, left } = size;
883
- const aspect = width / height;
884
- if (target instanceof THREE.Vector3)
885
- tempTarget.copy(target);
886
- else
887
- tempTarget.set(...target);
888
- const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
889
- if (is.orthographicCamera(camera)) {
890
- return {
891
- width: width / camera.zoom,
892
- height: height / camera.zoom,
893
- top,
894
- left,
895
- factor: 1,
896
- distance,
897
- aspect,
898
- };
899
- }
900
- else {
901
- const fov = (camera.fov * Math.PI) / 180; // convert vertical fov to radians
902
- const h = 2 * Math.tan(fov / 2) * distance; // visible height
903
- const w = h * (width / height);
904
- return { width: w, height: h, top, left, factor: width / w, distance, aspect };
905
- }
906
- }
907
- let performanceTimeout = undefined;
908
- const setPerformanceCurrent = (current) => set((state) => ({ performance: { ...state.performance, current } }));
909
- const pointer = new THREE.Vector2();
910
- return {
911
- pointerMissed$: pointerMissed$.asObservable(),
912
- events: { priority: 1, enabled: true, connected: false },
913
- invalidate: (frames = 1) => invalidate(store, frames),
914
- advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, store),
915
- legacy: false,
916
- linear: false,
917
- flat: false,
918
- controls: null,
919
- clock: new THREE.Clock(),
920
- pointer,
921
- frameloop: 'always',
922
- performance: {
923
- current: 1,
924
- min: 0.5,
925
- max: 1,
926
- debounce: 200,
927
- regress: () => {
928
- const state = get();
929
- // Clear timeout
930
- if (performanceTimeout)
931
- clearTimeout(performanceTimeout);
932
- // Set lower bound performance
933
- if (state.performance.current !== state.performance.min)
934
- setPerformanceCurrent(state.performance.min);
935
- // Go back to upper bound performance after a while unless something regresses meanwhile
936
- performanceTimeout = setTimeout(() => {
937
- setPerformanceCurrent(get('performance', 'max'));
938
- safeDetectChanges(cdr);
939
- }, state.performance.debounce);
940
- },
941
- },
942
- size: { width: 0, height: 0, top: 0, left: 0, updateStyle: false },
943
- viewport: {
944
- initialDpr: 0,
945
- dpr: 0,
946
- width: 0,
947
- height: 0,
948
- top: 0,
949
- left: 0,
950
- aspect: 0,
951
- distance: 0,
952
- factor: 0,
953
- getCurrentViewport,
954
- },
955
- setEvents: (events) => set((state) => ({ ...state, events: { ...state.events, ...events } })),
956
- setSize: (width, height, top, left) => {
957
- const camera = get('camera');
958
- const size = { width, height, top: top || 0, left: left || 0 };
959
- set((state) => ({
960
- size,
961
- viewport: { ...state.viewport, ...getCurrentViewport(camera, defaultTarget, size) },
962
- }));
963
- },
964
- setDpr: (dpr) => set((state) => {
965
- const resolved = makeDpr(dpr, window);
966
- return {
967
- viewport: {
968
- ...state.viewport,
969
- dpr: resolved,
970
- initialDpr: state.viewport.initialDpr || resolved,
971
- },
972
- };
973
- }),
974
- setFrameloop: (frameloop = 'always') => {
975
- const clock = get('clock');
976
- // if frameloop === "never" clock.elapsedTime is updated using advance(timestamp)
977
- clock.stop();
978
- clock.elapsedTime = 0;
979
- if (frameloop !== 'never') {
980
- clock.start();
981
- clock.elapsedTime = 0;
982
- }
983
- set(() => ({ frameloop }));
984
- },
985
- previousRoot: parent,
986
- internal: {
987
- active: false,
988
- priority: 0,
989
- frames: 0,
990
- lastEvent: new ElementRef(null),
991
- interaction: [],
992
- hovered: new Map(),
993
- subscribers: [],
994
- initialClick: [0, 0],
995
- initialHits: [],
996
- capturedMap: new Map(),
997
- subscribe: (callback, priority = 0, _store = store) => {
998
- const internal = get('internal');
999
- // If this subscription was given a priority, it takes rendering into its own hands
1000
- // For that reason we switch off automatic rendering and increase the manual flag
1001
- // As long as this flag is positive there can be no internal rendering at all
1002
- // because there could be multiple render subscriptions
1003
- internal.priority = internal.priority + (priority > 0 ? 1 : 0);
1004
- internal.subscribers.push({ callback, priority, store });
1005
- // Register subscriber and sort layers from lowest to highest, meaning,
1006
- // highest priority renders last (on top of the other frames)
1007
- internal.subscribers = internal.subscribers.sort((a, b) => (a.priority || 0) - (b.priority || 0));
1008
- return () => {
1009
- const internal = get('internal');
1010
- if (internal?.subscribers) {
1011
- // Decrease manual flag if this subscription had a priority
1012
- internal.priority = internal.priority - (priority > 0 ? 1 : 0);
1013
- // Remove subscriber from list
1014
- internal.subscribers = internal.subscribers.filter((s) => s.callback !== callback);
1015
- }
1016
- };
1017
- },
1018
- },
1019
- };
1020
- });
1021
- // NOTE: assign pointerMissed$ so we can use it in events
1022
- Object.defineProperty(store, 'pointerMissed$', { get: () => pointerMissed$ });
1023
- const state = store.get();
1024
- let oldSize = state.size;
1025
- let oldDpr = state.viewport.dpr;
1026
- let oldCamera = state.camera;
1027
- const _camera = store.select('camera');
1028
- const _size = store.select('size');
1029
- const _viewport = store.select('viewport');
1030
- effect(() => {
1031
- const [camera, size, viewport, gl] = [_camera(), _size(), _viewport(), store.get('gl')];
1032
- // Resize camera and renderer on changes to size and pixelratio
1033
- if (size !== oldSize || viewport.dpr !== oldDpr) {
1034
- oldSize = size;
1035
- oldDpr = viewport.dpr;
1036
- // Update camera & renderer
1037
- updateCamera(camera, size);
1038
- gl.setPixelRatio(viewport.dpr);
1039
- const updateStyle = typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement;
1040
- gl.setSize(size.width, size.height, updateStyle);
1041
- }
1042
- // Update viewport once the camera changes
1043
- if (camera !== oldCamera) {
1044
- oldCamera = camera;
1045
- updateCamera(camera, size);
1046
- // Update viewport
1047
- store.set((state) => ({
1048
- viewport: { ...state.viewport, ...state.viewport.getCurrentViewport(camera) },
1049
- }));
1050
- }
1051
- });
1052
- return store;
1053
- });
1054
- }
1055
- const NGT_STORE = new InjectionToken('NgtStore token');
1056
- const [injectNgtStore, provideNgtStore] = createInjectionToken(storeFactory, {
1057
- isRoot: false,
1058
- deps: [NGT_LOOP, DOCUMENT, Injector, [new Optional(), new SkipSelf(), NGT_STORE]],
1059
- token: NGT_STORE,
1060
- });
1061
-
1062
- function injectBeforeRender(cb, { priority = 0, injector } = {}) {
1063
- injector = assertInjectionContext(injectBeforeRender, injector);
1064
- return runInInjectionContext(injector, () => {
1065
- const store = injectNgtStore();
1066
- const sub = store.get('internal').subscribe(cb, priority, store);
1067
- inject(DestroyRef).onDestroy(() => void sub());
1068
- return sub;
1069
- });
1070
- }
1071
-
1072
- /**
1073
- * Release pointer captures.
1074
- * This is called by releasePointerCapture in the API, and when an object is removed.
1075
- */
1076
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
1077
- const captureData = captures.get(obj);
1078
- if (captureData) {
1079
- captures.delete(obj);
1080
- // If this was the last capturing object for this pointer
1081
- if (captures.size === 0) {
1082
- capturedMap.delete(pointerId);
1083
- captureData.target.releasePointerCapture(pointerId);
1084
- }
1085
- }
1086
- }
1087
- function removeInteractivity(store, object) {
1088
- const { internal } = store.get();
1089
- // Removes every trace of an object from the data store
1090
- internal.interaction = internal.interaction.filter((o) => o !== object);
1091
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
1092
- internal.hovered.forEach((value, key) => {
1093
- if (value.eventObject === object || value.object === object) {
1094
- // Clear out intersects, they are outdated by now
1095
- internal.hovered.delete(key);
1096
- }
1097
- });
1098
- internal.capturedMap.forEach((captures, pointerId) => {
1099
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
1100
- });
1101
- }
1102
- function createEvents(store) {
1103
- /** Calculates delta */
1104
- function calculateDistance(event) {
1105
- const internal = store.get('internal');
1106
- const dx = event.offsetX - internal.initialClick[0];
1107
- const dy = event.offsetY - internal.initialClick[1];
1108
- return Math.round(Math.sqrt(dx * dx + dy * dy));
1109
- }
1110
- /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
1111
- function filterPointerEvents(objects) {
1112
- return objects.filter((obj) => ['move', 'over', 'enter', 'out', 'leave'].some((name) => {
1113
- const eventName = `pointer${name}`;
1114
- return getLocalState(obj).handlers?.[eventName];
1115
- }));
1116
- }
1117
- function intersect(event, filter) {
1118
- const state = store.get();
1119
- const duplicates = new Set();
1120
- const intersections = [];
1121
- // Allow callers to eliminate event objects
1122
- const eventsObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
1123
- // Reset all raycaster cameras to undefined
1124
- for (let i = 0; i < eventsObjects.length; i++) {
1125
- const state = getLocalState(eventsObjects[i]).store.get();
1126
- if (state) {
1127
- state.raycaster.camera = undefined;
317
+ /**
318
+ * Release pointer captures.
319
+ * This is called by releasePointerCapture in the API, and when an object is removed.
320
+ */
321
+ function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
322
+ const captureData = captures.get(obj);
323
+ if (captureData) {
324
+ captures.delete(obj);
325
+ // If this was the last capturing object for this pointer
326
+ if (captures.size === 0) {
327
+ capturedMap.delete(pointerId);
328
+ captureData.target.releasePointerCapture(pointerId);
329
+ }
330
+ }
331
+ }
332
+ function removeInteractivity(store, object) {
333
+ const { internal } = store.snapshot;
334
+ // Removes every trace of an object from the data store
335
+ internal.interaction = internal.interaction.filter((o) => o !== object);
336
+ internal.initialHits = internal.initialHits.filter((o) => o !== object);
337
+ internal.hovered.forEach((value, key) => {
338
+ if (value.eventObject === object || value.object === object) {
339
+ // Clear out intersects, they are outdated by now
340
+ internal.hovered.delete(key);
341
+ }
342
+ });
343
+ internal.capturedMap.forEach((captures, pointerId) => {
344
+ releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
345
+ });
346
+ }
347
+ function createEvents(store) {
348
+ /** Calculates delta */
349
+ function calculateDistance(event) {
350
+ const internal = store.get('internal');
351
+ const dx = event.offsetX - internal.initialClick[0];
352
+ const dy = event.offsetY - internal.initialClick[1];
353
+ return Math.round(Math.sqrt(dx * dx + dy * dy));
354
+ }
355
+ /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
356
+ function filterPointerEvents(objects) {
357
+ return objects.filter((obj) => ['move', 'over', 'enter', 'out', 'leave'].some((name) => {
358
+ const eventName = `pointer${name}`;
359
+ return getLocalState(obj)?.handlers?.[eventName];
360
+ }));
361
+ }
362
+ function intersect(event, filter) {
363
+ const state = store.get();
364
+ const duplicates = new Set();
365
+ const intersections = [];
366
+ // Allow callers to eliminate event objects
367
+ const eventsObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
368
+ // Reset all raycaster cameras to undefined
369
+ for (let i = 0; i < eventsObjects.length; i++) {
370
+ const state = getLocalState(eventsObjects[i])?.store.snapshot;
371
+ if (state) {
372
+ state.raycaster.camera = undefined;
1128
373
  }
1129
374
  }
1130
375
  if (!state.previousRoot) {
@@ -1133,8 +378,8 @@ function createEvents(store) {
1133
378
  }
1134
379
  function handleRaycast(obj) {
1135
380
  const objLocalState = getLocalState(obj);
1136
- const objStore = objLocalState.store;
1137
- const objState = objStore?.get();
381
+ const objStore = objLocalState?.store;
382
+ const objState = objStore?.snapshot;
1138
383
  // Skip event handling when noEvents is set, or when the raycasters camera is null
1139
384
  if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
1140
385
  return [];
@@ -1154,8 +399,8 @@ function createEvents(store) {
1154
399
  .flatMap(handleRaycast)
1155
400
  // Sort by event priority and distance
1156
401
  .sort((a, b) => {
1157
- const aState = getLocalState(a.object).store.get();
1158
- const bState = getLocalState(b.object).store.get();
402
+ const aState = getLocalState(a.object)?.store?.snapshot;
403
+ const bState = getLocalState(b.object)?.store?.snapshot;
1159
404
  if (!aState || !bState)
1160
405
  return a.distance - b.distance;
1161
406
  return bState.events.priority - aState.events.priority || a.distance - b.distance;
@@ -1177,7 +422,7 @@ function createEvents(store) {
1177
422
  let eventObject = hit.object;
1178
423
  // bubble event up
1179
424
  while (eventObject) {
1180
- if (getLocalState(eventObject).eventCount) {
425
+ if (getLocalState(eventObject)?.eventCount) {
1181
426
  intersections.push({ ...hit, eventObject });
1182
427
  }
1183
428
  eventObject = eventObject.parent;
@@ -1185,7 +430,7 @@ function createEvents(store) {
1185
430
  }
1186
431
  // If the interaction is captured, make all capturing targets part of the intersect.
1187
432
  if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
1188
- for (let captureData of state.internal.capturedMap.get(event.pointerId).values()) {
433
+ for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
1189
434
  if (!duplicates.has(makeId(captureData.intersection)))
1190
435
  intersections.push(captureData.intersection);
1191
436
  }
@@ -1194,13 +439,12 @@ function createEvents(store) {
1194
439
  }
1195
440
  /** Handles intersections by forwarding them to handlers */
1196
441
  function handleIntersects(intersections, event, delta, callback) {
1197
- const rootState = store.get();
442
+ const rootState = store.snapshot;
1198
443
  // If anything has been found, forward it to the event listeners
1199
444
  if (intersections.length) {
1200
445
  const localState = { stopped: false };
1201
446
  for (const hit of intersections) {
1202
- const state = getLocalState(hit.object).store?.get() || rootState;
1203
- const { raycaster, pointer, camera, internal } = state;
447
+ const { raycaster, pointer, camera, internal } = getLocalState(hit.object)?.store?.snapshot || rootState;
1204
448
  const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
1205
449
  const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1206
450
  const setPointerCapture = (id) => {
@@ -1228,8 +472,8 @@ function createEvents(store) {
1228
472
  // Add native event props
1229
473
  const extractEventProps = {};
1230
474
  // 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.
1231
- for (let prop in event) {
1232
- let property = event[prop];
475
+ for (const prop in event) {
476
+ const property = event[prop];
1233
477
  // Only copy over atomics, leave functions alone as these should be
1234
478
  // called as event.nativeEvent.fn()
1235
479
  if (typeof property !== 'function')
@@ -1275,289 +519,897 @@ function createEvents(store) {
1275
519
  // Call subscribers
1276
520
  callback(raycastEvent);
1277
521
  // Event bubbling may be interrupted by stopPropagation
1278
- if (localState.stopped === true)
522
+ if (localState.stopped)
1279
523
  break;
1280
524
  }
1281
525
  }
1282
- return intersections;
1283
- }
1284
- function cancelPointer(intersections) {
1285
- const internal = store.get('internal');
1286
- for (const hoveredObj of internal.hovered.values()) {
1287
- // When no objects were hit or the the hovered object wasn't found underneath the cursor
1288
- // we call onPointerOut and delete the object from the hovered-elements map
1289
- if (!intersections.length ||
1290
- !intersections.find((hit) => hit.object === hoveredObj.object &&
1291
- hit.index === hoveredObj.index &&
1292
- hit.instanceId === hoveredObj.instanceId)) {
1293
- const eventObject = hoveredObj.eventObject;
1294
- const instance = getLocalState(eventObject);
1295
- const handlers = instance?.handlers;
1296
- internal.hovered.delete(makeId(hoveredObj));
1297
- if (instance?.eventCount) {
1298
- // Clear out intersects, they are outdated by now
1299
- const data = { ...hoveredObj, intersections };
1300
- handlers?.pointerout?.(data);
1301
- handlers?.pointerleave?.(data);
526
+ return intersections;
527
+ }
528
+ function cancelPointer(intersections) {
529
+ const internal = store.get('internal');
530
+ for (const hoveredObj of internal.hovered.values()) {
531
+ // When no objects were hit or the the hovered object wasn't found underneath the cursor
532
+ // we call onPointerOut and delete the object from the hovered-elements map
533
+ if (!intersections.length ||
534
+ !intersections.find((hit) => hit.object === hoveredObj.object &&
535
+ hit.index === hoveredObj.index &&
536
+ hit.instanceId === hoveredObj.instanceId)) {
537
+ const eventObject = hoveredObj.eventObject;
538
+ const instance = getLocalState(eventObject);
539
+ const handlers = instance?.handlers;
540
+ internal.hovered.delete(makeId(hoveredObj));
541
+ if (instance?.eventCount) {
542
+ // Clear out intersects, they are outdated by now
543
+ const data = { ...hoveredObj, intersections };
544
+ handlers?.pointerout?.(data);
545
+ handlers?.pointerleave?.(data);
546
+ }
547
+ }
548
+ }
549
+ }
550
+ function pointerMissed(event, objects) {
551
+ for (let i = 0; i < objects.length; i++) {
552
+ const instance = getLocalState(objects[i]);
553
+ instance?.handlers.pointermissed?.(event);
554
+ }
555
+ }
556
+ function handlePointer(name) {
557
+ // Deal with cancelation
558
+ switch (name) {
559
+ case 'pointerleave':
560
+ case 'pointercancel':
561
+ return () => cancelPointer([]);
562
+ case 'lostpointercapture':
563
+ return (event) => {
564
+ const { internal } = store.snapshot;
565
+ if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
566
+ // If the object event interface had lostpointercapture, we'd call it here on every
567
+ // object that's getting removed. We call it on the next frame because lostpointercapture
568
+ // fires before pointerup. Otherwise pointerUp would never be called if the event didn't
569
+ // happen in the object it originated from, leaving components in a in-between state.
570
+ requestAnimationFrame(() => {
571
+ // Only release if pointer-up didn't do it already
572
+ if (internal.capturedMap.has(event.pointerId)) {
573
+ internal.capturedMap.delete(event.pointerId);
574
+ cancelPointer([]);
575
+ }
576
+ });
577
+ }
578
+ };
579
+ }
580
+ // Any other pointer goes here ...
581
+ return function handleEvent(event) {
582
+ const pointerMissed$ = store['pointerMissed$'];
583
+ const internal = store.get('internal');
584
+ // prepareRay(event)
585
+ internal.lastEvent.nativeElement = event;
586
+ // Get fresh intersects
587
+ const isPointerMove = name === 'pointermove';
588
+ const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick';
589
+ const filter = isPointerMove ? filterPointerEvents : undefined;
590
+ const hits = intersect(event, filter);
591
+ const delta = isClickEvent ? calculateDistance(event) : 0;
592
+ // Save initial coordinates on pointer-down
593
+ if (name === 'pointerdown') {
594
+ internal.initialClick = [event.offsetX, event.offsetY];
595
+ internal.initialHits = hits.map((hit) => hit.eventObject);
596
+ }
597
+ // If a click yields no results, pass it back to the user as a miss
598
+ // Missed events have to come first in order to establish user-land side-effect clean up
599
+ if (isClickEvent && !hits.length) {
600
+ if (delta <= 2) {
601
+ pointerMissed(event, internal.interaction);
602
+ pointerMissed$.next(event);
603
+ }
604
+ }
605
+ // Take care of unhover
606
+ if (isPointerMove)
607
+ cancelPointer(hits);
608
+ function onIntersect(data) {
609
+ const eventObject = data.eventObject;
610
+ const instance = getLocalState(eventObject);
611
+ const handlers = instance?.handlers;
612
+ // Check presence of handlers
613
+ if (!instance?.eventCount)
614
+ return;
615
+ /*
616
+ MAYBE TODO, DELETE IF NOT:
617
+ Check if the object is captured, captured events should not have intersects running in parallel
618
+ But wouldn't it be better to just replace capturedMap with a single entry?
619
+ Also, are we OK with straight up making picking up multiple objects impossible?
620
+
621
+ const pointerId = (data as ThreeEvent<PointerEvent>).pointerId
622
+ if (pointerId !== undefined) {
623
+ const capturedMeshSet = internal.capturedMap.get(pointerId)
624
+ if (capturedMeshSet) {
625
+ const captured = capturedMeshSet.get(eventObject)
626
+ if (captured && captured.localState.stopped) return
627
+ }
628
+ }*/
629
+ if (isPointerMove) {
630
+ // Move event ...
631
+ if (handlers?.pointerover || handlers?.pointerenter || handlers?.pointerout || handlers?.pointerleave) {
632
+ // When enter or out is present take care of hover-state
633
+ const id = makeId(data);
634
+ const hoveredItem = internal.hovered.get(id);
635
+ if (!hoveredItem) {
636
+ // If the object wasn't previously hovered, book it and call its handler
637
+ internal.hovered.set(id, data);
638
+ handlers.pointerover?.(data);
639
+ handlers.pointerenter?.(data);
640
+ }
641
+ else if (hoveredItem.stopped) {
642
+ // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
643
+ data.stopPropagation();
644
+ }
645
+ }
646
+ // Call mouse move
647
+ handlers?.pointermove?.(data);
648
+ }
649
+ else {
650
+ // All other events ...
651
+ const handler = handlers?.[name];
652
+ if (handler) {
653
+ // Forward all events back to their respective handlers with the exception of click events,
654
+ // which must use the initial target
655
+ if (!isClickEvent || internal.initialHits.includes(eventObject)) {
656
+ // Missed events have to come first
657
+ pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
658
+ // Now call the handler
659
+ handler(data);
660
+ }
661
+ }
662
+ else {
663
+ // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
664
+ if (isClickEvent && internal.initialHits.includes(eventObject)) {
665
+ pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
666
+ }
667
+ }
668
+ }
669
+ }
670
+ handleIntersects(hits, event, delta, onIntersect);
671
+ };
672
+ }
673
+ return { handlePointer };
674
+ }
675
+
676
+ const DOM_EVENTS = {
677
+ click: false,
678
+ contextmenu: false,
679
+ dblclick: false,
680
+ wheel: false, // passive wheel errors with OrbitControls
681
+ pointerdown: true,
682
+ pointerup: true,
683
+ pointerleave: true,
684
+ pointermove: true,
685
+ pointercancel: true,
686
+ lostpointercapture: true,
687
+ };
688
+ const supportedEvents = [
689
+ 'click',
690
+ 'contextmenu',
691
+ 'dblclick',
692
+ 'pointerup',
693
+ 'pointerdown',
694
+ 'pointerover',
695
+ 'pointerout',
696
+ 'pointerenter',
697
+ 'pointerleave',
698
+ 'pointermove',
699
+ 'pointermissed',
700
+ 'pointercancel',
701
+ 'wheel',
702
+ ];
703
+ function createPointerEvents(store) {
704
+ const { handlePointer } = createEvents(store);
705
+ return {
706
+ priority: 1,
707
+ enabled: true,
708
+ compute: (event, root) => {
709
+ const state = root.get();
710
+ // https://github.com/pmndrs/react-three-fiber/pull/782
711
+ // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides
712
+ state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
713
+ state.raycaster.setFromCamera(state.pointer, state.camera);
714
+ },
715
+ connected: undefined,
716
+ handlers: Object.keys(DOM_EVENTS).reduce((handlers, supportedEventName) => {
717
+ handlers[supportedEventName] = handlePointer(supportedEventName);
718
+ return handlers;
719
+ }, {}),
720
+ update: () => {
721
+ const { events, internal } = store.get();
722
+ if (internal.lastEvent?.nativeElement && events.handlers)
723
+ events.handlers.pointermove(internal.lastEvent.nativeElement);
724
+ },
725
+ connect: (target) => {
726
+ const state = store.get();
727
+ state.events.disconnect?.();
728
+ state.setEvents({ connected: target });
729
+ Object.entries(state.events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
730
+ const passive = DOM_EVENTS[eventName];
731
+ target.addEventListener(eventName, eventHandler, { passive });
732
+ });
733
+ },
734
+ disconnect: () => {
735
+ const { events, setEvents } = store.get();
736
+ if (events.connected) {
737
+ Object.entries(events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
738
+ if (events.connected instanceof HTMLElement) {
739
+ events.connected.removeEventListener(eventName, eventHandler);
740
+ }
741
+ });
742
+ setEvents({ connected: undefined });
743
+ }
744
+ },
745
+ };
746
+ }
747
+
748
+ // This function prepares a set of changes to be applied to the instance
749
+ function diffProps(instance, props) {
750
+ const propsEntries = Object.entries(props);
751
+ const changes = [];
752
+ for (const [propKey, propValue] of propsEntries) {
753
+ let key = propKey;
754
+ if (is.colorSpaceExist(instance)) {
755
+ if (propKey === 'encoding') {
756
+ key = 'colorSpace';
757
+ }
758
+ else if (propKey === 'outputEncoding') {
759
+ key = 'outputColorSpace';
760
+ }
761
+ }
762
+ if (is.equ(propValue, instance[key]))
763
+ continue;
764
+ changes.push([propKey, propValue]);
765
+ }
766
+ return changes;
767
+ }
768
+ // This function applies a set of changes to the instance
769
+ function applyProps(instance, props) {
770
+ // if props is empty
771
+ if (!Object.keys(props).length)
772
+ return instance;
773
+ // filter equals, and reserved props
774
+ const localState = getLocalState(instance);
775
+ const rootState = localState?.store?.snapshot ?? {};
776
+ const changes = diffProps(instance, props);
777
+ for (let i = 0; i < changes.length; i++) {
778
+ let [key, value] = changes[i];
779
+ // Alias (output)encoding => (output)colorSpace (since r152)
780
+ // https://github.com/pmndrs/react-three-fiber/pull/2829
781
+ if (is.colorSpaceExist(instance)) {
782
+ const sRGBEncoding = 3001;
783
+ const SRGBColorSpace = 'srgb';
784
+ const LinearSRGBColorSpace = 'srgb-linear';
785
+ if (key === 'encoding') {
786
+ key = 'colorSpace';
787
+ value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
788
+ }
789
+ else if (key === 'outputEncoding') {
790
+ key = 'outputColorSpace';
791
+ value = value === sRGBEncoding ? SRGBColorSpace : LinearSRGBColorSpace;
792
+ }
793
+ }
794
+ const currentInstance = instance;
795
+ const targetProp = currentInstance[key];
796
+ // special treatmen for objects with support for set/copy, and layers
797
+ if (targetProp && targetProp['set'] && (targetProp['copy'] || targetProp instanceof THREE.Layers)) {
798
+ const isColor = targetProp instanceof THREE.Color;
799
+ // if value is an array
800
+ if (Array.isArray(value)) {
801
+ if (targetProp['fromArray'])
802
+ targetProp['fromArray'](value);
803
+ else
804
+ targetProp['set'](...value);
805
+ } // test again target.copy
806
+ else if (targetProp['copy'] &&
807
+ value &&
808
+ value.constructor &&
809
+ targetProp.constructor.name === value.constructor.name) {
810
+ targetProp['copy'](value);
811
+ if (!THREE.ColorManagement && !rootState.linear && isColor)
812
+ targetProp['convertSRGBToLinear']();
813
+ } // if nothing else fits, just set the single value, ignore undefined
814
+ else if (value !== undefined) {
815
+ const isColor = targetProp instanceof THREE.Color;
816
+ // allow setting array scalars
817
+ if (!isColor && targetProp['setScalar'])
818
+ targetProp['setScalar'](value);
819
+ // layers have no copy function, copy the mask
820
+ else if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers)
821
+ targetProp.mask = value.mask;
822
+ // otherwise just set ...
823
+ else
824
+ targetProp['set'](value);
825
+ // auto-convert srgb
826
+ if (!THREE.ColorManagement && !rootState?.linear && isColor)
827
+ targetProp.convertSRGBToLinear();
828
+ }
829
+ } // else just overwrite the value
830
+ else {
831
+ currentInstance[key] = value;
832
+ // auto-convert srgb textures
833
+ if (currentInstance[key] instanceof THREE.Texture &&
834
+ currentInstance[key].format === THREE.RGBAFormat &&
835
+ currentInstance[key].type === THREE.UnsignedByteType) {
836
+ const texture = currentInstance[key];
837
+ if (rootState?.gl) {
838
+ if (is.colorSpaceExist(texture) && is.colorSpaceExist(rootState.gl))
839
+ texture.colorSpace = rootState.gl.outputColorSpace;
840
+ else
841
+ texture.encoding = rootState.gl.outputEncoding;
1302
842
  }
1303
843
  }
1304
844
  }
845
+ checkUpdate(currentInstance[key]);
846
+ checkUpdate(targetProp);
847
+ invalidateInstance(instance);
1305
848
  }
1306
- function pointerMissed(event, objects) {
1307
- for (let i = 0; i < objects.length; i++) {
1308
- const instance = getLocalState(objects[i]);
1309
- instance?.handlers.pointermissed?.(event);
1310
- }
849
+ const instanceHandlersCount = localState?.eventCount;
850
+ const parent = localState?.instanceStore?.get('parent');
851
+ if (parent && rootState.internal && instance['raycast'] && instanceHandlersCount !== localState?.eventCount) {
852
+ // Pre-emptively remove the instance from the interaction manager
853
+ const index = rootState.internal.interaction.indexOf(instance);
854
+ if (index > -1)
855
+ rootState.internal.interaction.splice(index, 1);
856
+ // Add the instance to the interaction manager only when it has handlers
857
+ if (localState?.eventCount)
858
+ rootState.internal.interaction.push(instance);
1311
859
  }
1312
- function handlePointer(name) {
1313
- // Deal with cancelation
1314
- switch (name) {
1315
- case 'pointerleave':
1316
- case 'pointercancel':
1317
- return () => cancelPointer([]);
1318
- case 'lostpointercapture':
1319
- return (event) => {
1320
- const { internal } = store.get();
1321
- if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
1322
- // If the object event interface had onLostPointerCapture, we'd call it here on every
1323
- // object that's getting removed.
1324
- internal.capturedMap.delete(event.pointerId);
1325
- cancelPointer([]);
1326
- }
1327
- };
1328
- }
1329
- // Any other pointer goes here ...
1330
- return function handleEvent(event) {
1331
- const pointerMissed$ = store['pointerMissed$'];
1332
- const internal = store.get('internal');
1333
- // prepareRay(event)
1334
- internal.lastEvent.nativeElement = event;
1335
- // Get fresh intersects
1336
- const isPointerMove = name === 'pointermove';
1337
- const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick';
1338
- const filter = isPointerMove ? filterPointerEvents : undefined;
1339
- const hits = intersect(event, filter);
1340
- const delta = isClickEvent ? calculateDistance(event) : 0;
1341
- // Save initial coordinates on pointer-down
1342
- if (name === 'pointerdown') {
1343
- internal.initialClick = [event.offsetX, event.offsetY];
1344
- internal.initialHits = hits.map((hit) => hit.eventObject);
860
+ if (parent && localState?.afterUpdate && localState.afterUpdate.observed && changes.length) {
861
+ localState.afterUpdate.emit(instance);
862
+ }
863
+ return instance;
864
+ }
865
+
866
+ const shallowLoose = { objects: 'shallow', strict: false };
867
+ const roots = new Map();
868
+ function injectCanvasRootInitializer(injector) {
869
+ return assertInjector(injectCanvasRootInitializer, injector, () => {
870
+ const injectedStore = injectNgtStore();
871
+ const loop = injectNgtLoop();
872
+ return (canvas) => {
873
+ const exist = roots.has(canvas);
874
+ let store = roots.get(canvas);
875
+ if (store) {
876
+ console.warn('[NGT] Same canvas root is being created twice');
1345
877
  }
1346
- // If a click yields no results, pass it back to the user as a miss
1347
- // Missed events have to come first in order to establish user-land side-effect clean up
1348
- if (isClickEvent && !hits.length) {
1349
- if (delta <= 2) {
1350
- pointerMissed(event, internal.interaction);
1351
- pointerMissed$.next(event);
1352
- }
878
+ store ||= injectedStore;
879
+ if (!store) {
880
+ throw new Error('[NGT] No store initialized');
1353
881
  }
1354
- // Take care of unhover
1355
- if (isPointerMove)
1356
- cancelPointer(hits);
1357
- function onIntersect(data) {
1358
- const eventObject = data.eventObject;
1359
- const instance = getLocalState(eventObject);
1360
- const handlers = instance?.handlers;
1361
- // Check presence of handlers
1362
- if (!instance?.eventCount)
1363
- return;
1364
- /*
1365
- MAYBE TODO, DELETE IF NOT:
1366
- Check if the object is captured, captured events should not have intersects running in parallel
1367
- But wouldn't it be better to just replace capturedMap with a single entry?
1368
- Also, are we OK with straight up making picking up multiple objects impossible?
1369
-
1370
- const pointerId = (data as ThreeEvent<PointerEvent>).pointerId
1371
- if (pointerId !== undefined) {
1372
- const capturedMeshSet = internal.capturedMap.get(pointerId)
1373
- if (capturedMeshSet) {
1374
- const captured = capturedMeshSet.get(eventObject)
1375
- if (captured && captured.localState.stopped) return
1376
- }
1377
- }*/
1378
- if (isPointerMove) {
1379
- // Move event ...
1380
- if (handlers?.pointerover ||
1381
- handlers?.pointerenter ||
1382
- handlers?.pointerout ||
1383
- handlers?.pointerleave) {
1384
- // When enter or out is present take care of hover-state
1385
- const id = makeId(data);
1386
- const hoveredItem = internal.hovered.get(id);
1387
- if (!hoveredItem) {
1388
- // If the object wasn't previously hovered, book it and call its handler
1389
- internal.hovered.set(id, data);
1390
- handlers.pointerover?.(data);
1391
- handlers.pointerenter?.(data);
882
+ if (!exist) {
883
+ roots.set(canvas, store);
884
+ }
885
+ let isConfigured = false;
886
+ let lastCamera;
887
+ return {
888
+ isConfigured,
889
+ destroy: (timeout = 500) => {
890
+ const root = roots.get(canvas);
891
+ if (root) {
892
+ root.update((state) => ({ internal: { ...state.internal, active: false } }));
893
+ setTimeout(() => {
894
+ try {
895
+ const state = root.get();
896
+ state.events.disconnect?.();
897
+ state.gl?.renderLists?.dispose?.();
898
+ state.gl?.forceContextLoss?.();
899
+ if (state.gl?.xr)
900
+ state.xr.disconnect();
901
+ dispose(state);
902
+ roots.delete(canvas);
903
+ }
904
+ catch (e) {
905
+ console.error('[NGT] Unexpected error while destroying Canvas Root', e);
906
+ }
907
+ }, timeout);
908
+ }
909
+ },
910
+ configure: (inputs) => {
911
+ const { shadows = false, linear = false, flat = false, legacy = false, orthographic = false, frameloop = 'always', dpr = [1, 2], gl: glOptions, size: sizeOptions, camera: cameraOptions, raycaster: raycasterOptions, scene: sceneOptions, events, lookAt, performance, } = inputs;
912
+ const state = store.snapshot;
913
+ const stateToUpdate = {};
914
+ // setup renderer
915
+ let gl = state.gl;
916
+ if (!state.gl)
917
+ stateToUpdate.gl = gl = makeRendererInstance(glOptions, canvas);
918
+ // setup raycaster
919
+ let raycaster = state.raycaster;
920
+ if (!raycaster)
921
+ stateToUpdate.raycaster = raycaster = new THREE.Raycaster();
922
+ // set raycaster options
923
+ const { params, ...options } = raycasterOptions || {};
924
+ if (!is.equ(options, raycaster, shallowLoose))
925
+ applyProps(raycaster, options);
926
+ if (!is.equ(params, raycaster.params, shallowLoose)) {
927
+ applyProps(raycaster, { params: { ...raycaster.params, ...(params || {}) } });
928
+ }
929
+ // Create default camera, don't overwrite any user-set state
930
+ if (!state.camera || (state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose))) {
931
+ lastCamera = cameraOptions;
932
+ const isCamera = is.camera(cameraOptions);
933
+ let camera = isCamera ? cameraOptions : makeCameraInstance(orthographic, state.size);
934
+ if (!isCamera) {
935
+ camera.position.z = 5;
936
+ if (cameraOptions)
937
+ applyProps(camera, cameraOptions);
938
+ // always look at center or passed-in lookAt by default
939
+ if (!state.camera && !cameraOptions?.rotation && !cameraOptions?.quaternion) {
940
+ if (Array.isArray(lookAt))
941
+ camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
942
+ else if (lookAt instanceof THREE.Vector3)
943
+ camera.lookAt(lookAt);
944
+ else
945
+ camera.lookAt(0, 0, 0);
946
+ }
947
+ // update projection matrix after applyprops
948
+ camera.updateProjectionMatrix?.();
949
+ }
950
+ if (!is.instance(camera))
951
+ camera = prepare(camera, { store });
952
+ stateToUpdate.camera = camera;
953
+ // Configure raycaster
954
+ // https://github.com/pmndrs/react-xr/issues/300
955
+ raycaster.camera = camera;
956
+ }
957
+ // Set up scene (one time only!)
958
+ if (!state.scene) {
959
+ let scene;
960
+ if (sceneOptions instanceof THREE.Scene) {
961
+ scene = sceneOptions;
962
+ }
963
+ else {
964
+ scene = new THREE.Scene();
965
+ if (sceneOptions)
966
+ applyProps(scene, sceneOptions);
967
+ }
968
+ stateToUpdate.scene = prepare(scene, { store });
969
+ }
970
+ // Set up XR (one time only!)
971
+ if (!state.xr) {
972
+ // Handle frame behavior in WebXR
973
+ const handleXRFrame = (timestamp, frame) => {
974
+ const state = store.snapshot;
975
+ if (state.frameloop === 'never')
976
+ return;
977
+ loop.advance(timestamp, true, store, frame);
978
+ };
979
+ // Toggle render switching on session
980
+ const handleSessionChange = () => {
981
+ const state = store.snapshot;
982
+ state.gl.xr.enabled = state.gl.xr.isPresenting;
983
+ state.gl.xr.setAnimationLoop(state.gl.xr.isPresenting ? handleXRFrame : null);
984
+ if (!state.gl.xr.isPresenting)
985
+ loop.invalidate(store);
986
+ };
987
+ // WebXR session manager
988
+ const xr = {
989
+ connect: () => {
990
+ gl.xr.addEventListener('sessionstart', handleSessionChange);
991
+ gl.xr.addEventListener('sessionend', handleSessionChange);
992
+ },
993
+ disconnect: () => {
994
+ gl.xr.removeEventListener('sessionstart', handleSessionChange);
995
+ gl.xr.removeEventListener('sessionend', handleSessionChange);
996
+ },
997
+ };
998
+ // Subscribe to WebXR session events
999
+ if (gl.xr && typeof gl.xr.addEventListener === 'function')
1000
+ xr.connect();
1001
+ stateToUpdate.xr = xr;
1002
+ }
1003
+ // Set shadowmap
1004
+ if (gl.shadowMap) {
1005
+ const oldEnabled = gl.shadowMap.enabled;
1006
+ const oldType = gl.shadowMap.type;
1007
+ gl.shadowMap.enabled = !!shadows;
1008
+ if (typeof shadows === 'boolean') {
1009
+ gl.shadowMap.type = THREE.PCFSoftShadowMap;
1392
1010
  }
1393
- else if (hoveredItem.stopped) {
1394
- // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
1395
- data.stopPropagation();
1011
+ else if (typeof shadows === 'string') {
1012
+ const types = {
1013
+ basic: THREE.BasicShadowMap,
1014
+ percentage: THREE.PCFShadowMap,
1015
+ soft: THREE.PCFSoftShadowMap,
1016
+ variance: THREE.VSMShadowMap,
1017
+ };
1018
+ gl.shadowMap.type = types[shadows] ?? THREE.PCFSoftShadowMap;
1396
1019
  }
1397
- }
1398
- // Call mouse move
1399
- handlers?.pointermove?.(data);
1400
- }
1401
- else {
1402
- // All other events ...
1403
- const handler = handlers?.[name];
1404
- if (handler) {
1405
- // Forward all events back to their respective handlers with the exception of click events,
1406
- // which must use the initial target
1407
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1408
- // Missed events have to come first
1409
- pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1410
- // Now call the handler
1411
- handler(data);
1020
+ else if (is.obj(shadows)) {
1021
+ Object.assign(gl.shadowMap, shadows);
1412
1022
  }
1023
+ if (oldEnabled !== gl.shadowMap.enabled || oldType !== gl.shadowMap.type)
1024
+ checkNeedsUpdate(gl.shadowMap);
1413
1025
  }
1414
- else {
1415
- // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
1416
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1417
- pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1418
- }
1026
+ // Safely set color management if available.
1027
+ // Avoid accessing THREE.ColorManagement to play nice with older versions
1028
+ if (THREE.ColorManagement) {
1029
+ const ColorManagement = THREE.ColorManagement;
1030
+ if ('enabled' in ColorManagement)
1031
+ ColorManagement['enabled'] = !legacy ?? false;
1032
+ else if ('legacyMode' in ColorManagement)
1033
+ ColorManagement['legacyMode'] = legacy ?? true;
1419
1034
  }
1420
- }
1421
- }
1422
- handleIntersects(hits, event, delta, onIntersect);
1035
+ if (!isConfigured) {
1036
+ // set color space and tonemapping preferences once
1037
+ const LinearEncoding = 3000;
1038
+ const sRGBEncoding = 3001;
1039
+ applyProps(gl, {
1040
+ outputEncoding: linear ? LinearEncoding : sRGBEncoding,
1041
+ toneMapping: flat ? THREE.NoToneMapping : THREE.ACESFilmicToneMapping,
1042
+ });
1043
+ }
1044
+ // Update color management state
1045
+ if (state.legacy !== legacy)
1046
+ stateToUpdate.legacy = legacy;
1047
+ if (state.linear !== linear)
1048
+ stateToUpdate.linear = linear;
1049
+ if (state.flat !== flat)
1050
+ stateToUpdate.flat = flat;
1051
+ // Set gl props
1052
+ gl.setClearAlpha(0);
1053
+ gl.setPixelRatio(makeDpr(state.viewport.dpr));
1054
+ gl.setSize(state.size.width, state.size.height);
1055
+ if (is.obj(glOptions) &&
1056
+ !(typeof glOptions === 'function') &&
1057
+ !is.renderer(glOptions) &&
1058
+ !is.equ(glOptions, gl, shallowLoose)) {
1059
+ applyProps(gl, glOptions);
1060
+ }
1061
+ // Store events internally
1062
+ if (events && !state.events.handlers)
1063
+ stateToUpdate.events = events(store);
1064
+ // Check performance
1065
+ if (performance && !is.equ(performance, state.performance, shallowLoose)) {
1066
+ stateToUpdate.performance = { ...state.performance, ...performance };
1067
+ }
1068
+ store.update(stateToUpdate);
1069
+ // Check size, allow it to take on container bounds initially
1070
+ const size = computeInitialSize(canvas, sizeOptions);
1071
+ if (!is.equ(size, state.size, shallowLoose)) {
1072
+ state.setSize(size.width, size.height, size.top, size.left);
1073
+ }
1074
+ // Check pixelratio
1075
+ if (dpr && state.viewport.dpr !== makeDpr(dpr))
1076
+ state.setDpr(dpr);
1077
+ // Check frameloop
1078
+ if (state.frameloop !== frameloop)
1079
+ state.setFrameloop(frameloop);
1080
+ isConfigured = true;
1081
+ },
1082
+ };
1423
1083
  };
1084
+ });
1085
+ }
1086
+ function computeInitialSize(canvas, defaultSize) {
1087
+ if (defaultSize) {
1088
+ return defaultSize;
1089
+ }
1090
+ if (typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement && canvas.parentElement) {
1091
+ return canvas.parentElement.getBoundingClientRect();
1092
+ }
1093
+ if (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) {
1094
+ return { width: canvas.width, height: canvas.height, top: 0, left: 0 };
1095
+ }
1096
+ return { width: 0, height: 0, top: 0, left: 0 };
1097
+ }
1098
+ // Disposes an object and all its properties
1099
+ function dispose(obj) {
1100
+ if (obj.dispose && obj.type !== 'Scene')
1101
+ obj.dispose();
1102
+ for (const p in obj) {
1103
+ p.dispose?.();
1104
+ delete obj[p];
1424
1105
  }
1425
- return { handlePointer };
1426
1106
  }
1427
1107
 
1428
- const DOM_EVENTS = {
1429
- click: false,
1430
- contextmenu: false,
1431
- dblclick: false,
1432
- wheel: false,
1433
- pointerdown: true,
1434
- pointerup: true,
1435
- pointerleave: true,
1436
- pointermove: true,
1437
- pointercancel: true,
1438
- lostpointercapture: true,
1439
- };
1440
- const supportedEvents = [
1441
- 'click',
1442
- 'contextmenu',
1443
- 'dblclick',
1444
- 'pointerup',
1445
- 'pointerdown',
1446
- 'pointerover',
1447
- 'pointerout',
1448
- 'pointerenter',
1449
- 'pointerleave',
1450
- 'pointermove',
1451
- 'pointermissed',
1452
- 'pointercancel',
1453
- 'wheel',
1454
- ];
1455
- function createPointerEvents(store) {
1456
- const { handlePointer } = createEvents(store);
1457
- return {
1458
- priority: 1,
1459
- enabled: true,
1460
- compute: (event, root) => {
1461
- const state = root.get();
1462
- // https://github.com/pmndrs/react-three-fiber/pull/782
1463
- // Events trigger outside of canvas when moved, use offsetX/Y by default and allow overrides
1464
- state.pointer.set((event.offsetX / state.size.width) * 2 - 1, -(event.offsetY / state.size.height) * 2 + 1);
1465
- state.raycaster.setFromCamera(state.pointer, state.camera);
1466
- },
1467
- connected: undefined,
1468
- handlers: Object.keys(DOM_EVENTS).reduce((handlers, supportedEventName) => {
1469
- handlers[supportedEventName] = handlePointer(supportedEventName);
1470
- return handlers;
1471
- }, {}),
1472
- update: () => {
1473
- const { events, internal } = store.get();
1474
- if (internal.lastEvent?.nativeElement && events.handlers)
1475
- events.handlers.pointermove(internal.lastEvent.nativeElement);
1476
- },
1477
- connect: (target) => {
1478
- const state = store.get();
1479
- state.events.disconnect?.();
1480
- state.setEvents({ connected: target });
1481
- Object.entries(state.events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
1482
- const passive = DOM_EVENTS[eventName];
1483
- target.addEventListener(eventName, eventHandler, { passive });
1484
- });
1485
- },
1486
- disconnect: () => {
1487
- const { events, setEvents } = store.get();
1488
- if (events.connected) {
1489
- Object.entries(events.handlers ?? {}).forEach(([eventName, eventHandler]) => {
1490
- if (events.connected instanceof HTMLElement) {
1491
- events.connected.removeEventListener(eventName, eventHandler);
1492
- }
1493
- });
1494
- setEvents({ connected: undefined });
1108
+ function createSubs(callback, subs) {
1109
+ const sub = { callback };
1110
+ subs.add(sub);
1111
+ return () => void subs.delete(sub);
1112
+ }
1113
+ const globalEffects = new Set();
1114
+ const globalAfterEffects = new Set();
1115
+ const globalTailEffects = new Set();
1116
+ /**
1117
+ * Adds a global render callback which is called each frame.
1118
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addEffect
1119
+ */
1120
+ const addEffect = (callback) => createSubs(callback, globalEffects);
1121
+ /**
1122
+ * Adds a global after-render callback which is called each frame.
1123
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addAfterEffect
1124
+ */
1125
+ const addAfterEffect = (callback) => createSubs(callback, globalAfterEffects);
1126
+ /**
1127
+ * Adds a global callback which is called when rendering stops.
1128
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#addTail
1129
+ */
1130
+ const addTail = (callback) => createSubs(callback, globalTailEffects);
1131
+ function run(effects, timestamp) {
1132
+ if (!effects.size)
1133
+ return;
1134
+ for (const { callback } of effects.values()) {
1135
+ callback(timestamp);
1136
+ }
1137
+ }
1138
+ function flushGlobalEffects(type, timestamp) {
1139
+ switch (type) {
1140
+ case 'before':
1141
+ return run(globalEffects, timestamp);
1142
+ case 'after':
1143
+ return run(globalAfterEffects, timestamp);
1144
+ case 'tail':
1145
+ return run(globalTailEffects, timestamp);
1146
+ }
1147
+ }
1148
+ function render(timestamp, store, frame) {
1149
+ const state = store.snapshot;
1150
+ // Run local effects
1151
+ let delta = state.clock.getDelta();
1152
+ // In frameloop='never' mode, clock times are updated using the provided timestamp
1153
+ if (state.frameloop === 'never' && typeof timestamp === 'number') {
1154
+ delta = timestamp - state.clock.elapsedTime;
1155
+ state.clock.oldTime = state.clock.elapsedTime;
1156
+ state.clock.elapsedTime = timestamp;
1157
+ }
1158
+ // Call subscribers (beforeRender)
1159
+ const subscribers = state.internal.subscribers;
1160
+ for (let i = 0; i < subscribers.length; i++) {
1161
+ const subscription = subscribers[i];
1162
+ subscription.callback({ ...subscription.store.snapshot, delta, frame });
1163
+ }
1164
+ // Render content
1165
+ if (!state.internal.priority && state.gl.render)
1166
+ state.gl.render(state.scene, state.camera);
1167
+ // Decrease frame count
1168
+ state.internal.frames = Math.max(0, state.internal.frames - 1);
1169
+ return state.frameloop === 'always' ? 1 : state.internal.frames;
1170
+ }
1171
+ function createLoop(roots) {
1172
+ let running = false;
1173
+ let repeat;
1174
+ let frame;
1175
+ function loop(timestamp) {
1176
+ frame = requestAnimationFrame(loop);
1177
+ running = true;
1178
+ repeat = 0;
1179
+ // Run effects
1180
+ flushGlobalEffects('before', timestamp);
1181
+ // Render all roots
1182
+ for (const root of roots.values()) {
1183
+ const state = root.snapshot;
1184
+ // If the frameloop is invalidated, do not run another frame
1185
+ if (state.internal.active &&
1186
+ (state.frameloop === 'always' || state.internal.frames > 0) &&
1187
+ !state.gl.xr?.isPresenting) {
1188
+ repeat += render(timestamp, root);
1495
1189
  }
1496
- },
1190
+ }
1191
+ // Run after-effects
1192
+ flushGlobalEffects('after', timestamp);
1193
+ // Stop the loop if nothing invalidates it
1194
+ if (repeat === 0) {
1195
+ // Tail call effects, they are called when rendering stops
1196
+ flushGlobalEffects('tail', timestamp);
1197
+ // Flag end of operation
1198
+ running = false;
1199
+ return cancelAnimationFrame(frame);
1200
+ }
1201
+ }
1202
+ function invalidate(store, frames = 1) {
1203
+ const state = store?.snapshot;
1204
+ if (!state)
1205
+ return roots.forEach((root) => invalidate(root, frames));
1206
+ if (state.gl.xr?.isPresenting || !state.internal.active || state.frameloop === 'never')
1207
+ return;
1208
+ // Increase frames, do not go higher than 60
1209
+ state.internal.frames = Math.min(60, state.internal.frames + frames);
1210
+ // If the render-loop isn't active, start it
1211
+ if (!running) {
1212
+ running = true;
1213
+ requestAnimationFrame(loop);
1214
+ }
1215
+ }
1216
+ function advance(timestamp, runGlobalEffects = true, store, frame) {
1217
+ if (runGlobalEffects)
1218
+ flushGlobalEffects('before', timestamp);
1219
+ if (!store)
1220
+ for (const root of roots.values())
1221
+ render(timestamp, root);
1222
+ else
1223
+ render(timestamp, store, frame);
1224
+ if (runGlobalEffects)
1225
+ flushGlobalEffects('after', timestamp);
1226
+ }
1227
+ return {
1228
+ loop,
1229
+ /**
1230
+ * Invalidates the view, requesting a frame to be rendered. Will globally invalidate unless passed a root's state.
1231
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#invalidate
1232
+ */
1233
+ invalidate,
1234
+ /**
1235
+ * Advances the frameloop and runs render effects, useful for when manually rendering via `frameloop="never"`.
1236
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#advance
1237
+ */
1238
+ advance,
1497
1239
  };
1498
1240
  }
1241
+ const [injectNgtLoop] = createInjectionToken(() => createLoop(roots));
1499
1242
 
1500
- const cached = new Map();
1501
- function normalizeInputs(input) {
1502
- if (Array.isArray(input))
1503
- return input;
1504
- if (typeof input === 'string')
1505
- return [input];
1506
- return Object.values(input);
1507
- }
1508
- function load(loaderConstructorFactory, inputs, { extensions, onProgress, } = {}) {
1509
- return () => {
1510
- const urls = normalizeInputs(inputs());
1511
- const loader = new (loaderConstructorFactory(urls))();
1512
- if (extensions)
1513
- extensions(loader);
1514
- // TODO: reevaluate this
1515
- return urls.map((url) => {
1516
- if (!cached.has(url)) {
1517
- cached.set(url, new Promise((resolve, reject) => {
1518
- loader.load(url, (data) => {
1519
- if ('scene' in data)
1520
- Object.assign(data, makeObjectGraph(data['scene']));
1521
- resolve(data);
1522
- }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error}`)));
1243
+ function storeFactory(previousStore) {
1244
+ const document = inject(DOCUMENT);
1245
+ const window = document.defaultView;
1246
+ if (!window) {
1247
+ // TODO: revisit this when we need to support multiple platforms
1248
+ throw new Error(`[NGT] Window is not available.`);
1249
+ }
1250
+ const loop = injectNgtLoop();
1251
+ // NOTE: using Subject because we do not care about late-subscribers
1252
+ const pointerMissed$ = new Subject();
1253
+ const store = signalStore(({ get, update }) => {
1254
+ const { invalidate, advance } = loop;
1255
+ const position = new THREE.Vector3();
1256
+ const defaultTarget = new THREE.Vector3();
1257
+ const tempTarget = new THREE.Vector3();
1258
+ function getCurrentViewport(camera = get('camera'), target = defaultTarget, size = get('size')) {
1259
+ const { width, height, top, left } = size;
1260
+ const aspect = width / height;
1261
+ if (target instanceof THREE.Vector3)
1262
+ tempTarget.copy(target);
1263
+ else
1264
+ tempTarget.set(...target);
1265
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1266
+ if (is.orthographicCamera(camera)) {
1267
+ return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
1268
+ }
1269
+ const fov = (camera.fov * Math.PI) / 180; // convert vertical fov to radians
1270
+ const h = 2 * Math.tan(fov / 2) * distance; // visible height
1271
+ const w = h * (width / height);
1272
+ return { width: w, height: h, top, left, factor: width / w, distance, aspect };
1273
+ }
1274
+ let performanceTimeout = undefined;
1275
+ const setPerformanceCurrent = (current) => update((state) => ({ performance: { ...state.performance, current } }));
1276
+ const pointer = new THREE.Vector2();
1277
+ return {
1278
+ pointerMissed$: pointerMissed$.asObservable(),
1279
+ events: { priority: 1, enabled: true, connected: false },
1280
+ invalidate: (frames = 1) => invalidate(store, frames),
1281
+ advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, store),
1282
+ legacy: false,
1283
+ linear: false,
1284
+ flat: false,
1285
+ controls: null,
1286
+ clock: new THREE.Clock(),
1287
+ pointer,
1288
+ frameloop: 'always',
1289
+ performance: {
1290
+ current: 1,
1291
+ min: 0.5,
1292
+ max: 1,
1293
+ debounce: 200,
1294
+ regress: () => {
1295
+ const state = get();
1296
+ // Clear timeout
1297
+ if (performanceTimeout)
1298
+ clearTimeout(performanceTimeout);
1299
+ // Set lower bound performance
1300
+ if (state.performance.current !== state.performance.min)
1301
+ setPerformanceCurrent(state.performance.min);
1302
+ // Go back to upper bound performance after a while unless something regresses meanwhile
1303
+ performanceTimeout = setTimeout(() => {
1304
+ setPerformanceCurrent(get('performance', 'max'));
1305
+ // safeDetectChanges(cdr);
1306
+ }, state.performance.debounce);
1307
+ },
1308
+ },
1309
+ size: { width: 0, height: 0, top: 0, left: 0, updateStyle: false },
1310
+ viewport: {
1311
+ initialDpr: 0,
1312
+ dpr: 0,
1313
+ width: 0,
1314
+ height: 0,
1315
+ top: 0,
1316
+ left: 0,
1317
+ aspect: 0,
1318
+ distance: 0,
1319
+ factor: 0,
1320
+ getCurrentViewport,
1321
+ },
1322
+ setEvents: (events) => update((state) => ({ ...state, events: { ...state.events, ...events } })),
1323
+ setSize: (width, height, top, left) => {
1324
+ const camera = get('camera');
1325
+ const size = { width, height, top: top || 0, left: left || 0 };
1326
+ update((state) => ({
1327
+ size,
1328
+ viewport: { ...state.viewport, ...getCurrentViewport(camera, defaultTarget, size) },
1523
1329
  }));
1524
- }
1525
- return cached.get(url);
1526
- });
1527
- };
1528
- }
1529
- function injectNgtLoader(loaderConstructorFactory, inputs, { extensions, onProgress, injector, } = {}) {
1530
- injector = assertInjectionContext(injectNgtLoader, injector);
1531
- const response = signal(null);
1532
- return runInInjectionContext(injector, () => {
1533
- const cdr = inject(ChangeDetectorRef);
1534
- const effector = load(loaderConstructorFactory, inputs, { extensions, onProgress });
1535
- effect(() => {
1536
- const originalUrls = inputs();
1537
- Promise.all(effector()).then((results) => {
1538
- response.update(() => {
1539
- if (Array.isArray(originalUrls))
1540
- return results;
1541
- if (typeof originalUrls === 'string')
1542
- return results[0];
1543
- const keys = Object.keys(originalUrls);
1544
- return keys.reduce((result, key) => {
1545
- result[key] = results[keys.indexOf(key)];
1546
- return result;
1547
- }, {});
1548
- });
1549
- safeDetectChanges(cdr);
1550
- });
1551
- });
1552
- return response.asReadonly();
1330
+ },
1331
+ setDpr: (dpr) => update((state) => {
1332
+ const resolved = makeDpr(dpr, window);
1333
+ return { viewport: { ...state.viewport, dpr: resolved, initialDpr: state.viewport.initialDpr || resolved } };
1334
+ }),
1335
+ setFrameloop: (frameloop = 'always') => {
1336
+ const clock = get('clock');
1337
+ // if frameloop === "never" clock.elapsedTime is updated using advance(timestamp)
1338
+ clock.stop();
1339
+ clock.elapsedTime = 0;
1340
+ if (frameloop !== 'never') {
1341
+ clock.start();
1342
+ clock.elapsedTime = 0;
1343
+ }
1344
+ update(() => ({ frameloop }));
1345
+ },
1346
+ previousRoot: previousStore,
1347
+ internal: {
1348
+ active: false,
1349
+ priority: 0,
1350
+ frames: 0,
1351
+ lastEvent: new ElementRef(null),
1352
+ interaction: [],
1353
+ hovered: new Map(),
1354
+ subscribers: [],
1355
+ initialClick: [0, 0],
1356
+ initialHits: [],
1357
+ capturedMap: new Map(),
1358
+ subscribe: (callback, priority = 0, _store = store) => {
1359
+ const internal = get('internal');
1360
+ // If this subscription was given a priority, it takes rendering into its own hands
1361
+ // For that reason we switch off automatic rendering and increase the manual flag
1362
+ // As long as this flag is positive there can be no internal rendering at all
1363
+ // because there could be multiple render subscriptions
1364
+ internal.priority = internal.priority + (priority > 0 ? 1 : 0);
1365
+ internal.subscribers.push({ callback, priority, store: _store });
1366
+ // Register subscriber and sort layers from lowest to highest, meaning,
1367
+ // highest priority renders last (on top of the other frames)
1368
+ internal.subscribers = internal.subscribers.sort((a, b) => (a.priority || 0) - (b.priority || 0));
1369
+ return () => {
1370
+ const internal = get('internal');
1371
+ if (internal?.subscribers) {
1372
+ // Decrease manual flag if this subscription had a priority
1373
+ internal.priority = internal.priority - (priority > 0 ? 1 : 0);
1374
+ // Remove subscriber from list
1375
+ internal.subscribers = internal.subscribers.filter((s) => s.callback !== callback);
1376
+ }
1377
+ };
1378
+ },
1379
+ },
1380
+ };
1381
+ });
1382
+ Object.defineProperty(store, 'pointerMissed$', { get: () => pointerMissed$ });
1383
+ let { size: oldSize, viewport: { dpr: oldDpr }, camera: oldCamera, } = store.snapshot;
1384
+ const [camera, size, viewportDpr] = [store.select('camera'), store.select('size'), store.select('viewport', 'dpr')];
1385
+ effect(() => {
1386
+ const [newCamera, newSize, newDpr, gl] = [camera(), size(), viewportDpr(), store.snapshot.gl];
1387
+ // Resize camera and renderer on changes to size and pixel-ratio
1388
+ if (newSize !== oldSize || newDpr !== oldDpr) {
1389
+ oldSize = newSize;
1390
+ oldDpr = newDpr;
1391
+ // Update camera & renderer
1392
+ updateCamera(newCamera, newSize);
1393
+ gl.setPixelRatio(newDpr);
1394
+ const updateStyle = typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement;
1395
+ gl.setSize(newSize.width, newSize.height, updateStyle);
1396
+ }
1397
+ // Update viewport once the camera changes
1398
+ if (newCamera !== oldCamera) {
1399
+ oldCamera = newCamera;
1400
+ updateCamera(newCamera, newSize);
1401
+ // Update viewport
1402
+ store.update((state) => ({ viewport: { ...state.viewport, ...state.viewport.getCurrentViewport(newCamera) } }));
1403
+ }
1553
1404
  });
1405
+ return store;
1554
1406
  }
1555
- injectNgtLoader['preload'] = (loaderConstructorFactory, inputs, extensions) => {
1556
- Promise.all(load(loaderConstructorFactory, inputs, { extensions })());
1557
- };
1558
- injectNgtLoader['destroy'] = () => {
1559
- cached.clear();
1560
- };
1407
+ const NGT_STORE = new InjectionToken('NgtStore Token');
1408
+ const [injectNgtStore, provideNgtStore] = createInjectionToken(storeFactory, {
1409
+ isRoot: false,
1410
+ deps: [[new Optional(), new SkipSelf(), NGT_STORE]],
1411
+ token: NGT_STORE,
1412
+ });
1561
1413
 
1562
1414
  const catalogue = {};
1563
1415
  function extend(objects) {
@@ -1577,7 +1429,7 @@ const SPECIAL_PROPERTIES = {
1577
1429
  COMPOUND: 'ngtCompound',
1578
1430
  RENDER_PRIORITY: 'priority',
1579
1431
  ATTACH: 'attach',
1580
- VALUE: 'rawValue',
1432
+ RAW_VALUE: 'rawValue',
1581
1433
  REF: 'ref',
1582
1434
  };
1583
1435
  const SPECIAL_EVENTS = {
@@ -1586,20 +1438,21 @@ const SPECIAL_EVENTS = {
1586
1438
  AFTER_ATTACH: 'afterAttach',
1587
1439
  };
1588
1440
 
1441
+ const [injectNodeType, provideNodeType] = createInjectionToken(() => '', {
1442
+ isRoot: false,
1443
+ });
1589
1444
  class NgtCommonDirective {
1590
- static { this.processComment = true; }
1591
1445
  constructor() {
1592
1446
  this.vcr = inject(ViewContainerRef);
1593
1447
  this.zone = inject(NgZone);
1594
1448
  this.template = inject(TemplateRef);
1449
+ this.nodeType = injectNodeType();
1595
1450
  this.injected = false;
1596
1451
  this.shouldCreateView = true;
1597
- if (NgtCommonDirective.processComment) {
1598
- const commentNode = this.vcr.element.nativeElement;
1599
- if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT]) {
1600
- commentNode[SPECIAL_INTERNAL_ADD_COMMENT]();
1601
- delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT];
1602
- }
1452
+ const commentNode = this.vcr.element.nativeElement;
1453
+ if (commentNode[SPECIAL_INTERNAL_ADD_COMMENT]) {
1454
+ commentNode[SPECIAL_INTERNAL_ADD_COMMENT](this.nodeType);
1455
+ delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT];
1603
1456
  }
1604
1457
  inject(DestroyRef).onDestroy(() => {
1605
1458
  this.view?.destroy();
@@ -1612,16 +1465,16 @@ class NgtCommonDirective {
1612
1465
  this.view.destroy();
1613
1466
  }
1614
1467
  this.view = this.vcr.createEmbeddedView(this.template);
1615
- safeDetectChanges(this.view);
1468
+ this.view.detectChanges();
1616
1469
  }
1617
1470
  });
1618
1471
  }
1619
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtCommonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1620
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtCommonDirective, ngImport: i0 }); }
1472
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtCommonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1473
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.2.1", type: NgtCommonDirective, ngImport: i0 }); }
1621
1474
  }
1622
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtCommonDirective, decorators: [{
1475
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtCommonDirective, decorators: [{
1623
1476
  type: Directive
1624
- }], ctorParameters: function () { return []; } });
1477
+ }], ctorParameters: () => [] });
1625
1478
 
1626
1479
  class NgtArgs extends NgtCommonDirective {
1627
1480
  constructor() {
@@ -1645,12 +1498,12 @@ class NgtArgs extends NgtCommonDirective {
1645
1498
  validate() {
1646
1499
  return !this.injected && !!this.injectedArgs.length;
1647
1500
  }
1648
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtArgs, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1649
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtArgs, isStandalone: true, selector: "ng-template[args]", inputs: { args: "args" }, usesInheritance: true, ngImport: i0 }); }
1501
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtArgs, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1502
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.2.1", type: NgtArgs, isStandalone: true, selector: "ng-template[args]", inputs: { args: "args" }, providers: [provideNodeType('args')], usesInheritance: true, ngImport: i0 }); }
1650
1503
  }
1651
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtArgs, decorators: [{
1504
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtArgs, decorators: [{
1652
1505
  type: Directive,
1653
- args: [{ selector: 'ng-template[args]', standalone: true }]
1506
+ args: [{ selector: 'ng-template[args]', standalone: true, providers: [provideNodeType('args')] }]
1654
1507
  }], propDecorators: { args: [{
1655
1508
  type: Input
1656
1509
  }] } });
@@ -1677,12 +1530,12 @@ class NgtParent extends NgtCommonDirective {
1677
1530
  validate() {
1678
1531
  return !this.injected && !!this.injectedParent;
1679
1532
  }
1680
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtParent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1681
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtParent, isStandalone: true, selector: "ng-template[parent]", inputs: { parent: "parent" }, usesInheritance: true, ngImport: i0 }); }
1533
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtParent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1534
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.2.1", type: NgtParent, isStandalone: true, selector: "ng-template[parent]", inputs: { parent: "parent" }, providers: [provideNodeType('parent')], usesInheritance: true, ngImport: i0 }); }
1682
1535
  }
1683
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtParent, decorators: [{
1536
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtParent, decorators: [{
1684
1537
  type: Directive,
1685
- args: [{ selector: 'ng-template[parent]', standalone: true }]
1538
+ args: [{ selector: 'ng-template[parent]', standalone: true, providers: [provideNodeType('parent')] }]
1686
1539
  }], propDecorators: { parent: [{
1687
1540
  type: Input
1688
1541
  }] } });
@@ -1701,11 +1554,11 @@ function attach(object, value, paths = []) {
1701
1554
  }
1702
1555
  function detach(parent, child, attachProp) {
1703
1556
  const childLocalState = getLocalState(child);
1704
- if (Array.isArray(attachProp)) {
1705
- attach(parent, childLocalState.previousAttach, attachProp);
1706
- }
1707
- else {
1708
- childLocalState.previousAttach();
1557
+ if (childLocalState) {
1558
+ if (Array.isArray(attachProp))
1559
+ attach(parent, childLocalState.previousAttach, attachProp);
1560
+ else
1561
+ childLocalState.previousAttach();
1709
1562
  }
1710
1563
  }
1711
1564
  function assignEmpty(obj, base) {
@@ -1749,6 +1602,14 @@ var NgtQueueOpClassId;
1749
1602
  NgtQueueOpClassId[NgtQueueOpClassId["op"] = 1] = "op";
1750
1603
  NgtQueueOpClassId[NgtQueueOpClassId["done"] = 2] = "done";
1751
1604
  })(NgtQueueOpClassId || (NgtQueueOpClassId = {}));
1605
+ function kebabToPascal(str) {
1606
+ // split the string at each hyphen
1607
+ const parts = str.split('-');
1608
+ // map over the parts, capitalizing the first letter of each part
1609
+ const pascalParts = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1));
1610
+ // join the parts together to create the final PascalCase string
1611
+ return pascalParts.join('');
1612
+ }
1752
1613
  function attachThreeChild(parent, child) {
1753
1614
  const pLS = getLocalState(parent);
1754
1615
  const cLS = getLocalState(child);
@@ -1785,10 +1646,8 @@ function attachThreeChild(parent, child) {
1785
1646
  }
1786
1647
  // attach
1787
1648
  if (cLS.isRaw) {
1788
- if (cLS.parent && cLS.parent() !== parent) {
1789
- untracked(() => {
1790
- cLS.parent.set(parent);
1791
- });
1649
+ if (cLS.instanceStore.get('parent') !== parent) {
1650
+ cLS.setParent(parent);
1792
1651
  }
1793
1652
  // at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
1794
1653
  if (child.__ngt_renderer__[NgtRendererClassId.rawValue] === undefined)
@@ -1807,10 +1666,8 @@ function attachThreeChild(parent, child) {
1807
1666
  added = true;
1808
1667
  }
1809
1668
  pLS.add(child, added ? 'objects' : 'nonObjects');
1810
- if (cLS.parent && cLS.parent() !== parent) {
1811
- untracked(() => {
1812
- cLS.parent.set(parent);
1813
- });
1669
+ if (cLS.instanceStore.get('parent') !== parent) {
1670
+ cLS.setParent(parent);
1814
1671
  }
1815
1672
  if (cLS.afterAttach)
1816
1673
  cLS.afterAttach.emit({ parent, node: child });
@@ -1821,24 +1678,22 @@ function removeThreeChild(parent, child, dispose) {
1821
1678
  const pLS = getLocalState(parent);
1822
1679
  const cLS = getLocalState(child);
1823
1680
  // clear parent ref
1824
- untracked(() => {
1825
- cLS.parent?.set(null);
1826
- });
1681
+ cLS?.setParent(null);
1827
1682
  // remove child from parent
1828
- if (pLS.objects && untracked(pLS.objects))
1829
- pLS.remove(child, 'objects');
1830
- if (pLS.nonObjects && untracked(pLS.nonObjects))
1831
- pLS.remove(child, 'nonObjects');
1832
- if (cLS.attach) {
1683
+ pLS?.remove(child, 'objects');
1684
+ pLS?.remove(child, 'nonObjects');
1685
+ if (cLS?.attach) {
1833
1686
  detach(parent, child, cLS.attach);
1834
1687
  }
1835
1688
  else if (is.object3D(parent) && is.object3D(child)) {
1836
1689
  parent.remove(child);
1837
- removeInteractivity(cLS.store || pLS.store, child);
1690
+ const store = cLS?.store || pLS?.store;
1691
+ if (store)
1692
+ removeInteractivity(store, child);
1838
1693
  }
1839
- const isPrimitive = cLS.primitive;
1694
+ const isPrimitive = cLS?.primitive;
1840
1695
  if (!isPrimitive) {
1841
- removeThreeRecursive(cLS.objects ? untracked(cLS.objects) : [], child, !!dispose);
1696
+ removeThreeRecursive(cLS?.instanceStore.get('objects') || [], child, !!dispose);
1842
1697
  removeThreeRecursive(child.children, child, !!dispose);
1843
1698
  }
1844
1699
  // dispose
@@ -1851,16 +1706,12 @@ function removeThreeRecursive(array, parent, dispose) {
1851
1706
  if (array)
1852
1707
  [...array].forEach((child) => removeThreeChild(parent, child, dispose));
1853
1708
  }
1854
- function kebabToPascal(str) {
1855
- // split the string at each hyphen
1856
- const parts = str.split('-');
1857
- // map over the parts, capitalizing the first letter of each part
1858
- const pascalParts = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1));
1859
- // join the parts together to create the final PascalCase string
1860
- return pascalParts.join('');
1861
- }
1862
- function processThreeEvent(instance, priority, eventName, callback, zone, rootCdr, targetCdr) {
1709
+ function processThreeEvent(instance, priority, eventName, callback) {
1863
1710
  const lS = getLocalState(instance);
1711
+ if (!lS) {
1712
+ console.warn('[NGT] instance has not been prepared yet.');
1713
+ return () => { };
1714
+ }
1864
1715
  if (eventName === SPECIAL_EVENTS.BEFORE_RENDER) {
1865
1716
  return lS.store
1866
1717
  .get('internal')
@@ -1881,10 +1732,7 @@ function processThreeEvent(instance, priority, eventName, callback, zone, rootCd
1881
1732
  const updatedCallback = (event) => {
1882
1733
  if (previousHandler)
1883
1734
  previousHandler(event);
1884
- zone.run(() => {
1885
- callback(event);
1886
- safeDetectChanges(targetCdr, rootCdr);
1887
- });
1735
+ callback(event);
1888
1736
  };
1889
1737
  Object.assign(lS.handlers, { [eventName]: updatedCallback });
1890
1738
  // increment the count everytime
@@ -1895,23 +1743,24 @@ function processThreeEvent(instance, priority, eventName, callback, zone, rootCd
1895
1743
  lS.store.get('internal', 'interaction').push(instance);
1896
1744
  // clean up the event listener by removing the target from the interaction array
1897
1745
  return () => {
1898
- const localState = getLocalState(instance);
1899
- if (localState && localState.eventCount) {
1900
- localState.eventCount -= 1;
1901
- const index = localState.store
1902
- .get('internal', 'interaction')
1903
- .findIndex((obj) => obj.uuid === instance.uuid);
1746
+ const lS = getLocalState(instance);
1747
+ if (lS) {
1748
+ lS.eventCount -= 1;
1749
+ const interactions = lS.store.get('internal', 'interaction') || [];
1750
+ const index = interactions.findIndex((obj) => obj.uuid === instance.uuid);
1904
1751
  if (index >= 0)
1905
- localState.store.get('internal', 'interaction').splice(index, 1);
1752
+ interactions.splice(index, 1);
1906
1753
  }
1907
1754
  };
1908
1755
  }
1909
1756
 
1910
1757
  const NGT_COMPOUND_PREFIXES = new InjectionToken('NgtCompoundPrefixes');
1911
1758
  class NgtRendererStore {
1912
- constructor(root) {
1913
- this.root = root;
1914
- this.comments = [];
1759
+ constructor(rootState) {
1760
+ this.rootState = rootState;
1761
+ this.argsCommentNodes = [];
1762
+ this.parentCommentNodes = [];
1763
+ this.portalCommentsNodes = [];
1915
1764
  }
1916
1765
  createNode(type, node) {
1917
1766
  const state = [
@@ -1934,34 +1783,100 @@ class NgtRendererStore {
1934
1783
  const rendererNode = Object.assign(node, { __ngt_renderer__: state });
1935
1784
  // NOTE: assign ownerDocument to node so we can use HostListener in Component
1936
1785
  if (!rendererNode['ownerDocument'])
1937
- rendererNode['ownerDocument'] = this.root.document;
1786
+ rendererNode['ownerDocument'] = this.rootState.document;
1938
1787
  // NOTE: assign injectorFactory on non-three type since
1939
1788
  // rendererNode is an instance of DOM Node
1940
1789
  if (state[NgtRendererClassId.type] !== 'three') {
1941
- state[NgtRendererClassId.injectorFactory] = () => getDebugNode(rendererNode).injector;
1790
+ state[NgtRendererClassId.injectorFactory] = () => getDebugNode(rendererNode)?.injector;
1942
1791
  }
1943
1792
  if (state[NgtRendererClassId.type] === 'comment') {
1944
1793
  // NOTE: we attach an arrow function to the Comment node
1945
1794
  // In our directives, we can call this function to then start tracking the RendererNode
1946
1795
  // this is done to limit the amount of Nodes we need to process for getCreationState
1947
1796
  rendererNode[SPECIAL_INTERNAL_ADD_COMMENT] = (node) => {
1948
- if (node && node.__ngt_renderer__[NgtRendererClassId.type] === 'portal') {
1949
- this.portals.push(node);
1797
+ if (node === 'args') {
1798
+ this.argsCommentNodes.push(rendererNode);
1950
1799
  }
1951
- else {
1952
- this.comments.push(rendererNode);
1800
+ else if (node === 'parent') {
1801
+ this.parentCommentNodes.push(rendererNode);
1802
+ }
1803
+ else if (typeof node === 'object') {
1804
+ this.portalCommentsNodes.push(node);
1953
1805
  }
1954
1806
  };
1955
1807
  return rendererNode;
1956
1808
  }
1957
1809
  if (state[NgtRendererClassId.type] === 'compound') {
1958
1810
  state[NgtRendererClassId.queueOps] = new Set();
1959
- state[NgtRendererClassId.attributes] = {};
1960
- state[NgtRendererClassId.properties] = {};
1811
+ state[NgtRendererClassId.attributes] = state[NgtRendererClassId.properties] = {};
1961
1812
  return rendererNode;
1962
1813
  }
1963
1814
  return rendererNode;
1964
1815
  }
1816
+ isCompound(name) {
1817
+ return this.rootState.compoundPrefixes.some((prefix) => name.startsWith(prefix));
1818
+ }
1819
+ isDOM(node) {
1820
+ const rS = node['__ngt_renderer__'];
1821
+ return (!rS ||
1822
+ (rS[NgtRendererClassId.type] !== 'compound' &&
1823
+ (node instanceof Element || node instanceof Document || node instanceof Window)));
1824
+ }
1825
+ getClosestParentWithInstance(node) {
1826
+ let parent = node.__ngt_renderer__[NgtRendererClassId.parent];
1827
+ while (parent && parent.__ngt_renderer__[NgtRendererClassId.type] !== 'three') {
1828
+ parent = parent.__ngt_renderer__[NgtRendererClassId.portalContainer]
1829
+ ? parent.__ngt_renderer__[NgtRendererClassId.portalContainer]
1830
+ : parent.__ngt_renderer__[NgtRendererClassId.parent];
1831
+ }
1832
+ return parent;
1833
+ }
1834
+ getClosestParentWithCompound(node) {
1835
+ if (node.__ngt_renderer__[NgtRendererClassId.compoundParent]) {
1836
+ return node.__ngt_renderer__[NgtRendererClassId.compoundParent];
1837
+ }
1838
+ let parent = node.__ngt_renderer__[NgtRendererClassId.parent];
1839
+ if (parent &&
1840
+ parent.__ngt_renderer__[NgtRendererClassId.type] === 'compound' &&
1841
+ !parent.__ngt_renderer__[NgtRendererClassId.compounded]) {
1842
+ return parent;
1843
+ }
1844
+ while (parent &&
1845
+ (parent.__ngt_renderer__[NgtRendererClassId.type] === 'three' ||
1846
+ !parent.__ngt_renderer__[NgtRendererClassId.compoundParent] ||
1847
+ parent.__ngt_renderer__[NgtRendererClassId.type] !== 'compound')) {
1848
+ parent = parent.__ngt_renderer__[NgtRendererClassId.parent];
1849
+ }
1850
+ if (!parent)
1851
+ return null;
1852
+ if (parent.__ngt_renderer__[NgtRendererClassId.type] === 'three' &&
1853
+ parent.__ngt_renderer__[NgtRendererClassId.compoundParent]) {
1854
+ return parent.__ngt_renderer__[NgtRendererClassId.compoundParent];
1855
+ }
1856
+ if (!parent.__ngt_renderer__[NgtRendererClassId.compounded]) {
1857
+ return parent;
1858
+ }
1859
+ return null;
1860
+ }
1861
+ processPortalContainer(portal) {
1862
+ const injector = portal.__ngt_renderer__[NgtRendererClassId.injectorFactory]?.();
1863
+ if (!injector)
1864
+ return;
1865
+ const portalStore = injector.get(NGT_STORE, null);
1866
+ if (!portalStore)
1867
+ return;
1868
+ const portalContainer = portalStore.get('scene');
1869
+ if (!portalContainer)
1870
+ return;
1871
+ portal.__ngt_renderer__[NgtRendererClassId.portalContainer] = this.createNode('three', portalContainer);
1872
+ }
1873
+ getCreationState() {
1874
+ return [
1875
+ this.firstNonInjectedDirective('argsCommentNodes', NgtArgs)?.args || [],
1876
+ this.firstNonInjectedDirective('parentCommentNodes', NgtParent)?.parent || null,
1877
+ this.tryGetPortalStore(),
1878
+ ];
1879
+ }
1965
1880
  setParent(node, parent) {
1966
1881
  if (!node.__ngt_renderer__[NgtRendererClassId.parent]) {
1967
1882
  node.__ngt_renderer__[NgtRendererClassId.parent] = parent;
@@ -1983,19 +1898,16 @@ class NgtRendererStore {
1983
1898
  if (instanceRS && instanceRS[NgtRendererClassId.parent]) {
1984
1899
  const parentRS = instanceRS[NgtRendererClassId.parent].__ngt_renderer__;
1985
1900
  // NOTE: if instance is already compounded by its parent. skip
1986
- if (parentRS[NgtRendererClassId.type] === 'compound' &&
1987
- parentRS[NgtRendererClassId.compounded] === instance) {
1901
+ if (parentRS[NgtRendererClassId.type] === 'compound' && parentRS[NgtRendererClassId.compounded] === instance) {
1988
1902
  return;
1989
1903
  }
1990
1904
  }
1991
1905
  const rS = compound.__ngt_renderer__;
1992
1906
  rS[NgtRendererClassId.compounded] = instance;
1993
- const attributes = Object.keys(rS[NgtRendererClassId.attributes]);
1994
- const properties = Object.keys(rS[NgtRendererClassId.properties]);
1995
- for (const key of attributes) {
1907
+ for (const key of Object.keys(rS[NgtRendererClassId.attributes])) {
1996
1908
  this.applyAttribute(instance, key, rS[NgtRendererClassId.attributes][key]);
1997
1909
  }
1998
- for (const key of properties) {
1910
+ for (const key of Object.keys(rS[NgtRendererClassId.properties])) {
1999
1911
  this.applyProperty(instance, key, rS[NgtRendererClassId.properties][key]);
2000
1912
  }
2001
1913
  this.executeOperation(compound);
@@ -2005,6 +1917,7 @@ class NgtRendererStore {
2005
1917
  }
2006
1918
  executeOperation(node, type = 'op') {
2007
1919
  const rS = node.__ngt_renderer__;
1920
+ // TODO: maybe an array with pop() would work better
2008
1921
  if (rS[NgtRendererClassId.queueOps]?.size) {
2009
1922
  rS[NgtRendererClassId.queueOps].forEach((op) => {
2010
1923
  if (op[NgtQueueOpClassId.type] === type) {
@@ -2014,19 +1927,6 @@ class NgtRendererStore {
2014
1927
  });
2015
1928
  }
2016
1929
  }
2017
- processPortalContainer(portal) {
2018
- const injectorFactory = portal.__ngt_renderer__[NgtRendererClassId.injectorFactory];
2019
- const injector = injectorFactory?.();
2020
- if (!injector)
2021
- return;
2022
- const portalStore = injector.get(NGT_STORE, null);
2023
- if (!portalStore)
2024
- return;
2025
- const portalContainer = portalStore.get('scene');
2026
- if (!portalContainer)
2027
- return;
2028
- portal.__ngt_renderer__[NgtRendererClassId.portalContainer] = this.createNode('three', portalContainer);
2029
- }
2030
1930
  applyAttribute(node, name, value) {
2031
1931
  const rS = node.__ngt_renderer__;
2032
1932
  if (rS[NgtRendererClassId.destroyed])
@@ -2039,7 +1939,10 @@ class NgtRendererStore {
2039
1939
  priority = 0;
2040
1940
  console.warn(`[NGT] "priority" is an invalid number, default to 0`);
2041
1941
  }
2042
- getLocalState(node).priority = priority;
1942
+ const localState = getLocalState(node);
1943
+ if (localState) {
1944
+ localState.priority = priority;
1945
+ }
2043
1946
  }
2044
1947
  if (name === SPECIAL_PROPERTIES.COMPOUND) {
2045
1948
  // NOTE: we set the compound property on instance node now so we know that this instance is being compounded
@@ -2049,11 +1952,15 @@ class NgtRendererStore {
2049
1952
  if (name === SPECIAL_PROPERTIES.ATTACH) {
2050
1953
  // NOTE: handle attach as tring
2051
1954
  const paths = value.split('.');
2052
- if (paths.length)
2053
- getLocalState(node).attach = paths;
1955
+ if (paths.length) {
1956
+ const localState = getLocalState(node);
1957
+ if (localState) {
1958
+ localState.attach = paths;
1959
+ }
1960
+ }
2054
1961
  return;
2055
1962
  }
2056
- if (name === SPECIAL_PROPERTIES.VALUE) {
1963
+ if (name === SPECIAL_PROPERTIES.RAW_VALUE) {
2057
1964
  // NOTE: coercion
2058
1965
  let maybeCoerced = value;
2059
1966
  if (maybeCoerced === '' || maybeCoerced === 'true' || maybeCoerced === 'false') {
@@ -2078,9 +1985,10 @@ class NgtRendererStore {
2078
1985
  value.nativeElement = node;
2079
1986
  return;
2080
1987
  }
2081
- const parent = getLocalState(node).parent() || rS[NgtRendererClassId.parent];
1988
+ const localState = getLocalState(node);
1989
+ const parent = localState?.instanceStore.get('parent') || rS[NgtRendererClassId.parent];
2082
1990
  // [rawValue]
2083
- if (getLocalState(node).isRaw && name === SPECIAL_PROPERTIES.VALUE) {
1991
+ if (localState?.isRaw && name === SPECIAL_PROPERTIES.RAW_VALUE) {
2084
1992
  rS[NgtRendererClassId.rawValue] = value;
2085
1993
  if (parent)
2086
1994
  attachThreeChild(parent, node);
@@ -2088,7 +1996,8 @@ class NgtRendererStore {
2088
1996
  }
2089
1997
  // [attach]
2090
1998
  if (name === SPECIAL_PROPERTIES.ATTACH) {
2091
- getLocalState(node).attach = Array.isArray(value) ? value.map((v) => v.toString()) : value;
1999
+ if (localState)
2000
+ localState.attach = Array.isArray(value) ? value.map((v) => v.toString()) : value;
2092
2001
  if (parent)
2093
2002
  attachThreeChild(parent, node);
2094
2003
  return;
@@ -2102,62 +2011,8 @@ class NgtRendererStore {
2102
2011
  applyProps(node, { [name]: value });
2103
2012
  this.updateNativeProps(node, name, value);
2104
2013
  }
2105
- isCompound(name) {
2106
- return this.root.compoundPrefixes.some((prefix) => name.startsWith(prefix));
2107
- }
2108
- isDOM(node) {
2109
- const rS = node['__ngt_renderer__'];
2110
- return (!rS ||
2111
- (rS[NgtRendererClassId.type] !== 'compound' &&
2112
- (node instanceof Element || node instanceof Document || node instanceof Window)));
2113
- }
2114
2014
  get rootScene() {
2115
- return this.root.store.get('scene');
2116
- }
2117
- get portals() {
2118
- return this.root.portals;
2119
- }
2120
- getClosestParentWithInstance(node) {
2121
- let parent = node.__ngt_renderer__[NgtRendererClassId.parent];
2122
- while (parent && parent.__ngt_renderer__[NgtRendererClassId.type] !== 'three') {
2123
- parent = parent.__ngt_renderer__[NgtRendererClassId.portalContainer]
2124
- ? parent.__ngt_renderer__[NgtRendererClassId.portalContainer]
2125
- : parent.__ngt_renderer__[NgtRendererClassId.parent];
2126
- }
2127
- return parent;
2128
- }
2129
- getClosestParentWithCompound(node) {
2130
- if (node.__ngt_renderer__[NgtRendererClassId.compoundParent]) {
2131
- return node.__ngt_renderer__[NgtRendererClassId.compoundParent];
2132
- }
2133
- let parent = node.__ngt_renderer__[NgtRendererClassId.parent];
2134
- if (parent &&
2135
- parent.__ngt_renderer__[NgtRendererClassId.type] === 'compound' &&
2136
- !parent.__ngt_renderer__[NgtRendererClassId.compounded]) {
2137
- return parent;
2138
- }
2139
- while (parent &&
2140
- (parent.__ngt_renderer__[NgtRendererClassId.type] === 'three' ||
2141
- !parent.__ngt_renderer__[NgtRendererClassId.compoundParent] ||
2142
- parent.__ngt_renderer__[NgtRendererClassId.type] !== 'compound')) {
2143
- parent = parent.__ngt_renderer__[NgtRendererClassId.parent];
2144
- }
2145
- if (!parent)
2146
- return;
2147
- if (parent.__ngt_renderer__[NgtRendererClassId.type] === 'three' &&
2148
- parent.__ngt_renderer__[NgtRendererClassId.compoundParent]) {
2149
- return parent.__ngt_renderer__[NgtRendererClassId.compoundParent];
2150
- }
2151
- if (!parent.__ngt_renderer__[NgtRendererClassId.compounded]) {
2152
- return parent;
2153
- }
2154
- return null;
2155
- }
2156
- getCreationState() {
2157
- const injectedArgs = this.firstNonInjectedDirective(NgtArgs)?.args || [];
2158
- const injectedParent = this.firstNonInjectedDirective(NgtParent)?.parent || null;
2159
- const store = this.tryGetPortalStore();
2160
- return { injectedArgs, injectedParent, store };
2015
+ return this.rootState.store.get('scene');
2161
2016
  }
2162
2017
  destroy(node, parent) {
2163
2018
  const rS = node.__ngt_renderer__;
@@ -2167,18 +2022,17 @@ class NgtRendererStore {
2167
2022
  rS[NgtRendererClassId.compound] = undefined;
2168
2023
  rS[NgtRendererClassId.compoundParent] = undefined;
2169
2024
  const localState = getLocalState(node);
2170
- if (localState.objects) {
2171
- untracked(localState.objects).forEach((obj) => this.destroy(obj, parent));
2172
- }
2173
- if (localState.nonObjects) {
2174
- untracked(localState.nonObjects).forEach((obj) => this.destroy(obj, parent));
2025
+ if (localState?.instanceStore) {
2026
+ localState.instanceStore.get('objects').forEach((obj) => this.destroy(obj, parent));
2027
+ localState.instanceStore.get('nonObjects').forEach((obj) => this.destroy(obj, parent));
2175
2028
  }
2176
- if (localState.afterUpdate)
2029
+ if (localState?.afterUpdate)
2177
2030
  localState.afterUpdate.complete();
2178
- if (localState.afterAttach)
2031
+ if (localState?.afterAttach)
2179
2032
  localState.afterAttach.complete();
2180
2033
  delete localState['objects'];
2181
2034
  delete localState['nonObjects'];
2035
+ delete localState['parent'];
2182
2036
  delete localState['nativeProps'];
2183
2037
  delete localState['add'];
2184
2038
  delete localState['remove'];
@@ -2186,24 +2040,20 @@ class NgtRendererStore {
2186
2040
  delete localState['afterAttach'];
2187
2041
  delete localState['store'];
2188
2042
  delete localState['handlers'];
2189
- if (!localState.primitive) {
2043
+ if (!localState?.primitive) {
2190
2044
  delete node['__ngt__'];
2191
2045
  }
2192
2046
  }
2193
2047
  if (rS[NgtRendererClassId.type] === 'comment') {
2194
2048
  rS[NgtRendererClassId.injectorFactory] = null;
2195
2049
  delete node[SPECIAL_INTERNAL_ADD_COMMENT];
2196
- const index = this.comments.findIndex((comment) => comment === node);
2197
- if (index > -1) {
2198
- this.comments.splice(index, 1);
2050
+ if (!this.removeCommentNode(node, this.argsCommentNodes)) {
2051
+ this.removeCommentNode(node, this.parentCommentNodes);
2199
2052
  }
2200
2053
  }
2201
2054
  if (rS[NgtRendererClassId.type] === 'portal') {
2202
2055
  rS[NgtRendererClassId.injectorFactory] = null;
2203
- const index = this.portals.findIndex((portal) => portal === node);
2204
- if (index > -1) {
2205
- this.portals.splice(index, 1);
2206
- }
2056
+ this.removeCommentNode(node, this.portalCommentsNodes);
2207
2057
  }
2208
2058
  if (rS[NgtRendererClassId.type] === 'compound') {
2209
2059
  rS[NgtRendererClassId.compounded] = undefined;
@@ -2239,18 +2089,24 @@ class NgtRendererStore {
2239
2089
  this.removeChild(parent, node);
2240
2090
  }
2241
2091
  }
2092
+ removeCommentNode(node, nodes) {
2093
+ const index = nodes.findIndex((comment) => comment === node);
2094
+ if (index > -1) {
2095
+ nodes.splice(index, 1);
2096
+ return true;
2097
+ }
2098
+ return false;
2099
+ }
2242
2100
  updateNativeProps(node, key, value) {
2243
2101
  const localState = getLocalState(node);
2244
- if (!localState || !localState.nativeProps)
2245
- return;
2246
- localState.nativeProps.set({ [key]: value });
2102
+ localState?.setNativeProps(key, value);
2247
2103
  }
2248
- firstNonInjectedDirective(dir) {
2104
+ firstNonInjectedDirective(listProperty, dir) {
2249
2105
  let directive;
2250
2106
  const destroyed = [];
2251
- let i = this.comments.length - 1;
2107
+ let i = this[listProperty].length - 1;
2252
2108
  while (i >= 0) {
2253
- const comment = this.comments[i];
2109
+ const comment = this[listProperty][i];
2254
2110
  if (comment.__ngt_renderer__[NgtRendererClassId.destroyed]) {
2255
2111
  destroyed.push(i);
2256
2112
  i--;
@@ -2269,7 +2125,7 @@ class NgtRendererStore {
2269
2125
  i--;
2270
2126
  }
2271
2127
  destroyed.forEach((index) => {
2272
- this.comments.splice(index, 1);
2128
+ this[listProperty].splice(index, 1);
2273
2129
  });
2274
2130
  return directive;
2275
2131
  }
@@ -2277,10 +2133,10 @@ class NgtRendererStore {
2277
2133
  let store;
2278
2134
  const destroyed = [];
2279
2135
  // we only care about the portal states because NgtStore only differs per Portal
2280
- let i = this.portals.length - 1;
2136
+ let i = this.portalCommentsNodes.length - 1;
2281
2137
  while (i >= 0) {
2282
2138
  // loop through the portal state backwards to find the closest NgtStore
2283
- const portal = this.portals[i];
2139
+ const portal = this.portalCommentsNodes[i];
2284
2140
  if (portal.__ngt_renderer__[NgtRendererClassId.destroyed]) {
2285
2141
  destroyed.push(i);
2286
2142
  i--;
@@ -2300,72 +2156,62 @@ class NgtRendererStore {
2300
2156
  i--;
2301
2157
  }
2302
2158
  destroyed.forEach((index) => {
2303
- this.portals.splice(index, 1);
2159
+ this.portalCommentsNodes.splice(index, 1);
2304
2160
  });
2305
- return store || this.root.store;
2161
+ return store || this.rootState.store;
2306
2162
  }
2307
2163
  }
2308
2164
 
2309
2165
  class NgtRendererFactory {
2310
2166
  constructor() {
2311
2167
  this.delegateRendererFactory = inject(RendererFactory2, { skipSelf: true });
2312
- this.zone = inject(NgZone);
2313
2168
  this.catalogue = injectNgtCatalogue();
2314
- this.cdr = inject(ChangeDetectorRef);
2315
2169
  this.rendererMap = new Map();
2316
2170
  this.routedSet = new Set();
2317
- // all Renderer instances share the same Store
2171
+ // NOTE: all Renderer instances under the same NgtCanvas share the same Store
2318
2172
  this.rendererStore = new NgtRendererStore({
2319
- portals: [],
2320
2173
  store: injectNgtStore(),
2321
2174
  compoundPrefixes: inject(NGT_COMPOUND_PREFIXES),
2322
2175
  document: inject(DOCUMENT),
2323
2176
  });
2324
2177
  }
2325
2178
  createRenderer(hostElement, type) {
2326
- const delegate = this.delegateRendererFactory.createRenderer(hostElement, type);
2179
+ const delegateRenderer = this.delegateRendererFactory.createRenderer(hostElement, type);
2327
2180
  if (!type)
2328
- return delegate;
2329
- // TODO: handle html in canvas
2181
+ return delegateRenderer;
2182
+ // NOTE: might need to revisit this
2330
2183
  if (type['type'][HTML]) {
2331
- this.rendererMap.set(type.id, delegate);
2332
- return delegate;
2184
+ this.rendererMap.set(type.id, delegateRenderer);
2185
+ return delegateRenderer;
2333
2186
  }
2334
2187
  if (type['type'][ROUTED_SCENE]) {
2335
2188
  this.routedSet.add(type.id);
2336
2189
  }
2337
2190
  let renderer = this.rendererMap.get(type.id);
2338
2191
  if (!renderer) {
2339
- renderer = new NgtRenderer(delegate, this.rendererStore, this.catalogue, this.zone, this.cdr,
2192
+ this.rendererMap.set(type.id, (renderer = new NgtRenderer(delegateRenderer, this.rendererStore, this.catalogue,
2340
2193
  // setting root scene if there's no routed scene OR this component is the routed Scene
2341
- !hostElement && (this.routedSet.size === 0 || this.routedSet.has(type.id)));
2342
- this.rendererMap.set(type.id, renderer);
2194
+ !hostElement && (this.routedSet.size === 0 || this.routedSet.has(type.id)))));
2343
2195
  }
2344
2196
  return renderer;
2345
2197
  }
2346
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRendererFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2347
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRendererFactory }); }
2198
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtRendererFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
2199
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtRendererFactory }); }
2348
2200
  }
2349
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRendererFactory, decorators: [{
2201
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtRendererFactory, decorators: [{
2350
2202
  type: Injectable
2351
2203
  }] });
2352
- /**
2353
- * Anything abbreviated with rS/RS stands for RendererState
2354
- */
2355
2204
  class NgtRenderer {
2356
- constructor(delegate, store, catalogue, zone, cdr, root = true) {
2205
+ constructor(delegate, store, catalogue, isRoot = true) {
2357
2206
  this.delegate = delegate;
2358
2207
  this.store = store;
2359
2208
  this.catalogue = catalogue;
2360
- this.zone = zone;
2361
- this.cdr = cdr;
2362
- this.root = root;
2209
+ this.isRoot = isRoot;
2363
2210
  this.createText = this.delegate.createText.bind(this.delegate);
2364
2211
  this.destroy = this.delegate.destroy.bind(this.delegate);
2365
2212
  this.destroyNode = null;
2366
2213
  this.selectRootElement = this.delegate.selectRootElement.bind(this.delegate);
2367
2214
  this.nextSibling = this.delegate.nextSibling.bind(this.delegate);
2368
- this.removeAttribute = this.delegate.removeAttribute.bind(this.delegate);
2369
2215
  this.addClass = this.delegate.addClass.bind(this.delegate);
2370
2216
  this.removeClass = this.delegate.removeClass.bind(this.delegate);
2371
2217
  this.setStyle = this.delegate.setStyle.bind(this.delegate);
@@ -2375,41 +2221,37 @@ class NgtRenderer {
2375
2221
  createElement(name, namespace) {
2376
2222
  const element = this.delegate.createElement(name, namespace);
2377
2223
  // on first pass, we return the Root Scene as the root node
2378
- if (this.root) {
2379
- this.root = false;
2224
+ if (this.isRoot) {
2225
+ this.isRoot = false;
2380
2226
  const node = this.store.createNode('three', this.store.rootScene);
2381
- node.__ngt_renderer__[NgtRendererClassId.injectorFactory] = () => getDebugNode(element).injector;
2227
+ node.__ngt_renderer__[NgtRendererClassId.injectorFactory] = () => getDebugNode(element)?.injector;
2382
2228
  return node;
2383
2229
  }
2384
- // handle compound
2385
2230
  if (this.store.isCompound(name)) {
2386
2231
  return this.store.createNode('compound', element);
2387
2232
  }
2388
- // handle portal
2389
2233
  if (name === SPECIAL_DOM_TAG.NGT_PORTAL) {
2390
2234
  return this.store.createNode('portal', element);
2391
2235
  }
2392
- // handle raw value
2393
2236
  if (name === SPECIAL_DOM_TAG.NGT_VALUE) {
2394
2237
  return this.store.createNode('three', Object.assign({ __ngt_renderer__: { rawValue: undefined } },
2395
2238
  // NOTE: we assign this manually to a raw value node
2396
2239
  // because we say it is a 'three' node but we're not using prepare()
2397
- { __ngt__: { isRaw: true, parent: signal(null) } }));
2240
+ { __ngt__: { isRaw: true, parent: () => null } }));
2398
2241
  }
2399
- const { injectedArgs, injectedParent, store } = this.store.getCreationState();
2242
+ const [injectedArgs, injectedParent, store] = this.store.getCreationState();
2400
2243
  let parent = injectedParent;
2401
2244
  if (typeof injectedParent === 'string') {
2402
2245
  parent = store
2403
2246
  .get('scene')
2404
2247
  .getObjectByName(injectedParent);
2405
2248
  }
2406
- // handle primitive
2407
2249
  if (name === SPECIAL_DOM_TAG.NGT_PRIMITIVE) {
2408
2250
  if (!injectedArgs[0])
2409
2251
  throw new Error(`[NGT] ngt-primitive without args is invalid`);
2410
2252
  const object = injectedArgs[0];
2411
2253
  let localState = getLocalState(object);
2412
- if (!Object.keys(localState).length) {
2254
+ if (!localState) {
2413
2255
  // NOTE: if an object isn't already "prepared", we prepare it
2414
2256
  localState = getLocalState(prepare(object, { store, args: injectedArgs, primitive: true }));
2415
2257
  }
@@ -2421,8 +2263,7 @@ class NgtRenderer {
2421
2263
  }
2422
2264
  return node;
2423
2265
  }
2424
- const threeTag = name.startsWith('ngt') ? name.slice(4) : name;
2425
- const threeName = kebabToPascal(threeTag);
2266
+ const threeName = kebabToPascal(name.startsWith('ngt') ? name.slice(4) : name);
2426
2267
  const threeTarget = this.catalogue[threeName];
2427
2268
  // we have the THREE constructor here, handle it
2428
2269
  if (threeTarget) {
@@ -2470,13 +2311,14 @@ class NgtRenderer {
2470
2311
  }
2471
2312
  if (cRS?.[NgtRendererClassId.injectedParent]) {
2472
2313
  if (is.ref(cRS[NgtRendererClassId.injectedParent])) {
2473
- const injector = cRS[NgtRendererClassId.injectorFactory]().get(Injector, null);
2314
+ const injector = cRS[NgtRendererClassId.injectorFactory]()?.get(Injector, null);
2474
2315
  if (!injector) {
2475
2316
  console.warn(`[NGT] NgtRenderer is attempting to start an effect for injectedParent but no Injector is found.`);
2476
2317
  return;
2477
2318
  }
2478
2319
  const watcher = effect(() => {
2479
- const injectedParent = cRS[NgtRendererClassId.injectedParent].nativeElement;
2320
+ const injectedParent = cRS[NgtRendererClassId.injectedParent]
2321
+ .nativeElement;
2480
2322
  if (injectedParent && injectedParent !== parent) {
2481
2323
  this.appendChild(injectedParent, newChild);
2482
2324
  // only run this effect once
@@ -2511,8 +2353,9 @@ class NgtRenderer {
2511
2353
  }
2512
2354
  // if both are three instances, straightforward case
2513
2355
  if (pRS[NgtRendererClassId.type] === 'three' && cRS?.[NgtRendererClassId.type] === 'three') {
2356
+ const cLS = getLocalState(newChild);
2514
2357
  // if child already attached to a parent, skip
2515
- if (getLocalState(newChild).parent && untracked(getLocalState(newChild).parent))
2358
+ if (cLS?.instanceStore?.get('parent'))
2516
2359
  return;
2517
2360
  // attach THREE child
2518
2361
  attachThreeChild(parent, newChild);
@@ -2527,7 +2370,7 @@ class NgtRenderer {
2527
2370
  }
2528
2371
  // if only the parent is the THREE instance
2529
2372
  if (pRS[NgtRendererClassId.type] === 'three') {
2530
- for (const renderChild of cRS?.[NgtRendererClassId.children]) {
2373
+ for (const renderChild of cRS?.[NgtRendererClassId.children] || []) {
2531
2374
  this.appendChild(parent, renderChild);
2532
2375
  }
2533
2376
  }
@@ -2602,23 +2445,34 @@ class NgtRenderer {
2602
2445
  return rS[NgtRendererClassId.parent];
2603
2446
  return this.delegate.parentNode(node);
2604
2447
  }
2605
- setAttribute(el, name, value, namespace) {
2448
+ setAttributeInternal(el, name, value, namespace) {
2606
2449
  const rS = el.__ngt_renderer__;
2607
2450
  if (rS[NgtRendererClassId.type] === 'compound') {
2608
2451
  // we don't have the compound instance yet
2609
2452
  rS[NgtRendererClassId.attributes][name] = value;
2610
2453
  if (!rS[NgtRendererClassId.compounded]) {
2611
2454
  this.store.queueOperation(el, ['op', () => this.setAttribute(el, name, value, namespace)]);
2612
- return;
2455
+ return false;
2613
2456
  }
2614
- this.setAttribute(rS[NgtRendererClassId.compounded], name, value, namespace);
2615
- return;
2457
+ return this.setAttributeInternal(rS[NgtRendererClassId.compounded], name, value, namespace);
2616
2458
  }
2617
2459
  if (rS[NgtRendererClassId.type] === 'three') {
2618
2460
  this.store.applyAttribute(el, name, value);
2619
- return;
2461
+ return false;
2462
+ }
2463
+ return true;
2464
+ }
2465
+ setAttribute(el, name, value, namespace) {
2466
+ const useDelegate = this.setAttributeInternal(el, name, value, namespace);
2467
+ if (useDelegate) {
2468
+ this.delegate.setAttribute(el, name, value);
2469
+ }
2470
+ }
2471
+ removeAttribute(el, name, namespace) {
2472
+ const useDelegate = this.setAttributeInternal(el, name, undefined, namespace);
2473
+ if (useDelegate) {
2474
+ this.delegate.removeAttribute(el, name, namespace);
2620
2475
  }
2621
- return this.delegate.setAttribute(el, name, value);
2622
2476
  }
2623
2477
  setProperty(el, name, value) {
2624
2478
  // TODO: should we support ref value
@@ -2645,17 +2499,16 @@ class NgtRenderer {
2645
2499
  listen(target, eventName, callback) {
2646
2500
  const rS = target.__ngt_renderer__;
2647
2501
  // if the target doesn't have __ngt_renderer__, we delegate
2648
- // if target is DOM node, then we pass that to delegate Renderer
2502
+ // if target is DOM node, we delegate
2649
2503
  if (!rS || this.store.isDOM(target)) {
2650
2504
  return this.delegate.listen(target, eventName, callback);
2651
2505
  }
2652
2506
  if (rS[NgtRendererClassId.type] === 'three' ||
2653
2507
  (rS[NgtRendererClassId.type] === 'compound' && rS[NgtRendererClassId.compounded])) {
2654
2508
  const instance = rS[NgtRendererClassId.compounded] || target;
2655
- const priority = getLocalState(target).priority;
2656
- const targetCdr = rS[NgtRendererClassId.injectorFactory]?.().get(ChangeDetectorRef, null) ||
2657
- rS[NgtRendererClassId.parent]?.__ngt_renderer__?.[NgtRendererClassId.injectorFactory]?.().get(ChangeDetectorRef, null);
2658
- return processThreeEvent(instance, priority || 0, eventName, callback, this.zone, this.cdr, targetCdr);
2509
+ const localState = getLocalState(instance);
2510
+ const priority = localState?.priority ?? 0;
2511
+ return processThreeEvent(instance, priority || 0, eventName, callback);
2659
2512
  }
2660
2513
  if (rS[NgtRendererClassId.type] === 'compound' && !rS[NgtRendererClassId.compounded]) {
2661
2514
  this.store.queueOperation(target, [
@@ -2683,48 +2536,54 @@ class NgtRenderer {
2683
2536
  const cType = cRS[NgtRendererClassId.type];
2684
2537
  const isParentCompounded = pRS[NgtRendererClassId.compounded];
2685
2538
  const isChildCompounded = cRS[NgtRendererClassId.compounded];
2539
+ const cLS = getLocalState(child);
2686
2540
  // if child is three but haven't been attached to a parent yet
2687
- const isDanglingThreeChild = cType === 'three' && !untracked(getLocalState(child).parent);
2541
+ const isDanglingThreeChild = cType === 'three' && !cLS?.instanceStore?.get('parent');
2688
2542
  // or both parent and child are DOM elements
2689
2543
  // or they are compound AND haven't had a THREE instance yet
2690
2544
  const isParentStillDOM = pType === 'dom' || (pType === 'compound' && !isParentCompounded);
2691
2545
  const isChildStillDOM = cType === 'dom' || (cType === 'compound' && !isChildCompounded);
2692
2546
  // and the child is a compounded compound
2693
2547
  const isCompoundChildCompounded = cType === 'compound' && !!isChildCompounded;
2694
- return (isDanglingThreeChild ||
2695
- (isParentStillDOM && isChildStillDOM) ||
2696
- (isParentStillDOM && isCompoundChildCompounded));
2548
+ return (isDanglingThreeChild || (isParentStillDOM && isChildStillDOM) || (isParentStillDOM && isCompoundChildCompounded));
2697
2549
  }
2698
2550
  get data() {
2699
2551
  return this.delegate.data;
2700
2552
  }
2701
2553
  }
2702
- function provideNgtRenderer(store, compoundPrefixes, cdr) {
2554
+ function provideNgtRenderer(store, compoundPrefixes) {
2703
2555
  if (!compoundPrefixes.includes('ngts'))
2704
2556
  compoundPrefixes.push('ngts');
2705
2557
  if (!compoundPrefixes.includes('ngtp'))
2706
2558
  compoundPrefixes.push('ngtp');
2707
2559
  return makeEnvironmentProviders([
2708
- { provide: RendererFactory2, useClass: NgtRendererFactory },
2560
+ NgtRendererFactory,
2561
+ { provide: RendererFactory2, useExisting: NgtRendererFactory },
2709
2562
  { provide: NGT_COMPOUND_PREFIXES, useValue: compoundPrefixes },
2710
- { provide: ChangeDetectorRef, useValue: cdr },
2711
2563
  provideNgtStore(store),
2712
2564
  provideZoneChangeDetection({ runCoalescing: true, eventCoalescing: true }),
2713
2565
  ]);
2714
2566
  }
2715
2567
 
2716
2568
  class NgtCanvas {
2569
+ set _sceneGraphInputs(value) {
2570
+ this.sceneGraphInputs.set(value);
2571
+ }
2572
+ set _canvasInputs(value) {
2573
+ this.canvasInputs.update(value);
2574
+ }
2717
2575
  constructor() {
2718
2576
  this.store = injectNgtStore();
2719
2577
  this.initRoot = injectCanvasRootInitializer();
2578
+ this.autoEffect = injectAutoEffect();
2720
2579
  this.host = inject(ElementRef);
2721
2580
  this.viewContainerRef = inject(ViewContainerRef);
2722
- this.injector = inject(Injector);
2723
- this.environmentInjector = inject(EnvironmentInjector);
2724
2581
  this.zone = inject(NgZone);
2725
- this.destroyRef = inject(DestroyRef);
2726
- this.cdr = inject(ChangeDetectorRef);
2727
- this.inputs = signalStore({
2582
+ this.environmentInjector = inject(EnvironmentInjector);
2583
+ this.injector = inject(Injector);
2584
+ this.compoundPrefixes = [];
2585
+ this.sceneGraphInputs = signal({}, { equal: Object.is });
2586
+ this.canvasInputs = signalStore({
2728
2587
  shadows: false,
2729
2588
  linear: false,
2730
2589
  flat: false,
@@ -2734,102 +2593,49 @@ class NgtCanvas {
2734
2593
  dpr: [1, 2],
2735
2594
  events: createPointerEvents,
2736
2595
  });
2737
- this.sceneGraphInputs = {};
2738
- this.compoundPrefixes = [];
2739
2596
  this.created = new EventEmitter();
2740
- this.inputsEventSource = this.inputs.select('eventSource');
2741
- this.hbPointerEvents = computed(() => (!!this.inputsEventSource() ? 'none' : 'auto'));
2742
- }
2743
- set linear(linear) {
2744
- this.inputs.set({ linear });
2745
- }
2746
- set legacy(legacy) {
2747
- this.inputs.set({ legacy });
2748
- }
2749
- set flat(flat) {
2750
- this.inputs.set({ flat });
2751
- }
2752
- set orthographic(orthographic) {
2753
- this.inputs.set({ orthographic });
2754
- }
2755
- set frameloop(frameloop) {
2756
- this.inputs.set({ frameloop });
2757
- }
2758
- set dpr(dpr) {
2759
- this.inputs.set({ dpr });
2760
- }
2761
- set raycaster(raycaster) {
2762
- this.inputs.set({ raycaster });
2763
- }
2764
- set shadows(shadows) {
2765
- this.inputs.set({ shadows });
2766
- }
2767
- set camera(camera) {
2768
- this.inputs.set({ camera });
2769
- }
2770
- set scene(scene) {
2771
- this.inputs.set({ scene });
2772
- }
2773
- set gl(gl) {
2774
- this.inputs.set({ gl });
2775
- }
2776
- set eventSource(eventSource) {
2777
- this.inputs.set({ eventSource });
2778
- }
2779
- set eventPrefix(eventPrefix) {
2780
- this.inputs.set({ eventPrefix });
2781
- }
2782
- set lookAt(lookAt) {
2783
- this.inputs.set({ lookAt });
2784
- }
2785
- set performance(performance) {
2786
- this.inputs.set({ performance });
2787
- }
2788
- ngOnChanges(changes) {
2789
- if (changes['sceneGraphInputs'] && !changes['sceneGraphInputs'].firstChange && this.glRef) {
2790
- this.setSceneGraphInputs();
2791
- }
2792
- }
2793
- ngOnInit() {
2794
- // NOTE: we resolve glCanvas at this point, setup the configurator
2795
- this.configurator = this.initRoot(this.glCanvas.nativeElement);
2796
- this.destroyRef.onDestroy(() => {
2797
- this.glEnvironmentInjector?.destroy();
2597
+ // NOTE: this signal is updated outside of Zone
2598
+ this.resizeResult = signal({}, { equal: Object.is });
2599
+ this.eventSource = this.canvasInputs.select('eventSource');
2600
+ this.hbPointerEvents = computed(() => (this.eventSource() ? 'none' : 'auto'));
2601
+ afterNextRender(() => {
2602
+ this.zone.runOutsideAngular(() => {
2603
+ this.configurator = this.initRoot(this.glCanvas.nativeElement);
2604
+ this.noZoneResizeEffect();
2605
+ this.noZoneSceneGraphInputsEffect();
2606
+ });
2607
+ });
2608
+ inject(DestroyRef).onDestroy(() => {
2798
2609
  this.glRef?.destroy();
2799
- this.resizeEffectRef?.destroy();
2800
- injectNgtLoader.destroy();
2610
+ this.glEnvironmentInjector?.destroy();
2801
2611
  this.configurator?.destroy();
2802
2612
  });
2803
2613
  }
2804
- // NOTE: runs outside of Zone due to emitInZone: false
2805
- onResize(result) {
2806
- if (result.width > 0 && result.height > 0) {
2807
- this.resizeEffectRef?.destroy();
2808
- const inputs = this.inputs.select();
2809
- // NOTE: go back into zone so that effect runs
2810
- // TODO: Double-check when effect is made not depended on zone
2811
- this.resizeEffectRef = this.zone.run(() => effect(() => {
2812
- this.zone.runOutsideAngular(() => {
2813
- if (!this.configurator)
2814
- this.configurator = this.initRoot(this.glCanvas.nativeElement);
2815
- this.configurator.configure({ ...inputs(), size: result });
2614
+ noZoneResizeEffect() {
2615
+ this.autoEffect(() => {
2616
+ const resizeResult = this.resizeResult();
2617
+ if (resizeResult.width > 0 && resizeResult.height > 0) {
2618
+ if (!this.configurator)
2619
+ this.configurator = this.initRoot(this.glCanvas.nativeElement);
2620
+ this.configurator.configure({ ...this.canvasInputs.state(), size: resizeResult });
2621
+ untracked(() => {
2816
2622
  if (this.glRef) {
2817
- this.cdr.detectChanges();
2623
+ this.glRef.changeDetectorRef.detectChanges();
2818
2624
  }
2819
2625
  else {
2820
- this.render();
2626
+ this.noZoneRender();
2821
2627
  }
2822
2628
  });
2823
- }, { manualCleanup: true, injector: this.injector }));
2824
- }
2629
+ }
2630
+ });
2825
2631
  }
2826
- render() {
2632
+ noZoneRender() {
2633
+ // NOTE: destroy previous instances if existed
2827
2634
  this.glEnvironmentInjector?.destroy();
2828
2635
  this.glRef?.destroy();
2829
- // Flag the canvas active, rendering will now begin
2830
- this.store.set((state) => ({ internal: { ...state.internal, active: true } }));
2831
- const inputs = this.inputs.get();
2832
- const state = this.store.get();
2636
+ // NOTE: Flag the canvas active, rendering will now begin
2637
+ this.store.update((state) => ({ internal: { ...state.internal, active: true } }));
2638
+ const [inputs, state] = [this.canvasInputs.snapshot, this.store.snapshot];
2833
2639
  // connect to event source
2834
2640
  state.events.connect?.(inputs.eventSource
2835
2641
  ? is.ref(inputs.eventSource)
@@ -2840,111 +2646,83 @@ class NgtCanvas {
2840
2646
  if (inputs.eventPrefix) {
2841
2647
  state.setEvents({
2842
2648
  compute: (event, store) => {
2843
- const innerState = store.get();
2649
+ const { pointer, raycaster, camera, size } = store.snapshot;
2844
2650
  const x = event[(inputs.eventPrefix + 'X')];
2845
2651
  const y = event[(inputs.eventPrefix + 'Y')];
2846
- innerState.pointer.set((x / innerState.size.width) * 2 - 1, -(y / innerState.size.height) * 2 + 1);
2847
- innerState.raycaster.setFromCamera(innerState.pointer, innerState.camera);
2652
+ pointer.set((x / size.width) * 2 - 1, -(y / size.height) * 2 + 1);
2653
+ raycaster.setFromCamera(pointer, camera);
2848
2654
  },
2849
2655
  });
2850
2656
  }
2851
2657
  // emit created event if observed
2852
2658
  if (this.created.observed) {
2853
- // but go back into zone to do so
2854
- this.zone.run(() => {
2855
- this.created.emit(this.store.get());
2856
- });
2659
+ this.created.emit(this.store.snapshot);
2857
2660
  }
2858
2661
  if (!this.store.get('events', 'connected')) {
2859
2662
  this.store.get('events').connect?.(this.glCanvas.nativeElement);
2860
2663
  }
2861
- this.glEnvironmentInjector = createEnvironmentInjector([provideNgtRenderer(this.store, this.compoundPrefixes, this.cdr)], this.environmentInjector);
2664
+ this.glEnvironmentInjector = createEnvironmentInjector([provideNgtRenderer(this.store, this.compoundPrefixes)], this.environmentInjector);
2862
2665
  this.glRef = this.viewContainerRef.createComponent(this.sceneGraph, {
2863
2666
  environmentInjector: this.glEnvironmentInjector,
2864
2667
  injector: this.injector,
2865
2668
  });
2866
- this.overrideChangeDetectorRef();
2867
- this.setSceneGraphInputs();
2868
- }
2869
- overrideChangeDetectorRef() {
2870
- const originalDetectChanges = this.cdr.detectChanges.bind(this.cdr);
2871
- this.cdr.detectChanges = () => {
2872
- originalDetectChanges();
2873
- safeDetectChanges(this.glRef?.changeDetectorRef);
2874
- };
2669
+ this.glRef.changeDetectorRef.detectChanges();
2670
+ this.setSceneGraphInputs(untracked(this.sceneGraphInputs));
2875
2671
  }
2876
- setSceneGraphInputs() {
2877
- this.zone.run(() => {
2878
- if (this.glRef) {
2879
- for (const [key, value] of Object.entries(this.sceneGraphInputs)) {
2880
- this.glRef.setInput(key, value);
2881
- }
2882
- this.glRef.changeDetectorRef.detectChanges();
2883
- }
2672
+ noZoneSceneGraphInputsEffect() {
2673
+ this.autoEffect(() => {
2674
+ this.setSceneGraphInputs(this.sceneGraphInputs());
2884
2675
  });
2885
2676
  }
2886
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtCanvas, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2887
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.2", 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" }, host: { properties: { "style.pointerEvents": "hbPointerEvents()" }, styleAttribute: "display: block;position: relative;width: 100%;height: 100%;overflow: hidden;" }, providers: [provideNgxResizeOptions({ emitInZone: false, emitInitialResult: true }), provideNgtStore()], viewQueries: [{ propertyName: "glCanvas", first: true, predicate: ["glCanvas"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
2888
- <div (ngxResize)="onResize($event)" style="height: 100%; width: 100%;">
2677
+ setSceneGraphInputs(sceneGraphInputs) {
2678
+ if (this.glRef) {
2679
+ for (const [key, value] of Object.entries(sceneGraphInputs)) {
2680
+ this.glRef.setInput(key, value);
2681
+ }
2682
+ }
2683
+ }
2684
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtCanvas, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2685
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.2.1", type: NgtCanvas, isStandalone: true, selector: "ngt-canvas", inputs: { sceneGraph: "sceneGraph", compoundPrefixes: "compoundPrefixes", _sceneGraphInputs: ["sceneGraphInputs", "_sceneGraphInputs"], _canvasInputs: ["options", "_canvasInputs"] }, outputs: { created: "created" }, host: { properties: { "style.pointerEvents": "hbPointerEvents()" }, styleAttribute: "display: block;position: relative;width: 100%;height: 100%;overflow: hidden;" }, providers: [
2686
+ provideResizeOptions({ emitInZone: false, emitInitialResult: true, debounce: 250 }),
2687
+ provideNgtStore(),
2688
+ ], viewQueries: [{ propertyName: "glCanvas", first: true, predicate: ["glCanvas"], descendants: true, static: true }], ngImport: i0, template: `
2689
+ <div (ngxResize)="resizeResult.set($event)" style="height: 100%; width: 100%;">
2889
2690
  <canvas #glCanvas style="display: block;"></canvas>
2890
2691
  </div>
2891
2692
  `, isInline: true, dependencies: [{ kind: "directive", type: NgxResize, selector: "[ngxResize]", inputs: ["ngxResizeOptions"], outputs: ["ngxResize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2892
2693
  }
2893
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtCanvas, decorators: [{
2694
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtCanvas, decorators: [{
2894
2695
  type: Component,
2895
2696
  args: [{
2896
2697
  selector: 'ngt-canvas',
2897
2698
  standalone: true,
2898
2699
  template: `
2899
- <div (ngxResize)="onResize($event)" style="height: 100%; width: 100%;">
2700
+ <div (ngxResize)="resizeResult.set($event)" style="height: 100%; width: 100%;">
2900
2701
  <canvas #glCanvas style="display: block;"></canvas>
2901
2702
  </div>
2902
2703
  `,
2903
2704
  imports: [NgxResize],
2904
- providers: [provideNgxResizeOptions({ emitInZone: false, emitInitialResult: true }), provideNgtStore()],
2705
+ providers: [
2706
+ provideResizeOptions({ emitInZone: false, emitInitialResult: true, debounce: 250 }),
2707
+ provideNgtStore(),
2708
+ ],
2905
2709
  host: {
2906
2710
  style: 'display: block;position: relative;width: 100%;height: 100%;overflow: hidden;',
2907
2711
  '[style.pointerEvents]': 'hbPointerEvents()',
2908
2712
  },
2909
2713
  changeDetection: ChangeDetectionStrategy.OnPush,
2910
2714
  }]
2911
- }], propDecorators: { sceneGraph: [{
2715
+ }], ctorParameters: () => [], propDecorators: { sceneGraph: [{
2912
2716
  type: Input,
2913
2717
  args: [{ required: true }]
2914
- }], sceneGraphInputs: [{
2915
- type: Input
2916
2718
  }], compoundPrefixes: [{
2917
2719
  type: Input
2918
- }], linear: [{
2919
- type: Input
2920
- }], legacy: [{
2921
- type: Input
2922
- }], flat: [{
2923
- type: Input
2924
- }], orthographic: [{
2925
- type: Input
2926
- }], frameloop: [{
2927
- type: Input
2928
- }], dpr: [{
2929
- type: Input
2930
- }], raycaster: [{
2931
- type: Input
2932
- }], shadows: [{
2933
- type: Input
2934
- }], camera: [{
2935
- type: Input
2936
- }], scene: [{
2937
- type: Input
2938
- }], gl: [{
2939
- type: Input
2940
- }], eventSource: [{
2941
- type: Input
2942
- }], eventPrefix: [{
2943
- type: Input
2944
- }], lookAt: [{
2945
- type: Input
2946
- }], performance: [{
2947
- type: Input
2720
+ }], _sceneGraphInputs: [{
2721
+ type: Input,
2722
+ args: [{ alias: 'sceneGraphInputs' }]
2723
+ }], _canvasInputs: [{
2724
+ type: Input,
2725
+ args: [{ alias: 'options' }]
2948
2726
  }], created: [{
2949
2727
  type: Output
2950
2728
  }], glCanvas: [{
@@ -2952,62 +2730,82 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImpor
2952
2730
  args: ['glCanvas', { static: true }]
2953
2731
  }] } });
2954
2732
 
2955
- class NgtKey extends NgtCommonDirective {
2956
- constructor() {
2957
- super(...arguments);
2958
- this.lastKey = '';
2959
- }
2960
- static { this.processComment = false; }
2961
- validate() {
2962
- return false;
2963
- }
2964
- set key(key) {
2965
- const normalizedKey = JSON.stringify(key);
2966
- if (this.lastKey !== normalizedKey) {
2967
- this.lastKey = normalizedKey;
2968
- this.createView();
2969
- }
2970
- }
2971
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtKey, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2972
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtKey, isStandalone: true, selector: "ng-template[key]", inputs: { key: "key" }, usesInheritance: true, ngImport: i0 }); }
2733
+ const cached = new Map();
2734
+ function normalizeInputs(input) {
2735
+ if (Array.isArray(input))
2736
+ return input;
2737
+ if (typeof input === 'string')
2738
+ return [input];
2739
+ return Object.values(input);
2973
2740
  }
2974
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtKey, decorators: [{
2975
- type: Directive,
2976
- args: [{ selector: 'ng-template[key]', standalone: true }]
2977
- }], propDecorators: { key: [{
2978
- type: Input
2979
- }] } });
2980
-
2981
- class NgtRepeat extends NgForOf {
2982
- set ngForRepeat(count) {
2983
- this.ngForOf = Number.isInteger(count) ? Array.from({ length: count }, (_, i) => i) : [];
2984
- }
2985
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRepeat, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2986
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtRepeat, isStandalone: true, selector: "[ngFor][ngForRepeat]", inputs: { ngForRepeat: "ngForRepeat" }, usesInheritance: true, ngImport: i0 }); }
2741
+ function load(loaderConstructorFactory, inputs, { extensions, onProgress, } = {}) {
2742
+ return () => {
2743
+ const urls = normalizeInputs(inputs());
2744
+ const loader = new (loaderConstructorFactory(urls))();
2745
+ if (extensions)
2746
+ extensions(loader);
2747
+ // TODO: reevaluate this
2748
+ return urls.map((url) => {
2749
+ if (!cached.has(url)) {
2750
+ cached.set(url, new Promise((resolve, reject) => {
2751
+ loader.load(url, (data) => {
2752
+ if ('scene' in data) {
2753
+ Object.assign(data, makeObjectGraph(data['scene']));
2754
+ }
2755
+ resolve(data);
2756
+ }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error}`)));
2757
+ }));
2758
+ }
2759
+ return cached.get(url);
2760
+ });
2761
+ };
2987
2762
  }
2988
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRepeat, decorators: [{
2989
- type: Directive,
2990
- args: [{ selector: '[ngFor][ngForRepeat]', standalone: true }]
2991
- }], propDecorators: { ngForRepeat: [{
2992
- type: Input
2993
- }] } });
2763
+ function _injectNgtLoader(loaderConstructorFactory, inputs, { extensions, onProgress, injector, } = {}) {
2764
+ return assertInjector(_injectNgtLoader, injector, () => {
2765
+ const response = signal(null);
2766
+ const effector = load(loaderConstructorFactory, inputs, { extensions, onProgress });
2767
+ effect(() => {
2768
+ const originalUrls = inputs();
2769
+ Promise.all(effector()).then((results) => {
2770
+ response.update(() => {
2771
+ if (Array.isArray(originalUrls))
2772
+ return results;
2773
+ if (typeof originalUrls === 'string')
2774
+ return results[0];
2775
+ const keys = Object.keys(originalUrls);
2776
+ return keys.reduce((result, key) => {
2777
+ result[key] = results[keys.indexOf(key)];
2778
+ return result;
2779
+ }, {});
2780
+ });
2781
+ });
2782
+ });
2783
+ return response.asReadonly();
2784
+ });
2785
+ }
2786
+ _injectNgtLoader.preload = (loaderConstructorFactory, inputs, extensions) => {
2787
+ void Promise.all(load(loaderConstructorFactory, inputs, { extensions })());
2788
+ };
2789
+ _injectNgtLoader.destroy = () => {
2790
+ cached.clear();
2791
+ };
2792
+ const injectNgtLoader = _injectNgtLoader;
2994
2793
 
2995
2794
  function injectNgtRef(initial = null, injector) {
2996
- injector = assertInjectionContext(injectNgtRef);
2997
- const ref = is.ref(initial) ? initial : new ElementRef(initial);
2998
- const signalRef = signal(ref.nativeElement);
2999
- const readonlySignal = signalRef.asReadonly();
3000
- const cached = new Map();
3001
- return runInInjectionContext(injector, () => {
3002
- inject(DestroyRef).onDestroy(() => void cached.clear());
2795
+ return assertInjector(injectNgtRef, injector, () => {
2796
+ const ref = is.ref(initial) ? initial : new ElementRef(initial);
2797
+ const refSignal = signal(ref.nativeElement, { equal: Object.is });
2798
+ const readonlyRef = refSignal.asReadonly();
2799
+ const computedCached = new Map();
2800
+ inject(DestroyRef).onDestroy(() => void computedCached.clear());
3003
2801
  const children = (type = 'objects') => {
3004
- if (!cached.has(type)) {
3005
- cached.set(type, computed(() => {
3006
- const instance = readonlySignal();
2802
+ if (!computedCached.has(type)) {
2803
+ computedCached.set(type, computed(() => {
2804
+ const instance = readonlyRef();
3007
2805
  if (!instance)
3008
2806
  return [];
3009
2807
  const localState = getLocalState(instance);
3010
- if (!localState.objects || !localState.nonObjects)
2808
+ if (!localState?.instanceStore)
3011
2809
  return [];
3012
2810
  if (type === 'objects')
3013
2811
  return localState.objects();
@@ -3016,26 +2814,34 @@ function injectNgtRef(initial = null, injector) {
3016
2814
  return [...localState.objects(), ...localState.nonObjects()];
3017
2815
  }));
3018
2816
  }
3019
- return cached.get(type);
2817
+ return computedCached.get(type);
3020
2818
  };
3021
2819
  Object.defineProperties(ref, {
3022
2820
  nativeElement: {
3023
2821
  set: (newElement) => {
3024
2822
  untracked(() => {
3025
- if (newElement !== signalRef()) {
3026
- signalRef.set(newElement);
2823
+ if (newElement !== readonlyRef()) {
2824
+ refSignal.set(newElement);
3027
2825
  }
3028
2826
  });
3029
2827
  },
3030
- get: readonlySignal,
2828
+ get: readonlyRef,
3031
2829
  },
3032
- untracked: { get: () => untracked(readonlySignal) },
3033
- children: { get: () => children },
2830
+ children: { value: children },
3034
2831
  });
3035
2832
  return ref;
3036
2833
  });
3037
2834
  }
3038
2835
 
2836
+ function injectBeforeRender(cb, { priority = 0, injector } = {}) {
2837
+ return assertInjector(injectBeforeRender, injector, () => {
2838
+ const store = injectNgtStore();
2839
+ const sub = store.get('internal').subscribe(cb, priority, store);
2840
+ inject(DestroyRef).onDestroy(() => void sub());
2841
+ return sub;
2842
+ });
2843
+ }
2844
+
3039
2845
  const privateKeys = [
3040
2846
  'get',
3041
2847
  'set',
@@ -3049,39 +2855,66 @@ const privateKeys = [
3049
2855
  'size',
3050
2856
  'viewport',
3051
2857
  ];
2858
+ const [, providePortalStore] = createInjectionToken((parentStore) => {
2859
+ const parentState = parentStore.snapshot;
2860
+ const pointer = new THREE.Vector2();
2861
+ const raycaster = new THREE.Raycaster();
2862
+ return signalStore(({ update }) => {
2863
+ return {
2864
+ ...parentState,
2865
+ pointer,
2866
+ raycaster,
2867
+ previousRoot: parentStore,
2868
+ // Layers are allowed to override events
2869
+ setEvents: (events) => update((state) => ({ ...state, events: { ...state.events, ...events } })),
2870
+ };
2871
+ });
2872
+ }, { isRoot: false, token: NGT_STORE, deps: [[new SkipSelf(), NGT_STORE]] });
3052
2873
  class NgtPortalBeforeRender {
3053
2874
  constructor() {
3054
2875
  this.portalStore = injectNgtStore();
3055
2876
  this.injector = inject(Injector);
3056
2877
  this.renderPriority = 1;
3057
- this.beforeRender = new EventEmitter();
3058
- }
3059
- ngOnInit() {
3060
- let oldClear;
3061
- injectBeforeRender(({ delta, frame }) => {
3062
- this.beforeRender.emit({ ...this.portalStore.get(), delta, frame });
3063
- const { gl, scene, camera } = this.portalStore.get();
3064
- oldClear = gl.autoClear;
3065
- if (this.renderPriority === 1) {
3066
- // clear scene and render with default
3067
- gl.autoClear = true;
3068
- gl.render(this.parentScene, this.parentCamera);
3069
- }
3070
- // disable cleaning
3071
- gl.autoClear = false;
3072
- gl.clearDepth();
3073
- gl.render(scene, camera);
3074
- // restore
3075
- gl.autoClear = oldClear;
3076
- }, { priority: this.renderPriority, injector: this.injector });
3077
- }
3078
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtPortalBeforeRender, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3079
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtPortalBeforeRender, isStandalone: true, selector: "[ngtPortalBeforeRender]", inputs: { renderPriority: "renderPriority", parentScene: "parentScene", parentCamera: "parentCamera" }, outputs: { beforeRender: "beforeRender" }, ngImport: i0 }); }
3080
- }
3081
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtPortalBeforeRender, decorators: [{
3082
- type: Directive,
3083
- args: [{ selector: '[ngtPortalBeforeRender]', standalone: true }]
3084
- }], propDecorators: { renderPriority: [{
2878
+ afterNextRender(() => {
2879
+ let oldClear;
2880
+ injectBeforeRender(() => {
2881
+ const { gl, scene, camera } = this.portalStore.get();
2882
+ oldClear = gl.autoClear;
2883
+ if (this.renderPriority === 1) {
2884
+ // clear scene and render with default
2885
+ gl.autoClear = true;
2886
+ gl.render(this.parentScene, this.parentCamera);
2887
+ }
2888
+ // disable cleaning
2889
+ gl.autoClear = false;
2890
+ gl.clearDepth();
2891
+ gl.render(scene, camera);
2892
+ // restore
2893
+ gl.autoClear = oldClear;
2894
+ }, { priority: this.renderPriority, injector: this.injector });
2895
+ });
2896
+ }
2897
+ onPointerOver() {
2898
+ /* noop */
2899
+ }
2900
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtPortalBeforeRender, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2901
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.2.1", type: NgtPortalBeforeRender, isStandalone: true, selector: "ngt-portal-before-render", inputs: { renderPriority: "renderPriority", parentScene: "parentScene", parentCamera: "parentCamera" }, ngImport: i0, template: `
2902
+ <!-- Without an element that receives pointer events state.pointer will always be 0/0 -->
2903
+ <ngt-group (pointerover)="onPointerOver()" attach="none" />
2904
+ `, isInline: true }); }
2905
+ }
2906
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtPortalBeforeRender, decorators: [{
2907
+ type: Component,
2908
+ args: [{
2909
+ selector: 'ngt-portal-before-render',
2910
+ standalone: true,
2911
+ template: `
2912
+ <!-- Without an element that receives pointer events state.pointer will always be 0/0 -->
2913
+ <ngt-group (pointerover)="onPointerOver()" attach="none" />
2914
+ `,
2915
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
2916
+ }]
2917
+ }], ctorParameters: () => [], propDecorators: { renderPriority: [{
3085
2918
  type: Input
3086
2919
  }], parentScene: [{
3087
2920
  type: Input,
@@ -3089,8 +2922,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImpor
3089
2922
  }], parentCamera: [{
3090
2923
  type: Input,
3091
2924
  args: [{ required: true }]
3092
- }], beforeRender: [{
3093
- type: Output
3094
2925
  }] } });
3095
2926
  class NgtPortalContent {
3096
2927
  constructor(vcr, parentVcr) {
@@ -3100,87 +2931,55 @@ class NgtPortalContent {
3100
2931
  delete commentNode[SPECIAL_INTERNAL_ADD_COMMENT];
3101
2932
  }
3102
2933
  }
3103
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtPortalContent, deps: [{ token: i0.ViewContainerRef }, { token: i0.ViewContainerRef, skipSelf: true }], target: i0.ɵɵFactoryTarget.Directive }); }
3104
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.2", type: NgtPortalContent, isStandalone: true, selector: "ng-template[ngtPortalContent]", ngImport: i0 }); }
2934
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtPortalContent, deps: [{ token: i0.ViewContainerRef }, { token: i0.ViewContainerRef, skipSelf: true }], target: i0.ɵɵFactoryTarget.Directive }); }
2935
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.2.1", type: NgtPortalContent, isStandalone: true, selector: "ng-template[ngtPortalContent]", ngImport: i0 }); }
3105
2936
  }
3106
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtPortalContent, decorators: [{
2937
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtPortalContent, decorators: [{
3107
2938
  type: Directive,
3108
2939
  args: [{ selector: 'ng-template[ngtPortalContent]', standalone: true }]
3109
- }], ctorParameters: function () { return [{ type: i0.ViewContainerRef }, { type: i0.ViewContainerRef, decorators: [{
2940
+ }], ctorParameters: () => [{ type: i0.ViewContainerRef }, { type: i0.ViewContainerRef, decorators: [{
3110
2941
  type: SkipSelf
3111
- }] }]; } });
2942
+ }] }] });
3112
2943
  class NgtPortal {
3113
- set container(container) {
3114
- this.inputs.set({ container });
2944
+ set _portalInputs(value) {
2945
+ this.portalInputs.update(value);
3115
2946
  }
3116
- set portalState(state) {
3117
- this.inputs.set({ state });
2947
+ set _autoRender(value) {
2948
+ this.autoRender.set(value);
3118
2949
  }
3119
2950
  constructor() {
3120
- this.inputs = signalStore({ container: injectNgtRef(prepare(new THREE.Scene())) });
3121
- this.autoRender = true;
2951
+ this.portalInputs = signalStore({
2952
+ container: injectNgtRef(prepare(new THREE.Scene())),
2953
+ });
2954
+ this.autoRender = signal(false);
3122
2955
  this.autoRenderPriority = 1;
3123
- this.beforeRender = new EventEmitter();
2956
+ this.destroyRef = inject(DestroyRef);
2957
+ this.autoEffect = injectAutoEffect();
3124
2958
  this.parentStore = injectNgtStore({ skipSelf: true });
2959
+ this.portalStore = injectNgtStore({ self: true });
2960
+ this.portalRendered = signal(false);
2961
+ this.renderAutoBeforeRender = computed(() => this.portalRendered() && this.autoRender());
3125
2962
  this.parentScene = this.parentStore.get('scene');
3126
2963
  this.parentCamera = this.parentStore.get('camera');
3127
- this.portalStore = injectNgtStore({ self: true });
3128
- this.injector = inject(Injector);
3129
- this.zone = inject(NgZone);
3130
- this.raycaster = new THREE.Raycaster();
3131
- this.pointer = new THREE.Vector2();
3132
- this.portalContentRendered = false;
3133
- inject(DestroyRef).onDestroy(() => {
3134
- if (this.portalContentView && !this.portalContentView.destroyed) {
3135
- this.portalContentView.destroy();
3136
- }
3137
- });
3138
- }
3139
- ngOnInit() {
3140
- const previousState = this.parentStore.get();
3141
- const inputsState = this.inputs.get();
3142
- if (!inputsState.state && this.autoRender) {
3143
- inputsState.state = { events: { priority: this.autoRenderPriority + 1 } };
3144
- }
3145
- const { events, size, ...restInputsState } = inputsState.state || {};
3146
- const containerState = inputsState.container;
3147
- let container = is.ref(containerState) ? containerState.nativeElement : containerState;
3148
- if (!is.instance(container)) {
3149
- container = prepare(container);
3150
- }
3151
- const localState = getLocalState(container);
3152
- if (!localState.store) {
3153
- localState.store = this.portalStore;
3154
- }
3155
- this.portalStore.set({
3156
- ...previousState,
3157
- scene: container,
3158
- raycaster: this.raycaster,
3159
- pointer: this.pointer,
3160
- previousRoot: this.parentStore,
3161
- events: { ...previousState.events, ...(events || {}) },
3162
- size: { ...previousState.size, ...(size || {}) },
3163
- ...restInputsState,
3164
- setEvents: (events) => this.portalStore.set((state) => ({ ...state, events: { ...state.events, ...events } })),
3165
- });
3166
- const parentState = this.parentStore.select();
3167
- effect(() => {
3168
- const previous = parentState();
3169
- this.zone.runOutsideAngular(() => {
3170
- this.portalStore.set((state) => this.inject(previous, state));
2964
+ afterNextRender(() => {
2965
+ const parentState = this.parentStore.snapshot;
2966
+ const { container, state: { events = {}, size = {}, ...rest }, } = this.portalInputs.snapshot;
2967
+ this.portalStore.update({
2968
+ scene: (is.ref(container) ? container.nativeElement : container),
2969
+ events: { ...parentState.events, ...events },
2970
+ size: { ...parentState.size, ...size },
2971
+ ...rest,
3171
2972
  });
3172
- }, { injector: this.injector });
3173
- requestAnimationFrame(() => {
3174
- this.portalStore.set((injectState) => this.inject(this.parentStore.get(), injectState));
3175
- });
3176
- this.portalContentView = this.portalContentAnchor.createEmbeddedView(this.portalContentTemplate);
3177
- safeDetectChanges(this.portalContentView);
3178
- this.portalContentRendered = true;
3179
- }
3180
- onBeforeRender(portal) {
3181
- this.beforeRender.emit({
3182
- root: { ...this.parentStore.get(), delta: portal.delta, frame: portal.frame },
3183
- portal,
2973
+ this.autoEffect(() => {
2974
+ const previous = this.parentStore.state();
2975
+ this.portalStore.update((state) => this.inject(previous, state));
2976
+ });
2977
+ untracked(() => {
2978
+ const portalView = this.portalContentAnchor.createEmbeddedView(this.portalContentTemplate);
2979
+ portalView.detectChanges();
2980
+ this.destroyRef.onDestroy(portalView.destroy.bind(portalView));
2981
+ });
2982
+ this.portalRendered.set(true);
3184
2983
  });
3185
2984
  }
3186
2985
  inject(rootState, injectState) {
@@ -3191,7 +2990,7 @@ class NgtPortal {
3191
2990
  delete intersect[key];
3192
2991
  }
3193
2992
  });
3194
- const inputs = this.inputs.get();
2993
+ const inputs = this.portalInputs.snapshot;
3195
2994
  const { size, events, ...restInputsState } = inputs.state || {};
3196
2995
  let viewport = undefined;
3197
2996
  if (injectState && size) {
@@ -3203,8 +3002,6 @@ class NgtPortal {
3203
3002
  return {
3204
3003
  ...intersect,
3205
3004
  scene: is.ref(inputs.container) ? inputs.container.nativeElement : inputs.container,
3206
- raycaster: this.raycaster,
3207
- pointer: this.pointer,
3208
3005
  previousRoot: this.parentStore,
3209
3006
  events: { ...rootState.events, ...(injectState?.events || {}), ...events },
3210
3007
  size: { ...rootState.size, ...size },
@@ -3212,51 +3009,47 @@ class NgtPortal {
3212
3009
  ...restInputsState,
3213
3010
  };
3214
3011
  }
3215
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtPortal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3216
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.2", type: NgtPortal, isStandalone: true, selector: "ngt-portal", inputs: { container: "container", portalState: ["state", "portalState"], autoRender: "autoRender", autoRenderPriority: "autoRenderPriority" }, outputs: { beforeRender: "beforeRender" }, providers: [{ provide: NGT_STORE, useFactory: () => signalStore({}) }], 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 }], ngImport: i0, template: `
3012
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtPortal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3013
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.2.1", type: NgtPortal, isStandalone: true, selector: "ngt-portal", inputs: { _portalInputs: ["options", "_portalInputs"], _autoRender: ["autoRender", "_autoRender"], autoRenderPriority: "autoRenderPriority" }, providers: [providePortalStore()], 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 }], ngImport: i0, template: `
3217
3014
  <ng-container #portalContentAnchor>
3218
- <ng-container
3219
- *ngIf="autoRender && portalContentRendered"
3220
- ngtPortalBeforeRender
3221
- [renderPriority]="autoRenderPriority"
3222
- [parentScene]="parentScene"
3223
- [parentCamera]="parentCamera"
3224
- (beforeRender)="onBeforeRender($event)"
3225
- />
3015
+ @if (renderAutoBeforeRender()) {
3016
+ <ngt-portal-before-render
3017
+ [renderPriority]="autoRenderPriority"
3018
+ [parentScene]="parentScene"
3019
+ [parentCamera]="parentCamera"
3020
+ />
3021
+ }
3226
3022
  </ng-container>
3227
- `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgtPortalBeforeRender, selector: "[ngtPortalBeforeRender]", inputs: ["renderPriority", "parentScene", "parentCamera"], outputs: ["beforeRender"] }] }); }
3023
+ `, isInline: true, dependencies: [{ kind: "component", type: NgtPortalBeforeRender, selector: "ngt-portal-before-render", inputs: ["renderPriority", "parentScene", "parentCamera"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3228
3024
  }
3229
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtPortal, decorators: [{
3025
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtPortal, decorators: [{
3230
3026
  type: Component,
3231
3027
  args: [{
3232
3028
  selector: 'ngt-portal',
3233
3029
  standalone: true,
3234
3030
  template: `
3235
3031
  <ng-container #portalContentAnchor>
3236
- <ng-container
3237
- *ngIf="autoRender && portalContentRendered"
3238
- ngtPortalBeforeRender
3239
- [renderPriority]="autoRenderPriority"
3240
- [parentScene]="parentScene"
3241
- [parentCamera]="parentCamera"
3242
- (beforeRender)="onBeforeRender($event)"
3243
- />
3032
+ @if (renderAutoBeforeRender()) {
3033
+ <ngt-portal-before-render
3034
+ [renderPriority]="autoRenderPriority"
3035
+ [parentScene]="parentScene"
3036
+ [parentCamera]="parentCamera"
3037
+ />
3038
+ }
3244
3039
  </ng-container>
3245
3040
  `,
3246
- imports: [NgIf, NgtPortalBeforeRender],
3247
- providers: [{ provide: NGT_STORE, useFactory: () => signalStore({}) }],
3041
+ imports: [NgtPortalBeforeRender],
3042
+ providers: [providePortalStore()],
3043
+ changeDetection: ChangeDetectionStrategy.OnPush,
3248
3044
  }]
3249
- }], ctorParameters: function () { return []; }, propDecorators: { container: [{
3250
- type: Input
3251
- }], portalState: [{
3045
+ }], ctorParameters: () => [], propDecorators: { _portalInputs: [{
3252
3046
  type: Input,
3253
- args: ['state']
3254
- }], autoRender: [{
3255
- type: Input
3047
+ args: [{ alias: 'options' }]
3048
+ }], _autoRender: [{
3049
+ type: Input,
3050
+ args: [{ alias: 'autoRender' }]
3256
3051
  }], autoRenderPriority: [{
3257
3052
  type: Input
3258
- }], beforeRender: [{
3259
- type: Output
3260
3053
  }], portalContentTemplate: [{
3261
3054
  type: ContentChild,
3262
3055
  args: [NgtPortalContent, { read: TemplateRef, static: true }]
@@ -3272,14 +3065,14 @@ class NgtRoutedScene {
3272
3065
  constructor(router, cdr) {
3273
3066
  router.events
3274
3067
  .pipe(filter((event) => event instanceof ActivationEnd), takeUntilDestroyed())
3275
- .subscribe(() => safeDetectChanges(cdr));
3068
+ .subscribe(cdr.detectChanges.bind(cdr));
3276
3069
  }
3277
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRoutedScene, deps: [{ token: i1.Router }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
3278
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.2", type: NgtRoutedScene, isStandalone: true, selector: "ngt-routed-scene", ngImport: i0, template: `
3070
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtRoutedScene, deps: [{ token: i1.Router }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
3071
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.2.1", type: NgtRoutedScene, isStandalone: true, selector: "ngt-routed-scene", ngImport: i0, template: `
3279
3072
  <router-outlet />
3280
3073
  `, isInline: true, dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }] }); }
3281
3074
  }
3282
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImport: i0, type: NgtRoutedScene, decorators: [{
3075
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.1", ngImport: i0, type: NgtRoutedScene, decorators: [{
3283
3076
  type: Component,
3284
3077
  args: [{
3285
3078
  standalone: true,
@@ -3289,11 +3082,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.2", ngImpor
3289
3082
  `,
3290
3083
  imports: [RouterOutlet],
3291
3084
  }]
3292
- }], ctorParameters: function () { return [{ type: i1.Router }, { type: i0.ChangeDetectorRef }]; } });
3085
+ }], ctorParameters: () => [{ type: i1.Router }, { type: i0.ChangeDetectorRef }] });
3086
+
3087
+ // TODO: use scheduler instead of force CD
3088
+ function cdAwareSignal(initialValue, { injector, ...options } = {}) {
3089
+ return assertInjector(cdAwareSignal, injector, () => {
3090
+ if (!options.equal) {
3091
+ options.equal = Object.is;
3092
+ }
3093
+ const cdr = inject(ChangeDetectorRef);
3094
+ const source = signal(initialValue, options);
3095
+ const originalSet = source.set.bind(source);
3096
+ const originalUpdate = source.update.bind(source);
3097
+ source.set = (...args) => {
3098
+ originalSet(...args);
3099
+ cdr.detectChanges();
3100
+ };
3101
+ source.update = (...args) => {
3102
+ originalUpdate(...args);
3103
+ cdr.detectChanges();
3104
+ };
3105
+ return source;
3106
+ });
3107
+ }
3108
+
3109
+ function apiFactory(obj) {
3110
+ return obj.api;
3111
+ }
3112
+ function createApiToken(forwardedObject) {
3113
+ const [injectFn, provideFn] = createInjectionToken((apiFactory), {
3114
+ isRoot: false,
3115
+ deps: [forwardRef(forwardedObject)],
3116
+ });
3117
+ return [injectFn, () => provideFn()];
3118
+ }
3293
3119
 
3294
3120
  /**
3295
3121
  * Generated bundle index. Do not edit.
3296
3122
  */
3297
3123
 
3298
- export { HTML, NGT_STORE, NgtArgs, NgtCanvas, NgtKey, NgtParent, NgtPortal, NgtPortalContent, NgtRepeat, NgtRoutedScene, addAfterEffect, addEffect, addTail, applyProps, assertInjectionContext, checkNeedsUpdate, checkUpdate, createAttachFunction, createInjectionToken, diffProps, extend, getLocalState, injectBeforeRender, injectNgtLoader, injectNgtRef, injectNgtStore, invalidateInstance, is, makeDefaultCamera, makeDefaultRenderer, makeDpr, makeId, makeObjectGraph, prepare, provideNgtRenderer, provideNgtStore, safeDetectChanges, signalStore, updateCamera };
3124
+ export { HTML, NGT_STORE, NgtArgs, NgtCanvas, NgtPortal, NgtPortalContent, NgtRenderer, NgtRendererFactory, NgtRoutedScene, ROUTED_SCENE, addAfterEffect, addEffect, addTail, applyProps, cdAwareSignal, checkNeedsUpdate, checkUpdate, createApiToken, createAttachFunction, extend, getLocalState, injectBeforeRender, injectNgtLoader, injectNgtRef, injectNgtStore, invalidateInstance, is, makeCameraInstance, makeDpr, makeId, makeObjectGraph, makeRendererInstance, prepare, provideNgtRenderer, provideNgtStore, signalStore, updateCamera };
3299
3125
  //# sourceMappingURL=angular-three.mjs.map