Skip to content

Authored geometry: agents build objects, windows and doors in three.js - #994

Merged
wass08 merged 28 commits into
mainfrom
feat/freeform-elements
Oct 2, 2026
Merged

wass08 merged 28 commits into
mainfrom
feat/freeform-elements

Conversation

@wass08

@wass08 wass08 commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

What this changes

Agents build what Pascal's recipes cannot reproduce faithfully (a porch, lanterns, vaulted and tray ceilings, a fan-grille window, a carved door) by writing plain three.js. Pascal keeps the result as a node with a script source: an item, or a window / door whose fill comes from the script. Design: wiki/architecture/authored-objects.md.

  • @pascal-app/geometry-script (new): compiles a module to a GLB + manifest read from naming conventions (slot_<id> paint slots, typed part:<id> parts, lights, cutout, anchor:<id>, collider, clips). Derives resting surfaces and undersides, merges meshes per part and material, samples clips into TRS tracks held at their last pose. Ships a self-contained sandbox bundle (/sandbox).
  • Sandbox: the editor compiles in a worker inside a hidden sandbox="allow-scripts" iframe (opaque origin) with a CSP allowing only its own scripts and blob workers: no network, fonts, images or frames (verified: the same fetch succeeds without the CSP and fails with it). The editor hashes the returned GLB and code itself.
  • core: source on item, window, door (script hash, params, GLB hash, inline manifest ≤ 24 KiB); code is a separate text/javascript artifact, never in the scene. artifact:// resolved through a host ArtifactStore. Surface/underside helpers, resettledPosition, flushMountRotation, scriptedSize, scriptInteractive.
  • Agent tools (shared contracts, one operation each):
    • add_object — create or rebuild (by nodeId) a scripted object; children re-settle after a rebuild.
    • add_window / add_door — now take the native outline (rectangle / rounded / arch), type and panes, plus optional code / params; with nodeId they rebuild an opening (operation rescriptOpening).
    • get_source — the module and params of any scripted node. find_by_type — nodes and typed parts of one type.
    • place_item lands on an object's real surfaces, hangs ceiling items from its undersides.
  • viewer / nodes / editor: scripted openings render through the item's model path; the wall cuts the script's cutout mesh (real shape); the window/door systems keep hitbox and placement; panels show the script's params; handles only for declared width / height params; the Open control plays the open clip; GLB export bakes their clips; the selection toolbar sits on what renders. Parameters panel, X/Z tilt, item clip controls, ceiling hosting on authored undersides.
  • Release: release.yml publishes geometry-script before the editor. MCP inventory: 68 tools.

How it was tested

  • bun run ci in a clean clone with a frozen install: lint, skills, typecheck 13/13, tests 15/15 packages, build 9/9.
  • Live in the hosted app (paired PR in private-editor) through chat and MCP on a local reference house: porch, garage doors with clips, lanterns, vaulted and tray ceilings with hung and tilted lights, a fan-grille window as an item and as a window node, a scripted double door rebuilt by nodeId; sandbox compile from the chat; fork; bake.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

wass08 and others added 17 commits October 2, 2026 11:50
…o content-addressed artifacts

- @pascal-app/geometry-script: compiles a plain three.js module (allowlisted
  three/addons + three-bvh-csg imports) to a GLB + manifest read from naming
  conventions (part:, slot_, light:, anchor:, cutout, collider); runs in a
  browser worker, Bun or Node.
- core: GeometryScriptSource on item.source; artifact:// URLs resolved through
  a host-configured ArtifactStore (in-memory by default).
- editor: locked-down compile worker + authorScriptItem.
- nodes: item renderer hides collider meshes like cutout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…y subpath

Turbopack folds three's typeof-window guard to true in worker chunks, so the
worker defines window before three evaluates; the worker imports the compile
subpath so core (which needs window) stays out of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…ts and resting surfaces

- core: authorObjectTool contract + authorObject operation (the item a
  compiled module becomes; paint on vanished slots kept and reported; parts
  cap; host check by mount). Surfaces compile first and pass `compiled`.
- geometry-script: manifest parts carry userData.type and bounds; upward
  surfaces per part ({ part, y, polygon }) for placement; source guard moves
  into the compiler; item building moves to core.
- mcp: author_object registered from the shared contract; the host supplies
  compile + artifact storage (GeometryScriptHost), else scripts_unavailable.
  Annotation policy: destructive (an edit replaces geometry), 66 tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
