angular-three 4.2.2 → 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 });
@@ -614,6 +711,15 @@ function updateCamera(camera, size) {
614
711
  }
615
712
  }
616
713
 
714
+ /** RFC 4122 v4 UUID — safe in non-secure contexts (e.g. http:// on LAN). */
715
+ function uuidv4Fallback() {
716
+ const bytes = new Uint8Array(16);
717
+ crypto.getRandomValues(bytes);
718
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
719
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
720
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));
721
+ return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`;
722
+ }
617
723
  /**
618
724
  * @deprecated Use `getInstanceState` instead. Will be removed in 5.0.0
619
725
  * @param obj - The object to get local state from
@@ -646,6 +752,7 @@ function getInstanceState(obj) {
646
752
  return undefined;
647
753
  return obj.__ngt__ || undefined;
648
754
  }
755
+ const pointerEventRegistrations = new WeakMap();
649
756
  /**
650
757
  * Invalidates an instance, triggering a re-render of the scene.
651
758
  *
@@ -710,7 +817,7 @@ function prepare(object, type, instanceState) {
710
817
  const [_nonObjects] = [nonObjects(), geometryStamp()];
711
818
  return _nonObjects;
712
819
  }, ...(ngDevMode ? [{ debugName: "nonObjectsChanged" }] : /* istanbul ignore next */ []));
713
- instance.__ngt_id__ = crypto.randomUUID();
820
+ instance.__ngt_id__ = typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : uuidv4Fallback();
714
821
  instance.__ngt__ = {
715
822
  previousAttach: null,
716
823
  type,
@@ -721,16 +828,12 @@ function prepare(object, type, instanceState) {
721
828
  parent: hierarchyStore.parent,
722
829
  objects: hierarchyStore.objects,
723
830
  nonObjects: nonObjectsChanged,
724
- add(object, type) {
831
+ add(object, type, before) {
725
832
  const current = instance.__ngt__.hierarchyStore.snapshot[type];
726
- const foundIndex = current.findIndex((node) => object === node || (!!object['uuid'] && !!node['uuid'] && object['uuid'] === node['uuid']));
727
- if (foundIndex > -1) {
728
- current.splice(foundIndex, 1, object);
729
- instance.__ngt__.hierarchyStore.update({ [type]: current });
730
- }
731
- else {
732
- instance.__ngt__.hierarchyStore.update((prev) => ({ [type]: [...prev[type], object] }));
733
- }
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 });
734
837
  notifyAncestors(instance.__ngt__.hierarchyStore.snapshot.parent, type);
735
838
  },
736
839
  remove(object, type) {
@@ -753,26 +856,42 @@ function prepare(object, type, instanceState) {
753
856
  setPointerEvent: {
754
857
  value: (eventName, callback) => {
755
858
  const iS = getInstanceState(instance);
756
- if (!iS.handlers)
757
- iS.handlers = {};
758
- // try to get the previous handler. compound might have one, the THREE object might also have one with the same name
759
- const previousHandler = iS.handlers[eventName];
760
- // readjust the callback
761
- const updatedCallback = (event) => {
762
- if (previousHandler)
763
- previousHandler(event);
764
- callback(event);
765
- };
766
- Object.assign(iS.handlers, { [eventName]: updatedCallback });
767
- // 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;
768
880
  iS.eventCount += 1;
769
- // clean up the event listener by removing the target from the interaction array
881
+ let active = true;
770
882
  return () => {
771
- const iS = getInstanceState(instance);
772
- if (iS) {
773
- iS.handlers && delete iS.handlers[eventName];
774
- 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];
775
892
  }
893
+ if (iS.eventCount === 0)
894
+ iS.removeInteraction?.(iS.store);
776
895
  };
777
896
  },
778
897
  configurable: true,
@@ -808,10 +927,27 @@ function prepare(object, type, instanceState) {
808
927
  root = root.snapshot.previousRoot;
809
928
  }
810
929
  if (root.snapshot.internal) {
811
- const interactions = root.snapshot.internal.interaction;
812
- 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);
813
934
  if (index >= 0)
814
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
+ }
815
951
  }
816
952
  },
817
953
  configurable: true,
@@ -819,7 +955,8 @@ function prepare(object, type, instanceState) {
819
955
  });
820
956
  return instance;
821
957
  }
822
- const notificationCache = new Map();
958
+ const pendingAncestorNotifications = new Map();
959
+ let ancestorNotificationScheduled = false;
823
960
  /**
824
961
  * Notify ancestors about changes to a THREE.js objects' children
825
962
  *
@@ -827,37 +964,41 @@ const notificationCache = new Map();
827
964
  * in which case the model matrices will be settled later. `NgtsCenter` needs to know about this
828
965
  * matrices change to re-center everything inside of it.
829
966
  *
830
- * The implementation here uses a naive approach to reduce the number of notifications; we cache
831
- * the notifications by the instance ID and the type of the notification.
832
- *
833
- * 1. If there's no cache or
834
- * 2. If the type is different for the same instance or
835
- * 3. We've skipped the notifications for this instance more than a certain amount
836
- *
837
- * 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.
838
969
  */
839
970
  function notifyAncestors(instance, type) {
840
- if (!instance)
841
- return;
842
- const localState = getInstanceState(instance);
843
- if (!localState)
844
- return;
845
- const id = instance.__ngt_id__ || instance['uuid'];
846
- if (!id)
847
- return;
848
- const maxNotificationSkipCount = localState.store?.snapshot.maxNotificationSkipCount || 5;
849
- const cached = notificationCache.get(id);
850
- if (!cached || cached.lastType !== type || cached.skipCount > maxNotificationSkipCount) {
851
- notificationCache.set(id, { skipCount: 0, lastType: type });
852
- if (notificationCache.size === 1) {
853
- queueMicrotask(() => notificationCache.clear());
854
- }
855
- const { parent } = localState.hierarchyStore.snapshot;
856
- localState.hierarchyStore.update({ [type]: (localState.hierarchyStore.snapshot[type] || []).slice() });
857
- 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)
858
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);
859
1001
  }
860
- notificationCache.set(id, { ...cached, skipCount: cached.skipCount + 1 });
861
1002
  }
862
1003
 
863
1004
  /**
@@ -880,9 +1021,9 @@ function diffProps(instance, props) {
880
1021
  key = 'outputColorSpace';
881
1022
  }
882
1023
  }
883
- if (is.equ(propValue, instance[key]))
1024
+ if (is.equ(propValue, resolveInstanceKey(instance, key).targetProp))
884
1025
  continue;
885
- changes.push([propKey, propValue]);
1026
+ changes.push([key, propValue]);
886
1027
  }
887
1028
  return changes;
888
1029
  }
@@ -896,6 +1037,41 @@ const NGT_APPLY_PROPS = '__ngt_apply_props__';
896
1037
  // https://github.com/mrdoob/three.js/pull/27042
897
1038
  // https://github.com/mrdoob/three.js/pull/22748
898
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
+ }
899
1075
  /**
900
1076
  * Resolves a property key that may contain dot notation (pierced props).
901
1077
  *
@@ -971,11 +1147,7 @@ function applyProps(instance, props) {
971
1147
  const rootState = localState?.store?.snapshot ?? instance[NGT_APPLY_PROPS]?.snapshot ?? {};
972
1148
  const changes = diffProps(instance, props);
973
1149
  for (let i = 0; i < changes.length; i++) {
974
- let [key, value] = changes[i];
975
- // Ignore setting undefined props
976
- // https://github.com/pmndrs/react-three-fiber/issues/274
977
- if (value === undefined)
978
- continue;
1150
+ const [key, nextValue] = changes[i];
979
1151
  // Alias (output)encoding => (output)colorSpace (since r152)
980
1152
  // https://github.com/pmndrs/react-three-fiber/pull/2829
981
1153
  // if (is.colorSpaceExist(instance)) {
@@ -992,10 +1164,7 @@ function applyProps(instance, props) {
992
1164
  // }
993
1165
  // }
994
1166
  const { root, targetKey, targetProp } = resolveInstanceKey(instance, key);
995
- // we have switched due to pierced props
996
- if (root !== instance) {
997
- return applyProps(root, { [targetKey]: value });
998
- }
1167
+ const value = resolvePropertyValue(root, targetKey, targetProp, nextValue);
999
1168
  // Layers have no copy function, we must therefore copy the mask property
1000
1169
  if (targetProp instanceof THREE.Layers && value instanceof THREE.Layers) {
1001
1170
  targetProp.mask = value.mask;
@@ -1054,22 +1223,13 @@ function applyProps(instance, props) {
1054
1223
  }
1055
1224
  checkUpdate(root[targetKey]);
1056
1225
  checkUpdate(targetProp);
1057
- invalidateInstance(instance);
1058
1226
  }
1059
- const instanceHandlersCount = localState?.eventCount;
1060
1227
  const parent = localState?.hierarchyStore?.snapshot.parent;
1061
- if (parent && rootState.internal && instance['raycast'] && instanceHandlersCount !== localState?.eventCount) {
1062
- // Pre-emptively remove the instance from the interaction manager
1063
- const index = rootState.internal.interaction.indexOf(instance);
1064
- if (index > -1)
1065
- rootState.internal.interaction.splice(index, 1);
1066
- // Add the instance to the interaction manager only when it has handlers
1067
- if (localState?.eventCount)
1068
- rootState.internal.interaction.push(instance);
1069
- }
1070
1228
  if (parent && localState?.onUpdate && changes.length) {
1071
1229
  localState.onUpdate(instance);
1072
1230
  }
1231
+ if (changes.length)
1232
+ invalidateInstance(instance);
1073
1233
  // clearing the intermediate store from the instance
1074
1234
  if (instance[NGT_APPLY_PROPS])
1075
1235
  delete instance[NGT_APPLY_PROPS];
@@ -1083,6 +1243,7 @@ function applyProps(instance, props) {
1083
1243
  * allowing the custom renderer to instantiate objects when elements are created.
1084
1244
  */
1085
1245
  const catalogue = {};
1246
+ const catalogueOwnership = new Map();
1086
1247
  /**
1087
1248
  * Registers Three.js constructors for use in templates.
1088
1249
  *
@@ -1108,10 +1269,59 @@ const catalogue = {};
1108
1269
  * ```
1109
1270
  */
