feat(b05,b06): 물넘이포장 도수로/돌붙임 = 다른 구조물과 같은 칸 + 맨 아래 「길이 (m)」 — 그림 전용 (286-3)
- 명세 — 도수로/돌붙임 칸 = CHUTE_ITEMS(구조 · 형태 · 두께 · 면적) + 길이(ford_chute_{n}_length_m)
- 서버 _chute · TS chute 짝 — 길이 있으면 제원에 length_m
- 횡단 · 3D 띠 — 유출 성토사면을 노견에서 사면 따라 그 길이까지(비거나 사면보다 길면 원지반까지)
- 길이 = 그림 전용 · 집계 열 · B08 돌붙임 수량(면적)에 안 실림
- 시험 test_286_1 · 물넘이 명세 시험 기대값 새 칸으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -2,13 +2,16 @@
|
||||
* design_structure_ford_pavement.ts
|
||||
* 횡단배수 › 물넘이 포장 명세 — 사용자 구조물 표 03(`docs/raw/구조물정의_표/03_횡단배수_물넘이포장.md`) 그대로.
|
||||
*
|
||||
* 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도 + 월류 폭 · 높이 · 65-5) · 도수로/돌붙임(선택 · 구조 = 도수로 · 돌붙임).
|
||||
* 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_structure` ·
|
||||
* 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도 + 월류 폭 · 높이 · 65-5) · 도수로/돌붙임(선택 · 다른 구조물과
|
||||
* 같은 칸 `CHUTE_ITEMS` + 맨 아래 길이 · 286-3).
|
||||
* 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_*` ·
|
||||
* 월류 폭 · 높이 = 세월교와 같은 키(`ford_width_m` · `ford_height_m`) · 같은 계산(`design_structure_ford_calc`).
|
||||
* 경사각도는 옛 「바닥 경사(%)」(`ford_slope_pct` · B06 파임 기울기)와 뜻이 같은지 또렷하지 않아 잇지 않음.
|
||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||
* 길이(`ford_chute_{n}_length_m`) = 횡단 · 3D 띠를 사면 따라 그릴 길이 — 그림 전용(수량 · 내역 · 일위대가에 안 씀) ·
|
||||
* 비면 원지반까지.
|
||||
* ========================================================================== */
|
||||
|
||||
import { CHUTE_ITEMS } from "./design_structure_guard";
|
||||
import type { StructureSpec } from "./design_structure_spec";
|
||||
|
||||
export const FORD_PAVEMENT_SPEC: StructureSpec = {
|
||||
@@ -32,20 +35,8 @@ export const FORD_PAVEMENT_SPEC: StructureSpec = {
|
||||
prefix: "ford_chute",
|
||||
link: "gfordpavechute",
|
||||
max: 1,
|
||||
// 구조(기본 도수로) · 두께(cm · 기본 45 · 173-5) — 배관 · 박스 · 세월교 `CHUTE_ITEMS` 와 같은 칸(90-2)
|
||||
items: [
|
||||
{
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
choices: [
|
||||
{ value: "도수로", label: "도수로" },
|
||||
{ value: "돌붙임", label: "돌붙임" },
|
||||
],
|
||||
default: "도수로",
|
||||
},
|
||||
{ id: "thickness", label: "두께 (cm)", kind: "number", step: 1, default: "45" },
|
||||
],
|
||||
// 배관 · 박스 · 세월교와 같은 칸(구조 · 형태 · 두께 · 면적 · 90-2) + 맨 아래 길이(그림 전용 · 286-3)
|
||||
items: [...CHUTE_ITEMS, { id: "length_m", label: "길이 (m)", kind: "number", step: 0.1 }],
|
||||
},
|
||||
],
|
||||
// 표 03 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import type { ChuteSpec, CrossSection } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import type { OffsetPoint } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types";
|
||||
import { chuteBandOutline } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch";
|
||||
import { outletFillLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { fordChuteLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement";
|
||||
|
||||
/** 성토 조각 하나 — B06 오버레이가 띠를 그리는 선(절토 조각은 뺀다). */
|
||||
interface FillSegment {
|
||||
@@ -51,6 +51,6 @@ export function fordPavementChute(
|
||||
);
|
||||
if (owner !== section) return null;
|
||||
}
|
||||
const bands = chuteBands([{ points: outletFillLine(section.design) }], pave.chute);
|
||||
const bands = chuteBands([{ points: fordChuteLine(section.design, pave.chute) }], pave.chute);
|
||||
return bands.length ? { bands, halfM: Math.max(pave.span_m, 0.5) / 2 } : null;
|
||||
}
|
||||
|
||||
@@ -228,6 +228,8 @@ export interface CulvertSideSpec {
|
||||
export interface ChuteSpec {
|
||||
structure: string;
|
||||
thickness_m: number;
|
||||
/** 사면 따라 그릴 길이(m · 물넘이 포장 286-3) — 그림 전용 · 없으면 원지반까지. */
|
||||
length_m?: number;
|
||||
}
|
||||
|
||||
/** 배수관 측점의 세트(배관·기슭막이·보호공) 제원 — 횡단 카드 오버레이 입력. */
|
||||
|
||||
@@ -251,7 +251,8 @@ def _side_spec(
|
||||
def _chute(values: dict[str, Any], head: str = "chute_add") -> dict[str, Any] | None:
|
||||
"""도수로 · 돌붙임(90-2) — 첫 상자(`{head}_{n}_*` · 물넘이 포장 = `ford_chute` · 136-2) 구조 ·
|
||||
두께(cm → m · 빈 값 30 cm) · 구조를 안 고르면 None. 횡단이 유출 성토사면을 따라 띠로 그림.
|
||||
TS 짝 `chute`."""
|
||||
길이(`{head}_{n}_length_m` · 286-3) = 사면 따라 그릴 길이 — 그림 전용(수량 · 내역에 안 씀) ·
|
||||
비면 원지반까지. TS 짝 `chute`."""
|
||||
pattern = re.compile(rf"^{head}_(\d+)_structure$")
|
||||
picked = sorted(
|
||||
(int(hit.group(1)), str(value).strip())
|
||||
@@ -263,7 +264,11 @@ def _chute(values: dict[str, Any], head: str = "chute_add") -> dict[str, Any] |
|
||||
n, structure = picked[0]
|
||||
cm = _number(values.get(f"{head}_{n}_thickness"), None)
|
||||
cm = cm if cm and cm > 0 else CHUTE_THICKNESS_CM
|
||||
return {"structure": structure, "thickness_m": round(cm / 100.0, 3)}
|
||||
found = {"structure": structure, "thickness_m": round(cm / 100.0, 3)}
|
||||
length = _number(values.get(f"{head}_{n}_length_m"), None)
|
||||
if length and length > 0:
|
||||
found["length_m"] = round(length, 3)
|
||||
return found
|
||||
|
||||
|
||||
def _with_chute(
|
||||
|
||||
@@ -34,8 +34,8 @@ export interface FordPavementSpec {
|
||||
slope_pct: number | null;
|
||||
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
|
||||
defaulted?: string[];
|
||||
/** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠. */
|
||||
chute?: { structure: string; thickness_m: number };
|
||||
/** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠 · 길이 = 사면 따라(286-3 · 비면 원지반까지). */
|
||||
chute?: { structure: string; thickness_m: number; length_m?: number };
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
@@ -190,7 +190,8 @@ export function appendFordPavementOverlay(
|
||||
"b06-chart__ford-deck",
|
||||
),
|
||||
);
|
||||
if (spec.chute) appendChuteBand(svg, [{ points: outletFillLine(design) }], spec.chute, x, y);
|
||||
if (spec.chute)
|
||||
appendChuteBand(svg, [{ points: fordChuteLine(design, spec.chute) }], spec.chute, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -218,6 +219,29 @@ export function outletFillLine(design: CrossDesign): OffsetPoint[] {
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 물넘이 도수로 · 돌붙임 선(286-3) — 유출 성토사면 선을 노견에서 사면 따라 `length_m` 까지 자름 ·
|
||||
* 비거나 사면보다 길면 원지반까지. 횡단 띠 · 3D 띠가 같이 씀(그림 전용).
|
||||
*/
|
||||
export function fordChuteLine(design: CrossDesign, chute?: { length_m?: number }): OffsetPoint[] {
|
||||
const line = outletFillLine(design);
|
||||
let left = chute?.length_m ?? 0;
|
||||
if (!(left > 0)) return line;
|
||||
const out = line.slice(0, 1);
|
||||
for (let i = 1; i < line.length && left > 1e-9; i += 1) {
|
||||
const [a, b] = [line[i - 1], line[i]];
|
||||
const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation);
|
||||
if (step < 1e-9) continue;
|
||||
const t = Math.min(left / step, 1);
|
||||
out.push({
|
||||
offset: a.offset + (b.offset - a.offset) * t,
|
||||
elevation: a.elevation + (b.elevation - a.elevation) * t,
|
||||
});
|
||||
left -= step;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 물넘이 자리 노면 · 측구 설계선을 걷는다(66-2 · 66-3) — 노면은 파인 바닥(실선)과 옛 계획고(점선)가
|
||||
* 대신하고, 측구는 서지 않음. `from` = 설계선 그리기 앞의 자식 수(그 뒤에 붙은 선만 본다).
|
||||
|
||||
@@ -199,11 +199,12 @@ function sideSpec(
|
||||
}
|
||||
|
||||
/** 도수로 · 돌붙임(90-2) — 첫 상자(`{head}_{n}_*` · 물넘이 포장 = `ford_chute` · 136-2) 구조 · 두께
|
||||
* (cm → m · 빈 값 45 cm) · 구조를 안 고르면 null. 횡단이 유출 성토사면을 따라 띠로 그림. 파이썬 짝 `_chute`. */
|
||||
* (cm → m · 빈 값 45 cm) · 구조를 안 고르면 null. 횡단이 유출 성토사면을 따라 띠로 그림 · 길이
|
||||
* (`{head}_{n}_length_m` · 286-3) = 사면 따라 그릴 길이(그림 전용 · 비면 원지반까지). 파이썬 짝 `_chute`. */
|
||||
export function chute(
|
||||
values: Record<string, unknown>,
|
||||
head = "chute_add",
|
||||
): { structure: string; thickness_m: number } | null {
|
||||
): { structure: string; thickness_m: number; length_m?: number } | null {
|
||||
const pattern = new RegExp(`^${head}_(\\d+)_structure$`);
|
||||
const picked = Object.entries(values)
|
||||
.flatMap(([key, value]) => {
|
||||
@@ -215,7 +216,12 @@ export function chute(
|
||||
if (!picked.length) return null;
|
||||
const [n, structure] = picked[0];
|
||||
const cm = num(values[`${head}_${n}_thickness`], null);
|
||||
return { structure, thickness_m: round((cm && cm > 0 ? cm : CHUTE_THICKNESS_CM) / 100, 3) };
|
||||
const found = {
|
||||
structure,
|
||||
thickness_m: round((cm && cm > 0 ? cm : CHUTE_THICKNESS_CM) / 100, 3),
|
||||
};
|
||||
const length = num(values[`${head}_${n}_length_m`], null);
|
||||
return length && length > 0 ? { ...found, length_m: round(length, 3) } : found;
|
||||
}
|
||||
|
||||
/** 도수로 · 돌붙임이 있으면 세트에 `chute` 로 실음(90-2). */
|
||||
|
||||
@@ -42,7 +42,35 @@ const deck = (design) => ({
|
||||
right: ford.fordDeckElevationAt(design, SPEC, -3),
|
||||
});
|
||||
|
||||
// 286-3 도수로 · 돌붙임 길이 — 유출(좌 · 우가 높음) 성토사면 1:1.5(5 m 내려감 · 사면 길이 √(7.5²+5²) ≈ 9.01)
|
||||
const sets = loadTs(path.join(ROOT, "common_util", "common_util_culvert_sets.ts"));
|
||||
const slopeDesign = {
|
||||
design_elevation_m: 100,
|
||||
cross_slope_pct: 2,
|
||||
section_mode: "left_cut",
|
||||
road_edges: {
|
||||
left: { offset_m: 2.5, elevation_m: 100 },
|
||||
right: { offset_m: -2.5, elevation_m: 100 },
|
||||
},
|
||||
design_line: [
|
||||
{ offset_m: 10, elevation_m: 95 },
|
||||
{ offset_m: 2.5, elevation_m: 100 },
|
||||
{ offset_m: -2.5, elevation_m: 100 },
|
||||
{ offset_m: -6, elevation_m: 104 },
|
||||
],
|
||||
};
|
||||
const chuteOf = (extra) =>
|
||||
sets.chute(
|
||||
{ ford_chute_1_structure: "돌붙임", ford_chute_1_thickness: 30, ...extra },
|
||||
"ford_chute",
|
||||
);
|
||||
|
||||
const out = {
|
||||
chuteNoLength: chuteOf({}),
|
||||
chuteLength: chuteOf({ ford_chute_1_length_m: 3 }),
|
||||
lineNoLength: ford.fordChuteLine(slopeDesign, chuteOf({})),
|
||||
lineLength: ford.fordChuteLine(slopeDesign, chuteOf({ ford_chute_1_length_m: 3 })),
|
||||
lineTooLong: ford.fordChuteLine(slopeDesign, chuteOf({ ford_chute_1_length_m: 50 })),
|
||||
fordMarks: station.pipeStationMarks({ chainage_m: 230, facility: "ford_pavement" }),
|
||||
planted: station.plantedChainages(input.pipes, input.structures, input.types),
|
||||
deckLeftHigh: deck(designOf(105, 95, "right_cut")),
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
`resolve_extra_stations` 같은 값 · 라벨 「물넘이포장」 · B05 · B06 거름 걷음.
|
||||
- 286-2 물넘이 자리 = 측구 없음(서버 정본 · 화면 짝 · 거울 = `test_b06_cross_design_mirror`) ·
|
||||
노면 = 경사각도로 표고 낮은 쪽(설계선 양 끝 표고 · 같으면 단면유형).
|
||||
- 286-3 도수로/돌붙임 = 다른 구조물과 같은 칸 + 맨 아래 「길이 (m)」(`ford_chute_{n}_length_m`) ·
|
||||
비면 원지반까지 · 있으면 사면 따라 그 길이 · 서버 · TS 같은 값 · 집계 · B08 수량에 안 실림.
|
||||
|
||||
진짜 TS 는 `helper_286_1_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
|
||||
"""
|
||||
@@ -27,7 +29,10 @@ from B05_Profile.B05_Profile_Engine_Sections import resolve_extra_stations # no
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
||||
from B06_Section.B06_Section_Router_Design import ford_pavement_ranges, paved_at # noqa: E402
|
||||
from B06_Section.B06_Section_Engine_Culvert import _chute # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Router_Summary import _chutes # noqa: E402
|
||||
from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402
|
||||
from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows # noqa: E402
|
||||
|
||||
PIPES = [
|
||||
{"chainage_m": 95.0, "source": "user"},
|
||||
@@ -135,3 +140,64 @@ def test_저장_뒤_서버가_물넘이_측구를_맞춤():
|
||||
assert 'bool(design.get("ford_paved")) == ford_paved' in router
|
||||
refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts")
|
||||
assert "fordPaved: Boolean(section.ford_pavement)," in refresh
|
||||
|
||||
|
||||
CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30}
|
||||
|
||||
|
||||
def test_도수로_길이_서버_TS_같은_값(ts):
|
||||
assert (
|
||||
_chute(CHUTE, "ford_chute")
|
||||
== ts["chuteNoLength"]
|
||||
== {
|
||||
"structure": "돌붙임",
|
||||
"thickness_m": 0.3,
|
||||
}
|
||||
)
|
||||
long = {**CHUTE, "ford_chute_1_length_m": 3}
|
||||
assert _chute(long, "ford_chute") == ts["chuteLength"]
|
||||
assert ts["chuteLength"]["length_m"] == 3
|
||||
|
||||
|
||||
def test_도수로_길이_없음_원지반까지_있음_그_길이(ts):
|
||||
# 유출 = 좌(우가 표고 높음) · 노견(2.5, 100) → 원지반(10, 95)
|
||||
assert ts["lineNoLength"] == [
|
||||
{"offset": 2.5, "elevation": 100},
|
||||
{"offset": 10, "elevation": 95},
|
||||
]
|
||||
start, end = ts["lineLength"]
|
||||
assert start == {"offset": 2.5, "elevation": 100}
|
||||
assert (end["offset"] - 2.5) ** 2 + (100 - end["elevation"]) ** 2 == pytest.approx(9.0)
|
||||
assert end["offset"] == pytest.approx(2.5 + 3 * 7.5 / 9.0139, abs=1e-3)
|
||||
assert ts["lineTooLong"] == ts["lineNoLength"] # 사면보다 길면 원지반에서 멈춤
|
||||
|
||||
|
||||
def test_컨테이너_같은_칸_맨_아래_길이():
|
||||
spec = _read("A00_Common", "design", "design_structure_ford_pavement.ts")
|
||||
assert (
|
||||
'items: [...CHUTE_ITEMS, { id: "length_m", label: "길이 (m)", kind: "number", step: 0.1 }],'
|
||||
in spec
|
||||
)
|
||||
|
||||
|
||||
def test_길이는_집계_수량에_안_실림():
|
||||
options = {**CHUTE, "ford_chute_1_bond": "찰붙임", "ford_chute_1_area": 12}
|
||||
item = {"type_id": "ford_pavement", "options": options, "복사본": "pipe@50.0"}
|
||||
plain = container_rows(item, {"gfordpavechute1": "SS1"})
|
||||
long = container_rows(
|
||||
{**item, "options": {**options, "ford_chute_1_length_m": 7}}, {"gfordpavechute1": "SS1"}
|
||||
)
|
||||
# 집계 열이 읽는 칸(length_m · 쪽별 revet_length_m)은 그대로 — 길이는 안 읽히는 이름으로만
|
||||
for key in ("length_m", "inlet_revet_length_m", "outlet_revet_length_m", "area_m"):
|
||||
assert plain[0]["options"].get(key) == long[0]["options"].get(key)
|
||||
|
||||
class Sheet:
|
||||
def __init__(self) -> None:
|
||||
self.rows: list[tuple] = []
|
||||
|
||||
def add(self, at, key, sort, group, label, value, combos) -> None:
|
||||
self.rows.append((key, label, value))
|
||||
|
||||
sheet = Sheet()
|
||||
_chutes(sheet, 50.0, {**options, "ford_chute_1_length_m": 7}, {"gfordpavechute1": "SS1"}, {})
|
||||
assert sheet.rows == [("chute|찰붙임", "㎡", 12.0)] # 돌붙임 수량 = 면적 · 길이 안 탐
|
||||
|
||||
@@ -25,7 +25,9 @@ const load = (file) => {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
|
||||
// 같은 폴더 명세 import(`CHUTE_ITEMS` · 286-3)만 따라 옮김
|
||||
const near = (m) => (m.startsWith("./") ? load(path.join(path.dirname(file), `${m}.ts`)) : {});
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
const s = load(path.join(ROOT, "A00_Common/design/design_structure_spec.ts"));
|
||||
@@ -75,8 +77,9 @@ def test_저장_값은_도_와_인스턴스_키(out):
|
||||
"width": "6",
|
||||
"height": "0.3",
|
||||
} # 월류 폭 · 높이 = 옛 키 그대로 읽음(65-5)
|
||||
# 두께(cm · 90-2) — 빈 칸은 빈 값(새 상자만 기본 30)
|
||||
assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임", "thickness": ""}]}
|
||||
# 다른 구조물과 같은 칸(구조 · 형태 · 두께 · 면적) + 길이(286-3) — 빈 칸은 빈 값(새 상자만 기본값)
|
||||
chute = {"structure": "돌붙임", "bond": "", "thickness": "", "area": "", "length_m": ""}
|
||||
assert out["state"] == {"slope": [slope], "chute": [chute]}
|
||||
assert out["written"] == {
|
||||
"ford_slope_deg": 15,
|
||||
"ford_width_m": 6,
|
||||
|
||||
Reference in New Issue
Block a user