Splat Studio — Automation Architecture¶
Splat Studio maintains itself. It tracks its two upstreams
(@playcanvas/splat-transform and playcanvas), wires any new CLI capability into
the GUI, keeps a black-box regression suite green, and regenerates its own
documentation — all gated behind a reviewable pull request.
This document explains how those pieces fit together.
The big picture¶
flowchart TB
subgraph upstream [Upstreams]
ST[["@playcanvas/<br/>splat-transform"]]
PC[[playcanvas]]
end
subgraph cron [Weekly cron agent]
UD[skill: update-deps]
DOCS[skill: update-docs]
end
subgraph repo [Repository]
APP[GUI app<br/>client + server + electron]
TESTS[tests/e2e.mjs<br/>black-box suite]
GUIDE[docs/USER_GUIDE.md<br/>+ screenshots]
ARCH[docs/AUTOMATION.md]
end
ST -- new flags --> UD
PC -- new version --> UD
UD -- wire flag + add test --> APP
UD -- extend --> TESTS
UD --> DOCS
APP -- capture-docs.mjs --> GUIDE
DOCS -- regenerate --> GUIDE
DOCS -- regenerate --> ARCH
UD -- open PR --> PR{{Pull Request}}
DOCS --> PR
TESTS -- gate --> PR
PR -- review + merge --> repo
Everything funnels into a PR: nothing reaches main without passing the
regression suite and a human review.
Application architecture¶
The app the automation maintains is a standalone Electron desktop app. The one
hard constraint shapes the whole design: the splat-transform CLI's native WebGPU
(Dawn) device segfaults inside the Electron binary, so the CLI must run under a
real node.exe, never under Electron.
flowchart LR
subgraph electron [Electron main process]
MAIN[main.mjs<br/>window + menu + config]
end
subgraph node [Bundled real node.exe]
SRV[server/index.mjs<br/>Express API]
CLI[[splat-transform CLI]]
end
subgraph window [BrowserWindow renderer]
UI[client UI<br/>Vite + TS]
VIEW[viewer.ts<br/>PlayCanvas 3D]
end
MAIN -- spawns --> SRV
UI -- HTTP /api --> SRV
SRV -- spawn per job --> CLI
CLI -- outputs --> WS[(workspace/<br/>projects)]
SRV -- /files static --> VIEW
UI <--> VIEW
- Electron main owns the window, menu, and persisted workspace; it spawns the server under the bundled Node and points the window at it.
- Server is a thin Express layer: it lists projects/files, and runs each GUI
action as one background job — usually a
splat-transformCLI run, streaming the command + output back; the region-trim (server/ply-trim-worker.mjs) runs as a Node worker, since-B/-Scan only crop, not remove inside a region. It works on any single-file splat: non-PLY inputs are first decompressed to a temp PLY via the CLI, then trimmed (output is always.ply). - Renderer is the Vite/TypeScript UI plus the PlayCanvas viewport (
viewer.ts), which renders splats, collision wireframes, voxels, gizmos, the measure tools, and a render-to-texture camera preview. The shell is a dockable tab editor (dockview-core); panels + the viewport are tabs, with the layout persisted per workspace via/api/layout.
The release pipeline¶
Every push/merge to main ships a build. .github/workflows/release.yml runs on
windows-latest:
flowchart LR
PUSH([push to main]) --> CI[GitHub Actions<br/>windows-latest]
CI --> V[version = 0.1.<run>]
V --> FN[stage node.exe] --> B[build client] --> EB[electron-builder --win]
EB --> REL[[GitHub Release<br/>installer + portable + latest.yml]]
REL -. on launch .-> APP[installed app]
APP --> CHK{newer release<br/>than this build?}
CHK -- yes --> POP[dialog → open downloads page]
CHK -- no --> OK[stay]
The installed app checks the GitHub Releases API on startup (and via Help → Check for Updates…); when a newer version exists it offers to open the downloads page. It's a check-and-link updater by design — the user chooses when to update.
The dependency-update loop¶
A scheduled agent runs the splat-studio-update-deps skill on a cadence (weekly).
Each run is self-limiting — if nothing upstream changed, it exits in seconds.
flowchart TD
START([cron fires]) --> CHECK{npm view: newer<br/>splat-transform or<br/>playcanvas?}
CHECK -- no --> EXIT([exit, no-op])
CHECK -- yes --> BRANCH[create branch + worktree]
BRANCH --> BUMP[bump dependency]
BUMP --> DIFF[diff splat-transform --help<br/>against the wired flags]
DIFF --> NEW{new flags?}
NEW -- yes --> WIRE[wire each flag end-to-end<br/>server + client + tooltip<br/>skill: add-feature]
WIRE --> ADDTEST[add a regression test]
NEW -- no --> VERIFY
ADDTEST --> VERIFY[npm run typecheck && npm test]
VERIFY -- fail --> FIX[fix or report] --> VERIFY
VERIFY -- pass --> REFRESH[refresh docs<br/>skill: update-docs]
REFRESH --> PR[[open PR]]
PR --> DONE([await review])
New upstream capability therefore appears in the GUI automatically, but only lands after the suite passes and a human merges the PR.
The documentation-refresh loop¶
Documentation is treated as a build artifact, not hand-maintained prose that rots.
The splat-studio-update-docs skill regenerates it whenever the app changes — it's
invoked at the end of every dependency-update run, and can be run on its own.
sequenceDiagram
participant Agent as update-docs agent
participant Cap as capture-docs.mjs (Electron)
participant App as app (server + window)
participant Repo as docs/
Agent->>Cap: npm run docs:capture
Cap->>App: boot server + window on the demo splat
loop each documented panel
Cap->>App: open panel, set state, highlight controls
Cap->>App: capturePage()
Cap->>Repo: write screenshots/<panel>.png
end
Agent->>Repo: reconcile USER_GUIDE.md with current panels/flags
Agent->>Repo: update AUTOMATION.md if the pipeline changed
Agent->>Agent: open PR with refreshed docs
scripts/capture-docs.mjs runs the real app in an Electron window against the
synthetic demo-room splat, so the screenshots are reproducible on any machine and
show the actual rendered viewport — not mockups. Re-running overwrites the PNGs in
place, so a docs PR is a clean diff of only what visually changed.
The skill bank — .claude/skills/¶
| Skill | Responsibility |
|---|---|
splat-studio-control |
How to drive the app: server, project model, the full HTTP API for every function. |
splat-studio-mcp |
The MCP server contract: tools, consent, jobs, coordinate frames, extending the surface. |
splat-studio-workflows |
End-to-end MCP recipes (web optimization, collision, renders, cleanup, scaling, batch). |
splat-studio-test |
Run and extend the regression suite. |
splat-studio-add-feature |
Wire a new CLI flag (or viewer feature) into the GUI end-to-end, with a test. |
splat-studio-update-deps |
The autonomous routine: detect an upstream update, bump it, wire new flags, run tests, open a PR. |
splat-studio-update-docs |
Regenerate the user guide (text + screenshots) and this architecture doc after any change. |
The regression suite — tests/e2e.mjs¶
Black-box end-to-end: boots the server on a throwaway workspace seeded with a
synthetic splat + the sample generator, drives every server function over the
HTTP API, and asserts on the outputs. Run with npm test (add SKIP_GPU=1 on
machines without a GPU). This is the gate every change and dependency bump passes
through before its PR can merge.
Conventions¶
- Branch + PR per change (worktrees preferred); the suite must be green.
- Commits authored CodeByKeegan with Claude Code co-authorship (see the README's AI-assisted development section).
- Every GUI control's tooltip names the CLI flag it maps to, in parentheses.
- New feature work is tracked on an internal coverage board (one task per CLI flag).