fix(export): stamp openable only when an open clip bakes

A door/window only carries extras.openable + extras.clips when an open
animation actually bakes. Cased openings (no leaf) and fixed windows (no
operable sash) are no longer mislabelled openable, so the GLB never claims
a part opens when nothing moves. /viewer is unaffected (it already required
openable && clips). Adds a regression test for the no-clip case.

(Export barrels reordered by the formatter.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-23 15:44:36 -04:00
co-authored by Claude Opus 4.8
parent 0629c7d3e3
commit 6cfc6ae01b
4 changed files with 41 additions and 4 deletions
+1 -1
View File
@@ -218,7 +218,6 @@ export {
resolveCeilingPlanPointSnap,
} from './lib/ceiling-plan-snap'
export { EDITOR_LAYER } from './lib/constants'
export { exportSceneToGlb } from './lib/glb-export'
// Helper libs used by the kind-owned roof / stair / elevator panels.
export {
resolveCurrentBuildingId,
@@ -247,6 +246,7 @@ export {
getFloorplanWallThickness,
} from './lib/floorplan'
export { commitFreshPlacementSubtree } from './lib/fresh-planar-placement'
export { exportSceneToGlb } from './lib/glb-export'
export {
buildResetSurfaceMaterialUpdates,
buildRoofSurfaceMaterialPatch,
@@ -146,6 +146,38 @@ describe('prepareSceneForExport', () => {
expect(leafMarkerSurvived).toBe(false)
})
test('does not flag a door/window openable when no open clip bakes', () => {
// A cased opening (no swing leaf) / fixed window (no operable sash) builds
// no movable part, so no clip bakes and the node must not claim openable.
const root = new THREE.Group()
const openingGroup = new THREE.Group()
openingGroup.add(meshWithNodeMaterial(nodeMaterial()))
root.add(openingGroup)
const openingId = 'door_opening'
sceneRegistry.nodes.set(openingId, openingGroup)
const nodes: Record<string, AnyNode> = {
[openingId]: {
object: 'node',
id: openingId,
type: 'door',
name: 'Cased opening',
} as unknown as AnyNode,
}
const { scene, animations } = prepareSceneForExport(root, nodes)
expect(animations).toHaveLength(0)
const exported = scene.getObjectByProperty('name', openingId)
expect(exported?.userData).toEqual({
pascalId: openingId,
kind: 'door',
label: 'Cased opening',
})
expect(exported?.userData.openable).toBeUndefined()
expect(exported?.userData.clips).toBeUndefined()
})
test('keeps the zone identity node with its polygon and strips the fill mesh', () => {
const root = new THREE.Group()
const zoneGroup = new THREE.Group()
+7 -2
View File
@@ -497,10 +497,15 @@ function stampIdentity(
extras.label = getLevelDisplayName(node as LevelNode)
target.visible = true
}
// Only doors/windows that actually baked an open clip are openable. A cased
// opening (no leaf) or a fixed window (no operable sash) produces no clip, so
// it stays unflagged — the file never claims a part opens when nothing moves.
if (node.type === 'door' || node.type === 'window') {
extras.openable = true
const clipNames = clipNamesByNode.get(id)
if (clipNames) extras.clips = clipNames
if (clipNames?.length) {
extras.openable = true
extras.clips = clipNames
}
}
if (node.type === 'zone') {
// Zone fills are stripped from the bake; /viewer rebuilds the room from
+1 -1
View File
@@ -14,10 +14,10 @@ export { ErrorBoundary } from './components/error-boundary'
export { NodeRenderer } from './components/renderers/node-renderer'
export { default as Viewer, type ViewerHandle } from './components/viewer'
export {
GlbScene,
type GlbHover,
type GlbIdentity,
type GlbLevel,
GlbScene,
} from './components/viewer/glb-scene'
export type { HoverStyle, HoverStyles } from './components/viewer/post-processing'
export {