@ifc-lite/viewer 1.24.0 → 1.25.1
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 +30 -30
- package/CHANGELOG.md +86 -0
- package/dist/assets/{basketViewActivator-BxyL3ITR.js → basketViewActivator-Dkn92C04.js} +7 -7
- package/dist/assets/{bcf-OInQ7hh6.js → bcf-DP2AK1-_.js} +23 -23
- package/dist/assets/{deflate-D0Sm0vyt.js → deflate-BYqYwhkl.js} +1 -1
- package/dist/assets/{exporters-CsSbJLHQ.js → exporters-CZe0D8N-.js} +13 -13
- package/dist/assets/{geometry-controller.worker-DuPxLFYp.js → geometry-controller.worker-pD49_fH6.js} +2 -2
- package/dist/assets/{geometry.worker-DvOb53b0.js → geometry.worker-D4c-06r5.js} +1 -1
- package/dist/assets/{geotiff-DBMPIaHW.js → geotiff-By06vdeL.js} +10 -10
- package/dist/assets/{ids-DCWn6VHu.js → ids-DDkkb4mo.js} +3 -3
- package/dist/assets/{ifc-lite-BUH-uP-q.js → ifc-lite-DxGqDbjO.js} +2 -2
- package/dist/assets/{ifc-lite_bg-CeGXwVPt.wasm → ifc-lite_bg-BNeu7R_V.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-DHXAIZHs.wasm → ifc-lite_bg-DuxUZomW.wasm} +0 -0
- package/dist/assets/{index-DAbQbcNs.js → index-CqBdDOAZ.js} +34448 -33855
- package/dist/assets/{jpeg-Btyb9xCl.js → jpeg-B4IBTphL.js} +1 -1
- package/dist/assets/lens-PYsLu_MA.js +1 -0
- package/dist/assets/{lerc-4m4sf12j.js → lerc-DQ3jI0Ke.js} +1 -1
- package/dist/assets/{lzw-12qxv42v.js → lzw-CtdH775t.js} +1 -1
- package/dist/assets/{native-bridge-vefdDEJb.js → native-bridge-DA8wxaN_.js} +2 -2
- package/dist/assets/{packbits-Sk1wXwnQ.js → packbits-DG3zn49C.js} +1 -1
- package/dist/assets/{parser.worker-CIEsmhWI.js → parser.worker-BZZcO7DB.js} +1 -1
- package/dist/assets/raw-DY7Y_acr.js +1 -0
- package/dist/assets/{sandbox-B_rh0uLM.js → sandbox-D1pQT-5R.js} +9 -8
- package/dist/assets/{server-client-B-rg3bm8.js → server-client-D9xO_8yX.js} +1 -1
- package/dist/assets/{wasm-bridge-BMLcD_0Y.js → wasm-bridge-DMX8Acuf.js} +1 -1
- package/dist/assets/{webimage-BzKBQcAG.js → webimage-_-qCDjkn.js} +1 -1
- package/dist/assets/{workerHelpers-Bo7aHk9e.js → workerHelpers-Crstj4Oa.js} +1 -1
- package/dist/assets/{zstd-CfDyQt3x.js → zstd-DlfgC8gA.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +9 -9
- package/src/components/viewer/BCFPanel.tsx +8 -1
- package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
- package/src/components/viewer/LensPanel.tsx +50 -0
- package/src/components/viewer/Section2DPanel.tsx +62 -2
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +24 -0
- package/src/hooks/useBCF.ts +98 -16
- package/src/hooks/useDrawingGeneration.ts +149 -3
- package/src/hooks/useSymbolicAnnotations.ts +156 -11
- 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
|
@@ -32,6 +32,14 @@ export const AXIS_MAP: Record<'down' | 'front' | 'side', 'x' | 'y' | 'z'> = {
|
|
|
32
32
|
side: 'x',
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Depth of the slab IN FRONT of the section plane (in shifted-world
|
|
36
|
+
// metres) within which IFC annotation/grid primitives are kept. Beyond
|
|
37
|
+
// the slab they're culled — matches a typical plan-view "view depth"
|
|
38
|
+
// where dimensions for the next storey shouldn't bleed through. The
|
|
39
|
+
// shifted-bounds coordinate system the centroids and `position` both
|
|
40
|
+
// live in is already in metres (WASM applies `unit_scale` upstream).
|
|
41
|
+
export const ANNOTATION_VIEW_DEPTH = 1.2;
|
|
42
|
+
|
|
35
43
|
interface UseDrawingGenerationParams {
|
|
36
44
|
geometryResult: GeometryResult | null | undefined;
|
|
37
45
|
ifcDataStore: { source: Uint8Array } | null;
|
|
@@ -92,10 +100,28 @@ export function useDrawingGeneration({
|
|
|
92
100
|
// Track if this is a regeneration (vs initial generation)
|
|
93
101
|
const isRegeneratingRef = useRef(false);
|
|
94
102
|
|
|
103
|
+
// Symbolic lines carry the parent primitive's world-space centroid so the
|
|
104
|
+
// 2D Section filter below can cull them against the active cut plane —
|
|
105
|
+
// cardinal axis OR a face-picked custom plane. The drawing-2d package's
|
|
106
|
+
// DrawingLine has no per-line position slot; attaching the centroid as
|
|
107
|
+
// extra fields keeps the change local since the canvas ignores anything
|
|
108
|
+
// beyond DrawingLine's declared fields.
|
|
109
|
+
//
|
|
110
|
+
// Coordinate space matches the section cutter's input (shifted bounds):
|
|
111
|
+
// - worldX: read from the polyline's 2D x (already RTC-shifted by WASM)
|
|
112
|
+
// - worldZ: -(polyline 2D y) — WASM negates Z into the 2D y axis to
|
|
113
|
+
// match section-cut output handedness, so flip back here
|
|
114
|
+
// - worldY: from the WASM `worldY` accessor (vertical elevation)
|
|
115
|
+
type SymbolicDrawingLine = DrawingLine & {
|
|
116
|
+
worldX?: number;
|
|
117
|
+
worldY?: number;
|
|
118
|
+
worldZ?: number;
|
|
119
|
+
};
|
|
120
|
+
|
|
95
121
|
// Cache for symbolic representations - these don't change with section position
|
|
96
122
|
// Only re-parse when model or display options change
|
|
97
123
|
const symbolicCacheRef = useRef<{
|
|
98
|
-
lines:
|
|
124
|
+
lines: SymbolicDrawingLine[];
|
|
99
125
|
entities: Set<number>;
|
|
100
126
|
sourceId: string | null;
|
|
101
127
|
useSymbolic: boolean;
|
|
@@ -120,7 +146,7 @@ export function useDrawingGeneration({
|
|
|
120
146
|
|
|
121
147
|
// Parse symbolic representations if enabled (for hybrid mode)
|
|
122
148
|
// OPTIMIZATION: Cache symbolic data - it doesn't change with section position
|
|
123
|
-
let symbolicLines:
|
|
149
|
+
let symbolicLines: SymbolicDrawingLine[] = [];
|
|
124
150
|
let entitiesWithSymbols = new Set<number>();
|
|
125
151
|
|
|
126
152
|
// For multi-model: create cache key from model count and visible model IDs
|
|
@@ -167,6 +193,26 @@ export function useDrawingGeneration({
|
|
|
167
193
|
entitiesWithSymbols.add(poly.expressId);
|
|
168
194
|
const points = poly.points;
|
|
169
195
|
const pointCount = poly.pointCount;
|
|
196
|
+
// WASM exposes `worldY` on every symbolic primitive — the
|
|
197
|
+
// elevation of its parent placement (Z-up IFC, world-Y here).
|
|
198
|
+
// The .d.ts shipped with the @ifc-lite/wasm package lags
|
|
199
|
+
// behind the Rust source; read defensively so a stale build
|
|
200
|
+
// returns undefined instead of throwing.
|
|
201
|
+
const polyWorldY = (poly as unknown as { worldY?: number }).worldY;
|
|
202
|
+
// Centroid in shifted world coords — derived from the 2D
|
|
203
|
+
// points the WASM extractor already emits in section-cut
|
|
204
|
+
// space. point.x = world X (RTC-shifted); point.y =
|
|
205
|
+
// -world Z (negated to match cut-output handedness), so
|
|
206
|
+
// flip the sign back to recover world Z. Computed once
|
|
207
|
+
// per source polyline and shared across its segments.
|
|
208
|
+
let sumX = 0;
|
|
209
|
+
let sumY = 0;
|
|
210
|
+
for (let p = 0; p < pointCount; p++) {
|
|
211
|
+
sumX += points[p * 2];
|
|
212
|
+
sumY += points[p * 2 + 1];
|
|
213
|
+
}
|
|
214
|
+
const polyWorldX = pointCount > 0 ? sumX / pointCount : undefined;
|
|
215
|
+
const polyWorldZ = pointCount > 0 ? -sumY / pointCount : undefined;
|
|
170
216
|
|
|
171
217
|
for (let j = 0; j < pointCount - 1; j++) {
|
|
172
218
|
symbolicLines.push({
|
|
@@ -180,6 +226,9 @@ export function useDrawingGeneration({
|
|
|
180
226
|
ifcType: poly.ifcType,
|
|
181
227
|
modelIndex: symbolicModelIndex,
|
|
182
228
|
depth: 0,
|
|
229
|
+
worldX: polyWorldX,
|
|
230
|
+
worldY: polyWorldY,
|
|
231
|
+
worldZ: polyWorldZ,
|
|
183
232
|
});
|
|
184
233
|
}
|
|
185
234
|
|
|
@@ -195,6 +244,9 @@ export function useDrawingGeneration({
|
|
|
195
244
|
ifcType: poly.ifcType,
|
|
196
245
|
modelIndex: symbolicModelIndex,
|
|
197
246
|
depth: 0,
|
|
247
|
+
worldX: polyWorldX,
|
|
248
|
+
worldY: polyWorldY,
|
|
249
|
+
worldZ: polyWorldZ,
|
|
198
250
|
});
|
|
199
251
|
}
|
|
200
252
|
}
|
|
@@ -206,6 +258,12 @@ export function useDrawingGeneration({
|
|
|
206
258
|
|
|
207
259
|
entitiesWithSymbols.add(circle.expressId);
|
|
208
260
|
const numSegments = circle.isFullCircle ? 32 : 16;
|
|
261
|
+
const circleWorldY = (circle as unknown as { worldY?: number }).worldY;
|
|
262
|
+
// Centre in shifted world coords. circle.centerX is
|
|
263
|
+
// already RTC-shifted X; circle.centerY carries the
|
|
264
|
+
// negated Z (see polyline note above) — flip to recover.
|
|
265
|
+
const circleWorldX = circle.centerX;
|
|
266
|
+
const circleWorldZ = -circle.centerY;
|
|
209
267
|
|
|
210
268
|
for (let j = 0; j < numSegments; j++) {
|
|
211
269
|
const t1 = j / numSegments;
|
|
@@ -230,6 +288,9 @@ export function useDrawingGeneration({
|
|
|
230
288
|
ifcType: circle.ifcType,
|
|
231
289
|
modelIndex: symbolicModelIndex,
|
|
232
290
|
depth: 0,
|
|
291
|
+
worldX: circleWorldX,
|
|
292
|
+
worldY: circleWorldY,
|
|
293
|
+
worldZ: circleWorldZ,
|
|
233
294
|
});
|
|
234
295
|
}
|
|
235
296
|
}
|
|
@@ -375,9 +436,94 @@ export function useDrawingGeneration({
|
|
|
375
436
|
}
|
|
376
437
|
}
|
|
377
438
|
|
|
439
|
+
// When the user toggles `sectionPlane.flipped` on a cardinal axis,
|
|
440
|
+
// the cutter negates the 2D U axis (see `projectTo2D` in
|
|
441
|
+
// @ifc-lite/drawing-2d/math.ts and `data[6] = flipU` in the GPU
|
|
442
|
+
// cutter). Symbolic primitives come out of WASM in the cutter's
|
|
443
|
+
// UNFLIPPED basis — for the plan ('y') case `(line.x = worldX − rtc,
|
|
444
|
+
// line.y = −worldY + rtc)` — so on a flipped section the cut
|
|
445
|
+
// polygons land at −X while the symbolic lines stay at +X. The
|
|
446
|
+
// result the user reported: annotations sitting NEXT TO the model
|
|
447
|
+
// as if they were mirrored across the model's centre, instead of
|
|
448
|
+
// staying with the cut. Mirror symbolic X here to match the cutter
|
|
449
|
+
// for cardinal flipped sections. Custom face-pick planes use
|
|
450
|
+
// `projectTo2DBasis` (no U flip), so leave them untouched —
|
|
451
|
+
// symbolic alignment on an arbitrary basis is a separate problem
|
|
452
|
+
// and out of scope for this fix.
|
|
453
|
+
const mirrorSymbolicX = sectionPlane.flipped && !sectionPlane.custom;
|
|
454
|
+
const orientedSymbolicLines: SymbolicDrawingLine[] = mirrorSymbolicX
|
|
455
|
+
? symbolicLines.map((line) => ({
|
|
456
|
+
...line,
|
|
457
|
+
line: {
|
|
458
|
+
start: { x: -line.line.start.x, y: line.line.start.y },
|
|
459
|
+
end: { x: -line.line.end.x, y: line.line.end.y },
|
|
460
|
+
},
|
|
461
|
+
}))
|
|
462
|
+
: symbolicLines;
|
|
463
|
+
|
|
464
|
+
// Cull annotations to a thin view-depth slab IN FRONT of the cut.
|
|
465
|
+
//
|
|
466
|
+
// IfcAnnotation / IfcGridAxis polylines (dimensions, room tags, grid
|
|
467
|
+
// bubbles) live at a single elevation but have no body geometry —
|
|
468
|
+
// the `cutEntityIds.has(line.entityId)` filter below never matches
|
|
469
|
+
// them, so without this they render regardless of where the
|
|
470
|
+
// section sits.
|
|
471
|
+
//
|
|
472
|
+
// Reduce every cut mode (cardinal X/Y/Z + face-pick custom plane)
|
|
473
|
+
// to a single half-space test against a unit normal + signed
|
|
474
|
+
// distance. For cardinal axes the normal is the basis vector and
|
|
475
|
+
// distance is `position` (already in shifted-bounds coords, the
|
|
476
|
+
// same space the symbolic centroids land in). For custom planes
|
|
477
|
+
// the WASM cutter already uses `normal`/`distance` verbatim, so
|
|
478
|
+
// re-use both here for consistency with the cap.
|
|
479
|
+
//
|
|
480
|
+
// The kept window is `−ANNOTATION_VIEW_DEPTH ≤ signedDist ≤ 0` on
|
|
481
|
+
// the −normal side — the side BELOW a down-looking camera, where
|
|
482
|
+
// IFC dimensions live (authored at the storey's floor elevation,
|
|
483
|
+
// not at the cut height). This DIVERGES from
|
|
484
|
+
// `EdgeExtractor.filterEdgesByDepth`, which projects above the
|
|
485
|
+
// cut: annotations and projection edges are naturally on opposite
|
|
486
|
+
// sides of the cut plane. Flipped sections look at the same world
|
|
487
|
+
// from the opposite side, so the slab mirrors to
|
|
488
|
+
// `0 ≤ signedDist ≤ ANNOTATION_VIEW_DEPTH`.
|
|
489
|
+
//
|
|
490
|
+
// Anything on the wrong side of the cut, or farther than the view
|
|
491
|
+
// depth on the right side, is dropped — without the upper bound,
|
|
492
|
+
// dimensions from every storey beyond the cut stacked on top of
|
|
493
|
+
// each other because the half-space alone is unbounded along the
|
|
494
|
+
// camera axis.
|
|
495
|
+
//
|
|
496
|
+
// Annotations missing a recoverable centroid (older WASM build,
|
|
497
|
+
// or a degenerate polyline) are kept — over-rendering is preferable
|
|
498
|
+
// to silently dropping authored dimensions when the runtime can't
|
|
499
|
+
// classify them.
|
|
500
|
+
const cullNormal: [number, number, number] = sectionPlane.custom
|
|
501
|
+
? sectionPlane.custom.normal
|
|
502
|
+
: axis === 'x' ? [1, 0, 0]
|
|
503
|
+
: axis === 'y' ? [0, 1, 0]
|
|
504
|
+
: [0, 0, 1];
|
|
505
|
+
const cullDistance = sectionPlane.custom ? sectionPlane.custom.distance : position;
|
|
506
|
+
const annotationCulled = orientedSymbolicLines.filter((line) => {
|
|
507
|
+
const isAnnotationLike = line.ifcType === 'IfcAnnotation' || line.ifcType === 'IfcGridAxis';
|
|
508
|
+
if (!isAnnotationLike) return true;
|
|
509
|
+
const wx = line.worldX;
|
|
510
|
+
const wy = line.worldY;
|
|
511
|
+
const wz = line.worldZ;
|
|
512
|
+
if (wx === undefined || wy === undefined || wz === undefined) return true;
|
|
513
|
+
const signedDist =
|
|
514
|
+
wx * cullNormal[0] +
|
|
515
|
+
wy * cullNormal[1] +
|
|
516
|
+
wz * cullNormal[2] -
|
|
517
|
+
cullDistance;
|
|
518
|
+
if (sectionPlane.flipped) {
|
|
519
|
+
return signedDist >= 0 && signedDist <= ANNOTATION_VIEW_DEPTH;
|
|
520
|
+
}
|
|
521
|
+
return signedDist <= 0 && signedDist >= -ANNOTATION_VIEW_DEPTH;
|
|
522
|
+
});
|
|
523
|
+
|
|
378
524
|
// Only include symbolic lines for entities that are ACTUALLY being cut
|
|
379
525
|
// This filters out symbols from other floors/levels not intersected by the section plane
|
|
380
|
-
const relevantSymbolicLines =
|
|
526
|
+
const relevantSymbolicLines = annotationCulled.filter(line =>
|
|
381
527
|
line.entityId !== undefined && cutEntityIds.has(line.entityId)
|
|
382
528
|
);
|
|
383
529
|
|
|
@@ -60,10 +60,9 @@ export interface AnnotationText2D {
|
|
|
60
60
|
/**
|
|
61
61
|
* When true, the renderer rebuilds the glyph quad in screen-aligned
|
|
62
62
|
* (cameraRight, cameraUp) basis so the text always faces the camera.
|
|
63
|
-
* Set for
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* dimension/leader annotations that lie flat on the floor).
|
|
63
|
+
* Set for every annotation literal (grid bubbles, dimension callouts,
|
|
64
|
+
* leader labels) so they stay legible in any view — flat-in-plane text
|
|
65
|
+
* collapses to a sliver at oblique angles (issue #812). Defaults to false.
|
|
67
66
|
*/
|
|
68
67
|
billboard?: boolean;
|
|
69
68
|
/** sRGB straight-alpha tint (0..1). Defaults to renderer near-black. */
|
|
@@ -337,12 +336,13 @@ async function parseAnnotations(
|
|
|
337
336
|
// a little air between rows so descenders don't kiss the next cap.
|
|
338
337
|
const lineSpacing = perLineHeight * 1.2;
|
|
339
338
|
const bucket = ensureBucket(text.expressId, text.worldY);
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// shader rebuilds the quad in
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
339
|
+
// All annotation text — grid bubbles, dimension callouts, leader labels —
|
|
340
|
+
// billboards to the camera so it stays legible in any view orientation
|
|
341
|
+
// (top-down, eye-level, oblique). The shader rebuilds the quad in the
|
|
342
|
+
// screen-aligned basis at render time. Authored orientation is intentionally
|
|
343
|
+
// dropped: at oblique viewing angles, flat-in-plane text becomes a smeared
|
|
344
|
+
// sliver of pixels (issue #812). Anchor + alignment are preserved, so each
|
|
345
|
+
// label still sits at its authored insertion point.
|
|
346
346
|
// Read per-instance style metadata. WASM emits these for grid
|
|
347
347
|
// bubble parts (● fill / ○ outline / tag) and reserves them for
|
|
348
348
|
// future IfcTextStyle resolution on regular annotation text.
|
|
@@ -362,7 +362,7 @@ async function parseAnnotations(
|
|
|
362
362
|
content: lines[li],
|
|
363
363
|
alignment: text.alignment,
|
|
364
364
|
lineYOffset: -li * lineSpacing,
|
|
365
|
-
billboard:
|
|
365
|
+
billboard: true,
|
|
366
366
|
color: textColor,
|
|
367
367
|
targetPx,
|
|
368
368
|
};
|
|
@@ -604,6 +604,151 @@ 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.
|
|
662
|
+
//
|
|
663
|
+
// For a floor-plan cut at axis='down' the camera looks DOWN through the
|
|
664
|
+
// cut. "In front of the camera" is therefore the side BELOW the cut —
|
|
665
|
+
// where the floor and authored dimensions sit (IFC convention places
|
|
666
|
+
// dimension annotations at the storey's floor elevation, not at the
|
|
667
|
+
// cut height). The user's complaint: with the slab on the +normal
|
|
668
|
+
// side, you had to scrub the section DOWN into the floor before
|
|
669
|
+
// anything showed, and then the dimensions appeared one storey BELOW
|
|
670
|
+
// the cut. Mirror that — keep the slab on the −normal side for the
|
|
671
|
+
// unflipped down section, and flip it for the reflected-ceiling case.
|
|
672
|
+
//
|
|
673
|
+
// Note this DIVERGES from `profile-projector.isInProjectionRange`,
|
|
674
|
+
// which projects above the cut by default. Annotations live with the
|
|
675
|
+
// storey floor, the projection lives with the upper-storey volume —
|
|
676
|
+
// they're naturally on opposite sides of the cut plane.
|
|
677
|
+
//
|
|
678
|
+
// Tolerance lets annotations authored exactly on the cut plane (e.g.
|
|
679
|
+
// a storey at Z=0 with a section right at the storey datum) survive.
|
|
680
|
+
const TOL = 1e-3;
|
|
681
|
+
const rangeMin = (flipped ? sectionPosWorld : sectionPosWorld - viewDepth) - TOL;
|
|
682
|
+
const rangeMax = (flipped ? sectionPosWorld + viewDepth : sectionPosWorld) + TOL;
|
|
683
|
+
|
|
684
|
+
const lines: DrawingLine2D[] = [];
|
|
685
|
+
const texts: AnnotationText2D[] = [];
|
|
686
|
+
const fills: AnnotationFill2D[] = [];
|
|
687
|
+
|
|
688
|
+
// The drawing-2d cutter negates the 2D U axis on flipped cardinal cuts
|
|
689
|
+
// (see `projectTo2D` in @ifc-lite/drawing-2d/math.ts and `flipU` in the
|
|
690
|
+
// GPU cutter). Annotation primitives come out of WASM in the cutter's
|
|
691
|
+
// UNFLIPPED basis, so on a flipped section they'd sit beside the model
|
|
692
|
+
// (mirrored across X=0) instead of on top of it — exactly the
|
|
693
|
+
// "dimensions floating to the right of the floor plan" symptom. Mirror
|
|
694
|
+
// X for lines/texts/fills here so they line up with the section cut
|
|
695
|
+
// output drawn underneath. Y stays put (the cutter only flips U).
|
|
696
|
+
const pushLine = flipped
|
|
697
|
+
? (ln: DrawingLine2D) => lines.push({
|
|
698
|
+
line: {
|
|
699
|
+
start: { x: -ln.line.start.x, y: ln.line.start.y },
|
|
700
|
+
end: { x: -ln.line.end.x, y: ln.line.end.y },
|
|
701
|
+
},
|
|
702
|
+
category: ln.category,
|
|
703
|
+
})
|
|
704
|
+
: (ln: DrawingLine2D) => lines.push(ln);
|
|
705
|
+
const pushText = flipped
|
|
706
|
+
? (t: AnnotationText2D) => texts.push({ ...t, x: -t.x, dirX: -t.dirX })
|
|
707
|
+
: (t: AnnotationText2D) => texts.push(t);
|
|
708
|
+
const pushFill = flipped
|
|
709
|
+
? (f: AnnotationFill2D) => {
|
|
710
|
+
const src = f.points;
|
|
711
|
+
const dst = new Float32Array(src.length);
|
|
712
|
+
for (let i = 0; i < src.length; i += 2) {
|
|
713
|
+
dst[i] = -src[i];
|
|
714
|
+
dst[i + 1] = src[i + 1];
|
|
715
|
+
}
|
|
716
|
+
fills.push({ ...f, points: dst });
|
|
717
|
+
}
|
|
718
|
+
: (f: AnnotationFill2D) => fills.push(f);
|
|
719
|
+
|
|
720
|
+
for (const store of stores) {
|
|
721
|
+
const key = sourceKey(store);
|
|
722
|
+
if (!key) continue;
|
|
723
|
+
const cached = PARSE_CACHE.get(key);
|
|
724
|
+
if (!cached) continue;
|
|
725
|
+
|
|
726
|
+
for (const bucket of cached.byStorey.values()) {
|
|
727
|
+
const bucketY = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
728
|
+
if (bucketY < rangeMin || bucketY > rangeMax) continue;
|
|
729
|
+
for (const ln of bucket.lines) pushLine(ln);
|
|
730
|
+
for (const t of bucket.texts) pushText(t);
|
|
731
|
+
for (const f of bucket.fills) pushFill(f);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Loose annotations have no resolvable storey — include them if the
|
|
735
|
+
// fallback Y lands in the view range. That keeps malformed exports
|
|
736
|
+
// (e.g. 3DEXPERIENCE files with orphaned storeys) usable when the
|
|
737
|
+
// user is looking at the storey the fallback resolves to.
|
|
738
|
+
if (fallbackY >= rangeMin && fallbackY <= rangeMax) {
|
|
739
|
+
for (const ln of cached.loose) pushLine(ln);
|
|
740
|
+
for (const t of cached.looseTexts) pushText(t);
|
|
741
|
+
for (const f of cached.looseFills) pushFill(f);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (lines.length === 0 && texts.length === 0 && fills.length === 0) {
|
|
746
|
+
return EMPTY_DRAWING_ANNOTATIONS;
|
|
747
|
+
}
|
|
748
|
+
return { lines, texts, fills };
|
|
749
|
+
}, [enabled, axis, sectionPosWorld, viewDepth, flipped, fallbackY, stores, version]);
|
|
750
|
+
}
|
|
751
|
+
|
|
607
752
|
/**
|
|
608
753
|
* Hook for the WebGPU text + fill pipelines. Returns 3D-lifted texts and
|
|
609
754
|
* 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};
|