Repository navigation
Preserve authored scenes through conversion and level verification - #1108
Merged
Merged
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>
…tion # Conflicts: # packages/core/src/agent-operations/author-scene.test.ts # packages/core/src/agent-operations/author-scene.ts # packages/core/src/agent-operations/index.ts # packages/core/src/agent-tools/author-scene.ts # packages/mcp/src/tools/author-scene.ts
Aymericr
marked this pull request as ready for review
October 19, 2026 23:31
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit effe912. Configure here.
| const size = | ||
| (event.ortho || event.perspective) && event.standardSize && supersample === 1 | ||
| ? [event.standardSize.w, event.standardSize.h] | ||
| : canvasSize |
There was a problem hiding this comment.
Supersampled cameras ignore requested size
Medium Severity
captureDimensions uses the requested standardSize only when supersample === 1. An explicit ortho or perspective camera with supersampling still sizes from the live pane, so the capture inherits the editor aspect the new path is meant to avoid and the picture no longer matches the planned pose.
Reviewed by Cursor Bugbot for commit effe912. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What does this PR do?
Combine the author_scene converter from #1107 with the retained-design and level exploration work. Re-conversion now keeps stable node IDs, preserves unrelated/manual changes and rejects overlapping edits. All scripted geometry and room finishes compile before one scene commit, shared by MCP and chat hosts.
Preserve room cameras alongside explicit Solo and Exploded presentation so roof separation and interior verification remain observable. Hosts using the evidence-led design workflow own their brief policy; existing MCP sessions retain their brief behavior.
How to test
Validation: 82 targeted converter/MCP/view tests pass; core, geometry-script, MCP and editor TypeScript builds pass. Full upstream CI runs on this PR.
Checklist
Note
Medium Risk
Large changes to destructive
author_scenemerging and scene commits, plus roof-level and view-capture behavior; mitigated by conflict refusal and extensive tests but still core agent editing paths.Overview
author_sceneis now an incremental, building-scoped conversion instead of wiping and reminting levels. Stable payload-keyed node IDs andauthorSceneStateon the building supportreplace: truedeltas that merge against the last candidate viaauthoredSceneChanges, keeping manual moves/adds/deletes and refusing overlapping edits; first-time conversion refuses buildings that already have native work without a baseline, and copied buildings cannot hijack another building’s conversion.Hosts share one compile-then-commit path:
@pascal-app/geometry-script/author-scene’scompileAuthorScenebuilds the full candidate (items, openings, floor/wall finishes) beforefinalizeAuthorSceneapplies a single patch set; MCP adds abort/assertActive, a scene-changed guard, optionalbuildingId, and re-registersauthor_scenein the tool list.Retained authored roof/ceiling items (
metadata.role) are treated as geometry for roof-level rules, verification counts, interior/view hiding, clipboard paste, and capture framing—not ordinary storey furniture.view_scenegains optional stacked / exploded / solo presentation (with renderer confirmation), editor capture applies it, and bounds framing respects exploded vertical gap.Site can persist
presentation(lighting, exposure, edges, etc.) andshowGround; ceilings and imported GLTF items keep authored PBR/unlit appearance more faithfully;authorSceneToolis exported for MCP.Reviewed by Cursor Bugbot for commit effe912. Bugbot is set up for automated code reviews on this repo. Configure here.