PR #320 changed SiteNode.children from embedded BuildingNode/ItemNode objects to flat string[] IDs. PR #325 updated the runtime call sites but missed the three scene templates, which still mutated the site node's children array to embed full building objects after building the flat dict. This caused AnyNode.safeParse to fail for site_empty, site_2br, and site_garden in bun test --cwd packages/mcp. Remove the obsolete mutation blocks; each template already initialises site.children with the correct string id (e.g. ['building_empty']). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
280 lines
6.7 KiB
TypeScript
280 lines
6.7 KiB
TypeScript
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph'
|
||
import type { AnyNode, AnyNodeId } from '@pascal-app/core/schema'
|
||
|
||
/**
|
||
* "Garden house" — a simplified take on the Casa del Sol layout used in the
|
||
* MCP research fixtures.
|
||
*
|
||
* Footprint: 12 m × 8 m house centered at the origin, with a 12 m × 6 m
|
||
* back garden zone immediately to the north of the house, surrounded by a
|
||
* privacy fence on three sides.
|
||
*
|
||
* Contents:
|
||
* - 4 perimeter walls around the house
|
||
* - 1 front door (south wall), 1 large garden door (north wall)
|
||
* - 2 windows on the south wall, 1 window on each of east and west
|
||
* - 1 indoor "living" zone, 1 outdoor "garden" zone
|
||
* - 3 fence segments bounding the north/east/west of the garden
|
||
*/
|
||
|
||
const HOUSE_W = 6 // half-width of the house (12 m total)
|
||
const HOUSE_D = 4 // half-depth of the house (8 m total)
|
||
const GARDEN_DEPTH = 6 // depth of the back-garden zone along +z direction
|
||
|
||
const WALL_THICKNESS = 0.15
|
||
const WALL_HEIGHT = 2.7
|
||
|
||
type NodeMap = Record<string, AnyNode>
|
||
|
||
function wall(
|
||
id: string,
|
||
start: [number, number],
|
||
end: [number, number],
|
||
children: string[] = [],
|
||
): AnyNode {
|
||
return {
|
||
object: 'node',
|
||
id,
|
||
type: 'wall',
|
||
parentId: 'level_0',
|
||
visible: true,
|
||
metadata: {},
|
||
children,
|
||
thickness: WALL_THICKNESS,
|
||
height: WALL_HEIGHT,
|
||
start,
|
||
end,
|
||
frontSide: 'unknown',
|
||
backSide: 'unknown',
|
||
} as unknown as AnyNode
|
||
}
|
||
|
||
function door(id: string, parentWallId: string, width = 0.9): AnyNode {
|
||
return {
|
||
object: 'node',
|
||
id,
|
||
type: 'door',
|
||
parentId: parentWallId,
|
||
visible: true,
|
||
metadata: {},
|
||
wallId: parentWallId,
|
||
position: [0, 1.05, 0],
|
||
rotation: [0, 0, 0],
|
||
width,
|
||
height: 2.1,
|
||
frameThickness: 0.05,
|
||
frameDepth: 0.07,
|
||
threshold: true,
|
||
thresholdHeight: 0.02,
|
||
hingesSide: 'left',
|
||
swingDirection: 'inward',
|
||
segments: [
|
||
{
|
||
type: 'panel',
|
||
heightRatio: 0.5,
|
||
columnRatios: [1],
|
||
dividerThickness: 0.03,
|
||
panelDepth: 0.01,
|
||
panelInset: 0.04,
|
||
},
|
||
{
|
||
type: 'panel',
|
||
heightRatio: 0.5,
|
||
columnRatios: [1],
|
||
dividerThickness: 0.03,
|
||
panelDepth: 0.01,
|
||
panelInset: 0.04,
|
||
},
|
||
],
|
||
handle: true,
|
||
handleHeight: 1.05,
|
||
handleSide: 'right',
|
||
contentPadding: [0.04, 0.04],
|
||
doorCloser: false,
|
||
panicBar: false,
|
||
panicBarHeight: 1.0,
|
||
} as unknown as AnyNode
|
||
}
|
||
|
||
function makeWindow(id: string, parentWallId: string, width = 1.2): AnyNode {
|
||
return {
|
||
object: 'node',
|
||
id,
|
||
type: 'window',
|
||
parentId: parentWallId,
|
||
visible: true,
|
||
metadata: {},
|
||
wallId: parentWallId,
|
||
position: [0, 1.2, 0],
|
||
rotation: [0, 0, 0],
|
||
width,
|
||
height: 1.2,
|
||
frameThickness: 0.05,
|
||
frameDepth: 0.07,
|
||
columnRatios: [1],
|
||
rowRatios: [1],
|
||
columnDividerThickness: 0.03,
|
||
rowDividerThickness: 0.03,
|
||
sill: true,
|
||
sillDepth: 0.08,
|
||
sillThickness: 0.03,
|
||
} as unknown as AnyNode
|
||
}
|
||
|
||
function fence(id: string, start: [number, number], end: [number, number]): AnyNode {
|
||
return {
|
||
object: 'node',
|
||
id,
|
||
type: 'fence',
|
||
parentId: 'level_0',
|
||
visible: true,
|
||
metadata: {},
|
||
start,
|
||
end,
|
||
height: 1.8,
|
||
thickness: 0.08,
|
||
baseHeight: 0.22,
|
||
postSpacing: 2,
|
||
postSize: 0.1,
|
||
topRailHeight: 0.04,
|
||
groundClearance: 0,
|
||
edgeInset: 0.015,
|
||
baseStyle: 'grounded',
|
||
color: '#f3f4f6',
|
||
style: 'privacy',
|
||
} as unknown as AnyNode
|
||
}
|
||
|
||
function buildTemplate(): SceneGraph {
|
||
const nodes: NodeMap = {}
|
||
|
||
nodes.site_garden = {
|
||
object: 'node',
|
||
id: 'site_garden',
|
||
type: 'site',
|
||
parentId: null,
|
||
visible: true,
|
||
metadata: {},
|
||
polygon: {
|
||
type: 'polygon',
|
||
points: [
|
||
[-15, -15],
|
||
[15, -15],
|
||
[15, 15],
|
||
[-15, 15],
|
||
],
|
||
},
|
||
children: ['building_garden'],
|
||
} as unknown as AnyNode
|
||
|
||
nodes.building_garden = {
|
||
object: 'node',
|
||
id: 'building_garden',
|
||
type: 'building',
|
||
parentId: 'site_garden',
|
||
visible: true,
|
||
metadata: {},
|
||
position: [0, 0, 0],
|
||
rotation: [0, 0, 0],
|
||
children: ['level_0'],
|
||
} as unknown as AnyNode
|
||
|
||
// Openings
|
||
nodes.door_front = door('door_front', 'wall_s', 1.0)
|
||
nodes.door_garden = door('door_garden', 'wall_n', 1.6)
|
||
nodes.window_s1 = makeWindow('window_s1', 'wall_s', 1.2)
|
||
nodes.window_s2 = makeWindow('window_s2', 'wall_s', 1.2)
|
||
nodes.window_e = makeWindow('window_e', 'wall_e', 1.0)
|
||
nodes.window_w = makeWindow('window_w', 'wall_w', 1.0)
|
||
|
||
// House perimeter (south is front, north opens to the garden)
|
||
nodes.wall_n = wall('wall_n', [-HOUSE_W, -HOUSE_D], [HOUSE_W, -HOUSE_D], ['door_garden'])
|
||
nodes.wall_e = wall('wall_e', [HOUSE_W, -HOUSE_D], [HOUSE_W, HOUSE_D], ['window_e'])
|
||
nodes.wall_s = wall(
|
||
'wall_s',
|
||
[HOUSE_W, HOUSE_D],
|
||
[-HOUSE_W, HOUSE_D],
|
||
['door_front', 'window_s1', 'window_s2'],
|
||
)
|
||
nodes.wall_w = wall('wall_w', [-HOUSE_W, HOUSE_D], [-HOUSE_W, -HOUSE_D], ['window_w'])
|
||
|
||
// Zones
|
||
nodes.zone_living = {
|
||
object: 'node',
|
||
id: 'zone_living',
|
||
type: 'zone',
|
||
parentId: 'level_0',
|
||
visible: true,
|
||
metadata: {},
|
||
name: 'Living',
|
||
color: '#60a5fa',
|
||
polygon: [
|
||
[-HOUSE_W, -HOUSE_D],
|
||
[HOUSE_W, -HOUSE_D],
|
||
[HOUSE_W, HOUSE_D],
|
||
[-HOUSE_W, HOUSE_D],
|
||
],
|
||
} as unknown as AnyNode
|
||
|
||
nodes.zone_garden = {
|
||
object: 'node',
|
||
id: 'zone_garden',
|
||
type: 'zone',
|
||
parentId: 'level_0',
|
||
visible: true,
|
||
metadata: {},
|
||
name: 'Back garden',
|
||
color: '#86efac',
|
||
polygon: [
|
||
[-HOUSE_W, -HOUSE_D - GARDEN_DEPTH],
|
||
[HOUSE_W, -HOUSE_D - GARDEN_DEPTH],
|
||
[HOUSE_W, -HOUSE_D],
|
||
[-HOUSE_W, -HOUSE_D],
|
||
],
|
||
} as unknown as AnyNode
|
||
|
||
// Privacy fence along 3 sides of the garden.
|
||
nodes.fence_n = fence(
|
||
'fence_n',
|
||
[-HOUSE_W, -HOUSE_D - GARDEN_DEPTH],
|
||
[HOUSE_W, -HOUSE_D - GARDEN_DEPTH],
|
||
)
|
||
nodes.fence_e = fence('fence_e', [HOUSE_W, -HOUSE_D - GARDEN_DEPTH], [HOUSE_W, -HOUSE_D])
|
||
nodes.fence_w = fence('fence_w', [-HOUSE_W, -HOUSE_D], [-HOUSE_W, -HOUSE_D - GARDEN_DEPTH])
|
||
|
||
nodes.level_0 = {
|
||
object: 'node',
|
||
id: 'level_0',
|
||
type: 'level',
|
||
parentId: 'building_garden',
|
||
visible: true,
|
||
metadata: {},
|
||
level: 0,
|
||
children: [
|
||
'wall_n',
|
||
'wall_e',
|
||
'wall_s',
|
||
'wall_w',
|
||
'zone_living',
|
||
'zone_garden',
|
||
'fence_n',
|
||
'fence_e',
|
||
'fence_w',
|
||
],
|
||
} as unknown as AnyNode
|
||
|
||
return {
|
||
nodes: nodes as Record<AnyNodeId, AnyNode>,
|
||
rootNodeIds: ['site_garden'] as AnyNodeId[],
|
||
}
|
||
}
|
||
|
||
export const template: SceneGraph = buildTemplate()
|
||
|
||
export const metadata = {
|
||
id: 'garden-house',
|
||
name: 'Garden house',
|
||
description:
|
||
'12 × 8 m single-level house with a fenced back-garden zone; 4 walls, 2 doors, 4 windows, 3 privacy fences.',
|
||
} as const
|