@ifc-lite/viewer 1.25.1 → 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 +23 -0
- package/dist/assets/{basketViewActivator-Dkn92C04.js → basketViewActivator-CTgyKI3U.js} +6 -6
- package/dist/assets/{bcf-DP2AK1-_.js → bcf-7jQby1qi.js} +1 -1
- package/dist/assets/{deflate-BYqYwhkl.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-By06vdeL.js → geotiff-xZoE8BkO.js} +10 -10
- package/dist/assets/{ids-DDkkb4mo.js → ids-Cu73hD0Y.js} +21 -21
- package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
- package/dist/assets/{index-CqBdDOAZ.js → index-WSbA5iy6.js} +34803 -34679
- package/dist/assets/{jpeg-B4IBTphL.js → jpeg-DhwFEbqb.js} +1 -1
- package/dist/assets/{lerc-DQ3jI0Ke.js → lerc-Dz6BXOVb.js} +1 -1
- package/dist/assets/{lzw-CtdH775t.js → lzw-C9z0fG2o.js} +1 -1
- package/dist/assets/{native-bridge-DA8wxaN_.js → native-bridge-RvDmzO-2.js} +1 -1
- package/dist/assets/{packbits-DG3zn49C.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-D1pQT-5R.js → sandbox-DDSZ7rek.js} +2450 -2260
- package/dist/assets/{server-client-D9xO_8yX.js → server-client-Ctk8_Bof.js} +1 -1
- package/dist/assets/{webimage-_-qCDjkn.js → webimage-XFHVyVtC.js} +1 -1
- package/dist/assets/{zstd-DlfgC8gA.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/CommandPalette.tsx +5 -1
- package/src/components/viewer/MainToolbar.tsx +41 -19
- package/src/components/viewer/Viewport.tsx +48 -3
- 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/useIfcCache.ts +44 -18
- package/src/hooks/useIfcLoader.ts +1 -23
- package/src/hooks/useSymbolicAnnotations.ts +170 -35
- 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-CZe0D8N-.js +0 -5957
- package/dist/assets/geometry-controller.worker-pD49_fH6.js +0 -7
- package/dist/assets/geometry.worker-D4c-06r5.js +0 -1
- package/dist/assets/ifc-lite-DxGqDbjO.js +0 -7
- package/dist/assets/ifc-lite_bg-BNeu7R_V.wasm +0 -0
- package/dist/assets/ifc-lite_bg-DuxUZomW.wasm +0 -0
- package/dist/assets/parser.worker-BZZcO7DB.js +0 -182
- package/dist/assets/raw-DY7Y_acr.js +0 -1
- package/dist/assets/wasm-bridge-DMX8Acuf.js +0 -1
- package/dist/assets/workerHelpers-Crstj4Oa.js +0 -36
package/src/hooks/useIfcCache.ts
CHANGED
|
@@ -13,10 +13,11 @@ import { useCallback } from 'react';
|
|
|
13
13
|
import {
|
|
14
14
|
BinaryCacheWriter,
|
|
15
15
|
BinaryCacheReader,
|
|
16
|
+
type CachedEntityIndexColumns,
|
|
16
17
|
type IfcDataStore as CacheDataStore,
|
|
17
18
|
type GeometryData,
|
|
18
19
|
} from '@ifc-lite/cache';
|
|
19
|
-
import { SpatialHierarchyBuilder, StepTokenizer, CompactEntityIndexBuilder, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
20
|
+
import { SpatialHierarchyBuilder, StepTokenizer, CompactEntityIndex, CompactEntityIndexBuilder, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
|
|
20
21
|
import { buildSpatialIndexGuarded } from '../utils/loadingUtils.js';
|
|
21
22
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
22
23
|
|
|
@@ -30,6 +31,27 @@ import { calculateStoreyHeights } from '../utils/localParsingUtils.js';
|
|
|
30
31
|
export type { CacheResult } from '../services/cacheService.js';
|
|
31
32
|
export { getCached, setCached, deleteCached } from '../services/cacheService.js';
|
|
32
33
|
|
|
34
|
+
function buildEntityIndexFromCachedColumns(columns: CachedEntityIndexColumns): IfcDataStore['entityIndex'] {
|
|
35
|
+
const byId = new CompactEntityIndex(
|
|
36
|
+
columns.ids,
|
|
37
|
+
columns.byteOffsets,
|
|
38
|
+
columns.byteLengths,
|
|
39
|
+
columns.typeIndices,
|
|
40
|
+
columns.typeNames,
|
|
41
|
+
);
|
|
42
|
+
const byType = new Map<string, number[]>();
|
|
43
|
+
for (let i = 0; i < columns.ids.length; i++) {
|
|
44
|
+
const type = columns.typeNames[columns.typeIndices[i]];
|
|
45
|
+
let ids = byType.get(type);
|
|
46
|
+
if (!ids) {
|
|
47
|
+
ids = [];
|
|
48
|
+
byType.set(type, ids);
|
|
49
|
+
}
|
|
50
|
+
ids.push(columns.ids[i]);
|
|
51
|
+
}
|
|
52
|
+
return { byId, byType };
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
// ============================================================================
|
|
34
56
|
// Types
|
|
35
57
|
// ============================================================================
|
|
@@ -107,25 +129,28 @@ export function useIfcCache() {
|
|
|
107
129
|
if (cacheResult.sourceBuffer) {
|
|
108
130
|
dataStore.source = new Uint8Array(cacheResult.sourceBuffer);
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
typeList =
|
|
123
|
-
|
|
132
|
+
if (result.entityIndex) {
|
|
133
|
+
dataStore.entityIndex = buildEntityIndexFromCachedColumns(result.entityIndex);
|
|
134
|
+
} else {
|
|
135
|
+
// Backward compatibility for v3 caches: rebuild byte offsets from the
|
|
136
|
+
// source once, then future v4 writes persist this section.
|
|
137
|
+
const tokenizer = new StepTokenizer(dataStore.source);
|
|
138
|
+
const estimatedCount = dataStore.entities?.count ?? 100_000;
|
|
139
|
+
const indexBuilder = new CompactEntityIndexBuilder(estimatedCount);
|
|
140
|
+
const byType = new Map<string, number[]>();
|
|
141
|
+
|
|
142
|
+
for (const ref of tokenizer.scanEntitiesFast()) {
|
|
143
|
+
indexBuilder.add(ref.expressId, ref.type, ref.offset, ref.length);
|
|
144
|
+
let typeList = byType.get(ref.type);
|
|
145
|
+
if (!typeList) {
|
|
146
|
+
typeList = [];
|
|
147
|
+
byType.set(ref.type, typeList);
|
|
148
|
+
}
|
|
149
|
+
typeList.push(ref.expressId);
|
|
124
150
|
}
|
|
125
|
-
|
|
151
|
+
const compactByIdIndex = indexBuilder.build();
|
|
152
|
+
dataStore.entityIndex = { byId: compactByIdIndex, byType };
|
|
126
153
|
}
|
|
127
|
-
const compactByIdIndex = indexBuilder.build();
|
|
128
|
-
dataStore.entityIndex = { byId: compactByIdIndex, byType };
|
|
129
154
|
|
|
130
155
|
// Rebuild on-demand maps from relationships
|
|
131
156
|
// Pass entityIndex which contains ALL entity types including IfcPropertySet/IfcElementQuantity
|
|
@@ -254,6 +279,7 @@ export function useIfcCache() {
|
|
|
254
279
|
quantities: dataStore.quantities,
|
|
255
280
|
relationships: dataStore.relationships,
|
|
256
281
|
spatialHierarchy: dataStore.spatialHierarchy,
|
|
282
|
+
entityIndex: dataStore.entityIndex,
|
|
257
283
|
};
|
|
258
284
|
|
|
259
285
|
console.log('[useIfcCache] Writing cache buffer...');
|
|
@@ -1905,7 +1905,7 @@ export function useIfcLoader() {
|
|
|
1905
1905
|
// risking corruption.
|
|
1906
1906
|
const parserWasmApi = isNativeFileHandle(file) ? undefined : geometryProcessor.getApi();
|
|
1907
1907
|
return new IfcParser().parseColumnar(buffer, {
|
|
1908
|
-
wasmApi: parserWasmApi,
|
|
1908
|
+
wasmApi: parserWasmApi ?? undefined,
|
|
1909
1909
|
onSpatialReady: onPartialDataStore,
|
|
1910
1910
|
});
|
|
1911
1911
|
};
|
|
@@ -2032,34 +2032,12 @@ export function useIfcLoader() {
|
|
|
2032
2032
|
// When the parser worker is in use, hand the geometry workers the
|
|
2033
2033
|
// same SAB so we don't pay the file-bytes copy twice.
|
|
2034
2034
|
const geometryView = sharedSource ? new Uint8Array(sharedSource) : new Uint8Array(buffer);
|
|
2035
|
-
// Phase 2 of single-controller-rayon-design.md — opt-in via
|
|
2036
|
-
// localStorage so we can A/B compare against the N-worker
|
|
2037
|
-
// baseline without rolling out for everyone. Users (and the
|
|
2038
|
-
// benchmark harness) flip this with:
|
|
2039
|
-
// localStorage.setItem('ifc-lite:single-controller', '1')
|
|
2040
|
-
// and reload. Set to anything else (or unset) for the legacy
|
|
2041
|
-
// N-worker path. Safe: if the threaded WASM bundle fails to
|
|
2042
|
-
// load (no COI, Safari, etc.) the controller worker falls back
|
|
2043
|
-
// to per-task serial execution within the controller itself
|
|
2044
|
-
// (par_iter without an initialized pool).
|
|
2045
|
-
const useSingleController = (() => {
|
|
2046
|
-
try {
|
|
2047
|
-
return typeof localStorage !== 'undefined'
|
|
2048
|
-
&& localStorage.getItem('ifc-lite:single-controller') === '1';
|
|
2049
|
-
} catch {
|
|
2050
|
-
return false;
|
|
2051
|
-
}
|
|
2052
|
-
})();
|
|
2053
|
-
if (useSingleController) {
|
|
2054
|
-
console.log('[useIfc] single-controller path enabled (Phase 2)');
|
|
2055
|
-
}
|
|
2056
2035
|
const geometryEvents = shouldUseDesktopStableWasmGeometry
|
|
2057
2036
|
? geometryProcessor.processStreaming(geometryView, undefined, dynamicBatchConfig)
|
|
2058
2037
|
: geometryProcessor.processAdaptive(geometryView, {
|
|
2059
2038
|
sizeThreshold: 2 * 1024 * 1024, // 2MB threshold
|
|
2060
2039
|
batchSize: dynamicBatchConfig, // Dynamic batches: small first, then large
|
|
2061
2040
|
existingSab: sharedSource ?? undefined,
|
|
2062
|
-
useSingleController,
|
|
2063
2041
|
// Hand the streaming pre-pass's entity index to the parser
|
|
2064
2042
|
// worker so it skips a duplicate ~10 s WASM scan. Safe even
|
|
2065
2043
|
// when the parser falls back to main-thread (instance is
|
|
@@ -91,13 +91,31 @@ export interface AnnotationFill2D {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/** 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
|
+
*/
|
|
95
107
|
interface ParseResult {
|
|
108
|
+
// IfcAnnotation buckets
|
|
96
109
|
byStorey: Map<number, AnnotationsForStorey>;
|
|
97
|
-
/** Annotations with no resolvable storey — shown on every floor as a fallback. */
|
|
98
110
|
loose: DrawingLine2D[];
|
|
99
111
|
looseTexts: AnnotationText2D[];
|
|
100
112
|
looseFills: AnnotationFill2D[];
|
|
113
|
+
|
|
114
|
+
// IfcGridAxis buckets (issue #862)
|
|
115
|
+
gridByStorey: Map<number, AnnotationsForStorey>;
|
|
116
|
+
gridLoose: DrawingLine2D[];
|
|
117
|
+
gridLooseTexts: AnnotationText2D[];
|
|
118
|
+
gridLooseFills: AnnotationFill2D[];
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
const CIRCLE_SEGMENTS_FULL = 32;
|
|
@@ -212,6 +230,10 @@ async function parseAnnotations(
|
|
|
212
230
|
loose: [],
|
|
213
231
|
looseTexts: [],
|
|
214
232
|
looseFills: [],
|
|
233
|
+
gridByStorey: new Map(),
|
|
234
|
+
gridLoose: [],
|
|
235
|
+
gridLooseTexts: [],
|
|
236
|
+
gridLooseFills: [],
|
|
215
237
|
};
|
|
216
238
|
const source = store.source;
|
|
217
239
|
if (!source || source.byteLength === 0) {
|
|
@@ -256,6 +278,7 @@ async function parseAnnotations(
|
|
|
256
278
|
const ensureBucket = (
|
|
257
279
|
expressId: number,
|
|
258
280
|
primitiveWorldY: number,
|
|
281
|
+
ifcType: string,
|
|
259
282
|
): AnnotationsForStorey | null => {
|
|
260
283
|
let effectiveY: number | null = null;
|
|
261
284
|
if (Number.isFinite(primitiveWorldY) && primitiveWorldY !== 0) {
|
|
@@ -269,7 +292,11 @@ async function parseAnnotations(
|
|
|
269
292
|
}
|
|
270
293
|
if (effectiveY === null) return null;
|
|
271
294
|
const key = Math.round(effectiveY * 1000);
|
|
272
|
-
|
|
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);
|
|
273
300
|
if (!bucket) {
|
|
274
301
|
bucket = {
|
|
275
302
|
storeyId: key,
|
|
@@ -278,7 +305,7 @@ async function parseAnnotations(
|
|
|
278
305
|
texts: [],
|
|
279
306
|
fills: [],
|
|
280
307
|
};
|
|
281
|
-
|
|
308
|
+
storeyMap.set(key, bucket);
|
|
282
309
|
}
|
|
283
310
|
return bucket;
|
|
284
311
|
};
|
|
@@ -287,8 +314,9 @@ async function parseAnnotations(
|
|
|
287
314
|
const poly = collection.getPolyline(i);
|
|
288
315
|
if (!poly) continue;
|
|
289
316
|
if (poly.ifcType !== 'IfcAnnotation' && poly.ifcType !== 'IfcGridAxis') continue;
|
|
290
|
-
const bucket = ensureBucket(poly.expressId, poly.worldY);
|
|
291
|
-
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;
|
|
292
320
|
polylineToSegments(poly.points, poly.pointCount, poly.isClosed, out);
|
|
293
321
|
}
|
|
294
322
|
|
|
@@ -296,8 +324,9 @@ async function parseAnnotations(
|
|
|
296
324
|
const circle = collection.getCircle(i);
|
|
297
325
|
if (!circle) continue;
|
|
298
326
|
if (circle.ifcType !== 'IfcAnnotation' && circle.ifcType !== 'IfcGridAxis') continue;
|
|
299
|
-
const bucket = ensureBucket(circle.expressId, circle.worldY);
|
|
300
|
-
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;
|
|
301
330
|
circleToSegments(
|
|
302
331
|
circle.centerX,
|
|
303
332
|
circle.centerY,
|
|
@@ -335,7 +364,8 @@ async function parseAnnotations(
|
|
|
335
364
|
// Industry-standard line-spacing (CSS line-height ≈ 1.2). Picks up
|
|
336
365
|
// a little air between rows so descenders don't kiss the next cap.
|
|
337
366
|
const lineSpacing = perLineHeight * 1.2;
|
|
338
|
-
const bucket = ensureBucket(text.expressId, text.worldY);
|
|
367
|
+
const bucket = ensureBucket(text.expressId, text.worldY, text.ifcType);
|
|
368
|
+
const looseTextTarget = text.ifcType === 'IfcGridAxis' ? result.gridLooseTexts : result.looseTexts;
|
|
339
369
|
// All annotation text — grid bubbles, dimension callouts, leader labels —
|
|
340
370
|
// billboards to the camera so it stays legible in any view orientation
|
|
341
371
|
// (top-down, eye-level, oblique). The shader rebuilds the quad in the
|
|
@@ -366,7 +396,7 @@ async function parseAnnotations(
|
|
|
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
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
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);
|
|
552
621
|
}
|
|
553
622
|
|
|
554
|
-
|
|
555
|
-
|
|
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
|
/**
|
|
@@ -723,13 +812,19 @@ export function useSymbolicAnnotationsForDrawing(params: {
|
|
|
723
812
|
const cached = PARSE_CACHE.get(key);
|
|
724
813
|
if (!cached) continue;
|
|
725
814
|
|
|
726
|
-
|
|
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) => {
|
|
727
820
|
const bucketY = resolveBucketY(bucket.storeyElevation, fallbackY);
|
|
728
|
-
if (bucketY < rangeMin || bucketY > rangeMax)
|
|
821
|
+
if (bucketY < rangeMin || bucketY > rangeMax) return;
|
|
729
822
|
for (const ln of bucket.lines) pushLine(ln);
|
|
730
823
|
for (const t of bucket.texts) pushText(t);
|
|
731
824
|
for (const f of bucket.fills) pushFill(f);
|
|
732
|
-
}
|
|
825
|
+
};
|
|
826
|
+
for (const bucket of cached.byStorey.values()) collectBucket(bucket);
|
|
827
|
+
for (const bucket of cached.gridByStorey.values()) collectBucket(bucket);
|
|
733
828
|
|
|
734
829
|
// Loose annotations have no resolvable storey — include them if the
|
|
735
830
|
// fallback Y lands in the view range. That keeps malformed exports
|
|
@@ -739,6 +834,9 @@ export function useSymbolicAnnotationsForDrawing(params: {
|
|
|
739
834
|
for (const ln of cached.loose) pushLine(ln);
|
|
740
835
|
for (const t of cached.looseTexts) pushText(t);
|
|
741
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);
|
|
742
840
|
}
|
|
743
841
|
}
|
|
744
842
|
|
|
@@ -757,14 +855,24 @@ export function useSymbolicAnnotationsForDrawing(params: {
|
|
|
757
855
|
*/
|
|
758
856
|
export function useSymbolicAnnotationsRichData(params: {
|
|
759
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;
|
|
760
864
|
fallbackY?: number;
|
|
761
865
|
}): { texts: readonly AnnotationText3D[]; fills: readonly AnnotationFill3D[] } {
|
|
762
|
-
const { enabled, fallbackY = 0 } = params;
|
|
866
|
+
const { enabled, gridEnabled, gridSectionClip, fallbackY = 0 } = params;
|
|
867
|
+
const effectiveGridEnabled = gridEnabled ?? enabled;
|
|
763
868
|
const stores = useActiveStores();
|
|
764
|
-
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;
|
|
765
873
|
|
|
766
874
|
return useMemo(() => {
|
|
767
|
-
if (!enabled) return { texts: EMPTY_TEXTS, fills: EMPTY_FILLS };
|
|
875
|
+
if (!enabled && !effectiveGridEnabled) return { texts: EMPTY_TEXTS, fills: EMPTY_FILLS };
|
|
768
876
|
void version;
|
|
769
877
|
|
|
770
878
|
const texts: AnnotationText3D[] = [];
|
|
@@ -803,18 +911,45 @@ export function useSymbolicAnnotationsRichData(params: {
|
|
|
803
911
|
});
|
|
804
912
|
};
|
|
805
913
|
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
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
|
+
}
|
|
810
947
|
}
|
|
811
|
-
for (const t of cached.looseTexts) pushText(t, fallbackY);
|
|
812
|
-
for (const f of cached.looseFills) pushFill(f, fallbackY);
|
|
813
948
|
}
|
|
814
949
|
|
|
815
950
|
return {
|
|
816
951
|
texts: texts.length ? texts : EMPTY_TEXTS,
|
|
817
952
|
fills: fills.length ? fills : EMPTY_FILLS,
|
|
818
953
|
};
|
|
819
|
-
}, [enabled, stores, version, fallbackY]);
|
|
954
|
+
}, [enabled, effectiveGridEnabled, clipEnabled, clipPos, clipDepth, stores, version, fallbackY]);
|
|
820
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
|
});
|