fix(b06): 물넘이포장 도수로/돌붙임 길이 = 성토 사면보다 길면 원지반 이음 조각까지 한 선 · 월류 높이선 성토 쪽 = 성토선 점선 연장과 교점 (315)
- 315-1 fordChuteLine — 길이 맞춘 선의 첫 조각만 쓰던 흠(원지반 이음 조각 버림) · 이은 조각까지 한 선 · 길이 없음 · 짧은 길이 · 절토 쪽 그대로 - 315-2 overflowLine — 성토 쪽(노면 끝 밖 설계선이 내려감) = 성토 사면선을 노면 끝 너머 위로 연장해 월류 높이선과 만나는 자리에서 끝 · 절토 · 늘인 노면 끝(294-3) 그대로 · 같은 점선 양식(B06 카드 · B07 [상세] 같은 그림) - 시험 test_315_ford_chute_overflow 새로 · test_287 월류선 기대값 315 규칙 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/* 315 물넘이포장 도수로 · 돌붙임 길이(성토) · 월류 높이선 성토 쪽 연장 헬퍼 — 진짜 TS 를 Node 로 돌려 JSON 한 벌로. */
|
||||
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 load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`));
|
||||
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
|
||||
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
|
||||
return new Proxy({}, { get: () => () => {} });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
class Node {
|
||||
constructor(tag) {
|
||||
this.tag = tag;
|
||||
this.attrs = {};
|
||||
this.children = [];
|
||||
this.dataset = {};
|
||||
this.style = {};
|
||||
}
|
||||
setAttribute(k, v) {
|
||||
this.attrs[k] = String(v);
|
||||
}
|
||||
getAttribute(k) {
|
||||
return this.attrs[k] ?? null;
|
||||
}
|
||||
append(...kids) {
|
||||
this.children.push(...kids);
|
||||
}
|
||||
}
|
||||
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
|
||||
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"));
|
||||
const hatch = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"));
|
||||
const fillGround = hatch.groundLine(input.fillSamples);
|
||||
const dipGround = hatch.groundLine(input.dipSamples);
|
||||
const chute = (design, ground, length) =>
|
||||
pave.fordChuteLine(design, length ? { length_m: length } : {}, ground);
|
||||
// 그린 SVG — 띠(돌붙임) · 월류 높이선 점 수
|
||||
const svg = new Node("g");
|
||||
pave.appendFordPavementOverlay(
|
||||
svg,
|
||||
{
|
||||
span_m: 8,
|
||||
depth_m: 0.3,
|
||||
slope_pct: 5,
|
||||
chute: { structure: "돌붙임", thickness_m: 0.3, length_m: 6 },
|
||||
},
|
||||
input.fillDesign,
|
||||
(o) => o,
|
||||
(e) => e,
|
||||
fillGround,
|
||||
);
|
||||
const deck = svg.children.find((k) => k.attrs.class === "b06-chart__ford-deck-plan");
|
||||
const band = svg.children.find((k) => k.attrs.class === "b06-chart__culvert-pitching");
|
||||
|
||||
const out = {
|
||||
fillNone: chute(input.fillDesign, fillGround),
|
||||
fillShort: chute(input.fillDesign, fillGround, 1),
|
||||
fillLong: chute(input.fillDesign, fillGround, 6),
|
||||
dipNone: chute(input.dipDesign, dipGround),
|
||||
dipLong: chute(input.dipDesign, dipGround, 2),
|
||||
fillOverflow: pave.overflowLine(input.fillDesign, 0.3),
|
||||
dipOverflow: pave.overflowLine(input.dipDesign, 0.3),
|
||||
fillEdges: pave.fordEdges(input.fillDesign),
|
||||
dipEdges: pave.fordEdges(input.dipDesign),
|
||||
deckPoints: deck?.attrs.points ?? null,
|
||||
bandPoints: band?.attrs.points ?? null,
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -75,8 +75,10 @@ def test_포장층_노견까지_점선은_노면_위(ts):
|
||||
# 포장층 하나 = 노견 끝(±3)까지 · 윗면 = 설계 노면 · 두께 0.2
|
||||
assert len(ts["pavePoints"]) == 1
|
||||
assert _pts(ts["pavePoints"][0]) == pytest.approx([3, 100.15, -3, 99.85, -3, 99.65, 3, 99.95])
|
||||
# 월류 높이선 = 노면 + 0.4 · 같은 각도
|
||||
assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, -3, 100.25])
|
||||
# 월류 높이선 = 노면 + 0.4 · 같은 각도 · 우(성토) = 성토선 연장과 만난 자리 → 노면 끝(315-2)
|
||||
d = 0.4 / (2.65 / 4 - 0.3 / 6)
|
||||
meet = [-3 + d, 99.85 + 2.65 / 4 * d]
|
||||
assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, *meet, -3, 99.85])
|
||||
assert ts["inflowLeft"] is True # 노면 높은 쪽 = 유입 · 도수로 띠 = 낮은 쪽
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
"""315 물넘이포장 횡단 — 도수로 · 돌붙임 길이(성토) · 월류 높이선 성토 쪽 연장(요구 211-1).
|
||||
|
||||
못박는 것
|
||||
- 315-1 길이 없음 = 성토는 원지반까지(그대로) · 길이 > 성토 사면 = 사면 끝에서 원지반 따라 이어 그 길이
|
||||
(이은 조각까지 한 선 · 옛 흠 = 첫 조각만 그림) · 길이 < 사면 = 사면 따라 그 길이 · 절토(늘인 노면 끝이
|
||||
원지반) = 그대로(길이 없음 = 안 그림 · 길이 = 원지반 따라).
|
||||
- 315-2 월류 높이선 — 절토 쪽 = 노면 끝 위 월류 높이(그대로) · 성토 쪽 = 성토 사면선을 노면 끝 너머 위 ·
|
||||
안쪽으로 점선 연장해 월류 높이선과 만나는 자리에서 끝(같은 점선 양식 한 벌).
|
||||
|
||||
진짜 TS 는 `helper_315_ford_chute_overflow.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if str(HERE) not in sys.path:
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
from config.config_system import STANDARD_CROSS_SECTION # noqa: E402
|
||||
from test_b06_cross_design_mirror import _dip_ground # noqa: E402
|
||||
|
||||
KW = {
|
||||
"ground_type": "soil",
|
||||
"section_mode": "left_cut",
|
||||
"ditch_side": "left",
|
||||
"paved": True,
|
||||
"tilted": True,
|
||||
"tilt_pct": 5.0,
|
||||
"standard": STANDARD_CROSS_SECTION,
|
||||
}
|
||||
DEPTH = 0.3
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _fill_samples() -> list[dict]:
|
||||
"""좌 = 절토(원지반이 노면 위) · 우(낮은 쪽) = 성토 — 원지반 100 + 0.35·o."""
|
||||
return [
|
||||
{"offset_m": o / 2, "elevation_m": round(100.0 + 0.35 * o / 2, 4), "valid": True}
|
||||
for o in range(-40, 41)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
source = tmp_path_factory.mktemp("s315") / "in.json"
|
||||
payload = {
|
||||
"fillSamples": _fill_samples(),
|
||||
"fillDesign": compute_cross_design(_fill_samples(), 100.4, **KW),
|
||||
"dipSamples": _dip_ground(),
|
||||
"dipDesign": compute_cross_design(_dip_ground(), 100.4, **KW),
|
||||
}
|
||||
source.write_text(json.dumps(payload), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_315_ford_chute_overflow.cjs"), str(source)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _length(line: list[dict]) -> float:
|
||||
return sum(
|
||||
math.hypot(b["offset"] - a["offset"], b["elevation"] - a["elevation"])
|
||||
for a, b in zip(line, line[1:])
|
||||
)
|
||||
|
||||
|
||||
def _on_ground(p: dict) -> bool:
|
||||
return p["elevation"] == pytest.approx(100.0 + 0.35 * p["offset"], abs=1e-3)
|
||||
|
||||
|
||||
def test_길이_없음_성토는_원지반까지(ts):
|
||||
line = ts["fillNone"]
|
||||
assert line[0]["offset"] == pytest.approx(-2.0) # 낮은 쪽(우) 노견 끝
|
||||
assert _on_ground(line[-1]) and not _on_ground(line[0])
|
||||
assert all(b["elevation"] < a["elevation"] for a, b in zip(line, line[1:]))
|
||||
|
||||
|
||||
def test_성토보다_긴_길이는_원지반_따라_이어_그_길이(ts):
|
||||
slope = _length(ts["fillNone"])
|
||||
line = ts["fillLong"]
|
||||
assert slope < 6
|
||||
assert _length(line) == pytest.approx(6.0, abs=1e-6) # 옛 흠 = 사면 길이에서 멈춤
|
||||
toe = ts["fillNone"][-1]
|
||||
beyond = [p for p in line if p["offset"] < toe["offset"] - 1e-6]
|
||||
assert beyond and all(_on_ground(p) for p in beyond) # 사면 끝 너머 = 원지반 위
|
||||
assert line[: len(ts["fillNone"])] == ts["fillNone"] # 앞은 사면 그대로 · 한 선
|
||||
|
||||
|
||||
def test_짧은_길이는_사면_따라_그_길이(ts):
|
||||
line = ts["fillShort"]
|
||||
assert _length(line) == pytest.approx(1.0, abs=1e-6)
|
||||
assert not any(_on_ground(p) for p in line)
|
||||
|
||||
|
||||
def test_절토_쪽_그대로(ts):
|
||||
assert ts["dipNone"] == [] # 늘인 노면 끝이 원지반 · 길이 없음 = 안 그림
|
||||
line = ts["dipLong"] # 길이 = 늘인 끝에서 원지반 따라 그 길이
|
||||
assert line[0]["offset"] == pytest.approx(ts["dipEdges"]["right"]["offset_m"], abs=1e-6)
|
||||
assert _length(line) == pytest.approx(2.0, abs=1e-6)
|
||||
|
||||
|
||||
def test_그린_띠도_긴_길이(ts):
|
||||
nums = [float(v) for pair in ts["bandPoints"].split() for v in pair.split(",")]
|
||||
offsets = nums[0::2]
|
||||
assert min(offsets) < ts["fillNone"][-1]["offset"] - 1 # 띠가 사면 끝 너머 원지반까지
|
||||
|
||||
|
||||
def test_월류선_절토_쪽은_노면_끝_위(ts):
|
||||
edges = ts["fillEdges"]
|
||||
first = ts["fillOverflow"][0] # 좌 = 절토
|
||||
assert first["offset"] == pytest.approx(edges["left"]["offset_m"])
|
||||
assert first["elevation"] == pytest.approx(edges["left"]["elevation_m"] + DEPTH)
|
||||
dip = ts["dipOverflow"] # 양측 절토 = 두 점 그대로
|
||||
assert len(dip) == 2
|
||||
assert dip[-1]["offset"] == pytest.approx(ts["dipEdges"]["right"]["offset_m"])
|
||||
assert dip[-1]["elevation"] == pytest.approx(ts["dipEdges"]["right"]["elevation_m"] + DEPTH)
|
||||
|
||||
|
||||
def test_월류선_성토_쪽은_성토선_점선_연장과_만남(ts):
|
||||
edges = ts["fillEdges"]
|
||||
left, right = edges["left"], edges["right"]
|
||||
*_, meet, end = ts["fillOverflow"]
|
||||
# 끝 = 우 노면 끝(성토 사면 시작) · 그 앞 교점 = 노면 안쪽
|
||||
assert end == pytest.approx({"offset": right["offset_m"], "elevation": right["elevation_m"]})
|
||||
assert right["offset_m"] < meet["offset"] < left["offset_m"]
|
||||
# 교점은 월류 높이선 위
|
||||
road = (left["elevation_m"] - right["elevation_m"]) / (left["offset_m"] - right["offset_m"])
|
||||
over = right["elevation_m"] + DEPTH + road * (meet["offset"] - right["offset_m"])
|
||||
assert meet["elevation"] == pytest.approx(over, abs=1e-9)
|
||||
# 교점 → 노면 끝 = 성토 사면과 같은 물매(사면선의 연장)
|
||||
slope = ts["fillNone"]
|
||||
fill = (slope[0]["elevation"] - slope[1]["elevation"]) / (
|
||||
slope[0]["offset"] - slope[1]["offset"]
|
||||
)
|
||||
ext = (meet["elevation"] - end["elevation"]) / (meet["offset"] - end["offset"])
|
||||
assert ext == pytest.approx(fill, abs=1e-9)
|
||||
# 그린 점선 = 같은 점(같은 양식 한 벌)
|
||||
assert ts["deckPoints"].split()[-2:] == [
|
||||
f"{meet['offset']},{meet['elevation']}",
|
||||
f"{end['offset']},{end['elevation']}",
|
||||
]
|
||||
Reference in New Issue
Block a user