rendering arc: the imp node becomes the argument's geometry, not a fake proposition
Owner-approved brief (orchestrator): the canvas stops drawing implication nodes as first-class nodes and starts drawing them as what they are — the point where co-premise lines converge. - deriveView (core/graph.ts): single-premise chains collapse to ONE direct premise->conclusion arrow — no imp node minted (a chain is always N-in/ 1-out; one premise is the degenerate case). Multi-premise chains keep the imp node + fan. Dangling guard unchanged. - physics (core/layout.ts createGraphSimulation): per-link rest length — a link touching an arg_-prefixed endpoint gets HALF the base link distance, default or user-tuned alike; everything else about imp-node physics citizenship unchanged. - canvas (components/graph-canvas.ts): imp nodes are PURE GEOMETRY — invisible (no disc, no label) and unpickable (excluded from pickAt's hit-test projection). Edges INTO an imp node: headless, shaft runs to its CENTER (toRadius 0, no head triangle). Edges OUT: shaft starts at the center (fromRadius 0), arrowhead lands on the conclusion's disc edge. Single-premise direct arrows render as today's plain arrows. - comment repairs (same commit): the two stale double-shaft claims (CanvasEdge doc + the draw-pass block) rewritten to the truth — plain single shafts; bundles are headless converging lines + one arrowed out-edge; no double-shaft glyph exists anywhere. - tests (+9): deriveView pins (single-premise collapse, multi-premise fan, shared-conclusion distinctness, dangling drop); imp-link half- distance settle pins; arrowGeometry zero-radius laws + isImpNodeId pin; graph.test.ts's snapshot invariant now counts only MULTI-premise live chains (its fixture's single-premise chains mint zero imp nodes); layout settle fixture updated for the collapse.
This commit is contained in:
+31
-3
@@ -106,16 +106,18 @@ describe("incrementalLayout", () => {
|
||||
describe("settleGraphLayout", () => {
|
||||
it("dedupes repeated node lines, runs, and writes the whole ledger", () => {
|
||||
const written: Record<string, Position>[] = [];
|
||||
const lines = [node("a"), node("b"), node("a")]; // "a" appended twice
|
||||
const lines = [node("a"), node("b"), node("c"), node("d"), node("a")]; // "a" appended twice
|
||||
const store = {
|
||||
readNodes: () => lines.map((l) => ({ ...l })),
|
||||
readChains: () => [chain("arg_ab", ["a"], "b")],
|
||||
readChains: () => [chain("arg_ab", ["a"], "b"), chain("arg_bcd", ["b", "c"], "d")],
|
||||
readPositions: () => ({} as Record<string, Position>),
|
||||
writePositions: (p: Record<string, Position>) => void written.push(p),
|
||||
};
|
||||
settleGraphLayout(store);
|
||||
assert.equal(written.length, 1);
|
||||
assert.deepEqual(Object.keys(written[0]!).sort(), ["a", "arg_ab", "b"], "one position per unique node AND the derived implication node");
|
||||
// arg_ab is single-premise → collapsed to a direct arrow, NO imp
|
||||
// node; arg_bcd is multi-premise → its imp node joins the layout.
|
||||
assert.deepEqual(Object.keys(written[0]!).sort(), ["a", "arg_bcd", "b", "c", "d"], "positions for the unique nodes and ONLY the multi-premise chain's imp node");
|
||||
});
|
||||
|
||||
it("does nothing on an empty graph", () => {
|
||||
@@ -129,6 +131,32 @@ describe("settleGraphLayout", () => {
|
||||
assert.equal(written.length, 0);
|
||||
});
|
||||
});
|
||||
describe("imp-link half distance (rendering arc, owner 2026-09-30)", () => {
|
||||
const settle = (from: string, to: string, linkDistance: number) => {
|
||||
const nodes = [{ id: from, x: 0, y: 0 }, { id: to, x: 500, y: 0 }];
|
||||
const sim = createGraphSimulation(nodes, [{ from, to }], {
|
||||
linkDistance,
|
||||
chargeStrength: 0, // springs only — the rest length IS the equilibrium
|
||||
gravity: { x: 0, y: 0, strength: 0 },
|
||||
});
|
||||
for (let i = 0; i < 300; i++) sim.tick();
|
||||
const [a, b] = sim.nodes() as unknown as { id: string; x: number; y: number }[];
|
||||
assert.ok(a && b, "both nodes present");
|
||||
return Math.abs(b.x - a.x);
|
||||
};
|
||||
|
||||
it("a plain proposition link settles at the full rest length (tuned or default)", () => {
|
||||
assert.ok(Math.abs(settle("a", "b", 100) - 100) < 2, "plain link ≈ 100");
|
||||
});
|
||||
|
||||
it("a link TOUCHING an implication node gets HALF the rest length", () => {
|
||||
// into the imp point (premise → arg_x)
|
||||
assert.ok(Math.abs(settle("a", "arg_x", 100) - 50) < 2, "into-imp link ≈ 50");
|
||||
// out of it (arg_x → conclusion)
|
||||
assert.ok(Math.abs(settle("arg_x", "b", 100) - 50) < 2, "out-of-imp link ≈ 50");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clusterPull (the centroid force)", () => {
|
||||
it("compacts each cluster toward its OWN centroid; strength 0 = off", () => {
|
||||
// Two loose chains, far apart, edges only WITHIN each chain.
|
||||
|
||||
Reference in New Issue
Block a user