feat(804-6): 옛 수식 탭 걷음 · 줄 쌓기 = 첫 「수식」 탭(요구 686-1)
- M01 단가산출 기초 · M02 단가산출 상세 보기 = 수식(줄 쌓기 · 기본) · 단가산출 상세 둘 - 줄 쌓기에 M02 사본 [단가 갱신] · [재생성](미리 셈 → 알림 → 저장 · 고침 있으면 막음) · 판 바뀜(409) 문서 그대로면 새 판으로 다시 저장 옮김 - 옛 수식 문서 화면 걷음(formula_doc_screen · defs · edit · lines · math · math_model · rows · sub · formula_doc.css) · 이미 안 부르던 796-2 수식 나무 화면(LogicLab_Equation · Defs · Var · css · M01_Logic_Equation_Model · sheet_formula_cell · ui_template_equation_defs · edit) 걷음 — 18 파일 4,860 줄 - formula_doc_render = 줄 쌓기가 쓰는 reach 만 · 옵션 가로 CSS 는 line_stack.css 로 - 남김 = 계산기 common_util_formula_doc* · 서버 길 · 옵션 · 사본 표지 · 합계 상자 · KaTeX · 시험 페이지 tools/line-stack-test - 시험: test_796_2 지움(걷은 화면 원본 검사뿐) · 799_1 · 800_2 걷은 화면 줄만 뺌 · 799_2 · 802_4b2 · 804_2 보기 · 화면 파일 대상 바꿈 · test_804_6 새로(걷은 파일 · 부르는 곳 없음 · 첫 탭 · 사본 할일) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,267 +1,25 @@
|
||||
/* =============================================================================
|
||||
* formula_doc_render.ts
|
||||
* 수식 문서 그림(PLAN 802-3 · 요구 660~664) — 식 나무 → 조판 수식(분수 · 첨자 · 괄호) · 화면 공용(M01 · M02)
|
||||
* 기호 식(side "sym") = 기호만(한글 이름 · 표 칸 글 · DB 키 없음) · 값 식(side "val") = 기호 자리에 값 + 단위
|
||||
* 만약 = 탄 갈래만(값을 알 때) · 반올림 · 올림 · 버림 자리 감싸개 숨김(662-4) — 숨긴 글은 `data-pre` · `data-post`
|
||||
* 고침 글 꼴 = 공용 `serializeEq`(원자 `data-tok` · 분수 · 괄호 · 단위 뺌) — 보이는 그림이 곧 고침 자리
|
||||
* evalNode = 그림용 값(탄 갈래 · 값 식) — 셈 정본은 계산기(`common_util_formula_doc_calc.ts`)
|
||||
* reach = 지금 탄 갈래로 닿는 기호(정의 영역에 보일 것)
|
||||
* 수식 문서 닿는 기호(PLAN 802-3 · 804-6) — 줄 쌓기 산출근거(`line_stack_out.ts`)가 씀
|
||||
* reach = 지금 탄 갈래로 닿는 기호(만약 안 탄 갈래 · 숨김 자리 인자 빼고)
|
||||
* 옛 수식 문서 탭의 조판 그림(renderNode · 값 식 · 수 바꿈)은 804-6 에 걷음
|
||||
* ========================================================================== */
|
||||
|
||||
import { eqText, symbolOf } from "@ui/ui_template_equation";
|
||||
import {
|
||||
linkNames,
|
||||
splitId,
|
||||
textOf,
|
||||
varId,
|
||||
type DocVar,
|
||||
type FormulaDoc,
|
||||
type Node,
|
||||
} from "../common_util/common_util_formula_doc";
|
||||
import { isNum } from "../common_util/common_util_formula_doc_calc";
|
||||
import {
|
||||
leafNum,
|
||||
offLine,
|
||||
takenOf,
|
||||
tryParse,
|
||||
type Values,
|
||||
} from "../common_util/common_util_formula_doc_view";
|
||||
|
||||
export {
|
||||
evalNode,
|
||||
leafNum,
|
||||
lineMap,
|
||||
offLine,
|
||||
offZero,
|
||||
takenOf,
|
||||
tryParse,
|
||||
type Values,
|
||||
} from "../common_util/common_util_formula_doc_view";
|
||||
const HIDE = new Set(["반올림", "올림", "버림"]);
|
||||
const SHOW: Record<string, string> = {
|
||||
"*": "×",
|
||||
"/": "÷",
|
||||
"-": "−",
|
||||
"<=": "≤",
|
||||
">=": "≥",
|
||||
"!=": "≠",
|
||||
};
|
||||
const PREC: Record<string, number> = {
|
||||
또는: 1,
|
||||
그리고: 2,
|
||||
"<": 3,
|
||||
"<=": 3,
|
||||
">": 3,
|
||||
">=": 3,
|
||||
"==": 3,
|
||||
"!=": 3,
|
||||
"+": 4,
|
||||
"-": 4,
|
||||
"*": 5,
|
||||
"/": 5,
|
||||
};
|
||||
const prec = (n: Node): number => (n[0] === "bin" ? PREC[n[1]] : n[0] === "neg" ? 6 : 9);
|
||||
|
||||
export interface Ctx {
|
||||
side: "sym" | "val";
|
||||
vals: Values;
|
||||
vars: Map<string, DocVar>;
|
||||
/** ÷ 를 분수 대신 한 줄로(금액 줄 · 경쟁사 꼴) */
|
||||
inline?: boolean;
|
||||
/** 묶음 값의 비목(금액 묶음 줄 노 · 재 · 경 줄) */
|
||||
cost?: string;
|
||||
/** 수 마디 → 차례(값 식 숫자 고침) */
|
||||
lits?: Map<Node, number>;
|
||||
}
|
||||
|
||||
const span = (cls: string, text?: string): HTMLSpanElement => {
|
||||
const s = document.createElement("span");
|
||||
s.className = cls;
|
||||
if (text !== undefined) s.textContent = text;
|
||||
return s;
|
||||
};
|
||||
|
||||
/** 기호 글(`Q_시공^표`) → 기호 그림(첨자) */
|
||||
export function symNode(id: string): HTMLElement {
|
||||
const [기호, 아래, 위] = splitId(id);
|
||||
return symbolOf({ 기호, 아래, 위, 열쇠: id });
|
||||
}
|
||||
|
||||
/** 값 글 + 단위 */
|
||||
export function valueNode(value: unknown, unit = ""): HTMLElement {
|
||||
const box = span("fdoc__val");
|
||||
box.append(span("ui-eq__num", eqText(value)));
|
||||
if (unit) box.append(span("ui-eq__unit", ` ${unit}`));
|
||||
return box;
|
||||
}
|
||||
|
||||
const paren = (inner: HTMLElement): HTMLElement => {
|
||||
const p = span("ui-eq__paren");
|
||||
p.append(span("ui-eq__inner"));
|
||||
p.firstElementChild!.append(inner);
|
||||
return p;
|
||||
};
|
||||
|
||||
const op = (o: string): HTMLElement => span("ui-eq__op", SHOW[o] ?? o);
|
||||
|
||||
/** 감쌈 — 그림엔 안 보이는 앞 · 뒤 글(고침 때 그대로 되돌림) */
|
||||
const hidden = (pre: string, post: string, inner: HTMLElement): HTMLElement => {
|
||||
const w = span("fdoc__wrap");
|
||||
w.dataset.pre = pre;
|
||||
w.dataset.post = post;
|
||||
w.append(inner);
|
||||
return w;
|
||||
};
|
||||
|
||||
function varNode(id: string, c: Ctx): HTMLElement {
|
||||
if (c.side === "sym") {
|
||||
const a = span("ui-eq__var fdoc__atom");
|
||||
a.dataset.tok = id;
|
||||
a.dataset.var = id;
|
||||
a.append(symNode(id));
|
||||
return a;
|
||||
}
|
||||
let v = c.vals[id];
|
||||
if (v && typeof v === "object" && c.cost) v = (v as Values)[c.cost];
|
||||
const a = span("ui-eq__var fdoc__atom is-val");
|
||||
a.dataset.tok = id;
|
||||
a.dataset.var = id;
|
||||
if (v === null || v === undefined || typeof v === "object") {
|
||||
a.classList.add("is-missing"); // 값 없음 · 정의 필요 — 기호 그대로(빨강)
|
||||
a.append(symNode(id));
|
||||
} else a.append(valueNode(v, c.vars.get(id)?.단위 ?? ""));
|
||||
return a;
|
||||
}
|
||||
|
||||
/** 나무 → 조판 수식 */
|
||||
export function renderNode(n: Node, c: Ctx): HTMLElement {
|
||||
const go = (x: Node): HTMLElement => renderNode(x, c);
|
||||
switch (n[0]) {
|
||||
case "num": {
|
||||
const s = span("ui-eq__num fdoc__lit", eqText(n[1]));
|
||||
const at = c.lits?.get(n);
|
||||
if (at !== undefined) s.dataset.lit = String(at);
|
||||
return s;
|
||||
}
|
||||
case "str": {
|
||||
const s = span("ui-eq__str fdoc__atom", n[1]);
|
||||
s.dataset.tok = `'${n[1]}'`;
|
||||
return s;
|
||||
}
|
||||
case "var":
|
||||
return varNode(n[1], c);
|
||||
case "field": {
|
||||
if (c.side === "val" && n[1][0] === "var") {
|
||||
const v = c.vals[n[1][1]];
|
||||
const got = v && typeof v === "object" ? (v as Values)[n[2]] : undefined;
|
||||
if (isNum(got)) {
|
||||
const a = span("ui-eq__var fdoc__atom is-val");
|
||||
a.dataset.tok = textOf(n);
|
||||
a.dataset.var = n[1][1];
|
||||
a.append(valueNode(got));
|
||||
return a;
|
||||
}
|
||||
}
|
||||
const a = span("ui-eq__var fdoc__atom");
|
||||
a.dataset.tok = textOf(n);
|
||||
if (n[1][0] === "var") a.dataset.var = n[1][1];
|
||||
const base = n[1][0] === "var" ? symNode(n[1][1]) : span("", textOf(n[1]));
|
||||
a.append(base, span("ui-eq__script", `.${n[2]}`));
|
||||
return a;
|
||||
}
|
||||
case "neg": {
|
||||
const s = span("ui-eq__body");
|
||||
s.append(op("-"), n[1][0] === "bin" ? paren(go(n[1])) : go(n[1]));
|
||||
return s;
|
||||
}
|
||||
case "pct": {
|
||||
const s = span("ui-eq__body");
|
||||
s.append(prec(n[1]) < 9 ? paren(go(n[1])) : go(n[1]), span("ui-eq__op", "%"));
|
||||
return s;
|
||||
}
|
||||
case "call": {
|
||||
const [, name, items] = n;
|
||||
if (name === "만약") {
|
||||
const pick = takenOf(n, c.vals);
|
||||
if (pick) {
|
||||
const [t, a, b] = items.map(textOf);
|
||||
return pick === 1
|
||||
? hidden(`만약(${t}, `, `, ${b})`, go(items[1]))
|
||||
: hidden(`만약(${t}, ${a}, `, ")", go(items[2]));
|
||||
}
|
||||
}
|
||||
if (HIDE.has(name) && items.length >= 1 && items.length <= 2) {
|
||||
const post = items.length === 2 ? `, ${textOf(items[1])})` : ")";
|
||||
return hidden(`${name}(`, post, go(items[0]));
|
||||
}
|
||||
const s = span("ui-eq__body");
|
||||
const inner = span("ui-eq__body");
|
||||
items.forEach((x, i) => {
|
||||
if (i) inner.append(span("ui-eq__op", ","));
|
||||
inner.append(go(x));
|
||||
});
|
||||
s.append(span("ui-eq__fn", name), paren(inner));
|
||||
return s;
|
||||
}
|
||||
case "bin": {
|
||||
const [, o, a, b] = n;
|
||||
// 선택으로 꺼진 항(만약이 0 · 1 을 탐) = 안 보임(663-1) · 고침 글은 그대로
|
||||
const na = leafNum(a, c.vals);
|
||||
const nb = leafNum(b, c.vals);
|
||||
if ((o === "+" || o === "-") && nb === 0) return hidden("(", `) ${o} ${textOf(b)}`, go(a));
|
||||
if ((o === "*" || o === "/") && nb === 1) return hidden("(", `) ${o} ${textOf(b)}`, go(a));
|
||||
if ((o === "+" && na === 0) || (o === "*" && na === 1))
|
||||
return hidden(`${textOf(a)} ${o} (`, ")", go(b));
|
||||
if (o === "/" && !c.inline) {
|
||||
const f = span("ui-eq__frac");
|
||||
const top = span("ui-eq__top");
|
||||
const low = span("ui-eq__bottom");
|
||||
top.append(go(a));
|
||||
low.append(go(b));
|
||||
f.append(top, low);
|
||||
return f;
|
||||
}
|
||||
const mine = PREC[o];
|
||||
const s = span("ui-eq__body");
|
||||
const left = go(a);
|
||||
const right = go(b);
|
||||
s.append(
|
||||
prec(a) < mine || (mine === 3 && prec(a) === 3) ? paren(left) : left,
|
||||
op(o),
|
||||
prec(b) <= mine ? paren(right) : right,
|
||||
);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 수 마디 차례(나무 걸음 차례) */
|
||||
export function litsOf(n: Node): Map<Node, number> {
|
||||
const out = new Map<Node, number>();
|
||||
const go = (x: Node): void => {
|
||||
if (x[0] === "num") out.set(x, out.size);
|
||||
if (x[0] === "bin") {
|
||||
go(x[2]);
|
||||
go(x[3]);
|
||||
} else if (x[0] === "neg" || x[0] === "pct" || x[0] === "field") go(x[1]);
|
||||
else if (x[0] === "call") x[2].forEach(go);
|
||||
};
|
||||
go(n);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 차례 번째 수를 바꾼 식 글 */
|
||||
export function swapLit(text: string, at: number, now: number): string {
|
||||
const n = tryParse(text);
|
||||
if (!n) return text;
|
||||
const lits = litsOf(n);
|
||||
for (const [node, i] of lits) if (i === at) node[1] = now;
|
||||
for (const [node] of lits) (node as [string, number, string])[2] = String(node[1]);
|
||||
return textOf(n);
|
||||
}
|
||||
|
||||
/** 탄 갈래로 부르는 이름(만약 안 탄 갈래 · 숨김 자리 인자 빼고) */
|
||||
export function namesTaken(n: Node, vals: Values, out: string[] = []): string[] {
|
||||
function namesTaken(n: Node, vals: Values, out: string[] = []): string[] {
|
||||
const add = (x: string): void => {
|
||||
if (!out.includes(x)) out.push(x);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user