1110
1271
  function extend(objects) {
1111
- const keys = Object.keys(objects);
1112
- 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;
1113
1296
  return () => {
1114
- 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
+ }
1115
1325
  };
1116
1326
  }
1117
1327
  /**
@@ -1121,6 +1331,9 @@ function extend(objects) {
1121
1331
  */
1122
1332
  function remove(...keys) {
1123
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);
1124
1337
  delete catalogue[key];
1125
1338
  }
1126
1339
  }
@@ -1137,34 +1350,6 @@ function injectCatalogue() {
1137
1350
  return inject(NGT_CATALOGUE);
1138
1351
  }
1139
1352
 
1140
- function isRendererNode(node) {
1141
- return !!node && typeof node === 'object' && NGT_RENDERER_NODE_FLAG in node;
1142
- }
1143
- function createRendererNode(type, node, document) {
1144
- const state = [type, false, undefined, undefined, undefined, undefined, []];
1145
- const rendererNode = Object.assign(node, { [NGT_RENDERER_NODE_FLAG]: state });
1146
- // NOTE: assign ownerDocument to node so we can use HostListener in Component
1147
- if (!rendererNode['ownerDocument'])
1148
- rendererNode['ownerDocument'] = document;
1149
- // NOTE: Angular SSR calls `node.getAttribute()` to retrieve hydration info on a node
1150
- if (!('getAttribute' in rendererNode) || typeof rendererNode['getAttribute'] !== 'function') {
1151
- const getNodeAttribute = (name) => rendererNode[name];
1152
- getNodeAttribute[NGT_GET_NODE_ATTRIBUTE_FLAG] = true;
1153
- Object.defineProperty(rendererNode, 'getAttribute', { value: getNodeAttribute, configurable: true });
1154
- }
1155
- return rendererNode;
1156
- }
1157
- function setRendererParentNode(node, parent) {
1158
- if (!node.__ngt_renderer__[5 /* NgtRendererClassId.parent */]) {
1159
- node.__ngt_renderer__[5 /* NgtRendererClassId.parent */] = parent;
1160
- }
1161
- }
1162
- function addRendererChildNode(node, child) {
1163
- if (!node.__ngt_renderer__[6 /* NgtRendererClassId.children */].includes(child)) {
1164
- node.__ngt_renderer__[6 /* NgtRendererClassId.children */].push(child);
1165
- }
1166
- }
1167
-
1168
1353
  const idCache = {};
1169
1354
  /**
1170
1355
  * Generates a unique identifier.
@@ -1271,6 +1456,7 @@ function makeObjectGraph(object) {
1271
1456
  return data;
1272
1457
  }
1273
1458
 
1459
+ const NGT_EVENT_LAYER = Symbol('NGT_EVENT_LAYER');
1274
1460
  /**
1275
1461
  * @fileoverview Event handling system for Angular Three.
1276
1462
  *
@@ -1364,52 +1550,57 @@ function createEvents(store) {
1364
1550
  if (!current)
1365
1551
  eventsObjects.push(eventsObject);
1366
1552
  }
1367
- if (!state.previousRoot) {
1368
- // Make sure root-level pointer and ray are set up
1369
- state.events.compute?.(event, store, null);
1370
- }
1371
- // Skip work if there are no event objects
1372
- if (eventsObjects.length === 0)
1373
- return intersections;
1374
- // Reset all raycaster cameras to undefined - use for loop for better performance
1375
- const eventsObjectsLen = eventsObjects.length;
1376
- for (let i = 0; i < eventsObjectsLen; i++) {
1377
- const objectRootState = getInstanceState(eventsObjects[i])?.store?.snapshot;
1378
- if (objectRootState) {
1379
- objectRootState.raycaster.camera = undefined;
1380
- }
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]);
1381
1563
  }
1382
- // 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;
1383
1582
  const raycastResults = [];
1384
- function handleRaycast(obj) {
1385
- const objStore = getInstanceState(obj)?.store;
1386
- const objState = objStore?.snapshot;
1387
- // Skip event handling when noEvents is set, or when the raycasters camera is null
1388
- if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
1389
- return [];
1390
- // When the camera is undefined we have to call the event layers update function
1391
- if (objState.raycaster.camera === undefined) {
1392
- objState.events.compute?.(event, objStore, objState.previousRoot);
1393
- // If the camera is still undefined we have to skip this layer entirely
1394
- if (objState.raycaster.camera === undefined)
1395
- objState.raycaster.camera = null;
1396
- }
1397
- // Intersect object by object
1398
- return objState.raycaster.camera ? objState.raycaster.intersectObject(obj, true) : [];
1399
- }
1400
- // Collect events
1401
- for (let i = 0; i < eventsObjectsLen; i++) {
1402
- const objResults = handleRaycast(eventsObjects[i]);
1403
- 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)
1404
1592
  continue;
1405
- for (let j = 0; j < objResults.length; j++) {
1406
- 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);
1407
1598
  }
1408
1599
  }
1409
1600
  // Sort by event priority and distance
1410
1601
  raycastResults.sort((a, b) => {
1411
- const aState = getInstanceState(a.object)?.store?.snapshot;
1412
- 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;
1413
1604
  if (!aState || !bState)
1414
1605
  return a.distance - b.distance;
1415
1606
  return bState.events.priority - aState.events.priority || a.distance - b.distance;
@@ -1471,7 +1662,8 @@ function createEvents(store) {
1471
1662
  return;
1472
1663
  });
1473
1664
  }
1474
- 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;
1475
1667
  const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
1476
1668
  const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1477
1669
  const setPointerCapture = (id) => {
@@ -1618,6 +1810,15 @@ function createEvents(store) {
1618
1810
  const hits = intersect(event, filter);
1619
1811
  // Only calculate distance for click events to avoid unnecessary math
1620
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
+ };
1621
1822
  // Save initial coordinates on pointer-down
1622
1823
  if (name === 'pointerdown') {
1623
1824
  internal.initialClick = [event.offsetX, event.offsetY];
@@ -1675,23 +1876,13 @@ function createEvents(store) {
1675
1876
  // Forward all events back to their respective handlers with the exception of click events,
1676
1877
  // which must use the initial target
1677
1878
  if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1678
- // Get objects not in initialHits for pointer missed - avoid creating new arrays if possible
1679
- const missedObjects = internal.interaction.filter((object) => !internal.initialHits.includes(object));
1680
- // Call pointerMissed only if we have objects to notify
1681
- if (missedObjects.length > 0) {
1682
- pointerMissed(event, missedObjects);
1683
- }
1879
+ notifyMissedOnce();
1684
1880
  // Now call the handler
1685
1881
  handler(data);
1686
1882
  }
1687
1883
  }
1688
1884
  else if (isClickEvent && internal.initialHits.includes(eventObject)) {
1689
- // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
1690
- const missedObjects = internal.interaction.filter((object) => !internal.initialHits.includes(object));
1691
- // Call pointerMissed only if we have objects to notify
1692
- if (missedObjects.length > 0) {
1693
- pointerMissed(event, missedObjects);
1694
- }
1885
+ notifyMissedOnce();
1695
1886
  }
1696
1887
  }
1697
1888
  }
@@ -1750,10 +1941,22 @@ function attach(object, value, paths = [], useApplyProps = false) {
1750
1941
  function detach(parent, child, attachProp) {
1751
1942
  const childInstanceState = getInstanceState(child);
1752
1943
  if (childInstanceState) {
1753
- if (Array.isArray(attachProp))
1754
- 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
+ }
1755
1958
  else
1756
- childInstanceState.previousAttach?.();
1959
+ previousAttach?.();
1757
1960
  }
1758
1961
  }
1759
1962
  function assignEmpty(obj, base, shouldAssignStoreForApplyProps = false) {
@@ -1813,18 +2016,21 @@ function kebabToPascal(str) {
1813
2016
  }
1814
2017
  return pascalStr;
1815
2018
  }
1816
- function propagateStoreRecursively(node, parentNode) {
2019
+ function propagateStoreRecursively(node, parentNode, storeOverride) {
1817
2020
  const iS = getInstanceState(node);
1818
2021
  const pIS = getInstanceState(parentNode);
1819
2022
  if (!iS || !pIS)
1820
2023
  return;
2024
+ const store = storeOverride ?? pIS.store;
2025
+ if (!store)
2026
+ return;
1821
2027
  // assign store on child if not already exist
1822
2028
  // or child store is not the same as parent store
1823
2029
  // or child store is the parent of parent store
1824
- if (!iS.store || iS.store !== pIS.store || iS.store === pIS.store.snapshot.previousRoot) {
1825
- iS.store = pIS.store;
2030
+ if (!iS.store || iS.store !== store || iS.store === store.snapshot.previousRoot) {
2031
+ iS.store = store;
1826
2032
  // Call addInteraction if it exists
1827
- iS.addInteraction?.(pIS.store);
2033
+ iS.addInteraction?.(store);
1828
2034
  // Collect all children (objects and nonObjects)
1829
2035
  const children = [
1830
2036
  ...(iS.objects ? untracked(iS.objects) : []),
@@ -1832,60 +2038,88 @@ function propagateStoreRecursively(node, parentNode) {
1832
2038
  ];
1833
2039
  // Recursively reassign the store for each child
1834
2040
  for (const child of children) {
1835
- propagateStoreRecursively(child, node);
2041
+ propagateStoreRecursively(child, node, store);
1836
2042
  }
1837
2043
  }
1838
2044
  }
1839
- 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) {
1840
2057
  const pIS = getInstanceState(parent);
1841
2058
  const cIS = getInstanceState(child);
1842
2059
  if (!pIS || !cIS) {
1843
2060
  throw new Error(`[NGT] THREE instances need to be prepared with local state.`);
1844
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
+ }
1845
2078
  // whether the child is added to the parent with parent.add()
1846
2079
  let added = false;
1847
- // propagate store recursively
1848
- propagateStoreRecursively(child, parent);
1849
2080
  if (cIS.attach) {
1850
2081
  const attachProp = cIS.attach;
1851
2082
  if (typeof attachProp === 'function') {
1852
2083
  let attachCleanUp = undefined;
1853
2084
  if (cIS.type === 'ngt-value') {
1854
- if (cIS.hierarchyStore.snapshot.parent !== parent) {
1855
- cIS.setParent(parent);
1856
- }
1857
2085
  // at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
1858
2086
  if (child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */] === undefined)
