@ifc-lite/viewer 1.1.6 → 1.5.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/LICENSE +373 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
- package/dist/assets/arrow2-bb-jcVEo.js +2 -0
- package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
- package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
- package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
- package/dist/assets/event-DIOks52T.js +1 -0
- package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-Dgd6vzw_.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
- package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
- package/dist/favicon-16x16-cropped.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-192x192-cropped.png +0 -0
- package/dist/favicon-192x192.png +0 -0
- package/dist/favicon-32x32-cropped.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48-cropped.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon-512x512-cropped.png +0 -0
- package/dist/favicon-512x512.png +0 -0
- package/dist/favicon-64x64-cropped.png +0 -0
- package/dist/favicon-64x64.png +0 -0
- package/dist/favicon-96x96-cropped.png +0 -0
- package/dist/favicon-96x96.png +0 -0
- package/dist/favicon-square-512.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +3 -0
- package/dist/index.html +44 -0
- package/dist/logo.png +0 -0
- package/dist/manifest.json +48 -0
- package/index.html +33 -2
- package/package.json +34 -17
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16-cropped.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-192x192-cropped.png +0 -0
- package/public/favicon-192x192.png +0 -0
- package/public/favicon-32x32-cropped.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48-cropped.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon-512x512-cropped.png +0 -0
- package/public/favicon-512x512.png +0 -0
- package/public/favicon-64x64-cropped.png +0 -0
- package/public/favicon-64x64.png +0 -0
- package/public/favicon-96x96-cropped.png +0 -0
- package/public/favicon-96x96.png +0 -0
- package/public/favicon-square-512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +3 -0
- package/public/logo.png +0 -0
- package/public/manifest.json +48 -0
- package/src/App.tsx +2 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/BCFPanel.tsx +1164 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
- package/src/components/viewer/DataConnector.tsx +840 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
- package/src/components/viewer/EntityContextMenu.tsx +45 -17
- package/src/components/viewer/ExportChangesButton.tsx +195 -0
- package/src/components/viewer/ExportDialog.tsx +402 -0
- package/src/components/viewer/HierarchyPanel.tsx +1132 -218
- package/src/components/viewer/IDSPanel.tsx +661 -0
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
- package/src/components/viewer/MainToolbar.tsx +418 -94
- package/src/components/viewer/PropertiesPanel.tsx +1355 -91
- package/src/components/viewer/PropertyEditor.tsx +611 -0
- package/src/components/viewer/Section2DPanel.tsx +3313 -0
- package/src/components/viewer/SheetSetupPanel.tsx +502 -0
- package/src/components/viewer/StatusBar.tsx +27 -16
- package/src/components/viewer/TitleBlockEditor.tsx +437 -0
- package/src/components/viewer/ToolOverlays.tsx +935 -127
- package/src/components/viewer/ViewerLayout.tsx +40 -11
- package/src/components/viewer/Viewport.tsx +1276 -336
- package/src/components/viewer/ViewportContainer.tsx +554 -18
- package/src/components/viewer/ViewportOverlays.tsx +24 -7
- package/src/hooks/useBCF.ts +504 -0
- package/src/hooks/useIDS.ts +1065 -0
- package/src/hooks/useIfc.ts +1534 -205
- package/src/hooks/useIfcCache.ts +279 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -8
- package/src/hooks/useModelSelection.ts +61 -0
- package/src/hooks/useViewerSelectors.ts +218 -0
- package/src/hooks/useWebGPU.ts +80 -0
- package/src/index.css +265 -27
- package/src/lib/platform.ts +23 -0
- package/src/services/cacheService.ts +142 -0
- package/src/services/desktop-cache.ts +143 -0
- package/src/services/fs-cache.ts +212 -0
- package/src/services/ifc-cache.ts +14 -6
- package/src/store/constants.ts +85 -0
- package/src/store/index.ts +214 -0
- package/src/store/slices/bcfSlice.ts +372 -0
- package/src/store/slices/cameraSlice.ts +63 -0
- package/src/store/slices/dataSlice.test.ts +226 -0
- package/src/store/slices/dataSlice.ts +112 -0
- package/src/store/slices/drawing2DSlice.ts +340 -0
- package/src/store/slices/hoverSlice.ts +40 -0
- package/src/store/slices/idsSlice.ts +310 -0
- package/src/store/slices/loadingSlice.ts +33 -0
- package/src/store/slices/measurementSlice.test.ts +217 -0
- package/src/store/slices/measurementSlice.ts +293 -0
- package/src/store/slices/modelSlice.test.ts +271 -0
- package/src/store/slices/modelSlice.ts +211 -0
- package/src/store/slices/mutationSlice.ts +502 -0
- package/src/store/slices/sectionSlice.test.ts +125 -0
- package/src/store/slices/sectionSlice.ts +58 -0
- package/src/store/slices/selectionSlice.test.ts +286 -0
- package/src/store/slices/selectionSlice.ts +263 -0
- package/src/store/slices/sheetSlice.ts +565 -0
- package/src/store/slices/uiSlice.ts +58 -0
- package/src/store/slices/visibilitySlice.test.ts +304 -0
- package/src/store/slices/visibilitySlice.ts +277 -0
- package/src/store/types.test.ts +135 -0
- package/src/store/types.ts +248 -0
- package/src/store.ts +40 -515
- package/src/utils/ifcConfig.ts +82 -0
- package/src/utils/localParsingUtils.ts +287 -0
- package/src/utils/serverDataModel.ts +783 -0
- package/src/utils/spatialHierarchy.ts +283 -0
- package/src/utils/viewportUtils.ts +334 -0
- package/src/vite-env.d.ts +23 -0
- package/src/webgpu-types.d.ts +128 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +7 -0
- package/src-tauri/capabilities/default.json +18 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +21 -0
- package/src-tauri/src/main.rs +10 -0
- package/src-tauri/tauri.conf.json +39 -0
- package/vite.config.ts +174 -26
- package/public/ifc-lite_bg.wasm +0 -0
- package/public/web-ifc.wasm +0 -0
- package/src/components/Viewport.tsx +0 -723
- package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
* Spatial hierarchy utilities for IFC models
|
|
7
|
+
* Pure functions for building spatial structure from entities and relationships
|
|
8
|
+
* Extracted from useIfc.ts for reusability and testability
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
IfcTypeEnum,
|
|
13
|
+
RelationshipType,
|
|
14
|
+
type SpatialHierarchy,
|
|
15
|
+
type SpatialNode,
|
|
16
|
+
type EntityTable,
|
|
17
|
+
type RelationshipGraph,
|
|
18
|
+
} from '@ifc-lite/data';
|
|
19
|
+
|
|
20
|
+
// Spatial structure types that form the IFC containment hierarchy
|
|
21
|
+
const SPATIAL_TYPES = new Set([
|
|
22
|
+
IfcTypeEnum.IfcProject,
|
|
23
|
+
IfcTypeEnum.IfcSite,
|
|
24
|
+
IfcTypeEnum.IfcBuilding,
|
|
25
|
+
IfcTypeEnum.IfcBuildingStorey,
|
|
26
|
+
IfcTypeEnum.IfcSpace,
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rebuild spatial hierarchy from cache data (entities + relationships)
|
|
31
|
+
* OPTIMIZED: Uses index maps for O(1) lookups instead of O(n) linear searches
|
|
32
|
+
*
|
|
33
|
+
* @param entities - Entity table from parsed IFC
|
|
34
|
+
* @param relationships - Relationship graph from parsed IFC
|
|
35
|
+
* @returns Spatial hierarchy or undefined if no project found
|
|
36
|
+
*/
|
|
37
|
+
export function rebuildSpatialHierarchy(
|
|
38
|
+
entities: EntityTable,
|
|
39
|
+
relationships: RelationshipGraph
|
|
40
|
+
): SpatialHierarchy | undefined {
|
|
41
|
+
// PRE-BUILD INDEX MAP: O(n) once, then O(1) lookups
|
|
42
|
+
// This eliminates the O(n²) nested loops from before
|
|
43
|
+
const entityTypeMap = new Map<number, IfcTypeEnum>();
|
|
44
|
+
for (let i = 0; i < entities.count; i++) {
|
|
45
|
+
entityTypeMap.set(entities.expressId[i], entities.typeEnum[i]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const byStorey = new Map<number, number[]>();
|
|
49
|
+
const byBuilding = new Map<number, number[]>();
|
|
50
|
+
const bySite = new Map<number, number[]>();
|
|
51
|
+
const bySpace = new Map<number, number[]>();
|
|
52
|
+
const storeyElevations = new Map<number, number>();
|
|
53
|
+
const storeyHeights = new Map<number, number>();
|
|
54
|
+
const elementToStorey = new Map<number, number>();
|
|
55
|
+
|
|
56
|
+
// Find IfcProject
|
|
57
|
+
const projectIds = entities.getByType(IfcTypeEnum.IfcProject);
|
|
58
|
+
if (projectIds.length === 0) {
|
|
59
|
+
console.warn('[rebuildSpatialHierarchy] No IfcProject found');
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const projectId = projectIds[0];
|
|
63
|
+
|
|
64
|
+
// Build node tree recursively - NOW O(1) lookups!
|
|
65
|
+
function buildNode(expressId: number): SpatialNode {
|
|
66
|
+
// O(1) lookup instead of O(n) linear search
|
|
67
|
+
const typeEnum = entityTypeMap.get(expressId) ?? IfcTypeEnum.Unknown;
|
|
68
|
+
const name = entities.getName(expressId) || `Entity #${expressId}`;
|
|
69
|
+
|
|
70
|
+
// Get contained elements via IfcRelContainedInSpatialStructure
|
|
71
|
+
const rawContainedElements = relationships.getRelated(
|
|
72
|
+
expressId,
|
|
73
|
+
RelationshipType.ContainsElements,
|
|
74
|
+
'forward'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Filter out spatial structure elements - O(1) per element now!
|
|
78
|
+
const containedElements = rawContainedElements.filter((id) => {
|
|
79
|
+
const elemType = entityTypeMap.get(id);
|
|
80
|
+
return elemType !== undefined && !SPATIAL_TYPES.has(elemType);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Get aggregated children via IfcRelAggregates
|
|
84
|
+
const aggregatedChildren = relationships.getRelated(
|
|
85
|
+
expressId,
|
|
86
|
+
RelationshipType.Aggregates,
|
|
87
|
+
'forward'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Filter to spatial structure types and recurse - O(1) per child now!
|
|
91
|
+
const childNodes: SpatialNode[] = [];
|
|
92
|
+
for (const childId of aggregatedChildren) {
|
|
93
|
+
const childType = entityTypeMap.get(childId);
|
|
94
|
+
if (childType && SPATIAL_TYPES.has(childType) && childType !== IfcTypeEnum.IfcProject) {
|
|
95
|
+
childNodes.push(buildNode(childId));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add elements to appropriate maps
|
|
100
|
+
if (typeEnum === IfcTypeEnum.IfcBuildingStorey) {
|
|
101
|
+
byStorey.set(expressId, containedElements);
|
|
102
|
+
} else if (typeEnum === IfcTypeEnum.IfcBuilding) {
|
|
103
|
+
byBuilding.set(expressId, containedElements);
|
|
104
|
+
} else if (typeEnum === IfcTypeEnum.IfcSite) {
|
|
105
|
+
bySite.set(expressId, containedElements);
|
|
106
|
+
} else if (typeEnum === IfcTypeEnum.IfcSpace) {
|
|
107
|
+
bySpace.set(expressId, containedElements);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
expressId,
|
|
112
|
+
type: typeEnum,
|
|
113
|
+
name,
|
|
114
|
+
children: childNodes,
|
|
115
|
+
elements: containedElements,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const projectNode = buildNode(projectId);
|
|
120
|
+
|
|
121
|
+
// Build reverse lookup map: elementId -> storeyId
|
|
122
|
+
for (const [storeyId, elementIds] of byStorey) {
|
|
123
|
+
for (const elementId of elementIds) {
|
|
124
|
+
elementToStorey.set(elementId, storeyId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Pre-build space lookup for O(1) getContainingSpace
|
|
129
|
+
const elementToSpace = new Map<number, number>();
|
|
130
|
+
for (const [spaceId, elementIds] of bySpace) {
|
|
131
|
+
for (const elementId of elementIds) {
|
|
132
|
+
elementToSpace.set(elementId, spaceId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Note: storeyHeights remains empty for cache path - client uses on-demand property extraction
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
project: projectNode,
|
|
140
|
+
byStorey,
|
|
141
|
+
byBuilding,
|
|
142
|
+
bySite,
|
|
143
|
+
bySpace,
|
|
144
|
+
storeyElevations,
|
|
145
|
+
storeyHeights,
|
|
146
|
+
elementToStorey,
|
|
147
|
+
|
|
148
|
+
getStoreyElements(storeyId: number): number[] {
|
|
149
|
+
return byStorey.get(storeyId) ?? [];
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
getStoreyByElevation(): number | null {
|
|
153
|
+
return null;
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
getContainingSpace(elementId: number): number | null {
|
|
157
|
+
return elementToSpace.get(elementId) ?? null;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
getPath(elementId: number): SpatialNode[] {
|
|
161
|
+
const path: SpatialNode[] = [];
|
|
162
|
+
|
|
163
|
+
// DFS to find element in spatial tree
|
|
164
|
+
// Elements can be in SpatialNode.elements (e.g., IfcSpace) even if not in elementToStorey
|
|
165
|
+
const findPath = (node: SpatialNode, targetId: number): boolean => {
|
|
166
|
+
path.push(node);
|
|
167
|
+
if (node.elements.includes(targetId)) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
for (const child of node.children) {
|
|
171
|
+
if (findPath(child, targetId)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
path.pop();
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
findPath(projectNode, elementId);
|
|
180
|
+
return path;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Entity index type for property/quantity set lookup
|
|
187
|
+
*/
|
|
188
|
+
export interface EntityIndex {
|
|
189
|
+
byId: Map<number, unknown>;
|
|
190
|
+
byType: Map<string, number[]>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Result of rebuilding on-demand maps
|
|
195
|
+
*/
|
|
196
|
+
export interface OnDemandMaps {
|
|
197
|
+
onDemandPropertyMap: Map<number, number[]>;
|
|
198
|
+
onDemandQuantityMap: Map<number, number[]>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Rebuild on-demand property/quantity maps from relationships and entity types
|
|
203
|
+
* Uses FORWARD direction: pset -> elements (more efficient than inverse lookup)
|
|
204
|
+
* OPTIMIZED: Uses entityIndex.byType for property/quantity set lookup since
|
|
205
|
+
* the entity table may not include these types (filtered during fresh parse)
|
|
206
|
+
*
|
|
207
|
+
* @param entities - Entity table from parsed IFC
|
|
208
|
+
* @param relationships - Relationship graph from parsed IFC
|
|
209
|
+
* @param entityIndex - Optional entity index with byType map for cache loads
|
|
210
|
+
* @returns Maps from entity ID to property/quantity set IDs
|
|
211
|
+
*/
|
|
212
|
+
export function rebuildOnDemandMaps(
|
|
213
|
+
entities: EntityTable,
|
|
214
|
+
relationships: RelationshipGraph,
|
|
215
|
+
entityIndex?: EntityIndex
|
|
216
|
+
): OnDemandMaps {
|
|
217
|
+
const onDemandPropertyMap = new Map<number, number[]>();
|
|
218
|
+
const onDemandQuantityMap = new Map<number, number[]>();
|
|
219
|
+
|
|
220
|
+
// Use entityIndex.byType if available (needed for cache loads where entity table
|
|
221
|
+
// doesn't include IfcPropertySet/IfcElementQuantity entities)
|
|
222
|
+
// Fall back to entities.getByType() for fresh parses where entity table has these types
|
|
223
|
+
let propertySets: number[];
|
|
224
|
+
let quantitySets: number[];
|
|
225
|
+
|
|
226
|
+
if (entityIndex?.byType) {
|
|
227
|
+
// entityIndex.byType keys are the original type strings from the IFC file
|
|
228
|
+
// Check both common casings (STEP files may use either)
|
|
229
|
+
propertySets =
|
|
230
|
+
entityIndex.byType.get('IFCPROPERTYSET') || entityIndex.byType.get('IfcPropertySet') || [];
|
|
231
|
+
quantitySets =
|
|
232
|
+
entityIndex.byType.get('IFCELEMENTQUANTITY') ||
|
|
233
|
+
entityIndex.byType.get('IfcElementQuantity') ||
|
|
234
|
+
[];
|
|
235
|
+
} else {
|
|
236
|
+
// Fallback for when entityIndex is not provided
|
|
237
|
+
propertySets = entities.getByType(IfcTypeEnum.IfcPropertySet);
|
|
238
|
+
quantitySets = entities.getByType(IfcTypeEnum.IfcElementQuantity);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Process property sets
|
|
242
|
+
for (const psetId of propertySets) {
|
|
243
|
+
// Get elements defined by this pset (FORWARD: pset -> elements)
|
|
244
|
+
const definedElements = relationships.getRelated(
|
|
245
|
+
psetId,
|
|
246
|
+
RelationshipType.DefinesByProperties,
|
|
247
|
+
'forward'
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
for (const entityId of definedElements) {
|
|
251
|
+
let list = onDemandPropertyMap.get(entityId);
|
|
252
|
+
if (!list) {
|
|
253
|
+
list = [];
|
|
254
|
+
onDemandPropertyMap.set(entityId, list);
|
|
255
|
+
}
|
|
256
|
+
list.push(psetId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Process quantity sets
|
|
261
|
+
for (const qsetId of quantitySets) {
|
|
262
|
+
// Get elements defined by this qset (FORWARD: qset -> elements)
|
|
263
|
+
const definedElements = relationships.getRelated(
|
|
264
|
+
qsetId,
|
|
265
|
+
RelationshipType.DefinesByProperties,
|
|
266
|
+
'forward'
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
for (const entityId of definedElements) {
|
|
270
|
+
let list = onDemandQuantityMap.get(entityId);
|
|
271
|
+
if (!list) {
|
|
272
|
+
list = [];
|
|
273
|
+
onDemandQuantityMap.set(entityId, list);
|
|
274
|
+
}
|
|
275
|
+
list.push(qsetId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(
|
|
280
|
+
`[spatialHierarchy] Rebuilt on-demand maps: ${propertySets.length} psets, ${quantitySets.length} qsets -> ${onDemandPropertyMap.size} entities with properties, ${onDemandQuantityMap.size} with quantities`
|
|
281
|
+
);
|
|
282
|
+
return { onDemandPropertyMap, onDemandQuantityMap };
|
|
283
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
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
|
+
* Viewport utility functions
|
|
7
|
+
* Pure functions extracted from Viewport.tsx for reusability and testability
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MeshData } from '@ifc-lite/geometry';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 3D point/vector
|
|
18
|
+
*/
|
|
19
|
+
export interface Point3D {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
z: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Bounding box in 3D space
|
|
27
|
+
*/
|
|
28
|
+
export interface BoundingBox3D {
|
|
29
|
+
min: Point3D;
|
|
30
|
+
max: Point3D;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Section plane configuration
|
|
35
|
+
*/
|
|
36
|
+
export interface SectionPlaneConfig {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
height: number;
|
|
39
|
+
min?: number;
|
|
40
|
+
max?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render options for the WebGPU renderer
|
|
45
|
+
*/
|
|
46
|
+
export interface RenderOptions {
|
|
47
|
+
hiddenIds?: Set<number>;
|
|
48
|
+
isolatedIds?: Set<number> | null;
|
|
49
|
+
selectedId?: number | null;
|
|
50
|
+
selectedIds?: Set<number>;
|
|
51
|
+
clearColor?: [number, number, number, number];
|
|
52
|
+
sectionPlane?: SectionPlaneConfig;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Refs containing current visibility/selection state
|
|
57
|
+
*/
|
|
58
|
+
export interface ViewportStateRefs {
|
|
59
|
+
hiddenEntities: Set<number>;
|
|
60
|
+
isolatedEntities: Set<number> | null;
|
|
61
|
+
selectedEntityId: number | null;
|
|
62
|
+
clearColor: [number, number, number, number];
|
|
63
|
+
activeTool: string;
|
|
64
|
+
sectionPlane: { enabled: boolean; height: number };
|
|
65
|
+
sectionRange: { min: number; max: number } | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Entity Utilities
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Maximum coordinate threshold for valid geometry
|
|
74
|
+
* Matches CoordinateHandler's NORMAL_COORD_THRESHOLD (10km)
|
|
75
|
+
* Coordinates beyond this are likely corrupted or unshifted original coordinates
|
|
76
|
+
*/
|
|
77
|
+
const MAX_VALID_COORD = 10000;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a vertex coordinate is valid (finite and within reasonable bounds)
|
|
81
|
+
*/
|
|
82
|
+
function isValidCoord(x: number, y: number, z: number): boolean {
|
|
83
|
+
return Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) &&
|
|
84
|
+
Math.abs(x) < MAX_VALID_COORD &&
|
|
85
|
+
Math.abs(y) < MAX_VALID_COORD &&
|
|
86
|
+
Math.abs(z) < MAX_VALID_COORD;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get bounding box for a specific entity from geometry
|
|
91
|
+
* @param geometry - Array of mesh data
|
|
92
|
+
* @param entityId - Express ID of the entity
|
|
93
|
+
* @returns Bounding box or null if entity not found
|
|
94
|
+
*/
|
|
95
|
+
export function getEntityBounds(
|
|
96
|
+
geometry: MeshData[] | null,
|
|
97
|
+
entityId: number
|
|
98
|
+
): BoundingBox3D | null {
|
|
99
|
+
if (!geometry) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Find ALL meshes for this entity (entities can have multiple submeshes)
|
|
104
|
+
const matchingMeshes = geometry.filter(
|
|
105
|
+
(m) => m.expressId === entityId && m.positions.length >= 3
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (matchingMeshes.length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let minX = Infinity,
|
|
113
|
+
minY = Infinity,
|
|
114
|
+
minZ = Infinity;
|
|
115
|
+
let maxX = -Infinity,
|
|
116
|
+
maxY = -Infinity,
|
|
117
|
+
maxZ = -Infinity;
|
|
118
|
+
|
|
119
|
+
// Aggregate bounds across all submeshes
|
|
120
|
+
// Filter out corrupted/unshifted vertices (> 10km from origin)
|
|
121
|
+
for (const mesh of matchingMeshes) {
|
|
122
|
+
for (let i = 0; i < mesh.positions.length; i += 3) {
|
|
123
|
+
const x = mesh.positions[i];
|
|
124
|
+
const y = mesh.positions[i + 1];
|
|
125
|
+
const z = mesh.positions[i + 2];
|
|
126
|
+
// Skip corrupted vertices (NaN, Inf, or huge coordinates from unshifted data)
|
|
127
|
+
if (!isValidCoord(x, y, z)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
minX = Math.min(minX, x);
|
|
131
|
+
minY = Math.min(minY, y);
|
|
132
|
+
minZ = Math.min(minZ, z);
|
|
133
|
+
maxX = Math.max(maxX, x);
|
|
134
|
+
maxY = Math.max(maxY, y);
|
|
135
|
+
maxZ = Math.max(maxZ, z);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If no valid vertices found, return null
|
|
140
|
+
if (!Number.isFinite(minX) || !Number.isFinite(maxX)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
min: { x: minX, y: minY, z: minZ },
|
|
146
|
+
max: { x: maxX, y: maxY, z: maxZ },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get center point of an entity's bounding box
|
|
152
|
+
* @param geometry - Array of mesh data
|
|
153
|
+
* @param entityId - Express ID of the entity
|
|
154
|
+
* @returns Center point or null if entity not found
|
|
155
|
+
*/
|
|
156
|
+
export function getEntityCenter(
|
|
157
|
+
geometry: MeshData[] | null,
|
|
158
|
+
entityId: number
|
|
159
|
+
): Point3D | null {
|
|
160
|
+
const bounds = getEntityBounds(geometry, entityId);
|
|
161
|
+
if (!bounds) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
x: (bounds.min.x + bounds.max.x) / 2,
|
|
167
|
+
y: (bounds.min.y + bounds.max.y) / 2,
|
|
168
|
+
z: (bounds.min.z + bounds.max.z) / 2,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Calculate combined bounding box from multiple meshes
|
|
174
|
+
* @param meshes - Array of mesh data
|
|
175
|
+
* @returns Combined bounding box
|
|
176
|
+
*/
|
|
177
|
+
export function calculateGeometryBounds(meshes: MeshData[]): BoundingBox3D {
|
|
178
|
+
if (meshes.length === 0) {
|
|
179
|
+
return {
|
|
180
|
+
min: { x: -100, y: -100, z: -100 },
|
|
181
|
+
max: { x: 100, y: 100, z: 100 },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let minX = Infinity,
|
|
186
|
+
minY = Infinity,
|
|
187
|
+
minZ = Infinity;
|
|
188
|
+
let maxX = -Infinity,
|
|
189
|
+
maxY = -Infinity,
|
|
190
|
+
maxZ = -Infinity;
|
|
191
|
+
|
|
192
|
+
// Filter out corrupted/unshifted vertices (> 10km from origin)
|
|
193
|
+
for (const mesh of meshes) {
|
|
194
|
+
for (let i = 0; i < mesh.positions.length; i += 3) {
|
|
195
|
+
const x = mesh.positions[i];
|
|
196
|
+
const y = mesh.positions[i + 1];
|
|
197
|
+
const z = mesh.positions[i + 2];
|
|
198
|
+
// Skip corrupted vertices (NaN, Inf, or huge coordinates from unshifted data)
|
|
199
|
+
if (!isValidCoord(x, y, z)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
minX = Math.min(minX, x);
|
|
203
|
+
minY = Math.min(minY, y);
|
|
204
|
+
minZ = Math.min(minZ, z);
|
|
205
|
+
maxX = Math.max(maxX, x);
|
|
206
|
+
maxY = Math.max(maxY, y);
|
|
207
|
+
maxZ = Math.max(maxZ, z);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle degenerate cases:
|
|
212
|
+
// - Non-finite values (no valid positions found)
|
|
213
|
+
// - All three axes degenerate (single point)
|
|
214
|
+
// Note: Planar/linear geometry (only 1-2 axes equal) is valid and should NOT fall back
|
|
215
|
+
const isNonFinite = !Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(minZ) ||
|
|
216
|
+
!Number.isFinite(maxX) || !Number.isFinite(maxY) || !Number.isFinite(maxZ);
|
|
217
|
+
const isFullyDegenerate = minX === maxX && minY === maxY && minZ === maxZ;
|
|
218
|
+
|
|
219
|
+
if (isNonFinite || isFullyDegenerate) {
|
|
220
|
+
return {
|
|
221
|
+
min: { x: -100, y: -100, z: -100 },
|
|
222
|
+
max: { x: 100, y: 100, z: 100 },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
min: { x: minX, y: minY, z: minZ },
|
|
228
|
+
max: { x: maxX, y: maxY, z: maxZ },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Render Options Builder
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Build render options from viewport state refs
|
|
238
|
+
* Reduces code duplication - this object is constructed ~15+ times in Viewport.tsx
|
|
239
|
+
*
|
|
240
|
+
* @param refs - Object containing current state values from refs
|
|
241
|
+
* @returns Render options for the WebGPU renderer
|
|
242
|
+
*/
|
|
243
|
+
export function buildRenderOptions(refs: ViewportStateRefs): RenderOptions {
|
|
244
|
+
const options: RenderOptions = {
|
|
245
|
+
hiddenIds: refs.hiddenEntities,
|
|
246
|
+
isolatedIds: refs.isolatedEntities,
|
|
247
|
+
selectedId: refs.selectedEntityId,
|
|
248
|
+
clearColor: refs.clearColor,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Add section plane if enabled
|
|
252
|
+
if (refs.activeTool === 'section') {
|
|
253
|
+
options.sectionPlane = {
|
|
254
|
+
...refs.sectionPlane,
|
|
255
|
+
min: refs.sectionRange?.min,
|
|
256
|
+
max: refs.sectionRange?.max,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return options;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Build render options with additional selectedIds for multi-selection
|
|
265
|
+
*/
|
|
266
|
+
export function buildRenderOptionsWithSelection(
|
|
267
|
+
refs: ViewportStateRefs,
|
|
268
|
+
selectedIds?: Set<number>
|
|
269
|
+
): RenderOptions {
|
|
270
|
+
const options = buildRenderOptions(refs);
|
|
271
|
+
if (selectedIds) {
|
|
272
|
+
options.selectedIds = selectedIds;
|
|
273
|
+
}
|
|
274
|
+
return options;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// Throttling Utilities
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get render throttle interval based on mesh count
|
|
283
|
+
* Adaptive throttling: faster for small models, slower for large models
|
|
284
|
+
*
|
|
285
|
+
* @param meshCount - Number of meshes in the scene
|
|
286
|
+
* @returns Throttle interval in milliseconds
|
|
287
|
+
*/
|
|
288
|
+
export function getRenderThrottleMs(meshCount: number): number {
|
|
289
|
+
if (meshCount < 10000) {
|
|
290
|
+
return 16; // ~60fps for small models
|
|
291
|
+
} else if (meshCount < 50000) {
|
|
292
|
+
return 25; // ~40fps for medium models
|
|
293
|
+
} else {
|
|
294
|
+
return 33; // ~30fps for large models
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Theme Utilities
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get clear color based on theme
|
|
304
|
+
* @param theme - 'light' or 'dark'
|
|
305
|
+
* @returns RGBA clear color tuple
|
|
306
|
+
*/
|
|
307
|
+
export function getThemeClearColor(theme: 'light' | 'dark'): [number, number, number, number] {
|
|
308
|
+
if (theme === 'light') {
|
|
309
|
+
return [0.96, 0.96, 0.97, 1]; // Light gray
|
|
310
|
+
}
|
|
311
|
+
return [0.102, 0.106, 0.149, 1]; // Tokyo Night storm (#1a1b26)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// Scale Calculation
|
|
316
|
+
// ============================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Calculate world-space size for a scale bar
|
|
320
|
+
*
|
|
321
|
+
* @param viewportHeight - Canvas height in pixels
|
|
322
|
+
* @param cameraDistance - Camera distance from target
|
|
323
|
+
* @param fov - Field of view in radians
|
|
324
|
+
* @param scaleBarPixels - Scale bar width in pixels (default 96px = 6rem)
|
|
325
|
+
* @returns World-space size represented by the scale bar
|
|
326
|
+
*/
|
|
327
|
+
export function calculateScaleBarSize(
|
|
328
|
+
viewportHeight: number,
|
|
329
|
+
cameraDistance: number,
|
|
330
|
+
fov: number,
|
|
331
|
+
scaleBarPixels: number = 96
|
|
332
|
+
): number {
|
|
333
|
+
return (scaleBarPixels / viewportHeight) * (cameraDistance * Math.tan(fov / 2) * 2);
|
|
334
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
/// <reference types="vite/client" />
|
|
6
|
+
|
|
7
|
+
interface ImportMetaEnv {
|
|
8
|
+
readonly VITE_IFC_SERVER_URL?: string;
|
|
9
|
+
readonly VITE_SERVER_URL?: string;
|
|
10
|
+
readonly VITE_USE_SERVER?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ImportMeta {
|
|
14
|
+
readonly env: ImportMetaEnv;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Build-time constants injected by Vite define
|
|
18
|
+
declare const __APP_VERSION__: string;
|
|
19
|
+
declare const __BUILD_DATE__: string;
|
|
20
|
+
declare const __RELEASE_HISTORY__: Array<{
|
|
21
|
+
version: string;
|
|
22
|
+
highlights: Array<{ type: 'feature' | 'fix' | 'perf'; text: string }>;
|
|
23
|
+
}>;
|