angular-three 4.2.3 → 4.2.4-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ViewContainerRef, TemplateRef, Injector, effect, DestroyRef, Directive, input, linkedSignal, computed, InjectionToken, untracked, isSignal, signal, DOCUMENT, Injectable, ElementRef, booleanAttribute, resource, Pipe, numberAttribute, contentChild, SkipSelf, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, Component, output, model, Renderer2 } from '@angular/core';
2
+ import { inject, ViewContainerRef, TemplateRef, Renderer2, Injector, effect, DestroyRef, Directive, input, linkedSignal, computed, InjectionToken, untracked, isSignal, signal, DOCUMENT, Injectable, ElementRef, booleanAttribute, resource, Pipe, numberAttribute, contentChild, SkipSelf, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, Component, output, model } from '@angular/core';
3
3
  import { Subject, filter } from 'rxjs';
4
4
  import * as THREE from 'three';
5
5
  import { Group } from 'three';
@@ -15,29 +15,118 @@ import { ActivationEnd, RouterOutlet } from '@angular/router';
15
15
  */
16
16
  /** Flag indicating a node is managed by the Angular Three renderer */
17
17
  const NGT_RENDERER_NODE_FLAG = '__ngt_renderer__';
18
- /** Flag for canvas content template comments */
19
- const NGT_CANVAS_CONTENT_FLAG = '__ngt_renderer_canvas_content__';
20
- /** Flag for portal content template comments */
21
- const NGT_PORTAL_CONTENT_FLAG = '__ngt_renderer_portal_content__';
22
- /** Flag for args directive comments */
23
- const NGT_ARGS_FLAG = '__ngt_renderer_args__';
24
- /** Flag for parent directive comments */
25
- const NGT_PARENT_FLAG = '__ngt_renderer_parent__';
26
- /** Internal flag for adding comment nodes */
27
- const NGT_INTERNAL_ADD_COMMENT_FLAG = '__ngt_renderer_add_comment__';
28
- /** Internal flag for setting parent on comment nodes */
29
- const NGT_INTERNAL_SET_PARENT_COMMENT_FLAG = '__ngt_renderer_set_parent_comment__';
18
+ /** Internal hook for scoping structural directive context to embedded view creation */
19
+ const NGT_RENDERER_CONTEXT_FLAG = '__ngt_renderer_context__';
30
20
  /** Flag for getting node attributes */
31
21
  const NGT_GET_NODE_ATTRIBUTE_FLAG = '__ngt_get_node_attribute__';
32
22
  /** Flag for DOM parent element reference */
33
23
  const NGT_DOM_PARENT_FLAG = '__ngt_dom_parent__';
34
- /** Flag indicating the delegate renderer's destroyNode has been patched */
35
- const NGT_DELEGATE_RENDERER_DESTROY_NODE_PATCHED_FLAG = '__ngt_delegate_renderer_destroy_node_patched__';
36
24
  /** Flag for HTML directive classes */
37
25
  const NGT_HTML_FLAG = '__ngt_html__';
38
26
  /** Native Three.js EventDispatcher events */
39
27
  const THREE_NATIVE_EVENTS = ['added', 'removed', 'childadded', 'childremoved', 'change', 'disposed'];
40
28
 
29
+ function isRendererNode(node) {
30
+ return !!node && typeof node === 'object' && NGT_RENDERER_NODE_FLAG in node;
31
+ }
32
+ function isRendererNodeType(node, type) {
33
+ return isRendererNode(node) && node.__ngt_renderer__[0 /* NgtRendererClassId.type */] === type;
34
+ }
35
+ function createRendererNode(type, node, document) {
36
+ const state = [
37
+ type,
38
+ false,
39
+ undefined,
40
+ undefined,
41
+ undefined,
42
+ [],
43
+ undefined,
44
+ undefined,
45
+ undefined,
46
+ undefined,
47
+ ];
48
+ const rendererNode = Object.assign(node, { [NGT_RENDERER_NODE_FLAG]: state });
49
+ // NOTE: assign ownerDocument to node so we can use HostListener in Component
50
+ if (!rendererNode['ownerDocument'])
51
+ rendererNode['ownerDocument'] = document;
52
+ // NOTE: Angular SSR calls `node.getAttribute()` to retrieve hydration info on a node
53
+ if (!('getAttribute' in rendererNode) || typeof rendererNode['getAttribute'] !== 'function') {
54
+ const getNodeAttribute = (name) => rendererNode[name];
55
+ getNodeAttribute[NGT_GET_NODE_ATTRIBUTE_FLAG] = true;
56
+ Object.defineProperty(rendererNode, 'getAttribute', { value: getNodeAttribute, configurable: true });
57
+ }
58
+ return rendererNode;
59
+ }
60
+ function removeRendererChildNode(parent, child) {
61
+ const parentState = parent.__ngt_renderer__;
62
+ const childState = child.__ngt_renderer__;
63
+ const childIndex = parentState[5 /* NgtRendererClassId.children */].indexOf(child);
64
+ if (childIndex >= 0)
65
+ parentState[5 /* NgtRendererClassId.children */].splice(childIndex, 1);
66
+ if (childState[4 /* NgtRendererClassId.parent */] === parent) {
67
+ childState[4 /* NgtRendererClassId.parent */] = undefined;
68
+ }
69
+ }
70
+ function insertRendererChildNode(parent, child, refChild) {
71
+ if (parent === child || refChild === child)
72
+ return;
73
+ const childState = child.__ngt_renderer__;
74
+ const previousParent = childState[4 /* NgtRendererClassId.parent */];
75
+ if (previousParent && isRendererNode(previousParent)) {
76
+ removeRendererChildNode(previousParent, child);
77
+ }
78
+ const children = parent.__ngt_renderer__[5 /* NgtRendererClassId.children */];
79
+ const refIndex = refChild ? children.indexOf(refChild) : -1;
80
+ children.splice(refIndex >= 0 ? refIndex : children.length, 0, child);
81
+ childState[4 /* NgtRendererClassId.parent */] = parent;
82
+ assignRendererOwner(parent, child, refChild);
83
+ }
84
+ function getRendererNextSibling(node) {
85
+ const parent = node.__ngt_renderer__[4 /* NgtRendererClassId.parent */];
86
+ if (!parent || !isRendererNode(parent))
87
+ return null;
88
+ const siblings = parent.__ngt_renderer__[5 /* NgtRendererClassId.children */];
89
+ const index = siblings.indexOf(node);
90
+ return index >= 0 ? (siblings[index + 1] ?? null) : null;
91
+ }
92
+ function setRendererAnchor(node, anchor) {
93
+ if (!isRendererNode(node))
94
+ return false;
95
+ node.__ngt_renderer__[7 /* NgtRendererClassId.anchor */] = anchor;
96
+ return true;
97
+ }
98
+ function getRendererAnchor(node) {
99
+ return isRendererNode(node) ? node.__ngt_renderer__[7 /* NgtRendererClassId.anchor */] : undefined;
100
+ }
101
+ function markRendererViewHost(node) {
102
+ const state = node.__ngt_renderer__;
103
+ state[9 /* NgtRendererClassId.ownedNodes */] ??= new Set();
104
+ }
105
+ function assignRendererOwner(parent, child, reference) {
106
+ const parentState = parent.__ngt_renderer__;
107
+ const childState = child.__ngt_renderer__;
108
+ if (childState[8 /* NgtRendererClassId.owner */])
109
+ return;
110
+ const parentOwner = parentState[9 /* NgtRendererClassId.ownedNodes */] ? parent : parentState[8 /* NgtRendererClassId.owner */];
111
+ // Canvas and portal anchors are owned by their declaring view but are not logical
112
+ // children of the Three scene returned from parentNode(). Angular still supplies the
113
+ // anchor as the insertion reference, making it the lifecycle bridge into that scene.
114
+ const referenceOwner = isRendererNode(reference) ? reference.__ngt_renderer__[8 /* NgtRendererClassId.owner */] : undefined;
115
+ const owner = parentOwner ?? referenceOwner;
116
+ if (!owner)
117
+ return;
118
+ childState[8 /* NgtRendererClassId.owner */] = owner;
119
+ owner.__ngt_renderer__[9 /* NgtRendererClassId.ownedNodes */]?.add(child);
120
+ }
121
+ function releaseRendererOwner(node, clear = true) {
122
+ const state = node.__ngt_renderer__;
123
+ const owner = state[8 /* NgtRendererClassId.owner */];
124
+ if (owner !== node)
125
+ owner?.__ngt_renderer__[9 /* NgtRendererClassId.ownedNodes */]?.delete(node);
126
+ if (clear)
127
+ state[8 /* NgtRendererClassId.owner */] = undefined;
128
+ }
129
+
41
130
  /**
42
131
  * Collection of type checking and comparison utilities for Angular Three.
43
132
  *
@@ -151,13 +240,19 @@ class NgtCommonDirective {
151
240
  constructor() {
152
241
  this.vcr = inject(ViewContainerRef);
153
242
  this.template = inject(TemplateRef);
243
+ this.renderer = inject(Renderer2);
154
244
  this.injector = inject(Injector);
155
245
  this.injected = false;
156
246
  this.injectedValue = null;
157
247
  effect(() => {
158
- if (this.shouldSkipRender())
159
- return;
160
248
  const value = this.linkedValue();
249
+ if (this.shouldSkipRender()) {
250
+ this.injected = false;
251
+ this.injectedValue = value;
252
+ this.view?.destroy();
253
+ this.view = undefined;
254
+ return;
255
+ }
161
256
  if (is.equ(value, this.injectedValue)) {
162
257
  // we have the same value as before, no need to update
163
258
  return;
@@ -184,8 +279,14 @@ class NgtCommonDirective {
184
279
  if (this.view && !this.view.destroyed)
185
280
  this.view.destroy();
186
281
  this.beforeCreateView();
187
- this.view = this.vcr.createEmbeddedView(this.template);
188
- this.view.detectChanges();
282
+ const createView = () => {
283
+ const view = this.vcr.createEmbeddedView(this.template);
284
+ view.detectChanges();
285
+ return view;
286
+ };
287
+ this.view = this.renderer[NGT_RENDERER_CONTEXT_FLAG]
288
+ ? this.renderer[NGT_RENDERER_CONTEXT_FLAG](this.injector, createView)
289
+ : createView();
189
290
  }
190
291
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtCommonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
191
292
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: NgtCommonDirective, isStandalone: true, ngImport: i0 }); }
@@ -222,12 +323,7 @@ class NgtArgs extends NgtCommonDirective {
222
323
  return args == null || !Array.isArray(args) || (args.length === 1 && args[0] === null);
223
324
  }, ...(ngDevMode ? [{ debugName: "shouldSkipRender" }] : /* istanbul ignore next */ []));
224
325
  const commentNode = this.commentNode;
225
- commentNode.data = NGT_ARGS_FLAG;
226
- commentNode[NGT_ARGS_FLAG] = true;
227
- if (commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]) {
228
- commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]('args', this.injector);
229
- delete commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG];
230
- }
326
+ setRendererAnchor(commentNode, { kind: 'args', injector: this.injector });
231
327
  }