1859
2087
  return;
2088
+ if (cIS.hierarchyStore.snapshot.parent !== parent) {
2089
+ cIS.setParent(parent);
2090
+ }
1860
2091
  attachCleanUp = attachProp(parent, child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */], cIS.store);
1861
2092
  }
1862
2093
  else {
1863
2094
  attachCleanUp = attachProp(parent, child, cIS.store);
1864
2095
  }
1865
- if (attachCleanUp)
1866
- cIS.previousAttach = attachCleanUp;
2096
+ cIS.previousAttach = attachCleanUp;
1867
2097
  }
1868
2098
  else {
1869
2099
  // we skip attach none if set explicitly
1870
2100
  if (attachProp[0] === 'none') {
2101
+ cIS.previousAttach = undefined;
2102
+ pIS.add?.(child, 'nonObjects', before);
2103
+ cIS.setParent(parent);
1871
2104
  invalidateInstance(child);
2105
+ invalidateInstance(parent);
1872
2106
  return;
1873
2107
  }
1874
2108
  // handle material array
1875
2109
  if (attachProp[0] === 'material' &&
1876
- attachProp[1] &&
1877
- typeof Number(attachProp[1]) === 'number' &&
2110
+ attachProp[1] !== undefined &&
2111
+ !Number.isNaN(Number(attachProp[1])) &&
1878
2112
  is.three(child, 'isMaterial') &&
1879
2113
  !Array.isArray(parent['material'])) {
1880
2114
  parent['material'] = [];
1881
2115
  }
1882
2116
  if (cIS.type === 'ngt-value') {
1883
- if (cIS.hierarchyStore.snapshot.parent !== parent) {
1884
- cIS.setParent(parent);
1885
- }
1886
2117
  // at this point we don't have rawValue yet, so we bail and wait until the Renderer recalls attach
1887
2118
  if (child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */] === undefined)
1888
2119
  return;
2120
+ if (cIS.hierarchyStore.snapshot.parent !== parent) {
2121
+ cIS.setParent(parent);
2122
+ }
1889
2123
  // save prev value
1890
2124
  cIS.previousAttach = attachProp.reduce((value, key) => value[key], parent);
1891
2125
  attach(parent, child.__ngt_renderer__[2 /* NgtRendererClassId.rawValue */], attachProp, true);
@@ -1898,12 +2132,12 @@ function attachThreeNodes(parent, child) {
1898
2132
  }
1899
2133
  }
1900
2134
  else if (is.three(parent, 'isObject3D') && is.three(child, 'isObject3D')) {
1901
- parent.add(child);
2135
+ placeObject3DChild(parent, child, before);
1902
2136
  added = true;
1903
2137
  cIS.addInteraction?.(cIS.store || pIS.store);
1904
2138
  }
1905
2139
  if (pIS.add) {
1906
- pIS.add(child, added ? 'objects' : 'nonObjects');
2140
+ pIS.add(child, added ? 'objects' : 'nonObjects', before);
1907
2141
  }
1908
2142
  if (cIS.parent && untracked(cIS.parent) !== parent) {
1909
2143
  cIS.setParent(parent);
@@ -1915,7 +2149,7 @@ function attachThreeNodes(parent, child) {
1915
2149
  invalidateInstance(child);
1916
2150
  invalidateInstance(parent);
1917
2151
  }
1918
- function removeThreeChild(child, parent, dispose) {
2152
+ function removeThreeChild(child, parent, dispose = false) {
1919
2153
  const pIS = getInstanceState(parent);
1920
2154
  const cIS = getInstanceState(child);
1921
2155
  // clear parent ref
@@ -1935,8 +2169,9 @@ function removeThreeChild(child, parent, dispose) {
1935
2169
  }
1936
2170
  // dispose
1937
2171
  const isPrimitive = cIS?.type && cIS.type === 'ngt-primitive';
1938
- if (!isPrimitive && child['dispose'] && !is.three(child, 'isScene')) {
1939
- queueMicrotask(() => child['dispose']());
2172
+ if (dispose && !isPrimitive && child['dispose'] && !is.three(child, 'isScene')) {
2173
+ const disposeInstance = child['dispose'].bind(child);
2174
+ queueMicrotask(disposeInstance);
1940
2175
  }
1941
2176
  invalidateInstance(parent);
1942
2177
  }
@@ -1944,18 +2179,31 @@ function internalDestroyNode(node, removeChild) {
1944
2179
  const rS = node.__ngt_renderer__;
1945
2180
  if (!rS || rS[1 /* NgtRendererClassId.destroyed */])
1946
2181
  return;
1947
- 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 */];
1948
2190
  removeChild?.(node, child);
2191
+ if (destructionOwner && childOwner && childOwner !== destructionOwner)
2192
+ continue;
1949
2193
  internalDestroyNode(child, removeChild);
1950
2194
  }
1951
2195
  // clear out parent if haven't
1952
- rS[5 /* NgtRendererClassId.parent */] = undefined;
2196
+ rS[4 /* NgtRendererClassId.parent */] = undefined;
1953
2197
  // clear out children
1954
- rS[6 /* NgtRendererClassId.children */].length = 0;
2198
+ rS[5 /* NgtRendererClassId.children */].length = 0;
1955
2199
  // clear out NgtInstanceState
1956
- const iS = getInstanceState(node);
1957
2200
  if (iS) {
1958
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
+ }
1959
2207
  iS.removeInteraction?.(iS.store);
1960
2208
  delete temp['onAttach'];
1961
2209
  delete temp['onUpdate'];
@@ -1978,13 +2226,12 @@ function internalDestroyNode(node, removeChild) {
1978
2226
  delete node['__ngt__'];
1979
2227
  }
1980
2228
  }
1981
- // clear our debugNode
1982
- 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();
1983
2234
  if (rS[0 /* NgtRendererClassId.type */] === 'comment') {
1984
- delete node[NGT_INTERNAL_ADD_COMMENT_FLAG];
1985
- delete node[NGT_INTERNAL_SET_PARENT_COMMENT_FLAG];
1986
- delete node[NGT_CANVAS_CONTENT_FLAG];
1987
- delete node[NGT_PORTAL_CONTENT_FLAG];
1988
2235
  delete node[NGT_DOM_PARENT_FLAG];
1989
2236
  }
1990
2237
  // clear getAttribute if exist
@@ -1997,10 +2244,19 @@ function internalDestroyNode(node, removeChild) {
1997
2244
  rS[1 /* NgtRendererClassId.destroyed */] = true;
1998
2245
  }
1999
2246
 
2247
+ var _a$1;
2000
2248
  /**
2001
2249
  * Injection token for renderer factory options.
2002
2250
  */
2003
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
+ }
2004
2260
  /**
2005
2261
  * Angular renderer factory for Three.js elements.
2006
2262
  *
@@ -2029,46 +2285,45 @@ class NgtRendererFactory2 {
2029
2285
  this.catalogue = injectCatalogue();
2030
2286
  this.document = inject(DOCUMENT);
2031
2287
  this.options = inject(NGT_RENDERER_OPTIONS, { optional: true }) || {};
2032
- this.rendererMap = new Map();
2288
+ this.rendererByDelegate = new WeakMap();
2033
2289
  }
2034
2290
  createRenderer(hostElement, type) {
2035
2291
  const delegateRenderer = this.delegateRendererFactory.createRenderer(hostElement, type);
2036
2292
  if (!type)
2037
2293
  return delegateRenderer;
2038
- let renderer = this.rendererMap.get(type.id);
2039
- if (renderer) {
2040
- if (renderer instanceof NgtRenderer2) {
2041
- renderer.count += 1;
2042
- if (renderer.delegateRenderer !== delegateRenderer) {
2043
- 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);
2044
2306
  }
2045
2307
  }
2046
- return renderer;
2308
+ markRendererViewHost(hostRendererNode);
2047
2309
  }
2048
- if (hostElement && !isRendererNode(hostElement)) {
2049
- createRendererNode('platform', hostElement, this.document);
2050
- }
2051
- if (Reflect.get(type, 'type')?.[NGT_HTML_FLAG]) {
2052
- this.rendererMap.set(type.id, delegateRenderer);
2053
- // patch delegate destroyNode so we can destroy this HTML node
2054
- // TODO: make sure we really need to do this
2055
- const originalDestroyNode = delegateRenderer.destroyNode?.bind(delegateRenderer);
2056
- if (!originalDestroyNode || !(NGT_DELEGATE_RENDERER_DESTROY_NODE_PATCHED_FLAG in originalDestroyNode)) {
2057
- delegateRenderer.destroyNode = (node) => {
2058
- originalDestroyNode?.(node);
2059
- if (node !== hostElement)
2060
- return;
2061
- internalDestroyNode(node, null);
2062
- };
2063
- Object.assign(delegateRenderer.destroyNode, {
2064
- [NGT_DELEGATE_RENDERER_DESTROY_NODE_PATCHED_FLAG]: true,
2065
- });
2066
- }
2067
- return delegateRenderer;
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);
2068
2314
  }
2069
- this.rendererMap.set(type.id, (renderer = new NgtRenderer2(delegateRenderer, this.catalogue, this.document, this.options)));
2070
2315
  return renderer;
2071
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
+ }
2072
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 }); }
2073
2328
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtRendererFactory2 }); }
2074
2329
  }
@@ -2088,40 +2343,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
2088
2343
  * @internal
2089
2344
  */
