@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
package/src/hooks/useBCF.ts
CHANGED
|
@@ -77,6 +77,13 @@ export function setGlobalRendererRef(ref: React.RefObject<Renderer | null>): voi
|
|
|
77
77
|
globalRendererRef = ref;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Get the global renderer instance (for direct rendering control, e.g., IDS snapshot capture)
|
|
82
|
+
*/
|
|
83
|
+
export function getGlobalRenderer(): Renderer | null {
|
|
84
|
+
return globalRendererRef?.current ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
80
87
|
/**
|
|
81
88
|
* Clear the global references (called on unmount to prevent memory leaks)
|
|
82
89
|
*/
|
|
@@ -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
|
+
import { useCallback } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
GraphicOverrideEngine,
|
|
8
|
+
renderFrame,
|
|
9
|
+
renderTitleBlock,
|
|
10
|
+
calculateDrawingTransform,
|
|
11
|
+
type Drawing2D,
|
|
12
|
+
type DrawingSheet,
|
|
13
|
+
type ElementData,
|
|
14
|
+
type TitleBlockExtras,
|
|
15
|
+
} from '@ifc-lite/drawing-2d';
|
|
16
|
+
import { getFillColorForType } from '@/components/viewer/Drawing2DCanvas';
|
|
17
|
+
import { formatDistance } from '@/components/viewer/tools/formatDistance';
|
|
18
|
+
|
|
19
|
+
interface UseDrawingExportParams {
|
|
20
|
+
drawing: Drawing2D | null;
|
|
21
|
+
displayOptions: { showHiddenLines: boolean; scale: number };
|
|
22
|
+
sectionPlane: { axis: 'down' | 'front' | 'side'; position: number; flipped: boolean };
|
|
23
|
+
activePresetId: string | null;
|
|
24
|
+
entityColorMap: Map<number, [number, number, number, number]>;
|
|
25
|
+
overridesEnabled: boolean;
|
|
26
|
+
overrideEngine: GraphicOverrideEngine;
|
|
27
|
+
measure2DResults: Array<{ id: string; start: { x: number; y: number }; end: { x: number; y: number }; distance: number }>;
|
|
28
|
+
sheetEnabled: boolean;
|
|
29
|
+
activeSheet: DrawingSheet | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface UseDrawingExportResult {
|
|
33
|
+
formatDistance: (distance: number) => string;
|
|
34
|
+
handleExportSVG: () => void;
|
|
35
|
+
handlePrint: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useDrawingExport({
|
|
39
|
+
drawing,
|
|
40
|
+
displayOptions,
|
|
41
|
+
sectionPlane,
|
|
42
|
+
activePresetId,
|
|
43
|
+
entityColorMap,
|
|
44
|
+
overridesEnabled,
|
|
45
|
+
overrideEngine,
|
|
46
|
+
measure2DResults,
|
|
47
|
+
sheetEnabled,
|
|
48
|
+
activeSheet,
|
|
49
|
+
}: UseDrawingExportParams): UseDrawingExportResult {
|
|
50
|
+
|
|
51
|
+
// Generate SVG that matches the canvas rendering exactly
|
|
52
|
+
const generateExportSVG = useCallback((): string | null => {
|
|
53
|
+
if (!drawing) return null;
|
|
54
|
+
|
|
55
|
+
const { bounds } = drawing;
|
|
56
|
+
const width = bounds.max.x - bounds.min.x;
|
|
57
|
+
const height = bounds.max.y - bounds.min.y;
|
|
58
|
+
|
|
59
|
+
// Add padding around the drawing
|
|
60
|
+
const padding = Math.max(width, height) * 0.1;
|
|
61
|
+
const viewMinX = bounds.min.x - padding;
|
|
62
|
+
const viewMinY = bounds.min.y - padding;
|
|
63
|
+
const viewWidth = width + padding * 2;
|
|
64
|
+
const viewHeight = height + padding * 2;
|
|
65
|
+
|
|
66
|
+
// SVG dimensions in mm (assuming model is in meters, scale 1:100)
|
|
67
|
+
const scale = displayOptions.scale || 100;
|
|
68
|
+
const svgWidthMm = (viewWidth * 1000) / scale;
|
|
69
|
+
const svgHeightMm = (viewHeight * 1000) / scale;
|
|
70
|
+
|
|
71
|
+
// Convert mm on paper to model units (meters)
|
|
72
|
+
// At 1:100 scale, 1mm on paper = 0.1m in model space
|
|
73
|
+
// Formula: modelUnits = paperMm * scale / 1000
|
|
74
|
+
const mmToModel = (mm: number) => mm * scale / 1000;
|
|
75
|
+
|
|
76
|
+
// Helper to escape XML
|
|
77
|
+
const escapeXml = (str: string): string => {
|
|
78
|
+
return str
|
|
79
|
+
.replace(/&/g, '&')
|
|
80
|
+
.replace(/</g, '<')
|
|
81
|
+
.replace(/>/g, '>')
|
|
82
|
+
.replace(/"/g, '"')
|
|
83
|
+
.replace(/'/g, ''');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Axis-specific flipping (matching canvas rendering)
|
|
87
|
+
// - 'down' (plan view): DON'T flip Y so north (Z+) is up
|
|
88
|
+
// - 'front' and 'side': flip Y so height (Y+) is up
|
|
89
|
+
// - 'side': also flip X to look from conventional direction
|
|
90
|
+
const currentAxis = sectionPlane.axis;
|
|
91
|
+
const flipY = currentAxis !== 'down';
|
|
92
|
+
const flipX = currentAxis === 'side';
|
|
93
|
+
|
|
94
|
+
// Helper to get polygon path with axis-specific coordinate transformation
|
|
95
|
+
const polygonToPath = (polygon: { outer: { x: number; y: number }[]; holes: { x: number; y: number }[][] }): string => {
|
|
96
|
+
const transformPt = (x: number, y: number) => ({
|
|
97
|
+
x: flipX ? -x : x,
|
|
98
|
+
y: flipY ? -y : y,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let path = '';
|
|
102
|
+
if (polygon.outer.length > 0) {
|
|
103
|
+
const first = transformPt(polygon.outer[0].x, polygon.outer[0].y);
|
|
104
|
+
path += `M ${first.x.toFixed(4)} ${first.y.toFixed(4)}`;
|
|
105
|
+
for (let i = 1; i < polygon.outer.length; i++) {
|
|
106
|
+
const pt = transformPt(polygon.outer[i].x, polygon.outer[i].y);
|
|
107
|
+
path += ` L ${pt.x.toFixed(4)} ${pt.y.toFixed(4)}`;
|
|
108
|
+
}
|
|
109
|
+
path += ' Z';
|
|
110
|
+
}
|
|
111
|
+
for (const hole of polygon.holes) {
|
|
112
|
+
if (hole.length > 0) {
|
|
113
|
+
const holeFirst = transformPt(hole[0].x, hole[0].y);
|
|
114
|
+
path += ` M ${holeFirst.x.toFixed(4)} ${holeFirst.y.toFixed(4)}`;
|
|
115
|
+
for (let i = 1; i < hole.length; i++) {
|
|
116
|
+
const pt = transformPt(hole[i].x, hole[i].y);
|
|
117
|
+
path += ` L ${pt.x.toFixed(4)} ${pt.y.toFixed(4)}`;
|
|
118
|
+
}
|
|
119
|
+
path += ' Z';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return path;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Calculate viewBox with axis-specific flipping
|
|
126
|
+
const viewBoxMinX = flipX ? -viewMinX - viewWidth : viewMinX;
|
|
127
|
+
const viewBoxMinY = flipY ? -viewMinY - viewHeight : viewMinY;
|
|
128
|
+
|
|
129
|
+
// Start building SVG
|
|
130
|
+
let svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
131
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
132
|
+
width="${svgWidthMm.toFixed(2)}mm"
|
|
133
|
+
height="${svgHeightMm.toFixed(2)}mm"
|
|
134
|
+
viewBox="${viewBoxMinX.toFixed(4)} ${viewBoxMinY.toFixed(4)} ${viewWidth.toFixed(4)} ${viewHeight.toFixed(4)}">
|
|
135
|
+
<rect x="${viewBoxMinX.toFixed(4)}" y="${viewBoxMinY.toFixed(4)}" width="${viewWidth.toFixed(4)}" height="${viewHeight.toFixed(4)}" fill="#FFFFFF"/>
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
// 1. FILL CUT POLYGONS (with color from IFC materials or override engine)
|
|
139
|
+
svg += ' <g id="polygon-fills">\n';
|
|
140
|
+
for (const polygon of drawing.cutPolygons) {
|
|
141
|
+
let fillColor = getFillColorForType(polygon.ifcType);
|
|
142
|
+
let opacity = 1;
|
|
143
|
+
|
|
144
|
+
// Use actual IFC material colors from the mesh data
|
|
145
|
+
if (activePresetId === 'preset-3d-colors') {
|
|
146
|
+
const materialColor = entityColorMap.get(polygon.entityId);
|
|
147
|
+
if (materialColor) {
|
|
148
|
+
const r = Math.round(materialColor[0] * 255);
|
|
149
|
+
const g = Math.round(materialColor[1] * 255);
|
|
150
|
+
const b = Math.round(materialColor[2] * 255);
|
|
151
|
+
fillColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
152
|
+
opacity = materialColor[3];
|
|
153
|
+
}
|
|
154
|
+
} else if (overridesEnabled) {
|
|
155
|
+
const elementData: ElementData = {
|
|
156
|
+
expressId: polygon.entityId,
|
|
157
|
+
ifcType: polygon.ifcType,
|
|
158
|
+
};
|
|
159
|
+
const result = overrideEngine.applyOverrides(elementData);
|
|
160
|
+
fillColor = result.style.fillColor;
|
|
161
|
+
opacity = result.style.opacity;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pathData = polygonToPath(polygon.polygon);
|
|
165
|
+
svg += ` <path d="${pathData}" fill="${fillColor}" fill-opacity="${opacity.toFixed(2)}" fill-rule="evenodd" data-entity-id="${polygon.entityId}" data-ifc-type="${escapeXml(polygon.ifcType)}"/>\n`;
|
|
166
|
+
}
|
|
167
|
+
svg += ' </g>\n';
|
|
168
|
+
|
|
169
|
+
// 2. STROKE CUT POLYGON OUTLINES (with color from override engine)
|
|
170
|
+
svg += ' <g id="polygon-outlines">\n';
|
|
171
|
+
for (const polygon of drawing.cutPolygons) {
|
|
172
|
+
let strokeColor = '#000000';
|
|
173
|
+
let lineWeight = 0.5;
|
|
174
|
+
|
|
175
|
+
if (overridesEnabled) {
|
|
176
|
+
const elementData: ElementData = {
|
|
177
|
+
expressId: polygon.entityId,
|
|
178
|
+
ifcType: polygon.ifcType,
|
|
179
|
+
};
|
|
180
|
+
const result = overrideEngine.applyOverrides(elementData);
|
|
181
|
+
strokeColor = result.style.strokeColor;
|
|
182
|
+
lineWeight = result.style.lineWeight;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const pathData = polygonToPath(polygon.polygon);
|
|
186
|
+
// Convert line weight (mm on paper) to model units
|
|
187
|
+
const svgLineWeight = mmToModel(lineWeight);
|
|
188
|
+
svg += ` <path d="${pathData}" fill="none" stroke="${strokeColor}" stroke-width="${svgLineWeight.toFixed(4)}" data-entity-id="${polygon.entityId}"/>\n`;
|
|
189
|
+
}
|
|
190
|
+
svg += ' </g>\n';
|
|
191
|
+
|
|
192
|
+
// 3. DRAW PROJECTION/SILHOUETTE LINES
|
|
193
|
+
// Pre-compute bounds for line validation
|
|
194
|
+
const lineBounds = drawing.bounds;
|
|
195
|
+
const lineMargin = Math.max(lineBounds.max.x - lineBounds.min.x, lineBounds.max.y - lineBounds.min.y) * 0.5;
|
|
196
|
+
const lineMinX = lineBounds.min.x - lineMargin;
|
|
197
|
+
const lineMaxX = lineBounds.max.x + lineMargin;
|
|
198
|
+
const lineMinY = lineBounds.min.y - lineMargin;
|
|
199
|
+
const lineMaxY = lineBounds.max.y + lineMargin;
|
|
200
|
+
|
|
201
|
+
svg += ' <g id="drawing-lines">\n';
|
|
202
|
+
for (const line of drawing.lines) {
|
|
203
|
+
// Skip 'cut' lines - they're triangulation edges, already handled by polygons
|
|
204
|
+
if (line.category === 'cut') continue;
|
|
205
|
+
|
|
206
|
+
// Skip hidden lines if not showing
|
|
207
|
+
if (!displayOptions.showHiddenLines && line.visibility === 'hidden') continue;
|
|
208
|
+
|
|
209
|
+
// Skip lines with invalid coordinates
|
|
210
|
+
const { start, end } = line.line;
|
|
211
|
+
if (!isFinite(start.x) || !isFinite(start.y) || !isFinite(end.x) || !isFinite(end.y)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (start.x < lineMinX || start.x > lineMaxX || start.y < lineMinY || start.y > lineMaxY ||
|
|
215
|
+
end.x < lineMinX || end.x > lineMaxX || end.y < lineMinY || end.y > lineMaxY) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Set line style based on category
|
|
220
|
+
let strokeColor = '#000000';
|
|
221
|
+
let lineWidth = 0.25;
|
|
222
|
+
let dashArray = '';
|
|
223
|
+
|
|
224
|
+
switch (line.category) {
|
|
225
|
+
case 'projection':
|
|
226
|
+
lineWidth = 0.25;
|
|
227
|
+
strokeColor = '#000000';
|
|
228
|
+
break;
|
|
229
|
+
case 'hidden':
|
|
230
|
+
lineWidth = 0.18;
|
|
231
|
+
strokeColor = '#666666';
|
|
232
|
+
dashArray = '2 1';
|
|
233
|
+
break;
|
|
234
|
+
case 'silhouette':
|
|
235
|
+
lineWidth = 0.35;
|
|
236
|
+
strokeColor = '#000000';
|
|
237
|
+
break;
|
|
238
|
+
case 'crease':
|
|
239
|
+
lineWidth = 0.18;
|
|
240
|
+
strokeColor = '#000000';
|
|
241
|
+
break;
|
|
242
|
+
case 'boundary':
|
|
243
|
+
lineWidth = 0.25;
|
|
244
|
+
strokeColor = '#000000';
|
|
245
|
+
break;
|
|
246
|
+
case 'annotation':
|
|
247
|
+
lineWidth = 0.13;
|
|
248
|
+
strokeColor = '#000000';
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Hidden visibility overrides
|
|
253
|
+
if (line.visibility === 'hidden') {
|
|
254
|
+
strokeColor = '#888888';
|
|
255
|
+
dashArray = '2 1';
|
|
256
|
+
lineWidth *= 0.7;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Convert line width from mm on paper to model units
|
|
260
|
+
const svgLineWidth = mmToModel(lineWidth);
|
|
261
|
+
const dashAttr = dashArray ? ` stroke-dasharray="${dashArray.split(' ').map(d => mmToModel(parseFloat(d)).toFixed(4)).join(' ')}"` : '';
|
|
262
|
+
|
|
263
|
+
// Transform line endpoints with axis-specific flipping
|
|
264
|
+
const startT = { x: flipX ? -start.x : start.x, y: flipY ? -start.y : start.y };
|
|
265
|
+
const endT = { x: flipX ? -end.x : end.x, y: flipY ? -end.y : end.y };
|
|
266
|
+
svg += ` <line x1="${startT.x.toFixed(4)}" y1="${startT.y.toFixed(4)}" x2="${endT.x.toFixed(4)}" y2="${endT.y.toFixed(4)}" stroke="${strokeColor}" stroke-width="${svgLineWidth.toFixed(4)}"${dashAttr}/>\n`;
|
|
267
|
+
}
|
|
268
|
+
svg += ' </g>\n';
|
|
269
|
+
|
|
270
|
+
// 4. DRAW COMPLETED MEASUREMENTS
|
|
271
|
+
if (measure2DResults.length > 0) {
|
|
272
|
+
svg += ' <g id="measurements">\n';
|
|
273
|
+
for (const result of measure2DResults) {
|
|
274
|
+
const { start, end, distance } = result;
|
|
275
|
+
// Transform measurement points with axis-specific flipping
|
|
276
|
+
const startT = { x: flipX ? -start.x : start.x, y: flipY ? -start.y : start.y };
|
|
277
|
+
const endT = { x: flipX ? -end.x : end.x, y: flipY ? -end.y : end.y };
|
|
278
|
+
const midX = (startT.x + endT.x) / 2;
|
|
279
|
+
const midY = (startT.y + endT.y) / 2;
|
|
280
|
+
const labelText = formatDistance(distance);
|
|
281
|
+
|
|
282
|
+
// Measurement styling (all in mm on paper, converted to model units)
|
|
283
|
+
const measureColor = '#2196F3';
|
|
284
|
+
const measureLineWidth = mmToModel(0.4); // 0.4mm line on paper
|
|
285
|
+
const endpointRadius = mmToModel(1.5); // 1.5mm radius on paper
|
|
286
|
+
|
|
287
|
+
// Draw line
|
|
288
|
+
svg += ` <line x1="${startT.x.toFixed(4)}" y1="${startT.y.toFixed(4)}" x2="${endT.x.toFixed(4)}" y2="${endT.y.toFixed(4)}" stroke="${measureColor}" stroke-width="${measureLineWidth.toFixed(4)}"/>\n`;
|
|
289
|
+
|
|
290
|
+
// Draw endpoints
|
|
291
|
+
svg += ` <circle cx="${startT.x.toFixed(4)}" cy="${startT.y.toFixed(4)}" r="${endpointRadius.toFixed(4)}" fill="${measureColor}"/>\n`;
|
|
292
|
+
svg += ` <circle cx="${endT.x.toFixed(4)}" cy="${endT.y.toFixed(4)}" r="${endpointRadius.toFixed(4)}" fill="${measureColor}"/>\n`;
|
|
293
|
+
|
|
294
|
+
// Draw label background and text
|
|
295
|
+
// Use 3mm text height on paper for readable labels
|
|
296
|
+
const fontSize = mmToModel(3);
|
|
297
|
+
const labelWidth = labelText.length * fontSize * 0.6; // Approximate text width
|
|
298
|
+
const labelHeight = fontSize * 1.4;
|
|
299
|
+
const labelStroke = mmToModel(0.2);
|
|
300
|
+
|
|
301
|
+
svg += ` <rect x="${(midX - labelWidth / 2).toFixed(4)}" y="${(midY - labelHeight / 2).toFixed(4)}" width="${labelWidth.toFixed(4)}" height="${labelHeight.toFixed(4)}" fill="rgba(255,255,255,0.95)" stroke="${measureColor}" stroke-width="${labelStroke.toFixed(4)}"/>\n`;
|
|
302
|
+
svg += ` <text x="${midX.toFixed(4)}" y="${midY.toFixed(4)}" font-family="Arial, sans-serif" font-size="${fontSize.toFixed(4)}" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-weight="500">${escapeXml(labelText)}</text>\n`;
|
|
303
|
+
}
|
|
304
|
+
svg += ' </g>\n';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
svg += '</svg>';
|
|
308
|
+
return svg;
|
|
309
|
+
}, [drawing, displayOptions, activePresetId, entityColorMap, overridesEnabled, overrideEngine, measure2DResults, sectionPlane.axis]);
|
|
310
|
+
|
|
311
|
+
// Generate SVG with drawing sheet (frame, title block, scale bar)
|
|
312
|
+
// This generates coordinates directly in paper mm space (like the canvas rendering)
|
|
313
|
+
const generateSheetSVG = useCallback((): string | null => {
|
|
314
|
+
if (!drawing || !activeSheet) return null;
|
|
315
|
+
|
|
316
|
+
const { bounds } = drawing;
|
|
317
|
+
|
|
318
|
+
// Sheet dimensions in mm
|
|
319
|
+
const paperWidth = activeSheet.paper.widthMm;
|
|
320
|
+
const paperHeight = activeSheet.paper.heightMm;
|
|
321
|
+
const viewport = activeSheet.viewportBounds;
|
|
322
|
+
|
|
323
|
+
// Calculate transform to fit drawing into viewport
|
|
324
|
+
const drawingTransform = calculateDrawingTransform(
|
|
325
|
+
{ minX: bounds.min.x, minY: bounds.min.y, maxX: bounds.max.x, maxY: bounds.max.y },
|
|
326
|
+
viewport,
|
|
327
|
+
activeSheet.scale
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const { translateX, translateY, scaleFactor } = drawingTransform;
|
|
331
|
+
|
|
332
|
+
// Axis-specific flipping (matching canvas rendering)
|
|
333
|
+
// - 'down' (plan view): DON'T flip Y so north (Z+) is up
|
|
334
|
+
// - 'front' and 'side': flip Y so height (Y+) is up
|
|
335
|
+
// - 'side': also flip X to look from conventional direction
|
|
336
|
+
const currentAxis = sectionPlane.axis;
|
|
337
|
+
const flipY = currentAxis !== 'down';
|
|
338
|
+
const flipX = currentAxis === 'side';
|
|
339
|
+
|
|
340
|
+
// Helper: convert model coordinates to paper mm (matching canvas rendering exactly)
|
|
341
|
+
const modelToPaper = (x: number, y: number): { x: number; y: number } => {
|
|
342
|
+
const adjustedX = flipX ? -x : x;
|
|
343
|
+
const adjustedY = flipY ? -y : y;
|
|
344
|
+
return {
|
|
345
|
+
x: adjustedX * scaleFactor + translateX,
|
|
346
|
+
y: adjustedY * scaleFactor + translateY,
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Start building SVG (paper coordinates in mm)
|
|
351
|
+
let svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
352
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
353
|
+
width="${paperWidth}mm"
|
|
354
|
+
height="${paperHeight}mm"
|
|
355
|
+
viewBox="0 0 ${paperWidth} ${paperHeight}">
|
|
356
|
+
<!-- Background -->
|
|
357
|
+
<rect x="0" y="0" width="${paperWidth}" height="${paperHeight}" fill="#FFFFFF"/>
|
|
358
|
+
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
// Create clipping path for viewport FIRST (so it can be used by drawing content)
|
|
362
|
+
svg += ` <defs>
|
|
363
|
+
<clipPath id="viewport-clip">
|
|
364
|
+
<rect x="${viewport.x.toFixed(2)}" y="${viewport.y.toFixed(2)}" width="${viewport.width.toFixed(2)}" height="${viewport.height.toFixed(2)}"/>
|
|
365
|
+
</clipPath>
|
|
366
|
+
</defs>
|
|
367
|
+
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
// Drawing content FIRST (so frame/title block render on top)
|
|
371
|
+
svg += ` <g id="drawing-content" clip-path="url(#viewport-clip)">
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
// Helper to escape XML
|
|
375
|
+
const escapeXml = (str: string): string => {
|
|
376
|
+
return str
|
|
377
|
+
.replace(/&/g, '&')
|
|
378
|
+
.replace(/</g, '<')
|
|
379
|
+
.replace(/>/g, '>')
|
|
380
|
+
.replace(/"/g, '"')
|
|
381
|
+
.replace(/'/g, ''');
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Helper to get polygon path in paper coordinates
|
|
385
|
+
const polygonToPath = (polygon: { outer: { x: number; y: number }[]; holes: { x: number; y: number }[][] }): string => {
|
|
386
|
+
let path = '';
|
|
387
|
+
if (polygon.outer.length > 0) {
|
|
388
|
+
const first = modelToPaper(polygon.outer[0].x, polygon.outer[0].y);
|
|
389
|
+
path += `M ${first.x.toFixed(4)} ${first.y.toFixed(4)}`;
|
|
390
|
+
for (let i = 1; i < polygon.outer.length; i++) {
|
|
391
|
+
const pt = modelToPaper(polygon.outer[i].x, polygon.outer[i].y);
|
|
392
|
+
path += ` L ${pt.x.toFixed(4)} ${pt.y.toFixed(4)}`;
|
|
393
|
+
}
|
|
394
|
+
path += ' Z';
|
|
395
|
+
}
|
|
396
|
+
for (const hole of polygon.holes) {
|
|
397
|
+
if (hole.length > 0) {
|
|
398
|
+
const holeFirst = modelToPaper(hole[0].x, hole[0].y);
|
|
399
|
+
path += ` M ${holeFirst.x.toFixed(4)} ${holeFirst.y.toFixed(4)}`;
|
|
400
|
+
for (let i = 1; i < hole.length; i++) {
|
|
401
|
+
const pt = modelToPaper(hole[i].x, hole[i].y);
|
|
402
|
+
path += ` L ${pt.x.toFixed(4)} ${pt.y.toFixed(4)}`;
|
|
403
|
+
}
|
|
404
|
+
path += ' Z';
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return path;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Render polygon fills
|
|
411
|
+
svg += ' <g id="polygon-fills">\n';
|
|
412
|
+
for (const polygon of drawing.cutPolygons) {
|
|
413
|
+
let fillColor = getFillColorForType(polygon.ifcType);
|
|
414
|
+
let opacity = 1;
|
|
415
|
+
|
|
416
|
+
if (activePresetId === 'preset-3d-colors') {
|
|
417
|
+
const materialColor = entityColorMap.get(polygon.entityId);
|
|
418
|
+
if (materialColor) {
|
|
419
|
+
const r = Math.round(materialColor[0] * 255);
|
|
420
|
+
const g = Math.round(materialColor[1] * 255);
|
|
421
|
+
const b = Math.round(materialColor[2] * 255);
|
|
422
|
+
fillColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
423
|
+
opacity = materialColor[3];
|
|
424
|
+
}
|
|
425
|
+
} else if (overridesEnabled) {
|
|
426
|
+
const elementData: ElementData = {
|
|
427
|
+
expressId: polygon.entityId,
|
|
428
|
+
ifcType: polygon.ifcType,
|
|
429
|
+
};
|
|
430
|
+
const result = overrideEngine.applyOverrides(elementData);
|
|
431
|
+
fillColor = result.style.fillColor;
|
|
432
|
+
opacity = result.style.opacity;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const pathData = polygonToPath(polygon.polygon);
|
|
436
|
+
if (pathData) {
|
|
437
|
+
svg += ` <path d="${pathData}" fill="${fillColor}" fill-opacity="${opacity.toFixed(2)}" fill-rule="evenodd" data-entity-id="${polygon.entityId}" data-ifc-type="${escapeXml(polygon.ifcType)}"/>\n`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
svg += ' </g>\n';
|
|
441
|
+
|
|
442
|
+
// Render polygon outlines
|
|
443
|
+
svg += ' <g id="polygon-outlines">\n';
|
|
444
|
+
for (const polygon of drawing.cutPolygons) {
|
|
445
|
+
let strokeColor = '#000000';
|
|
446
|
+
let lineWeight = 0.5;
|
|
447
|
+
|
|
448
|
+
if (overridesEnabled) {
|
|
449
|
+
const elementData: ElementData = {
|
|
450
|
+
expressId: polygon.entityId,
|
|
451
|
+
ifcType: polygon.ifcType,
|
|
452
|
+
};
|
|
453
|
+
const result = overrideEngine.applyOverrides(elementData);
|
|
454
|
+
strokeColor = result.style.strokeColor;
|
|
455
|
+
lineWeight = result.style.lineWeight;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const pathData = polygonToPath(polygon.polygon);
|
|
459
|
+
if (pathData) {
|
|
460
|
+
// lineWeight is in mm on paper
|
|
461
|
+
const svgLineWeight = lineWeight * 0.3; // Scale down for better appearance
|
|
462
|
+
svg += ` <path d="${pathData}" fill="none" stroke="${strokeColor}" stroke-width="${svgLineWeight.toFixed(4)}" data-entity-id="${polygon.entityId}"/>\n`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
svg += ' </g>\n';
|
|
466
|
+
|
|
467
|
+
// Render drawing lines
|
|
468
|
+
const lineBounds = drawing.bounds;
|
|
469
|
+
const lineMargin = Math.max(lineBounds.max.x - lineBounds.min.x, lineBounds.max.y - lineBounds.min.y) * 0.5;
|
|
470
|
+
const lineMinX = lineBounds.min.x - lineMargin;
|
|
471
|
+
const lineMaxX = lineBounds.max.x + lineMargin;
|
|
472
|
+
const lineMinY = lineBounds.min.y - lineMargin;
|
|
473
|
+
const lineMaxY = lineBounds.max.y + lineMargin;
|
|
474
|
+
|
|
475
|
+
svg += ' <g id="drawing-lines">\n';
|
|
476
|
+
for (const line of drawing.lines) {
|
|
477
|
+
if (line.category === 'cut') continue;
|
|
478
|
+
if (!displayOptions.showHiddenLines && line.visibility === 'hidden') continue;
|
|
479
|
+
|
|
480
|
+
const { start, end } = line.line;
|
|
481
|
+
if (!isFinite(start.x) || !isFinite(start.y) || !isFinite(end.x) || !isFinite(end.y)) continue;
|
|
482
|
+
if (start.x < lineMinX || start.x > lineMaxX || start.y < lineMinY || start.y > lineMaxY ||
|
|
483
|
+
end.x < lineMinX || end.x > lineMaxX || end.y < lineMinY || end.y > lineMaxY) continue;
|
|
484
|
+
|
|
485
|
+
let strokeColor = '#000000';
|
|
486
|
+
let lineWidth = 0.25;
|
|
487
|
+
let dashArray = '';
|
|
488
|
+
|
|
489
|
+
switch (line.category) {
|
|
490
|
+
case 'projection': lineWidth = 0.25; break;
|
|
491
|
+
case 'hidden': lineWidth = 0.18; strokeColor = '#666666'; dashArray = '1 0.5'; break;
|
|
492
|
+
case 'silhouette': lineWidth = 0.35; break;
|
|
493
|
+
case 'crease': lineWidth = 0.18; break;
|
|
494
|
+
case 'boundary': lineWidth = 0.25; break;
|
|
495
|
+
case 'annotation': lineWidth = 0.13; break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (line.visibility === 'hidden') {
|
|
499
|
+
strokeColor = '#888888';
|
|
500
|
+
dashArray = '1 0.5';
|
|
501
|
+
lineWidth *= 0.7;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const paperStart = modelToPaper(start.x, start.y);
|
|
505
|
+
const paperEnd = modelToPaper(end.x, end.y);
|
|
506
|
+
|
|
507
|
+
// lineWidth is in mm on paper
|
|
508
|
+
const svgLineWidth = lineWidth * 0.3;
|
|
509
|
+
const dashAttr = dashArray ? ` stroke-dasharray="${dashArray}"` : '';
|
|
510
|
+
svg += ` <line x1="${paperStart.x.toFixed(4)}" y1="${paperStart.y.toFixed(4)}" x2="${paperEnd.x.toFixed(4)}" y2="${paperEnd.y.toFixed(4)}" stroke="${strokeColor}" stroke-width="${svgLineWidth.toFixed(4)}"${dashAttr}/>\n`;
|
|
511
|
+
}
|
|
512
|
+
svg += ' </g>\n';
|
|
513
|
+
|
|
514
|
+
svg += ' </g>\n\n';
|
|
515
|
+
|
|
516
|
+
// Render frame (on top of drawing content)
|
|
517
|
+
const frameResult = renderFrame(activeSheet.paper, activeSheet.frame);
|
|
518
|
+
svg += frameResult.svgElements;
|
|
519
|
+
svg += '\n';
|
|
520
|
+
|
|
521
|
+
// Render title block with scale bar and north arrow inside
|
|
522
|
+
// Pass effectiveScaleFactor from the actual transform (not just configured scale)
|
|
523
|
+
// This ensures scale bar shows correct values when dynamically scaled
|
|
524
|
+
const titleBlockExtras: TitleBlockExtras = {
|
|
525
|
+
scaleBar: activeSheet.scaleBar,
|
|
526
|
+
northArrow: activeSheet.northArrow,
|
|
527
|
+
scale: activeSheet.scale,
|
|
528
|
+
effectiveScaleFactor: scaleFactor,
|
|
529
|
+
};
|
|
530
|
+
const titleBlockResult = renderTitleBlock(
|
|
531
|
+
activeSheet.titleBlock,
|
|
532
|
+
frameResult.innerBounds,
|
|
533
|
+
activeSheet.revisions,
|
|
534
|
+
titleBlockExtras
|
|
535
|
+
);
|
|
536
|
+
svg += titleBlockResult.svgElements;
|
|
537
|
+
svg += '\n';
|
|
538
|
+
|
|
539
|
+
svg += '</svg>';
|
|
540
|
+
return svg;
|
|
541
|
+
}, [drawing, activeSheet, displayOptions, activePresetId, entityColorMap, overridesEnabled, overrideEngine]);
|
|
542
|
+
|
|
543
|
+
// Export SVG
|
|
544
|
+
const handleExportSVG = useCallback(() => {
|
|
545
|
+
// Use sheet export if enabled, otherwise raw drawing export
|
|
546
|
+
const svg = (sheetEnabled && activeSheet) ? generateSheetSVG() : generateExportSVG();
|
|
547
|
+
if (!svg) return;
|
|
548
|
+
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
|
549
|
+
const url = URL.createObjectURL(blob);
|
|
550
|
+
const a = document.createElement('a');
|
|
551
|
+
a.href = url;
|
|
552
|
+
const filename = (sheetEnabled && activeSheet)
|
|
553
|
+
? `${activeSheet.name.replace(/\s+/g, '-')}-${sectionPlane.axis}-${sectionPlane.position}.svg`
|
|
554
|
+
: `section-${sectionPlane.axis}-${sectionPlane.position}.svg`;
|
|
555
|
+
a.download = filename;
|
|
556
|
+
a.click();
|
|
557
|
+
URL.revokeObjectURL(url);
|
|
558
|
+
}, [generateExportSVG, generateSheetSVG, sheetEnabled, activeSheet, sectionPlane]);
|
|
559
|
+
|
|
560
|
+
// Print handler
|
|
561
|
+
const handlePrint = useCallback(() => {
|
|
562
|
+
// Use sheet export if enabled, otherwise raw drawing export
|
|
563
|
+
const svg = (sheetEnabled && activeSheet) ? generateSheetSVG() : generateExportSVG();
|
|
564
|
+
if (!svg) return;
|
|
565
|
+
|
|
566
|
+
// Create a new window for printing
|
|
567
|
+
const printWindow = window.open('', '_blank', 'width=800,height=600');
|
|
568
|
+
if (!printWindow) {
|
|
569
|
+
alert('Please allow popups to print');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const title = (sheetEnabled && activeSheet)
|
|
574
|
+
? `${activeSheet.name} - ${sectionPlane.axis} at ${sectionPlane.position}%`
|
|
575
|
+
: `Section Drawing - ${sectionPlane.axis} at ${sectionPlane.position}%`;
|
|
576
|
+
|
|
577
|
+
// Write print-friendly HTML with the SVG
|
|
578
|
+
printWindow.document.write(`
|
|
579
|
+
<!DOCTYPE html>
|
|
580
|
+
<html>
|
|
581
|
+
<head>
|
|
582
|
+
<title>${title}</title>
|
|
583
|
+
<style>
|
|
584
|
+
@media print {
|
|
585
|
+
@page { margin: ${(sheetEnabled && activeSheet) ? '0' : '1cm'}; }
|
|
586
|
+
body { margin: 0; }
|
|
587
|
+
}
|
|
588
|
+
body {
|
|
589
|
+
display: flex;
|
|
590
|
+
justify-content: center;
|
|
591
|
+
align-items: center;
|
|
592
|
+
min-height: 100vh;
|
|
593
|
+
margin: 0;
|
|
594
|
+
padding: ${(sheetEnabled && activeSheet) ? '0' : '20px'};
|
|
595
|
+
box-sizing: border-box;
|
|
596
|
+
}
|
|
597
|
+
svg {
|
|
598
|
+
max-width: 100%;
|
|
599
|
+
max-height: 100vh;
|
|
600
|
+
width: auto;
|
|
601
|
+
height: auto;
|
|
602
|
+
}
|
|
603
|
+
</style>
|
|
604
|
+
</head>
|
|
605
|
+
<body>
|
|
606
|
+
${svg}
|
|
607
|
+
<script>
|
|
608
|
+
window.onload = function() {
|
|
609
|
+
window.print();
|
|
610
|
+
window.onafterprint = function() { window.close(); };
|
|
611
|
+
};
|
|
612
|
+
</script>
|
|
613
|
+
</body>
|
|
614
|
+
</html>
|
|
615
|
+
`);
|
|
616
|
+
printWindow.document.close();
|
|
617
|
+
}, [generateExportSVG, generateSheetSVG, sheetEnabled, activeSheet, sectionPlane]);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
formatDistance,
|
|
621
|
+
handleExportSVG,
|
|
622
|
+
handlePrint,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export { useDrawingExport };
|
|
627
|
+
export default useDrawingExport;
|