@ifc-lite/viewer 1.24.0 → 1.25.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/.turbo/turbo-build.log +28 -28
- package/CHANGELOG.md +49 -0
- package/dist/assets/{basketViewActivator-BxyL3ITR.js → basketViewActivator-CU8_toGq.js} +7 -7
- package/dist/assets/{bcf-OInQ7hh6.js → bcf-DXGDhw56.js} +23 -23
- package/dist/assets/{deflate-D0Sm0vyt.js → deflate-Bb1_H2Yf.js} +1 -1
- package/dist/assets/{exporters-CsSbJLHQ.js → exporters-DZhLN0ux.js} +58 -58
- package/dist/assets/{geometry-controller.worker-DuPxLFYp.js → geometry-controller.worker-DQOSYqtw.js} +2 -2
- package/dist/assets/{geometry.worker-DvOb53b0.js → geometry.worker-B62e03Ao.js} +1 -1
- package/dist/assets/{geotiff-DBMPIaHW.js → geotiff-y0ZxbRJd.js} +10 -10
- package/dist/assets/{ids-DCWn6VHu.js → ids-DruUNtfD.js} +3 -3
- package/dist/assets/{ifc-lite-BUH-uP-q.js → ifc-lite-Ch2T9pP9.js} +2 -2
- package/dist/assets/{ifc-lite_bg-DHXAIZHs.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-CeGXwVPt.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
- package/dist/assets/{index-DAbQbcNs.js → index-Dr88ZlSY.js} +31236 -30870
- package/dist/assets/{jpeg-Btyb9xCl.js → jpeg-B3_loqFe.js} +1 -1
- package/dist/assets/lens-PYsLu_MA.js +1 -0
- package/dist/assets/{lerc-4m4sf12j.js → lerc-nkwS8ZUe.js} +1 -1
- package/dist/assets/{lzw-12qxv42v.js → lzw-D3cW5Wpg.js} +1 -1
- package/dist/assets/{native-bridge-vefdDEJb.js → native-bridge-BcYJooq8.js} +2 -2
- package/dist/assets/{packbits-Sk1wXwnQ.js → packbits-DDN4xzB5.js} +1 -1
- package/dist/assets/{parser.worker-CIEsmhWI.js → parser.worker-BW1IMUed.js} +1 -1
- package/dist/assets/raw-CoIXstQ-.js +1 -0
- package/dist/assets/{sandbox-B_rh0uLM.js → sandbox-DETNEyQb.js} +9 -8
- package/dist/assets/{server-client-B-rg3bm8.js → server-client-CmzJOeS7.js} +1 -1
- package/dist/assets/{wasm-bridge-BMLcD_0Y.js → wasm-bridge-CT7mK9W0.js} +1 -1
- package/dist/assets/{webimage-BzKBQcAG.js → webimage-CBjgg4up.js} +1 -1
- package/dist/assets/{workerHelpers-Bo7aHk9e.js → workerHelpers-IEQDo8r3.js} +1 -1
- package/dist/assets/{zstd-CfDyQt3x.js → zstd-C8oQ6qdS.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +9 -9
- package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
- package/src/components/viewer/LensPanel.tsx +50 -0
- package/src/components/viewer/Section2DPanel.tsx +58 -2
- package/src/hooks/useSymbolicAnnotations.ts +101 -0
- package/src/lib/lens/adapter.ts +14 -0
- package/src/store/index.ts +1 -0
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/dist/assets/lens-CpjUdqpw.js +0 -1
- package/dist/assets/raw-BCj_dK21.js +0 -1
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
type Drawing2D,
|
|
10
10
|
type ElementData,
|
|
11
11
|
} from '@ifc-lite/drawing-2d';
|
|
12
|
+
import type { DrawingLine2D } from '@ifc-lite/renderer';
|
|
12
13
|
import { formatDistance } from './tools/formatDistance';
|
|
13
14
|
import { formatArea, computePolygonCentroid } from './tools/computePolygonArea';
|
|
14
15
|
import { drawCloudOnCanvas } from './tools/cloudPathGenerator';
|
|
15
16
|
import type { PolygonArea2DResult, TextAnnotation2D, CloudAnnotation2D, Annotation2DTool, Point2D, SelectedAnnotation2D } from '@/store/slices/drawing2DSlice';
|
|
17
|
+
import type { AnnotationFill2D, AnnotationText2D } from '@/hooks/useSymbolicAnnotations';
|
|
16
18
|
|
|
17
19
|
// Fill colors for IFC types (architectural convention)
|
|
18
20
|
const IFC_TYPE_FILL_COLORS: Record<string, string> = {
|
|
@@ -53,6 +55,142 @@ export function getFillColorForType(ifcType: string): string {
|
|
|
53
55
|
return IFC_TYPE_FILL_COLORS[ifcType] || IFC_TYPE_FILL_COLORS.default;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// ─── IFC annotation overlay helpers (issue #812) ─────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Linear sRGB straight-alpha [0..1] tuple → CSS `rgba(...)`. */
|
|
61
|
+
function rgbaToCss(c: readonly [number, number, number, number]): string {
|
|
62
|
+
const r = Math.round(Math.max(0, Math.min(1, c[0])) * 255);
|
|
63
|
+
const g = Math.round(Math.max(0, Math.min(1, c[1])) * 255);
|
|
64
|
+
const b = Math.round(Math.max(0, Math.min(1, c[2])) * 255);
|
|
65
|
+
return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, c[3]))})`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Map IFC `BoxAlignment` to canvas2d `textAlign` + `textBaseline`. Mirrors
|
|
70
|
+
* the renderer's `parseBoxAlignment` semantics so the 2D overlay anchors
|
|
71
|
+
* text the same way the 3D pipeline does. Unknown / empty strings default
|
|
72
|
+
* to bottom-left (IFC4 IfcTextLiteralWithExtent default).
|
|
73
|
+
*/
|
|
74
|
+
function alignmentToCanvas(s: string): { align: CanvasTextAlign; baseline: CanvasTextBaseline } {
|
|
75
|
+
const norm = (s ?? '').toLowerCase().trim();
|
|
76
|
+
let align: CanvasTextAlign = 'left';
|
|
77
|
+
let baseline: CanvasTextBaseline = 'alphabetic';
|
|
78
|
+
if (norm.includes('right')) align = 'right';
|
|
79
|
+
else if (norm.includes('center')) align = 'center';
|
|
80
|
+
if (norm.includes('top')) baseline = 'top';
|
|
81
|
+
else if (norm.includes('middle') || (norm.includes('center') && !norm.includes('center-'))) baseline = 'middle';
|
|
82
|
+
return { align, baseline };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render IFC annotation fills, lines, and text into the canvas, in screen
|
|
87
|
+
* pixels. The caller supplies:
|
|
88
|
+
* - `modelToScreen` – drawing-coord → screen-pixel conversion (accounts
|
|
89
|
+
* for axis flips and sheet-mode paper scale)
|
|
90
|
+
* - `mmLineToScreen` – mm line weight → screen px stroke width
|
|
91
|
+
* - `worldHeightToScreenPx` – world-units text height → screen px font size
|
|
92
|
+
*
|
|
93
|
+
* Called from both the sheet-mode and direct-mode render paths in
|
|
94
|
+
* `Drawing2DCanvas`. Splitting it out keeps the two paths in sync without
|
|
95
|
+
* duplicating ~80 lines of canvas calls.
|
|
96
|
+
*
|
|
97
|
+
* Text rendering uses identity transform (no canvas rotate-with-scale) so
|
|
98
|
+
* `direct mode`'s y-flip doesn't mirror glyphs. The baseline direction is
|
|
99
|
+
* recovered in screen space from `dirX/dirY` mapped through `modelToScreen`.
|
|
100
|
+
*/
|
|
101
|
+
function drawIfcAnnotationsScreenSpace(
|
|
102
|
+
ctx: CanvasRenderingContext2D,
|
|
103
|
+
lines: readonly DrawingLine2D[] | undefined,
|
|
104
|
+
texts: readonly AnnotationText2D[] | undefined,
|
|
105
|
+
fills: readonly AnnotationFill2D[] | undefined,
|
|
106
|
+
modelToScreen: (x: number, y: number) => { x: number; y: number },
|
|
107
|
+
mmLineToScreen: (mmWeight: number) => number,
|
|
108
|
+
worldHeightToScreenPx: (worldHeight: number) => number,
|
|
109
|
+
): void {
|
|
110
|
+
// Fills first so lines/text composite cleanly on top.
|
|
111
|
+
if (fills && fills.length > 0) {
|
|
112
|
+
for (const fill of fills) {
|
|
113
|
+
const pts = fill.points;
|
|
114
|
+
if (pts.length < 6) continue;
|
|
115
|
+
const holes = fill.holesOffsets;
|
|
116
|
+
|
|
117
|
+
ctx.fillStyle = rgbaToCss(fill.color);
|
|
118
|
+
ctx.beginPath();
|
|
119
|
+
|
|
120
|
+
// Outer ring runs from index 0 up to the first hole offset (or end of
|
|
121
|
+
// points if no holes). Each hole offset is a vertex index where the
|
|
122
|
+
// next ring starts. Path subpaths use moveTo + lineTo + closePath; the
|
|
123
|
+
// even-odd fill rule handles the holes.
|
|
124
|
+
const ringStarts: number[] = [0];
|
|
125
|
+
for (let i = 0; i < holes.length; i++) ringStarts.push(holes[i]);
|
|
126
|
+
ringStarts.push(pts.length / 2); // sentinel end
|
|
127
|
+
|
|
128
|
+
for (let ri = 0; ri < ringStarts.length - 1; ri++) {
|
|
129
|
+
const start = ringStarts[ri];
|
|
130
|
+
const end = ringStarts[ri + 1];
|
|
131
|
+
if (end - start < 3) continue;
|
|
132
|
+
const first = modelToScreen(pts[start * 2], pts[start * 2 + 1]);
|
|
133
|
+
ctx.moveTo(first.x, first.y);
|
|
134
|
+
for (let i = start + 1; i < end; i++) {
|
|
135
|
+
const p = modelToScreen(pts[i * 2], pts[i * 2 + 1]);
|
|
136
|
+
ctx.lineTo(p.x, p.y);
|
|
137
|
+
}
|
|
138
|
+
ctx.closePath();
|
|
139
|
+
}
|
|
140
|
+
ctx.fill('evenodd');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (lines && lines.length > 0) {
|
|
145
|
+
// Slightly heavier than the drawing-2d 'annotation' category (0.13 mm)
|
|
146
|
+
// so coplanar overlays read clearly against the cut polygons beneath.
|
|
147
|
+
// This is the 2D equivalent of the 3D thicker-lines suggestion in #812.
|
|
148
|
+
const lineWidthMm = 0.2;
|
|
149
|
+
ctx.strokeStyle = '#000000';
|
|
150
|
+
ctx.lineWidth = mmLineToScreen(lineWidthMm);
|
|
151
|
+
ctx.setLineDash([]);
|
|
152
|
+
ctx.beginPath();
|
|
153
|
+
for (const ln of lines) {
|
|
154
|
+
const a = modelToScreen(ln.line.start.x, ln.line.start.y);
|
|
155
|
+
const b = modelToScreen(ln.line.end.x, ln.line.end.y);
|
|
156
|
+
ctx.moveTo(a.x, a.y);
|
|
157
|
+
ctx.lineTo(b.x, b.y);
|
|
158
|
+
}
|
|
159
|
+
ctx.stroke();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (texts && texts.length > 0) {
|
|
163
|
+
for (const t of texts) {
|
|
164
|
+
if (!t.content) continue;
|
|
165
|
+
const anchor = modelToScreen(t.x, t.y);
|
|
166
|
+
// Recover baseline direction in SCREEN space. modelToScreen may flip
|
|
167
|
+
// axes (e.g. direct-mode flips Y), so atan2(dirY, dirX) on raw model
|
|
168
|
+
// dirs would draw the text mirrored on those axes.
|
|
169
|
+
const baselineEnd = modelToScreen(t.x + t.dirX, t.y + t.dirY);
|
|
170
|
+
const sx = baselineEnd.x - anchor.x;
|
|
171
|
+
const sy = baselineEnd.y - anchor.y;
|
|
172
|
+
const angle = Math.abs(sx) + Math.abs(sy) > 1e-6 ? Math.atan2(sy, sx) : 0;
|
|
173
|
+
|
|
174
|
+
const fontPx = t.targetPx && t.targetPx > 0 ? t.targetPx : worldHeightToScreenPx(t.height);
|
|
175
|
+
const { align, baseline } = alignmentToCanvas(t.alignment);
|
|
176
|
+
// Multi-line literals stack downward in world Y in 3D. In 2D screen
|
|
177
|
+
// space the equivalent is `+ fontPx` per line below the anchor along
|
|
178
|
+
// the baseline-perpendicular (handled by the canvas rotate below).
|
|
179
|
+
const lineOffsetPx = (t.lineYOffset ?? 0) * (fontPx / Math.max(1e-6, t.height));
|
|
180
|
+
|
|
181
|
+
ctx.save();
|
|
182
|
+
ctx.fillStyle = t.color ? rgbaToCss(t.color) : '#000000';
|
|
183
|
+
ctx.font = `${fontPx}px system-ui, sans-serif`;
|
|
184
|
+
ctx.textAlign = align;
|
|
185
|
+
ctx.textBaseline = baseline;
|
|
186
|
+
ctx.translate(anchor.x, anchor.y);
|
|
187
|
+
ctx.rotate(angle);
|
|
188
|
+
ctx.fillText(t.content, 0, lineOffsetPx);
|
|
189
|
+
ctx.restore();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
56
194
|
// Static constants to avoid creating new objects/arrays on every render
|
|
57
195
|
const CANVAS_STYLE = { imageRendering: 'crisp-edges' as const };
|
|
58
196
|
const EMPTY_MEASURE_RESULTS: Measure2DResultData[] = [];
|
|
@@ -97,6 +235,10 @@ interface Drawing2DCanvasProps {
|
|
|
97
235
|
cloudAnnotations?: CloudAnnotation2D[];
|
|
98
236
|
// Selection
|
|
99
237
|
selectedAnnotation?: SelectedAnnotation2D | null;
|
|
238
|
+
// IFC annotation overlay (issue #812)
|
|
239
|
+
ifcAnnotationLines?: readonly DrawingLine2D[];
|
|
240
|
+
ifcAnnotationTexts?: readonly AnnotationText2D[];
|
|
241
|
+
ifcAnnotationFills?: readonly AnnotationFill2D[];
|
|
100
242
|
}
|
|
101
243
|
|
|
102
244
|
export function Drawing2DCanvas({
|
|
@@ -126,6 +268,9 @@ export function Drawing2DCanvas({
|
|
|
126
268
|
cloudAnnotationPoints = [],
|
|
127
269
|
cloudAnnotations = [],
|
|
128
270
|
selectedAnnotation = null,
|
|
271
|
+
ifcAnnotationLines,
|
|
272
|
+
ifcAnnotationTexts,
|
|
273
|
+
ifcAnnotationFills,
|
|
129
274
|
}: Drawing2DCanvasProps): React.ReactElement {
|
|
130
275
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
131
276
|
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
|
@@ -630,6 +775,17 @@ export function Drawing2DCanvas({
|
|
|
630
775
|
ctx.stroke();
|
|
631
776
|
ctx.setLineDash([]);
|
|
632
777
|
}
|
|
778
|
+
|
|
779
|
+
// IFC annotation overlay (issue #812)
|
|
780
|
+
drawIfcAnnotationsScreenSpace(
|
|
781
|
+
ctx,
|
|
782
|
+
ifcAnnotationLines,
|
|
783
|
+
ifcAnnotationTexts,
|
|
784
|
+
ifcAnnotationFills,
|
|
785
|
+
modelToScreen,
|
|
786
|
+
(mm) => Math.max(0.5, mmToScreen(mm) * 0.3),
|
|
787
|
+
(worldHeight) => Math.max(8, worldHeight * drawingTransform.scaleFactor * transform.scale),
|
|
788
|
+
);
|
|
633
789
|
};
|
|
634
790
|
|
|
635
791
|
drawModelContent();
|
|
@@ -945,6 +1101,27 @@ export function Drawing2DCanvas({
|
|
|
945
1101
|
}
|
|
946
1102
|
|
|
947
1103
|
ctx.restore();
|
|
1104
|
+
|
|
1105
|
+
// IFC annotation overlay (issue #812). Rendered after ctx.restore so we
|
|
1106
|
+
// can size lines and text in screen pixels rather than fighting the
|
|
1107
|
+
// ctx.scale applied above (which would inverse-scale everything).
|
|
1108
|
+
const directScaleX = sectionAxis === 'side' ? -transform.scale : transform.scale;
|
|
1109
|
+
const directScaleY = sectionAxis === 'down' ? transform.scale : -transform.scale;
|
|
1110
|
+
drawIfcAnnotationsScreenSpace(
|
|
1111
|
+
ctx,
|
|
1112
|
+
ifcAnnotationLines,
|
|
1113
|
+
ifcAnnotationTexts,
|
|
1114
|
+
ifcAnnotationFills,
|
|
1115
|
+
(x, y) => ({ x: x * directScaleX + transform.x, y: y * directScaleY + transform.y }),
|
|
1116
|
+
// No paper scale here: take a baseline 0.3 px per "default mm" so
|
|
1117
|
+
// weights match the heavier projection lines visually. Annotation
|
|
1118
|
+
// strokes in IFC are intentionally lighter than projection lines,
|
|
1119
|
+
// but a hair too thin on a 1× screen disappears entirely.
|
|
1120
|
+
(mmWeight) => Math.max(0.5, mmWeight * transform.scale * 0.3),
|
|
1121
|
+
// World height directly to screen pixels through the active zoom.
|
|
1122
|
+
// 8 px floor so labels stay legible when zoomed way out.
|
|
1123
|
+
(worldHeight) => Math.max(8, worldHeight * transform.scale),
|
|
1124
|
+
);
|
|
948
1125
|
}
|
|
949
1126
|
|
|
950
1127
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1399,7 +1576,7 @@ export function Drawing2DCanvas({
|
|
|
1399
1576
|
}
|
|
1400
1577
|
}
|
|
1401
1578
|
}
|
|
1402
|
-
}, [drawing, transform, showHiddenLines, canvasSize, overrideEngine, overridesEnabled, entityColorMap, useIfcMaterials, measureMode, measureStart, measureCurrent, measureResults, measureSnapPoint, sheetEnabled, activeSheet, sectionAxis, isPinned, annotation2DActiveTool, annotation2DCursorPos, polygonAreaPoints, polygonAreaResults, textAnnotations, textAnnotationEditing, cloudAnnotationPoints, cloudAnnotations, selectedAnnotation]);
|
|
1579
|
+
}, [drawing, transform, showHiddenLines, canvasSize, overrideEngine, overridesEnabled, entityColorMap, useIfcMaterials, measureMode, measureStart, measureCurrent, measureResults, measureSnapPoint, sheetEnabled, activeSheet, sectionAxis, isPinned, annotation2DActiveTool, annotation2DCursorPos, polygonAreaPoints, polygonAreaResults, textAnnotations, textAnnotationEditing, cloudAnnotationPoints, cloudAnnotations, selectedAnnotation, ifcAnnotationLines, ifcAnnotationTexts, ifcAnnotationFills]);
|
|
1403
1580
|
|
|
1404
1581
|
return (
|
|
1405
1582
|
<canvas
|
|
@@ -44,6 +44,7 @@ const TYPE_LABELS: Record<string, string> = {
|
|
|
44
44
|
quantity: 'Quantity',
|
|
45
45
|
classification: 'Classification',
|
|
46
46
|
material: 'Material',
|
|
47
|
+
model: 'Model',
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
interface LensPanelProps {
|
|
@@ -285,6 +286,11 @@ function RuleEditor({
|
|
|
285
286
|
onRequestDiscovery: (categories: { properties?: boolean; quantities?: boolean; classifications?: boolean; materials?: boolean }) => void;
|
|
286
287
|
}) {
|
|
287
288
|
const criteriaType = rule.criteria.type;
|
|
289
|
+
const loadedModels = useViewerStore((s) => s.models);
|
|
290
|
+
const modelOptions = useMemo(
|
|
291
|
+
() => Array.from(loadedModels.values()).sort((a, b) => a.name.localeCompare(b.name)),
|
|
292
|
+
[loadedModels],
|
|
293
|
+
);
|
|
288
294
|
|
|
289
295
|
// Trigger lazy discovery when user selects a criteria type that needs it
|
|
290
296
|
useEffect(() => {
|
|
@@ -300,6 +306,18 @@ function RuleEditor({
|
|
|
300
306
|
}
|
|
301
307
|
}, [criteriaType, discovered, onRequestDiscovery]);
|
|
302
308
|
|
|
309
|
+
// Auto-populate the single available model so the selector-hidden branch
|
|
310
|
+
// doesn't leave a model rule permanently invalid.
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (criteriaType !== 'model') return;
|
|
313
|
+
if (modelOptions.length !== 1) return;
|
|
314
|
+
if (rule.criteria.modelId) return;
|
|
315
|
+
const updated = { ...rule.criteria, modelId: modelOptions[0].id };
|
|
316
|
+
onChange({ criteria: updated, name: deriveRuleName(updated) });
|
|
317
|
+
// deriveRuleName is stable for this render; depending on rule.criteria/onChange is enough.
|
|
318
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
319
|
+
}, [criteriaType, modelOptions, rule.criteria, onChange]);
|
|
320
|
+
|
|
303
321
|
// Derived lists from discovered data
|
|
304
322
|
const ifcClasses = useMemo(() => discovered?.classes ?? [], [discovered]);
|
|
305
323
|
const psetNames = useMemo((): string[] => {
|
|
@@ -350,6 +368,9 @@ function RuleEditor({
|
|
|
350
368
|
case 'material':
|
|
351
369
|
base.materialName = '';
|
|
352
370
|
break;
|
|
371
|
+
case 'model':
|
|
372
|
+
base.modelId = modelOptions.length === 1 ? modelOptions[0].id : '';
|
|
373
|
+
break;
|
|
353
374
|
}
|
|
354
375
|
onChange({ criteria: base, name: rule.name === 'New Rule' ? TYPE_LABELS[newType] : rule.name });
|
|
355
376
|
};
|
|
@@ -363,6 +384,10 @@ function RuleEditor({
|
|
|
363
384
|
case 'quantity': return criteria.quantityName || 'Quantity';
|
|
364
385
|
case 'classification': return criteria.classificationCode || criteria.classificationSystem || 'Classification';
|
|
365
386
|
case 'material': return criteria.materialName || 'Material';
|
|
387
|
+
case 'model': {
|
|
388
|
+
const selected = modelOptions.find(m => m.id === criteria.modelId);
|
|
389
|
+
return selected?.name || 'Model';
|
|
390
|
+
}
|
|
366
391
|
default: return 'Rule';
|
|
367
392
|
}
|
|
368
393
|
};
|
|
@@ -516,6 +541,30 @@ function RuleEditor({
|
|
|
516
541
|
/>
|
|
517
542
|
)}
|
|
518
543
|
|
|
544
|
+
{/* Model: dropdown from loaded federated models */}
|
|
545
|
+
{criteriaType === 'model' && (
|
|
546
|
+
modelOptions.length <= 1 ? (
|
|
547
|
+
<span className="flex-1 min-w-0 text-xs text-zinc-400 dark:text-zinc-500 truncate">
|
|
548
|
+
{modelOptions.length === 0 ? 'No models loaded' : modelOptions[0]?.name ?? 'Model'}
|
|
549
|
+
</span>
|
|
550
|
+
) : (
|
|
551
|
+
<select
|
|
552
|
+
value={rule.criteria.modelId ?? ''}
|
|
553
|
+
onChange={(e) => {
|
|
554
|
+
const modelId = e.target.value;
|
|
555
|
+
const updated = { ...rule.criteria, modelId };
|
|
556
|
+
onChange({ criteria: updated, name: deriveRuleName(updated) });
|
|
557
|
+
}}
|
|
558
|
+
className={cn(selectClass, 'flex-1 min-w-0')}
|
|
559
|
+
>
|
|
560
|
+
<option value="">Model...</option>
|
|
561
|
+
{modelOptions.map(m => (
|
|
562
|
+
<option key={m.id} value={m.id}>{m.name}</option>
|
|
563
|
+
))}
|
|
564
|
+
</select>
|
|
565
|
+
)
|
|
566
|
+
)}
|
|
567
|
+
|
|
519
568
|
<button
|
|
520
569
|
onClick={onRemove}
|
|
521
570
|
className="text-zinc-400 hover:text-red-500 dark:text-zinc-500 dark:hover:text-red-400 p-0.5 flex-shrink-0"
|
|
@@ -644,6 +693,7 @@ function LensEditor({
|
|
|
644
693
|
case 'quantity': return !!c.quantitySet && !!c.quantityName;
|
|
645
694
|
case 'classification': return !!c.classificationSystem || !!c.classificationCode;
|
|
646
695
|
case 'material': return !!c.materialName;
|
|
696
|
+
case 'model': return !!c.modelId;
|
|
647
697
|
default: return false;
|
|
648
698
|
}
|
|
649
699
|
};
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';
|
|
15
|
-
import { X, Download, Eye, EyeOff, Maximize2, ZoomIn, ZoomOut, Loader2, Printer, GripVertical, MoreHorizontal, RefreshCw, Pin, PinOff, Palette, Ruler, Trash2, FileText, Shapes, Box, PenTool, Hexagon, Type, Cloud, MousePointer2 } from 'lucide-react';
|
|
15
|
+
import { X, Download, Eye, EyeOff, Maximize2, ZoomIn, ZoomOut, Loader2, Printer, GripVertical, MoreHorizontal, RefreshCw, Pin, PinOff, Palette, Ruler, Trash2, FileText, Shapes, Box, PenTool, Hexagon, Type, Cloud, MousePointer2, Tag } from 'lucide-react';
|
|
16
16
|
import { Button } from '@/components/ui/button';
|
|
17
17
|
import {
|
|
18
18
|
DropdownMenu,
|
|
@@ -31,11 +31,12 @@ import { SheetSetupPanel } from './SheetSetupPanel';
|
|
|
31
31
|
import { TitleBlockEditor } from './TitleBlockEditor';
|
|
32
32
|
import { TextAnnotationEditor } from './TextAnnotationEditor';
|
|
33
33
|
import { Drawing2DCanvas } from './Drawing2DCanvas';
|
|
34
|
-
import { useDrawingGeneration } from '@/hooks/useDrawingGeneration';
|
|
34
|
+
import { useDrawingGeneration, AXIS_MAP } from '@/hooks/useDrawingGeneration';
|
|
35
35
|
import { useMeasure2D } from '@/hooks/useMeasure2D';
|
|
36
36
|
import { useAnnotation2D } from '@/hooks/useAnnotation2D';
|
|
37
37
|
import { useViewControls } from '@/hooks/useViewControls';
|
|
38
38
|
import { useDrawingExport } from '@/hooks/useDrawingExport';
|
|
39
|
+
import { useSymbolicAnnotationsForDrawing } from '@/hooks/useSymbolicAnnotations';
|
|
39
40
|
|
|
40
41
|
interface Section2DPanelProps {
|
|
41
42
|
mergedGeometry?: GeometryResult | null;
|
|
@@ -279,6 +280,43 @@ export function Section2DPanel({
|
|
|
279
280
|
setMeasure2DSnapPoint, cancelMeasure2D, completeMeasure2D,
|
|
280
281
|
});
|
|
281
282
|
|
|
283
|
+
// ─── IFC annotation overlay (issue #812) ──────────────────────────────────
|
|
284
|
+
// Re-derive the section's world-coord cut position from the same bounds
|
|
285
|
+
// useDrawingGeneration uses, so the annotation filter stays in lockstep
|
|
286
|
+
// with the cut. Empty/missing bounds collapse to an inert range → hook
|
|
287
|
+
// returns empty, the overlay simply does nothing.
|
|
288
|
+
const ifcAnnotationsForDrawing = useMemo(() => {
|
|
289
|
+
const bounds = geometryResult?.coordinateInfo?.shiftedBounds;
|
|
290
|
+
if (!bounds) {
|
|
291
|
+
return { sectionPosWorld: 0, viewDepth: 0, fallbackY: 0 };
|
|
292
|
+
}
|
|
293
|
+
const axis = AXIS_MAP[sectionPlane.axis];
|
|
294
|
+
const axisMin = bounds.min[axis];
|
|
295
|
+
const axisMax = bounds.max[axis];
|
|
296
|
+
const sectionPosWorld = axisMin + (sectionPlane.position / 100) * (axisMax - axisMin);
|
|
297
|
+
const viewDepth = (axisMax - axisMin) * 0.5; // matches useDrawingGeneration's maxDepth
|
|
298
|
+
// For loose annotations (no resolvable storey), fall back to mid-Y like
|
|
299
|
+
// the 3D viewport does. This lets storeyless models still surface their
|
|
300
|
+
// annotations on the relevant section.
|
|
301
|
+
const yMin = bounds.min.y;
|
|
302
|
+
const yMax = bounds.max.y;
|
|
303
|
+
const fallbackY = Number.isFinite(yMin) && Number.isFinite(yMax) ? (yMin + yMax) * 0.5 : 0;
|
|
304
|
+
return { sectionPosWorld, viewDepth, fallbackY };
|
|
305
|
+
}, [geometryResult, sectionPlane.axis, sectionPlane.position]);
|
|
306
|
+
|
|
307
|
+
const ifcAnnotationData = useSymbolicAnnotationsForDrawing({
|
|
308
|
+
enabled: displayOptions.showIfcAnnotations && status === 'ready',
|
|
309
|
+
axis: sectionPlane.axis,
|
|
310
|
+
sectionPosWorld: ifcAnnotationsForDrawing.sectionPosWorld,
|
|
311
|
+
viewDepth: ifcAnnotationsForDrawing.viewDepth,
|
|
312
|
+
flipped: sectionPlane.flipped,
|
|
313
|
+
fallbackY: ifcAnnotationsForDrawing.fallbackY,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const toggleIfcAnnotations = useCallback(() => {
|
|
317
|
+
updateDisplayOptions({ showIfcAnnotations: !displayOptions.showIfcAnnotations });
|
|
318
|
+
}, [displayOptions.showIfcAnnotations, updateDisplayOptions]);
|
|
319
|
+
|
|
282
320
|
const annotationHandlers = useAnnotation2D({
|
|
283
321
|
drawing, viewTransform, sectionAxis: sectionPlane.axis, containerRef,
|
|
284
322
|
activeTool: annotation2DActiveTool, setActiveTool: setAnnotation2DActiveTool,
|
|
@@ -536,6 +574,17 @@ export function Section2DPanel({
|
|
|
536
574
|
{displayOptions.useSymbolicRepresentations ? <Shapes className="h-4 w-4" /> : <Box className="h-4 w-4" />}
|
|
537
575
|
</Button>
|
|
538
576
|
|
|
577
|
+
{/* IFC Annotations overlay toggle (issue #812) */}
|
|
578
|
+
<Button
|
|
579
|
+
variant={displayOptions.showIfcAnnotations ? 'default' : 'ghost'}
|
|
580
|
+
size="icon-sm"
|
|
581
|
+
onClick={toggleIfcAnnotations}
|
|
582
|
+
title={displayOptions.showIfcAnnotations ? 'Hide IFC annotations on this section' : 'Show IFC annotations on this section'}
|
|
583
|
+
disabled={sectionPlane.axis !== 'down'}
|
|
584
|
+
>
|
|
585
|
+
<Tag className="h-4 w-4" />
|
|
586
|
+
</Button>
|
|
587
|
+
|
|
539
588
|
{/* Annotation Tools Dropdown */}
|
|
540
589
|
<DropdownMenu>
|
|
541
590
|
<DropdownMenuTrigger asChild>
|
|
@@ -703,6 +752,10 @@ export function Section2DPanel({
|
|
|
703
752
|
{displayOptions.useSymbolicRepresentations ? <Shapes className="h-4 w-4 mr-2" /> : <Box className="h-4 w-4 mr-2" />}
|
|
704
753
|
{displayOptions.useSymbolicRepresentations ? 'Symbolic (Plan)' : 'Section Cut (Body)'}
|
|
705
754
|
</DropdownMenuItem>
|
|
755
|
+
<DropdownMenuItem onClick={toggleIfcAnnotations} disabled={sectionPlane.axis !== 'down'}>
|
|
756
|
+
<Tag className="h-4 w-4 mr-2" />
|
|
757
|
+
IFC Annotations {displayOptions.showIfcAnnotations ? 'On' : 'Off'}
|
|
758
|
+
</DropdownMenuItem>
|
|
706
759
|
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool('none')}>
|
|
707
760
|
<MousePointer2 className="h-4 w-4 mr-2" />
|
|
708
761
|
Select / Pan {annotation2DActiveTool === 'none' ? '(On)' : ''}
|
|
@@ -849,6 +902,9 @@ export function Section2DPanel({
|
|
|
849
902
|
cloudAnnotationPoints={cloudAnnotation2DPoints}
|
|
850
903
|
cloudAnnotations={cloudAnnotations2D}
|
|
851
904
|
selectedAnnotation={selectedAnnotation2D}
|
|
905
|
+
ifcAnnotationLines={ifcAnnotationData.lines}
|
|
906
|
+
ifcAnnotationTexts={ifcAnnotationData.texts}
|
|
907
|
+
ifcAnnotationFills={ifcAnnotationData.fills}
|
|
852
908
|
/>
|
|
853
909
|
{/* Subtle updating indicator - shows while regenerating without hiding the drawing */}
|
|
854
910
|
{isRegenerating && (
|
|
@@ -604,6 +604,107 @@ export interface AnnotationFill3D {
|
|
|
604
604
|
const EMPTY_TEXTS: readonly AnnotationText3D[] = Object.freeze([]);
|
|
605
605
|
const EMPTY_FILLS: readonly AnnotationFill3D[] = Object.freeze([]);
|
|
606
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Hook for the 2D Section panel: filters the shared parse cache to
|
|
609
|
+
* annotations whose world position falls inside the section's view-range
|
|
610
|
+
* on the cut axis, returning data in the Drawing2D coordinate frame.
|
|
611
|
+
*
|
|
612
|
+
* For `axis='down'` (floor plan), the parser's 2D coords already match
|
|
613
|
+
* the drawing-2d coord frame directly (x = world x, y = world z, with
|
|
614
|
+
* worldY = the cut axis). For elevation views (`axis='front'`,
|
|
615
|
+
* `axis='side'`), this hook returns empty: most authored IFC annotations
|
|
616
|
+
* are floor-plan symbols (dimensions, leaders, room labels) and don't
|
|
617
|
+
* project meaningfully onto a vertical drawing without a separate
|
|
618
|
+
* reorientation pass. Wiring those up cleanly is a follow-up.
|
|
619
|
+
*
|
|
620
|
+
* The section position is in world units (already converted from the
|
|
621
|
+
* 0-100% slider via `axisMin + (position / 100) * (axisMax - axisMin)`
|
|
622
|
+
* by the caller — Section2DPanel computes the same value to feed the
|
|
623
|
+
* drawing generator).
|
|
624
|
+
*/
|
|
625
|
+
export interface DrawingAnnotationData {
|
|
626
|
+
lines: DrawingLine2D[];
|
|
627
|
+
texts: AnnotationText2D[];
|
|
628
|
+
fills: AnnotationFill2D[];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const EMPTY_DRAWING_ANNOTATIONS: DrawingAnnotationData = {
|
|
632
|
+
lines: [],
|
|
633
|
+
texts: [],
|
|
634
|
+
fills: [],
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
export function useSymbolicAnnotationsForDrawing(params: {
|
|
638
|
+
enabled: boolean;
|
|
639
|
+
axis: 'down' | 'front' | 'side';
|
|
640
|
+
/** Section plane world-coord position along the cut axis. */
|
|
641
|
+
sectionPosWorld: number;
|
|
642
|
+
/** View depth in world units (typically half the model extent on the cut axis). */
|
|
643
|
+
viewDepth: number;
|
|
644
|
+
flipped: boolean;
|
|
645
|
+
/** Fallback world Y for annotations with no resolvable storey. */
|
|
646
|
+
fallbackY?: number;
|
|
647
|
+
}): DrawingAnnotationData {
|
|
648
|
+
const { enabled, axis, sectionPosWorld, viewDepth, flipped, fallbackY = 0 } = params;
|
|
649
|
+
const stores = useActiveStores();
|
|
650
|
+
const version = useAnnotationParseTrigger(enabled, stores);
|
|
651
|
+
|
|
652
|
+
return useMemo(() => {
|
|
653
|
+
if (!enabled) return EMPTY_DRAWING_ANNOTATIONS;
|
|
654
|
+
// Only floor plans (axis='down') are supported on this pass. Annotations
|
|
655
|
+
// for elevations/sections need a coord-reorientation pass that is not
|
|
656
|
+
// worth building until there's a real authored elevation symbol to test
|
|
657
|
+
// against. Returning empty quietly keeps the toggle a no-op there.
|
|
658
|
+
if (axis !== 'down') return EMPTY_DRAWING_ANNOTATIONS;
|
|
659
|
+
void version;
|
|
660
|
+
|
|
661
|
+
// Section view range in world Y. Matches the convention used by
|
|
662
|
+
// `profile-projector.isInProjectionRange`:
|
|
663
|
+
// not flipped → [sectionPos, sectionPos + viewDepth]
|
|
664
|
+
// flipped → [sectionPos - viewDepth, sectionPos]
|
|
665
|
+
// We expand the range by a small tolerance so annotations sitting
|
|
666
|
+
// exactly on the cut plane still match (storey elevations are
|
|
667
|
+
// typically the cut Y).
|
|
668
|
+
const TOL = 1e-3;
|
|
669
|
+
const rangeMin = (flipped ? sectionPosWorld - viewDepth : sectionPosWorld) - TOL;
|
|
670
|
+
const rangeMax = (flipped ? sectionPosWorld : sectionPosWorld + viewDepth) + TOL;
|
|
671
|
+
|
|
672
|
+
const lines: DrawingLine2D[] = [];
|
|
673
|
+
const texts: AnnotationText2D[] = [];
|
|
674
|
+
const fills: AnnotationFill2D[] = [];
|
|
675
|
+
|
|
676
|
+
for (const store of stores) {
|
|
677
|
+
const key = sourceKey(store);
|
|
678
|
+
if (!key) continue;
|
|
679
|
+
const cached = PARSE_CACHE.get(key);
|
|
680
|
+
if (!cached) continue;
|
|
681
|
+
|
|
682
|
+
for (const bucket of cached.byStorey.values()) {
|
|
683
|
+
const bucketY = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
684
|
+
if (bucketY < rangeMin || bucketY > rangeMax) continue;
|
|
685
|
+
for (const ln of bucket.lines) lines.push(ln);
|
|
686
|
+
for (const t of bucket.texts) texts.push(t);
|
|
687
|
+
for (const f of bucket.fills) fills.push(f);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Loose annotations have no resolvable storey — include them if the
|
|
691
|
+
// fallback Y lands in the view range. That keeps malformed exports
|
|
692
|
+
// (e.g. 3DEXPERIENCE files with orphaned storeys) usable when the
|
|
693
|
+
// user is looking at the storey the fallback resolves to.
|
|
694
|
+
if (fallbackY >= rangeMin && fallbackY <= rangeMax) {
|
|
695
|
+
for (const ln of cached.loose) lines.push(ln);
|
|
696
|
+
for (const t of cached.looseTexts) texts.push(t);
|
|
697
|
+
for (const f of cached.looseFills) fills.push(f);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (lines.length === 0 && texts.length === 0 && fills.length === 0) {
|
|
702
|
+
return EMPTY_DRAWING_ANNOTATIONS;
|
|
703
|
+
}
|
|
704
|
+
return { lines, texts, fills };
|
|
705
|
+
}, [enabled, axis, sectionPosWorld, viewDepth, flipped, fallbackY, stores, version]);
|
|
706
|
+
}
|
|
707
|
+
|
|
607
708
|
/**
|
|
608
709
|
* Hook for the WebGPU text + fill pipelines. Returns 3D-lifted texts and
|
|
609
710
|
* fills for every active model. Shares the parse cache with
|
package/src/lib/lens/adapter.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { FederatedModel } from '@/store/types';
|
|
|
25
25
|
|
|
26
26
|
interface ModelEntry {
|
|
27
27
|
id: string;
|
|
28
|
+
name: string;
|
|
28
29
|
ifcDataStore: IfcDataStore;
|
|
29
30
|
idOffset: number;
|
|
30
31
|
maxExpressId: number;
|
|
@@ -58,6 +59,7 @@ export function createLensDataProvider(
|
|
|
58
59
|
if (model.ifcDataStore) {
|
|
59
60
|
entries.push({
|
|
60
61
|
id: model.id,
|
|
62
|
+
name: model.name,
|
|
61
63
|
ifcDataStore: model.ifcDataStore,
|
|
62
64
|
idOffset: model.idOffset ?? 0,
|
|
63
65
|
maxExpressId: model.maxExpressId ?? 0,
|
|
@@ -67,6 +69,7 @@ export function createLensDataProvider(
|
|
|
67
69
|
} else if (legacyDataStore) {
|
|
68
70
|
entries.push({
|
|
69
71
|
id: 'legacy',
|
|
72
|
+
name: 'Model',
|
|
70
73
|
ifcDataStore: legacyDataStore,
|
|
71
74
|
idOffset: 0,
|
|
72
75
|
maxExpressId: computeMaxExpressId(legacyDataStore),
|
|
@@ -285,6 +288,17 @@ export function createLensDataProvider(
|
|
|
285
288
|
if (info.materials?.length) return info.materials[0]?.name;
|
|
286
289
|
return undefined;
|
|
287
290
|
},
|
|
291
|
+
|
|
292
|
+
getModelId(globalId: number): string | undefined {
|
|
293
|
+
const resolved = resolveGlobalId(globalId, entries);
|
|
294
|
+
if (!resolved) return undefined;
|
|
295
|
+
return resolved.entry.id;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
getModelName(modelId: string): string | undefined {
|
|
299
|
+
const entry = entries.find(e => e.id === modelId);
|
|
300
|
+
return entry?.name ?? modelId;
|
|
301
|
+
},
|
|
288
302
|
};
|
|
289
303
|
}
|
|
290
304
|
|
package/src/store/index.ts
CHANGED
|
@@ -305,6 +305,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
305
305
|
show3DOverlay: true,
|
|
306
306
|
scale: 100,
|
|
307
307
|
useSymbolicRepresentations: false,
|
|
308
|
+
showIfcAnnotations: true,
|
|
308
309
|
},
|
|
309
310
|
// Graphic overrides (keep presets, reset active and custom)
|
|
310
311
|
activePresetId: 'preset-3d-colors',
|
|
@@ -91,6 +91,13 @@ export interface Drawing2DState {
|
|
|
91
91
|
scale: number;
|
|
92
92
|
/** Use authored symbolic representations (Plan/Annotation) when available instead of section cut */
|
|
93
93
|
useSymbolicRepresentations: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether to overlay IfcAnnotation curves, text, and fills on the 2D
|
|
96
|
+
* section view. Filtered to annotations whose world position falls
|
|
97
|
+
* inside the section's view-range on the cut axis (issue #812 follow-up
|
|
98
|
+
* to the IfcAnnotation text feature).
|
|
99
|
+
*/
|
|
100
|
+
showIfcAnnotations: boolean;
|
|
94
101
|
};
|
|
95
102
|
/** Available graphic override presets */
|
|
96
103
|
graphicOverridePresets: GraphicOverridePreset[];
|
|
@@ -236,6 +243,7 @@ const getDefaultDisplayOptions = (): Drawing2DState['drawing2DDisplayOptions'] =
|
|
|
236
243
|
show3DOverlay: true, // Show 3D overlay by default
|
|
237
244
|
scale: 100, // 1:100 default
|
|
238
245
|
useSymbolicRepresentations: false, // Default to section cut (Body geometry)
|
|
246
|
+
showIfcAnnotations: true, // Mirror the 3D Class Visibility default
|
|
239
247
|
});
|
|
240
248
|
|
|
241
249
|
const getDefaultState = (): Drawing2DState => ({
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const z=["ifcType","attribute","property","quantity","classification","material"],D=["Name","Description","ObjectType","Tag"],x=["#E53935","#1E88E5","#FDD835","#43A047","#8E24AA","#00ACC1","#FF8F00","#6D4C41","#EC407A","#5C6BC0","#26A69A","#78909C"],w={IfcWallStandardCase:"IfcWall",IfcSlabStandardCase:"IfcSlab",IfcColumnStandardCase:"IfcColumn",IfcBeamStandardCase:"IfcBeam",IfcStairFlight:"IfcStair",IfcRampFlight:"IfcRamp"};function h(t,a,n){switch(t.type){case"ifcType":return E(t,a,n);case"property":return b(t,a,n);case"material":return M(t,a,n);case"attribute":return N(t,a,n);case"quantity":return A(t,a,n);case"classification":return I(t,a,n);default:return!1}}function E(t,a,n){if(!t.ifcType)return!1;const e=n.getEntityType(a);return e?e===t.ifcType?!0:w[e]===t.ifcType:!1}function b(t,a,n){if(!t.propertySet||!t.propertyName)return!1;const e=n.getPropertyValue(a,t.propertySet,t.propertyName);return t.operator==="exists"?e!=null:t.operator==="contains"&&t.propertyValue!==void 0?String(e??"").toLowerCase().includes(t.propertyValue.toLowerCase()):t.propertyValue!==void 0?String(e??"")===t.propertyValue:e!=null}function M(t,a,n){if(!t.materialName)return!1;const e=t.materialName.toLowerCase();if(n.getMaterialName){const s=n.getMaterialName(a);return s?s.toLowerCase().includes(e):!1}const o=n.getPropertySets(a);if(!o||o.length===0)return!1;for(const s of o)if(s.name.toLowerCase().includes("material")){for(const l of s.properties)if(String(l.value??"").toLowerCase().includes(e))return!0}return!1}function N(t,a,n){if(!t.attributeName||!n.getEntityAttribute)return!1;const e=n.getEntityAttribute(a,t.attributeName);return t.operator==="exists"?e!==void 0&&e!=="":t.operator==="contains"&&t.attributeValue!==void 0?(e??"").toLowerCase().includes(t.attributeValue.toLowerCase()):t.attributeValue!==void 0?(e??"")===t.attributeValue:e!==void 0&&e!==""}function A(t,a,n){if(!t.quantitySet||!t.quantityName||!n.getQuantityValue)return!1;const e=n.getQuantityValue(a,t.quantitySet,t.quantityName);return t.operator==="exists"?e!=null:e==null?!1:t.operator==="contains"&&t.quantityValue!==void 0?String(e).toLowerCase().includes(t.quantityValue.toLowerCase()):t.quantityValue!==void 0?String(e)===t.quantityValue:!0}function I(t,a,n){if(!t.classificationSystem&&!t.classificationCode||!n.getClassifications)return!1;const e=n.getClassifications(a);if(!e||e.length===0)return!1;for(const o of e){const s=!t.classificationSystem||(o.system??"").toLowerCase().includes(t.classificationSystem.toLowerCase()),l=!t.classificationCode||(o.identification??"").toLowerCase().includes(t.classificationCode.toLowerCase());if(s&&l)return!0}return!1}const T=[.6,.6,.6,.15];function C(t,a){const n=t.replace("#",""),e=parseInt(n.substring(0,2),16)/255,o=parseInt(n.substring(2,4),16)/255,s=parseInt(n.substring(4,6),16)/255;return[e,o,s,a]}function _(t){const a=Math.round(t[0]*255).toString(16).padStart(2,"0"),n=Math.round(t[1]*255).toString(16).padStart(2,"0"),e=Math.round(t[2]*255).toString(16).padStart(2,"0");return`#${a}${n}${e}`}function O(t){return t[3]<.2}const L=137.508;function q(t,a,n){const e=(1-Math.abs(2*n-1))*a,o=e*(1-Math.abs(t/60%2-1)),s=n-e/2;let l=0,p=0,i=0;t<60?(l=e,p=o):t<120?(l=o,p=e):t<180?(p=e,i=o):t<240?(p=o,i=e):t<300?(l=o,i=e):(l=e,i=o);const m=Math.round((l+s)*255),y=Math.round((p+s)*255),f=Math.round((i+s)*255);return`#${m.toString(16).padStart(2,"0")}${y.toString(16).padStart(2,"0")}${f.toString(16).padStart(2,"0")}`}function V(t){const a=t*L%360,n=t%2===0?.65:.8,o=[.45,.55,.35][t%3];return q(a,n,o)}function P(t,a){const n=performance.now(),e=t.rules.filter(i=>i.enabled);if(e.length===0)return{colorMap:new Map,hiddenIds:new Set,ruleCounts:new Map,ruleEntityIds:new Map,executionTime:performance.now()-n};const o=new Map,s=new Set,l=new Map,p=new Map;for(const i of e)l.set(i.id,0),p.set(i.id,[]);return a.forEachEntity(i=>{let m=!1;for(const y of e)if(h(y.criteria,i,a)){m=!0,l.set(y.id,(l.get(y.id)??0)+1),p.get(y.id).push(i),F(y,i,o,s);break}m||o.set(i,T)}),{colorMap:o,hiddenIds:s,ruleCounts:l,ruleEntityIds:p,executionTime:performance.now()-n}}function F(t,a,n,e){switch(t.action){case"colorize":n.set(a,C(t.color,1));break;case"transparent":n.set(a,C(t.color,.3));break;case"hide":e.add(a);break}}function W(t,a){const n=performance.now(),e=new Map,o=[];a.forEachEntity(f=>{const S=R(t,f,a),u=S!=null?String(S).trim():"";if(u===""){o.push(f);return}let r=e.get(u);r||(r=[],e.set(u,r)),r.push(f)});const s=Array.from(e.entries()).sort((f,S)=>S[1].length-f[1].length),l=new Map,p=new Set,i=new Map,m=new Map,y=[];for(let f=0;f<s.length;f++){const[S,u]=s[f],r=V(f),c=`auto-${f}`,d=C(r,1);for(const g of u)l.set(g,d);i.set(c,u.length),m.set(c,u),y.push({id:c,name:S,color:r,count:u.length})}for(const f of o)l.set(f,T);return{colorMap:l,hiddenIds:p,ruleCounts:i,ruleEntityIds:m,legend:y,executionTime:performance.now()-n}}function R(t,a,n){switch(t.source){case"ifcType":return n.getEntityType(a);case"attribute":return!t.propertyName||!n.getEntityAttribute?void 0:n.getEntityAttribute(a,t.propertyName);case"property":if(!t.psetName||!t.propertyName)return;{const e=n.getPropertyValue(a,t.psetName,t.propertyName);return e!=null?String(e):void 0}case"quantity":return!t.psetName||!t.propertyName||!n.getQuantityValue?void 0:n.getQuantityValue(a,t.psetName,t.propertyName);case"classification":if(!n.getClassifications)return;{const e=n.getClassifications(a);if(!e||e.length===0)return;const o=e[0],s=[];return o.system&&s.push(o.system),o.identification&&s.push(o.identification),s.length>0?s.join(": "):o.name}case"material":return n.getMaterialName?n.getMaterialName(a):void 0;default:return}}const Q=[{id:"lens-by-class",name:"By IFC Class",builtin:!0,rules:[],autoColor:{source:"ifcType"}},{id:"lens-structural",name:"Structural",builtin:!0,rules:[{id:"col",name:"Columns",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcColumn"},action:"colorize",color:"#E53935"},{id:"beam",name:"Beams",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcBeam"},action:"colorize",color:"#1E88E5"},{id:"slab",name:"Slabs",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcSlab"},action:"colorize",color:"#FDD835"},{id:"footing",name:"Footings",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcFooting"},action:"colorize",color:"#43A047"}]},{id:"lens-envelope",name:"Building Envelope",builtin:!0,rules:[{id:"roof",name:"Roofs",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcRoof"},action:"colorize",color:"#C62828"},{id:"curtwall",name:"Curtain Walls",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcCurtainWall"},action:"colorize",color:"#0277BD"},{id:"window",name:"Windows",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcWindow"},action:"colorize",color:"#4FC3F7"},{id:"door",name:"Doors",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcDoor"},action:"colorize",color:"#00695C"},{id:"wall",name:"Walls",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcWall"},action:"colorize",color:"#8D6E63"}]},{id:"lens-openings",name:"Openings & Circulation",builtin:!0,rules:[{id:"door",name:"Doors",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcDoor"},action:"colorize",color:"#00897B"},{id:"window",name:"Windows",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcWindow"},action:"colorize",color:"#42A5F5"},{id:"stair",name:"Stairs",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcStairFlight"},action:"colorize",color:"#FF8F00"},{id:"ramp",name:"Ramps",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcRamp"},action:"colorize",color:"#7CB342"},{id:"railing",name:"Railings",enabled:!0,criteria:{type:"ifcType",ifcType:"IfcRailing"},action:"colorize",color:"#78909C"}]},{id:"lens-auto-material",name:"By Material",builtin:!0,rules:[],autoColor:{source:"material"}}],B=30;function $(t){const a=new Set;return t.forEachEntity(n=>{const e=t.getEntityType(n);e&&a.add(e)}),Array.from(a).sort()}function v(t,a){const n={},e=a.properties===!0,o=a.quantities===!0,s=a.classifications===!0,l=a.materials===!0;if(!e&&!o&&!s&&!l)return n;const p=e?new Map:null,i=o?new Map:null,m=s?new Set:null,y=l?new Set:null,f=[],S=new Map;t.forEachEntity(u=>{const r=t.getEntityType(u);if(!r)return;const c=S.get(r)??0;c<B&&(f.push(u),S.set(r,c+1))});for(const u of f){if(p){const r=t.getPropertySets(u);for(const c of r){if(!c.name)continue;let d=p.get(c.name);d||(d=new Set,p.set(c.name,d));for(const g of c.properties)g.name&&d.add(g.name)}}if(i&&t.getQuantitySets){const r=t.getQuantitySets(u);for(const c of r){if(!c.name)continue;let d=i.get(c.name);d||(d=new Set,i.set(c.name,d));for(const g of c.quantities)g.name&&d.add(g.name)}}if(m&&t.getClassifications){const r=t.getClassifications(u);for(const c of r)c.system&&m.add(c.system)}if(y&&t.getMaterialName){const r=t.getMaterialName(u);r&&y.add(r)}}if(p){const u=new Map;for(const[r,c]of p)u.set(r,Array.from(c).sort());n.propertySets=u}if(i){const u=new Map;for(const[r,c]of i)u.set(r,Array.from(c).sort());n.quantitySets=u}return m&&(n.classificationSystems=Array.from(m).sort()),y&&(n.materials=Array.from(y).sort()),n}export{z as A,Q as B,D as E,x as L,v as a,P as b,$ as d,W as e,C as h,O as i,_ as r};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{B as o}from"./geotiff-DBMPIaHW.js";import"./sandbox-B_rh0uLM.js";import"./lens-CpjUdqpw.js";import"./__vite-browser-external-B1O5LaIO.js";class c extends o{decodeBlock(e){return e}}export{c as default};
|