2090
2345
  class NgtRenderer2 {
2091
- constructor(delegateRenderer, catalogue, document, options, count = 1) {
2346
+ static { _a$1 = NGT_RENDERER_CONTEXT_FLAG; }
2347
+ constructor(delegateRenderer, catalogue, document, options) {
2092
2348
  this.delegateRenderer = delegateRenderer;
2093
2349
  this.catalogue = catalogue;
2094
2350
  this.document = document;
2095
2351
  this.options = options;
2096
- this.count = count;
2097
- this.argsInjectors = [];
2098
- this.parentInjectors = [];
2099
- this.destroyNode = (node) => {
2100
- 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
+ }
2101
2364
  };
2102
- this.addClass = this.delegateRenderer.addClass.bind(this.delegateRenderer);
2103
- this.removeClass = this.delegateRenderer.removeClass.bind(this.delegateRenderer);
2104
- this.setStyle = this.delegateRenderer.setStyle.bind(this.delegateRenderer);
2105
- this.removeStyle = this.delegateRenderer.removeStyle.bind(this.delegateRenderer);
2106
- this.selectRootElement = this.delegateRenderer.selectRootElement.bind(this.delegateRenderer);
2107
- this.nextSibling = this.delegateRenderer.nextSibling.bind(this.delegateRenderer);
2108
- this.setValue = this.delegateRenderer.setValue.bind(this.delegateRenderer);
2365
+ this.data = { ...this.delegateRenderer.data, __ngt_renderer__: true };
2109
2366
  if (!this.options.verbose) {
2110
2367
  this.options.verbose = false;
2111
2368
  }
2112
2369
  }
2113
- get data() {
2114
- return { ...this.delegateRenderer.data, __ngt_renderer__: true };
2115
- }
2116
2370
  destroy() {
2117
- if (this.count > 1) {
2118
- this.count -= 1;
2119
- return;
2120
- }
2121
- // this is the last instance of the same NgtRenderer2
2122
- this.count = 0;
2123
- this.argsInjectors = [];
2124
- this.parentInjectors = [];
2371
+ this.delegateRenderer.destroy();
2125
2372
  }
2126
2373
  createElement(name, namespace) {
2127
2374
  const platformElement = this.delegateRenderer.createElement(name, namespace);
@@ -2132,8 +2379,8 @@ class NgtRenderer2 {
2132
2379
  return createRendererNode('three', prepare(platformElement, 'ngt-value'), this.document);
2133
2380
  }
2134
2381
  const [injectedArgs, injectedParent] = [
2135
- this.getNgtDirective(NgtArgs, this.argsInjectors)?.value || [],
2136
- this.getNgtDirective(NgtParent, this.parentInjectors)?.value,
2382
+ this.getNgtDirective(NgtArgs)?.value || [],
2383
+ this.getNgtDirective(NgtParent)?.value,
2137
2384
  ];
2138
2385
  if (name === 'ngt-primitive') {
2139
2386
  if (!injectedArgs[0])
@@ -2145,7 +2392,7 @@ class NgtRenderer2 {
2145
2392
  prepare(object, 'ngt-primitive');
2146
2393
  const primitiveRendererNode = createRendererNode('three', object, this.document);
2147
2394
  if (injectedParent) {
2148
- primitiveRendererNode.__ngt_renderer__[5 /* NgtRendererClassId.parent */] =
2395
+ primitiveRendererNode.__ngt_renderer__[6 /* NgtRendererClassId.parentOverride */] =
2149
2396
  injectedParent;
2150
2397
  }
2151
2398
  return primitiveRendererNode;
@@ -2175,7 +2422,7 @@ class NgtRenderer2 {
2175
2422
  instanceState.attach = ['material'];
2176
2423
  }
2177
2424
  if (injectedParent) {
2178
- rendererNode.__ngt_renderer__[5 /* NgtRendererClassId.parent */] =
2425
+ rendererNode.__ngt_renderer__[6 /* NgtRendererClassId.parentOverride */] =
2179
2426
  injectedParent;
2180
2427
  }
2181
2428
  return rendererNode;
@@ -2185,31 +2432,24 @@ class NgtRenderer2 {
2185
2432
  createComment(value) {
2186
2433
  const commentNode = this.delegateRenderer.createComment(value);
2187
2434
  const commentRendererNode = createRendererNode('comment', commentNode, this.document);
2188
- // NOTE: we attach an arrow function to the Comment node
2189
- // In our directives, we can call this function to then start tracking the RendererNode
2190
- // this is done to limit the amount of Nodes we need to process for getCreationState
2191
- Object.assign(commentRendererNode, {
2192
- [NGT_INTERNAL_ADD_COMMENT_FLAG]: (type, injector) => {
2193
- if (type === 'args') {
2194
- this.argsInjectors.push(injector);
2195
- }
2196
- else if (type === 'parent') {
2197
- Object.assign(commentRendererNode, {
2198
- [NGT_INTERNAL_SET_PARENT_COMMENT_FLAG]: (ngtParent) => {
2199
- commentRendererNode.__ngt_renderer__[5 /* NgtRendererClassId.parent */] = ngtParent;
2200
- },
2201
- });
2202
- this.parentInjectors.push(injector);
2203
- }
2204
- commentRendererNode.__ngt_renderer__[4 /* NgtRendererClassId.injector */] = injector;
2205
- },
2206
- });
2207
2435
  return commentRendererNode;
2208
2436
  }
2209
2437
  createText(value) {
2210
2438
  const textNode = this.delegateRenderer.createText(value);
2211
2439
  return createRendererNode('text', textNode, this.document);
2212
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
+ }
2213
2453
  appendChild(parent, newChild, refChild, isMove) {
2214
2454
  const delegatedFn = refChild
2215
2455
  ? this.delegateRenderer.insertBefore.bind(this.delegateRenderer, parent, newChild, refChild, isMove)
@@ -2221,10 +2461,13 @@ class NgtRenderer2 {
2221
2461
  console.warn('[NGT dev mode] One of parent or child is not a renderer node.', { parent, newChild });
2222
2462
  return delegatedFn();
2223
2463
  }
2464
+ if (parent === newChild || refChild === newChild)
2465
+ return;
2466
+ this.setNodeRelationship(parent, newChild, refChild);
2224
2467
  if (cRS[0 /* NgtRendererClassId.type */] === 'comment') {
2225
- // 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.
2226
2470
  // comment usually means it's part of a templateRef ViewContainerRef or structural directive
2227
- setRendererParentNode(newChild, parent);
2228
2471
  // if parent is not three, we'll delegate to the renderer
2229
2472
  if (pRS[0 /* NgtRendererClassId.type */] !== 'three') {
2230
2473
  delegatedFn();
@@ -2232,52 +2475,43 @@ class NgtRenderer2 {
2232
2475
  return;
2233
2476
  }
2234
2477
  if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2235
- if (newChild[NGT_DOM_PARENT_FLAG] && newChild[NGT_DOM_PARENT_FLAG] instanceof HTMLElement) {
2236
- 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);
2237
2485
  }
2238
- if (pRS[5 /* NgtRendererClassId.parent */] && !cRS[5 /* NgtRendererClassId.parent */]) {
2239
- return this.appendChild(pRS[5 /* NgtRendererClassId.parent */], newChild);
2486
+ else {
2487
+ delegatedFn();
2240
2488
  }
2241
- return delegatedFn();
2489
+ if (threeParent)
2490
+ this.attachThreeDescendants(threeParent, newChild, insertionReference, portalStore);
2491
+ return;
2242
2492
  }
2243
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2244
- return this.appendThreeRendererNodes(parent, newChild);
2493
+ if (isRendererNodeType(parent, 'three') && isRendererNodeType(newChild, 'three')) {
2494
+ return this.appendThreeRendererNodes(parent, newChild, refChild);
2245
2495
  }
2246
- if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2247
- // if platform has parent, delegate to that parent
2248
- if (pRS[5 /* NgtRendererClassId.parent */]) {
2249
- // but track the child for this parent as well
2250
- addRendererChildNode(parent, newChild);
2251
- 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));
2252
2500
  }
2253
- // platform can also have normal parentNode
2254
- const platformParentNode = this.delegateRenderer.parentNode(parent);
2255
- if (platformParentNode) {
2256
- return this.appendChild(platformParentNode, newChild);
2257
- }
2258
- // if not, set up parent and child relationship for this pair then bail
2259
- this.setNodeRelationship(parent, newChild);
2260
2501
  return;
2261
2502
  }
2262
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2263
- if (!cRS[5 /* NgtRendererClassId.parent */]) {
2264
- setRendererParentNode(newChild, parent);
2265
- }
2266
- for (const child of cRS[6 /* NgtRendererClassId.children */]) {
2267
- this.appendChild(parent, child);
2268
- }
2269
- for (const platformChildNode of newChild['childNodes'] || []) {
2270
- if (!isRendererNode(platformChildNode) ||
2271
- platformChildNode.__ngt_renderer__[0 /* NgtRendererClassId.type */] !== 'platform')
2272
- continue;
2273
- this.appendChild(parent, platformChildNode);
2274
- }
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);
2275
2509
  return;
2276
2510
  }
2277
- if (pRS[0 /* NgtRendererClassId.type */] === 'portal' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2278
- if (!cRS[5 /* NgtRendererClassId.parent */] && pRS[3 /* NgtRendererClassId.portalContainer */]) {
2279
- return this.appendChild(pRS[3 /* NgtRendererClassId.portalContainer */], newChild);
2280
- }
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));
2281
2515
  return;
2282
2516
  }
