Orphan discs in regions + orphan exemption from depth anchoring
Two follow-ups on the semantic-cluster wave, both user-reported: 1. Region coloring: edge-only dilation (the anti-double-counting rule from the Louvain era) left 114 of 338 cluster members completely uncolored — semantic clusters contain members that share no intra-cluster edge with anyone (90 edge-orphans + isolated micro-topics), and 20 of 45 clusters painted nothing at all. Fix: a disc (radius = dilation, same as a round cap) for exactly the members no intra-cluster edge stroke reached. Every member is painted by its edges' caps OR one disc, never both — the no-double-counting rule survives. Verified live: all 74 orphans-in-clusters sit on tinted region pixels. 2. Depth modes (ORPHAN RULE): a node with no edges has no dependency structure, so depth-0 stamping put 90 unconnected propositions on the vertical baseline / radial base ring — meaning they don't have. Orphans are now exempt from the depth force and get the regular view's unprojected forces instead (full center gravity + full cluster pull). Structured-node projections unchanged (x-only gravity in vertical, none in radial; tangential-only cluster pull in radial). Gravity in depth modes is a custom force with EXACTLY d3's forceX/forceY math (strength × alpha — the first draft forgot the alpha and shifted the settle). Verified live: radial orphans follow their topics instead of the base ring; vertical orphans spread 1006 world units (6/90 near the baseline band vs 90/90 before).
This commit is contained in:
@@ -270,4 +270,46 @@ describe("depthAnchor force", () => {
|
||||
const child = next.get("child")!;
|
||||
assert.ok(child.y < old.y - 40, `child rises above its premises (child=${child.y.toFixed(1)}, old=${old.y.toFixed(1)})`);
|
||||
});
|
||||
|
||||
it("orphans are exempt from depth anchoring — regular forces govern them (ORPHAN RULE)", () => {
|
||||
// A premise→conclusion chain plus an unconnected loner. Gravity's
|
||||
// center is FAR from the baseline, so the two are distinguishable:
|
||||
// a depth-stamped orphan would sit on the baseline; an exempt one
|
||||
// follows gravity instead.
|
||||
const ns = [
|
||||
{ id: "base", x: 0, y: 0 },
|
||||
{ id: "derived", x: 10, y: 0 },
|
||||
{ id: "loner", x: 0, y: 0 },
|
||||
];
|
||||
const sim = createGraphSimulation(ns, [{ from: "base", to: "derived" }], {
|
||||
gravity: { x: 0, y: 500, strength: 0.1 },
|
||||
depthAnchor: { mode: "vertical", strength: 1.5, spacing: 120, center: { x: 0, y: 0 } },
|
||||
});
|
||||
for (let i = 0; i < 400; i++) sim.tick();
|
||||
const byId = new Map(sim.nodes().map((n) => [n.id, n]));
|
||||
assert.ok(Math.abs(byId.get("base")!.y!) < 40, "structured base sits on the baseline");
|
||||
assert.ok(byId.get("derived")!.y! < -60, "derived rises a level");
|
||||
const lonerY = byId.get("loner")!.y!;
|
||||
assert.ok(lonerY > 250, `the orphan follows gravity, not the baseline (y=${lonerY.toFixed(0)})`);
|
||||
assert.ok(Math.abs(lonerY - 500) < 120, `the orphan converges on gravity's center (y=${lonerY.toFixed(0)})`);
|
||||
});
|
||||
|
||||
it("radial: orphans are exempt — they gather at the center, not on the base ring", () => {
|
||||
const ns = [
|
||||
{ id: "base", x: 0, y: 0 },
|
||||
{ id: "mid", x: 10, y: 0 },
|
||||
{ id: "loner", x: 300, y: 300 },
|
||||
];
|
||||
const sim = createGraphSimulation(ns, [{ from: "base", to: "mid" }], {
|
||||
gravity: { x: 0, y: 0, strength: 0.1 },
|
||||
depthAnchor: { mode: "radial", strength: 1.5, spacing: 80, baseRadius: 150, center: { x: 0, y: 0 } },
|
||||
});
|
||||
for (let i = 0; i < 400; i++) sim.tick();
|
||||
const r = (id: string) => {
|
||||
const n = sim.nodes().find((x) => x.id === id)!;
|
||||
return Math.hypot(n.x!, n.y!);
|
||||
};
|
||||
assert.ok(Math.abs(r("base") - 150) < 40, `base sits on the base ring (r=${r("base").toFixed(0)})`);
|
||||
assert.ok(r("loner") < 60, `the orphan follows gravity to the center, not the ring (r=${r("loner").toFixed(0)})`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user