diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts index 6ab1911b7..30b1312f8 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Detail.ts @@ -1,14 +1,22 @@ /* ============================================================================= * M02_DF_Detail.ts - * 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 겹침/안 맞음 · 흐름 줄 · 끊김 까닭 · DOM 없음 - * 모델의 traceBack · traceForward · breaks · applyRule · ruleLabel 이 아직 준비 중이면 같은 뜻의 작은 셈으로 메움 + * 상세 내용 판(선 · 알약) 공용 — 값 읽기 · 규칙 미리 보기 · 끊김 글 · 흐름 줄 · DOM 없음 + * 셈은 모델 것 그대로(Eval 의 evalLogic · breaks · Model 의 traceBack · traceForward · Rule 의 ruleLabel) * 글 [한, 영] 짝은 아래 TEXT * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; -import { breaks, evalLogic, type CfEvalResult, type CfValue, type CfValues } from "./M02_DF_Eval"; import { - portsIn, + breaks, + evalLogic, + type CfBreak, + type CfEvalOptions, + type CfEvalResult, + type CfValue, + type CfValues, +} from "./M02_DF_Eval"; +import { + portDir, portsOut, traceBack, traceForward, @@ -17,26 +25,15 @@ import { type CfLink, type CfLogic, type CfPort, - type CfRound, type CfRule, type CfStep, } from "./M02_DF_Model"; -import { applyRule, ruleLabel } from "./M02_DF_Rule"; +import { ruleLabel } from "./M02_DF_Rule"; const TEXT = { Table: ["표 {n}줄", "table, {n} rows"], Empty: ["값 없음", "no value"], - LabelLookup: ["표에서 「{col}」 찾기", "Look up “{col}” in the table"], - LabelGate: ["「{pill}」 {op} {value} 일 때만 보냄", "Send only when “{pill}” {op} {value}"], - LabelScale: ["값 {op} {value}", "value {op} {value}"], - LabelRound: ["소수 {places}자리 {method}", "{places} decimals, {method}"], NoRule: ["규칙 없음 — 값이 그대로 감", "No rule — the value goes as is"], - Blocked: ["통과 안 됨", "does not pass"], - NoRowLater: ["표에서 맞는 줄은 계산 뒤에 보임", "The matching row shows after calculation"], - EmptyIn: ["빈 받는 자리 — 선이 없음", "Empty receiving spot — no line"], - NoCell: ["받은 알약을 칸에 놓지 않음", "The received pill is not placed in a cell"], - Dead: ["아직 어디로도 안 감", "Goes nowhere yet"], - NoValue: ["값이 아직 없음", "No value yet"], } as const; export const td = (key: keyof typeof TEXT, fill: Record = {}): string => @@ -48,16 +45,22 @@ export const td = (key: keyof typeof TEXT, fill: Record export const GATE_OPS: CfGateOp[] = ["=", "≠", "≥", "≤", ">", "<"]; export const SCALE_OPS: ("×" | "÷" | "+%" | "-%")[] = ["×", "÷", "+%", "-%"]; +/** 셈에 넘길 것 — 값 묶음 · 다른 로직들 */ +export interface DetailOptions { + values?: CfValues; + logics?: CfEvalOptions["logics"]; +} + export const boxOf = (lg: CfLogic, id: string): CfBox | undefined => lg.boxes.find((b) => b.id === id); export const portText = (lg: CfLogic, p: CfPort): string => `${boxOf(lg, p.box)?.name ?? "?"} › ${p.name}`; -/** 계산 한 번 — 못 하면 null */ -export function evalSafe(lg: CfLogic, values?: CfValues): CfEvalResult | null { +/** 계산 한 번 — 한창 만드는 중 막히면 null */ +export function evalSafe(lg: CfLogic, o: DetailOptions = {}): CfEvalResult | null { try { - return evalLogic(lg, values); + return evalLogic(lg, o.values, { logics: o.logics }); } catch { return null; } @@ -79,176 +82,55 @@ export function unitOf(lg: CfLogic, p: CfPort): string { } /** 알약이 나가는(out) 쪽인가 */ -export const isOut = (box: CfBox, name: string): boolean => - portsOut(box).some((p) => p.name === name); +export const isOut = (box: CfBox, name: string): boolean => portDir(box, name) === "out"; -/* ── 규칙 ─────────────────────────────────────────────────────────────── */ +/** 선 위 규칙 글 */ +export const labelOf = (rule: CfRule | undefined): string => + rule ? ruleLabel(rule) : td("NoRule"); -const num = (v: unknown): number | null => { - const n = typeof v === "number" ? v : Number(v); - return Number.isFinite(n) && v !== "" && v !== null ? n : null; -}; - -/** 조건 통과 — 숫자면 숫자끼리 · 아니면 글끼리 */ -export function gatePass(op: CfGateOp, v: unknown, value: number | string): boolean { - const a = num(v); - const b = num(value); - const [x, y] = a !== null && b !== null ? [a, b] : [String(v ?? ""), String(value)]; - switch (op) { - case "=": - return x === y; - case "≠": - return x !== y; - case "≥": - return x >= y; - case "≤": - return x <= y; - case ">": - return x > y; - default: - return x < y; - } +/** 지나가기 전 값 → 지난 뒤 값 — 지난 뒤 값은 셈이 받는 알약에 실제로 들여보낸 것 · 못 들어갔으면 끊긴 까닭 */ +export function preview(link: CfLink, ev: CfEvalResult | null): { before: string; after: string } { + const before = showValue(valueOf(ev, link.from)); + if (!link.rule) return { before, after: before }; + const arrived = ev?.arrived?.[link.to.box]?.[link.to.name]; + if (arrived !== undefined && arrived !== null) return { before, after: showValue(arrived) }; + const why = ev?.breaks?.find((b) => b.link === link.id)?.text; + return { before, after: why ?? td("Empty") }; } -export function roundValue(r: CfRound, v: number): number { - const k = 10 ** r.places; - const f = r.method === "올림" ? Math.ceil : r.method === "버림" ? Math.floor : Math.round; - return f(v * k) / k; +/** 이 선에서 알릴 것 — 이 선이 막힌 까닭 · 같은 알약에서 나간 조건 선의 겹침 · 안 맞음 */ +export function linkNotes(link: CfLink, ev: CfEvalResult | null): string[] { + return (ev?.breaks ?? []) + .filter( + (b) => + b.link === link.id || + ((b.kind === "조건 겹침" || b.kind === "조건 안 맞음") && + b.box === link.from.box && + b.port === link.from.name), + ) + .map((b) => b.text); } -/** 선 위 규칙 글 — 모델이 준비되면 그것, 아니면 같은 뜻의 글 */ -export function labelOf(lg: CfLogic, rule: CfRule | undefined): string { - if (!rule) return td("NoRule"); - try { - return ruleLabel(rule); - } catch { - if (rule.kind === "lookup") return td("LabelLookup", { col: rule.col }); - if (rule.kind === "gate") - return td("LabelGate", { pill: portText(lg, rule.from), op: rule.op, value: rule.value }); - if (rule.kind === "scale") return td("LabelScale", { op: rule.op, value: rule.value }); - return td("LabelRound", { places: rule.places, method: rule.method }); - } -} - -/** 지나가기 전 값 → 지난 뒤 값 미리 보기 */ -export function preview( - link: CfLink, - rule: CfRule | undefined, - ev: CfEvalResult | null, -): { before: string; after: string } { - const raw = valueOf(ev, link.from); - const before = showValue(raw); - if (!rule) return { before, after: before }; - const conds: Record = {}; - if (rule.kind === "lookup") for (const c of rule.conds) conds[c.col] = valueOf(ev, c.from); - try { - const r = applyRule(rule, raw, conds); - return { - before, - after: r.value === null ? (r.why ?? td("Empty")) : showValue(r.value as CfValue), - }; - } catch { - const n = num(raw); - if (rule.kind === "gate") - return { - before, - after: gatePass(rule.op, valueOf(ev, rule.from), rule.value) ? before : td("Blocked"), - }; - if (rule.kind === "lookup" || n === null) return { before, after: td("NoRowLater") }; - if (rule.kind === "round") return { before, after: showValue(roundValue(rule, n)) }; - const v = rule.value; - const out = - rule.op === "×" - ? n * v - : rule.op === "÷" - ? v - ? n / v - : NaN - : rule.op === "+%" - ? n * (1 + v / 100) - : n * (1 - v / 100); - return { before, after: Number.isFinite(out) ? showValue(out) : td("Empty") }; - } -} - -/** 같은 알약에서 나간 조건 선 — 지금 값으로 통과하는 수 */ -export function gateReport( - lg: CfLogic, - link: CfLink, - ev: CfEvalResult | null, -): { total: number; pass: string[] } { - const same = lg.links.filter( - (l) => - l.from.box === link.from.box && l.from.name === link.from.name && l.rule?.kind === "gate", - ); - const pass = same - .filter((l) => { - const r = l.rule as Extract; - return gatePass(r.op, valueOf(ev, r.from), r.value); - }) - .map((l) => l.id); - return { total: same.length, pass }; -} - -/* ── 흐름 · 끊김 ─────────────────────────────────────────────────────── */ - -/** 거슬러 오르는 길(dir "in" = 온 길) · 따라가는 길(dir "out") — 모델 것, 없으면 작은 셈 */ +/** 온 길(dir "in") · 가는 길(dir "out") — 모델 것에서 자기 자신 줄은 뺌 */ export function trace(lg: CfLogic, p: CfPort, dir: "in" | "out"): CfStep[] { - try { - return dir === "in" ? traceBack(lg, p) : traceForward(lg, p); - } catch { - const out: CfStep[] = []; - const seen = new Set(); - const walk = (port: CfPort, back: boolean): void => { - const key = `${port.box}/${port.name}`; - if (seen.has(key)) return; - seen.add(key); - const box = boxOf(lg, port.box); - if (!box) return; - if (back) { - const into = lg.links.filter( - (l) => l.to.box === port.box && (isOut(box, port.name) || l.to.name === port.name), - ); - for (const l of into) { - out.push({ box: l.from.box, port: l.from.name, dir: "out", link: l.id, rule: l.rule }); - walk(l.from, true); - } - } else { - const outs = lg.links.filter((l) => l.from.box === port.box && l.from.name === port.name); - for (const l of outs) { - out.push({ box: l.to.box, port: l.to.name, dir: "in", link: l.id, rule: l.rule }); - const next = boxOf(lg, l.to.box); - for (const o of next ? portsOut(next) : []) walk({ box: next!.id, name: o.name }, false); - } - } - }; - walk(p, dir === "in"); - return out; - } + const steps = dir === "in" ? traceBack(lg, p) : traceForward(lg, p); + return steps.filter((s) => !(s.box === p.box && s.port === p.name)); } /** 그 상자(알약)에서 끊긴 곳 — 쉬운 말 */ -export function breakLines(lg: CfLogic, boxId: string, name?: string): string[] { +export function breakLines( + lg: CfLogic, + boxId: string, + name?: string, + o: DetailOptions = {}, +): string[] { + let list: CfBreak[] = []; try { - return breaks(lg) - .filter((b) => b.box === boxId && (!name || !b.port || b.port === name)) - .map((b) => b.text); + list = breaks(lg, o.values, { logics: o.logics }); } catch { - // 모델의 끊긴 곳 목록이 준비되기 전 — 같은 뜻의 작은 셈 + // 만드는 중 셈이 막히면 끊긴 곳 없음으로 둠 } - const box = boxOf(lg, boxId); - if (!box) return []; - const out: string[] = []; - for (const p of portsIn(box)) { - if (name && p.name !== name) continue; - if (!lg.links.some((l) => l.to.box === boxId && l.to.name === p.name)) out.push(td("EmptyIn")); - else if (box.func?.inputs.some((i) => i.port === p.name && !i.cell)) out.push(td("NoCell")); - } - for (const p of portsOut(box)) { - if (name && p.name !== name) continue; - if (!lg.links.some((l) => l.from.box === boxId && l.from.name === p.name)) out.push(td("Dead")); - } - return out; + return list + .filter((b) => b.box === boxId && (!name || !b.port || b.port === name)) + .map((b) => b.text); } - -export const noValue = (): string => td("NoValue"); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts index 587a5c960..2b9ac51e0 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram_Parts.ts @@ -220,22 +220,8 @@ export function traceOf( 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}`; - } -} +/** 선 위 규칙 글 — Rule 의 ruleLabel · 규칙 없으면 「」 */ +export const linkLabel = (link: CfLink): string => (link.rule ? ruleLabel(link.rule) : ""); /** 굽은 선 가운데 점 — 양 끝 손잡이가 마주 보는 베지어라 t = 0.5 가 두 끝의 가운데 */ export const midOf = ( diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts index cb0516e43..9b6380065 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Eval.ts @@ -347,7 +347,8 @@ function otherLogic( } const inner: CfValues = {}; for (const d of designInputs(other)) { - const v = d.port in arrive ? arrive[d.port] : given[d.port]; + // 값 차례 = 이어진 선 > 넘긴 값 > 상자에 둔 예시값 > 그 로직 자기 예시값(inner 에 안 넣음) + const v = d.port in arrive ? arrive[d.port] : (given[d.port] ?? ref?.examples?.[d.port]); if (v !== undefined && v !== null) (inner[d.box] ??= {})[d.port] = v; } const ev = evalLogic(other, inner, { logics: opts.logics, depth: (opts.depth ?? 0) + 1 }); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts index f6263d2ce..541fd358c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Build.ts @@ -227,7 +227,11 @@ export function checkMaterial(el: CfElementRef, others: CfElementRef[]): string if (!el.name) return "자리 이름을 적어 주세요"; if (others.some((o) => o.name === el.name)) return "같은 이름의 알약이 이미 있음"; if (el.mode === "rule" && !el.rule?.from.name) return "규칙에 쓸 변수 알약을 골라 주세요"; - if (el.mode !== "fixed" && !Object.keys(el.scope ?? {}).length) + if ( + el.mode !== "fixed" && + !(el.mode === "choice" && el.default) && + !Object.keys(el.scope ?? {}).length + ) return "범위(구분 · 상세구분 · 찾는 말) 하나는 정해 주세요"; return null; } @@ -255,7 +259,7 @@ export function logicRefOf(key: string, other: CfLogic): CfLogicRef { }; } -/** 다른 로직 입력 넣기 · 빼기(null) — 빼면 변수 상태로 돌아가고 그 알약에서 나간 선도 지움 */ +/** 다른 로직 입력 넣기 · 빼기(null) — 빼면 변수 상태로 돌아가고 이 상자의 알약에 닿은 선은 모두 지움 */ export function setLogicInput(lg: CfLogic, id: string, ref: CfLogicRef | null): CfLogic { return { ...lg, @@ -266,7 +270,7 @@ export function setLogicInput(lg: CfLogic, id: string, ref: CfLogicRef | null): : { mode: "variable", elements: [], variables: [] }; return { ...b, input, unit: unitOfInput(input) }; }), - links: lg.links.filter((l) => l.from.box !== id), + links: lg.links.filter((l) => l.from.box !== id && l.to.box !== id), // 나가던 선 · 그 로직의 받는 알약으로 들어오던 선 모두 }; } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts index d24043210..0d2e45136 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts @@ -291,7 +291,7 @@ function itemList( export async function previewOf( el1: CfElementRef, lg: CfLogic, -): Promise<{ found: number; names: string[]; cheapest: MatItem | null }> { +): Promise<{ found: number; names: string[]; cheapest: MatItem | null; unit: string }> { const scope = el1.scope ?? {}; const word = scope.찾는말 ?? ""; if (el1.mode === "rule" && el1.rule) { @@ -300,7 +300,7 @@ export async function previewOf( ); const value = pill?.example; if (value === null || value === undefined || value === "") - return { found: 0, names: [], cheapest: null }; + return { found: 0, names: [], cheapest: null, unit: "" }; const r = await searchMaterials({ q: `${word} ${value}`.trim(), sub: scope.구분, @@ -313,6 +313,7 @@ export async function previewOf( found: hit.length, names: hit.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()), cheapest: cheapestOf(hit), + unit: commonUnit(hit), }; } const r = await searchMaterials({ @@ -326,9 +327,14 @@ export async function previewOf( found: r.total, names: r.items.slice(0, 3).map((i) => `${i.name} ${i.spec}`.trim()), cheapest: cheapestOf(r.items), + unit: commonUnit(r.items), }; } +/** 잡힌 품목 단위가 모두 같으면 그 단위 · 아니면 빈 글 */ +const commonUnit = (items: MatItem[]): string => + items.length && items.every((i) => i.unit === items[0].unit) ? items[0].unit : ""; + const cheapestOf = (items: MatItem[]): MatItem | null => items .filter((i) => i.price !== null) @@ -541,7 +547,11 @@ export function renderMaterial( const why = checkMaterial(cand, els); name.setError(why ?? undefined); if (why) return void showToast(why, "error"); - on.change(addEl(lg, cand)); + // 잡히는 재료의 단위가 하나로 같으면 그 단위를 알약 단위로 + void previewOf(cand, lg).then( + (p) => on.change(addEl(lg, p.unit ? { ...cand, unit: p.unit } : cand)), + () => on.change(addEl(lg, cand)), + ); }, }); const clear = createButton({ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts index 32f1d9fb9..1d7c4043c 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_LinkPanel.ts @@ -8,7 +8,6 @@ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; -import type { CfValues } from "./M02_DF_Eval"; import { portsOut, reaches, @@ -22,12 +21,13 @@ import { GATE_OPS, SCALE_OPS, evalSafe, - gateReport, labelOf, + linkNotes, portText, preview, unitOf, valueOf, + type DetailOptions, } from "./M02_DF_Detail"; import { suggestPlaces } from "./M02_DF_UnitPick"; import "./M02_DF_LinkPanel.css"; @@ -317,12 +317,12 @@ export function renderLinkPanel( lg: CfLogic, linkId: string, on: { change(lg: CfLogic): void }, - opts: { values?: CfValues; readOnly?: boolean } = {}, + opts: DetailOptions & { readOnly?: boolean } = {}, ): void { const link = lg.links.find((l) => l.id === linkId); host.classList.add("m02-dflink"); if (!link) return host.replaceChildren(); - const ev = evalSafe(lg, opts.values); + const ev = evalSafe(lg, opts); const table = valueOf(ev, link.from); const headers = Array.isArray(table) && Array.isArray(table[0]) @@ -358,7 +358,7 @@ export function renderLinkPanel( if (rule?.kind === "scale") parts.push(...scaleEditor(ctx, rule)); if (rule?.kind === "round") parts.push(...roundEditor(ctx, rule, unitOf(lg, link.from))); - const pv = preview(link, rule, ev); + const pv = preview(link, ev); parts.push( row( el("span", { className: "m02-dflink-label", text: tk("Before") }), @@ -369,16 +369,11 @@ export function renderLinkPanel( ), row( el("span", { className: "m02-dflink-label", text: tk("Label") }), - el("span", { text: labelOf(lg, rule) }), + el("span", { text: labelOf(rule) }), ), ); - const warns: string[] = []; - if (rule?.kind === "gate") { - const rep = gateReport(lg, link, ev); - if (rep.pass.length > 1) warns.push(tk("WarnBoth", rep.pass.length)); - if (rep.total > 0 && rep.pass.length === 0) warns.push(tk("WarnNone", rep.total)); - } + const warns: string[] = linkNotes(link, ev); if (rule?.kind === "lookup") { if (!rule.conds.length) warns.push(tk("WarnLookup")); if (!rule.col) warns.push(tk("WarnTake")); diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Model.ts b/M02_MasterTemplete/DataFlow/M02_DF_Model.ts index 73a18d287..f8ee64e95 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Model.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Model.ts @@ -64,6 +64,8 @@ export interface CfLogicRef { outputs: CfPill[]; /** 받는 알약 = 그 로직의 「설계 때 정할 것」(변수 · 재료 자리) */ designs: CfPill[]; + /** 선 없는 받는 알약의 예시값(키 = 받는 알약 이름) — 값 차례 = 이어진 선 > 넘긴 값 > 이것 > 그 로직 자기 예시값 */ + examples?: Record; } export interface CfInput { @@ -154,6 +156,8 @@ export interface CfDesign { unit: string; variable?: CfVariable; element?: CfElementRef; + /** 보일 예시값 — 변수 = variable.example · 다른 로직 = logic.examples(없으면 빈 칸 → 그 로직 자기 예시값) */ + example?: number | string | null; } /** 사용자 요소 — 그룹에 없는 새 요소(마스터에 안 씀 · `_data` 에 저장) */ @@ -567,7 +571,14 @@ export function designInputs(lg: CfLogic): CfDesign[] { for (const b of lg.boxes) { const vars = b.input?.variables ?? b.func?.variables ?? []; for (const v of vars) - out.push({ box: b.id, port: v.name, kind: "변수", unit: v.unit || "-", variable: clone(v) }); + out.push({ + box: b.id, + port: v.name, + kind: "변수", + unit: v.unit || "-", + variable: clone(v), + example: v.example, + }); for (const e of b.input?.mode === "group" ? b.input.elements : []) if (e.mode === "choice") out.push({ @@ -580,7 +591,13 @@ export function designInputs(lg: CfLogic): CfDesign[] { if (b.input?.mode === "logic") for (const d of b.input.logic?.designs ?? []) if (!lg.links.some((l) => l.to.box === b.id && l.to.name === d.name)) - out.push({ box: b.id, port: d.name, kind: "다른 로직", unit: d.unit || "-" }); + out.push({ + box: b.id, + port: d.name, + kind: "다른 로직", + unit: d.unit || "-", + example: b.input.logic?.examples?.[d.name] ?? null, + }); } return out; } diff --git a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts index 3c92bea06..dcd3a4aa3 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_PillPanel.ts @@ -7,7 +7,6 @@ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el } from "@ui/ui_template_elements"; -import type { CfValues } from "./M02_DF_Eval"; import type { CfLogic, CfStep } from "./M02_DF_Model"; import { boxOf, @@ -20,6 +19,7 @@ import { trace, unitOf, valueOf, + type DetailOptions, } from "./M02_DF_Detail"; import "./M02_DF_LinkPanel.css"; @@ -50,6 +50,7 @@ const line = (label: string, ...value: (HTMLElement | string)[]): HTMLElement => function steps( lg: CfLogic, list: CfStep[], + arrow: string, empty: string, onBox?: (id: string) => void, ): HTMLElement { @@ -58,7 +59,7 @@ function steps( className: "m02-dfpill-list", children: list.map((s) => { const parts: (HTMLElement | string)[] = [ - s.dir === "out" ? "◀" : "▶", + arrow, onBox ? createButton({ label: portText(lg, { box: s.box, name: s.port }), @@ -68,8 +69,7 @@ function steps( }) : portText(lg, { box: s.box, name: s.port }), ]; - if (s.rule) - parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(lg, s.rule) })); + if (s.rule) parts.push(el("span", { className: "m02-dfpill-rule", text: labelOf(s.rule) })); return el("li", { className: "m02-dfpill-step", children: parts }); }), }); @@ -82,20 +82,18 @@ export function renderPillPanel( boxId: string, port: string, on: { pick?(boxId: string): void } = {}, - opts: { values?: CfValues } = {}, + opts: DetailOptions = {}, ): void { host.classList.add("m02-dfpill"); const box = boxOf(lg, boxId); if (!box) return host.replaceChildren(); - const ev = evalSafe(lg, opts.values); + const ev = evalSafe(lg, opts); const out = isOut(box, port); - // 받는 알약의 값 = 그 자리로 들어오는 선의 보낸 알약 값(규칙 지나기 전) + // 받는 알약의 값 = 선 규칙을 지나 실제로 들어온 값(없으면 보낸 알약 값) const incoming = lg.links.find((l) => l.to.box === boxId && l.to.name === port); const value = out ? valueOf(ev, { box: boxId, name: port }) - : incoming - ? valueOf(ev, incoming.from) - : undefined; + : (ev?.arrived?.[boxId]?.[port] ?? (incoming ? valueOf(ev, incoming.from) : undefined)); const unit = out ? unitOf(lg, { box: boxId, name: port }) : incoming @@ -103,7 +101,7 @@ export function renderPillPanel( : box.unit; const back = trace(lg, { box: boxId, name: port }, "in"); const forward = trace(lg, { box: boxId, name: port }, "out"); - const breaks = breakLines(lg, boxId, port); + const breaks = breakLines(lg, boxId, port, opts); host.replaceChildren( el("div", { className: "m02-dflink-head", @@ -118,13 +116,13 @@ export function renderPillPanel( el("div", { children: [ el("div", { className: "m02-dflink-label", text: tp("From") }), - steps(lg, back, tp("Start"), on.pick), + steps(lg, back, "◀", tp("Start"), on.pick), ], }), el("div", { children: [ el("div", { className: "m02-dflink-label", text: tp("To") }), - steps(lg, forward, tp("End"), on.pick), + steps(lg, forward, "▶", tp("End"), on.pick), ], }), el("div", { diff --git a/resources/tester/test_m02_df_inputpanel_material.py b/resources/tester/test_m02_df_inputpanel_material.py index 43355562d..7f5b2657b 100644 --- a/resources/tester/test_m02_df_inputpanel_material.py +++ b/resources/tester/test_m02_df_inputpanel_material.py @@ -230,10 +230,13 @@ def test_다른_로직_상자는_출력_단위를_따르고_빼면_변수로_돌 assert box["unit"] == "㎥/시간" assert call("pillNames", box) == ["작업량"] lg["links"] = [ - {"id": "l1", "from": {"box": "b1", "name": "작업량"}, "to": {"box": "b2", "name": "x"}} + {"id": "l1", "from": {"box": "b1", "name": "작업량"}, "to": {"box": "b2", "name": "x"}}, + {"id": "l2", "from": {"box": "b2", "name": "관경"}, "to": {"box": "b1", "name": "q"}}, + {"id": "l3", "from": {"box": "b2", "name": "관경"}, "to": {"box": "b3", "name": "y"}}, ] back = call("setLogicInput", lg, "b1", None) - assert back["boxes"][0]["input"]["mode"] == "variable" and back["links"] == [] + assert back["boxes"][0]["input"]["mode"] == "variable" + assert [x["id"] for x in back["links"]] == ["l3"] assert call("reconcileUnit", lg, "b1") == lg # 다른 로직 상자 단위는 안 건드림 diff --git a/resources/tester/test_m02_df_model.py b/resources/tester/test_m02_df_model.py index c8b0663bd..56bdf7c3d 100644 --- a/resources/tester/test_m02_df_model.py +++ b/resources/tester/test_m02_df_model.py @@ -592,3 +592,20 @@ def test_끝수_함수_출력_출력_상자() -> None: "out([a, E.evalLogic(lg, V).cost.노무비]);" ) assert got == [537000, 540000] + + +def test_다_다른_로직_받는_알약_예시값_차례() -> None: + got = _run( + LOGIC + + "lg = M.updateBox(lg, other.id, { input: { ...lg.boxes[1].input, logic: { ...lg.boxes[1].input.logic, examples: { 계수: 10, 면적: 99 } } } });\n" + "const a = E.evalLogic(lg, {}, { logics: LOGICS }).cost.경비;\n" + "const b = E.evalLogic(lg, { [other.id]: { 계수: 2 } }, { logics: LOGICS }).cost.경비;\n" + "const noEx = M.updateBox(lg, other.id, { input: { ...lg.boxes[1].input, logic: { ...lg.boxes[1].input.logic, examples: undefined } } });\n" + "out([a, b, E.evalLogic(noEx, {}, { logics: LOGICS }).cost.경비," + " M.designInputs(lg).map(d => [d.kind, d.port, d.example]), M.designInputs(noEx).map(d => d.example)]);" + ) + a, b, c, designs, plain = got + # 면적 = 이어진 선(길이 7) > 예시값 99 · 계수 = 상자 예시값 10 · 넘긴 값 2 가 이김 · 없으면 그 로직 예시값 3 + assert (a, b, c) == (70000, 14000, 21000) + assert designs == [["변수", "길이", 7], ["다른 로직", "계수", 10]] + assert plain == [7, None]