diff --git a/packages/core/src/lib/opening-casing.test.ts b/packages/core/src/lib/opening-casing.test.ts index 1a38dc8d77..2d45f260d4 100644 --- a/packages/core/src/lib/opening-casing.test.ts +++ b/packages/core/src/lib/opening-casing.test.ts @@ -6,6 +6,7 @@ import { type OpeningCasingBoard, openingCasingBoards, openingCasingFaces, + openingCasingWidth, resolveOpeningCasingHost, } from './opening-casing' @@ -55,10 +56,12 @@ describe('opening casing schema', () => { WindowNode.parse({ id: 'window_old', width: 1.2, height: 1 }), DoorNode.parse({ id: 'door_old', width: 0.9, height: 2.1 }), ]) { - expect(legacy.casingFaceA).toBe(false) - expect(legacy.casingFaceB).toBe(false) + // Loading adds no casing fields: absent faces are uncased. + expect('casingFaceA' in legacy || 'casingFaceB' in legacy || 'casingWidth' in legacy).toBe( + false, + ) expect(openingCasingBoards(legacy, host)).toEqual([]) - expect(legacy.casingWidth).toBeCloseTo(2 * INCH, 12) + expect(openingCasingWidth(legacy)).toBeCloseTo(2 * INCH, 12) } expect(DEFAULT_OPENING_CASING_WIDTH).toBeCloseTo(2 * INCH, 12) expect(OPENING_CASING_PROUD).toBeCloseTo(0.75 * INCH, 12) @@ -67,7 +70,7 @@ describe('opening casing schema', () => { test('a casing face turned off stays off', () => { const parsed = WindowNode.parse({ id: 'window_off', casingFaceA: false, casingWidth: 0.09 }) expect(parsed.casingFaceA).toBe(false) - expect(parsed.casingFaceB).toBe(false) + expect(parsed.casingFaceB).toBeUndefined() expect(parsed.casingWidth).toBe(0.09) }) }) diff --git a/packages/core/src/schema/host-children.test.ts b/packages/core/src/schema/host-children.test.ts index 8326ee75cc..304b226cba 100644 --- a/packages/core/src/schema/host-children.test.ts +++ b/packages/core/src/schema/host-children.test.ts @@ -57,10 +57,6 @@ test.each(baseline)( ? { ...expected, floorThresholdVersion: 1, - // Saved before casing: no casing. - casingFaceA: false, - casingFaceB: false, - casingWidth: 0.0508, } : saved.type === 'fence' ? { diff --git a/packages/core/src/schema/nodes/door.ts b/packages/core/src/schema/nodes/door.ts index 915e34f57c..62fa97a950 100644 --- a/packages/core/src/schema/nodes/door.ts +++ b/packages/core/src/schema/nodes/door.ts @@ -3,12 +3,7 @@ import { z } from 'zod' import { BaseNode, nodeType, objectId } from '../base' import { GeometryScriptSource } from '../geometry-source' import { MaterialSchema } from '../material' -import { - DEFAULT_DOOR_CONTENT_PADDING, - DEFAULT_OPENING_CASING_WIDTH, - DoorType, - defaultDoorSegments, -} from './opening-types' +import { DEFAULT_DOOR_CONTENT_PADDING, DoorType, defaultDoorSegments } from './opening-types' export { DoorType } from './opening-types' @@ -144,10 +139,11 @@ export const DoorNode = BaseNode.extend({ // opening (head and both legs, none at the floor), never inside it, so the // wall cutout stays width × height. The faces are the host's `a` / `b` // (WallFace), not the door's front and back, so flipping the swing leaves - // the casing where it is. Absent casing fields keep saved openings uncased. - casingFaceA: z.boolean().default(false), - casingFaceB: z.boolean().default(false), - casingWidth: z.number().positive().default(DEFAULT_OPENING_CASING_WIDTH), + // the casing where it is. Optional, never defaulted, so loading a saved + // opening adds no fields: absent faces are uncased, an absent width is 2 in. + casingFaceA: z.boolean().optional(), + casingFaceB: z.boolean().optional(), + casingWidth: z.number().positive().optional(), // Swing hingesSide: z.enum(['left', 'right']).default('left'), diff --git a/packages/core/src/schema/nodes/window.ts b/packages/core/src/schema/nodes/window.ts index 8d56f0e6b9..c6160fed8c 100644 --- a/packages/core/src/schema/nodes/window.ts +++ b/packages/core/src/schema/nodes/window.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import { BaseNode, nodeType, objectId } from '../base' import { GeometryScriptSource } from '../geometry-source' import { MaterialSchema } from '../material' -import { DEFAULT_OPENING_CASING_WIDTH, WindowType } from './opening-types' +import { WindowType } from './opening-types' export { WindowType } from './opening-types' @@ -122,11 +122,12 @@ export const WindowNode = BaseNode.extend({ // Casing: flat boards on the finished faces of the host wall around the // opening, never inside it, so the wall cutout stays width × height. The // faces are the host's `a` / `b` (WallFace), not the unit's own front and - // back, so flipping the window leaves its casing where it is. Absent casing fields keep - // saved openings uncased. - casingFaceA: z.boolean().default(false), - casingFaceB: z.boolean().default(false), - casingWidth: z.number().positive().default(DEFAULT_OPENING_CASING_WIDTH), + // back, so flipping the window leaves its casing where it is. Optional, never + // defaulted, so loading a saved opening adds no fields: absent faces are + // uncased, an absent width is 2 in. + casingFaceA: z.boolean().optional(), + casingFaceB: z.boolean().optional(), + casingWidth: z.number().positive().optional(), }).describe(dedent`Window node - a parametric window placed on a wall - position: center of the window in wall-local coordinate system - width/height: overall outer dimensions, which are also the wall cutout diff --git a/packages/core/src/store/use-scene-window-migration.test.ts b/packages/core/src/store/use-scene-window-migration.test.ts index 7d957c7250..169305717f 100644 --- a/packages/core/src/store/use-scene-window-migration.test.ts +++ b/packages/core/src/store/use-scene-window-migration.test.ts @@ -83,8 +83,8 @@ describe('scene window migrations', () => { expect(window.columnRatios).toEqual([1]) expect(window.rowRatios).toEqual([1]) expect(window.frameThickness).toBe(0.05) - expect(window.casingFaceA).toBe(false) - expect(window.casingFaceB).toBe(false) + // Casing has no default: a window saved before it stays uncased, fieldless. + expect('casingFaceA' in window).toBe(false) expect(window.sill).toBe(true) // …and authored fields survive. expect(window.width).toBe(1.2) diff --git a/packages/core/src/systems/wall/wall-merge.test.ts b/packages/core/src/systems/wall/wall-merge.test.ts index 9edb65d1da..58e4c3810c 100644 --- a/packages/core/src/systems/wall/wall-merge.test.ts +++ b/packages/core/src/systems/wall/wall-merge.test.ts @@ -228,7 +228,7 @@ describe('explicit wall merge through the store', () => { const moved = nodes[absorbedWindow.id] as WindowNode expect(moved.position[0]).toBeCloseTo(expectedX) expect(moved.side).toBe('back') - expect([moved.casingFaceA, moved.casingFaceB]).toEqual([false, false]) + expect([moved.casingFaceA, moved.casingFaceB]).toEqual([undefined, undefined]) // A door on the reversed wall turns with its frame — depth and yaw — while // its hinges and handle, hung off that yaw, keep their stored side. const door = nodes[absorbedDoor.id] as DoorNode diff --git a/packages/nodes/src/door/floorplan-move.ts b/packages/nodes/src/door/floorplan-move.ts index 5ffb4b48aa..26a93c793a 100644 --- a/packages/nodes/src/door/floorplan-move.ts +++ b/packages/nodes/src/door/floorplan-move.ts @@ -84,8 +84,8 @@ export const doorFloorplanMoveTarget: FloorplanMoveTarget = ({ node }) roofSegmentId: undefined roofFace: undefined visible: true - casingFaceA: boolean - casingFaceB: boolean + casingFaceA?: boolean + casingFaceB?: boolean } | null = null // R flips the door's facing (front ↔ back) mid-placement. `apply` re-derives diff --git a/packages/nodes/src/shared/opening-casing.test.ts b/packages/nodes/src/shared/opening-casing.test.ts index ea3a6db467..497f33e06d 100644 --- a/packages/nodes/src/shared/opening-casing.test.ts +++ b/packages/nodes/src/shared/opening-casing.test.ts @@ -6,6 +6,7 @@ import { type GeometryContext, LevelNode, OPENING_CASING_PROUD, + openingCasingWidth, sceneRegistry, slotPaintMaterial, useScene, @@ -224,7 +225,7 @@ test('casing paint targets each host side, inherits the frame, and previews and part.geometry.computeBoundingBox() const local = part.geometry.boundingBox!.getCenter(new Vector3()) if (part.geometry.type === 'ExtrudeGeometry') { - local.x = node.width / 2 + node.casingWidth / 2 + local.x = node.width / 2 + openingCasingWidth(node) / 2 local.y = 0 } const center = local.applyMatrix4(mesh.matrixWorld) diff --git a/packages/nodes/src/window/floorplan-move.ts b/packages/nodes/src/window/floorplan-move.ts index ecc604157e..fdffd24508 100644 --- a/packages/nodes/src/window/floorplan-move.ts +++ b/packages/nodes/src/window/floorplan-move.ts @@ -87,8 +87,8 @@ export const windowFloorplanMoveTarget: FloorplanMoveTarget = ({ nod roofSegmentId: undefined roofFace: undefined visible: true - casingFaceA: boolean - casingFaceB: boolean + casingFaceA?: boolean + casingFaceB?: boolean } | null = null // R flips the window's facing (front ↔ back) mid-placement — see