Skip to content

feat: blob-tracking layer foundation — CPU tracker + inner-effects modules (plan 008 S1.2/S2.1)#101

Closed
git-chad wants to merge 1 commit into
advisor/001-test-baselinefrom
advisor/008-blob-tracking
Closed

feat: blob-tracking layer foundation — CPU tracker + inner-effects modules (plan 008 S1.2/S2.1)#101
git-chad wants to merge 1 commit into
advisor/001-test-baselinefrom
advisor/008-blob-tracking

Conversation

@git-chad

@git-chad git-chad commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Addresses #91 (Plan 008). This lands the pure, fully unit-tested foundation the plan deliberately isolates as independently shippable; the GPU/DOM stages are scoped as a documented follow-up (rationale below).

What's in

  • src/lib/blob-tracking/tracker.ts (S1.2) — the CPU tracker: zero DOM/three imports (runs under bun test, mirrors into the package unchanged). Motion/luminance binarization with auto fallback (30 static steps → luminance, re-arms on motion), 4-connected component labeling, top-N by area, persistent greedy-NN matching + EMA smoothing + 10-frame grace despawn + monotonic ids + 16-entry position-history ring, reset(), and the once-per-distinct-time step guard that makes export prewarm idempotent.
  • src/lib/blob-tracking/inner-effects.ts (S2.1) — the inner-effect set (EFFECT_LAYER_TYPES minus blur) and tolerant parseInnerEffectParams/serializeInnerEffectParams (bad JSON → defaults, unknown keys dropped, type-mismatched values ignored).
  • 19 tests covering the full S1.2 + S2.1 case lists (clustering, min-size, top-N, persistent id stability, grace despawn, EMA convergence, reset, step-guard, auto-fallback/re-arm; round-trip, bad JSON, unknown keys/types).

Both modules are unwired — no blob-tracking layer is registered, so editor behavior is unchanged and no half-working option appears in the picker.

S1.1 readback spike

readRenderTargetPixelsAsync(rt, x, y, w, h) is present in the installed three@0.183.2 (Renderer.js:2884, returns a Promise via copyTextureToBuffer) — matches the plan's assumption, STOP condition does not trigger.

What's deferred, and why

S1.3–S1.5 (registration + BlobTrackingPass GPU pass + export-determinism), S2.2–S2.4 (child pass + sidebar + 22-type matrix), S3 (decoration overlay), S4 (package mirror + changeset + README + preview). These are GPU/DOM-bound and unverifiable in this automated run — no WebGPU browser is reachable (chrome tooling pinned to Chrome stable, not installed). Shipping a readback pass, temporal export stepping, and a decoration canvas unverified risks precisely the failure modes the plan flags (nondeterministic export, decorations leaking into the mask, child-pass GPU leaks). The tracker API (step/getBlobs/reset) is shaped to be exactly what BlobTrackingPass will consume, so this is genuine unblocking work, not throwaway. Full deferral map in plans/008-artifacts/status.md.

Verification

  • bun test → 71 pass (19 new) · bun run typecheck:tsc, bun run lint → exit 0

App-only, no layer registered → no changeset. Stacked on #93 (Plan 001) → #92. Merge order: #92#93 → this.

🤖 Generated with Claude Code

The two pure, browser-independent modules plan 008 isolates as its
independently-landable foundation, with full unit-test coverage:

- src/lib/blob-tracking/tracker.ts — CPU blob tracker (S1.2), zero DOM/three
  imports so it runs under bun test and mirrors into the package unchanged:
  motion/luminance binarization with auto fallback, 4-connected component
  labeling, top-N by area, persistent greedy nearest-neighbor matching with
  EMA smoothing, grace despawn, monotonic ids, position-history ring, reset(),
  and a once-per-distinct-time step guard for export determinism
- src/lib/blob-tracking/inner-effects.ts — inner-effect set (S2.1: effect
  types minus blur) with tolerant parse/serialize of parameter overrides
- 19 tests covering the full S1.2 and S2.1 case lists

