Authored geometry: agents build objects, windows and doors in three.js - #994
Conversation
…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
What this PR doesIt 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,
Good place to start: the design note |
| } | ||
| open.paused = false | ||
| open.timeScale = -1 | ||
| open.play() |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 042d242. Configure here.
| ) | ||
| for (const { id, data } of changes?.update ?? []) { | ||
| useScene.getState().updateNode(id as AnyNodeId, data) | ||
| } |
There was a problem hiding this comment.
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.
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 }) |
There was a problem hiding this comment.
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)
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) |
There was a problem hiding this comment.
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.
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 })) |
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this comment.
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)
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
| const rebuild = (id: string, value: GeometryScriptParamValue) => { | ||
| setBusy(true) | ||
| setError(null) | ||
| rebuildAuthoredObject(node.id, { ...source.params, [id]: value }) |
There was a problem hiding this comment.
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)
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], |
There was a problem hiding this comment.
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)
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
| nodeType: node.type, | ||
| levelId: level, | ||
| }) | ||
| continue |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 15f11ec. Configure here.
| manifest: Manifest & Pick<GeometryArtifactManifest, 'bounds'>, | ||
| ): number | null { | ||
| return highest(mainSurfaces(manifest))?.y ?? null | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 15f11ec. Configure here.
| }, COMPILE_TIMEOUT_MS) | ||
| pending.set(id, { resolve, reject, timeout }) | ||
| active.postMessage(request) | ||
| }) |
There was a problem hiding this comment.
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)
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
| ? requestedPosition[1] - hy | ||
| : surface | ||
| ? surface.y * target.scale[1] - drop | ||
| : (target.asset.surface?.height ?? target.asset.dimensions[1]) * target.scale[1] |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 56a1bd8. Configure here.
…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
| } | ||
| entry.area += area | ||
| entry.points.push([a.x, a.z], [b.x, b.z], [c.x, c.z]) | ||
| undersides.set(key, entry) |
There was a problem hiding this comment.
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.
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]] |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 3fd2614. Configure here.
…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
| 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]] |
There was a problem hiding this comment.
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)
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] |
There was a problem hiding this comment.
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)
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 }) |
There was a problem hiding this comment.
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)
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', | ||
| ) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 77bc1ff. Configure here.
…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
| ? requestedPosition[1] - hy | ||
| : surface | ||
| ? surface.y * target.scale[1] - drop | ||
| : (target.asset.surface?.height ?? target.asset.dimensions[1]) * target.scale[1] |
There was a problem hiding this comment.
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)
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), | ||
| ] |
There was a problem hiding this comment.
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)
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
| const [, height] = scriptedSize(source.manifest) | ||
| return ( | ||
| <group position-y={-height / 2} ref={ref} userData={{ [SCRIPTED_MODEL_FLAG]: true }}> | ||
| <ScriptedModel setSettled={setSettled} view={view} /> |
There was a problem hiding this comment.
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)
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]] |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit ff8b3de. Configure here.
| intensityRange: [0, light.intensity], | ||
| distance: light.distance, | ||
| offset: light.position, | ||
| }) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit ff8b3de. Configure here.
…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
There was a problem hiding this comment.
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).
❌ 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), |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 079691c. Configure here.


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: anitem, or awindow/doorwhose 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, typedpart:<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="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.sourceonitem,window,door(script hash, params, GLB hash, inline manifest ≤ 24 KiB); code is a separatetext/javascriptartifact, never in the scene.artifact://resolved through a hostArtifactStore. Surface/underside helpers,resettledPosition,flushMountRotation,scriptedSize,scriptInteractive.add_object— create or rebuild (bynodeId) 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 optionalcode/params; withnodeIdthey rebuild an opening (operationrescriptOpening).get_source— the module and params of any scripted node.find_by_type— nodes and typed parts of one type.place_itemlands on an object's real surfaces, hangs ceiling items from its undersides.cutoutmesh (real shape); the window/door systems keep hitbox and placement; panels show the script's params; handles only for declaredwidth/heightparams; the Open control plays theopenclip; 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.ymlpublishes geometry-script before the editor. MCP inventory: 68 tools.How it was tested
bun run ciin a clean clone with a frozen install: lint, skills, typecheck 13/13, tests 15/15 packages, build 9/9.nodeId; sandbox compile from the chat; fork; bake.🤖 Generated with Claude Code
https://claude.ai/code/session_01QUGvAXaawxPa5rnLSE53iZ