@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
|
@@ -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
|
+
}
|