feat(B06 관 글): 371-2 횡단 카드 관 글 L 뒤 기울기 S=x.x% · 툴팁 매설 경사 지움
- 「관매설 …, L=8 m (7.5), S=0.9%」 소수 한 자리 · 역경사 0.0 · 관 숨김(세월교 · 박스 흙막이) · B07 덧그림 글 없음 - 시험 test_371_2_pipe_slope_text · helper_371_2_pipe_slope Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
@@ -313,12 +313,12 @@ export function appendCulvertOverlay(
|
|||||||
|
|
||||||
const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : "";
|
const kindLabel = culvert.pipe_kind ? `${culvert.pipe_kind} ` : "";
|
||||||
const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter);
|
const wallThickness = pipeWallThicknessM(culvert.pipe_kind, diameter);
|
||||||
const pipeInfo = `관매설 ${kindLabel}Φ${Math.round(diameter * 1000)}m/m, L=${pipeLengthText(pipe.lengthM)}`;
|
// 관 기울기 = L 뒤 소수 한 자리 · 역경사는 0.0(371-2)
|
||||||
|
const pipeInfo = `관매설 ${kindLabel}Φ${Math.round(diameter * 1000)}m/m, L=${pipeLengthText(pipe.lengthM)}, S=${Math.max(pipe.slopePct, 0).toFixed(1)}%`;
|
||||||
// 관 정보 글 = 노폭 · 노견 라벨 위 줄(225-1 · B06 카드만 — 그 라벨 없는 덧그림엔 안 그림)
|
// 관 정보 글 = 노폭 · 노견 라벨 위 줄(225-1 · B06 카드만 — 그 라벨 없는 덧그림엔 안 그림)
|
||||||
if (!hidePipe) appendPipeInfoText(layer, pipeInfo, null);
|
if (!hidePipe) appendPipeInfoText(layer, pipeInfo, null);
|
||||||
const tip =
|
const tip =
|
||||||
pipeInfo +
|
pipeInfo +
|
||||||
` · 매설 경사 ${pipe.slopePct.toFixed(1)}%(수평 가능)` +
|
|
||||||
` · 관 두께 ${(wallThickness * 1000).toFixed(1)}㎜(외경선 기준)` +
|
` · 관 두께 ${(wallThickness * 1000).toFixed(1)}㎜(외경선 기준)` +
|
||||||
` · 최소 토피 ${culvert.min_cover_m.toFixed(1)}m(별표2 복토 교차 참조)`;
|
` · 최소 토피 ${culvert.min_cover_m.toFixed(1)}m(별표2 복토 교차 참조)`;
|
||||||
// 외경(관 두께 바깥) → 내경 순으로 2겹. 도면처럼 관벽 두께가 보이게 한다(사용자 ③).
|
// 외경(관 두께 바깥) → 내경 순으로 2겹. 도면처럼 관벽 두께가 보이게 한다(사용자 ③).
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/* 371-2 관 글 기울기 헬퍼 — 짝 = `test_371_2_pipe_slope_text.py`.
|
||||||
|
* B07 픽스처 배관 측점에 `appendCulvertOverlay` 를 가짜 SVG 로 그려 관 글 · 툴팁을 읽음.
|
||||||
|
*/
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, "..", "..");
|
||||||
|
const cache = new Map();
|
||||||
|
const noop = new Proxy({}, { get: () => () => "" });
|
||||||
|
|
||||||
|
/** 아주 작은 SVG 가짜 — 글 자리 판정에 쓰는 것만(class 고르기 · 속성 · 부모). */
|
||||||
|
class El {
|
||||||
|
constructor(tag) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.attrs = {};
|
||||||
|
this.children = [];
|
||||||
|
this.parentNode = null;
|
||||||
|
this.ownerSVGElement = null;
|
||||||
|
this.textContent = "";
|
||||||
|
}
|
||||||
|
setAttribute(k, v) {
|
||||||
|
this.attrs[k] = String(v);
|
||||||
|
}
|
||||||
|
getAttribute(k) {
|
||||||
|
return this.attrs[k] ?? null;
|
||||||
|
}
|
||||||
|
appendChild(c) {
|
||||||
|
c.parentNode = this;
|
||||||
|
c.ownerSVGElement = this.tag === "svg" ? this : this.ownerSVGElement;
|
||||||
|
this.children.push(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
append(...cs) {
|
||||||
|
cs.forEach((c) => this.appendChild(c));
|
||||||
|
}
|
||||||
|
addEventListener() {}
|
||||||
|
all() {
|
||||||
|
return this.children.flatMap((c) => [c, ...c.all()]);
|
||||||
|
}
|
||||||
|
querySelectorAll(sel) {
|
||||||
|
return this.all().filter((e) => (e.attrs.class ?? "").split(" ").includes(sel.slice(1)));
|
||||||
|
}
|
||||||
|
querySelector(sel) {
|
||||||
|
return this.querySelectorAll(sel)[0] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.document = { addEventListener: () => undefined, createElementNS: (_ns, t) => new El(t) };
|
||||||
|
|
||||||
|
function loadTs(file) {
|
||||||
|
const full = path.resolve(file);
|
||||||
|
if (cache.has(full)) return cache.get(full);
|
||||||
|
if (full.endsWith(".json")) return JSON.parse(fs.readFileSync(full, "utf8"));
|
||||||
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||||
|
}).outputText;
|
||||||
|
const box = { exports: {} };
|
||||||
|
cache.set(full, box.exports);
|
||||||
|
const resolve = (base) =>
|
||||||
|
fs.existsSync(base) && fs.statSync(base).isFile() ? base : `${base}.ts`;
|
||||||
|
const req = (r) => {
|
||||||
|
if (r.startsWith("@ui/") || r.startsWith("@config/")) return noop;
|
||||||
|
if (r.startsWith("@util/")) return loadTs(resolve(path.join(ROOT, "common_util", r.slice(6))));
|
||||||
|
return loadTs(resolve(path.join(path.dirname(full), r)));
|
||||||
|
};
|
||||||
|
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||||
|
cache.set(full, box.exports);
|
||||||
|
return box.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { computeCulvertLayout } = loadTs(
|
||||||
|
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert_Geom.ts"),
|
||||||
|
);
|
||||||
|
const { appendCulvertOverlay } = loadTs(
|
||||||
|
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Culvert.ts"),
|
||||||
|
);
|
||||||
|
const sections = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"),
|
||||||
|
).cross_sections;
|
||||||
|
const section = sections.find((s) => s.culvert && !s.culvert.hidden_pipe);
|
||||||
|
|
||||||
|
/** 노폭 라벨이 있는 B06 카드 꼴 svg 에 그려 관 글 · 관 툴팁을 돌려줌. */
|
||||||
|
function draw(layout, hidePipe) {
|
||||||
|
const svg = new El("svg");
|
||||||
|
const road = new El("text");
|
||||||
|
road.setAttribute("class", "b06-chart__carriageway-label");
|
||||||
|
road.setAttribute("x", 200);
|
||||||
|
road.setAttribute("y", 180);
|
||||||
|
svg.appendChild(road);
|
||||||
|
const plot = new El("g");
|
||||||
|
svg.appendChild(plot);
|
||||||
|
appendCulvertOverlay(
|
||||||
|
plot,
|
||||||
|
layout,
|
||||||
|
(o) => o,
|
||||||
|
(z) => z,
|
||||||
|
undefined,
|
||||||
|
hidePipe,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const titles = svg
|
||||||
|
.all()
|
||||||
|
.filter((e) => e.tag === "title")
|
||||||
|
.map((e) => e.textContent);
|
||||||
|
return {
|
||||||
|
infos: svg.querySelectorAll(".b06-chart__pipe-info").map((e) => e.textContent),
|
||||||
|
pipeTips: titles.filter((t) => t.startsWith("관매설")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const layout = computeCulvertLayout(section, section.samples);
|
||||||
|
const reverse = { ...layout, pipe: { ...layout.pipe, slopePct: -0.43 } };
|
||||||
|
process.stdout.write(
|
||||||
|
JSON.stringify({
|
||||||
|
slopePct: layout.pipe.slopePct,
|
||||||
|
lengthM: layout.pipe.lengthM,
|
||||||
|
pipe: draw(layout, false),
|
||||||
|
reverse: draw(reverse, false),
|
||||||
|
hidden: draw(layout, true),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""PLAN 371-2 — B06 횡단 카드 관 글 L 뒤 기울기(요구 240-16).
|
||||||
|
|
||||||
|
규칙: 「관매설 {관종}Φ…m/m, L=8 m (7.6), S=2.5%」(소수 한 자리 · 역경사 0.0) ·
|
||||||
|
툴팁 「매설 경사」 지움 · 세월교 · 박스 흙막이 세트(hidden_pipe = 관 숨김)는 관 글 없음 ·
|
||||||
|
B07 덧그림(노폭 라벨 없음)도 없음(225-1).
|
||||||
|
짝 = `helper_371_2_pipe_slope.cjs`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
HELPER = Path(__file__).parent / "helper_371_2_pipe_slope.cjs"
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def out() -> dict:
|
||||||
|
done = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||||
|
["node", str(HELPER)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8"
|
||||||
|
)
|
||||||
|
assert done.returncode == 0, done.stderr
|
||||||
|
return json.loads(done.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_관_글_L_뒤에_기울기_소수_한_자리(out: dict) -> None:
|
||||||
|
slope = f"{out['slopePct']:.1f}"
|
||||||
|
assert out["pipe"]["infos"] == [f"관매설 파형강관 Φ1000m/m, L=8 m (7.5), S={slope}%"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_역경사는_0_0(out: dict) -> None:
|
||||||
|
assert out["reverse"]["infos"][0].endswith(", S=0.0%")
|
||||||
|
|
||||||
|
|
||||||
|
def test_툴팁에_매설_경사_없음(out: dict) -> None:
|
||||||
|
tips = out["pipe"]["pipeTips"]
|
||||||
|
assert tips and all("매설 경사" not in tip for tip in tips)
|
||||||
|
|
||||||
|
|
||||||
|
def test_관_숨김_세트는_관_글_없음(out: dict) -> None:
|
||||||
|
assert out["hidden"] == {"infos": [], "pipeTips": []}
|
||||||
Reference in New Issue
Block a user