Merge pull request #325 from jelharou/fix/site-node-children-string-ids
fix: update mcp and nodes for SiteNode.children string[] schema change
This commit is contained in:
@@ -1,40 +1,12 @@
|
|||||||
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph'
|
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,
|
* Previously re-embedded `SiteNode.children` from flat IDs back to full node
|
||||||
* but core's `SiteNode` schema expects an array of embedded `BuildingNode` /
|
* objects to match the old schema. Since `SiteNode.children` is now
|
||||||
* `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the
|
* `string[]` (upstream change), `cloneSceneGraph` / `forkSceneGraph` already
|
||||||
* cloned graph validating against `AnyNode`, re-embed the site children from
|
* produce the correct form — this function is a no-op kept for call-site
|
||||||
* the flat dict.
|
* compatibility.
|
||||||
*
|
|
||||||
* Pure: returns a new graph without mutating the input.
|
|
||||||
*/
|
*/
|
||||||
export function rehydrateSiteChildren(graph: SceneGraph): SceneGraph {
|
export function rehydrateSiteChildren(graph: SceneGraph): SceneGraph {
|
||||||
const out: SceneGraph = {
|
return graph
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
import { forkSceneGraph, type SceneGraph } from '@pascal-app/core/clone-scene-graph'
|
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 { z } from 'zod'
|
||||||
import type { SceneOperations } from '../../operations'
|
import type { SceneOperations } from '../../operations'
|
||||||
import { ErrorCode, throwMcpError } from '../errors'
|
import { ErrorCode, throwMcpError } from '../errors'
|
||||||
@@ -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
|
* Count how many nodes in a graph fail `AnyNode` validation. Used to keep the
|
||||||
* tool from returning silently corrupt variants.
|
* tool from returning silently corrupt variants.
|
||||||
@@ -140,8 +103,6 @@ export function registerGenerateVariants(server: McpServer, bridge: SceneOperati
|
|||||||
for (const kind of mutations) {
|
for (const kind of mutations) {
|
||||||
forked = applyMutation(forked, rng, kind)
|
forked = applyMutation(forked, rng, kind)
|
||||||
}
|
}
|
||||||
// Re-embed site children so variants match the SiteNode schema.
|
|
||||||
forked = rehydrateSiteChildren(forked)
|
|
||||||
|
|
||||||
const invalidCount = countInvalidNodes(forked)
|
const invalidCount = countInvalidNodes(forked)
|
||||||
if (invalidCount > 0) {
|
if (invalidCount > 0) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'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 { NodeRenderer, unionPolygons, useNodeEvents, useViewer } from '@pascal-app/viewer'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
|
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
|
||||||
@@ -117,7 +117,7 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
|
|||||||
<group ref={ref} {...handlers}>
|
<group ref={ref} {...handlers}>
|
||||||
{/* Render children (buildings and items) */}
|
{/* Render children (buildings and items) */}
|
||||||
{node.children.map((childId) => (
|
{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 */}
|
{/* Ground fill: site polygon with slab holes, occludes below-grade geometry */}
|
||||||
|
|||||||
Reference in New Issue
Block a user