- core: geometrySurfaceAt / geometryRestingHeight. An object's main surface
  (non-roof, at least half its footprint) wins under the point, so a bench
  anywhere on a porch lands on the landing, never the beam or the canopy;
  roof-typed parts never receive. author_object sets asset.surface from it.
- compiler: one surface outline per mesh and height (a cross-mesh hull merged
  a beam and its returns into one surface).
- mcp place_item: can target an item; level coordinates map into the host
  frame; on an authored host it lands on the surface below, reports restingOn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…l batch

- compiler merges each part's meshes by material (the porch: ~80 meshes → 16),
  part extras and helpers kept.
- itemBatchable excludes only animated items: light effects drive separate
  light objects, so a lit porch or lamp batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…js clips

- compiler exports the module's group.animations (tracks rebound to uuids, so
  part:<id> targets survive; animated subtrees are not merged), manifest lists
  clips.
- author_object: a clip named open becomes the item's open/close toggle (close
  plays 'close' or 'open' reversed), 'loop' runs throughout; other clips stay
  in the artifact for later controls.
- item renderer: open-close mode plays open once and holds, reverses on close;
  the bake keeps the opening as a once clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…port

- An animation effect can name its toggle (control); authored objects get a
  play toggle per clip besides open/close/loop, labelled with the clip name
  (a music box's 'Princess twirl'). E plays them all.
- itemClipRegistry holds every clip of a node; the GLB export emits each as
  '<id>: <name>' (catalog items keep '<id>: loop').

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…survives glTF

GLTFExporter keeps only position/quaternion/scale tracks and silently dropped
an Euler rotation[y] track, leaving an authored music box with an empty clip.
Each clip now plays once in a mixer and is sampled (30 fps) back into
position/quaternion/scale tracks; a clip that moves nothing is refused with
the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
The params a script declares become inspector controls (sliders, toggles,
option segments); a change re-runs the script with the new values as one undo
step, stale rebuilds dropped. Duplicate keeps the script and paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
The 64-part refusal blocked a 245-part porch and surfaced an AI-facing rule
to the person. No cap for now; the parts guidance stays in the tool
description. A failed rebuild says so plainly and logs the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
- Manual: authored items are face hosts — hovering a downward face (a vault
  plane, a soffit, a beam) with a ceiling item hangs it there, upright, as
  the object's child; catalog items keep their top-surface behaviour.
- AI: the compiler records undersides as plane + outline (sloped ones
  included); place_item hangs a ceiling item from the lowest underside above
  the point. A hanging item always takes the underside; a resting one only
  honours an explicit height above 0 (models fill 0 in by default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…d undersides

- Item panel: X and Z rotation sliders around Y (aim a spot, lean a frame).
- Flush ceiling fixtures (recessed flag, or 15 cm tall or less: catalog data
  often lacks the flag) seat along an authored underside and tilt with its
  slope; pendants and fans hang plumb. Same rule for manual placement,
  place_on_surface and MCP place_item (core flushMountRotation / mountsFlush).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
One shared read-only tool for MCP and chat: nodes of a kind, items of a
category, light emitters, and the typed parts recorded in authored objects
(a porch's columns, a vault's cans) with level-space bounds. An authored
object whose typed parts are all of the type counts as one (a lantern);
a mixed one answers with its parts. Inventory: 67 tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
A catalog item's cutout still cuts its bounding box; an authored object's
cutout mesh (an arched window, a round porthole) becomes the CSG brush itself,
in wall space, its depth stretched to twice the wall thickness so it clears
both faces. A flat cutter falls back to the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
A rebuild (param change, code edit; panel, chat or MCP) re-seats the
object's children in the same undo step: a resting item back on the surface
under it, moved onto the main surface's nearest point when that shrank away
(a box on a narrowed porch), a hanging one from the underside above it.
The result lists what moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…d objects

- release.yml publishes @pascal-app/geometry-script before the editor that
  depends on it, in the same version bump.
- bun.lock lists the new workspace package.
- pascal-3d skill: when to use author_object and find_by_type.
- wiki/architecture/authored-objects.md: the model, the module, the naming
  conventions, compile hosts, agent tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…rt out of core

The reference inventory now lists the manifest and script-param keys an
authored item defines. The author_object description describes the three
import in words: the architecture scan reads core source text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
@pascal

pascal Bot commented Oct 2, 2026

Copy link
Copy Markdown

What this PR does

It adds a way for agents (and people) to build objects that Pascal's node recipes can't express, by writing a plain three.js module. A new package, @pascal-app/geometry-script, runs that module and compiles it to a GLB plus a manifest read from naming conventions (slot_<id> materials, part:<id> groups, lights, cutout, anchor:<id>, collider, animation clips). The result is stored content-addressed as artifact://<sha256> and lands in the scene as an ordinary item carrying its script source, so paint, hosting, the move tool, collections, bake and export keep working on it. On top of that it wires two agent tools (author_object, find_by_type), lets place_item target an item so things rest on or hang from authored geometry, and adds inspector controls for a script's declared params and clips.

File Change What changed
packages/geometry-script/src/compile.ts added The compiler: runs the module, reads the manifest from naming conventions, merges meshes per part and material, derives surfaces and undersides, samples clips, emits GLB + sha256
packages/geometry-script/src/transform.ts added Module transform for the sandboxed run (three, three/addons, three-bvh-csg imports)
packages/geometry-script/package.json added New published package, three as a peer dep, three-bvh-csg as a dep
packages/core/src/schema/geometry-source.ts added Zod schema for the script source, param specs, and the artifact manifest; 48 KB cap on inline code
packages/core/src/schema/nodes/item.ts modified item.source on the item node
packages/core/src/schema/asset-url.ts modified Accepts artifact:// asset URLs
packages/core/src/lib/artifact-store.ts added Host-configurable ArtifactStore behind artifact://<sha256>, in-memory by default
packages/core/src/lib/geometry-surfaces.ts added Surface helpers: geometrySurfaceAt, geometryUndersideAt, resettledPosition, flush-mount rotation
packages/core/src/agent-operations/author-object.ts added The create/edit operation: builds the item and its asset from compiled output, keeps identity, placement and paint on a rebuild, re-settles children, reports orphaned paint slots
packages/core/src/agent-operations/find-by-type.ts added Read-only query over nodes, item categories, light emitters and typed parts, with part bounds in level space
packages/core/src/agent-tools/author-object.ts added Tool contract and the long authoring guide (module shape, mounts, naming conventions)
packages/core/src/agent-tools/find-by-type.ts added Tool contract for the query
packages/core/src/contracts/reference-inventory.ts modified Inventory now 67 tools, with annotations and justifications
packages/core/src/hooks/scene-registry/item-clip-registry.ts modified Registry holds every clip of a node, not one
packages/editor/src/lib/geometry-script/geometry-script.worker.ts added Locked-down compile worker (network and storage removed)
packages/editor/src/lib/geometry-script/client.ts added Worker client with a 20 s timeout that kills and restarts the worker
packages/editor/src/lib/geometry-script/author.ts added compileAndStoreGeometryScript / rebuildAuthoredObject
packages/editor/src/lib/geometry-script/protocol.ts, worker-window-shim.ts added Worker request/response types and the window shim
packages/editor/src/components/tools/item/use-placement-coordinator.tsx modified Hand placement routes ceiling items to an authored object's underside
packages/editor/src/lib/glb-export.ts modified Export path adjusted for artifact-backed items
packages/mcp/src/tools/author-object.ts added MCP registration with a host-supplied GeometryScriptHost; refuses with scripts_unavailable or no_active_scene
packages/mcp/src/tools/place-item.ts modified place_item can target an item: rest on the surface below, hang from the underside above
packages/mcp/src/server.ts, src/index.ts, tools/index.ts, tools/shared-tools.ts modified Host plumbing and tool wiring
packages/mcp/src/tools/read-tool-annotations.test.ts modified Covers the two new tools' annotations
packages/nodes/src/item/renderer.tsx modified Renders authored objects, their lights and clip players
packages/nodes/src/item/authored-face-host.ts added Ceiling items hang under downward-facing faces (down to about 70°) of an authored object
packages/nodes/src/item/authored-params.tsx added Parameters section in the inspector, rebuilding on change
packages/nodes/src/item/panel.tsx, definition.ts modified Panel sections, X/Z rotation sliders, open/close and per-clip players
packages/nodes/src/shared/node-batch/batchable.ts modified Lit items batch now; only animated ones are excluded
packages/viewer/src/systems/wall/wall-system.tsx modified A cutout cuts the real shape (arch, circle) instead of the bounding box
packages/viewer/src/lib/asset-url.ts modified Resolves artifact:// URLs through the store
.github/workflows/release.yml modified Publishes geometry-script before the editor and includes it in version sync and tags
Remaining files added/modified Index re-exports, editor/package.json, bun.lock, package tsconfig, LICENSE and README, the batch test, plus the design note wiki/architecture/authored-objects.md and the pascal-3d skill update described above (56 files in total)

Good place to start: the design note wiki/architecture/authored-objects.md, then packages/geometry-script/src/compile.ts for the naming conventions the whole feature rests on, then packages/core/src/agent-operations/author-object.ts for what a rebuild preserves.

@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.

Stale Bugbot comment from a previous run.

}
open.paused = false
open.timeScale = -1
open.play()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reverse close skips clip reset

High Severity

Closing an authored object without a close clip plays open reversed but never resets the finished LoopOnce action. The dedicated close path does call reset. After open finishes, three.js leaves the action completed, so play() with timeScale = -1 often does nothing. Garage doors and hatches that rely on the documented reverse-open fallback open once and then stay open.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 042d242. Configure here.

)
for (const { id, data } of changes?.update ?? []) {
useScene.getState().updateNode(id as AnyNodeId, data)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rebuild records multiple undo steps

Medium Severity

rebuildAuthoredObject claims one undo step but calls updateNode once per changed id. Each call is its own scene commit. When children resettle with the object, undo first reverts only some positions and leaves the new geometry in place, so the scene no longer matches either revision.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 042d242. Configure here.

if (child?.type !== 'item' || child.wallId) continue
const position = resettledPosition(compiled.manifest, child, next.scale)
if (!position || position.every((v, i) => Math.abs(v - child.position[i]!) < 1e-4)) continue
resettled.push({ id: child.id, position })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rebuild drops fixture tilt

Medium Severity

A rebuild only writes resettled position. Flush ceiling children (recessed cans) keep their old Euler tilt while resettledPosition moves them onto the new underside. Changing a vault or tray slope leaves those fixtures hanging at the previous angle.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 042d242. Configure here.

const rotation: [number, number, number] = flush
? flushMountRotation([hit.normal.x, hit.normal.y, hit.normal.z], yaw)
: [0, yaw, 0]
const cursor = hit.world.clone().add(offset)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Face-host cursor uses mixed spaces

Medium Severity

The committed hang/flush offset is applied in the host’s local frame, but the cursor adds that same local vector to a world point. When the authored host has yaw (or the new X/Z tilt), the ghost does not sit on the mesh the click will commit to.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 042d242. Configure here.

if (!clip) return
itemClipRegistry.set(node.id, { clip, loop: true })
const entries = scripted
? animations.map((clip) => ({ clip, loop: clip.name !== 'open', name: clip.name }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Baked close clips loop

Medium Severity

Authored clips are registered with loop: clip.name !== 'open', so a close clip is marked looping for bake. Live playback uses LoopOnce for both open and close. The exported viewer can replay a close clip continuously instead of playing it once and holding.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 042d242. Configure here.

if (mesh.name === 'cutout') {
child.visible = false
}
// `cutout` drives wall openings, `collider` the walkthrough; neither renders.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rebuild leaves stale animation controls

Medium Severity

initItem is idempotent and this effect only depends on node.id. A rebuild keeps that id while scriptInteractive replaces asset.interactive. New Lights or Open toggles never get control slots; if lights are added, the old Open value is reused as the light switch. Toggles then drive the wrong effect or do nothing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 042d242. Configure here.

author_object told models never to build doors or windows, so asked for an
arched window with a fan grille the chat offered a rectangular substitute.
add_door/add_window make rectangular openings; author_object builds every
other shape with a cutout shaped like the opening, and the opening tools
say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

const rebuild = (id: string, value: GeometryScriptParamValue) => {
setBusy(true)
setError(null)
rebuildAuthoredObject(node.id, { ...source.params, [id]: value })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overlapping rebuilds drop param edits

Medium Severity

Each inspector rebuild snapshots source.params from the last committed node and later in-flight rebuilds of other params win via the generation guard. Changing a second control before the first compile finishes compiles the old first value, so the earlier edit is discarded.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e30f17f. Configure here.

name: input.name ?? asset.name,
parentId: parent.id,
...(parent.type === 'wall' ? { wallId: parent.id, side: input.side ?? 'front' } : {}),
position: (input.position as Vec3 | undefined) ?? [0, 0, 0],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Item host skips surface snap

Medium Severity

author_object accepts an item parent for floor mounts, but create always uses the given position or [0, 0, 0]. It never lands the new object on the host's resting surface, unlike place_item and rebuild resettle.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e30f17f. Configure here.

…es; openings never become authored objects

The opening tools hid what the window and door already do (arched and
rounded outlines, opening types, pane grids), so models called arches
impossible or built them as items without a mark, schedule row, plan symbol
or IfcWindow. add_window/add_door now take openingShape, archHeight,
cornerRadius, windowType/doorType and columns × rows; author_object refuses
doors and windows. Reverts the 'rectangular only' wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

Comment thread packages/geometry-script/src/compile.ts
nodeType: node.type,
levelId: level,
})
continue

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 search swallows porch parts

Medium Severity

wholeAuthored treats an object as a single match when every typed part shares the query type. Untyped parts do not count as mixed, so a porch that only marks columns (as the skill suggests) is returned as one node and the per-column part rows with level-space bounds are skipped.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15f11ec. Configure here.

manifest: Manifest & Pick<GeometryArtifactManifest, 'bounds'>,
): number | null {
return highest(mainSurfaces(manifest))?.y ?? null
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Porch rest height can be roof

Medium Severity

geometryRestingHeight returns null unless a non-shelter surface covers at least half the object's XZ footprint. A porch landing under a larger roof fails that test, so asset.surface is omitted and the editor's item-host height falls back to dimensions[1] — the roof, not the landing.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15f11ec. Configure here.

}, COMPILE_TIMEOUT_MS)
pending.set(id, { resolve, reject, timeout })
active.postMessage(request)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Compile timeout cancels other jobs

