diff --git a/components/graph-canvas.ts b/components/graph-canvas.ts
index 285437b..5007412 100644
--- a/components/graph-canvas.ts
+++ b/components/graph-canvas.ts
@@ -117,7 +117,7 @@ const LABEL_LINE_HEIGHT = 1.3;
/** Zoom span (camera-ratio units) over which labels fade together. */
export const LABELS_FADE_SPAN = 0.5;
-const GLOW_PAD = 9; // px the halo extends beyond the node disc (constant)
+const GLOW_PAD = 6; // px the halo extends beyond the node disc (constant)
const GLOW_ALPHA = 0.45; // halo opacity at the disc edge, fading outward
const HOVER_FADE_MS = 180; // hover halo opacity fade length
const ZOOM_EASE_MS = 45; // wheel-zoom glide: exponential-ease time constant (~250ms per notch)
@@ -139,7 +139,7 @@ const HULL_FILL_LAYERS = [
] as const;
const HULL_STROKE_PAD = 14;
const HULL_CORNER_RADIUS = 9;
-const REGION_LABEL_SIZE = 18;
+const REGION_LABEL_SIZE = 14;
/** All "px" constants above are screen px AT FIT ZOOM. The renderer scales
* them into world units (รท kFit) once per geometry build; camera zoom then
diff --git a/components/graph-view.tsx b/components/graph-view.tsx
index b27c485..54d4a4b 100644
--- a/components/graph-view.tsx
+++ b/components/graph-view.tsx
@@ -500,33 +500,33 @@ export function GraphView() {
changeSettings((cur) => ({ ...cur, nodeSize: v }))}
/>
changeSettings((cur) => ({ ...cur, edgeWidth: v }))}
/>
changeSettings((cur) => ({ ...cur, arrowLength: v }))}
/>
changeSettings((cur) => ({ ...cur, labelSize: v }))}
/>
diff --git a/src/core/render-settings.ts b/src/core/render-settings.ts
index 8946890..0ca1e2b 100644
--- a/src/core/render-settings.ts
+++ b/src/core/render-settings.ts
@@ -22,18 +22,23 @@ export interface GraphRenderSettings {
arrowLength: number;
/** Camera-ratio threshold for node labels (see module doc). */
labelZoom: number;
- /** Node + hover label font size, px. */
+ /** Node + hover label font size, px at fit zoom (scales with camera). */
labelSize: number;
/** Toggle for the cluster region labels (hulls always draw). */
clusterLabels: boolean;
}
+// Sizes are screen px AT FIT ZOOM (the world-space renderer scales them
+// with the camera, so a label reads at labelSize x k/kFit on screen).
+// Defaults were re-tuned for world-scaling on 2026-09-13: the old values
+// were calibrated in the screen-fixed era, so everything rendered ~2x
+// too big once sizes started scaling with the graph.
export const DEFAULT_RENDER_SETTINGS: GraphRenderSettings = {
- nodeSize: 6,
+ nodeSize: 4,
edgeWidth: 1,
- arrowLength: 5,
+ arrowLength: 4,
labelZoom: 0.5,
- labelSize: 12,
+ labelSize: 7,
clusterLabels: true,
};
@@ -51,12 +56,12 @@ export function sanitizeRenderSettings(raw: unknown): GraphRenderSettings {
return clamp(n, min, max);
};
return {
- nodeSize: num(r.nodeSize, DEFAULT_RENDER_SETTINGS.nodeSize, 2, 14),
- edgeWidth: num(r.edgeWidth, DEFAULT_RENDER_SETTINGS.edgeWidth, 0.5, 4),
- arrowLength: num(r.arrowLength, DEFAULT_RENDER_SETTINGS.arrowLength, 2, 15),
+ nodeSize: num(r.nodeSize, DEFAULT_RENDER_SETTINGS.nodeSize, 1, 14),
+ edgeWidth: num(r.edgeWidth, DEFAULT_RENDER_SETTINGS.edgeWidth, 0.25, 4),
+ arrowLength: num(r.arrowLength, DEFAULT_RENDER_SETTINGS.arrowLength, 1, 15),
// Range matches the log slider's span [LABEL_ZOOM_MIN, LABEL_ZOOM_MAX].
labelZoom: num(r.labelZoom, DEFAULT_RENDER_SETTINGS.labelZoom, 0.0625, 4),
- labelSize: num(r.labelSize, DEFAULT_RENDER_SETTINGS.labelSize, 6, 24),
+ labelSize: num(r.labelSize, DEFAULT_RENDER_SETTINGS.labelSize, 3, 24),
clusterLabels:
typeof r.clusterLabels === "boolean" ? r.clusterLabels : DEFAULT_RENDER_SETTINGS.clusterLabels,
};
diff --git a/tests/render-settings.test.ts b/tests/render-settings.test.ts
index 55d2b5c..7cddeef 100644
--- a/tests/render-settings.test.ts
+++ b/tests/render-settings.test.ts
@@ -44,13 +44,21 @@ describe("sanitizeRenderSettings", () => {
clusterLabels: "yes" as unknown as boolean,
});
assert.equal(s.nodeSize, 14); // clamped to max
- assert.equal(s.edgeWidth, 0.5); // clamped to min
- assert.equal(s.arrowLength, 2);
+ assert.equal(s.edgeWidth, 0.25); // clamped to min
+ assert.equal(s.arrowLength, 1);
assert.equal(s.labelZoom, LABEL_ZOOM_MAX);
assert.equal(s.labelSize, DEFAULT_RENDER_SETTINGS.labelSize); // NaN-safe reset
assert.equal(s.clusterLabels, DEFAULT_RENDER_SETTINGS.clusterLabels);
});
+ test("the widened low ends accept small sizes", () => {
+ const s = sanitizeRenderSettings({ nodeSize: 1, edgeWidth: 0.25, arrowLength: 1, labelSize: 3 });
+ assert.deepEqual(
+ { nodeSize: s.nodeSize, edgeWidth: s.edgeWidth, arrowLength: s.arrowLength, labelSize: s.labelSize },
+ { nodeSize: 1, edgeWidth: 0.25, arrowLength: 1, labelSize: 3 },
+ );
+ });
+
test("a fully valid object passes through unchanged", () => {
const valid = { nodeSize: 8, edgeWidth: 2, arrowLength: 7, labelZoom: 0.25, labelSize: 16, clusterLabels: false };
assert.deepEqual(sanitizeRenderSettings(valid), valid);