Inference-depth anchoring playground: vertical + radial modes

The graph's follows/supports edges encode derivation (premise ->
conclusion); the layout can now express that rank spatially, as a
session-only playground toggle (nothing promoted yet):

- inferenceDepths: longest chain from a base node, CYCLE-SAFE by
  SCC condensation (Tarjan, iterative) — the DAG property is typical,
  not guaranteed; cycle members share their component's depth (mutual
  support = equal footing). O(V+E), pure, deterministic.
- DepthAnchor force: per-node target scalar — vertical mode rows
  (baseline at the anchor centroid, arrows up), radial mode rings
  (foundations center, outer boundary = edge of knowledge). The rank
  is computed, not simulated: pairwise up-forces can't give baselines.
- Mode projections: gravity x-only in vertical / dropped in radial;
  cluster pull x-only in vertical / tangential-only in radial; the
  anchor re-blend preserves user arrangement in the FREE dimension
  only (x / angle) — meaning owns the constrained axis, memory owns
  the free one.
- setPhysicsTuning reheats (alpha 1): toggling a mode re-forms the
  graph live and commits the new rest state; drag release is springy
  along the constrained axis (node returns toward its level/ring).
- Sweep on the real graph: strength 0.8 = 215/216 arrows correct with
  organic sag; 1.5 = crisp rows. 174 base nodes, max chain depth 7,
  zero cycles in current data. replay-layout --depth=<mode> bakes a
  mode into the ledger for rest-state experiments.
This commit is contained in:
amosblomqvist
2026-09-13 10:20:50 +03:00
parent 7ebdf065c8
commit 3a1c18b9de
5 changed files with 422 additions and 8 deletions
+38
View File
@@ -60,6 +60,7 @@ import {
createGraphSimulation,
INCREMENTAL_GRAVITY,
LINK_DISTANCE,
type DepthAnchorMode,
type GraphRenderSettings,
type LayoutNode,
type Position,
@@ -218,6 +219,14 @@ export interface PhysicsTuning {
clusterPull: number;
/** Per-tick velocity damping (d3 default 0.4). */
velocityDecay: number;
/** Inference-depth anchoring mode (see DepthAnchor in core). */
depthMode: DepthAnchorMode;
/** Depth-anchor pull strength. */
depthStrength: number;
/** Spacing between inference levels (world units). */
depthSpacing: number;
/** Inner ring radius, radial mode (world units). */
depthBaseRadius: number;
}
export const DEFAULT_PHYSICS_TUNING: PhysicsTuning = {
@@ -227,6 +236,13 @@ export const DEFAULT_PHYSICS_TUNING: PhysicsTuning = {
gravity: INCREMENTAL_GRAVITY,
clusterPull: CLUSTER_PULL_STRENGTH,
velocityDecay: 0.4,
depthMode: "off",
// 0.8 by sweep on the real graph: 215/216 arrows read upward with
// organic sag (mean |dev| from level ~34u at spacing 60); 1.5 gives
// crisp rows (dev ~18) if the promoted default wants them.
depthStrength: 0.8,
depthSpacing: 60,
depthBaseRadius: 220,
};
/** Clamp anything into a usable tuning (number inputs can be empty/NaN). */
@@ -241,6 +257,10 @@ export function sanitizePhysicsTuning(input: Partial<PhysicsTuning> | null | und
gravity: Math.min(Math.max(num(input?.gravity, d.gravity), 0), 1),
clusterPull: Math.min(Math.max(num(input?.clusterPull, d.clusterPull), 0), 2),
velocityDecay: Math.min(Math.max(num(input?.velocityDecay, d.velocityDecay), 0), 0.99),
depthMode: input?.depthMode === "vertical" || input?.depthMode === "radial" ? input.depthMode : "off",
depthStrength: Math.min(Math.max(num(input?.depthStrength, d.depthStrength), 0), 2),
depthSpacing: Math.min(Math.max(num(input?.depthSpacing, d.depthSpacing), 1), 500),
depthBaseRadius: Math.min(Math.max(num(input?.depthBaseRadius, d.depthBaseRadius), 0), 2000),
};
}
@@ -670,6 +690,14 @@ export class GraphCanvasRenderer {
setPhysicsTuning(input: Partial<PhysicsTuning>): void {
this.physicsTuning = sanitizePhysicsTuning(input);
this.buildSim();
// Reheat: a tuning change re-anneals the layout from its current
// state, so mode toggles (off/vertical/radial) visibly RE-FORM the
// graph instead of waiting for the next drag. Mid-drag is safe —
// buildSim preserved the pinned node's fx/fy.
if (this.sim) {
this.sim.alpha(1);
this.simActive = true;
}
this.markDirty();
}
@@ -1085,6 +1113,16 @@ export class GraphCanvasRenderer {
linkDistance: t.linkDistance,
clusterPull:
data.clusters.length > 0 ? { clusters: data.clusters, strength: t.clusterPull } : undefined,
depthAnchor:
t.depthMode === "off"
? undefined
: {
mode: t.depthMode,
strength: t.depthStrength,
spacing: t.depthSpacing,
baseRadius: t.depthBaseRadius,
center: this.simCenter,
},
});
this.simById = new Map(data.nodes.map((n) => [n.id, n as LayoutNode]));
}