diff --git a/apps/viewer/src/components/viewer/GLBExportDialog.tsx b/apps/viewer/src/components/viewer/GLBExportDialog.tsx index 016c0e8c1..e0be8616f 100644 --- a/apps/viewer/src/components/viewer/GLBExportDialog.tsx +++ b/apps/viewer/src/components/viewer/GLBExportDialog.tsx @@ -56,7 +56,7 @@ type ColorSource = 'rendering' | 'shading'; * matches what the user sees in the viewport. */ function buildHiddenIfcTypes( - typeVisibility: { spaces: boolean; spatialZones: boolean; openings: boolean; virtualElements: boolean; site: boolean }, + typeVisibility: { spaces: boolean; spatialZones: boolean; openings: boolean; virtualElements: boolean; site: boolean; ifcAnnotations: boolean }, ): Set { const out = new Set(); if (!typeVisibility.spaces) out.add('IfcSpace'); @@ -64,6 +64,7 @@ function buildHiddenIfcTypes( if (!typeVisibility.openings) out.add('IfcOpeningElement'); if (!typeVisibility.virtualElements) out.add('IfcVirtualElement'); if (!typeVisibility.site) out.add('IfcSite'); + if (!typeVisibility.ifcAnnotations) out.add('IfcAnnotation'); return out; } diff --git a/apps/viewer/src/components/viewer/ViewportContainer.tsx b/apps/viewer/src/components/viewer/ViewportContainer.tsx index bbf22f962..a61a81442 100644 --- a/apps/viewer/src/components/viewer/ViewportContainer.tsx +++ b/apps/viewer/src/components/viewer/ViewportContainer.tsx @@ -699,6 +699,7 @@ export function ViewportContainer() { prevVis.openings !== typeVisibility.openings || prevVis.virtualElements !== typeVisibility.virtualElements || prevVis.site !== typeVisibility.site || + prevVis.ifcAnnotations !== typeVisibility.ifcAnnotations || filteredTypeModeRef.current !== effectiveViewMode; const sourceChanged = filteredSourceRef.current !== allMeshes; if (typeVisChanged || sourceChanged || allMeshes.length < filteredSourceLenRef.current) { @@ -709,7 +710,7 @@ export function ViewportContainer() { filteredTypeModeRef.current = effectiveViewMode; } - const needsFilter = !typeVisibility.spaces || !typeVisibility.spatialZones || !typeVisibility.openings || !typeVisibility.virtualElements || !typeVisibility.site; + const needsFilter = !typeVisibility.spaces || !typeVisibility.spatialZones || !typeVisibility.openings || !typeVisibility.virtualElements || !typeVisibility.site || !typeVisibility.ifcAnnotations; const prevCacheLen = cache.length; // Only process NEW meshes since last run — O(batch_size) not O(total) @@ -739,6 +740,12 @@ export function ViewportContainer() { if (ifcType === 'IfcOpeningElement' && !typeVisibility.openings) continue; if (ifcType === 'IfcVirtualElement' && !typeVisibility.virtualElements) continue; if (ifcType === 'IfcSite' && !typeVisibility.site) continue; + // IfcAnnotation can carry real 3D solid geometry (e.g. Bonsai + // plan-view "DRAWING" boxes) on top of the 2D symbolic curve overlay. + // The `ifcAnnotations` toggle drives the curve overlay (Viewport.tsx); + // honour it here too so the toggle also hides those 3D meshes instead + // of leaving them rendered as stray cubes (issue #1354). + if (ifcType === 'IfcAnnotation' && !typeVisibility.ifcAnnotations) continue; } // Mesh alpha flows through unchanged. The previous code re-multiplied diff --git a/apps/viewer/src/store/basketVisibleSet.test.ts b/apps/viewer/src/store/basketVisibleSet.test.ts index bc5c56111..ce14cf02b 100644 --- a/apps/viewer/src/store/basketVisibleSet.test.ts +++ b/apps/viewer/src/store/basketVisibleSet.test.ts @@ -255,6 +255,64 @@ describe('basketVisibleSet', () => { }); }); + describe('type visibility: IfcAnnotation (issue #1354)', () => { + const meshes = [ + { expressId: 1, ifcType: 'IfcWall' }, + { expressId: 2, ifcType: 'IfcAnnotation' }, + ]; + + it('includes IfcAnnotation 3D meshes when the toggle is on', () => { + useViewerStore.setState({ + selectedEntitiesSet: new Set(), + selectedEntity: null, + selectedEntityIds: new Set(), + hierarchyBasketSelection: new Set(), + geometryResult: { meshes } as any, + typeVisibility: { ...useViewerStore.getState().typeVisibility, ifcAnnotations: true }, + }); + invalidateVisibleBasketCache(); + + const refs = getVisibleBasketEntityRefsFromStore(); + assert.ok(refs.some((r) => entityRefToString(r) === 'legacy:2')); + }); + + it('drops IfcAnnotation 3D meshes when the toggle is off', () => { + useViewerStore.setState({ + selectedEntitiesSet: new Set(), + selectedEntity: null, + selectedEntityIds: new Set(), + hierarchyBasketSelection: new Set(), + geometryResult: { meshes } as any, + typeVisibility: { ...useViewerStore.getState().typeVisibility, ifcAnnotations: false }, + }); + invalidateVisibleBasketCache(); + + const refs = getVisibleBasketEntityRefsFromStore(); + assert.ok(refs.some((r) => entityRefToString(r) === 'legacy:1')); + assert.ok(!refs.some((r) => entityRefToString(r) === 'legacy:2')); + }); + + it('drops IfcAnnotation 3D meshes on the models (federated) path too', () => { + // The gate also runs through `state.models` in collectVisibleCandidates, + // not just the legacy `state.geometryResult` fallback. Lock both paths. + const model = { visible: true, idOffset: 0, geometryResult: { meshes } } as any; + useViewerStore.setState({ + selectedEntitiesSet: new Set(), + selectedEntity: null, + selectedEntityIds: new Set(), + hierarchyBasketSelection: new Set(), + geometryResult: null, + models: new Map([['m1', model]]), + typeVisibility: { ...useViewerStore.getState().typeVisibility, ifcAnnotations: false }, + }); + invalidateVisibleBasketCache(); + + const refs = getVisibleBasketEntityRefsFromStore(); + assert.ok(refs.some((r) => r.expressId === 1)); + assert.ok(!refs.some((r) => r.expressId === 2)); + }); + }); + describe('federation: unresolved globalId in multi-model', () => { it('getBasketSelectionRefsFromStore returns array when models exist', () => { useViewerStore.setState({ diff --git a/apps/viewer/src/store/basketVisibleSet.ts b/apps/viewer/src/store/basketVisibleSet.ts index ae08fff8d..22fc30234 100644 --- a/apps/viewer/src/store/basketVisibleSet.ts +++ b/apps/viewer/src/store/basketVisibleSet.ts @@ -113,6 +113,9 @@ function matchesTypeVisibility(ifcType: string | undefined, typeVisibility: View if (ifcType === 'IfcOpeningElement' && !typeVisibility.openings) return false; if (ifcType === 'IfcVirtualElement' && !typeVisibility.virtualElements) return false; if (ifcType === 'IfcSite' && !typeVisibility.site) return false; + // IfcAnnotation 3D mesh geometry (e.g. Bonsai plan-view boxes) tracks the + // same toggle that hides the 2D symbolic curve overlay (issue #1354). + if (ifcType === 'IfcAnnotation' && !typeVisibility.ifcAnnotations) return false; return true; }