Files
editor/packages/mcp/src/tools/check-collisions.ts
T

80 lines
2.3 KiB
TypeScript

import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import type { AnyNodeId, ItemNode } from '@pascal-app/core/schema'
import { getScaledDimensions } from '@pascal-app/core/schema'
import { z } from 'zod'
import type { SceneOperations } from '../operations'
import { NodeIdSchema } from './schemas'
export const checkCollisionsInput = {
levelId: NodeIdSchema.optional(),
}
export const checkCollisionsOutput = {
collisions: z.array(
z.object({
aId: z.string(),
bId: z.string(),
kind: z.string(),
}),
),
}
type AABB = { minX: number; maxX: number; minZ: number; maxZ: number }
function itemAabb(item: ItemNode): AABB {
const [x, , z] = item.position
const [w, , d] = getScaledDimensions(item)
const halfW = w / 2
const halfD = d / 2
return {
minX: x - halfW,
maxX: x + halfW,
minZ: z - halfD,
maxZ: z + halfD,
}
}
function aabbOverlap(a: AABB, b: AABB): boolean {
return a.minX < b.maxX && a.maxX > b.minX && a.minZ < b.maxZ && a.maxZ > b.minZ
}
export function registerCheckCollisions(server: McpServer, bridge: SceneOperations): void {
server.registerTool(
'check_collisions',
{
title: 'Check collisions',
description:
'Detect overlapping item footprints via an axis-aligned 2D bounding-box test. Optionally scoped to a single level.',
inputSchema: checkCollisionsInput,
outputSchema: checkCollisionsOutput,
},
async ({ levelId }) => {
const filter: { type: 'item'; levelId?: AnyNodeId } = { type: 'item' }
if (levelId) filter.levelId = levelId as AnyNodeId
const items = bridge.findNodes(filter) as ItemNode[]
const boxes = items.map((i) => ({ item: i, aabb: itemAabb(i) }))
const collisions: { aId: string; bId: string; kind: string }[] = []
for (let i = 0; i < boxes.length; i++) {
for (let j = i + 1; j < boxes.length; j++) {
const a = boxes[i]!
const b = boxes[j]!
if (aabbOverlap(a.aabb, b.aabb)) {
collisions.push({
aId: a.item.id as string,
bId: b.item.id as string,
kind: 'item-aabb',
})
}
}
}
const payload = { collisions }
return {
content: [{ type: 'text' as const, text: JSON.stringify(payload) }],
structuredContent: payload,
}
},
)
}