angular-three 4.0.0-next.96 → 4.0.0-next.98

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, ElementRef, booleanAttribute, Pipe, numberAttribute, contentChild, SkipSelf, Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Injectable, output, model, Renderer2 } from '@angular/core';
2
+ import { inject, ViewContainerRef, TemplateRef, Injector, effect, DestroyRef, Directive, input, linkedSignal, computed, InjectionToken, untracked, isSignal, signal, ElementRef, booleanAttribute, resource, Pipe, numberAttribute, contentChild, SkipSelf, Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, Injectable, output, model, Renderer2 } from '@angular/core';
3
3
  import { DOCUMENT } from '@angular/common';
4
4
  import { Subject, filter } from 'rxjs';
5
5
  import * as THREE from 'three';
@@ -903,880 +903,978 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImpor
903
903
  }], ctorParameters: () => [] });
904
904
  const NgtSelection = [NgtSelectionApi, NgtSelect];
905
905
 
906
- var _a;
907
- //
908
- const NGT_HTML_DOM_ELEMENT = new InjectionToken('NGT_HTML_DOM_ELEMENT');
909
- function provideHTMLDomElement(...args) {
910
- if (args.length === 0) {
911
- return { provide: NGT_HTML_DOM_ELEMENT, useFactory: () => 'gl' };
912
- }
913
- if (args.length === 1) {
914
- return { provide: NGT_HTML_DOM_ELEMENT, useFactory: args[0] };
915
- }
916
- return { provide: NGT_HTML_DOM_ELEMENT, useFactory: args.pop(), deps: args };
917
- }
918
- class NgtHTML {
919
- static { _a = NGT_HTML_FLAG; }
920
- static { this[_a] = true; }
921
- constructor() {
922
- this.domElement = inject(NGT_HTML_DOM_ELEMENT, { self: true, optional: true });
923
- const host = inject(ElementRef);
924
- const store = injectStore();
925
- if (this.domElement === 'gl') {
926
- Object.assign(host.nativeElement, {
927
- [NGT_DOM_PARENT_FLAG]: store.snapshot.gl.domElement.parentElement,
928
- });
929
- }
930
- else if (this.domElement) {
931
- Object.assign(host.nativeElement, { [NGT_DOM_PARENT_FLAG]: this.domElement });
906
+ /**
907
+ * Release pointer captures.
908
+ * This is called by releasePointerCapture in the API, and when an object is removed.
909
+ */
910
+ function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
911
+ const captureData = captures.get(obj);
912
+ if (captureData) {
913
+ captures.delete(obj);
914
+ // If this was the last capturing object for this pointer
915
+ if (captures.size === 0) {
916
+ capturedMap.delete(pointerId);
917
+ captureData.target.releasePointerCapture(pointerId);
932
918
  }
933
- inject(DestroyRef).onDestroy(() => {
934
- host.nativeElement[NGT_DOM_PARENT_FLAG] = null;
935
- delete host.nativeElement[NGT_DOM_PARENT_FLAG];
936
- });
937
- }
938
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHTML, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
939
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.1", type: NgtHTML, isStandalone: true, ngImport: i0 }); }
940
- }
941
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHTML, decorators: [{
942
- type: Directive
943
- }], ctorParameters: () => [] });
944
-
945
- const cached = new Map();
946
- const memoizedLoaders = new WeakMap();
947
- function normalizeInputs(input) {
948
- let urls = [];
949
- if (Array.isArray(input)) {
950
- urls = input;
951
919
  }
952
- else if (typeof input === 'string') {
953
- urls = [input];
954
- }
955
- else {
956
- urls = Object.values(input);
957
- }
958
- return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
959
920
  }
