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:
+19
-9
@@ -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: "⟹",
|
||||
|
||||
+19
-1
@@ -196,6 +196,14 @@ export interface LayoutEdge {
|
||||
}
|
||||
|
||||
export const LINK_DISTANCE = 40;
|
||||
/** Implication-node id prefix (the mint law lives in core/graph.ts's
|
||||
* newGraphChainId). Layout-side: links TOUCHING an imp node get HALF
|
||||
* the base rest length — the argument's geometry (a converging point)
|
||||
* sits tight against its premises/conclusion instead of spread like a
|
||||
* real proposition citizen (owner ruling, rendering arc 2026-09-30). */
|
||||
const IMP_ID_PREFIX = "arg_";
|
||||
const impLinkId = (v: string | number | { id: string }): string =>
|
||||
typeof v === "object" ? v.id : String(v);
|
||||
export const CHARGE_STRENGTH = -240; // 2026-09-17: −120 → −240 (user: repulsion default too weak); the default is shared with render-settings (gm-centered in CHARGE_RANGE).
|
||||
/** Per-tick anchor re-blend: fraction of an anchored node's displacement
|
||||
* from its previous position that SURVIVES each tick. 0.85 = gentle
|
||||
@@ -238,6 +246,7 @@ export function createGraphSimulation(
|
||||
degree.set(e.to, (degree.get(e.to) ?? 0) + 1);
|
||||
}
|
||||
const linkStrength = options.linkStrength ?? 1;
|
||||
const linkDistance = options.linkDistance ?? LINK_DISTANCE;
|
||||
|
||||
const sim = forceSimulation<LayoutNode, SimLink>(nodes)
|
||||
.force(
|
||||
@@ -246,7 +255,16 @@ export function createGraphSimulation(
|
||||
edges.map((e) => ({ source: e.from, target: e.to })),
|
||||
)
|
||||
.id((d) => d.id)
|
||||
.distance(options.linkDistance ?? LINK_DISTANCE)
|
||||
// Per-link rest length (rendering arc, owner 2026-09-30): a
|
||||
// link touching an implication node (arg_-prefixed id — the
|
||||
// string form pre-init, the node ref after) gets HALF the
|
||||
// base distance, default or user-tuned alike.
|
||||
.distance(
|
||||
(l) =>
|
||||
impLinkId(l.source).startsWith(IMP_ID_PREFIX) || impLinkId(l.target).startsWith(IMP_ID_PREFIX)
|
||||
? linkDistance / 2
|
||||
: linkDistance,
|
||||
)
|
||||
.strength(
|
||||
(l) =>
|
||||
(linkStrength * 1) /
|
||||
|
||||
Reference in New Issue
Block a user