Cluster labels: topmost layer + user-facing size setting

- Draw order: the cluster-label block moves from layer 5 to THE LAST
  layer of draw() — labels can never be occluded by node labels or
  hover names (user 2026-09-17; chose "topmost" after reassessing that
  discs were never actually on top — the white-out over discs is
  source-over compositing of the 85%-alpha text over near-white node
  fill, an optical contrast flip, not a draw-order bug).
- clusterLabelSize: new persisted setting (default 14 = the old
  REGION_LABEL_SIZE constant, so rendering is pixel-identical). Screen
  px at ANY zoom (drawn at size/k — camera-compensated, UI-like, same
  law as before). Log-slider range [7, 28] puts the default dead-center
  per the house slider law. Sanitized: old persisted files (no field)
  fall back to 14.
- Panel: the Cluster labels checkbox moves from the Labels section
  into Regions (user: cluster settings live with regions), joined by
  the new "Cluster label size" slider. Labels stay decoupled from the
  Background colors compute switch, as designed on 2026-09-16.
This commit is contained in:
amosblomqvist
2026-09-17 09:35:11 +03:00
parent b24bce3f83
commit 2f4b9c82e4
4 changed files with 80 additions and 28 deletions
+17
View File
@@ -10,6 +10,7 @@ import { describe, it as test } from "node:test";
import {
ARROW_LENGTH_RANGE,
CHARGE_RANGE,
CLUSTER_LABEL_SIZE_RANGE,
CLUSTER_PULL_RANGE,
DEFAULT_RENDER_SETTINGS,
DEPTH_BASE_RADIUS_RANGE,
@@ -53,6 +54,7 @@ describe("sanitizeRenderSettings", () => {
assert.equal(s.labelSize, DEFAULT_RENDER_SETTINGS.labelSize);
assert.equal(s.labelZoom, DEFAULT_RENDER_SETTINGS.labelZoom);
assert.equal(s.clusterLabels, DEFAULT_RENDER_SETTINGS.clusterLabels);
assert.equal(s.clusterLabelSize, DEFAULT_RENDER_SETTINGS.clusterLabelSize); // old files → 14
});
test("out-of-range values clamp; wrong types reset", () => {
@@ -63,6 +65,7 @@ describe("sanitizeRenderSettings", () => {
labelZoom: 100,
labelSize: "big" as unknown as number,
clusterLabels: "yes" as unknown as boolean,
clusterLabelSize: "huge" as unknown as number,
});
assert.equal(s.nodeSize, NODE_SIZE_RANGE.max); // clamped to max
assert.equal(s.edgeWidth, EDGE_WIDTH_RANGE.min); // clamped to min
@@ -70,6 +73,19 @@ describe("sanitizeRenderSettings", () => {
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);
assert.equal(s.clusterLabelSize, DEFAULT_RENDER_SETTINGS.clusterLabelSize); // NaN-safe reset
});
test("clusterLabelSize clamps to its own range (slider law)", () => {
// Default 14 is the geometric mean of [7, 28] → dead-center slider.
assert.equal(
Math.sqrt(CLUSTER_LABEL_SIZE_RANGE.min * CLUSTER_LABEL_SIZE_RANGE.max),
DEFAULT_RENDER_SETTINGS.clusterLabelSize,
);
const s = sanitizeRenderSettings({ clusterLabelSize: 999 });
assert.equal(s.clusterLabelSize, CLUSTER_LABEL_SIZE_RANGE.max);
const s2 = sanitizeRenderSettings({ clusterLabelSize: 1 });
assert.equal(s2.clusterLabelSize, CLUSTER_LABEL_SIZE_RANGE.min);
});
test("the low ends accept small sizes (log-slider floors)", () => {
@@ -88,6 +104,7 @@ describe("sanitizeRenderSettings", () => {
labelZoom: 0.25,
labelSize: 4,
clusterLabels: false,
clusterLabelSize: 20,
regions: DEFAULT_REGION_TUNING,
physics: DEFAULT_PHYSICS_TUNING,
};