Skip to content

Gravitational lensing: SIS + NFW thin-lens with precomputed NFW image-finding LUT#365

Open
rulkens wants to merge 40 commits into
mainfrom
feat/gravitational-lensing
Open

Gravitational lensing: SIS + NFW thin-lens with precomputed NFW image-finding LUT#365
rulkens wants to merge 40 commits into
mainfrom
feat/gravitational-lensing

Conversation

@rulkens

@rulkens rulkens commented Jun 24, 2026

Copy link
Copy Markdown
Owner

What this is

Vertex-stage gravitational lensing for the galaxy point cloud: each background
galaxy is deflected by foreground cluster lenses, rendered as a primary image
plus a counter image (12 verts/point when lensing is on). Two lens profiles —
SIS (analytic) and NFW (driven by a precomputed image-finding LUT).

The branch is large because it stacks several prior lensing commits; the
NFW image-finding LUT is the work from this session and the part most worth
reviewing closely.

Where to focus the review (this session's LUT work)

These commits, newest first:

  • e6d1194a fix(lensing): drive NFW primary image from the LUT — the
    consistency fix. Before this, the NFW primary used the source-plane summed
    deflection while the counter used the exact LUT inverse, so the two images
    were solved by different methods and visibly crossed (inner/outer rings
    overlapping, unlike SIS). Now the LUT sample is hoisted above the image-kind
    branch so the dominant NFW lens's primary and counter read one inverse
    solution. The primary's source-plane push is swapped for the LUT-exact image
    position only inside the LUT's y-domain (y <= Y_MAX), continuous at the
    boundary (xPrimary -> y as y -> Y_MAX). SIS + NFW-counter math unchanged.
  • 9622ece4 drive the NFW counter image from the LUT — option (b) texture
    params on lensedPosition, the (y, s) -> UV inverse map, the 12-vert gate
    for both modes, and the TS↔WESL constants parity test.
  • 88e4d21b wire the NFW LUT into the @group(3) scene group — BGL bindings
    2/3 (rgba16float texture + filtering sampler, both VERTEX), bind-group
    entries, LUT built at startup, lifecycle/teardown.
  • 41b6317e createNfwLensLutTexture GPU resourcergba16float + linear
    sampler (dodges the non-universal float32-filterable feature), f16 pack.
  • bd8f7bfe extract lensedPosition into lib/lensing.wesl — faithful 1:1
    lift, pre-wires the shared deflection model.
  • 4372bdb0 NFW image-finding LUT generator (CPU)buildNfwLensLut:
    dense-scan + bisection root finder, log-scaled s-axis, channels
    [xPrimary, muPrimary, xCounter, muCounter].

Architecture

  • The LUT inverts y = x − s·nfwShape(x) on the CPU into a 256×64 rgba16float
    texture, sampled in the points vertex stage — no per-vertex root-finding.
  • s-axis is log-scaled (LOG_K = 4.0); the shader UV map is the algebraic
    inverse of the generator's forward map. TS↔WESL constant parity is test-guarded
    (nfwLutConstants.parity.test.ts) since ?static WESL linking injects no values.
  • LUT texture + sampler co-host in @group(3) (scene group) alongside focus and
    the lensing uniform buffer — the only group secondary pick renderers don't declare.

Verification

  • npm run build — green (WESL links; the iOS-fatal-shader class is the reason
    the build gate matters here).
  • npm test — 3070 passing.
  • npm run typecheck — clean.

Outstanding before merge

  • Visual confirmation of NFW lensing is the one manual DoD gate not yet
    satisfied — I have not eyeballed the NFW rings rendering. Worth a look on the
    dev server before this lands.
  • Plan: docs/superpowers/plans/2026-06-24-nfw-lensing-lut.md (+ -part2.md).
    Disk/quad lensing coverage was deferred (galaxies large enough to be disks are
    near-camera and rarely lensed); the lensedPosition extraction pre-wires it.

🤖 Generated with Claude Code

claude and others added 27 commits June 21, 2026 21:59
Deflect background galaxies around a foreground lens directly in the
points vertex stage, where each source's distance from the camera and
the lens is known. This makes the effect physically honest: only sources
*behind* the lens are bent, and the deflection scales with D_ls/D_s — the
kind of distance-dependence a screen-space post-process pinch filter
can't express (it would bend the foreground and Milky Way too).

The lens model is a singular isothermal sphere (SIS): constant Einstein
deflection theta_E, source at separation beta imaged outward to
theta = beta + theta_E, so sources near the line of sight pile into an
Einstein ring. A Gaussian envelope (~3 theta_E) localises the effect so
an idealised SIS doesn't shift the whole sky. New helper:
shaders/lib/lensing.wesl.

Prototype shortcuts (to be formalised in a spec/plan):
- Lens centre is the camera orbit target (the thing you've zoomed into),
  not a mass-weighted cluster.
- theta_E is a UI-exaggerated angle in degrees (real cluster Einstein
  radii are ~arcseconds, invisible at zoom).
- Only the SIS primary image is rendered; the inner counter-image and
  NFW profiles are follow-ups.

Wiring:
- io.wesl Uniforms gains a lensing block appended at offset 176 (struct
  grows 176 -> 208 bytes); picker in-place writes are all < 176 so they
  stay valid and picking lenses with the same params.
- pointRenderer packs lensCenterWorld/lensEnabled/lensThetaE; threaded
  via PointDrawSettings + pointSpritesPass.
- Toggle + exaggerated Einstein-radius slider live in the Debug panel
  (debug settings cluster), off by default. Settings auto-wake the
  renderer via the existing WAKE_ROUTES.

Tests updated for the new debug fields + PointDrawSettings; full suite
green (2978 tests). WESL link validated via the real wesl-plugin under
vitest (pointRenderer test imports points/vertex.wesl?static).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7a4Pwz8yTrDtRcwGtojZb
Brings the lifted-out picking work (PR #362) onto the gravitational-lensing
spike. The only conflict was in pointRenderer.ts, where both sides reshaped
the uniform pack: main extracted packing into packPointUniforms.ts as the
single source of truth (176 bytes), while the spike extended the layout to
208 bytes with a lensing block at bytes 176..207.

Resolution keeps main's structure (draw() returns the packed ArrayBuffer via
packPointUniforms) and ports the lensing block into packPointUniforms.ts:
UNIFORM_BYTES → 208, plus the lensCenterWorld/lensEnabled/lensThetaERad writes.
Added a byte-offset guard test for the new block so the layout can't drift
(the iOS-freeze footgun). PointDrawSettings, the pointSpritesPass call site,
and the WGSL Uniforms struct already carried the block and auto-merged clean.

3033 tests pass; typecheck clean on both projects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nses

Builds on the single-lens SIS prototype (34ebb91) into a physically
grounded, multi-cluster effect:

- Counter-image: each source draws as two quads (vertex_index 0..5 =
  primary, 6..11 = inner counter-image), filling the disk inside the
  Einstein ring that the primary alone left hollow. Draw widens to
  draw(12, N) only when lensing is on; off path is unchanged.

- Physical magnification: each image's intensity scales by |mu|
  (primary 1 + d/b, counter d/b - 1), so the ring reads genuinely
  bright and the counter-images fade to nothing at the Einstein radius.
  Capped at the finite-source limit to protect the additive HDR target.

- Multi-cluster lenses: every in-view cluster becomes a lens. The vertex
  stage loops over the lens array, SUMMING each foreground lens's
  deflection (weak-shear field + per-ring push) for the primary, and
  rendering the dominant lens (largest d/b) for the counter-image -
  sidestepping the 2^N multi-image blowup. Lenses pack into the points
  uniform tail (192 -> 448 bytes, no new bind group; the pick pass
  inherits them), capped at MAX_LENSES (16) for per-vertex ALU / iOS.

- buildClusterLenses (pure util + tests): selects clusters in front of
  the camera, sets each Einstein radius to master-angle x significance
  (normalised-M500 mass proxy), keeps the most massive MAX_LENSES.

Single-plane only (no inter-lens bending) and angular Einstein radius
(observer-distance-independent, the physically faithful SIS behaviour).
Full suite green (3042 tests); WESL links to valid WGSL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gate each SIS lens's deflection on the impact parameter b = D_l·sin β
(perpendicular distance from the cluster centre to the source's line of
sight) via a world-Mpc Gaussian envelope, on top of the existing angular
one. The angular envelope's physical width is ~3·θ_E·D_l, which grows
with the eye→lens distance, so a pulled-back camera lensed an ever-wider
cone of background. Capping b at a fixed Mpc scale confines each lens to
a fixed-width pencil along the line of sight: sources directly behind the
cluster (b → 0) stay bent at any depth so the ring survives, off-axis
sources are not, and a distant camera no longer widens the reach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the NFW work from claude/skymap-gravitational-lensing-57c5cg into
this branch. That commit was written against the single-lens prototype;
here it is ported into the multi-cluster lensTerm architecture (summed
deflection + dominant-lens counter-image + magnification + impact-
parameter envelope) this branch has since grown.

- lib/lensing.wesl: nfwShape() (Wright & Brainerd g(x)/x, peak-normalised)
  and a mode branch in lensTerm. NFW's x = beta*D_l/r_s IS the impact
  parameter in scale-radius units, so it self-localises exactly like the
  SIS impact-parameter envelope — the two designs compose cleanly. SIS
  keeps the angular + impact-parameter envelopes; NFW needs neither.
- io.wesl / packPointUniforms: the two header pad slots before the lens
  array become lensMode (u32) + lensScaleRadius (f32). Struct stays 448
  bytes; picker in-place offsets (all < 176) untouched.
- PointDrawSettings: lensMode + lensScaleRadiusMpc apply to every in-view
  lens this frame (drop the single-lens lensCenterWorld/lensStrengthRad).
- New LensMode type; debug settings gain lensMode + lensScaleRadiusMpc
  with slice actions, selectors, defaults; DebugPanel SIS/NFW radio +
  NFW-only scale-radius slider.

Full suite green (3043 tests); typecheck + WESL link clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull the gravitational-lensing fields (enabled, count, mode, scaleRadius,
+ the 16 lens vec4s) out of the points uniform tail into a SEPARATE,
shared LensingUniforms bind group at @group(4), mirroring the existing
focusUniform shared-bind-group pattern. Behaviour-neutral — no visual
change. The points uniform shrinks back to 176 bytes; the lens data lives
in one 272-byte buffer written once per frame in renderFrame, bound by
the points AND pick pipelines (and, next, the MCPM volume raymarch — the
reason for the extraction: the same buffer can deform the volume field).

- New: LensingUniforms{Bgl,Buffer,Value} types, createLensingUniformsBgl
  (visibility VERTEX|FRAGMENT), createLensingUniformBuffer factory, pure
  packLensingUniforms (MAX_LENSES + LENSING_UNIFORM_BYTES move here),
  lib/lensingUniforms.wesl struct.
- packPointUniforms / points/io.wesl revert to 176 bytes (lens tail gone).
- PointDrawSettings drops lenses/lensMode/lensScaleRadiusMpc, gains
  lensingBindGroup; keeps lensEnabled (CPU 12-vs-6 draw gate).
- points/vertex.wesl reads lensing.* from @group(4); pointRenderer +
  pickRenderer thread lensingBgl + bind the shared group at @group(4).
- buildClusterLenses moves to renderFrame (writes the buffer once/frame);
  pointSpritesPass just passes the bind group.
- initGpu creates lensingBgl + lensingUniform; engine.ts destroy chain.

Full suite green (3045 tests); typecheck + WESL link clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The counter-image and magnification are the SIS closed-form result
(constant deflection, dθ/dβ = 1). NFW's deflection varies, so reusing
the SIS counter placed a spurious inner ghost — the 'weird second set of
points'. Stopgap: in NFW mode cull the counter quad and render the
primary only (the counter branch is now gated on SIS mode); SIS is
unchanged and keeps its exact counter + magnification.

The proper fix is specced in
docs/superpowers/specs/2026-06-24-nfw-lensing-lut-image-finding-design.md:
precompute the inverse NFW lens equation (y = x − s·nfwShape(x)) into a
2D (source-position × reduced-strength) texture LUT on the CPU, sample it
once per vertex to place the correct multi-image set with accurate
magnification — no per-vertex root-finding, and cheaper than today's
analytic NFW. The LUT folds into the shared LensingUniforms bind group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The LensingUniforms extraction added a 5th bind group to the points + pick
pipelines (uniforms, fade, source, focus, lensing) — but WebGPU caps a
pipeline at maxBindGroups = 4, so pipeline creation failed
('bindGroupLayoutCount (5) is larger than the maximum allowed (4)'), and
iOS/WebKit is stricter still. The points pipeline already used all four.

Fix: the shared lensing buffer rides binding 1 of the points-specific
@group(0) (uniforms at binding 0) instead of its own @group(4). This keeps
points + pick at 4 groups and stays out of the disk pipelines' @group(1)
focus binding (no ripple there). The buffer is still the shared, engine-
owned, write-once unit — LensingUniformBuffer now exposes `.buffer`, which
the point + pick renderers embed in their @group(0) bind group at
construction; the standalone `.bindGroup` remains for a pipeline with a free
group (the volume raymarch) to bind directly. Drops the per-draw
lensingBindGroup (PointDrawSettings) and the @group(4) wiring.

Full suite green (3045 tests); typecheck + WESL link clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes to the lensing render path:

- Co-host the shared LensingUniforms buffer at @group(3) @binding(1)
  (alongside cluster focus) and revert @group(0) to camera-only. Group 0
  is the shared camera group the secondary pick renderers (structure
  rings, Milky-Way) reuse by binding only groups 1+2, so adding lensing
  there broke their group-0 layout compatibility (the
  structure-marker-camera-bgl mismatch). Group 3 is the only group those
  pickers don't declare, so co-hosting lensing there ripples to nothing
  else.

- Draw the counter-image quad (12 vs 6 vertices) only when the lens is on
  AND the profile is SIS. NFW has no closed-form counter image, so its
  second quad was always culled — pure wasted vertex work. The primary
  image is still deflected in NFW.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared-LensingUniforms extraction landed split, not unified: lensing
co-hosts the @group(3) focus group for points + pick, with a standalone
BGL reserved for the volume raymarch. Repoint the spec's architecture
section to that reality — the LUT texture/sampler extend the focus group
(@binding(2)/(3), VERTEX) for points+pick; the standalone group gains
fragment-visible entries with the later volume phase. Also flag the
rgba32float-vs-linear-sampler filtering caveat for the plan to resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior-neutral refactor of the @group(3) bind group that now hosts
cluster focus + gravitational lensing (and, next, the lensing LUT).

The name "FocusUniforms" described binding 0's content but was used as
the whole group's identity — it lied harder with each co-tenant. Rename
the group-level surfaces to their boundary: SceneUniformsBgl,
createSceneUniformsBgl, sceneBgl, sceneBindGroup. FocusUniforms (the
struct/buffer at binding 0), createFocusUniformBuffer, and
lib/focusUniforms.wesl keep the focus name — they genuinely are focus.

Also un-braid the assembly: createFocusUniformBuffer did two jobs
(allocate the focus buffer AND compose the multi-tenant bind group). It
now owns only the focus buffer (exposes `buffer`); a new
createSceneBindGroup assembles the group from the already-built focus +
lensing buffers, so the next tenant (the LUT texture + sampler) is added
there, not in a focus-named factory. state.gpu.sceneBindGroup holds the
assembled group.

Fixes stale post-@group(3)-move docs on EngineGpuHandles (lensing is
read by points/pick via the scene group, not @group(4); the standalone
lensingBgl is reserved for the volume raymarch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 tasks / 7 phases: CPU LUT generator, behaviour-neutral lensedPosition
extraction, rgba16float LUT texture resource, scene-group wiring + NFW
counter from the table (12-vertex gate for both modes), disk/quad coverage
behaviour change with a user-confirmation smoke test, entanglement-radar pass.
Supersedes the spec's stale focusUniforms file naming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A galaxy large enough to resolve into a disk is near the camera (small D_s),
so it is rarely behind a lens with meaningful deflection; the only concrete
trigger is a crossfade snap, which the Phase-2 lensedPosition seam makes a
trivial one-line-per-shader follow-up. Now 9 tasks / 6 phases; removed the
manual disk smoke-test gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Points + pick read the lensing buffer via the @group(3) scene group at
binding 1, not @group(4). The standalone lensingUniforms BGL is reserved
for the volume raymarch (VERTEX|FRAGMENT) reading the same bytes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure CPU generator inverting the dimensionless NFW lens equation
y = x - s*nfwShape(x) over a (y, s) grid via dense scan + bisection,
emitting [xPrimary, muPrimary, xCounter, muCounter] per cell. nfwShape
ported from lib/lensing.wesl; log-scaled s-axis (LOG_K=4) documented with
its inverse for the shader. Six tests cover the s->0, super-critical,
MU_MAX-clamp, and dropped-third-image limits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behaviour-neutral: lift the inline multi-lens summation, dominant-lens pick,
and SIS-counter assembly out of points/vertex.wesl into a shared
lensedPosition(...) + LensedImage struct, so points and pick share one
deflection model and the disks can adopt it later with a one-line call. The
vs block collapses to a single call; downstream intensity/cull math unchanged.
Proven by a ?static link test + a draw-count parity test (SIS 12, NFW 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upload the NfwLensLut into an N*M rgba16float texture_2d (f16-packed via the
existing floatToF16) with a clamp-to-edge linear sampler; expose texture/view/
sampler + an idempotent destroy(). rgba16float+linear dodges the non-universal
float32-filterable feature; never texture_1d (iOS drops the frame). Not yet
bound to any pipeline. Four fake-device tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend createSceneUniformsBgl with the LUT texture (binding 2) + sampler
(binding 3), VERTEX, and createSceneBindGroup to bind them; build the LUT
once at startup in initGpu (dims from the new data/nfwLensLut.ts constants
home) and thread its view + sampler into the scene bind group. New
EngineGpuHandles.lensLutTexture follows the null->non-null->null lifecycle
and is released in destroy(). Not yet sampled by any shader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lensedPosition samples the precomputed LUT (texture+sampler passed through
the fn boundary) for the dominant NFW lens: (y,s)=(beta,strengthEff)*D_l/r_s
mapped to UV via the algebraic inverse of the generator's log-s map, signed
xCounter placing the inner image. LensTerm gains dL + strengthEff so the
query needs no second lensTerm call. SIS stays analytic. The vertex-count
gate now doubles to 12 for both modes (NFW's counter is real). LOG_K exported
+ a TS<->WESL parity test guards the three LUT axis constants from drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Correct three stale @group(4) comments (lensingUniforms.ts, points/io.wesl,
  packPointUniforms.ts) to @group(3) @binding(1) — points/pick read the lensing
  buffer via the scene group, not a 5th group.
- Guard NFW_SHAPE_PEAK + LENS_MU_MAX against TS<->WESL drift: export them and
  extend nfwLutConstants.parity.test.ts (a NFW_SHAPE_PEAK drift would silently
  mis-scale every NFW ring).
- Drop the stale 'SIS-only' counter-quad comment (NFW draws 12 too) and soften
  the pick comment (only the primary lensed image is hit-testable at draw(6)).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The NFW dominant lens's primary used the source-plane summed deflection
while its counter used the exact LUT inverse, so the two images were
solved by different methods and visibly crossed (inner/outer rings
overlapping, unlike SIS). Hoist the LUT sample above the image-kind
branch so the dominant NFW lens's primary and counter read one inverse
solution, swapping the primary's source-plane push for the LUT-exact
image position inside the LUT's y-domain. Continuous at y = Y_MAX
(xPrimary -> y, so the swap tends to the source-plane push there).

SIS path and NFW-counter arithmetic are byte-identical (the LUT sample
is only hoisted, not changed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
skymap 6a90a46 Commit Preview URL

Branch Preview URL
Jun 25 2026, 12:17 AM

rulkens and others added 2 commits June 24, 2026 15:45
…per-lens r_s)

Drive each cluster lens from its physical R500 (the deflection-at-infinity
alpha_inf proportional to R500^2, the NFW r_s = R500/c500), exposed via one
dimensionless strength multiplier: 0 = off, 1 = physical, log slider to ~1000x.
Per-lens r_s grows the lensing uniform to two vec4 per lens; the global
scaleRadius knob retires.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnosis of the ~50% points-pass regression under lensing: 6->12 vert
widening + the per-vertex lens loop run on all ~30M invocations. R3
(precompute per-lens eye-relative geometry) folds into the physical
cluster lensing plan; R2 (broad-phase cone reject), R4 (pay-as-you-go
vert width), R1 (compute cull + indirect counter draw), and the two A/B
probes captured here for follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rulkens and others added 11 commits June 24, 2026 16:08
…ed in

Adds the 8-task TDD plan and folds the per-lens eye-relative geometry
precompute (dirLens + dL baked into the uniform) into both the plan and
the spec's uniform-layout section, so lensTerm drops the per-vertex
subtract / length / normalize on ~30M invocations/frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leRadiusMpc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ngth

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ader scaleRadius

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted geom in lensing.wesl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…adiusMpc setting

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_s write

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…528-byte LensData

Final whole-branch review surfaced three doc-comments in files outside the
implementation diff still describing the retired single-vec4 / global-scaleRadius
uniform. Comment-only; the code already allocates LENSING_UNIFORM_BYTES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Lensing, Flow, and Label-Effects tuning sections each carried their
own labelled range-input row — two byte-identical local Sliders plus a
hand-rolled variant. Lift the row into one DebugSlider (label + pre-
formatted readout + numeric onChange) and rewire all three sections,
which also makes Label Effects' sliders visually consistent with its
siblings. Adds a focused render/onChange test for the shared component.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

2 participants