Medium Severity

Each compile’s 20s timer calls reset(), which terminates the shared worker and rejects every pending job. The worker’s async message handler also runs compiles concurrently. A slow or hung script aborts later inspector rebuilds that were still in progress.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15f11ec. Configure here.

…ption scopes it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

? requestedPosition[1] - hy
: surface
? surface.y * target.scale[1] - drop
: (target.asset.surface?.height ?? target.asset.dimensions[1]) * target.scale[1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hanging place ignores explicit Y

Medium Severity

The tool text says a ceiling item hangs from the underside unless position[1] is above 0, but explicitY is !hanging && requestedPosition[1] > 0. Hanging placements cannot override height. If no underside covers the point, the item is put at the host’s resting height with no drop, so a pendant sits in the top surface.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 56a1bd8. Configure here.

wass08 and others added 2 commits October 2, 2026 13:53
…ene; read_source

source.code becomes source.script, the sha256 of the module stored as a
text/javascript artifact, so publishing a scene no longer publishes the code
and an edit no longer carries it twice against the operation cap. The
compiler returns both hashes and keeps the inline manifest under 24 KiB
(outlines thinned to 16 corners, then the smallest surfaces dropped). The
artifact store reads text back; author_object rebuilds from params alone;
read_source (shared contract, read-only) returns the module and its params.
MCP hosts gain readArtifact. Inventory 68.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…em's pipeline

A window or door takes the same source as an authored item when its fields
cannot express the design. add_window/add_door accept code and params
(compiled and stored as author_object does, on MCP and in the chat); the
size is the compiled bounds. The window and door systems keep the hitbox
and wall placement but leave the frame to the renderer, which shows the
artifact through the item's model path (paint slots, clips, lights,
settling); the wall cuts the script's cutout mesh, or the outline until it
loads. The Parameters panel replaces the parametric frame's fields, the
open clip is the Open control, author_object edits and read_source reads
any scripted node. Script helpers (scriptSource, scriptInteractive,
scriptedSize) move to core/lib/geometry-script-node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

}
entry.area += area
entry.points.push([a.x, a.z], [b.x, b.z], [c.x, c.z])
undersides.set(key, entry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Undersides merge across heights

High Severity

Underside grouping keys a mesh only by normal direction and omits plane offset, so parallel faces at different heights on one mesh collapse into a single plane. A tray or stepped soffit then reports one height across the whole footprint, and hanging fixtures snap to the wrong underside.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3fd2614. Configure here.

const underside = geometryUndersideAt(manifest, x, z)
if (!underside) return null
const drop = mountsFlush(child.asset) ? 0.02 : (child.asset.dimensions?.[1] ?? 0)
return [child.position[0], underside.y * sy - drop, child.position[2]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resettle drops flush tilt

Medium Severity

Rebuild resettle only rewrites a hanging child's Y and never its rotation, and the gap is a vertical drop rather than an offset along the underside normal. Flush fixtures keep a stale tilt when a vault's slope or height changes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3fd2614. Configure here.

Comment thread packages/core/src/agent-operations/author-object.ts Outdated
…hor_object scripts a native opening

The compiler re-samples clips through a mixer whose action looped, so the
sample at the clip's end wrapped to its first frame: an open clip opened,
then snapped shut, and closing (open reversed) ended open. The action now
plays once and holds; a regression test reads the stored GLB back.

author_object with new code may give an existing window or door its script,
keeping its place and mark, instead of the agent deleting and re-adding it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

const underside = geometryUndersideAt(manifest, x, z)
if (!underside) return null
const drop = mountsFlush(child.asset) ? 0.02 : (child.asset.dimensions?.[1] ?? 0)
return [child.position[0], underside.y * sy - drop, child.position[2]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flush offset ignores surface normal

Medium Severity

place_item and resettledPosition pull a flush fixture down by 0.02 on Y only. The editor face-host offsets along the underside normal. On a sloped vault the MCP/rebuild pose is not flush and can intersect the plane.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77bc1ff. Configure here.

? requestedPosition[1] - hy
: surface
? surface.y * target.scale[1] - drop
: (target.asset.surface?.height ?? target.asset.dimensions[1]) * target.scale[1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Porch placement snaps to roof

Medium Severity

When the target point is not over a receiving surface, place_item sits the item at asset.surface.height or the full bounding-box height. Shelter parts (roof, canopy) are excluded from rest surfaces, so a point under a porch overhang lands at roof height instead of on the landing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77bc1ff. Configure here.

if (helper) child.visible = false

const captured = captureItemMeshMaterials(mesh)
if (mesh.name !== 'cutout') meshEntries.push({ mesh, captured })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rebuild resets open state

Medium Severity

Each rebuild writes a new artifact:// src, which remounts the model via key={node.asset.src}. That tears down initItem and recreates controls from defaults, so an open garage door or playing clip closes when a parameter is changed.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77bc1ff. Configure here.

if (SOURCE_GUARD.test(input.code)) {
throw new Error(
'Geometry scripts cannot use dynamic import, eval, Function constructors, process or require',
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Guard rejects the word process

Medium Severity

SOURCE_GUARD matches the standalone word process anywhere in the module, including comments and ordinary identifiers. A script that says // process each vertex or function process(mesh) fails compile with a sandbox error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77bc1ff. Configure here.

wass08 and others added 2 commits October 2, 2026 14:55
…drive; scripts add an open clip

A window or door built from a script kept the parametric arrows and
rounding dots, which moved its box without changing the script. Its handles
now come from the script: a width param keeps the side arrows, a height
param the top arrow, both rebuilding the script on release (the opposite
edge kept, in one undo step); without them only the door's move handle
shows. The 2D width arrows hide on scripted openings. add_door/add_window
ask for an open clip unless the opening is fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…r on what renders, and open by default

- GLB export: a window or door built from a script bakes its own clips
  (through the item path) instead of the parametric door/window baker, so
  its open/close reaches the file.
- The selection toolbar of a scripted node sits above its visible meshes,
  not its hidden hit box, cutout and collider; without a height param there
  is no height arrow to clear, so it uses the default lift.
- The hit box of a scripted opening is the script's bounds, never stale
  width/height fields.
- author_object's motion rule asks a window or door for an open clip (the
  path that scripts an existing door), and a position passed to it is taken
  as given; only without one does the bottom edge stay put.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

? requestedPosition[1] - hy
: surface
? surface.y * target.scale[1] - drop
: (target.asset.surface?.height ?? target.asset.dimensions[1]) * target.scale[1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flush offset ignores surface normal

Medium Severity

Hand placement seats a flush fixture 0.02 along the underside normal, but place_item and resettledPosition subtract 0.02 on Y only. On a sloped vault the can is embedded or left floating, and a rebuild moves a correctly placed can off the surface.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 63df9f1. Configure here.

round(item.position[0] + Math.cos(yaw) * lx + Math.sin(yaw) * lz),
round(item.position[1] + y * sy),
round(item.position[2] - Math.sin(yaw) * lx + Math.cos(yaw) * lz),
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Item queries ignore pitch and roll

Medium Severity

find_by_type and place_item convert between level space and an item only with yaw and scale. This PR adds X/Z rotation on items, so part bounds and drop points are wrong on a rolled or pitched host.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 63df9f1. Configure here.

…hor_object is for objects

The door's guidance (an open clip, width/height params) lives on add_door,
but every update went through author_object, which never mentions doors:
the scripted door came out without its open clip. A window or door is now
created and rebuilt through its own tool: add_window/add_door take nodeId
(new code, or the stored script with new params; wallId becomes optional)
through a rescriptOpening operation shared by the chat, the MCP, the
Parameters panel and the resize arrows. author_object refuses an opening's
id with where to go, and its description drops the door sentence. A wider
rebuild stays on its wall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ

@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.

Stale Bugbot comment from a previous run.

const [, height] = scriptedSize(source.manifest)
return (
<group position-y={-height / 2} ref={ref} userData={{ [SCRIPTED_MODEL_FLAG]: true }}>
<ScriptedModel setSettled={setSettled} view={view} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Opening lights sit too high

High Severity

Scripted door and window lights use the opening's center as the origin, while scriptInteractive stores positions from the artifact's bottom-center. The mesh is registered at the opening center and the model is only shifted down visually, so bulbs appear half a height too high.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff8b3de. Configure here.

const underside = geometryUndersideAt(manifest, x, z)
if (!underside) return null
const drop = mountsFlush(child.asset) ? 0.02 : (child.asset.dimensions?.[1] ?? 0)
return [child.position[0], underside.y * sy - drop, child.position[2]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flush fixtures ignore sloped undersides

Medium Severity

Hanging a flush fixture from an authored underside subtracts a world-Y drop from the plane height and leaves X and Z unchanged. On a vault the can leaves the surface, unlike the editor face host which offsets along the hit normal.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff8b3de. Configure here.

intensityRange: [0, light.intensity],
distance: light.distance,
offset: light.position,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Authored lights ignore object rotation

Medium Severity

Light positions from the script are artifact-local, but the item light pool adds them on world axes to the object's origin. Rotating a porch or lantern leaves the bulbs behind in world space.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff8b3de. Configure here.

wass08 and others added 2 commits October 2, 2026 16:30
…ame with a CSP

Model-written code ran in a same-origin worker with network globals
removed: a deterrent, not isolation. It now runs in a worker inside a
hidden sandboxed iframe (allow-scripts only: opaque origin, no cookies,
storage or access to the editor) whose CSP allows only its own scripts and
blob workers; the worker inherits it, so the code cannot reach the network,
load fonts or images, or open frames (checked: the same fetch succeeds
without the CSP and fails with it). The compiler ships as one classic-script
bundle (@pascal-app/geometry-script/sandbox, built by scripts/build-sandbox)
that the frame starts from a blob, loaded only on the first compile. The
editor hashes the returned GLB and the code itself; nothing the sandbox
reports about itself is trusted. The same-origin worker and its window shim
are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
…becomes get_source

The tools follow the inventory's verbs: add_<kind> creates (and, with
nodeId, rebuilds) a kind, get_* reads, update_node/delete_node edit any
node. Both are unreleased, so the rename costs nothing outside this branch.
Identifiers and files follow (addObjectTool, addObject, add-object.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ
@wass08 wass08 changed the title Authored objects: agents build items in plain three.js Authored geometry: agents build objects, windows and doors in three.js Oct 2, 2026

@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 1 potential issue.

There are 24 total unresolved issues (including 23 from previous reviews).

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 079691c. Configure here.

(
node as { asset?: { interactive?: { effects?: { kind: string }[] } } }
).asset?.interactive?.effects?.some((effect) => effect.kind === 'animation'),
) || itemClipRegistry.has(node.id as string),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Scripted openings freeze when batched

High Severity

Scripted doors and windows play their open clip through item interactive state, but doorBatchable and windowBatchable only skip a node that is in doorAnimations or windowAnimations. A scripted garage door or sash therefore stays in the wall batch, so its clip never moves the visible mesh.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 079691c. Configure here.

@wass08
wass08 merged commit 67f8041 into main Oct 2, 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