Skip to content

mcp + core: author_scene converts a whole three.js design into native, editable nodes - #1107

Merged
wass08 merged 26 commits into
mainfrom
feat/author-scene
Oct 19, 2026
Merged

wass08 merged 26 commits into
mainfrom
feat/author-scene

Conversation

@wass08

@wass08 wass08 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

A frontier model writes a whole design as free three.js, richer than tool-by-tool building; author_scene then 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: defects by the agent's own ids, warnings, defaults (what Pascal filled in), snapped wall ends, an ids map, verify_scene. The module and the agent's adapter are kept with the scene. replace: true writes 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):

  • Finish scripts: a room's floor and a wall face can carry a module built for exactly that polygon or face, cut by Pascal to the region and its openings, rebuilt by the structure reconcile when the room or wall changes, and measured: the report warns when a finish leaves part of the floor or face bare. The native wall skirting is exposed in the payload.
  • The compiler bakes the back face of double-sided materials (the viewer draws front faces only), so a curtain built as a plane shows.
  • A new door's handle hangs opposite its hinges (add_door too); 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_scene reads hosted items through their host, lets stacked things stack, and keeps a door's keep-out to its swing side.
  • An authored object's glass stays Pascal's glass; exact slot colours keep the module's roughness and metalness; a light with no fixture becomes a ceiling point in its room; a wall-hosted item keeps its distance from the face.
  • A preview colour for every textured library material.

The tool is MCP-first: it is not in AGENT_TOOL_CONTRACTS until the chat can run it; the private app registers it with the hosted effects.

How to test

  1. bun test in packages/core, packages/mcp, packages/geometry-script, packages/nodes, packages/editor and packages/viewer/src/systems/wall: all green on this branch.
  2. packages/mcp/src/tools/author-scene.test.ts runs 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 with get_node and get_source. Expect the room written, the floor and face builds cut to their regions, the glass slot on library:preset-glass, and a finish_gaps warning on the floor module that leaves a strip bare.
  3. Against a running server (the private app with PASCAL_SERVER_SCRIPT_COMPILE=1, or a local editor with a script host): call author_scene with bench-style payload of four walls, a door, a window, a floorCode and a wallCode; 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.
  4. bun run check-types: two errors remain in packages/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

  • I've tested this locally with bun dev (through the private community app on :3008 against these packages, eight agent conversions end to end)
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 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, stable ids). Hosts compile modules first; floor/face finish scripts are listed for post-write compilation via new setRoomFloorScript / setWallFaceScript and refreshFinishScripts.

get_source now resolves room floor scripts and wall face scripts (readableScript + optional face). Paint blocks scripted faces/floors and supports trim roles like skirting on both faces.

Layout verification is tightened: item-space transforms 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 (handleSide on add_door / create-room).

Large author-scene.test.ts and 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.

wass08 and others added 26 commits October 19, 2026 16:34
…, 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.

metadata: { authorScene: { elevation: level.elevation } },
name: level.name,
level: index,
children: [],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.

id: `region_${k}`,
polygon: r.polygon,
finish: finishRef(r.finish, id) ?? r.finish,
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.

})
place(node)
report.ids[`column/${index}/${i}`] = node.id
report.columns++

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e52c6e1. Configure here.

@wass08
wass08 merged commit ccd0623 into main Oct 19, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant