feat(M02): 흐름도 다이어그램 — 넓힌 타입에 맞춤 · 선 규칙 이름표(CfLink.rule) · 흐름 길(CfStep) · 끊긴 곳(CfBreak) · 고른 선 · 상자는 흐리게 안 함(PLAN 35-9)
- DataFlow 전부 검사 초록 · Diagram 639줄 + Parts 248줄 - ORCA 견본(상자 6 · 선 5 · 규칙 넷): 이름표 「표: 석공 칸」「× 1.3」「지반 = 암일 때」「소수 2 반올림」 선 가운데 0.1 안 · 온 길 상자 3 · 선 2 · 알약 5 / 가는 길 상자 2 · 선 2 · 알약 3 / 나머지 흐림(색 + 굵기 3.5 + 점선) · 끊긴 곳 상자 · 선 · 알약 표시 · 누르면 까닭 · 알약 고름 on.select(f1, 알약) · 선 고름 진하게 · 가운데 단추 끌기 판 +60,+30(상자 안 움직임) · 왼쪽 빈 곳 끌기 판 그대로 · Space+끌기 판 −50 · 상자 머리 끌기 on.move · user-select none · 밝음 · 어두움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<string, string>;
|
||||
/** 흐름 길 — 온 길 · 가는 길 */
|
||||
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 },
|
||||
|
||||
Reference in New Issue
Block a user