960
- function load(loaderConstructorFactory, inputs, { extensions, onLoad, onProgress, } = {}) {
961
- return () => {
962
- const urls = normalizeInputs(inputs());
963
- let loader = memoizedLoaders.get(loaderConstructorFactory(urls));
964
- if (!loader) {
965
- loader = new (loaderConstructorFactory(urls))();
966
- memoizedLoaders.set(loaderConstructorFactory(urls), loader);
921
+ function removeInteractivity(store, object) {
922
+ const { internal } = store.snapshot;
923
+ // Removes every trace of an object from the data store
924
+ internal.interaction = internal.interaction.filter((o) => o !== object);
925
+ internal.initialHits = internal.initialHits.filter((o) => o !== object);
926
+ internal.hovered.forEach((value, key) => {
927
+ if (value.eventObject === object || value.object === object) {
928
+ // Clear out intersects, they are outdated by now
929
+ internal.hovered.delete(key);
967
930
  }
968
- if (extensions)
969
- extensions(loader);
970
- return urls.map((url) => {
971
- if (url === '')
972
- return Promise.resolve(null);
973
- if (!cached.has(url)) {
974
- cached.set(url, new Promise((resolve, reject) => {
975
- loader.load(url, (data) => {
976
- if ('scene' in data) {
977
- Object.assign(data, makeObjectGraph(data['scene']));
978
- }
979
- if (onLoad) {
980
- onLoad(data);
981
- }
982
- resolve(data);
983
- }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
984
- }));
985
- }
986
- return cached.get(url);
987
- });
988
- };
989
- }
990
- function _injectLoader(loaderConstructorFactory, inputs, { extensions, onProgress, onLoad, injector, } = {}) {
991
- return assertInjector(_injectLoader, injector, () => {
992
- const response = signal(null);
993
- const cachedResultPromisesEffect = load(loaderConstructorFactory, inputs, {
994
- extensions,
995
- onProgress,
996
- onLoad: onLoad,
997
- });
998
- effect(() => {
999
- const originalUrls = inputs();
1000
- const cachedResultPromises = cachedResultPromisesEffect();
1001
- Promise.all(cachedResultPromises).then((results) => {
1002
- response.update(() => {
1003
- if (Array.isArray(originalUrls))
1004
- return results;
1005
- if (typeof originalUrls === 'string')
1006
- return results[0];
1007
- const keys = Object.keys(originalUrls);
1008
- return keys.reduce((result, key) => {
1009
- // @ts-ignore
1010
- result[key] = results[keys.indexOf(key)];
1011
- return result;
1012
- }, {});
1013
- });
1014
- });
1015
- });
1016
- return response.asReadonly();
931
+ });
932
+ internal.capturedMap.forEach((captures, pointerId) => {
933
+ releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
1017
934
  });
1018
935
  }
1019
- _injectLoader.preload = (loaderConstructorFactory, inputs, extensions, onLoad) => {
1020
- const effects = load(loaderConstructorFactory, inputs, { extensions, onLoad })();
1021
- if (effects) {
1022
- void Promise.all(effects);
936
+ function createEvents(store) {
937
+ /** Calculates delta */
938
+ function calculateDistance(event) {
939
+ const internal = store.snapshot.internal;
940
+ const dx = event.offsetX - internal.initialClick[0];
941
+ const dy = event.offsetY - internal.initialClick[1];
942
+ return Math.round(Math.sqrt(dx * dx + dy * dy));
1023
943
  }
1024
- };
1025
- _injectLoader.destroy = () => {
1026
- cached.clear();
1027
- };
1028
- _injectLoader.clear = (urls) => {
1029
- const urlToClear = Array.isArray(urls) ? urls : [urls];
1030
- urlToClear.forEach((url) => {
1031
- cached.delete(url);
1032
- });
1033
- };
1034
- const injectLoader = _injectLoader;
1035
-
1036
- const RGBA_REGEX = /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*(\d*\.?\d+)?\)/;
1037
- const DEFAULT_COLOR = 0x000000;
1038
- class NgtHexify {
1039
- constructor() {
1040
- this.document = inject(DOCUMENT, { optional: true });
1041
- this.cache = {};
944
+ /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
945
+ function filterPointerEvents(objects) {
946
+ return objects.filter((obj) => ['move', 'over', 'enter', 'out', 'leave'].some((name) => {
947
+ const eventName = `pointer${name}`;
948
+ return getInstanceState(obj)?.handlers?.[eventName];
949
+ }));
1042
950
  }
1043
- /**
1044
- * transforms a:
1045
- * - hex string to a hex number
1046
- * - rgb string to a hex number
1047
- * - rgba string to a hex number
1048
- * - css color string to a hex number
1049
- *
1050
- * always default to black if failed
1051
- * @param value
1052
- */
1053
- transform(value) {
1054
- if (value == null)
1055
- return DEFAULT_COLOR;
1056
- if (value.startsWith('#')) {
1057
- if (!this.cache[value]) {
1058
- this.cache[value] = this.hexStringToNumber(value);
951
+ function intersect(event, filter) {
952
+ const state = store.snapshot;
953
+ const duplicates = new Set();
954
+ const intersections = [];
955
+ // Allow callers to eliminate event objects
956
+ const eventsObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
957
+ // Reset all raycaster cameras to undefined
958
+ for (let i = 0; i < eventsObjects.length; i++) {
959
+ const objectRootState = getInstanceState(eventsObjects[i])?.store?.snapshot;
960
+ if (objectRootState) {
961
+ objectRootState.raycaster.camera = undefined;
1059
962
  }
1060
- return this.cache[value];
1061
963
  }
1062
- if (!this.ctx) {
1063
- this.ctx = this.document?.createElement('canvas').getContext('2d');
1064
- }
1065
- if (!this.ctx) {
1066
- console.warn('[NGT] hexify: canvas context is not available');
1067
- return DEFAULT_COLOR;
964
+ if (!state.previousRoot) {
965
+ // Make sure root-level pointer and ray are set up
966
+ state.events.compute?.(event, store, null);
1068
967
  }
1069
- this.ctx.fillStyle = value;
1070
- const computedValue = this.ctx.fillStyle;
1071
- if (computedValue.startsWith('#')) {
1072
- if (!this.cache[computedValue]) {
1073
- this.cache[computedValue] = this.hexStringToNumber(computedValue);
968
+ function handleRaycast(obj) {
969
+ const objStore = getInstanceState(obj)?.store;
970
+ const objState = objStore?.snapshot;
971
+ // Skip event handling when noEvents is set, or when the raycasters camera is null
972
+ if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
973
+ return [];
974
+ // When the camera is undefined we have to call the event layers update function
975
+ if (objState.raycaster.camera === undefined) {
976
+ objState.events.compute?.(event, objStore, objState.previousRoot);
977
+ // If the camera is still undefined we have to skip this layer entirely
978
+ if (objState.raycaster.camera === undefined)
979
+ objState.raycaster.camera = null;
1074
980
  }
1075
- return this.cache[computedValue];
981
+ // Intersect object by object
982
+ return objState.raycaster.camera ? objState.raycaster.intersectObject(obj, true) : [];
1076
983
  }
1077
- if (!computedValue.startsWith('rgba')) {
1078
- console.warn(`[NGT] hexify: invalid color format. Expected rgba or hex, receive: ${computedValue}`);
1079
- return DEFAULT_COLOR;
984
+ // Collect events
985
+ let hits = eventsObjects
986
+ // Intersect objects
987
+ .flatMap(handleRaycast)
988
+ // Sort by event priority and distance
989
+ .sort((a, b) => {
990
+ const aState = getInstanceState(a.object)?.store?.snapshot;
991
+ const bState = getInstanceState(b.object)?.store?.snapshot;
992
+ if (!aState || !bState)
993
+ return a.distance - b.distance;
994
+ return bState.events.priority - aState.events.priority || a.distance - b.distance;
995
+ })
996
+ // Filter out duplicates
997
+ .filter((item) => {
998
+ const id = makeId(item);
999
+ if (duplicates.has(id))
1000
+ return false;
1001
+ duplicates.add(id);
1002
+ return true;
1003
+ });
1004
+ // https://github.com/mrdoob/three.js/issues/16031
1005
+ // Allow custom userland intersect sort order, this likely only makes sense on the root filter
1006
+ if (state.events.filter)
1007
+ hits = state.events.filter(hits, store);
1008
+ // Bubble up the events, find the event source (eventObject)
1009
+ for (const hit of hits) {
1010
+ let eventObject = hit.object;
1011
+ // bubble event up
1012
+ while (eventObject) {
1013
+ if (getInstanceState(eventObject)?.eventCount)
1014
+ intersections.push({ ...hit, eventObject });
1015
+ eventObject = eventObject.parent;
1016
+ }
1080
1017
  }
1081
- const match = computedValue.match(RGBA_REGEX);
1082
- if (!match) {
1083
- console.warn(`[NGT] hexify: invalid color format. Expected rgba or hex, receive: ${computedValue}`);
1084
- return DEFAULT_COLOR;
1085
- }
1086
- const r = parseInt(match[1], 10);
1087
- const g = parseInt(match[2], 10);
1088
- const b = parseInt(match[3], 10);
1089
- const a = match[4] ? parseFloat(match[4]) : 1.0;
1090
- const cacheKey = `${r}:${g}:${b}:${a}`;
1091
- // check result from cache
1092
- if (!this.cache[cacheKey]) {
1093
- // Convert the components to hex strings
1094
- const hexR = this.componentToHex(r);
1095
- const hexG = this.componentToHex(g);
1096
- const hexB = this.componentToHex(b);
1097
- const hexA = this.componentToHex(Math.round(a * 255));
1098
- // Combine the hex components into a single hex string
1099
- const hex = `#${hexR}${hexG}${hexB}${hexA}`;
1100
- this.cache[cacheKey] = this.hexStringToNumber(hex);
1018
+ // If the interaction is captured, make all capturing targets part of the intersect.
1019
+ if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
1020
+ for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
1021
+ if (!duplicates.has(makeId(captureData.intersection)))
1022
+ intersections.push(captureData.intersection);
1023
+ }
1101
1024
  }
1102
- return this.cache[cacheKey];
1103
- }
1104
- hexStringToNumber(hexString) {
1105
- return parseInt(hexString.replace('#', ''), 16);
1025
+ return intersections;
1106
1026
  }
1107
- componentToHex(component) {
1108
- const hex = component.toString(16);
1109
- return hex.length === 1 ? '0' + hex : hex;
1027
+ /** Handles intersections by forwarding them to handlers */
1028
+ function handleIntersects(intersections, event, delta, callback) {
1029
+ const rootState = store.snapshot;
1030
+ // If anything has been found, forward it to the event listeners
1031
+ if (intersections.length) {
1032
+ const localState = { stopped: false };
1033
+ for (const hit of intersections) {
1034
+ let instanceState = getInstanceState(hit.object);
1035
+ // If the object is not managed by NGT, it might be parented to an element which is.
1036
+ // Traverse upwards until we find a managed parent and use its state instead.
1037
+ if (!instanceState) {
1038
+ hit.object.traverseAncestors((ancestor) => {
1039
+ const parentInstanceState = getInstanceState(ancestor);
1040
+ if (parentInstanceState) {
1041
+ instanceState = parentInstanceState;
1042
+ return false;
1043
+ }
1044
+ return;
1045
+ });
1046
+ }
1047
+ const { raycaster, pointer, camera, internal } = instanceState?.store?.snapshot || rootState;
1048
+ const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
1049
+ const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1050
+ const setPointerCapture = (id) => {
1051
+ const captureData = { intersection: hit, target: event.target };
1052
+ if (internal.capturedMap.has(id)) {
1053
+ // if the pointerId was previously captured, we add the hit to the
1054
+ // event capturedMap.
1055
+ internal.capturedMap.get(id).set(hit.eventObject, captureData);
1056
+ }
1057
+ else {
1058
+ // if the pointerId was not previously captured, we create a map
1059
+ // containing the hitObject, and the hit. hitObject is used for
1060
+ // faster access.
1061
+ internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]]));
1062
+ }
1063
+ // Call the original event now
1064
+ event.target.setPointerCapture(id);
1065
+ };
1066
+ const releasePointerCapture = (id) => {
1067
+ const captures = internal.capturedMap.get(id);
1068
+ if (captures) {
1069
+ releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
1070
+ }
1071
+ };
1072
+ // Add native event props
1073
+ const extractEventProps = {};
1074
+ // This iterates over the event's properties including the inherited ones. Native PointerEvents have most of their props as getters which are inherited, but polyfilled PointerEvents have them all as their own properties (i.e. not inherited). We can't use Object.keys() or Object.entries() as they only return "own" properties; nor Object.getPrototypeOf(event) as that *doesn't* return "own" properties, only inherited ones.
1075
+ for (const prop in event) {
1076
+ const property = event[prop];
1077
+ // Only copy over atomics, leave functions alone as these should be
1078
+ // called as event.nativeEvent.fn()
1079
+ if (typeof property !== 'function')
1080
+ extractEventProps[prop] = property;
1081
+ }
1082
+ const raycastEvent = {
1083
+ ...hit,
1084
+ ...extractEventProps,
1085
+ pointer,
1086
+ intersections,
1087
+ stopped: localState.stopped,
1088
+ delta,
1089
+ unprojectedPoint,
1090
+ ray: raycaster.ray,
1091
+ camera,
1092
+ // Hijack stopPropagation, which just sets a flag
1093
+ stopPropagation() {
1094
+ // https://github.com/pmndrs/react-three-fiber/issues/596
1095
+ // Events are not allowed to stop propagation if the pointer has been captured
1096
+ const capturesForPointer = 'pointerId' in event && internal.capturedMap.get(event.pointerId);
1097
+ // We only authorize stopPropagation...
1098
+ if (
1099
+ // ...if this pointer hasn't been captured
1100
+ !capturesForPointer ||
1101
+ // ... or if the hit object is capturing the pointer
1102
+ capturesForPointer.has(hit.eventObject)) {
1103
+ raycastEvent.stopped = localState.stopped = true;
1104
+ // Propagation is stopped, remove all other hover records
1105
+ // An event handler is only allowed to flush other handlers if it is hovered itself
1106
+ if (internal.hovered.size &&
1107
+ Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1108
+ // Objects cannot flush out higher up objects that have already caught the event
1109
+ const higher = intersections.slice(0, intersections.indexOf(hit));
1110
+ cancelPointer([...higher, hit]);
1111
+ }
1112
+ }
1113
+ },
1114
+ // there should be a distinction between target and currentTarget
1115
+ target: { hasPointerCapture, setPointerCapture, releasePointerCapture },
1116
+ currentTarget: { hasPointerCapture, setPointerCapture, releasePointerCapture },
1117
+ nativeEvent: event,
1118
+ };
1119
+ // Call subscribers
1120
+ callback(raycastEvent);
1121
+ // Event bubbling may be interrupted by stopPropagation
1122
+ if (localState.stopped)
1123
+ break;
1124
+ }
1125
+ }
1126
+ return intersections;
1110
1127
  }
1111
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHexify, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1112
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.1", ngImport: i0, type: NgtHexify, isStandalone: true, name: "hexify" }); }
1113
- }
1114
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHexify, decorators: [{
1115
- type: Pipe,
1116
- args: [{ name: 'hexify', pure: true }]
1117
- }] });
1118
-
1119
- const catalogue = {};
1120
- function extend(objects) {
1121
- Object.assign(catalogue, objects);
1122
- }
1123
- const NGT_CATALOGUE = new InjectionToken('NGT_CATALOGUE', { factory: () => catalogue });
1124
- function injectCatalogue() {
1125
- return inject(NGT_CATALOGUE);
1126
- }
1127
-
1128
- function omit(objFn, keysToOmit, equal = (a, b) => is.equ(a, b, { objects: 'shallow', arrays: 'shallow' })) {
1129
- return computed(() => {
1130
- const obj = objFn();
1131
- const result = {};
1132
- for (const key of Object.keys(obj)) {
1133
- if (keysToOmit.includes(key))
1134
- continue;
1135
- Object.assign(result, { [key]: obj[key] });
1128
+ function cancelPointer(intersections) {
1129
+ const internal = store.snapshot.internal;
1130
+ for (const hoveredObj of internal.hovered.values()) {
1131
+ // When no objects were hit or the the hovered object wasn't found underneath the cursor
1132
+ // we call onPointerOut and delete the object from the hovered-elements map
1133
+ if (!intersections.length ||
1134
+ !intersections.find((hit) => hit.object === hoveredObj.object &&
1135
+ hit.index === hoveredObj.index &&
1136
+ hit.instanceId === hoveredObj.instanceId)) {
1137
+ const eventObject = hoveredObj.eventObject;
1138
+ const instance = getInstanceState(eventObject);
1139
+ const handlers = instance?.handlers;
1140
+ internal.hovered.delete(makeId(hoveredObj));
1141
+ if (instance?.eventCount) {
1142
+ // Clear out intersects, they are outdated by now
1143
+ const data = { ...hoveredObj, intersections };
1144
+ handlers?.pointerout?.(data);
1145
+ handlers?.pointerleave?.(data);
1146
+ }
1147
+ }
1136
1148
  }
1137
- return result;
1138
- }, { equal });
1139
- }
1140
- function pick(objFn, keyOrKeys, equal) {
1141
- if (Array.isArray(keyOrKeys)) {
1142
- if (!equal) {
1143
- equal = (a, b) => is.equ(a, b, { objects: 'shallow', arrays: 'shallow' });
1149
+ }
1150
+ function pointerMissed(event, objects) {
1151
+ for (let i = 0; i < objects.length; i++) {
1152
+ const instance = getInstanceState(objects[i]);
1153
+ instance?.handlers.pointermissed?.(event);
1144
1154
  }
1145
- return computed(() => {
1146
- const obj = objFn();
1147
- const result = {};
1148
- for (const key of keyOrKeys) {
1149
- if (!(key in obj))
1150
- continue;
1151
- Object.assign(result, { [key]: obj[key] });
1152
- }
1153
- return result;
1154
- }, { equal });
1155
1155
  }
1156
- return computed(() => objFn()[keyOrKeys], { equal });
1157
- }
1158
- function merge(objFn, toMerge, mode = 'override', equal = (a, b) => is.equ(a, b, { objects: 'shallow', arrays: 'shallow' })) {
1159
- if (mode === 'override')
1160
- return computed(() => ({ ...objFn(), ...toMerge }), { equal });
1161
- return computed(() => ({ ...toMerge, ...objFn() }), { equal });
1162
- }
1163
- function createVectorComputed(vectorCtor) {
1164
- return ((inputOrOptions, keyOrKeepUndefined, keepUndefined) => {
1165
- if (typeof keyOrKeepUndefined === 'undefined' || typeof keyOrKeepUndefined === 'boolean') {
1166
- keepUndefined = !!keyOrKeepUndefined;
1167
- const input = inputOrOptions;
1168
- return computed(() => {
1169
- const value = input();
1170
- if (keepUndefined && value == undefined)
1171
- return undefined;
1172
- if (typeof value === 'number')
1173
- return new vectorCtor().setScalar(value);
1174
- else if (Array.isArray(value))
1175
- return new vectorCtor(...value);
1176
- else if (value)
1177
- return value;
1178
- else
1179
- return new vectorCtor();
1180
- }, { equal: (a, b) => !!a && !!b && a.equals(b) });
1156
+ function handlePointer(name) {
1157
+ // Deal with cancelation
1158
+ switch (name) {
1159
+ case 'pointerleave':
1160
+ case 'pointercancel':
1161
+ return () => cancelPointer([]);
1162
+ case 'lostpointercapture':
1163
+ return (event) => {
1164
+ const { internal } = store.snapshot;
1165
+ if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
1166
+ // If the object event interface had lostpointercapture, we'd call it here on every
1167
+ // object that's getting removed. We call it on the next frame because lostpointercapture
1168
+ // fires before pointerup. Otherwise pointerUp would never be called if the event didn't
1169
+ // happen in the object it originated from, leaving components in a in-between state.
1170
+ requestAnimationFrame(() => {
1171
+ // Only release if pointer-up didn't do it already
1172
+ if (internal.capturedMap.has(event.pointerId)) {
1173
+ internal.capturedMap.delete(event.pointerId);
1174
+ cancelPointer([]);
1175
+ }
1176
+ });
1177
+ }
1178
+ };
1181
1179
  }
1182
- const options = inputOrOptions;
1183
- const key = keyOrKeepUndefined;
1184
- return computed(() => {
1185
- const value = options()[key];
1186
- if (keepUndefined && value == undefined)
1187
- return undefined;
1188
- if (typeof value === 'number')
1189
- return new vectorCtor().setScalar(value);
1190
- else if (Array.isArray(value))
1191
- return new vectorCtor(...value);
1192
- else if (value)
1193
- return value;
1194
- else
1195
- return new vectorCtor();
1196
- }, { equal: (a, b) => !!a && !!b && a.equals(b) });
1197
- });
1198
- }
1199
- const vector2 = createVectorComputed(THREE.Vector2);
1200
- const vector3 = createVectorComputed(THREE.Vector3);
1201
- const vector4 = createVectorComputed(THREE.Vector4);
1180
+ // Any other pointer goes here ...
1181
+ return function handleEvent(event) {
1182
+ // NOTE: __pointerMissed$ on NgtStore is private subject since we only expose the Observable
1183
+ const pointerMissed$ = store['__pointerMissed$'];
1184
+ const internal = store.snapshot.internal;
1185
+ // prepareRay(event)
1186
+ internal.lastEvent.nativeElement = event;
1187
+ // Get fresh intersects
1188
+ const isPointerMove = name === 'pointermove';
1189
+ const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick';
1190
+ const filter = isPointerMove ? filterPointerEvents : undefined;
1191
+ const hits = intersect(event, filter);
1192
+ const delta = isClickEvent ? calculateDistance(event) : 0;
1193
+ // Save initial coordinates on pointer-down
1194
+ if (name === 'pointerdown') {
1195
+ internal.initialClick = [event.offsetX, event.offsetY];
1196
+ internal.initialHits = hits.map((hit) => hit.eventObject);
1197
+ }
1198
+ // If a click yields no results, pass it back to the user as a miss
1199
+ // Missed events have to come first in order to establish user-land side-effect clean up
1200
+ if (isClickEvent && !hits.length) {
1201
+ if (delta <= 2) {
1202
+ pointerMissed(event, internal.interaction);
1203
+ pointerMissed$.next(event);
1204
+ }
1205
+ }
1206
+ // Take care of unhover
1207
+ if (isPointerMove)
1208
+ cancelPointer(hits);
1209
+ function onIntersect(data) {
1210
+ const eventObject = data.eventObject;
1211
+ const instance = getInstanceState(eventObject);
1212
+ const handlers = instance?.handlers;
1213
+ // Check presence of handlers
1214
+ if (!instance?.eventCount)
1215
+ return;
1216
+ /*
1217
+ MAYBE TODO, DELETE IF NOT:
1218
+ Check if the object is captured, captured events should not have intersects running in parallel
1219
+ But wouldn't it be better to just replace capturedMap with a single entry?
1220
+ Also, are we OK with straight up making picking up multiple objects impossible?
1202
1221
 
1203
- class NgtPortalAutoRender {
1204
- constructor() {
1205
- this.portalStore = injectStore({ host: true });
1206
- this.parentStore = injectStore({ skipSelf: true });
1207
- this.portal = inject(NgtPortalImpl, { host: true });
1208
- this.renderPriority = input(1, { alias: 'autoRender', transform: (value) => numberAttribute(value, 1) });
1209
- effect(() => {
1210
- // this.portalStore.update((state) => ({ events: { ...state.events, priority: this.renderPriority() + 1 } }));
1211
- });
1212
- effect((onCleanup) => {
1213
- const portalRendered = this.portal.portalRendered();
1214
- if (!portalRendered)
1215
- return;
1216
- // track state
1217
- const [renderPriority, { internal }] = [this.renderPriority(), this.portalStore()];
1218
- let oldClean;
1219
- const cleanup = internal.subscribe(({ gl, scene, camera }) => {
1220
- const [parentScene, parentCamera] = [
1221
- this.parentStore.snapshot.scene,
1222
- this.parentStore.snapshot.camera,
1223
- ];
1224
- oldClean = gl.autoClear;
1225
- if (renderPriority === 1) {
1226
- // clear scene and render with default
1227
- gl.autoClear = true;
1228
- gl.render(parentScene, parentCamera);
1222
+ const pointerId = (data as ThreeEvent<PointerEvent>).pointerId
1223
+ if (pointerId !== undefined) {
1224
+ const capturedMeshSet = internal.capturedMap.get(pointerId)
1225
+ if (capturedMeshSet) {
1226
+ const captured = capturedMeshSet.get(eventObject)
1227
+ if (captured && captured.localState.stopped) return
1228
+ }
1229
+ }*/
1230
+ if (isPointerMove) {
1231
+ // Move event ...
1232
+ if (handlers?.pointerover ||
1233
+ handlers?.pointerenter ||
1234
+ handlers?.pointerout ||
1235
+ handlers?.pointerleave) {
1236
+ // When enter or out is present take care of hover-state
1237
+ const id = makeId(data);
1238
+ const hoveredItem = internal.hovered.get(id);
1239
+ if (!hoveredItem) {
1240
+ // If the object wasn't previously hovered, book it and call its handler
1241
+ internal.hovered.set(id, data);
1242
+ handlers.pointerover?.(data);
1243
+ handlers.pointerenter?.(data);
1244
+ }
1245
+ else if (hoveredItem.stopped) {
1246
+ // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
1247
+ data.stopPropagation();
1248
+ }
1249
+ }
1250
+ // Call mouse move
1251
+ handlers?.pointermove?.(data);
1252
+ }
1253
+ else {
1254
+ // All other events ...
1255
+ const handler = handlers?.[name];
1256
+ if (handler) {
1257
+ // Forward all events back to their respective handlers with the exception of click events,
1258
+ // which must use the initial target
1259
+ if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1260
+ // Missed events have to come first
1261
+ pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1262
+ // Now call the handler
1263
+ handler(data);
1264
+ }
1265
+ }
1266
+ else {
1267
+ // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
1268
+ if (isClickEvent && internal.initialHits.includes(eventObject)) {
1269
+ pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1270
+ }
1271
+ }
1229
1272
  }
1230
- // disable cleaning
1231
- gl.autoClear = false;
1232
- gl.clearDepth();
1233
- gl.render(scene, camera);
1234
- // restore
1235
- gl.autoClear = oldClean;
1236
- }, renderPriority, this.portalStore);
1237
- onCleanup(() => cleanup());
1238
- });
1273
+ }
1274
+ handleIntersects(hits, event, delta, onIntersect);
1275
+ };
1239
1276
  }
1240
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalAutoRender, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1241
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.1", type: NgtPortalAutoRender, isStandalone: true, selector: "ngt-portal[autoRender]", inputs: { renderPriority: { classPropertyName: "renderPriority", publicName: "autoRender", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
1277
+ return { handlePointer };
1242
1278
  }
1243
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalAutoRender, decorators: [{
1244
- type: Directive,
1245
- args: [{ selector: 'ngt-portal[autoRender]' }]
1246
- }], ctorParameters: () => [] });
1247
- class NgtPortalContent {
1248
- static ngTemplateContextGuard(_, ctx) {
1249
- return true;
1279
+
1280
+ var _a;
1281
+ //
1282
+ const NGT_HTML_DOM_ELEMENT = new InjectionToken('NGT_HTML_DOM_ELEMENT');
1283
+ function provideHTMLDomElement(...args) {
1284
+ if (args.length === 0) {
1285
+ return { provide: NGT_HTML_DOM_ELEMENT, useFactory: () => 'gl' };
1286
+ }
1287
+ if (args.length === 1) {
1288
+ return { provide: NGT_HTML_DOM_ELEMENT, useFactory: args[0] };
1250
1289
  }
1290
+ return { provide: NGT_HTML_DOM_ELEMENT, useFactory: args.pop(), deps: args };
1291
+ }
1292
+ class NgtHTML {
1293
+ static { _a = NGT_HTML_FLAG; }
1294
+ static { this[_a] = true; }
1251
1295
  constructor() {
1252
- const host = inject(ElementRef, { skipSelf: true });
1253
- const { element } = inject(ViewContainerRef);
1254
- const injector = inject(Injector);
1255
- const commentNode = element.nativeElement;
1296
+ this.domElement = inject(NGT_HTML_DOM_ELEMENT, { self: true, optional: true });
1297
+ const host = inject(ElementRef);
1256
1298
  const store = injectStore();
1257
- commentNode.data = NGT_PORTAL_CONTENT_FLAG;
1258
- commentNode[NGT_PORTAL_CONTENT_FLAG] = store;
1259
- commentNode[NGT_DOM_PARENT_FLAG] = host.nativeElement;
1260
- if (commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]) {
1261
- commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]('portal', injector);
1262
- delete commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG];
1299
+ if (this.domElement === 'gl') {
1300
+ Object.assign(host.nativeElement, {
1301
+ [NGT_DOM_PARENT_FLAG]: store.snapshot.gl.domElement.parentElement,
1302
+ });
1303
+ }
1304
+ else if (this.domElement) {
1305
+ Object.assign(host.nativeElement, { [NGT_DOM_PARENT_FLAG]: this.domElement });
1263
1306
  }
1307
+ inject(DestroyRef).onDestroy(() => {
1308
+ host.nativeElement[NGT_DOM_PARENT_FLAG] = null;
1309
+ delete host.nativeElement[NGT_DOM_PARENT_FLAG];
1310
+ });
1264
1311
  }
1265
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalContent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1266
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.1", type: NgtPortalContent, isStandalone: true, selector: "ng-template[portalContent]", ngImport: i0 }); }
1312
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHTML, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1313
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.1", type: NgtHTML, isStandalone: true, ngImport: i0 }); }
1267
1314
  }
1268
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalContent, decorators: [{
1269
- type: Directive,
1270
- args: [{ selector: 'ng-template[portalContent]' }]
1315
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHTML, decorators: [{
1316
+ type: Directive
1271
1317
  }], ctorParameters: () => [] });
1272
- function mergeState(previousRoot, store, container, pointer, raycaster, events, size) {
1273
- // we never want to spread the id
1274
- const { id: _, ...previousState } = previousRoot.snapshot;
1275
- const state = store.snapshot;
1276
- let viewport = undefined;
1277
- if (state.camera && size) {
1278
- const camera = state.camera;
1279
- // calculate the override viewport, if present
1280
- viewport = previousState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
1281
- // update the portal camera, if it differs from the previous layer
1282
- if (camera !== previousState.camera)
1283
- updateCamera(camera, size);
1318
+
1319
+ const cached$1 = new Map();
1320
+ const memoizedLoaders$1 = new WeakMap();
1321
+ function normalizeInputs$1(input) {
1322
+ let urls = [];
1323
+ if (Array.isArray(input)) {
1324
+ urls = input;
1284
1325
  }
1285
- return {
1286
- // the intersect consists of the previous root state
1287
- ...previousState,
1288
- ...state,
1289
- // portals have their own scene, which forms the root, a raycaster and a pointer
1290
- scene: container,
1291
- pointer,
1292
- raycaster,
1293
- // their previous root is the layer before it
1294
- previousRoot,
1295
- events: { ...previousState.events, ...state.events, ...events },
1296
- size: { ...previousState.size, ...size },
1297
- viewport: { ...previousState.viewport, ...viewport },
1298
- // layers are allowed to override events
1299
- setEvents: (events) => store.update((state) => ({ ...state, events: { ...state.events, ...events } })),
1300
- };
1326
+ else if (typeof input === 'string') {
1327
+ urls = [input];
1328
+ }
1329
+ else {
1330
+ urls = Object.values(input);
1331
+ }
1332
+ return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
1301
1333
  }
1302
- class NgtPortalImpl {
1303
- constructor() {
1304
- this.container = input.required();
1305
- this.state = input({});
1306
- this.contentRef = contentChild.required(NgtPortalContent, { read: TemplateRef });
1307
- this.anchorRef = contentChild.required(NgtPortalContent, { read: ViewContainerRef });
1308
- this.previousStore = injectStore({ skipSelf: true });
1309
- this.portalStore = injectStore();
1310
- this.injector = inject(Injector);
1311
- this.size = pick(this.state, 'size');
1312
- this.events = pick(this.state, 'events');
1313
- this.restState = omit(this.state, ['size', 'events']);
1314
- this.portalContentRendered = signal(false);
1315
- this.portalRendered = this.portalContentRendered.asReadonly();
1316
- extend({ Group });
1317
- effect(() => {
1318
- let [container, anchor, content] = [
1319
- this.container(),
1320
- this.anchorRef(),
1321
- this.contentRef(),
1322
- this.previousStore(),
1323
- ];
1324
- const [size, events, restState] = [untracked(this.size), untracked(this.events), untracked(this.restState)];
1325
- if (!is.instance(container)) {
1326
- container = prepare(container, 'ngt-portal', { store: this.portalStore });
1327
- }
1328
- const instanceState = getInstanceState(container);
1329
- if (instanceState && instanceState.store !== this.portalStore) {
1330
- instanceState.store = this.portalStore;
1331
- }
1332
- this.portalStore.update(restState, mergeState(this.previousStore, this.portalStore, container, this.portalStore.snapshot.pointer, this.portalStore.snapshot.raycaster, events, size));
1333
- if (this.portalViewRef) {
1334
- this.portalViewRef.detectChanges();
1335
- return;
1334
+ function load(loaderConstructorFactory, inputs, { extensions, onLoad, onProgress, } = {}) {
1335
+ return () => {
1336
+ const urls = normalizeInputs$1(inputs());
1337
+ let loader = memoizedLoaders$1.get(loaderConstructorFactory(urls));
1338
+ if (!loader) {
1339
+ loader = new (loaderConstructorFactory(urls))();
1340
+ memoizedLoaders$1.set(loaderConstructorFactory(urls), loader);
1341
+ }
1342
+ if (extensions)
1343
+ extensions(loader);
1344
+ return urls.map((url) => {
1345
+ if (url === '')
1346
+ return Promise.resolve(null);
1347
+ if (!cached$1.has(url)) {
1348
+ cached$1.set(url, new Promise((resolve, reject) => {
1349
+ loader.load(url, (data) => {
1350
+ if ('scene' in data) {
1351
+ Object.assign(data, makeObjectGraph(data['scene']));
1352
+ }
1353
+ if (onLoad) {
1354
+ onLoad(data);
1355
+ }
1356
+ resolve(data);
1357
+ }, onProgress, (error) => reject(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
1358
+ }));
1336
1359
  }
1337
- this.portalViewRef = anchor.createEmbeddedView(content, { injector: this.injector }, { injector: this.injector });
1338
- this.portalViewRef.detectChanges();
1339
- this.portalContentRendered.set(true);
1360
+ return cached$1.get(url);
1340
1361
  });
1341
- }
1342
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalImpl, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1343
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.1", 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: [
1344
- {
1345
- provide: NGT_STORE,
1346
- useFactory: (previousStore) => {
1347
- const pointer = new THREE.Vector2();
1348
- const raycaster = new THREE.Raycaster();
1349
- const { id: _skipId, ...previousState } = previousStore.snapshot;
1350
- const store = signalState({
1351
- id: makeId(),
1352
- ...previousState,
1353
- scene: null,
1354
- previousRoot: previousStore,
1355
- pointer,
1356
- raycaster,
1357
- });
1358
- store.update(mergeState(previousStore, store, null, pointer, raycaster));
1359
- return store;
1360
- },
1361
- deps: [[new SkipSelf(), NGT_STORE]],
1362
- },
1363
- ], queries: [{ propertyName: "contentRef", first: true, predicate: NgtPortalContent, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "anchorRef", first: true, predicate: NgtPortalContent, descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: `
1364
- @if (portalRendered()) {
1365
- <!-- Without an element that receives pointer events state.pointer will always be 0/0 -->
1366
- <ngt-group (pointerover)="(undefined)" attach="none" />
1367
- }
1368
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1362
+ };
1369
1363
  }
1370
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalImpl, decorators: [{
1371
- type: Component,
1372
- args: [{
1373
- selector: 'ngt-portal',
1374
- template: `
1375
- @if (portalRendered()) {
1376
- <!-- Without an element that receives pointer events state.pointer will always be 0/0 -->
1377
- <ngt-group (pointerover)="(undefined)" attach="none" />
1378
- }
1379
- `,
1380
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
1381
- changeDetection: ChangeDetectionStrategy.OnPush,
1382
- providers: [
1383
- {
1384
- provide: NGT_STORE,
1385
- useFactory: (previousStore) => {
1386
- const pointer = new THREE.Vector2();
1387
- const raycaster = new THREE.Raycaster();
1388
- const { id: _skipId, ...previousState } = previousStore.snapshot;
1389
- const store = signalState({
1390
- id: makeId(),
1391
- ...previousState,
1392
- scene: null,
1393
- previousRoot: previousStore,
1394
- pointer,
1395
- raycaster,
1396
- });
1397
- store.update(mergeState(previousStore, store, null, pointer, raycaster));
1398
- return store;
1399
- },
1400
- deps: [[new SkipSelf(), NGT_STORE]],
1401
- },
1402
- ],
1403
- }]
1404
- }], ctorParameters: () => [] });
1405
- const NgtPortal = [NgtPortalImpl, NgtPortalContent];
1406
-
1407
1364
  /**
1408
- * Release pointer captures.
1409
- * This is called by releasePointerCapture in the API, and when an object is removed.
1365
+ * @deprecated Use loaderResource instead. Will be removed in v5.0.0
1366
+ * @since v4.0.0~
1367
+ */
1368
+ function _injectLoader(loaderConstructorFactory, inputs, { extensions, onProgress, onLoad, injector, } = {}) {
1369
+ return assertInjector(_injectLoader, injector, () => {
1370
+ const response = signal(null);
1371
+ const cachedResultPromisesEffect = load(loaderConstructorFactory, inputs, {
1372
+ extensions,
1373
+ onProgress,
1374
+ onLoad: onLoad,
1375
+ });
1376
+ effect(() => {
1377
+ const originalUrls = inputs();
1378
+ const cachedResultPromises = cachedResultPromisesEffect();
1379
+ Promise.all(cachedResultPromises).then((results) => {
1380
+ response.update(() => {
1381
+ if (Array.isArray(originalUrls))
1382
+ return results;
1383
+ if (typeof originalUrls === 'string')
1384
+ return results[0];
1385
+ const keys = Object.keys(originalUrls);
1386
+ return keys.reduce((result, key) => {
1387
+ // @ts-ignore
1388
+ result[key] = results[keys.indexOf(key)];
1389
+ return result;
1390
+ }, {});
1391
+ });
1392
+ });
1393
+ });
1394
+ return response.asReadonly();
1395
+ });
1396
+ }
1397
+ _injectLoader.preload = (loaderConstructorFactory, inputs, extensions, onLoad) => {
1398
+ const effects = load(loaderConstructorFactory, inputs, { extensions, onLoad })();
1399
+ if (effects) {
1400
+ void Promise.all(effects);
1401
+ }
1402
+ };
1403
+ _injectLoader.destroy = () => {
1404
+ cached$1.clear();
1405
+ };
1406
+ _injectLoader.clear = (urls) => {
1407
+ const urlToClear = Array.isArray(urls) ? urls : [urls];
1408
+ urlToClear.forEach((url) => {
1409
+ cached$1.delete(url);
1410
+ });
1411
+ };
1412
+ /**
1413
+ * @deprecated Use loaderResource instead. Will be removed in v5.0.0
1414
+ * @since v4.0.0~
1410
1415
  */
1411
- function releaseInternalPointerCapture(capturedMap, obj, captures, pointerId) {
1412
- const captureData = captures.get(obj);
1413
- if (captureData) {
1414
- captures.delete(obj);
1415
- // If this was the last capturing object for this pointer
1416
- if (captures.size === 0) {
1417
- capturedMap.delete(pointerId);
1418
- captureData.target.releasePointerCapture(pointerId);
1419
- }
1416
+ const injectLoader = _injectLoader;
1417
+
1418
+ function normalizeInputs(input) {
1419
+ let urls = [];
1420
+ if (Array.isArray(input)) {
1421
+ urls = input;
1420
1422
  }
1421
- }
1422
- function removeInteractivity(store, object) {
1423
- const { internal } = store.snapshot;
1424
- // Removes every trace of an object from the data store
1425
- internal.interaction = internal.interaction.filter((o) => o !== object);
1426
- internal.initialHits = internal.initialHits.filter((o) => o !== object);
1427
- internal.hovered.forEach((value, key) => {
1428
- if (value.eventObject === object || value.object === object) {
1429
- // Clear out intersects, they are outdated by now
1430
- internal.hovered.delete(key);
1431
- }
1432
- });
1433
- internal.capturedMap.forEach((captures, pointerId) => {
1434
- releaseInternalPointerCapture(internal.capturedMap, object, captures, pointerId);
1435
- });
1436
- }
1437
- function createEvents(store) {
1438
- /** Calculates delta */
1439
- function calculateDistance(event) {
1440
- const internal = store.snapshot.internal;
1441
- const dx = event.offsetX - internal.initialClick[0];
1442
- const dy = event.offsetY - internal.initialClick[1];
1443
- return Math.round(Math.sqrt(dx * dx + dy * dy));
1423
+ else if (typeof input === 'string') {
1424
+ urls = [input];
1444
1425
  }
1445
- /** Returns true if an instance has a valid pointer-event registered, this excludes scroll, clicks etc */
1446
- function filterPointerEvents(objects) {
1447
- return objects.filter((obj) => ['move', 'over', 'enter', 'out', 'leave'].some((name) => {
1448
- const eventName = `pointer${name}`;
1449
- return getInstanceState(obj)?.handlers?.[eventName];
1450
- }));
1426
+ else {
1427
+ urls = Object.values(input);
1451
1428
  }
1452
- function intersect(event, filter) {
1453
- const state = store.snapshot;
1454
- const duplicates = new Set();
1455
- const intersections = [];
1456
- // Allow callers to eliminate event objects
1457
- const eventsObjects = filter ? filter(state.internal.interaction) : state.internal.interaction;
1458
- // Reset all raycaster cameras to undefined
1459
- for (let i = 0; i < eventsObjects.length; i++) {
1460
- const objectRootState = getInstanceState(eventsObjects[i])?.store?.snapshot;
1461
- if (objectRootState) {
1462
- objectRootState.raycaster.camera = undefined;
1463
- }
1464
- }
1465
- if (!state.previousRoot) {
1466
- // Make sure root-level pointer and ray are set up
1467
- state.events.compute?.(event, store, null);
1468
- }
1469
- function handleRaycast(obj) {
1470
- const objStore = getInstanceState(obj)?.store;
1471
- const objState = objStore?.snapshot;
1472
- // Skip event handling when noEvents is set, or when the raycasters camera is null
1473
- if (!objState || !objState.events.enabled || objState.raycaster.camera === null)
1474
- return [];
1475
- // When the camera is undefined we have to call the event layers update function
1476
- if (objState.raycaster.camera === undefined) {
1477
- objState.events.compute?.(event, objStore, objState.previousRoot);
1478
- // If the camera is still undefined we have to skip this layer entirely
1479
- if (objState.raycaster.camera === undefined)
1480
- objState.raycaster.camera = null;
1481
- }
1482
- // Intersect object by object
1483
- return objState.raycaster.camera ? objState.raycaster.intersectObject(obj, true) : [];
1484
- }
1485
- // Collect events
1486
- let hits = eventsObjects
1487
- // Intersect objects
1488
- .flatMap(handleRaycast)
1489
- // Sort by event priority and distance
1490
- .sort((a, b) => {
1491
- const aState = getInstanceState(a.object)?.store?.snapshot;
1492
- const bState = getInstanceState(b.object)?.store?.snapshot;
1493
- if (!aState || !bState)
1494
- return a.distance - b.distance;
1495
- return bState.events.priority - aState.events.priority || a.distance - b.distance;
1496
- })
1497
- // Filter out duplicates
1498
- .filter((item) => {
1499
- const id = makeId(item);
1500
- if (duplicates.has(id))
1501
- return false;
1502
- duplicates.add(id);
1503
- return true;
1429
+ return urls.map((url) => (url.includes('undefined') || url.includes('null') || !url ? '' : url));
1430
+ }
1431
+ const cached = new Map();
1432
+ const memoizedLoaders = new WeakMap();
1433
+ function getLoaderRequestParams(input, loaderConstructorFactory, extensions) {
1434
+ const urls = input();
1435
+ const LoaderConstructor = loaderConstructorFactory(urls);
1436
+ const normalizedUrls = normalizeInputs(urls);
1437
+ let loader = memoizedLoaders.get(LoaderConstructor);
1438
+ if (!loader) {
1439
+ loader = new LoaderConstructor();
1440
+ memoizedLoaders.set(LoaderConstructor, loader);
1441
+ }
1442
+ if (extensions)
1443
+ extensions(loader);
1444
+ return { urls, normalizedUrls, loader };
1445
+ }
1446
+ function getLoaderPromises(request, onProgress) {
1447
+ return request.normalizedUrls.map((url) => {
1448
+ if (url === '')
1449
+ return Promise.resolve(null);
1450
+ const cachedPromise = cached.get(url);
1451
+ if (cachedPromise)
1452
+ return cachedPromise;
1453
+ const promise = new Promise((res, rej) => {
1454
+ request.loader.load(url, (data) => {
1455
+ if ('scene' in data) {
1456
+ Object.assign(data, makeObjectGraph(data['scene']));
1457
+ }
1458
+ res(data);
1459
+ }, onProgress, (error) => rej(new Error(`[NGT] Could not load ${url}: ${error?.message}`)));
1504
1460
  });
1505
- // https://github.com/mrdoob/three.js/issues/16031
1506
- // Allow custom userland intersect sort order, this likely only makes sense on the root filter
1507
- if (state.events.filter)
1508
- hits = state.events.filter(hits, store);
1509
- // Bubble up the events, find the event source (eventObject)
1510
- for (const hit of hits) {
1511
- let eventObject = hit.object;
1512
- // bubble event up
1513
- while (eventObject) {
1514
- if (getInstanceState(eventObject)?.eventCount)
1515
- intersections.push({ ...hit, eventObject });
1516
- eventObject = eventObject.parent;
1517
- }
1518
- }
1519
- // If the interaction is captured, make all capturing targets part of the intersect.
1520
- if ('pointerId' in event && state.internal.capturedMap.has(event.pointerId)) {
1521
- for (const captureData of state.internal.capturedMap.get(event.pointerId).values()) {
1522
- if (!duplicates.has(makeId(captureData.intersection)))
1523
- intersections.push(captureData.intersection);
1524
- }
1525
- }
1526
- return intersections;
1527
- }
1528
- /** Handles intersections by forwarding them to handlers */
1529
- function handleIntersects(intersections, event, delta, callback) {
1530
- const rootState = store.snapshot;
1531
- // If anything has been found, forward it to the event listeners
1532
- if (intersections.length) {
1533
- const localState = { stopped: false };
1534
- for (const hit of intersections) {
1535
- let instanceState = getInstanceState(hit.object);
1536
- // If the object is not managed by NGT, it might be parented to an element which is.
1537
- // Traverse upwards until we find a managed parent and use its state instead.
1538
- if (!instanceState) {
1539
- hit.object.traverseAncestors((ancestor) => {
1540
- const parentInstanceState = getInstanceState(ancestor);
1541
- if (parentInstanceState) {
1542
- instanceState = parentInstanceState;
1543
- return false;
1544
- }
1545
- return;
1546
- });
1461
+ cached.set(url, promise);
1462
+ return promise;
1463
+ });
1464
+ }
1465
+ function loaderResource(loaderConstructorFactory, input, { extensions, onLoad, onProgress, injector, } = {}) {
1466
+ return assertInjector(loaderResource, injector, () => {
1467
+ return resource({
1468
+ request: () => getLoaderRequestParams(input, loaderConstructorFactory, extensions),
1469
+ loader: async ({ request }) => {
1470
+ const loadedResults = await Promise.all(getLoaderPromises(request, onProgress));
1471
+ let results;
1472
+ if (Array.isArray(request.urls)) {
1473
+ results = loadedResults;
1547
1474
  }
1548
- const { raycaster, pointer, camera, internal } = instanceState?.store?.snapshot || rootState;
1549
- const unprojectedPoint = new THREE.Vector3(pointer.x, pointer.y, 0).unproject(camera);
1550
- const hasPointerCapture = (id) => internal.capturedMap.get(id)?.has(hit.eventObject) ?? false;
1551
- const setPointerCapture = (id) => {
1552
- const captureData = { intersection: hit, target: event.target };
1553
- if (internal.capturedMap.has(id)) {
1554
- // if the pointerId was previously captured, we add the hit to the
1555
- // event capturedMap.
1556
- internal.capturedMap.get(id).set(hit.eventObject, captureData);
1557
- }
1558
- else {
1559
- // if the pointerId was not previously captured, we create a map
1560
- // containing the hitObject, and the hit. hitObject is used for
1561
- // faster access.
1562
- internal.capturedMap.set(id, new Map([[hit.eventObject, captureData]]));
1563
- }
1564
- // Call the original event now
1565
- event.target.setPointerCapture(id);
1566
- };
1567
- const releasePointerCapture = (id) => {
1568
- const captures = internal.capturedMap.get(id);
1569
- if (captures) {
1570
- releaseInternalPointerCapture(internal.capturedMap, hit.eventObject, captures, id);
1571
- }
1572
- };
1573
- // Add native event props
1574
- const extractEventProps = {};
1575
- // This iterates over the event's properties including the inherited ones. Native PointerEvents have most of their props as getters which are inherited, but polyfilled PointerEvents have them all as their own properties (i.e. not inherited). We can't use Object.keys() or Object.entries() as they only return "own" properties; nor Object.getPrototypeOf(event) as that *doesn't* return "own" properties, only inherited ones.
1576
- for (const prop in event) {
1577
- const property = event[prop];
1578
- // Only copy over atomics, leave functions alone as these should be
1579
- // called as event.nativeEvent.fn()
1580
- if (typeof property !== 'function')
1581
- extractEventProps[prop] = property;
1475
+ else if (typeof request.urls === 'string') {
1476
+ results = loadedResults[0];
1582
1477
  }
1583
- const raycastEvent = {
1584
- ...hit,
1585
- ...extractEventProps,
1586
- pointer,
1587
- intersections,
1588
- stopped: localState.stopped,
1589
- delta,
1590
- unprojectedPoint,
1591
- ray: raycaster.ray,
1592
- camera,
1593
- // Hijack stopPropagation, which just sets a flag
1594
- stopPropagation() {
1595
- // https://github.com/pmndrs/react-three-fiber/issues/596
1596
- // Events are not allowed to stop propagation if the pointer has been captured
1597
- const capturesForPointer = 'pointerId' in event && internal.capturedMap.get(event.pointerId);
1598
- // We only authorize stopPropagation...
1599
- if (
1600
- // ...if this pointer hasn't been captured
1601
- !capturesForPointer ||
1602
- // ... or if the hit object is capturing the pointer
1603
- capturesForPointer.has(hit.eventObject)) {
1604
- raycastEvent.stopped = localState.stopped = true;
1605
- // Propagation is stopped, remove all other hover records
1606
- // An event handler is only allowed to flush other handlers if it is hovered itself
1607
- if (internal.hovered.size &&
1608
- Array.from(internal.hovered.values()).find((i) => i.eventObject === hit.eventObject)) {
1609
- // Objects cannot flush out higher up objects that have already caught the event
1610
- const higher = intersections.slice(0, intersections.indexOf(hit));
1611
- cancelPointer([...higher, hit]);
1612
- }
1613
- }
1614
- },
1615
- // there should be a distinction between target and currentTarget
1616
- target: { hasPointerCapture, setPointerCapture, releasePointerCapture },
1617
- currentTarget: { hasPointerCapture, setPointerCapture, releasePointerCapture },
1618
- nativeEvent: event,
1619
- };
1620
- // Call subscribers
1621
- callback(raycastEvent);
1622
- // Event bubbling may be interrupted by stopPropagation
1623
- if (localState.stopped)
1624
- break;
1478
+ else {
1479
+ const keys = Object.keys(request.urls);
1480
+ results = keys.reduce((result, key) => {
1481
+ // @ts-ignore
1482
+ result[key] = loadedResults[keys.indexOf(key)];
1483
+ return result;
1484
+ }, {});
1485
+ }
1486
+ if (onLoad) {
1487
+ onLoad(results);
1488
+ }
1489
+ return results;
1490
+ },
1491
+ });
1492
+ });
1493
+ }
1494
+ loaderResource.preload = (loaderConstructor, inputs, extensions) => {
1495
+ const params = getLoaderRequestParams(() => inputs, () => loaderConstructor, extensions);
1496
+ void Promise.all(getLoaderPromises(params));
1497
+ };
1498
+ loaderResource.destroy = () => {
1499
+ cached.clear();
1500
+ };
1501
+ loaderResource.clear = (urls) => {
1502
+ const urlToClear = Array.isArray(urls) ? urls : [urls];
1503
+ urlToClear.forEach((url) => {
1504
+ cached.delete(url);
1505
+ });
1506
+ };
1507
+
1508
+ const RGBA_REGEX = /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*(\d*\.?\d+)?\)/;
1509
+ const DEFAULT_COLOR = 0x000000;
1510
+ class NgtHexify {
1511
+ constructor() {
1512
+ this.document = inject(DOCUMENT, { optional: true });
1513
+ this.cache = {};
1514
+ }
1515
+ /**
1516
+ * transforms a:
1517
+ * - hex string to a hex number
1518
+ * - rgb string to a hex number
1519
+ * - rgba string to a hex number
1520
+ * - css color string to a hex number
1521
+ *
1522
+ * always default to black if failed
1523
+ * @param value
1524
+ */
1525
+ transform(value) {
1526
+ if (value == null)
1527
+ return DEFAULT_COLOR;
1528
+ if (value.startsWith('#')) {
1529
+ if (!this.cache[value]) {
1530
+ this.cache[value] = this.hexStringToNumber(value);
1625
1531
  }
1532
+ return this.cache[value];
1626
1533
  }
1627
- return intersections;
1628
- }
1629
- function cancelPointer(intersections) {
1630
- const internal = store.snapshot.internal;
1631
- for (const hoveredObj of internal.hovered.values()) {
1632
- // When no objects were hit or the the hovered object wasn't found underneath the cursor
1633
- // we call onPointerOut and delete the object from the hovered-elements map
1634
- if (!intersections.length ||
1635
- !intersections.find((hit) => hit.object === hoveredObj.object &&
1636
- hit.index === hoveredObj.index &&
1637
- hit.instanceId === hoveredObj.instanceId)) {
1638
- const eventObject = hoveredObj.eventObject;
1639
- const instance = getInstanceState(eventObject);
1640
- const handlers = instance?.handlers;
1641
- internal.hovered.delete(makeId(hoveredObj));
1642
- if (instance?.eventCount) {
1643
- // Clear out intersects, they are outdated by now
1644
- const data = { ...hoveredObj, intersections };
1645
- handlers?.pointerout?.(data);
1646
- handlers?.pointerleave?.(data);
1647
- }
1534
+ if (!this.ctx) {
1535
+ this.ctx = this.document?.createElement('canvas').getContext('2d');
1536
+ }
1537
+ if (!this.ctx) {
1538
+ console.warn('[NGT] hexify: canvas context is not available');
1539
+ return DEFAULT_COLOR;
1540
+ }
1541
+ this.ctx.fillStyle = value;
1542
+ const computedValue = this.ctx.fillStyle;
1543
+ if (computedValue.startsWith('#')) {
1544
+ if (!this.cache[computedValue]) {
1545
+ this.cache[computedValue] = this.hexStringToNumber(computedValue);
1648
1546
  }
1547
+ return this.cache[computedValue];
1548
+ }
1549
+ if (!computedValue.startsWith('rgba')) {
1550
+ console.warn(`[NGT] hexify: invalid color format. Expected rgba or hex, receive: ${computedValue}`);
1551
+ return DEFAULT_COLOR;
1552
+ }
1553
+ const match = computedValue.match(RGBA_REGEX);
1554
+ if (!match) {
1555
+ console.warn(`[NGT] hexify: invalid color format. Expected rgba or hex, receive: ${computedValue}`);
1556
+ return DEFAULT_COLOR;
1557
+ }
1558
+ const r = parseInt(match[1], 10);
1559
+ const g = parseInt(match[2], 10);
1560
+ const b = parseInt(match[3], 10);
1561
+ const a = match[4] ? parseFloat(match[4]) : 1.0;
1562
+ const cacheKey = `${r}:${g}:${b}:${a}`;
1563
+ // check result from cache
1564
+ if (!this.cache[cacheKey]) {
1565
+ // Convert the components to hex strings
1566
+ const hexR = this.componentToHex(r);
1567
+ const hexG = this.componentToHex(g);
1568
+ const hexB = this.componentToHex(b);
1569
+ const hexA = this.componentToHex(Math.round(a * 255));
1570
+ // Combine the hex components into a single hex string
1571
+ const hex = `#${hexR}${hexG}${hexB}${hexA}`;
1572
+ this.cache[cacheKey] = this.hexStringToNumber(hex);
1649
1573
  }
1574
+ return this.cache[cacheKey];
1650
1575
  }
1651
- function pointerMissed(event, objects) {
1652
- for (let i = 0; i < objects.length; i++) {
1653
- const instance = getInstanceState(objects[i]);
1654
- instance?.handlers.pointermissed?.(event);
1576
+ hexStringToNumber(hexString) {
1577
+ return parseInt(hexString.replace('#', ''), 16);
1578
+ }
1579
+ componentToHex(component) {
1580
+ const hex = component.toString(16);
1581
+ return hex.length === 1 ? '0' + hex : hex;
1582
+ }
1583
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHexify, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
1584
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.1", ngImport: i0, type: NgtHexify, isStandalone: true, name: "hexify" }); }
1585
+ }
1586
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtHexify, decorators: [{
1587
+ type: Pipe,
1588
+ args: [{ name: 'hexify', pure: true }]
1589
+ }] });
1590
+
1591
+ const catalogue = {};
1592
+ function extend(objects) {
1593
+ Object.assign(catalogue, objects);
1594
+ }
1595
+ const NGT_CATALOGUE = new InjectionToken('NGT_CATALOGUE', { factory: () => catalogue });
1596
+ function injectCatalogue() {
1597
+ return inject(NGT_CATALOGUE);
1598
+ }
1599
+
1600
+ function omit(objFn, keysToOmit, equal = (a, b) => is.equ(a, b, { objects: 'shallow', arrays: 'shallow' })) {
1601
+ return computed(() => {
1602
+ const obj = objFn();
1603
+ const result = {};
1604
+ for (const key of Object.keys(obj)) {
1605
+ if (keysToOmit.includes(key))
1606
+ continue;
1607
+ Object.assign(result, { [key]: obj[key] });
1608
+ }
1609
+ return result;
1610
+ }, { equal });
1611
+ }
1612
+ function pick(objFn, keyOrKeys, equal) {
1613
+ if (Array.isArray(keyOrKeys)) {
1614
+ if (!equal) {
1615
+ equal = (a, b) => is.equ(a, b, { objects: 'shallow', arrays: 'shallow' });
1655
1616
  }
1617
+ return computed(() => {
1618
+ const obj = objFn();
1619
+ const result = {};
1620
+ for (const key of keyOrKeys) {
1621
+ if (!(key in obj))
1622
+ continue;
1623
+ Object.assign(result, { [key]: obj[key] });
1624
+ }
1625
+ return result;
1626
+ }, { equal });
1656
1627
  }
1657
- function handlePointer(name) {
1658
- // Deal with cancelation
1659
- switch (name) {
1660
- case 'pointerleave':
1661
- case 'pointercancel':
1662
- return () => cancelPointer([]);
1663
- case 'lostpointercapture':
1664
- return (event) => {
1665
- const { internal } = store.snapshot;
1666
- if ('pointerId' in event && internal.capturedMap.has(event.pointerId)) {
1667
- // If the object event interface had lostpointercapture, we'd call it here on every
1668
- // object that's getting removed. We call it on the next frame because lostpointercapture
1669
- // fires before pointerup. Otherwise pointerUp would never be called if the event didn't
1670
- // happen in the object it originated from, leaving components in a in-between state.
1671
- requestAnimationFrame(() => {
1672
- // Only release if pointer-up didn't do it already
1673
- if (internal.capturedMap.has(event.pointerId)) {
1674
- internal.capturedMap.delete(event.pointerId);
1675
- cancelPointer([]);
1676
- }
1677
- });
1678
- }
1679
- };
1628
+ return computed(() => objFn()[keyOrKeys], { equal });
1629
+ }
1630
+ function merge(objFn, toMerge, mode = 'override', equal = (a, b) => is.equ(a, b, { objects: 'shallow', arrays: 'shallow' })) {
1631
+ if (mode === 'override')
1632
+ return computed(() => ({ ...objFn(), ...toMerge }), { equal });
1633
+ return computed(() => ({ ...toMerge, ...objFn() }), { equal });
1634
+ }
1635
+ function createVectorComputed(vectorCtor) {
1636
+ return ((inputOrOptions, keyOrKeepUndefined, keepUndefined) => {
1637
+ if (typeof keyOrKeepUndefined === 'undefined' || typeof keyOrKeepUndefined === 'boolean') {
1638
+ keepUndefined = !!keyOrKeepUndefined;
1639
+ const input = inputOrOptions;
1640
+ return computed(() => {
1641
+ const value = input();
1642
+ if (keepUndefined && value == undefined)
1643
+ return undefined;
1644
+ if (typeof value === 'number')
1645
+ return new vectorCtor().setScalar(value);
1646
+ else if (Array.isArray(value))
1647
+ return new vectorCtor(...value);
1648
+ else if (value)
1649
+ return value;
1650
+ else
1651
+ return new vectorCtor();
1652
+ }, { equal: (a, b) => !!a && !!b && a.equals(b) });
1653
+ }
1654
+ const options = inputOrOptions;
1655
+ const key = keyOrKeepUndefined;
1656
+ return computed(() => {
1657
+ const value = options()[key];
1658
+ if (keepUndefined && value == undefined)
1659
+ return undefined;
1660
+ if (typeof value === 'number')
1661
+ return new vectorCtor().setScalar(value);
1662
+ else if (Array.isArray(value))
1663
+ return new vectorCtor(...value);
1664
+ else if (value)
1665
+ return value;
1666
+ else
1667
+ return new vectorCtor();
1668
+ }, { equal: (a, b) => !!a && !!b && a.equals(b) });
1669
+ });
1670
+ }
1671
+ const vector2 = createVectorComputed(THREE.Vector2);
1672
+ const vector3 = createVectorComputed(THREE.Vector3);
1673
+ const vector4 = createVectorComputed(THREE.Vector4);
1674
+
1675
+ class NgtPortalAutoRender {
1676
+ constructor() {
1677
+ this.portalStore = injectStore({ host: true });
1678
+ this.parentStore = injectStore({ skipSelf: true });
1679
+ this.portal = inject(NgtPortalImpl, { host: true });
1680
+ this.renderPriority = input(1, { alias: 'autoRender', transform: (value) => numberAttribute(value, 1) });
1681
+ effect(() => {
1682
+ // this.portalStore.update((state) => ({ events: { ...state.events, priority: this.renderPriority() + 1 } }));
1683
+ });
1684
+ effect((onCleanup) => {
1685
+ const portalRendered = this.portal.portalRendered();
1686
+ if (!portalRendered)
1687
+ return;
1688
+ // track state
1689
+ const [renderPriority, { internal }] = [this.renderPriority(), this.portalStore()];
1690
+ let oldClean;
1691
+ const cleanup = internal.subscribe(({ gl, scene, camera }) => {
1692
+ const [parentScene, parentCamera] = [
1693
+ this.parentStore.snapshot.scene,
1694
+ this.parentStore.snapshot.camera,
1695
+ ];
1696
+ oldClean = gl.autoClear;
1697
+ if (renderPriority === 1) {
1698
+ // clear scene and render with default
1699
+ gl.autoClear = true;
1700
+ gl.render(parentScene, parentCamera);
1701
+ }
1702
+ // disable cleaning
1703
+ gl.autoClear = false;
1704
+ gl.clearDepth();
1705
+ gl.render(scene, camera);
1706
+ // restore
1707
+ gl.autoClear = oldClean;
1708
+ }, renderPriority, this.portalStore);
1709
+ onCleanup(() => cleanup());
1710
+ });
1711
+ }
1712
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalAutoRender, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1713
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.1", type: NgtPortalAutoRender, isStandalone: true, selector: "ngt-portal[autoRender]", inputs: { renderPriority: { classPropertyName: "renderPriority", publicName: "autoRender", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
1714
+ }
1715
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalAutoRender, decorators: [{
1716
+ type: Directive,
1717
+ args: [{ selector: 'ngt-portal[autoRender]' }]
1718
+ }], ctorParameters: () => [] });
1719
+ class NgtPortalContent {
1720
+ static ngTemplateContextGuard(_, ctx) {
1721
+ return true;
1722
+ }
1723
+ constructor() {
1724
+ const host = inject(ElementRef, { skipSelf: true });
1725
+ const { element } = inject(ViewContainerRef);
1726
+ const injector = inject(Injector);
1727
+ const commentNode = element.nativeElement;
1728
+ const store = injectStore();
1729
+ commentNode.data = NGT_PORTAL_CONTENT_FLAG;
1730
+ commentNode[NGT_PORTAL_CONTENT_FLAG] = store;
1731
+ commentNode[NGT_DOM_PARENT_FLAG] = host.nativeElement;
1732
+ if (commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]) {
1733
+ commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG]('portal', injector);
1734
+ delete commentNode[NGT_INTERNAL_ADD_COMMENT_FLAG];
1680
1735
  }
1681
- // Any other pointer goes here ...
1682
- return function handleEvent(event) {
1683
- // NOTE: __pointerMissed$ on NgtStore is private subject since we only expose the Observable
1684
- const pointerMissed$ = store['__pointerMissed$'];
1685
- const internal = store.snapshot.internal;
1686
- // prepareRay(event)
1687
- internal.lastEvent.nativeElement = event;
1688
- // Get fresh intersects
1689
- const isPointerMove = name === 'pointermove';
1690
- const isClickEvent = name === 'click' || name === 'contextmenu' || name === 'dblclick';
1691
- const filter = isPointerMove ? filterPointerEvents : undefined;
1692
- const hits = intersect(event, filter);
1693
- const delta = isClickEvent ? calculateDistance(event) : 0;
1694
- // Save initial coordinates on pointer-down
1695
- if (name === 'pointerdown') {
1696
- internal.initialClick = [event.offsetX, event.offsetY];
1697
- internal.initialHits = hits.map((hit) => hit.eventObject);
1736
+ }
1737
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalContent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1738
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.1", type: NgtPortalContent, isStandalone: true, selector: "ng-template[portalContent]", ngImport: i0 }); }
1739
+ }
1740
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalContent, decorators: [{
1741
+ type: Directive,
1742
+ args: [{ selector: 'ng-template[portalContent]' }]
1743
+ }], ctorParameters: () => [] });
1744
+ function mergeState(previousRoot, store, container, pointer, raycaster, events, size) {
1745
+ // we never want to spread the id
1746
+ const { id: _, ...previousState } = previousRoot.snapshot;
1747
+ const state = store.snapshot;
1748
+ let viewport = undefined;
1749
+ if (state.camera && size) {
1750
+ const camera = state.camera;
1751
+ // calculate the override viewport, if present
1752
+ viewport = previousState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
1753
+ // update the portal camera, if it differs from the previous layer
1754
+ if (camera !== previousState.camera)
1755
+ updateCamera(camera, size);
1756
+ }
1757
+ return {
1758
+ // the intersect consists of the previous root state
1759
+ ...previousState,
1760
+ ...state,
1761
+ // portals have their own scene, which forms the root, a raycaster and a pointer
1762
+ scene: container,
1763
+ pointer,
1764
+ raycaster,
1765
+ // their previous root is the layer before it
1766
+ previousRoot,
1767
+ events: { ...previousState.events, ...state.events, ...events },
1768
+ size: { ...previousState.size, ...size },
1769
+ viewport: { ...previousState.viewport, ...viewport },
1770
+ // layers are allowed to override events
1771
+ setEvents: (events) => store.update((state) => ({ ...state, events: { ...state.events, ...events } })),
1772
+ };
1773
+ }
1774
+ class NgtPortalImpl {
1775
+ constructor() {
1776
+ this.container = input.required();
1777
+ this.state = input({});
1778
+ this.contentRef = contentChild.required(NgtPortalContent, { read: TemplateRef });
1779
+ this.anchorRef = contentChild.required(NgtPortalContent, { read: ViewContainerRef });
1780
+ this.previousStore = injectStore({ skipSelf: true });
1781
+ this.portalStore = injectStore();
1782
+ this.injector = inject(Injector);
1783
+ this.size = pick(this.state, 'size');
1784
+ this.events = pick(this.state, 'events');
1785
+ this.restState = omit(this.state, ['size', 'events']);
1786
+ this.portalContentRendered = signal(false);
1787
+ this.portalRendered = this.portalContentRendered.asReadonly();
1788
+ extend({ Group });
1789
+ effect(() => {
1790
+ let [container, anchor, content] = [
1791
+ this.container(),
1792
+ this.anchorRef(),
1793
+ this.contentRef(),
1794
+ this.previousStore(),
1795
+ ];
1796
+ const [size, events, restState] = [untracked(this.size), untracked(this.events), untracked(this.restState)];
1797
+ if (!is.instance(container)) {
1798
+ container = prepare(container, 'ngt-portal', { store: this.portalStore });
1698
1799
  }
1699
- // If a click yields no results, pass it back to the user as a miss
1700
- // Missed events have to come first in order to establish user-land side-effect clean up
1701
- if (isClickEvent && !hits.length) {
1702
- if (delta <= 2) {
1703
- pointerMissed(event, internal.interaction);
1704
- pointerMissed$.next(event);
1705
- }
1800
+ const instanceState = getInstanceState(container);
1801
+ if (instanceState && instanceState.store !== this.portalStore) {
1802
+ instanceState.store = this.portalStore;
1706
1803
  }
1707
- // Take care of unhover
1708
- if (isPointerMove)
1709
- cancelPointer(hits);
1710
- function onIntersect(data) {
1711
- const eventObject = data.eventObject;
1712
- const instance = getInstanceState(eventObject);
1713
- const handlers = instance?.handlers;
1714
- // Check presence of handlers
1715
- if (!instance?.eventCount)
1716
- return;
1717
- /*
1718
- MAYBE TODO, DELETE IF NOT:
1719
- Check if the object is captured, captured events should not have intersects running in parallel
1720
- But wouldn't it be better to just replace capturedMap with a single entry?
1721
- Also, are we OK with straight up making picking up multiple objects impossible?
1722
-
1723
- const pointerId = (data as ThreeEvent<PointerEvent>).pointerId
1724
- if (pointerId !== undefined) {
1725
- const capturedMeshSet = internal.capturedMap.get(pointerId)
1726
- if (capturedMeshSet) {
1727
- const captured = capturedMeshSet.get(eventObject)
1728
- if (captured && captured.localState.stopped) return
1729
- }
1730
- }*/
1731
- if (isPointerMove) {
1732
- // Move event ...
1733
- if (handlers?.pointerover ||
1734
- handlers?.pointerenter ||
1735
- handlers?.pointerout ||
1736
- handlers?.pointerleave) {
1737
- // When enter or out is present take care of hover-state
1738
- const id = makeId(data);
1739
- const hoveredItem = internal.hovered.get(id);
1740
- if (!hoveredItem) {
1741
- // If the object wasn't previously hovered, book it and call its handler
1742
- internal.hovered.set(id, data);
1743
- handlers.pointerover?.(data);
1744
- handlers.pointerenter?.(data);
1745
- }
1746
- else if (hoveredItem.stopped) {
1747
- // If the object was previously hovered and stopped, we shouldn't allow other items to proceed
1748
- data.stopPropagation();
1749
- }
1750
- }
1751
- // Call mouse move
1752
- handlers?.pointermove?.(data);
1753
- }
1754
- else {
1755
- // All other events ...
1756
- const handler = handlers?.[name];
1757
- if (handler) {
1758
- // Forward all events back to their respective handlers with the exception of click events,
1759
- // which must use the initial target
1760
- if (!isClickEvent || internal.initialHits.includes(eventObject)) {
1761
- // Missed events have to come first
1762
- pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1763
- // Now call the handler
1764
- handler(data);
1765
- }
1766
- }
1767
- else {
1768
- // Trigger onPointerMissed on all elements that have pointer over/out handlers, but not click and weren't hit
1769
- if (isClickEvent && internal.initialHits.includes(eventObject)) {
1770
- pointerMissed(event, internal.interaction.filter((object) => !internal.initialHits.includes(object)));
1771
- }
1772
- }
1773
- }
1804
+ this.portalStore.update(restState, mergeState(this.previousStore, this.portalStore, container, this.portalStore.snapshot.pointer, this.portalStore.snapshot.raycaster, events, size));
1805
+ if (this.portalViewRef) {
1806
+ this.portalViewRef.detectChanges();
1807
+ return;
1774
1808
  }
1775
- handleIntersects(hits, event, delta, onIntersect);
1776
- };
1809
+ this.portalViewRef = anchor.createEmbeddedView(content, { injector: this.injector }, { injector: this.injector });
1810
+ this.portalViewRef.detectChanges();
1811
+ this.portalContentRendered.set(true);
1812
+ });
1777
1813
  }
1778
- return { handlePointer };
1814
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalImpl, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1815
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.1", 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: [
1816
+ {
1817
+ provide: NGT_STORE,
1818
+ useFactory: (previousStore) => {
1819
+ const pointer = new THREE.Vector2();
1820
+ const raycaster = new THREE.Raycaster();
1821
+ const { id: _skipId, ...previousState } = previousStore.snapshot;
1822
+ const store = signalState({
1823
+ id: makeId(),
1824
+ ...previousState,
1825
+ scene: null,
1826
+ previousRoot: previousStore,
1827
+ pointer,
1828
+ raycaster,
1829
+ });
1830
+ store.update(mergeState(previousStore, store, null, pointer, raycaster));
1831
+ return store;
1832
+ },
1833
+ deps: [[new SkipSelf(), NGT_STORE]],
1834
+ },
1835
+ ], queries: [{ propertyName: "contentRef", first: true, predicate: NgtPortalContent, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "anchorRef", first: true, predicate: NgtPortalContent, descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: `
1836
+ @if (portalRendered()) {
1837
+ <!-- Without an element that receives pointer events state.pointer will always be 0/0 -->
1838
+ <ngt-group (pointerover)="(undefined)" attach="none" />
1839
+ }
1840
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1779
1841
  }
1842
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: NgtPortalImpl, decorators: [{
1843
+ type: Component,
1844
+ args: [{
1845
+ selector: 'ngt-portal',
1846
+ template: `
1847
+ @if (portalRendered()) {
1848
+ <!-- Without an element that receives pointer events state.pointer will always be 0/0 -->
1849
+ <ngt-group (pointerover)="(undefined)" attach="none" />
1850
+ }
1851
+ `,
1852
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
1853
+ changeDetection: ChangeDetectionStrategy.OnPush,
1854
+ providers: [
1855
+ {
1856
+ provide: NGT_STORE,
1857
+ useFactory: (previousStore) => {
1858
+ const pointer = new THREE.Vector2();
1859
+ const raycaster = new THREE.Raycaster();
1860
+ const { id: _skipId, ...previousState } = previousStore.snapshot;
1861
+ const store = signalState({
1862
+ id: makeId(),
1863
+ ...previousState,
1864
+ scene: null,
1865
+ previousRoot: previousStore,
1866
+ pointer,
1867
+ raycaster,
1868
+ });
1869
+ store.update(mergeState(previousStore, store, null, pointer, raycaster));
1870
+ return store;
1871
+ },
1872
+ deps: [[new SkipSelf(), NGT_STORE]],
1873
+ },
1874
+ ],
1875
+ }]
1876
+ }], ctorParameters: () => [] });
1877
+ const NgtPortal = [NgtPortalImpl, NgtPortalContent];
1780
1878
 
1781
1879
  // This function prepares a set of changes to be applied to the instance
1782
1880
  function diffProps(instance, props) {
@@ -2775,8 +2873,8 @@ class NgtRenderer2 {
2775
2873
  }
2776
2874
 
2777
2875
  const shallowLoose = { objects: 'shallow', strict: false };
2778
- function injectCanvasRootInitializer(injector) {
2779
- return assertInjector(injectCanvasRootInitializer, injector, () => {
2876
+ function canvasRootInitializer(injector) {
2877
+ return assertInjector(canvasRootInitializer, injector, () => {
2780
2878
  const injectedStore = injectStore();
2781
2879
  const loop = injectLoop();
2782
2880
  return (canvas) => {
@@ -3080,9 +3178,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImpor
3080
3178
  * )
3081
3179
  * ```
3082
3180
  */
3083
- function injectBeforeRender(cb, { priority = 0, injector } = {}) {
3181
+ function beforeRender(cb, { priority = 0, injector } = {}) {
3084
3182
  if (typeof priority === 'function') {
3085
- const effectRef = assertInjector(injectBeforeRender, injector, () => {
3183
+ const effectRef = assertInjector(beforeRender, injector, () => {
3086
3184
  const store = injectStore();
3087
3185
  const ref = effect((onCleanup) => {
3088
3186
  const p = priority();
@@ -3094,13 +3192,18 @@ function injectBeforeRender(cb, { priority = 0, injector } = {}) {
3094
3192
  });
3095
3193
  return effectRef.destroy.bind(effectRef);
3096
3194
  }
3097
- return assertInjector(injectBeforeRender, injector, () => {
3195
+ return assertInjector(beforeRender, injector, () => {
3098
3196
  const store = injectStore();
3099
3197
  const sub = store.snapshot.internal.subscribe(cb, priority, store);
3100
3198
  inject(DestroyRef).onDestroy(() => void sub());
3101
3199
  return sub;
3102
3200
  });
3103
3201
  }
3202
+ /**
3203
+ * @deprecated Use `beforeRender` instead. Will be removed in v5.0.0
3204
+ * @since v4.0.0
3205
+ */
3206
+ const injectBeforeRender = beforeRender;
3104
3207
 
3105
3208
  /**
3106
3209
  * As host directive:
@@ -3146,7 +3249,7 @@ class NgtObjectEvents {
3146
3249
  return ngtObject();
3147
3250
  return ngtObject;
3148
3251
  });
3149
- injectObjectEvents(obj, {
3252
+ objectEvents(obj, {
3150
3253
  click: this.emitEvent('click'),
3151
3254
  dblclick: this.emitEvent('dblclick'),
3152
3255
  contextmenu: this.emitEvent('contextmenu'),
@@ -3172,8 +3275,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImpor
3172
3275
  type: Directive,
3173
3276
  args: [{ selector: '[ngtObjectEvents]' }]
3174
3277
  }], ctorParameters: () => [] });
3175
- function injectObjectEvents(target, events, { injector } = {}) {
3176
- return assertInjector(injectObjectEvents, injector, () => {
3278
+ /**
3279
+ * @deprecated use objectEvents instead. Will be removed in v5.0.0
3280
+ * @since v4.0.0
3281
+ */
3282
+ const injectObjectEvents = objectEvents;
3283
+ function objectEvents(target, events, { injector } = {}) {
3284
+ return assertInjector(objectEvents, injector, () => {
3177
3285
  const renderer = inject(Renderer2);
3178
3286
  const cleanUps = [];
3179
3287
  effect((onCleanup) => {
@@ -3207,5 +3315,5 @@ function hasListener(...emitterRefs) {
3207
3315
  * Generated bundle index. Do not edit.
3208
3316
  */
3209
3317
 
3210
- 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, NgtHTML, NgtHexify, NgtObjectEvents, NgtParent, NgtPortal, NgtPortalAutoRender, NgtPortalContent, NgtPortalImpl, NgtRenderer2, NgtRendererFactory2, NgtRoutedScene, NgtSelect, NgtSelection, NgtSelectionApi, THREE_NATIVE_EVENTS, addAfterEffect, addEffect, addTail, applyProps, attach, checkNeedsUpdate, checkUpdate, createAttachFunction, createEvents, detach, dispose, extend, flushGlobalEffects, getEmitter, getInstanceState, getLocalState, hasListener, injectBeforeRender, injectCanvasRootInitializer, injectCatalogue, injectLoader, injectLoop, injectObjectEvents, injectStore, invalidateInstance, is, makeCameraInstance, makeDpr, makeId, makeObjectGraph, makeRendererInstance, merge, omit, pick, prepare, provideHTMLDomElement, removeInteractivity, resolveInstanceKey, resolveRef, roots, signalState, storeFactory, updateCamera, vector2, vector3, vector4 };
3318
+ 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, 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, 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, removeInteractivity, resolveInstanceKey, resolveRef, roots, signalState, storeFactory, updateCamera, vector2, vector3, vector4 };
3211
3319
  //# sourceMappingURL=angular-three.mjs.map