Files
learnharness/app/api/graph/render-settings/route.ts
T
amosblomqvist ecadededd9 World-space rendering: geometry cached once, zoom pays only rasterization
Fixes the zoom frame-rate collapse and rebuilds draw() around a single
camera transform, per the user's Obsidian-style sizing proposal.

Bug fix first: the eased zoom leaked a queued rAF callback per frame --
advanceZoom applies each step through d3-zoom, whose zoom event called
markDirty() from inside the tick loop, scheduling an extra draw while the
loop scheduled its own. Draws multiplied per displayed frame; sustained
wheel ran at ~20fps against a 120Hz display. markDirty now skips
scheduling while inTick (verified: 62 frames/500ms during a 28x zoom
burst, matching pan).

World-space model: every draw call now runs in graph coordinates under
one setTransform(k). Sizes are defined as screen-px-at-fit and converted
once per geometry build (/ kFit), so nodes, node labels, arrowheads and
the hover halo scale with the graph as you zoom (0.25x painted area at
half zoom, exactly k^2) while edge widths and hull borders stay
viewer-relative hairlines (/ k). Cluster hull geometry (convex hull +
three offset fills + solid boundary -- dashes dropped for now) is built
as cached Path2Ds per data/visibility/settings change; camera moves do
zero geometry math, making zoom per-frame cost identical to pan. Label
wrapping runs once per build, not per frame. Region labels stay
viewer-relative (18px at any zoom) as UI chrome. Picking radius scales
with the world-sized discs.

Glide feel: ZOOM_EASE_MS 120 -> 45, convergence 0.1% -> 0.2%; a notch now
settles in ~260ms instead of ~750ms. Resize still paints synchronously.

Also fixes barrel-law lint violations from the settings wave (import
from @/src/core and @/src/server, not deep module paths).
2026-09-13 07:18:37 +03:00

32 lines
1.1 KiB
TypeScript

/**
* GET/PUT /api/graph/render-settings — server persistence for the graph
* view's rendering controls (node/edge/arrow/label sizes, the label-zoom
* threshold, the cluster-labels toggle). The file lives at
* data/render-settings.json (config.RENDER_SETTINGS_FILE).
*
* GET → 200 {settings} once saved, 200 null on first boot (client keeps
* in-memory defaults until the user changes something).
* PUT → 200 {settings} (the sanitized copy actually persisted — the
* client may echo it to stay honest with clamped values).
*
* Every body is passed through sanitizeRenderSettings (src/core) before
* touching disk: garbage clamps or falls back to defaults, never throws.
*/
import { readRenderSettings, writeRenderSettings } from "@/src/server";
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json(readRenderSettings());
}
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(writeRenderSettings(body));
}