diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index dbcdcf73d..a6ccd0742 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -14,6 +14,7 @@ import { clampZoom, fitView, KIND_COLOR, + linkLabel, midOf, pillsOf, resolveDrop, @@ -135,7 +136,7 @@ export function renderDiagram( const pillEl = (b: CfBox, p: CfPill, dir: "in" | "out"): HTMLElement => { const wired = dir === "out" || lg.links.some((l) => l.to.box === b.id && l.to.name === p.name); const chosen = st.pill?.box === b.id && st.pill.port === p.name && st.pill.dir === dir; - const broken = breaks.find((k) => k.port?.box === b.id && k.port.name === p.name); + const broken = breaks.find((k) => !k.link && k.box === b.id && k.port === p.name); const sel: DfPillSel = { box: b.id, port: p.name, dir }; const btn = el("button", { className: @@ -146,7 +147,7 @@ export function renderDiagram( chosen ? "is-chosen" : "", broken ? "is-broken" : "", st.linking && dir === "in" && b.id !== st.linking.box ? "is-target" : "", - ].join(" ") + markOf((s) => !!s.pills?.some((x) => x.box === b.id && x.name === p.name)), + ].join(" ") + markOf((s) => s.some((x) => x.box === b.id && x.port === p.name)), attrs: { type: "button", "data-box": b.id, @@ -165,7 +166,7 @@ export function renderDiagram( if (st.linking && dir === "in") return tryLink(st.linking, { box: b.id, name: p.name, dir }); st.linking = null; if (chosen) select(null); - else select(b.id, sel, broken?.why ?? ""); + else select(b.id, sel, broken?.text ?? ""); }); if (dir === "out") btn.addEventListener("pointerdown", (ev) => dragLink(ev, { box: b.id, name: p.name })); @@ -197,7 +198,7 @@ export function renderDiagram( const boxEl = (b: CfBox): HTMLElement => { const { ins, outs } = pillsOf(b); const kind = tt(`K_${b.kind}` as keyof typeof TEXT); - const broken = breaks.filter((k) => k.box === b.id); + const broken = breaks.filter((k) => !k.link && !k.port && k.box === b.id); const head = el("div", { className: "m02-dfdg__head", children: [ @@ -209,7 +210,7 @@ export function renderDiagram( const node = el("div", { className: `m02-dfdg__box is-${b.kind}${st.selected === b.id ? " is-selected" : ""}` + - markOf((s) => !!s.boxes?.includes(b.id)), + (st.selected === b.id ? "" : markOf((s) => s.some((x) => x.box === b.id))), attrs: { "data-box": b.id, tabindex: "0", @@ -236,8 +237,8 @@ export function renderDiagram( }), ...(broken.length ? [ - warn(broken.map((k) => k.why).join(" · "), () => - select(b.id, undefined, broken.map((k) => k.why).join(" · ")), + warn(broken.map((k) => k.text).join(" · "), () => + select(b.id, undefined, broken.map((k) => k.text).join(" · ")), ), ] : []), @@ -349,7 +350,7 @@ export function renderDiagram( ), ); } else if (selLink) { - const rule = labels[selLink.id]; + const rule = labels[selLink.id] ?? linkLabel(selLink); const line = tt("LineOf", { a: endName(selLink.from), b: endName(selLink.to) }); tools.push( el("span", { text: rule ? `${line} · ${rule}` : line }), @@ -470,7 +471,8 @@ export function renderDiagram( const strong = st.selected === l.id || (selBox !== null && (l.from.box === selBox.id || l.to.box === selBox.id)); - const mark = markOf((s) => !!s.links?.includes(l.id)); + const traced = markOf((s) => s.some((x) => x.link === l.id)); + const mark = strong && traced === " is-dim" ? "" : traced; // 고른 선은 흐리게 하지 않음 const dim = !mark && st.selected !== null && !strong ? " is-dim" : ""; const g = add(svg, "g", { class: `m02-dfdg__link${strong ? " is-strong" : ""}${dim}${mark}`, @@ -480,7 +482,7 @@ export function renderDiagram( add(g, "path", { d, class: "m02-dfdg__hit" }); add(g, "path", { d, class: "m02-dfdg__wire" }); const mid = midOf(a, b); - const rule = labels[l.id]; + const rule = labels[l.id] ?? linkLabel(l); if (rule) { const tag = add(g, "g", { class: "m02-dfdg__rule" }); const box = add(tag, "rect", { rx: "4", ry: "4" }); @@ -498,7 +500,7 @@ export function renderDiagram( } const broken = breaks.filter((k) => k.link === l.id); if (broken.length) { - const why = broken.map((k) => k.why).join(" · "); + const why = broken.map((k) => k.text).join(" · "); const badge = add(g, "g", { class: "m02-dfdg__linkwarn", "data-why": why }); add(badge, "circle", { cx: mid.x.toFixed(1), @@ -516,7 +518,7 @@ export function renderDiagram( } g.addEventListener("click", (ev) => { ev.stopPropagation(); - const why = broken.map((k) => k.why).join(" · "); + const why = broken.map((k) => k.text).join(" · "); select(st.selected === l.id ? null : l.id, undefined, st.selected === l.id ? "" : why); }); } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index b618b1a03..587a5c960 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -13,7 +13,11 @@ import { type CfLogic, type CfPill, type CfPort, + type CfLink, + type CfStep, } from "./M02_DF_Model"; +import type { CfBreak } from "./M02_DF_Eval"; +import { ruleLabel } from "./M02_DF_Rule"; export const TEXT = { Title: ["다이어그램", "Diagram"], @@ -81,21 +85,6 @@ export interface DfPillSel { dir: "in" | "out"; } -/** 흐름 길 한쪽 — 칠할 상자 · 선 · 알약 */ -export interface DfTraceSet { - boxes?: string[]; - links?: string[]; - pills?: { box: string; name: string }[]; -} - -/** 끊긴 곳 — 상자 · 선 · 알약 중 하나 + 까닭 */ -export interface DfBreak { - box?: string; - link?: string; - port?: { box: string; name: string }; - why: string; -} - export interface DiagramOn { /** 상자 · 선 id · 없음 — 알약을 고르면 상자 id 와 알약 */ select(id: string | null, pill?: DfPillSel): void; @@ -114,12 +103,12 @@ export interface DiagramOpts { selected?: string | null; /** 고른 알약 */ selectedPill?: DfPillSel | null; - /** 선 id → 선 규칙 글(「표: 석공 칸」 · 「× 1.3」) — 없으면 이름표 없음 */ + /** 선 id → 선 규칙 글 — 없으면 선의 규칙(CfLink.rule)에서 · 규칙 없으면 이름표 없음 */ labels?: Record; - /** 흐름 길 — 온 길 · 가는 길 */ - trace?: { back?: DfTraceSet; forward?: DfTraceSet } | null; - /** 끊긴 곳 */ - breaks?: DfBreak[]; + /** 흐름 길 — Model traceBack · traceForward 걸음 */ + trace?: { back?: CfStep[]; forward?: CfStep[] } | null; + /** 끊긴 곳 — Eval breaks */ + breaks?: CfBreak[]; } /* ── 알약 · 자리 ────────────────────────────────────────────────────── */ @@ -216,18 +205,38 @@ export const showValue = (v: unknown): string => { return String(v).slice(0, 12); }; -/* ── 흐름 길 ────────────────────────────────────────────────────────── */ +/* ── 흐름 길 · 선 규칙 글 ───────────────────────────────────────────── */ /** 흐름 길 자리 — 온 길 · 가는 길 · 흐림(길이 있는데 안 듦) · 「」(길 없음) */ export type TraceMark = "back" | "fwd" | "dim" | ""; -export function traceOf(trace: DiagramOpts["trace"], pick: (s: DfTraceSet) => boolean): TraceMark { - if (!trace || (!trace.back && !trace.forward)) return ""; +export function traceOf( + trace: DiagramOpts["trace"], + pick: (steps: CfStep[]) => boolean, +): TraceMark { + if (!trace || (!trace.back?.length && !trace.forward?.length)) return ""; if (trace.back && pick(trace.back)) return "back"; if (trace.forward && pick(trace.forward)) return "fwd"; return "dim"; } +/** 선 위 규칙 글 — Rule 의 ruleLabel · 준비 전이면 같은 꼴로 */ +export function linkLabel(link: CfLink): string { + const r = link.rule; + if (!r) return ""; + try { + return ruleLabel(r); + } catch { + if (r.kind === "lookup") return `표: ${r.col} 칸`; + if (r.kind === "gate") return `${r.from.name} ${r.op} ${r.value}일 때`; + if (r.kind === "scale") + return r.op === "×" || r.op === "÷" + ? `${r.op} ${r.value}` + : `${r.op === "+%" ? "+" : "−"}${r.value}%`; + return `소수 ${r.places} ${r.method}`; + } +} + /** 굽은 선 가운데 점 — 양 끝 손잡이가 마주 보는 베지어라 t = 0.5 가 두 끝의 가운데 */ export const midOf = ( a: { x: number; y: number },