@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,627 @@
|
|
|
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
|
+
* useDrawingGeneration - Custom hook for 2D drawing generation logic
|
|
7
|
+
*
|
|
8
|
+
* Extracts the drawing generation pipeline from Section2DPanel, including:
|
|
9
|
+
* - Section cut generation via Drawing2DGenerator
|
|
10
|
+
* - Symbolic representation parsing and caching
|
|
11
|
+
* - Hybrid drawing creation (symbolic + section cut)
|
|
12
|
+
* - Bounding box alignment for symbolic lines
|
|
13
|
+
* - Auto-generation effects (panel open, overlay enable, geometry change)
|
|
14
|
+
* - Section plane change detection with overlap protection
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
Drawing2DGenerator,
|
|
20
|
+
createSectionConfig,
|
|
21
|
+
type Drawing2D,
|
|
22
|
+
type DrawingLine,
|
|
23
|
+
type SectionConfig,
|
|
24
|
+
} from '@ifc-lite/drawing-2d';
|
|
25
|
+
import { GeometryProcessor, type GeometryResult } from '@ifc-lite/geometry';
|
|
26
|
+
|
|
27
|
+
// Axis conversion from semantic (down/front/side) to geometric (x/y/z)
|
|
28
|
+
export const AXIS_MAP: Record<'down' | 'front' | 'side', 'x' | 'y' | 'z'> = {
|
|
29
|
+
down: 'y',
|
|
30
|
+
front: 'z',
|
|
31
|
+
side: 'x',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface UseDrawingGenerationParams {
|
|
35
|
+
geometryResult: GeometryResult | null | undefined;
|
|
36
|
+
ifcDataStore: { source: Uint8Array } | null;
|
|
37
|
+
sectionPlane: { axis: 'down' | 'front' | 'side'; position: number; flipped: boolean };
|
|
38
|
+
displayOptions: { showHiddenLines: boolean; useSymbolicRepresentations: boolean; show3DOverlay: boolean; scale: number };
|
|
39
|
+
combinedHiddenIds: Set<number>;
|
|
40
|
+
combinedIsolatedIds: Set<number> | null;
|
|
41
|
+
computedIsolatedIds?: Set<number> | null;
|
|
42
|
+
models: Map<string, { id: string; visible: boolean; idOffset?: number }>;
|
|
43
|
+
panelVisible: boolean;
|
|
44
|
+
drawing: Drawing2D | null;
|
|
45
|
+
// Store actions
|
|
46
|
+
setDrawing: (d: Drawing2D | null) => void;
|
|
47
|
+
setDrawingStatus: (s: 'idle' | 'generating' | 'ready' | 'error') => void;
|
|
48
|
+
setDrawingProgress: (p: number, phase: string) => void;
|
|
49
|
+
setDrawingError: (e: string | null) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface UseDrawingGenerationResult {
|
|
53
|
+
generateDrawing: (isRegenerate?: boolean) => Promise<void>;
|
|
54
|
+
doRegenerate: () => Promise<void>;
|
|
55
|
+
isRegenerating: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useDrawingGeneration({
|
|
59
|
+
geometryResult,
|
|
60
|
+
ifcDataStore,
|
|
61
|
+
sectionPlane,
|
|
62
|
+
displayOptions,
|
|
63
|
+
combinedHiddenIds,
|
|
64
|
+
combinedIsolatedIds,
|
|
65
|
+
computedIsolatedIds,
|
|
66
|
+
models,
|
|
67
|
+
panelVisible,
|
|
68
|
+
drawing,
|
|
69
|
+
setDrawing,
|
|
70
|
+
setDrawingStatus,
|
|
71
|
+
setDrawingProgress,
|
|
72
|
+
setDrawingError,
|
|
73
|
+
}: UseDrawingGenerationParams): UseDrawingGenerationResult {
|
|
74
|
+
// Track if this is a regeneration (vs initial generation)
|
|
75
|
+
const isRegeneratingRef = useRef(false);
|
|
76
|
+
|
|
77
|
+
// Cache for symbolic representations - these don't change with section position
|
|
78
|
+
// Only re-parse when model or display options change
|
|
79
|
+
const symbolicCacheRef = useRef<{
|
|
80
|
+
lines: DrawingLine[];
|
|
81
|
+
entities: Set<number>;
|
|
82
|
+
sourceId: string | null;
|
|
83
|
+
useSymbolic: boolean;
|
|
84
|
+
} | null>(null);
|
|
85
|
+
|
|
86
|
+
// Generate drawing when panel opens
|
|
87
|
+
const generateDrawing = useCallback(async (isRegenerate = false) => {
|
|
88
|
+
if (!geometryResult?.meshes || geometryResult.meshes.length === 0) {
|
|
89
|
+
// Clear the drawing when no geometry is available (e.g., all models hidden)
|
|
90
|
+
setDrawing(null);
|
|
91
|
+
setDrawingStatus('idle');
|
|
92
|
+
setDrawingError('No visible geometry');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Only show full loading overlay for initial generation, not regeneration
|
|
97
|
+
if (!isRegenerate) {
|
|
98
|
+
setDrawingStatus('generating');
|
|
99
|
+
setDrawingProgress(0, 'Initializing...');
|
|
100
|
+
}
|
|
101
|
+
isRegeneratingRef.current = isRegenerate;
|
|
102
|
+
|
|
103
|
+
// Parse symbolic representations if enabled (for hybrid mode)
|
|
104
|
+
// OPTIMIZATION: Cache symbolic data - it doesn't change with section position
|
|
105
|
+
let symbolicLines: DrawingLine[] = [];
|
|
106
|
+
let entitiesWithSymbols = new Set<number>();
|
|
107
|
+
|
|
108
|
+
// For multi-model: create cache key from model count and visible model IDs
|
|
109
|
+
// For single-model: use source byteLength as before
|
|
110
|
+
const modelCacheKey = models.size > 0
|
|
111
|
+
? `${models.size}-${[...models.values()].filter(m => m.visible).map(m => m.id).sort().join(',')}`
|
|
112
|
+
: (ifcDataStore?.source ? String(ifcDataStore.source.byteLength) : null);
|
|
113
|
+
|
|
114
|
+
const useSymbolic = displayOptions.useSymbolicRepresentations && !!ifcDataStore?.source;
|
|
115
|
+
|
|
116
|
+
// Check if we can use cached symbolic data
|
|
117
|
+
const cache = symbolicCacheRef.current;
|
|
118
|
+
const cacheValid = cache &&
|
|
119
|
+
cache.sourceId === modelCacheKey &&
|
|
120
|
+
cache.useSymbolic === useSymbolic;
|
|
121
|
+
|
|
122
|
+
if (useSymbolic) {
|
|
123
|
+
if (cacheValid) {
|
|
124
|
+
// Use cached data - FAST PATH
|
|
125
|
+
symbolicLines = cache.lines;
|
|
126
|
+
entitiesWithSymbols = cache.entities;
|
|
127
|
+
} else {
|
|
128
|
+
// Need to parse - only on first load or when model changes
|
|
129
|
+
try {
|
|
130
|
+
if (!isRegenerate) {
|
|
131
|
+
setDrawingProgress(5, 'Parsing symbolic representations...');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const processor = new GeometryProcessor();
|
|
135
|
+
try {
|
|
136
|
+
await processor.init();
|
|
137
|
+
|
|
138
|
+
const symbolicCollection = processor.parseSymbolicRepresentations(ifcDataStore!.source);
|
|
139
|
+
// For single-model (legacy) mode, model index is always 0
|
|
140
|
+
// Multi-model symbolic parsing would require iterating over each model separately
|
|
141
|
+
const symbolicModelIndex = 0;
|
|
142
|
+
|
|
143
|
+
if (symbolicCollection && !symbolicCollection.isEmpty) {
|
|
144
|
+
// Process polylines
|
|
145
|
+
for (let i = 0; i < symbolicCollection.polylineCount; i++) {
|
|
146
|
+
const poly = symbolicCollection.getPolyline(i);
|
|
147
|
+
if (!poly) continue;
|
|
148
|
+
|
|
149
|
+
entitiesWithSymbols.add(poly.expressId);
|
|
150
|
+
const points = poly.points;
|
|
151
|
+
const pointCount = poly.pointCount;
|
|
152
|
+
|
|
153
|
+
for (let j = 0; j < pointCount - 1; j++) {
|
|
154
|
+
symbolicLines.push({
|
|
155
|
+
line: {
|
|
156
|
+
start: { x: points[j * 2], y: points[j * 2 + 1] },
|
|
157
|
+
end: { x: points[(j + 1) * 2], y: points[(j + 1) * 2 + 1] }
|
|
158
|
+
},
|
|
159
|
+
category: 'silhouette',
|
|
160
|
+
visibility: 'visible',
|
|
161
|
+
entityId: poly.expressId,
|
|
162
|
+
ifcType: poly.ifcType,
|
|
163
|
+
modelIndex: symbolicModelIndex,
|
|
164
|
+
depth: 0,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (poly.isClosed && pointCount > 2) {
|
|
169
|
+
symbolicLines.push({
|
|
170
|
+
line: {
|
|
171
|
+
start: { x: points[(pointCount - 1) * 2], y: points[(pointCount - 1) * 2 + 1] },
|
|
172
|
+
end: { x: points[0], y: points[1] }
|
|
173
|
+
},
|
|
174
|
+
category: 'silhouette',
|
|
175
|
+
visibility: 'visible',
|
|
176
|
+
entityId: poly.expressId,
|
|
177
|
+
ifcType: poly.ifcType,
|
|
178
|
+
modelIndex: symbolicModelIndex,
|
|
179
|
+
depth: 0,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Process circles/arcs
|
|
185
|
+
for (let i = 0; i < symbolicCollection.circleCount; i++) {
|
|
186
|
+
const circle = symbolicCollection.getCircle(i);
|
|
187
|
+
if (!circle) continue;
|
|
188
|
+
|
|
189
|
+
entitiesWithSymbols.add(circle.expressId);
|
|
190
|
+
const numSegments = circle.isFullCircle ? 32 : 16;
|
|
191
|
+
|
|
192
|
+
for (let j = 0; j < numSegments; j++) {
|
|
193
|
+
const t1 = j / numSegments;
|
|
194
|
+
const t2 = (j + 1) / numSegments;
|
|
195
|
+
const a1 = circle.startAngle + t1 * (circle.endAngle - circle.startAngle);
|
|
196
|
+
const a2 = circle.startAngle + t2 * (circle.endAngle - circle.startAngle);
|
|
197
|
+
|
|
198
|
+
symbolicLines.push({
|
|
199
|
+
line: {
|
|
200
|
+
start: {
|
|
201
|
+
x: circle.centerX + circle.radius * Math.cos(a1),
|
|
202
|
+
y: circle.centerY + circle.radius * Math.sin(a1),
|
|
203
|
+
},
|
|
204
|
+
end: {
|
|
205
|
+
x: circle.centerX + circle.radius * Math.cos(a2),
|
|
206
|
+
y: circle.centerY + circle.radius * Math.sin(a2),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
category: 'silhouette',
|
|
210
|
+
visibility: 'visible',
|
|
211
|
+
entityId: circle.expressId,
|
|
212
|
+
ifcType: circle.ifcType,
|
|
213
|
+
modelIndex: symbolicModelIndex,
|
|
214
|
+
depth: 0,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
processor.dispose();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Cache the parsed data
|
|
224
|
+
symbolicCacheRef.current = {
|
|
225
|
+
lines: symbolicLines,
|
|
226
|
+
entities: entitiesWithSymbols,
|
|
227
|
+
sourceId: modelCacheKey,
|
|
228
|
+
useSymbolic,
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.warn('Symbolic parsing failed:', error);
|
|
232
|
+
symbolicLines = [];
|
|
233
|
+
entitiesWithSymbols = new Set<number>();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
// Clear cache if symbolic is disabled
|
|
238
|
+
if (cache && cache.useSymbolic) {
|
|
239
|
+
symbolicCacheRef.current = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let generator: Drawing2DGenerator | null = null;
|
|
244
|
+
try {
|
|
245
|
+
generator = new Drawing2DGenerator();
|
|
246
|
+
await generator.initialize();
|
|
247
|
+
|
|
248
|
+
// Convert semantic axis to geometric
|
|
249
|
+
const axis = AXIS_MAP[sectionPlane.axis];
|
|
250
|
+
|
|
251
|
+
// Calculate section position from percentage using coordinateInfo bounds
|
|
252
|
+
const bounds = geometryResult.coordinateInfo.shiftedBounds;
|
|
253
|
+
|
|
254
|
+
const axisMin = bounds.min[axis];
|
|
255
|
+
const axisMax = bounds.max[axis];
|
|
256
|
+
const position = axisMin + (sectionPlane.position / 100) * (axisMax - axisMin);
|
|
257
|
+
|
|
258
|
+
// Calculate max depth as half the model extent
|
|
259
|
+
const maxDepth = (axisMax - axisMin) * 0.5;
|
|
260
|
+
|
|
261
|
+
// Adjust progress to account for symbolic parsing phase (0-20%)
|
|
262
|
+
const progressOffset = symbolicLines.length > 0 ? 20 : 0;
|
|
263
|
+
const progressScale = symbolicLines.length > 0 ? 0.8 : 1;
|
|
264
|
+
const progressCallback = (stage: string, prog: number) => {
|
|
265
|
+
setDrawingProgress(progressOffset + prog * 100 * progressScale, stage);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Create section config
|
|
269
|
+
const config: SectionConfig = createSectionConfig(axis, position, {
|
|
270
|
+
projectionDepth: maxDepth,
|
|
271
|
+
includeHiddenLines: displayOptions.showHiddenLines,
|
|
272
|
+
scale: displayOptions.scale,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Override the flipped setting
|
|
276
|
+
config.plane.flipped = sectionPlane.flipped;
|
|
277
|
+
|
|
278
|
+
// Filter meshes by visibility (respect 3D hiding/isolation)
|
|
279
|
+
let meshesToProcess = geometryResult.meshes;
|
|
280
|
+
|
|
281
|
+
// Filter out hidden entities (using combined multi-model set)
|
|
282
|
+
if (combinedHiddenIds.size > 0) {
|
|
283
|
+
meshesToProcess = meshesToProcess.filter(
|
|
284
|
+
mesh => !combinedHiddenIds.has(mesh.expressId)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Filter by isolation (if active, using combined multi-model set)
|
|
289
|
+
if (combinedIsolatedIds !== null) {
|
|
290
|
+
meshesToProcess = meshesToProcess.filter(
|
|
291
|
+
mesh => combinedIsolatedIds.has(mesh.expressId)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Also filter by computedIsolatedIds (storey selection)
|
|
296
|
+
if (computedIsolatedIds !== null && computedIsolatedIds !== undefined && computedIsolatedIds.size > 0) {
|
|
297
|
+
const isolatedSet = computedIsolatedIds;
|
|
298
|
+
meshesToProcess = meshesToProcess.filter(
|
|
299
|
+
mesh => isolatedSet.has(mesh.expressId)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// If all meshes were filtered out by visibility, clear the drawing
|
|
304
|
+
if (meshesToProcess.length === 0) {
|
|
305
|
+
setDrawing(null);
|
|
306
|
+
setDrawingStatus('idle');
|
|
307
|
+
setDrawingError(null);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await generator.generate(meshesToProcess, config, {
|
|
312
|
+
includeHiddenLines: false, // Disable - causes internal mesh edges
|
|
313
|
+
includeProjection: false, // Disable - causes triangulation lines
|
|
314
|
+
includeEdges: false, // Disable - causes triangulation lines
|
|
315
|
+
mergeLines: true,
|
|
316
|
+
onProgress: progressCallback,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// If we have symbolic representations, create a hybrid drawing
|
|
320
|
+
if (symbolicLines.length > 0 && entitiesWithSymbols.size > 0) {
|
|
321
|
+
// Get entity IDs that actually appear in the section cut (these are being cut by the plane)
|
|
322
|
+
const cutEntityIds = new Set<number>();
|
|
323
|
+
for (const line of result.lines) {
|
|
324
|
+
if (line.entityId !== undefined) {
|
|
325
|
+
cutEntityIds.add(line.entityId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Also check cut polygons for entity IDs
|
|
329
|
+
for (const poly of result.cutPolygons ?? []) {
|
|
330
|
+
if ((poly as { entityId?: number }).entityId !== undefined) {
|
|
331
|
+
cutEntityIds.add((poly as { entityId?: number }).entityId!);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Only include symbolic lines for entities that are ACTUALLY being cut
|
|
336
|
+
// This filters out symbols from other floors/levels not intersected by the section plane
|
|
337
|
+
const relevantSymbolicLines = symbolicLines.filter(line =>
|
|
338
|
+
line.entityId !== undefined && cutEntityIds.has(line.entityId)
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Get the set of entities that have both symbols AND are being cut
|
|
342
|
+
const entitiesWithRelevantSymbols = new Set<number>();
|
|
343
|
+
for (const line of relevantSymbolicLines) {
|
|
344
|
+
if (line.entityId !== undefined) {
|
|
345
|
+
entitiesWithRelevantSymbols.add(line.entityId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Align symbolic geometry with section cut geometry using bounding box matching
|
|
350
|
+
// Plan representations often have different local origins than Body representations
|
|
351
|
+
// So we compute per-entity transforms to align Plan bbox center with section cut bbox center
|
|
352
|
+
|
|
353
|
+
// Build per-entity bounding boxes for section cut
|
|
354
|
+
const sectionCutBounds = new Map<number, { minX: number; minY: number; maxX: number; maxY: number }>();
|
|
355
|
+
const updateBounds = (entityId: number, x: number, y: number) => {
|
|
356
|
+
const bounds = sectionCutBounds.get(entityId) ?? { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
|
357
|
+
bounds.minX = Math.min(bounds.minX, x);
|
|
358
|
+
bounds.minY = Math.min(bounds.minY, y);
|
|
359
|
+
bounds.maxX = Math.max(bounds.maxX, x);
|
|
360
|
+
bounds.maxY = Math.max(bounds.maxY, y);
|
|
361
|
+
sectionCutBounds.set(entityId, bounds);
|
|
362
|
+
};
|
|
363
|
+
for (const line of result.lines) {
|
|
364
|
+
if (line.entityId === undefined) continue;
|
|
365
|
+
updateBounds(line.entityId, line.line.start.x, line.line.start.y);
|
|
366
|
+
updateBounds(line.entityId, line.line.end.x, line.line.end.y);
|
|
367
|
+
}
|
|
368
|
+
// Include cut polygon vertices in bounds computation
|
|
369
|
+
for (const poly of result.cutPolygons ?? []) {
|
|
370
|
+
const entityId = (poly as { entityId?: number }).entityId;
|
|
371
|
+
if (entityId === undefined) continue;
|
|
372
|
+
for (const pt of poly.polygon.outer) {
|
|
373
|
+
updateBounds(entityId, pt.x, pt.y);
|
|
374
|
+
}
|
|
375
|
+
for (const hole of poly.polygon.holes) {
|
|
376
|
+
for (const pt of hole) {
|
|
377
|
+
updateBounds(entityId, pt.x, pt.y);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Build per-entity bounding boxes for symbolic
|
|
383
|
+
const symbolicBounds = new Map<number, { minX: number; minY: number; maxX: number; maxY: number }>();
|
|
384
|
+
for (const line of relevantSymbolicLines) {
|
|
385
|
+
if (line.entityId === undefined) continue;
|
|
386
|
+
const bounds = symbolicBounds.get(line.entityId) ?? { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
|
387
|
+
bounds.minX = Math.min(bounds.minX, line.line.start.x, line.line.end.x);
|
|
388
|
+
bounds.minY = Math.min(bounds.minY, line.line.start.y, line.line.end.y);
|
|
389
|
+
bounds.maxX = Math.max(bounds.maxX, line.line.start.x, line.line.end.x);
|
|
390
|
+
bounds.maxY = Math.max(bounds.maxY, line.line.start.y, line.line.end.y);
|
|
391
|
+
symbolicBounds.set(line.entityId, bounds);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Compute per-entity alignment transforms (center-to-center offset)
|
|
395
|
+
const alignmentOffsets = new Map<number, { dx: number; dy: number }>();
|
|
396
|
+
for (const entityId of entitiesWithRelevantSymbols) {
|
|
397
|
+
const scBounds = sectionCutBounds.get(entityId);
|
|
398
|
+
const symBounds = symbolicBounds.get(entityId);
|
|
399
|
+
if (scBounds && symBounds) {
|
|
400
|
+
const scCenterX = (scBounds.minX + scBounds.maxX) / 2;
|
|
401
|
+
const scCenterY = (scBounds.minY + scBounds.maxY) / 2;
|
|
402
|
+
const symCenterX = (symBounds.minX + symBounds.maxX) / 2;
|
|
403
|
+
const symCenterY = (symBounds.minY + symBounds.maxY) / 2;
|
|
404
|
+
alignmentOffsets.set(entityId, {
|
|
405
|
+
dx: scCenterX - symCenterX,
|
|
406
|
+
dy: scCenterY - symCenterY,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Apply alignment offsets to symbolic lines
|
|
412
|
+
const alignedSymbolicLines = relevantSymbolicLines.map(line => {
|
|
413
|
+
const offset = line.entityId !== undefined ? alignmentOffsets.get(line.entityId) : undefined;
|
|
414
|
+
if (offset) {
|
|
415
|
+
return {
|
|
416
|
+
...line,
|
|
417
|
+
line: {
|
|
418
|
+
start: { x: line.line.start.x + offset.dx, y: line.line.start.y + offset.dy },
|
|
419
|
+
end: { x: line.line.end.x + offset.dx, y: line.line.end.y + offset.dy },
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return line;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Filter out section cut lines for entities that have relevant symbolic representations
|
|
427
|
+
const filteredLines = result.lines.filter((line: DrawingLine) =>
|
|
428
|
+
line.entityId === undefined || !entitiesWithRelevantSymbols.has(line.entityId)
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Also filter cut polygons for entities with relevant symbols
|
|
432
|
+
const filteredCutPolygons = result.cutPolygons?.filter((poly: { entityId?: number }) =>
|
|
433
|
+
poly.entityId === undefined || !entitiesWithRelevantSymbols.has(poly.entityId)
|
|
434
|
+
) ?? [];
|
|
435
|
+
|
|
436
|
+
// Combine filtered section cuts with aligned symbolic lines
|
|
437
|
+
const combinedLines = [...filteredLines, ...alignedSymbolicLines];
|
|
438
|
+
|
|
439
|
+
// Recalculate bounds with combined lines and polygons
|
|
440
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
441
|
+
for (const line of combinedLines) {
|
|
442
|
+
minX = Math.min(minX, line.line.start.x, line.line.end.x);
|
|
443
|
+
minY = Math.min(minY, line.line.start.y, line.line.end.y);
|
|
444
|
+
maxX = Math.max(maxX, line.line.start.x, line.line.end.x);
|
|
445
|
+
maxY = Math.max(maxY, line.line.start.y, line.line.end.y);
|
|
446
|
+
}
|
|
447
|
+
// Include polygon vertices in bounds
|
|
448
|
+
for (const poly of filteredCutPolygons) {
|
|
449
|
+
for (const pt of poly.polygon.outer) {
|
|
450
|
+
minX = Math.min(minX, pt.x);
|
|
451
|
+
minY = Math.min(minY, pt.y);
|
|
452
|
+
maxX = Math.max(maxX, pt.x);
|
|
453
|
+
maxY = Math.max(maxY, pt.y);
|
|
454
|
+
}
|
|
455
|
+
for (const hole of poly.polygon.holes) {
|
|
456
|
+
for (const pt of hole) {
|
|
457
|
+
minX = Math.min(minX, pt.x);
|
|
458
|
+
minY = Math.min(minY, pt.y);
|
|
459
|
+
maxX = Math.max(maxX, pt.x);
|
|
460
|
+
maxY = Math.max(maxY, pt.y);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Create hybrid drawing
|
|
466
|
+
const hybridDrawing: Drawing2D = {
|
|
467
|
+
...result,
|
|
468
|
+
lines: combinedLines,
|
|
469
|
+
cutPolygons: filteredCutPolygons,
|
|
470
|
+
bounds: {
|
|
471
|
+
min: { x: isFinite(minX) ? minX : result.bounds.min.x, y: isFinite(minY) ? minY : result.bounds.min.y },
|
|
472
|
+
max: { x: isFinite(maxX) ? maxX : result.bounds.max.x, y: isFinite(maxY) ? maxY : result.bounds.max.y },
|
|
473
|
+
},
|
|
474
|
+
stats: {
|
|
475
|
+
...result.stats,
|
|
476
|
+
cutLineCount: combinedLines.length,
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
setDrawing(hybridDrawing);
|
|
481
|
+
} else {
|
|
482
|
+
setDrawing(result);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Always set status to ready (whether initial generation or regeneration)
|
|
486
|
+
setDrawingStatus('ready');
|
|
487
|
+
isRegeneratingRef.current = false;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error('Drawing generation failed:', error);
|
|
490
|
+
setDrawingError(error instanceof Error ? error.message : 'Generation failed');
|
|
491
|
+
} finally {
|
|
492
|
+
// Always cleanup generator to prevent resource leaks
|
|
493
|
+
generator?.dispose();
|
|
494
|
+
}
|
|
495
|
+
}, [
|
|
496
|
+
geometryResult,
|
|
497
|
+
ifcDataStore,
|
|
498
|
+
sectionPlane,
|
|
499
|
+
displayOptions,
|
|
500
|
+
combinedHiddenIds,
|
|
501
|
+
combinedIsolatedIds,
|
|
502
|
+
computedIsolatedIds,
|
|
503
|
+
models,
|
|
504
|
+
setDrawing,
|
|
505
|
+
setDrawingStatus,
|
|
506
|
+
setDrawingProgress,
|
|
507
|
+
setDrawingError,
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
// Track panel visibility and geometry for detecting changes
|
|
511
|
+
const prevPanelVisibleRef = useRef(false);
|
|
512
|
+
const prevOverlayEnabledRef = useRef(false);
|
|
513
|
+
const prevMeshCountRef = useRef(0);
|
|
514
|
+
|
|
515
|
+
// Auto-generate when panel opens (or 3D overlay is enabled) and no drawing exists
|
|
516
|
+
// Also regenerate when geometry changes significantly (e.g., models hidden/shown)
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
const wasVisible = prevPanelVisibleRef.current;
|
|
519
|
+
const wasOverlayEnabled = prevOverlayEnabledRef.current;
|
|
520
|
+
const prevMeshCount = prevMeshCountRef.current;
|
|
521
|
+
const currentMeshCount = geometryResult?.meshes?.length ?? 0;
|
|
522
|
+
const hasGeometry = currentMeshCount > 0;
|
|
523
|
+
|
|
524
|
+
// Track panel visibility separately from overlay
|
|
525
|
+
const panelJustOpened = panelVisible && !wasVisible;
|
|
526
|
+
const overlayJustEnabled = displayOptions.show3DOverlay && !wasOverlayEnabled;
|
|
527
|
+
const isNowActive = panelVisible || displayOptions.show3DOverlay;
|
|
528
|
+
const geometryChanged = currentMeshCount !== prevMeshCount;
|
|
529
|
+
|
|
530
|
+
// Always update refs
|
|
531
|
+
prevPanelVisibleRef.current = panelVisible;
|
|
532
|
+
prevOverlayEnabledRef.current = displayOptions.show3DOverlay;
|
|
533
|
+
prevMeshCountRef.current = currentMeshCount;
|
|
534
|
+
|
|
535
|
+
if (isNowActive) {
|
|
536
|
+
if (!hasGeometry) {
|
|
537
|
+
// No geometry available - clear the drawing
|
|
538
|
+
if (drawing) {
|
|
539
|
+
setDrawing(null);
|
|
540
|
+
setDrawingStatus('idle');
|
|
541
|
+
}
|
|
542
|
+
} else if (panelJustOpened || overlayJustEnabled || !drawing || geometryChanged) {
|
|
543
|
+
// Generate if:
|
|
544
|
+
// 1. Panel just opened, OR
|
|
545
|
+
// 2. Overlay just enabled, OR
|
|
546
|
+
// 3. No drawing exists, OR
|
|
547
|
+
// 4. Geometry changed significantly (models hidden/shown)
|
|
548
|
+
generateDrawing();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}, [panelVisible, displayOptions.show3DOverlay, drawing, geometryResult, generateDrawing, setDrawing, setDrawingStatus]);
|
|
552
|
+
|
|
553
|
+
// Auto-regenerate when section plane changes
|
|
554
|
+
// Strategy: INSTANT - no debounce, but prevent overlapping computations
|
|
555
|
+
// The generation time itself acts as natural batching for fast slider movements
|
|
556
|
+
const sectionRef = useRef({ axis: sectionPlane.axis, position: sectionPlane.position, flipped: sectionPlane.flipped });
|
|
557
|
+
const isGeneratingRef = useRef(false);
|
|
558
|
+
const latestSectionRef = useRef({ axis: sectionPlane.axis, position: sectionPlane.position, flipped: sectionPlane.flipped });
|
|
559
|
+
const [isRegenerating, setIsRegenerating] = useState(false);
|
|
560
|
+
|
|
561
|
+
// Stable regenerate function that handles overlapping calls
|
|
562
|
+
const doRegenerate = useCallback(async () => {
|
|
563
|
+
if (isGeneratingRef.current) {
|
|
564
|
+
// Already generating - the latest position is already tracked in latestSectionRef
|
|
565
|
+
// When current generation finishes, it will check if another is needed
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
isGeneratingRef.current = true;
|
|
570
|
+
setIsRegenerating(true);
|
|
571
|
+
|
|
572
|
+
// Capture position at start of generation
|
|
573
|
+
const targetSection = { ...latestSectionRef.current };
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
await generateDrawing(true);
|
|
577
|
+
} finally {
|
|
578
|
+
isGeneratingRef.current = false;
|
|
579
|
+
setIsRegenerating(false);
|
|
580
|
+
|
|
581
|
+
// Check if section changed while we were generating
|
|
582
|
+
const current = latestSectionRef.current;
|
|
583
|
+
if (
|
|
584
|
+
current.axis !== targetSection.axis ||
|
|
585
|
+
current.position !== targetSection.position ||
|
|
586
|
+
current.flipped !== targetSection.flipped
|
|
587
|
+
) {
|
|
588
|
+
// Position changed during generation - regenerate immediately with latest
|
|
589
|
+
// Use microtask to avoid blocking
|
|
590
|
+
queueMicrotask(() => doRegenerate());
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}, [generateDrawing]);
|
|
594
|
+
|
|
595
|
+
useEffect(() => {
|
|
596
|
+
// Always update latest section ref (even if generating)
|
|
597
|
+
latestSectionRef.current = { axis: sectionPlane.axis, position: sectionPlane.position, flipped: sectionPlane.flipped };
|
|
598
|
+
|
|
599
|
+
// Check if section plane actually changed from last processed
|
|
600
|
+
const prev = sectionRef.current;
|
|
601
|
+
if (
|
|
602
|
+
prev.axis === sectionPlane.axis &&
|
|
603
|
+
prev.position === sectionPlane.position &&
|
|
604
|
+
prev.flipped === sectionPlane.flipped
|
|
605
|
+
) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Update processed ref
|
|
610
|
+
sectionRef.current = { axis: sectionPlane.axis, position: sectionPlane.position, flipped: sectionPlane.flipped };
|
|
611
|
+
|
|
612
|
+
// If panel is visible OR 3D overlay is enabled, and we have geometry, regenerate INSTANTLY
|
|
613
|
+
if ((panelVisible || displayOptions.show3DOverlay) && geometryResult?.meshes) {
|
|
614
|
+
// Start immediately - no debounce
|
|
615
|
+
// doRegenerate handles preventing overlaps and will auto-regenerate with latest when done
|
|
616
|
+
doRegenerate();
|
|
617
|
+
}
|
|
618
|
+
}, [panelVisible, displayOptions.show3DOverlay, sectionPlane.axis, sectionPlane.position, sectionPlane.flipped, geometryResult, combinedHiddenIds, combinedIsolatedIds, computedIsolatedIds, doRegenerate]);
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
generateDrawing,
|
|
622
|
+
doRegenerate,
|
|
623
|
+
isRegenerating,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export default useDrawingGeneration;
|