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:
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Server-side commit for the live physics drag (PUT /api/graph/positions).
|
||||
* The client's renderer anneals a drag to rest and PUTs the new rest
|
||||
* state; the server trusts it for the nodes it knows about and, if the
|
||||
* graph grew while the user was dragging (a merge landed), runs ONE
|
||||
* incremental settle (settleGraphLayout) so the fresh nodes are absorbed
|
||||
* into the frame the user just arranged — client positions act as
|
||||
* anchors, never as a clobber.
|
||||
*
|
||||
* Every body is sanitized: garbage coordinates clamp or drop, unknown
|
||||
* ids are discarded, nothing throws.
|
||||
*/
|
||||
import type { Position } from "../core/contracts.ts";
|
||||
import { getGraph } from "../core/graph.ts";
|
||||
import { settleGraphLayout } from "../core/layout.ts";
|
||||
import { stores } from "./stores.ts";
|
||||
|
||||
/** Max |x| or |y| accepted — NaN/Infinity clamp here, absurd too. */
|
||||
const MAX_COORD = 1e6;
|
||||
|
||||
/**
|
||||
* Accepts the raw PUT body (a Record of id → {x, y}) and returns the
|
||||
* sanitized positions actually persisted (empty object = nothing written,
|
||||
* e.g. a body with no known ids — the caller reports but never fails).
|
||||
*/
|
||||
export function commitPositions(input: unknown): Record<string, Position> {
|
||||
if (typeof input !== "object" || input === null) return {};
|
||||
const raw = input as Record<string, unknown>;
|
||||
|
||||
// Only ids the graph actually has (first line wins, like getGraph).
|
||||
const known = new Map<string, boolean>();
|
||||
for (const line of stores.graphStore.readNodes()) {
|
||||
if (!known.has(line.id)) known.set(line.id, true);
|
||||
}
|
||||
|
||||
const positions: Record<string, Position> = {};
|
||||
for (const [id, value] of Object.entries(raw)) {
|
||||
if (!known.has(id)) continue;
|
||||
if (typeof value !== "object" || value === null) continue;
|
||||
const { x, y } = value as Record<string, unknown>;
|
||||
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y))
|
||||
continue;
|
||||
positions[id] = {
|
||||
x: Math.min(Math.max(x, -MAX_COORD), MAX_COORD),
|
||||
y: Math.min(Math.max(y, -MAX_COORD), MAX_COORD),
|
||||
};
|
||||
}
|
||||
if (Object.keys(positions).length === 0) return {};
|
||||
|
||||
stores.graphStore.writePositions(positions);
|
||||
|
||||
// The graph grew past what the client knew (a merge landed mid-drag):
|
||||
// absorb the new nodes with the client's rest state as anchors.
|
||||
const unique = getGraph(stores.graphStore).nodes.length;
|
||||
if (unique > Object.keys(positions).length) settleGraphLayout(stores.graphStore);
|
||||
return positions;
|
||||
}
|
||||
Reference in New Issue
Block a user