@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
@@ -0,0 +1,280 @@
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
+ import { useMemo, useState, useCallback, useEffect } from 'react';
6
+ import type { IfcDataStore } from '@ifc-lite/parser';
7
+ import type { GeometryResult } from '@ifc-lite/geometry';
8
+ import type { FederatedModel } from '@/store';
9
+ import type { TreeNode, UnifiedStorey } from './types';
10
+ import {
11
+ buildUnifiedStoreys,
12
+ getUnifiedStoreyElements as getUnifiedStoreyElementsFn,
13
+ buildTreeData,
14
+ buildTypeTree,
15
+ filterNodes,
16
+ splitNodes,
17
+ } from './treeDataBuilder';
18
+
19
+ export type GroupingMode = 'spatial' | 'type';
20
+
21
+ interface UseHierarchyTreeParams {
22
+ models: Map<string, FederatedModel>;
23
+ ifcDataStore: IfcDataStore | null | undefined;
24
+ isMultiModel: boolean;
25
+ geometryResult?: GeometryResult | null;
26
+ }
27
+
28
+ /**
29
+ * Build a stable Set of global IDs that have geometry.
30
+ * Only rebuilds when the actual set of IDs changes, NOT when mesh colors change.
31
+ */
32
+ function buildGeometricIdSet(
33
+ models: Map<string, FederatedModel>,
34
+ legacyGeometry: GeometryResult | null | undefined,
35
+ ): Set<number> {
36
+ const ids = new Set<number>();
37
+ if (models.size > 0) {
38
+ for (const [, model] of models) {
39
+ if (model.geometryResult) {
40
+ for (const mesh of model.geometryResult.meshes) {
41
+ ids.add(mesh.expressId);
42
+ }
43
+ }
44
+ }
45
+ } else if (legacyGeometry) {
46
+ for (const mesh of legacyGeometry.meshes) {
47
+ ids.add(mesh.expressId);
48
+ }
49
+ }
50
+ return ids;
51
+ }
52
+
53
+ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryResult }: UseHierarchyTreeParams) {
54
+ const [searchQuery, setSearchQuery] = useState('');
55
+ const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
56
+ const [hasInitializedExpansion, setHasInitializedExpansion] = useState(false);
57
+ const [groupingMode, setGroupingMode] = useState<GroupingMode>(() =>
58
+ (typeof window !== 'undefined' && localStorage.getItem('hierarchy-grouping') as GroupingMode) || 'spatial'
59
+ );
60
+
61
+ // Build unified storey data for multi-model mode (moved before useEffect that depends on it)
62
+ const unifiedStoreys = useMemo(
63
+ (): UnifiedStorey[] => buildUnifiedStoreys(models),
64
+ [models]
65
+ );
66
+
67
+ // Auto-expand nodes on initial load based on model count
68
+ useEffect(() => {
69
+ // Only run once when data is first loaded
70
+ if (hasInitializedExpansion) return;
71
+
72
+ const newExpanded = new Set<string>();
73
+
74
+ if (models.size === 1) {
75
+ // Single model in federation: expand full hierarchy to show all storeys
76
+ const [, model] = Array.from(models.entries())[0];
77
+ const hierarchy = model.ifcDataStore?.spatialHierarchy;
78
+
79
+ // Wait until spatial hierarchy is computed before initializing
80
+ if (!hierarchy?.project) {
81
+ return; // Don't mark as initialized - will retry when hierarchy is ready
82
+ }
83
+
84
+ // Expand Project -> Site -> Building to reveal storeys
85
+ const project = hierarchy.project;
86
+ const projectNodeId = `root-${project.expressId}`;
87
+ newExpanded.add(projectNodeId);
88
+
89
+ for (const site of project.children || []) {
90
+ const siteNodeId = `${projectNodeId}-${site.expressId}`;
91
+ newExpanded.add(siteNodeId);
92
+
93
+ for (const building of site.children || []) {
94
+ const buildingNodeId = `${siteNodeId}-${building.expressId}`;
95
+ newExpanded.add(buildingNodeId);
96
+ }
97
+ }
98
+ } else if (models.size > 1) {
99
+ // Multi-model: expand all model entries in Models section
100
+ // But collapse if there are too many items (rough estimate based on viewport)
101
+ const totalItems = unifiedStoreys.length + models.size;
102
+ const estimatedRowHeight = 36;
103
+ const availableHeight = window.innerHeight * 0.6; // Estimate panel takes ~60% of viewport
104
+ const maxVisibleItems = Math.floor(availableHeight / estimatedRowHeight);
105
+
106
+ if (totalItems <= maxVisibleItems) {
107
+ // Enough space - expand all model entries
108
+ for (const [modelId] of models) {
109
+ newExpanded.add(`model-${modelId}`);
110
+ }
111
+ }
112
+ // If not enough space, leave collapsed (newExpanded stays empty for models)
113
+ } else if (models.size === 0 && ifcDataStore?.spatialHierarchy?.project) {
114
+ // Legacy single-model mode (loaded via loadFile, not in models Map)
115
+ const hierarchy = ifcDataStore.spatialHierarchy;
116
+ const project = hierarchy.project;
117
+ const projectNodeId = `root-${project.expressId}`;
118
+ newExpanded.add(projectNodeId);
119
+
120
+ for (const site of project.children || []) {
121
+ const siteNodeId = `${projectNodeId}-${site.expressId}`;
122
+ newExpanded.add(siteNodeId);
123
+
124
+ for (const building of site.children || []) {
125
+ const buildingNodeId = `${siteNodeId}-${building.expressId}`;
126
+ newExpanded.add(buildingNodeId);
127
+ }
128
+ }
129
+ } else {
130
+ // No data loaded yet
131
+ return;
132
+ }
133
+
134
+ if (newExpanded.size > 0) {
135
+ setExpandedNodes(newExpanded);
136
+ }
137
+ setHasInitializedExpansion(true);
138
+ }, [models, ifcDataStore, hasInitializedExpansion, unifiedStoreys.length]);
139
+
140
+ // Reset expansion state when all data is cleared
141
+ useEffect(() => {
142
+ if (models.size === 0 && !ifcDataStore) {
143
+ setHasInitializedExpansion(false);
144
+ setExpandedNodes(new Set());
145
+ }
146
+ }, [models.size, ifcDataStore]);
147
+
148
+ // Get all element IDs for a unified storey (as global IDs)
149
+ const getUnifiedStoreyElements = useCallback(
150
+ (unifiedStorey: UnifiedStorey): number[] => getUnifiedStoreyElementsFn(unifiedStorey, models),
151
+ [models]
152
+ );
153
+
154
+ // Stable mesh count — only changes when models are added/removed, not on color updates.
155
+ // Used as a dep proxy so the geometric ID set doesn't rebuild on every color change.
156
+ const meshCount = useMemo(() => {
157
+ if (models.size > 0) {
158
+ let count = 0;
159
+ for (const [, model] of models) {
160
+ count += model.geometryResult?.meshes.length ?? 0;
161
+ }
162
+ return count;
163
+ }
164
+ return geometryResult?.meshes.length ?? 0;
165
+ }, [models, geometryResult?.meshes.length]);
166
+
167
+ // Pre-computed set of global IDs with geometry — stable across color changes
168
+ const geometricIds = useMemo(
169
+ () => buildGeometricIdSet(models, geometryResult),
170
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- meshCount is a stable proxy
171
+ [models, meshCount]
172
+ );
173
+
174
+ // Build the tree data structure based on grouping mode
175
+ // Note: hiddenEntities intentionally NOT in deps - visibility computed lazily for performance
176
+ const treeData = useMemo(
177
+ (): TreeNode[] => {
178
+ if (groupingMode === 'type') {
179
+ return buildTypeTree(models, ifcDataStore, expandedNodes, isMultiModel, geometricIds);
180
+ }
181
+ return buildTreeData(models, ifcDataStore, expandedNodes, isMultiModel, unifiedStoreys);
182
+ },
183
+ [models, ifcDataStore, expandedNodes, isMultiModel, unifiedStoreys, groupingMode, geometricIds]
184
+ );
185
+
186
+ // Filter nodes based on search
187
+ const filteredNodes = useMemo(
188
+ () => filterNodes(treeData, searchQuery),
189
+ [treeData, searchQuery]
190
+ );
191
+
192
+ // Split filtered nodes into storeys and models sections (for multi-model mode)
193
+ const { storeysNodes, modelsNodes } = useMemo(
194
+ () => splitNodes(filteredNodes, isMultiModel),
195
+ [filteredNodes, isMultiModel]
196
+ );
197
+
198
+ const toggleExpand = useCallback((nodeId: string) => {
199
+ setExpandedNodes(prev => {
200
+ const next = new Set(prev);
201
+ if (next.has(nodeId)) {
202
+ next.delete(nodeId);
203
+ } else {
204
+ next.add(nodeId);
205
+ }
206
+ return next;
207
+ });
208
+ }, []);
209
+
210
+ // Get all elements for a node (handles type groups, unified storeys, single storeys, model contributions, and elements)
211
+ const getNodeElements = useCallback((node: TreeNode): number[] => {
212
+ if (node.type === 'type-group') {
213
+ // GlobalIds are pre-stored on the node during tree construction — O(1)
214
+ return node.expressIds;
215
+ }
216
+ if (node.type === 'unified-storey') {
217
+ // Get all elements from all models for this unified storey
218
+ const unified = unifiedStoreys.find(u => `unified-${u.key}` === node.id);
219
+ if (unified) {
220
+ return getUnifiedStoreyElements(unified);
221
+ }
222
+ } else if (node.type === 'model-header' && node.id.startsWith('contrib-')) {
223
+ // Model contribution header inside a unified storey - get elements for this model's storey
224
+ const storeyId = node.expressIds[0];
225
+ const modelId = node.modelIds[0];
226
+ const model = models.get(modelId);
227
+ if (model?.ifcDataStore?.spatialHierarchy) {
228
+ const localIds = (model.ifcDataStore.spatialHierarchy.byStorey.get(storeyId) as number[]) || [];
229
+ // Convert local expressIds to global IDs using model's idOffset
230
+ const offset = model.idOffset ?? 0;
231
+ return localIds.map(id => id + offset);
232
+ }
233
+ } else if (node.type === 'IfcBuildingStorey') {
234
+ // Get storey elements
235
+ const storeyId = node.expressIds[0];
236
+ const modelId = node.modelIds[0];
237
+
238
+ // Try legacy dataStore first (no offset needed, IDs are already global)
239
+ if (ifcDataStore?.spatialHierarchy) {
240
+ const elements = ifcDataStore.spatialHierarchy.byStorey.get(storeyId);
241
+ if (elements) return elements as number[];
242
+ }
243
+
244
+ // Or from the model in federation - need to apply idOffset
245
+ const model = models.get(modelId);
246
+ if (model?.ifcDataStore?.spatialHierarchy) {
247
+ const localIds = (model.ifcDataStore.spatialHierarchy.byStorey.get(storeyId) as number[]) || [];
248
+ const offset = model.idOffset ?? 0;
249
+ return localIds.map(id => id + offset);
250
+ }
251
+ } else if (node.type === 'element') {
252
+ return node.expressIds;
253
+ }
254
+ // Spatial containers (Project, Site, Building) and top-level models don't have direct element visibility toggle
255
+ return [];
256
+ }, [models, ifcDataStore, unifiedStoreys, getUnifiedStoreyElements]);
257
+
258
+ // Persist grouping mode preference
259
+ const handleSetGroupingMode = useCallback((mode: GroupingMode) => {
260
+ setGroupingMode(mode);
261
+ if (typeof window !== 'undefined') {
262
+ localStorage.setItem('hierarchy-grouping', mode);
263
+ }
264
+ }, []);
265
+
266
+ return {
267
+ searchQuery,
268
+ setSearchQuery,
269
+ groupingMode,
270
+ setGroupingMode: handleSetGroupingMode,
271
+ unifiedStoreys,
272
+ treeData,
273
+ filteredNodes,
274
+ storeysNodes,
275
+ modelsNodes,
276
+ toggleExpand,
277
+ getNodeElements,
278
+ getUnifiedStoreyElements,
279
+ };
280
+ }