232
328
  validate() {
233
329
  return !this.injected && !!this.injectedValue?.length;
@@ -299,12 +395,13 @@ function update(timestamp, store, frame) {
299
395
  let delta = state.clock.getDelta();
300
396
  // In frameloop='never' mode, clock times are updated using the provided timestamp
301
397
  if (state.frameloop === 'never' && typeof timestamp === 'number') {
302
- delta = timestamp - state.clock.elapsedTime;
303
- state.clock.oldTime = state.clock.elapsedTime;
304
- state.clock.elapsedTime = timestamp;
398
+ const elapsedTime = timestamp / 1000;
399
+ delta = elapsedTime - state.clock.elapsedTime;
400
+ state.clock.oldTime = timestamp;
401
+ state.clock.elapsedTime = elapsedTime;
305
402
  }
306
403
  // Call subscribers (beforeRender)
307
- const subscribers = state.internal.subscribers;
404
+ const subscribers = state.internal.subscribers.slice();
308
405
  for (let i = 0; i < subscribers.length; i++) {
309
406
  const subscription = subscribers[i];
310
407
  subscription.callback({ ...subscription.store.snapshot, delta, frame });
@@ -655,6 +752,7 @@ function getInstanceState(obj) {
655
752
  return undefined;
656
753
  return obj.__ngt__ || undefined;
657
754
  }
755
+ const pointerEventRegistrations = new WeakMap();
658
756
  /**
659
757
  * Invalidates an instance, triggering a re-render of the scene.
660
758
  *
@@ -719,8 +817,7 @@ function prepare(object, type, instanceState) {
719
817
  const [_nonObjects] = [nonObjects(), geometryStamp()];
720
818
  return _nonObjects;
721
819
  }, ...(ngDevMode ? [{ debugName: "nonObjectsChanged" }] : /* istanbul ignore next */ []));
722
- instance.__ngt_id__ =
723
- typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : uuidv4Fallback();
820
+ instance.__ngt_id__ = typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : uuidv4Fallback();
724
821
  instance.__ngt__ = {
725
822
  previousAttach: null,
726
823
  type,
@@ -731,16 +828,12 @@ function prepare(object, type, instanceState) {
731
828
  parent: hierarchyStore.parent,
732
829
  objects: hierarchyStore.objects,
733
830
  nonObjects: nonObjectsChanged,
734
- add(object, type) {
831
+ add(object, type, before) {
735
832
  const current = instance.__ngt__.hierarchyStore.snapshot[type];
736
- const foundIndex = current.findIndex((node) => object === node || (!!object['uuid'] && !!node['uuid'] && object['uuid'] === node['uuid']));
737
- if (foundIndex > -1) {
738
- current.splice(foundIndex, 1, object);
739
- instance.__ngt__.hierarchyStore.update({ [type]: current });
740
- }
741
- else {
742
- instance.__ngt__.hierarchyStore.update((prev) => ({ [type]: [...prev[type], object] }));
743
- }
833
+ const next = current.filter((node) => object !== node && (!object['uuid'] || !node['uuid'] || object['uuid'] !== node['uuid']));
834
+ const beforeIndex = before ? next.indexOf(before) : -1;
835
+ next.splice(beforeIndex >= 0 ? beforeIndex : next.length, 0, object);
836
+ instance.__ngt__.hierarchyStore.update({ [type]: next });
744
837
  notifyAncestors(instance.__ngt__.hierarchyStore.snapshot.parent, type);
745
838
  },
746
839
  remove(object, type) {
@@ -763,26 +856,42 @@ function prepare(object, type, instanceState) {
763
856
  setPointerEvent: {
764
857
  value: (eventName, callback) => {
765
858
  const iS = getInstanceState(instance);
766
- if (!iS.handlers)
767
- iS.handlers = {};
768
- // try to get the previous handler. compound might have one, the THREE object might also have one with the same name
769
- const previousHandler = iS.handlers[eventName];
770
- // readjust the callback
771
- const updatedCallback = (event) => {
772
- if (previousHandler)
773
- previousHandler(event);
774
- callback(event);
775
- };
776
- Object.assign(iS.handlers, { [eventName]: updatedCallback });
777
- // increment the count everytime
859
+ const handlers = (iS.handlers ??= {});
860
+ let registrations = pointerEventRegistrations.get(iS);
861
+ if (!registrations) {
862
+ registrations = new Map();
863
+ pointerEventRegistrations.set(iS, registrations);
864
+ }
865
+ let listeners = registrations.get(eventName);
866
+ if (!listeners) {
867
+ listeners = new Map();
868
+ registrations.set(eventName, listeners);
869
+ const existingHandler = handlers[eventName];
870
+ if (existingHandler)
871
+ listeners.set(Symbol('existing'), existingHandler);
872
+ }
873
+ const token = Symbol(eventName);
874
+ listeners.set(token, callback);
875
+ const dispatch = ((event) => {
876
+ for (const listener of [...listeners.values()])
877
+ listener(event);
878
+ });
879
+ handlers[eventName] = dispatch;
778
880
  iS.eventCount += 1;
779
- // clean up the event listener by removing the target from the interaction array
881
+ let active = true;
780
882
  return () => {
781
- const iS = getInstanceState(instance);
782
- if (iS) {
783
- iS.handlers && delete iS.handlers[eventName];
784
- iS.eventCount -= 1;
883
+ if (!active)
884
+ return;
885
+ active = false;
886
+ if (getInstanceState(instance) !== iS || !listeners?.delete(token))
887
+ return;
888
+ iS.eventCount = Math.max(0, iS.eventCount - 1);
889
+ if (listeners.size === 0) {
890
+ registrations?.delete(eventName);
891
+ delete handlers[eventName];
785
892
  }
893
+ if (iS.eventCount === 0)
894
+ iS.removeInteraction?.(iS.store);
786
895
  };
787
896
  },
788
897
  configurable: true,
@@ -818,10 +927,27 @@ function prepare(object, type, instanceState) {
818
927
  root = root.snapshot.previousRoot;
819
928
  }
820
929
  if (root.snapshot.internal) {
821
- const interactions = root.snapshot.internal.interaction;
822
- const index = interactions.findIndex((obj) => obj.uuid === instance.uuid);
930
+ const internal = root.snapshot.internal;
931
+ const object = instance;
932
+ const interactions = internal.interaction;
933
+ const index = interactions.findIndex((obj) => obj.uuid === object.uuid);
823
934
  if (index >= 0)
824
935
  interactions.splice(index, 1);
936
+ internal.initialHits = internal.initialHits.filter((hit) => hit !== object);
937
+ for (const [key, hovered] of internal.hovered) {
938
+ if (hovered.eventObject === object || hovered.object === object)
939
+ internal.hovered.delete(key);
940
+ }
941
+ for (const [pointerId, captures] of internal.capturedMap) {
942
+ const capture = captures.get(object);
943
+ if (!capture)
944
+ continue;
945
+ captures.delete(object);
946
+ if (captures.size === 0) {
947
+ internal.capturedMap.delete(pointerId);
948
+ capture.target.releasePointerCapture(pointerId);
949
+ }
950
+ }
825
951
  }
826
952
  },
827
953
  configurable: true,
@@ -829,7 +955,8 @@ function prepare(object, type, instanceState) {
829
955
  });
830
956
  return instance;
831
957
  }
832
- const notificationCache = new Map();
958
+ const pendingAncestorNotifications = new Map();
959
+ let ancestorNotificationScheduled = false;
833
960
  /**
834
961
  * Notify ancestors about changes to a THREE.js objects' children
835
962
  *
@@ -837,37 +964,41 @@ const notificationCache = new Map();
837
964
  * in which case the model matrices will be settled later. `NgtsCenter` needs to know about this
838
965
  * matrices change to re-center everything inside of it.
839
966
  *
840
- * The implementation here uses a naive approach to reduce the number of notifications; we cache
841
- * the notifications by the instance ID and the type of the notification.
842
- *
843
- * 1. If there's no cache or
844
- * 2. If the type is different for the same instance or
845
- * 3. We've skipped the notifications for this instance more than a certain amount
846
- *
847
- * then we'll proceed with notification
967
+ * Notifications are coalesced by ancestor and hierarchy kind. Angular renderer
968
+ * transactions flush them at `RendererFactory2.end`; direct calls use a microtask fallback.
848
969
  */
849
970
  function notifyAncestors(instance, type) {
850
- if (!instance)
851
- return;
852
- const localState = getInstanceState(instance);
853
- if (!localState)
854
- return;
855
- const id = instance.__ngt_id__ || instance['uuid'];
856
- if (!id)
857
- return;
858
- const maxNotificationSkipCount = localState.store?.snapshot.maxNotificationSkipCount || 5;
859
- const cached = notificationCache.get(id);
860
- if (!cached || cached.lastType !== type || cached.skipCount > maxNotificationSkipCount) {
861
- notificationCache.set(id, { skipCount: 0, lastType: type });
862
- if (notificationCache.size === 1) {
863
- queueMicrotask(() => notificationCache.clear());
864
- }
865
- const { parent } = localState.hierarchyStore.snapshot;
866
- localState.hierarchyStore.update({ [type]: (localState.hierarchyStore.snapshot[type] || []).slice() });
867
- notifyAncestors(parent, type);
971
+ let current = instance;
972
+ while (current) {
973
+ const localState = getInstanceState(current);
974
+ if (!localState?.hierarchyStore)
975
+ break;
976
+ let types = pendingAncestorNotifications.get(current);
977
+ if (!types)
978
+ pendingAncestorNotifications.set(current, (types = new Set()));
979
+ types.add(type);
980
+ current = localState.hierarchyStore.snapshot.parent;
981
+ }
982
+ if (pendingAncestorNotifications.size === 0 || ancestorNotificationScheduled)
868
983
  return;
984
+ ancestorNotificationScheduled = true;
985
+ queueMicrotask(flushAncestorNotifications);
986
+ }
987
+ /** @internal */
988
+ function flushAncestorNotifications() {
989
+ ancestorNotificationScheduled = false;
990
+ const batch = [...pendingAncestorNotifications];
991
+ pendingAncestorNotifications.clear();
992
+ for (const [ancestor, types] of batch) {
993
+ const localState = getInstanceState(ancestor);
994
+ if (!localState?.hierarchyStore)
995
+ continue;
996
+ const snapshot = localState.hierarchyStore.snapshot;
997
+ const update = {};
998
+ for (const pendingType of types)
999
+ update[pendingType] = snapshot[pendingType].slice();
1000
+ localState.hierarchyStore.update(update);
869
1001
  }
870
- notificationCache.set(id, { ...cached, skipCount: cached.skipCount + 1 });
871
1002
  }
872
1003
 
873
1004
  /**
@@ -890,9 +1021,9 @@ function diffProps(instance, props) {
890
1021
  key = 'outputColorSpace';
891
1022
  }
892
1023
  }
893
- if (is.equ(propValue, instance[key]))
1024
+ if (is.equ(propValue, resolveInstanceKey(instance, key).targetProp))
894
1025
  continue;
895
- changes.push([propKey, propValue]);
1026
+ changes.push([key, propValue]);
896
1027
  }
897
1028
  return changes;
898
1029
  }
@@ -906,6 +1037,41 @@ const NGT_APPLY_PROPS = '__ngt_apply_props__';
906
1037
  // https://github.com/mrdoob/three.js/pull/27042
907
1038
  // https://github.com/mrdoob/three.js/pull/22748
908
1039
  const colorMaps = ['map', 'emissiveMap', 'sheenColorMap', 'specularColorMap', 'envMap'];
1040
+ const defaultProperties = new WeakMap();
1041
+ function snapshotPropertyValue(value) {
1042
+ if (value instanceof THREE.Layers) {
1043
+ const snapshot = new THREE.Layers();
1044
+ snapshot.mask = value.mask;
1045
+ return snapshot;
1046
+ }
1047
+ if (ArrayBuffer.isView(value) && 'slice' in value && typeof value.slice === 'function') {
1048
+ return value.slice();
1049
+ }
1050
+ if (value &&
1051
+ typeof value === 'object' &&
1052
+ 'clone' in value &&
1053
+ typeof value.clone === 'function' &&
1054
+ 'set' in value &&
1055
+ typeof value.set === 'function' &&
1056
+ 'copy' in value &&
1057
+ typeof value.copy === 'function') {
1058
+ // Math-like values are mutated in place by applyProps and need a value
1059
+ // snapshot. Disposable resources and other assigned objects deliberately
1060
+ // retain their original identity for default restoration.
1061
+ return value.clone();
1062
+ }
1063
+ return value;
1064
+ }
1065
+ function resolvePropertyValue(root, key, currentValue, nextValue) {
1066
+ let defaults = defaultProperties.get(root);
1067
+ if (!defaults) {
1068
+ defaults = new Map();
1069
+ defaultProperties.set(root, defaults);
1070
+ }
1071
+ if (!defaults.has(key))
1072
+ defaults.set(key, snapshotPropertyValue(currentValue));
1073
+ return nextValue === undefined ? snapshotPropertyValue(defaults.get(key)) : nextValue;
1074
+ }
909
1075
  /**
910
1076
  * Resolves a property key that may contain dot notation (pierced props).
911
1077
  *
@@ -981,11 +1147,7 @@ function applyProps(instance, props) {
981
1147
  const rootState = localState?.store?.snapshot ?? instance[NGT_APPLY_PROPS]?.snapshot ?? {};
982
1148
  const changes = diffProps(instance, props);
983
1149
  for (let i = 0; i < changes.length; i++) {
984
- let [key, value] = changes[i];
985
- // Ignore setting undefined props
986
- // https://github.com/pmndrs/react-three-fiber/issues/274
987
- if (value === undefined)
988
- continue;
1150
+ const [key, nextValue] = changes[i];
989
1151
  // Alias (output)encoding => (output)colorSpace (since r152)
990
1152
  // https://github.com/pmndrs/react-three-fiber/pull/2829
991
1153
  // if (is.colorSpaceExist(instance)) {
@@ -1002,10 +1164,7 @@ function applyProps(instance, props) {
1002
1164
  // }
1003
1165
  // }
1004
1166
  const { root, targetKey, targetProp } = resolveInstanceKey(instance, key);
1005
- // we have switched due to pierced props
1006
- if (root !== instance) {
1007
- return applyProps(root, { [targetKey]: value });
1008
- }
1167
+ const value = resolvePropertyValue(root, targetKey, targetProp, nextValue);
1009
1168
  // Layers have no copy function, we must therefore copy the mask property
1010
1169
  if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers) {
1011
1170
  targetProp.mask = value.mask;
@@ -1064,22 +1223,13 @@ function applyProps(instance, props) {
1064
1223
  }
1065
1224
  checkUpdate(root[targetKey]);
1066
1225
  checkUpdate(targetProp);
1067
- invalidateInstance(instance);
1068
1226
  }
1069
- const instanceHandlersCount = localState?.eventCount;
1070
1227
  const parent = localState?.hierarchyStore?.snapshot.parent;
1071
- if (parent && rootState.internal && instance['raycast'] && instanceHandlersCount !== localState?.eventCount) {
1072
- // Pre-emptively remove the instance from the interaction manager
1073
- const index = rootState.internal.interaction.indexOf(instance);
1074
- if (index > -1)
1075
- rootState.internal.interaction.splice(index, 1);
1076
- // Add the instance to the interaction manager only when it has handlers
1077
- if (localState?.eventCount)
1078
- rootState.internal.interaction.push(instance);
1079
- }
1080
1228
  if (parent && localState?.onUpdate && changes.length) {
1081
1229
  localState.onUpdate(instance);
1082
1230
  }
1231
+ if (changes.length)
1232
+ invalidateInstance(instance);
1083
1233
  // clearing the intermediate store from the instance
1084
1234
  if (instance[NGT_APPLY_PROPS])
1085
1235
  delete instance[NGT_APPLY_PROPS];
@@ -1093,6 +1243,7 @@ function applyProps(instance, props) {
1093
1243
  * allowing the custom renderer to instantiate objects when elements are created.
1094
1244
  */
1095
1245
  const catalogue = {};
1246
+ const catalogueOwnership = new Map();
1096
1247
  /**
1097
1248
  * Registers Three.js constructors for use in templates.
1098
1249
  *
@@ -1118,10 +1269,59 @@ const catalogue = {};
1118
1269
  * ```
1119
1270
  */
1120
1271
  function extend(objects) {
1121
- const keys = Object.keys(objects);
1122
- Object.assign(catalogue, objects);
1272
+ const registrations = Object.entries(objects).map(([key, value]) => {
1273
+ let ownership = catalogueOwnership.get(key);
1274
+ if (!ownership) {
1275
+ ownership = {
1276
+ baseline: catalogue[key],
1277
+ hadBaseline: Object.prototype.hasOwnProperty.call(catalogue, key),
1278
+ registrations: [],
1279
+ };
1280
+ catalogueOwnership.set(key, ownership);
1281
+ }
1282
+ const registrationValue = value;
1283
+ let registration = ownership.registrations.at(-1);
1284
+ if (!registration || registration.value !== registrationValue) {
1285
+ registration = { value: registrationValue, count: 0 };
1286
+ ownership.registrations.push(registration);
1287
+ }
1288
+ // Repeated component-level extend calls commonly register the same Three
1289
+ // constructor. Coalesce those owners so the catalogue retains one layer,
1290
+ // not one record per component instance.
1291
+ registration.count++;
1292
+ catalogue[key] = registration.value;
1293
+ return { key, ownership, registration };
1294
+ });
1295
+ let cleaned = false;
1123
1296
  return () => {
1124
- remove(...keys);
1297
+ if (cleaned)
1298
+ return;
1299
+ cleaned = true;
1300
+ for (const { key, ownership, registration } of registrations) {
1301
+ // A public remove followed by a new extend starts a new ownership era. An
1302
+ // older cleanup must never affect that newer registration.
1303
+ if (catalogueOwnership.get(key) !== ownership)
1304
+ continue;
1305
+ registration.count--;
1306
+ if (registration.count > 0)
1307
+ continue;
1308
+ const registrationIndex = ownership.registrations.indexOf(registration);
1309
+ if (registrationIndex === -1)
1310
+ continue;
1311
+ ownership.registrations.splice(registrationIndex, 1);
1312
+ const activeRegistration = ownership.registrations.at(-1);
1313
+ if (activeRegistration) {
1314
+ catalogue[key] = activeRegistration.value;
1315
+ continue;
1316
+ }
1317
+ catalogueOwnership.delete(key);
1318
+ if (ownership.hadBaseline) {
1319
+ catalogue[key] = ownership.baseline;
1320
+ }
1321
+ else {
1322
+ delete catalogue[key];
1323
+ }
1324
+ }
1125
1325
  };
1126
1326
  }
1127
1327
  /**
@@ -1131,6 +1331,9 @@ function extend(objects) {
1131
1331
  */
1132
1332
  function remove(...keys) {
1133
1333
  for (const key of keys) {
1334
+ // Explicit removal is authoritative. Invalidate all outstanding cleanup
1335
+ // tokens so they cannot restore an older value later.
1336
+ catalogueOwnership.delete(key);
1134
1337
  delete catalogue[key];
1135
1338
  }
1136
1339
  }
@@ -1147,34 +1350,6 @@ function injectCatalogue() {
1147
1350
  return inject(NGT_CATALOGUE);
1148
1351
  }
1149
1352
 
1150
- function isRendererNode(node) {
1151
- return !!node && typeof node === 'object' && NGT_RENDERER_NODE_FLAG in node;
1152
- }
1153
- function createRendererNode(type, node, document) {
1154
- const state = [type, false, undefined, undefined, undefined, undefined, []];
1155
- const rendererNode = Object.assign(node, { [NGT_RENDERER_NODE_FLAG]: state });
1156
- // NOTE: assign ownerDocument to node so we can use HostListener in Component
1157
- if (!rendererNode['ownerDocument'])
1158
- rendererNode['ownerDocument'] = document;
1159
- // NOTE: Angular SSR calls `node.getAttribute()` to retrieve hydration info on a node
1160
- if (!('getAttribute' in rendererNode) || typeof rendererNode['getAttribute'] !== 'function') {
1161
- const getNodeAttribute = (name) => rendererNode[name];
1162
- getNodeAttribute[NGT_GET_NODE_ATTRIBUTE_FLAG] = true;
1163
- Object.defineProperty(rendererNode, 'getAttribute', { value: getNodeAttribute, configurable: true });
1164
- }
1165
- return rendererNode;
1166
- }
1167
- function setRendererParentNode(node, parent) {
1168
- if (!node.__ngt_renderer__[5 /* NgtRendererClassId.parent */]) {
1169
- node.__ngt_renderer__[5 /* NgtRendererClassId.parent */] = parent;
1170
- }
1171
- }
1172
- function addRendererChildNode(node, child) {
1173
- if (!node.__ngt_renderer__[6 /* NgtRendererClassId.children */].includes(child)) {
1174
- node.__ngt_renderer__[6 /* NgtRendererClassId.children */].push(child);
1175
- }
1176
- }
1177
-
1178
1353
  const idCache = {};
1179
1354
  /**
1180
1355
  * Generates a unique identifier.
@@ -1281,6 +1456,7 @@ function makeObjectGraph(object) {
1281
1456
  return data;
1282
1457
  }
1283
1458
 
1459
+ const NGT_EVENT_LAYER = Symbol('NGT_EVENT_LAYER');
1284
1460
  /**
1285
1461
  * @fileoverview Event handling system for Angular Three.
1286
1462
  *
@@ -1374,52 +1550,57 @@ function createEvents(store) {
1374
1550
  if (!current)
1375
1551
  eventsObjects.push(eventsObject);
1376
1552
  }
1377
- if (!state.previousRoot) {
1378
- // Make sure root-level pointer and ray are set up
1379
- state.events.compute?.(event, store, null);
1380
- }
1381
- // Skip work if there are no event objects
1382
- if (eventsObjects.length === 0)
1383
- return intersections;
1384
- // Reset all raycaster cameras to undefined - use for loop for better performance
1385
- const eventsObjectsLen = eventsObjects.length;
1386
- for (let i = 0; i < eventsObjectsLen; i++) {
1387
- const objectRootState = getInstanceState(eventsObjects[i])?.store?.snapshot;
1388
- if (objectRootState) {
1389
- objectRootState.raycaster.camera = undefined;
1390
- }
1553
+ const objectsByLayer = new Map();
1554
+ for (const object of eventsObjects) {
1555
+ const objectStore = getInstanceState(object)?.store;
1556
+ if (!objectStore)
1557
+ continue;
1558
+ const layerObjects = objectsByLayer.get(objectStore);
1559
+ if (layerObjects)
1560
+ layerObjects.push(object);
1561
+ else
1562
+ objectsByLayer.set(objectStore, [object]);
1391
1563
  }
1392
- // Pre-allocate array to avoid garbage collection
1564
+ // The root computation must precede portal/layer computations, which may derive their
1565
+ // pointer or ray from previousRoot. Each layer is reset, computed, and raycast once.
1566
+ const eventLayers = [];
1567
+ const scheduledLayers = new Set();
1568
+ const scheduleLayer = (eventLayer) => {
1569
+ if (scheduledLayers.has(eventLayer))
1570
+ return;
1571
+ scheduledLayers.add(eventLayer);
1572
+ const previousRoot = eventLayer.snapshot.previousRoot;
1573
+ if (previousRoot)
1574
+ scheduleLayer(previousRoot);
1575
+ eventLayers.push(eventLayer);
1576
+ };
1577
+ scheduleLayer(store);
1578
+ for (const objectStore of objectsByLayer.keys())
1579
+ scheduleLayer(objectStore);
1580
+ for (const eventLayer of eventLayers)
1581
+ eventLayer.snapshot.raycaster.camera = undefined;
1393
1582
  const raycastResults = [];
1394
- function handleRaycast(obj) {
1395
- const objStore = getInstanceState(obj)?.store;
1396
- const objState = objStore?.snapshot;
1397
- // Skip event handling when noEvents is set, or when the raycasters camera is null
1398
- if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
1399
- return [];
1400
- // When the camera is undefined we have to call the event layers update function
1401
- if (objState.raycaster.camera === undefined) {
1402
- objState.events.compute?.(event, objStore, objState.previousRoot);
1403
- // If the camera is still undefined we have to skip this layer entirely
1404
- if (objState.raycaster.camera === undefined)
1405
- objState.raycaster.camera = null;
1406
- }
1407
- // Intersect object by object
1408
- return objState.raycaster.camera ? objState.raycaster.intersectObject(obj, true) : [];
1409
- }
1410
- // Collect events
1411
- for (let i = 0; i < eventsObjectsLen; i++) {
1412
- const objResults = handleRaycast(eventsObjects[i]);
1413
- if (objResults.length <= 0)
1583
+ for (const eventLayer of eventLayers) {
1584
+ const layerState = eventLayer.snapshot;
1585
+ if (!layerState.events.enabled)
1586
+ continue;
1587
+ layerState.events.compute?.(event, eventLayer, layerState.previousRoot);
1588
+ if (layerState.raycaster.camera === undefined)
1589
+ layerState.raycaster.camera = null;
1590
+ const layerObjects = objectsByLayer.get(eventLayer);
1591
+ if (!layerState.raycaster.camera || !layerObjects?.length)
1414
1592
  continue;
1415
- for (let j = 0; j < objResults.length; j++) {
1416
- raycastResults.push(objResults[j]);
1593
+ const layerResults = layerState.raycaster.intersectObjects(layerObjects, true);
1594
+ for (let index = 0; index < layerResults.length; index++) {
1595
+ const result = layerResults[index];
1596
+ result[NGT_EVENT_LAYER] = eventLayer;
1597
+ raycastResults.push(result);
1417
1598
  }
1418
1599
  }
1419
1600
  // Sort by event priority and distance
1420
1601
  raycastResults.sort((a, b) => {
1421
- const aState = getInstanceState(a.object)?.store?.snapshot;
1422
- const bState = getInstanceState(b.object)?.store?.snapshot;
1602
+ const aState = a[NGT_EVENT_LAYER]?.snapshot ?? getInstanceState(a.object)?.store?.snapshot;
1603
+ const bState = b[NGT_EVENT_LAYER]?.snapshot ?? getInstanceState(b.object)?.store?.snapshot;
1423
1604
  if (!aState || !bState)
1424
1605
  return a.distance - b.distance;
1425
1606
  return bState.events.priority - aState.events.priority || a.distance - b.distance;
@@ -1481,7 +1662,8 @@ function createEvents(store) {
1481
1662
  return;
1482
1663
  });
1483
1664
  }
1484
- const { raycaster, pointer, camera, internal } = instanceState?.store?.snapshot || rootState;
1665
+ const eventLayer = hit[NGT_EVENT_LAYER];
1666
+ const { raycaster, pointer, camera, internal } = eventLayer?.snapshot || instanceState?.store?.snapshot || rootState;
1485
1667
  const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
1486
1668
  const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1487
1669
  const setPointerCapture = (id) => {
@@ -1628,6 +1810,15 @@ function createEvents(store) {
1628
1810
  const hits = intersect(event, filter);
1629
1811
  // Only calculate distance for click events to avoid unnecessary math
1630
1812
  const delta = isClickEvent ? calculateDistance(event) : 0;
1813
+ let missedNotified = false;
1814
+ const notifyMissedOnce = () => {
1815
+ if (missedNotified || !isClickEvent)
1816
+ return;
1817
+ missedNotified = true;
1818
+ const missedObjects = internal.interaction.filter((object) => !internal.initialHits.includes(object));
1819
+ if (missedObjects.length > 0)
1820
+ pointerMissed(event, missedObjects);
1821
+ };
1631
1822
  // Save initial coordinates on pointer-down
1632
1823
  if (name === 'pointerdown') {
1633
1824
  internal.initialClick = [event.offsetX, event.offsetY];
@@ -1685,23 +1876,13 @@ function createEvents(store) {
1685
1876
  // Forward all events back to their respective handlers with the exception of click events,
1686
1877
  // which must use the initial target
1687
1878
  if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1688
- // Get objects not in initialHits for pointer missed - avoid creating new arrays if possible
1689
- const missedObjects = internal.interaction.filter((object) => !internal.initialHits.includes(object));
1690
- // Call pointerMissed only if we have objects to notify
1691
- if (missedObjects.length > 0) {
1692
- pointerMissed(event, missedObjects);
1693
- }
1879
+ notifyMissedOnce();
1694
1880
  // Now call the handler
1695
1881
  handler(data);
1696
1882
  }
1697
1883
  }
1698
1884
  else if (isClickEvent && internal.initialHits.includes(eventObject)) {
1699
- // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
1700
- const missedObjects = internal.interaction.filter((object) => !internal.initialHits.includes(object));
1701
- // Call pointerMissed only if we have objects to notify
1702
- if (missedObjects.length > 0) {
1703
- pointerMissed(event, missedObjects);
1704
- }
1885
+ notifyMissedOnce();
1705
1886
  }
1706
1887
  }
1707
1888
  }
@@ -1760,10 +1941,22 @@ function attach(object, value, paths = [], useApplyProps = false) {
1760
1941
  function detach(parent, child, attachProp) {
1761
1942
  const childInstanceState = getInstanceState(child);
1762
1943
  if (childInstanceState) {
1763
- if (Array.isArray(attachProp))
1764
- attach(parent, childInstanceState.previousAttach, attachProp, childInstanceState.type === 'ngt-value');
1944
+ const previousAttach = childInstanceState.previousAttach;
1945
+ childInstanceState.previousAttach = undefined;
1946
+ if (Array.isArray(attachProp)) {
1947
+ const attachedValue = attachProp.reduce((value, key) => (value == null ? undefined : value[key]), parent);
1948
+ // Property bindings are applied after Angular has appended every sibling.
1949
+ // An auto-attached resource can therefore retain a stale restoration value
1950
+ // after a later sibling has replaced the same slot. Only the resource that
1951
+ // still owns the slot may restore it. Raw values use applyProps and can be
1952
+ // represented by a transformed value (for example, a THREE.Color), so their
1953
+ // detach path remains unconditional.
1954
+ if (childInstanceState.type !== 'ngt-value' && attachedValue !== child)
1955
+ return;
1956
+ attach(parent, previousAttach, attachProp, childInstanceState.type === 'ngt-value');
1957
+ }
1765
1958
  else
1766
- childInstanceState.previousAttach?.();
1959
+ previousAttach?.();
1767
1960
  }
1768
1961
  }
1769
1962
  function assignEmpty(obj, base, shouldAssignStoreForApplyProps = false) {
@@ -1823,18 +2016,21 @@ function kebabToPascal(str) {
1823
2016
  }
1824
2017
  return pascalStr;
1825
2018
  }
1826
- function propagateStoreRecursively(node, parentNode) {
2019
+ function propagateStoreRecursively(node, parentNode, storeOverride) {
1827
2020
  const iS = getInstanceState(node);
1828
2021
  const pIS = getInstanceState(parentNode);
1829
2022
  if (!iS || !pIS)
1830
2023
  return;
2024
+ const store = storeOverride ?? pIS.store;
2025
+ if (!store)
2026
+ return;
1831
2027
  // assign store on child if not already exist
1832
2028
  // or child store is not the same as parent store
1833
2029
  // or child store is the parent of parent store
1834
- if (!iS.store || iS.store !== pIS.store || iS.store === pIS.store.snapshot.previousRoot) {
1835
- iS.store = pIS.store;
2030
+ if (!iS.store || iS.store !== store || iS.store === store.snapshot.previousRoot) {
2031
+ iS.store = store;
1836
2032
  // Call addInteraction if it exists
1837
- iS.addInteraction?.(pIS.store);
2033
+ iS.addInteraction?.(store);
1838
2034
  // Collect all children (objects and nonObjects)
1839
2035
  const children = [
1840
2036
  ...(iS.objects ? untracked(iS.objects) : []),
@@ -1842,60 +2038,88 @@ function propagateStoreRecursively(node, parentNode) {
1842
2038
  ];
1843
2039
  // Recursively reassign the store for each child
1844
2040
  for (const child of children) {
1845
- propagateStoreRecursively(child, node);
2041
+ propagateStoreRecursively(child, node, store);
1846
2042
  }
1847
2043
  }
1848
2044
  }
1849
- function attachThreeNodes(parent, child) {
2045
+ function placeObject3DChild(parent, child, before) {
2046
+ if (child.parent !== parent)
2047
+ parent.add(child);
2048
+ const currentIndex = parent.children.indexOf(child);
2049
+ if (currentIndex < 0)
2050
+ return;
2051
+ parent.children.splice(currentIndex, 1);
2052
+ const beforeIndex = before ? parent.children.indexOf(before) : -1;
2053
+ const insertionIndex = beforeIndex < 0 ? parent.children.length : Math.min(beforeIndex, parent.children.length);
2054
+ parent.children.splice(insertionIndex, 0, child);
2055
+ }
2056
+ function attachThreeNodes(parent, child, before, storeOverride) {
1850
2057
  const pIS = getInstanceState(parent);
1851
2058
  const cIS = getInstanceState(child);
1852
2059
  if (!pIS || !cIS) {
1853
2060
  throw new Error(`[NGT] THREE instances need to be prepared with local state.`);
1854
2061
  }
2062
+ propagateStoreRecursively(child, parent, storeOverride);
2063
+ if (untracked(cIS.parent) === parent) {
2064
+ if (!cIS.attach &&
2065
+ is.three(parent, 'isObject3D') &&
2066
+ is.three(child, 'isObject3D')) {
2067
+ placeObject3DChild(parent, child, before);
2068
+ pIS.add?.(child, 'objects', before);
2069
+ }
2070
+ else {
2071
+ // Attached resources have no physical sibling order. A logical move must
2072
+ // not invoke their attach callback or overwrite their restoration value.
2073
+ pIS.add?.(child, 'nonObjects', before);
2074
+ }
2075
+ invalidateInstance(parent);
2076
+ return;
2077
+ }
1855
2078
  // whether the child is added to the parent with parent.add()
1856
2079
  let added = false;
1857
- // propagate store recursively
1858
- propagateStoreRecursively(child, parent);
1859
2080
  if (cIS.attach) {
1860
2081
  const attachProp = cIS.attach;
1861
2082
  if (typeof attachProp === 'function') {
1862
2083
  let attachCleanUp = undefined;
1863
2084
  if (cIS.type === 'ngt-value') {
1864
- if (cIS.hierarchyStore.snapshot.parent !== parent) {
1865
- cIS.setParent(parent);
1866
- }
1867
2085
  // at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
1868
2086
  if (child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */] === undefined)
1869
2087
  return;
2088
+ if (cIS.hierarchyStore.snapshot.parent !== parent) {
2089
+ cIS.setParent(parent);
2090
+ }
1870
2091
  attachCleanUp = attachProp(parent, child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */], cIS.store);
1871
2092
  }
1872
2093
  else {
1873
2094
  attachCleanUp = attachProp(parent, child, cIS.store);
1874
2095
  }
1875
- if (attachCleanUp)
1876
- cIS.previousAttach = attachCleanUp;
2096
+ cIS.previousAttach = attachCleanUp;
1877
2097
  }
1878
2098
  else {
1879
2099
  // we skip attach none if set explicitly
1880
2100
  if (attachProp[0] === 'none') {
2101
+ cIS.previousAttach = undefined;
2102
+ pIS.add?.(child, 'nonObjects', before);
2103
+ cIS.setParent(parent);
1881
2104
  invalidateInstance(child);
2105
+ invalidateInstance(parent);
1882
2106
  return;
1883
2107
  }
1884
2108
  // handle material array
1885
2109
  if (attachProp[0] === 'material' &&
1886
- attachProp[1] &&
1887
- typeof Number(attachProp[1]) === 'number' &&
2110
+ attachProp[1] !== undefined &&
2111
+ !Number.isNaN(Number(attachProp[1])) &&
1888
2112
  is.three(child, 'isMaterial') &&
1889
2113
  !Array.isArray(parent['material'])) {
1890
2114
  parent['material'] = [];
1891
2115
  }
1892
2116
  if (cIS.type === 'ngt-value') {
1893
- if (cIS.hierarchyStore.snapshot.parent !== parent) {
1894
- cIS.setParent(parent);
1895
- }
1896
2117
  // at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
1897
2118
  if (child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */] === undefined)
1898
2119
  return;
2120
+ if (cIS.hierarchyStore.snapshot.parent !== parent) {
2121
+ cIS.setParent(parent);
2122
+ }
1899
2123
  // save prev value
1900
2124
  cIS.previousAttach = attachProp.reduce((value, key) => value[key], parent);
1901
2125
  attach(parent, child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */], attachProp, true);
@@ -1908,12 +2132,12 @@ function attachThreeNodes(parent, child) {
1908
2132
  }
1909
2133
  }
