Repository navigation
mcp + core: author_scene converts a whole three.js design into native, editable nodes - #1107
Conversation
…, written as native nodes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, keep its provenance, answer the gate report Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d things stack, and keeps a door's keep-out to its swing side Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…one call Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tadata classified Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…il the chat can run it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r itself is not wiped Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n id map, square columns, slab finishes as slots, rugs allowed, shared enclosures reported Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ours per wall face, walls as hosts, and reports defaults and warnings Four fresh agents converted real designs with author_scene and the payload could not say what they knew: every window came out fixed, every door plain and hinged with its handle on the hinge edge, sage became mid grey through the nearest library colour, one colour painted both faces, baseboards and casings had no wall to follow, and 43 twelve-millimetre "wall cap" slabs lifted the walls onto them with nothing in the report to say so. An opening now takes `type` (DoorType / WindowType), `style` (the door and window style presets, applied as add_door/add_window apply them; a style on an operable window is dropped and said), `leaves` and `code` (a module compiled and stored as an item's, keyed `<wallId>/<index>`, written as the scripted door or window). A door's handle is set opposite its hinge. A hex colour is one exact scene material per distinct colour (roughness 0.9, metalness 0, named by the hex) reused across walls, floors, regions and slabs; `colorLeft` / `colorRight` paint one face (left = front = role a). `items[].host` accepts a wall id: the item is written as a wall-side item on the face nearest its point, facing the room, as place_items does. Hosted things and lights are written in their host's frame, so a rotated or wall-hung host keeps them where they were. The report gains `defaults` (one sentence per thing Pascal filled in or dropped), `warnings` (`thin_slab`, `many_slabs`, `many_regions`: written, but doubtful) and the defect `slab_over_wall`, which refuses a slab over a wall's footprint with its top above half the wall's height. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…as scene materials The host compiles every module the payload carries, the items' and the openings' with code, keyed by the payload's own ids (`<wallId>/<index>` for an opening), through the same compileAndStore as add_door and add_window, and lends the operation the scene's materials so a hex colour is one exact scene material rather than the nearest library colour. The stored payload drops an opening's code as it drops an item's: each module is its own artifact. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…wall hosts, defaults and warnings The pascal-3d skill, its conversion workflow and the agent-surfaces page name the payload's new fields and the report's defaults, warnings and slab_over_wall, and carry two rules the acceptance runs needed: Pascal builds a wall's cap, the floor plate and the ceiling itself, so a module's caps, underlays and planks fold into the wall, the floor finish and the room (said in notes), and whatever opens in the module carries an open clip so it keeps working after conversion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An importer wrote 12 mm "wall caps" exactly over 2.6 m walls, and every wall elected its own cap as its base: wall base 2.662, level height 5.26. The election now drops a slab whose underside reaches the wall's own top and whose footprint lies on the wall body (between its faces, within its length, 5 cm of slack). The top is measured from the lowest candidate (an authored height above it, else the storey plane), since the base being elected cannot decide it. A floor or terrace reaching past the wall keeps carrying it however short the wall, and an explicitly preferred host stays. The slab-elevation clamp probes just past its bound instead of past the storey plane, where a slab now reads as resting on the wall. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The viewer draws every material front side only (its WebGPU MRT workaround), so a curtain built as a DoubleSide plane showed nothing from the room. The compiler now appends the triangles again, wound the other way with negated normals, and sets the material to FrontSide; a BackSide material keeps only the flipped copy. Runs before the triangle count and the per-material merge, so the manifest counts what is exported. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hinge and handle sides are both measured from the door's own left, so a door written with hingesSide right and the schema's default handle had both on one edge. add_door (and create_room's doors through it) now set handleSide = hingesSide === 'right' ? 'left' : 'right' unless the input names a handle side; the tool takes an optional handleSide. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A one-face paint looked flipped after a room resize. The push itself (setWallGeometry, which the editor's room arrows commit) keeps every wall, its direction, its slots and its children; what changes on the first structural edit is frontSide/backSide, which the kernel classifies from the room's face. The test writes the walls as an importer does, with the room on the back wall's b face, and pins all of that. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…of stamping interior Every wall was written frontSide interior, backSide exterior whatever its direction, so half the walls of a converted flat carried the room on the face tagged exterior until the first edit reclassified them: the facing hide rule, cladding and door facing then flipped. The same detection the editor runs classifies them at write time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…payload written again is not wiped The salt loop probed a 'probe' id no conversion ever writes, so a payload written over itself kept revision 0, minted the ids already in the scene, created nothing and deleted the old level: the hosted store refused it as a scene wipe. The test passed vacuously on an empty create list; it now demands the second write's creates and the first level's deletion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…exact item and opening colours, and ceiling points for lights with no fixture What the four converting agents asked for after comparing pictures. A wall-hosted item was flattened onto the wall face, so a pleated curtain sat half inside the wall: its outward distance from the face is kept, and only a point inside the wall body is moved to the face and reported. An authored item's slots went through the library match, silently (oak to an orange texture, a red book to parquet): they are written as exact scene materials now, as walls already were; an opening's color is painted on its panel and frame (frameColor for a frame of its own); a scripted opening keeps its declared type. A light with no fixture was hosted on the nearest item (a rug, a bed): it now hangs from the ceiling of the room containing it, on a minimal ceiling point the operation writes itself, with the nearest item only as the reported fallback outside any room. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… when the room changes A design converted from three.js faked plank and tile floors with free-standing objects or fixed floor regions that did not follow the room. A room's floor finish can now be a three.js module that Pascal keeps on the zone and runs for exactly the room's floor. - Schema: zone.floor.script (ZoneFloorScript) is the script source record items use (language, script and artifact hashes, params, manifest) plus `inputs`, the key of the floor it was built for. Absent on every existing scene. - geometry-script: compile input `inputs` reaches the build in `params` (and as `inputs`), is never returned among the stored params, and keeps the build in the host's frame instead of re-origining it for its mount. The sandbox worker and the editor's worker client carry it. - core lib/floor-script: floorScriptInputs (the room's outline inside the wall faces clipped to its plate, holes, and the plate top, level coordinates), floorScriptInputsKey, staleFloorScripts, refreshFloorScripts(nodes, host) for any FloorScriptHost (read the module, compile and store for a floor). - core setRoomFloorScript operation: writes a compile on the zone with the key of its inputs, or removes it; refuses Blender builds, a room with no floor, unmeasurable output. - Paint: the script owns the floor's colours. A scripted room gets its own plate top cell, and floorPlatePaintRefusal (the editor's Paint, and the slab paint commit) and set_zone_intent's floor finish/regions refuse with a sentence until the script is removed. - Rendering: the zone renderer draws the artifact through the scripted-model path, 1 mm over the plate, with no pointer events, so clicks reach the floor. - Editor: the tab rebuilds stale floor scripts after a settled change (sandbox compile, artifact store), writing outside history like the reconcile's derived writes and only while the room's floor is still the one built for; the room panel shows "Scripted finish" with View code and Remove; a paste into another project brings the floor script's artifacts. - Tests: built for the floor inside the wall faces at the plate top; compiled once and again only after the structure reconcile re-derives the room (fake host); paint refused through the Paint tool, the slab commit and the room intent until removed; no floor, no script; an old zone parses unchanged; compile with inputs stays in the host frame; clipboard copies the artifacts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…script
Converting agents faked baseboards with free-standing items hosted on walls and
plank floors with fixed floor regions, because the payload had no way to say
either. Both finishes are native now, so the payload carries them.
- Payload: a wall's `skirting { height (0.1 m), depth (0.012 m), color? }` and a
room's `floorCode` (a three.js module given params.polygon, params.holes and
params.elevation, at most 48k), with `floor` as its fallback. Old payloads
parse unchanged.
- Operation: once the rooms exist, setWallSkirting on each wall with skirting,
on the faces a room stands on, its colour through the payload's exact
materials; a refusal is a `skirting_refused` defect naming the wall. Core
cannot compile, so authorSceneFloorScripts(payload, report) lists
{ roomId, zoneId, code } for the host; the report gains `floorScripts`.
- MCP: after the write, each room's floor (floorScriptInputs) is compiled
through the GeometryScriptHost with `inputs` (compile and build carry them;
ScriptedKind 'floor', no artifact kind) and written with setRoomFloorScript;
a failure is `floor_script_failed` and the room keeps its floor. The stored
payload drops floorCode like the other modules.
- get_source: readableScript reads a room's floor script by its zone id
(zone.floor.script); the contract says so for both surfaces.
- Skill, workflow reference and agent-surfaces describe both.
- Tests: skirting on the room's face in its exact colour, defaults, no skirting
when absent; a wall with no room is a defect; rooms with floorCode listed by
zone, unenclosed ones not; through a real MCP client and the real compiler,
a wall's skirting painted and a zone's floor.script built inside the wall
faces and read back with get_source; a failing floor module is a defect and
the room keeps its finish.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…when the wall changes
A converted design faked bathroom tiles, panelling to a dado and boards with
objects hosted on the wall, which did not follow the wall or its openings. A
wall face's finish can now be a three.js module that Pascal keeps on the wall
and runs for exactly that face, the floor script's twin.
- Schema: wall.finishScripts { a?, b? } (WallFaceScript) is the script source
record a floor script is, plus `inputs` (the key of the face it was built for)
and `toHeight` (metres above the face's base; absent, the whole face). Absent
on every existing scene.
- core lib/wall-face-script: wallFaceScriptInputs(nodes, wallId, face, toHeight?)
gives the face in its own frame (x along the wall from its start, y up from
the face's lowest floor, z out of the face): `outline` {x0,y0,x1,y1} between
the face's mitred ends up to the wall's top or toHeight, `holes` (doors from
the floor under them, windows from their sill, clipped), `base` ([x, y] points
of the face's floor), `thickness`; wallFaceScriptInputsKey, wallFaceScriptRecord,
wallFaceScriptPlacement, wallFaceScriptSkirtingGap, wallFaceScriptPaintRefusal.
Curved and curtain walls have no face to build on.
- One refresh for both finishes: staleFloorScripts / refreshFloorScripts /
FloorScriptHost become lib/finish-script's staleFinishScripts,
refreshFinishScripts and FinishScriptHost over rooms' floors and walls' faces,
and finishScriptWrites, which writes a rebuild only while its floor or face is
still the one it was rebuilt for. The editor tab's refresh
(initFinishScriptRefresh, finish-scripts.ts) runs both, outside history.
- core setWallFaceScript operation: writes a compile on a face with its key and
toHeight, or removes it; refuses Blender builds, a face with nothing to build
on, unmeasurable output, a toHeight not above 0.
- Rendering: the wall renderer draws the artifact under the wall mesh at the
face's base, 1 mm proud of the face, with no pointer events; face b's build is
mirrored through the wall (scale z -1) so x still runs from the wall's start.
The zone and wall renderers share FinishScriptModel.
- Skirting: it keeps running along the base and stops over the build's span
where the build comes down into its band, as at a door; a splashback above it
leaves it whole.
- Paint: a face scripted to the top refuses paint (the face, its regions, the
room's finish on it) in the editor's Paint, the wall's paint commit and the
`paint` operation, with a sentence; the other face, and a face scripted to
toHeight, stay paintable.
- get_source: readableScript reads a wall's face script with a `face` argument
(optional when one face has one); the contract says so. ScriptedKind gains
'wall-face'. Turning a wall around swaps its face scripts; the reference
inventory and the clipboard's artifact copy know them.
- Wall panel: "Scripted finish" with View code and Remove per scripted side.
- Tests: the face's outline, door and window in wall-local metres; built once
and again only after an opening is added or the wall lengthened (fake host);
paint refused on the scripted face, allowed on the other and on a dado face,
through the operation and the wall's paint commit; the skirting gives way
under panelling and not under a splashback; placement on each face; a curved
wall refused; get_source through a real MCP client; reversal swaps the
scripts; an old wall parses unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eft/faceRight and a room's wallCode
Converting agents still faked bathroom tiles, panelling and boards with items
hosted on walls, because the payload had no way to say a wall face's finish.
Wall face scripts are native now, so the payload carries them, wired the way
room floor scripts are.
- Payload: a wall's `faceLeft` / `faceRight` ({ code (at most 48k), toHeight?
metres from the face's base, default the whole face }), left and right as
colorLeft / colorRight define them (left = front = face a), and a room's
`wallCode` (+ `wallToHeight`) for every face the room looks into, a wall's
own module winning. Old payloads parse unchanged.
- Operation: authorSceneFaceScripts(payload, report, nodes) lists
{ wallId, face, nodeId, code, toHeight? } once walls and zones exist; a
room's faces are the spans of the derived room its zone was made from (the
faces setWallSkirting runs along). It needs the nodes because a zone alone
does not say which face of each wall it looks into. The report gains
`faceScripts`; a face no room looks into (or with nothing to build on) is a
`no_face` defect named <wallId>/<face>, and a face two rooms' different
wallCodes claim is a `face_shared` warning (the first room builds it).
- MCP: after the write and the floor scripts, each face is read
(wallFaceScriptInputs with toHeight), compiled as a 'wall-face' with
`inputs` and written with setWallFaceScript; a failure is
`face_script_failed` naming <wallId>/<face> and the face keeps its paint.
The stored payload drops the faces' and wallCode's code.
- Skill, workflow reference and agent-surfaces describe it.
- Tests: faces listed for a wall's own module and a room's wallCode (to
1.2 m), the wall's own winning, none for an old payload; a face no room
looks into is no_face; two rooms on one face warn; through a real MCP client
and the real compiler, tiles skipping the windows on one face inside its
bounds, the other walls panelled to 1.2 m, read back with get_source and
face; a failing face module is a defect and the face stays painted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gion and openings A finish module was asked to clip itself to the floor polygon and skip the door and window holes; the first one written against that contract dropped whole tiles touching an opening and left whole planks out of a staggered row. Every module gets this slightly wrong, so the compiler now does it: a build with host inputs (a floor's polygon and holes, a face's outline and hole rectangles) is cut after the module runs, before the merge and export. A mesh wholly inside and touching no hole is kept as built (no CSG), one wholly outside or in a hole is dropped, and one across the boundary is intersected with the region's prism (three-bvh-csg; closed meshes gain the cut faces, open ones are trimmed), keeping its material and part. Triangles and bounds are measured after the cut; output stays byte-deterministic. The author_scene contract (floorCode, faceLeft/faceRight, wallCode), the skill reference and the wiki now say the module lays out and Pascal cuts; holes stay in the inputs for layouts that want them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r face a finish script covers The cut trims what a floor or face module builds past its region, but cannot add what it never built: odd plank rows started 40 cm in left a bare strip along the wall and the report was green. After the cut the compiler now measures the build seen square-on to its region (strips cut at every corner's height, exact for boards and tiles laid square, about 5 ms for a 20 m2 plank floor) and writes `coverage` (0..1, joints count as bare) and `uncovered` (up to four rectangles bounding the largest bare patches, in the host's frame; a joint is never a patch) in the manifest. author_scene warns `finish_gaps` with the share and where each patch lies when coverage is under 0.98 and a patch exists, so tile joints alone stay silent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…act slots A converted house showed its window glass and a pendant's bulb opaque, and two agents painted the originals back by hand: every authored slot with a colour was written as an exact matte material (roughness 0.9, metalness 0, opaque), over the glass the library match had given it and over the module's own emissive material. A slot add_object writes as glass (named glass, or transparent in the manifest) now stays library:preset-glass, on items, scripted doors and scripted windows alike. A scene material has no emission (MaterialProperties is colour, roughness, metalness, opacity, transparent, side), so an emissive slot keeps the module's own material and a bulb stays lit, its light effect unchanged. Every other exact slot carries the module's roughness and metalness (0.9 / 0 only when absent), one scene material per colour and surface, named by them when not matte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e exemption goes Main refuses a thin wide box only where no floor lies under it; a rug on a room floor is built without naming it. The exemption author_scene added for rugs is no longer needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.
| levelId: levelNode.id, | ||
| skirting: w.spec.skirting, | ||
| }) | ||
| report.ids[w.spec.id] = node.id |
There was a problem hiding this comment.
Duplicate payload IDs overwrite across levels
High Severity
Wall and item ids are stored in global maps keyed only by the payload id, with no level prefix and no uniqueness check. A second storey that reuses names such as north or bed overwrites the first storey’s compiled module, report.ids entry, light fixture, and face-script target, so those nodes get the wrong geometry or are skipped with no defect.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.
| metadata: { authorScene: { elevation: level.elevation } }, | ||
| name: level.name, | ||
| level: index, | ||
| children: [], |
There was a problem hiding this comment.
Level elevation is never applied
Medium Severity
The payload’s elevation is stored only on metadata.authorScene and is not written to baseElevation or height, nor reported in defaults. Upper storeys sit on Pascal’s wall-derived stack even when the agent gave a different floor height.
Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.
| id: `region_${k}`, | ||
| polygon: r.polygon, | ||
| finish: finishRef(r.finish, id) ?? r.finish, | ||
| })) |
There was a problem hiding this comment.
Invalid region finish is still written
Medium Severity
When finishRef rejects a floor region colour, the code still stores the raw r.finish string on the zone. The room’s main floor finish correctly stays unset on the same error, so a bad region colour either crashes ZoneNode.parse or persists an invalid material.
Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.
| }) | ||
| place(node) | ||
| report.ids[`column/${index}/${i}`] = node.id | ||
| report.columns++ |
There was a problem hiding this comment.
Column colour is silently dropped
Low Severity
A column’s color is copied into metadata.authorScene and never passed through finishRef or paint. Walls, slabs, openings, and rooms in the same function apply payload colours. The column is written unpainted with no defaults or defects entry.
Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.


