@ifc-lite/viewer 1.25.0 → 1.25.2
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 +79 -84
- package/CHANGELOG.md +60 -0
- package/dist/assets/{basketViewActivator-CU8_toGq.js → basketViewActivator-CTgyKI3U.js} +6 -6
- package/dist/assets/{bcf-DXGDhw56.js → bcf-7jQby1qi.js} +1 -1
- package/dist/assets/{deflate-Bb1_H2Yf.js → deflate-Cfp9t1Df.js} +1 -1
- package/dist/assets/exporters-DfSvJPi4.js +4660 -0
- package/dist/assets/geometry.worker-Cyn5BybV.js +1 -0
- package/dist/assets/{geotiff-y0ZxbRJd.js → geotiff-xZoE8BkO.js} +10 -10
- package/dist/assets/{ids-DruUNtfD.js → ids-Cu73hD0Y.js} +21 -21
- package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
- package/dist/assets/{index-Dr88ZlSY.js → index-WSbA5iy6.js} +31959 -31608
- package/dist/assets/{jpeg-B3_loqFe.js → jpeg-DhwFEbqb.js} +1 -1
- package/dist/assets/{lerc-nkwS8ZUe.js → lerc-Dz6BXOVb.js} +1 -1
- package/dist/assets/{lzw-D3cW5Wpg.js → lzw-C9z0fG2o.js} +1 -1
- package/dist/assets/{native-bridge-BcYJooq8.js → native-bridge-RvDmzO-2.js} +1 -1
- package/dist/assets/{packbits-DDN4xzB5.js → packbits-jfwifz7C.js} +1 -1
- package/dist/assets/parser.worker-C594dWxH.js +182 -0
- package/dist/assets/raw-R2QfzPAR.js +1 -0
- package/dist/assets/{sandbox-DETNEyQb.js → sandbox-DDSZ7rek.js} +2450 -2260
- package/dist/assets/{server-client-CmzJOeS7.js → server-client-Ctk8_Bof.js} +1 -1
- package/dist/assets/{webimage-CBjgg4up.js → webimage-XFHVyVtC.js} +1 -1
- package/dist/assets/{zstd-C8oQ6qdS.js → zstd-3q5qcl5V.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +22 -21
- package/src/App.tsx +4 -0
- package/src/components/viewer/BCFPanel.tsx +8 -1
- package/src/components/viewer/CommandPalette.tsx +5 -1
- package/src/components/viewer/MainToolbar.tsx +41 -19
- package/src/components/viewer/Section2DPanel.tsx +6 -2
- package/src/components/viewer/Viewport.tsx +48 -3
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +24 -0
- package/src/components/viewer/hierarchy/ifc-icons.ts +60 -0
- package/src/components/viewer/useGeometryStreaming.ts +113 -18
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +61 -0
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +28 -0
- package/src/hooks/ingest/viewerModelIngest.ts +55 -11
- package/src/hooks/useBCF.ts +98 -16
- package/src/hooks/useDrawingGeneration.ts +149 -3
- package/src/hooks/useIfcCache.ts +44 -18
- package/src/hooks/useIfcLoader.ts +1 -23
- package/src/hooks/useSymbolicAnnotations.ts +240 -61
- package/src/store/constants.ts +19 -3
- package/src/store/index.ts +1 -0
- package/src/store/slices/visibilitySlice.ts +2 -1
- package/src/store/types.ts +9 -0
- package/src/utils/serverDataModel.test.ts +51 -1
- package/src/utils/serverDataModel.ts +2 -26
- package/vite.config.ts +0 -5
- package/dist/assets/exporters-DZhLN0ux.js +0 -5957
- package/dist/assets/geometry-controller.worker-DQOSYqtw.js +0 -7
- package/dist/assets/geometry.worker-B62e03Ao.js +0 -1
- package/dist/assets/ifc-lite-Ch2T9pP9.js +0 -7
- package/dist/assets/ifc-lite_bg-D7O1WHgP.wasm +0 -0
- package/dist/assets/ifc-lite_bg-iH_07wf8.wasm +0 -0
- package/dist/assets/parser.worker-BW1IMUed.js +0 -182
- package/dist/assets/raw-CoIXstQ-.js +0 -1
- package/dist/assets/wasm-bridge-CT7mK9W0.js +0 -1
- package/dist/assets/workerHelpers-IEQDo8r3.js +0 -36
|
@@ -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. */
|
|
@@ -92,13 +91,31 @@ export interface AnnotationFill2D {
|
|
|
92
91
|
};
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
/** Cached parse result keyed by source identity.
|
|
94
|
+
/** Cached parse result keyed by source identity.
|
|
95
|
+
*
|
|
96
|
+
* IfcAnnotation and IfcGridAxis primitives are stored in PARALLEL bucket
|
|
97
|
+
* collections (issue #862). They share the same parse pass and the same
|
|
98
|
+
* storey-resolution logic, but the renderer treats them differently:
|
|
99
|
+
*
|
|
100
|
+
* - Annotation buckets always lift every storey (memory
|
|
101
|
+
* `feedback_3d_annotation_overlay_no_section_filter.md`: the user
|
|
102
|
+
* expects every storey's dimensions to be visible in 3D).
|
|
103
|
+
* - Grid buckets get optional section-plane filtering and an
|
|
104
|
+
* independent visibility toggle, so dense-grid models can hide
|
|
105
|
+
* grids per storey without losing dimensions.
|
|
106
|
+
*/
|
|
96
107
|
interface ParseResult {
|
|
108
|
+
// IfcAnnotation buckets
|
|
97
109
|
byStorey: Map<number, AnnotationsForStorey>;
|
|
98
|
-
/** Annotations with no resolvable storey — shown on every floor as a fallback. */
|
|
99
110
|
loose: DrawingLine2D[];
|
|
100
111
|
looseTexts: AnnotationText2D[];
|
|
101
112
|
looseFills: AnnotationFill2D[];
|
|
113
|
+
|
|
114
|
+
// IfcGridAxis buckets (issue #862)
|
|
115
|
+
gridByStorey: Map<number, AnnotationsForStorey>;
|
|
116
|
+
gridLoose: DrawingLine2D[];
|
|
117
|
+
gridLooseTexts: AnnotationText2D[];
|
|
118
|
+
gridLooseFills: AnnotationFill2D[];
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
const CIRCLE_SEGMENTS_FULL = 32;
|
|
@@ -213,6 +230,10 @@ async function parseAnnotations(
|
|
|
213
230
|
loose: [],
|
|
214
231
|
looseTexts: [],
|
|
215
232
|
looseFills: [],
|
|
233
|
+
gridByStorey: new Map(),
|
|
234
|
+
gridLoose: [],
|
|
235
|
+
gridLooseTexts: [],
|
|
236
|
+
gridLooseFills: [],
|
|
216
237
|
};
|
|
217
238
|
const source = store.source;
|
|
218
239
|
if (!source || source.byteLength === 0) {
|
|
@@ -257,6 +278,7 @@ async function parseAnnotations(
|
|
|
257
278
|
const ensureBucket = (
|
|
258
279
|
expressId: number,
|
|
259
280
|
primitiveWorldY: number,
|
|
281
|
+
ifcType: string,
|
|
260
282
|
): AnnotationsForStorey | null => {
|
|
261
283
|
let effectiveY: number | null = null;
|
|
262
284
|
if (Number.isFinite(primitiveWorldY) && primitiveWorldY !== 0) {
|
|
@@ -270,7 +292,11 @@ async function parseAnnotations(
|
|
|
270
292
|
}
|
|
271
293
|
if (effectiveY === null) return null;
|
|
272
294
|
const key = Math.round(effectiveY * 1000);
|
|
273
|
-
|
|
295
|
+
// Issue #862: IfcGridAxis primitives land in a parallel bucket
|
|
296
|
+
// collection so the renderer can section-clip + visibility-toggle
|
|
297
|
+
// them independently of IfcAnnotation (text/dimension symbols).
|
|
298
|
+
const storeyMap = ifcType === 'IfcGridAxis' ? result.gridByStorey : result.byStorey;
|
|
299
|
+
let bucket = storeyMap.get(key);
|
|
274
300
|
if (!bucket) {
|
|
275
301
|
bucket = {
|
|
276
302
|
storeyId: key,
|
|
@@ -279,7 +305,7 @@ async function parseAnnotations(
|
|
|
279
305
|
texts: [],
|
|
280
306
|
fills: [],
|
|
281
307
|
};
|
|
282
|
-
|
|
308
|
+
storeyMap.set(key, bucket);
|
|
283
309
|
}
|
|
284
310
|
return bucket;
|
|
285
311
|
};
|
|
@@ -288,8 +314,9 @@ async function parseAnnotations(
|
|
|
288
314
|
const poly = collection.getPolyline(i);
|
|
289
315
|
if (!poly) continue;
|
|
290
316
|
if (poly.ifcType !== 'IfcAnnotation' && poly.ifcType !== 'IfcGridAxis') continue;
|
|
291
|
-
const bucket = ensureBucket(poly.expressId, poly.worldY);
|
|
292
|
-
const
|
|
317
|
+
const bucket = ensureBucket(poly.expressId, poly.worldY, poly.ifcType);
|
|
318
|
+
const looseTarget = poly.ifcType === 'IfcGridAxis' ? result.gridLoose : result.loose;
|
|
319
|
+
const out = bucket ? bucket.lines : looseTarget;
|
|
293
320
|
polylineToSegments(poly.points, poly.pointCount, poly.isClosed, out);
|
|
294
321
|
}
|
|
295
322
|
|
|
@@ -297,8 +324,9 @@ async function parseAnnotations(
|
|
|
297
324
|
const circle = collection.getCircle(i);
|
|
298
325
|
if (!circle) continue;
|
|
299
326
|
if (circle.ifcType !== 'IfcAnnotation' && circle.ifcType !== 'IfcGridAxis') continue;
|
|
300
|
-
const bucket = ensureBucket(circle.expressId, circle.worldY);
|
|
301
|
-
const
|
|
327
|
+
const bucket = ensureBucket(circle.expressId, circle.worldY, circle.ifcType);
|
|
328
|
+
const looseTarget = circle.ifcType === 'IfcGridAxis' ? result.gridLoose : result.loose;
|
|
329
|
+
const out = bucket ? bucket.lines : looseTarget;
|
|
302
330
|
circleToSegments(
|
|
303
331
|
circle.centerX,
|
|
304
332
|
circle.centerY,
|
|
@@ -336,13 +364,15 @@ async function parseAnnotations(
|
|
|
336
364
|
// Industry-standard line-spacing (CSS line-height ≈ 1.2). Picks up
|
|
337
365
|
// a little air between rows so descenders don't kiss the next cap.
|
|
338
366
|
const lineSpacing = perLineHeight * 1.2;
|
|
339
|
-
const bucket = ensureBucket(text.expressId, text.worldY);
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
367
|
+
const bucket = ensureBucket(text.expressId, text.worldY, text.ifcType);
|
|
368
|
+
const looseTextTarget = text.ifcType === 'IfcGridAxis' ? result.gridLooseTexts : result.looseTexts;
|
|
369
|
+
// All annotation text — grid bubbles, dimension callouts, leader labels —
|
|
370
|
+
// billboards to the camera so it stays legible in any view orientation
|
|
371
|
+
// (top-down, eye-level, oblique). The shader rebuilds the quad in the
|
|
372
|
+
// screen-aligned basis at render time. Authored orientation is intentionally
|
|
373
|
+
// dropped: at oblique viewing angles, flat-in-plane text becomes a smeared
|
|
374
|
+
// sliver of pixels (issue #812). Anchor + alignment are preserved, so each
|
|
375
|
+
// label still sits at its authored insertion point.
|
|
346
376
|
// Read per-instance style metadata. WASM emits these for grid
|
|
347
377
|
// bubble parts (● fill / ○ outline / tag) and reserves them for
|
|
348
378
|
// future IfcTextStyle resolution on regular annotation text.
|
|
@@ -362,11 +392,11 @@ async function parseAnnotations(
|
|
|
362
392
|
content: lines[li],
|
|
363
393
|
alignment: text.alignment,
|
|
364
394
|
lineYOffset: -li * lineSpacing,
|
|
365
|
-
billboard:
|
|
395
|
+
billboard: true,
|
|
366
396
|
color: textColor,
|
|
367
397
|
targetPx,
|
|
368
398
|
};
|
|
369
|
-
(bucket ? bucket.texts :
|
|
399
|
+
(bucket ? bucket.texts : looseTextTarget).push(t2d);
|
|
370
400
|
}
|
|
371
401
|
}
|
|
372
402
|
|
|
@@ -389,8 +419,9 @@ async function parseAnnotations(
|
|
|
389
419
|
}
|
|
390
420
|
: undefined,
|
|
391
421
|
};
|
|
392
|
-
const bucket = ensureBucket(fill.expressId, fill.worldY);
|
|
393
|
-
|
|
422
|
+
const bucket = ensureBucket(fill.expressId, fill.worldY, fill.ifcType);
|
|
423
|
+
const looseFillTarget = fill.ifcType === 'IfcGridAxis' ? result.gridLooseFills : result.looseFills;
|
|
424
|
+
(bucket ? bucket.fills : looseFillTarget).push(f2d);
|
|
394
425
|
}
|
|
395
426
|
} finally {
|
|
396
427
|
processor.dispose();
|
|
@@ -521,17 +552,48 @@ function resolveBucketY(elevation: number | null, fallbackY: number): number {
|
|
|
521
552
|
return elevation === null ? fallbackY : elevation;
|
|
522
553
|
}
|
|
523
554
|
|
|
555
|
+
/** Section-clip parameters for grid lines (issue #862). Grids ARE clipped
|
|
556
|
+
* by the active section plane; IfcAnnotation overlays are NOT (per the
|
|
557
|
+
* feedback_3d_annotation_overlay_no_section_filter memory). When
|
|
558
|
+
* `enabled === false` no clipping happens — grids lift to every storey
|
|
559
|
+
* same as annotations.
|
|
560
|
+
*/
|
|
561
|
+
export interface SectionClipForGrid {
|
|
562
|
+
enabled: boolean;
|
|
563
|
+
/** World coord on the cut axis (e.g. world-Y for axis='down'). */
|
|
564
|
+
posWorld: number;
|
|
565
|
+
/** Half-thickness of the visible band around the cut, world units. */
|
|
566
|
+
viewDepth: number;
|
|
567
|
+
/** Cut axis. Only `'down'` performs vertical clipping; other axes pass through unfiltered (grid lines are vertical and don't project meaningfully onto elevation cuts). */
|
|
568
|
+
axis: 'down' | 'front' | 'side';
|
|
569
|
+
}
|
|
570
|
+
|
|
524
571
|
export function useSymbolicAnnotations(params: {
|
|
572
|
+
/** Enable IfcAnnotation lift (the existing default behaviour). */
|
|
525
573
|
enabled: boolean;
|
|
574
|
+
/**
|
|
575
|
+
* Enable IfcGrid lift. Independent of `enabled` so a user can hide
|
|
576
|
+
* annotations while keeping grids, or vice versa (issue #862).
|
|
577
|
+
* Defaults to `enabled` so existing call sites that don't set it
|
|
578
|
+
* keep the legacy combined behaviour.
|
|
579
|
+
*/
|
|
580
|
+
gridEnabled?: boolean;
|
|
581
|
+
/** Section clipping for grids only — see [`SectionClipForGrid`]. */
|
|
582
|
+
gridSectionClip?: SectionClipForGrid;
|
|
526
583
|
/** World Y to use for annotations with no resolvable storey. Defaults to 0. */
|
|
527
584
|
fallbackY?: number;
|
|
528
585
|
}): Float32Array {
|
|
529
|
-
const { enabled, fallbackY = 0 } = params;
|
|
586
|
+
const { enabled, gridEnabled, gridSectionClip, fallbackY = 0 } = params;
|
|
587
|
+
const effectiveGridEnabled = gridEnabled ?? enabled;
|
|
530
588
|
const stores = useActiveStores();
|
|
531
|
-
|
|
589
|
+
// Trigger parse if EITHER subset is enabled — the parse pass is shared.
|
|
590
|
+
const version = useAnnotationParseTrigger(enabled || effectiveGridEnabled, stores);
|
|
591
|
+
const clipEnabled = !!gridSectionClip && gridSectionClip.enabled && gridSectionClip.axis === 'down';
|
|
592
|
+
const clipPos = clipEnabled ? gridSectionClip!.posWorld : 0;
|
|
593
|
+
const clipDepth = clipEnabled ? gridSectionClip!.viewDepth : 0;
|
|
532
594
|
|
|
533
595
|
return useMemo(() => {
|
|
534
|
-
if (!enabled) return EMPTY_F32;
|
|
596
|
+
if (!enabled && !effectiveGridEnabled) return EMPTY_F32;
|
|
535
597
|
void version; // depend on parse-completion ticks
|
|
536
598
|
|
|
537
599
|
const verts: number[] = [];
|
|
@@ -546,22 +608,49 @@ export function useSymbolicAnnotations(params: {
|
|
|
546
608
|
continue;
|
|
547
609
|
}
|
|
548
610
|
if (debugEnabled()) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
611
|
+
console.log(
|
|
612
|
+
`[annotations] store ${storeIdx}: annotation buckets=${cached.byStorey.size}+${cached.loose.length}loose, grid buckets=${cached.gridByStorey.size}+${cached.gridLoose.length}loose (annot=${enabled}, grid=${effectiveGridEnabled}, clip=${clipEnabled})`,
|
|
613
|
+
);
|
|
552
614
|
}
|
|
553
615
|
|
|
554
|
-
|
|
555
|
-
|
|
616
|
+
if (enabled) {
|
|
617
|
+
for (const bucket of cached.byStorey.values()) {
|
|
618
|
+
liftTo3DLineList(bucket.lines, resolveBucketY(bucket.storeyElevation, fallbackY), verts);
|
|
619
|
+
}
|
|
620
|
+
liftTo3DLineList(cached.loose, fallbackY, verts);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (effectiveGridEnabled) {
|
|
624
|
+
// Issue #862: section-clip grid buckets only — IfcAnnotation
|
|
625
|
+
// intentionally bypasses this per the feedback memory ("the
|
|
626
|
+
// user expects every storey's dimensions/grid bubbles to lift
|
|
627
|
+
// into the viewport when [the annotation toggle is] on, even
|
|
628
|
+
// while a section cut is active").
|
|
629
|
+
if (clipEnabled) {
|
|
630
|
+
const lo = clipPos - clipDepth;
|
|
631
|
+
const hi = clipPos + clipDepth;
|
|
632
|
+
for (const bucket of cached.gridByStorey.values()) {
|
|
633
|
+
const y = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
634
|
+
if (y < lo || y > hi) continue;
|
|
635
|
+
liftTo3DLineList(bucket.lines, y, verts);
|
|
636
|
+
}
|
|
637
|
+
if (fallbackY >= lo && fallbackY <= hi) {
|
|
638
|
+
liftTo3DLineList(cached.gridLoose, fallbackY, verts);
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
for (const bucket of cached.gridByStorey.values()) {
|
|
642
|
+
liftTo3DLineList(bucket.lines, resolveBucketY(bucket.storeyElevation, fallbackY), verts);
|
|
643
|
+
}
|
|
644
|
+
liftTo3DLineList(cached.gridLoose, fallbackY, verts);
|
|
645
|
+
}
|
|
556
646
|
}
|
|
557
|
-
liftTo3DLineList(cached.loose, fallbackY, verts);
|
|
558
647
|
storeIdx++;
|
|
559
648
|
}
|
|
560
649
|
|
|
561
650
|
if (debugEnabled()) console.log(`[annotations] total 3D line vertices: ${verts.length / 3} from ${stores.length} stores`);
|
|
562
651
|
if (verts.length === 0) return EMPTY_F32;
|
|
563
652
|
return new Float32Array(verts);
|
|
564
|
-
}, [enabled, stores, version, fallbackY]);
|
|
653
|
+
}, [enabled, effectiveGridEnabled, clipEnabled, clipPos, clipDepth, stores, version, fallbackY]);
|
|
565
654
|
}
|
|
566
655
|
|
|
567
656
|
/**
|
|
@@ -658,43 +747,96 @@ export function useSymbolicAnnotationsForDrawing(params: {
|
|
|
658
747
|
if (axis !== 'down') return EMPTY_DRAWING_ANNOTATIONS;
|
|
659
748
|
void version;
|
|
660
749
|
|
|
661
|
-
// Section view range in world Y.
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
750
|
+
// Section view range in world Y.
|
|
751
|
+
//
|
|
752
|
+
// For a floor-plan cut at axis='down' the camera looks DOWN through the
|
|
753
|
+
// cut. "In front of the camera" is therefore the side BELOW the cut —
|
|
754
|
+
// where the floor and authored dimensions sit (IFC convention places
|
|
755
|
+
// dimension annotations at the storey's floor elevation, not at the
|
|
756
|
+
// cut height). The user's complaint: with the slab on the +normal
|
|
757
|
+
// side, you had to scrub the section DOWN into the floor before
|
|
758
|
+
// anything showed, and then the dimensions appeared one storey BELOW
|
|
759
|
+
// the cut. Mirror that — keep the slab on the −normal side for the
|
|
760
|
+
// unflipped down section, and flip it for the reflected-ceiling case.
|
|
761
|
+
//
|
|
762
|
+
// Note this DIVERGES from `profile-projector.isInProjectionRange`,
|
|
763
|
+
// which projects above the cut by default. Annotations live with the
|
|
764
|
+
// storey floor, the projection lives with the upper-storey volume —
|
|
765
|
+
// they're naturally on opposite sides of the cut plane.
|
|
766
|
+
//
|
|
767
|
+
// Tolerance lets annotations authored exactly on the cut plane (e.g.
|
|
768
|
+
// a storey at Z=0 with a section right at the storey datum) survive.
|
|
668
769
|
const TOL = 1e-3;
|
|
669
|
-
const rangeMin = (flipped ? sectionPosWorld - viewDepth
|
|
670
|
-
const rangeMax = (flipped ? sectionPosWorld : sectionPosWorld
|
|
770
|
+
const rangeMin = (flipped ? sectionPosWorld : sectionPosWorld - viewDepth) - TOL;
|
|
771
|
+
const rangeMax = (flipped ? sectionPosWorld + viewDepth : sectionPosWorld) + TOL;
|
|
671
772
|
|
|
672
773
|
const lines: DrawingLine2D[] = [];
|
|
673
774
|
const texts: AnnotationText2D[] = [];
|
|
674
775
|
const fills: AnnotationFill2D[] = [];
|
|
675
776
|
|
|
777
|
+
// The drawing-2d cutter negates the 2D U axis on flipped cardinal cuts
|
|
778
|
+
// (see `projectTo2D` in @ifc-lite/drawing-2d/math.ts and `flipU` in the
|
|
779
|
+
// GPU cutter). Annotation primitives come out of WASM in the cutter's
|
|
780
|
+
// UNFLIPPED basis, so on a flipped section they'd sit beside the model
|
|
781
|
+
// (mirrored across X=0) instead of on top of it — exactly the
|
|
782
|
+
// "dimensions floating to the right of the floor plan" symptom. Mirror
|
|
783
|
+
// X for lines/texts/fills here so they line up with the section cut
|
|
784
|
+
// output drawn underneath. Y stays put (the cutter only flips U).
|
|
785
|
+
const pushLine = flipped
|
|
786
|
+
? (ln: DrawingLine2D) => lines.push({
|
|
787
|
+
line: {
|
|
788
|
+
start: { x: -ln.line.start.x, y: ln.line.start.y },
|
|
789
|
+
end: { x: -ln.line.end.x, y: ln.line.end.y },
|
|
790
|
+
},
|
|
791
|
+
category: ln.category,
|
|
792
|
+
})
|
|
793
|
+
: (ln: DrawingLine2D) => lines.push(ln);
|
|
794
|
+
const pushText = flipped
|
|
795
|
+
? (t: AnnotationText2D) => texts.push({ ...t, x: -t.x, dirX: -t.dirX })
|
|
796
|
+
: (t: AnnotationText2D) => texts.push(t);
|
|
797
|
+
const pushFill = flipped
|
|
798
|
+
? (f: AnnotationFill2D) => {
|
|
799
|
+
const src = f.points;
|
|
800
|
+
const dst = new Float32Array(src.length);
|
|
801
|
+
for (let i = 0; i < src.length; i += 2) {
|
|
802
|
+
dst[i] = -src[i];
|
|
803
|
+
dst[i + 1] = src[i + 1];
|
|
804
|
+
}
|
|
805
|
+
fills.push({ ...f, points: dst });
|
|
806
|
+
}
|
|
807
|
+
: (f: AnnotationFill2D) => fills.push(f);
|
|
808
|
+
|
|
676
809
|
for (const store of stores) {
|
|
677
810
|
const key = sourceKey(store);
|
|
678
811
|
if (!key) continue;
|
|
679
812
|
const cached = PARSE_CACHE.get(key);
|
|
680
813
|
if (!cached) continue;
|
|
681
814
|
|
|
682
|
-
|
|
815
|
+
// Drawing-2D pulls BOTH annotation and grid buckets (issue #862
|
|
816
|
+
// split them at parse time so the 3D viewport can clip them
|
|
817
|
+
// separately — the 2D Section panel still wants the combined
|
|
818
|
+
// overlay).
|
|
819
|
+
const collectBucket = (bucket: AnnotationsForStorey) => {
|
|
683
820
|
const bucketY = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
684
|
-
if (bucketY < rangeMin || bucketY > rangeMax)
|
|
685
|
-
for (const ln of bucket.lines)
|
|
686
|
-
for (const t of bucket.texts)
|
|
687
|
-
for (const f of bucket.fills)
|
|
688
|
-
}
|
|
821
|
+
if (bucketY < rangeMin || bucketY > rangeMax) return;
|
|
822
|
+
for (const ln of bucket.lines) pushLine(ln);
|
|
823
|
+
for (const t of bucket.texts) pushText(t);
|
|
824
|
+
for (const f of bucket.fills) pushFill(f);
|
|
825
|
+
};
|
|
826
|
+
for (const bucket of cached.byStorey.values()) collectBucket(bucket);
|
|
827
|
+
for (const bucket of cached.gridByStorey.values()) collectBucket(bucket);
|
|
689
828
|
|
|
690
829
|
// Loose annotations have no resolvable storey — include them if the
|
|
691
830
|
// fallback Y lands in the view range. That keeps malformed exports
|
|
692
831
|
// (e.g. 3DEXPERIENCE files with orphaned storeys) usable when the
|
|
693
832
|
// user is looking at the storey the fallback resolves to.
|
|
694
833
|
if (fallbackY >= rangeMin && fallbackY <= rangeMax) {
|
|
695
|
-
for (const ln of cached.loose)
|
|
696
|
-
for (const t of cached.looseTexts)
|
|
697
|
-
for (const f of cached.looseFills)
|
|
834
|
+
for (const ln of cached.loose) pushLine(ln);
|
|
835
|
+
for (const t of cached.looseTexts) pushText(t);
|
|
836
|
+
for (const f of cached.looseFills) pushFill(f);
|
|
837
|
+
for (const ln of cached.gridLoose) pushLine(ln);
|
|
838
|
+
for (const t of cached.gridLooseTexts) pushText(t);
|
|
839
|
+
for (const f of cached.gridLooseFills) pushFill(f);
|
|
698
840
|
}
|
|
699
841
|
}
|
|
700
842
|
|
|
@@ -713,14 +855,24 @@ export function useSymbolicAnnotationsForDrawing(params: {
|
|
|
713
855
|
*/
|
|
714
856
|
export function useSymbolicAnnotationsRichData(params: {
|
|
715
857
|
enabled: boolean;
|
|
858
|
+
/** Lift grid-bubble texts + fills. Independent of `enabled` (issue #862).
|
|
859
|
+
* Defaults to `enabled` for legacy callers. */
|
|
860
|
+
gridEnabled?: boolean;
|
|
861
|
+
/** Section clipping for grid texts/fills only — same semantics as
|
|
862
|
+
* [`useSymbolicAnnotations`]. */
|
|
863
|
+
gridSectionClip?: SectionClipForGrid;
|
|
716
864
|
fallbackY?: number;
|
|
717
865
|
}): { texts: readonly AnnotationText3D[]; fills: readonly AnnotationFill3D[] } {
|
|
718
|
-
const { enabled, fallbackY = 0 } = params;
|
|
866
|
+
const { enabled, gridEnabled, gridSectionClip, fallbackY = 0 } = params;
|
|
867
|
+
const effectiveGridEnabled = gridEnabled ?? enabled;
|
|
719
868
|
const stores = useActiveStores();
|
|
720
|
-
const version = useAnnotationParseTrigger(enabled, stores);
|
|
869
|
+
const version = useAnnotationParseTrigger(enabled || effectiveGridEnabled, stores);
|
|
870
|
+
const clipEnabled = !!gridSectionClip && gridSectionClip.enabled && gridSectionClip.axis === 'down';
|
|
871
|
+
const clipPos = clipEnabled ? gridSectionClip!.posWorld : 0;
|
|
872
|
+
const clipDepth = clipEnabled ? gridSectionClip!.viewDepth : 0;
|
|
721
873
|
|
|
722
874
|
return useMemo(() => {
|
|
723
|
-
if (!enabled) return { texts: EMPTY_TEXTS, fills: EMPTY_FILLS };
|
|
875
|
+
if (!enabled && !effectiveGridEnabled) return { texts: EMPTY_TEXTS, fills: EMPTY_FILLS };
|
|
724
876
|
void version;
|
|
725
877
|
|
|
726
878
|
const texts: AnnotationText3D[] = [];
|
|
@@ -759,18 +911,45 @@ export function useSymbolicAnnotationsRichData(params: {
|
|
|
759
911
|
});
|
|
760
912
|
};
|
|
761
913
|
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
914
|
+
if (enabled) {
|
|
915
|
+
for (const bucket of cached.byStorey.values()) {
|
|
916
|
+
const y = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
917
|
+
for (const t of bucket.texts) pushText(t, y);
|
|
918
|
+
for (const f of bucket.fills) pushFill(f, y);
|
|
919
|
+
}
|
|
920
|
+
for (const t of cached.looseTexts) pushText(t, fallbackY);
|
|
921
|
+
for (const f of cached.looseFills) pushFill(f, fallbackY);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (effectiveGridEnabled) {
|
|
925
|
+
if (clipEnabled) {
|
|
926
|
+
const lo = clipPos - clipDepth;
|
|
927
|
+
const hi = clipPos + clipDepth;
|
|
928
|
+
for (const bucket of cached.gridByStorey.values()) {
|
|
929
|
+
const y = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
930
|
+
if (y < lo || y > hi) continue;
|
|
931
|
+
for (const t of bucket.texts) pushText(t, y);
|
|
932
|
+
for (const f of bucket.fills) pushFill(f, y);
|
|
933
|
+
}
|
|
934
|
+
if (fallbackY >= lo && fallbackY <= hi) {
|
|
935
|
+
for (const t of cached.gridLooseTexts) pushText(t, fallbackY);
|
|
936
|
+
for (const f of cached.gridLooseFills) pushFill(f, fallbackY);
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
for (const bucket of cached.gridByStorey.values()) {
|
|
940
|
+
const y = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
941
|
+
for (const t of bucket.texts) pushText(t, y);
|
|
942
|
+
for (const f of bucket.fills) pushFill(f, y);
|
|
943
|
+
}
|
|
944
|
+
for (const t of cached.gridLooseTexts) pushText(t, fallbackY);
|
|
945
|
+
for (const f of cached.gridLooseFills) pushFill(f, fallbackY);
|
|
946
|
+
}
|
|
766
947
|
}
|
|
767
|
-
for (const t of cached.looseTexts) pushText(t, fallbackY);
|
|
768
|
-
for (const f of cached.looseFills) pushFill(f, fallbackY);
|
|
769
948
|
}
|
|
770
949
|
|
|
771
950
|
return {
|
|
772
951
|
texts: texts.length ? texts : EMPTY_TEXTS,
|
|
773
952
|
fills: fills.length ? fills : EMPTY_FILLS,
|
|
774
953
|
};
|
|
775
|
-
}, [enabled, stores, version, fallbackY]);
|
|
954
|
+
}, [enabled, effectiveGridEnabled, clipEnabled, clipPos, clipDepth, stores, version, fallbackY]);
|
|
776
955
|
}
|
package/src/store/constants.ts
CHANGED
|
@@ -159,6 +159,7 @@ export const TYPE_VISIBILITY_STORAGE_KEYS = {
|
|
|
159
159
|
openings: 'ifc-lite-ifc-openings-visible',
|
|
160
160
|
site: 'ifc-lite-ifc-site-visible',
|
|
161
161
|
ifcAnnotations: 'ifc-lite-ifc-annotations-visible',
|
|
162
|
+
ifcGrid: 'ifc-lite-ifc-grid-visible',
|
|
162
163
|
} as const;
|
|
163
164
|
|
|
164
165
|
/** Legacy alias — kept until external callers migrate. */
|
|
@@ -178,13 +179,16 @@ function readPersistedBool(key: string, fallback: boolean): boolean {
|
|
|
178
179
|
|
|
179
180
|
// Semantic defaults applied when no localStorage preference is set.
|
|
180
181
|
// IfcSpace / IfcOpeningElement off — they cover walls and confuse novices
|
|
181
|
-
// on first load. IfcSite + IfcAnnotation
|
|
182
|
-
// design intent users expect to see by default.
|
|
182
|
+
// on first load. IfcSite + IfcAnnotation + IfcGrid on — all three convey
|
|
183
|
+
// design intent users expect to see by default. (Issue #862 split grid
|
|
184
|
+
// into its own toggle so dense-grid models can hide grids without losing
|
|
185
|
+
// dimensions/labels.)
|
|
183
186
|
const SEMANTIC_DEFAULTS = {
|
|
184
187
|
spaces: false,
|
|
185
188
|
openings: false,
|
|
186
189
|
site: true,
|
|
187
190
|
ifcAnnotations: true,
|
|
191
|
+
ifcGrid: true,
|
|
188
192
|
} as const;
|
|
189
193
|
|
|
190
194
|
export const TYPE_VISIBILITY_DEFAULTS = {
|
|
@@ -194,8 +198,20 @@ export const TYPE_VISIBILITY_DEFAULTS = {
|
|
|
194
198
|
OPENINGS: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.openings, SEMANTIC_DEFAULTS.openings),
|
|
195
199
|
/** IfcSite visibility — persisted across reloads. */
|
|
196
200
|
SITE: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.site, SEMANTIC_DEFAULTS.site),
|
|
197
|
-
/** IfcAnnotation
|
|
201
|
+
/** IfcAnnotation visibility (text, dimensions, leaders) — persisted. */
|
|
198
202
|
IFC_ANNOTATIONS: readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.ifcAnnotations, SEMANTIC_DEFAULTS.ifcAnnotations),
|
|
203
|
+
/**
|
|
204
|
+
* IfcGrid visibility (axis lines + bubble tags) — persisted. Issue
|
|
205
|
+
* #862. Migration: if the new key isn't set yet, fall back to the
|
|
206
|
+
* legacy combined `ifcAnnotations` preference. That way a user who
|
|
207
|
+
* previously turned the combined "Annotations & Grids" toggle off
|
|
208
|
+
* keeps grids hidden after upgrade, instead of grids silently
|
|
209
|
+
* reappearing (PR #868 review, chatgpt-codex P2).
|
|
210
|
+
*/
|
|
211
|
+
IFC_GRID: readPersistedBool(
|
|
212
|
+
TYPE_VISIBILITY_STORAGE_KEYS.ifcGrid,
|
|
213
|
+
readPersistedBool(TYPE_VISIBILITY_STORAGE_KEYS.ifcAnnotations, SEMANTIC_DEFAULTS.ifcGrid),
|
|
214
|
+
),
|
|
199
215
|
} as const;
|
|
200
216
|
|
|
201
217
|
// ============================================================================
|
package/src/store/index.ts
CHANGED
|
@@ -208,6 +208,7 @@ const createViewerStore = () => create<ViewerState>()((...args) => ({
|
|
|
208
208
|
openings: TYPE_VISIBILITY_DEFAULTS.OPENINGS,
|
|
209
209
|
site: TYPE_VISIBILITY_DEFAULTS.SITE,
|
|
210
210
|
ifcAnnotations: TYPE_VISIBILITY_DEFAULTS.IFC_ANNOTATIONS,
|
|
211
|
+
ifcGrid: TYPE_VISIBILITY_DEFAULTS.IFC_GRID,
|
|
211
212
|
},
|
|
212
213
|
|
|
213
214
|
// Visibility (multi-model)
|
|
@@ -43,7 +43,7 @@ export interface VisibilitySlice {
|
|
|
43
43
|
clearAllFilters: () => void;
|
|
44
44
|
showAll: () => void;
|
|
45
45
|
isEntityVisible: (id: number) => boolean;
|
|
46
|
-
toggleTypeVisibility: (type: 'spaces' | 'openings' | 'site' | 'ifcAnnotations') => void;
|
|
46
|
+
toggleTypeVisibility: (type: 'spaces' | 'openings' | 'site' | 'ifcAnnotations' | 'ifcGrid') => void;
|
|
47
47
|
/** Set all hidden entities at once (for BCF viewpoint application) */
|
|
48
48
|
setHiddenEntities: (ids: Set<number>) => void;
|
|
49
49
|
/** Set all isolated entities at once (for BCF viewpoint with defaultVisibility=false) */
|
|
@@ -80,6 +80,7 @@ export const createVisibilitySlice: StateCreator<VisibilitySlice, [], [], Visibi
|
|
|
80
80
|
openings: TYPE_VISIBILITY_DEFAULTS.OPENINGS,
|
|
81
81
|
site: TYPE_VISIBILITY_DEFAULTS.SITE,
|
|
82
82
|
ifcAnnotations: TYPE_VISIBILITY_DEFAULTS.IFC_ANNOTATIONS,
|
|
83
|
+
ifcGrid: TYPE_VISIBILITY_DEFAULTS.IFC_GRID,
|
|
83
84
|
},
|
|
84
85
|
|
|
85
86
|
// Initial state (multi-model)
|
package/src/store/types.ts
CHANGED
|
@@ -199,6 +199,15 @@ export interface TypeVisibility {
|
|
|
199
199
|
site: boolean;
|
|
200
200
|
/** IfcAnnotation (2D symbolic curves) - on by default when present */
|
|
201
201
|
ifcAnnotations: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* IfcGrid axis lines + bubble tags — split from `ifcAnnotations`
|
|
204
|
+
* (issue #862). Default true to match the legacy combined behaviour;
|
|
205
|
+
* users with dense grids that obscure components can hide grids while
|
|
206
|
+
* keeping annotations on. Unlike `ifcAnnotations`, grids are also
|
|
207
|
+
* section-clipped when a 3D section plane is active so each storey's
|
|
208
|
+
* grid lines only show for storeys near the cut.
|
|
209
|
+
*/
|
|
210
|
+
ifcGrid: boolean;
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
// ============================================================================
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { describe, it } from 'node:test';
|
|
7
7
|
import type { DataModel } from '@ifc-lite/server-client';
|
|
8
|
-
import { IfcTypeEnum } from '@ifc-lite/data';
|
|
8
|
+
import { IfcTypeEnum, RelationshipType } from '@ifc-lite/data';
|
|
9
9
|
import { convertServerDataModel, type ServerParseResult } from './serverDataModel';
|
|
10
10
|
|
|
11
11
|
const parseResult: ServerParseResult = {
|
|
@@ -87,4 +87,54 @@ describe('convertServerDataModel', () => {
|
|
|
87
87
|
assert.deepEqual(dataStore.spatialHierarchy?.getPath(4).map((node) => node.expressId), [1, 2, 3]);
|
|
88
88
|
assert.deepEqual(dataStore.spatialHierarchy?.byBuilding.get(2), []);
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
it('uses the canonical parser relationship map for server relationships', () => {
|
|
92
|
+
const dataModel: DataModel = {
|
|
93
|
+
entities: new Map([
|
|
94
|
+
[1, { entity_id: 1, type_name: 'IFCPROJECT', global_id: '0', name: 'Project', has_geometry: false }],
|
|
95
|
+
[2, { entity_id: 2, type_name: 'IFCBUILDING', global_id: '1', name: 'Building', has_geometry: false }],
|
|
96
|
+
[3, { entity_id: 3, type_name: 'IFCDOCUMENTREFERENCE', global_id: '', name: 'Spec', has_geometry: false }],
|
|
97
|
+
]),
|
|
98
|
+
propertySets: new Map(),
|
|
99
|
+
quantitySets: new Map(),
|
|
100
|
+
relationships: [
|
|
101
|
+
{ rel_type: 'IFCRELNESTS', relating_id: 1, related_id: 2 },
|
|
102
|
+
{ rel_type: 'IFCRELASSOCIATESDOCUMENT', relating_id: 3, related_id: 2 },
|
|
103
|
+
],
|
|
104
|
+
spatialHierarchy: {
|
|
105
|
+
nodes: [
|
|
106
|
+
{
|
|
107
|
+
entity_id: 1,
|
|
108
|
+
parent_id: 0,
|
|
109
|
+
level: 0,
|
|
110
|
+
path: 'Project',
|
|
111
|
+
type_name: 'IFCPROJECT',
|
|
112
|
+
name: 'Project',
|
|
113
|
+
children_ids: [2],
|
|
114
|
+
element_ids: [],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
entity_id: 2,
|
|
118
|
+
parent_id: 1,
|
|
119
|
+
level: 1,
|
|
120
|
+
path: 'Project/Building',
|
|
121
|
+
type_name: 'IFCBUILDING',
|
|
122
|
+
name: 'Building',
|
|
123
|
+
children_ids: [],
|
|
124
|
+
element_ids: [],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
project_id: 1,
|
|
128
|
+
element_to_storey: new Map(),
|
|
129
|
+
element_to_building: new Map(),
|
|
130
|
+
element_to_site: new Map(),
|
|
131
|
+
element_to_space: new Map(),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const dataStore = convertServerDataModel(dataModel, parseResult, { size: 1 }, []);
|
|
136
|
+
|
|
137
|
+
assert.deepEqual(dataStore.relationships.getRelated(1, RelationshipType.Aggregates, 'forward'), [2]);
|
|
138
|
+
assert.deepEqual(dataStore.relationships.getRelated(3, RelationshipType.AssociatesDocument, 'forward'), [2]);
|
|
139
|
+
});
|
|
90
140
|
});
|