Live physics drag + splat cluster regions

The renderer grows a live d3 sim (createGraphSimulation, shared with
the server settle): pointerdown on a node pins fx/fy and pointer capture
(preventDefault kills the compat mousedown, so d3-zoom never pans); the
first CLICK_SLOP-exceeding move reheats to alphaTarget 0.3; release
un-pins and the sim anneals (release-and-relax) — alpha < 0.005 settles
it, a 250ms debounced onCommit PUTs the rest state. Verified live: 123fps
during a 320ms drag (regions rebuilding every 3rd frame), 85-tick settle,
then zero idle frames. A slop-free press stays a click (select, no reheat,
no commit). PUT /api/graph/positions sanitizes (finite coords, known ids)
and if the graph grew mid-drag runs ONE incremental settle with the
client's arrangement as anchors (50-id PUT test: anchored drift 0.15).

Cluster regions swap convex hulls for a low-res world-space SPLAT bitmap:
per-member radial-gradient discs (alpha 0.2) + intra-cluster edges as
round-cap strokes (Minkowski dilation — concave shapes finally render),
upscaled with bilinear smoothing (the upscale IS the blur, no ctx.filter:
Safari). One tiny blit per frame replaces 32 clusters x 4 offset
Path2D polygons re-rasterized at DPR 2 (the old GPU coil-whine path);
during the live sim the bitmap rebuilds every 3rd frame. cluster-geometry.ts
retires convexHull/offsetPolygon/traceRoundedPath (tests follow; clusterHue
stays). Region labels stay (centroid + clusterHue, inverse-gated on zoom).
This commit is contained in:
amosblomqvist
2026-09-13 09:17:05 +03:00
parent 7eceb58303
commit 05712037b2
7 changed files with 444 additions and 223 deletions
+24
View File
@@ -0,0 +1,24 @@
/**
* PUT /api/graph/positions — the live-physics drag commit. The client
* anneals a drag to rest and sends the new positions; the server
* sanitizes (finite coords, known ids only), writes the ledger, and if
* the graph grew mid-drag runs one incremental settle to absorb the new
* nodes with the client's arrangement as anchors. See
* src/server/commit-positions.ts.
*
* PUT → 200 {positions: {...}} with whatever survived sanitizing
* (empty object = nothing written).
*/
import { commitPositions } from "@/src/server";
export const dynamic = "force-dynamic";
export async function PUT(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json({ error: "invalid JSON body" }, { status: 400 });
}
return Response.json({ positions: commitPositions(body) });
}