fix(section): 횡단 벽 형상 한 벌 · 형태별 높이 한도 · 원지반 맞춤 삭제 · 노면 위 금지(54-42)

- 관 유입·유출 흙막이 · 추가 흙막이 · 사면안정 흙막이 · 옹벽 모두 공용 buildRevetWallGeometry
- 외곽 기울기 형태 무관(outlineLeanOf) — 형태는 안 무늬만
- 형태별 높이 한도(메 2.0 · 찰 3.0 · 콘크리트 5.0) 자르기 · 경고 삭제 — 입력 높이 그대로
- 추가 흙막이 원지반 맞춤 늘이기 삭제(돌쌓기(메) 원지반 따라 연장 흠)
- 벽 윗면 노면 위 금지 — 넘으면 노면에서 멈춤
- 다단 묶음 _Cross_Culvert_Tiers 로 분리(Geom 700줄 이하)
- 시험 test_b06_wall_shape_unified 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-06 14:48:53 +09:00
co-authored by Claude Opus 5.5
parent 43ff2931c2
commit fb3b24f13f
14 changed files with 337 additions and 339 deletions
@@ -28,8 +28,8 @@ logger = logging.getLogger(__name__)
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 정본이 누가거리를 0.01m로 끊어 쓴다.
_EDGE_TOLERANCE_M = 0.02
# 구조물 종류 → 횡단 기하가 아는 **형태** 이름. 형태가 벽 높이 한계·두께를 정하므로
# (`B06_Section_UI_Cross_Revetment.revetHeightLimit`) 가장 가까운 것으로 잇는다.
# 구조물 종류 → 횡단 기하가 아는 **형태** 이름. 형태는 벽 안 무늬만 정하므로
# 가장 가까운 것으로 잇는다.
_FORM_BY_TYPE = {
"masonry_wet": "돌쌓기(찰)",
"masonry_dry": "돌쌓기(메)",
+1 -3
View File
@@ -14,7 +14,6 @@ import {
REVET_EMBED_DEPTH_M,
FILL_SLOPE_RATIO_MIN,
pipeWallThicknessM,
revetHeightLimit,
} from "./B06_Section_UI_Cross_Culvert_Geom";
import type {
BasinShape,
@@ -182,8 +181,7 @@ export function appendCulvertOverlay(
`${roleLabel(wall.role)} 기슭막이 ${wall.form ?? ""} H=${(
wall.height + REVET_EMBED_DEPTH_M
).toFixed(1)}m` +
`(상단 = 사면선 접점, 전면 1:${wall.lean}(품셈 13-4-4 [주]⑪ 표준경사)` +
`, 높이 한계 ${revetHeightLimit(wall.form).toFixed(1)}m — 교본 7-3)` +
`(상단 = 사면선 접점, 전면 1:${wall.lean}(품셈 13-4-4 [주]⑪ 표준경사))` +
(wall.floatGapM > 0.01
? ` · ⚠ 바닥 원지반 이격 ${wall.floatGapM.toFixed(2)}m — 하부 지지 구조물 별도(추가 예정)`
: ` · 근입 ${REVET_EMBED_DEPTH_M.toFixed(2)}m(실무 기초콘크리트 H=0.5 — 법정 규정 없음)`) +
@@ -75,35 +75,15 @@ export function pipeWallThicknessM(kind: string | null, diameterM: number): numb
*/
export const REVET_EMBED_DEPTH_M = 0.5;
/**
* 기슭막이 형태별 높이 한계(m) — 지식DB `돌쌓기.md` §1 / `흙막이.md` §2:
* 돌 **찰쌓기 3.0m 이하**, 돌 **메쌓기 2.0m 이하**(교본 7-3 돌흙막이 기준),
* 돌망태 2m 정도. 큰돌쌓기는 총높이 한계 없음(1일 시공 1.5m 제한만).
* 초과 시 콘크리트 옹벽 등 상위 구조물 검토 대상이다.
* 통나무·목재틀·바자는 지식DB에 수치가 없어 **2.0m**로 둔다(2026-08-29 사용자 확정).
*/
export function revetHeightLimit(form: string | null | undefined): number {
if (!form) return 3.0;
if (form.includes("메")) return 2.0;
if (form.includes("찰")) return 3.0;
if (form.includes("돌망태")) return 2.0;
if (form.includes("통나무") || form.includes("목재") || form.includes("바자")) return 2.0;
if (form.includes("콘크리트")) return 5.0; // 프로젝트 확정값 — materialLimit과 동일
return 3.0;
}
/* ── 기슭막이 재질·높이 조작(2026-08-22 사용자 확정) ─────────────────────
* 높이 조작이 교본 형태별 한계와 부딪히면 **재질을 바꿔야** 더 올릴 수 있다:
* 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(프로젝트 기본값,
* 외부 기준 미확인). 높이 제어·표기·한계 기준은 모두 **순수 높이**(벽 바닥~상단,
* 근입 0.5m 포함) — 배관 기슭막이 2026-08-23, 추가·독립 기슭막이 2026-08-29
* 사용자 확정으로 기준을 하나로 맞췄다. */
/* 기슭막이 높이는 형태와 무관하게 **사용자 입력 그대로** — 형태별 높이 한계·원지반 맞춤
* 자동 자르기 없음. 형태가 바꾸는 것은 안 무늬뿐. 높이 기준은 **순수 높이**(벽 바닥~상단,
* 근입 0.5m 포함). 단 하나 — 벽 윗면은 노면 위로 올라오지 않음(`buildRevetWallGeometry`). */
export type RevetMaterial = "dry" | "wet" | "concrete";
/**
* 기슭막이 **형태** — B05 폼(`B05_Profile_UI_Drainage_Facility_Fields`)과 같은 한 벌이다.
* 조정창 선택지도 이 목록을 쓴다(2026-08-29 사용자: "B05 폼 형태로 가는 게 맞다").
* 높이 한계는 `revetHeightLimit(form)`, 도형의 돌 표현은 `materialFromForm(form)`이 정한다.
* 도형의 돌 표현은 `materialFromForm(form)`이 정한다.
*/
export const REVET_FORMS = [
"돌쌓기(찰)",
@@ -130,10 +110,6 @@ export function revetFormOf(m: string | null | undefined, specForm?: string | nu
export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"];
export function materialLimit(material: RevetMaterial): number {
return material === "dry" ? 2.0 : material === "wet" ? 3.0 : 5.0;
}
export function materialLabel(material: RevetMaterial): string {
return material === "dry" ? "메쌓기" : material === "wet" ? "찰쌓기" : "콘크리트";
}
@@ -149,7 +125,7 @@ export function materialFromForm(form: string | null | undefined): RevetMaterial
* 벽 한 매의 제원(형태·재질·높이) — 배관 벽과 독립 기슭막이가 같이 쓴다.
*
* 조정창이 고르는 값은 **형태**(B05 폼과 같은 6종 — 2026-08-29 사용자)이고, 재질(돌
* 표현)과 높이 한계는 형태에서 파생된다. 높이 기준은 **순수 높이**(바닥~상단, 근입
* 표현)은 형태에서 파생된다. 높이 기준은 **순수 높이**(바닥~상단, 근입
* 0.5m 포함). 독립 기슭막이(관 숨김)는 관경이 없어 하한이 근입뿐이고 기본 높이는
* 사용자가 고른 형태 높이(`revet_height_m`)다 — 배관은 관이 벽 밖으로 삐져나오지
* 않도록 관경 기준 최소 높이를 하한·기본으로 쓴다.
@@ -159,16 +135,14 @@ export function revetWallSpec(
adjust: { h?: number | null; m?: string | null },
hiddenPipe: boolean,
diameterM: number,
): { form: string; material: RevetMaterial; limit: number; pureHeight: number; height: number } {
): { form: string; material: RevetMaterial; pureHeight: number; height: number } {
const form = revetFormOf(adjust.m, spec.revet_form);
const limit = revetHeightLimit(form);
const floor = hiddenPipe ? REVET_EMBED_DEPTH_M : pipeWallMinPureHeight(diameterM);
const preferred = hiddenPipe ? (spec.revet_height_m ?? floor) : floor;
const pureHeight = Math.min(Math.max(adjust.h ?? preferred, floor), Math.max(limit, floor));
const pureHeight = Math.max(adjust.h ?? preferred, floor);
return {
form,
material: materialFromForm(form),
limit,
pureHeight,
height: pureHeight - REVET_EMBED_DEPTH_M,
};
@@ -18,20 +18,18 @@ import {
FILL_SLOPE_MAX_LENGTH_M,
FILL_SLOPE_RATIO_MIN,
materialFromForm,
materialLabel,
revetFormOf,
revetHeightLimit,
REVET_EMBED_DEPTH_M,
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import type {
OffsetPoint,
OutletFillSegment,
WallAdjust,
WallLayout,
} from "./B06_Section_UI_Cross_Culvert_Types";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
export interface OutletExtrasInput {
/** 성토부선 시작점 = 관 유출 하단 꼭짓점(배관 벽의 성토선 시작 자리). */
@@ -215,64 +213,10 @@ function trailingFill(
};
}
/**
* 추가 기슭막이 벽 1매 — 배관용 기슭막이와 같은 형상(배면 수직·전면 1:0.3,
* 수평 기초 근입 0.5m), 배관·마감면만 없다. 높이는 눈금 없이 **정확값**을 쓴다 —
* 0.1m 올림을 하면 상단이 1:1.2 사면선을 벗어난다(사이 물매 정확 유지가 우선).
*/
function buildExtraWall(
index: number,
anchor: OffsetPoint,
outward: number,
height: number,
material: RevetMaterial,
floatGapM: number,
form: string | null = null,
lean: number = REVET_LEAN_RATIO,
): WallLayout {
const thickness = REVET_THICKNESS_M;
const baseWidth = thickness * 1.5 + lean * height;
const backOffset = anchor.offset - outward * (baseWidth / 2);
const topJoint = backOffset + outward * (thickness / 2);
const topElevation = anchor.elevation + height;
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
const topFront = topJoint + outward * thickness;
const frontXAt = (elevation: number): number =>
topFront + outward * lean * (topElevation - elevation);
// 하단 = 기준선(anchor) 아래 근입 0.5m **고정** — 원지반에 묻혀도 바닥을 더
// 내리지 않는다. 벽 형상은 높이값으로만 정한다(2026-08-23 사용자 확정 —
// 종전 전면 발끝 지반 추적 수렴 삭제).
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
const bottomFront: OffsetPoint = {
offset: frontXAt(bottomElevation),
elevation: bottomElevation,
};
return {
role: "extra",
extraIndex: index,
form: form ?? materialLabel(material),
lengthM: null,
backOffset,
outerOffset: bottomFront.offset,
base: anchor.elevation,
height,
floatGapM,
material,
lean,
outward,
topBack,
topJoint: { offset: topJoint, elevation: topElevation },
bottomBack,
bottomFront,
points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront],
};
}
/**
* 유출측 성토부선·다단 기슭막이 일괄 계산 (2026-08-22 4축 조작 체계).
*
* 단별 벽 높이 h = 사용자 설정 ?? 기본 1.5m (0.5~재질 한계로 절삭). 벽 상단은
* 단별 벽 높이 h = 사용자 설정 ?? 기본 1.5m (하한 0.5 · 형태별 상한 없음). 벽 상단은
* 항상 src에서 내려오는 **1:1.2 성토선 위** — 자동 자리는 높이 h 벽이 지반에
* 앉으며 상단이 선에 닿는 지점(heightAt(x)=h 교차점, 닫힌식):
* heightAt(x) = (src.elev − 지반(x) − (D − 0.25t)/1.2) / (1 − 0.15/1.2)
@@ -304,20 +248,15 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
// (2026-08-29 사용자: 조정창 선택지를 형태로 통일).
const form = revetFormOf(adjust.m, input.ownerForm);
const material = materialFromForm(form);
const limit = revetHeightLimit(form);
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함) — 배관 기준벽과 같은 기준으로
// 통일했다(2026-08-29 사용자: 추가 기슭막이 높이가 하단~상단이 아니었다).
const height = Math.min(
Math.max(
adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M,
EXTRA_WALL_MIN_HEIGHT_M,
REVET_EMBED_DEPTH_M + 0.1,
),
limit,
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함) — 배관 기준벽과 같은 기준.
// 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠).
const height = Math.max(
adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M,
EXTRA_WALL_MIN_HEIGHT_M,
REVET_EMBED_DEPTH_M + 0.1,
);
/** 기준선(근입 위)~상단 — 도형·자리 계산은 종전대로 이 값으로 한다.
* 지형이 요청 높이만큼 안 나오면 아래 스캔 뒤 **낮춰서** 바닥을 지반에 앉힌다. */
let exposed = height - REVET_EMBED_DEPTH_M;
/** 기준선(근입 위)~상단 — 도형·자리 계산은 이 값으로 한다. */
const exposed = height - REVET_EMBED_DEPTH_M;
// 전면 기울기 — 형태·요청 높이로 표준경사(2026-09-14 B5). 자리 닫힌식의 분모도 따라감.
const lean = input.leanFor?.(form, height) ?? REVET_LEAN_RATIO;
const heightDenominator = 1 - lean / 2 / FILL_SLOPE_RATIO_MIN;
@@ -425,27 +364,20 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
appliedX = Math.round(appliedX * 10) / 10;
const topElevation = autoTop - appliedD;
const anchorX = autoOffset + outward * appliedX;
// 바닥을 **원지반에 앉힌다**(2026-09-06 사용자 지적: 연계 기슭막이가 사면과 안 맞음).
// 상단은 1:1.2 성토선에 붙어 있어야 하므로 자리는 그대로 두고 **키만** 지반까지
// 맞춘다 — 지반이 낮으면 키우고(형태 한계까지), 높으면 줄인다(최소 높이까지).
// 예전에는 요청 높이를 그대로 써 바닥이 최대 0.87m 떠 있었다(실측 264.06·731.31).
// 높이는 입력 그대로 — 원지반에 맞춰 키우거나 줄이지 않는다(뜨면 floatGap 경고).
const groundBase = groundAt(anchorX);
const fitted = Math.min(
Math.max(topElevation - groundBase, EXTRA_WALL_MIN_HEIGHT_M - REVET_EMBED_DEPTH_M),
limit - REVET_EMBED_DEPTH_M,
);
const appliedExposed = Number.isFinite(fitted) ? fitted : exposed;
const base = topElevation - appliedExposed; // 근입 0.5 위 기준선
const wall = buildExtraWall(
i,
{ offset: anchorX, elevation: base },
const base = topElevation - exposed; // 근입 0.5 위 기준선
const wall = buildRevetWallGeometry({
anchor: { offset: anchorX, elevation: base },
outward,
appliedExposed,
height: exposed,
material,
Math.max(0, base - groundBase),
role: "extra",
form,
extraIndex: i,
floatGapM: Math.max(0, base - groundBase),
lean,
);
});
// 요청한 좌우 이동을 지형이 막았으면 그 양을 남긴다 — 「눌러도 안 움직인다」의 까닭을
// 툴팁으로 알리기 위함이다(2026-09-06 실측: 다섯 측점 중 넷이 1.0m 요청에 0.0m 이동).
const blocked = Math.max(0, (requestedX ?? 0) - appliedX);
@@ -459,11 +391,11 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
wall.shiftFloorM = Math.round(floor * 100) / 100;
}
walls.push(wall);
// 되받기 — 지반에 맞춰 조정한 값이 **실제 적용 높이**다(조정창이 그대로 받아 적는다).
// 되받기 — 적용 높이(하한만 걸림)를 조정창이 그대로 받아 적는다.
appliedAdjusts.push({
x: appliedX,
d: appliedD,
h: adjust.h != null ? appliedExposed + REVET_EMBED_DEPTH_M : null,
h: adjust.h != null ? height : null,
m: adjust.m,
});
// 성토선: src → (수평 선반 x>0이면 선반 끝) → 이음선 상단점. 사면길이 = 경사부.
+47 -133
View File
@@ -9,22 +9,19 @@
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
import {
BASIN_MAX_FILL_SLOPE_M,
materialLabel,
revetWallSpec,
PIPE_WALL_DEFAULT_RUN_M,
FILL_SLOPE_MAX_LENGTH_M,
FILL_SLOPE_RATIO_MAX,
FILL_SLOPE_RATIO_MIN,
MIN_PIPE_COVER_M,
REVET_EMBED_DEPTH_M,
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
REVET_TRAP_TOP_M,
revetHeightLimit,
revetTargetHeight,
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
import { ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
import type {
BasinLayout,
@@ -50,12 +47,16 @@ import {
inletChoiceAvailability,
resolveBasinChoice,
} from "./B06_Section_UI_Cross_Culvert_Basin";
import type { OutletExtrasResult } from "./B06_Section_UI_Cross_Culvert_Extra";
import { buildExtrasAt, inletGroundConnector } from "./B06_Section_UI_Cross_Culvert_Extra";
import { inletGroundConnector } from "./B06_Section_UI_Cross_Culvert_Extra";
import {
buildCulvertTiers,
drawnFillPoints,
outletPipeEnd,
slopeZeroAnchor,
} from "./B06_Section_UI_Cross_Culvert_Tiers";
import type { FillSlopeSegment, WallVertical } from "./B06_Section_UI_Cross_Culvert_Solve";
import {
cutLength,
fillWallBaseWidth,
outletSlopeFactory,
fillSlopeOf,
groundInterpolator,
@@ -130,7 +131,7 @@ export function computeCulvertLayout(
const inletInvert = Math.min(groundAt(inletInfo.edge.offset_m), invertCap(inletInfo.edge));
const inlet: OffsetPoint = { offset: inletInfo.edge.offset_m, elevation: inletInvert };
// 벽 제원(4축): 재질이 높이 한계(메2/찰3/콘5)를 정한다. 높이 기준 = **순수 높이**
// 벽 제원(4축): 형태별 높이 한계 없음(사용자 입력 그대로). 높이 기준 = **순수 높이**
// (바닥~상단 — 2026-08-23 사용자). 하한·기본 = 관경 + 아래 0.5 + 위 0.5 —
// 관이 벽 위로 삐져나오거나 바닥에 붙으면 안 된다. 기하는 종전대로
// invert~상단 계산용 높이(= 순수 − 0.5)로 돈다.
@@ -215,16 +216,6 @@ export function computeCulvertLayout(
inlet: null,
outlet: null,
};
// d의 0점 = **성토선 길이 0 지점**(벽 상단 = 노견 — 2026-08-23 사용자 확정).
// 그 자리(하단선 중점)는 이음선 상단점이 노견 끝에 오는 offset이고, 기본 자리는
// 성토선을 타고 경사길이 4m 내려간 지점(PIPE_WALL_DEFAULT_RUN_M)이다.
const slopeZeroAnchor = (
edge: { offset_m: number },
outward: number,
wallHeight: number,
lean: number,
): number =>
edge.offset_m + outward * (fillWallBaseWidth(wallHeight, lean) / 2 - REVET_TRAP_TOP_M);
if (!basinReason && designAt) {
const inletBaseAt = (offset: number): number =>
Math.min(groundAt(offset), invertCap(inletInfo.edge));
@@ -368,50 +359,28 @@ export function computeCulvertLayout(
}
return null;
}
// 형상: 배면 수직 + 계류측 1:n 평행사변형 띠(n = 표준경사). 높이는 vertical(계산용)이 들고 온다.
const height =
vertical?.height ?? Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form));
// 형상 = 공용 `buildRevetWallGeometry`(추가·사면안정 흙막이·옹벽과 한 벌). 높이는
// vertical(계산용)이 들고 온다 — 형태별 높이 한계 없음, 윗면은 노면에서 멈춤.
const height = vertical?.height ?? revetTargetHeight(diameter);
if (!(height > 0.05)) return null;
const floatGapM = vertical?.floatGapM ?? 0;
// 자리 기준 = **하단선 중점**(2026-08-21) — anchor.elevation은 그 자리 관 invert.
const baseWidth = thickness * 1.5 + lean * height;
const backOffset = anchor.offset - outward * (baseWidth / 2);
const topJoint = backOffset + outward * (thickness / 2);
const topElevation = anchor.elevation + height;
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
// 상단 변: 사다리꼴 상단(t/2) + 띠(t). 이음선은 상단 변 중간점에서 1:0.3으로 바닥까지.
const topFront = topJoint + outward * thickness;
// 하단선 = **수평 기초**, 깊이 = 기준선(관 invert) 아래 근입 0.5m 고정 —
// 원지반에 묻혀도 바닥을 더 내리지 않는다. 벽 형상은 높이값으로만 정한다
// (2026-08-23 사용자 확정 — 종전 전면 발끝 지반 추적 수렴 삭제).
const frontXAt = (elevation: number): number =>
topFront + outward * lean * (topElevation - elevation);
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
const bottomFront: OffsetPoint = {
offset: frontXAt(bottomElevation),
elevation: bottomElevation,
};
const wall: WallLayout = {
role: spec.role,
form: formLabel ?? materialLabel(material),
lengthM: spec.revet_length_m ?? null,
backOffset,
outerOffset: bottomFront.offset,
base: anchor.elevation,
// 높이 = **계산용 높이(관 invert~상단, 근입 제외)로 통일**(2026-08-22 사용자
// 확정 — 기초는 시공 시 묻히는 부분이라 높이·한계 검사 모두 이 기준).
height,
floatGapM,
material,
lean,
const side = outward > 0 ? edges.left : edges.right;
const wall = buildRevetWallGeometry({
anchor,
outward,
topBack,
topJoint: { offset: topJoint, elevation: topElevation },
bottomBack,
bottomFront,
points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront],
};
height,
material,
role: spec.role,
form: formLabel,
lengthM: spec.revet_length_m ?? null,
thickness,
floatGapM: vertical?.floatGapM ?? 0,
lean,
roadElevation: side.elevation_m,
});
const topJoint = wall.topJoint.offset;
const topElevation = wall.topJoint.elevation;
const topFront = wall.points[2].offset;
const bottomFront = wall.bottomFront;
walls.push(wall);
// 성토 사면선은 **이음선 상단점**에서 끊고, 끝단 표고도 그 점에 맞춘다(사용자 ②).
if (outward > 0) {
@@ -496,17 +465,6 @@ export function computeCulvertLayout(
);
// ── 관 축 확정 — 관 하단선은 시작점과 유출 벽 전면 기준선 교차점을 잇는다.
const pipeStart = basinPipeEnd ?? inlet;
/** 유출 관 하단 끝점 목표 = 벽 바닥 기준 0.5m 상단(기준선)과 **전면 경사선의
* 교차점**(2026-08-22 사용자 ① — 성토부선 시작 규칙과 같은 자리). */
const outletPipeEnd = (wall: WallLayout): OffsetPoint => {
// 기준 = 바닥 +0.5(= 관 invert 자리 — 2026-08-23부터 바닥은 invert−0.5 고정).
const reference = wall.bottomBack.elevation + REVET_EMBED_DEPTH_M;
return {
offset:
wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - reference),
elevation: reference,
};
};
// 유출 벽이 사면 끝과 다른 자리면 관 끝도 그 자리 기준으로 맞춘다(올림 연장분은
// 유출 쪽 — 2026-08-20 확정). 관 하단선은 전면 기준선 교차점을 지나 정수 길이로.
if (outletWall) {
@@ -631,66 +589,26 @@ export function computeCulvertLayout(
)
: null;
// 다단 벽 기울기 — 같은 측점·같은 설치 측 규칙으로 높이마다 표준경사(형태 무관 · B08 은 다단을 안 셈).
const extraLean = (form: string, pureHeightM: number): number =>
outlineLeanOf({
form,
height_m: pureHeightM,
section_mode: section.design?.section_mode,
side: culvert.hidden_pipe ? culvert.inlet.face_side : null,
});
// 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로).
// 유출측과 **집수정 계류측**이 같은 체계를 쓴다: 5m 넘으면 다단을 둘 수 있다.
const outletStart = outletWall ? pipeCorners.outlet.bottom : null;
const outletInput = {
startBottomElevation: outletWall?.bottomBack.elevation ?? 0,
outward: outletInfo.outward,
// 다단 흙막이(유출측 · 집수정 계류측) — `_Culvert_Tiers` 가 푼다.
const { extras, basinExtras, extrasForArea, basinExtrasForArea } = buildCulvertTiers({
section,
outletStart: outletWall ? pipeCorners.outlet.bottom : null,
outletBottom: outletWall?.bottomBack.elevation ?? 0,
basinStart: culvert.hidden_pipe
? inletWall
? pipeCorners.inlet.bottom
: null
: basinFillStart,
basinBottom: culvert.hidden_pipe ? (inletWall?.bottomBack.elevation ?? 0) : basinFillBottom,
outletInfo,
inletInfo,
groundAt,
limitOffset: outletInfo.limit,
adjusts: (revetShift?.extras ?? []).map(adjustOf),
ownerForm: outletWallSpec.form,
extras: (revetShift?.extras ?? []).map(adjustOf),
basinExtras: (revetShift?.basinExtras ?? []).map(adjustOf),
outletForm: outletWallSpec.form,
inletForm: inletWallSpec.form,
equalize: equalizeExtras === true,
leanFor: extraLean,
};
const extras = buildExtrasAt(outletStart, outletInput);
// 독립 기슭막이(관 숨김)는 집수정이 없어 이 채널이 **유입측 벽의 성토부선·다단**이다 —
// 유출측(`extras`)과 같은 시작점(관 하단 꼭짓점)·같은 `bextra` 키(2026-08-29 사용자).
const basinStart = culvert.hidden_pipe
? inletWall
? pipeCorners.inlet.bottom
: null
: basinFillStart;
const basinInput = {
startBottomElevation: culvert.hidden_pipe
? (inletWall?.bottomBack.elevation ?? 0)
: basinFillBottom,
outward: inletInfo.outward,
groundAt,
limitOffset: inletInfo.limit,
adjusts: (revetShift?.basinExtras ?? []).map(adjustOf),
ownerForm: inletWallSpec.form,
equalize: false,
leanFor: extraLean,
};
const basinExtras = buildExtrasAt(basinStart, basinInput);
/** ⭐ 면적용 다단 — **미확정 단(높이 안 적음)부터 아래는 없는 것**으로 봄(2026-09-14 브레인 판정
* 「미확정 벽은 성토도 안 깎음 · 벽 금액과 성토 감소가 함께」). 그림은 그대로 다 그림.
* B08 도 첫 미확정 단 아래는 미확정으로 셈(`facility_structures`) — 두 곳이 같은 선에서 끊김. */
const confirmedOnly = (
built: OutletExtrasResult,
start: OffsetPoint | null,
input: typeof outletInput,
): OutletExtrasResult => {
const firstUnset = built.appliedAdjusts.findIndex((applied) => applied.h == null);
if (firstUnset < 0) return built;
return buildExtrasAt(start, {
...input,
adjusts: input.adjusts.slice(0, firstUnset),
equalize: false,
});
};
const extrasForArea = confirmedOnly(extras, outletStart, outletInput);
const basinExtrasForArea = confirmedOnly(basinExtras, basinStart, basinInput);
});
// ── 성토 사면 구간 확정. 벽 자리가 굳은 뒤에 물매를 역산해야 관 길이 맞춤(벽 이동)이
// 접점을 다시 깨뜨리지 않는다 — 종전 어긋남의 직접 원인이 이 순서였다.
@@ -714,10 +632,6 @@ export function computeCulvertLayout(
// **물량이 안 바뀐다**(2026-09-07 사용자 확정). 기준벽 사면은 노견~벽 상단까지고, 그
// 바깥은 지금까지 원지반으로 봐서 면적이 0이었다. 화면이 그리는 선(`outletFill.segments`)과
// 같은 선을 면적도 보게 맞춘다 — 그리지 않는 `cut` 갈래(벽이 원지반에 묻힌 자리)는 뺀다.
const drawnFillPoints = (result: OutletExtrasResult): OffsetPoint[] =>
result.segments
.filter((segment) => segment.kind !== "cut")
.flatMap((segment) => segment.points);
// ↓ 되돌릴 자리(2026-09-07) — 「단이 2개 이상일 때만 반영」으로 좁히려면 아래 두 줄의
// `drawnFillPoints(extras)` 를 `extras.walls.length ? drawnFillPoints(extras) : []` 로
// 바꾸면 된다(basinExtras 도 같은 꼴). 다만 그러면 화면이 그리는 성토부선과 면적이
@@ -0,0 +1,128 @@
/* =============================================================================
* B06_Section_UI_Cross_Culvert_Tiers.ts
* 배수관 세트 기하(`_Cross_Culvert_Geom`)에서 떼어 낸 **벽 자리 도우미 + 다단 흙막이** 층.
* 다단 벽은 `buildExtrasAt`(공용 `buildRevetWallGeometry`)이 세우고, 여기서는 유출측과
* 집수정 계류측 두 채널의 입력을 짜고 면적용(미확정 단 제외) 결과를 함께 돌려준다.
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { REVET_EMBED_DEPTH_M, REVET_TRAP_TOP_M } from "./B06_Section_UI_Cross_Culvert_Const";
import type { OutletExtrasResult } from "./B06_Section_UI_Cross_Culvert_Extra";
import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra";
import { fillWallBaseWidth } from "./B06_Section_UI_Cross_Culvert_Solve";
import type { OffsetPoint, WallAdjust, WallLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
// d의 0점 = **성토선 길이 0 지점**(벽 상단 = 노견 — 2026-08-23 사용자 확정).
// 그 자리(하단선 중점)는 이음선 상단점이 노견 끝에 오는 offset이고, 기본 자리는
// 성토선을 타고 경사길이 4m 내려간 지점(PIPE_WALL_DEFAULT_RUN_M)이다.
export const slopeZeroAnchor = (
edge: { offset_m: number },
outward: number,
wallHeight: number,
lean: number,
): number => edge.offset_m + outward * (fillWallBaseWidth(wallHeight, lean) / 2 - REVET_TRAP_TOP_M);
/** 유출 관 하단 끝점 목표 = 벽 바닥 기준 0.5m 상단(기준선)과 **전면 경사선의
* 교차점**(2026-08-22 사용자 ① — 성토부선 시작 규칙과 같은 자리). */
export const outletPipeEnd = (wall: WallLayout): OffsetPoint => {
// 기준 = 바닥 +0.5(= 관 invert 자리 — 2026-08-23부터 바닥은 invert−0.5 고정).
const reference = wall.bottomBack.elevation + REVET_EMBED_DEPTH_M;
return {
offset:
wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - reference),
elevation: reference,
};
};
// 다단 기슭막이의 성토부선까지 트림에 넣는다 — 넣지 않으면 단을 올려도 폐회로가 그대로라
// **물량이 안 바뀐다**(2026-09-07 사용자 확정). 화면이 그리는 선(`outletFill.segments`)과
// 같은 선을 면적도 보게 맞춘다 — 그리지 않는 `cut` 갈래(벽이 원지반에 묻힌 자리)는 뺀다.
export const drawnFillPoints = (result: OutletExtrasResult): OffsetPoint[] =>
result.segments.filter((segment) => segment.kind !== "cut").flatMap((segment) => segment.points);
interface SideInfo {
outward: number;
limit: number;
}
/** 다단 흙막이 두 채널 — 유출측(`extras`)과 집수정 계류측·독립 기슭막이 유입측(`basinExtras`). */
export function buildCulvertTiers(input: {
section: CrossSection;
outletStart: OffsetPoint | null;
outletBottom: number;
basinStart: OffsetPoint | null;
basinBottom: number;
outletInfo: SideInfo;
inletInfo: SideInfo;
groundAt: (offset: number) => number;
extras: WallAdjust[];
basinExtras: WallAdjust[];
outletForm: string;
inletForm: string;
equalize: boolean;
}): {
extras: OutletExtrasResult;
basinExtras: OutletExtrasResult;
extrasForArea: OutletExtrasResult;
basinExtrasForArea: OutletExtrasResult;
} {
const { section, groundAt } = input;
const culvert = section.culvert;
// 다단 벽 기울기 — 같은 측점·같은 설치 측 규칙으로 높이마다 표준경사(형태 무관 · B08 은 다단을 안 셈).
const extraLean = (form: string, pureHeightM: number): number =>
outlineLeanOf({
form,
height_m: pureHeightM,
section_mode: section.design?.section_mode,
side: culvert?.hidden_pipe ? culvert.inlet.face_side : null,
});
// 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로).
// 유출측과 **집수정 계류측**이 같은 체계를 쓴다: 5m 넘으면 다단을 둘 수 있다.
const outletInput = {
startBottomElevation: input.outletBottom,
outward: input.outletInfo.outward,
groundAt,
limitOffset: input.outletInfo.limit,
adjusts: input.extras,
ownerForm: input.outletForm,
equalize: input.equalize,
leanFor: extraLean,
};
const extras = buildExtrasAt(input.outletStart, outletInput);
// 독립 기슭막이(관 숨김)는 집수정이 없어 이 채널이 **유입측 벽의 성토부선·다단**이다 —
// 유출측(`extras`)과 같은 시작점(관 하단 꼭짓점)·같은 `bextra` 키(2026-08-29 사용자).
const basinInput = {
startBottomElevation: input.basinBottom,
outward: input.inletInfo.outward,
groundAt,
limitOffset: input.inletInfo.limit,
adjusts: input.basinExtras,
ownerForm: input.inletForm,
equalize: false,
leanFor: extraLean,
};
const basinExtras = buildExtrasAt(input.basinStart, basinInput);
/** ⭐ 면적용 다단 — **미확정 단(높이 안 적음)부터 아래는 없는 것**으로 봄(2026-09-14 브레인 판정
* 「미확정 벽은 성토도 안 깎음 · 벽 금액과 성토 감소가 함께」). 그림은 그대로 다 그림.
* B08 도 첫 미확정 단 아래는 미확정으로 셈(`facility_structures`) — 두 곳이 같은 선에서 끊김. */
const confirmedOnly = (
built: OutletExtrasResult,
start: OffsetPoint | null,
channel: typeof outletInput,
): OutletExtrasResult => {
const firstUnset = built.appliedAdjusts.findIndex((applied) => applied.h == null);
if (firstUnset < 0) return built;
return buildExtrasAt(start, {
...channel,
adjusts: channel.adjusts.slice(0, firstUnset),
equalize: false,
});
};
return {
extras,
basinExtras,
extrasForArea: confirmedOnly(extras, input.outletStart, outletInput),
basinExtrasForArea: confirmedOnly(basinExtras, input.basinStart, basinInput),
};
}
@@ -11,8 +11,6 @@ import type { BasinAdjust, CulvertDesignTrim } from "./B06_Section_UI_Cross_Culv
import {
basinSpanOfSpec,
FILL_SLOPE_RATIO_MIN,
materialLabel,
materialLimit,
revetSpanOfSpec,
} from "./B06_Section_UI_Cross_Culvert_Const";
import { EXTRA_SPAN_DEFAULT } from "./B06_Section_UI_Cross_Culvert_Const";
@@ -539,12 +537,6 @@ export function computeCardCulvert(
applied,
]),
];
const wallOf = (role: RevetKey) =>
role.startsWith("bextra")
? layout.basinExtras[Number(role.slice(6))]
: role.startsWith("extra")
? layout.extraWalls[Number(role.slice(5))]
: layout.walls.find((wall) => wall.role === role);
for (const [role, applied] of roles) {
const requested = revetOffset.adjustFor(section, role);
// 축별로 요청이 잘렸으면 왜 안 됐는지 토스트로 알린다(조작 중인 벽만).
@@ -565,17 +557,6 @@ export function computeCardCulvert(
"info",
);
}
// 높이가 재질 한계에 잘렸으면 재질 변경을 안내(2026-08-22 사용자 — 경고와
// 높이 해제는 재질 선택으로).
const wall = wallOf(role);
if (requested.h != null && applied.h != null && requested.h > applied.h + 0.05 && wall) {
showToast(
L("B06_Cross_Height_Limit")
.replace("{mat}", materialLabel(wall.material))
.replace("{limit}", materialLimit(wall.material).toFixed(1)),
"info",
);
}
if (requested.h != null && applied.h != null && requested.h < applied.h - 0.05) {
showToast(L("B06_Cross_Height_Floor"), "info");
}
+10 -22
View File
@@ -24,9 +24,8 @@ import {
PIPE_WALL_DEFAULT_RUN_M,
REVET_EMBED_DEPTH_M,
REVET_THICKNESS_M,
revetHeightLimit,
} from "./B06_Section_UI_Cross_Culvert_Const";
import { leanOf } from "./B06_Section_UI_Cross_Lean";
import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
import { buildExtrasAt, slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra";
@@ -66,12 +65,6 @@ export interface RevetmentSpec {
face_slope_ratio?: number | string | null;
}
/** 표준경사 표를 읽는 벽 종류 → 형태. B08 이 돌쌓기 식으로 세는 것만(큰돌쌓기·옹벽은 종전 0.3). */
const TABLE_FORM_BY_TYPE: Record<string, string> = {
masonry_wet: "돌쌓기(찰)",
masonry_dry: "돌쌓기(메)",
};
/** 조정창 조작값 중 이 벽이 쓰는 축 — 배관 벽과 같은 형태(x 좌우·d 사면·h 높이). */
export interface RevetmentAdjust {
x?: number;
@@ -187,19 +180,15 @@ export function computeRevetmentLayout(
const limit = sampleLimit(section, outward);
if (!edge || !groundAt || limit === null) return null;
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준,
// 2026-08-29 사용자). 재질(형태) 한계도 이 기준으로 자른다.
const pureHeight = Math.min(
Math.max(requestedHeight, REVET_EMBED_DEPTH_M + 0.1),
revetHeightLimit(spec.form),
);
// 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준).
// 형태별 높이 한계 없음 — 사용자 입력 그대로(하한만 둠).
const pureHeight = Math.max(requestedHeight, REVET_EMBED_DEPTH_M + 0.1);
/** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */
const height = pureHeight - REVET_EMBED_DEPTH_M;
// 전면 기울기 — B08 이 그 벽을 셀 때와 같은 칸(종류·요청 높이·단면유형·설치 측·사용자 칸)으로
// 표준경사를 고름(2026-09-14 B5). 높이는 형태 한계로 자르기 **전** 값 — B08 은 저장 높이를 씀.
const lean = leanOf({
form: TABLE_FORM_BY_TYPE[spec.type_id] ?? (spec.type_id === "revetment" ? spec.form : null),
// 전면 기울기 — 관 흙막이와 같은 `outlineLeanOf`(형태 무관 · 형태는 안 무늬만). 단면유형 ·
// 설치 측 · 높이 · 사용자 칸은 그대로 탐. 수량(B08)은 자기 칸으로 따로 셈.
const lean = outlineLeanOf({
height_m: requestedHeight,
section_mode: design.section_mode,
side: spec.side,
@@ -264,6 +253,7 @@ export function computeRevetmentLayout(
lengthM,
floatGapM,
lean,
roadElevation: edge.elevation_m,
});
// 넣은 좌우 이동이 **각도 하한**에 눌려 통째로 무시됐으면 그 하한을 남긴다(2026-09-07).
// 실측 — d 2.6m 자리에서 하한이 1.0m 를 넘어 「1.0m 를 넣어도 0.00m 이동」이 났고,
@@ -288,7 +278,7 @@ export function computeRevetmentLayout(
ownerForm: spec.form,
equalize: false,
leanFor: (form, pureHeightM) =>
leanOf({
outlineLeanOf({
form,
height_m: pureHeightM,
section_mode: design.section_mode,
@@ -402,9 +392,7 @@ export function appendRevetmentOverlay(
const keyId = index === 0 ? "own" : `own-extra${index - 1}`;
const tooltip =
`독립 기슭막이 ${wall.form ?? ""} H=${(wall.height + REVET_EMBED_DEPTH_M).toFixed(1)}m` +
`(상단 = 성토선 접점, 전면 1:${wall.lean}(품셈 13-4-4 [주]⑪ 표준경사), 높이 한계 ${revetHeightLimit(
wall.form,
).toFixed(1)}m — 교본 7-3)` +
`(상단 = 성토선 접점, 전면 1:${wall.lean}(품셈 13-4-4 [주]⑪ 표준경사))` +
(wall.floatGapM > 0.01
? ` · ⚠ 바닥 원지반 이격 ${wall.floatGapM.toFixed(2)}m — 하부 지지 별도`
: ` · 근입 ${REVET_EMBED_DEPTH_M.toFixed(2)}m`) +
@@ -15,7 +15,7 @@
* ========================================================================== */
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
import { REVET_FORMS, revetHeightLimit } from "./B06_Section_UI_Cross_Culvert_Const";
import { REVET_FORMS } from "./B06_Section_UI_Cross_Culvert_Const";
import type { BasinAdjust, WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import type { SpanValues } from "./B06_Section_UI_Cross_Culvert_Wire";
import { L } from "./B06_Section_UI_Section_Common";
@@ -230,7 +230,6 @@ export function buildStructurePanel(
const option = document.createElement("option");
option.value = form;
option.textContent = form;
option.title = `${L("B06_Cross_Height_Label")} ~${revetHeightLimit(form).toFixed(1)}m`;
materialSelect.append(option);
}
materialSelect.addEventListener("change", () => {
+10 -4
View File
@@ -5,9 +5,8 @@
* (2026-08-28 사용자: "배관용에서 배관만 빼면 대부분 동일해야"). 벽 1매의 합성 단면
* (하부 사다리꼴 + 상부 평행사변형, 배면 수직·전면 1:0.3)과 그 돌쌓기 표현은 여기 하나뿐.
*
* 배관 벽 계산은 아직 `_Cross_Culvert_Geom`이 자체 인라인으로 만든다 — 결과가 이 함수와
* 동일함을 화면으로 확인한 뒤 그쪽도 이 함수로 바꾼다(계획서 후속). 지금은 독립 기슭막이가
* 이 함수를 써서 배관 벽과 같은 모양·자리로 선다.
* 관 유입·유출 흙막이 · 추가 흙막이 · 사면안정 흙막이 · 옹벽이 모두 이 함수 하나로 선다.
* 형태가 바꾸는 것은 안 무늬(`_Cross_Wall_Hatch`)뿐 — 외곽은 높이·자리·기울기로만 정함.
* ========================================================================== */
import {
@@ -57,6 +56,8 @@ export interface RevetWallGeometryInput {
extraIndex?: number;
/** 전면 기울기 1:n 의 n — 표준경사(`leanOf`). 없으면 종전 0.3. */
lean?: number;
/** 노면(계획고) — 벽 윗면이 이보다 높으면 여기서 멈춤. 높이 한도는 이것 하나. */
roadElevation?: number | null;
}
/**
@@ -65,7 +66,12 @@ export interface RevetWallGeometryInput {
* 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초.
*/
export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout {
const { anchor, outward, height, material, role } = input;
const { anchor, outward, material, role } = input;
const road = input.roadElevation;
const height =
road != null && anchor.elevation + input.height > road + 1e-9
? Math.max(road - anchor.elevation, 0)
: input.height;
const thickness = input.thickness ?? REVET_THICKNESS_M;
const lean = input.lean ?? REVET_LEAN_RATIO;
const baseWidth = thickness * 1.5 + lean * height;
@@ -16,8 +16,6 @@ import {
applySpanPatch,
EXTRA_WALL_MIN_HEIGHT_M,
REVET_EMBED_DEPTH_M,
revetFormOf,
revetHeightLimit,
revetWallSpec,
} from "./B06_Section_UI_Cross_Culvert_Const";
import type { CrossSection } from "./B06_Section_Api_Fetch";
@@ -121,8 +119,7 @@ export function applyPipeOptionsToCache(
h: height ?? blank,
m: typeof form === "string" ? form : blank,
});
// 형태마다 높이 한계가 있다(돌쌓기(메) 2.0m 등). 기하가 잘라 낸 실제 높이를
// 폼에 되돌린다 — 숫자만 커지고 그림은 그대로인 상태를 남기지 않는다.
// 높이 하한(관경 기준 최소)에 걸린 실제 높이를 폼에 되돌린다 — 형태별 상한은 없음.
if (height === undefined) continue;
const applied = revetWallSpec(
role === "outlet" ? culvert.outlet : culvert.inlet,
@@ -135,8 +132,8 @@ export function applyPipeOptionsToCache(
[`${prefix}_revet_height_m`]: Number(applied.toFixed(1)),
});
showToast(
`${prefix === "outlet" ? "유출" : "유입"} 기슭막이 높이는 형태 한계로 ` +
`${applied.toFixed(1)}m까지만 적용됩니다.`,
`${prefix === "outlet" ? "유출" : "유입"} 기슭막이 높이는 최소 ` +
`${applied.toFixed(1)}m로 적용됩니다.`,
"error",
);
}
@@ -170,7 +167,7 @@ const soilSent = new Map<string, { h: number | null; m: string | null }>();
* 흙막이 n 컨테이너(`soil_add_{n}_*`) → 유출 다단 n 단(`extra{n-1}`) 그림(54-10). 형태 · 높이 = 조정값
* (m · h · 빈 칸 = 자동) · 길이 · 전 · 후 = 단별 구간값(빈 칸 = 그대로). 지형이 세운 단까지만 —
* 넘친 컨테이너 값은 파일에만 남음. 높이 = 요청 · 서는 키는 기하 규칙 그대로(지형 맞춤 · 형태 한계) ·
* 형태 한계 · 하한을 넘은 요청만 폼에 되돌림.
* 하한을 넘은 요청만 폼에 되돌림(형태별 상한 없음).
*/
function applySoilTiers(
ctx: PipeOptionsContext,
@@ -178,13 +175,6 @@ function applySoilTiers(
patch: Record<string, number | string>,
whole: boolean,
): void {
const culvert = owner.culvert!;
const ownerForm = revetWallSpec(
culvert.outlet,
ctx.revetOffset.adjustFor(owner, "outlet"),
false,
culvert.diameter_m,
).form;
const value = (n: number, item: string): number | undefined => {
const raw = patch[`soil_add_${n}_${item}`];
return raw === undefined || raw === "" || !Number.isFinite(Number(raw))
@@ -202,20 +192,10 @@ function applySoilTiers(
m: typeof form === "string" && form ? form : blank,
};
if (want.h != null) {
const limit = revetHeightLimit(
revetFormOf(want.m ?? ctx.revetOffset.adjustFor(owner, role).m, ownerForm),
);
const h = Math.min(
Math.max(want.h, EXTRA_WALL_MIN_HEIGHT_M, REVET_EMBED_DEPTH_M + 0.1),
limit,
);
const h = Math.max(want.h, EXTRA_WALL_MIN_HEIGHT_M, REVET_EMBED_DEPTH_M + 0.1);
if (Math.abs(h - want.h) > 0.05) {
clipped[`soil_add_${n}_height`] = Number(h.toFixed(1));
showToast(
`흙막이 ${n} 높이는 ${h < want.h ? "형태 한계로" : "최소"} ${h.toFixed(1)}m` +
`${h < want.h ? "까지만" : "로"} 적용됩니다.`,
"error",
);
showToast(`흙막이 ${n} 높이는 최소 ${h.toFixed(1)}m로 적용됩니다.`, "error");
}
want.h = Number(h.toFixed(1));
}
@@ -0,0 +1,98 @@
"""횡단 벽 형상 한 벌 — 관 유입·유출 흙막이 · 추가 흙막이 · 사면안정 흙막이 · 옹벽.
규칙(10-6 사용자):
① 네 벽 모두 공용 `buildRevetWallGeometry` 로 선다 — 형태는 안 무늬만 바꿈(외곽 같음).
② 형태별 높이 한도(메 2.0 · 찰 3.0 · 콘크리트 5.0) · 원지반 맞춤 자동 자르기 없음.
③ 벽 윗면은 노면 위로 올라오지 않음(넘으면 노면에서 멈춤).
서버 면적은 같은 TS 를 Node 로 돌리므로(`B06_Section_Server_Calc_Node.ts`) 이 함수가 곧 서버 기하.
"""
import json
import re
import subprocess
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[2]
B06 = PROJECT_ROOT / "B06_Section"
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { writeFileSync } from "node:fs";
import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall.js";
import { revetWallSpec, REVET_FORMS } from "./B06_Section_UI_Cross_Culvert_Const.js";
const anchor = { offset: 5, elevation: 100 };
const outline = (form, role, extra = {}) =>
buildRevetWallGeometry({ anchor, outward: 1, height: 2.5, material: "dry", role, form, ...extra })
.points.map((p) => [p.offset, p.elevation]);
writeFileSync(process.argv[2], JSON.stringify({
forms: REVET_FORMS.map((form) => outline(form, "outlet")),
roles: ["inlet", "outlet", "extra"].map((role) => outline("돌쌓기(메)", role)),
capped: buildRevetWallGeometry({
anchor, outward: 1, height: 5, material: "dry", role: "outlet", roadElevation: 103,
}).topJoint.elevation,
under: buildRevetWallGeometry({
anchor, outward: 1, height: 2, material: "dry", role: "outlet", roadElevation: 103,
}).topJoint.elevation,
dry: revetWallSpec({ revet_form: "돌쌓기(메)" }, { h: 3.0 }, true, 0.3).pureHeight,
concrete: revetWallSpec({ revet_form: "콘크리트" }, { h: 7.5 }, true, 0.3).pureHeight,
}));
"""
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_벽_외곽_한_벌_높이_입력_그대로_노면_멈춤(tmp_path: Path) -> None:
out = tmp_path / "js"
# 형(type) import 가 끌어오는 페이지 파일은 별칭을 못 풀어 오류를 내지만 산출물은 나옴.
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(B06 / "B06_Section_UI_Cross_Wall.ts"),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--skipLibCheck",
"--ignoreConfig",
],
cwd=str(PROJECT_ROOT),
capture_output=True,
)
for emitted in out.rglob("*.js"):
emitted.write_text(
re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")),
encoding="utf-8",
)
base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Wall.js"))
(base / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "out.json"
subprocess.run( # noqa: S603
["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True
)
got = json.loads(result.read_text(encoding="utf-8"))
assert all(outline == got["forms"][0] for outline in got["forms"]), "형태마다 외곽이 다름"
assert all(outline == got["roles"][0] for outline in got["roles"]), "벽 갈래마다 외곽이 다름"
assert got["capped"] == pytest.approx(103.0), "노면 위로 올라옴"
assert got["under"] == pytest.approx(102.0), "노면 아래 벽이 잘림"
assert got["dry"] == pytest.approx(3.0), "돌쌓기(메) 3.0 이 잘림"
assert got["concrete"] == pytest.approx(7.5), "콘크리트 7.5 가 잘림"
def test_네_벽이_공용_함수로_서고_형태_한도가_없다() -> None:
for name in ("Culvert_Geom", "Culvert_Extra", "Revetment"):
source = (B06 / f"B06_Section_UI_Cross_{name}.ts").read_text(encoding="utf-8")
assert "buildRevetWallGeometry(" in source, f"{name} 가 공용 벽 함수를 안 씀"
for path in B06.glob("*.ts"):
source = path.read_text(encoding="utf-8")
assert "revetHeightLimit" not in source, f"{path.name} 에 형태 높이 한도가 남음"
assert "materialLimit" not in source, f"{path.name} 에 재질 높이 한도가 남음"
extra = (B06 / "B06_Section_UI_Cross_Culvert_Extra.ts").read_text(encoding="utf-8")
assert "fitted" not in extra, "추가 흙막이가 원지반에 맞춰 늘어남"
+6 -2
View File
@@ -220,8 +220,12 @@ def test_저장된_표준단면이_브라우저까지_간다():
# 값으로 재는 확인은 화면 실측에 있음(보조 창 b269ea34 81.77m — 단 0 → 1 에서 성토
# 10.9027 → 10.6743㎡). 계획서에 그 수치가 남아 있다.
def test_다단_성토부선이_면적_트림에_들어간다():
source = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Geom.ts").read_text(
encoding="utf-8"
# 다단 · 묻힌 구간 거르기는 `_Culvert_Tiers` 로 나뉘어 있어 두 파일을 함께 읽음.
source = "".join(
(PROJECT_ROOT / "B06_Section" / f"B06_Section_UI_Cross_Culvert_{name}.ts").read_text(
encoding="utf-8"
)
for name in ("Geom", "Tiers")
)
assert "const extendTrimSlope = (" in source, "트림 확장 자리가 사라짐"
# 유출측·집수정 계류측 **둘 다** — 한쪽만 넣으면 그쪽 단만 물량에 잡힌다.
-4
View File
@@ -267,10 +267,6 @@ export const ui_locales_b2 = {
B06_Cross_Move_Label: ["이동(좌우·사면 상하)", "Move (lateral / along slope)"],
B06_Cross_Height_Minus: ["높이 −0.1m", "Height −0.1m"],
B06_Cross_Height_Plus: ["높이 +0.1m", "Height +0.1m"],
B06_Cross_Height_Limit: [
"{mat} 높이 한계 {limit}m — 더 올리려면 재질을 변경하세요",
"{mat} height limit {limit}m — change material to go higher",
],
B06_Cross_Height_Floor: ["최소 높이라 더 낮출 수 없습니다", "Already at the minimum height"],
B06_Cross_Basin_Limit_Pipe: [
"여기까지입니다 — 더 옮기면 배관 길이가 달라집니다(I형은 관을 감싸는 구조)",