@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.
Files changed (164) hide show
  1. package/LICENSE +373 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
  4. package/dist/assets/arrow2-bb-jcVEo.js +2 -0
  5. package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
  6. package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
  7. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  8. package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
  9. package/dist/assets/event-DIOks52T.js +1 -0
  10. package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
  11. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  12. package/dist/assets/index-Dgd6vzw_.js +65252 -0
  13. package/dist/assets/index-v3mcCUPN.css +1 -0
  14. package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
  15. package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
  16. package/dist/favicon-16x16-cropped.png +0 -0
  17. package/dist/favicon-16x16.png +0 -0
  18. package/dist/favicon-192x192-cropped.png +0 -0
  19. package/dist/favicon-192x192.png +0 -0
  20. package/dist/favicon-32x32-cropped.png +0 -0
  21. package/dist/favicon-32x32.png +0 -0
  22. package/dist/favicon-48x48-cropped.png +0 -0
  23. package/dist/favicon-48x48.png +0 -0
  24. package/dist/favicon-512x512-cropped.png +0 -0
  25. package/dist/favicon-512x512.png +0 -0
  26. package/dist/favicon-64x64-cropped.png +0 -0
  27. package/dist/favicon-64x64.png +0 -0
  28. package/dist/favicon-96x96-cropped.png +0 -0
  29. package/dist/favicon-96x96.png +0 -0
  30. package/dist/favicon-square-512.png +0 -0
  31. package/dist/favicon.ico +0 -0
  32. package/dist/favicon.png +0 -0
  33. package/dist/favicon.svg +3 -0
  34. package/dist/index.html +44 -0
  35. package/dist/logo.png +0 -0
  36. package/dist/manifest.json +48 -0
  37. package/index.html +33 -2
  38. package/package.json +34 -17
  39. package/public/apple-touch-icon.png +0 -0
  40. package/public/favicon-16x16-cropped.png +0 -0
  41. package/public/favicon-16x16.png +0 -0
  42. package/public/favicon-192x192-cropped.png +0 -0
  43. package/public/favicon-192x192.png +0 -0
  44. package/public/favicon-32x32-cropped.png +0 -0
  45. package/public/favicon-32x32.png +0 -0
  46. package/public/favicon-48x48-cropped.png +0 -0
  47. package/public/favicon-48x48.png +0 -0
  48. package/public/favicon-512x512-cropped.png +0 -0
  49. package/public/favicon-512x512.png +0 -0
  50. package/public/favicon-64x64-cropped.png +0 -0
  51. package/public/favicon-64x64.png +0 -0
  52. package/public/favicon-96x96-cropped.png +0 -0
  53. package/public/favicon-96x96.png +0 -0
  54. package/public/favicon-square-512.png +0 -0
  55. package/public/favicon.ico +0 -0
  56. package/public/favicon.png +0 -0
  57. package/public/favicon.svg +3 -0
  58. package/public/logo.png +0 -0
  59. package/public/manifest.json +48 -0
  60. package/src/App.tsx +2 -0
  61. package/src/components/ui/alert.tsx +62 -0
  62. package/src/components/ui/badge.tsx +39 -0
  63. package/src/components/ui/dialog.tsx +120 -0
  64. package/src/components/ui/label.tsx +27 -0
  65. package/src/components/ui/select.tsx +151 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +120 -0
  68. package/src/components/ui/tabs.tsx +1 -1
  69. package/src/components/viewer/BCFPanel.tsx +1164 -0
  70. package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
  71. package/src/components/viewer/DataConnector.tsx +840 -0
  72. package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
  73. package/src/components/viewer/EntityContextMenu.tsx +45 -17
  74. package/src/components/viewer/ExportChangesButton.tsx +195 -0
  75. package/src/components/viewer/ExportDialog.tsx +402 -0
  76. package/src/components/viewer/HierarchyPanel.tsx +1132 -218
  77. package/src/components/viewer/IDSPanel.tsx +661 -0
  78. package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
  79. package/src/components/viewer/MainToolbar.tsx +418 -94
  80. package/src/components/viewer/PropertiesPanel.tsx +1355 -91
  81. package/src/components/viewer/PropertyEditor.tsx +611 -0
  82. package/src/components/viewer/Section2DPanel.tsx +3313 -0
  83. package/src/components/viewer/SheetSetupPanel.tsx +502 -0
  84. package/src/components/viewer/StatusBar.tsx +27 -16
  85. package/src/components/viewer/TitleBlockEditor.tsx +437 -0
  86. package/src/components/viewer/ToolOverlays.tsx +935 -127
  87. package/src/components/viewer/ViewerLayout.tsx +40 -11
  88. package/src/components/viewer/Viewport.tsx +1276 -336
  89. package/src/components/viewer/ViewportContainer.tsx +554 -18
  90. package/src/components/viewer/ViewportOverlays.tsx +24 -7
  91. package/src/hooks/useBCF.ts +504 -0
  92. package/src/hooks/useIDS.ts +1065 -0
  93. package/src/hooks/useIfc.ts +1534 -205
  94. package/src/hooks/useIfcCache.ts +279 -0
  95. package/src/hooks/useKeyboardShortcuts.ts +50 -8
  96. package/src/hooks/useModelSelection.ts +61 -0
  97. package/src/hooks/useViewerSelectors.ts +218 -0
  98. package/src/hooks/useWebGPU.ts +80 -0
  99. package/src/index.css +265 -27
  100. package/src/lib/platform.ts +23 -0
  101. package/src/services/cacheService.ts +142 -0
  102. package/src/services/desktop-cache.ts +143 -0
  103. package/src/services/fs-cache.ts +212 -0
  104. package/src/services/ifc-cache.ts +14 -6
  105. package/src/store/constants.ts +85 -0
  106. package/src/store/index.ts +214 -0
  107. package/src/store/slices/bcfSlice.ts +372 -0
  108. package/src/store/slices/cameraSlice.ts +63 -0
  109. package/src/store/slices/dataSlice.test.ts +226 -0
  110. package/src/store/slices/dataSlice.ts +112 -0
  111. package/src/store/slices/drawing2DSlice.ts +340 -0
  112. package/src/store/slices/hoverSlice.ts +40 -0
  113. package/src/store/slices/idsSlice.ts +310 -0
  114. package/src/store/slices/loadingSlice.ts +33 -0
  115. package/src/store/slices/measurementSlice.test.ts +217 -0
  116. package/src/store/slices/measurementSlice.ts +293 -0
  117. package/src/store/slices/modelSlice.test.ts +271 -0
  118. package/src/store/slices/modelSlice.ts +211 -0
  119. package/src/store/slices/mutationSlice.ts +502 -0
  120. package/src/store/slices/sectionSlice.test.ts +125 -0
  121. package/src/store/slices/sectionSlice.ts +58 -0
  122. package/src/store/slices/selectionSlice.test.ts +286 -0
  123. package/src/store/slices/selectionSlice.ts +263 -0
  124. package/src/store/slices/sheetSlice.ts +565 -0
  125. package/src/store/slices/uiSlice.ts +58 -0
  126. package/src/store/slices/visibilitySlice.test.ts +304 -0
  127. package/src/store/slices/visibilitySlice.ts +277 -0
  128. package/src/store/types.test.ts +135 -0
  129. package/src/store/types.ts +248 -0
  130. package/src/store.ts +40 -515
  131. package/src/utils/ifcConfig.ts +82 -0
  132. package/src/utils/localParsingUtils.ts +287 -0
  133. package/src/utils/serverDataModel.ts +783 -0
  134. package/src/utils/spatialHierarchy.ts +283 -0
  135. package/src/utils/viewportUtils.ts +334 -0
  136. package/src/vite-env.d.ts +23 -0
  137. package/src/webgpu-types.d.ts +128 -0
  138. package/src-tauri/Cargo.toml +29 -0
  139. package/src-tauri/build.rs +7 -0
  140. package/src-tauri/capabilities/default.json +18 -0
  141. package/src-tauri/icons/128x128.png +0 -0
  142. package/src-tauri/icons/128x128@2x.png +0 -0
  143. package/src-tauri/icons/32x32.png +0 -0
  144. package/src-tauri/icons/Square107x107Logo.png +0 -0
  145. package/src-tauri/icons/Square142x142Logo.png +0 -0
  146. package/src-tauri/icons/Square150x150Logo.png +0 -0
  147. package/src-tauri/icons/Square284x284Logo.png +0 -0
  148. package/src-tauri/icons/Square30x30Logo.png +0 -0
  149. package/src-tauri/icons/Square310x310Logo.png +0 -0
  150. package/src-tauri/icons/Square44x44Logo.png +0 -0
  151. package/src-tauri/icons/Square71x71Logo.png +0 -0
  152. package/src-tauri/icons/Square89x89Logo.png +0 -0
  153. package/src-tauri/icons/StoreLogo.png +0 -0
  154. package/src-tauri/icons/icon.icns +0 -0
  155. package/src-tauri/icons/icon.ico +0 -0
  156. package/src-tauri/icons/icon.png +0 -0
  157. package/src-tauri/src/lib.rs +21 -0
  158. package/src-tauri/src/main.rs +10 -0
  159. package/src-tauri/tauri.conf.json +39 -0
  160. package/vite.config.ts +174 -26
  161. package/public/ifc-lite_bg.wasm +0 -0
  162. package/public/web-ifc.wasm +0 -0
  163. package/src/components/Viewport.tsx +0 -723
  164. 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
+ }>;