Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_2
This commit is contained in:
@@ -440,6 +440,9 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
|
||||
return sheet
|
||||
|
||||
|
||||
REST = "앞 조건이 안 맞아 쉬는 갈래"
|
||||
|
||||
|
||||
def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | None = None) -> dict:
|
||||
"""셈 — {ports, outputs, cost, errors, breaks, filled}(filled = 받는 칸에 값만 되돌려 넣음)."""
|
||||
ctx = {"load": load, "local": local, "stack": stack or [lg.get("key") or ""]}
|
||||
@@ -448,6 +451,7 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
name = lambda i: boxes.get(i, {}).get("name") or i # noqa: E731
|
||||
errors, breaks, ports, arrived, outputs = [], [], {}, {}, {}
|
||||
blocked: set[str] = set()
|
||||
resting: set[str] = set() # 조건이 안 맞아 쉬는 상자 — Eval 과 같은 뜻
|
||||
cost: dict[str, Any] = {**dict.fromkeys(COSTS, Decimal(0)), "값들": {}}
|
||||
|
||||
def through(ln: dict) -> Any:
|
||||
@@ -464,13 +468,15 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
got, why = apply_rule(rule, v, conds)
|
||||
if why:
|
||||
blocked.add(ln["to"].get("box"))
|
||||
if why == "통과 안 됨":
|
||||
resting.add(ln["to"].get("box"))
|
||||
text = f"「{name(src.get('box'))}」 → 「{name(ln['to'].get('box'))}」 {why}"
|
||||
kind = "오류" if why in ("표에 여럿 맞음", "오류") else why
|
||||
to = ln["to"]
|
||||
breaks.append(
|
||||
{
|
||||
"kind": kind,
|
||||
# 무게 — 통과 안 됨 = 쉼(조건이 안 맞아 쉬는 갈래) · 나머지 = 문제(Eval CfBreak.level 과 같음)
|
||||
# 무게 — 통과 안 됨 = 쉼 · 나머지 = 문제(Eval CfBreak.level 과 같음)
|
||||
"level": "쉼" if kind == "통과 안 됨" else "문제",
|
||||
"box": to.get("box"),
|
||||
"port": to.get("name"),
|
||||
@@ -483,9 +489,32 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
|
||||
return None if why else got
|
||||
|
||||
def arrive(box: dict) -> dict:
|
||||
got = {
|
||||
ln["to"].get("name"): through(ln) for ln in links if ln["to"].get("box") == box["id"]
|
||||
}
|
||||
"""받는 값 — 쉬는 갈래 빈 값은 「쉼」 줄만 · 선이 모두 쉬면 상자도 쉼(Eval 과 같음)."""
|
||||
ins = [ln for ln in links if ln["to"].get("box") == box["id"]]
|
||||
got, rest = {}, 0
|
||||
for ln in ins:
|
||||
src, to = ln.get("from") or {}, ln["to"]
|
||||
if (
|
||||
src.get("box") in resting
|
||||
and ports.get(src.get("box"), {}).get(src.get("name")) is None
|
||||
):
|
||||
rest += 1
|
||||
got[to.get("name")] = None
|
||||
breaks.append(
|
||||
{
|
||||
"kind": "통과 안 됨",
|
||||
"level": "쉼",
|
||||
"box": to.get("box"),
|
||||
"port": to.get("name"),
|
||||
"link": ln.get("id"),
|
||||
"text": f"「{name(src.get('box'))}」 → 「{name(to.get('box'))}」 — {REST}",
|
||||
}
|
||||
)
|
||||
continue
|
||||
got[to.get("name")] = through(ln)
|
||||
if ins and rest == len(ins):
|
||||
resting.add(box["id"])
|
||||
blocked.add(box["id"])
|
||||
arrived[box["id"]] = got
|
||||
return got
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@ import {
|
||||
clampZoom,
|
||||
fitView,
|
||||
KIND_COLOR,
|
||||
depEnd,
|
||||
depInTrace,
|
||||
depPath,
|
||||
follow,
|
||||
depLines,
|
||||
isRest,
|
||||
restParts,
|
||||
linkLabel,
|
||||
midOf,
|
||||
pillsOf,
|
||||
@@ -84,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 });
|
||||
@@ -158,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: {
|
||||
@@ -483,7 +487,7 @@ export function renderDiagram(
|
||||
parent.append(n);
|
||||
return n;
|
||||
};
|
||||
const label = (parent: Element, x: number, y: number, text: string, cls: string): void => {
|
||||
const label = (parent: Element, x: number, y: number, text: string, cls: string): number => {
|
||||
const tag = add(parent, "g", { class: cls });
|
||||
const box = add(tag, "rect", { rx: "4", ry: "4" });
|
||||
const t = add(tag, "text", {
|
||||
@@ -497,6 +501,7 @@ export function renderDiagram(
|
||||
Object.entries({ x: x - w / 2, y: y - 9, width: w, height: 18 }).forEach(([k, v]) =>
|
||||
box.setAttribute(k, v.toFixed(1)),
|
||||
);
|
||||
return w;
|
||||
};
|
||||
const defs = add(svg, "defs", {});
|
||||
const arrow = add(defs, "marker", {
|
||||
@@ -509,7 +514,16 @@ export function renderDiagram(
|
||||
orient: "auto",
|
||||
});
|
||||
add(arrow, "path", { d: "M0,0 L8,4 L0,8 z", class: "m02-dfdg__arrow" });
|
||||
const mids = new Map<string, { x: number; y: number }>();
|
||||
const dot = add(defs, "marker", {
|
||||
id: "m02-dfdg-dot",
|
||||
viewBox: "0 0 8 8",
|
||||
refX: "4",
|
||||
refY: "4",
|
||||
markerWidth: "6",
|
||||
markerHeight: "6",
|
||||
});
|
||||
add(dot, "circle", { cx: "4", cy: "4", r: "3", class: "m02-dfdg__arrow" });
|
||||
const mids = new Map<string, { x: number; y: number; w: number }>();
|
||||
for (const l of lg.links) {
|
||||
const a = at(l.from, "out");
|
||||
const b = at(l.to, "in");
|
||||
@@ -531,9 +545,8 @@ export function renderDiagram(
|
||||
add(g, "path", { d, class: "m02-dfdg__hit" });
|
||||
add(g, "path", { d, class: "m02-dfdg__wire" });
|
||||
const mid = midOf(a, b);
|
||||
mids.set(l.id, mid);
|
||||
const rule = labels[l.id] ?? linkLabel(l);
|
||||
if (rule) label(g, mid.x, mid.y, rule, "m02-dfdg__rule");
|
||||
mids.set(l.id, { ...mid, w: rule ? label(g, mid.x, mid.y, rule, "m02-dfdg__rule") : 0 });
|
||||
if (rest) label(g, mid.x, mid.y + (rule ? 18 : 0), tt("Rest"), "m02-dfdg__restmark");
|
||||
const broken = breaks.filter((k) => k.link === l.id);
|
||||
const why = [...broken, ...rests.filter((k) => k.link === l.id)]
|
||||
@@ -559,14 +572,14 @@ export function renderDiagram(
|
||||
}
|
||||
// 기댐 선 — 규칙이 기대는 알약 → 그 규칙이 있는 곳 · 흐린 점선 · 화살
|
||||
for (const dep of depLines(lg)) {
|
||||
// 시작 = 조건 알약 오른쪽 · 끝 = 그 규칙 선의 이름표 가장자리 또는 받는 재료 알약 오른쪽
|
||||
const a = at(dep.from, "out");
|
||||
const b =
|
||||
"link" in dep.to
|
||||
? mids.get(dep.to.link)
|
||||
: at({ box: dep.to.box, name: dep.to.port }, "out", "far");
|
||||
if (!a || !b) continue;
|
||||
const dx = Math.max(40, Math.abs(b.x - a.x) / 2);
|
||||
const d = `M${a.x},${a.y} C${a.x + dx},${a.y} ${b.x - dx},${b.y} ${b.x},${b.y}`;
|
||||
const tag = "link" in dep.to ? mids.get(dep.to.link) : null;
|
||||
const pill = "box" in dep.to ? at({ box: dep.to.box, name: dep.to.port }, "out") : null;
|
||||
const end = tag ? depEnd(a, tag) : pill && { ...pill, side: "right" as const };
|
||||
if (!a || !end) continue;
|
||||
const b = { x: end.x, y: end.y };
|
||||
const d = depPath(a, b, end.side);
|
||||
const where = "link" in dep.to ? dep.to.link : dep.to.box;
|
||||
const steps = opts.trace;
|
||||
const traced =
|
||||
@@ -583,7 +596,12 @@ export function renderDiagram(
|
||||
"data-ends": [a.x, a.y, b.x, b.y].map((n) => n.toFixed(1)).join(","),
|
||||
});
|
||||
add(g, "path", { d, class: "m02-dfdg__hit" });
|
||||
add(g, "path", { d, class: "m02-dfdg__depwire", "marker-end": "url(#m02-dfdg-arrow)" });
|
||||
add(g, "path", {
|
||||
d,
|
||||
class: "m02-dfdg__depwire",
|
||||
"marker-start": "url(#m02-dfdg-dot)",
|
||||
"marker-end": "url(#m02-dfdg-arrow)",
|
||||
});
|
||||
const m = midOf(a, b);
|
||||
label(g, m.x, m.y, dep.text, "m02-dfdg__deptext");
|
||||
g.addEventListener("click", (ev) => {
|
||||
|
||||
@@ -284,9 +284,52 @@ export function depInTrace(dep: DepLine, steps: CfStep[]): boolean {
|
||||
return src && dst;
|
||||
}
|
||||
|
||||
type Pt = { x: number; y: number };
|
||||
|
||||
/** 이름표에 닿는 끝 — 시작이 왼쪽이면 이름표 왼쪽 가장자리 · 오른쪽이면 오른쪽 가장자리(이름표가 없으면 선 가운데) */
|
||||
export function depEnd(a: Pt | null, tag: Pt & { w: number }): Pt & { side: "left" | "right" } {
|
||||
const fromLeft = !a || a.x <= tag.x;
|
||||
return {
|
||||
x: tag.x + (fromLeft ? -1 : 1) * (tag.w / 2 + 2),
|
||||
y: tag.y,
|
||||
side: fromLeft ? "left" : "right",
|
||||
};
|
||||
}
|
||||
|
||||
/** 기댐 선 길 — 알약 오른쪽에서 나가 끝에 닿음 · side = 끝으로 들어가는 쪽(재료 알약은 오른쪽에서) */
|
||||
export function depPath(a: Pt, b: Pt, side: "left" | "right"): string {
|
||||
const dx = Math.max(40, Math.abs(b.x - a.x) / 2);
|
||||
const bx = side === "left" ? b.x - dx : b.x + dx;
|
||||
return `M${a.x},${a.y} C${a.x + dx},${a.y} ${bx},${b.y} ${b.x},${b.y}`;
|
||||
}
|
||||
|
||||
/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — 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));
|
||||
// 내보내는 알약 값만(들어온 값은 빼고) · 쉼이 그 상자에 걸렸나(Eval 쉼 줄은 link 가 늘 있음)
|
||||
const outs = portsOut(b).map((p) => values[b.id]?.[p.name]);
|
||||
const hit = rests.some((k) => k.box === b.id);
|
||||
if (allRest || (hit && outs.every(empty))) boxes.add(b.id);
|
||||
}
|
||||
return { links, pills, boxes };
|
||||
}
|
||||
|
||||
/* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */
|
||||
|
||||
/** 누른 뒤 5px 넘게 움직이면 끌기 — 끈 뒤 따라오는 click 한 번만 삼킴 */
|
||||
|
||||
@@ -254,6 +254,10 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.m02-df__detail-row[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m02-df__detail-row h4 {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@@ -474,6 +474,8 @@ function openDataFlow(): void {
|
||||
if (boxId && !lg.boxes.some((b) => b.id === boxId)) pick = null;
|
||||
const box = lg.boxes.find((b) => b.id === pickedBox());
|
||||
drawHead(detail, lg, pick);
|
||||
// 설정 줄(상자 이름 · 색)은 상자를 고른 때만 — 선 · 알약이면 줄째 숨김
|
||||
(detail.settings.parentElement as HTMLElement).hidden = !box;
|
||||
colorRow.hidden = !box;
|
||||
boxName.root.hidden = !box || box.kind === "output";
|
||||
if (box && document.activeElement !== boxName.input) boxName.input.value = box.name;
|
||||
|
||||
@@ -263,21 +263,6 @@ export function evalLogic(
|
||||
/** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */
|
||||
const through = (l: CfLink): CfValue => {
|
||||
const v = valueOf(l.from.box, l.from.name);
|
||||
// 쉬는 갈래에서 온 빈 값 — 받는 상자도 쉼(셈하지 않음 · 문제로 안 셈)
|
||||
if (v === null && resting.has(l.from.box)) {
|
||||
resting.add(l.to.box);
|
||||
blocked.add(l.to.box);
|
||||
const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) });
|
||||
found.push({
|
||||
kind: "통과 안 됨",
|
||||
level: "쉼",
|
||||
box: l.to.box,
|
||||
port: l.to.name,
|
||||
link: l.id,
|
||||
text,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
if (!l.rule) return v;
|
||||
if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null;
|
||||
const conds = Object.fromEntries(
|
||||
@@ -311,10 +296,33 @@ export function evalLogic(
|
||||
if (res.why) blocked.add(l.to.box);
|
||||
return res.why ? null : res.value;
|
||||
};
|
||||
/** 받는 값 — 쉬는 갈래에서 온 빈 값은 「쉼」 줄만 남김 · 들어오는 선이 모두 쉬는 갈래면 받는 상자도 쉼
|
||||
* (하나라도 살아 있으면 셈 · 쉬는 갈래 값은 빈 칸) · 조건 선 바로 뒤 상자는 through 가 막음 */
|
||||
const arrive = (box: CfBox): Record<string, CfValue> => {
|
||||
const got = Object.fromEntries(
|
||||
lg.links.filter((l) => l.to.box === box.id).map((l) => [l.to.name, through(l)]),
|
||||
);
|
||||
const ins = lg.links.filter((l) => l.to.box === box.id);
|
||||
const got: Record<string, CfValue> = {};
|
||||
let rest = 0;
|
||||
for (const l of ins) {
|
||||
if (resting.has(l.from.box) && valueOf(l.from.box, l.from.name) === null) {
|
||||
rest++;
|
||||
got[l.to.name] = null;
|
||||
const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) });
|
||||
found.push({
|
||||
kind: "통과 안 됨",
|
||||
level: "쉼",
|
||||
box: l.to.box,
|
||||
port: l.to.name,
|
||||
link: l.id,
|
||||
text,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
got[l.to.name] = through(l);
|
||||
}
|
||||
if (ins.length && rest === ins.length) {
|
||||
resting.add(box.id);
|
||||
blocked.add(box.id);
|
||||
}
|
||||
arrived[box.id] = got;
|
||||
return got;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "item3",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "면적산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"면적번": "SA-000003"
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"판": 1,
|
||||
"줄": [
|
||||
{
|
||||
"로직": "GC000297",
|
||||
"입력": "형식",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GC000297",
|
||||
"입력": "두께",
|
||||
"자리": "면적",
|
||||
"알약": "입력|GC000297|두께"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "구분",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "믹서",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "지역",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "재료배합",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "골재최대치수",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "수중",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "모래상태",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "자갈상태",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "모래단위중량",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "자갈단위중량",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000156",
|
||||
"입력": "자재지역",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000162",
|
||||
"입력": "포장두께",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000162",
|
||||
"입력": "운반",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000162",
|
||||
"입력": "운반감률",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000162",
|
||||
"입력": "공급감률",
|
||||
"자리": "선택"
|
||||
},
|
||||
{
|
||||
"로직": "GF000162",
|
||||
"입력": "종단경사",
|
||||
"자리": "선택"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"종류": "통합문서",
|
||||
"판": 1,
|
||||
"열": "item3",
|
||||
"서식": [
|
||||
{}
|
||||
],
|
||||
"시트": [
|
||||
{
|
||||
"id": "s1",
|
||||
"이름": "재료산출",
|
||||
"칸": {}
|
||||
}
|
||||
],
|
||||
"활성": "s1",
|
||||
"재료번": "SM-000003"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"양식": "구조물도면",
|
||||
"종류": "structure",
|
||||
"판": 1,
|
||||
"열": "item3",
|
||||
"도면": {
|
||||
"format": 6,
|
||||
"entities": [],
|
||||
"layers": [
|
||||
{
|
||||
"id": "기본",
|
||||
"name": "기본",
|
||||
"isVisible": true,
|
||||
"isLocked": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"도번": "SD-000003"
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
"id": "item1",
|
||||
"단위": "m",
|
||||
"꼴": "수",
|
||||
"일위대가": null,
|
||||
"일위대가": "UA000006",
|
||||
"자재조합": null,
|
||||
"경로": [
|
||||
"기슭막이",
|
||||
@@ -95,6 +95,25 @@
|
||||
"ss"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "item3",
|
||||
"단위": null,
|
||||
"꼴": "수",
|
||||
"일위대가": null,
|
||||
"자재조합": null,
|
||||
"경로": [
|
||||
"기슭막이",
|
||||
"ㄴㄴ"
|
||||
],
|
||||
"머리": [
|
||||
"기슭막이",
|
||||
"ㄴㄴ",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "memo",
|
||||
"머리": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"그룹": "구조물집계표조합",
|
||||
"판": 1,
|
||||
"비고": "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · 키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열",
|
||||
"다음": 54,
|
||||
"다음": 55,
|
||||
"지운번호": [
|
||||
"01010100",
|
||||
"01010101",
|
||||
@@ -74,7 +74,7 @@
|
||||
"분류2": "ss",
|
||||
"분류3": "aaa",
|
||||
"단위": "m",
|
||||
"일위대가": null,
|
||||
"일위대가": "UA000006",
|
||||
"자재조합": null,
|
||||
"도번": "SD-000001",
|
||||
"면적번": "SA-000001",
|
||||
@@ -106,6 +106,27 @@
|
||||
"재료번": "SM-000002",
|
||||
"바인딩": null,
|
||||
"펼침": null
|
||||
},
|
||||
{
|
||||
"키": "SS000054",
|
||||
"열": "item3",
|
||||
"경로": [
|
||||
"기슭막이",
|
||||
"ㄴㄴ"
|
||||
],
|
||||
"이름": "기슭막이 ㄴㄴ",
|
||||
"종류": "기슭막이",
|
||||
"분류1": "ㄴㄴ",
|
||||
"분류2": null,
|
||||
"분류3": null,
|
||||
"단위": null,
|
||||
"일위대가": null,
|
||||
"자재조합": null,
|
||||
"도번": "SD-000003",
|
||||
"면적번": "SA-000003",
|
||||
"재료번": "SM-000003",
|
||||
"바인딩": null,
|
||||
"펼침": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ const out = jobs.map((job) => {
|
||||
if (job.op === "rest") return job.breaks.map((b) => m.isRest(b));
|
||||
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;
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -248,3 +254,153 @@ def test_한눈에_90_아래로_안_내림과_알약_값():
|
||||
)
|
||||
assert fit["zoom"] == 0.9
|
||||
assert values == ["97,882.512", "268,908", "0.364", "깬돌", "· 2줄"]
|
||||
|
||||
|
||||
def test_기댐_선_끝이_알약과_이름표에_붙음():
|
||||
left, right, path_left, path_right = run(
|
||||
[
|
||||
{"op": "end", "a": {"x": 100, "y": 50}, "tag": {"x": 400, "y": 120, "w": 80}},
|
||||
{"op": "end", "a": {"x": 600, "y": 50}, "tag": {"x": 400, "y": 120, "w": 80}},
|
||||
{"op": "path", "a": {"x": 100, "y": 50}, "b": {"x": 358, "y": 120}, "side": "left"},
|
||||
{"op": "path", "a": {"x": 100, "y": 50}, "b": {"x": 130, "y": 300}, "side": "right"},
|
||||
]
|
||||
)
|
||||
# 시작이 왼쪽이면 이름표 왼쪽 가장자리(400 − 40 − 2) · 오른쪽이면 오른쪽 가장자리
|
||||
assert left == {"x": 358, "y": 120, "side": "left"}
|
||||
assert right == {"x": 442, "y": 120, "side": "right"}
|
||||
# 길은 시작 알약에서 나가 끝에서 끝남 · 재료 알약은 오른쪽에서 들어감(끝 손잡이가 끝보다 오른쪽)
|
||||
assert path_left.startswith("M100,50 ") and path_left.endswith(" 358,120")
|
||||
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"]
|
||||
|
||||
|
||||
def test_지반_토사_쉼_흐림은_브레이커로_깨기_하나():
|
||||
def fn(id_, name, ins, out):
|
||||
return {
|
||||
"id": id_,
|
||||
"kind": "function",
|
||||
"name": name,
|
||||
"color": "",
|
||||
"unit": "-",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"func": {
|
||||
"sheet": {},
|
||||
"inputs": [{"port": p, "cell": f"B{i + 1}"} for i, p in enumerate(ins)],
|
||||
"variables": [],
|
||||
"output": {"cell": "B9", "name": out, "unit": "원"},
|
||||
},
|
||||
}
|
||||
|
||||
gate = {"kind": "gate", "from": port("ig", "지반"), "op": "=", "value": "암"}
|
||||
ground = {
|
||||
"key": None,
|
||||
"name": "지반",
|
||||
"resultUnit": "원/㎥",
|
||||
"userElements": [],
|
||||
"boxes": [
|
||||
{
|
||||
"id": "ig",
|
||||
"kind": "input",
|
||||
"name": "지반 · 굴착량",
|
||||
"color": "",
|
||||
"unit": "-",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{"name": "지반", "type": "choice", "unit": "-", "example": "토사"},
|
||||
{"name": "굴착량", "type": "number", "unit": "㎥", "example": 1},
|
||||
],
|
||||
},
|
||||
},
|
||||
fn("exc", "굴삭기로 파기", ["굴착량", "시간당 경비"], "굴삭기 경비"),
|
||||
fn("brk", "브레이커로 깨기", ["굴착량", "시간당 경비"], "브레이커 경비"),
|
||||
fn("sum", "1㎥ 경비", ["굴삭기 경비", "브레이커 경비", "굴착량"], "1㎥ 경비"),
|
||||
{
|
||||
"id": "out",
|
||||
"kind": "output",
|
||||
"name": "경비",
|
||||
"color": "",
|
||||
"unit": "원",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"output": {"cost": "경비", "unit": "원"},
|
||||
},
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": port("ig", "굴착량"),
|
||||
"to": port("exc", "굴착량"),
|
||||
"rule": {**gate, "op": "≠"},
|
||||
},
|
||||
{"id": "l2", "from": port("ig", "굴착량"), "to": port("brk", "굴착량"), "rule": gate},
|
||||
{"id": "l5", "from": port("exc", "굴삭기 경비"), "to": port("sum", "굴삭기 경비")},
|
||||
{"id": "l6", "from": port("brk", "브레이커 경비"), "to": port("sum", "브레이커 경비")},
|
||||
{"id": "l7", "from": port("ig", "굴착량"), "to": port("sum", "굴착량")},
|
||||
{"id": "l8", "from": port("sum", "1㎥ 경비"), "to": port("out", "값")},
|
||||
],
|
||||
}
|
||||
# Eval 쉼 줄은 link 가 늘 있음 — 조건 선 l2(받는 상자 브레이커) · 빈 값이 흘러간 l6(받는 상자 1㎥ 경비)
|
||||
rests = [
|
||||
{
|
||||
"kind": "통과 안 됨",
|
||||
"level": "쉼",
|
||||
"box": "brk",
|
||||
"port": "굴착량",
|
||||
"link": "l2",
|
||||
"text": "암이 아님",
|
||||
},
|
||||
{
|
||||
"kind": "통과 안 됨",
|
||||
"level": "쉼",
|
||||
"box": "sum",
|
||||
"port": "브레이커 경비",
|
||||
"link": "l6",
|
||||
"text": "쉼",
|
||||
},
|
||||
]
|
||||
values = {
|
||||
"brk": {"시간당 경비": 80000, "굴착량": None, "브레이커 경비": None},
|
||||
"sum": {"굴삭기 경비": 2000, "브레이커 경비": None, "굴착량": 1, "1㎥ 경비": 2000},
|
||||
"out": {"값": 2000},
|
||||
}
|
||||
[got] = run([{"op": "restParts", "lg": ground, "rests": rests, "values": values}])
|
||||
assert got["boxes"] == ["brk"]
|
||||
assert got["links"] == ["l2", "l6"]
|
||||
assert got["pills"] == ["brk|굴착량", "sum|브레이커 경비"]
|
||||
|
||||
@@ -643,3 +643,40 @@ def test_쉬는_갈래와_문제_무게() -> None:
|
||||
# 하나도 안 맞음 · 표에 없는 조건만 문제 — 쉬는 갈래 넷은 문제로 안 셈
|
||||
assert b_problems == ["표에 없는 조건", "조건 안 맞음"]
|
||||
assert outs["b7"] is None # 쉬는 갈래 끝 출력은 빈 값(0 아님)
|
||||
|
||||
|
||||
def _ground() -> dict:
|
||||
return json.loads((CF / "_sample" / "ground.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _server(lg: dict, values: dict) -> dict:
|
||||
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
|
||||
|
||||
return cp.run(lg, values, lambda key: {}, [])
|
||||
|
||||
|
||||
def test_만나는_상자는_살아_있는_선이_있으면_셈_서버와_같음() -> None:
|
||||
lg = _ground()
|
||||
soil = {"in-ground": {"지반": "토사"}}
|
||||
rock = {"in-ground": {"지반": "암"}}
|
||||
got = _run(
|
||||
f"const lg = {json.dumps(lg, ensure_ascii=False)};\n"
|
||||
f"const s = E.evalLogic(lg, {json.dumps(soil, ensure_ascii=False)});\n"
|
||||
f"const r = E.evalLogic(lg, {json.dumps(rock, ensure_ascii=False)});\n"
|
||||
"out([s.outputs, s.breaks.map(b => [b.kind, b.level, b.link]), r.outputs, r.breaks.map(b => [b.kind, b.level, b.link]),"
|
||||
" s.ports['fn-sum'], r.ports['fn-sum']]);"
|
||||
)
|
||||
s_out, s_breaks, r_out, r_breaks, s_sum, r_sum = got
|
||||
assert s_out == {"out-exp": 2000} # 굴삭기 갈래만 · 브레이커 갈래는 쉼(빈 칸)
|
||||
assert s_breaks == [["통과 안 됨", "쉼", "l2"], ["통과 안 됨", "쉼", "l6"]]
|
||||
assert [b for b in s_breaks if b[1] == "문제"] == []
|
||||
assert r_breaks == [["통과 안 됨", "쉼", "l1"], ["통과 안 됨", "쉼", "l5"]]
|
||||
assert r_out["out-exp"] == r_sum["1㎥ 경비"] and r_out["out-exp"] != 2000 # 브레이커 갈래 값
|
||||
assert s_sum == {"1㎥ 경비": 2000}
|
||||
# 서버(Compute) = 화면(Eval)
|
||||
for values, screen_out, screen_breaks in ((soil, s_out, s_breaks), (rock, r_out, r_breaks)):
|
||||
srv = _server(lg, values)
|
||||
assert {k: float(v) for k, v in srv["outputs"].items()} == {
|
||||
k: float(v) for k, v in screen_out.items()
|
||||
}
|
||||
assert [[b["kind"], b["level"], b["link"]] for b in srv["breaks"]] == screen_breaks
|
||||
|
||||
Reference in New Issue
Block a user