fix(M02): 흐름도 쉼 흐림 — 상자는 들어오는 선이 모두 쉼(또는 쉼이 걸리고 내보내는 값이 모두 빈 값)일 때만 · 일부면 그 선과 받는 알약만(PLAN 35-9 브레인 재검증)
- restParts(Parts) · 받는 알약 쉼 모양(점선 · 흐린 글) - 시험 test_m02_df_diagram_parts.py 6 통과 · DataFlow 전부 검사 초록 · Diagram 699줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
This commit is contained in:
@@ -498,3 +498,11 @@
|
||||
gap: var(--spacing-8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 쉼 — 받는 알약만(상자 흐림은 들어오는 선이 모두 쉼일 때) */
|
||||
.m02-dfdg__pill.is-in.is-rest {
|
||||
border-style: dashed;
|
||||
border-color: var(--color-text-muted);
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
follow,
|
||||
depLines,
|
||||
isRest,
|
||||
restParts,
|
||||
linkLabel,
|
||||
midOf,
|
||||
pillsOf,
|
||||
@@ -86,11 +87,11 @@ export function renderDiagram(
|
||||
/** 「!」 는 문제에만 · 쉼(조건이 안 맞아 이번엔 안 지나감)은 회색 점선 선 + 받는 상자 흐리게 */
|
||||
const breaks = allBreaks.filter((k) => !isRest(k));
|
||||
const rests = allBreaks.filter(isRest);
|
||||
const restLinks = new Set(rests.map((k) => k.link).filter(Boolean));
|
||||
const restBoxes = new Set([
|
||||
...rests.filter((k) => !k.link).map((k) => k.box),
|
||||
...lg.links.filter((l) => restLinks.has(l.id)).map((l) => l.to.box),
|
||||
]);
|
||||
const {
|
||||
links: restLinks,
|
||||
pills: restPills,
|
||||
boxes: restBoxes,
|
||||
} = restParts(lg, rests, opts.values ?? {});
|
||||
const boxName = (id: string): string => lg.boxes.find((b) => b.id === id)?.name ?? "";
|
||||
const redraw = (): void =>
|
||||
renderDiagram(host, lg, on, { ...opts, selected: st.selected, selectedPill: st.pill });
|
||||
@@ -160,6 +161,7 @@ export function renderDiagram(
|
||||
wired ? "" : "is-empty",
|
||||
chosen ? "is-chosen" : "",
|
||||
broken ? "is-broken" : "",
|
||||
dir === "in" && restPills.has(`${b.id}|${p.name}`) ? "is-rest" : "",
|
||||
st.linking && dir === "in" && b.id !== st.linking.box ? "is-target" : "",
|
||||
].join(" ") + markOf((s) => s.some((x) => x.box === b.id && x.port === p.name)),
|
||||
attrs: {
|
||||
|
||||
@@ -306,6 +306,29 @@ export function depPath(a: Pt, b: Pt, side: "left" | "right"): string {
|
||||
/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — Eval 의 level 그대로 */
|
||||
export const isRest = (k: CfBreak): boolean => k.level === "쉼";
|
||||
|
||||
/** 쉼 모양 자리 — 선 · 받는 알약(「상자|알약」) · 흐릴 상자
|
||||
* 상자 흐림은 들어오는 선이 모두 쉼일 때 · 또는 상자에 걸린 쉼이 있고 그 상자 내보내는 값이 모두 빈 값일 때만 */
|
||||
export function restParts(
|
||||
lg: CfLogic,
|
||||
rests: CfBreak[],
|
||||
values: Record<string, Record<string, unknown>>,
|
||||
): { links: Set<string>; pills: Set<string>; boxes: Set<string> } {
|
||||
const links = new Set(rests.map((k) => k.link).filter((x): x is string => !!x));
|
||||
const pills = new Set(
|
||||
lg.links.filter((l) => links.has(l.id)).map((l) => `${l.to.box}|${l.to.name}`),
|
||||
);
|
||||
const boxes = new Set<string>();
|
||||
const empty = (v: unknown): boolean => v === null || v === undefined || v === "";
|
||||
for (const b of lg.boxes) {
|
||||
const incoming = lg.links.filter((l) => l.to.box === b.id);
|
||||
const allRest = incoming.length > 0 && incoming.every((l) => links.has(l.id));
|
||||
const outs = Object.values(values[b.id] ?? {});
|
||||
const hit = rests.some((k) => !k.link && k.box === b.id);
|
||||
if (allRest || (hit && outs.every(empty))) boxes.add(b.id);
|
||||
}
|
||||
return { links, pills, boxes };
|
||||
}
|
||||
|
||||
/* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */
|
||||
|
||||
/** 누른 뒤 5px 넘게 움직이면 끌기 — 끈 뒤 따라오는 click 한 번만 삼킴 */
|
||||
|
||||
@@ -46,6 +46,10 @@ const out = jobs.map((job) => {
|
||||
if (job.op === "fit") return m.fitView(job.rects, job.view);
|
||||
if (job.op === "value") return job.values.map((v) => m.showValue(v));
|
||||
if (job.op === "end") return m.depEnd(job.a, job.tag);
|
||||
if (job.op === "restParts") {
|
||||
const r = m.restParts(job.lg, job.rests, job.values ?? {});
|
||||
return { links: [...r.links].sort(), pills: [...r.pills].sort(), boxes: [...r.boxes].sort() };
|
||||
}
|
||||
if (job.op === "path") return m.depPath(job.a, job.b, job.side);
|
||||
return null;
|
||||
});
|
||||
@@ -269,3 +273,34 @@ def test_기댐_선_끝이_알약과_이름표에_붙음():
|
||||
assert path_right.startswith("M100,50 ") and path_right.endswith(" 130,300")
|
||||
handle = float(path_right.split(" ")[2].split(",")[0])
|
||||
assert handle > 130
|
||||
|
||||
|
||||
def test_쉼은_일부면_선과_받는_알약만_모두면_상자도():
|
||||
two_in = {
|
||||
**LOGIC,
|
||||
"links": [
|
||||
{"id": "a", "from": port("v", "지반"), "to": port("f", "품")},
|
||||
{"id": "b", "from": port("m", "흄관"), "to": port("f", "흄관")},
|
||||
],
|
||||
}
|
||||
one_rest = [
|
||||
{"kind": "통과 안 됨", "level": "쉼", "box": "f", "port": "흄관", "link": "b", "text": "쉼"}
|
||||
]
|
||||
all_rest = one_rest + [
|
||||
{"kind": "통과 안 됨", "level": "쉼", "box": "f", "port": "품", "link": "a", "text": "쉼"}
|
||||
]
|
||||
box_rest = [{"kind": "통과 안 됨", "level": "쉼", "box": "m", "text": "쉼"}]
|
||||
part, every, filled, empty = run(
|
||||
[
|
||||
{"op": "restParts", "lg": two_in, "rests": one_rest, "values": {"f": {"금액": 2000}}},
|
||||
{"op": "restParts", "lg": two_in, "rests": all_rest, "values": {"f": {"금액": None}}},
|
||||
{"op": "restParts", "lg": two_in, "rests": box_rest, "values": {"m": {"흄관": 52000}}},
|
||||
{"op": "restParts", "lg": two_in, "rests": box_rest, "values": {"m": {"흄관": None}}},
|
||||
]
|
||||
)
|
||||
# 하나만 쉼 — 셈하는 상자는 안 흐림 · 그 선과 받는 알약만
|
||||
assert part == {"links": ["b"], "pills": ["f|흄관"], "boxes": []}
|
||||
# 들어오는 선이 모두 쉼 — 상자도 흐림
|
||||
assert every["boxes"] == ["f"]
|
||||
# 상자에 걸린 쉼 — 내보내는 값이 있으면 안 흐림 · 모두 빈 값이면 흐림
|
||||
assert filled["boxes"] == [] and empty["boxes"] == ["m"]
|
||||
|
||||
Reference in New Issue
Block a user