@ifc-lite/viewer 1.7.0 → 1.9.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 +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
* useBimHost — React hook that initializes the SDK and BimHost.
|
|
7
|
+
*
|
|
8
|
+
* This hook:
|
|
9
|
+
* 1. Creates a LocalBackend backed by the Zustand store
|
|
10
|
+
* 2. Creates a BimContext (the `bim` object)
|
|
11
|
+
* 3. Starts a BimHost listening on BroadcastChannel 'ifc-lite'
|
|
12
|
+
* 4. External tools (ifc-scripts, ifc-flow) can connect to control the viewer
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* function App() {
|
|
16
|
+
* const bim = useBimHost();
|
|
17
|
+
* // bim is available for internal use
|
|
18
|
+
* // External tools can connect via BroadcastChannel 'ifc-lite'
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useRef, useEffect, useMemo } from 'react';
|
|
23
|
+
import { createBimContext, BimHost, type BimContext } from '@ifc-lite/sdk';
|
|
24
|
+
import { useViewerStore } from '../store/index.js';
|
|
25
|
+
import { LocalBackend } from './local-backend.js';
|
|
26
|
+
|
|
27
|
+
const BROADCAST_CHANNEL = 'ifc-lite';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the SDK with a local backend and start the BimHost.
|
|
31
|
+
* Returns the BimContext for internal use.
|
|
32
|
+
*/
|
|
33
|
+
export function useBimHost(): BimContext {
|
|
34
|
+
const hostRef = useRef<BimHost | null>(null);
|
|
35
|
+
const backendRef = useRef<LocalBackend | null>(null);
|
|
36
|
+
|
|
37
|
+
// Create local backend and BimContext once — single shared backend
|
|
38
|
+
const bim = useMemo(() => {
|
|
39
|
+
const storeApi = {
|
|
40
|
+
getState: useViewerStore.getState,
|
|
41
|
+
subscribe: useViewerStore.subscribe,
|
|
42
|
+
};
|
|
43
|
+
const backend = new LocalBackend(storeApi);
|
|
44
|
+
backendRef.current = backend;
|
|
45
|
+
return createBimContext({ backend });
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Start BimHost for external connections — reuse the same backend
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const backend = backendRef.current;
|
|
51
|
+
if (!backend) return;
|
|
52
|
+
const host = new BimHost(backend);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
host.listenBroadcast(BROADCAST_CHANNEL);
|
|
56
|
+
} catch {
|
|
57
|
+
// BroadcastChannel not available (e.g., in some test environments)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hostRef.current = host;
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
host.close();
|
|
64
|
+
hostRef.current = null;
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return bim;
|
|
69
|
+
}
|
package/src/store/constants.ts
CHANGED
|
@@ -51,11 +51,19 @@ export const EDGE_LOCK_DEFAULTS = {
|
|
|
51
51
|
// UI Defaults
|
|
52
52
|
// ============================================================================
|
|
53
53
|
|
|
54
|
+
/** Resolve the initial theme: localStorage override > system preference > dark fallback */
|
|
55
|
+
function getInitialTheme(): 'light' | 'dark' {
|
|
56
|
+
if (typeof window === 'undefined') return 'dark';
|
|
57
|
+
const saved = localStorage.getItem('ifc-lite-theme');
|
|
58
|
+
if (saved === 'light' || saved === 'dark') return saved;
|
|
59
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
export const UI_DEFAULTS = {
|
|
55
63
|
/** Default active tool */
|
|
56
64
|
ACTIVE_TOOL: 'select',
|
|
57
|
-
/** Default theme */
|
|
58
|
-
THEME:
|
|
65
|
+
/** Default theme – respects user's OS colour-scheme preference */
|
|
66
|
+
THEME: getInitialTheme(),
|
|
59
67
|
/** Default hover tooltips state */
|
|
60
68
|
HOVER_TOOLTIPS_ENABLED: false,
|
|
61
69
|
} as const;
|
package/src/store/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { createIdsSlice, type IDSSlice } from './slices/idsSlice.js';
|
|
|
30
30
|
import { createListSlice, type ListSlice } from './slices/listSlice.js';
|
|
31
31
|
import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
|
|
32
32
|
import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
|
|
33
|
+
import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
|
|
33
34
|
|
|
34
35
|
// Import constants for reset function
|
|
35
36
|
import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_DEFAULTS } from './constants.js';
|
|
@@ -43,8 +44,11 @@ export type { EntityRef, SchemaVersion, FederatedModel, MeasurementConstraintEdg
|
|
|
43
44
|
// Re-export utility functions for entity references
|
|
44
45
|
export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore } from './types.js';
|
|
45
46
|
|
|
47
|
+
// Re-export single source of truth for globalId → EntityRef resolution
|
|
48
|
+
export { resolveEntityRef } from './resolveEntityRef.js';
|
|
49
|
+
|
|
46
50
|
// Re-export Drawing2D types
|
|
47
|
-
export type { Drawing2DState, Drawing2DStatus } from './slices/drawing2DSlice.js';
|
|
51
|
+
export type { Drawing2DState, Drawing2DStatus, Annotation2DTool, PolygonArea2DResult, TextAnnotation2D, CloudAnnotation2D, SelectedAnnotation2D } from './slices/drawing2DSlice.js';
|
|
48
52
|
|
|
49
53
|
// Re-export Sheet types
|
|
50
54
|
export type { SheetState } from './slices/sheetSlice.js';
|
|
@@ -64,6 +68,9 @@ export type { PinboardSlice } from './slices/pinboardSlice.js';
|
|
|
64
68
|
// Re-export Lens types
|
|
65
69
|
export type { LensSlice, Lens, LensRule, LensCriteria } from './slices/lensSlice.js';
|
|
66
70
|
|
|
71
|
+
// Re-export Script types
|
|
72
|
+
export type { ScriptSlice } from './slices/scriptSlice.js';
|
|
73
|
+
|
|
67
74
|
// Combined store type
|
|
68
75
|
export type ViewerState = LoadingSlice &
|
|
69
76
|
SelectionSlice &
|
|
@@ -82,7 +89,8 @@ export type ViewerState = LoadingSlice &
|
|
|
82
89
|
IDSSlice &
|
|
83
90
|
ListSlice &
|
|
84
91
|
PinboardSlice &
|
|
85
|
-
LensSlice &
|
|
92
|
+
LensSlice &
|
|
93
|
+
ScriptSlice & {
|
|
86
94
|
resetViewerState: () => void;
|
|
87
95
|
};
|
|
88
96
|
|
|
@@ -109,6 +117,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
109
117
|
...createListSlice(...args),
|
|
110
118
|
...createPinboardSlice(...args),
|
|
111
119
|
...createLensSlice(...args),
|
|
120
|
+
...createScriptSlice(...args),
|
|
112
121
|
|
|
113
122
|
// Reset all viewer state when loading new file
|
|
114
123
|
// Note: Does NOT clear models - use clearAllModels() for that
|
|
@@ -205,6 +214,16 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
205
214
|
measure2DLockedAxis: null,
|
|
206
215
|
measure2DResults: [],
|
|
207
216
|
measure2DSnapPoint: null,
|
|
217
|
+
// Annotation tools
|
|
218
|
+
annotation2DActiveTool: 'none' as const,
|
|
219
|
+
annotation2DCursorPos: null,
|
|
220
|
+
polygonArea2DPoints: [],
|
|
221
|
+
polygonArea2DResults: [],
|
|
222
|
+
textAnnotations2D: [],
|
|
223
|
+
textAnnotation2DEditing: null,
|
|
224
|
+
cloudAnnotation2DPoints: [],
|
|
225
|
+
cloudAnnotations2D: [],
|
|
226
|
+
selectedAnnotation2D: null,
|
|
208
227
|
// Drawing Sheet
|
|
209
228
|
activeSheet: null,
|
|
210
229
|
sheetEnabled: false,
|
|
@@ -238,6 +257,13 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
|
|
|
238
257
|
// Pinboard - clear pinned entities on new file
|
|
239
258
|
pinboardEntities: new Set<string>(),
|
|
240
259
|
|
|
260
|
+
// Script - reset execution state but keep saved scripts and editor content
|
|
261
|
+
scriptPanelVisible: false,
|
|
262
|
+
scriptExecutionState: 'idle' as const,
|
|
263
|
+
scriptLastResult: null,
|
|
264
|
+
scriptLastError: null,
|
|
265
|
+
scriptDeleteConfirmId: null,
|
|
266
|
+
|
|
241
267
|
// Lens - deactivate but keep saved lenses
|
|
242
268
|
activeLensId: null,
|
|
243
269
|
lensPanelVisible: false,
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
* Single source of truth for resolving a globalId to an EntityRef.
|
|
7
|
+
*
|
|
8
|
+
* Every code path that needs an EntityRef from a globalId MUST use this
|
|
9
|
+
* function. It guarantees consistent modelId values so that basket
|
|
10
|
+
* add/remove keys always match, regardless of which UI surface triggered
|
|
11
|
+
* the selection.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EntityRef } from './types.js';
|
|
15
|
+
import { useViewerStore } from './index.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a globalId (renderer-space) to an EntityRef (model-space).
|
|
19
|
+
*
|
|
20
|
+
* Resolution order:
|
|
21
|
+
* 1. resolveGlobalIdFromModels (offset-based range check — the canonical path)
|
|
22
|
+
* 2. First loaded model as fallback (single-model, offset 0)
|
|
23
|
+
* 3. 'legacy' sentinel for truly legacy single-model mode (no federation map)
|
|
24
|
+
*
|
|
25
|
+
* ALWAYS returns an EntityRef — never null. This ensures all callers
|
|
26
|
+
* (multi-select, basket, context menu) can proceed without null-guards.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveEntityRef(globalId: number): EntityRef {
|
|
29
|
+
const state = useViewerStore.getState();
|
|
30
|
+
const resolved = state.resolveGlobalIdFromModels(globalId);
|
|
31
|
+
if (resolved) {
|
|
32
|
+
return { modelId: resolved.modelId, expressId: resolved.expressId };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fallback: single-model mode where offset is 0 → globalId === expressId
|
|
36
|
+
if (state.models.size > 0) {
|
|
37
|
+
const firstModelId = state.models.keys().next().value as string;
|
|
38
|
+
return { modelId: firstModelId, expressId: globalId };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Legacy single-model mode: no models in federation map yet.
|
|
42
|
+
// 'legacy' is recognized by PropertiesPanel for fallback to legacy ifcDataStore.
|
|
43
|
+
return { modelId: 'legacy', expressId: globalId };
|
|
44
|
+
}
|
|
@@ -15,6 +15,9 @@ import { BUILT_IN_PRESETS } from '@ifc-lite/drawing-2d';
|
|
|
15
15
|
|
|
16
16
|
export type Drawing2DStatus = 'idle' | 'generating' | 'ready' | 'error';
|
|
17
17
|
|
|
18
|
+
/** Active 2D annotation tool */
|
|
19
|
+
export type Annotation2DTool = 'none' | 'measure' | 'polygon-area' | 'text' | 'cloud';
|
|
20
|
+
|
|
18
21
|
/** Point in 2D drawing coordinates */
|
|
19
22
|
export interface Point2D {
|
|
20
23
|
x: number;
|
|
@@ -29,6 +32,39 @@ export interface Measure2DResult {
|
|
|
29
32
|
distance: number; // in drawing units (typically meters)
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** Polygon area measurement result */
|
|
36
|
+
export interface PolygonArea2DResult {
|
|
37
|
+
id: string;
|
|
38
|
+
points: Point2D[]; // Closed polygon vertices (drawing coords)
|
|
39
|
+
area: number; // Computed area in m²
|
|
40
|
+
perimeter: number; // Computed perimeter in m
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Text box annotation */
|
|
44
|
+
export interface TextAnnotation2D {
|
|
45
|
+
id: string;
|
|
46
|
+
position: Point2D; // Top-left corner (drawing coords)
|
|
47
|
+
text: string;
|
|
48
|
+
fontSize: number; // Font size in screen px (default 14)
|
|
49
|
+
color: string; // Text color (default '#000000')
|
|
50
|
+
backgroundColor: string; // Background fill
|
|
51
|
+
borderColor: string; // Border color
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Cloud (revision cloud) annotation */
|
|
55
|
+
export interface CloudAnnotation2D {
|
|
56
|
+
id: string;
|
|
57
|
+
points: Point2D[]; // Rectangle corners (drawing coords, 2 points: topLeft, bottomRight)
|
|
58
|
+
color: string; // Cloud stroke color (default '#E53935')
|
|
59
|
+
label: string; // Optional label text inside cloud
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Reference to a selected annotation */
|
|
63
|
+
export interface SelectedAnnotation2D {
|
|
64
|
+
type: 'measure' | 'polygon' | 'text' | 'cloud';
|
|
65
|
+
id: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
export interface Drawing2DState {
|
|
33
69
|
/** Current drawing data (null when not generated) */
|
|
34
70
|
drawing2D: Drawing2D | null;
|
|
@@ -80,6 +116,34 @@ export interface Drawing2DState {
|
|
|
80
116
|
measure2DResults: Measure2DResult[];
|
|
81
117
|
/** Current snap point (if snapping to geometry) */
|
|
82
118
|
measure2DSnapPoint: Point2D | null;
|
|
119
|
+
|
|
120
|
+
// Annotation Tool System
|
|
121
|
+
/** Active annotation tool (none = pan mode) */
|
|
122
|
+
annotation2DActiveTool: Annotation2DTool;
|
|
123
|
+
/** Current cursor position in drawing coords for preview rendering */
|
|
124
|
+
annotation2DCursorPos: Point2D | null;
|
|
125
|
+
|
|
126
|
+
// Polygon Area Measurement
|
|
127
|
+
/** Points being placed for in-progress polygon */
|
|
128
|
+
polygonArea2DPoints: Point2D[];
|
|
129
|
+
/** Completed polygon area measurements */
|
|
130
|
+
polygonArea2DResults: PolygonArea2DResult[];
|
|
131
|
+
|
|
132
|
+
// Text Annotations
|
|
133
|
+
/** Placed text annotations */
|
|
134
|
+
textAnnotations2D: TextAnnotation2D[];
|
|
135
|
+
/** ID of text annotation currently being edited (null = none) */
|
|
136
|
+
textAnnotation2DEditing: string | null;
|
|
137
|
+
|
|
138
|
+
// Cloud Annotations
|
|
139
|
+
/** Rectangle corners being placed for in-progress cloud (0-2 points) */
|
|
140
|
+
cloudAnnotation2DPoints: Point2D[];
|
|
141
|
+
/** Completed cloud annotations */
|
|
142
|
+
cloudAnnotations2D: CloudAnnotation2D[];
|
|
143
|
+
|
|
144
|
+
// Selection
|
|
145
|
+
/** Currently selected annotation (null = none) */
|
|
146
|
+
selectedAnnotation2D: SelectedAnnotation2D | null;
|
|
83
147
|
}
|
|
84
148
|
|
|
85
149
|
export interface Drawing2DSlice extends Drawing2DState {
|
|
@@ -121,6 +185,45 @@ export interface Drawing2DSlice extends Drawing2DState {
|
|
|
121
185
|
completeMeasure2D: () => void;
|
|
122
186
|
/** Cancel current measurement */
|
|
123
187
|
cancelMeasure2D: () => void;
|
|
188
|
+
|
|
189
|
+
// Annotation Tool Actions
|
|
190
|
+
/** Set active annotation tool (also manages measure2DMode for backward compat) */
|
|
191
|
+
setAnnotation2DActiveTool: (tool: Annotation2DTool) => void;
|
|
192
|
+
/** Update cursor position for annotation previews */
|
|
193
|
+
setAnnotation2DCursorPos: (pos: Point2D | null) => void;
|
|
194
|
+
|
|
195
|
+
// Polygon Area Actions
|
|
196
|
+
addPolygonArea2DPoint: (point: Point2D) => void;
|
|
197
|
+
completePolygonArea2D: (area: number, perimeter: number) => void;
|
|
198
|
+
cancelPolygonArea2D: () => void;
|
|
199
|
+
removePolygonArea2DResult: (id: string) => void;
|
|
200
|
+
clearPolygonArea2DResults: () => void;
|
|
201
|
+
|
|
202
|
+
// Text Annotation Actions
|
|
203
|
+
addTextAnnotation2D: (annotation: TextAnnotation2D) => void;
|
|
204
|
+
updateTextAnnotation2D: (id: string, updates: Partial<TextAnnotation2D>) => void;
|
|
205
|
+
removeTextAnnotation2D: (id: string) => void;
|
|
206
|
+
setTextAnnotation2DEditing: (id: string | null) => void;
|
|
207
|
+
clearTextAnnotations2D: () => void;
|
|
208
|
+
|
|
209
|
+
// Cloud Annotation Actions
|
|
210
|
+
addCloudAnnotation2DPoint: (point: Point2D) => void;
|
|
211
|
+
completeCloudAnnotation2D: (label?: string) => void;
|
|
212
|
+
cancelCloudAnnotation2D: () => void;
|
|
213
|
+
removeCloudAnnotation2D: (id: string) => void;
|
|
214
|
+
clearCloudAnnotations2D: () => void;
|
|
215
|
+
|
|
216
|
+
// Selection Actions
|
|
217
|
+
/** Set the selected annotation (null to deselect) */
|
|
218
|
+
setSelectedAnnotation2D: (sel: SelectedAnnotation2D | null) => void;
|
|
219
|
+
/** Delete the currently selected annotation */
|
|
220
|
+
deleteSelectedAnnotation2D: () => void;
|
|
221
|
+
/** Move an annotation to a new origin position (used during drag) */
|
|
222
|
+
moveAnnotation2D: (sel: SelectedAnnotation2D, newOrigin: Point2D) => void;
|
|
223
|
+
|
|
224
|
+
// Bulk Actions
|
|
225
|
+
/** Clear all annotations (measurements, polygons, text, clouds) */
|
|
226
|
+
clearAllAnnotations2D: () => void;
|
|
124
227
|
}
|
|
125
228
|
|
|
126
229
|
const getDefaultDisplayOptions = (): Drawing2DState['drawing2DDisplayOptions'] => ({
|
|
@@ -155,6 +258,17 @@ const getDefaultState = (): Drawing2DState => ({
|
|
|
155
258
|
measure2DLockedAxis: null,
|
|
156
259
|
measure2DResults: [],
|
|
157
260
|
measure2DSnapPoint: null,
|
|
261
|
+
// Annotation tools
|
|
262
|
+
annotation2DActiveTool: 'none',
|
|
263
|
+
annotation2DCursorPos: null,
|
|
264
|
+
polygonArea2DPoints: [],
|
|
265
|
+
polygonArea2DResults: [],
|
|
266
|
+
textAnnotations2D: [],
|
|
267
|
+
textAnnotation2DEditing: null,
|
|
268
|
+
cloudAnnotation2DPoints: [],
|
|
269
|
+
cloudAnnotations2D: [],
|
|
270
|
+
// Selection
|
|
271
|
+
selectedAnnotation2D: null,
|
|
158
272
|
});
|
|
159
273
|
|
|
160
274
|
export const createDrawing2DSlice: StateCreator<Drawing2DSlice, [], [], Drawing2DSlice> = (set, get) => ({
|
|
@@ -337,4 +451,211 @@ export const createDrawing2DSlice: StateCreator<Drawing2DSlice, [], [], Drawing2
|
|
|
337
451
|
measure2DLockedAxis: null,
|
|
338
452
|
measure2DSnapPoint: null,
|
|
339
453
|
}),
|
|
454
|
+
|
|
455
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
456
|
+
// ANNOTATION TOOL ACTIONS
|
|
457
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
458
|
+
|
|
459
|
+
setAnnotation2DActiveTool: (tool) => {
|
|
460
|
+
const state = get();
|
|
461
|
+
// Cancel any in-progress work from previous tool
|
|
462
|
+
const resetState: Partial<Drawing2DState> = {
|
|
463
|
+
annotation2DActiveTool: tool,
|
|
464
|
+
annotation2DCursorPos: null,
|
|
465
|
+
// Keep measure2DMode in sync for backward compatibility
|
|
466
|
+
measure2DMode: tool === 'measure',
|
|
467
|
+
// Clear in-progress state from all tools
|
|
468
|
+
measure2DStart: null,
|
|
469
|
+
measure2DCurrent: null,
|
|
470
|
+
measure2DShiftLocked: false,
|
|
471
|
+
measure2DLockedAxis: null,
|
|
472
|
+
measure2DSnapPoint: null,
|
|
473
|
+
polygonArea2DPoints: [],
|
|
474
|
+
cloudAnnotation2DPoints: [],
|
|
475
|
+
textAnnotation2DEditing: null,
|
|
476
|
+
selectedAnnotation2D: null,
|
|
477
|
+
};
|
|
478
|
+
set(resetState);
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
setAnnotation2DCursorPos: (pos) => set({ annotation2DCursorPos: pos }),
|
|
482
|
+
|
|
483
|
+
// Polygon Area Actions
|
|
484
|
+
addPolygonArea2DPoint: (point) => set((state) => ({
|
|
485
|
+
polygonArea2DPoints: [...state.polygonArea2DPoints, point],
|
|
486
|
+
})),
|
|
487
|
+
|
|
488
|
+
completePolygonArea2D: (area, perimeter) => {
|
|
489
|
+
const state = get();
|
|
490
|
+
if (state.polygonArea2DPoints.length < 3) return;
|
|
491
|
+
|
|
492
|
+
const result: PolygonArea2DResult = {
|
|
493
|
+
id: `poly-area-${Date.now()}`,
|
|
494
|
+
points: [...state.polygonArea2DPoints],
|
|
495
|
+
area,
|
|
496
|
+
perimeter,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
set({
|
|
500
|
+
polygonArea2DResults: [...state.polygonArea2DResults, result],
|
|
501
|
+
polygonArea2DPoints: [],
|
|
502
|
+
annotation2DCursorPos: null,
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
cancelPolygonArea2D: () => set({
|
|
507
|
+
polygonArea2DPoints: [],
|
|
508
|
+
annotation2DCursorPos: null,
|
|
509
|
+
}),
|
|
510
|
+
|
|
511
|
+
removePolygonArea2DResult: (id) => set((state) => ({
|
|
512
|
+
polygonArea2DResults: state.polygonArea2DResults.filter((r) => r.id !== id),
|
|
513
|
+
})),
|
|
514
|
+
|
|
515
|
+
clearPolygonArea2DResults: () => set({ polygonArea2DResults: [] }),
|
|
516
|
+
|
|
517
|
+
// Text Annotation Actions
|
|
518
|
+
addTextAnnotation2D: (annotation) => set((state) => ({
|
|
519
|
+
textAnnotations2D: [...state.textAnnotations2D, annotation],
|
|
520
|
+
})),
|
|
521
|
+
|
|
522
|
+
updateTextAnnotation2D: (id, updates) => set((state) => ({
|
|
523
|
+
textAnnotations2D: state.textAnnotations2D.map((a) =>
|
|
524
|
+
a.id === id ? { ...a, ...updates } : a
|
|
525
|
+
),
|
|
526
|
+
})),
|
|
527
|
+
|
|
528
|
+
removeTextAnnotation2D: (id) => set((state) => ({
|
|
529
|
+
textAnnotations2D: state.textAnnotations2D.filter((a) => a.id !== id),
|
|
530
|
+
textAnnotation2DEditing: state.textAnnotation2DEditing === id ? null : state.textAnnotation2DEditing,
|
|
531
|
+
})),
|
|
532
|
+
|
|
533
|
+
setTextAnnotation2DEditing: (id) => set({ textAnnotation2DEditing: id }),
|
|
534
|
+
|
|
535
|
+
clearTextAnnotations2D: () => set({
|
|
536
|
+
textAnnotations2D: [],
|
|
537
|
+
textAnnotation2DEditing: null,
|
|
538
|
+
}),
|
|
539
|
+
|
|
540
|
+
// Cloud Annotation Actions
|
|
541
|
+
addCloudAnnotation2DPoint: (point) => set((state) => ({
|
|
542
|
+
cloudAnnotation2DPoints: [...state.cloudAnnotation2DPoints, point],
|
|
543
|
+
})),
|
|
544
|
+
|
|
545
|
+
completeCloudAnnotation2D: (label = '') => {
|
|
546
|
+
const state = get();
|
|
547
|
+
if (state.cloudAnnotation2DPoints.length < 2) return;
|
|
548
|
+
|
|
549
|
+
const result: CloudAnnotation2D = {
|
|
550
|
+
id: `cloud-${Date.now()}`,
|
|
551
|
+
points: [...state.cloudAnnotation2DPoints],
|
|
552
|
+
color: '#E53935',
|
|
553
|
+
label,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
set({
|
|
557
|
+
cloudAnnotations2D: [...state.cloudAnnotations2D, result],
|
|
558
|
+
cloudAnnotation2DPoints: [],
|
|
559
|
+
annotation2DCursorPos: null,
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
cancelCloudAnnotation2D: () => set({
|
|
564
|
+
cloudAnnotation2DPoints: [],
|
|
565
|
+
annotation2DCursorPos: null,
|
|
566
|
+
}),
|
|
567
|
+
|
|
568
|
+
removeCloudAnnotation2D: (id) => set((state) => ({
|
|
569
|
+
cloudAnnotations2D: state.cloudAnnotations2D.filter((a) => a.id !== id),
|
|
570
|
+
})),
|
|
571
|
+
|
|
572
|
+
clearCloudAnnotations2D: () => set({ cloudAnnotations2D: [] }),
|
|
573
|
+
|
|
574
|
+
// Selection Actions
|
|
575
|
+
setSelectedAnnotation2D: (sel) => set({ selectedAnnotation2D: sel }),
|
|
576
|
+
|
|
577
|
+
deleteSelectedAnnotation2D: () => {
|
|
578
|
+
const state = get();
|
|
579
|
+
const sel = state.selectedAnnotation2D;
|
|
580
|
+
if (!sel) return;
|
|
581
|
+
|
|
582
|
+
switch (sel.type) {
|
|
583
|
+
case 'measure':
|
|
584
|
+
set({ measure2DResults: state.measure2DResults.filter((r) => r.id !== sel.id), selectedAnnotation2D: null });
|
|
585
|
+
break;
|
|
586
|
+
case 'polygon':
|
|
587
|
+
set({ polygonArea2DResults: state.polygonArea2DResults.filter((r) => r.id !== sel.id), selectedAnnotation2D: null });
|
|
588
|
+
break;
|
|
589
|
+
case 'text':
|
|
590
|
+
set({
|
|
591
|
+
textAnnotations2D: state.textAnnotations2D.filter((a) => a.id !== sel.id),
|
|
592
|
+
selectedAnnotation2D: null,
|
|
593
|
+
textAnnotation2DEditing: state.textAnnotation2DEditing === sel.id ? null : state.textAnnotation2DEditing,
|
|
594
|
+
});
|
|
595
|
+
break;
|
|
596
|
+
case 'cloud':
|
|
597
|
+
set({ cloudAnnotations2D: state.cloudAnnotations2D.filter((a) => a.id !== sel.id), selectedAnnotation2D: null });
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
|
|
602
|
+
moveAnnotation2D: (sel, newOrigin) => {
|
|
603
|
+
const state = get();
|
|
604
|
+
switch (sel.type) {
|
|
605
|
+
case 'measure': {
|
|
606
|
+
const result = state.measure2DResults.find((r) => r.id === sel.id);
|
|
607
|
+
if (!result) return;
|
|
608
|
+
const dx = newOrigin.x - result.start.x;
|
|
609
|
+
const dy = newOrigin.y - result.start.y;
|
|
610
|
+
set({ measure2DResults: state.measure2DResults.map((r) =>
|
|
611
|
+
r.id === sel.id ? { ...r, start: { x: r.start.x + dx, y: r.start.y + dy }, end: { x: r.end.x + dx, y: r.end.y + dy } } : r
|
|
612
|
+
) });
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'polygon': {
|
|
616
|
+
const result = state.polygonArea2DResults.find((r) => r.id === sel.id);
|
|
617
|
+
if (!result) return;
|
|
618
|
+
const dx = newOrigin.x - result.points[0].x;
|
|
619
|
+
const dy = newOrigin.y - result.points[0].y;
|
|
620
|
+
set({ polygonArea2DResults: state.polygonArea2DResults.map((r) =>
|
|
621
|
+
r.id === sel.id ? { ...r, points: r.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) } : r
|
|
622
|
+
) });
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
case 'text': {
|
|
626
|
+
set({ textAnnotations2D: state.textAnnotations2D.map((a) =>
|
|
627
|
+
a.id === sel.id ? { ...a, position: newOrigin } : a
|
|
628
|
+
) });
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case 'cloud': {
|
|
632
|
+
const cloud = state.cloudAnnotations2D.find((a) => a.id === sel.id);
|
|
633
|
+
if (!cloud || cloud.points.length < 2) return;
|
|
634
|
+
const dx = newOrigin.x - cloud.points[0].x;
|
|
635
|
+
const dy = newOrigin.y - cloud.points[0].y;
|
|
636
|
+
set({ cloudAnnotations2D: state.cloudAnnotations2D.map((a) =>
|
|
637
|
+
a.id === sel.id ? { ...a, points: a.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) } : a
|
|
638
|
+
) });
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
// Bulk Actions
|
|
645
|
+
clearAllAnnotations2D: () => set({
|
|
646
|
+
measure2DResults: [],
|
|
647
|
+
measure2DStart: null,
|
|
648
|
+
measure2DCurrent: null,
|
|
649
|
+
measure2DShiftLocked: false,
|
|
650
|
+
measure2DLockedAxis: null,
|
|
651
|
+
measure2DSnapPoint: null,
|
|
652
|
+
polygonArea2DPoints: [],
|
|
653
|
+
polygonArea2DResults: [],
|
|
654
|
+
textAnnotations2D: [],
|
|
655
|
+
textAnnotation2DEditing: null,
|
|
656
|
+
cloudAnnotation2DPoints: [],
|
|
657
|
+
cloudAnnotations2D: [],
|
|
658
|
+
annotation2DCursorPos: null,
|
|
659
|
+
selectedAnnotation2D: null,
|
|
660
|
+
}),
|
|
340
661
|
});
|
|
@@ -11,18 +11,24 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { StateCreator } from 'zustand';
|
|
14
|
-
import type { Lens, LensRule, LensCriteria } from '@ifc-lite/lens';
|
|
14
|
+
import type { Lens, LensRule, LensCriteria, AutoColorSpec, AutoColorLegendEntry, DiscoveredLensData } from '@ifc-lite/lens';
|
|
15
15
|
import { BUILTIN_LENSES } from '@ifc-lite/lens';
|
|
16
16
|
|
|
17
17
|
// Re-export types so existing consumer imports from this file still work
|
|
18
|
-
export type { Lens, LensRule, LensCriteria };
|
|
18
|
+
export type { Lens, LensRule, LensCriteria, AutoColorSpec, AutoColorLegendEntry, DiscoveredLensData };
|
|
19
19
|
|
|
20
20
|
// Re-export constants for consumers that import from this file
|
|
21
|
-
export {
|
|
21
|
+
export {
|
|
22
|
+
COMMON_IFC_CLASSES, COMMON_IFC_TYPES, LENS_PALETTE,
|
|
23
|
+
LENS_CRITERIA_TYPES, AUTO_COLOR_SOURCES, ENTITY_ATTRIBUTE_NAMES,
|
|
24
|
+
} from '@ifc-lite/lens';
|
|
22
25
|
|
|
23
26
|
/** localStorage key for persisting custom lenses */
|
|
24
27
|
const STORAGE_KEY = 'ifc-lite-custom-lenses';
|
|
25
28
|
|
|
29
|
+
/** Ephemeral lens ID created when coloring from list column headers */
|
|
30
|
+
export const AUTO_COLOR_FROM_LIST_ID = 'auto-color-from-list';
|
|
31
|
+
|
|
26
32
|
/** Built-in lens IDs — used to detect overrides */
|
|
27
33
|
const BUILTIN_IDS = new Set(BUILTIN_LENSES.map(l => l.id));
|
|
28
34
|
|
|
@@ -98,6 +104,10 @@ export interface LensSlice {
|
|
|
98
104
|
lensRuleCounts: Map<string, number>;
|
|
99
105
|
/** Computed: ruleId → matched entity global IDs for the active lens */
|
|
100
106
|
lensRuleEntityIds: Map<string, number[]>;
|
|
107
|
+
/** Auto-color legend entries (one per distinct value) for UI display */
|
|
108
|
+
lensAutoColorLegend: AutoColorLegendEntry[];
|
|
109
|
+
/** Discovered data from loaded models (classes instant, rest lazy) */
|
|
110
|
+
discoveredLensData: DiscoveredLensData | null;
|
|
101
111
|
|
|
102
112
|
// Actions
|
|
103
113
|
createLens: (lens: Lens) => void;
|
|
@@ -110,12 +120,18 @@ export interface LensSlice {
|
|
|
110
120
|
setLensHiddenIds: (ids: Set<number>) => void;
|
|
111
121
|
setLensRuleCounts: (counts: Map<string, number>) => void;
|
|
112
122
|
setLensRuleEntityIds: (ids: Map<string, number[]>) => void;
|
|
123
|
+
setLensAutoColorLegend: (legend: AutoColorLegendEntry[]) => void;
|
|
124
|
+
setDiscoveredLensData: (data: DiscoveredLensData | null) => void;
|
|
125
|
+
/** Merge lazy-discovered data sources (psets, quantities, etc.) into existing discovered data */
|
|
126
|
+
mergeDiscoveredData: (patch: Partial<DiscoveredLensData>) => void;
|
|
113
127
|
/** Get the active lens configuration */
|
|
114
128
|
getActiveLens: () => Lens | null;
|
|
115
129
|
/** Import lenses from parsed JSON array */
|
|
116
130
|
importLenses: (lenses: Lens[]) => void;
|
|
117
131
|
/** Export all lenses (builtins + custom) as serializable array */
|
|
118
132
|
exportLenses: () => Lens[];
|
|
133
|
+
/** Create and activate an auto-color lens from a data column spec */
|
|
134
|
+
activateAutoColorFromColumn: (spec: AutoColorSpec, label: string) => void;
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set, get) => ({
|
|
@@ -127,6 +143,8 @@ export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set,
|
|
|
127
143
|
lensHiddenIds: new Set(),
|
|
128
144
|
lensRuleCounts: new Map(),
|
|
129
145
|
lensRuleEntityIds: new Map(),
|
|
146
|
+
lensAutoColorLegend: [],
|
|
147
|
+
discoveredLensData: null,
|
|
130
148
|
|
|
131
149
|
// Actions
|
|
132
150
|
createLens: (lens) => set((state) => {
|
|
@@ -161,6 +179,12 @@ export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set,
|
|
|
161
179
|
setLensHiddenIds: (lensHiddenIds) => set({ lensHiddenIds }),
|
|
162
180
|
setLensRuleCounts: (lensRuleCounts) => set({ lensRuleCounts }),
|
|
163
181
|
setLensRuleEntityIds: (lensRuleEntityIds) => set({ lensRuleEntityIds }),
|
|
182
|
+
setLensAutoColorLegend: (lensAutoColorLegend) => set({ lensAutoColorLegend }),
|
|
183
|
+
setDiscoveredLensData: (discoveredLensData) => set({ discoveredLensData }),
|
|
184
|
+
mergeDiscoveredData: (patch) => set((state) => {
|
|
185
|
+
if (!state.discoveredLensData) return {};
|
|
186
|
+
return { discoveredLensData: { ...state.discoveredLensData, ...patch } };
|
|
187
|
+
}),
|
|
164
188
|
|
|
165
189
|
getActiveLens: () => {
|
|
166
190
|
const { savedLenses, activeLensId } = get();
|
|
@@ -179,6 +203,24 @@ export const createLensSlice: StateCreator<LensSlice, [], [], LensSlice> = (set,
|
|
|
179
203
|
}),
|
|
180
204
|
|
|
181
205
|
exportLenses: () => {
|
|
182
|
-
return get().savedLenses.map(({ id, name, rules }) =>
|
|
206
|
+
return get().savedLenses.map(({ id, name, rules, autoColor }) => {
|
|
207
|
+
const out: Lens = { id, name, rules };
|
|
208
|
+
if (autoColor) out.autoColor = autoColor;
|
|
209
|
+
return out;
|
|
210
|
+
});
|
|
183
211
|
},
|
|
212
|
+
|
|
213
|
+
activateAutoColorFromColumn: (spec, label) => set((state) => {
|
|
214
|
+
const lensId = AUTO_COLOR_FROM_LIST_ID;
|
|
215
|
+
const lens: Lens = {
|
|
216
|
+
id: lensId,
|
|
217
|
+
name: `Color by ${label}`,
|
|
218
|
+
rules: [],
|
|
219
|
+
autoColor: spec,
|
|
220
|
+
};
|
|
221
|
+
// Replace existing ephemeral lens or add new
|
|
222
|
+
const filtered = state.savedLenses.filter(l => l.id !== lensId);
|
|
223
|
+
const next = [...filtered, lens];
|
|
224
|
+
return { savedLenses: next, activeLensId: lensId, lensPanelVisible: true };
|
|
225
|
+
}),
|
|
184
226
|
});
|