fix(B06): 물넘이 포장 측구 끔 · 경사 = 패널 경사각 · 세월교 · 물넘이 월류 높이 점선(PLAN 66-2 · 66-3)
- 물넘이 자리 노면 · 측구 설계선 걷음 → 옛 계획고 점선(바닥 + 월류 높이)이 드러남 · 측구 자리는 바닥 끝 ~ 절토 비탈 발 실선 - 노면 경사 = ford_slope_deg(°) → % · 없으면 옛 ford_slope_pct · 둘 다 없으면 기본 3° (파이썬 · TS 짝 + 거울 시험) - 방향 = 지금대로 절토측 높고 성토측 낮음 - 세월교 점선 = 노면 + 월류 높이(ford_height_m) · 패널에서 고치면 저장 전에도 바로 따라옴 - 물넘이 패널 월류 높이 · 경사각도 캐시 제원에 바로 얹음(폭 절반 걸친 옆 카드 포함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
@@ -109,6 +109,8 @@ FORD_DEFAULT_WIDTH_M = 10.0
|
||||
# 물넘이포장 월류 폭 기본값(m) — 같은 폼이 물넘이에 채우는 값(2026-08-18 사용자 확정,
|
||||
# 지식DB 폭 수치 근거 없음).
|
||||
FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0
|
||||
# 물넘이포장 노면 경사각 기본값(°) — 좌측 패널 「경사각도」 기본 3°(PLAN 65-5 · 66-2).
|
||||
FORD_PAVEMENT_DEFAULT_SLOPE_DEG = 3.0
|
||||
|
||||
# BOX암거 부재 두께 — **등록부 칸**(`wall_thickness_m`·`slab_thickness_m`)이 정본이다
|
||||
# (2026-09-15 브레인 판정 ① · 제안값 = 산림과임업기술(임도) 예제 벽 0.3·판 0.25 · KDS 300㎜ 병기).
|
||||
@@ -434,7 +436,14 @@ def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
values = dict(options or {})
|
||||
width = _number(values.get("ford_width_m"), _number(defaults.get("ford_width_m"), None))
|
||||
depth = _number(values.get("ford_height_m"), None)
|
||||
slope = _number(values.get("ford_slope_pct"), None)
|
||||
# 노면 경사 = 좌측 패널 경사각(°) → % (66-2) · 없으면 옛 「바닥 경사(%)」 · 둘 다 없으면 기본 3°
|
||||
degree = _number(values.get("ford_slope_deg"), None)
|
||||
legacy = _number(values.get("ford_slope_pct"), None)
|
||||
fallback = None
|
||||
if degree is None and legacy is None:
|
||||
fallback = _number(defaults.get("ford_slope_deg"), FORD_PAVEMENT_DEFAULT_SLOPE_DEG)
|
||||
used = degree if degree is not None else fallback
|
||||
slope = round(math.tan(math.radians(used)) * 100.0, 4) if used is not None else legacy
|
||||
span = width if width and width > 0 else FORD_PAVEMENT_DEFAULT_WIDTH_M
|
||||
return {
|
||||
"type": "ford_pavement",
|
||||
@@ -442,7 +451,9 @@ def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]:
|
||||
# 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
|
||||
"depth_m": depth if depth and depth > 0 else None,
|
||||
"slope_pct": slope,
|
||||
"defaulted": _defaulted(values, [("ford_width_m", "월류 폭", span)]),
|
||||
"defaulted": _defaulted(
|
||||
values, [("ford_width_m", "월류 폭", span), ("ford_slope_deg", "경사각", fallback)]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ export interface FordControl {
|
||||
pipe_count?: number;
|
||||
pipe_kind?: string;
|
||||
ford_width_m?: number;
|
||||
ford_height_m?: number;
|
||||
},
|
||||
) => void;
|
||||
/** 날개벽 제원 저장 — 좌측 폼이 보낸다(계획서 24-4a). 같은 정본으로 간다. */
|
||||
|
||||
@@ -22,6 +22,8 @@ let hatchSeq = 0;
|
||||
|
||||
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
|
||||
export interface FordPavementSpec {
|
||||
/** 관 지점 누가거리 — 폭 절반 걸친 옆 카드도 같은 값. */
|
||||
chainage_m?: number;
|
||||
span_m: number;
|
||||
/** 노선 중심에서 잰 파임 깊이(m). 없으면 그리지 않는다 — 수치를 지어내지 않는다. */
|
||||
depth_m: number | null;
|
||||
@@ -85,9 +87,13 @@ export function appendFordSurfaceDropPlan(
|
||||
design: CrossDesign,
|
||||
x: (offset: number) => number,
|
||||
y: (elevation: number) => number,
|
||||
ford?: { sides: ReadonlyArray<{ outward: number; wallTopOuter: OffsetPoint }> } | null,
|
||||
ford?: {
|
||||
sides: ReadonlyArray<{ outward: number; wallTopOuter: OffsetPoint }>;
|
||||
ford?: { overflow_depth_m?: number };
|
||||
} | null,
|
||||
): void {
|
||||
const drop = design.surface_drop_m ?? 0;
|
||||
// 점선 = 노면 + 월류 높이(66-3) — 패널 월류 높이(세트 제원)가 먼저 · 저장 뒤 내린 양과 같은 값
|
||||
const drop = ford?.ford?.overflow_depth_m ?? design.surface_drop_m ?? 0;
|
||||
if (!(drop > 0) || !design.road_edges) return;
|
||||
const { left, right } = design.road_edges;
|
||||
// +offset이 좌측이다 — 좌측 벽 상단 → 좌 노견 → 우 노견 → 우측 벽 상단 순.
|
||||
@@ -175,3 +181,79 @@ export function appendFordPavementOverlay(
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 물넘이 자리 노면 · 측구 설계선을 걷는다(66-2 · 66-3) — 노면은 파인 바닥(실선)과 옛 계획고(점선)가
|
||||
* 대신하고, 측구는 서지 않음. `from` = 설계선 그리기 앞의 자식 수(그 뒤에 붙은 선만 본다).
|
||||
* 측구 자리는 바닥 끝에서 절토 비탈 발까지 실선으로 잇는다. 면적 · 서버 설계선은 그대로(그림만).
|
||||
*/
|
||||
export function hideFordPavedRoad(
|
||||
layer: SVGElement,
|
||||
from: number,
|
||||
design: CrossDesign,
|
||||
spec: FordPavementSpec,
|
||||
x: (offset: number) => number,
|
||||
y: (elevation: number) => number,
|
||||
): void {
|
||||
const edges = design.road_edges;
|
||||
const designLine = design.design_line;
|
||||
if (!edges || !designLine?.length) return;
|
||||
const ditch = design.ditch;
|
||||
const ditchWidth =
|
||||
design.ditch_enabled === false || ditch.type === "none"
|
||||
? 0
|
||||
: ditch.type === "standard"
|
||||
? ditch.top_width_m
|
||||
: ditch.width_m;
|
||||
// +offset = 좌측 · 측구는 노견 바깥 그 쪽에 붙는다
|
||||
const leftEdge = Math.max(edges.left.offset_m, edges.right.offset_m);
|
||||
const rightEdge = Math.min(edges.left.offset_m, edges.right.offset_m);
|
||||
const hi = leftEdge + (design.ditch_side === "left" ? ditchWidth : 0);
|
||||
const lo = rightEdge - (design.ditch_side === "right" ? ditchWidth : 0);
|
||||
const [xa, xb] = [x(lo), x(hi)];
|
||||
const inside = (px: number): boolean =>
|
||||
px >= Math.min(xa, xb) - 0.5 && px <= Math.max(xa, xb) + 0.5;
|
||||
for (const node of [...layer.children].slice(from)) {
|
||||
if (node.tagName !== "line" || !node.classList.contains("b06-chart__design-cross")) continue;
|
||||
if (inside(Number(node.getAttribute("x1"))) && inside(Number(node.getAttribute("x2"))))
|
||||
node.remove();
|
||||
}
|
||||
if (!(ditchWidth > 0) || !design.ditch_side) return;
|
||||
const edge = design.ditch_side === "left" ? leftEdge : rightEdge;
|
||||
const foot = design.ditch_side === "left" ? hi : lo;
|
||||
const sorted = [...designLine].sort((a, b) => a.offset_m - b.offset_m);
|
||||
const at = sorted.find((point) => Math.abs(point.offset_m - foot) < 1e-3);
|
||||
if (!at) return;
|
||||
const edgeElevation = design.ditch_side === "left" ? edges.left : edges.right;
|
||||
layer.append(
|
||||
line(
|
||||
[
|
||||
`${x(edge)},${y(fordDeckElevationAt(design, spec, edge, edgeElevation.elevation_m))}`,
|
||||
`${x(foot)},${y(at.elevation_m)}`,
|
||||
],
|
||||
"b06-chart__ford-deck",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 좌측 패널의 물넘이 칸을 캐시 제원에 얹는다(66-2 · 66-3) — 월류 높이 = 파임 깊이 · 경사각(°) → %.
|
||||
* 서버 `_ford_pavement_set` 과 같은 환산(저장 뒤 서버가 정본으로 다시 냄). 바꿨으면 true.
|
||||
*/
|
||||
export function applyFordPavementOptions(
|
||||
spec: FordPavementSpec,
|
||||
patch: Record<string, number | string>,
|
||||
): boolean {
|
||||
const num = (key: string): number | undefined => {
|
||||
const value = patch[key] === "" ? NaN : Number(patch[key]);
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
};
|
||||
const height = num("ford_height_m");
|
||||
const degree = num("ford_slope_deg");
|
||||
if (height !== undefined) spec.depth_m = height > 0 ? height : null;
|
||||
if (degree !== undefined) {
|
||||
spec.slope_pct = Number((Math.tan((degree * Math.PI) / 180) * 100).toFixed(4));
|
||||
spec.defaulted = spec.defaulted?.filter((item) => !item.startsWith("경사각 "));
|
||||
}
|
||||
return height !== undefined || degree !== undefined;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "./B06_Section_UI_Cross_Areas";
|
||||
import {
|
||||
appendFordPavementOverlay,
|
||||
hideFordPavedRoad,
|
||||
appendFordSurfaceDropPlan,
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { culvertWallsStandAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
@@ -468,11 +469,22 @@ export function createCrossSectionCard(
|
||||
// 같은 트림 값을 쓰므로 "그림은 이런데 수량은 저렇다"가 생기지 않는다.
|
||||
applyStructureAreas(section, designTrim);
|
||||
appendCrossDesignOverlay(plotLayer, section.design, x, toDisplayY, drawSamples, designTrim, {
|
||||
const designFrom = plotLayer.childElementCount;
|
||||
layer: svg,
|
||||
x: (CROSS_PAD.left + widthPx - CROSS_PAD.right) / 2,
|
||||
y: heightPx - CROSS_PAD.bottom - 4,
|
||||
});
|
||||
// 사토장(유용토운반작업장) — 교본 6장 3절이 횡단도 표시를 요구한다. 값은 설계 결과에서
|
||||
// 물넘이 자리 = 노면 · 측구 설계선 걷음(66-2 · 66-3 — 옛 계획고 점선이 드러남)
|
||||
if (fordPaved && section.ford_pavement)
|
||||
hideFordPavedRoad(
|
||||
plotLayer,
|
||||
designFrom,
|
||||
section.design,
|
||||
section.ford_pavement,
|
||||
x,
|
||||
toDisplayY,
|
||||
);
|
||||
// 그대로 오고 여기서 다시 세지 않는다. 선 종류는 터파기 파선과 갈라 둔다.
|
||||
appendSpoilFillOverlay(plotLayer, section.design, x, toDisplayY);
|
||||
// 배수관 세트 — 조정창이 쓸 카드 상태를 받아 둔다(2026-08-19) · 링크 카드도 채운다(2026-08-24).
|
||||
|
||||
@@ -49,6 +49,7 @@ const DEFAULTED_LABELS: Record<string, string> = {
|
||||
pipe_kind: "관종",
|
||||
pipe_diameter_mm: "관경",
|
||||
ford_width_m: "월류 폭",
|
||||
ford_height_m: "월류 높이",
|
||||
pipe_count: "배관 수량",
|
||||
body_width_m: "본체 폭",
|
||||
body_height_m: "본체 높이",
|
||||
@@ -199,6 +200,9 @@ export function createFordControls(deps: FordControlDeps): FordControls {
|
||||
if (patch.pipe_kind) spec.pipe_kind = patch.pipe_kind;
|
||||
// 월류 폭 = 구체의 도로 진행 방향 길이(`span_m`) — 백엔드 `_ford_set`과 같은 자리.
|
||||
if (patch.ford_width_m) spec.span_m = patch.ford_width_m;
|
||||
// 월류 높이 — 횡단 점선(노면 + 월류 높이 · 66-3)이 바로 따라옴 · 노면 내림은 저장 뒤 서버
|
||||
if (patch.ford_height_m !== undefined)
|
||||
spec.overflow_depth_m = Math.max(patch.ford_height_m, 0);
|
||||
}
|
||||
dropDefaulted(spec, patch);
|
||||
deps.queuePipeOptions(chainageM, patch);
|
||||
@@ -265,6 +269,8 @@ export function applyFordFormOptions(
|
||||
if (typeof patch.pipe_kind === "string") pipe.pipe_kind = patch.pipe_kind;
|
||||
const widthM = num("ford_width_m");
|
||||
if (widthM !== undefined) pipe.ford_width_m = widthM;
|
||||
const heightM = num("ford_height_m");
|
||||
if (heightM !== undefined) pipe.ford_height_m = heightM;
|
||||
if (Object.keys(pipe).length) control.setPipe(chainageM, pipe);
|
||||
|
||||
applyWingFormOptions(control, chainageM, patch);
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { applyBoxFormOptions, applyFordFormOptions } from "./B06_Section_UI_Page_Ford_Controls";
|
||||
import { applyFordPavementOptions } from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import {
|
||||
applySpanPatch,
|
||||
revetWallSpec,
|
||||
@@ -50,6 +51,18 @@ export function applyPipeOptionsToCache(
|
||||
patch: Record<string, number | string>,
|
||||
whole = true,
|
||||
): void {
|
||||
// 물넘이포장 — 월류 높이 · 경사각을 캐시 제원에 얹음(66-2 · 66-3) · 폭 절반 걸친 옆 카드도
|
||||
const paved = (ctx.detail()?.cross_sections ?? []).filter(
|
||||
(section) =>
|
||||
section.ford_pavement &&
|
||||
Math.abs((section.ford_pavement.chainage_m ?? section.chainage_m) - chainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (paved.length) {
|
||||
for (const section of paved)
|
||||
if (applyFordPavementOptions(section.ford_pavement!, patch))
|
||||
ctx.refreshCard(section.chainage_m);
|
||||
return;
|
||||
}
|
||||
const owner = ctx
|
||||
.detail()
|
||||
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M);
|
||||
|
||||
@@ -45,6 +45,7 @@ export const FORD_SLAB_THICKNESS_M = 0.3;
|
||||
export const FORD_WALL_THICKNESS_M = 0.2;
|
||||
export const FORD_DEFAULT_WIDTH_M = 10.0;
|
||||
export const FORD_PAVEMENT_DEFAULT_WIDTH_M = 5.0;
|
||||
export const FORD_PAVEMENT_DEFAULT_SLOPE_DEG = 3.0;
|
||||
export const BOX_COVER_M = 0.5;
|
||||
/** 관 벽 최소 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조).
|
||||
* 높이를 안 적은 관 벽은 이 값으로 섬(2026-09-14 브레인 판정 · 옛 등록부 기본 2.5 걷음). */
|
||||
@@ -330,14 +331,25 @@ function fordPavementSet(
|
||||
const defaults = registry.ford_pavement ?? {};
|
||||
const width = num(values.ford_width_m, num(defaults.ford_width_m, null));
|
||||
const depth = num(values.ford_height_m, null);
|
||||
// 노면 경사 = 경사각(°) → % (66-2) · 없으면 옛 바닥 경사(%) · 둘 다 없으면 기본 3°
|
||||
const degree = num(values.ford_slope_deg, null);
|
||||
const legacy = num(values.ford_slope_pct, null);
|
||||
const fallback =
|
||||
degree === null && legacy === null
|
||||
? num(defaults.ford_slope_deg, FORD_PAVEMENT_DEFAULT_SLOPE_DEG)
|
||||
: null;
|
||||
const used = degree ?? fallback;
|
||||
const span = width && width > 0 ? width : FORD_PAVEMENT_DEFAULT_WIDTH_M;
|
||||
return {
|
||||
type: "ford_pavement",
|
||||
span_m: span,
|
||||
// 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다).
|
||||
depth_m: depth && depth > 0 ? depth : null,
|
||||
slope_pct: num(values.ford_slope_pct, null),
|
||||
defaulted: defaulted(values, [["ford_width_m", "월류 폭", span]]),
|
||||
slope_pct: used !== null ? round(Math.tan((used * Math.PI) / 180) * 100, 4) : legacy,
|
||||
defaulted: defaulted(values, [
|
||||
["ford_width_m", "월류 폭", span],
|
||||
["ford_slope_deg", "경사각", fallback],
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -82,10 +82,17 @@ POINTS = [
|
||||
"form": "메쌓기",
|
||||
},
|
||||
},
|
||||
# 물넘이포장 경사각(°) → %(66-2) · 경사 칸이 없으면 기본 3°
|
||||
{
|
||||
"chainage_m": 140.0,
|
||||
"facility": "ford_pavement",
|
||||
"options": {"ford_height_m": 0.3, "ford_slope_deg": 5},
|
||||
},
|
||||
{"chainage_m": 160.0, "facility": "ford_pavement", "options": {"ford_height_m": 0.2}},
|
||||
]
|
||||
|
||||
# 물넘이포장(폭 9m)이 ±4.5m 까지 걸치는지 보려고 95·105 측점을 함께 둔다.
|
||||
CHAINAGES = [0.0, 20.0, 40.0, 60.0, 80.0, 95.0, 100.0, 105.0, 120.0, 140.0]
|
||||
CHAINAGES = [0.0, 20.0, 40.0, 60.0, 80.0, 95.0, 100.0, 105.0, 120.0, 140.0, 160.0]
|
||||
|
||||
_TS_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""물넘이포장 노면 경사 = 좌측 패널 경사각(°) → %(PLAN 66-2).
|
||||
|
||||
경사각이 있으면 그 값 · 없으면 옛 「바닥 경사(%)」 · 둘 다 없으면 기본 3°.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from B06_Section.B06_Section_Engine_Culvert import _ford_pavement_set
|
||||
|
||||
|
||||
def test_slope_degree_becomes_percent() -> None:
|
||||
spec = _ford_pavement_set({"ford_height_m": 0.3, "ford_slope_deg": 5})
|
||||
assert spec["slope_pct"] == round(math.tan(math.radians(5)) * 100, 4)
|
||||
assert not any("경사각" in item for item in spec["defaulted"])
|
||||
|
||||
|
||||
def test_slope_defaults_to_three_degrees() -> None:
|
||||
spec = _ford_pavement_set({"ford_height_m": 0.3})
|
||||
assert spec["slope_pct"] == round(math.tan(math.radians(3)) * 100, 4)
|
||||
assert "경사각 3" in spec["defaulted"]
|
||||
|
||||
|
||||
def test_legacy_percent_kept_without_degree() -> None:
|
||||
spec = _ford_pavement_set({"ford_height_m": 0.3, "ford_slope_pct": 2.0})
|
||||
assert spec["slope_pct"] == 2.0
|
||||
Reference in New Issue
Block a user