@pascal-app/core 0.1.13 → 0.2.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.
- package/dist/events/bus.d.ts +14 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +5 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +10 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +8 -8
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +88 -36
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +16 -8
- package/dist/hooks/spatial-grid/spatial-grid.d.ts +3 -3
- package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid.js +2 -2
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +2 -2
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/wall-spatial-grid.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +1 -1
- package/dist/schema/collections.d.ts +11 -0
- package/dist/schema/collections.d.ts.map +1 -0
- package/dist/schema/collections.js +2 -0
- package/dist/schema/index.d.ts +11 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +11 -7
- package/dist/schema/nodes/door.d.ts +78 -0
- package/dist/schema/nodes/door.d.ts.map +1 -0
- package/dist/schema/nodes/door.js +67 -0
- package/dist/schema/nodes/item.d.ts +234 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +65 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +11 -1
- package/dist/schema/nodes/roof-segment.d.ts +51 -0
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment.js +36 -0
- package/dist/schema/nodes/roof.d.ts +1 -4
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +9 -16
- package/dist/schema/nodes/site.d.ts +46 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/types.d.ts +191 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +23 -4
- package/dist/store/use-interactive.d.ts +18 -0
- package/dist/store/use-interactive.d.ts.map +1 -0
- package/dist/store/use-interactive.js +50 -0
- package/dist/store/use-scene.d.ts +10 -1
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +180 -57
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +5 -0
- package/dist/systems/door/door-system.d.ts +2 -0
- package/dist/systems/door/door-system.d.ts.map +1 -0
- package/dist/systems/door/door-system.js +211 -0
- package/dist/systems/item/item-system.js +3 -2
- package/dist/systems/roof/roof-system.d.ts +11 -3
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +705 -210
- package/dist/systems/slab/slab-system.js +3 -3
- package/dist/systems/wall/wall-mitering.js +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +6 -6
- package/dist/systems/window/window-system.js +3 -3
- package/package.json +6 -6
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { TemporalState } from 'zundo';
|
|
2
2
|
import { type StoreApi, type UseBoundStore } from 'zustand';
|
|
3
|
+
import type { Collection, CollectionId } from '../schema/collections';
|
|
3
4
|
import type { AnyNode, AnyNodeId } from '../schema/types';
|
|
4
5
|
export type SceneState = {
|
|
5
6
|
nodes: Record<AnyNodeId, AnyNode>;
|
|
6
7
|
rootNodeIds: AnyNodeId[];
|
|
7
8
|
dirtyNodes: Set<AnyNodeId>;
|
|
9
|
+
collections: Record<CollectionId, Collection>;
|
|
8
10
|
loadScene: () => void;
|
|
9
11
|
clearScene: () => void;
|
|
12
|
+
unloadScene: () => void;
|
|
10
13
|
setScene: (nodes: Record<AnyNodeId, AnyNode>, rootNodeIds: AnyNodeId[]) => void;
|
|
11
14
|
markDirty: (id: AnyNodeId) => void;
|
|
12
15
|
clearDirty: (id: AnyNodeId) => void;
|
|
@@ -22,10 +25,16 @@ export type SceneState = {
|
|
|
22
25
|
}[]) => void;
|
|
23
26
|
deleteNode: (id: AnyNodeId) => void;
|
|
24
27
|
deleteNodes: (ids: AnyNodeId[]) => void;
|
|
28
|
+
createCollection: (name: string, nodeIds?: AnyNodeId[]) => CollectionId;
|
|
29
|
+
deleteCollection: (id: CollectionId) => void;
|
|
30
|
+
updateCollection: (id: CollectionId, data: Partial<Omit<Collection, 'id'>>) => void;
|
|
31
|
+
addToCollection: (id: CollectionId, nodeId: AnyNodeId) => void;
|
|
32
|
+
removeFromCollection: (id: CollectionId, nodeId: AnyNodeId) => void;
|
|
25
33
|
};
|
|
26
34
|
type UseSceneStore = UseBoundStore<StoreApi<SceneState>> & {
|
|
27
|
-
temporal: StoreApi<TemporalState<Pick<SceneState, 'nodes' | 'rootNodeIds'>>>;
|
|
35
|
+
temporal: StoreApi<TemporalState<Pick<SceneState, 'nodes' | 'rootNodeIds' | 'collections'>>>;
|
|
28
36
|
};
|
|
29
37
|
declare const useScene: UseSceneStore;
|
|
30
38
|
export default useScene;
|
|
39
|
+
export declare function clearSceneHistory(): void;
|
|
31
40
|
//# sourceMappingURL=use-scene.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAIrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AA8CzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAErE,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aAqMf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB,wBAAgB,iBAAiB,SAKhC"}
|
package/dist/store/use-scene.js
CHANGED
|
@@ -1,35 +1,82 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { temporal } from 'zundo';
|
|
3
3
|
import { create } from 'zustand';
|
|
4
|
-
import { persist } from 'zustand/middleware';
|
|
5
4
|
import { BuildingNode } from '../schema';
|
|
5
|
+
import { generateCollectionId } from '../schema/collections';
|
|
6
6
|
import { LevelNode } from '../schema/nodes/level';
|
|
7
7
|
import { SiteNode } from '../schema/nodes/site';
|
|
8
|
-
import { isObject } from '../utils/types';
|
|
9
8
|
import * as nodeActions from './actions/node-actions';
|
|
10
|
-
|
|
9
|
+
function migrateNodes(nodes) {
|
|
10
|
+
const patchedNodes = { ...nodes };
|
|
11
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
12
|
+
// 1. Item scale migration
|
|
13
|
+
if (node.type === 'item' && !('scale' in node)) {
|
|
14
|
+
patchedNodes[id] = { ...node, scale: [1, 1, 1] };
|
|
15
|
+
}
|
|
16
|
+
// 2. Old roof to new roof + segment migration
|
|
17
|
+
if (node.type === 'roof' && !('children' in node)) {
|
|
18
|
+
const oldRoof = node;
|
|
19
|
+
const suffix = id.includes('_') ? id.split('_')[1] : Math.random().toString(36).slice(2);
|
|
20
|
+
const segmentId = `rseg_${suffix}`;
|
|
21
|
+
const segment = {
|
|
22
|
+
object: 'node',
|
|
23
|
+
id: segmentId,
|
|
24
|
+
type: 'roof-segment',
|
|
25
|
+
parentId: id,
|
|
26
|
+
visible: oldRoof.visible ?? true,
|
|
27
|
+
metadata: {},
|
|
28
|
+
position: [0, 0, 0],
|
|
29
|
+
rotation: 0,
|
|
30
|
+
roofType: 'gable',
|
|
31
|
+
width: oldRoof.length ?? 8,
|
|
32
|
+
depth: (oldRoof.leftWidth ?? 2.2) + (oldRoof.rightWidth ?? 2.2),
|
|
33
|
+
wallHeight: 0,
|
|
34
|
+
roofHeight: oldRoof.height ?? 2.5,
|
|
35
|
+
wallThickness: 0.1,
|
|
36
|
+
deckThickness: 0.1,
|
|
37
|
+
overhang: 0.3,
|
|
38
|
+
shingleThickness: 0.05,
|
|
39
|
+
};
|
|
40
|
+
patchedNodes[segmentId] = segment;
|
|
41
|
+
patchedNodes[id] = {
|
|
42
|
+
...oldRoof,
|
|
43
|
+
children: [segmentId],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return patchedNodes;
|
|
48
|
+
}
|
|
49
|
+
const useScene = create()(temporal((set, get) => ({
|
|
11
50
|
// 1. Flat dictionary of all nodes
|
|
12
51
|
nodes: {},
|
|
13
52
|
// 2. Root node IDs
|
|
14
53
|
rootNodeIds: [],
|
|
15
54
|
// 3. Dirty set
|
|
16
55
|
dirtyNodes: new Set(),
|
|
17
|
-
|
|
56
|
+
// 4. Collections
|
|
57
|
+
collections: {},
|
|
58
|
+
unloadScene: () => {
|
|
18
59
|
set({
|
|
19
60
|
nodes: {},
|
|
20
61
|
rootNodeIds: [],
|
|
21
62
|
dirtyNodes: new Set(),
|
|
63
|
+
collections: {},
|
|
22
64
|
});
|
|
65
|
+
},
|
|
66
|
+
clearScene: () => {
|
|
67
|
+
get().unloadScene();
|
|
23
68
|
get().loadScene(); // Default scene
|
|
24
69
|
},
|
|
25
70
|
setScene: (nodes, rootNodeIds) => {
|
|
71
|
+
// Apply backward compatibility migrations
|
|
72
|
+
const patchedNodes = migrateNodes(nodes);
|
|
26
73
|
set({
|
|
27
|
-
nodes,
|
|
74
|
+
nodes: patchedNodes,
|
|
28
75
|
rootNodeIds,
|
|
29
76
|
dirtyNodes: new Set(),
|
|
30
77
|
});
|
|
31
78
|
// Mark all nodes as dirty to trigger re-validation
|
|
32
|
-
Object.values(
|
|
79
|
+
Object.values(patchedNodes).forEach((node) => {
|
|
33
80
|
get().markDirty(node.id);
|
|
34
81
|
});
|
|
35
82
|
},
|
|
@@ -75,60 +122,112 @@ const useScene = create()(persist(temporal((set, get) => ({
|
|
|
75
122
|
// --- DELETE ---
|
|
76
123
|
deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids),
|
|
77
124
|
deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]),
|
|
125
|
+
// --- COLLECTIONS ---
|
|
126
|
+
createCollection: (name, nodeIds = []) => {
|
|
127
|
+
const id = generateCollectionId();
|
|
128
|
+
const collection = { id, name, nodeIds };
|
|
129
|
+
set((state) => {
|
|
130
|
+
const nextCollections = { ...state.collections, [id]: collection };
|
|
131
|
+
// Denormalize: stamp collectionId onto each node
|
|
132
|
+
const nextNodes = { ...state.nodes };
|
|
133
|
+
for (const nodeId of nodeIds) {
|
|
134
|
+
const node = nextNodes[nodeId];
|
|
135
|
+
if (!node)
|
|
136
|
+
continue;
|
|
137
|
+
const existing = ('collectionIds' in node ? node.collectionIds : undefined) ?? [];
|
|
138
|
+
nextNodes[nodeId] = { ...node, collectionIds: [...existing, id] };
|
|
139
|
+
}
|
|
140
|
+
return { collections: nextCollections, nodes: nextNodes };
|
|
141
|
+
});
|
|
142
|
+
return id;
|
|
143
|
+
},
|
|
144
|
+
deleteCollection: (id) => {
|
|
145
|
+
set((state) => {
|
|
146
|
+
const col = state.collections[id];
|
|
147
|
+
const nextCollections = { ...state.collections };
|
|
148
|
+
delete nextCollections[id];
|
|
149
|
+
// Remove collectionId from all member nodes
|
|
150
|
+
const nextNodes = { ...state.nodes };
|
|
151
|
+
for (const nodeId of col?.nodeIds ?? []) {
|
|
152
|
+
const node = nextNodes[nodeId];
|
|
153
|
+
if (!(node && 'collectionIds' in node))
|
|
154
|
+
continue;
|
|
155
|
+
nextNodes[nodeId] = {
|
|
156
|
+
...node,
|
|
157
|
+
collectionIds: node.collectionIds.filter((cid) => cid !== id),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { collections: nextCollections, nodes: nextNodes };
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
updateCollection: (id, data) => {
|
|
164
|
+
set((state) => {
|
|
165
|
+
const col = state.collections[id];
|
|
166
|
+
if (!col)
|
|
167
|
+
return state;
|
|
168
|
+
return { collections: { ...state.collections, [id]: { ...col, ...data } } };
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
addToCollection: (id, nodeId) => {
|
|
172
|
+
set((state) => {
|
|
173
|
+
const col = state.collections[id];
|
|
174
|
+
if (!col || col.nodeIds.includes(nodeId))
|
|
175
|
+
return state;
|
|
176
|
+
const nextCollections = {
|
|
177
|
+
...state.collections,
|
|
178
|
+
[id]: { ...col, nodeIds: [...col.nodeIds, nodeId] },
|
|
179
|
+
};
|
|
180
|
+
const node = state.nodes[nodeId];
|
|
181
|
+
if (!node)
|
|
182
|
+
return { collections: nextCollections };
|
|
183
|
+
const existing = ('collectionIds' in node ? node.collectionIds : undefined) ?? [];
|
|
184
|
+
const nextNodes = {
|
|
185
|
+
...state.nodes,
|
|
186
|
+
[nodeId]: { ...node, collectionIds: [...existing, id] },
|
|
187
|
+
};
|
|
188
|
+
return { collections: nextCollections, nodes: nextNodes };
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
removeFromCollection: (id, nodeId) => {
|
|
192
|
+
set((state) => {
|
|
193
|
+
const col = state.collections[id];
|
|
194
|
+
if (!col)
|
|
195
|
+
return state;
|
|
196
|
+
const nextCollections = {
|
|
197
|
+
...state.collections,
|
|
198
|
+
[id]: { ...col, nodeIds: col.nodeIds.filter((n) => n !== nodeId) },
|
|
199
|
+
};
|
|
200
|
+
const node = state.nodes[nodeId];
|
|
201
|
+
if (!(node && 'collectionIds' in node))
|
|
202
|
+
return { collections: nextCollections };
|
|
203
|
+
const nextNodes = {
|
|
204
|
+
...state.nodes,
|
|
205
|
+
[nodeId]: {
|
|
206
|
+
...node,
|
|
207
|
+
collectionIds: node.collectionIds.filter((cid) => cid !== id),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
return { collections: nextCollections, nodes: nextNodes };
|
|
211
|
+
});
|
|
212
|
+
},
|
|
78
213
|
}), {
|
|
79
214
|
partialize: (state) => {
|
|
80
|
-
const { nodes, rootNodeIds } = state;
|
|
81
|
-
return { nodes, rootNodeIds };
|
|
215
|
+
const { nodes, rootNodeIds, collections } = state;
|
|
216
|
+
return { nodes, rootNodeIds, collections };
|
|
82
217
|
},
|
|
83
218
|
limit: 50, // Limit to last 50 actions
|
|
84
|
-
}), {
|
|
85
|
-
name: 'editor-storage',
|
|
86
|
-
partialize: (state) => ({
|
|
87
|
-
nodes: Object.fromEntries(Object.entries(state.nodes).filter(([_, node]) => {
|
|
88
|
-
const meta = node.metadata;
|
|
89
|
-
const isTransient = isObject(meta) && 'isTransient' in meta && meta.isTransient === true;
|
|
90
|
-
return !isTransient;
|
|
91
|
-
})),
|
|
92
|
-
rootNodeIds: state.rootNodeIds,
|
|
93
|
-
}),
|
|
94
|
-
onRehydrateStorage: (state) => {
|
|
95
|
-
console.log('hydrating...');
|
|
96
|
-
return (state, error) => {
|
|
97
|
-
if (error) {
|
|
98
|
-
console.log('an error happened during hydration', error);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (!state) {
|
|
102
|
-
console.log('hydration finished - no state');
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Migration: Wrap old scenes (where root is not a SiteNode) in a SiteNode
|
|
106
|
-
const rootId = state.rootNodeIds?.[0];
|
|
107
|
-
const rootNode = rootId ? state.nodes[rootId] : null;
|
|
108
|
-
if (rootNode && rootNode.type !== 'site') {
|
|
109
|
-
console.log('Migrating old scene: wrapping in SiteNode');
|
|
110
|
-
// Collect existing root nodes (should be BuildingNode or ItemNode)
|
|
111
|
-
const existingRoots = (state.rootNodeIds || [])
|
|
112
|
-
.map(id => state.nodes[id])
|
|
113
|
-
.filter(node => node?.type === 'building' || node?.type === 'item');
|
|
114
|
-
// Create a new SiteNode with existing roots as children
|
|
115
|
-
const site = SiteNode.parse({
|
|
116
|
-
children: existingRoots,
|
|
117
|
-
});
|
|
118
|
-
// Add site to nodes
|
|
119
|
-
state.nodes[site.id] = site;
|
|
120
|
-
// Update root to be the site
|
|
121
|
-
state.rootNodeIds = [site.id];
|
|
122
|
-
console.log('Migration complete: scene now has SiteNode as root');
|
|
123
|
-
}
|
|
124
|
-
console.log('hydration finished');
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
219
|
}));
|
|
128
220
|
export default useScene;
|
|
129
|
-
// Track previous temporal state lengths
|
|
221
|
+
// Track previous temporal state lengths and node snapshot for diffing
|
|
130
222
|
let prevPastLength = 0;
|
|
131
223
|
let prevFutureLength = 0;
|
|
224
|
+
let prevNodesSnapshot = null;
|
|
225
|
+
export function clearSceneHistory() {
|
|
226
|
+
useScene.temporal.getState().clear();
|
|
227
|
+
prevPastLength = 0;
|
|
228
|
+
prevFutureLength = 0;
|
|
229
|
+
prevNodesSnapshot = null;
|
|
230
|
+
}
|
|
132
231
|
// Subscribe to the temporal store (Undo/Redo events)
|
|
133
232
|
useScene.temporal.subscribe((state) => {
|
|
134
233
|
const currentPastLength = state.pastStates.length;
|
|
@@ -138,16 +237,40 @@ useScene.temporal.subscribe((state) => {
|
|
|
138
237
|
const didUndo = currentFutureLength > prevFutureLength;
|
|
139
238
|
const didRedo = currentPastLength > prevPastLength && currentFutureLength < prevFutureLength;
|
|
140
239
|
if (didUndo || didRedo) {
|
|
240
|
+
// Capture the previous snapshot before RAF fires
|
|
241
|
+
const snapshotBefore = prevNodesSnapshot;
|
|
141
242
|
// Use RAF to ensure all middleware and store updates are complete
|
|
142
243
|
requestAnimationFrame(() => {
|
|
143
244
|
const currentNodes = useScene.getState().nodes;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
245
|
+
const { markDirty } = useScene.getState();
|
|
246
|
+
if (snapshotBefore) {
|
|
247
|
+
// Diff: only mark nodes that actually changed
|
|
248
|
+
for (const [id, node] of Object.entries(currentNodes)) {
|
|
249
|
+
if (snapshotBefore[id] !== node) {
|
|
250
|
+
markDirty(id);
|
|
251
|
+
// Also mark parent so merged geometries update
|
|
252
|
+
if (node.parentId)
|
|
253
|
+
markDirty(node.parentId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Nodes that were deleted (exist in prev but not current)
|
|
257
|
+
for (const [id, node] of Object.entries(snapshotBefore)) {
|
|
258
|
+
if (!currentNodes[id]) {
|
|
259
|
+
if (node.parentId)
|
|
260
|
+
markDirty(node.parentId);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// No snapshot to diff against — fall back to marking all
|
|
266
|
+
for (const node of Object.values(currentNodes)) {
|
|
267
|
+
markDirty(node.id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
148
270
|
});
|
|
149
271
|
}
|
|
150
|
-
// Update tracked lengths
|
|
272
|
+
// Update tracked lengths and snapshot
|
|
151
273
|
prevPastLength = currentPastLength;
|
|
152
274
|
prevFutureLength = currentFutureLength;
|
|
275
|
+
prevNodesSnapshot = useScene.getState().nodes;
|
|
153
276
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAA;AAO1D,eAAO,MAAM,aAAa,YAuBzB,CAAA;
|
|
1
|
+
{"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAA;AAO1D,eAAO,MAAM,aAAa,YAuBzB,CAAA;AAqBD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,GAAG,KAAK,CAAC,cAAc,CA+CtF"}
|
|
@@ -34,6 +34,11 @@ function updateCeilingGeometry(node, mesh) {
|
|
|
34
34
|
const newGeo = generateCeilingGeometry(node);
|
|
35
35
|
mesh.geometry.dispose();
|
|
36
36
|
mesh.geometry = newGeo;
|
|
37
|
+
const gridMesh = mesh.getObjectByName('ceiling-grid');
|
|
38
|
+
if (gridMesh) {
|
|
39
|
+
gridMesh.geometry.dispose();
|
|
40
|
+
gridMesh.geometry = newGeo;
|
|
41
|
+
}
|
|
37
42
|
// Position at the ceiling height
|
|
38
43
|
mesh.position.y = (node.height ?? 2.5) - 0.01; // Slight offset to avoid z-fighting with upper-level slabs
|
|
39
44
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,UAAU,YA2BtB,CAAA"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useFrame } from '@react-three/fiber';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu';
|
|
4
|
+
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
5
|
+
import useScene from '../../store/use-scene';
|
|
6
|
+
const baseMaterial = new MeshStandardNodeMaterial({
|
|
7
|
+
name: 'door-base',
|
|
8
|
+
color: '#f2f0ed',
|
|
9
|
+
roughness: 0.5,
|
|
10
|
+
metalness: 0,
|
|
11
|
+
});
|
|
12
|
+
const glassMaterial = new MeshStandardNodeMaterial({
|
|
13
|
+
name: 'door-glass',
|
|
14
|
+
color: 'lightblue',
|
|
15
|
+
roughness: 0.05,
|
|
16
|
+
metalness: 0.1,
|
|
17
|
+
transparent: true,
|
|
18
|
+
opacity: 0.35,
|
|
19
|
+
side: DoubleSide,
|
|
20
|
+
depthWrite: false,
|
|
21
|
+
});
|
|
22
|
+
// Invisible material for root mesh — used as selection hitbox only
|
|
23
|
+
const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
|
|
24
|
+
export const DoorSystem = () => {
|
|
25
|
+
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
26
|
+
const clearDirty = useScene((state) => state.clearDirty);
|
|
27
|
+
useFrame(() => {
|
|
28
|
+
if (dirtyNodes.size === 0)
|
|
29
|
+
return;
|
|
30
|
+
const nodes = useScene.getState().nodes;
|
|
31
|
+
dirtyNodes.forEach((id) => {
|
|
32
|
+
const node = nodes[id];
|
|
33
|
+
if (!node || node.type !== 'door')
|
|
34
|
+
return;
|
|
35
|
+
const mesh = sceneRegistry.nodes.get(id);
|
|
36
|
+
if (!mesh)
|
|
37
|
+
return; // Keep dirty until mesh mounts
|
|
38
|
+
updateDoorMesh(node, mesh);
|
|
39
|
+
clearDirty(id);
|
|
40
|
+
// Rebuild the parent wall so its cutout reflects the updated door geometry
|
|
41
|
+
if (node.parentId) {
|
|
42
|
+
useScene.getState().dirtyNodes.add(node.parentId);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}, 3);
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
function addBox(parent, material, w, h, d, x, y, z) {
|
|
49
|
+
const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
|
|
50
|
+
m.position.set(x, y, z);
|
|
51
|
+
parent.add(m);
|
|
52
|
+
}
|
|
53
|
+
function updateDoorMesh(node, mesh) {
|
|
54
|
+
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
|
55
|
+
mesh.geometry.dispose();
|
|
56
|
+
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
|
|
57
|
+
mesh.material = hitboxMaterial;
|
|
58
|
+
// Sync transform from node (React may lag behind the system by a frame during drag)
|
|
59
|
+
mesh.position.set(node.position[0], node.position[1], node.position[2]);
|
|
60
|
+
mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2]);
|
|
61
|
+
// Dispose and remove all old visual children; preserve 'cutout'
|
|
62
|
+
for (const child of [...mesh.children]) {
|
|
63
|
+
if (child.name === 'cutout')
|
|
64
|
+
continue;
|
|
65
|
+
if (child instanceof THREE.Mesh)
|
|
66
|
+
child.geometry.dispose();
|
|
67
|
+
mesh.remove(child);
|
|
68
|
+
}
|
|
69
|
+
const { width, height, frameThickness, frameDepth, threshold, thresholdHeight, segments, handle, handleHeight, handleSide, doorCloser, panicBar, panicBarHeight, contentPadding, hingesSide, } = node;
|
|
70
|
+
// Leaf occupies the full opening (no bottom frame bar — door opens to floor)
|
|
71
|
+
const leafW = width - 2 * frameThickness;
|
|
72
|
+
const leafH = height - frameThickness; // only top frame
|
|
73
|
+
const leafDepth = 0.04;
|
|
74
|
+
// Leaf center is shifted down from door center by half the top frame
|
|
75
|
+
const leafCenterY = -frameThickness / 2;
|
|
76
|
+
// ── Frame members ──
|
|
77
|
+
// Left post — full height
|
|
78
|
+
addBox(mesh, baseMaterial, frameThickness, height, frameDepth, -width / 2 + frameThickness / 2, 0, 0);
|
|
79
|
+
// Right post — full height
|
|
80
|
+
addBox(mesh, baseMaterial, frameThickness, height, frameDepth, width / 2 - frameThickness / 2, 0, 0);
|
|
81
|
+
// Head (top bar) — full width
|
|
82
|
+
addBox(mesh, baseMaterial, width, frameThickness, frameDepth, 0, height / 2 - frameThickness / 2, 0);
|
|
83
|
+
// ── Threshold (inside the frame) ──
|
|
84
|
+
if (threshold) {
|
|
85
|
+
addBox(mesh, baseMaterial, leafW, thresholdHeight, frameDepth, 0, -height / 2 + thresholdHeight / 2, 0);
|
|
86
|
+
}
|
|
87
|
+
// ── Leaf — contentPadding border strips (no full backing; glass areas are open) ──
|
|
88
|
+
const cpX = contentPadding[0];
|
|
89
|
+
const cpY = contentPadding[1];
|
|
90
|
+
if (cpY > 0) {
|
|
91
|
+
// Top strip
|
|
92
|
+
addBox(mesh, baseMaterial, leafW, cpY, leafDepth, 0, leafCenterY + leafH / 2 - cpY / 2, 0);
|
|
93
|
+
// Bottom strip
|
|
94
|
+
addBox(mesh, baseMaterial, leafW, cpY, leafDepth, 0, leafCenterY - leafH / 2 + cpY / 2, 0);
|
|
95
|
+
}
|
|
96
|
+
if (cpX > 0) {
|
|
97
|
+
const innerH = leafH - 2 * cpY;
|
|
98
|
+
// Left strip
|
|
99
|
+
addBox(mesh, baseMaterial, cpX, innerH, leafDepth, -leafW / 2 + cpX / 2, leafCenterY, 0);
|
|
100
|
+
// Right strip
|
|
101
|
+
addBox(mesh, baseMaterial, cpX, innerH, leafDepth, leafW / 2 - cpX / 2, leafCenterY, 0);
|
|
102
|
+
}
|
|
103
|
+
// Content area inside padding
|
|
104
|
+
const contentW = leafW - 2 * cpX;
|
|
105
|
+
const contentH = leafH - 2 * cpY;
|
|
106
|
+
// ── Segments (stacked top to bottom within content area) ──
|
|
107
|
+
const totalRatio = segments.reduce((sum, s) => sum + s.heightRatio, 0);
|
|
108
|
+
const contentTop = leafCenterY + contentH / 2;
|
|
109
|
+
let segY = contentTop;
|
|
110
|
+
for (const seg of segments) {
|
|
111
|
+
const segH = (seg.heightRatio / totalRatio) * contentH;
|
|
112
|
+
const segCenterY = segY - segH / 2;
|
|
113
|
+
const numCols = seg.columnRatios.length;
|
|
114
|
+
const colSum = seg.columnRatios.reduce((a, b) => a + b, 0);
|
|
115
|
+
const usableW = contentW - (numCols - 1) * seg.dividerThickness;
|
|
116
|
+
const colWidths = seg.columnRatios.map((r) => (r / colSum) * usableW);
|
|
117
|
+
// Column x-centers (relative to mesh center)
|
|
118
|
+
const colXCenters = [];
|
|
119
|
+
let cx = -contentW / 2;
|
|
120
|
+
for (let c = 0; c < numCols; c++) {
|
|
121
|
+
colXCenters.push(cx + colWidths[c] / 2);
|
|
122
|
+
cx += colWidths[c];
|
|
123
|
+
if (c < numCols - 1)
|
|
124
|
+
cx += seg.dividerThickness;
|
|
125
|
+
}
|
|
126
|
+
// Column dividers within this segment
|
|
127
|
+
cx = -contentW / 2;
|
|
128
|
+
for (let c = 0; c < numCols - 1; c++) {
|
|
129
|
+
cx += colWidths[c];
|
|
130
|
+
addBox(mesh, baseMaterial, seg.dividerThickness, segH, leafDepth + 0.001, cx + seg.dividerThickness / 2, segCenterY, 0);
|
|
131
|
+
cx += seg.dividerThickness;
|
|
132
|
+
}
|
|
133
|
+
// Segment content per column
|
|
134
|
+
for (let c = 0; c < numCols; c++) {
|
|
135
|
+
const colW = colWidths[c];
|
|
136
|
+
const colX = colXCenters[c];
|
|
137
|
+
if (seg.type === 'glass') {
|
|
138
|
+
// Glass only — no opaque backing so it's truly transparent
|
|
139
|
+
const glassDepth = Math.max(0.004, leafDepth * 0.15);
|
|
140
|
+
addBox(mesh, glassMaterial, colW, segH, glassDepth, colX, segCenterY, 0);
|
|
141
|
+
}
|
|
142
|
+
else if (seg.type === 'panel') {
|
|
143
|
+
// Opaque leaf backing for this column
|
|
144
|
+
addBox(mesh, baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
|
|
145
|
+
// Raised panel detail
|
|
146
|
+
const panelW = colW - 2 * seg.panelInset;
|
|
147
|
+
const panelH = segH - 2 * seg.panelInset;
|
|
148
|
+
if (panelW > 0.01 && panelH > 0.01) {
|
|
149
|
+
const effectiveDepth = Math.abs(seg.panelDepth) < 0.002 ? 0.005 : Math.abs(seg.panelDepth);
|
|
150
|
+
const panelZ = leafDepth / 2 + effectiveDepth / 2;
|
|
151
|
+
addBox(mesh, baseMaterial, panelW, panelH, effectiveDepth, colX, segCenterY, panelZ);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// 'empty' — opaque backing, no detail
|
|
156
|
+
addBox(mesh, baseMaterial, colW, segH, leafDepth, colX, segCenterY, 0);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
segY -= segH;
|
|
160
|
+
}
|
|
161
|
+
// ── Handle ──
|
|
162
|
+
if (handle) {
|
|
163
|
+
// Convert from floor-based height to mesh-center-based Y
|
|
164
|
+
const handleY = handleHeight - height / 2;
|
|
165
|
+
// Handle grip sits on the front face (+Z) of the leaf
|
|
166
|
+
const faceZ = leafDepth / 2;
|
|
167
|
+
// X position: handleSide refers to which side the grip is on
|
|
168
|
+
const handleX = handleSide === 'right' ? leafW / 2 - 0.045 : -leafW / 2 + 0.045;
|
|
169
|
+
// Backplate
|
|
170
|
+
addBox(mesh, baseMaterial, 0.028, 0.14, 0.01, handleX, handleY, faceZ + 0.005);
|
|
171
|
+
// Grip lever
|
|
172
|
+
addBox(mesh, baseMaterial, 0.022, 0.1, 0.035, handleX, handleY, faceZ + 0.025);
|
|
173
|
+
}
|
|
174
|
+
// ── Door closer (commercial hardware at top) ──
|
|
175
|
+
if (doorCloser) {
|
|
176
|
+
const closerY = leafCenterY + leafH / 2 - 0.04;
|
|
177
|
+
// Body
|
|
178
|
+
addBox(mesh, baseMaterial, 0.28, 0.055, 0.055, 0, closerY, leafDepth / 2 + 0.03);
|
|
179
|
+
// Arm (simplified as thin bar to frame side)
|
|
180
|
+
addBox(mesh, baseMaterial, 0.14, 0.015, 0.015, leafW / 4, closerY + 0.025, leafDepth / 2 + 0.015);
|
|
181
|
+
}
|
|
182
|
+
// ── Panic bar ──
|
|
183
|
+
if (panicBar) {
|
|
184
|
+
const barY = panicBarHeight - height / 2;
|
|
185
|
+
addBox(mesh, baseMaterial, leafW * 0.72, 0.04, 0.055, 0, barY, leafDepth / 2 + 0.03);
|
|
186
|
+
}
|
|
187
|
+
// ── Hinges (3 knuckle-style hinges on the hinge side) ──
|
|
188
|
+
{
|
|
189
|
+
const hingeX = hingesSide === 'right' ? leafW / 2 - 0.012 : -leafW / 2 + 0.012;
|
|
190
|
+
const hingeZ = 0; // centered in leaf depth
|
|
191
|
+
const hingeH = 0.1;
|
|
192
|
+
const hingeW = 0.024;
|
|
193
|
+
const hingeD = leafDepth + 0.016;
|
|
194
|
+
// Bottom hinge ~0.25m from floor, middle hinge, top hinge ~0.25m from top
|
|
195
|
+
const leafBottom = leafCenterY - leafH / 2;
|
|
196
|
+
const leafTop = leafCenterY + leafH / 2;
|
|
197
|
+
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeX, leafBottom + 0.25, hingeZ);
|
|
198
|
+
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeX, (leafBottom + leafTop) / 2, hingeZ);
|
|
199
|
+
addBox(mesh, baseMaterial, hingeW, hingeH, hingeD, hingeX, leafTop - 0.25, hingeZ);
|
|
200
|
+
}
|
|
201
|
+
// ── Cutout (for wall CSG) — always full door dimensions, 1m deep ──
|
|
202
|
+
let cutout = mesh.getObjectByName('cutout');
|
|
203
|
+
if (!cutout) {
|
|
204
|
+
cutout = new THREE.Mesh();
|
|
205
|
+
cutout.name = 'cutout';
|
|
206
|
+
mesh.add(cutout);
|
|
207
|
+
}
|
|
208
|
+
cutout.geometry.dispose();
|
|
209
|
+
cutout.geometry = new THREE.BoxGeometry(node.width, node.height, 1.0);
|
|
210
|
+
cutout.visible = false;
|
|
211
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useFrame } from '@react-three/fiber';
|
|
|
2
2
|
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
3
3
|
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
|
|
4
4
|
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
|
|
5
|
+
import { getScaledDimensions } from '../../schema';
|
|
5
6
|
import useScene from '../../store/use-scene';
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// ITEM SYSTEM
|
|
@@ -36,12 +37,12 @@ export const ItemSystem = () => {
|
|
|
36
37
|
if (parentNode?.type !== 'item') {
|
|
37
38
|
// Floor item: elevate by slab height (using full footprint overlap)
|
|
38
39
|
const levelId = resolveLevelId(item, nodes);
|
|
39
|
-
const slabElevation = spatialGridManager.getSlabElevationForItem(levelId, item.position, item
|
|
40
|
+
const slabElevation = spatialGridManager.getSlabElevationForItem(levelId, item.position, getScaledDimensions(item), item.rotation);
|
|
40
41
|
mesh.position.y = slabElevation + item.position[1];
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
clearDirty(id);
|
|
44
45
|
});
|
|
45
|
-
});
|
|
46
|
+
}, 2);
|
|
46
47
|
return null;
|
|
47
48
|
};
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import
|
|
2
|
+
import { Brush } from 'three-bvh-csg';
|
|
3
|
+
import type { RoofSegmentNode } from '../../schema';
|
|
3
4
|
export declare const RoofSystem: () => null;
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* Generate complete hollow-shell geometry for a roof segment.
|
|
7
|
+
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
6
8
|
*/
|
|
7
|
-
export declare function
|
|
9
|
+
export declare function getRoofSegmentBrushes(node: RoofSegmentNode): {
|
|
10
|
+
deckSlab: Brush;
|
|
11
|
+
shinSlab: Brush;
|
|
12
|
+
wallBrush: Brush;
|
|
13
|
+
innerBrush: Brush;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare function generateRoofSegmentGeometry(node: RoofSegmentNode): THREE.BufferGeometry;
|
|
8
16
|
//# sourceMappingURL=roof-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;AAwBjF,eAAO,MAAM,UAAU,YAqFtB,CAAA;AAmLD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAkRlF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC,cAAc,CAoDvF"}
|