@ifc-lite/viewer 1.26.0 → 1.28.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/.turbo/turbo-build.log +45 -38
- package/CHANGELOG.md +93 -0
- package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
- package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/deflate-DNGgs8Ur.js +1 -0
- package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
- package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
- package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
- package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
- package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
- package/dist/assets/index-BtbXFKsX.css +1 -0
- package/dist/assets/index.es-CWfqZyyr.js +6866 -0
- package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
- package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
- package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
- package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
- package/dist/assets/pdf-CRwaZf3s.js +135 -0
- package/dist/assets/raw-CJgQdyuZ.js +1 -0
- package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
- package/dist/assets/server-client-cTCJ-853.js +719 -0
- package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
- package/dist/assets/xlsx-B1YOg2QB.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
- package/dist/index.html +10 -10
- package/package.json +27 -23
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +280 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +46 -7
- package/src/components/viewer/MainToolbar.tsx +19 -2
- package/src/components/viewer/PropertiesPanel.tsx +84 -8
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +18 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +481 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +1 -26
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +23 -1
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +32 -9
- package/src/hooks/useIfcFederation.ts +42 -810
- package/src/hooks/useIfcLoader.ts +361 -488
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/search/result-export.ts +7 -1
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +9 -4
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +16 -1
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/desktopModelSnapshot.ts +2 -1
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/deflate-Cnx0il6E.js +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-8md211IW.js +0 -182
- package/dist/assets/raw-BQrAgxwT.js +0 -1
- package/dist/assets/server-client-Bk4c1CPO.js +0 -626
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
|
@@ -17,6 +17,9 @@ export interface ListSlice {
|
|
|
17
17
|
listResult: ListResult | null;
|
|
18
18
|
listPanelVisible: boolean;
|
|
19
19
|
listExecuting: boolean;
|
|
20
|
+
/** A list definition handed off from elsewhere (e.g. "Create list" in the
|
|
21
|
+
* search filter) for the ListPanel to open straight into the builder. */
|
|
22
|
+
pendingListDraft: ListDefinition | null;
|
|
20
23
|
|
|
21
24
|
// Actions
|
|
22
25
|
setListDefinitions: (definitions: ListDefinition[]) => void;
|
|
@@ -28,6 +31,7 @@ export interface ListSlice {
|
|
|
28
31
|
setListPanelVisible: (visible: boolean) => void;
|
|
29
32
|
toggleListPanel: () => void;
|
|
30
33
|
setListExecuting: (executing: boolean) => void;
|
|
34
|
+
setPendingListDraft: (definition: ListDefinition | null) => void;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export const createListSlice: StateCreator<ListSlice, [], [], ListSlice> = (set, get) => ({
|
|
@@ -37,6 +41,7 @@ export const createListSlice: StateCreator<ListSlice, [], [], ListSlice> = (set,
|
|
|
37
41
|
listResult: null,
|
|
38
42
|
listPanelVisible: false,
|
|
39
43
|
listExecuting: false,
|
|
44
|
+
pendingListDraft: null,
|
|
40
45
|
|
|
41
46
|
// Actions
|
|
42
47
|
setListDefinitions: (listDefinitions) => {
|
|
@@ -71,4 +76,5 @@ export const createListSlice: StateCreator<ListSlice, [], [], ListSlice> = (set,
|
|
|
71
76
|
setListPanelVisible: (listPanelVisible) => set({ listPanelVisible }),
|
|
72
77
|
toggleListPanel: () => set((state) => ({ listPanelVisible: !state.listPanelVisible })),
|
|
73
78
|
setListExecuting: (listExecuting) => set({ listExecuting }),
|
|
79
|
+
setPendingListDraft: (pendingListDraft) => set({ pendingListDraft }),
|
|
74
80
|
});
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
computeSlabSplitGeometry,
|
|
70
70
|
type SlabLikeType,
|
|
71
71
|
} from '@/lib/slab-edit.js';
|
|
72
|
+
import { getModelLengthUnitScale } from '@/lib/length-unit-scale.js';
|
|
72
73
|
import type { Point2D } from '@/lib/polygon-clip.js';
|
|
73
74
|
|
|
74
75
|
/**
|
|
@@ -676,7 +677,9 @@ function generateChangeSetId(): string {
|
|
|
676
677
|
*/
|
|
677
678
|
function getOrCreateStoreEditor(
|
|
678
679
|
get: () => ViewerState,
|
|
679
|
-
|
|
680
|
+
// Editors are cached in-place on the (non-reactive) `storeEditors`
|
|
681
|
+
// Map below, so the Zustand setter is intentionally unused here.
|
|
682
|
+
_set: (partial: Partial<ViewerState>) => void,
|
|
680
683
|
modelId: string,
|
|
681
684
|
): StoreEditor | null {
|
|
682
685
|
const state = get();
|
|
@@ -691,9 +694,14 @@ function getOrCreateStoreEditor(
|
|
|
691
694
|
if (!dataStore) return null;
|
|
692
695
|
|
|
693
696
|
const editor = new StoreEditor(dataStore, view);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
set({
|
|
697
|
+
// `storeEditors` is an internal, non-reactive cache (no component
|
|
698
|
+
// subscribes to it). Mutate the existing Map in place rather than
|
|
699
|
+
// `set({...})` — the read functions (readSlabFootprint, etc.) call
|
|
700
|
+
// this during render via GeometryEditCard's `splittable` memo, and a
|
|
701
|
+
// reactive `set()` there triggers React's "cannot update a component
|
|
702
|
+
// while rendering a different component" warning. In-place caching
|
|
703
|
+
// keeps the editor memoised without scheduling a render-phase update.
|
|
704
|
+
state.storeEditors.set(modelId, editor);
|
|
697
705
|
return editor;
|
|
698
706
|
}
|
|
699
707
|
|
|
@@ -1862,7 +1870,7 @@ export const createMutationSlice: StateCreator<
|
|
|
1862
1870
|
if (!editor) return null;
|
|
1863
1871
|
const dataStore = get().models.get(modelId)?.ifcDataStore;
|
|
1864
1872
|
if (!dataStore) return null;
|
|
1865
|
-
const chain = resolveSlabEditChain(dataStore, view, editor, expressId);
|
|
1873
|
+
const chain = resolveSlabEditChain(dataStore, view, editor, expressId, getModelLengthUnitScale(dataStore));
|
|
1866
1874
|
if (!chain) return null;
|
|
1867
1875
|
const storeyId = dataStore.spatialHierarchy?.elementToStorey.get(expressId);
|
|
1868
1876
|
const storeyElevation =
|
|
@@ -1883,7 +1891,7 @@ export const createMutationSlice: StateCreator<
|
|
|
1883
1891
|
const { view, editor, dataStore, storeyExpressId } = ctx;
|
|
1884
1892
|
const state = get();
|
|
1885
1893
|
|
|
1886
|
-
const chain = resolveSlabEditChain(dataStore, view, editor, expressId);
|
|
1894
|
+
const chain = resolveSlabEditChain(dataStore, view, editor, expressId, getModelLengthUnitScale(dataStore));
|
|
1887
1895
|
if (!chain) {
|
|
1888
1896
|
return {
|
|
1889
1897
|
ok: false,
|
|
@@ -24,7 +24,7 @@ import type { StateCreator } from 'zustand';
|
|
|
24
24
|
import type { Tier1Index } from '@/lib/search/tier1-index';
|
|
25
25
|
import type { SearchResult, MatchField } from '@/lib/search/tier0-scan';
|
|
26
26
|
import type { FilterRule, Combinator } from '@/lib/search/filter-rules';
|
|
27
|
-
import type { FilterSchema, PsetQtoSchema } from '@/lib/search/filter-schema';
|
|
27
|
+
import type { FilterSchema, PsetQtoSchema, FilterValueSchema } from '@/lib/search/filter-schema';
|
|
28
28
|
|
|
29
29
|
/** Index lifecycle state for a single model. */
|
|
30
30
|
export type Tier1IndexStatus = 'pending' | 'building' | 'ready' | 'error';
|
|
@@ -60,6 +60,10 @@ export interface SearchVimCycleState {
|
|
|
60
60
|
*/
|
|
61
61
|
export type SearchFieldFilter = MatchField | 'all';
|
|
62
62
|
|
|
63
|
+
/** Which tab the advanced modal renders. Lets callers (e.g. the inline
|
|
64
|
+
* filter button) open straight to the Filter builder. */
|
|
65
|
+
export type SearchModalTab = 'search' | 'filter';
|
|
66
|
+
|
|
63
67
|
/**
|
|
64
68
|
* Tabular result from a Filter run. Flat snapshot so the modal can
|
|
65
69
|
* re-render rows without holding live evaluator state.
|
|
@@ -94,6 +98,9 @@ export interface FilterSchemaCacheEntry {
|
|
|
94
98
|
basic: FilterSchema;
|
|
95
99
|
/** Expensive pass — pset / qto names. Lazy; null until first request. */
|
|
96
100
|
psetQto: PsetQtoSchema | null;
|
|
101
|
+
/** Expensive pass — distinct material / classification / property values
|
|
102
|
+
* for chip value suggestions. Lazy; null until first request. */
|
|
103
|
+
values: FilterValueSchema | null;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
export interface SearchSlice {
|
|
@@ -109,6 +116,8 @@ export interface SearchSlice {
|
|
|
109
116
|
searchVimCycle: SearchVimCycleState | null;
|
|
110
117
|
/** Advanced search modal (⌘⇧F) is open. */
|
|
111
118
|
searchModalOpen: boolean;
|
|
119
|
+
/** Which tab the advanced modal shows. Remembered across opens. */
|
|
120
|
+
searchModalTab: SearchModalTab;
|
|
112
121
|
/** Field chip filter active inside the modal. Defaults to 'all'. */
|
|
113
122
|
searchFieldFilter: SearchFieldFilter;
|
|
114
123
|
/** Per-modelId include filter inside the modal. `null` means all models included. */
|
|
@@ -147,6 +156,7 @@ export interface SearchSlice {
|
|
|
147
156
|
|
|
148
157
|
setSearchModalOpen: (open: boolean) => void;
|
|
149
158
|
toggleSearchModal: () => void;
|
|
159
|
+
setSearchModalTab: (tab: SearchModalTab) => void;
|
|
150
160
|
setSearchFieldFilter: (filter: SearchFieldFilter) => void;
|
|
151
161
|
/** Toggle a model in/out of the include filter. If the filter is null,
|
|
152
162
|
* the first toggle materialises it as "all models except this one". */
|
|
@@ -177,6 +187,7 @@ export interface SearchSlice {
|
|
|
177
187
|
// ── Schema cache actions ──────────────────────────────────────────
|
|
178
188
|
setFilterSchema: (modelId: string, basic: FilterSchema) => void;
|
|
179
189
|
setFilterPsetQtoSchema: (modelId: string, psetQto: PsetQtoSchema) => void;
|
|
190
|
+
setFilterValueSchema: (modelId: string, values: FilterValueSchema) => void;
|
|
180
191
|
removeFilterSchema: (modelId: string) => void;
|
|
181
192
|
}
|
|
182
193
|
|
|
@@ -187,6 +198,7 @@ export const createSearchSlice: StateCreator<SearchSlice, [], [], SearchSlice> =
|
|
|
187
198
|
searchIndexes: new Map(),
|
|
188
199
|
searchVimCycle: null,
|
|
189
200
|
searchModalOpen: false,
|
|
201
|
+
searchModalTab: 'search',
|
|
190
202
|
searchFieldFilter: 'all',
|
|
191
203
|
searchModelFilter: null,
|
|
192
204
|
searchFilterResult: null,
|
|
@@ -239,6 +251,7 @@ export const createSearchSlice: StateCreator<SearchSlice, [], [], SearchSlice> =
|
|
|
239
251
|
|
|
240
252
|
setSearchModalOpen: (searchModalOpen) => set({ searchModalOpen }),
|
|
241
253
|
toggleSearchModal: () => set((state) => ({ searchModalOpen: !state.searchModalOpen })),
|
|
254
|
+
setSearchModalTab: (searchModalTab) => set({ searchModalTab }),
|
|
242
255
|
setSearchFieldFilter: (searchFieldFilter) => set({ searchFieldFilter }),
|
|
243
256
|
|
|
244
257
|
toggleSearchModelFilter: (modelId, availableModelIds) =>
|
|
@@ -316,7 +329,11 @@ export const createSearchSlice: StateCreator<SearchSlice, [], [], SearchSlice> =
|
|
|
316
329
|
set((state) => {
|
|
317
330
|
const next = new Map(state.searchFilterSchema);
|
|
318
331
|
const existing = next.get(modelId);
|
|
319
|
-
next.set(modelId, {
|
|
332
|
+
next.set(modelId, {
|
|
333
|
+
basic,
|
|
334
|
+
psetQto: existing?.psetQto ?? null,
|
|
335
|
+
values: existing?.values ?? null,
|
|
336
|
+
});
|
|
320
337
|
return { searchFilterSchema: next };
|
|
321
338
|
}),
|
|
322
339
|
|
|
@@ -327,7 +344,16 @@ export const createSearchSlice: StateCreator<SearchSlice, [], [], SearchSlice> =
|
|
|
327
344
|
// Only meaningful once a basic schema has been set — the modal
|
|
328
345
|
// calls discoverFilterSchema first then queues the heavy pass.
|
|
329
346
|
if (!existing) return {};
|
|
330
|
-
next.set(modelId, {
|
|
347
|
+
next.set(modelId, { ...existing, psetQto });
|
|
348
|
+
return { searchFilterSchema: next };
|
|
349
|
+
}),
|
|
350
|
+
|
|
351
|
+
setFilterValueSchema: (modelId, values) =>
|
|
352
|
+
set((state) => {
|
|
353
|
+
const next = new Map(state.searchFilterSchema);
|
|
354
|
+
const existing = next.get(modelId);
|
|
355
|
+
if (!existing) return {};
|
|
356
|
+
next.set(modelId, { ...existing, values });
|
|
331
357
|
return { searchFilterSchema: next };
|
|
332
358
|
}),
|
|
333
359
|
|
|
@@ -192,15 +192,23 @@ describe('acquireFileBuffer', () => {
|
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
// Sanity check: the IFC
|
|
196
|
-
// (
|
|
195
|
+
// Sanity check: the IFC/STEP path still SAB-streams. addModel now delegates
|
|
196
|
+
// to the canonical loadFile (one load path), so the acquireFileBuffer SAB
|
|
197
|
+
// streaming lives there — assert addModel routes through loadFile, and that
|
|
198
|
+
// loadFile keeps acquireFileBuffer for the STEP/IFC binary path. (IFCX is
|
|
199
|
+
// still guarded above: its federation entry points stay on file.arrayBuffer.)
|
|
197
200
|
const addModelStart = source.indexOf('const addModel = useCallback');
|
|
198
201
|
assert.ok(addModelStart >= 0, 'expected addModel declaration');
|
|
199
202
|
const addModelEnd = source.indexOf('}, [', addModelStart);
|
|
200
203
|
const addModelBody = source.slice(addModelStart, addModelEnd);
|
|
201
204
|
assert.ok(
|
|
202
|
-
addModelBody.includes('
|
|
203
|
-
'addModel
|
|
205
|
+
addModelBody.includes('loadFile('),
|
|
206
|
+
'addModel must delegate to the canonical loadFile (one load path)',
|
|
207
|
+
);
|
|
208
|
+
const loaderSource = readFileSync(join(here, '..', 'hooks', 'useIfcLoader.ts'), 'utf8');
|
|
209
|
+
assert.ok(
|
|
210
|
+
loaderSource.includes('acquireFileBuffer'),
|
|
211
|
+
'loadFile (IFC/STEP path) must keep using acquireFileBuffer() for SAB streaming',
|
|
204
212
|
);
|
|
205
213
|
});
|
|
206
214
|
|
|
@@ -346,13 +346,14 @@ export async function restoreDesktopMetadataSnapshot(
|
|
|
346
346
|
};
|
|
347
347
|
dataStore.spatialHierarchy = deserializeSpatialHierarchy(metadata.spatialHierarchy);
|
|
348
348
|
|
|
349
|
-
const { onDemandPropertyMap, onDemandQuantityMap } = rebuildOnDemandMaps(
|
|
349
|
+
const { onDemandPropertyMap, onDemandQuantityMap, onDemandMaterialMap } = rebuildOnDemandMaps(
|
|
350
350
|
dataStore.entities,
|
|
351
351
|
dataStore.relationships,
|
|
352
352
|
dataStore.entityIndex,
|
|
353
353
|
);
|
|
354
354
|
dataStore.onDemandPropertyMap = onDemandPropertyMap;
|
|
355
355
|
dataStore.onDemandQuantityMap = onDemandQuantityMap;
|
|
356
|
+
dataStore.onDemandMaterialMap = onDemandMaterialMap;
|
|
356
357
|
|
|
357
358
|
return dataStore;
|
|
358
359
|
}
|
|
@@ -44,3 +44,35 @@ export function buildSpatialIndexGuarded(
|
|
|
44
44
|
console.warn('[loadingUtils] Failed to build spatial index:', err);
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a spatial index for a specific (e.g. federated) model.
|
|
50
|
+
*
|
|
51
|
+
* Unlike {@link buildSpatialIndexGuarded}, this never touches the active-model
|
|
52
|
+
* slot: a federated model is usually not the active one, so guarding on / writing
|
|
53
|
+
* through `ifcDataStore` (`setIfcDataStore`) would either discard the index or
|
|
54
|
+
* mutate the wrong model. Instead it guards on the target model still holding the
|
|
55
|
+
* same store and publishes through `updateModel(modelId, ...)`.
|
|
56
|
+
*
|
|
57
|
+
* @param meshes - Final mesh array with correct IDs and world-space positions
|
|
58
|
+
* @param modelId - The federated model to attach the spatial index to
|
|
59
|
+
* @param dataStore - That model's IfcDataStore (mutated in place)
|
|
60
|
+
*/
|
|
61
|
+
export function buildSpatialIndexForModel(
|
|
62
|
+
meshes: MeshData[],
|
|
63
|
+
modelId: string,
|
|
64
|
+
dataStore: IfcDataStore,
|
|
65
|
+
): void {
|
|
66
|
+
if (meshes.length === 0) return;
|
|
67
|
+
|
|
68
|
+
buildSpatialIndexAsync(meshes).then(spatialIndex => {
|
|
69
|
+
const state = useViewerStore.getState();
|
|
70
|
+
const model = state.models.get(modelId);
|
|
71
|
+
// Model removed, or its store was replaced since this build started.
|
|
72
|
+
if (!model || model.ifcDataStore !== dataStore) return;
|
|
73
|
+
dataStore.spatialIndex = spatialIndex;
|
|
74
|
+
state.updateModel(modelId, { ifcDataStore: dataStore });
|
|
75
|
+
}).catch(err => {
|
|
76
|
+
console.warn('[loadingUtils] Failed to build spatial index for model:', err);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -267,5 +267,11 @@ export function buildIfcDataStoreFromNativeMetadata(snapshot: NativeMetadataSnap
|
|
|
267
267
|
quantities: undefined as unknown as IfcDataStore['quantities'],
|
|
268
268
|
relationships: undefined as unknown as IfcDataStore['relationships'],
|
|
269
269
|
spatialHierarchy: hierarchy,
|
|
270
|
+
// Native spatial snapshots are metadata-only (no source buffer / property
|
|
271
|
+
// or quantity tables), so the IfcStoreBase accessors return empties.
|
|
272
|
+
getEntity: () => null,
|
|
273
|
+
getEntitiesByType: () => [],
|
|
274
|
+
getProperties: () => [],
|
|
275
|
+
getQuantities: () => [],
|
|
270
276
|
} as IfcDataStore;
|
|
271
277
|
}
|
|
@@ -38,6 +38,9 @@ describe('convertServerDataModel', () => {
|
|
|
38
38
|
{ rel_type: 'IFCRELAGGREGATES', relating_id: 2, related_id: 3 },
|
|
39
39
|
{ rel_type: 'IFCRELCONTAINEDINSPATIALSTRUCTURE', relating_id: 3, related_id: 4 },
|
|
40
40
|
],
|
|
41
|
+
classifications: [],
|
|
42
|
+
materials: [],
|
|
43
|
+
documents: [],
|
|
41
44
|
spatialHierarchy: {
|
|
42
45
|
nodes: [
|
|
43
46
|
{
|
|
@@ -101,6 +104,9 @@ describe('convertServerDataModel', () => {
|
|
|
101
104
|
{ rel_type: 'IFCRELNESTS', relating_id: 1, related_id: 2 },
|
|
102
105
|
{ rel_type: 'IFCRELASSOCIATESDOCUMENT', relating_id: 3, related_id: 2 },
|
|
103
106
|
],
|
|
107
|
+
classifications: [],
|
|
108
|
+
materials: [],
|
|
109
|
+
documents: [],
|
|
104
110
|
spatialHierarchy: {
|
|
105
111
|
nodes: [
|
|
106
112
|
{
|
|
@@ -711,5 +711,12 @@ export function convertServerDataModel(
|
|
|
711
711
|
relationships,
|
|
712
712
|
spatialHierarchy,
|
|
713
713
|
spatialIndex,
|
|
714
|
+
// IfcStoreBase accessors: server-parsed models carry pre-built property/
|
|
715
|
+
// quantity tables but no source buffer, so entity extraction is unavailable
|
|
716
|
+
// (the `entities` table remains the primary path for basic attributes).
|
|
717
|
+
getEntity: () => null,
|
|
718
|
+
getEntitiesByType: () => [],
|
|
719
|
+
getProperties: (expressId: number) => properties.getForEntity(expressId),
|
|
720
|
+
getQuantities: (expressId: number) => quantities.getForEntity(expressId),
|
|
714
721
|
};
|
|
715
722
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
RelationshipType,
|
|
12
12
|
StringTable,
|
|
13
13
|
} from '@ifc-lite/data';
|
|
14
|
-
import { rebuildSpatialHierarchy } from './spatialHierarchy';
|
|
14
|
+
import { rebuildSpatialHierarchy, rebuildOnDemandMaps } from './spatialHierarchy';
|
|
15
15
|
|
|
16
16
|
describe('rebuildSpatialHierarchy', () => {
|
|
17
17
|
it('preserves IFC4.3 facility-part trees during cache rebuilds', () => {
|
|
@@ -152,3 +152,55 @@ describe('rebuildSpatialHierarchy', () => {
|
|
|
152
152
|
assert.equal(hierarchy.elementToStorey.get(6), 3);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
describe('rebuildOnDemandMaps', () => {
|
|
157
|
+
const makeEntityIndex = (byType: Map<string, number[]>) => ({
|
|
158
|
+
byId: { get: () => undefined, has: () => false, size: 0 },
|
|
159
|
+
byType,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('rebuilds onDemandMaterialMap from AssociatesMaterial edges (cache parity)', () => {
|
|
163
|
+
const strings = new StringTable();
|
|
164
|
+
const entities = new EntityTableBuilder(2, strings);
|
|
165
|
+
entities.add(5, 'IFCBEAM', 'b0', 'Beam', '', '', true);
|
|
166
|
+
entities.add(10, 'IFCMATERIAL', 'm0', 'Concrete', '', '');
|
|
167
|
+
|
|
168
|
+
const builder = new RelationshipGraphBuilder();
|
|
169
|
+
// material(10) -> element(5) forward, matching the columnar parser.
|
|
170
|
+
builder.addEdge(10, 5, RelationshipType.AssociatesMaterial, 100);
|
|
171
|
+
// pset(20) -> element(5), so the property map still rebuilds too.
|
|
172
|
+
builder.addEdge(20, 5, RelationshipType.DefinesByProperties, 101);
|
|
173
|
+
|
|
174
|
+
const entityIndex = makeEntityIndex(new Map<string, number[]>([
|
|
175
|
+
['IFCMATERIAL', [10]],
|
|
176
|
+
['IFCPROPERTYSET', [20]],
|
|
177
|
+
]));
|
|
178
|
+
|
|
179
|
+
const { onDemandMaterialMap, onDemandPropertyMap } = rebuildOnDemandMaps(
|
|
180
|
+
entities.build(),
|
|
181
|
+
builder.build(),
|
|
182
|
+
entityIndex,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
assert.equal(onDemandMaterialMap.size, 1);
|
|
186
|
+
assert.equal(onDemandMaterialMap.get(5), 10);
|
|
187
|
+
assert.deepEqual(onDemandPropertyMap.get(5), [20]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('matches material definitions case-insensitively (mixed-case byType keys)', () => {
|
|
191
|
+
const strings = new StringTable();
|
|
192
|
+
const entities = new EntityTableBuilder(2, strings);
|
|
193
|
+
entities.add(5, 'IFCWALL', 'w0', 'Wall', '', '', true);
|
|
194
|
+
entities.add(40, 'IFCMATERIALLAYERSET', 'ls0', 'Buildup', '', '');
|
|
195
|
+
|
|
196
|
+
const builder = new RelationshipGraphBuilder();
|
|
197
|
+
builder.addEdge(40, 5, RelationshipType.AssociatesMaterial, 100);
|
|
198
|
+
|
|
199
|
+
const entityIndex = makeEntityIndex(new Map<string, number[]>([
|
|
200
|
+
['IfcMaterialLayerSet', [40]], // mixed-case, as some cache writers emit
|
|
201
|
+
]));
|
|
202
|
+
|
|
203
|
+
const { onDemandMaterialMap } = rebuildOnDemandMaps(entities.build(), builder.build(), entityIndex);
|
|
204
|
+
assert.equal(onDemandMaterialMap.get(5), 40);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -208,8 +208,22 @@ export interface EntityIndex {
|
|
|
208
208
|
export interface OnDemandMaps {
|
|
209
209
|
onDemandPropertyMap: Map<number, number[]>;
|
|
210
210
|
onDemandQuantityMap: Map<number, number[]>;
|
|
211
|
+
/** element/type expressId -> associated material definition expressId. */
|
|
212
|
+
onDemandMaterialMap: Map<number, number>;
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
/** IFC material *definition* classes that can be the RelatingMaterial of an
|
|
216
|
+
* IfcRelAssociatesMaterial — the source nodes of AssociatesMaterial edges. */
|
|
217
|
+
const MATERIAL_DEF_TYPES = new Set([
|
|
218
|
+
'IFCMATERIAL',
|
|
219
|
+
'IFCMATERIALLAYERSET',
|
|
220
|
+
'IFCMATERIALLAYERSETUSAGE',
|
|
221
|
+
'IFCMATERIALPROFILESET',
|
|
222
|
+
'IFCMATERIALPROFILESETUSAGE',
|
|
223
|
+
'IFCMATERIALCONSTITUENTSET',
|
|
224
|
+
'IFCMATERIALLIST',
|
|
225
|
+
]);
|
|
226
|
+
|
|
213
227
|
/**
|
|
214
228
|
* Rebuild on-demand property/quantity maps from relationships and entity types
|
|
215
229
|
* Uses FORWARD direction: pset -> elements (more efficient than inverse lookup)
|
|
@@ -228,6 +242,7 @@ export function rebuildOnDemandMaps(
|
|
|
228
242
|
): OnDemandMaps {
|
|
229
243
|
const onDemandPropertyMap = new Map<number, number[]>();
|
|
230
244
|
const onDemandQuantityMap = new Map<number, number[]>();
|
|
245
|
+
const onDemandMaterialMap = new Map<number, number>();
|
|
231
246
|
|
|
232
247
|
// Use entityIndex.byType if available (needed for cache loads where entity table
|
|
233
248
|
// doesn't include IfcPropertySet/IfcElementQuantity entities)
|
|
@@ -288,8 +303,33 @@ export function rebuildOnDemandMaps(
|
|
|
288
303
|
}
|
|
289
304
|
}
|
|
290
305
|
|
|
306
|
+
// Process material associations (FORWARD: material definition -> elements),
|
|
307
|
+
// mirroring the columnar parser's onDemandMaterialMap. Needed so cache-loaded
|
|
308
|
+
// models populate the Materials tab + per-material totals, which read this map
|
|
309
|
+
// (the relationship-graph fallback only covers single-element lookups, not the
|
|
310
|
+
// model-wide usage index). Requires entityIndex.byType to enumerate material
|
|
311
|
+
// definitions — the cached graph preserves AssociatesMaterial edges.
|
|
312
|
+
let materialDefCount = 0;
|
|
313
|
+
if (entityIndex?.byType) {
|
|
314
|
+
for (const [typeKey, ids] of entityIndex.byType) {
|
|
315
|
+
if (!MATERIAL_DEF_TYPES.has(typeKey.toUpperCase())) continue;
|
|
316
|
+
for (const materialId of ids) {
|
|
317
|
+
materialDefCount += 1;
|
|
318
|
+
const associated = relationships.getRelated(
|
|
319
|
+
materialId,
|
|
320
|
+
RelationshipType.AssociatesMaterial,
|
|
321
|
+
'forward'
|
|
322
|
+
);
|
|
323
|
+
for (const entityId of associated) {
|
|
324
|
+
// Last association wins, matching the columnar parser's `.set` build.
|
|
325
|
+
onDemandMaterialMap.set(entityId, materialId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
291
331
|
console.log(
|
|
292
|
-
`[spatialHierarchy] Rebuilt on-demand maps: ${propertySets.length} psets, ${quantitySets.length} qsets -> ${onDemandPropertyMap.size} entities with properties, ${onDemandQuantityMap.size} with quantities`
|
|
332
|
+
`[spatialHierarchy] Rebuilt on-demand maps: ${propertySets.length} psets, ${quantitySets.length} qsets, ${materialDefCount} material defs -> ${onDemandPropertyMap.size} entities with properties, ${onDemandQuantityMap.size} with quantities, ${onDemandMaterialMap.size} with materials`
|
|
293
333
|
);
|
|
294
|
-
return { onDemandPropertyMap, onDemandQuantityMap };
|
|
334
|
+
return { onDemandPropertyMap, onDemandQuantityMap, onDemandMaterialMap };
|
|
295
335
|
}
|
package/src/vite-env.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ interface ImportMetaEnv {
|
|
|
17
17
|
readonly VITE_LLM_IMAGE_MODELS?: string;
|
|
18
18
|
/** Comma-separated model IDs that support file attachment context */
|
|
19
19
|
readonly VITE_LLM_FILE_ATTACHMENT_MODELS?: string;
|
|
20
|
+
/** Build-time default Cesium ion access token */
|
|
21
|
+
readonly VITE_CESIUM_ION_TOKEN?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
interface ImportMeta {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as r}from"./pako.esm-Cram60i4.js";import{B as o}from"./geotiff-A5UjhI6L.js";import"./sandbox-CsRXlgCO.js";import"./lens-PYsLu_MA.js";import"./__vite-browser-external-B1O5LaIO.js";class d extends o{decodeBlock(e){return r(new Uint8Array(e)).buffer}}export{d as default};
|