fix(B06 맹암거): 371-3 노면 = 표준 경사 · 암거 경사 = 형상 하단 경계(높은 쪽 노면 끝 − 높이에서 내림)
- 암거 경사가 노면으로 가던 길 끊음(tilt_ranges None ↔ facilityRanges null · Cross_Refresh · design_pipeline_cross) · 측구 없음 · 낮은 쪽 한 경사 그대로 - underdrainOutline 하단 = 높은 쪽 노면 끝 z − H + 암거 경사 · 절토 교점 · 성토 교점 · 돌 띠 따라감 · 경사 비면 옛 동작 - 저장분은 tilt_pct 달라 서버가 다시 셈 · B06 · B07 같은 함수(묶음 다시 지음) - 시험 test_288 하단 경계 · 349_0n 맹암거 자리 경사 비움 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
@@ -66,7 +66,7 @@ function carried(section: CrossSection): Record<string, unknown> {
|
||||
: null,
|
||||
paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement),
|
||||
tilted: Boolean(section.ford_pavement || section.underdrain),
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null,
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
|
||||
surfaceDropM: drop > 0 ? drop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
paved: pavedBySpecs(section),
|
||||
// 물넘이 자리 = 측구 없음 · 지대 낮은 쪽 한 경사(286-2 · 287-4 · 서버 `tilt_ranges` 짝 = 얹힌 제원)
|
||||
tilted: Boolean(section.ford_pavement || section.underdrain),
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null,
|
||||
tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3)
|
||||
standard,
|
||||
berm: bermAt(section.chainage_m, design),
|
||||
rockBoundaryOffsetM,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""구조물 자리 기울기 — 물넘이포장 · 맹암거 측점의 노면(287-4 · 288-1).
|
||||
|
||||
그 자리 횡단 설계 = 측구 없음 · 노면 = 한 경사로 지대 낮은 쪽(`compute_cross_design(tilted=…,
|
||||
tilt_pct=…)`). 경사 = 물넘이 경사각도(%) · 맹암거 암거 경사(%) · 비면 표준 횡단경사.
|
||||
tilt_pct=…)`). 경사 = 물넘이 경사각도(%) · 맹암거 = 표준 횡단경사(암거 경사는 형상 하단 · 371-3).
|
||||
· 물넘이포장 = 관 지점 정본(pipe_points) 범위(기준 ± 월류 폭/2).
|
||||
· 맹암거 = 구조물 정본(structures.json) A군 점형 · 기준 측점 한 곳 — 횡단 dict 에
|
||||
`underdrain`(높이 · 경사)을 얹어 횡단도가 돌 무늬 형상을 그림(TS 짝
|
||||
@@ -78,7 +78,8 @@ def tilt_ranges(project_root: Path | None) -> list[TiltRange]:
|
||||
half = float(spec.get("span_m") or 0.0) / 2.0
|
||||
out.append((at - half, at + half, spec.get("slope_pct")))
|
||||
for drain in load_underdrains(project_root):
|
||||
out.append((drain["chainage_m"], drain["chainage_m"], drain["slope_pct"]))
|
||||
# 노면 = 표준 경사 · 암거 경사는 맹암거 하단 경계만(371-3 · TS `facilityRanges` 짝)
|
||||
out.append((drain["chainage_m"], drain["chainage_m"], None))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* B06_Section_UI_Cross_Underdrain.ts
|
||||
* 맹암거 횡단 형상(288-1 · 요구 183-3).
|
||||
*
|
||||
* · 노면 = 설계 노면 — 서버 · TS 짝이 암거 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음.
|
||||
* · 옵셋선 = 노면을 암거 높이만큼 아래로 같은 각도.
|
||||
* · 노면 = 설계 노면 — 서버 · TS 짝이 표준 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음.
|
||||
* · 옵셋선(하단 경계) = 높은 쪽 노면 끝에서 암거 높이 아래 · 암거 경사로 낮은 쪽으로 내림(371-3) ·
|
||||
* 경사가 비면 노면과 같은 각도.
|
||||
* · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남.
|
||||
* · 양측절토(낮은 쪽 늘인 노면 `tilt_reach`) = 늘인 노면이 원지반과 만나는 점에서 수직으로 끊음(356-1).
|
||||
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(306-1) — 켜마다 반 칸 엇갈리고 이웃과 겹친 돌 ·
|
||||
@@ -42,16 +43,18 @@ export function drawCrossedStones(group: SVGElement, frame: BandFrame, heightM:
|
||||
|
||||
type Edge = { offset_m: number; elevation_m: number };
|
||||
|
||||
/** 하단 경계(옵셋선) — 표고 = `z0 + grade · (o − o0)`. */
|
||||
type Bottom = { o0: number; z0: number; grade: number };
|
||||
|
||||
/** 한쪽 둘레(노면 끝 → 옵셋선과 만나는 자리) — `outward` = 바깥 부호(+1 좌 · −1 우). */
|
||||
function sidePath(
|
||||
edge: Edge,
|
||||
outward: 1 | -1,
|
||||
line: readonly Edge[],
|
||||
roadGrade: number,
|
||||
heightM: number,
|
||||
bottom: Bottom,
|
||||
): OffsetPoint[] {
|
||||
const start = { offset: edge.offset_m, elevation: edge.elevation_m };
|
||||
const offZ = (o: number) => edge.elevation_m + roadGrade * (o - edge.offset_m) - heightM;
|
||||
const offZ = (o: number) => bottom.z0 + bottom.grade * (o - bottom.o0);
|
||||
const outside = line
|
||||
.filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6)
|
||||
.sort((a, b) => (a.offset_m - b.offset_m) * outward);
|
||||
@@ -60,7 +63,8 @@ function sidePath(
|
||||
if (first.elevation_m >= edge.elevation_m) {
|
||||
// 절토 — 사면을 안쪽 아래로 늘여 옵셋선과 만남
|
||||
const k = (first.elevation_m - edge.elevation_m) / (first.offset_m - edge.offset_m);
|
||||
const d = Math.abs(k - roadGrade) < 1e-9 ? 0 : -heightM / (k - roadGrade);
|
||||
const g = bottom.grade;
|
||||
const d = Math.abs(k - g) < 1e-9 ? 0 : (offZ(edge.offset_m) - edge.elevation_m) / (k - g);
|
||||
const offset = edge.offset_m + d;
|
||||
return [start, { offset, elevation: offZ(offset) }];
|
||||
}
|
||||
@@ -83,25 +87,38 @@ function sidePath(
|
||||
return path;
|
||||
}
|
||||
|
||||
/** 하단 경계 — 높은 쪽(상류) 노면 끝 − 높이에서 암거 경사로 낮은 쪽으로 내림(371-3) · 경사가 비면 노면과 평행. */
|
||||
function bottomOf(left: Edge, right: Edge, heightM: number, slopePct: number | null): Bottom {
|
||||
const roadGrade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m);
|
||||
const high = left.elevation_m >= right.elevation_m ? left : right;
|
||||
const low = high === left ? right : left;
|
||||
const grade =
|
||||
slopePct && slopePct > 0
|
||||
? (Math.sign(low.offset_m - high.offset_m) * -slopePct) / 100
|
||||
: roadGrade;
|
||||
return { o0: high.offset_m, z0: high.elevation_m - heightM, grade };
|
||||
}
|
||||
|
||||
/** 맹암거 형상 둘레 — 좌 노면 끝 → 우 노면 끝 → 우 둘레 → 옵셋선 → 좌 둘레(위로). 높이 없으면 null. */
|
||||
export function underdrainOutline(
|
||||
design: CrossDesign,
|
||||
heightM: number | null,
|
||||
slopePct: number | null = null,
|
||||
): OffsetPoint[] | null {
|
||||
const edges = design.road_edges;
|
||||
if (!edges || !(heightM && heightM > 0)) return null;
|
||||
const { left, right } = edges;
|
||||
const span = left.offset_m - right.offset_m;
|
||||
if (!(Math.abs(span) > 1e-9)) return null;
|
||||
const grade = (left.elevation_m - right.elevation_m) / span;
|
||||
const bottom = bottomOf(left, right, heightM, slopePct);
|
||||
// 양측절토(356-1) — 낮은 쪽 늘인 노면이 원지반과 만나는 점(tilt_reach)에서 끊음 · 너머 점 거름 → 꼬리가 수직으로 닫음
|
||||
const reach = design.tilt_reach;
|
||||
const outward = reach?.side === "left" ? 1 : -1;
|
||||
const line = (design.design_line ?? []).filter(
|
||||
(p) => !reach || (p.offset_m - reach.offset_m) * outward <= 1e-6,
|
||||
);
|
||||
const leftPath = sidePath(left, 1, line, grade, heightM);
|
||||
const rightPath = sidePath(right, -1, line, grade, heightM);
|
||||
const leftPath = sidePath(left, 1, line, bottom);
|
||||
const rightPath = sidePath(right, -1, line, bottom);
|
||||
return [
|
||||
{ offset: left.offset_m, elevation: left.elevation_m },
|
||||
...rightPath,
|
||||
@@ -117,7 +134,7 @@ export function appendUnderdrainOverlay(
|
||||
x: (offset: number) => number,
|
||||
y: (elevation: number) => number,
|
||||
): SVGGElement | null {
|
||||
const outline = spec ? underdrainOutline(design, spec.height_m) : null;
|
||||
const outline = spec ? underdrainOutline(design, spec.height_m, spec.slope_pct) : null;
|
||||
if (!spec || !outline) return null;
|
||||
const px = (p: OffsetPoint): [number, number] => [x(p.offset), y(p.elevation)];
|
||||
const points = outline.map((p) => px(p).join(",")).join(" ");
|
||||
@@ -136,15 +153,19 @@ export function appendUnderdrainOverlay(
|
||||
clip.append(clipShape);
|
||||
const stones = document.createElementNS(SVG_NS, "g");
|
||||
stones.setAttribute("clip-path", `url(#${clipId})`);
|
||||
const height = spec.height_m!;
|
||||
const offsets = outline.map((p) => p.offset);
|
||||
const [lo, hi] = [Math.min(...offsets), Math.max(...offsets)];
|
||||
const { left, right } = design.road_edges;
|
||||
const grade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m);
|
||||
const roadZ = (o: number) => left.elevation_m + grade * (o - left.offset_m);
|
||||
const bottom = bottomOf(left, right, spec.height_m!, spec.slope_pct);
|
||||
const bottomZ = (o: number) => bottom.z0 + bottom.grade * (o - bottom.o0);
|
||||
// 돌 띠 = 노면 ~ 하단 경계(두께가 자리마다 다름 · 켜 수는 두꺼운 쪽)
|
||||
const height = Math.max(roadZ(lo) - bottomZ(lo), roadZ(hi) - bottomZ(hi), spec.height_m!);
|
||||
const at = (u: number, v: number): OffsetPoint => {
|
||||
const offset = lo + (hi - lo) * u;
|
||||
return { offset, elevation: roadZ(offset) - height * (v + 0.5) };
|
||||
const top = roadZ(offset);
|
||||
return { offset, elevation: top + (bottomZ(offset) - top) * (v + 0.5) };
|
||||
};
|
||||
const [ax, ay] = px(at(0, 0));
|
||||
const [bx, by] = px(at(1, 0));
|
||||
|
||||
@@ -63,8 +63,9 @@ export function facilityRanges(
|
||||
const depth = num(spec.overflow_depth_m) ?? 0;
|
||||
if (spec.type === "ford" && depth > 0) drops.push([at, depth]);
|
||||
}
|
||||
// 노면 = 표준 경사 · 암거 경사는 맹암거 하단 경계만(371-3 · 파이썬 `tilt_ranges` 짝)
|
||||
for (const drain of underdrainSpecsFrom(structures))
|
||||
tilt.push([drain.chainage_m, drain.chainage_m, drain.slope_pct]);
|
||||
tilt.push([drain.chainage_m, drain.chainage_m, null]);
|
||||
return { paved, tilt, drops };
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { catalogRow } from "./common_util_structure_catalog";
|
||||
|
||||
const EDGE_TOLERANCE_M = 0.02;
|
||||
|
||||
/** 맹암거 제원 — 높이 · 경사가 비면 null(높이 없으면 형상 없음 · 경사 없으면 표준 횡단경사). */
|
||||
/** 맹암거 제원 — 높이 · 경사가 비면 null(높이 없으면 형상 없음 · 경사 = 형상 하단 경계 · 없으면 노면과 평행 · 371-3). */
|
||||
export interface UnderdrainSpec {
|
||||
structure_id: string | null;
|
||||
chainage_m: number;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user