@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
  3. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  4. package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
  5. package/dist/assets/index-yTqs8kgX.css +1 -0
  6. package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
  7. package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/package.json +18 -15
  10. package/src/components/viewer/BCFPanel.tsx +7 -789
  11. package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
  12. package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
  13. package/src/components/viewer/HierarchyPanel.tsx +110 -842
  14. package/src/components/viewer/IDSExportDialog.tsx +281 -0
  15. package/src/components/viewer/IDSPanel.tsx +126 -17
  16. package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
  17. package/src/components/viewer/LensPanel.tsx +603 -0
  18. package/src/components/viewer/MainToolbar.tsx +188 -21
  19. package/src/components/viewer/PropertiesPanel.tsx +171 -663
  20. package/src/components/viewer/PropertyEditor.tsx +866 -77
  21. package/src/components/viewer/Section2DPanel.tsx +76 -2648
  22. package/src/components/viewer/ToolOverlays.tsx +3 -1097
  23. package/src/components/viewer/ViewerLayout.tsx +132 -45
  24. package/src/components/viewer/Viewport.tsx +237 -1659
  25. package/src/components/viewer/ViewportContainer.tsx +11 -3
  26. package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
  27. package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
  28. package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
  29. package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
  30. package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
  31. package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
  32. package/src/components/viewer/hierarchy/types.ts +54 -0
  33. package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
  34. package/src/components/viewer/lists/ListBuilder.tsx +486 -0
  35. package/src/components/viewer/lists/ListPanel.tsx +540 -0
  36. package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
  37. package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
  38. package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
  39. package/src/components/viewer/properties/DocumentCard.tsx +89 -0
  40. package/src/components/viewer/properties/MaterialCard.tsx +201 -0
  41. package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
  42. package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
  43. package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
  44. package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
  45. package/src/components/viewer/properties/encodingUtils.ts +29 -0
  46. package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
  47. package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
  48. package/src/components/viewer/tools/SectionPanel.tsx +183 -0
  49. package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
  50. package/src/components/viewer/tools/formatDistance.ts +18 -0
  51. package/src/components/viewer/tools/sectionConstants.ts +14 -0
  52. package/src/components/viewer/useAnimationLoop.ts +166 -0
  53. package/src/components/viewer/useGeometryStreaming.ts +398 -0
  54. package/src/components/viewer/useKeyboardControls.ts +221 -0
  55. package/src/components/viewer/useMouseControls.ts +1009 -0
  56. package/src/components/viewer/useRenderUpdates.ts +165 -0
  57. package/src/components/viewer/useTouchControls.ts +245 -0
  58. package/src/hooks/ids/idsColorSystem.ts +125 -0
  59. package/src/hooks/ids/idsDataAccessor.ts +237 -0
  60. package/src/hooks/ids/idsExportService.ts +444 -0
  61. package/src/hooks/useBCF.ts +7 -0
  62. package/src/hooks/useDrawingExport.ts +627 -0
  63. package/src/hooks/useDrawingGeneration.ts +627 -0
  64. package/src/hooks/useFloorplanView.ts +108 -0
  65. package/src/hooks/useIDS.ts +270 -463
  66. package/src/hooks/useIfc.ts +26 -1628
  67. package/src/hooks/useIfcFederation.ts +803 -0
  68. package/src/hooks/useIfcLoader.ts +508 -0
  69. package/src/hooks/useIfcServer.ts +465 -0
  70. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  71. package/src/hooks/useLens.ts +129 -0
  72. package/src/hooks/useMeasure2D.ts +365 -0
  73. package/src/hooks/useViewControls.ts +218 -0
  74. package/src/lib/ifc4-pset-definitions.test.ts +161 -0
  75. package/src/lib/ifc4-pset-definitions.ts +621 -0
  76. package/src/lib/ifc4-qto-definitions.ts +315 -0
  77. package/src/lib/lens/adapter.ts +138 -0
  78. package/src/lib/lens/index.ts +5 -0
  79. package/src/lib/lists/adapter.ts +69 -0
  80. package/src/lib/lists/index.ts +28 -0
  81. package/src/lib/lists/persistence.ts +64 -0
  82. package/src/services/fs-cache.ts +1 -1
  83. package/src/services/tauri-modules.d.ts +25 -0
  84. package/src/store/index.ts +38 -2
  85. package/src/store/slices/cameraSlice.ts +14 -1
  86. package/src/store/slices/dataSlice.ts +14 -1
  87. package/src/store/slices/lensSlice.ts +184 -0
  88. package/src/store/slices/listSlice.ts +74 -0
  89. package/src/store/slices/pinboardSlice.ts +114 -0
  90. package/src/store/types.ts +5 -0
  91. package/src/utils/ifcConfig.ts +16 -3
  92. package/src/utils/serverDataModel.ts +64 -101
  93. package/src/vite-env.d.ts +3 -0
  94. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  95. package/dist/assets/index-v3mcCUPN.css +0 -1
@@ -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
+ });
@@ -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
  // ============================================================================
@@ -13,11 +13,24 @@ import type { DynamicBatchConfig } from '@ifc-lite/geometry';
13
13
  // Server Configuration
14
14
  // ============================================================================
15
15
 
16
- /** IFC server URL - only set via environment variable, no default (pure client-side by default) */
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
- /** Enable server parsing - only if server URL is explicitly configured */
20
- export const USE_SERVER = SERVER_URL !== '' && import.meta.env.VITE_USE_SERVER !== 'false';
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)