@ifc-lite/viewer 1.17.6 → 1.18.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 +17 -14
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +513 -0
- package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/{exporters-CcPS9MK5.js → exporters-B_OBqIyD.js} +3235 -2648
- package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/{ifc-lite_bg-BINvzoCP.wasm → ifc-lite_bg-ADjKXSms.wasm} +0 -0
- package/dist/assets/{index-Bfms9I4A.js → index-BKq-M3Mk.js} +44124 -30920
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-SHXiQwFW.js} +1 -1
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BuZK7OST.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +10 -10
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/ChatPanel.tsx +64 -2
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +69 -10
- package/src/components/viewer/PropertiesPanel.tsx +222 -22
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +24 -4
- package/src/components/viewer/Viewport.tsx +11 -1
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/hooks/useIfcLoader.ts +22 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/index.css +66 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +6 -0
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +70 -1
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store.ts +14 -0
- package/dist/assets/index-_bfZsDCC.css +0 -1
- package/dist/assets/sandbox-C8575tul.js +0 -5951
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Schedule backend adapter — drives the `bim.schedule.*` API by calling
|
|
7
|
+
* `extractScheduleOnDemand` against the viewer's active (or requested) model.
|
|
8
|
+
*
|
|
9
|
+
* Results are cached per-model until the underlying `ifcDataStore` identity
|
|
10
|
+
* changes, so repeated script / panel queries don't re-parse the STEP source.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ScheduleBackendMethods, ScheduleExtractionData } from '@ifc-lite/sdk';
|
|
14
|
+
import { extractScheduleOnDemand, type IfcDataStore } from '@ifc-lite/parser';
|
|
15
|
+
import type { StoreApi } from './types.js';
|
|
16
|
+
import { getModelForRef } from './model-compat.js';
|
|
17
|
+
|
|
18
|
+
const EMPTY_EXTRACTION: ScheduleExtractionData = {
|
|
19
|
+
workSchedules: [],
|
|
20
|
+
tasks: [],
|
|
21
|
+
sequences: [],
|
|
22
|
+
hasSchedule: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Best-effort resolution of the data store to extract from: explicit modelId,
|
|
27
|
+
* then the legacy single-model store, then the first federated model.
|
|
28
|
+
*/
|
|
29
|
+
function resolveStore(store: StoreApi, modelId?: string): IfcDataStore | null {
|
|
30
|
+
const state = store.getState();
|
|
31
|
+
if (modelId) {
|
|
32
|
+
const model = getModelForRef(state, modelId);
|
|
33
|
+
return (model?.ifcDataStore as IfcDataStore | undefined) ?? null;
|
|
34
|
+
}
|
|
35
|
+
if (state.ifcDataStore) return state.ifcDataStore as IfcDataStore;
|
|
36
|
+
// Respect the user's active model selection before falling back to the
|
|
37
|
+
// first federated entry — other namespaces (query, selection, viewer)
|
|
38
|
+
// follow the same pattern.
|
|
39
|
+
const activeId = state.activeModelId as string | null | undefined;
|
|
40
|
+
if (activeId) {
|
|
41
|
+
const active = getModelForRef(state, activeId);
|
|
42
|
+
if (active?.ifcDataStore) return active.ifcDataStore as IfcDataStore;
|
|
43
|
+
}
|
|
44
|
+
const firstFederated = state.models?.values().next().value;
|
|
45
|
+
return (firstFederated?.ifcDataStore as IfcDataStore | undefined) ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createScheduleAdapter(store: StoreApi): ScheduleBackendMethods {
|
|
49
|
+
/** Cache keyed by IfcDataStore identity (WeakMap avoids leaks on model swap). */
|
|
50
|
+
const cache = new WeakMap<IfcDataStore, ScheduleExtractionData>();
|
|
51
|
+
|
|
52
|
+
const extract = (modelId?: string): ScheduleExtractionData => {
|
|
53
|
+
const ds = resolveStore(store, modelId);
|
|
54
|
+
if (!ds) return EMPTY_EXTRACTION;
|
|
55
|
+
const cached = cache.get(ds);
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
try {
|
|
58
|
+
const result = extractScheduleOnDemand(ds) as ScheduleExtractionData;
|
|
59
|
+
cache.set(ds, result);
|
|
60
|
+
return result;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.warn('[schedule-adapter] extraction failed', err);
|
|
63
|
+
return EMPTY_EXTRACTION;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
data: (modelId) => extract(modelId),
|
|
69
|
+
tasks: (modelId) => extract(modelId).tasks,
|
|
70
|
+
workSchedules: (modelId) => extract(modelId).workSchedules,
|
|
71
|
+
sequences: (modelId) => extract(modelId).sequences,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `bim.store.*` adapter — implements StoreBackendMethods on top of the
|
|
7
|
+
* viewer's per-model MutablePropertyView. Routes through the same overlay
|
|
8
|
+
* that bim.mutate.* uses, so document-level edits and property edits stack
|
|
9
|
+
* coherently into a single export.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { StoreEditor } from '@ifc-lite/mutations';
|
|
13
|
+
import {
|
|
14
|
+
addBeamToStore,
|
|
15
|
+
addColumnToStore,
|
|
16
|
+
addDoorToStore,
|
|
17
|
+
addMemberToStore,
|
|
18
|
+
addPlateToStore,
|
|
19
|
+
addRoofToStore,
|
|
20
|
+
addSlabToStore,
|
|
21
|
+
addSpaceToStore,
|
|
22
|
+
addWallToStore,
|
|
23
|
+
addWindowToStore,
|
|
24
|
+
resolveSpatialAnchor,
|
|
25
|
+
type BeamInStoreParams,
|
|
26
|
+
type ColumnInStoreParams,
|
|
27
|
+
type DoorInStoreParams,
|
|
28
|
+
type MemberInStoreParams,
|
|
29
|
+
type PlateInStoreParams,
|
|
30
|
+
type RoofInStoreParams,
|
|
31
|
+
type SlabInStoreParams,
|
|
32
|
+
type SpaceInStoreParams,
|
|
33
|
+
type WallInStoreParams,
|
|
34
|
+
type WindowInStoreParams,
|
|
35
|
+
} from '@ifc-lite/create';
|
|
36
|
+
import type {
|
|
37
|
+
AddBeamInStoreParams,
|
|
38
|
+
AddColumnInStoreParams,
|
|
39
|
+
AddDoorInStoreParams,
|
|
40
|
+
AddMemberInStoreParams,
|
|
41
|
+
AddPlateInStoreParams,
|
|
42
|
+
AddRoofInStoreParams,
|
|
43
|
+
AddSlabInStoreParams,
|
|
44
|
+
AddSpaceInStoreParams,
|
|
45
|
+
AddWallInStoreParams,
|
|
46
|
+
AddWindowInStoreParams,
|
|
47
|
+
EntityRef,
|
|
48
|
+
StoreBackendMethods,
|
|
49
|
+
} from '@ifc-lite/sdk';
|
|
50
|
+
import type { StoreApi } from './types.js';
|
|
51
|
+
import { getModelForRef, LEGACY_MODEL_ID } from './model-compat.js';
|
|
52
|
+
import { getOrCreateMutationView, normalizeMutationModelId } from './mutation-view.js';
|
|
53
|
+
|
|
54
|
+
export function createStoreAdapter(store: StoreApi): StoreBackendMethods {
|
|
55
|
+
// One StoreEditor per (modelId, MutablePropertyView) pair. Editors are
|
|
56
|
+
// cheap, but caching avoids re-scanning the entity index on every call.
|
|
57
|
+
const editors = new WeakMap<object, StoreEditor>();
|
|
58
|
+
|
|
59
|
+
function resolveDataStore(modelId: string) {
|
|
60
|
+
const state = store.getState();
|
|
61
|
+
const refModelId = modelId === 'legacy' ? LEGACY_MODEL_ID : modelId;
|
|
62
|
+
const model = getModelForRef(state, refModelId);
|
|
63
|
+
return model?.ifcDataStore ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getEditor(modelId: string): StoreEditor | null {
|
|
67
|
+
const view = getOrCreateMutationView(store, modelId);
|
|
68
|
+
if (!view) return null;
|
|
69
|
+
let editor = editors.get(view);
|
|
70
|
+
if (editor) return editor;
|
|
71
|
+
|
|
72
|
+
const dataStore = resolveDataStore(modelId);
|
|
73
|
+
if (!dataStore) return null;
|
|
74
|
+
|
|
75
|
+
editor = new StoreEditor(dataStore, view);
|
|
76
|
+
editors.set(view, editor);
|
|
77
|
+
return editor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
addEntity(modelId: string, def: { type: string; attributes: unknown[] }): EntityRef {
|
|
82
|
+
const normalizedId = normalizeMutationModelId(store.getState(), modelId);
|
|
83
|
+
const editor = getEditor(modelId);
|
|
84
|
+
if (!editor) {
|
|
85
|
+
throw new Error(`bim.store.addEntity: no model loaded for id "${modelId}"`);
|
|
86
|
+
}
|
|
87
|
+
const ref = editor.addEntity(def.type, def.attributes as Parameters<StoreEditor['addEntity']>[1]);
|
|
88
|
+
return { modelId: normalizedId, expressId: ref.expressId };
|
|
89
|
+
},
|
|
90
|
+
removeEntity(ref: EntityRef): boolean {
|
|
91
|
+
const editor = getEditor(ref.modelId);
|
|
92
|
+
if (!editor) return false;
|
|
93
|
+
return editor.removeEntity(ref.expressId);
|
|
94
|
+
},
|
|
95
|
+
setPositionalAttribute(ref: EntityRef, index: number, value: unknown): void {
|
|
96
|
+
const editor = getEditor(ref.modelId);
|
|
97
|
+
if (!editor) {
|
|
98
|
+
throw new Error(`bim.store.setPositionalAttribute: no model loaded for id "${ref.modelId}"`);
|
|
99
|
+
}
|
|
100
|
+
editor.setPositionalAttribute(ref.expressId, index, value as Parameters<StoreEditor['setPositionalAttribute']>[2]);
|
|
101
|
+
},
|
|
102
|
+
addColumn(modelId: string, storeyExpressId: number, params: AddColumnInStoreParams): EntityRef {
|
|
103
|
+
const editor = getEditor(modelId);
|
|
104
|
+
const dataStore = resolveDataStore(modelId);
|
|
105
|
+
if (!editor || !dataStore) {
|
|
106
|
+
throw new Error(`bim.store.addColumn: no model loaded for id "${modelId}"`);
|
|
107
|
+
}
|
|
108
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
109
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
110
|
+
const result = addColumnToStore(editor, anchor, params as ColumnInStoreParams);
|
|
111
|
+
return { modelId: normalizedModelId, expressId: result.columnId };
|
|
112
|
+
},
|
|
113
|
+
addWall(modelId: string, storeyExpressId: number, params: AddWallInStoreParams): EntityRef {
|
|
114
|
+
const editor = getEditor(modelId);
|
|
115
|
+
const dataStore = resolveDataStore(modelId);
|
|
116
|
+
if (!editor || !dataStore) {
|
|
117
|
+
throw new Error(`bim.store.addWall: no model loaded for id "${modelId}"`);
|
|
118
|
+
}
|
|
119
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
120
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
121
|
+
const result = addWallToStore(editor, anchor, params as WallInStoreParams);
|
|
122
|
+
return { modelId: normalizedModelId, expressId: result.wallId };
|
|
123
|
+
},
|
|
124
|
+
addSlab(modelId: string, storeyExpressId: number, params: AddSlabInStoreParams): EntityRef {
|
|
125
|
+
const editor = getEditor(modelId);
|
|
126
|
+
const dataStore = resolveDataStore(modelId);
|
|
127
|
+
if (!editor || !dataStore) {
|
|
128
|
+
throw new Error(`bim.store.addSlab: no model loaded for id "${modelId}"`);
|
|
129
|
+
}
|
|
130
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
131
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
132
|
+
const result = addSlabToStore(editor, anchor, params as SlabInStoreParams);
|
|
133
|
+
return { modelId: normalizedModelId, expressId: result.slabId };
|
|
134
|
+
},
|
|
135
|
+
addBeam(modelId: string, storeyExpressId: number, params: AddBeamInStoreParams): EntityRef {
|
|
136
|
+
const editor = getEditor(modelId);
|
|
137
|
+
const dataStore = resolveDataStore(modelId);
|
|
138
|
+
if (!editor || !dataStore) {
|
|
139
|
+
throw new Error(`bim.store.addBeam: no model loaded for id "${modelId}"`);
|
|
140
|
+
}
|
|
141
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
142
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
143
|
+
const result = addBeamToStore(editor, anchor, params as BeamInStoreParams);
|
|
144
|
+
return { modelId: normalizedModelId, expressId: result.beamId };
|
|
145
|
+
},
|
|
146
|
+
addDoor(modelId: string, storeyExpressId: number, params: AddDoorInStoreParams): EntityRef {
|
|
147
|
+
const editor = getEditor(modelId);
|
|
148
|
+
const dataStore = resolveDataStore(modelId);
|
|
149
|
+
if (!editor || !dataStore) throw new Error(`bim.store.addDoor: no model loaded for id "${modelId}"`);
|
|
150
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
151
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
152
|
+
const result = addDoorToStore(editor, anchor, params as DoorInStoreParams);
|
|
153
|
+
return { modelId: normalizedModelId, expressId: result.doorId };
|
|
154
|
+
},
|
|
155
|
+
addWindow(modelId: string, storeyExpressId: number, params: AddWindowInStoreParams): EntityRef {
|
|
156
|
+
const editor = getEditor(modelId);
|
|
157
|
+
const dataStore = resolveDataStore(modelId);
|
|
158
|
+
if (!editor || !dataStore) throw new Error(`bim.store.addWindow: no model loaded for id "${modelId}"`);
|
|
159
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
160
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
161
|
+
const result = addWindowToStore(editor, anchor, params as WindowInStoreParams);
|
|
162
|
+
return { modelId: normalizedModelId, expressId: result.windowId };
|
|
163
|
+
},
|
|
164
|
+
addSpace(modelId: string, storeyExpressId: number, params: AddSpaceInStoreParams): EntityRef {
|
|
165
|
+
const editor = getEditor(modelId);
|
|
166
|
+
const dataStore = resolveDataStore(modelId);
|
|
167
|
+
if (!editor || !dataStore) throw new Error(`bim.store.addSpace: no model loaded for id "${modelId}"`);
|
|
168
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
169
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
170
|
+
const result = addSpaceToStore(editor, anchor, params as SpaceInStoreParams);
|
|
171
|
+
return { modelId: normalizedModelId, expressId: result.spaceId };
|
|
172
|
+
},
|
|
173
|
+
addRoof(modelId: string, storeyExpressId: number, params: AddRoofInStoreParams): EntityRef {
|
|
174
|
+
const editor = getEditor(modelId);
|
|
175
|
+
const dataStore = resolveDataStore(modelId);
|
|
176
|
+
if (!editor || !dataStore) throw new Error(`bim.store.addRoof: no model loaded for id "${modelId}"`);
|
|
177
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
178
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
179
|
+
const result = addRoofToStore(editor, anchor, params as RoofInStoreParams);
|
|
180
|
+
return { modelId: normalizedModelId, expressId: result.roofId };
|
|
181
|
+
},
|
|
182
|
+
addPlate(modelId: string, storeyExpressId: number, params: AddPlateInStoreParams): EntityRef {
|
|
183
|
+
const editor = getEditor(modelId);
|
|
184
|
+
const dataStore = resolveDataStore(modelId);
|
|
185
|
+
if (!editor || !dataStore) throw new Error(`bim.store.addPlate: no model loaded for id "${modelId}"`);
|
|
186
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
187
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
188
|
+
const result = addPlateToStore(editor, anchor, params as PlateInStoreParams);
|
|
189
|
+
return { modelId: normalizedModelId, expressId: result.plateId };
|
|
190
|
+
},
|
|
191
|
+
addMember(modelId: string, storeyExpressId: number, params: AddMemberInStoreParams): EntityRef {
|
|
192
|
+
const editor = getEditor(modelId);
|
|
193
|
+
const dataStore = resolveDataStore(modelId);
|
|
194
|
+
if (!editor || !dataStore) throw new Error(`bim.store.addMember: no model loaded for id "${modelId}"`);
|
|
195
|
+
const anchor = resolveSpatialAnchor(dataStore, storeyExpressId);
|
|
196
|
+
const normalizedModelId = normalizeMutationModelId(store.getState(), modelId);
|
|
197
|
+
const result = addMemberToStore(editor, anchor, params as MemberInStoreParams);
|
|
198
|
+
return { modelId: normalizedModelId, expressId: result.memberId };
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -28,6 +28,9 @@ function findDescendantNode(root: SpatialNode, expressId: number): SpatialNode |
|
|
|
28
28
|
*/
|
|
29
29
|
function expandSpatialRef(ref: EntityRef, model: ModelLike): number[] {
|
|
30
30
|
const dataStore = model.ifcDataStore;
|
|
31
|
+
// Native-metadata-only models have no parsed data store — visibility
|
|
32
|
+
// expansion isn't possible, fall back to the single ref.
|
|
33
|
+
if (!dataStore) return [ref.expressId];
|
|
31
34
|
const typeName = dataStore.entities.getTypeName(ref.expressId) || '';
|
|
32
35
|
if (!isSpatialStructureTypeName(typeName) || isSpaceLikeSpatialTypeName(typeName)) {
|
|
33
36
|
return [ref.expressId];
|
package/src/sdk/local-backend.ts
CHANGED
|
@@ -18,10 +18,12 @@ import type {
|
|
|
18
18
|
VisibilityBackendMethods,
|
|
19
19
|
ViewerBackendMethods,
|
|
20
20
|
MutateBackendMethods,
|
|
21
|
+
StoreBackendMethods,
|
|
21
22
|
SpatialBackendMethods,
|
|
22
23
|
ExportBackendMethods,
|
|
23
24
|
LensBackendMethods,
|
|
24
25
|
FilesBackendMethods,
|
|
26
|
+
ScheduleBackendMethods,
|
|
25
27
|
} from '@ifc-lite/sdk';
|
|
26
28
|
import type { StoreApi } from './adapters/types.js';
|
|
27
29
|
import { LEGACY_MODEL_ID } from './adapters/model-compat.js';
|
|
@@ -31,10 +33,12 @@ import { createSelectionAdapter } from './adapters/selection-adapter.js';
|
|
|
31
33
|
import { createVisibilityAdapter } from './adapters/visibility-adapter.js';
|
|
32
34
|
import { createViewerAdapter } from './adapters/viewer-adapter.js';
|
|
33
35
|
import { createMutateAdapter } from './adapters/mutate-adapter.js';
|
|
36
|
+
import { createStoreAdapter } from './adapters/store-adapter.js';
|
|
34
37
|
import { createSpatialAdapter } from './adapters/spatial-adapter.js';
|
|
35
38
|
import { createLensAdapter } from './adapters/lens-adapter.js';
|
|
36
39
|
import { createExportAdapter } from './adapters/export-adapter.js';
|
|
37
40
|
import { createFilesAdapter } from './adapters/files-adapter.js';
|
|
41
|
+
import { createScheduleAdapter } from './adapters/schedule-adapter.js';
|
|
38
42
|
|
|
39
43
|
export class LocalBackend implements BimBackend {
|
|
40
44
|
readonly model: ModelBackendMethods;
|
|
@@ -43,38 +47,42 @@ export class LocalBackend implements BimBackend {
|
|
|
43
47
|
readonly visibility: VisibilityBackendMethods;
|
|
44
48
|
readonly viewer: ViewerBackendMethods;
|
|
45
49
|
readonly mutate: MutateBackendMethods;
|
|
50
|
+
readonly store: StoreBackendMethods;
|
|
46
51
|
readonly spatial: SpatialBackendMethods;
|
|
47
52
|
readonly export: ExportBackendMethods;
|
|
48
53
|
readonly lens: LensBackendMethods;
|
|
49
54
|
readonly files: FilesBackendMethods;
|
|
55
|
+
readonly schedule: ScheduleBackendMethods;
|
|
50
56
|
|
|
51
|
-
private
|
|
57
|
+
private storeApi: StoreApi;
|
|
52
58
|
|
|
53
59
|
constructor(store: StoreApi) {
|
|
54
|
-
this.
|
|
60
|
+
this.storeApi = store;
|
|
55
61
|
this.model = createModelAdapter(store);
|
|
56
62
|
this.query = createQueryAdapter(store);
|
|
57
63
|
this.selection = createSelectionAdapter(store);
|
|
58
64
|
this.visibility = createVisibilityAdapter(store);
|
|
59
65
|
this.viewer = createViewerAdapter(store);
|
|
60
66
|
this.mutate = createMutateAdapter(store);
|
|
67
|
+
this.store = createStoreAdapter(store);
|
|
61
68
|
this.spatial = createSpatialAdapter(store);
|
|
62
69
|
this.lens = createLensAdapter(store);
|
|
63
70
|
this.export = createExportAdapter(store);
|
|
64
71
|
this.files = createFilesAdapter(store);
|
|
72
|
+
this.schedule = createScheduleAdapter(store);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
subscribe(event: BimEventType, handler: (data: unknown) => void): () => void {
|
|
68
76
|
switch (event) {
|
|
69
77
|
case 'selection:changed':
|
|
70
|
-
return this.
|
|
78
|
+
return this.storeApi.subscribe((state, prev) => {
|
|
71
79
|
if (state.selectedEntities !== prev.selectedEntities) {
|
|
72
80
|
handler({ refs: state.selectedEntities ?? [] });
|
|
73
81
|
}
|
|
74
82
|
});
|
|
75
83
|
|
|
76
84
|
case 'model:loaded':
|
|
77
|
-
return this.
|
|
85
|
+
return this.storeApi.subscribe((state, prev) => {
|
|
78
86
|
if (state.models.size > prev.models.size) {
|
|
79
87
|
for (const [id, model] of state.models) {
|
|
80
88
|
if (!prev.models.has(id)) {
|
|
@@ -108,7 +116,7 @@ export class LocalBackend implements BimBackend {
|
|
|
108
116
|
});
|
|
109
117
|
|
|
110
118
|
case 'model:removed':
|
|
111
|
-
return this.
|
|
119
|
+
return this.storeApi.subscribe((state, prev) => {
|
|
112
120
|
if (state.models.size < prev.models.size) {
|
|
113
121
|
for (const id of prev.models.keys()) {
|
|
114
122
|
if (!state.models.has(id)) {
|
|
@@ -119,7 +127,7 @@ export class LocalBackend implements BimBackend {
|
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
case 'visibility:changed':
|
|
122
|
-
return this.
|
|
130
|
+
return this.storeApi.subscribe((state, prev) => {
|
|
123
131
|
if (
|
|
124
132
|
state.hiddenEntities !== prev.hiddenEntities ||
|
|
125
133
|
state.isolatedEntities !== prev.isolatedEntities ||
|
|
@@ -130,14 +138,14 @@ export class LocalBackend implements BimBackend {
|
|
|
130
138
|
});
|
|
131
139
|
|
|
132
140
|
case 'mutation:changed':
|
|
133
|
-
return this.
|
|
141
|
+
return this.storeApi.subscribe((state, prev) => {
|
|
134
142
|
if (state.mutationVersion !== prev.mutationVersion) {
|
|
135
143
|
handler({});
|
|
136
144
|
}
|
|
137
145
|
});
|
|
138
146
|
|
|
139
147
|
case 'lens:changed':
|
|
140
|
-
return this.
|
|
148
|
+
return this.storeApi.subscribe((state, prev) => {
|
|
141
149
|
if (state.activeLensId !== prev.activeLensId) {
|
|
142
150
|
handler({ lensId: state.activeLensId });
|
|
143
151
|
}
|
|
@@ -10,7 +10,9 @@ import { readNativeFile } from '@/services/file-dialog';
|
|
|
10
10
|
const exportHydrationByModel = new Map<string, Promise<IfcDataStore | null>>();
|
|
11
11
|
|
|
12
12
|
function isDesktopRuntime(): boolean {
|
|
13
|
-
|
|
13
|
+
// `globalThis` and `Window` aren't structurally compatible per TS, so
|
|
14
|
+
// route through `unknown` first — the cast is intentional.
|
|
15
|
+
const win = globalThis as unknown as Window & { __TAURI_INTERNALS__?: { invoke?: unknown } };
|
|
14
16
|
return typeof win.__TAURI_INTERNALS__?.invoke === 'function';
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -6,10 +6,41 @@ import type {
|
|
|
6
6
|
NativeMetadataEntityDetails,
|
|
7
7
|
NativeMetadataEntitySummary,
|
|
8
8
|
NativeMetadataSnapshot,
|
|
9
|
+
NativeMetadataSpatialNode,
|
|
9
10
|
} from '@/store/types';
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
MetadataBootstrapPayload,
|
|
13
|
+
MetadataBootstrapEntitySummary,
|
|
14
|
+
MetadataBootstrapSpatialNode,
|
|
15
|
+
} from '@ifc-lite/geometry';
|
|
11
16
|
import { getNativeModelSnapshot, setNativeModelSnapshot } from './desktop-cache';
|
|
12
17
|
|
|
18
|
+
function bootstrapKindToNative(kind: string): 'spatial' | 'element' {
|
|
19
|
+
return kind === 'spatial' ? 'spatial' : 'element';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function summaryFromBootstrap(node: MetadataBootstrapEntitySummary): NativeMetadataEntitySummary {
|
|
23
|
+
return {
|
|
24
|
+
expressId: node.expressId,
|
|
25
|
+
type: node.typeName,
|
|
26
|
+
name: node.name,
|
|
27
|
+
globalId: node.globalId ?? null,
|
|
28
|
+
kind: bootstrapKindToNative(node.kind),
|
|
29
|
+
hasChildren: node.hasChildren,
|
|
30
|
+
elementCount: node.elementCount,
|
|
31
|
+
elevation: node.elevation ?? null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function spatialNodeFromBootstrap(node: MetadataBootstrapSpatialNode | null): NativeMetadataSpatialNode | null {
|
|
36
|
+
if (!node) return null;
|
|
37
|
+
return {
|
|
38
|
+
...summaryFromBootstrap(node),
|
|
39
|
+
children: node.children.map((c) => spatialNodeFromBootstrap(c)).filter((n): n is NativeMetadataSpatialNode => n !== null),
|
|
40
|
+
elements: node.elements.map(summaryFromBootstrap),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
13
44
|
type InvokeFn = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;
|
|
14
45
|
|
|
15
46
|
async function getInvoke(): Promise<InvokeFn> {
|
|
@@ -21,13 +52,6 @@ async function getInvoke(): Promise<InvokeFn> {
|
|
|
21
52
|
return core.invoke as InvokeFn;
|
|
22
53
|
}
|
|
23
54
|
|
|
24
|
-
interface NativeMetadataBootstrapPayload {
|
|
25
|
-
cacheKey: string;
|
|
26
|
-
schemaVersion: string;
|
|
27
|
-
entityCount: number;
|
|
28
|
-
spatialTree: NativeMetadataSnapshot['spatialTree'];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
55
|
function toSchemaVersion(schemaVersion: string): NativeMetadataSnapshot['schemaVersion'] {
|
|
32
56
|
if (schemaVersion === 'IFC4X3' || schemaVersion === 'IFC4' || schemaVersion === 'IFC5') {
|
|
33
57
|
return schemaVersion;
|
|
@@ -45,24 +69,23 @@ export function nativeMetadataSnapshotFromBootstrap(
|
|
|
45
69
|
filePath: path,
|
|
46
70
|
schemaVersion: toSchemaVersion(payload.schemaVersion),
|
|
47
71
|
entityCount: payload.entityCount,
|
|
48
|
-
spatialTree: payload.spatialTree ?? null,
|
|
72
|
+
spatialTree: spatialNodeFromBootstrap(payload.spatialTree ?? null),
|
|
49
73
|
};
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
export async function bootstrapNativeMetadata(path: string, cacheKey: string): Promise<NativeMetadataSnapshot> {
|
|
53
77
|
const invoke = await getInvoke();
|
|
54
|
-
|
|
78
|
+
// The Tauri command returns a bootstrap-shaped payload (typeName /
|
|
79
|
+
// kind unfolded). Route through the shared `nativeMetadataSnapshotFromBootstrap`
|
|
80
|
+
// helper so both constructors apply the same `spatialNodeFromBootstrap`
|
|
81
|
+
// normalization — without this the `from cached snapshot` and
|
|
82
|
+
// `bootstrap fresh` paths produce subtly different shapes and the
|
|
83
|
+
// property panel reads break for the freshly-bootstrapped case.
|
|
84
|
+
const result = await invoke<MetadataBootstrapPayload>('bootstrap_native_metadata', {
|
|
55
85
|
path,
|
|
56
86
|
cacheKey,
|
|
57
87
|
});
|
|
58
|
-
return
|
|
59
|
-
mode: 'desktop-lazy',
|
|
60
|
-
cacheKey: result.cacheKey,
|
|
61
|
-
filePath: path,
|
|
62
|
-
schemaVersion: toSchemaVersion(result.schemaVersion),
|
|
63
|
-
entityCount: result.entityCount,
|
|
64
|
-
spatialTree: result.spatialTree ?? null,
|
|
65
|
-
};
|
|
88
|
+
return nativeMetadataSnapshotFromBootstrap(path, result);
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
export async function restoreNativeMetadataSnapshot(cacheKey: string): Promise<NativeMetadataSnapshot | null> {
|
|
@@ -139,7 +139,10 @@ export async function openGenericFileDialog(options: GenericFileDialogOptions =
|
|
|
139
139
|
const bytes = await readNativeFile(normalizedPath);
|
|
140
140
|
const pathSegments = normalizedPath.split(/[\\/]/);
|
|
141
141
|
const name = pathSegments[pathSegments.length - 1] || 'document';
|
|
142
|
-
|
|
142
|
+
// Slice to a fresh ArrayBuffer view — TS5+ narrows `Uint8Array` to
|
|
143
|
+
// `Uint8Array<ArrayBufferLike>` which `BlobPart` doesn't accept.
|
|
144
|
+
const blobPart = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength).slice();
|
|
145
|
+
return new File([blobPart], name, { type: 'application/octet-stream' });
|
|
143
146
|
} catch (error) {
|
|
144
147
|
console.warn('[FileDialog] Failed to open generic native file dialog:', error);
|
|
145
148
|
return null;
|
|
@@ -17,9 +17,34 @@ declare module '@tauri-apps/plugin-fs' {
|
|
|
17
17
|
export function exists(path: string): Promise<boolean>;
|
|
18
18
|
export function remove(path: string): Promise<void>;
|
|
19
19
|
export function readDir(path: string): Promise<Array<{ name: string | null }>>;
|
|
20
|
+
export function stat(path: string): Promise<{ size: number; mtime?: number | null }>;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
declare module '@tauri-apps/api/path' {
|
|
23
24
|
export function appDataDir(): Promise<string>;
|
|
24
25
|
export function join(...paths: string[]): Promise<string>;
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
declare module '@tauri-apps/api/core' {
|
|
29
|
+
/**
|
|
30
|
+
* Tauri's IPC entry point. Generic by command name.
|
|
31
|
+
*/
|
|
32
|
+
export function invoke<T = unknown>(cmd: string, args?: Record<string, unknown>): Promise<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare module '@tauri-apps/plugin-dialog' {
|
|
36
|
+
export interface OpenDialogOptions {
|
|
37
|
+
title?: string;
|
|
38
|
+
multiple?: boolean;
|
|
39
|
+
directory?: boolean;
|
|
40
|
+
defaultPath?: string;
|
|
41
|
+
filters?: Array<{ name: string; extensions: string[] }>;
|
|
42
|
+
}
|
|
43
|
+
export interface SaveDialogOptions {
|
|
44
|
+
title?: string;
|
|
45
|
+
defaultPath?: string;
|
|
46
|
+
filters?: Array<{ name: string; extensions: string[] }>;
|
|
47
|
+
}
|
|
48
|
+
export function open(options?: OpenDialogOptions): Promise<string | string[] | null>;
|
|
49
|
+
export function save(options?: SaveDialogOptions): Promise<string | null>;
|
|
50
|
+
}
|
|
@@ -341,6 +341,9 @@ function collectVisibleCandidates(state: ViewerStateSnapshot): VisibleCandidate[
|
|
|
341
341
|
if (state.models.size > 0) {
|
|
342
342
|
for (const [modelId, model] of state.models) {
|
|
343
343
|
if (!model.visible) continue;
|
|
344
|
+
// Native-metadata models have no parsed geometry result. Skip them
|
|
345
|
+
// — they can't contribute mesh-level visible candidates.
|
|
346
|
+
if (!model.geometryResult) continue;
|
|
344
347
|
const offset = model.idOffset ?? 0;
|
|
345
348
|
for (const mesh of model.geometryResult.meshes) {
|
|
346
349
|
if (!matchesTypeVisibility(mesh.ifcType, state.typeVisibility)) continue;
|
package/src/store/globalId.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import type { EntityRef, FederatedModel } from './types.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Shape accepted by `toGlobalIdFromModels` — re-export it so consumers can
|
|
8
|
+
* drop the `as unknown as Map<...>` cast when threading the store's
|
|
9
|
+
* federated `models` map through downstream helpers. */
|
|
10
|
+
export type ForwardModelMapLike = ReadonlyMap<string, { idOffset?: number }>;
|
|
8
11
|
type ReverseModelMapLike = ReadonlyMap<string, Pick<FederatedModel, 'idOffset' | 'maxExpressId'>>;
|
|
9
12
|
|
|
10
13
|
/**
|