Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_8
This commit is contained in:
@@ -32,6 +32,7 @@ export const FORD_PAVEMENT_SPEC: StructureSpec = {
|
||||
prefix: "ford_chute",
|
||||
link: "gfordpavechute",
|
||||
max: 1,
|
||||
// 구조 · 두께(cm · 새 상자 30) — 배관 · 박스 · 세월교 `CHUTE_ITEMS` 와 같은 칸(90-2)
|
||||
items: [
|
||||
{
|
||||
id: "structure",
|
||||
@@ -42,6 +43,7 @@ export const FORD_PAVEMENT_SPEC: StructureSpec = {
|
||||
{ value: "돌붙임", label: "돌붙임" },
|
||||
],
|
||||
},
|
||||
{ id: "thickness", label: "두께 (cm)", kind: "number", step: 1, default: "30" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -110,6 +110,20 @@ export const SOIL_ADD: SpecContainer = {
|
||||
items: guardItems(),
|
||||
};
|
||||
|
||||
/** 도수로/돌붙임 칸 — 구조 · 두께(cm · 새 상자 30 · 90-2 사용자 확정) · 물넘이 포장(`ford_chute`)도 같은 칸. */
|
||||
export const CHUTE_ITEMS: SpecItem[] = [
|
||||
{
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
choices: [
|
||||
{ value: "도수로", label: "도수로" },
|
||||
{ value: "돌붙임", label: "돌붙임" },
|
||||
],
|
||||
},
|
||||
{ id: "thickness", label: "두께 (cm)", kind: "number", step: 1, default: "30" },
|
||||
];
|
||||
|
||||
/** 도수로/돌붙임(선택 · 하나만 · 54-48) — 옛 둘 이상은 보이되 [추가]만 숨김. */
|
||||
export const CHUTE_ADD: SpecContainer = {
|
||||
id: "chute_add",
|
||||
@@ -118,15 +132,5 @@ export const CHUTE_ADD: SpecContainer = {
|
||||
prefix: "chute_add",
|
||||
link: "gchuteadd",
|
||||
max: 1,
|
||||
items: [
|
||||
{
|
||||
id: "structure",
|
||||
label: "구조",
|
||||
kind: "select",
|
||||
choices: [
|
||||
{ value: "도수로", label: "도수로" },
|
||||
{ value: "돌붙임", label: "돌붙임" },
|
||||
],
|
||||
},
|
||||
],
|
||||
items: CHUTE_ITEMS,
|
||||
};
|
||||
|
||||
@@ -220,10 +220,12 @@ export interface CulvertSideSpec {
|
||||
face_side?: string | null;
|
||||
face_role?: string | null;
|
||||
face_slope_ratio?: number | string | null;
|
||||
/** 보호공(물받이) 길이 = 낙차고 × 2 (사방교본 교차 참조, 2026-08-19 사용자 확정). */
|
||||
apron_length_m?: number;
|
||||
/** 보호공 두께 1.0m 내외 (사방교본 교차 참조). */
|
||||
apron_thickness_m?: number;
|
||||
}
|
||||
|
||||
/** 도수로 · 돌붙임(90-2) — 유출 성토사면을 따라 사면 안쪽으로 두께만큼 띠 · 안에 돌(그림만 · 면적 무관). */
|
||||
export interface ChuteSpec {
|
||||
structure: string;
|
||||
thickness_m: number;
|
||||
}
|
||||
|
||||
/** 배수관 측점의 세트(배관·기슭막이·보호공) 제원 — 횡단 카드 오버레이 입력. */
|
||||
@@ -249,6 +251,7 @@ export interface CulvertSet {
|
||||
foundation?: string | null;
|
||||
/** 세월교 · 박스 흙막이(73-4) — 자리 기준 본체. 있으면 벽 자동 자리 = 관(세월교) · 본체(박스) 끝. */
|
||||
body?: "box" | "ford";
|
||||
chute?: ChuteSpec;
|
||||
}
|
||||
|
||||
/** 세월교 날개벽 한쪽 — 횡단면엔 안 보이고 바닥판 연장량만 넘긴다. */
|
||||
@@ -280,6 +283,7 @@ export interface FordSet {
|
||||
wing_out: FordWingSpec;
|
||||
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
|
||||
defaulted?: string[];
|
||||
chute?: ChuteSpec;
|
||||
}
|
||||
|
||||
/** BOX암거 측점의 세트 제원 — 상판·내공(유로)·저판 + 날개벽 투영 연장. */
|
||||
@@ -300,6 +304,7 @@ export interface BoxSet {
|
||||
wing_out: FordWingSpec;
|
||||
/** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */
|
||||
defaulted?: string[];
|
||||
chute?: ChuteSpec;
|
||||
}
|
||||
|
||||
export interface CrossSection extends SectionStation {
|
||||
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -63,12 +64,8 @@ _SPAN_LINKED_TYPES: frozenset[str] = frozenset({"ford_pavement"})
|
||||
# ⚠ 교차 참조 ① 관 위 최소 토피(m). 임도 배수관 토피 규정이 지식DB에 없어 별표2
|
||||
# 교량·암거 "복토 시 흙 두께 50㎝ 이상"을 끌어왔다. B05 계획고 하향 차단 기준이다.
|
||||
MIN_PIPE_COVER_M = 0.5
|
||||
# ⚠ 교차 참조 ② 보호공(물받이) 길이 = 낙차고(기슭막이 높이) × 배수.
|
||||
# 사방기술교본 바닥막이 물받이 "길이 = 낙차고의 2~3배" 중 2배 채택.
|
||||
APRON_LENGTH_FACTOR = 2.0
|
||||
# 보호공 두께(m) — 사용자 확정 0.45 (2026-08-20, 실무 돌붙임 L3=45와 정합).
|
||||
# 사방교본 물받이 "두께 1m 내외"는 바닥막이용 교차 참조라 채택하지 않았다.
|
||||
APRON_THICKNESS_M = 0.45
|
||||
# 도수로 · 돌붙임 두께 빈 값(cm) — 사용자 확정 30 cm(10-8 · 90-2).
|
||||
CHUTE_THICKNESS_CM = 30.0
|
||||
|
||||
# 기슭막이 전면 기울기(1:n). 돌쌓기 전면 1:0.3(교본 7-3) = 사방교본 계획비탈
|
||||
# 1:0.3~0.5의 급한 쪽. 지식DB 근거가 있는 값이라 교차 참조 표시가 붙지 않는다.
|
||||
@@ -226,15 +223,36 @@ def _side_spec(
|
||||
"face_slope": REVET_FACE_SLOPE,
|
||||
}
|
||||
)
|
||||
# 보호공은 기슭막이 바닥의 세굴 방지 구조 — 낙차고(기슭막이 높이)에 종속한다.
|
||||
if height is not None and height > 0:
|
||||
spec["apron_length_m"] = round(height * APRON_LENGTH_FACTOR, 3)
|
||||
spec["apron_thickness_m"] = APRON_THICKNESS_M
|
||||
return spec
|
||||
|
||||
|
||||
def _chute(values: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""도수로 · 돌붙임(90-2) — 첫 상자(`chute_add_{n}_*`) 구조 · 두께(cm → m · 빈 값 30 cm) ·
|
||||
구조를 안 고르면 None. 횡단이 유출 성토사면을 따라 띠로 그림. TS 짝 `chute`."""
|
||||
head = re.compile(r"^chute_add_(\d+)_structure$")
|
||||
picked = sorted(
|
||||
(int(hit.group(1)), str(value).strip())
|
||||
for key, value in values.items()
|
||||
if (hit := head.match(key)) and str(value or "").strip()
|
||||
)
|
||||
if not picked:
|
||||
return None
|
||||
n, structure = picked[0]
|
||||
cm = _number(values.get(f"chute_add_{n}_thickness"), None)
|
||||
cm = cm if cm and cm > 0 else CHUTE_THICKNESS_CM
|
||||
return {"structure": structure, "thickness_m": round(cm / 100.0, 3)}
|
||||
|
||||
|
||||
def _with_chute(spec: dict[str, Any], values: dict[str, Any]) -> dict[str, Any]:
|
||||
"""도수로 · 돌붙임이 있으면 세트에 `chute` 로 실음(90-2)."""
|
||||
chute = _chute(values)
|
||||
if chute:
|
||||
spec["chute"] = chute
|
||||
return spec
|
||||
|
||||
|
||||
def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""관 1개소의 세트 제원(관 + 유입·유출 기슭막이 + 보호공)."""
|
||||
"""관 1개소의 세트 제원(관 + 유입·유출 기슭막이 + 도수로 · 돌붙임)."""
|
||||
defaults = _registry_pipe_defaults()
|
||||
values = dict(options or {})
|
||||
diameter_mm = _number(
|
||||
@@ -242,7 +260,7 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
)
|
||||
kind = values.get("pipe_kind")
|
||||
diameter_m = round((diameter_mm or 1000.0) / 1000.0, 3)
|
||||
return {
|
||||
spec = {
|
||||
"type": "pipe",
|
||||
"pipe_kind": str(kind) if kind else None,
|
||||
"diameter_m": diameter_m,
|
||||
@@ -253,6 +271,7 @@ def _culvert_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"inlet": _side_spec(values, defaults, "inlet", diameter_m),
|
||||
"outlet": _side_spec(values, defaults, "outlet", diameter_m),
|
||||
}
|
||||
return _with_chute(spec, values)
|
||||
|
||||
|
||||
def _revet_side(values: dict[str, Any], role: str) -> dict[str, Any]:
|
||||
@@ -286,9 +305,6 @@ def _revet_side(values: dict[str, Any], role: str) -> dict[str, Any]:
|
||||
"face_role": values.get("face_role"),
|
||||
"face_slope_ratio": values.get("face_slope_ratio"),
|
||||
}
|
||||
if height is not None and height > 0:
|
||||
spec["apron_length_m"] = round(height * APRON_LENGTH_FACTOR, 3)
|
||||
spec["apron_thickness_m"] = APRON_THICKNESS_M
|
||||
return spec
|
||||
|
||||
|
||||
@@ -334,7 +350,7 @@ def _guard_set(values: dict[str, Any], body: str, diameter_m: float) -> dict[str
|
||||
sides[role]["revet_berm_m"] = _number(values.get(f"{role}_revet_berm_m"), None)
|
||||
if not any(side["structure"] for side in sides.values()):
|
||||
return None
|
||||
return {
|
||||
guard = {
|
||||
"type": "pipe",
|
||||
"hidden_pipe": True,
|
||||
"body": body,
|
||||
@@ -346,6 +362,7 @@ def _guard_set(values: dict[str, Any], body: str, diameter_m: float) -> dict[str
|
||||
"min_cover_m": 0.0,
|
||||
**sides,
|
||||
}
|
||||
return _with_chute(guard, values) # 흙막이 쪽 유출 띠는 흙막이 성토선을 따름(90-2)
|
||||
|
||||
|
||||
def _wing_spec(values: dict[str, Any], defaults: dict[str, Any], side: str) -> dict[str, Any]:
|
||||
@@ -459,7 +476,7 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
guard = _guard_set(values, "ford", spec["diameter_m"])
|
||||
if guard:
|
||||
spec["guard"] = guard
|
||||
return spec
|
||||
return _with_chute(spec, values)
|
||||
|
||||
|
||||
def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
@@ -536,7 +553,7 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
guard = _guard_set(values, "box", box_body_height(spec))
|
||||
if guard:
|
||||
spec["guard"] = guard
|
||||
return spec
|
||||
return _with_chute(spec, values)
|
||||
|
||||
|
||||
def box_body_height(box: dict[str, Any]) -> float:
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import type { BoxLayout, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
export {
|
||||
computeBoxLayout,
|
||||
@@ -127,6 +128,17 @@ export function appendBoxOverlay(
|
||||
polyline.append(title);
|
||||
layer.append(polyline);
|
||||
}
|
||||
// 도수로 · 돌붙임 띠(90-2) — 유출 끝 ~ 원지반 성토부선 따라(흙막이 쪽은 배관 오버레이가 그림)
|
||||
if (box.chute)
|
||||
appendChuteBand(
|
||||
layer,
|
||||
layout.sides
|
||||
.filter((side) => side.wingRole === "outlet")
|
||||
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut").map((s) => s.points)),
|
||||
box.chute,
|
||||
x,
|
||||
toDisplayY,
|
||||
);
|
||||
// ⑥ 끝 바깥 절 · 성토선 — 세월교 측벽과 같은 체계(79-7). 흙막이 쪽은 기하가 걷어 둠.
|
||||
for (const side of layout.sides) {
|
||||
const mark = side.role === "left" ? "좌" : "우";
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
WallLayout,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Geom";
|
||||
import { appendWallTrench, foundationChoice } from "./B06_Section_UI_Cross_Excavation";
|
||||
import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
import { appendChuteBand, appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
// 기하 계산 진입점과 공개 상수·타입은 여기서 재수출한다 — B05(최소 토피)와 횡단 뷰가
|
||||
// 이 모듈 경로로 이미 참조하고 있어 분리 후에도 import 경로를 바꾸지 않는다.
|
||||
@@ -101,6 +101,16 @@ export function appendCulvertOverlay(
|
||||
layer.append(line);
|
||||
};
|
||||
const drawOutletFill = (): void => {
|
||||
// 도수로 · 돌붙임 띠(90-2) — 유출 끝 ~ 원지반 성토 조각(단 사이 포함) · 연동 카드엔 안 그림
|
||||
// (B07 은 `linked` 없이 관 숨김으로만 알림 — 관 숨긴 배관 = 연동 카드)
|
||||
if (culvert.chute && !linked && (!hidePipe || culvert.body))
|
||||
appendChuteBand(
|
||||
layer,
|
||||
layout.outletFill.segments.filter((s) => s.kind !== "cut").map((s) => s.points),
|
||||
culvert.chute,
|
||||
x,
|
||||
toDisplayY,
|
||||
);
|
||||
for (const segment of layout.outletFill.segments) {
|
||||
// 유출부가 지면에 닿은 뒤의 절토선은 그리지 않는다(2026-08-24 사용자) — 벽이
|
||||
// 이미 원지반에 묻힌 구간이라 도면에 더 보탤 정보가 없다. 면적·수량 계산은
|
||||
|
||||
@@ -20,9 +20,6 @@ export const REVET_THICKNESS_M = 0.45;
|
||||
* 못 가른 자리만 이 값. 돌쌓기 벽은 `B06_Section_UI_Cross_Lean.leanOf`(품셈 13-4-4 [주]⑪ 표준경사). */
|
||||
export const REVET_LEAN_RATIO = 0.3;
|
||||
|
||||
/** 돌붙임 두께(m) — 실무 돌붙임 L3=45(울진 1공구 수량집계표). */
|
||||
export const PITCHING_THICKNESS_M = 0.45;
|
||||
|
||||
/** 사다리꼴(배면부) 상단 폭(m) — 띠 두께의 절반(2026-08-20 사용자 확정). */
|
||||
export const REVET_TRAP_TOP_M = REVET_THICKNESS_M / 2;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
|
||||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
|
||||
export {
|
||||
computeFordLayout,
|
||||
@@ -127,6 +128,17 @@ export function appendFordOverlay(
|
||||
layer.append(pipeOuter, pipeInner);
|
||||
const bodyShapes = [slab, pipeOuter, pipeInner];
|
||||
|
||||
// 도수로 · 돌붙임 띠(90-2) — 유출 측벽 끝 ~ 원지반 성토부선 따라(흙막이 쪽은 배관 오버레이가 그림)
|
||||
if (ford.chute)
|
||||
appendChuteBand(
|
||||
layer,
|
||||
layout.sides
|
||||
.filter((side) => side.role === "outlet")
|
||||
.flatMap((side) => side.fillSegments.filter((s) => s.kind !== "cut").map((s) => s.points)),
|
||||
ford.chute,
|
||||
x,
|
||||
toDisplayY,
|
||||
);
|
||||
// ④ 계류측 마감선 — 구조물이 원지반 위면 성토부선, 안이면 절토선(집수정과 같은 체계).
|
||||
for (const side of layout.sides) {
|
||||
const role = side.role === "inlet" ? "유입" : "유출";
|
||||
|
||||
@@ -44,7 +44,7 @@ export function hatchOfForm(form: string | null | undefined): WallHatch {
|
||||
}
|
||||
|
||||
/** 띠 안의 국소 좌표 → 화면 px. u = 바닥(0)~상단(1), v = 배면(-0.5)~전면(+0.5). */
|
||||
interface BandFrame {
|
||||
export interface BandFrame {
|
||||
point: (u: number, v: number) => [number, number];
|
||||
/** 띠 축 기울기(도) — 사각형·원의 세로축을 벽 경사에 맞춘다. */
|
||||
leanDegrees: number;
|
||||
@@ -64,7 +64,7 @@ function line(a: [number, number], b: [number, number]): SVGLineElement {
|
||||
}
|
||||
|
||||
/** 띠 중심선 위 한 칸 — 회전 사각형(돌·격자 칸). */
|
||||
function cell(
|
||||
export function cell(
|
||||
frame: BandFrame,
|
||||
u: number,
|
||||
v: number,
|
||||
@@ -87,14 +87,20 @@ function cell(
|
||||
}
|
||||
|
||||
/** 켜 수 — 켜 높이가 목표치에 가장 가깝게, 최소 2켜. */
|
||||
function courses(spanM: number, targetM: number): number {
|
||||
export function courses(spanM: number, targetM: number): number {
|
||||
return Math.max(2, Math.round(spanM / targetM));
|
||||
}
|
||||
|
||||
function drawStones(group: SVGElement, frame: BandFrame, wet: boolean): void {
|
||||
/** 돌 한 줄 — `thicknessM` = 띠 두께(벽 = 흙막이 띠 · 도수로/돌붙임 = 그 두께 · 90-2). */
|
||||
export function drawStones(
|
||||
group: SVGElement,
|
||||
frame: BandFrame,
|
||||
wet: boolean,
|
||||
thicknessM = REVET_THICKNESS_M,
|
||||
): void {
|
||||
const count = courses(frame.spanM, 0.45);
|
||||
const courseM = frame.spanM / count;
|
||||
const widthM = REVET_THICKNESS_M * (wet ? 0.76 : 0.9);
|
||||
const widthM = thicknessM * (wet ? 0.76 : 0.9);
|
||||
const heightM = courseM * (wet ? 0.7 : 0.86);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
group.append(cell(frame, (i + 0.5) / count, 0, widthM, heightM, 0.3));
|
||||
@@ -308,3 +314,89 @@ export function appendWallHatch(
|
||||
// 옹벽만 — 흙막이(찰쌓기 · 콘크리트 등)에는 없음(73-2).
|
||||
if (wall.retaining) drawDrains(group, wall, x, toDisplayY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 도수로 · 돌붙임 띠(90-2) — 유출 성토사면 선(윗면 = 사면선)을 사면 안쪽(아래)으로 두께만큼 오프셋한
|
||||
* 띠 · 안에 돌(흙막이 돌과 같은 그리기 · 두께 인자). 선마다 한 띠 · 그림만(면적 · 터파기 무관).
|
||||
* 도수로 · 돌붙임 형상 같음 · 배관 · 박스 · 세월교 오버레이가 부름(B07 은 오버레이를 따라옴).
|
||||
*/
|
||||
export function appendChuteBand(
|
||||
layer: SVGElement,
|
||||
lines: OffsetPoint[][],
|
||||
chute: { structure: string; thickness_m: number },
|
||||
x: (offset: number) => number,
|
||||
toDisplayY: (elevation: number) => number,
|
||||
): void {
|
||||
const thickness = chute.thickness_m;
|
||||
if (!(thickness > 0)) return;
|
||||
const pixelsPerMeter = Math.abs(x(1) - x(0)) || 1;
|
||||
const px = (p: OffsetPoint): [number, number] => [x(p.offset), toDisplayY(p.elevation)];
|
||||
for (const raw of lines) {
|
||||
const top = raw.filter(
|
||||
(p, i) =>
|
||||
i === 0 ||
|
||||
Math.hypot(p.offset - raw[i - 1].offset, p.elevation - raw[i - 1].elevation) > 1e-6,
|
||||
);
|
||||
if (top.length < 2) continue;
|
||||
// 변마다 아래쪽(사면 안쪽) 단위 법선
|
||||
const normals = top.slice(1).map((p, i) => {
|
||||
const dx = p.offset - top[i].offset;
|
||||
const dz = p.elevation - top[i].elevation;
|
||||
const length = Math.hypot(dx, dz);
|
||||
const sign = dx / length > 0 ? -1 : 1;
|
||||
return { offset: (sign * -dz) / length, elevation: (sign * dx) / length };
|
||||
});
|
||||
// 꺾임은 두 오프셋 선의 교점(마이터 · 너무 뾰족하면 두 배까지)
|
||||
const bottom = top.map((p, i) => {
|
||||
const a = normals[Math.max(i - 1, 0)];
|
||||
const b = normals[Math.min(i, normals.length - 1)];
|
||||
const sum = { offset: a.offset + b.offset, elevation: a.elevation + b.elevation };
|
||||
const k = thickness / Math.max(sum.offset * a.offset + sum.elevation * a.elevation, 0.5);
|
||||
return { offset: p.offset + sum.offset * k, elevation: p.elevation + sum.elevation * k };
|
||||
});
|
||||
const outline = [...top, ...bottom.reverse()];
|
||||
const band = document.createElementNS(SVG_NS, "polygon");
|
||||
band.setAttribute("points", outline.map((p) => px(p).join(",")).join(" "));
|
||||
band.setAttribute("class", "b06-chart__culvert-pitching");
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = `유출부 ${chute.structure} — 두께 ${thickness.toFixed(2)}m · 성토사면 따라 원지반까지`;
|
||||
band.append(title);
|
||||
clipSeq += 1;
|
||||
const clipId = `b06-chute-clip-${clipSeq}`;
|
||||
const clip = document.createElementNS(SVG_NS, "clipPath");
|
||||
clip.setAttribute("id", clipId);
|
||||
const clipShape = document.createElementNS(SVG_NS, "polygon");
|
||||
clipShape.setAttribute("points", band.getAttribute("points") ?? "");
|
||||
clip.append(clipShape);
|
||||
const group = document.createElementNS(SVG_NS, "g");
|
||||
group.setAttribute("clip-path", `url(#${clipId})`);
|
||||
top.slice(1).forEach((end, i) => {
|
||||
const start = top[i];
|
||||
const normal = normals[i];
|
||||
const [ax, ay] = px(start);
|
||||
const [bx, by] = px(end);
|
||||
drawStones(
|
||||
group,
|
||||
{
|
||||
spanM: Math.hypot(end.offset - start.offset, end.elevation - start.elevation),
|
||||
pixelsPerMeter,
|
||||
point: (u, v) =>
|
||||
px({
|
||||
offset:
|
||||
start.offset +
|
||||
(end.offset - start.offset) * u +
|
||||
normal.offset * thickness * (v + 0.5),
|
||||
elevation:
|
||||
start.elevation +
|
||||
(end.elevation - start.elevation) * u +
|
||||
normal.elevation * thickness * (v + 0.5),
|
||||
}),
|
||||
leanDegrees: (Math.atan2(bx - ax, -(by - ay)) * 180) / Math.PI,
|
||||
},
|
||||
false,
|
||||
thickness,
|
||||
);
|
||||
});
|
||||
layer.append(band, clip, group);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
stroke-width: 2.2;
|
||||
}
|
||||
|
||||
/* 유출부 돌붙임 — 성토사면을 따라 원지반까지 덮는 돌붙임 띠(실무 도면의 돌 연쇄) */
|
||||
/* 유출부 도수로 · 돌붙임 띠(90-2) — 성토사면 안쪽으로 두께만큼 · 안에 돌(`appendChuteBand`) */
|
||||
.b06-chart__culvert-pitching {
|
||||
fill: color-mix(in srgb, #3fa34d 18%, transparent);
|
||||
stroke: #3fa34d;
|
||||
|
||||
@@ -37,8 +37,7 @@ const SECTION_KEYS: Record<string, string> = {
|
||||
const SPAN_LINKED_TYPES = new Set(["ford_pavement"]);
|
||||
|
||||
export const MIN_PIPE_COVER_M = 0.5;
|
||||
export const APRON_LENGTH_FACTOR = 2.0;
|
||||
export const APRON_THICKNESS_M = 0.45;
|
||||
export const CHUTE_THICKNESS_CM = 30.0;
|
||||
export const REVET_FACE_SLOPE = 0.3;
|
||||
export const INLET_STRUCTURE_BASIN = "집수정";
|
||||
export const FORD_SLAB_THICKNESS_M = 0.3;
|
||||
@@ -124,15 +123,35 @@ function sideSpec(
|
||||
spec.revet_before_m = before;
|
||||
spec.revet_after_m = after;
|
||||
spec.face_slope = REVET_FACE_SLOPE;
|
||||
// 보호공은 기슭막이 바닥의 세굴 방지 구조 — 낙차고(기슭막이 높이)에 종속한다.
|
||||
if (height !== null && height > 0) {
|
||||
spec.apron_length_m = round(height * APRON_LENGTH_FACTOR, 3);
|
||||
spec.apron_thickness_m = APRON_THICKNESS_M;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
/** 관 1개소의 세트 제원(관 + 유입·유출 기슭막이 + 보호공). */
|
||||
/** 도수로 · 돌붙임(90-2) — 첫 상자(`chute_add_{n}_*`) 구조 · 두께(cm → m · 빈 값 30 cm) · 구조를 안
|
||||
* 고르면 null. 횡단이 유출 성토사면을 따라 띠로 그림. 파이썬 짝 `_chute`. */
|
||||
export function chute(
|
||||
values: Record<string, unknown>,
|
||||
): { structure: string; thickness_m: number } | null {
|
||||
const picked = Object.entries(values)
|
||||
.flatMap(([key, value]) => {
|
||||
const hit = /^chute_add_(\d+)_structure$/.exec(key);
|
||||
const structure = String(value ?? "").trim();
|
||||
return hit && structure ? [[Number(hit[1]), structure] as const] : [];
|
||||
})
|
||||
.sort((a, b) => a[0] - b[0]);
|
||||
if (!picked.length) return null;
|
||||
const [n, structure] = picked[0];
|
||||
const cm = num(values[`chute_add_${n}_thickness`], null);
|
||||
return { structure, thickness_m: round((cm && cm > 0 ? cm : CHUTE_THICKNESS_CM) / 100, 3) };
|
||||
}
|
||||
|
||||
/** 도수로 · 돌붙임이 있으면 세트에 `chute` 로 실음(90-2). */
|
||||
function withChute(spec: CulvertSetSpec, values: Record<string, unknown>): CulvertSetSpec {
|
||||
const found = chute(values);
|
||||
if (found) spec.chute = found;
|
||||
return spec;
|
||||
}
|
||||
|
||||
/** 관 1개소의 세트 제원(관 + 유입·유출 기슭막이 + 도수로 · 돌붙임). */
|
||||
function culvertSet(
|
||||
options: Record<string, unknown>,
|
||||
registry: CulvertRegistryDefaults,
|
||||
@@ -140,18 +159,21 @@ function culvertSet(
|
||||
const defaults = registry.pipe ?? {};
|
||||
const diameterMm = num(options.pipe_diameter_mm, num(defaults.pipe_diameter_mm, 1000.0));
|
||||
const diameterM = pipeDiameterM(diameterMm);
|
||||
return {
|
||||
type: "pipe",
|
||||
pipe_kind: text(options.pipe_kind),
|
||||
diameter_m: diameterM,
|
||||
min_cover_m: MIN_PIPE_COVER_M,
|
||||
// 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로
|
||||
// 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이
|
||||
// 확인되면 그때 나눌 것.
|
||||
foundation: text(options.revet_foundation),
|
||||
inlet: sideSpec(options, defaults, "inlet", diameterM),
|
||||
outlet: sideSpec(options, defaults, "outlet", diameterM),
|
||||
};
|
||||
return withChute(
|
||||
{
|
||||
type: "pipe",
|
||||
pipe_kind: text(options.pipe_kind),
|
||||
diameter_m: diameterM,
|
||||
min_cover_m: MIN_PIPE_COVER_M,
|
||||
// 관 기슭막이 기초 축 — **유입·유출 한 칸으로** 받는다. 한 공사에서 한쪽만 기초유로
|
||||
// 하는 일은 드물어 폼을 둘로 늘리지 않았다(2026-09-09). 실무가 갈라 쓰는 것이
|
||||
// 확인되면 그때 나눌 것.
|
||||
foundation: text(options.revet_foundation),
|
||||
inlet: sideSpec(options, defaults, "inlet", diameterM),
|
||||
outlet: sideSpec(options, defaults, "outlet", diameterM),
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,10 +197,6 @@ function revetSide(values: Record<string, unknown>, role: string): CulvertSetSpe
|
||||
face_role: values.face_role ?? null,
|
||||
face_slope_ratio: values.face_slope_ratio ?? null,
|
||||
};
|
||||
if (height !== null && height > 0) {
|
||||
spec.apron_length_m = round(height * APRON_LENGTH_FACTOR, 3);
|
||||
spec.apron_thickness_m = APRON_THICKNESS_M;
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
@@ -222,19 +240,23 @@ export function guardSet(
|
||||
if (inlet.structure) inlet.revet_berm_m = num(values.inlet_revet_berm_m, null);
|
||||
const outlet = side("outlet", outletKey);
|
||||
if (!inlet.structure && !outlet.structure) return null;
|
||||
return {
|
||||
type: "pipe",
|
||||
hidden_pipe: true,
|
||||
body,
|
||||
side: "양쪽",
|
||||
tiers: 1,
|
||||
foundation: null,
|
||||
pipe_kind: null,
|
||||
diameter_m: diameterM,
|
||||
min_cover_m: 0.0,
|
||||
inlet,
|
||||
outlet,
|
||||
};
|
||||
// 흙막이 쪽 유출 띠는 흙막이 성토선을 따름(90-2)
|
||||
return withChute(
|
||||
{
|
||||
type: "pipe",
|
||||
hidden_pipe: true,
|
||||
body,
|
||||
side: "양쪽",
|
||||
tiers: 1,
|
||||
foundation: null,
|
||||
pipe_kind: null,
|
||||
diameter_m: diameterM,
|
||||
min_cover_m: 0.0,
|
||||
inlet,
|
||||
outlet,
|
||||
},
|
||||
values,
|
||||
);
|
||||
}
|
||||
|
||||
/** 박스 본체 전체 높이(상판 + 내공 + 저판) — 흙막이 세트의 관경 자리(파이썬 `wall_body_height`). */
|
||||
@@ -373,7 +395,7 @@ function fordSet(
|
||||
...wingDefaulted(values, wingOut, "out"),
|
||||
],
|
||||
};
|
||||
return withGuard(spec, guardSet(values, "ford", Number(spec.diameter_m)));
|
||||
return withChute(withGuard(spec, guardSet(values, "ford", Number(spec.diameter_m))), values);
|
||||
}
|
||||
|
||||
/** 물넘이포장 1개소 — 구조물이 아니라 **파인 노면**이라 형상이 다르다. */
|
||||
@@ -441,7 +463,7 @@ function boxSet(
|
||||
...wingDefaulted(values, wingOut, "out"),
|
||||
],
|
||||
};
|
||||
return withGuard(spec, guardSet(values, "box", boxBodyHeight(spec)));
|
||||
return withChute(withGuard(spec, guardSet(values, "box", boxBodyHeight(spec))), values);
|
||||
}
|
||||
|
||||
/** 관 지점 목록 → 누가거리(소수 2자리)별 세트 제원. */
|
||||
|
||||
@@ -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