Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 6 additions & 34 deletions packages/mcp/src/lib/rehydrate-site-children.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph'
import type { AnyNode } from '@pascal-app/core/schema'

/**
* `cloneSceneGraph` normalises `SiteNode.children` to an array of node IDs,
* but core's `SiteNode` schema expects an array of embedded `BuildingNode` /
* `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the
* cloned graph validating against `AnyNode`, re-embed the site children from
* the flat dict.
*
* Pure: returns a new graph without mutating the input.
* Previously re-embedded `SiteNode.children` from flat IDs back to full node
* objects to match the old schema. Since `SiteNode.children` is now
* `string[]` (upstream change), `cloneSceneGraph` / `forkSceneGraph` already
* produce the correct form — this function is a no-op kept for call-site
* compatibility.
*/
export function rehydrateSiteChildren(graph: SceneGraph): SceneGraph {
const out: SceneGraph = {
nodes: { ...graph.nodes },
rootNodeIds: [...graph.rootNodeIds],
...(graph.collections ? { collections: graph.collections } : {}),
}
for (const [id, node] of Object.entries(out.nodes)) {
if (node.type !== 'site') continue
const childrenField = (node as { children?: unknown[] }).children
if (!Array.isArray(childrenField)) continue
const rehydrated: AnyNode[] = []
for (const child of childrenField) {
if (typeof child === 'string') {
const target = out.nodes[child as keyof typeof out.nodes]
if (target && (target.type === 'building' || target.type === 'item')) {
rehydrated.push(target)
}
} else if (child && typeof child === 'object' && 'id' in (child as Record<string, unknown>)) {
rehydrated.push(child as AnyNode)
}
}
out.nodes[id as keyof typeof out.nodes] = {
...(node as AnyNode),
children: rehydrated,
} as AnyNode
}
return out
return graph
}
41 changes: 1 addition & 40 deletions packages/mcp/src/tools/variants/generate-variants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { forkSceneGraph, type SceneGraph } from '@pascal-app/core/clone-scene-graph'
import { type AnyNode, AnyNode as AnyNodeSchema } from '@pascal-app/core/schema'
import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema'
import { z } from 'zod'
import type { SceneOperations } from '../../operations'
import { ErrorCode, throwMcpError } from '../errors'
Expand Down Expand Up @@ -43,43 +43,6 @@ export const generateVariantsOutput = {
),
}

/**
* `forkSceneGraph` normalises `SiteNode.children` to string IDs, but the
* `SiteNode` schema declares that field as an array of full `BuildingNode` /
* `ItemNode` objects (see CROSS_CUTTING §2). To keep variants validating
* against `AnyNode`, re-embed the site children from the flat dict.
*
* Pure: returns a new graph without mutating the input.
*/
function rehydrateSiteChildren(graph: SceneGraph): SceneGraph {
const out: SceneGraph = {
nodes: { ...graph.nodes },
rootNodeIds: [...graph.rootNodeIds],
...(graph.collections ? { collections: graph.collections } : {}),
}
for (const [id, node] of Object.entries(out.nodes)) {
if (node.type !== 'site') continue
const childrenField = (node as { children?: unknown[] }).children
if (!Array.isArray(childrenField)) continue
const rehydrated: AnyNode[] = []
for (const child of childrenField) {
if (typeof child === 'string') {
const target = out.nodes[child as keyof typeof out.nodes]
if (target && (target.type === 'building' || target.type === 'item')) {
rehydrated.push(target)
}
} else if (child && typeof child === 'object' && 'id' in (child as Record<string, unknown>)) {
rehydrated.push(child as AnyNode)
}
}
out.nodes[id as keyof typeof out.nodes] = {
...(node as AnyNode),
children: rehydrated,
} as AnyNode
}
return out
}

/**
* Count how many nodes in a graph fail `AnyNode` validation. Used to keep the
* tool from returning silently corrupt variants.
Expand Down Expand Up @@ -140,8 +103,6 @@ export function registerGenerateVariants(server: McpServer, bridge: SceneOperati
for (const kind of mutations) {
forked = applyMutation(forked, rng, kind)
}
// Re-embed site children so variants match the SiteNode schema.
forked = rehydrateSiteChildren(forked)

const invalidCount = countInvalidNodes(forked)
if (invalidCount > 0) {
Expand Down
4 changes: 2 additions & 2 deletions packages/nodes/src/site/renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core'
import { type AnyNodeId, type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core'
import { NodeRenderer, unionPolygons, useNodeEvents, useViewer } from '@pascal-app/viewer'
import { useMemo, useRef } from 'react'
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
Expand Down Expand Up @@ -117,7 +117,7 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
<group ref={ref} {...handlers}>
{/* Render children (buildings and items) */}
{node.children.map((childId) => (
<NodeRenderer key={childId} nodeId={childId} />
<NodeRenderer key={childId} nodeId={childId as AnyNodeId} />
))}

{/* Ground fill: site polygon with slab holes, occludes below-grade geometry */}
Expand Down
Loading