Merge remote-tracking branch 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -131,7 +131,8 @@ function reshaped(rise, slope) {
|
||||
const s = clone(pipe);
|
||||
for (const p of s.samples) {
|
||||
const t = ((p.offset_m ?? 0) - start.offset) * outward;
|
||||
if (t >= 0 && typeof p.elevation_m === "number")
|
||||
// 시작점 바로 앞 표본까지(−1 m) — 보간이 시작점을 옛 지반에 두지 않게(371-1 벽 자리 이동)
|
||||
if (t >= -1 && typeof p.elevation_m === "number")
|
||||
p.elevation_m = start.elevation + rise + slope * t;
|
||||
}
|
||||
return study(
|
||||
@@ -141,4 +142,5 @@ function reshaped(rise, slope) {
|
||||
}
|
||||
out.pipeDown = reshaped(0.3, -0.15); // 3도보다 가파르게 내림 → 3도 선이 만남
|
||||
out.pipeUp = reshaped(0.3, 0.1); // 오르막 → 3도 선이 못 만남 → 복합경사
|
||||
out.pipeOpen = reshaped(-0.3, -0.5); // 끝 바깥이 지반 아래로 열림 → 성토부선(371-1)
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
|
||||
@@ -107,6 +107,8 @@ process.stdout.write(
|
||||
attached,
|
||||
sections: sections.map((s) => s.underdrain ?? null),
|
||||
outline: draw.underdrainOutline(design, 1),
|
||||
// 371-3 암거 경사 3 % — 하단 = 높은 쪽(좌) 노면 끝 − 1 m 에서 3 % 로 낮은 쪽(우)으로 내림
|
||||
sloped: draw.underdrainOutline(design, 1, 3),
|
||||
bothCut: draw.underdrainOutline(bothCut, 1),
|
||||
noHeight: draw.underdrainOutline(design, null),
|
||||
drew: !!group,
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
);
|
||||
@@ -54,11 +54,14 @@ def test_3도_선이_원지반을_못_만나면_복합경사(out: dict) -> None:
|
||||
assert len(study["cut"]) == 1
|
||||
tail = study["cut"][0]
|
||||
assert tail["rises"] is True and tail["grade"] == -1 and tail["endGap"] == 0
|
||||
assert study["cutArea"] > study["cutAreaWithout"]
|
||||
# 371-1 — 벽 자리 0.35 m 도로 쪽 · 오르는 선 0.3 m 라 면적 차가 백엔드 설계선 끝에 묻힘 → 폐회로에 듦만 봄
|
||||
assert study["inTrim"] is True
|
||||
|
||||
|
||||
def test_성토_끝은_그대로(out: dict) -> None:
|
||||
assert out["pipeBase"] == ["fill", "fill"] # 묻히지 않은 기본 = 성토부선 그대로
|
||||
# 371-1 — 다단 사이 시작 = 윗단 발끝 · 단이 0.5 m 내려가 기본 자료 끝 벽이 0.3 m 묻힘 → 3도 선
|
||||
assert out["pipeBase"] == ["fill", "cut"]
|
||||
assert out["pipeOpen"]["cut"] == [] # 묻히지 않은 끝 = 성토부선 그대로
|
||||
|
||||
|
||||
def test_유출_절토선은_그리고_면적에_넣음() -> None:
|
||||
|
||||
@@ -40,10 +40,11 @@ def out(tmp_path_factory) -> dict:
|
||||
|
||||
|
||||
def test_안_넓힌_측점은_종전_그대로(out: dict) -> None:
|
||||
"""225-1 — 정수 맞춤 없앰: 벽은 기본 자리 · 관 길이 = 그려진 길이 0.1 m(5.772 → 5.8)."""
|
||||
"""225-1 — 정수 맞춤 없앰: 벽은 기본 자리 · 관 길이 = 그려진 길이 0.1 m(5.772 → 5.8).
|
||||
371-1 — 기본 자리 = 성토 위 이음(끝에서 0.1 m) 기준 · 벽 0.35 m 도로 쪽(5.772 → 5.422)."""
|
||||
base = out["base"]
|
||||
assert (base["edge"], base["pureHeight"], base["lengthM"]) == (2, 2, 5.8)
|
||||
assert base["cut"] == pytest.approx(5.772)
|
||||
assert (base["edge"], base["pureHeight"], base["lengthM"]) == (2, 2, 5.4)
|
||||
assert base["cut"] == pytest.approx(5.422)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, extend", [("shifted", 1.0), ("shifted2", 1.3)])
|
||||
|
||||
@@ -61,10 +61,12 @@ def test_묻힌_유출_벽_설계선이_안_접힘(out: dict, case: str) -> None
|
||||
|
||||
def test_저장값_사본_면적(out: dict) -> None:
|
||||
"""접힌 폐회로(절토 27.94 · 성토 0.34 ㎡) → 노견~벽~3도 선 절토만."""
|
||||
assert out["stored"]["cut"] == pytest.approx(34.0391, abs=1e-3)
|
||||
# 371-1 — 성토 위 이음 = 윗면 끝에서 0.1 m · 기본 자리 새 이음 기준(34.0391 → 34.2539)
|
||||
assert out["stored"]["cut"] == pytest.approx(34.2539, abs=1e-3)
|
||||
assert out["stored"]["fill"] == 0
|
||||
# 225-1 — 유출 벽이 관 길이 맞춤으로 밀리지 않고 기본 자리(16.8211 → 16.0929)
|
||||
assert out["auto"]["cut"] == pytest.approx(16.0929, abs=1e-3)
|
||||
# 371-1 — 기본 자리 = 새 이음 기준(16.0929 → 16.2613)
|
||||
assert out["auto"]["cut"] == pytest.approx(16.2613, abs=1e-3)
|
||||
|
||||
|
||||
def test_절토_쪽_배관_유출도_안_접힘(out: dict) -> None:
|
||||
|
||||
@@ -72,7 +72,8 @@ def test_좌우_한_번은_d_를_지금_자리로_묶음(out: dict) -> None:
|
||||
def test_관_길이는_벽_자리에서_0_1m_로(out: dict) -> None:
|
||||
"""유출 벽을 0.1 m 씩 밀면 관 길이도 0.1 m 씩 — 정수 올림 · 길이 맞춤 이동 없음."""
|
||||
lengths = [case["lengthM"] for case in out["moved"]]
|
||||
assert lengths == [7.9, 8.0, 8.1, 8.2]
|
||||
# 371-1 — 기본 자리 = 성토 위 이음 기준 · 벽 0.35 m 도로 쪽(7.9 → 7.5)
|
||||
assert lengths == [7.5, 7.6, 7.7, 7.8]
|
||||
for case in out["moved"]:
|
||||
assert case["lengthM"] == round(case["cut"], 1)
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""288 맹암거 횡단 형상(요구 183-3 · 9).
|
||||
|
||||
못박는 것
|
||||
- 288-1 맹암거 기준 측점 = 측구 없음 · 노면 = 암거 경사로 지대 낮은 쪽 한 경사(물넘이 286 · 287 길 —
|
||||
서버 `tilt_ranges` · `tilt_args` · TS 짝 `tilted`) · 제원 얹기 서버 `attach_underdrains` ↔ TS
|
||||
`attachUnderdrainSpecs` 같은 값 · 형상 = 노면을 암거 높이만큼 아래로 옵셋 → 성토 쪽은 성토 사면과
|
||||
만남 · 절토 쪽은 절토 사면을 늘여 만남 · 그 안 돌 무늬 · 높이 비면 안 그림.
|
||||
- 288-1 맹암거 기준 측점 = 측구 없음 · 노면 = 표준 경사로 지대 낮은 쪽 한 경사
|
||||
(물넘이 286 · 287 길 — 서버 `tilt_ranges` · `tilt_args` · TS 짝 `tilted` · 암거 경사는
|
||||
노면에 안 감 371-3) · 제원 얹기 서버 `attach_underdrains` ↔ TS `attachUnderdrainSpecs`
|
||||
같은 값 · 형상 = 노면을 암거 높이만큼 아래로 옵셋 → 성토 쪽은 성토 사면과 만남 ·
|
||||
절토 쪽은 절토 사면을 늘여 만남 · 그 안 돌 무늬 · 높이 비면 안 그림.
|
||||
- 371-3 암거 경사 = 형상 하단 경계 — 높은 쪽 노면 끝 − 높이에서 암거 경사로 낮은 쪽으로 내림.
|
||||
- 288-2 맹암거 형상 누름 = 그 구조물 id 고름(구조물 목록 창구 · 알약 · 폼 · 측점선 따라옴) · 물넘이와 같은
|
||||
판정면(보라 손잡이 아래 · 끌기 = 고름 아님 · 287-5) · 노면공 포장 범위 고름과 따로.
|
||||
|
||||
@@ -86,8 +88,9 @@ def ts(tmp_path_factory) -> dict:
|
||||
|
||||
def test_맹암거_자리_기울기_서버(project):
|
||||
ranges = tilt.tilt_ranges(project)
|
||||
assert ranges == [(40.0, 40.0, 4.0), (80.0, 80.0, None)]
|
||||
assert tilt.tilt_args(40.0, ranges) == {"tilted": True, "tilt_pct": 4.0}
|
||||
# 371-3 — 암거 경사(4 %)는 노면에 안 감 · 노면 = 표준 경사
|
||||
assert ranges == [(40.0, 40.0, None), (80.0, 80.0, None)]
|
||||
assert tilt.tilt_args(40.0, ranges) == {"tilted": True, "tilt_pct": None}
|
||||
assert tilt.tilt_args(80.01, ranges) == {"tilted": True, "tilt_pct": None}
|
||||
assert tilt.tilt_args(60.0, ranges) == {"tilted": False, "tilt_pct": None}
|
||||
|
||||
@@ -110,8 +113,9 @@ def test_맹암거_측점_설계_측구_없음_낮은_쪽_기울기():
|
||||
for o in range(-24, 25)
|
||||
]
|
||||
kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"}
|
||||
got = compute_cross_design(samples, 100.4, tilted=True, tilt_pct=4.0, **kwargs)
|
||||
assert got["ditch_enabled"] is False and got["cross_slope_pct"] == 4.0
|
||||
got = compute_cross_design(samples, 100.4, tilted=True, tilt_pct=None, **kwargs)
|
||||
standard = compute_cross_design(samples, 100.4, **kwargs)["cross_slope_pct"]
|
||||
assert got["ditch_enabled"] is False and got["cross_slope_pct"] == standard
|
||||
left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right"))
|
||||
assert left > right # 지반이 좌로 오름 = 우가 낮음
|
||||
|
||||
@@ -131,6 +135,20 @@ def test_형상_옵셋선_성토_절토와_만남(ts):
|
||||
assert ts["drew"] is True and ts["stones"] > 0
|
||||
|
||||
|
||||
def test_하단_경계는_암거_경사(ts):
|
||||
"""371-3 — 노면 5 % · 암거 3 % · 높이 1 m: 하단 = 좌(높은 쪽) 끝 99.15 에서 3 % 로 우로 내림."""
|
||||
outline = [(p["offset"], p["elevation"]) for p in ts["sloped"]]
|
||||
expected = [
|
||||
(3, 100.15),
|
||||
(-3, 99.85),
|
||||
(-4.391304, 98.928261), # 우 성토 사면 ∩ 하단(99.15 − 0.03 × 7.391)
|
||||
(1.872180, 99.116165), # 좌 절토 사면을 늘인 선 ∩ 하단
|
||||
]
|
||||
assert len(outline) == len(expected)
|
||||
for got, want in zip(outline, expected):
|
||||
assert got == pytest.approx(want, abs=1e-5)
|
||||
|
||||
|
||||
def test_양측절토_늘인_노면_원지반_교점에서_끊음(ts):
|
||||
"""356-1 — 낮은 쪽 노면 끝도 절토(tilt_reach) = reach 에서 수직으로 끊음 · 너머 원지반 따라가지 않음."""
|
||||
outline = [(p["offset"], p["elevation"]) for p in ts["bothCut"]]
|
||||
|
||||
@@ -221,6 +221,9 @@ def test_포장_기울임_하강_판정_서버와_같음(r, server):
|
||||
hits["tilted"] += py["tilted"]
|
||||
hits["drop"] += py["drop"] > 0
|
||||
assert all(hits.values()), hits # 셋 다 실제로 걸림
|
||||
# 371-3 — 맹암거 자리 = 기울임 · 경사 비움(노면 표준 · 암거 경사는 형상 하단 경계)
|
||||
drain = [py for ts_at, py in zip(r["ts"]["at"], r["rules"]) if ts_at["chainage"] == 160.0]
|
||||
assert drain == [{"paved": False, "tilted": True, "tilt_pct": None, "drop": 0.0}]
|
||||
|
||||
|
||||
KEYS = (
|
||||
|
||||
@@ -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": []}
|
||||
@@ -36,6 +36,9 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
under: buildRevetWallGeometry({
|
||||
anchor, outward: 1, height: 2, material: "dry", role: "outlet", roadElevation: 103,
|
||||
}).topJoint.elevation,
|
||||
fill: (({ fillTop, topJoint, points }) => [fillTop.offset, fillTop.elevation, topJoint.offset, points[2].offset, points[2].elevation])(
|
||||
buildRevetWallGeometry({ anchor, outward: 1, height: 2.5, material: "dry", role: "outlet" }),
|
||||
),
|
||||
dry: revetWallSpec({ revet_form: "돌쌓기(메)" }, { h: 3.0 }, true, 0.3).pureHeight,
|
||||
concrete: revetWallSpec({ revet_form: "콘크리트" }, { h: 7.5 }, true, 0.3).pureHeight,
|
||||
}));
|
||||
@@ -84,6 +87,11 @@ def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path
|
||||
assert got["under"] == pytest.approx(102.0), "노면 아래 벽이 잘림"
|
||||
assert got["dry"] == pytest.approx(3.0), "돌쌓기(메) 3.0 이 잘림"
|
||||
assert got["concrete"] == pytest.approx(7.5), "콘크리트 7.5 가 잘림"
|
||||
# 371-1 — 성토 위 이음 = 윗면 사면 쪽 끝에서 0.1 m 안 · 무늬 이음선(topJoint)은 그대로
|
||||
fill_offset, fill_elevation, joint_offset, front_offset, front_elevation = got["fill"]
|
||||
assert fill_offset == pytest.approx(front_offset - 0.1), "성토 위 이음이 끝에서 0.1 m 아님"
|
||||
assert fill_elevation == pytest.approx(front_elevation), "성토 위 이음이 윗면 표고 아님"
|
||||
assert joint_offset == pytest.approx(front_offset - 0.45), "무늬 이음선이 옮겨짐"
|
||||
|
||||
|
||||
def test_네_벽이_공용_함수로_서고_형태_한도가_없다() -> None:
|
||||
|
||||
Reference in New Issue
Block a user