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:
amosblomqvist
2026-09-30 04:04:29 +03:00
parent 8d72de596a
commit 719f7f9934
6 changed files with 206 additions and 41 deletions
+19 -9
View File
@@ -303,15 +303,19 @@ export function getGraph(graphStore: Pick<GraphStore, "readNodes" | "readChains"
return { nodes: [...byId.values()], chains };
}
/** The DERIVED view — what the canvas renders. One implication NODE per
* chain (id = the chain's arg_ id; name "⟹"; statement = the chain
* line; the chain's provenance/createdAt) plus UNTYPED arrows: each
* premise → the implication node, the implication node → the
* conclusion. Rendering specialization (invisible / physics-free)
* comes later (owner, 2026-09-30: physics is fine with imp nodes as
* regular nodes for now). Dangling chains (a member node missing)
* drop here — the deletion cascades should prevent them; this is the
* honest belt. */
/** The DERIVED view — what the canvas renders. A SINGLE-premise chain
* collapses to ONE direct arrow premise → conclusion (no imp node:
* a chain is always N-in/1-out, so one premise is the degenerate
* 1-in-1-out case). A MULTI-premise chain mints an implication NODE
* (id = the chain's arg_ id; name "⟹"; statement = the chain line;
* the chain's provenance/createdAt) plus arrows: each premise → the
* implication node, the implication node → the conclusion — the
* canvas renders imp nodes as pure geometry (owner, rendering arc:
* invisible, unpickable; co-premise lines converge headless to a
* point, one arrowed line leaves it). Physics keeps imp nodes as
* regular citizens (half link distance — layout.ts). Dangling chains
* (a member node missing) drop here — the deletion cascades should
* prevent them; this is the honest belt. */
export function deriveView(graph: DerivedGraph): { nodes: GraphNode[]; edges: GraphEdge[] } {
const names: Record<string, { name: string }> = {};
for (const node of graph.nodes) names[node.id] = { name: node.name };
@@ -320,6 +324,12 @@ export function deriveView(graph: DerivedGraph): { nodes: GraphNode[]; edges: Gr
const edges: GraphEdge[] = [];
for (const chain of graph.chains) {
if (!live.has(chain.conclusion) || !chain.premises.every((premise) => live.has(premise))) continue;
// Single premise: the argument IS a plain arrow — no geometry
// point needed (the 1-in/1-out collapse; rendering arc, owner).
if (chain.premises.length === 1) {
edges.push({ from: chain.premises[0]!, to: chain.conclusion });
continue;
}
impNodes.push({
id: chain.id,
name: "⟹",