Skip to content

Experimental world-transform camera renderer - #2

Open
DanFessler wants to merge 12 commits into
mainfrom
world-transform
Open

DanFessler wants to merge 12 commits into
mainfrom
world-transform

Conversation

@DanFessler

Copy link
Copy Markdown
Owner

Builds on #1 (Tier 1); merging this also lands #1.

Experimental worldTransform option

While the camera moves, the workspace is laid out once, for a view covering the whole move, and each frame sets one transform on the layer, instead of laying out every panel on every frame. When the camera stops, everything is laid out normally again.

  • true / false (default)
  • { minPanels: n }: only for moves with at least n panels in view where they start or end, decided when the camera starts moving
  • "auto": { minPanels: 40 }. With the CPU slowed 4×, laying out 36 panels per frame kept up and 49 began to stutter.

With 100 panels and the CPU slowed 4×, slow frames during an animated zoom fall from about 40% to under 10%, and during a pinch from about 25% to about 5%.

Trade-offs while the camera moves, documented in docs/performance.md: chrome, text and gaps scale like a picture; zooming in looks slightly soft until it stops; collapsed groups and icon tiles are chosen for the laid-out view.

Also:

  • React prop and world-transform attribute on the custom element
  • IDE demo: a status-bar button and palette command cycle off / auto / on
  • npm run bench takes WORLD=1|auto, SIZES=6x6,8x8 and THROTTLES=4
  • Bundle-size figures in the docs updated (core 38 kB gzipped)

Tests

E2E checks for the world transform:

  • panels are exactly where the camera puts them on every frame (LTR and RTL; gaps scale with the layer);
  • overlay floats stay put;
  • a 100-panel move restyles only a few elements per frame;
  • the end state matches the normal renderer;
  • pinch geometry is correct.

E2E checks for the threshold:

  • a light layout stays on the normal renderer;
  • moves with 40 or more panels in view use world mode from their first frame;
  • panels are counted at the move's ends, not across the whole layout;
  • a custom threshold works;
  • a move that starts during a layout animation is decided from its start.

The custom-element attribute is covered too.

🤖 Generated with Claude Code

DanFessler and others added 12 commits September 28, 2026 18:28
A view whose content fails to mount (or whose iframe function throws) shows a
fallback with its title, the error and Try again, and the rest of the workspace
carries on. In React, every view's content, icon and accessory renders inside its
own error boundary, so one throwing component no longer unmounts the workspace.

Everything that throws, from view code, callbacks, close guards or event
listeners, goes to a new error event with its source, view and type; without a
listener it goes to the console. ws.reportError sends an app's own errors down the
same channel, errorFallback replaces the fallback (core and React, per workspace or
per type), and React gets an onError prop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With direction "rtl" (or "auto", the default, on a right-to-left page) the
workspace mirrors: a row's first child sits on the right, tabs and menus run right
to left, and floating windows are placed from the right. The document stays
direction-neutral; the mirror is applied where layout coordinates meet the screen,
so dividers, docking, tab reordering, floating resize, collapsed groups and every
navigation gesture follow the pointer. Arrow keys follow reading order in tab
lists and menus and move dividers the way they point. Key combinations in menus
keep reading left to right. React gets a direction prop, the web component a
direction attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A permissions option (and React prop) sets what users may change through the
interface: rearrange, resize, close, float and hide, each on by default, or false
to lock everything. Each one covers every path to its action: drags, dividers and
their keys, floating-window edges, close buttons, middle-click, Delete, panel menu
items and shortcuts. Locked dividers and resize handles aren't shown. Calls from
code are never limited, and permissions can change at any time.

Also fixes Close other tabs closing tabs whose type isn't closable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sanitize now trusts nothing about its input: any value at all comes back as a
valid document without throwing. Wrong types, nulls, missing or duplicate ids,
non-numeric rects, bad restore targets and malformed framings are repaired, with
fresh ids rather than lost content. A fuzz test runs 3000 corrupted layouts
through it and checks every invariant the workspace relies on.

The workspace repairs a document before deciding on unregistered view types, so
setDocument accepts anything, and a saved layout with the wrong shape falls back to
the default layout. Placeholders for unregistered types are marked view-missing,
and their records survive saving until the type is registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rendering looked up every view's panel and region by walking the whole layout, for
each view on every frame: views × panels work per frame, 30,000 tree visits at 300
views. The workspace now indexes views and panels once per document change. Views
in background tabs aren't repositioned while things move, and dividers hide
during camera moves as they already do during layout animations.

With the CPU slowed 4× and 300 views, slow frames during a tab drag fall from 98%
to 3%, during a zoom from 84% to 22%.

scripts/bench.mjs measures startup, memory and frame timing for 16, 108 and 300
views, at full speed and slowed 4×, and prints the tables docs/performance.md
quotes. The e2e stress scenario takes its grid size from the URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The built-in icons were parsed from markup, which throws where Trusted Types are
enforced, so the workspace couldn't start at all. Collapsed groups drew their
lines with inline style attributes, which a policy blocking them leaves
unpositioned, and icon-only panels copied their icon by re-parsing its markup.
All three now build elements and set positions through the style object. A new
e2e scenario runs under style-src-attr 'none' and require-trusted-types-for
'script' and checks icons, menus, collapsed groups and icon tiles, with no
violations.

Icon markup that an app passes as a string is still HTML, and documented as such.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erformance, security

New guides for handling errors, locking and permissions, and right-to-left
layouts; new Performance (measured with npm run bench) and Security pages.
Persistence covers damaged and outdated layouts, unknown view types and several
layouts per user or team. The API references, theming and keyboard pages cover
the new options, events, parts and attributes. The core's size is now quoted as
37 kB gzipped, and the changelog has an Unreleased entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the camera moves, lay the workspace out once for a view covering the
whole move and move it with one transform on the layer, instead of laying
out every panel each frame. Off by default; toggle in the IDE status bar or
palette, or W in the e2e stress scenario.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut cost

Predicts a normal moving frame's cost from timed layouts (calibrated by
measured moving frames) and uses the world transform when it would take
over 70% of a display frame, returning below 35%. Late frames switch a
move mid-way and bias the next few. Moves with under 12 panels never use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the measured per-machine choice with a deterministic rule: a camera
move uses the world transform when at least minPanels panels are in view
where it starts or ends. "auto" is { minPanels: 40 }, from benchmarks
where 36 panels kept up with the CPU slowed 4x and 49 began to stutter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A move sent somewhere else mid-way is decided again for its new end.
Tests wait for the camera to be still across animation frames instead of
wall-clock intervals, and record only frames the camera moved in: WebKit
in CI draws slowly and may run a frame's callbacks before the workspace's.
E2E_PORT picks another port for the test server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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