1910
2134
  else if (is.three(parent, 'isObject3D') && is.three(child, 'isObject3D')) {
1911
- parent.add(child);
2135
+ placeObject3DChild(parent, child, before);
1912
2136
  added = true;
1913
2137
  cIS.addInteraction?.(cIS.store || pIS.store);
1914
2138
  }
1915
2139
  if (pIS.add) {
1916
- pIS.add(child, added ? 'objects' : 'nonObjects');
2140
+ pIS.add(child, added ? 'objects' : 'nonObjects', before);
1917
2141
  }
1918
2142
  if (cIS.parent && untracked(cIS.parent) !== parent) {
1919
2143
  cIS.setParent(parent);
@@ -1925,7 +2149,7 @@ function attachThreeNodes(parent, child) {
1925
2149
  invalidateInstance(child);
1926
2150
  invalidateInstance(parent);
1927
2151
  }
1928
- function removeThreeChild(child, parent, dispose) {
2152
+ function removeThreeChild(child, parent, dispose = false) {
1929
2153
  const pIS = getInstanceState(parent);
1930
2154
  const cIS = getInstanceState(child);
1931
2155
  // clear parent ref
@@ -1945,8 +2169,9 @@ function removeThreeChild(child, parent, dispose) {
1945
2169
  }
1946
2170
  // dispose
1947
2171
  const isPrimitive = cIS?.type && cIS.type === 'ngt-primitive';
1948
- if (!isPrimitive && child['dispose'] && !is.three(child, 'isScene')) {
1949
- queueMicrotask(() => child['dispose']());
2172
+ if (dispose && !isPrimitive && child['dispose'] && !is.three(child, 'isScene')) {
2173
+ const disposeInstance = child['dispose'].bind(child);
2174
+ queueMicrotask(disposeInstance);
1950
2175
  }
1951
2176
  invalidateInstance(parent);
1952
2177
  }
@@ -1954,18 +2179,31 @@ function internalDestroyNode(node, removeChild) {
1954
2179
  const rS = node.__ngt_renderer__;
1955
2180
  if (!rS || rS[1 /* NgtRendererClassId.destroyed */])
1956
2181
  return;
1957
- for (const child of rS[6 /* NgtRendererClassId.children */].slice()) {
2182
+ const iS = getInstanceState(node);
2183
+ const physicalParent = iS?.parent ? untracked(iS.parent) : null;
2184
+ if (physicalParent) {
2185
+ removeThreeChild(node, physicalParent, false);
2186
+ }
2187
+ for (const child of rS[5 /* NgtRendererClassId.children */].slice()) {
2188
+ const destructionOwner = rS[9 /* NgtRendererClassId.ownedNodes */] ? node : rS[8 /* NgtRendererClassId.owner */];
2189
+ const childOwner = child.__ngt_renderer__[8 /* NgtRendererClassId.owner */];
1958
2190
  removeChild?.(node, child);
2191
+ if (destructionOwner && childOwner && childOwner !== destructionOwner)
2192
+ continue;
1959
2193
  internalDestroyNode(child, removeChild);
1960
2194
  }
1961
2195
  // clear out parent if haven't
1962
- rS[5 /* NgtRendererClassId.parent */] = undefined;
2196
+ rS[4 /* NgtRendererClassId.parent */] = undefined;
1963
2197
  // clear out children
1964
- rS[6 /* NgtRendererClassId.children */].length = 0;
2198
+ rS[5 /* NgtRendererClassId.children */].length = 0;
1965
2199
  // clear out NgtInstanceState
1966
- const iS = getInstanceState(node);
1967
2200
  if (iS) {
1968
2201
  const temp = iS;
2202
+ const isPrimitive = iS.type === 'ngt-primitive';
2203
+ if (!isPrimitive && node['dispose'] && !is.three(node, 'isScene')) {
2204
+ const disposeInstance = node['dispose'].bind(node);
2205
+ queueMicrotask(disposeInstance);
2206
+ }
1969
2207
  iS.removeInteraction?.(iS.store);
1970
2208
  delete temp['onAttach'];
1971
2209
  delete temp['onUpdate'];
@@ -1988,13 +2226,12 @@ function internalDestroyNode(node, removeChild) {
1988
2226
  delete node['__ngt__'];
1989
2227
  }
1990
2228
  }
1991
- // clear our debugNode
1992
- rS[4 /* NgtRendererClassId.injector */] = undefined;
2229
+ // clear renderer metadata
2230
+ rS[6 /* NgtRendererClassId.parentOverride */] = undefined;
2231
+ rS[7 /* NgtRendererClassId.anchor */] = undefined;
2232
+ rS[8 /* NgtRendererClassId.owner */] = undefined;
2233
+ rS[9 /* NgtRendererClassId.ownedNodes */]?.clear();
1993
2234
  if (rS[0 /* NgtRendererClassId.type */] === 'comment') {
1994
- delete node[NGT_INTERNAL_ADD_COMMENT_FLAG];
1995
- delete node[NGT_INTERNAL_SET_PARENT_COMMENT_FLAG];
1996
- delete node[NGT_CANVAS_CONTENT_FLAG];
1997
- delete node[NGT_PORTAL_CONTENT_FLAG];
1998
2235
  delete node[NGT_DOM_PARENT_FLAG];
1999
2236
  }
2000
2237
  // clear getAttribute if exist
@@ -2007,10 +2244,19 @@ function internalDestroyNode(node, removeChild) {
2007
2244
  rS[1 /* NgtRendererClassId.destroyed */] = true;
2008
2245
  }
2009
2246
 
2247
+ var _a$1;
2010
2248
  /**
2011
2249
  * Injection token for renderer factory options.
2012
2250
  */
2013
2251
  const NGT_RENDERER_OPTIONS = new InjectionToken('NGT_RENDERER_OPTIONS');
2252
+ /**
2253
+ * Angular's public RendererType2 intentionally omits the component constructor. The runtime
2254
+ * renderer type currently carries it as `type`; keep that private Angular dependency isolated
2255
+ * here so upgrades have one compatibility seam to verify.
2256
+ */
2257
+ function isNgtHTMLRendererType(type) {
2258
+ return !!Reflect.get(type, 'type')?.[NGT_HTML_FLAG];
2259
+ }
2014
2260
  /**
2015
2261
  * Angular renderer factory for Three.js elements.
2016
2262
  *
@@ -2039,46 +2285,45 @@ class NgtRendererFactory2 {
2039
2285
  this.catalogue = injectCatalogue();
2040
2286
  this.document = inject(DOCUMENT);
2041
2287
  this.options = inject(NGT_RENDERER_OPTIONS, { optional: true }) || {};
2042
- this.rendererMap = new Map();
2288
+ this.rendererByDelegate = new WeakMap();
2043
2289
  }
2044
2290
  createRenderer(hostElement, type) {
2045
2291
  const delegateRenderer = this.delegateRendererFactory.createRenderer(hostElement, type);
2046
2292
  if (!type)
2047
2293
  return delegateRenderer;
2048
- let renderer = this.rendererMap.get(type.id);
2049
- if (renderer) {
2050
- if (renderer instanceof NgtRenderer2) {
2051
- renderer.count += 1;
2052
- if (renderer.delegateRenderer !== delegateRenderer) {
2053
- renderer.delegateRenderer = delegateRenderer;
2294
+ if (isNgtHTMLRendererType(type))
2295
+ return delegateRenderer;
2296
+ if (hostElement) {
2297
+ let hostRendererNode;
2298
+ if (isRendererNode(hostElement)) {
2299
+ hostRendererNode = hostElement;
2300
+ }
2301
+ else {
2302
+ const logicalParent = delegateRenderer.parentNode(hostElement);
2303
+ hostRendererNode = createRendererNode('platform', hostElement, this.document);
2304
+ if (isRendererNode(logicalParent)) {
2305
+ insertRendererChildNode(logicalParent, hostRendererNode);
2054
2306
  }
2055
2307
  }
2056
- return renderer;
2308
+ markRendererViewHost(hostRendererNode);
2057
2309
  }
2058
- if (hostElement && !isRendererNode(hostElement)) {
2059
- createRendererNode('platform', hostElement, this.document);
2310
+ let renderer = this.rendererByDelegate.get(delegateRenderer);
2311
+ if (!renderer) {
2312
+ renderer = new NgtRenderer2(delegateRenderer, this.catalogue, this.document, this.options);
2313
+ this.rendererByDelegate.set(delegateRenderer, renderer);
2060
2314
  }
2061
- if (Reflect.get(type, 'type')?.[NGT_HTML_FLAG]) {
2062
- this.rendererMap.set(type.id, delegateRenderer);
2063
- // patch delegate destroyNode so we can destroy this HTML node
2064
- // TODO: make sure we really need to do this
2065
- const originalDestroyNode = delegateRenderer.destroyNode?.bind(delegateRenderer);
2066
- if (!originalDestroyNode || !(NGT_DELEGATE_RENDERER_DESTROY_NODE_PATCHED_FLAG in originalDestroyNode)) {
2067
- delegateRenderer.destroyNode = (node) => {
2068
- originalDestroyNode?.(node);
2069
- if (node !== hostElement)
2070
- return;
2071
- internalDestroyNode(node, null);
2072
- };
2073
- Object.assign(delegateRenderer.destroyNode, {
2074
- [NGT_DELEGATE_RENDERER_DESTROY_NODE_PATCHED_FLAG]: true,
2075
- });
2076
- }
2077
- return delegateRenderer;
2078
- }
2079
- this.rendererMap.set(type.id, (renderer = new NgtRenderer2(delegateRenderer, this.catalogue, this.document, this.options)));
2080
2315
  return renderer;
2081
2316
  }
2317
+ begin() {
2318
+ this.delegateRendererFactory.begin?.();
2319
+ }
2320
+ end() {
2321
+ flushAncestorNotifications();
2322
+ this.delegateRendererFactory.end?.();
2323
+ }
2324
+ whenRenderingDone() {
2325
+ return this.delegateRendererFactory.whenRenderingDone?.() ?? Promise.resolve();
2326
+ }
2082
2327
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtRendererFactory2, deps: [{ token: i0.RendererFactory2 }], target: i0.ɵɵFactoryTarget.Injectable }); }
2083
2328
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtRendererFactory2 }); }
2084
2329
  }
@@ -2098,40 +2343,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2098
2343
  * @internal
2099
2344
  */
2100
2345
  class NgtRenderer2 {
2101
- constructor(delegateRenderer, catalogue, document, options, count = 1) {
2346
+ static { _a$1 = NGT_RENDERER_CONTEXT_FLAG; }
2347
+ constructor(delegateRenderer, catalogue, document, options) {
2102
2348
  this.delegateRenderer = delegateRenderer;
2103
2349
  this.catalogue = catalogue;
2104
2350
  this.document = document;
2105
2351
  this.options = options;
2106
- this.count = count;
2107
- this.argsInjectors = [];
2108
- this.parentInjectors = [];
2109
- this.destroyNode = (node) => {
2110
- internalDestroyNode(node, this.removeChild.bind(this));
2352
+ this.directiveInjectors = [];
2353
+ this.parameterKeys = new WeakMap();
2354
+ this.attachedListeners = new WeakMap();
2355
+ this.updatedListeners = new WeakMap();
2356
+ this[_a$1] = (injector, callback) => {
2357
+ this.directiveInjectors.push(injector);
2358
+ try {
2359
+ return callback();
2360
+ }
2361
+ finally {
2362
+ this.directiveInjectors.pop();
2363
+ }
2111
2364
  };
2112
- this.addClass = this.delegateRenderer.addClass.bind(this.delegateRenderer);
2113
- this.removeClass = this.delegateRenderer.removeClass.bind(this.delegateRenderer);
2114
- this.setStyle = this.delegateRenderer.setStyle.bind(this.delegateRenderer);
2115
- this.removeStyle = this.delegateRenderer.removeStyle.bind(this.delegateRenderer);
2116
- this.selectRootElement = this.delegateRenderer.selectRootElement.bind(this.delegateRenderer);
2117
- this.nextSibling = this.delegateRenderer.nextSibling.bind(this.delegateRenderer);
2118
- this.setValue = this.delegateRenderer.setValue.bind(this.delegateRenderer);
2365
+ this.data = { ...this.delegateRenderer.data, __ngt_renderer__: true };
2119
2366
  if (!this.options.verbose) {
2120
2367
  this.options.verbose = false;
2121
2368
  }
2122
2369
  }
2123
- get data() {
2124
- return { ...this.delegateRenderer.data, __ngt_renderer__: true };
2125
- }
2126
2370
  destroy() {
2127
- if (this.count > 1) {
2128
- this.count -= 1;
2129
- return;
2130
- }
2131
- // this is the last instance of the same NgtRenderer2
2132
- this.count = 0;
2133
- this.argsInjectors = [];
2134
- this.parentInjectors = [];
2371
+ this.delegateRenderer.destroy();
2135
2372
  }
2136
2373
  createElement(name, namespace) {
2137
2374
  const platformElement = this.delegateRenderer.createElement(name, namespace);
@@ -2142,8 +2379,8 @@ class NgtRenderer2 {
2142
2379
  return createRendererNode('three', prepare(platformElement, 'ngt-value'), this.document);
2143
2380
  }
2144
2381
  const [injectedArgs, injectedParent] = [
2145
- this.getNgtDirective(NgtArgs, this.argsInjectors)?.value || [],
2146
- this.getNgtDirective(NgtParent, this.parentInjectors)?.value,
2382
+ this.getNgtDirective(NgtArgs)?.value || [],
2383
+ this.getNgtDirective(NgtParent)?.value,
2147
2384
  ];
2148
2385
  if (name === 'ngt-primitive') {
2149
2386
  if (!injectedArgs[0])
@@ -2155,7 +2392,7 @@ class NgtRenderer2 {
2155
2392
  prepare(object, 'ngt-primitive');
2156
2393
  const primitiveRendererNode = createRendererNode('three', object, this.document);
2157
2394
  if (injectedParent) {
2158
- primitiveRendererNode.__ngt_renderer__[5 /* NgtRendererClassId.parent */] =
2395
+ primitiveRendererNode.__ngt_renderer__[6 /* NgtRendererClassId.parentOverride */] =
2159
2396
  injectedParent;
2160
2397
  }
2161
2398
  return primitiveRendererNode;
@@ -2185,7 +2422,7 @@ class NgtRenderer2 {
2185
2422
  instanceState.attach = ['material'];
2186
2423
  }
2187
2424
  if (injectedParent) {
2188
- rendererNode.__ngt_renderer__[5 /* NgtRendererClassId.parent */] =
2425
+ rendererNode.__ngt_renderer__[6 /* NgtRendererClassId.parentOverride */] =
2189
2426
  injectedParent;
2190
2427
  }
2191
2428
  return rendererNode;
@@ -2195,31 +2432,24 @@ class NgtRenderer2 {
2195
2432
  createComment(value) {
2196
2433
  const commentNode = this.delegateRenderer.createComment(value);
2197
2434
  const commentRendererNode = createRendererNode('comment', commentNode, this.document);
2198
- // NOTE: we attach an arrow function to the Comment node
2199
- // In our directives, we can call this function to then start tracking the RendererNode
2200
- // this is done to limit the amount of Nodes we need to process for getCreationState
2201
- Object.assign(commentRendererNode, {
2202
- [NGT_INTERNAL_ADD_COMMENT_FLAG]: (type, injector) => {
2203
- if (type === 'args') {
2204
- this.argsInjectors.push(injector);
2205
- }
2206
- else if (type === 'parent') {
2207
- Object.assign(commentRendererNode, {
2208
- [NGT_INTERNAL_SET_PARENT_COMMENT_FLAG]: (ngtParent) => {
2209
- commentRendererNode.__ngt_renderer__[5 /* NgtRendererClassId.parent */] = ngtParent;
2210
- },
2211
- });
2212
- this.parentInjectors.push(injector);
2213
- }
2214
- commentRendererNode.__ngt_renderer__[4 /* NgtRendererClassId.injector */] = injector;
2215
- },
2216
- });
2217
2435
  return commentRendererNode;
2218
2436
  }
2219
2437
  createText(value) {
2220
2438
  const textNode = this.delegateRenderer.createText(value);
2221
2439
  return createRendererNode('text', textNode, this.document);
2222
2440
  }
2441
+ destroyNode(node) {
2442
+ const shouldDelegate = !isRendererNode(node) || node.__ngt_renderer__[0 /* NgtRendererClassId.type */] !== 'three';
2443
+ if (isRendererNode(node)) {
2444
+ this.destroyOwnedNodes(node);
2445
+ // Unlink from the owner's set but retain the owner marker until recursive
2446
+ // destruction has used it to protect foreign/projected descendants.
2447
+ releaseRendererOwner(node, false);
2448
+ }
2449
+ internalDestroyNode(node, this.removeChild.bind(this));
2450
+ if (shouldDelegate)
2451
+ this.delegateRenderer.destroyNode?.(node);
2452
+ }
2223
2453
  appendChild(parent, newChild, refChild, isMove) {
2224
2454
  const delegatedFn = refChild
2225
2455
  ? this.delegateRenderer.insertBefore.bind(this.delegateRenderer, parent, newChild, refChild, isMove)
@@ -2231,10 +2461,13 @@ class NgtRenderer2 {
2231
2461
  console.warn('[NGT dev mode] One of parent or child is not a renderer node.', { parent, newChild });
2232
2462
  return delegatedFn();
2233
2463
  }
2464
+ if (parent === newChild || refChild === newChild)
2465
+ return;
2466
+ this.setNodeRelationship(parent, newChild, refChild);
2234
2467
  if (cRS[0 /* NgtRendererClassId.type */] === 'comment') {
2235
- // if child is a comment, we'll set the parent then bail.
2468
+ // A comment is a logical anchor. It remains in the ordered host tree even when
2469
+ // the physical Three graph has no corresponding node.
2236
2470
  // comment usually means it's part of a templateRef ViewContainerRef or structural directive
2237
- setRendererParentNode(newChild, parent);
2238
2471
  // if parent is not three, we'll delegate to the renderer
2239
2472
  if (pRS[0 /* NgtRendererClassId.type */] !== 'three') {
2240
2473
  delegatedFn();
@@ -2242,52 +2475,43 @@ class NgtRenderer2 {
2242
2475
  return;
2243
2476
  }
2244
2477
  if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2245
- if (newChild[NGT_DOM_PARENT_FLAG] && newChild[NGT_DOM_PARENT_FLAG] instanceof HTMLElement) {
2246
- return this.delegateRenderer.appendChild(newChild[NGT_DOM_PARENT_FLAG], newChild);
2478
+ const threeParent = this.findNearestThreeParent(parent);
2479
+ const portalStore = this.findNearestPortalStore(parent);
2480
+ const insertionReference = threeParent
2481
+ ? this.findThreeReferenceAfterLogicalNode(threeParent, newChild)
2482
+ : null;
2483
+ if (newChild[NGT_DOM_PARENT_FLAG]) {
2484
+ this.delegateRenderer.appendChild(newChild[NGT_DOM_PARENT_FLAG], newChild);
2247
2485
  }
2248
- if (pRS[5 /* NgtRendererClassId.parent */] && !cRS[5 /* NgtRendererClassId.parent */]) {
2249
- return this.appendChild(pRS[5 /* NgtRendererClassId.parent */], newChild);
2486
+ else {
2487
+ delegatedFn();
2250
2488
  }
2251
- return delegatedFn();
2489
+ if (threeParent)
2490
+ this.attachThreeDescendants(threeParent, newChild, insertionReference, portalStore);
2491
+ return;
2252
2492
  }
2253
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2254
- return this.appendThreeRendererNodes(parent, newChild);
2493
+ if (isRendererNodeType(parent, 'three') && isRendererNodeType(newChild, 'three')) {
2494
+ return this.appendThreeRendererNodes(parent, newChild, refChild);
2255
2495
  }
2256
- if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2257
- // if platform has parent, delegate to that parent
2258
- if (pRS[5 /* NgtRendererClassId.parent */]) {
2259
- // but track the child for this parent as well
2260
- addRendererChildNode(parent, newChild);
2261
- return this.appendChild(pRS[5 /* NgtRendererClassId.parent */], newChild);
2496
+ if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && isRendererNodeType(newChild, 'three')) {
2497
+ const threeParent = this.findNearestThreeParent(parent);
2498
+ if (threeParent) {
2499
+ this.appendThreeRendererNodes(threeParent, newChild, refChild, this.findNearestPortalStore(parent));
2262
2500
  }
2263
- // platform can also have normal parentNode
2264
- const platformParentNode = this.delegateRenderer.parentNode(parent);
2265
- if (platformParentNode) {
2266
- return this.appendChild(platformParentNode, newChild);
2267
- }
2268
- // if not, set up parent and child relationship for this pair then bail
2269
- this.setNodeRelationship(parent, newChild);
2270
2501
  return;
2271
2502
  }
2272
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2273
- if (!cRS[5 /* NgtRendererClassId.parent */]) {
2274
- setRendererParentNode(newChild, parent);
2275
- }
2276
- for (const child of cRS[6 /* NgtRendererClassId.children */]) {
2277
- this.appendChild(parent, child);
2278
- }
2279
- for (const platformChildNode of newChild['childNodes'] || []) {
2280
- if (!isRendererNode(platformChildNode) ||
2281
- platformChildNode.__ngt_renderer__[0 /* NgtRendererClassId.type */] !== 'platform')
2282
- continue;
2283
- this.appendChild(parent, platformChildNode);
2284
- }
2503
+ if (isRendererNodeType(parent, 'three') &&
2504
+ (cRS[0 /* NgtRendererClassId.type */] === 'platform' ||
2505
+ cRS[0 /* NgtRendererClassId.type */] === 'portal' ||
2506
+ cRS[0 /* NgtRendererClassId.type */] === 'text')) {
2507
+ if (isRendererNodeType(newChild, 'platform'))
2508
+ this.attachThreeDescendants(parent, newChild);
2285
2509
  return;
2286
2510
  }
2287
- if (pRS[0 /* NgtRendererClassId.type */] === 'portal' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2288
- if (!cRS[5 /* NgtRendererClassId.parent */] && pRS[3 /* NgtRendererClassId.portalContainer */]) {
2289
- return this.appendChild(pRS[3 /* NgtRendererClassId.portalContainer */], newChild);
2290
- }
2511
+ if (isRendererNodeType(parent, 'portal') && isRendererNodeType(newChild, 'three')) {
2512
+ const portalContainer = parent.__ngt_renderer__[3 /* NgtRendererClassId.portalContainer */];
2513
+ if (portalContainer)
2514
+ return this.appendThreeRendererNodes(portalContainer, newChild, refChild, this.portalStore(parent));
2291
2515
  return;
2292
2516
  }
2293
2517
  if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'portal') {
@@ -2296,14 +2520,12 @@ class NgtRenderer2 {
2296
2520
  return delegatedFn();
2297
2521
  }
2298
2522
  insertBefore(parent, newChild, refChild, isMove) {
2299
- // if both are comments and the reference child is NgtCanvasContent, we'll assign the same flag to the newChild
2300
- // this means that the NgtCanvas component is embedding. This flag allows the Renderer to get the root scene
2301
- // when it tries to attach the template under `ng-template[canvasContent]`
2302
- if (refChild &&
2303
- refChild[NGT_CANVAS_CONTENT_FLAG] &&
2304
- refChild instanceof Comment &&
2305
- newChild instanceof Comment) {
2306
- Object.assign(newChild, { [NGT_CANVAS_CONTENT_FLAG]: refChild[NGT_CANVAS_CONTENT_FLAG] });
2523
+ const referenceAnchor = getRendererAnchor(refChild);
2524
+ if (referenceAnchor &&
2525
+ (referenceAnchor.kind === 'canvas' || referenceAnchor.kind === 'portal') &&
2526
+ isRendererNode(newChild) &&
2527
+ newChild.__ngt_renderer__[0 /* NgtRendererClassId.type */] === 'comment') {
2528
+ setRendererAnchor(newChild, referenceAnchor);
2307
2529
  }
2308
2530
  // if there is no parent, we delegate
2309
2531
  if (!parent) {
@@ -2311,21 +2533,19 @@ class NgtRenderer2 {
2311
2533
  }
2312
2534
  return this.appendChild(parent, newChild, refChild, isMove);
2313
2535
  }
2314
- removeChild(parent, oldChild, isHostElement) {
2536
+ removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval) {
2315
2537
  if (parent === null) {
2316
2538
  parent = this.parentNode(oldChild);
2317
2539
  }
2318
2540
  const cRS = oldChild.__ngt_renderer__;
2319
2541
  if (!cRS) {
2320
2542
  try {
2321
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2543
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2322
2544
  }
2323
2545
  catch {
2324
2546
  return;
2325
2547
  }
2326
2548
  }
2327
- // disassociate things from oldChild
2328
- cRS[5 /* NgtRendererClassId.parent */] = null;
2329
2549
  // if parent is still undefined
2330
2550
  if (parent == null) {
2331
2551
  if (cRS[1 /* NgtRendererClassId.destroyed */]) {
@@ -2338,39 +2558,38 @@ class NgtRenderer2 {
2338
2558
  }
2339
2559
  const pRS = parent.__ngt_renderer__;
2340
2560
  if (!pRS) {
2341
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2561
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2342
2562
  }
2343
- const childIndex = pRS[6 /* NgtRendererClassId.children */].indexOf(oldChild);
2344
- if (childIndex >= 0) {
2345
- // disassociate oldChild from parent children
2346
- pRS[6 /* NgtRendererClassId.children */].splice(childIndex, 1);
2563
+ if (cRS[0 /* NgtRendererClassId.type */] !== 'three') {
2564
+ this.detachThreeDescendants(oldChild);
2347
2565
  }
2348
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2349
- return removeThreeChild(oldChild, parent, true);
2566
+ removeRendererChildNode(parent, oldChild);
2567
+ if (cRS[0 /* NgtRendererClassId.type */] === 'three') {
2568
+ const childState = getInstanceState(oldChild);
2569
+ const threeParent = childState?.parent ? untracked(childState.parent) : null;
2570
+ if (threeParent) {
2571
+ return removeThreeChild(oldChild, threeParent, false);
2572
+ }
2573
+ return;
2350
2574
  }
2351
2575
  if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2352
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2576
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2353
2577
  }
2354
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2578
+ if (pRS[0 /* NgtRendererClassId.type */] === 'three' &&
2579
+ (cRS[0 /* NgtRendererClassId.type */] === 'platform' ||
2580
+ cRS[0 /* NgtRendererClassId.type */] === 'portal' ||
2581
+ cRS[0 /* NgtRendererClassId.type */] === 'text')) {
2355
2582
  return;
2356
2583
  }
2357
- if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2358
- const childLS = getInstanceState(oldChild);
2359
- if (!childLS)
2360
- return;
2361
- const threeParent = childLS.parent ? untracked(childLS.parent) : null;
2362
- if (!threeParent)
2363
- return;
2364
- return this.removeChild(threeParent, oldChild);
2365
- }
2366
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2584
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2367
2585
  }
2368
2586
  parentNode(node) {
2369
- if (node &&
2370
- (node[NGT_CANVAS_CONTENT_FLAG] || node[NGT_PORTAL_CONTENT_FLAG]) &&
2371
- node instanceof Comment &&
2372
- isRendererNode(node)) {
2373
- const store = node[NGT_CANVAS_CONTENT_FLAG] || node[NGT_PORTAL_CONTENT_FLAG];
2587
+ const anchor = getRendererAnchor(node);
2588
+ if (anchor &&
2589
+ (anchor.kind === 'canvas' || anchor.kind === 'portal') &&
2590
+ isRendererNode(node) &&
2591
+ node.__ngt_renderer__[0 /* NgtRendererClassId.type */] === 'comment') {
2592
+ const store = anchor.store;
2374
2593
  // this should not happen but if it does, we'll delegate to the renderer
2375
2594
  if (!store) {
2376
2595
  return this.delegateRenderer.parentNode(node);
@@ -2380,24 +2599,18 @@ class NgtRenderer2 {
2380
2599
  if (!rootScene) {
2381
2600
  return this.delegateRenderer.parentNode(node);
2382
2601
  }
2383
- // if root scene is not a renderer node, we'll make it a renderer node here
2384
- if (!(NGT_RENDERER_NODE_FLAG in rootScene)) {
2385
- const sceneRendererNode = createRendererNode('three', rootScene, this.document);
2386
- // set parent to the comment too
2387
- setRendererParentNode(node, sceneRendererNode);
2388
- }
2389
- if (node[NGT_PORTAL_CONTENT_FLAG] &&
2390
- node[NGT_DOM_PARENT_FLAG] &&
2391
- isRendererNode(node[NGT_DOM_PARENT_FLAG])) {
2392
- const portalContentParent = node[NGT_DOM_PARENT_FLAG];
2602
+ // if root scene is not a Three renderer node, make it one here
2603
+ const rendererRootScene = isRendererNodeType(rootScene, 'three')
2604
+ ? rootScene
2605
+ : createRendererNode('three', rootScene, this.document);
2606
+ if (anchor.kind === 'portal' && anchor.domParent && isRendererNode(anchor.domParent)) {
2607
+ const portalContentParent = anchor.domParent;
2393
2608
  const portalContentParentRS = portalContentParent.__ngt_renderer__;
2394
- if (!portalContentParentRS[3 /* NgtRendererClassId.portalContainer */]) {
2395
- portalContentParentRS[3 /* NgtRendererClassId.portalContainer */] = rootScene;
2396
- }
2609
+ portalContentParentRS[3 /* NgtRendererClassId.portalContainer */] = rendererRootScene;
2397
2610
  }
2398
- return rootScene;
2611
+ return rendererRootScene;
2399
2612
  }
2400
- const rendererParentNode = node.__ngt_renderer__?.[5 /* NgtRendererClassId.parent */];
2613
+ const rendererParentNode = node.__ngt_renderer__?.[4 /* NgtRendererClassId.parent */];
2401
2614
  // returns the renderer parent node if it exists, otherwise returns the delegateRenderer parentNode
2402
2615
  return rendererParentNode ?? this.delegateRenderer.parentNode(node);
2403
2616
  }
@@ -2406,6 +2619,15 @@ class NgtRenderer2 {
2406
2619
  if (!rS || rS[1 /* NgtRendererClassId.destroyed */])
2407
2620
  return this.delegateRenderer.removeAttribute(el, name, namespace);
2408
2621
  if (rS[0 /* NgtRendererClassId.type */] === 'three') {
2622
+ const instanceState = getInstanceState(el);
2623
+ const parent = (instanceState?.parent ? untracked(instanceState.parent) : null) ?? this.findTargetThreeParent(el);
2624
+ if (name === 'attach') {
2625
+ this.updateAttachment(el, undefined, parent);
2626
+ return;
2627
+ }
2628
+ applyProps(el, { [name]: undefined });
2629
+ if (name === 'geometry')
2630
+ untracked(() => instanceState?.updateGeometryStamp());
2409
2631
  return;
2410
2632
  }
2411
2633
  return this.delegateRenderer.removeAttribute(el, name, namespace);
@@ -2421,12 +2643,9 @@ class NgtRenderer2 {
2421
2643
  }
2422
2644
  if (rS[0 /* NgtRendererClassId.type */] === 'three') {
2423
2645
  if (name === 'attach') {
2424
- const paths = value.split('.');
2425
- if (paths.length) {
2426
- const instanceState = getInstanceState(el);
2427
- if (instanceState)
2428
- instanceState.attach = paths;
2429
- }
2646
+ const instanceState = getInstanceState(el);
2647
+ const parent = (instanceState?.parent ? untracked(instanceState.parent) : null) ?? this.findTargetThreeParent(el);
2648
+ this.updateAttachment(el, value, parent);
2430
2649
  return;
2431
2650
  }
2432
2651
  // coercion for primitive values
@@ -2453,44 +2672,55 @@ class NgtRenderer2 {
2453
2672
  // NOTE: untrack all signal updates because this is during setProperty which is a reactive context
2454
2673
  // attaching potentially updates signals which is not allowed
2455
2674
  const rS = el.__ngt_renderer__;
2456
- if (!rS || rS[1 /* NgtRendererClassId.destroyed */]) {
2675
+ if (!rS)
2676
+ return this.delegateRenderer.setProperty(el, name, value);
2677
+ if (rS[1 /* NgtRendererClassId.destroyed */]) {
2457
2678
  this.options.verbose &&
2458
2679
  console.warn('[NGT dev mode] setProperty is invoked on destroyed renderer node.', { el, name, value });
2459
2680
  return;
2460
2681
  }
2461
- if (rS[0 /* NgtRendererClassId.type */] === 'three') {
2682
+ if (isRendererNodeType(el, 'three')) {
2683
+ const threeState = el.__ngt_renderer__;
2462
2684
  const instanceState = getInstanceState(el);
2463
- const parent = instanceState?.hierarchyStore.snapshot.parent || rS[5 /* NgtRendererClassId.parent */];
2685
+ const attachedParent = instanceState?.hierarchyStore.snapshot.parent ?? null;
2686
+ const parent = attachedParent ?? this.findTargetThreeParent(el);
2464
2687
  if (name === 'parameters') {
2688
+ const parameters = value && typeof value === 'object' ? { ...value } : {};
2465
2689
  // NOTE: short-cut for null raycast to prevent upstream from creating a nullRaycast property
2466
- if ('raycast' in value && value['raycast'] === null) {
2467
- value['raycast'] = () => null;
2690
+ if ('raycast' in parameters && parameters['raycast'] === null) {
2691
+ parameters['raycast'] = () => null;
2468
2692
  }
2469
- applyProps(el, value);
2470
- if ('geometry' in value && is.three(value['geometry'], 'isBufferGeometry')) {
2693
+ const previousKeys = this.parameterKeys.get(el) ?? new Set();
2694
+ const nextKeys = new Set(Object.keys(parameters));
2695
+ const removedParameters = Object.fromEntries([...previousKeys]
2696
+ .filter((key) => key !== 'attach' && !nextKeys.has(key))
2697
+ .map((key) => [key, undefined]));
2698
+ const { attach: nextAttach, ...nextParameters } = parameters;
2699
+ applyProps(el, { ...removedParameters, ...nextParameters });
2700
+ this.parameterKeys.set(el, nextKeys);
2701
+ if (nextKeys.has('geometry') || (previousKeys.has('geometry') && !nextKeys.has('geometry'))) {
2471
2702
  untracked(() => instanceState?.updateGeometryStamp());
2472
2703
  }
2473
- if ('attach' in value && value['attach'] !== undefined) {
2474
- if (instanceState)
2475
- instanceState.attach = this.normalizeAttach(value['attach']);
2476
- if (parent)
2477
- untracked(() => attachThreeNodes(parent, el));
2704
+ if ('attach' in parameters || previousKeys.has('attach')) {
2705
+ this.updateAttachment(el, nextAttach, parent);
2478
2706
  }
2479
2707
  return;
2480
2708
  }
2481
2709
  // [rawValue]
2482
2710
  if (instanceState?.type === 'ngt-value' && name === 'rawValue') {
2483
- rS[2 /* NgtRendererClassId.rawValue */] = value;
2484
- if (parent)
2485
- untracked(() => attachThreeNodes(parent, el));
2711
+ untracked(() => {
2712
+ if (attachedParent)
2713
+ removeThreeChild(el, attachedParent, false);
2714
+ threeState[2 /* NgtRendererClassId.rawValue */] = value;
2715
+ if (parent) {
2716
+ attachThreeNodes(parent, el, undefined, instanceState.store ?? undefined);
2717
+ }
2718
+ });
2486
2719
  return;
2487
2720
  }
2488
2721
  // [attach]
2489
2722
  if (name === 'attach') {
2490
- if (instanceState)
2491
- instanceState.attach = this.normalizeAttach(value);
2492
- if (parent)
2493
- untracked(() => attachThreeNodes(parent, el));
2723
+ this.updateAttachment(el, value, parent);
2494
2724
  return;
2495
2725
  }
2496
2726
  // NOTE: short-cut for null raycast to prevent upstream from creating a nullRaycast property
@@ -2498,7 +2728,7 @@ class NgtRenderer2 {
2498
2728
  value = () => null;
2499
2729
  }
2500
2730
  applyProps(el, { [name]: value });
2501
- if (instanceState && name === 'geometry' && is.three(value, 'isBufferGeometry')) {
2731
+ if (instanceState && name === 'geometry') {
2502
2732
  untracked(() => {
2503
2733
  instanceState.updateGeometryStamp();
2504
2734
  });
@@ -2507,13 +2737,13 @@ class NgtRenderer2 {
2507
2737
  }
2508
2738
  return this.delegateRenderer.setProperty(el, name, value);
2509
2739
  }
2510
- listen(target, eventName, callback) {
2740
+ listen(target, eventName, callback, options) {
2511
2741
  if (typeof target === 'string') {
2512
- return this.delegateRenderer.listen(target, eventName, callback);
2742
+ return this.delegateRenderer.listen(target, eventName, callback, options);
2513
2743
  }
2514
2744
  const rS = target.__ngt_renderer__;
2515
2745
  if (!rS) {
2516
- return this.delegateRenderer.listen(target, eventName, callback);
2746
+ return this.delegateRenderer.listen(target, eventName, callback, options);
2517
2747
  }
2518
2748
  if (rS[1 /* NgtRendererClassId.destroyed */])
2519
2749
  return () => { };
@@ -2528,43 +2758,34 @@ class NgtRenderer2 {
2528
2758
  return () => { };
2529
2759
  }
2530
2760
  if (eventName === 'attached') {
2531
- iS.onAttach = callback;
2532
- const parent = iS.parent && untracked(iS.parent);
2533
- if (parent)
2534
- iS.onAttach({ parent, node: target });
2535
- return () => {
2536
- iS.onAttach = undefined;
2537
- };
2761
+ return this.registerWithOptions(callback, options, (listener) => this.listenToInstanceEvent(target, 'attached', listener));
2538
2762
  }
2539
2763
  if (eventName === 'updated') {
2540
- iS.onUpdate = callback;
2541
- return () => {
2542
- iS.onUpdate = undefined;
2543
- };
2764
+ return this.registerWithOptions(callback, options, (listener) => this.listenToInstanceEvent(target, 'updated', listener));
2544
2765
  }
2545
2766
  if (THREE_NATIVE_EVENTS.includes(eventName) && target instanceof THREE.EventDispatcher) {
2546
2767
  // NOTE: rename to dispose because that's the event type, not disposed.
2547
2768
  if (eventName === 'disposed') {
2548
2769
  eventName = 'dispose';
2549
2770
  }
2550
- if (target.parent &&
2551
- (eventName === 'added' || eventName === 'removed')) {
2552
- callback({ type: eventName, target });
2553
- }
2554
- target.addEventListener(eventName, callback);
2555
- return () => {
2556
- target.removeEventListener(eventName, callback);
2557
- };
2771
+ return this.registerWithOptions(callback, options, (listener) => {
2772
+ target.addEventListener(eventName, listener);
2773
+ if (target.parent && eventName === 'added') {
2774
+ listener({ type: eventName, target });
2775
+ }
2776
+ return () => target.removeEventListener(eventName, listener);
2777
+ });
2558
2778
  }
2559
- const cleanup = iS.setPointerEvent?.(eventName, callback) || (() => { });
2779
+ const cleanup = this.registerWithOptions(callback, options, (listener) => iS.setPointerEvent?.(eventName, listener) || (() => { }));
2560
2780
  // this means the object has already been attached to the parent and has its store propagated
2561
2781
  if (iS.store)
2562
2782
  iS.addInteraction?.(iS.store);
2563
2783
  return cleanup;
2564
2784
  }
2565
- return this.delegateRenderer.listen(target, eventName, callback);
2785
+ return this.delegateRenderer.listen(target, eventName, callback, options);
2566
2786
  }
2567
- appendThreeRendererNodes(parent, child) {
2787
+ appendThreeRendererNodes(parent, child, refChild, storeOverride) {
2788
+ parent = child.__ngt_renderer__[6 /* NgtRendererClassId.parentOverride */] ?? parent;
2568
2789
  // if parent and child are the same, skip
2569
2790
  if (parent === child) {
2570
2791
  this.options.verbose &&
@@ -2574,31 +2795,226 @@ class NgtRenderer2 {
2574
2795
  });
2575
2796
  return;
2576
2797
  }
2577
- const cIS = getInstanceState(child);
2578
- // if child is already attached to a parent, skip
2579
- if (cIS?.hierarchyStore.snapshot.parent) {
2580
- this.options.verbose &&
2581
- console.warn('[NGT dev mode] appending THREE.js parent and child but child is already attached', {
2582
- parent,
2583
- child,
2584
- });
2585
- return;
2798
+ if (!is.instance(parent) || !is.instance(child)) {
2799
+ throw new Error('[NGT] THREE renderer nodes need to be prepared with local instance state.');
2800
+ }
2801
+ const childState = getInstanceState(child);
2802
+ const currentParent = childState?.parent ? untracked(childState.parent) : null;
2803
+ if (currentParent && currentParent !== parent) {
2804
+ removeThreeChild(child, currentParent, false);
2586
2805
  }
2587
- // set the relationship
2588
- this.setNodeRelationship(parent, child);
2589
- // attach THREE child
2590
- attachThreeNodes(parent, child);
2806
+ const before = this.findThreeInsertionReference(parent, child, refChild);
2807
+ const anchor = getRendererAnchor(refChild);
2808
+ const attachmentStore = storeOverride ?? (anchor?.kind === 'canvas' || anchor?.kind === 'portal' ? anchor.store : undefined);
2809
+ attachThreeNodes(parent, child, is.instance(before) ? before : null, attachmentStore);
2591
2810
  return;
2592
2811
  }
2593
- setNodeRelationship(parent, child) {
2594
- setRendererParentNode(child, parent);
2595
- addRendererChildNode(parent, child);
2812
+ setNodeRelationship(parent, child, refChild) {
2813
+ const anchor = getRendererAnchor(refChild);
2814
+ const logicalParent = anchor?.kind === 'portal' && anchor.domParent && isRendererNode(anchor.domParent)
2815
+ ? anchor.domParent
2816
+ : parent;
2817
+ insertRendererChildNode(logicalParent, child, refChild);
2818
+ }
2819
+ findNearestThreeParent(node) {
2820
+ let current = node;
2821
+ while (current && isRendererNode(current)) {
2822
+ const state = current.__ngt_renderer__;
2823
+ if (state[0 /* NgtRendererClassId.type */] === 'three')
2824
+ return current;
2825
+ if (state[0 /* NgtRendererClassId.type */] === 'portal' && state[3 /* NgtRendererClassId.portalContainer */]) {
2826
+ return state[3 /* NgtRendererClassId.portalContainer */];
2827
+ }
2828
+ current = state[4 /* NgtRendererClassId.parent */];
2829
+ }
2830
+ return null;
2831
+ }
2832
+ findTargetThreeParent(node) {
2833
+ if (!isRendererNodeType(node, 'three'))
2834
+ return null;
2835
+ const state = node.__ngt_renderer__;
2836
+ const parentOverride = state[6 /* NgtRendererClassId.parentOverride */];
2837
+ if (parentOverride && is.instance(parentOverride))
2838
+ return parentOverride;
2839
+ const logicalParent = this.findNearestThreeParent(state[4 /* NgtRendererClassId.parent */]);
2840
+ return logicalParent && is.instance(logicalParent) ? logicalParent : null;
2841
+ }
2842
+ portalStore(node) {
2843
+ const anchor = getRendererAnchor(node);
2844
+ return anchor?.kind === 'portal' ? anchor.store : undefined;
2845
+ }
2846
+ findNearestPortalStore(node) {
2847
+ let current = node;
2848
+ while (current && isRendererNode(current)) {
2849
+ const store = this.portalStore(current);
2850
+ if (store)
2851
+ return store;
2852
+ current = current.__ngt_renderer__[4 /* NgtRendererClassId.parent */];
2853
+ }
2854
+ return undefined;
2855
+ }
2856
+ findFirstThreeDescendant(node, physicalParent) {
2857
+ if (!node || !isRendererNode(node))
2858
+ return null;
2859
+ const state = node.__ngt_renderer__;
2860
+ if (isRendererNodeType(node, 'three')) {
2861
+ if (!is.instance(node) || !is.instance(physicalParent))
2862
+ return null;
2863
+ if (!is.three(node, 'isObject3D'))
2864
+ return null;
2865
+ if (!is.three(physicalParent, 'isObject3D'))
2866
+ return null;
2867
+ const instanceState = getInstanceState(node);
2868
+ if (instanceState?.parent && untracked(instanceState.parent) === physicalParent) {
2869
+ if (physicalParent.children.includes(node))
2870
+ return node;
2871
+ }
2872
+ return null;
2873
+ }
2874
+ for (const child of state[5 /* NgtRendererClassId.children */]) {
2875
+ const descendant = this.findFirstThreeDescendant(child, physicalParent);
2876
+ if (descendant)
2877
+ return descendant;
2878
+ }
2879
+ return null;
2880
+ }
2881
+ findThreeInsertionReference(physicalParent, child, requestedRef) {
2882
+ const directReference = this.findFirstThreeDescendant(requestedRef, physicalParent);
2883
+ if (directReference)
2884
+ return directReference;
2885
+ return this.findThreeReferenceAfterLogicalNode(physicalParent, child);
2886
+ }
2887
+ findThreeReferenceAfterLogicalNode(physicalParent, node) {
2888
+ let current = node;
2889
+ while (current) {
2890
+ const logicalParent = current.__ngt_renderer__[4 /* NgtRendererClassId.parent */];
2891
+ if (!logicalParent || !isRendererNode(logicalParent))
2892
+ return null;
2893
+ const siblings = logicalParent.__ngt_renderer__[5 /* NgtRendererClassId.children */];
2894
+ const currentIndex = siblings.indexOf(current);
2895
+ for (let index = currentIndex + 1; index < siblings.length; index++) {
2896
+ const reference = this.findFirstThreeDescendant(siblings[index], physicalParent);
2897
+ if (reference)
2898
+ return reference;
2899
+ }
2900
+ if (logicalParent === physicalParent)
2901
+ return null;
2902
+ current = logicalParent;
2903
+ }
2904
+ return null;
2905
+ }
2906
+ attachThreeDescendants(parent, node, refChild, storeOverride) {
2907
+ for (const child of node.__ngt_renderer__[5 /* NgtRendererClassId.children */]) {
2908
+ if (child.__ngt_renderer__[0 /* NgtRendererClassId.type */] === 'three') {
2909
+ this.appendThreeRendererNodes(parent, child, refChild, storeOverride);
2910
+ }
2911
+ else {
2912
+ this.attachThreeDescendants(parent, child, refChild, storeOverride);
2913
+ }
2914
+ }
2915
+ }
2916
+ detachThreeDescendants(node) {
2917
+ for (const child of node.__ngt_renderer__[5 /* NgtRendererClassId.children */]) {
2918
+ if (child.__ngt_renderer__[0 /* NgtRendererClassId.type */] === 'three') {
2919
+ const instanceState = getInstanceState(child);
2920
+ const physicalParent = instanceState?.parent ? untracked(instanceState.parent) : null;
2921
+ if (physicalParent) {
2922
+ removeThreeChild(child, physicalParent, false);
2923
+ }
2924
+ }
2925
+ else {
2926
+ this.detachThreeDescendants(child);
2927
+ }
2928
+ }
2929
+ }
2930
+ destroyOwnedNodes(host) {
2931
+ const ownedNodes = host.__ngt_renderer__[9 /* NgtRendererClassId.ownedNodes */];
2932
+ if (!ownedNodes)
2933
+ return;
2934
+ for (const ownedNode of [...ownedNodes]) {
2935
+ releaseRendererOwner(ownedNode, false);
2936
+ internalDestroyNode(ownedNode, this.removeChild.bind(this));
2937
+ }
2938
+ ownedNodes.clear();
2939
+ }
2940
+ listenToInstanceEvent(target, eventName, callback) {
2941
+ const instanceState = getInstanceState(target);
2942
+ if (!instanceState)
2943
+ return () => { };
2944
+ const registry = eventName === 'attached' ? this.attachedListeners : this.updatedListeners;
2945
+ let bucket = registry.get(target);
2946
+ if (!bucket) {
2947
+ const listeners = new Map();
2948
+ const existing = eventName === 'attached' ? instanceState.onAttach : instanceState.onUpdate;
2949
+ if (existing)
2950
+ listeners.set(Symbol('existing'), existing);
2951
+ const dispatch = (event) => {
2952
+ for (const listener of [...listeners.values()])
2953
+ listener(event);
2954
+ };
2955
+ bucket = { listeners, dispatch };
2956
+ registry.set(target, bucket);
2957
+ if (eventName === 'attached')
2958
+ instanceState.onAttach = dispatch;
2959
+ else
2960
+ instanceState.onUpdate = dispatch;
2961
+ }
2962
+ const token = Symbol(eventName);
2963
+ bucket.listeners.set(token, callback);
2964
+ if (eventName === 'attached') {
2965
+ const parent = instanceState.parent && untracked(instanceState.parent);
2966
+ if (parent)
2967
+ callback({ parent, node: target });
2968
+ }
2969
+ let active = true;
2970
+ return () => {
2971
+ if (!active)
2972
+ return;
2973
+ active = false;
2974
+ if (!bucket?.listeners.delete(token) || bucket.listeners.size > 0)
2975
+ return;
2976
+ registry.delete(target);
2977
+ if (eventName === 'attached' && instanceState.onAttach === bucket.dispatch) {
2978
+ instanceState.onAttach = undefined;
2979
+ }
2980
+ else if (eventName === 'updated' && instanceState.onUpdate === bucket.dispatch) {
2981
+ instanceState.onUpdate = undefined;
2982
+ }
2983
+ };
2596
2984
  }
2597
- getNgtDirective(directive, injectors) {
2985
+ registerWithOptions(callback, options, register) {
2986
+ if (!options?.once)
2987
+ return register(callback);
2988
+ let cleanup;
2989
+ let cleanupPending = false;
2990
+ let active = true;
2991
+ const onceCallback = (event) => {
2992
+ if (!active)
2993
+ return;
2994
+ active = false;
2995
+ try {
2996
+ return callback(event);
2997
+ }
2998
+ finally {
2999
+ if (cleanup)
3000
+ cleanup();
3001
+ else
3002
+ cleanupPending = true;
3003
+ }
3004
+ };
3005
+ cleanup = register(onceCallback);
3006
+ if (cleanupPending)
3007
+ cleanup();
3008
+ return () => {
3009
+ active = false;
3010
+ cleanup?.();
3011
+ };
3012
+ }
3013
+ getNgtDirective(directive) {
2598
3014
  let directiveInstance;
2599
- let i = injectors.length - 1;
3015
+ let i = this.directiveInjectors.length - 1;
2600
3016
  while (i >= 0) {
2601
- const injector = injectors[i];
3017
+ const injector = this.directiveInjectors[i];
2602
3018
  const instance = injector.get(directive, null);
2603
3019
  if (instance && typeof instance === 'object' && instance.validate()) {
2604
3020
  directiveInstance = instance;
@@ -2609,12 +3025,75 @@ class NgtRenderer2 {
2609
3025
  return directiveInstance;
2610
3026
  }
2611
3027
  normalizeAttach(attach) {
3028
+ if (attach == null)
3029
+ return undefined;
2612
3030
  if (typeof attach === 'function')
2613
3031
  return attach;
2614
3032
  if (typeof attach === 'string')
2615
3033
  return attach.split('.');
2616
3034
  return attach.flatMap((item) => item.toString().split('.'));
2617
3035
  }
3036
+ updateAttachment(node, attach, parent) {
3037
+ untracked(() => {
3038
+ const instanceState = getInstanceState(node);
3039
+ if (!instanceState)
3040
+ return;
3041
+ const nextAttach = this.normalizeAttach(attach);
3042
+ const currentAttach = instanceState.attach;
3043
+ const isSameAttach = currentAttach === nextAttach ||
3044
+ (Array.isArray(currentAttach) &&
3045
+ Array.isArray(nextAttach) &&
3046
+ currentAttach.length === nextAttach.length &&
3047
+ currentAttach.every((part, index) => part === nextAttach[index]));
3048
+ if (isSameAttach)
3049
+ return;
3050
+ const attachedParent = instanceState.hierarchyStore.snapshot.parent;
3051
+ if (attachedParent)
3052
+ removeThreeChild(node, attachedParent, false);
3053
+ instanceState.attach = nextAttach;
3054
+ if (parent) {
3055
+ attachThreeNodes(parent, node, undefined, instanceState.store ?? undefined);
3056
+ }
3057
+ });
3058
+ }
3059
+ addClass(el, name) {
3060
+ if (el?.__ngt_renderer__?.[0 /* NgtRendererClassId.type */] === 'three')
3061
+ return;
3062
+ this.delegateRenderer.addClass(el, name);
3063
+ }
3064
+ removeClass(el, name) {
3065
+ if (el?.__ngt_renderer__?.[0 /* NgtRendererClassId.type */] === 'three')
3066
+ return;
3067
+ this.delegateRenderer.removeClass(el, name);
3068
+ }
3069
+ setStyle(el, style, value, flags) {
3070
+ if (el?.__ngt_renderer__?.[0 /* NgtRendererClassId.type */] === 'three')
3071
+ return;
3072
+ this.delegateRenderer.setStyle(el, style, value, flags);
3073
+ }
3074
+ removeStyle(el, style, flags) {
3075
+ if (el?.__ngt_renderer__?.[0 /* NgtRendererClassId.type */] === 'three')
3076
+ return;
3077
+ this.delegateRenderer.removeStyle(el, style, flags);
3078
+ }
3079
+ selectRootElement(selectorOrNode, preserveContent) {
3080
+ return this.delegateRenderer.selectRootElement(selectorOrNode, preserveContent);
3081
+ }
3082
+ nextSibling(node) {
3083
+ if (isRendererNode(node)) {
3084
+ const state = node.__ngt_renderer__;
3085
+ if (state[4 /* NgtRendererClassId.parent */])
3086
+ return getRendererNextSibling(node);
3087
+ if (state[0 /* NgtRendererClassId.type */] === 'three')
3088
+ return null;
3089
+ }
3090
+ return this.delegateRenderer.nextSibling(node);
3091
+ }
3092
+ setValue(node, value) {
3093
+ if (node?.__ngt_renderer__?.[0 /* NgtRendererClassId.type */] === 'three')
3094
+ return;
3095
+ this.delegateRenderer.setValue(node, value);
3096
+ }
2618
3097
  }
2619
3098
 
2620
3099
  /**
@@ -2643,6 +3122,12 @@ function storeFactory() {
2643
3122
  const defaultTarget = new THREE.Vector3();
2644
3123
  const tempTarget = new THREE.Vector3();
2645
3124
  let performanceTimeout = undefined;
3125
+ const cancelPerformanceRegression = () => {
3126
+ if (performanceTimeout === undefined)
3127
+ return;
3128
+ clearTimeout(performanceTimeout);
3129
+ performanceTimeout = undefined;
3130
+ };
2646
3131
  const pointer = new THREE.Vector2();
2647
3132
  // getCurrentViewport will mutate this instead of creating a new object everytime
2648
3133
  const tempViewport = {
@@ -2682,17 +3167,19 @@ function storeFactory() {
2682
3167
  regress: () => {
2683
3168
  const state = store.snapshot;
2684
3169
  // Clear timeout
2685
- if (performanceTimeout)
2686
- clearTimeout(performanceTimeout);
3170
+ cancelPerformanceRegression();
2687
3171
  // Set lower bound performance
2688
3172
  if (state.performance.current !== state.performance.min)
2689
3173
  store.update((state) => ({
2690
3174
  performance: { ...state.performance, current: state.performance.min },
2691
3175
  }));
2692
3176
  // Go back to upper bound performance after a while unless something regresses meanwhile
2693
- performanceTimeout = setTimeout(() => store.update((state) => ({
2694
- performance: { ...state.performance, current: store.snapshot.performance.max },
2695
- })), state.performance.debounce);
3177
+ performanceTimeout = setTimeout(() => {
3178
+ performanceTimeout = undefined;
3179
+ store.update((state) => ({
3180
+ performance: { ...state.performance, current: store.snapshot.performance.max },
3181
+ }));
3182
+ }, state.performance.debounce);
2696
3183
  },
2697
3184
  },
2698
3185
  size: { width: 0, height: 0, top: 0, left: 0 },
@@ -2780,27 +3267,34 @@ function storeFactory() {
2780
3267
  subscribers: [],
2781
3268
  subscribe: (callback, priority = 0, _store = store) => {
2782
3269
  const internal = _store.snapshot.internal;
3270
+ const subscription = { callback, priority, store: _store };
2783
3271
  // If this subscription was given a priority, it takes rendering into its own hands
2784
3272
  // For that reason we switch off automatic rendering and increase the manual flag
2785
3273
  // As long as this flag is positive there can be no internal rendering at all
2786
3274
  // because there could be multiple render subscriptions
2787
- internal.priority = internal.priority + (priority > 0 ? 1 : 0);
2788
- internal.subscribers.push({ callback, priority, store: _store });
3275
+ internal.priority += priority > 0 ? 1 : 0;
2789
3276
  // Register subscriber and sort layers from lowest to highest, meaning,
2790
3277
  // highest priority renders last (on top of the other frames)
2791
- internal.subscribers = internal.subscribers.sort((a, b) => (a.priority || 0) - (b.priority || 0));
3278
+ internal.subscribers = [...internal.subscribers, subscription].sort((a, b) => (a.priority || 0) - (b.priority || 0));
3279
+ let active = true;
2792
3280
  return () => {
3281
+ if (!active)
3282
+ return;
3283
+ active = false;
2793
3284
  const internal = _store.snapshot.internal;
2794
- if (internal?.subscribers) {
2795
- // Decrease manual flag if this subscription had a priority
2796
- internal.priority = internal.priority - (priority > 0 ? 1 : 0);
2797
- // Remove subscriber from list
2798
- internal.subscribers = internal.subscribers.filter((s) => s.callback !== callback);
2799
- }
3285
+ const index = internal?.subscribers.indexOf(subscription);
3286
+ if (index == null || index < 0)
3287
+ return;
3288
+ internal.subscribers = internal.subscribers.filter((record) => record !== subscription);
3289
+ if (priority > 0)
3290
+ internal.priority = Math.max(0, internal.priority - 1);
2800
3291
  };
2801
3292
  },
2802
3293
  },
2803
3294
  });
3295
+ Object.defineProperty(store.snapshot.performance.regress, 'cancel', {
3296
+ value: cancelPerformanceRegression,
3297
+ });
2804
3298
  Object.defineProperty(store, '__pointerMissed$', { get: () => pointerMissed$ });
2805
3299
  let { size: oldSize, viewport: { dpr: oldDpr }, camera: oldCamera, } = store.snapshot;
2806
3300
  effect(() => {
@@ -2914,22 +3408,11 @@ class NgtParent extends NgtCommonDirective {
2914
3408
  this.linkedValue = linkedSignal(this._parent, ...(ngDevMode ? [{ debugName: "linkedValue" }] : /* istanbul ignore next */ []));
2915
3409
  this.shouldSkipRender = computed(() => !this._parent(), ...(ngDevMode ? [{ debugName: "shouldSkipRender" }] : /* istanbul ignore next */ []));
2916
3410
  const commentNode = this.commentNode;
2917
- commentNode.data = NGT_PARENT_FLAG;
2918
- commentNode[NGT_PARENT_FLAG] = true;
2919
- if (commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]) {
2920
- commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]('parent', this.injector);
2921
- delete commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG];
2922
- }
3411
+ setRendererAnchor(commentNode, { kind: 'parent', injector: this.injector });
2923
3412
  }
2924
3413
  validate() {
2925
3414
  return !this.injected && !!this.injectedValue;
2926
3415
  }
2927
- beforeCreateView() {
2928
- const commentNode = this.commentNode;
2929
- if (commentNode[NGT_INTERNAL_SET_PARENT_COMMENT_FLAG]) {
2930
- commentNode[NGT_INTERNAL_SET_PARENT_COMMENT_FLAG](this.injectedValue);
2931
- }
2932
- }
2933
3416
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtParent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2934
3417
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: NgtParent, isStandalone: true, selector: "ng-template[parent]", inputs: { parent: { classPropertyName: "parent", publicName: "parent", isSignal: true, isRequired: true, transformFunction: null } }, usesInheritance: true, ngImport: i0 }); }
2935
3418
  }
@@ -3095,8 +3578,135 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3095
3578
  type: Directive
3096
3579
  }], ctorParameters: () => [] });
3097
3580
 
3098
- const cached$1 = new Map();
3099
- const memoizedLoaders$1 = new WeakMap();
3581
+ const defaultConfigurationKey = Symbol('ngt-default-loader-configuration');
3582
+ /**
3583
+ * Interns semantic tuples component-by-component so equivalent configuration
3584
+ * and multi-part URL tuples share an identity without retaining object parts.
3585
+ */
3586
+ class SemanticKeyRegistry {
3587
+ constructor() {
3588
+ this.root = new SemanticKeyNode();
3589
+ }
3590
+ resolve(key) {
3591
+ if (!Array.isArray(key))
3592
+ return key;
3593
+ let node = this.root;
3594
+ for (const part of key) {
3595
+ node = node.child(part);
3596
+ }
3597
+ return (node.value ??= {});
3598
+ }
3599
+ clear() {
3600
+ this.root = new SemanticKeyNode();
3601
+ }
3602
+ }
3603
+ class SemanticKeyNode {
3604
+ constructor() {
3605
+ this.primitiveChildren = new Map();
3606
+ this.objectChildren = new WeakMap();
3607
+ }
3608
+ child(part) {
3609
+ if ((typeof part === 'object' && part !== null) || typeof part === 'function') {
3610
+ let child = this.objectChildren.get(part);
3611
+ if (!child) {
3612
+ child = new SemanticKeyNode();
3613
+ this.objectChildren.set(part, child);
3614
+ }
3615
+ return child;
3616
+ }
3617
+ let child = this.primitiveChildren.get(part);
3618
+ if (!child) {
3619
+ child = new SemanticKeyNode();
3620
+ this.primitiveChildren.set(part, child);
3621
+ }
3622
+ return child;
3623
+ }
3624
+ }
3625
+ /**
3626
+ * Owns configured loader instances and their result promises. Constructor,
3627
+ * configuration, and URL are deliberately separate cache dimensions.
3628
+ */
3629
+ class NgtLoaderCache {
3630
+ constructor() {
3631
+ this.constructors = new Map();
3632
+ this.configurationKeys = new SemanticKeyRegistry();
3633
+ this.requestKeys = new SemanticKeyRegistry();
3634
+ }
3635
+ configurationKey(extensions, key) {
3636
+ if (key !== undefined)
3637
+ return this.configurationKeys.resolve(key);
3638
+ return extensions ? {} : defaultConfigurationKey;
3639
+ }
3640
+ getOrCreate(LoaderConstructor, configurationKey, request, extensions, load) {
3641
+ const normalizedConfigurationKey = this.configurationKeys.resolve(configurationKey);
3642
+ const requestKey = this.requestKeys.resolve(request);
3643
+ const entry = this.getEntry(LoaderConstructor, normalizedConfigurationKey, extensions);
3644
+ const cachedPromise = entry.promises.get(requestKey);
3645
+ if (cachedPromise)
3646
+ return cachedPromise;
3647
+ let promise;
3648
+ promise = load(entry.loader).catch((error) => {
3649
+ this.evict(LoaderConstructor, normalizedConfigurationKey, entry, requestKey, promise);
3650
+ throw error;
3651
+ });
3652
+ entry.promises.set(requestKey, promise);
3653
+ return promise;
3654
+ }
3655
+ clear(urls) {
3656
+ const requestsToClear = Array.isArray(urls)
3657
+ ? [this.requestKeys.resolve(urls), ...urls.map((url) => this.requestKeys.resolve(url))]
3658
+ : [this.requestKeys.resolve(urls)];
3659
+ for (const [LoaderConstructor, configurations] of this.constructors) {
3660
+ for (const [configurationKey, entry] of configurations) {
3661
+ for (const requestKey of requestsToClear)
3662
+ entry.promises.delete(requestKey);
3663
+ if (entry.promises.size === 0)
3664
+ configurations.delete(configurationKey);
3665
+ }
3666
+ if (configurations.size === 0)
3667
+ this.constructors.delete(LoaderConstructor);
3668
+ }
3669
+ }
3670
+ destroy() {
3671
+ this.constructors.clear();
3672
+ this.configurationKeys.clear();
3673
+ this.requestKeys.clear();
3674
+ }
3675
+ getConfigurations(LoaderConstructor) {
3676
+ let configurations = this.constructors.get(LoaderConstructor);
3677
+ if (!configurations) {
3678
+ configurations = new Map();
3679
+ this.constructors.set(LoaderConstructor, configurations);
3680
+ }
3681
+ return configurations;
3682
+ }
3683
+ getEntry(LoaderConstructor, configurationKey, extensions) {
3684
+ const configurations = this.getConfigurations(LoaderConstructor);
3685
+ let entry = configurations.get(configurationKey);
3686
+ if (entry)
3687
+ return entry;
3688
+ const loader = new LoaderConstructor();
3689
+ extensions?.(loader);
3690
+ entry = { loader, promises: new Map() };
3691
+ configurations.set(configurationKey, entry);
3692
+ return entry;
3693
+ }
3694
+ evict(LoaderConstructor, configurationKey, entry, requestKey, promise) {
3695
+ if (entry.promises.get(requestKey) !== promise)
3696
+ return;
3697
+ entry.promises.delete(requestKey);
3698
+ if (entry.promises.size > 0)
3699
+ return;
3700
+ const configurations = this.constructors.get(LoaderConstructor);
3701
+ if (configurations?.get(configurationKey) !== entry)
3702
+ return;
3703
+ configurations.delete(configurationKey);
3704
+ if (configurations.size === 0)
3705
+ this.constructors.delete(LoaderConstructor);
3706
+ }
3707
+ }
3708
+
3709
+ const loaderCache$1 = new NgtLoaderCache();
3100
3710
  function normalizeInputs$1(input) {
3101
3711
  let urls = [];
3102
3712
  if (Array.isArray(input)) {
@@ -3110,33 +3720,26 @@ function normalizeInputs$1(input) {
3110
3720
  }
3111
3721
  return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
3112
3722
  }
3113
- function load(loaderConstructorFactory, inputs, { extensions, onLoad, onProgress, } = {}) {
3114
- return () => {
3723
+ function load(loaderConstructorFactory, inputs, { extensions, cacheKey, onLoad, onProgress, } = {}) {
3724
+ return (consumerOnLoad = onLoad) => {
3115
3725
  const urls = normalizeInputs$1(inputs());
3116
- let loader = memoizedLoaders$1.get(loaderConstructorFactory(urls));
3117
- if (!loader) {
3118
- loader = new (loaderConstructorFactory(urls))();
3119
- memoizedLoaders$1.set(loaderConstructorFactory(urls), loader);
3120
- }
3121
- if (extensions)
3122
- extensions(loader);
3726
+ const LoaderConstructor = loaderConstructorFactory(urls);
3727
+ const configurationKey = loaderCache$1.configurationKey(extensions, cacheKey?.());
3123
3728
  return urls.map((url) => {
3124
3729
  if (url === '')
3125
3730
  return Promise.resolve(null);
3126
- if (!cached$1.has(url)) {
3127
- cached$1.set(url, new Promise((resolve, reject) => {
3128
- loader.load(url, (data) => {
3129
- if ('scene' in data) {
3130
- Object.assign(data, makeObjectGraph(data['scene']));
3131
- }
3132
- if (onLoad) {
3133
- onLoad(data);
3134
- }
3135
- resolve(data);
3136
- }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
3137
- }));
3138
- }
3139
- return cached$1.get(url);
3731
+ const promise = loaderCache$1.getOrCreate(LoaderConstructor, configurationKey, url, extensions, (loader) => new Promise((resolve, reject) => {
3732
+ loader.load(url, (data) => {
3733
+ if ('scene' in data) {
3734
+ Object.assign(data, makeObjectGraph(data['scene']));
3735
+ }
3736
+ resolve(data);
3737
+ }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
3738
+ }));
3739
+ return promise.then((data) => {
3740
+ consumerOnLoad?.(data);
3741
+ return data;
3742
+ });
3140
3743
  });
3141
3744
  };
3142
3745
  }
@@ -3144,18 +3747,30 @@ function load(loaderConstructorFactory, inputs, { extensions, onLoad, onProgress
3144
3747
  * @deprecated Use loaderResource instead. Will be removed in v5.0.0
3145
3748
  * @since v4.0.0~
3146
3749
  */
3147
- function _injectLoader(loaderConstructorFactory, inputs, { extensions, onProgress, onLoad, injector, } = {}) {
3750
+ function _injectLoader(loaderConstructorFactory, inputs, { extensions, cacheKey, onProgress, onLoad, injector, } = {}) {
3148
3751
  return assertInjector(_injectLoader, injector, () => {
3149
3752
  const response = signal(null, ...(ngDevMode ? [{ debugName: "response" }] : /* istanbul ignore next */ []));
3150
3753
  const cachedResultPromisesEffect = load(loaderConstructorFactory, inputs, {
3151
3754
  extensions,
3755
+ cacheKey,
3152
3756
  onProgress,
3153
- onLoad: onLoad,
3154
3757
  });
3155
- effect(() => {
3758
+ effect((onCleanup) => {
3759
+ let active = true;
3760
+ onCleanup(() => {
3761
+ active = false;
3762
+ });
3156
3763
  const originalUrls = inputs();
3157
- const cachedResultPromises = cachedResultPromisesEffect();
3158
- Promise.all(cachedResultPromises).then((results) => {
3764
+ const cachedResultPromises = cachedResultPromisesEffect((data) => {
3765
+ if (active)
3766
+ onLoad?.(data);
3767
+ });
3768
+ void Promise.all(cachedResultPromises)
3769
+ .then((results) => {
3770
+ // A cached request can resolve after the reactive input has moved on.
3771
+ // Keep the shared promise, but never publish a stale generation.
3772
+ if (!active)
3773
+ return;
3159
3774
  response.update(() => {
3160
3775
  if (Array.isArray(originalUrls))
3161
3776
  return results;
@@ -3168,25 +3783,23 @@ function _injectLoader(loaderConstructorFactory, inputs, { extensions, onProgres
3168
3783
  return result;
3169
3784
  }, {});
3170
3785
  });
3171
- });
3786
+ })
3787
+ .catch(() => undefined);
3172
3788
  });
3173
3789
  return response.asReadonly();
3174
3790
  });
3175
3791
  }
3176
- _injectLoader.preload = (loaderConstructorFactory, inputs, extensions, onLoad) => {
3177
- const effects = load(loaderConstructorFactory, inputs, { extensions, onLoad })();
3792
+ _injectLoader.preload = (loaderConstructorFactory, inputs, extensions, onLoad, cacheKey) => {
3793
+ const effects = load(loaderConstructorFactory, inputs, { extensions, cacheKey, onLoad })();
3178
3794
  if (effects) {
3179
- void Promise.all(effects);
3795
+ void Promise.all(effects).catch(() => undefined);
3180
3796
  }
3181
3797
  };
3182
3798
  _injectLoader.destroy = () => {
3183
- cached$1.clear();
3799
+ loaderCache$1.destroy();
3184
3800
  };
3185
3801
  _injectLoader.clear = (urls) => {
3186
- const urlToClear = Array.isArray(urls) ? urls : [urls];
3187
- urlToClear.forEach((url) => {
3188
- cached$1.delete(url);
3189
- });
3802
+ loaderCache$1.clear(urls);
3190
3803
  };
3191
3804
  /**
3192
3805
  * @deprecated Use loaderResource instead. Will be removed in v5.0.0
@@ -3214,38 +3827,26 @@ function normalizeInputs(input) {
3214
3827
  }
3215
3828
  return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
3216
3829
  }
3217
- const cached = new Map();
3218
- const memoizedLoaders = new WeakMap();
3219
- function getLoaderResourceParams(input, loaderConstructorFactory, extensions) {
3830
+ const loaderCache = new NgtLoaderCache();
3831
+ function getLoaderResourceParams(input, loaderConstructorFactory, extensions, cacheKey) {
3220
3832
  const urls = input();
3221
3833
  const LoaderConstructor = loaderConstructorFactory(urls);
3222
3834
  const normalizedUrls = normalizeInputs(urls);
3223
- let loader = memoizedLoaders.get(LoaderConstructor);
3224
- if (!loader) {
3225
- loader = new LoaderConstructor();
3226
- memoizedLoaders.set(LoaderConstructor, loader);
3227
- }
3228
- if (extensions)
3229
- extensions(loader);
3230
- return { urls, normalizedUrls, loader };
3835
+ const configurationKey = loaderCache.configurationKey(extensions, cacheKey?.());
3836
+ return { urls, normalizedUrls, LoaderConstructor, configurationKey, extensions };
3231
3837
  }
3232
3838
  function getLoaderPromises(params, onProgress) {
3233
3839
  return params.normalizedUrls.map((url) => {
3234
3840
  if (url === '')
3235
3841
  return Promise.resolve(null);
3236
- const cachedPromise = cached.get(url);
3237
- if (cachedPromise)
3238
- return cachedPromise;
3239
- const promise = new Promise((res, rej) => {
3240
- params.loader.load(url, (data) => {
3842
+ return loaderCache.getOrCreate(params.LoaderConstructor, params.configurationKey, url, params.extensions, (loader) => new Promise((res, rej) => {
3843
+ loader.load(url, (data) => {
3241
3844
  if ('scene' in data) {
3242
3845
  Object.assign(data, makeObjectGraph(data['scene']));
3243
3846
  }
3244
3847
  res(data);
3245
3848
  }, onProgress, (error) => rej(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
3246
- });
3247
- cached.set(url, promise);
3248
- return promise;
3849
+ }));
3249
3850
  });
3250
3851
  }
3251
3852
  /**
@@ -3285,11 +3886,11 @@ function getLoaderPromises(params, onProgress) {
3285
3886
  * }
3286
3887
  * ```
3287
3888
  */
3288
- function loaderResource(loaderConstructorFactory, input, { extensions, onLoad, onProgress, injector, } = {}) {
3889
+ function loaderResource(loaderConstructorFactory, input, { extensions, cacheKey, onLoad, onProgress, injector, } = {}) {
3289
3890
  return assertInjector(loaderResource, injector, () => {
3290
3891
  return resource({
3291
- params: () => getLoaderResourceParams(input, loaderConstructorFactory, extensions),
3292
- loader: async ({ params }) => {
3892
+ params: () => getLoaderResourceParams(input, loaderConstructorFactory, extensions, cacheKey),
3893
+ loader: async ({ params, abortSignal }) => {
3293
3894
  // TODO: use the abortSignal when THREE.Loader supports it
3294
3895
  const loadedResults = await Promise.all(getLoaderPromises(params, onProgress));
3295
3896
  let results;
@@ -3307,25 +3908,22 @@ function loaderResource(loaderConstructorFactory, input, { extensions, onLoad, o
3307
3908
  return result;
3308
3909
  }, {});
3309
3910
  }
3310
- if (onLoad)
3311
- onLoad(results);
3911
+ if (!abortSignal.aborted)
3912
+ onLoad?.(results);
3312
3913
  return results;
3313
3914
  },
3314
3915
  });
3315
3916
  });
3316
3917
  }
3317
- loaderResource.preload = (loaderConstructor, inputs, extensions) => {
3318
- const params = getLoaderResourceParams(() => inputs, () => loaderConstructor, extensions);
3319
- void Promise.all(getLoaderPromises(params));
3918
+ loaderResource.preload = (loaderConstructor, inputs, extensions, cacheKey) => {
3919
+ const params = getLoaderResourceParams(() => inputs, () => loaderConstructor, extensions, cacheKey !== undefined ? () => cacheKey : undefined);
3920
+ void Promise.all(getLoaderPromises(params)).catch(() => undefined);
3320
3921
  };
3321
3922
  loaderResource.destroy = () => {
3322
- cached.clear();
3923
+ loaderCache.destroy();
3323
3924
  };
3324
3925
  loaderResource.clear = (urls) => {
3325
- const urlToClear = Array.isArray(urls) ? urls : [urls];
3326
- urlToClear.forEach((url) => {
3327
- cached.delete(url);
3328
- });
3926
+ loaderCache.clear(urls);
3329
3927
  };
3330
3928
 
3331
3929
  const RGBA_REGEX = /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*(\d*\.?\d+)?\)/;
@@ -3649,7 +4247,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3649
4247
  * ```
3650
4248
  */
3651
4249
  class NgtPortalContent {
3652
- static ngTemplateContextGuard(_, ctx) {
4250
+ static ngTemplateContextGuard(_, _ctx) {
3653
4251
  return true;
3654
4252
  }
3655
4253
  constructor() {
@@ -3657,9 +4255,10 @@ class NgtPortalContent {
3657
4255
  const { element } = inject(ViewContainerRef);
3658
4256
  const commentNode = element.nativeElement;
3659
4257
  const store = injectStore();
3660
- commentNode.data = NGT_PORTAL_CONTENT_FLAG;
3661
- commentNode[NGT_PORTAL_CONTENT_FLAG] = store;
3662
- commentNode[NGT_DOM_PARENT_FLAG] = host.nativeElement;
4258
+ const domParent = host.nativeElement;
4259
+ const anchor = { kind: 'portal', store, domParent };
4260
+ setRendererAnchor(commentNode, anchor);
4261
+ setRendererAnchor(domParent, anchor);
3663
4262
  }
3664
4263
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtPortalContent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3665
4264
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: NgtPortalContent, isStandalone: true, selector: "ng-template[portalContent]", ngImport: i0 }); }
@@ -3668,34 +4267,115 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3668
4267
  type: Directive,
3669
4268
  args: [{ selector: 'ng-template[portalContent]' }]
3670
4269
  }], ctorParameters: () => [] });
3671
- function mergeState(previousRoot, store, container, pointer, raycaster, events, size) {
3672
- // we never want to spread the id
3673
- const { id: _, ...previousState } = previousRoot.snapshot;
4270
+ function mergeState(previousRoot, store, previousState, container, runtime, restState, events, size) {
4271
+ // The portal id belongs to this store. Everything else starts from the
4272
+ // latest parent snapshot so removed overrides cannot leave stale values.
4273
+ const { id: _, ...inheritedState } = previousState;
3674
4274
  const state = store.snapshot;
4275
+ capturePortalLocalState(previousState, state, runtime);
4276
+ const layeredState = { ...inheritedState, ...runtime.localState, ...restState };
4277
+ const camera = layeredState.camera;
4278
+ const mergedSize = { ...previousState.size, ...size };
3675
4279
  let viewport = undefined;
3676
- if (state.camera && size) {
3677
- const camera = state.camera;
4280
+ if (camera && (camera !== previousState.camera || size)) {
3678
4281
  // calculate the override viewport, if present
3679
- viewport = previousState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
3680
- // update the portal camera, if it differs from the previous layer
3681
- if (camera !== previousState.camera)
3682
- updateCamera(camera, size);
3683
- }
3684
- return {
3685
- // the intersect consists of the previous root state
3686
- ...previousState,
3687
- ...state,
4282
+ viewport = previousState.viewport.getCurrentViewport(camera, new THREE.Vector3(), mergedSize);
4283
+ // A portal-local camera owns its projection until the portal explicitly
4284
+ // overrides the inherited size. Camera components may intentionally provide
4285
+ // custom orthographic bounds or a custom perspective aspect.
4286
+ if (size && camera !== previousState.camera)
4287
+ updateCamera(camera, mergedSize);
4288
+ }
4289
+ runtime.inputEvents = events;
4290
+ const mergedState = {
4291
+ // Inherited values are refreshed on every effect run. Current input
4292
+ // overrides are applied over that fresh snapshot, never over stale portal
4293
+ // state from a previous run.
4294
+ ...layeredState,
4295
+ id: state.id,
3688
4296
  // portals have their own scene, which forms the root, a raycaster and a pointer
3689
4297
  scene: container,
3690
- pointer,
3691
- raycaster,
4298
+ pointer: runtime.pointer,
4299
+ raycaster: runtime.raycaster,
3692
4300
  // their previous root is the layer before it
3693
4301
  previousRoot,
3694
- events: { ...previousState.events, ...state.events, ...events },
3695
- size: { ...previousState.size, ...size },
3696
- viewport: { ...previousState.viewport, ...viewport },
3697
- // layers are allowed to override events
3698
- setEvents: (events) => store.update((state) => ({ ...state, events: { ...state.events, ...events } })),
4302
+ // The render loop and subscriptions are portal runtime state. Replacing
4303
+ // them with a new parent object would discard portal-local mutations.
4304
+ internal: state.internal,
4305
+ events: { ...previousState.events, ...runtime.localEvents, ...events },
4306
+ size: mergedSize,
4307
+ viewport: { ...previousState.viewport, ...viewport, ...restState.viewport },
4308
+ setEvents: runtime.setEvents,
4309
+ invalidate: runtime.invalidate,
4310
+ };
4311
+ runtime.lastMerged = mergedState;
4312
+ return mergedState;
4313
+ }
4314
+ function capturePortalLocalState(parent, current, runtime) {
4315
+ for (const key of ['camera', 'controls']) {
4316
+ const tracker = runtime.localStateTrackers[key];
4317
+ tracker.baselines.add(parent[key]);
4318
+ const previousValue = runtime.lastMerged?.[key];
4319
+ const currentValue = current[key];
4320
+ if (!runtime.lastMerged || Object.is(currentValue, previousValue))
4321
+ continue;
4322
+ if (tracker.baselines.has(currentValue)) {
4323
+ tracker.values.length = 0;
4324
+ delete runtime.localState[key];
4325
+ continue;
4326
+ }
4327
+ const restoredIndex = tracker.values.lastIndexOf(currentValue);
4328
+ if (restoredIndex >= 0)
4329
+ tracker.values.length = restoredIndex + 1;
4330
+ else
4331
+ tracker.values.push(currentValue);
4332
+ Object.assign(runtime.localState, { [key]: tracker.values.at(-1) });
4333
+ }
4334
+ }
4335
+ function movePortalChildren(store, previousContainer, nextContainer) {
4336
+ if (!previousContainer || previousContainer === nextContainer)
4337
+ return;
4338
+ const previousState = getInstanceState(previousContainer);
4339
+ if (!previousState)
4340
+ return;
4341
+ const children = [...previousState.objects(), ...previousState.nonObjects()];
4342
+ for (const child of children) {
4343
+ const childState = getInstanceState(child);
4344
+ if (childState?.store !== store || childState.parent() !== previousContainer)
4345
+ continue;
4346
+ removeThreeChild(child, previousContainer, false);
4347
+ attachThreeNodes(nextContainer, child, undefined, store);
4348
+ }
4349
+ }
4350
+ const portalRuntimes = new WeakMap();
4351
+ const portalContainerOwnership = new WeakMap();
4352
+ function claimPortalContainer(container, store, fallback) {
4353
+ const instanceState = getInstanceState(container);
4354
+ if (!instanceState)
4355
+ return () => undefined;
4356
+ let ownership = portalContainerOwnership.get(container);
4357
+ if (!ownership) {
4358
+ ownership = { baseline: instanceState.store ?? fallback, claims: [] };
4359
+ portalContainerOwnership.set(container, ownership);
4360
+ }
4361
+ const token = Symbol('portal-container');
4362
+ ownership.claims.push({ token, store });
4363
+ instanceState.store = store;
4364
+ let active = true;
4365
+ return () => {
4366
+ if (!active)
4367
+ return;
4368
+ active = false;
4369
+ const current = portalContainerOwnership.get(container);
4370
+ if (!current)
4371
+ return;
4372
+ const index = current.claims.findIndex((claim) => claim.token === token);
4373
+ if (index >= 0)
4374
+ current.claims.splice(index, 1);
4375
+ const latest = current.claims.at(-1);
4376
+ instanceState.store = latest?.store ?? current.baseline;
4377
+ if (!latest)
4378
+ portalContainerOwnership.delete(container);
3699
4379
  };
3700
4380
  }
3701
4381
  /**
@@ -3719,6 +4399,16 @@ function mergeState(previousRoot, store, container, pointer, raycaster, events,
3719
4399
  * ```
3720
4400
  */
3721
4401
  class NgtPortalImpl {
4402
+ /** @internal Subscribe to invalidations originating in this portal layer. */
4403
+ onInvalidate(listener) {
4404
+ const runtime = portalRuntimes.get(this.portalStore);
4405
+ if (!runtime)
4406
+ throw new Error('[NGT] Portal store runtime was not initialized');
4407
+ runtime.invalidationListeners.add(listener);
4408
+ return () => {
4409
+ runtime.invalidationListeners.delete(listener);
4410
+ };
4411
+ }
3722
4412
  constructor() {
3723
4413
  this.container = input.required(...(ngDevMode ? [{ debugName: "container" }] : /* istanbul ignore next */ []));
3724
4414
  this.state = input({}, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
@@ -3727,6 +4417,7 @@ class NgtPortalImpl {
3727
4417
  this.previousStore = injectStore({ skipSelf: true });
3728
4418
  this.portalStore = injectStore();
3729
4419
  this.injector = inject(Injector);
4420
+ this.document = inject(DOCUMENT);
3730
4421
  this.size = pick(this.state, 'size');
3731
4422
  this.events = pick(this.state, 'events');
3732
4423
  this.restState = omit(this.state, ['size', 'events']);
@@ -3734,21 +4425,42 @@ class NgtPortalImpl {
3734
4425
  this.portalRendered = this.portalContentRendered.asReadonly();
3735
4426
  extend({ Group });
3736
4427
  effect(() => {
3737
- let [container, anchor, content] = [
4428
+ // A makeDefault camera/controls update writes directly to the portal
4429
+ // store. Track those supported local overrides without tracking the
4430
+ // whole state object that this effect itself merges.
4431
+ this.portalStore.camera();
4432
+ this.portalStore.controls();
4433
+ const [inputContainer, anchor, content, previousState, size, events, restState] = [
3738
4434
  this.container(),
3739
4435
  this.anchorRef(),
3740
4436
  this.contentRef(),
3741
4437
  this.previousStore(),
4438
+ this.size(),
4439
+ this.events(),
4440
+ this.restState(),
3742
4441
  ];
3743
- const [size, events, restState] = [untracked(this.size), untracked(this.events), untracked(this.restState)];
3744
- if (!is.instance(container)) {
3745
- container = prepare(container, 'ngt-portal', { store: this.portalStore });
4442
+ const runtime = portalRuntimes.get(this.portalStore);
4443
+ if (!runtime)
4444
+ throw new Error('[NGT] Portal store runtime was not initialized');
4445
+ const instanceContainer = is.instance(inputContainer)
4446
+ ? inputContainer
4447
+ : prepare(inputContainer, 'ngt-portal', { store: this.previousStore });
4448
+ const container = isRendererNodeType(instanceContainer, 'three')
4449
+ ? instanceContainer
4450
+ : createRendererNode('three', instanceContainer, this.document);
4451
+ const portalAnchor = getRendererAnchor(anchor.element.nativeElement);
4452
+ if (portalAnchor?.kind === 'portal' && portalAnchor.domParent) {
4453
+ portalAnchor.domParent.__ngt_renderer__[3 /* NgtRendererClassId.portalContainer */] = container;
3746
4454
  }
3747
4455
  const instanceState = getInstanceState(container);
3748
- if (instanceState && instanceState.store !== this.portalStore) {
3749
- instanceState.store = this.portalStore;
4456
+ if (instanceState && runtime.container !== container) {
4457
+ const nextRelease = claimPortalContainer(container, this.portalStore, this.previousStore);
4458
+ movePortalChildren(this.portalStore, runtime.container, container);
4459
+ runtime.containerClaim?.release();
4460
+ runtime.container = container;
4461
+ runtime.containerClaim = { container, release: nextRelease };
3750
4462
  }
3751
- this.portalStore.update(restState, mergeState(this.previousStore, this.portalStore, container, this.portalStore.snapshot.pointer, this.portalStore.snapshot.raycaster, events, size));
4463
+ this.portalStore.update(mergeState(this.previousStore, this.portalStore, previousState, container, runtime, restState, events, size));
3752
4464
  if (this.portalViewRef) {
3753
4465
  this.portalViewRef.detectChanges();
3754
4466
  return;
@@ -3758,6 +4470,12 @@ class NgtPortalImpl {
3758
4470
  this.portalViewRef.detectChanges();
3759
4471
  this.portalContentRendered.set(true);
3760
4472
  });
4473
+ inject(DestroyRef).onDestroy(() => {
4474
+ const runtime = portalRuntimes.get(this.portalStore);
4475
+ runtime?.containerClaim?.release();
4476
+ runtime?.invalidationListeners.clear();
4477
+ portalRuntimes.delete(this.portalStore);
4478
+ });
3761
4479
  }
3762
4480
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtPortalImpl, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3763
4481
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: NgtPortalImpl, isStandalone: true, selector: "ngt-portal", inputs: { container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: true, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
@@ -3775,7 +4493,32 @@ class NgtPortalImpl {
3775
4493
  pointer,
3776
4494
  raycaster,
3777
4495
  });
3778
- store.update(mergeState(previousStore, store, null, pointer, raycaster));
4496
+ const runtime = {
4497
+ pointer,
4498
+ raycaster,
4499
+ localEvents: {},
4500
+ invalidate: () => undefined,
4501
+ invalidationListeners: new Set(),
4502
+ localState: {},
4503
+ localStateTrackers: {
4504
+ camera: { baselines: new Set(), values: [] },
4505
+ controls: { baselines: new Set(), values: [] },
4506
+ },
4507
+ setEvents: () => undefined,
4508
+ };
4509
+ runtime.setEvents = (events) => {
4510
+ Object.assign(runtime.localEvents, events);
4511
+ store.update((state) => ({
4512
+ events: { ...state.events, ...events, ...runtime.inputEvents },
4513
+ }));
4514
+ };
4515
+ runtime.invalidate = (frames) => {
4516
+ for (const listener of runtime.invalidationListeners)
4517
+ listener();
4518
+ previousStore.snapshot.invalidate(frames);
4519
+ };
4520
+ portalRuntimes.set(store, runtime);
4521
+ store.update(mergeState(previousStore, store, previousStore.snapshot, null, runtime, {}));
3779
4522
  return store;
3780
4523
  },
3781
4524
  deps: [[new SkipSelf(), NGT_STORE]],
@@ -3814,7 +4557,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3814
4557
  pointer,
3815
4558
  raycaster,
3816
4559
  });
3817
- store.update(mergeState(previousStore, store, null, pointer, raycaster));
4560
+ const runtime = {
4561
+ pointer,
4562
+ raycaster,
4563
+ localEvents: {},
4564
+ invalidate: () => undefined,
4565
+ invalidationListeners: new Set(),
4566
+ localState: {},
4567
+ localStateTrackers: {
4568
+ camera: { baselines: new Set(), values: [] },
4569
+ controls: { baselines: new Set(), values: [] },
4570
+ },
4571
+ setEvents: () => undefined,
4572
+ };
4573
+ runtime.setEvents = (events) => {
4574
+ Object.assign(runtime.localEvents, events);
4575
+ store.update((state) => ({
4576
+ events: { ...state.events, ...events, ...runtime.inputEvents },
4577
+ }));
4578
+ };
4579
+ runtime.invalidate = (frames) => {
4580
+ for (const listener of runtime.invalidationListeners)
4581
+ listener();
4582
+ previousStore.snapshot.invalidate(frames);
4583
+ };
4584
+ portalRuntimes.set(store, runtime);
4585
+ store.update(mergeState(previousStore, store, previousStore.snapshot, null, runtime, {}));
3818
4586
  return store;
3819
4587
  },
3820
4588
  deps: [[new SkipSelf(), NGT_STORE]],
@@ -3836,6 +4604,95 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3836
4604
  const NgtPortal = [NgtPortalImpl, NgtPortalContent];
3837
4605
 
3838
4606
  const shallowLoose = { objects: 'shallow', strict: false };
4607
+ const rootLifecycles = new WeakMap();
4608
+ function cloneConfigurationValue(value) {
4609
+ if (Array.isArray(value))
4610
+ return value.slice();
4611
+ if (value && typeof value === 'object' && 'clone' in value && typeof value.clone === 'function') {
4612
+ return value.clone();
4613
+ }
4614
+ return value;
4615
+ }
4616
+ function snapshotCameraOptions(camera) {
4617
+ if (!camera || is.three(camera, 'isCamera'))
4618
+ return camera;
4619
+ const options = camera;
4620
+ return Object.fromEntries(Object.entries(options).map(([key, value]) => [key, cloneConfigurationValue(value)]));
4621
+ }
4622
+ function configurationValuesEqual(previous, next) {
4623
+ if (Object.is(previous, next))
4624
+ return true;
4625
+ if (Array.isArray(previous) && Array.isArray(next)) {
4626
+ return previous.length === next.length && previous.every((value, index) => Object.is(value, next[index]));
4627
+ }
4628
+ if (previous &&
4629
+ next &&
4630
+ typeof previous === 'object' &&
4631
+ typeof next === 'object' &&
4632
+ 'equals' in previous &&
4633
+ typeof previous.equals === 'function') {
4634
+ return previous.equals(next);
4635
+ }
4636
+ return false;
4637
+ }
4638
+ function cameraOptionsEqual(previous, next) {
4639
+ if (Object.is(previous, next))
4640
+ return true;
4641
+ if (!previous || !next)
4642
+ return false;
4643
+ if (is.three(previous, 'isCamera') || is.three(next, 'isCamera'))
4644
+ return false;
4645
+ const previousKeys = Object.keys(previous);
4646
+ const nextKeys = Object.keys(next);
4647
+ const previousRecord = previous;
4648
+ const nextRecord = next;
4649
+ return (previousKeys.length === nextKeys.length &&
4650
+ previousKeys.every((key) => Object.prototype.hasOwnProperty.call(nextRecord, key) &&
4651
+ configurationValuesEqual(previousRecord[key], nextRecord[key])));
4652
+ }
4653
+ function cameraConfigurationsEqual(previous, next) {
4654
+ return (!!previous &&
4655
+ previous.orthographic === next.orthographic &&
4656
+ cameraOptionsEqual(previous.camera, next.camera) &&
4657
+ configurationValuesEqual(previous.lookAt, next.lookAt));
4658
+ }
4659
+ function createManagedCamera(configuration, store, size = store.snapshot.size) {
4660
+ const { camera: cameraOptions, lookAt, orthographic } = configuration;
4661
+ const isCamera = is.three(cameraOptions, 'isCamera');
4662
+ let camera = isCamera ? cameraOptions : makeCameraInstance(orthographic, size);
4663
+ if (!isCamera) {
4664
+ camera.position.z = 5;
4665
+ if (cameraOptions) {
4666
+ applyProps(camera, cameraOptions);
4667
+ if ('aspect' in cameraOptions ||
4668
+ 'left' in cameraOptions ||
4669
+ 'right' in cameraOptions ||
4670
+ 'top' in cameraOptions ||
4671
+ 'bottom' in cameraOptions) {
4672
+ Object.assign(camera, { manual: true });
4673
+ camera.updateProjectionMatrix();
4674
+ }
4675
+ }
4676
+ if (!cameraOptions?.rotation && !cameraOptions?.quaternion) {
4677
+ if (Array.isArray(lookAt))
4678
+ camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
4679
+ else if (typeof lookAt === 'number')
4680
+ camera.lookAt(lookAt, lookAt, lookAt);
4681
+ else if (lookAt?.isVector3)
4682
+ camera.lookAt(lookAt);
4683
+ else
4684
+ camera.lookAt(0, 0, 0);
4685
+ }
4686
+ camera.updateProjectionMatrix?.();
4687
+ }
4688
+ if (!is.instance(camera))
4689
+ camera = prepare(camera, '', { store });
4690
+ return camera;
4691
+ }
4692
+ function cancelPerformanceRegression(store) {
4693
+ const regress = store.snapshot.performance.regress;
4694
+ regress.cancel?.();
4695
+ }
3839
4696
  /**
3840
4697
  * Creates a canvas root initializer function.
3841
4698
  *
@@ -3857,10 +4714,12 @@ function canvasRootInitializer(injector) {
3857
4714
  return assertInjector(canvasRootInitializer, injector, () => {
3858
4715
  const injectedStore = injectStore();
3859
4716
  const loop = injectLoop();
4717
+ const rootInjector = inject(Injector);
3860
4718
  return (canvas) => {
3861
4719
  const exist = roots.has(canvas);
3862
4720
  let store = roots.get(canvas);
3863
- if (store) {
4721
+ const previousLifecycle = rootLifecycles.get(canvas);
4722
+ if (store && !previousLifecycle?.teardown) {
3864
4723
  console.warn('[NGT] Same canvas root is being created twice');
3865
4724
  }
3866
4725
  store ||= injectedStore;
@@ -3870,33 +4729,95 @@ function canvasRootInitializer(injector) {
3870
4729
  if (!exist) {
3871
4730
  roots.set(canvas, store);
3872
4731
  }
4732
+ if (previousLifecycle?.teardown) {
4733
+ clearTimeout(previousLifecycle.teardown);
4734
+ previousLifecycle.teardown = undefined;
4735
+ const performance = store.snapshot.performance;
4736
+ if (performance.current !== performance.max) {
4737
+ store.update((state) => ({
4738
+ performance: { ...state.performance, current: state.performance.max },
4739
+ }));
4740
+ }
4741
+ }
4742
+ const owner = {};
4743
+ const lifecycle = previousLifecycle?.store === store ? previousLifecycle : { store, owner };
4744
+ lifecycle.owner = owner;
4745
+ rootLifecycles.set(canvas, lifecycle);
4746
+ lifecycle.cameraWatcher ??= effect(() => {
4747
+ const activeCamera = store.camera();
4748
+ const desired = lifecycle.cameraConfiguration;
4749
+ if (!desired ||
4750
+ !lifecycle.managedCamera ||
4751
+ activeCamera !== lifecycle.managedCamera ||
4752
+ cameraConfigurationsEqual(lifecycle.appliedCameraConfiguration, desired))
4753
+ return;
4754
+ const camera = createManagedCamera(desired, store);
4755
+ lifecycle.managedCamera = camera;
4756
+ lifecycle.appliedCameraConfiguration = desired;
4757
+ const raycaster = store.snapshot.raycaster;
4758
+ if (raycaster)
4759
+ raycaster.camera = camera;
4760
+ store.update({ camera });
4761
+ }, { injector: rootInjector });
3873
4762
  let isConfigured = false;
3874
- let lastCamera;
4763
+ let destroyRequested = false;
3875
4764
  return {
3876
- isConfigured,
4765
+ get isConfigured() {
4766
+ return isConfigured;
4767
+ },
3877
4768
  destroy: (timeout = 500) => {
4769
+ if (destroyRequested)
4770
+ return;
4771
+ destroyRequested = true;
3878
4772
  const root = roots.get(canvas);
3879
- if (root) {
4773
+ if (root &&
4774
+ root === store &&
4775
+ rootLifecycles.get(canvas) === lifecycle &&
4776
+ lifecycle.owner === owner) {
3880
4777
  root.update((state) => ({ internal: { ...state.internal, active: false } }));
3881
- setTimeout(() => {
4778
+ cancelPerformanceRegression(root);
4779
+ const teardown = setTimeout(() => {
4780
+ if (roots.get(canvas) !== root ||
4781
+ rootLifecycles.get(canvas) !== lifecycle ||
4782
+ lifecycle.owner !== owner ||
4783
+ lifecycle.teardown !== teardown)
4784
+ return;
4785
+ lifecycle.teardown = undefined;
3882
4786
  try {
3883
4787
  const state = root.snapshot;
3884
- state.events.disconnect?.();
3885
- state.gl?.renderLists?.dispose?.();
3886
- state.gl?.dispose?.();
3887
- state.gl?.forceContextLoss?.();
3888
- if (state.gl?.xr)
3889
- state.xr.disconnect();
3890
- dispose(state.scene);
3891
- roots.delete(canvas);
4788
+ const failures = [];
4789
+ const attempt = (cleanup) => {
4790
+ try {
4791
+ cleanup();
4792
+ }
4793
+ catch (error) {
4794
+ failures.push(error);
4795
+ }
4796
+ };
4797
+ attempt(() => state.events.disconnect?.());
4798
+ attempt(() => state.gl?.renderLists?.dispose?.());
4799
+ attempt(() => state.gl?.dispose?.());
4800
+ attempt(() => state.gl?.forceContextLoss?.());
4801
+ attempt(() => state.xr?.disconnect?.());
4802
+ attempt(() => dispose(state.scene));
4803
+ attempt(() => lifecycle.cameraWatcher?.destroy());
4804
+ if (failures.length) {
4805
+ console.error('[NGT] Errors while destroying Canvas Root', failures);
4806
+ }
3892
4807
  }
3893
- catch (e) {
3894
- console.error('[NGT] Unexpected error while destroying Canvas Root', e);
4808
+ finally {
4809
+ if (roots.get(canvas) === root)
4810
+ roots.delete(canvas);
4811
+ if (rootLifecycles.get(canvas) === lifecycle)
4812
+ rootLifecycles.delete(canvas);
3895
4813
  }
3896
4814
  }, timeout);
4815
+ lifecycle.teardown = teardown;
3897
4816
  }
3898
4817
  },
3899
4818
  configure: (inputs) => {
4819
+ if (destroyRequested || rootLifecycles.get(canvas) !== lifecycle || lifecycle.owner !== owner)
4820
+ return;
3900
4821
  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;
3901
4822
  const state = store.snapshot;
3902
4823
  const stateToUpdate = {};
@@ -3915,44 +4836,20 @@ function canvasRootInitializer(injector) {
3915
4836
  if (!is.equ(params, raycaster.params, shallowLoose)) {
3916
4837
  applyProps(raycaster, { params: { ...raycaster.params, ...(params || {}) } });
3917
4838
  }
3918
- // Create default camera, don't overwrite any user-set state
4839
+ const cameraConfiguration = {
4840
+ camera: snapshotCameraOptions(cameraOptions),
4841
+ lookAt: cloneConfigurationValue(lookAt),
4842
+ orthographic,
4843
+ };
4844
+ lifecycle.cameraConfiguration = cameraConfiguration;
4845
+ // Create the root-managed camera, but don't overwrite a camera installed directly in the store.
3919
4846
  if (!state.camera ||
3920
- (state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose))) {
3921
- lastCamera = cameraOptions;
3922
- const isCamera = is.three(cameraOptions, 'isCamera');
3923
- let camera = isCamera
3924
- ? cameraOptions
3925
- : makeCameraInstance(orthographic, sizeOptions ?? state.size);
3926
- if (!isCamera) {
3927
- camera.position.z = 5;
3928
- if (cameraOptions) {
3929
- applyProps(camera, cameraOptions);
3930
- if ('aspect' in cameraOptions ||
3931
- 'left' in cameraOptions ||
3932
- 'right' in cameraOptions ||
3933
- 'top' in cameraOptions ||
3934
- 'bottom' in cameraOptions) {
3935
- Object.assign(camera, { manual: true });
3936
- camera?.updateProjectionMatrix();
3937
- }
3938
- }
3939
- // always look at center or passed-in lookAt by default
3940
- if (!state.camera && !cameraOptions?.rotation && !cameraOptions?.quaternion) {
3941
- if (Array.isArray(lookAt))
3942
- camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
3943
- else if (typeof lookAt === 'number')
3944
- camera.lookAt(lookAt, lookAt, lookAt);
3945
- else if (lookAt?.isVector3)
3946
- camera.lookAt(lookAt);
3947
- else
3948
- camera.lookAt(0, 0, 0);
3949
- }
3950
- // update projection matrix after applyprops
3951
- camera.updateProjectionMatrix?.();
3952
- }
3953
- if (!is.instance(camera))
3954
- camera = prepare(camera, '', { store });
4847
+ (state.camera === lifecycle.managedCamera &&
4848
+ !cameraConfigurationsEqual(lifecycle.appliedCameraConfiguration, cameraConfiguration))) {
4849
+ const camera = createManagedCamera(cameraConfiguration, store, sizeOptions ?? state.size);
3955
4850
  stateToUpdate.camera = camera;
4851
+ lifecycle.managedCamera = camera;
4852
+ lifecycle.appliedCameraConfiguration = cameraConfiguration;
3956
4853
  // Configure raycaster
3957
4854
  // https://github.com/pmndrs/react-xr/issues/300
3958
4855
  raycaster.camera = camera;
@@ -4321,15 +5218,24 @@ function elementEvents(target, events, { injector } = {}) {
4321
5218
  const targetObj = resolveRef(target());
4322
5219
  if (!targetObj || !is.instance(targetObj))
4323
5220
  return;
5221
+ const currentCleanUps = [];
4324
5222
  Object.keys(events).forEach((eventName) => {
4325
- cleanUps.push(renderer.listen(targetObj, eventName, events[eventName]));
5223
+ const cleanUp = renderer.listen(targetObj, eventName, events[eventName]);
5224
+ currentCleanUps.push(cleanUp);
5225
+ cleanUps.push(cleanUp);
4326
5226
  });
4327
5227
  onCleanup(() => {
4328
- cleanUps.forEach((cleanUp) => cleanUp());
5228
+ for (const cleanUp of currentCleanUps) {
5229
+ cleanUp();
5230
+ const index = cleanUps.indexOf(cleanUp);
5231
+ if (index >= 0)
5232
+ cleanUps.splice(index, 1);
5233
+ }
4329
5234
  });
4330
5235
  });
4331
5236
  inject(DestroyRef).onDestroy(() => {
4332
- cleanUps.forEach((cleanUp) => cleanUp());
5237
+ for (const cleanUp of cleanUps.splice(0))
5238
+ cleanUp();
4333
5239
  });
4334
5240
  return cleanUps;
4335
5241
  });
@@ -4448,15 +5354,24 @@ function objectEvents(target, events, { injector } = {}) {
4448
5354
  const targetObj = resolveRef(target());
4449
5355
  if (!targetObj || !is.instance(targetObj))
4450
5356
  return;
5357
+ const currentCleanUps = [];
4451
5358
  Object.entries(events).forEach(([eventName, eventHandler]) => {
4452
- cleanUps.push(renderer.listen(targetObj, eventName, eventHandler));
5359
+ const cleanUp = renderer.listen(targetObj, eventName, eventHandler);
5360
+ currentCleanUps.push(cleanUp);
5361
+ cleanUps.push(cleanUp);
4453
5362
  });
4454
5363
  onCleanup(() => {
4455
- cleanUps.forEach((cleanUp) => cleanUp());
5364
+ for (const cleanUp of currentCleanUps) {
5365
+ cleanUp();
5366
+ const index = cleanUps.indexOf(cleanUp);
5367
+ if (index >= 0)
5368
+ cleanUps.splice(index, 1);
5369
+ }
4456
5370
  });
4457
5371
  });
4458
5372
  inject(DestroyRef).onDestroy(() => {
4459
- cleanUps.forEach((cleanUp) => cleanUp());
5373
+ for (const cleanUp of cleanUps.splice(0))
5374
+ cleanUp();
4460
5375
  });
4461
5376
  return cleanUps;
4462
5377
  });
@@ -4488,5 +5403,5 @@ function hasListener(...emitterRefs) {
4488
5403
  * Generated bundle index. Do not edit.
4489
5404
  */
4490
5405
 
4491
- export { NGT_APPLY_PROPS, NGT_ARGS_FLAG, NGT_CANVAS_CONTENT_FLAG, NGT_CATALOGUE, NGT_DELEGATE_RENDERER_DESTROY_NODE_PATCHED_FLAG, NGT_DOM_PARENT_FLAG, NGT_GET_NODE_ATTRIBUTE_FLAG, NGT_HTML_FLAG, NGT_INTERNAL_ADD_COMMENT_FLAG, NGT_INTERNAL_SET_PARENT_COMMENT_FLAG, NGT_LOOP, NGT_PARENT_FLAG, NGT_PORTAL_CONTENT_FLAG, NGT_RENDERER_NODE_FLAG, NGT_RENDERER_OPTIONS, NGT_STORE, NgtArgs, NgtElementEvents, NgtHTML, NgtHexify, NgtObjectEvents, NgtParent, NgtPortal, NgtPortalAutoRender, NgtPortalContent, NgtPortalImpl, NgtRenderer2, NgtRendererFactory2, NgtRoutedScene, NgtSelect, NgtSelection, NgtSelectionApi, THREE_NATIVE_EVENTS, addAfterEffect, addEffect, addTail, applyProps, attach, beforeRender, canvasRootInitializer, checkNeedsUpdate, checkUpdate, createAttachFunction, createEvents, detach, dispose, elementEvents, extend, flushGlobalEffects, getEmitter, getInstanceState, getLocalState, hasListener, injectBeforeRender, injectCatalogue, injectLoader, injectLoop, injectObjectEvents, injectStore, invalidateInstance, is, loaderResource, makeCameraInstance, makeDpr, makeId, makeObjectGraph, makeRendererInstance, merge, objectEvents, omit, pick, prepare, provideHTMLDomElement, remove, removeInteractivity, resolveInstanceKey, resolveRef, roots, signalState, storeFactory, toDeepSignal, updateCamera, uuidv4Fallback, vector2, vector3, vector4 };
5406
+ export { NGT_APPLY_PROPS, NGT_CATALOGUE, NGT_DOM_PARENT_FLAG, NGT_GET_NODE_ATTRIBUTE_FLAG, NGT_HTML_FLAG, NGT_LOOP, NGT_RENDERER_CONTEXT_FLAG, NGT_RENDERER_NODE_FLAG, NGT_RENDERER_OPTIONS, NGT_STORE, NgtArgs, NgtElementEvents, NgtHTML, NgtHexify, NgtObjectEvents, NgtParent, NgtPortal, NgtPortalAutoRender, NgtPortalContent, NgtPortalImpl, NgtRenderer2, NgtRendererFactory2, NgtRoutedScene, NgtSelect, NgtSelection, NgtSelectionApi, THREE_NATIVE_EVENTS, addAfterEffect, addEffect, addTail, applyProps, attach, beforeRender, canvasRootInitializer, checkNeedsUpdate, checkUpdate, createAttachFunction, createEvents, detach, dispose, elementEvents, extend, flushAncestorNotifications, flushGlobalEffects, getEmitter, getInstanceState, getLocalState, hasListener, injectBeforeRender, injectCatalogue, injectLoader, injectLoop, injectObjectEvents, injectStore, invalidateInstance, is, loaderResource, makeCameraInstance, makeDpr, makeId, makeObjectGraph, makeRendererInstance, merge, objectEvents, omit, pick, prepare, provideHTMLDomElement, remove, removeInteractivity, resolveInstanceKey, resolveRef, roots, setRendererAnchor, signalState, storeFactory, toDeepSignal, updateCamera, uuidv4Fallback, vector2, vector3, vector4 };
4492
5407
  //# sourceMappingURL=angular-three.mjs.map