2283
2517
  if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'portal') {
@@ -2286,14 +2520,12 @@ class NgtRenderer2 {
2286
2520
  return delegatedFn();
2287
2521
  }
2288
2522
  insertBefore(parent, newChild, refChild, isMove) {
2289
- // if both are comments and the reference child is NgtCanvasContent, we'll assign the same flag to the newChild
2290
- // this means that the NgtCanvas component is embedding. This flag allows the Renderer to get the root scene
2291
- // when it tries to attach the template under `ng-template[canvasContent]`
2292
- if (refChild &&
2293
- refChild[NGT_CANVAS_CONTENT_FLAG] &&
2294
- refChild instanceof Comment &&
2295
- newChild instanceof Comment) {
2296
- 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);
2297
2529
  }
2298
2530
  // if there is no parent, we delegate
2299
2531
  if (!parent) {
@@ -2301,21 +2533,19 @@ class NgtRenderer2 {
2301
2533
  }
2302
2534
  return this.appendChild(parent, newChild, refChild, isMove);
2303
2535
  }
2304
- removeChild(parent, oldChild, isHostElement) {
2536
+ removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval) {
2305
2537
  if (parent === null) {
2306
2538
  parent = this.parentNode(oldChild);
2307
2539
  }
2308
2540
  const cRS = oldChild.__ngt_renderer__;
2309
2541
  if (!cRS) {
2310
2542
  try {
2311
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2543
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2312
2544
  }
2313
2545
  catch {
2314
2546
  return;
2315
2547
  }
2316
2548
  }
2317
- // disassociate things from oldChild
2318
- cRS[5 /* NgtRendererClassId.parent */] = null;
2319
2549
  // if parent is still undefined
2320
2550
  if (parent == null) {
2321
2551
  if (cRS[1 /* NgtRendererClassId.destroyed */]) {
@@ -2328,39 +2558,38 @@ class NgtRenderer2 {
2328
2558
  }
2329
2559
  const pRS = parent.__ngt_renderer__;
2330
2560
  if (!pRS) {
2331
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2561
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2332
2562
  }
2333
- const childIndex = pRS[6 /* NgtRendererClassId.children */].indexOf(oldChild);
2334
- if (childIndex >= 0) {
2335
- // disassociate oldChild from parent children
2336
- pRS[6 /* NgtRendererClassId.children */].splice(childIndex, 1);
2563
+ if (cRS[0 /* NgtRendererClassId.type */] !== 'three') {
2564
+ this.detachThreeDescendants(oldChild);
2337
2565
  }
2338
- if (pRS[0 /* NgtRendererClassId.type */] === 'three' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2339
- 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;
2340
2574
  }
2341
2575
  if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'platform') {
2342
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2576
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2343
2577
  }
2344
- 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')) {
2345
2582
  return;
2346
2583
  }
2347
- if (pRS[0 /* NgtRendererClassId.type */] === 'platform' && cRS[0 /* NgtRendererClassId.type */] === 'three') {
2348
- const childLS = getInstanceState(oldChild);
2349
- if (!childLS)
2350
- return;
2351
- const threeParent = childLS.parent ? untracked(childLS.parent) : null;
2352
- if (!threeParent)
2353
- return;
2354
- return this.removeChild(threeParent, oldChild);
2355
- }
2356
- return this.delegateRenderer.removeChild(parent, oldChild, isHostElement);
2584
+ return this.delegateRenderer.removeChild(parent, oldChild, isHostElement, requireSynchronousElementRemoval);
2357
2585
  }
2358
2586
  parentNode(node) {
2359
- if (node &&
2360
- (node[NGT_CANVAS_CONTENT_FLAG] || node[NGT_PORTAL_CONTENT_FLAG]) &&
2361
- node instanceof Comment &&
2362
- isRendererNode(node)) {
2363
- 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;
2364
2593
  // this should not happen but if it does, we'll delegate to the renderer
2365
2594
  if (!store) {
2366
2595
  return this.delegateRenderer.parentNode(node);
@@ -2370,24 +2599,18 @@ class NgtRenderer2 {
2370
2599
  if (!rootScene) {
2371
2600
  return this.delegateRenderer.parentNode(node);
2372
2601
  }
2373
- // if root scene is not a renderer node, we'll make it a renderer node here
2374
- if (!(NGT_RENDERER_NODE_FLAG in rootScene)) {
2375
- const sceneRendererNode = createRendererNode('three', rootScene, this.document);
2376
- // set parent to the comment too
2377
- setRendererParentNode(node, sceneRendererNode);
2378
- }
2379
- if (node[NGT_PORTAL_CONTENT_FLAG] &&
2380
- node[NGT_DOM_PARENT_FLAG] &&
2381
- isRendererNode(node[NGT_DOM_PARENT_FLAG])) {
2382
- 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;
2383
2608
  const portalContentParentRS = portalContentParent.__ngt_renderer__;
2384
- if (!portalContentParentRS[3 /* NgtRendererClassId.portalContainer */]) {
2385
- portalContentParentRS[3 /* NgtRendererClassId.portalContainer */] = rootScene;
2386
- }
2609
+ portalContentParentRS[3 /* NgtRendererClassId.portalContainer */] = rendererRootScene;
2387
2610
  }
2388
- return rootScene;
2611
+ return rendererRootScene;
2389
2612
  }
2390
- const rendererParentNode = node.__ngt_renderer__?.[5 /* NgtRendererClassId.parent */];
2613
+ const rendererParentNode = node.__ngt_renderer__?.[4 /* NgtRendererClassId.parent */];
2391
2614
  // returns the renderer parent node if it exists, otherwise returns the delegateRenderer parentNode
2392
2615
  return rendererParentNode ?? this.delegateRenderer.parentNode(node);
2393
2616
  }
@@ -2396,6 +2619,15 @@ class NgtRenderer2 {
2396
2619
  if (!rS || rS[1 /* NgtRendererClassId.destroyed */])
2397
2620
  return this.delegateRenderer.removeAttribute(el, name, namespace);
2398
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());
2399
2631
  return;
2400
2632
  }
2401
2633
  return this.delegateRenderer.removeAttribute(el, name, namespace);
@@ -2411,12 +2643,9 @@ class NgtRenderer2 {
2411
2643
  }
2412
2644
  if (rS[0 /* NgtRendererClassId.type */] === 'three') {
2413
2645
  if (name === 'attach') {
2414
- const paths = value.split('.');
2415
- if (paths.length) {
2416
- const instanceState = getInstanceState(el);
2417
- if (instanceState)
2418
- instanceState.attach = paths;
2419
- }
2646
+ const instanceState = getInstanceState(el);
2647
+ const parent = (instanceState?.parent ? untracked(instanceState.parent) : null) ?? this.findTargetThreeParent(el);
2648
+ this.updateAttachment(el, value, parent);
2420
2649
  return;
2421
2650
  }
2422
2651
  // coercion for primitive values
@@ -2443,44 +2672,55 @@ class NgtRenderer2 {
2443
2672
  // NOTE: untrack all signal updates because this is during setProperty which is a reactive context
2444
2673
  // attaching potentially updates signals which is not allowed
2445
2674
  const rS = el.__ngt_renderer__;
2446
- if (!rS || rS[1 /* NgtRendererClassId.destroyed */]) {
2675
+ if (!rS)
2676
+ return this.delegateRenderer.setProperty(el, name, value);
2677
+ if (rS[1 /* NgtRendererClassId.destroyed */]) {
2447
2678
  this.options.verbose &&
2448
2679
  console.warn('[NGT dev mode] setProperty is invoked on destroyed renderer node.', { el, name, value });
2449
2680
  return;
2450
2681
  }
2451
- if (rS[0 /* NgtRendererClassId.type */] === 'three') {
2682
+ if (isRendererNodeType(el, 'three')) {
2683
+ const threeState = el.__ngt_renderer__;
2452
2684
  const instanceState = getInstanceState(el);
2453
- 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);
2454
2687
  if (name === 'parameters') {
2688
+ const parameters = value && typeof value === 'object' ? { ...value } : {};
2455
2689
  // NOTE: short-cut for null raycast to prevent upstream from creating a nullRaycast property
2456
- if ('raycast' in value && value['raycast'] === null) {
2457
- value['raycast'] = () => null;
2690
+ if ('raycast' in parameters && parameters['raycast'] === null) {
2691
+ parameters['raycast'] = () => null;
2458
2692
  }
2459
- applyProps(el, value);
2460
- 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'))) {
2461
2702
  untracked(() => instanceState?.updateGeometryStamp());
2462
2703
  }
2463
- if ('attach' in value && value['attach'] !== undefined) {
2464
- if (instanceState)
2465
- instanceState.attach = this.normalizeAttach(value['attach']);
2466
- if (parent)
2467
- untracked(() => attachThreeNodes(parent, el));
2704
+ if ('attach' in parameters || previousKeys.has('attach')) {
2705
+ this.updateAttachment(el, nextAttach, parent);
2468
2706
  }
2469
2707
  return;
2470
2708
  }
2471
2709
  // [rawValue]
2472
2710
  if (instanceState?.type === 'ngt-value' && name === 'rawValue') {
2473
- rS[2 /* NgtRendererClassId.rawValue */] = value;
2474
- if (parent)
2475
- 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
+ });
2476
2719
  return;
2477
2720
  }
2478
2721
  // [attach]
2479
2722
  if (name === 'attach') {
2480
- if (instanceState)
2481
- instanceState.attach = this.normalizeAttach(value);
2482
- if (parent)
2483
- untracked(() => attachThreeNodes(parent, el));
2723
+ this.updateAttachment(el, value, parent);
2484
2724
  return;
2485
2725
  }
2486
2726
  // NOTE: short-cut for null raycast to prevent upstream from creating a nullRaycast property
@@ -2488,7 +2728,7 @@ class NgtRenderer2 {
2488
2728
  value = () => null;
2489
2729
  }
2490
2730
  applyProps(el, { [name]: value });
2491
- if (instanceState && name === 'geometry' && is.three(value, 'isBufferGeometry')) {
2731
+ if (instanceState && name === 'geometry') {
2492
2732
  untracked(() => {
2493
2733
  instanceState.updateGeometryStamp();
2494
2734
  });
@@ -2497,13 +2737,13 @@ class NgtRenderer2 {
2497
2737
  }
2498
2738
  return this.delegateRenderer.setProperty(el, name, value);
2499
2739
  }
2500
- listen(target, eventName, callback) {
2740
+ listen(target, eventName, callback, options) {
2501
2741
  if (typeof target === 'string') {
2502
- return this.delegateRenderer.listen(target, eventName, callback);
2742
+ return this.delegateRenderer.listen(target, eventName, callback, options);
2503
2743
  }
2504
2744
  const rS = target.__ngt_renderer__;
2505
2745
  if (!rS) {
2506
- return this.delegateRenderer.listen(target, eventName, callback);
2746
+ return this.delegateRenderer.listen(target, eventName, callback, options);
2507
2747
  }
2508
2748
  if (rS[1 /* NgtRendererClassId.destroyed */])
2509
2749
  return () => { };
@@ -2518,43 +2758,34 @@ class NgtRenderer2 {
2518
2758
  return () => { };
2519
2759
  }
2520
2760
  if (eventName === 'attached') {
2521
- iS.onAttach = callback;
2522
- const parent = iS.parent && untracked(iS.parent);
2523
- if (parent)
2524
- iS.onAttach({ parent, node: target });
2525
- return () => {
2526
- iS.onAttach = undefined;
2527
- };
2761
+ return this.registerWithOptions(callback, options, (listener) => this.listenToInstanceEvent(target, 'attached', listener));
2528
2762
  }
2529
2763
  if (eventName === 'updated') {
2530
- iS.onUpdate = callback;
2531
- return () => {
2532
- iS.onUpdate = undefined;
2533
- };
2764
+ return this.registerWithOptions(callback, options, (listener) => this.listenToInstanceEvent(target, 'updated', listener));
2534
2765
  }
2535
2766
  if (THREE_NATIVE_EVENTS.includes(eventName) && target instanceof THREE.EventDispatcher) {
2536
2767
  // NOTE: rename to dispose because that's the event type, not disposed.
2537
2768
  if (eventName === 'disposed') {
2538
2769
  eventName = 'dispose';
2539
2770
  }
2540
- if (target.parent &&
2541
- (eventName === 'added' || eventName === 'removed')) {
2542
- callback({ type: eventName, target });
2543
- }
2544
- target.addEventListener(eventName, callback);
2545
- return () => {
2546
- target.removeEventListener(eventName, callback);
2547
- };
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
+ });
2548
2778
  }
2549
- const cleanup = iS.setPointerEvent?.(eventName, callback) || (() => { });
2779
+ const cleanup = this.registerWithOptions(callback, options, (listener) => iS.setPointerEvent?.(eventName, listener) || (() => { }));
2550
2780
  // this means the object has already been attached to the parent and has its store propagated
2551
2781
  if (iS.store)
2552
2782
  iS.addInteraction?.(iS.store);
2553
2783
  return cleanup;
2554
2784
  }
2555
- return this.delegateRenderer.listen(target, eventName, callback);
2785
+ return this.delegateRenderer.listen(target, eventName, callback, options);
2556
2786
  }
2557
- appendThreeRendererNodes(parent, child) {
2787
+ appendThreeRendererNodes(parent, child, refChild, storeOverride) {
2788
+ parent = child.__ngt_renderer__[6 /* NgtRendererClassId.parentOverride */] ?? parent;
2558
2789
  // if parent and child are the same, skip
2559
2790
  if (parent === child) {
2560
2791
  this.options.verbose &&
@@ -2564,31 +2795,226 @@ class NgtRenderer2 {
2564
2795
  });
2565
2796
  return;
2566
2797
  }
2567
- const cIS = getInstanceState(child);
2568
- // if child is already attached to a parent, skip
2569
- if (cIS?.hierarchyStore.snapshot.parent) {
2570
- this.options.verbose &&
2571
- console.warn('[NGT dev mode] appending THREE.js parent and child but child is already attached', {
2572
- parent,
2573
- child,
2574
- });
2575
- 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);
2576
2805
  }
2577
- // set the relationship
2578
- this.setNodeRelationship(parent, child);
2579
- // attach THREE child
2580
- 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);
2581
2810
  return;
2582
2811
  }
2583
- setNodeRelationship(parent, child) {
2584
- setRendererParentNode(child, parent);
2585
- 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
+ };
2586
2984
  }
2587
- 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) {
2588
3014
  let directiveInstance;
2589
- let i = injectors.length - 1;
3015
+ let i = this.directiveInjectors.length - 1;
2590
3016
  while (i >= 0) {
2591
- const injector = injectors[i];
3017
+ const injector = this.directiveInjectors[i];
2592
3018
  const instance = injector.get(directive, null);
2593
3019
  if (instance && typeof instance === 'object' && instance.validate()) {
2594
3020
  directiveInstance = instance;
@@ -2599,12 +3025,75 @@ class NgtRenderer2 {
2599
3025
  return directiveInstance;
2600
3026
  }
2601
3027
  normalizeAttach(attach) {
3028
+ if (attach == null)
3029
+ return undefined;
2602
3030
  if (typeof attach === 'function')
2603
3031
  return attach;
2604
3032
  if (typeof attach === 'string')
2605
3033
  return attach.split('.');
2606
3034
  return attach.flatMap((item) => item.toString().split('.'));
2607
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
+ }
2608
3097
  }
2609
3098
 
2610
3099
  /**
@@ -2633,6 +3122,12 @@ function storeFactory() {
2633
3122
  const defaultTarget = new THREE.Vector3();
2634
3123
  const tempTarget = new THREE.Vector3();
2635
3124
  let performanceTimeout = undefined;
3125
+ const cancelPerformanceRegression = () => {
3126
+ if (performanceTimeout === undefined)
3127
+ return;
3128
+ clearTimeout(performanceTimeout);
3129
+ performanceTimeout = undefined;
3130
+ };
2636
3131
  const pointer = new THREE.Vector2();
2637
3132
  // getCurrentViewport will mutate this instead of creating a new object everytime
2638
3133
  const tempViewport = {
@@ -2672,17 +3167,19 @@ function storeFactory() {
2672
3167
  regress: () => {
2673
3168
  const state = store.snapshot;
2674
3169
  // Clear timeout
2675
- if (performanceTimeout)
2676
- clearTimeout(performanceTimeout);
3170
+ cancelPerformanceRegression();
2677
3171
  // Set lower bound performance
2678
3172
  if (state.performance.current !== state.performance.min)
2679
3173
  store.update((state) => ({
2680
3174
  performance: { ...state.performance, current: state.performance.min },
2681
3175
  }));
2682
3176
  // Go back to upper bound performance after a while unless something regresses meanwhile
2683
- performanceTimeout = setTimeout(() => store.update((state) => ({
2684
- performance: { ...state.performance, current: store.snapshot.performance.max },
2685
- })), 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);
2686
3183
  },
2687
3184
  },
2688
3185
  size: { width: 0, height: 0, top: 0, left: 0 },
@@ -2770,27 +3267,34 @@ function storeFactory() {
2770
3267
  subscribers: [],
2771
3268
  subscribe: (callback, priority = 0, _store = store) => {
2772
3269
  const internal = _store.snapshot.internal;
3270
+ const subscription = { callback, priority, store: _store };
2773
3271
  // If this subscription was given a priority, it takes rendering into its own hands
2774
3272
  // For that reason we switch off automatic rendering and increase the manual flag
2775
3273
  // As long as this flag is positive there can be no internal rendering at all
2776
3274
  // because there could be multiple render subscriptions
2777
- internal.priority = internal.priority + (priority > 0 ? 1 : 0);
2778
- internal.subscribers.push({ callback, priority, store: _store });
3275
+ internal.priority += priority > 0 ? 1 : 0;
2779
3276
  // Register subscriber and sort layers from lowest to highest, meaning,
2780
3277
  // highest priority renders last (on top of the other frames)
2781
- 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;
2782
3280
  return () => {
3281
+ if (!active)
3282
+ return;
3283
+ active = false;
2783
3284
  const internal = _store.snapshot.internal;
2784
- if (internal?.subscribers) {
2785
- // Decrease manual flag if this subscription had a priority
2786
- internal.priority = internal.priority - (priority > 0 ? 1 : 0);
2787
- // Remove subscriber from list
2788
- internal.subscribers = internal.subscribers.filter((s) => s.callback !== callback);
2789
- }
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);
2790
3291
  };
2791
3292
  },
2792
3293
  },
2793
3294
  });
3295
+ Object.defineProperty(store.snapshot.performance.regress, 'cancel', {
3296
+ value: cancelPerformanceRegression,
3297
+ });
2794
3298
  Object.defineProperty(store, '__pointerMissed$', { get: () => pointerMissed$ });
2795
3299
  let { size: oldSize, viewport: { dpr: oldDpr }, camera: oldCamera, } = store.snapshot;
2796
3300
  effect(() => {
@@ -2904,22 +3408,11 @@ class NgtParent extends NgtCommonDirective {
2904
3408
  this.linkedValue = linkedSignal(this._parent, ...(ngDevMode ? [{ debugName: "linkedValue" }] : /* istanbul ignore next */ []));
2905
3409
  this.shouldSkipRender = computed(() => !this._parent(), ...(ngDevMode ? [{ debugName: "shouldSkipRender" }] : /* istanbul ignore next */ []));
2906
3410
  const commentNode = this.commentNode;
2907
- commentNode.data = NGT_PARENT_FLAG;
2908
- commentNode[NGT_PARENT_FLAG] = true;
2909
- if (commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]) {
2910
- commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]('parent', this.injector);
2911
- delete commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG];
2912
- }
3411
+ setRendererAnchor(commentNode, { kind: 'parent', injector: this.injector });
2913
3412
  }
