Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_8
This commit is contained in:
@@ -3360,7 +3360,8 @@
|
||||
"chute_add": "도수로/돌붙임",
|
||||
"chute_add.structure": "구조",
|
||||
"chute_add.structure=도수로": "도수로",
|
||||
"chute_add.structure=돌붙임": "돌붙임"
|
||||
"chute_add.structure=돌붙임": "돌붙임",
|
||||
"chute_add.thickness": "두께 (cm)"
|
||||
}
|
||||
},
|
||||
"box_culvert": {
|
||||
@@ -3422,7 +3423,8 @@
|
||||
"chute_add": "도수로/돌붙임",
|
||||
"chute_add.structure": "구조",
|
||||
"chute_add.structure=도수로": "도수로",
|
||||
"chute_add.structure=돌붙임": "돌붙임"
|
||||
"chute_add.structure=돌붙임": "돌붙임",
|
||||
"chute_add.thickness": "두께 (cm)"
|
||||
}
|
||||
},
|
||||
"ford_pavement": {
|
||||
@@ -3492,7 +3494,8 @@
|
||||
"chute_add": "도수로/돌붙임",
|
||||
"chute_add.structure": "구조",
|
||||
"chute_add.structure=도수로": "도수로",
|
||||
"chute_add.structure=돌붙임": "돌붙임"
|
||||
"chute_add.structure=돌붙임": "돌붙임",
|
||||
"chute_add.thickness": "두께 (cm)"
|
||||
}
|
||||
},
|
||||
"revetment": {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/* 90-2 도수로 · 돌붙임 띠 헬퍼 — `appendChuteBand`(B06_Section_UI_Cross_Wall_Hatch.ts)를 그 자리에서
|
||||
* 트랜스파일해 흉내 DOM 에 그림 · 결과 = 띠 꼭짓점(m) · 돌 수(표준출력 JSON). 짝 = `test_90_chute_band.py`.
|
||||
*/
|
||||
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();
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
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 localRequire = (request) => loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
class Node {
|
||||
constructor(tag) {
|
||||
this.tag = tag;
|
||||
this.attrs = {};
|
||||
this.children = [];
|
||||
}
|
||||
setAttribute(k, v) {
|
||||
this.attrs[k] = String(v);
|
||||
}
|
||||
getAttribute(k) {
|
||||
return this.attrs[k] ?? null;
|
||||
}
|
||||
append(...kids) {
|
||||
this.children.push(...kids);
|
||||
}
|
||||
set textContent(v) {
|
||||
this.text = v;
|
||||
}
|
||||
}
|
||||
globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) };
|
||||
|
||||
const { appendChuteBand } = loadTs(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Wall_Hatch.ts"),
|
||||
);
|
||||
const SCALE = 100;
|
||||
const run = (line) => {
|
||||
const layer = new Node("g");
|
||||
appendChuteBand(
|
||||
layer,
|
||||
[line],
|
||||
{ structure: "돌붙임", thickness_m: 0.3 },
|
||||
(o) => o * SCALE,
|
||||
(e) => -e * SCALE,
|
||||
);
|
||||
const band = layer.children.find((k) => k.tag === "polygon");
|
||||
const group = layer.children.find((k) => k.tag === "g");
|
||||
const points = band.attrs.points.split(" ").map((p) => {
|
||||
const [px, py] = p.split(",").map(Number);
|
||||
return { offset: px / SCALE, elevation: -py / SCALE };
|
||||
});
|
||||
return {
|
||||
points,
|
||||
stones: group.children.filter((k) => k.tag === "rect").length,
|
||||
clipped: group.attrs["clip-path"]?.startsWith("url(#") ?? false,
|
||||
title: band.children[0]?.text ?? "",
|
||||
};
|
||||
};
|
||||
const out = {
|
||||
// 좌측 유출(+ 바깥) · 성토 1:1.2 → 단 사이 수평 조각 → 다시 성토(흙막이 단 사이 포함)
|
||||
left: run([
|
||||
{ offset: 5, elevation: 10 },
|
||||
{ offset: 7.4, elevation: 8 },
|
||||
{ offset: 8.4, elevation: 8 },
|
||||
{ offset: 10.8, elevation: 6 },
|
||||
]),
|
||||
// 우측 유출(− 바깥) · 곧은 성토 한 줄
|
||||
right: run([
|
||||
{ offset: -5, elevation: 10 },
|
||||
{ offset: -8.6, elevation: 7 },
|
||||
]),
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -75,7 +75,8 @@ def test_저장_값은_도_와_인스턴스_키(out):
|
||||
"width": "6",
|
||||
"height": "0.3",
|
||||
} # 월류 폭 · 높이 = 옛 키 그대로 읽음(65-5)
|
||||
assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임"}]}
|
||||
# 두께(cm · 90-2) — 빈 칸은 빈 값(새 상자만 기본 30)
|
||||
assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임", "thickness": ""}]}
|
||||
assert out["written"] == {
|
||||
"ford_slope_deg": 15,
|
||||
"ford_width_m": 6,
|
||||
|
||||
@@ -64,7 +64,8 @@ def test_유입_유출은_구조와_흙막이_칸만(out):
|
||||
"before",
|
||||
"after",
|
||||
]
|
||||
assert out[spec]["chute_add"] == [["structure", None, None]]
|
||||
# 도수로/돌붙임 = 구조 · 두께(cm · 새 상자 30 · 90-2)
|
||||
assert out[spec]["chute_add"] == [["structure", None, None], ["thickness", None, "30"]]
|
||||
keys = out[f"{spec}Keys"]
|
||||
assert keys["prefixes"] == ["soil_add_", "chute_add_"]
|
||||
# 측벽 높이 · 좌우 · 상하 · 바닥판 · 날개벽 치수 칸 없음
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""90-2 도수로 · 돌붙임 띠 — 윗면 = 유출 성토사면 선 · 사면 안쪽(아래)으로 두께만큼 · 안에 돌.
|
||||
|
||||
화면 TS(`appendChuteBand`)를 그 자리에서 돌린 값(`helper_90_chute_band.cjs`).
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
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(HERE / "helper_90_chute_band.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
def _distance(p: dict, a: dict, b: dict) -> float:
|
||||
dx, dz = b["offset"] - a["offset"], b["elevation"] - a["elevation"]
|
||||
cross = (p["offset"] - a["offset"]) * dz - (p["elevation"] - a["elevation"]) * dx
|
||||
return abs(cross) / math.hypot(dx, dz)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["left", "right"])
|
||||
def test_띠_두께는_사면에서_직각으로_0_30m_아래(out: dict, case: str) -> None:
|
||||
points = out[case]["points"]
|
||||
n = len(points) // 2
|
||||
top, bottom = points[:n], points[n:][::-1]
|
||||
for i, low in enumerate(bottom):
|
||||
# 꼭짓점마다 붙은 변(앞 · 뒤)까지 직각 거리 = 두께
|
||||
for j in {max(i - 1, 0), min(i, n - 2)}:
|
||||
assert _distance(low, top[j], top[j + 1]) == pytest.approx(0.3, abs=1e-6)
|
||||
assert low["elevation"] < top[i]["elevation"] # 사면 안쪽 = 아래
|
||||
|
||||
|
||||
def test_안에_돌_윗면은_사면선(out: dict) -> None:
|
||||
for case in out.values():
|
||||
assert case["stones"] >= 2 and case["clipped"]
|
||||
assert "돌붙임" in case["title"] and "0.30m" in case["title"]
|
||||
assert out["left"]["points"][:4] == [
|
||||
{"offset": 5, "elevation": 10},
|
||||
{"offset": 7.4, "elevation": 8},
|
||||
{"offset": 8.4, "elevation": 8},
|
||||
{"offset": 10.8, "elevation": 6},
|
||||
]
|
||||
@@ -13,8 +13,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
|
||||
APRON_LENGTH_FACTOR,
|
||||
APRON_THICKNESS_M,
|
||||
CHUTE_THICKNESS_CM,
|
||||
MIN_PIPE_COVER_M,
|
||||
REVET_FACE_SLOPE,
|
||||
_culvert_set,
|
||||
@@ -24,10 +23,9 @@ from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
|
||||
|
||||
|
||||
def test_cross_reference_constants_match_plan():
|
||||
"""계획서 3-0 표 값 — 토피 0.5m·보호공 길이 2배·두께 0.45m(사용자 확정)·전면 1:0.3."""
|
||||
"""계획서 3-0 표 값 — 토피 0.5m·전면 1:0.3 · 도수로/돌붙임 빈 두께 30 cm(90-2 사용자 확정)."""
|
||||
assert MIN_PIPE_COVER_M == 0.5
|
||||
assert APRON_LENGTH_FACTOR == 2.0
|
||||
assert APRON_THICKNESS_M == 0.45
|
||||
assert CHUTE_THICKNESS_CM == 30.0
|
||||
assert REVET_FACE_SLOPE == 0.3
|
||||
# 배관은 수평도 가능 — 세트에 경사 강제값을 싣지 않는다(2026-08-20 사용자 확정).
|
||||
assert "min_slope" not in _culvert_set(None)
|
||||
@@ -54,9 +52,8 @@ def test_saved_values_drive_members():
|
||||
assert part["revet_form"] is None
|
||||
assert part["revet_height_m"] == 2.0
|
||||
assert part["face_slope"] == REVET_FACE_SLOPE
|
||||
# 보호공 = 낙차고(2.0) × 2 = 4.0m, 두께 0.45m(사용자 확정).
|
||||
assert part["apron_length_m"] == 4.0
|
||||
assert part["apron_thickness_m"] == 0.45
|
||||
# 옛 보호공(apron) 칸은 걷음 — 유출 보호는 도수로/돌붙임 띠(90-2)
|
||||
assert "apron_length_m" not in part
|
||||
|
||||
|
||||
def test_basin_inlet_skips_revet_and_apron():
|
||||
@@ -64,10 +61,9 @@ def test_basin_inlet_skips_revet_and_apron():
|
||||
spec = _culvert_set({"inlet_type": "집수정", "outlet_type": "기슭막이"})
|
||||
assert spec["inlet"]["structure"] == "집수정"
|
||||
assert "revet_height_m" not in spec["inlet"]
|
||||
assert "apron_length_m" not in spec["inlet"]
|
||||
# 유출측은 그대로 세트를 갖춘다.
|
||||
assert spec["outlet"]["structure"] == "기슭막이"
|
||||
assert spec["outlet"]["apron_length_m"] == 4.0 # 빈 높이 = 관경 기준 최소 2.0 × 2
|
||||
assert spec["outlet"]["revet_height_m"] == 2.0 # 빈 높이 = 관경 기준 최소
|
||||
|
||||
|
||||
def test_string_diameter_and_custom_height():
|
||||
@@ -77,7 +73,15 @@ def test_string_diameter_and_custom_height():
|
||||
)
|
||||
assert spec["diameter_m"] == 0.8
|
||||
assert spec["outlet"]["revet_height_m"] == 1.5
|
||||
assert spec["outlet"]["apron_length_m"] == 3.0 # 1.5 × 2
|
||||
|
||||
|
||||
def test_chute_thickness_cm_to_m():
|
||||
"""도수로/돌붙임(90-2) — 구조를 고르면 `chute` · 두께 cm → m · 빈 두께 30 cm · 안 고르면 없음."""
|
||||
assert "chute" not in _culvert_set({"chute_add_1_thickness": 40})
|
||||
spec = _culvert_set({"chute_add_1_structure": "돌붙임", "chute_add_1_thickness": "45"})
|
||||
assert spec["chute"] == {"structure": "돌붙임", "thickness_m": 0.45}
|
||||
blank = _culvert_set({"chute_add_1_structure": "도수로", "chute_add_1_thickness": ""})
|
||||
assert blank["chute"] == {"structure": "도수로", "thickness_m": 0.3}
|
||||
|
||||
|
||||
def test_attach_only_pipe_facilities(tmp_path):
|
||||
|
||||
@@ -57,6 +57,10 @@ POINTS = [
|
||||
"inlet_type": "흙막이",
|
||||
"inlet_revet_form": "콘크리트",
|
||||
"inlet_revet_height_m": 1.5,
|
||||
"inlet_revet_berm_m": 2,
|
||||
"outlet_revet_berm_m": 3, # 유출 수평공간 칸 없음(89-4) — 세트에 안 실림
|
||||
# 도수로/돌붙임(90-2) — 빈 두께 = 30 cm
|
||||
"chute_add_1_structure": "도수로",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -72,6 +76,8 @@ POINTS = [
|
||||
"outlet_revet_form": "돌쌓기(찰)",
|
||||
"outlet_revet_height_m": 3.5,
|
||||
"outlet_revet_length_m": 10,
|
||||
"chute_add_2_structure": "돌붙임",
|
||||
"chute_add_2_thickness": 45,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -199,3 +205,8 @@ def test_ford_box_guard_attached_as_culvert() -> None:
|
||||
assert box["diameter_m"] == round(b["top_thickness_m"] + 2.5 + b["slab_thickness_m"], 3)
|
||||
# 흙막이 없는 시설엔 안 얹음
|
||||
assert "culvert" not in sections[100.0]
|
||||
# 유입만 수평공간(89-4) · 도수로/돌붙임 두께 cm → m(90-2)
|
||||
assert ford["inlet"]["revet_berm_m"] == 2 and "revet_berm_m" not in ford["outlet"]
|
||||
assert sections[60.0]["ford"]["chute"] == {"structure": "도수로", "thickness_m": 0.3}
|
||||
assert box["chute"] == {"structure": "돌붙임", "thickness_m": 0.45}
|
||||
assert "chute" not in sections[40.0]["culvert"]
|
||||
|
||||
Reference in New Issue
Block a user