@ifc-lite/viewer 1.6.0 → 1.7.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/CHANGELOG.md +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
package/src/store/index.ts
CHANGED
|
@@ -27,6 +27,9 @@ import { createDrawing2DSlice, type Drawing2DSlice } from './slices/drawing2DSli
|
|
|
27
27
|
import { createSheetSlice, type SheetSlice } from './slices/sheetSlice.js';
|
|
28
28
|
import { createBcfSlice, type BCFSlice } from './slices/bcfSlice.js';
|
|
29
29
|
import { createIdsSlice, type IDSSlice } from './slices/idsSlice.js';
|
|
30
|
+
import { createListSlice, type ListSlice } from './slices/listSlice.js';
|
|
31
|
+
import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
|
|
32
|
+
import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
|
|
30
33
|
|
|
31
34
|
// Import constants for reset function
|
|
32
35
|
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_DEFAULTS } from './constants.js';
|
|
@@ -35,7 +38,7 @@ import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_D
|
|
|
35
38
|
export type * from './types.js';
|
|
36
39
|
|
|
37
40
|
// Explicitly re-export multi-model types that need to be imported by name
|
|
38
|
-
export type { EntityRef, SchemaVersion, FederatedModel, MeasurementConstraintEdge } from './types.js';
|
|
41
|
+
export type { EntityRef, SchemaVersion, FederatedModel, MeasurementConstraintEdge, OrthogonalAxis } from './types.js';
|
|
39
42
|
|
|
40
43
|
// Re-export utility functions for entity references
|
|
41
44
|
export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore } from './types.js';
|
|
@@ -52,6 +55,15 @@ export type { BCFSlice, BCFSliceState } from './slices/bcfSlice.js';
|
|
|
52
55
|
// Re-export IDS types
|
|
53
56
|
export type { IDSSlice, IDSSliceState, IDSDisplayOptions, IDSFilterMode } from './slices/idsSlice.js';
|
|
54
57
|
|
|
58
|
+
// Re-export List types
|
|
59
|
+
export type { ListSlice } from './slices/listSlice.js';
|
|
60
|
+
|
|
61
|
+
// Re-export Pinboard types
|
|
62
|
+
export type { PinboardSlice } from './slices/pinboardSlice.js';
|
|
63
|
+
|
|
64
|
+
// Re-export Lens types
|
|
65
|
+
export type { LensSlice, Lens, LensRule, LensCriteria } from './slices/lensSlice.js';
|
|
66
|
+
|
|
55
67
|
// Combined store type
|
|
56
68
|
export type ViewerState = LoadingSlice &
|
|
57
69
|
SelectionSlice &
|
|
@@ -67,7 +79,10 @@ export type ViewerState = LoadingSlice &
|
|
|
67
79
|
Drawing2DSlice &
|
|
68
80
|
SheetSlice &
|
|
69
81
|
BCFSlice &
|
|
70
|
-
IDSSlice &
|
|
82
|
+
IDSSlice &
|
|
83
|
+
ListSlice &
|
|
84
|
+
PinboardSlice &
|
|
85
|
+
LensSlice & {
|
|
71
86
|
resetViewerState: () => void;
|
|
72
87
|
};
|
|
73
88
|
|
|
@@ -91,6 +106,9 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
91
106
|
...createSheetSlice(...args),
|
|
92
107
|
...createBcfSlice(...args),
|
|
93
108
|
...createIdsSlice(...args),
|
|
109
|
+
...createListSlice(...args),
|
|
110
|
+
...createPinboardSlice(...args),
|
|
111
|
+
...createLensSlice(...args),
|
|
94
112
|
|
|
95
113
|
// Reset all viewer state when loading new file
|
|
96
114
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
@@ -153,6 +171,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
153
171
|
azimuth: CAMERA_DEFAULTS.AZIMUTH,
|
|
154
172
|
elevation: CAMERA_DEFAULTS.ELEVATION,
|
|
155
173
|
},
|
|
174
|
+
projectionMode: 'perspective' as const,
|
|
156
175
|
|
|
157
176
|
// UI
|
|
158
177
|
activeTool: UI_DEFAULTS.ACTIVE_TOOL,
|
|
@@ -209,6 +228,23 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
209
228
|
idsActiveSpecificationId: null,
|
|
210
229
|
idsActiveEntityId: null,
|
|
211
230
|
// Keep idsDocument, idsValidationReport, idsLocale - user's work
|
|
231
|
+
|
|
232
|
+
// Lists - reset result but keep definitions (user's saved lists)
|
|
233
|
+
listPanelVisible: false,
|
|
234
|
+
activeListId: null,
|
|
235
|
+
listResult: null,
|
|
236
|
+
listExecuting: false,
|
|
237
|
+
|
|
238
|
+
// Pinboard - clear pinned entities on new file
|
|
239
|
+
pinboardEntities: new Set<string>(),
|
|
240
|
+
|
|
241
|
+
// Lens - deactivate but keep saved lenses
|
|
242
|
+
activeLensId: null,
|
|
243
|
+
lensPanelVisible: false,
|
|
244
|
+
lensColorMap: new Map<number, string>(),
|
|
245
|
+
lensHiddenIds: new Set<number>(),
|
|
246
|
+
lensRuleCounts: new Map<string, number>(),
|
|
247
|
+
lensRuleEntityIds: new Map<string, number[]>(),
|
|
212
248
|
});
|
|
213
249
|
},
|
|
214
250
|
}));
|
|
@@ -7,19 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { StateCreator } from 'zustand';
|
|
10
|
-
import type { CameraRotation, CameraCallbacks } from '../types.js';
|
|
10
|
+
import type { CameraRotation, CameraCallbacks, ProjectionMode } from '../types.js';
|
|
11
11
|
import { CAMERA_DEFAULTS } from '../constants.js';
|
|
12
12
|
|
|
13
13
|
export interface CameraSlice {
|
|
14
14
|
// State
|
|
15
15
|
cameraRotation: CameraRotation;
|
|
16
16
|
cameraCallbacks: CameraCallbacks;
|
|
17
|
+
projectionMode: ProjectionMode;
|
|
17
18
|
onCameraRotationChange: ((rotation: CameraRotation) => void) | null;
|
|
18
19
|
onScaleChange: ((scale: number) => void) | null;
|
|
19
20
|
|
|
20
21
|
// Actions
|
|
21
22
|
setCameraRotation: (rotation: CameraRotation) => void;
|
|
22
23
|
setCameraCallbacks: (callbacks: CameraCallbacks) => void;
|
|
24
|
+
setProjectionMode: (mode: ProjectionMode) => void;
|
|
25
|
+
toggleProjectionMode: () => void;
|
|
23
26
|
setOnCameraRotationChange: (callback: ((rotation: CameraRotation) => void) | null) => void;
|
|
24
27
|
updateCameraRotationRealtime: (rotation: CameraRotation) => void;
|
|
25
28
|
setOnScaleChange: (callback: ((scale: number) => void) | null) => void;
|
|
@@ -33,12 +36,22 @@ export const createCameraSlice: StateCreator<CameraSlice, [], [], CameraSlice> =
|
|
|
33
36
|
elevation: CAMERA_DEFAULTS.ELEVATION,
|
|
34
37
|
},
|
|
35
38
|
cameraCallbacks: {},
|
|
39
|
+
projectionMode: 'perspective',
|
|
36
40
|
onCameraRotationChange: null,
|
|
37
41
|
onScaleChange: null,
|
|
38
42
|
|
|
39
43
|
// Actions
|
|
40
44
|
setCameraRotation: (cameraRotation) => set({ cameraRotation }),
|
|
41
45
|
setCameraCallbacks: (cameraCallbacks) => set({ cameraCallbacks }),
|
|
46
|
+
setProjectionMode: (projectionMode) => {
|
|
47
|
+
get().cameraCallbacks.setProjectionMode?.(projectionMode);
|
|
48
|
+
set({ projectionMode });
|
|
49
|
+
},
|
|
50
|
+
toggleProjectionMode: () => {
|
|
51
|
+
const newMode = get().projectionMode === 'perspective' ? 'orthographic' : 'perspective';
|
|
52
|
+
get().cameraCallbacks.setProjectionMode?.(newMode);
|
|
53
|
+
set({ projectionMode: newMode });
|
|
54
|
+
},
|
|
42
55
|
setOnCameraRotationChange: (onCameraRotationChange) => set({ onCameraRotationChange }),
|
|
43
56
|
|
|
44
57
|
updateCameraRotationRealtime: (rotation) => {
|
|
@@ -22,6 +22,10 @@ export interface DataSlice {
|
|
|
22
22
|
setGeometryResult: (result: GeometryResult | null) => void;
|
|
23
23
|
appendGeometryBatch: (meshes: GeometryResult['meshes'], coordinateInfo?: CoordinateInfo) => void;
|
|
24
24
|
updateMeshColors: (updates: Map<number, [number, number, number, number]>) => void;
|
|
25
|
+
/** Set pending color updates for the renderer without cloning mesh data.
|
|
26
|
+
* Use this for transient overlays (lens, IDS) where the source-of-truth
|
|
27
|
+
* mesh colors should remain unchanged. */
|
|
28
|
+
setPendingColorUpdates: (updates: Map<number, [number, number, number, number]>) => void;
|
|
25
29
|
clearPendingColorUpdates: () => void;
|
|
26
30
|
updateCoordinateInfo: (coordinateInfo: CoordinateInfo) => void;
|
|
27
31
|
}
|
|
@@ -79,9 +83,16 @@ export const createDataSlice: StateCreator<DataSlice, [], [], DataSlice> = (set)
|
|
|
79
83
|
}),
|
|
80
84
|
|
|
81
85
|
updateMeshColors: (updates) => set((state) => {
|
|
82
|
-
if (!state.geometryResult) return {};
|
|
83
86
|
// Clone the Map to prevent external mutation of pendingColorUpdates
|
|
84
87
|
const clonedUpdates = new Map(updates);
|
|
88
|
+
|
|
89
|
+
if (!state.geometryResult) {
|
|
90
|
+
// Federation mode: no local geometryResult (geometry lives in models Map).
|
|
91
|
+
// Still set pendingColorUpdates so useGeometryStreaming applies to the WebGPU scene.
|
|
92
|
+
return { pendingColorUpdates: clonedUpdates };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Legacy/single mode: update both the data model and pending color updates
|
|
85
96
|
const updatedMeshes = state.geometryResult.meshes.map(mesh => {
|
|
86
97
|
const newColor = clonedUpdates.get(mesh.expressId);
|
|
87
98
|
if (newColor) {
|
|
@@ -98,6 +109,8 @@ export const createDataSlice: StateCreator<DataSlice, [], [], DataSlice> = (set)
|
|
|
98
109
|
};
|
|
99
110
|
}),
|
|
100
111
|
|
|
112
|
+
setPendingColorUpdates: (updates) => set({ pendingColorUpdates: new Map(updates) }),
|
|
113
|
+
|
|
101
114
|
clearPendingColorUpdates: () => set({ pendingColorUpdates: null }),
|
|
102
115
|
|
|
103
116
|
updateCoordinateInfo: (coordinateInfo) => set((state) => {
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
* Lens state slice
|
|
7
|
+
*
|
|
8
|
+
* Rule-based 3D filtering and coloring system.
|
|
9
|
+
* Types, constants, presets, and evaluation logic live in @ifc-lite/lens.
|
|
10
|
+
* This slice manages Zustand state, CRUD actions, and localStorage persistence.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { StateCreator } from 'zustand';
|
|
14
|
+
import type { Lens, LensRule, LensCriteria } from '@ifc-lite/lens';
|
|
15
|
+
import { BUILTIN_LENSES } from '@ifc-lite/lens';
|
|
16
|
+
|
|
17
|
+
// Re-export types so existing consumer imports from this file still work
|
|
18
|
+
export type { Lens, LensRule, LensCriteria };
|
|
19
|
+
|
|
20
|
+
// Re-export constants for consumers that import from this file
|
|
21
|
+
export { COMMON_IFC_CLASSES, COMMON_IFC_TYPES, LENS_PALETTE } from '@ifc-lite/lens';
|
|
22
|
+
|
|
23
|
+
/** localStorage key for persisting custom lenses */
|
|
24
|
+
const STORAGE_KEY = 'ifc-lite-custom-lenses';
|
|
25
|
+
|
|
26
|
+
/** Built-in lens IDs — used to detect overrides */
|
|
27
|
+
const BUILTIN_IDS = new Set(BUILTIN_LENSES.map(l => l.id));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load saved lenses from localStorage.
|
|
31
|
+
* Returns both custom lenses and built-in overrides (user edits to builtin lenses).
|
|
32
|
+
* Built-in overrides replace the default builtin when merging in initial state.
|
|
33
|
+
*/
|
|
34
|
+
function loadSavedLenses(): { custom: Lens[]; builtinOverrides: Map<string, Lens> } {
|
|
35
|
+
try {
|
|
36
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
37
|
+
if (!raw) return { custom: [], builtinOverrides: new Map() };
|
|
38
|
+
const parsed = JSON.parse(raw) as Lens[];
|
|
39
|
+
if (!Array.isArray(parsed)) return { custom: [], builtinOverrides: new Map() };
|
|
40
|
+
const valid = parsed.filter(l => l.id && l.name && Array.isArray(l.rules));
|
|
41
|
+
const builtinOverrides = new Map<string, Lens>();
|
|
42
|
+
const custom: Lens[] = [];
|
|
43
|
+
for (const l of valid) {
|
|
44
|
+
if (BUILTIN_IDS.has(l.id)) {
|
|
45
|
+
builtinOverrides.set(l.id, { ...l, builtin: true });
|
|
46
|
+
} else {
|
|
47
|
+
custom.push(l);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { custom, builtinOverrides };
|
|
51
|
+
} catch {
|
|
52
|
+
return { custom: [], builtinOverrides: new Map() };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Persist lenses to localStorage.
|
|
58
|
+
* Saves custom lenses + any built-in lenses the user has edited (overrides).
|
|
59
|
+
*/
|
|
60
|
+
function saveLenses(lenses: Lens[]): void {
|
|
61
|
+
try {
|
|
62
|
+
// Save non-builtin custom lenses
|
|
63
|
+
const custom = lenses.filter(l => !l.builtin);
|
|
64
|
+
// Also save built-in lenses that differ from their defaults (user overrides)
|
|
65
|
+
const builtinOverrides = lenses.filter(l => {
|
|
66
|
+
if (!l.builtin) return false;
|
|
67
|
+
const original = BUILTIN_LENSES.find(b => b.id === l.id);
|
|
68
|
+
if (!original) return false;
|
|
69
|
+
// Quick check: has the user changed the rules or name?
|
|
70
|
+
return l.name !== original.name ||
|
|
71
|
+
JSON.stringify(l.rules) !== JSON.stringify(original.rules);
|
|
72
|
+
});
|
|
73
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([...custom, ...builtinOverrides]));
|
|
74
|
+
} catch {
|
|
75
|
+
// quota exceeded or unavailable — silently ignore
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Build initial lens list: builtins (with overrides applied) + custom */
|
|
80
|
+
function buildInitialLenses(): Lens[] {
|
|
81
|
+
const { custom, builtinOverrides } = loadSavedLenses();
|
|
82
|
+
const builtins = BUILTIN_LENSES.map(l =>
|
|
83
|
+
builtinOverrides.has(l.id) ? builtinOverrides.get(l.id)! : { ...l },
|
|
84
|
+
);
|
|
85
|
+
return [...builtins, ...custom];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface LensSlice {
|
|
89
|
+
// State
|
|
90
|
+
savedLenses: Lens[];
|
|
91
|
+
activeLensId: string | null;
|
|
92
|
+
lensPanelVisible: boolean;
|
|
93
|
+
/** Computed: globalId → hex color for entities matched by active lens */
|
|
94
|
+
lensColorMap: Map<number, string>;
|
|
95
|
+
/** Computed: globalIds to hide via lens rules */
|
|
96
|
+
lensHiddenIds: Set<number>;
|
|
97
|
+
/** Computed: ruleId → matched entity count for the active lens */
|
|
98
|
+
lensRuleCounts: Map<string, number>;
|
|
99
|
+
/** Computed: ruleId → matched entity global IDs for the active lens */
|
|
100
|
+
lensRuleEntityIds: Map<string, number[]>;
|
|
101
|
+
|
|
102
|
+
// Actions
|
|
103
|
+
createLens: (lens: Lens) => void;
|
|
104
|
+
updateLens: (id: string, patch: Partial<Lens>) => void;
|
|
105
|
+
deleteLens: (id: string) => void;
|
|
106
|
+
setActiveLens: (id: string | null) => void;
|
|
107
|
+
toggleLensPanel: () => void;
|
|
108
|
+
setLensPanelVisible: (visible: boolean) => void;
|
|
109
|
+
setLensColorMap: (map: Map<number, string>) => void;
|
|
110
|
+
setLensHiddenIds: (ids: Set<number>) => void;
|
|
111
|
+
setLensRuleCounts: (counts: Map<string, number>) => void;
|
|
112
|
+
setLensRuleEntityIds: (ids: Map<string, number[]>) => void;
|
|
113
|
+
/** Get the active lens configuration */
|
|
114
|
+
getActiveLens: () => Lens | null;
|
|
115
|
+
/** Import lenses from parsed JSON array */
|
|
116
|
+
importLenses: (lenses: Lens[]) => void;
|
|
117
|
+
/** Export all lenses (builtins + custom) as serializable array */
|
|
118
|
+
exportLenses: () => Lens[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set, get) => ({
|
|
122
|
+
// Initial state — builtins (with user overrides applied) + custom lenses
|
|
123
|
+
savedLenses: buildInitialLenses(),
|
|
124
|
+
activeLensId: null,
|
|
125
|
+
lensPanelVisible: false,
|
|
126
|
+
lensColorMap: new Map(),
|
|
127
|
+
lensHiddenIds: new Set(),
|
|
128
|
+
lensRuleCounts: new Map(),
|
|
129
|
+
lensRuleEntityIds: new Map(),
|
|
130
|
+
|
|
131
|
+
// Actions
|
|
132
|
+
createLens: (lens) => set((state) => {
|
|
133
|
+
const next = [...state.savedLenses, lens];
|
|
134
|
+
saveLenses(next);
|
|
135
|
+
return { savedLenses: next };
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
updateLens: (id, patch) => set((state) => {
|
|
139
|
+
const next = state.savedLenses.map(l => l.id === id ? { ...l, ...patch } : l);
|
|
140
|
+
saveLenses(next);
|
|
141
|
+
return { savedLenses: next };
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
deleteLens: (id) => set((state) => {
|
|
145
|
+
const lens = state.savedLenses.find(l => l.id === id);
|
|
146
|
+
if (lens?.builtin) return {};
|
|
147
|
+
const next = state.savedLenses.filter(l => l.id !== id);
|
|
148
|
+
saveLenses(next);
|
|
149
|
+
return {
|
|
150
|
+
savedLenses: next,
|
|
151
|
+
activeLensId: state.activeLensId === id ? null : state.activeLensId,
|
|
152
|
+
};
|
|
153
|
+
}),
|
|
154
|
+
|
|
155
|
+
setActiveLens: (activeLensId) => set({ activeLensId }),
|
|
156
|
+
|
|
157
|
+
toggleLensPanel: () => set((state) => ({ lensPanelVisible: !state.lensPanelVisible })),
|
|
158
|
+
setLensPanelVisible: (lensPanelVisible) => set({ lensPanelVisible }),
|
|
159
|
+
|
|
160
|
+
setLensColorMap: (lensColorMap) => set({ lensColorMap }),
|
|
161
|
+
setLensHiddenIds: (lensHiddenIds) => set({ lensHiddenIds }),
|
|
162
|
+
setLensRuleCounts: (lensRuleCounts) => set({ lensRuleCounts }),
|
|
163
|
+
setLensRuleEntityIds: (lensRuleEntityIds) => set({ lensRuleEntityIds }),
|
|
164
|
+
|
|
165
|
+
getActiveLens: () => {
|
|
166
|
+
const { savedLenses, activeLensId } = get();
|
|
167
|
+
return savedLenses.find(l => l.id === activeLensId) ?? null;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
importLenses: (lenses) => set((state) => {
|
|
171
|
+
// Merge: skip duplicates by id, strip builtin flag from imports
|
|
172
|
+
const existingIds = new Set(state.savedLenses.map(l => l.id));
|
|
173
|
+
const newLenses = lenses
|
|
174
|
+
.filter(l => l.id && l.name && Array.isArray(l.rules) && !existingIds.has(l.id))
|
|
175
|
+
.map(l => ({ ...l, builtin: false }));
|
|
176
|
+
const next = [...state.savedLenses, ...newLenses];
|
|
177
|
+
saveLenses(next);
|
|
178
|
+
return { savedLenses: next };
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
exportLenses: () => {
|
|
182
|
+
return get().savedLenses.map(({ id, name, rules }) => ({ id, name, rules }));
|
|
183
|
+
},
|
|
184
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
* List state slice - configurable property tables from IFC data
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { StateCreator } from 'zustand';
|
|
10
|
+
import type { ListDefinition, ListResult } from '@ifc-lite/lists';
|
|
11
|
+
import { loadListDefinitions, saveListDefinitions } from '../../lib/lists/persistence.js';
|
|
12
|
+
|
|
13
|
+
export interface ListSlice {
|
|
14
|
+
// State
|
|
15
|
+
listDefinitions: ListDefinition[];
|
|
16
|
+
activeListId: string | null;
|
|
17
|
+
listResult: ListResult | null;
|
|
18
|
+
listPanelVisible: boolean;
|
|
19
|
+
listExecuting: boolean;
|
|
20
|
+
|
|
21
|
+
// Actions
|
|
22
|
+
setListDefinitions: (definitions: ListDefinition[]) => void;
|
|
23
|
+
addListDefinition: (definition: ListDefinition) => void;
|
|
24
|
+
updateListDefinition: (id: string, updates: Partial<ListDefinition>) => void;
|
|
25
|
+
deleteListDefinition: (id: string) => void;
|
|
26
|
+
setActiveListId: (id: string | null) => void;
|
|
27
|
+
setListResult: (result: ListResult | null) => void;
|
|
28
|
+
setListPanelVisible: (visible: boolean) => void;
|
|
29
|
+
toggleListPanel: () => void;
|
|
30
|
+
setListExecuting: (executing: boolean) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const createListSlice: StateCreator<ListSlice, [], [], ListSlice> = (set, get) => ({
|
|
34
|
+
// Initial state - load saved definitions
|
|
35
|
+
listDefinitions: loadListDefinitions(),
|
|
36
|
+
activeListId: null,
|
|
37
|
+
listResult: null,
|
|
38
|
+
listPanelVisible: false,
|
|
39
|
+
listExecuting: false,
|
|
40
|
+
|
|
41
|
+
// Actions
|
|
42
|
+
setListDefinitions: (listDefinitions) => {
|
|
43
|
+
set({ listDefinitions });
|
|
44
|
+
saveListDefinitions(listDefinitions);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
addListDefinition: (definition) => {
|
|
48
|
+
const updated = [...get().listDefinitions, definition];
|
|
49
|
+
set({ listDefinitions: updated });
|
|
50
|
+
saveListDefinitions(updated);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
updateListDefinition: (id, updates) => {
|
|
54
|
+
const updated = get().listDefinitions.map(d =>
|
|
55
|
+
d.id === id ? { ...d, ...updates, updatedAt: Date.now() } : d
|
|
56
|
+
);
|
|
57
|
+
set({ listDefinitions: updated });
|
|
58
|
+
saveListDefinitions(updated);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
deleteListDefinition: (id) => {
|
|
62
|
+
const updated = get().listDefinitions.filter(d => d.id !== id);
|
|
63
|
+
const activeListId = get().activeListId === id ? null : get().activeListId;
|
|
64
|
+
const listResult = get().activeListId === id ? null : get().listResult;
|
|
65
|
+
set({ listDefinitions: updated, activeListId, listResult });
|
|
66
|
+
saveListDefinitions(updated);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
setActiveListId: (activeListId) => set({ activeListId }),
|
|
70
|
+
setListResult: (listResult) => set({ listResult }),
|
|
71
|
+
setListPanelVisible: (listPanelVisible) => set({ listPanelVisible }),
|
|
72
|
+
toggleListPanel: () => set((state) => ({ listPanelVisible: !state.listPanelVisible })),
|
|
73
|
+
setListExecuting: (listExecuting) => set({ listExecuting }),
|
|
74
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
* Pinboard state slice
|
|
7
|
+
*
|
|
8
|
+
* Persistent selection basket for tracking components across sessions.
|
|
9
|
+
* Users can pin entities, then isolate/show the pinned set.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StateCreator } from 'zustand';
|
|
13
|
+
import type { EntityRef } from '../types.js';
|
|
14
|
+
import { entityRefToString, stringToEntityRef } from '../types.js';
|
|
15
|
+
|
|
16
|
+
/** Minimal interface for accessing isolation + models from the combined store */
|
|
17
|
+
interface CombinedStoreAccess {
|
|
18
|
+
isolateEntities?: (ids: number[]) => void;
|
|
19
|
+
models?: Map<string, { idOffset: number }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PinboardSlice {
|
|
23
|
+
// State
|
|
24
|
+
/** Serialized EntityRef strings for O(1) membership check */
|
|
25
|
+
pinboardEntities: Set<string>;
|
|
26
|
+
|
|
27
|
+
// Actions
|
|
28
|
+
/** Add entities to pinboard */
|
|
29
|
+
addToPinboard: (refs: EntityRef[]) => void;
|
|
30
|
+
/** Remove entities from pinboard */
|
|
31
|
+
removeFromPinboard: (refs: EntityRef[]) => void;
|
|
32
|
+
/** Replace pinboard contents */
|
|
33
|
+
setPinboard: (refs: EntityRef[]) => void;
|
|
34
|
+
/** Clear pinboard */
|
|
35
|
+
clearPinboard: () => void;
|
|
36
|
+
/** Isolate pinboard entities (show only pinned) */
|
|
37
|
+
showPinboard: () => void;
|
|
38
|
+
/** Check if entity is pinned */
|
|
39
|
+
isInPinboard: (ref: EntityRef) => boolean;
|
|
40
|
+
/** Get pinboard count */
|
|
41
|
+
getPinboardCount: () => number;
|
|
42
|
+
/** Get all pinboard entities as EntityRef array */
|
|
43
|
+
getPinboardEntities: () => EntityRef[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSlice> = (set, get) => ({
|
|
47
|
+
// Initial state
|
|
48
|
+
pinboardEntities: new Set(),
|
|
49
|
+
|
|
50
|
+
// Actions
|
|
51
|
+
addToPinboard: (refs) => {
|
|
52
|
+
set((state) => {
|
|
53
|
+
const next = new Set(state.pinboardEntities);
|
|
54
|
+
for (const ref of refs) {
|
|
55
|
+
next.add(entityRefToString(ref));
|
|
56
|
+
}
|
|
57
|
+
return { pinboardEntities: next };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
removeFromPinboard: (refs) => {
|
|
62
|
+
set((state) => {
|
|
63
|
+
const next = new Set(state.pinboardEntities);
|
|
64
|
+
for (const ref of refs) {
|
|
65
|
+
next.delete(entityRefToString(ref));
|
|
66
|
+
}
|
|
67
|
+
return { pinboardEntities: next };
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
setPinboard: (refs) => {
|
|
72
|
+
const next = new Set<string>();
|
|
73
|
+
for (const ref of refs) {
|
|
74
|
+
next.add(entityRefToString(ref));
|
|
75
|
+
}
|
|
76
|
+
set({ pinboardEntities: next });
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
clearPinboard: () => set({ pinboardEntities: new Set() }),
|
|
80
|
+
|
|
81
|
+
showPinboard: () => {
|
|
82
|
+
const entities = get().getPinboardEntities();
|
|
83
|
+
if (entities.length === 0) return;
|
|
84
|
+
|
|
85
|
+
// Access combined store methods via typed interface
|
|
86
|
+
const store = get() as unknown as CombinedStoreAccess;
|
|
87
|
+
if (!store.isolateEntities) return;
|
|
88
|
+
|
|
89
|
+
// Convert EntityRef to global IDs for isolation
|
|
90
|
+
const globalIds: number[] = [];
|
|
91
|
+
for (const ref of entities) {
|
|
92
|
+
if (store.models) {
|
|
93
|
+
const model = store.models.get(ref.modelId);
|
|
94
|
+
const offset = model?.idOffset ?? 0;
|
|
95
|
+
globalIds.push(ref.expressId + offset);
|
|
96
|
+
} else {
|
|
97
|
+
globalIds.push(ref.expressId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
store.isolateEntities(globalIds);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
isInPinboard: (ref) => get().pinboardEntities.has(entityRefToString(ref)),
|
|
104
|
+
|
|
105
|
+
getPinboardCount: () => get().pinboardEntities.size,
|
|
106
|
+
|
|
107
|
+
getPinboardEntities: () => {
|
|
108
|
+
const result: EntityRef[] = [];
|
|
109
|
+
for (const str of get().pinboardEntities) {
|
|
110
|
+
result.push(stringToEntityRef(str));
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
},
|
|
114
|
+
});
|
package/src/store/types.ts
CHANGED
|
@@ -149,6 +149,8 @@ export interface CameraRotation {
|
|
|
149
149
|
elevation: number;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export type ProjectionMode = 'perspective' | 'orthographic';
|
|
153
|
+
|
|
152
154
|
export interface CameraCallbacks {
|
|
153
155
|
setPresetView?: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right') => void;
|
|
154
156
|
fitAll?: () => void;
|
|
@@ -158,6 +160,9 @@ export interface CameraCallbacks {
|
|
|
158
160
|
frameSelection?: () => void;
|
|
159
161
|
orbit?: (deltaX: number, deltaY: number) => void;
|
|
160
162
|
projectToScreen?: (worldPos: { x: number; y: number; z: number }) => { x: number; y: number } | null;
|
|
163
|
+
setProjectionMode?: (mode: ProjectionMode) => void;
|
|
164
|
+
toggleProjectionMode?: () => void;
|
|
165
|
+
getProjectionMode?: () => ProjectionMode;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
// ============================================================================
|
package/src/utils/ifcConfig.ts
CHANGED
|
@@ -13,11 +13,24 @@ import type { DynamicBatchConfig } from '@ifc-lite/geometry';
|
|
|
13
13
|
// Server Configuration
|
|
14
14
|
// ============================================================================
|
|
15
15
|
|
|
16
|
-
/** IFC server URL -
|
|
16
|
+
/** IFC server URL - set via environment variable for server-side IFC processing */
|
|
17
17
|
export const SERVER_URL = import.meta.env.VITE_IFC_SERVER_URL || import.meta.env.VITE_SERVER_URL || '';
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Enable server-side IFC parsing (disabled by default — uses client-side WASM).
|
|
21
|
+
*
|
|
22
|
+
* The server URL may be present for other features (e.g. superset integration)
|
|
23
|
+
* without intending to route normal IFC loading through it.
|
|
24
|
+
*
|
|
25
|
+
* To enable server-side IFC processing for development:
|
|
26
|
+
* 1. Set VITE_IFC_SERVER_URL (or VITE_SERVER_URL) to the server endpoint
|
|
27
|
+
* 2. Set VITE_USE_SERVER=true
|
|
28
|
+
*
|
|
29
|
+
* Example .env:
|
|
30
|
+
* VITE_IFC_SERVER_URL=https://ifc-server.example.com
|
|
31
|
+
* VITE_USE_SERVER=true
|
|
32
|
+
*/
|
|
33
|
+
export const USE_SERVER = SERVER_URL !== '' && import.meta.env.VITE_USE_SERVER === 'true';
|
|
21
34
|
|
|
22
35
|
// ============================================================================
|
|
23
36
|
// File Size Thresholds (in bytes unless noted)
|