Skip to content

Framing build animation: a lazy nested reveal, an animation speed, and a level that follows the project - #1093

Merged
wass08 merged 47 commits into
pascalorg:mainfrom
AxiomeCG:feat/framing-build-animation-20261015
Oct 16, 2026
Merged

wass08 merged 47 commits into
pascalorg:mainfrom
AxiomeCG:feat/framing-build-animation-20261015

Conversation

@AxiomeCG

@AxiomeCG AxiomeCG commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Framing build animation: a lazy nested reveal at the Framing level, one pace, an animation speed, a level that follows the project, walls that keep their finish through the rise, and a reveal that follows edits, deletions and the person's display

Base: pascalorg/editor main (bb7d014c0, #1001), merged in, not rebased. Head: branch feat/framing-build-animation-20261015 (faa6e27c3), from the AxiomeCG fork. 47 commits; squash-merge.
Pairs with the private-editor PR "Framing build animation: Bones builds each wall in its preroll". That PR pins this head and must be re-pinned to the squash commit once this one merges.

What is in it (read this first)

  • Framing gets its own schedule in the construction reveal (it was treated as Full): floors, rooms and pieces staggered, roofs after the walls, furniture last, at one pace (FRAMING_PACE 1.75) and with a cap that grows with the build. Full, Simple and Off keep their tables (a test pins them).
  • Animation speed setting, ×0.5 to ×2, read when a plan is made.
  • The level follows the project: Framing where the framing plugin is installed, Full where it is not; the Framing option is greyed with a tip and wears the Pro badge when it is not available.
  • Walls keep their finish while they rise (texture scale per metre, preset re-skin). Full gains it too.
  • The animation follows what happens: a build always shows (even on a hidden level), an edit replays its reveal, a deletion plays the creation backwards, deleting a room or a floor takes it apart, a roof's covering is laid from the eave to the ridge, the cutaway never hides what plays, Follow Pascal pulls back to the whole house and looks at an outside edit from outside.
  • Plugin hooks for staging members in a preroll (setRevealPreroll, getRevealTurn, revealClockMs, getRevealPlanSpeed, emitRevealPuff, commitRevealCandidates, planLazyReveal).
  • Core gains four optional registry fields (derivedWrites on a commit; revealOnEdit, exterior and revealLayout on a kind). No schema, no migration, no mcp or app change.

What changes for a person

The Construction animation setting already offers Framing, described as "Full, plus the framing members dropping in first, where the framing plugin draws them". Today the viewer treats Framing exactly as Full and nothing reads it. This PR gives Framing its own schedule in the construction reveal and two settings around it.

The Framing schedule

  • floors a beat apart, the rooms of a floor a beat apart, the pieces of a room one by one around it, every start jittered;
  • roofs once every floor is up, then furniture room by room;
  • a big build takes proportionally longer instead of being squeezed (cap 70 s at the Framing pace);
  • the whole Framing timeline runs at one pace, FRAMING_PACE = 1.75: a build that took 20 s now takes about 35 s, and a smaller or larger one scales the same;
  • a rise, scale or settle overshoots and settles (back.out), gentler for a rising wall (about 2 % past its top, so it does not push into the slab over it while floors build side by side);
  • a new node whose kind declares no reveal, added where its host was already there, comes in on a default reveal instead of popping (under a host still coming in, it comes in with it).

Full, Simple and Off keep their schedules. Full reads REVEAL_TIMING / RETRACT_TIMING and a test pins their tables as literals.

Animation speed

Settings, Preferences, under Construction animation: Animation speed ×0.5, ×0.75, ×1, ×1.5, ×2 (default ×1). Every time of a schedule is divided by the speed, caps included: Full, Framing (at its pace) and the undo's rewind. ×1 returns the shipped tables themselves. The speed is read when a plan is made: what is already moving keeps its own durations, so a change applies to the next build and never moves one that is playing. Persisted with the level under the same key (pascal-construction-reveal); a person saved before it is at ×1.

The level follows the project

The level that plays is the person's choice, or, with none of their own, Framing where the project has the plugin that draws the framing and Full where it has not. Nothing is stored for the default; it is read per project.

Case Level that plays Framing option
no choice, framing plugin available Framing enabled
no choice, plugin absent Full greyed, tip "Framing needs the Architect plugin."
chose Full / Simple / Off the choice per plugin
chose Framing, plugin removed Full (the choice is kept) greyed; Framing returns with the plugin
level saved before this change that level, as a choice per plugin

"Available" is one read: the kind bones:framing is registered and isNodeKindEnabled says its plugin is installed in the project. The editor never names a plugin id (on main it resolves to pascal:procedural). Hosts read useConstructionRevealLevel() / getConstructionRevealLevel() instead of the saved level. A SegmentedControl option can be disabled (greyed, stays hoverable so a tooltip can say why).

Walls keep their finish through the rise

A wall rises by scaling in Y, which squashed its textures while it moved and let a preset finish go stale. Now:

  • while a wall rises its UVs are counter-scaled along the vertical, so a texture keeps its tiles per metre (reveal-texture-scale.ts: the mesh draws a clone of its geometry, shared materials and cached geometry untouched; the clone goes when the reveal ends);
  • a wall face whose finish comes from a material preset (a slot, the assembly's cladding, a preset ref) re-skins when the library's materials change, and curtain-wall materials resolve preset refs: the wall batch watches the library version and the face signature carries the preset.

This is the same rise Full plays, so Full gains it too.

The animation follows what is built, edited and removed

  • A build always shows. The level a reveal plays on shows while it plays, whatever the level display hides (levelsRevealing); the reveal publishes the levels it works on. An undo plays back the kinds that came in with a default reveal.
  • Follow Pascal shows the whole house. It pulls back to the finished house with the floors above shown, and the person's display (cutaway, floor, hidden floors) comes back when it ends.
  • An edit replays its reveal, quickly. Each kind lists the fields that make its picture (revealOnEdit); a commit that changes one replays the node's reveal. What the structure reconciler writes as a consequence of an edit (derivedWrites) is not an edit and does not replay. Many edits begin spread over one second (the cap spread over the pieces), not piled on its last frame.
  • A deletion plays the creation backwards, from a snapshot of the node, through the reveal's deletes prop.
  • A roof is covered, not dropped. At the Framing level a roof keeps its frame (Bones' frame falls in a preroll of its own) and its covering is laid over it in place, from the eave to the ridge: the node holds still and its meshes wear a world-height mask whose level moves with every frame from the lowest edge to the highest (an ease in and out, no steps; a flat roof sweeps along its longer plan side), in a time that follows its size: 450 ms for each metre of sweep at pace 1, never under 1.4 s nor over 4.2 s (at the Framing pace 0.79 s a metre, 2.45 s to 7.35 s); its parts are not staged on their own. Undo and a deletion lift the covering off from the ridge, the same sweep backwards in 0.6 of that time. The speed setting scales all of it. Full, Simple and Off are unchanged.
  • Deleting a room or a floor takes it apart. At the Framing level a deletion (and an undo) plays the build's own plan read from its end: the furniture lifts out first, then the roof's covering, the openings, the walls and the slabs last, the floors from the top down, each room as it went up, with the prerolls a plugin asked kept after each piece's going. A deleted floor's pictures stand in their building, so they outlive the floor's own object. A derived plate or ceiling goes with its room (the reconciler derives them; a plate it merely derives anew is not a deletion). Capped at 14 s however many pieces it takes back. Full keeps its rewind.
  • The cutaway never hides what plays. The reveal publishes the nodes playing (moving in, going out, carried, an edit replaying: nodesRevealing); in cutaway the wall cutout cache keeps a wall visible for as long as it, an opening it hosts or a node it stands under plays, and goes back to what the view wants after. The person's wall mode is never written; walls down stays their choice. A deleted wall plays its deletion in its own picture (the wall's visible variant, as a capture draws it), not under the cutaway, unless that palette cannot draw the groups the copy carries (a room-wide paint preview was live): then the copy keeps what it wears.
  • Follow looks at an outside edit from outside. An exterior wall bent, a window or door moved along one, a roof or the ground edited is an outside step: Follow sets the walls up for it and frames the exterior face (it would have fallen back to the cutaway, which hides the wall that plays), and puts the presentation back. A commit that changes one such piece takes the outside view for all of it; a build keeps its single cutaway and never writes the walls up in between; a mode the person picks meanwhile is theirs. A window or door moved along its wall replays (position joins revealOnEdit); a wall moved does not replay its windows. sceneViewFacing answers for an exterior wall (the way its exterior side faces).
  • What stands above a deleted floor waits for it. Deleting a storey re-stacks the levels above it at once (a roof level drops by the storey's height), while the floor still comes apart for seconds. The levels above a deleted floor now keep their place while its pictures remain, then come down onto what is below in one eased move, in the rewind's own drop time at the level and speed (systems/level/level-hold.ts, read by LevelSystem ahead of the stacked elevation; presentation only, no scene write). Everything a held level holds goes with it (the roof, its Bones frame). A load, a capture, an export, the reveal stopping and an undo release the hold at once. A level object that has just mounted (a restored floor, a new storey, a load) now cuts every level to its height in the same frame instead of sliding about 250 ms, so an undo never runs a floor and its roof through each other.
  • A guard test (reveal-coverage.test.ts): every built-in kind an agent can create is staged by the reveal, on a level and on a roof, at the Full and Framing levels; a kind that declares revealOnEdit lists only fields its schema has.

Framing wears the Pro badge

The plugin that draws the framing is a Pro one on main (the Architect plugin pulls it in), so the Framing option wears the mark the plugin manager shows next to a plugin that needs a plan. The manager's Badge becomes a shared primitive used by both.

For plugins

The reveal exposes what a plugin needs to stage its own members at the Framing level:

  • setRevealPreroll asks for a preroll before nodes of the lazy schedule move in; the schedule leaves the time and the node starts after it;
  • getRevealTurn and revealClockMs give each node's turn on the reveal's clock (a preroll already playing keeps its turn when the plan is made again);
  • getRevealPlanSpeed tells a plugin which speed the plan in force was made at;
  • emitRevealPuff lends the reveal's dust;
  • commitRevealCandidates, planLazyReveal, LAZY_REVEAL_BASE_TIMING, LAZY_REVEAL_TIMING, pacedLazyTiming, FRAMING_PACE are exported from @pascal-app/viewer.

Candidates carry their room (cluster): the room a node stands in (for a node on a line, the room on either side of its middle), else runs of the lines that meet end to end. What is a room and what is a line is declared by the kind (capabilities.revealLayout: the zone and the wall declare it), so a plugin kind that declares the same is laid out the same way.

Scope

packages/viewer (reveal schedule, candidates, pose, texture scale, wall material signature, construction-reveal system, level system, exports), packages/editor (the construction-reveal store, the two settings, the Pro badge, SegmentedControl's per-option disabled, Follow Pascal), packages/core (derivedWrites, revealOnEdit, exterior, revealLayout), and packages/nodes (the wall batch's library-version watch, curtain-wall preset refs, each kind's revealOnEdit list, exterior and revealLayout declarations, the coverage guard). 99 files, +8,587 / -330, of which 1,090 lines are one JSON test fixture and ~3,800 are tests and fixtures. Core gains four optional registry fields (derivedWrites on a commit; revealOnEdit, exterior and revealLayout on a kind), nothing else; no mcp or app change. No schema, no migration.

How to test

  1. Settings, Construction animation: Framing (needs a project where the framing plugin is installed; otherwise it is greyed with its tip "Framing needs the Architect plugin.").
  2. Ask the agent for a multi-room, two-storey build (any commit that creates many walls).
  3. Watch: slabs first, floors offset, rooms side by side, roof after the walls; nothing pops.
  4. Settings, Animation speed: ×2 and ×0.5; the next build plays in half and double the time.
  5. Watch a textured or painted wall rise: its texture keeps its scale and its finish is the one it settles in.
  6. Switch to Full and repeat: the schedule is as before (it gains the wall finish fix).
  7. Edit a built piece (change a wall's height, move a door): it replays its reveal in a moment; a stair's or a roof's structure follow-up writes do not.
  8. Delete a piece (select, Delete): it plays its creation backwards; undo plays it in again.
  9. At Framing, ask for a build with a roof: the roof's frame falls and stays, then the covering closes over it continuously from the eave to the ridge, a small roof in about 2.5 s and a large one in at most about 7 s (it is not dropped whole); delete the roof and the covering lifts off from the ridge.
  10. At Framing, delete a room, then a whole floor: furniture lifts out, the roof covering and openings go, then the walls and the slabs, the top floor first and each room as it went up; undo plays it back in. At Full the rewind is as before.
  11. With the cutaway on, ask for a build: nothing that plays is hidden by the cutaway while it plays (a wall a window is being added to stays up for it); delete a wall: it plays in its own picture, not stippled.
  12. Turn Follow Pascal on, bend an exterior wall or move a window along it: the camera looks at it from outside with the walls up, then your display comes back. Turn Follow Pascal on and ask for a build: the camera pulls back to the whole house, floors above shown; when it ends the display you had comes back.

Architecture review (16 Oct)

A read-only architecture review (the editor's review-architecture skill) found two blockers, both the same rule broken (DECISIONS.md E-002: no kind-specific branching keyed on node.type in packages/viewer or packages/editor; the behaviour belongs on the kind's definition). Both are fixed in this PR; the review's suggestions and nits are not part of this change.

  • Follow Pascal's "seen from outside" (editor). follow-pascal-exterior.ts held a hand-kept set of 22 kind names and node.type branches for wall, door, window, slab and the storey kinds. It now asks the registry: the new optional capabilities.exterior?: boolean | ((node, nodes) => boolean | undefined). true for the roof, roof segment, every roof accessory, the fence and its gates, the site; false for level, building and zone; a function for the wall (an exterior face) and for the door and the window (in a roof segment, or in such a wall). The helper keeps its walk up the parents (a true kind outdoors, up to the first false one) and its "stands in no storey" fallback. Six of the names were not kinds on main (ramp, rock, terrain-tile, terrain-break, facade, and fence-feature, whose real kinds are fence-gate and fence-opening) and a slab's terrainMode is a field no schema has: they are gone, and no real node's answer changes. A plugin kind now says it stands outdoors the same way.
  • The reveal's host, rooms and lines (viewer). (1) A node hosted on a roof defaulted to a drop through parent.type === 'roof': it is now any ancestor whose capabilities.reveal.style is assemble (only the roof declares it, so the result is the same). (2) The deletion ghost of a wall the cutaway was hiding was dressed through a node.type === 'wall' check and the wall's own palette builder: the wall cutout (WallCutoutCache) now keeps the wall's visible palette on the mesh it cuts away (visibleMaterialsOf), and the ghost wears it, with the hasMaterialsForGroups guard kept; the reveal names no wall kind. (3) The room-by-room layout needed "this node outlines a room" and "this node stands on a line": the new optional capabilities.revealLayout?: { room?, line? }, set by the zone (room: its polygon) and the wall (line: its two ends). The as unknown as { start; end } casts are gone, and the plates and ceilings a deleted room or wall takes with it are those of any kind that declares a revealLayout.
  • Both fields are documented in wiki/architecture/node-definitions.md (capability reference) with their doc comments in packages/core/src/registry/types.ts.
  • New tests: a plugin-style kind that declares the fields is treated as the built-in it imitates: outdoors for Follow (always, by a field, by what holds it); staged on an assembled host; laid out in its room and run along the lines it meets; taking a derived plate with it when it goes (reveal-declared-kinds.test.ts, follow-pascal-exterior.test.ts); what the built-in kinds declare (exterior-declarations.test.ts); the cutout keeps the visible palette (wall-cutout-cache.test.ts).

Bugbot (16 Oct)

  • Stale derived writes skip later reveals (history-control.ts): real. A derived write that set a value the node already had, deleted a node that was not there, or cancelled out inside a history step left its note in the module map, and the next real commit inherited it; applyNodeChangesActionImpl never noted changes.delete. Notes are now taken only for the fields whose value changes and the nodes that exist, after the last early return; a no-op commit or transaction clears the map; deletes through applyNodeChanges are noted as the whole node. Six tests in derived-commit-nodes.test.ts (they fail before the fix).
  • Bugbot round 2 (16 Oct), three findings, all real (each reproduced by a test that failed before the fix):
    • Loose wall runs ignore outline order (reveal-candidates.ts): layoutRuns walked an outline breadth-first, so a run of six was two stretches; it walks depth-first from a free end (open chain) or the first wall (closed outline), so a run is one stretch. New reveal-candidates-runs.test.ts (3 tests).
    • Follow outdoor latch raises walls mid-build (follow-pascal.tsx): outsideBuild was read from the global edit queue at every word and kept for the session, so an exterior edit queued by another commit, or an exterior commit just before an interior one, took a build outdoors and wrote the walls up between its steps. Only an edit's own words (changed) read the queue now, a build or a deletion clears the latch, and the first edit after the reveal said complete starts from false. Two tests in follow-pascal.test.tsx.
    • Deletion ghosts omitted from nodesRevealing (construction-reveal.tsx): a ghost is out of the scene, so the cutaway could not find the wall a departing opening stood in. Each ghost keeps its host (the nearest node the commit leaves in the scene that is not a storey, building or site; CONTAINERS is exported from reveal-candidates.ts) and nodesAtPlay publishes it. Test "the wall an opening is leaving stays in play for as long as its picture plays".
  • Bugbot round 3 (16 Oct), one finding, real (reproduced by a test that failed before the fix):
    • Fallback Full click erases Framing (construction-animation-setting.tsx, use-construction-reveal.ts): the control shows the level that plays, so a saved Framing shows as Full while the plugin is gone, and a click on that pressed Full wrote chosen: true, level: 'full' over the kept Framing. chooseConstructionLevel now ignores a click on the level that plays when the person has already chosen (the kept choice comes back with the plugin); any other level, or a click with no choice yet, is a choice as before. Tests: "clicking the level that plays" (3).

Merged with main again on 16 Oct (#1001)

upstream/main moved to bb7d014c0 (#1001: local plugins, surface stairs and deck railings) and merged in (ba4cc7323). Two content conflicts, in nodes/src/fence/definition.ts and nodes/src/stair/definition.ts: main added hostRefFields beside this PR's revealOnEdit in the same capabilities block; both are kept. Every suite green after it (viewer 805, editor 2587, core 4955, nodes 4960, plugin-bones 1404, hosted 2682, typecheck 26/26; load timeouts green when rerun alone).

Merged with main on 16 Oct

upstream/main moved to 3dd378b8e (#1085 kitchen catalog and magnetic placement, #1061 @types/three 0.186, #1091 room-wide paint preview keeps walls raycastable) and merged in (d7e00a05a, a merge commit, no rebase). One content conflict, in packages/viewer/src/systems/wall/wall-cutout-cache.ts:

  • Both sides edited update() at the same spot: this PR's in-play stamps (wallsInPlay, playChanged) and main's loop that re-applies the walls a paint preview deferred. Both are kept. The in-play block runs first, so this.inPlay is current when main's loop calls apply, which now reads it for hidden; then main's deferred loop; playChanged still joins invalidated and the per-wall apply condition. In apply the branch's hidden expression and main's mayAssign guard around wall.mesh.material = … are both kept.
  • A wall deferred by the preview is still applied, and a wall in play is still stamped. New test in wall-cutout-cache.test.ts ("a wall in play while a room-wide preview holds its groups is stamped now and given its palette when the preview lets go"): the play starts under a preview that holds the wall's groups (stamp visible at once, preview kept), the preview lets go while it still plays (visible palette), the play ends (hidden palette and stamp). It fails if main's loop or playChanged is dropped.
  • The deletion ghost (construction-reveal/ghost-dress.ts) assigned the wall's visible palette to its copy without main's guard. It now assigns it only when hasMaterialsForGroups(copy, palette) holds, so a deletion during a room-wide preview cannot leave a copy with a group and no material (a raycast over it would throw on material.side). Test in ghost-dress.test.ts; two tests that used a six-group box with a three-entry palette now give the mesh the one group a wall's geometry has (ghost-dress.test.ts, construction-reveal.test.tsx).
  • @types/three 0.186 raised no type error in this PR's code.

Merge with main (15 Oct)

Main was merged in twice (upstream/main 683ce0c22 then dcd304a30); the Framing work is unchanged by it except for what is listed here.

  • SegmentedControl: main added a per-option disabled at the same time. Main's version is kept (isDisabled, opacity-40); the one difference is that the native disabled attribute stays the control's own, so a greyed option keeps aria-disabled, ignores its click and stays hoverable for its tooltip.
  • roof-segment/definition.ts: main added paint and slots, this PR revealOnEdit; both are kept. The list also gains barrelRise, barrelSegments, roofSystem and glazing (the fields main's barrel roofs and glazed roofs added), with a test.
  • node-batch.test.ts and follow-pascal.test.tsx: main's Biome 2.5 reformatted the tests this PR also edits; main's layout is taken and the PR's cases (revealing, the floors-above-shown assertion, the 1.5 s build) are kept. Three other test files are reformatted with Biome 2.5.
  • A type annotation in follow-pascal.test.tsx (main's toEqual is stricter).

How it was checked

Run on the head (f936603cd), with the dists rebuilt first (turbo build of the editor packages, 7 of 7 tasks), bun install --frozen-lockfile on the merged lockfile:

  • packages/viewer: bun test src 803 pass, 0 fail, Ran 803 tests across 90 files (with the held levels above a deleted floor: Full, Framing, speed, Off, undo mid-dismantle and after).
  • packages/editor: bun test src 2534 pass, 0 fail.
  • packages/core: bun test src 4939 pass, 0 fail.
  • packages/nodes: bun test 4997 pass, 1 skip, 0 fail; the rise test mounts real walls (solid and rendered shading), drives a construction reveal and checks each frame that the faces keep their materials and metre-scale UVs, then that the settled batch has the same UVs.
  • reveal-add-volume.test.ts runs a frozen two-storey, three-room commit (__fixtures__/add-volume-commit.json): every wall, slab, room and roof goes through the timeline; walls are grouped by room; slabs first; at most two starts in a 60 fps frame; the build is never a pulse.
  • New tests: each speed step scales Full, Framing and the rewind by exactly 1/speed; ×1 equals today's tables; caps scale; raising the speed mid-wall leaves its progress per frame at the old rate and the next build plans at the new one; persistence and reload; the default, the greyed option, its tooltip, the fall back and the saved choice (store and drawn control); a wall in play under a room-wide paint preview; a deletion ghost under one.
  • turbo run check-types --continue: 11 of 13 tasks pass. Two fail, both main's: editor#check-types (8 TS2307 for react-scan, geist/font/pixel and @webxr/plugin, not installed in the test tree; the same 8 as on a clean upstream/main) and @pascal-app/editor#check-types (ceiling-bracket-batch.test.ts TS2578 and TS2352 on main's three/src/renderers/common/Attributes.js cast from [Deps] upgrade @types/three to 0.186 to match three r186 #1061; the file is not touched here, and a clean upstream/main checkout gives the same two errors and no other).
  • Biome on the 88 source and config files this branch changes against main: 0 errors (4 pre-existing plugin warnings in node-batch.test.ts, from main).
  • Not run: e2e, any browser or GPU check. The animation itself was not watched in this pass.

Limits

  • The cap is 70 s at the Framing pace (before the speed): a very large commit is pulled together past it.
  • The Framing schedule applies to what an agent commits (the host's reveals policy); a person's own edits stay instant.
  • Not scaled by the speed: the roof lifts and glides of a moved piece (not build animations).
  • A kind that declares only a default reveal replays nothing on an edit.
  • Follow looks at an edited piece, not at a deleted one.
  • An undo of a deleted floor is a cut, not a play (an undo plays nothing in this code base): the floor and the levels above appear at their heights in the same frame.
  • The undo of a turn that created a storey (retract, then history restore) is not held: a roof that existed before the turn still settles by the old lerp when that storey goes.
  • A person's delete plays for every local commit, so a tool that deletes and recreates a node shows both.
  • Main does not clamp a window or door to its wall when the wall is shortened (the window stays at 6 m on a 4 m wall): shortening a wall replays the wall only, and the window plays nothing. That is main's behaviour; R&D's clamp is not part of this PR.
  • The deconstruction plays the build's order read backwards, capped at 14 s: a very large deletion is compressed past it.
  • The covering sweep is a mask over the roof's meshes: a node with nothing drawn has an empty sweep and plays nothing.
  • Terrain tiles and breaks are not staged by this PR (main has none).
  • The Pro badge is static text in the editor package (the plugin that draws the framing is Pro on the hosted product); a host without that plan model would want it to come from the plugin's own panel badge.

🤖 Generated with Claude Code

AxiomeCG and others added 30 commits October 15, 2026 12:26
…r undeclared kinds

The framing level now plays on its own schedule (`planLazyReveal`,
`LAZY_REVEAL_TIMING`): floors a beat apart, the rooms of a floor a beat
apart, the pieces of a room one by one around it, every start jittered;
roofs once every floor is up, then furniture room by room. A big build
takes proportionally longer and is pulled together only past the 40 s cap.
Full keeps `planReveal` and its timing unchanged.

Candidates carry their room: the zone a node stands in (a wall, the zone
on either side of its middle), else runs of walls that meet end to end.

A plugin may ask for a preroll before nodes of the lazy schedule
(`setRevealPreroll`) and reads each node's turn on the reveal's clock
(`getRevealTurn`, `revealClockMs`); a preroll already playing keeps its
turn when the plan is made again. `emitRevealPuff` lends the reveal's dust.

On the lazy schedule a rise, scale or settle overshoots and settles
(back.out), drops and settles fall from higher, and drops anticipate.

A new node whose kind declares no reveal, added where its host was
already there, now comes in on a default reveal instead of popping; under
a host still coming in it comes in with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 1.6 a rising wall peaked 9 % past its top, into the slab over it while
the floors build side by side. The lazy schedule's overshoot is now per
style: rise 0.7 (about 2 % past), scale and settle 1.6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's case: one add_volume commit (two storeys, three rooms below,
a hip roof) staged at the framing level. Every wall, slab, room and roof
it makes goes through the timeline; its walls are grouped by the zones it
makes and the rooms go up side by side; slabs first, floors offset, the
roof once the walls are up, at most two starts in a 60 fps frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_PACE times as long

FRAMING_PACE (1.75) multiplies every offset, gap, beat, motion duration and the cap of the lazy
schedule, so a build that took 20 s takes 35 s and a smaller or larger one scales the same;
jitter, overshoots and heights do not move. The original numbers stay as LAZY_REVEAL_BASE_TIMING
and LAZY_REVEAL_TIMING is pacedLazyTiming(base, FRAMING_PACE). Full reads REVEAL_TIMING and
RETRACT_TIMING and is untouched; a test pins its tables as literals. The viewer exports
commitRevealCandidates and the pace for Bones and its measurements.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A speed of x0.5 to x2 divides every time of a schedule: Full's REVEAL_TIMING, Framing's lazy table
(at its pace) and the undo's rewind, caps included. x1 returns the shipped tables themselves.
ConstructionReveal takes a speed; it is read when a plan is made, the plan's speed is kept until
what waits has started, and what is already moving keeps its own durations, so a change applies
to the next build and never moves one playing. getRevealPlanSpeed tells a plugin that stages its
own pieces which speed the plan in force was made at.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… project

Settings, Preferences: Animation speed (x0.5, x0.75, x1, x1.5, x2), persisted with the level under
the same key and normalised to a step. The level that plays is the person's choice, or, with none of
their own, Framing where the project has the plugin that draws the framing and Full where it has not;
Framing is greyed out with a tip ("Framing needs the Bones plugin.") without it and falls back to
Full, and the saved choice comes back with the plugin. The plugin is read through the registered
framing kind and the project's installed plugins, so it holds wherever the kind lives. Hosts read the
level through useConstructionRevealLevel; a segmented control's option can be disabled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The plugin that draws the framing is a Pro one, so its level wears the mark the plugin
manager shows next to a plugin that needs a plan. The manager's badge becomes a shared
primitive, used by both.
Where the framing plugin is missing the Framing option is greyed out, and its tip no longer
names Bones: on the hosted product the plugin a person installs is Architect, which brings
Procedural and with it the framing kinds.
…e level display hides; an undo plays back the kinds that came in with a default reveal

A build above the selected level, in the solo view of another, or an undo of such a build, used to start at once and unseen. The reveal now publishes the levels it has work on (levelsRevealing); the level system shows them, the node batch re-offers its sources when they change, and Follow's line of sight counts them. Only the author's own eye on a level still keeps its build instant.
… by the reveal, on a level and on a roof, at the full and framing levels
…s above shown, and the person's display comes back when it ends
…es), and a kind lists the fields that make its picture (revealOnEdit)
…kly, and a deletion plays the creation backwards from a snapshot

Edits: nodes list revealOnEdit fields (wall, fence, slab, ceiling, column, stair, openings, cabinet, shelf, item scale, roof segment); a change to one of them from an edit, not from reconciliation, re-rises (or re-scales, re-settles) the mounted node at the animation speed, in the changes' groups. Deletions: the node is out of the store at once; a clone of its root, taken as the commit lands, plays the reverse reveal staggered like a build in reverse, and is removed. The host chooses which commits play deletions. Follow looks at an edited piece.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t frame

The edits' stagger is the cap divided over the pieces. The wiki says how an edit and a deletion play.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… schemas main has

Main's shelf and cabinet have no curveRadius, its roof segment no sweptProfile, and it has no facade kind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…is laid over it in place, from eave to ridge

An assembled node (the roof) played the same drop of its whole shell at the Framing level as at Full, after Bones' frame had been shown and taken away. Framing now plays it as a covering: the node holds still and its meshes wear a world-height mask that opens row by row from the lowest edge (the eave) to the highest (the ridge); a flat roof sweeps along its longer plan side. Its parts are not staged on their own. Undo and a deletion lift the covering off from the ridge in reverse, then the bare node goes. The speed setting scales the sweep. Full, Simple and Off are as they were: the Full tables are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ki says how a roof is covered at Framing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… room by room, as it went up

At the Framing level a deletion (and an undo) plays the build's own plan read from its end: the furniture lifts out first, then the roof's covering, the openings, the walls and the slabs last, the floors from the top down, each room as it went up, with the prerolls a plugin asked kept after each piece's going (getRetractTurn). A deleted floor's pictures stand in their building, so they outlive the floor's own object. Full keeps its rewind. The reverse plan is capped at 14 s however many pieces it takes back.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t stood in

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… a room is deleted

The slabs and ceilings of a floor are all derived by the reconciler, and a deletion skipped every derived surface, so the last pieces of a floor never came apart. They play when the commit also takes a wall or a room off their level; a plate the reconciler merely derives anew still is not a deletion.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… its size

The covering was laid in 4 to 14 steps, one row at a time, over a fixed 2.6 s. Its mask level now moves with every frame from the lowest edge to the highest (an ease in and out, no steps), over a time of 450 ms for each metre of sweep at pace 1, never under 1.4 s nor over 4.2 s (at the Framing pace 0.79 s a metre, 2.45 s to 7.35 s); a lifting off, for an undo or a deletion, plays the same sweep backwards in 0.6 of that. The speed setting scales all of it; Full is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An exterior wall faces the way its exterior side does: what a camera that follows an edit of it
looks at it along. An interior wall, and a wall with no exterior side, still answer null.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The reveal publishes the nodes playing (moving in, going out, carried, an edit replaying) in nodesRevealing; in cutaway the wall cutout cache keeps a wall stamped visible for as long as it, an opening it hosts or a node it stands under plays, and goes back to what the view wants after. The person's wall mode is never written; walls down stays their choice.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…om outside, with the walls up

An exterior wall bent, a window moved along one, a roof or the ground edited is an outside step: Follow sets the walls up for it and frames the exterior face (it would have fallen back to the cutaway, which hides the wall that plays). A commit that changes one such piece takes the outside view for all of it. A build keeps its single cutaway and never writes the walls up in between. The walls going up is a presentation change Follow puts back; a mode the person picks meanwhile is theirs. A window or door moved along its wall replays (position joins revealOnEdit).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eplay its windows

The viewer index exports commitEditCandidates for the test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er the cutaway

The picture a deleted wall plays is a copy of what stood there and wore the material its source had in that frame: for a wall the cutaway was hiding, the stipple, for the whole reverse play. The copy now wears the wall's own visible variant, as a capture draws it; a wall the cutaway showed and the person's other wall modes keep what they wear.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@AxiomeCG
AxiomeCG marked this pull request as ready for review October 15, 2026 23:07

@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/core/src/store/history-control.ts
… with a no-op commit

A derived write that set a value a node already had, deleted a node that was not there, or
cancelled out inside a history step left its note in the module map. The next real commit
inherited it, and the reveal read that commit's own edit as derived and skipped its replay.
Notes now take only the fields whose value changed and the nodes that exist, a no-op
commit or transaction clears them, and applyNodeChanges notes its deletes as the whole node.

@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/viewer/src/lib/reveal-candidates.ts
…0261016

# Conflicts:
#	packages/nodes/src/fence/definition.ts
#	packages/nodes/src/stair/definition.ts

@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/editor/src/components/editor/follow-pascal.tsx
…opening keeps its wall in play, Follow reads an outside edit per commit

- A run of loose walls is walked along the outline (an open chain from one of its ends), so a run is one stretch of it and the pieces come up around the outline.
- A deleted node's picture plays from a snapshot out of the scene: the node it hung on (when it stays and is not a storey or a building) is published in nodesRevealing, so the cutaway leaves the wall a departing opening stood in.
- Follow takes a commit outdoors only from the edit it is hearing: a build, a deletion or the edit after a finished play never inherits what another commit queued.

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

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

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 faa6e27. Configure here.

if (played.current) outsideBuild.current = false
const changed = outsideBuild.current ? [] : revealChangedNodeIds()
if (changed.length && anyExterior(useScene.getState().nodes, changed))
outsideBuild.current = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exterior latch clears mid-commit

Medium Severity

The Follow Pascal outdoor latch turns off on every reveal event that is not changed: true. A single commit can both replay an exterior edit and start other staged pieces (a new slab the reconciler just created, furniture in the same turn). Those starts are not marked changed, so they clear outsideBuild. The camera then falls back to cutaway and hides the wall that is still playing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit faa6e27. Configure here.

@wass08
wass08 merged commit 82153a0 into pascalorg:main Oct 16, 2026
5 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.

2 participants