/* ============================================================================= * M02_DF_FuncExtra.ts * 함수 판 보조 — 식 보기 그리기 · 칸 연결 표 손잡이 · 우클릭 「조건 만들기」 에 주는 것 * 판 본체(M02_DF_FuncPanel)가 700줄 안에 들도록 뗌 · 상태는 모르고 손잡이(ExtraContext)만 받음 * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { showToast } from "@ui/ui_template_elements"; import type { ConditionOptions } from "../../A00_Common/spreadsheet/spreadsheet_condition"; import { renderCellTable } from "./M02_DF_CellTable"; import type { DetailOptions } from "./M02_DF_Detail"; import { cellValuesOf, formulaLines, renderFormulaView } from "./M02_DF_FormulaView"; import { TEXT } from "./M02_DF_FuncText"; import { valueChoices, type CfFunc, type CfLogic, type CfVariable } from "./M02_DF_Model"; export interface ExtraContext { lg: CfLogic; boxId: string; detail: DetailOptions; /** 선 규칙을 지나 들어온 값 */ arrived: Record; readOnly: boolean; place(port: string, cell: string): void; setOutput(cell: string): void; patch(make: (f: CfFunc) => CfFunc): void; used(f: CfFunc): Map; } const busy = (): string => TEXT.Busy[currentLanguageIndex === 1 ? 1 : 0]; const boxOf = (x: ExtraContext) => x.lg.boxes.find((b) => b.id === x.boxId); /** 식 보기 — 칸 식을 고치면 부른 쪽이 다시 부름 */ export function drawFormula(host: HTMLElement, x: ExtraContext): void { const box = boxOf(x); if (!box?.func) return; renderFormulaView(host, formulaLines(box, cellValuesOf(box, x.arrived))); } /** 칸 연결 표 — 표에서 칸 바꾸기 · 풀기(입력/변수/결과 모두 같은 규칙: 이미 쓰는 칸 막음 · 결과 칸 하나) */ export function drawCells(host: HTMLElement, x: ExtraContext): void { const box = boxOf(x); if (!box?.func) return; renderCellTable(host, x.lg, box, x.readOnly, { move: (kind, key, cell) => { if (kind === "in") return x.place(key, cell); if (kind === "out") return x.setOutput(cell); if (box.func && x.used(box.func).has(cell)) return void showToast(busy(), "error"); x.patch((f) => ({ ...f, variables: f.variables.map((v) => (v.name === key ? { ...v, cell } : v)), })); }, release: (kind, key) => x.patch((f) => kind === "in" ? { ...f, inputs: f.inputs.map((i) => (i.port === key ? { ...i, cell: "" } : i)) } : kind === "var" ? { ...f, variables: f.variables.filter((v) => v.name !== key) } : { ...f, output: { ...f.output, cell: "" } }, ), }); } /** 우클릭 「조건 만들기」 에 주는 것 — 칸에 올 값 목록(알약이면 valueChoices · 변수면 고르기 목록) · 이름 붙은 칸 * get 으로 부를 때마다 지금 상태를 다시 읽음(나중에 놓은 알약 · 만든 변수도 목록에 뜸) */ export function conditionsOf(get: () => ExtraContext): ConditionOptions { return { valuesFor: (a1) => { const x = get(); const f = boxOf(x)?.func; const at = a1.toUpperCase(); const v = f?.variables.find((y) => y.cell?.toUpperCase() === at); if (v) return v.type === "choice" && v.choices?.length ? [...v.choices] : null; const port = f?.inputs.find((i) => i.cell.toUpperCase() === at)?.port; const link = port ? x.lg.links.find((l) => l.to.box === x.boxId && l.to.name === port) : undefined; return link ? valueChoices(x.lg, link.from, { values: x.detail.values }) : null; }, pills: () => { const x = get(); const f = boxOf(x)?.func; return [ ...(f?.inputs ?? []).filter((i) => i.cell).map((i) => ({ name: i.port, cell: i.cell })), ...(f?.variables ?? []).filter((v) => v.cell).map((v) => ({ name: v.name, cell: v.cell! })), ]; }, }; } /** 변수 만들기 폼 → 변수 · 틀리면 까닭(글 열쇠) — 같은 이름 · 이미 쓰는 칸 · 숫자 아님 · 범위 거꾸로 */ export interface VarFormValues { name: string; type: CfVariable["type"]; unit: string; min: string; max: string; choices: string; example: string; } export function variableFrom( form: VarFormValues, f: CfFunc, cell: string, used: (f: CfFunc) => Map, ): | { v: CfVariable; cell: string } | { why: "NeedName" | "SameName" | "Busy" | "NotNumber" | "BadRange" } { const name = form.name.trim(); if (!name) return { why: "NeedName" }; const mine = f.variables.find((v) => v.name === name); if (!mine && [...f.inputs.map((i) => i.port), ...f.variables.map((v) => v.name)].includes(name)) return { why: "SameName" }; if (used({ ...f, variables: f.variables.filter((v) => v.name !== name) }).has(cell)) return { why: "Busy" }; const v: CfVariable = { name, type: form.type, unit: form.unit.trim() || "-", example: null, cell, }; let example: number | string | null = form.example.trim() === "" ? null : form.example.trim(); if (form.type === "number") { if (example !== null) { const n = Number(String(example).replace(/,/g, "")); if (!Number.isFinite(n)) return { why: "NotNumber" }; example = n; } const lo = form.min.trim() === "" ? null : Number(form.min); const hi = form.max.trim() === "" ? null : Number(form.max); if (lo !== null && hi !== null) { if (!(lo < hi)) return { why: "BadRange" }; v.range = [lo, hi]; } } else if (form.type === "choice") { v.choices = form.choices .split(",") .map((x) => x.trim()) .filter(Boolean); } v.example = example; return { v, cell }; }