What does this PR do?
A frontier model writes a whole design as free three.js, richer than tool-by-tool building;
author_scenethen turns it into native, editable Pascal nodes in one MCP call. The agent reads its own module any way it likes and sends a payload: levels, walls as centerlines with openings (type, style, hinge, exact colours, or a module for a non-standard one), rooms with a seed point and a floor finish, slabs, columns, railings, every piece of furniture as an authored object with its own module, lights. Pascal compiles and stores the modules, derives rooms, floors and ceilings, paints, runs its checks and answers a gate report:defectsby the agent's own ids,warnings,defaults(what Pascal filled in),snappedwall ends, anidsmap,verify_scene. The module and the agent's adapter are kept with the scene.replace: truewrites over a previous conversion with fresh ids.Pascal fixes that came out of eight fresh-agent conversions (GPT-6 Astra and Claude Opus, flats, a bedroom, a two-storey house, a living room from a photo):
add_doortoo); a slab lying on a wall's top is its cap, never its support; a wall's room side is read from the plan; a room-edge push keeps a painted wall's finishes and children.verify_scenereads hosted items through their host, lets stacked things stack, and keeps a door's keep-out to its swing side.The tool is MCP-first: it is not in
AGENT_TOOL_CONTRACTSuntil the chat can run it; the private app registers it with the hosted effects.How to test
bun testinpackages/core,packages/mcp,packages/geometry-script,packages/nodes,packages/editorandpackages/viewer/src/systems/wall: all green on this branch.packages/mcp/src/tools/author-scene.test.tsruns the whole thing through a real MCP client and the real compiler: a bedroom payload with a scripted window, a glass door, a baseboard on a wall, a plank floor module laid past the room edge, a tile face module laid over a window, sage on one wall face; it reads the nodes back withget_nodeandget_source. Expect the room written, the floor and face builds cut to their regions, the glass slot onlibrary:preset-glass, and afinish_gapswarning on the floor module that leaves a strip bare.PASCAL_SERVER_SCRIPT_COMPILE=1, or a local editor with a script host): callauthor_scenewithbench-style payload of four walls, a door, a window, afloorCodeand awallCode; open the project; expect walls, openings, a room with the scripted floor, tiled faces, skirting; move a wall and expect the floor and face scripts to rebuild. The eight converted projects on the owner's :3008 are the reference.bun run check-types: two errors remain inpackages/editor/src/components/systems/ceiling/ceiling-bracket-batch.test.ts, unchanged since the base commit (deps refresh [Deps] chore(deps): refresh MCP, rendering, framework and CSS dependencies #1037), not from this branch.Screenshots / screen recording
Not added yet. (Pictures of the converted scenes beside the raw three.js renders are in the private repo's
bench/author-scene/briefs/.)Checklist
bun dev(through the private community app on :3008 against these packages, eight agent conversions end to end)bun checkto verify)mainbranch🤖 Generated with Claude Code
Note
Medium Risk
Large new conversion pipeline touches scene structure, materials, and agent tools; layout/clearance logic changes affect verify_scene behavior across furnished scenes.
Overview
Adds
author_scene, a one-shot path from an agent’s three.js design payload to native scene nodes: walls (join snapping, openings, skirting, paint), rooms/zones, slabs, items, lights, materials, structure reconcile, and a detailed report (defects,warnings,defaults, stableids). Hosts compile modules first; floor/face finish scripts are listed for post-write compilation via newsetRoomFloorScript/setWallFaceScriptandrefreshFinishScripts.get_sourcenow resolves room floor scripts and wall face scripts (readableScript+ optionalface). Paint blocks scripted faces/floors and supports trim roles likeskirtingon both faces.Layout verification is tightened:
item-spacetransforms hosted item footprints/heights; door keepouts respect swing direction and height; rugs, wall-mounted items, and stacked host/child items are excluded from false positives. Doors default handle opposite hinges (handleSideonadd_door/create-room).Large
author-scene.test.tsand related tests cover conversion, materials, lights, skirting, and script defects.Reviewed by Cursor Bugbot for commit e52c6e1. Bugbot is set up for automated code reviews on this repo. Configure here.