Both modules are intentionally unwired: no blob-tracking layer is registered,
so editor behavior is unchanged and no unverified option appears in the
picker. The GPU/DOM stages (BlobTrackingPass, decorations, export
determinism, package mirror) are deferred — they need a WebGPU browser this
run had no access to. See plans/008-artifacts/status.md, which also records
the S1.1 readback-API spike result (API present in three@0.183.2; no STOP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
shader-lab Ready Ready Preview, Comment Jul 6, 2026 11:46pm

git-chad added a commit that referenced this pull request Jul 7, 2026
* chore: fix pre-existing lint errors

Biome lint fails on the unmodified tree with 4 errors and 7 warnings,
which blocks every CI/verification gate that requires a clean lint run:

- convert type-only `three/webgpu` namespace imports to `import type`
  in four package pass files (useImportType errors)
- remove unused biome-ignore suppression comments in both three-tsl.d.ts
  ambient declaration files (suppressions/unused)
- comment the two intentionally empty callbacks (noEmptyBlockStatements)
- simplify the splat() guard expression in the fluid runtime
  (useSimplifiedLogicExpression; boolean-algebra equivalent)

The two useExhaustiveDependencies warnings in properties-sidebar.tsx are
left as-is: the "extra" dependencies are intentional effect triggers and
removing them would change behavior. Warnings do not fail the lint gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: add baseline tests for project-file, parameter-schema, timeline and composition

The repo had zero test files while `bun test` has been wired up in
package.json through its entire history. This adds the first suite over
the pure logic that user data flows through:

- parseLabProjectFile: valid v1/v2 parse, clone semantics, and every
  validation error message
- parameter-schema: clone semantics, default building, equality, and
  signature stability/order-independence
- evaluateTimelineForLayers: linear/step/color/vec2 interpolation,
  clamping outside the keyframe range, disabled/unknown-layer tracks,
  property bindings and multi-track merging
- composition crop math: centered crops, degenerate canvases, and
  frame intersection (including the 1x1 clamp for disjoint frames)

50 tests, no source changes. Plan 001 (#84).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: validate editor app lint, types and tests in CI

CI previously only typechecked and built packages/shader-lab-react —
the ~230-file editor app was never linted, typechecked, or tested. Add
the three app validation steps after the package build (the app's
tsconfig resolves @basementstudio/shader-lab from the package's dist/,
so the build must run first).

Uses typecheck:tsc (stock TypeScript) rather than typecheck (tsgo
preview build) so CI gates on the stable toolchain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: close video encoder when export fails or is cancelled

Add an idempotent close() to VideoExportEncoder and call it from
exportVideo's finally block whenever finalize() did not complete, so
aborted or failed exports release the WebCodecs hardware encoder
instead of leaking it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: time out stalled video texture loads

createVideoTexture now rejects after 30s if neither the playing event
nor an error settles the promise, detaching the error handler and
releasing the video element. Resolve/reject are wrapped in settle
helpers that clear the timer and guard against double settlement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: only invoke onRuntimeError with null after a previously reported error

ShaderLabComposition called the consumer onRuntimeError callback with
null after every successful renderer initialization (and after every
successful shader recompile via the internal error channel), because
null was used as a success/cleared signal on the same path as real
errors. Consumers following the README example logged "null" on every
clean init, and apps mirroring the message into UI state showed a
permanent false error.

The composition now tracks the last message actually delivered to the
consumer in a ref that survives config-change re-inits:

- null on a clean init is a no-op toward the consumer (internal error
  overlay state is still cleared)
- after a real error was delivered, resolving it calls the consumer
  with null exactly once so error UIs can recover
- consecutive identical errors and repeated clears are deduplicated

Also documents the callback semantics in the onRuntimeError JSDoc and
the README error-handling example, and adds a patch changeset.

Fixes #81

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: lazy-load export dialog and video muxers

Cut the video-export stack out of the editor route's first load:

- editor-topbar.tsx: load EditorExportDialog via next/dynamic
  (ssr: false) and only mount it after the export dialog has been
  opened once, so the ~46 KB dialog chunk is fetched on demand.
- video-export-encoder.ts: drop the static mp4-muxer/webm-muxer
  imports; createMuxer is now async and dynamically imports only
  the muxer for the chosen format (per-format loading), deferring
  ~58 KB more until an export actually runs.

First Load JS for /tools/shader-lab: 2656.0 KB -> 2556.1 KB
(measured by summing script chunks referenced by the prerendered
route HTML; Next 16 Turbopack no longer prints size columns).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: validate .lab imports with zod

Replace the manual shape checks in parseLabProjectFile with a zod v4
schema that validates layers (discriminated by kind, enum-checked type,
param value types), timeline tracks/keyframes, assets and composition.
Unknown keys pass through via looseObject so files with extra data keep
importing, and fields added to BaseLayer after the .lab format shipped
(maskConfig, fluidInteractionEvents, runtimeError) stay optional for
legacy files. Schema issues map to the exact pre-existing user-facing
error strings. Also adds hasImportedCustomShaderCode, which detects
custom-shader layers whose sourceCode differs from the built-in
starters, plus four new validation test cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: require confirmation before running imported custom shader code

Gate .lab project import behind a window.confirm when the parsed file
contains custom-shader layers with non-starter sourceCode; cancelling
aborts the whole import before applyLabProjectFile touches any store.
window.confirm is a stopgap noted for a design follow-up. Also cap
parseSvgRasterResolution at 8192 to close the unbounded-canvas DoS from
untrusted svgRasterResolution params.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: add renderer drift inventory and consolidation design (plan 006)

Deliverables for the renderer-deduplication spike:

- plans/006-artifacts/drift-inventory.md — machine-generated classification
  of all 90 files across the four trees (script committed alongside);
  normalization covers both import-specifier style and biome formatting so
  only logic drift counts
- plans/006-artifacts/consolidation-design.md — source-of-truth decision
  (package), consumption mechanism (fluid-style source re-export shims),
  type-unification analysis, phased migration order, and the CI guardrail

The one-pass pilot (step 3) hit the plan's STOP condition and was reverted:
the app's ambient module declaration types every package import as a silent
`any` (probed empirically), and the package PassNode lacks three methods the
app pipeline calls on every pass every frame — a package-sourced pass
type-checks vacuously and throws at runtime. The design doc's Phase 0
(base-class reconciliation) is the recommended follow-up plan; after it the
pilot pass becomes a two-line shim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: replace localeCompare in parameter signatures

parameterValuesSignature runs per layer per frame in the render loop's
change-detection path. Parameter keys are plain ASCII identifiers, so
lexicographic comparison is equally deterministic and injective while
avoiding ICU collation on every comparison. Signatures are ephemeral
(compared only against the previous frame, never persisted) and the only
callers are pipeline-manager.ts and tests, so the ordering change is safe.

The comparator is extracted to a compareKeys helper because an inline
nested ternary violates the repo's noNestedTernary rule.

This is step 2 of plan 007 (#90) — the unconditional micro-fix. The
structural identity fast-paths (step 4) are profile-gated and were NOT
applied: this run had no WebGPU-capable browser to produce the required
measurements or the stale-canvas regression matrix. See
plans/007-artifacts/profile.md for the gate rationale and the follow-up
procedure. A stop after step 2 is an explicitly valid plan outcome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: add blob-tracking tracker and inner-effects foundation (plan 008)

The two pure, browser-independent modules plan 008 isolates as its
independently-landable foundation, with full unit-test coverage:

- src/lib/blob-tracking/tracker.ts — CPU blob tracker (S1.2), zero DOM/three
  imports so it runs under bun test and mirrors into the package unchanged:
  motion/luminance binarization with auto fallback, 4-connected component
  labeling, top-N by area, persistent greedy nearest-neighbor matching with
  EMA smoothing, grace despawn, monotonic ids, position-history ring, reset(),
  and a once-per-distinct-time step guard for export determinism
- src/lib/blob-tracking/inner-effects.ts — inner-effect set (S2.1: effect
  types minus blur) with tolerant parse/serialize of parameter overrides
- 19 tests covering the full S1.2 and S2.1 case lists

Both modules are intentionally unwired: no blob-tracking layer is registered,
so editor behavior is unchanged and no unverified option appears in the
picker. The GPU/DOM stages (BlobTrackingPass, decorations, export
determinism, package mirror) are deferred — they need a WebGPU browser this
run had no access to. See plans/008-artifacts/status.md, which also records
the S1.1 readback-API spike result (API present in three@0.183.2; no STOP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
git-chad added a commit that referenced this pull request Jul 7, 2026
@git-chad

git-chad commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Consolidated into #104.

@git-chad git-chad closed this Jul 7, 2026
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