2914
3413
  validate() {
2915
3414
  return !this.injected && !!this.injectedValue;
2916
3415
  }
2917
- beforeCreateView() {
2918
- const commentNode = this.commentNode;
2919
- if (commentNode[NGT_INTERNAL_SET_PARENT_COMMENT_FLAG]) {
2920
- commentNode[NGT_INTERNAL_SET_PARENT_COMMENT_FLAG](this.injectedValue);
2921
- }
2922
- }
2923
3416
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtParent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2924
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 }); }
2925
3418
  }
@@ -3085,8 +3578,135 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3085
3578
  type: Directive
3086
3579
  }], ctorParameters: () => [] });
3087
3580
 
3088
- const cached$1 = new Map();
3089
- 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();
3090
3710
  function normalizeInputs$1(input) {
3091
3711
  let urls = [];
3092
3712
  if (Array.isArray(input)) {
@@ -3100,33 +3720,26 @@ function normalizeInputs$1(input) {
3100
3720
  }
3101
3721
  return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
3102
3722
  }
3103
- function load(loaderConstructorFactory, inputs, { extensions, onLoad, onProgress, } = {}) {
3104
- return () => {
3723
+ function load(loaderConstructorFactory, inputs, { extensions, cacheKey, onLoad, onProgress, } = {}) {
3724
+ return (consumerOnLoad = onLoad) => {
3105
3725
  const urls = normalizeInputs$1(inputs());
3106
- let loader = memoizedLoaders$1.get(loaderConstructorFactory(urls));
3107
- if (!loader) {
3108
- loader = new (loaderConstructorFactory(urls))();
3109
- memoizedLoaders$1.set(loaderConstructorFactory(urls), loader);
3110
- }
3111
- if (extensions)
3112
- extensions(loader);
3726
+ const LoaderConstructor = loaderConstructorFactory(urls);
3727
+ const configurationKey = loaderCache$1.configurationKey(extensions, cacheKey?.());
3113
3728
  return urls.map((url) => {
3114
3729
  if (url === '')
3115
3730
  return Promise.resolve(null);
3116
- if (!cached$1.has(url)) {
3117
- cached$1.set(url, new Promise((resolve, reject) => {
3118
- loader.load(url, (data) => {
3119
- if ('scene' in data) {
3120
- Object.assign(data, makeObjectGraph(data['scene']));
3121
- }
3122
- if (onLoad) {
3123
- onLoad(data);
3124
- }
3125
- resolve(data);
3126
- }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
3127
- }));
3128
- }
3129
- 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
+ });
3130
3743
  });
3131
3744
  };
3132
3745
  }
@@ -3134,18 +3747,30 @@ function load(loaderConstructorFactory, inputs, { extensions, onLoad, onProgress
3134
3747
  * @deprecated Use loaderResource instead. Will be removed in v5.0.0
3135
3748
  * @since v4.0.0~
3136
3749
  */
3137
- function _injectLoader(loaderConstructorFactory, inputs, { extensions, onProgress, onLoad, injector, } = {}) {
3750
+ function _injectLoader(loaderConstructorFactory, inputs, { extensions, cacheKey, onProgress, onLoad, injector, } = {}) {
3138
3751
  return assertInjector(_injectLoader, injector, () => {
3139
3752
  const response = signal(null, ...(ngDevMode ? [{ debugName: "response" }] : /* istanbul ignore next */ []));
3140
3753
  const cachedResultPromisesEffect = load(loaderConstructorFactory, inputs, {
3141
3754
  extensions,
3755
+ cacheKey,
3142
3756
  onProgress,
3143
- onLoad: onLoad,
3144
3757
  });
3145
- effect(() => {
3758
+ effect((onCleanup) => {
3759
+ let active = true;
3760
+ onCleanup(() => {
3761
+ active = false;
3762
+ });
3146
3763
  const originalUrls = inputs();
3147
- const cachedResultPromises = cachedResultPromisesEffect();
3148
- 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;
3149
3774
  response.update(() => {
3150
3775
  if (Array.isArray(originalUrls))
3151
3776
  return results;
@@ -3158,25 +3783,23 @@ function _injectLoader(loaderConstructorFactory, inputs, { extensions, onProgres
3158
3783
  return result;
3159
3784
  }, {});
3160
3785
  });
3161
- });
3786
+ })
3787
+ .catch(() => undefined);
3162
3788
  });
3163
3789
  return response.asReadonly();
3164
3790
  });
3165
3791
  }
3166
- _injectLoader.preload = (loaderConstructorFactory, inputs, extensions, onLoad) => {
3167
- const effects = load(loaderConstructorFactory, inputs, { extensions, onLoad })();
3792
+ _injectLoader.preload = (loaderConstructorFactory, inputs, extensions, onLoad, cacheKey) => {
3793
+ const effects = load(loaderConstructorFactory, inputs, { extensions, cacheKey, onLoad })();
3168
3794
  if (effects) {
3169
- void Promise.all(effects);
3795
+ void Promise.all(effects).catch(() => undefined);
3170
3796
  }
3171
3797
  };
3172
3798
  _injectLoader.destroy = () => {
3173
- cached$1.clear();
3799
+ loaderCache$1.destroy();
3174
3800
  };
3175
3801
  _injectLoader.clear = (urls) => {
3176
- const urlToClear = Array.isArray(urls) ? urls : [urls];
3177
- urlToClear.forEach((url) => {
3178
- cached$1.delete(url);
3179
- });
3802
+ loaderCache$1.clear(urls);
3180
3803
  };
3181
3804
  /**
3182
3805
  * @deprecated Use loaderResource instead. Will be removed in v5.0.0
@@ -3204,38 +3827,26 @@ function normalizeInputs(input) {
3204
3827
  }
3205
3828
  return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
3206
3829
  }
3207
- const cached = new Map();
3208
- const memoizedLoaders = new WeakMap();
3209
- function getLoaderResourceParams(input, loaderConstructorFactory, extensions) {
3830
+ const loaderCache = new NgtLoaderCache();
3831
+ function getLoaderResourceParams(input, loaderConstructorFactory, extensions, cacheKey) {
3210
3832
  const urls = input();
3211
3833
  const LoaderConstructor = loaderConstructorFactory(urls);
3212
3834
  const normalizedUrls = normalizeInputs(urls);
3213
- let loader = memoizedLoaders.get(LoaderConstructor);
3214
- if (!loader) {
3215
- loader = new LoaderConstructor();
3216
- memoizedLoaders.set(LoaderConstructor, loader);
3217
- }
3218
- if (extensions)
3219
- extensions(loader);
3220
- return { urls, normalizedUrls, loader };
3835
+ const configurationKey = loaderCache.configurationKey(extensions, cacheKey?.());
3836
+ return { urls, normalizedUrls, LoaderConstructor, configurationKey, extensions };
3221
3837
  }
3222
3838
  function getLoaderPromises(params, onProgress) {
3223
3839
  return params.normalizedUrls.map((url) => {
3224
3840
  if (url === '')
3225
3841
  return Promise.resolve(null);
3226
- const cachedPromise = cached.get(url);
3227
- if (cachedPromise)
3228
- return cachedPromise;
3229
- const promise = new Promise((res, rej) => {
3230
- 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) => {
3231
3844
  if ('scene' in data) {
3232
3845
  Object.assign(data, makeObjectGraph(data['scene']));
3233
3846
  }
3234
3847
  res(data);
3235
3848
  }, onProgress, (error) => rej(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
3236
- });
3237
- cached.set(url, promise);
3238
- return promise;
3849
+ }));
3239
3850
  });
3240
3851
  }
3241
3852
  /**
@@ -3275,11 +3886,11 @@ function getLoaderPromises(params, onProgress) {
3275
3886
  * }
3276
3887
  * ```
3277
3888
  */
3278
- function loaderResource(loaderConstructorFactory, input, { extensions, onLoad, onProgress, injector, } = {}) {
3889
+ function loaderResource(loaderConstructorFactory, input, { extensions, cacheKey, onLoad, onProgress, injector, } = {}) {
3279
3890
  return assertInjector(loaderResource, injector, () => {
3280
3891
  return resource({
3281
- params: () => getLoaderResourceParams(input, loaderConstructorFactory, extensions),
3282
- loader: async ({ params }) => {
3892
+ params: () => getLoaderResourceParams(input, loaderConstructorFactory, extensions, cacheKey),
3893
+ loader: async ({ params, abortSignal }) => {
3283
3894
  // TODO: use the abortSignal when THREE.Loader supports it
3284
3895
  const loadedResults = await Promise.all(getLoaderPromises(params, onProgress));
3285
3896
  let results;
@@ -3297,25 +3908,22 @@ function loaderResource(loaderConstructorFactory, input, { extensions, onLoad, o
3297
3908
  return result;
3298
3909
  }, {});
3299
3910
  }
3300
- if (onLoad)
3301
- onLoad(results);
3911
+ if (!abortSignal.aborted)
3912
+ onLoad?.(results);
3302
3913
  return results;
3303
3914
  },
3304
3915
  });
3305
3916
  });
3306
3917
  }
3307
- loaderResource.preload = (loaderConstructor, inputs, extensions) => {
3308
- const params = getLoaderResourceParams(() => inputs, () => loaderConstructor, extensions);
3309
- 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);
3310
3921
  };
3311
3922
  loaderResource.destroy = () => {
3312
- cached.clear();
3923
+ loaderCache.destroy();
3313
3924
  };
3314
3925
  loaderResource.clear = (urls) => {
3315
- const urlToClear = Array.isArray(urls) ? urls : [urls];
3316
- urlToClear.forEach((url) => {
3317
- cached.delete(url);
3318
- });
3926
+ loaderCache.clear(urls);
3319
3927
  };
3320
3928
 
3321
3929
  const RGBA_REGEX = /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*(\d*\.?\d+)?\)/;
@@ -3639,7 +4247,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3639
4247
  * ```
3640
4248
  */
3641
4249
  class NgtPortalContent {
3642
- static ngTemplateContextGuard(_, ctx) {
4250
+ static ngTemplateContextGuard(_, _ctx) {
3643
4251
  return true;
3644
4252
  }
3645
4253
  constructor() {
@@ -3647,9 +4255,10 @@ class NgtPortalContent {
3647
4255
  const { element } = inject(ViewContainerRef);
3648
4256
  const commentNode = element.nativeElement;
3649
4257
  const store = injectStore();
3650
- commentNode.data = NGT_PORTAL_CONTENT_FLAG;
3651
- commentNode[NGT_PORTAL_CONTENT_FLAG] = store;
3652
- 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);
3653
4262
  }
3654
4263
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtPortalContent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
3655
4264
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: NgtPortalContent, isStandalone: true, selector: "ng-template[portalContent]", ngImport: i0 }); }
@@ -3658,34 +4267,115 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3658
4267
  type: Directive,
3659
4268
  args: [{ selector: 'ng-template[portalContent]' }]
3660
4269
  }], ctorParameters: () => [] });
3661
- function mergeState(previousRoot, store, container, pointer, raycaster, events, size) {
3662
- // we never want to spread the id
3663
- 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;
3664
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 };
3665
4279
  let viewport = undefined;
3666
- if (state.camera && size) {
3667
- const camera = state.camera;
4280
+ if (camera && (camera !== previousState.camera || size)) {
3668
4281
  // calculate the override viewport, if present
3669
- viewport = previousState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
3670
- // update the portal camera, if it differs from the previous layer
3671
- if (camera !== previousState.camera)
3672
- updateCamera(camera, size);
3673
- }
3674
- return {
3675
- // the intersect consists of the previous root state
3676
- ...previousState,
3677
- ...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,
3678
4296
  // portals have their own scene, which forms the root, a raycaster and a pointer
3679
4297
  scene: container,
3680
- pointer,
3681
- raycaster,
4298
+ pointer: runtime.pointer,
4299
+ raycaster: runtime.raycaster,
3682
4300
  // their previous root is the layer before it
3683
4301
  previousRoot,
3684
- events: { ...previousState.events, ...state.events, ...events },
3685
- size: { ...previousState.size, ...size },
3686
- viewport: { ...previousState.viewport, ...viewport },
3687
- // layers are allowed to override events
3688
- 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);
3689
4379
  };
3690
4380
  }
3691
4381
  /**
@@ -3709,6 +4399,16 @@ function mergeState(previousRoot, store, container, pointer, raycaster, events,
3709
4399
  * ```
3710
4400
  */
3711
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
+ }
3712
4412
  constructor() {
3713
4413
  this.container = input.required(...(ngDevMode ? [{ debugName: "container" }] : /* istanbul ignore next */ []));
3714
4414
  this.state = input({}, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
@@ -3717,6 +4417,7 @@ class NgtPortalImpl {
3717
4417
  this.previousStore = injectStore({ skipSelf: true });
3718
4418
  this.portalStore = injectStore();
3719
4419
  this.injector = inject(Injector);
4420
+ this.document = inject(DOCUMENT);
3720
4421
  this.size = pick(this.state, 'size');
3721
4422
  this.events = pick(this.state, 'events');
3722
4423
  this.restState = omit(this.state, ['size', 'events']);
@@ -3724,21 +4425,42 @@ class NgtPortalImpl {
3724
4425
  this.portalRendered = this.portalContentRendered.asReadonly();
3725
4426
  extend({ Group });
3726
4427
  effect(() => {
3727
- 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] = [
3728
4434
  this.container(),
3729
4435
  this.anchorRef(),
3730
4436
  this.contentRef(),
3731
4437
  this.previousStore(),
4438
+ this.size(),
4439
+ this.events(),
4440
+ this.restState(),
3732
4441
  ];
3733
- const [size, events, restState] = [untracked(this.size), untracked(this.events), untracked(this.restState)];
3734
- if (!is.instance(container)) {
3735
- 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;
3736
4454
  }
3737
4455
  const instanceState = getInstanceState(container);
3738
- if (instanceState && instanceState.store !== this.portalStore) {
3739
- 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 };
3740
4462
  }
3741
- 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));
3742
4464
  if (this.portalViewRef) {
3743
4465
  this.portalViewRef.detectChanges();
3744
4466
  return;
@@ -3748,6 +4470,12 @@ class NgtPortalImpl {
3748
4470
  this.portalViewRef.detectChanges();
3749
4471
  this.portalContentRendered.set(true);
3750
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
+ });
3751
4479
  }
3752
4480
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgtPortalImpl, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3753
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: [
@@ -3765,7 +4493,32 @@ class NgtPortalImpl {
3765
4493
  pointer,
3766
4494
  raycaster,
3767
4495
  });
3768
- 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, {}));
3769
4522
  return store;
3770
4523
  },
