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:
+39
-18
@@ -86,14 +86,23 @@ export interface CanvasNode {
|
||||
}
|
||||
|
||||
/** One directed edge of the DERIVED view (chains-primary, 2026-09-30):
|
||||
* premise → implication node → conclusion, UNTYPED — every arrow is an
|
||||
* implication, so every arrow renders the double-shaft glyph (⟹) +
|
||||
* arrowhead (DECIDED 2026-09-14, docs/vision-edge-glyph.md). */
|
||||
* premise → implication node → conclusion, UNTYPED, all PLAIN
|
||||
* single-shaft arrows — there is no double-shaft glyph anywhere
|
||||
* (the 2026-09-14 ruling died with typed edges). An argument renders
|
||||
* as its geometry: co-premise lines converge HEADLESS onto the
|
||||
* implication point, one arrowed line leaves it; a single-premise
|
||||
* argument is one ordinary arrow (see deriveView). */
|
||||
export interface CanvasEdge {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
/** Implication-node ids (the mint law: newGraphChainId in core/graph.ts).
|
||||
* On the canvas an imp node is PURE GEOMETRY (owner, rendering arc
|
||||
* 2026-09-30): invisible — no disc, no label — and UNPICKABLE,
|
||||
* excluded from every hit-test path. Exported for the test pin. */
|
||||
export const isImpNodeId = (id: string): boolean => id.startsWith("arg_");
|
||||
|
||||
/** One cluster region: member node ids + the LLM-coined label (or null). */
|
||||
export interface CanvasCluster {
|
||||
id: string;
|
||||
@@ -1048,6 +1057,7 @@ export class GraphCanvasRenderer {
|
||||
const projected: { id: string; x: number; y: number }[] = [];
|
||||
for (const n of this.data.nodes) {
|
||||
if (this.visible && !this.visible.has(n.id)) continue;
|
||||
if (isImpNodeId(n.id)) continue; // pure geometry — never picked
|
||||
projected.push({ id: n.id, x: n.x * k + tx, y: n.y * k + ty });
|
||||
}
|
||||
const hit = pickNearest(projected, px, py, maxDist);
|
||||
@@ -1218,21 +1228,24 @@ export class GraphCanvasRenderer {
|
||||
const visible = this.visible;
|
||||
const isVisible = (id: string) => !visible || visible.has(id);
|
||||
|
||||
// Node discs: one Path2D, one fill() per frame.
|
||||
// Node discs: one Path2D, one fill() per frame. Implication nodes
|
||||
// are pure geometry — never a disc, never a label, never picked.
|
||||
const nodesPath = new Path2D();
|
||||
for (const n of data.nodes) {
|
||||
if (!isVisible(n.id)) continue;
|
||||
if (!isVisible(n.id) || isImpNodeId(n.id)) continue;
|
||||
nodesPath.moveTo(n.x + nodeR, n.y);
|
||||
nodesPath.arc(n.x, n.y, nodeR, 0, Math.PI * 2);
|
||||
}
|
||||
this.nodesPath = nodesPath;
|
||||
|
||||
// Edge shafts + arrowheads: tip lands on the target disc edge. Both
|
||||
// relation types draw the same arrow for now — the double-shaft
|
||||
// glyph was rejected (2026-09-15); differentiation is pending a new
|
||||
// approach. Two owner rulings 2026-09-29: the supports draw toggle
|
||||
// CHAINS-PRIMARY (2026-09-30): one path family — every derived arrow
|
||||
// is an implication, double-shafted + arrowheaded uniformly.
|
||||
// Edge shafts + arrowheads (rendering arc, owner 2026-09-30): all
|
||||
// arrows are PLAIN single shafts — the old double-shaft glyph is
|
||||
// gone entirely. An argument's geometry: lines INTO an implication
|
||||
// node are HEADLESS and run all the way to the imp node's CENTER
|
||||
// (co-premise lines converge to a point); the line OUT of it starts
|
||||
// at the center and its arrowhead lands on the conclusion's disc
|
||||
// edge. Single-premise arguments arrive here as ordinary
|
||||
// proposition→proposition edges and draw exactly like any arrow.
|
||||
const arrowLen = settings.arrowLength;
|
||||
const shafts = new Path2D();
|
||||
const heads = new Path2D();
|
||||
@@ -1241,14 +1254,22 @@ export class GraphCanvasRenderer {
|
||||
const a = this.nodeById.get(e.from);
|
||||
const b = this.nodeById.get(e.to);
|
||||
if (!a || !b) continue;
|
||||
const g = arrowGeometry(a, b, nodeR, nodeR, arrowLen);
|
||||
const intoImp = isImpNodeId(e.to);
|
||||
const outOfImp = isImpNodeId(e.from);
|
||||
const g = arrowGeometry(a, b, outOfImp ? 0 : nodeR, intoImp ? 0 : nodeR, arrowLen);
|
||||
if (!g) continue;
|
||||
shafts.moveTo(g.shaftFrom.x, g.shaftFrom.y);
|
||||
shafts.lineTo(g.shaftTo.x, g.shaftTo.y);
|
||||
heads.moveTo(g.tip.x, g.tip.y);
|
||||
heads.lineTo(g.left.x, g.left.y);
|
||||
heads.lineTo(g.right.x, g.right.y);
|
||||
heads.closePath();
|
||||
if (intoImp) {
|
||||
// toRadius 0 put the tip AT the imp node's center: the
|
||||
// headless shaft terminates there (no head triangle).
|
||||
shafts.lineTo(g.tip.x, g.tip.y);
|
||||
} else {
|
||||
shafts.lineTo(g.shaftTo.x, g.shaftTo.y);
|
||||
heads.moveTo(g.tip.x, g.tip.y);
|
||||
heads.lineTo(g.left.x, g.left.y);
|
||||
heads.lineTo(g.right.x, g.right.y);
|
||||
heads.closePath();
|
||||
}
|
||||
}
|
||||
this.shaftsPath = shafts;
|
||||
this.headsPath = heads;
|
||||
@@ -1266,7 +1287,7 @@ export class GraphCanvasRenderer {
|
||||
ctx.font = this.worldLabelFont;
|
||||
const maxW = LABEL_MAX_WIDTH * (settings.labelSize / LABEL_SIZE);
|
||||
for (const n of data.nodes) {
|
||||
if (isVisible(n.id)) this.labelLines.set(n.id, wrapLabel(ctx, n.label, maxW));
|
||||
if (isVisible(n.id) && !isImpNodeId(n.id)) this.labelLines.set(n.id, wrapLabel(ctx, n.label, maxW));
|
||||
}
|
||||
ctx.font = prevFont;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user