3771
4524
  deps: [[new SkipSelf(), NGT_STORE]],
@@ -3804,7 +4557,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3804
4557
  pointer,
3805
4558
  raycaster,
3806
4559
  });
3807
- 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, {}));
3808
4586
  return store;
3809
4587
  },
3810
4588
  deps: [[new SkipSelf(), NGT_STORE]],
@@ -3826,6 +4604,95 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3826
4604
  const NgtPortal = [NgtPortalImpl, NgtPortalContent];
3827
4605
 
3828
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
+ }
3829
4696
  /**
3830
4697
  * Creates a canvas root initializer function.
3831
4698
  *
@@ -3847,10 +4714,12 @@ function canvasRootInitializer(injector) {
3847
4714
  return assertInjector(canvasRootInitializer, injector, () => {
3848
4715
  const injectedStore = injectStore();
3849
4716
  const loop = injectLoop();
4717
+ const rootInjector = inject(Injector);
3850
4718
  return (canvas) => {
3851
4719
  const exist = roots.has(canvas);
3852
4720
  let store = roots.get(canvas);
3853
- if (store) {
4721
+ const previousLifecycle = rootLifecycles.get(canvas);
4722
+ if (store && !previousLifecycle?.teardown) {
3854
4723
  console.warn('[NGT] Same canvas root is being created twice');
3855
4724
  }
3856
4725
  store ||= injectedStore;
@@ -3860,33 +4729,95 @@ function canvasRootInitializer(injector) {
3860
4729
  if (!exist) {
3861
4730
  roots.set(canvas, store);
3862
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 });
3863
4762
  let isConfigured = false;
3864
- let lastCamera;
4763
+ let destroyRequested = false;
3865
4764
  return {
3866
- isConfigured,
4765
+ get isConfigured() {
4766
+ return isConfigured;
4767
+ },
3867
4768
  destroy: (timeout = 500) => {
4769
+ if (destroyRequested)
4770
+ return;
4771
+ destroyRequested = true;
3868
4772
  const root = roots.get(canvas);
3869
- if (root) {
4773
+ if (root &&
4774
+ root === store &&
4775
+ rootLifecycles.get(canvas) === lifecycle &&
4776
+ lifecycle.owner === owner) {
3870
4777
  root.update((state) => ({ internal: { ...state.internal, active: false } }));
3871
- 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;
3872
4786
  try {
3873
4787
  const state = root.snapshot;
3874
- state.events.disconnect?.();
3875
- state.gl?.renderLists?.dispose?.();
3876
- state.gl?.dispose?.();
3877
- state.gl?.forceContextLoss?.();
3878
- if (state.gl?.xr)
3879
- state.xr.disconnect();
3880
- dispose(state.scene);
3881
- 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
+ }
3882
4807
  }
3883
- catch (e) {
3884
- 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);
3885
4813
  }
3886
4814
  }, timeout);
4815
+ lifecycle.teardown = teardown;
3887
4816
  }
3888
4817
  },
3889
4818
  configure: (inputs) => {
4819
+ if (destroyRequested || rootLifecycles.get(canvas) !== lifecycle || lifecycle.owner !== owner)
4820
+ return;
3890
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;
3891
4822
  const state = store.snapshot;
3892
4823
  const stateToUpdate = {};
@@ -3905,44 +4836,20 @@ function canvasRootInitializer(injector) {
3905
4836
  if (!is.equ(params, raycaster.params, shallowLoose)) {
3906
4837
  applyProps(raycaster, { params: { ...raycaster.params, ...(params || {}) } });
3907
4838
  }
3908
- // 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.
3909
4846
  if (!state.camera ||
3910
- (state.camera === lastCamera && !is.equ(lastCamera, cameraOptions, shallowLoose))) {
3911
- lastCamera = cameraOptions;
3912
- const isCamera = is.three(cameraOptions, 'isCamera');
3913
- let camera = isCamera
3914
- ? cameraOptions
3915
- : makeCameraInstance(orthographic, sizeOptions ?? state.size);
3916
- if (!isCamera) {
3917
- camera.position.z = 5;
3918
- if (cameraOptions) {
3919
- applyProps(camera, cameraOptions);
3920
- if ('aspect' in cameraOptions ||
3921
- 'left' in cameraOptions ||
3922
- 'right' in cameraOptions ||
3923
- 'top' in cameraOptions ||
3924
- 'bottom' in cameraOptions) {
3925
- Object.assign(camera, { manual: true });
3926
- camera?.updateProjectionMatrix();
3927
- }
3928
- }
3929
- // always look at center or passed-in lookAt by default
3930
- if (!state.camera && !cameraOptions?.rotation && !cameraOptions?.quaternion) {
3931
- if (Array.isArray(lookAt))
3932
- camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
3933
- else if (typeof lookAt === 'number')
3934
- camera.lookAt(lookAt, lookAt, lookAt);
3935
- else if (lookAt?.isVector3)
3936
- camera.lookAt(lookAt);
3937
- else
3938
- camera.lookAt(0, 0, 0);
3939
- }
3940
- // update projection matrix after applyprops
3941
- camera.updateProjectionMatrix?.();
3942
- }
3943
- if (!is.instance(camera))
3944
- camera = prepare(camera, '', { store });
4847
+ (state.camera === lifecycle.managedCamera &&
4848
+ !cameraConfigurationsEqual(lifecycle.appliedCameraConfiguration, cameraConfiguration))) {
4849
+ const camera = createManagedCamera(cameraConfiguration, store, sizeOptions ?? state.size);
3945
4850
  stateToUpdate.camera = camera;
4851
+ lifecycle.managedCamera = camera;
4852
+ lifecycle.appliedCameraConfiguration = cameraConfiguration;
3946
4853
  // Configure raycaster
3947
4854
  // https://github.com/pmndrs/react-xr/issues/300
3948
4855
  raycaster.camera = camera;
@@ -4311,15 +5218,24 @@ function elementEvents(target, events, { injector } = {}) {
4311
5218
  const targetObj = resolveRef(target());
4312
5219
  if (!targetObj || !is.instance(targetObj))
4313
5220
  return;
5221
+ const currentCleanUps = [];
4314
5222
  Object.keys(events).forEach((eventName) => {
4315
- 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);
4316
5226
  });
4317
5227
  onCleanup(() => {
4318
- 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
+ }
4319
5234
  });
4320
5235
  });
4321
5236
  inject(DestroyRef).onDestroy(() => {
4322
- cleanUps.forEach((cleanUp) => cleanUp());
5237
+ for (const cleanUp of cleanUps.splice(0))
5238
+ cleanUp();
4323
5239
  });
4324
5240
  return cleanUps;
4325
5241
  });
@@ -4438,15 +5354,24 @@ function objectEvents(target, events, { injector } = {}) {
4438
5354
  const targetObj = resolveRef(target());
4439
5355
  if (!targetObj || !is.instance(targetObj))
4440
5356
  return;
5357
+ const currentCleanUps = [];
4441
5358
  Object.entries(events).forEach(([eventName, eventHandler]) => {
4442
- cleanUps.push(renderer.listen(targetObj, eventName, eventHandler));
5359
+ const cleanUp = renderer.listen(targetObj, eventName, eventHandler);
5360
+ currentCleanUps.push(cleanUp);
5361
+ cleanUps.push(cleanUp);
4443
5362
  });
4444
5363
  onCleanup(() => {
4445
- 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
+ }
4446
5370
  });
4447
5371
  });
4448
5372
  inject(DestroyRef).onDestroy(() => {
4449
- cleanUps.forEach((cleanUp) => cleanUp());
5373
+ for (const cleanUp of cleanUps.splice(0))
5374
+ cleanUp();
4450
5375
  });
4451
5376
  return cleanUps;
4452
5377
  });
@@ -4478,5 +5403,5 @@ function hasListener(...emitterRefs) {
4478
5403
  * Generated bundle index. Do not edit.
4479
5404
  */
4480
5405
 
4481
- 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, 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 };
4482
5407
  //# sourceMappingURL=angular-three.mjs.map