fix(B06): 박스 흙막이 기본 높이 = 박스 높이 값 · 저장 x 벽 종전 자리 그대로 · 흙막이 조정창 제목 · 박스 날개벽 툴팁(79b)

- 박스 흙막이 높이 빈 칸 = 박스 높이 값(2 m 박스 → 2 m) · 윗면 = 본체 윗면 · 바닥 = 윗면 − 높이 · 세월교는 그대로
- x 원점(사면 경사길이 4 m 지점) 되돌림 — 손대지 않은 벽(x · d 빈 칸)만 성토사면 제일 위 · 손으로 x 를 넣은 독립 기슭막이 · 옹벽 · 관 보호공은 종전 자리
- 세월교 · 박스 흙막이 조정창 제목 「흙막이(유입 · 유출)」 · 박스 에이프런 툴팁 유입 · 유출 날개벽 이름 바로잡음
- 시험: test_79_guard_shape — 박스 높이 값 · 저장 x 있는 관 보호공 · 독립 기슭막이 = 종전 자리

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-08 00:26:10 +09:00
co-authored by Claude Opus 5.5
parent e9539fcb12
commit 77e8aa2709
17 changed files with 128 additions and 44 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ const guardContainer = (
{ value: "흙막이", label: "흙막이" },
],
},
...bodyGuardItems(id),
...bodyGuardItems(id, "박스 높이(자동)"),
],
});
+1 -1
View File
@@ -35,7 +35,7 @@ const guardContainer = (id: "inlet" | "outlet", title: string, link: string): Sp
key: `${id}_type`,
choices: guard(id === "inlet" ? "in" : "out"),
},
...bodyGuardItems(id),
...bodyGuardItems(id, "본체 끝 높이(자동)"),
],
});
+3 -3
View File
@@ -82,11 +82,11 @@ export const revetKeys = (side: "inlet" | "outlet"): Record<string, string> => (
after: `${side}_revet_after_m`,
});
/** 세월교 · 박스 유입 · 유출 흙막이 칸(79-7) — 높이 빈 칸 = 본체 끝 높이(횡단도가 셈) ·
/** 세월교 · 박스 유입 · 유출 흙막이 칸(79-7) — 높이 빈 칸 = 자동(`heightHint` · 횡단도가 셈) ·
* 절토부 앞 수평공간 칸(`{side}_revet_berm_m`)을 더함. */
export function bodyGuardItems(side: "inlet" | "outlet"): SpecItem[] {
export function bodyGuardItems(side: "inlet" | "outlet", heightHint: string): SpecItem[] {
const items = guardItems({ 흙막이: revetKeys(side) }, undefined, true).map((item) =>
item.id === "height" ? { ...item, default: undefined, hint: "본체 끝 높이(자동)" } : item,
item.id === "height" ? { ...item, default: undefined, hint: heightHint } : item,
);
const berm: SpecItem = {
id: "berm",
+14 -5
View File
@@ -4,7 +4,8 @@
*
* 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D ·
* 면적 길을 그대로 탐. 다른 점은 자동 자리 하나 — 벽 **배면(수직)이 본체 끝**에 붙어 벽 몸이 본체
* 바깥에 섬 · 벽 윗면 = 본체 윗면(세월교 상판 · 박스 상판) · 높이 기본 = 본체 끝 높이(79-7 · 그림
* 바깥에 섬 · 벽 윗면 = 본체 윗면(세월교 상판 · 박스 상판) · 높이 기본 = 세월교 본체 끝 높이 ·
* 박스 높이 값(2 m 박스 → 2 m · 79b · 바닥 = 윗면 − 높이)(79-7 · 그림
* `tmp/79_세월교_기본위치.png`). 좌우 x 는 그 자리 기준 · 상하 d 는 배관과 같은 절대값.
* 본체가 이 측점에 없으면(연동 옆 측점) 배관 규칙 그대로 — 소유 측점 값을 절대값으로 넘겨 받음.
* ========================================================================== */
@@ -25,11 +26,12 @@ import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall";
type Role = "inlet" | "outlet";
/** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) 표고(79-7). */
/** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) 표고 · 흙막이 기본 순수 높이(79-7 · 79b). */
export interface BodyEnd {
offset: number;
top: number;
bottom: number;
height: number;
}
/** 본체 끝(세월교 관 끝 · 박스 끝) + 본체 설계선 트림 — 본체가 이 측점에 없으면 null. */
@@ -56,7 +58,13 @@ export function bodyGuardBase(
(s) => (s.role === "left") === ((role === "inlet") === inletOnLeft),
);
return side
? { offset: side.offset, top: side.topElevation, bottom: side.bottomElevation }
? {
offset: side.offset,
top: side.topElevation,
bottom: side.bottomElevation,
// 박스 높이 값 그대로(사용자 원문 2 m → 2 m · 79b)
height: layout.box.inner_height_m,
}
: null;
};
return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim };
@@ -75,6 +83,7 @@ export function bodyGuardBase(
offset: side.pipeEnd.offset,
top: side.top.elevation,
bottom: side.slabTopElevation - layout.ford.slab_thickness_m,
height: side.top.elevation - (side.slabTopElevation - layout.ford.slab_thickness_m),
}
: null;
};
@@ -83,8 +92,8 @@ export function bodyGuardBase(
return null;
}
/** 흙막이 기본 순수 높이(근입 포함) = 본체 끝 높이(윗면 ~ 바닥 · 79-7). */
export const bodyGuardHeight = (end: BodyEnd): number => end.top - end.bottom;
/** 흙막이 기본 순수 높이(근입 포함) — 세월교 = 본체 끝 높이 · 박스 = 박스 높이 값(79-7 · 79b). */
export const bodyGuardHeight = (end: BodyEnd): number => end.height;
/**
* 벽 배면을 본체 끝에 붙인 배관 벽 4축(x · d) — `placePipeWall` 역산.
+2 -1
View File
@@ -80,7 +80,8 @@ export function appendBoxOverlay(
side.apron,
"b06-chart__ford-slab",
`${mark}측 날개벽 구간 바닥 — 수평 · 두께 ${box.slab_thickness_m.toFixed(2)}m` +
` (날개벽 ${wingText(side.role === "left" ? box.wing_in : box.wing_out)} 투영 연장)`,
` (${side.wingRole === "inlet" ? "유입" : "유출"} 날개벽 ` +
`${wingText(side.wingRole === "inlet" ? box.wing_in : box.wing_out)} 투영 연장)`,
),
);
}
+5 -1
View File
@@ -65,6 +65,8 @@ export interface BoxSideLayout {
fillSegments: OutletFillSegment[];
/** 끝이 원지반에 묻힐 때의 절토선(계획선). */
cutLine: BasinLayout["cutLine"];
/** 이 끝의 날개벽 칸 — 유입(상단측) · 유출(툴팁 이름 · 79b). */
wingRole: "inlet" | "outlet";
/** 적용된 조작값(한계에 걸린 뒤). */
adjust: BoxSideAdjust;
}
@@ -146,7 +148,8 @@ export function computeBoxLayout(
const offset = edgeOffset + outward * (reach + lengthM);
const bottomElevation = topElevation - bodyHeight;
// 날개벽 구간 바닥 — 각 끝 표고에서 **수평**(2026-08-25 사용자).
const wing = (role === "left") === inletOnLeft ? box.wing_in : box.wing_out;
const wingRole = (role === "left") === inletOnLeft ? "inlet" : "outlet";
const wing = wingRole === "inlet" ? box.wing_in : box.wing_out;
const extend = Math.max(wing.slab_extend_m, 0);
const outer = offset + outward * extend;
const apron: OffsetPoint[] = extend
@@ -195,6 +198,7 @@ export function computeBoxLayout(
fillLine,
fillSegments,
cutLine,
wingRole,
adjust: { lengthM, riseM },
};
};
@@ -184,19 +184,26 @@ export const FILL_SLOPE_RATIO_MAX = 2.0;
export const FILL_SLOPE_MAX_LENGTH_M = 5.0;
/**
* 흙막이 · 옹벽 · 관 보호공 기본 자리 = 성토사면을 따라 **가능한 범위 안 제일 위**(79-5) —
* 성토선 길이 0 지점(벽 상단 = 노견). 상하(d) 조작의 0점도 그 점이고, d는 1:1.2 성토선을
* 타고 내려간 수평 성분(m)이다. 관 보호공은 배치 풀이의 한계(토피 · 역경사)와 관 m 단위
* 맞춤(유출 벽을 사면 아래로 밂)이 그 위에서 자리를 정한다.
* 좌우(x) 원점 = 노견에서 성토사면 경사길이 4m 지점(2026-08-23 · 독립 기슭막이 · 옹벽의 x 원점).
* 상하(d) 조작의 0점은 성토선 길이 0 지점(벽 상단 = 노견)이고, d는 1:1.2 성토선을 타고
* 내려간 수평 성분(m)이다. 저장된 x 는 이 원점 기준이라 절대 자리가 그대로다(79b).
*/
export const PIPE_WALL_DEFAULT_SLOPE_M = 0;
/** 기본 자리의 수평 성분(m) — d 기본값. */
export const PIPE_WALL_DEFAULT_SLOPE_M = 4.0;
/** x 원점(경사길이 4m)의 수평 성분(m). */
/** 세월교 · 박스 흙막이 절토부 앞 수평공간 기본값(m · 79-7) — 흙막이 컨테이너 칸이 비면.
* 관 유입 접속선의 「1m 공간 확보」(2026-08-23 사용자)와 같은 값. */
export const GUARD_CUT_BERM_M = 1.0;
export const PIPE_WALL_DEFAULT_RUN_M =
(PIPE_WALL_DEFAULT_SLOPE_M * FILL_SLOPE_RATIO_MIN) / Math.hypot(1, FILL_SLOPE_RATIO_MIN);
/**
* 관 보호공 d(상하) 빈 칸의 기본값 — x 도 비었으면 성토사면 **제일 위**(0 · 79-5), 손으로 x 를
* 넣어 둔 벽은 종전 자리(경사길이 4m)라 절대 자리가 안 바뀜(79b). 관은 그 위에서 m 단위 맞춤이
* 유출 벽을 사면 아래로 민다.
*/
export const pipeWallDefaultD = (adjust: { x: number }): number =>
Math.abs(adjust.x) > 1e-9 ? PIPE_WALL_DEFAULT_RUN_M : 0;
/**
* 성토 전체고(노견 − 원지반) 기준(m) — 이 높이를 넘으면 1:1.2로도 사면이 5m를 넘어
* 구조물이 필요해진다(3m × √(1+1.2²) = 4.69m, 3.2m에서 5.0m). 기슭막이를 자연 사면
@@ -9,7 +9,7 @@
import type { CrossSection, CulvertSideSpec, SectionSample } from "./B06_Section_Api_Fetch";
import {
revetWallSpec,
PIPE_WALL_DEFAULT_RUN_M,
pipeWallDefaultD,
FILL_SLOPE_MAX_LENGTH_M,
FILL_SLOPE_RATIO_MAX,
FILL_SLOPE_RATIO_MIN,
@@ -250,7 +250,7 @@ export function computeCulvertLayout(
// 독립 기슭막이(관 숨김)는 관 토피·역경사 제약이 없다 — 유입도 노견까지 자유롭게 오르내린다.
invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : invertCap(inletInfo.edge),
adjust: onBody(adjInlet, inletBody),
defaultD: PIPE_WALL_DEFAULT_RUN_M,
defaultD: pipeWallDefaultD(adjInlet),
minX,
});
appliedAdjust.inlet.x = placed.x - (inletBody?.x ?? 0);
@@ -439,7 +439,7 @@ export function computeCulvertLayout(
// 독립 기슭막이는 양쪽 벽이 독립 — 유출을 유입 invert로 묶는 역경사 클램프를 뺀다.
invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : inlet.elevation,
adjust: onBody(adjOutlet, outletBody),
defaultD: PIPE_WALL_DEFAULT_RUN_M,
defaultD: pipeWallDefaultD(adjOutlet),
minX,
});
appliedAdjust.outlet.x = placed.x - (outletBody?.x ?? 0);
@@ -381,7 +381,7 @@ export function placePipeWall(input: {
/** invert 상한 — 유입: 노면−관경−토피, 유출: 유입 invert(역경사 방지). */
invertCap: number;
adjust: WallAdjust;
/** d 미지정(null) 시의 기본 자리 — 성토선 0 지점(제일 위 · 79-5). */
/** d 미지정(null) 시의 기본 자리 — 성토선 0 지점(제일 위 · 79-5) · x 를 넣은 벽은 4m 지점(79b). */
defaultD: number;
/** x 하한 — 기본 0(안쪽 금지) · 세월교 · 박스 흙막이는 본체 끝이 0점이라 안 막음(73-4). */
minX?: number;
@@ -19,7 +19,7 @@ import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean";
// d의 0점 = **성토선 길이 0 지점**(벽 상단 = 노견 — 2026-08-23 사용자 확정).
// 그 자리(하단선 중점)는 이음선 상단점이 노견 끝에 오는 offset이고, 기본 자리는
// 그 점 자체다(PIPE_WALL_DEFAULT_RUN_M = 0 · 79-5).
// 그 점 자체다 · 손으로 x 를 넣은 벽만 경사길이 4m 지점(`pipeWallDefaultD` · 79-5 · 79b).
export const slopeZeroAnchor = (
edge: { offset_m: number },
outward: number,
+13 -9
View File
@@ -202,20 +202,25 @@ export function computeRevetmentLayout(
const roadSlope = roadSlopePerOutward(design, side, outward);
const autoOffset =
edge.offset_m + outward * (fillWallBaseWidth(height, lean) / 2 - REVET_THICKNESS_M / 2);
// 자동 자리 = 성토사면(1:1.2) 제일 위(노견 · 79-5) — 배관 유출 벽과 같은 기준값.
// x 원점 = 성토사면(1:1.2) 경사길이 4m 지점 — 저장된 x 의 절대 자리 그대로(79b).
const autoRun = PIPE_WALL_DEFAULT_RUN_M;
/**
* 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직
* 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다
* (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다).
*/
const defaultDrop = autoRun / FILL_SLOPE_RATIO_MIN;
const requestedX = Math.max(adjust?.x ?? 0, 0);
/** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리. */
const xFloor = (drop: number): number => Math.max(0, FILL_SLOPE_RATIO_MIN * drop - autoRun);
// 손대지 않은 벽(x · d 빈 칸) = 성토사면 제일 위(노견 · 79-5) · 손으로 넣은 벽은 종전 기본(79b).
const untouched = adjust?.d == null && !adjust?.x;
const defaultDrop = untouched ? 0 : autoRun / FILL_SLOPE_RATIO_MIN;
/** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리 — 음수 = 원점보다
* 사면 위(노견 쪽)라 제일 위 자리는 x = −autoRun 이다. */
const xFloor = (drop: number): number => FILL_SLOPE_RATIO_MIN * drop - autoRun;
const requestedX = untouched ? Number.NEGATIVE_INFINITY : (adjust?.x ?? 0);
// 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0.
const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10;
const appliedX = Math.round(Math.max(requestedX, xFloor(appliedDrop)) * 10) / 10;
// 0.1 눈금 반올림(종전과 같음) · 반올림이 하한 아래로 내리면 하한(제일 위 자리가 노견 안쪽으로 안 감).
const floor = xFloor(appliedDrop);
const appliedX = Math.max(Math.round(Math.max(requestedX, floor) * 10) / 10, floor);
const anchorOffset = autoOffset + outward * (autoRun + appliedX);
const baseElevation = edge.elevation_m - appliedDrop - height;
@@ -238,9 +243,8 @@ export function computeRevetmentLayout(
// 넣은 좌우 이동이 **각도 하한**에 눌려 통째로 무시됐으면 그 하한을 남긴다(2026-09-07).
// 실측 — d 2.6m 자리에서 하한이 1.0m 를 넘어 「1.0m 를 넣어도 0.00m 이동」이 났고,
// 지형에 막힌 것이 아니라 `shiftBlockedM` 은 0 이어서 까닭을 알 길이 없었다.
const floorNow = xFloor(appliedDrop);
if (requestedX > 0.005 && floorNow > requestedX + 0.005) {
tier1.shiftFloorM = Math.round(floorNow * 100) / 100;
if (requestedX > 0.005 && floor > requestedX + 0.005) {
tier1.shiftFloorM = Math.round(floor * 100) / 100;
}
// 다단 — 배관과 같은 함수. 1단 벽 전면 하단 꼭짓점을 성토부선 시작점으로.
@@ -60,6 +60,8 @@ export interface StructurePanelDeps {
/** 독립 기슭막이(관 숨김) 카드인가 — 유입·유출 벽이 같은 성토 로직이라 조정창도
* 좌우 같은 행을 준다(집수정 형식 숨김 · 유입측 단 수 행 표시 — 2026-08-29). */
hiddenPipe: () => boolean;
/** 세월교 · BOX암거 흙막이 세트인가 — 조정창 제목을 「흙막이」로(79b). */
bodyGuard?: () => boolean;
/** 이동 조작 가능 여부 — 집수정(자리 고정)은 숨긴다. */
canNudge: (key: RevetKey) => boolean;
/** 좌우·상하 걸음이 0.1m인가(독립·추가 기슭막이). 배관 기준벽은 false = 1m. */
@@ -517,16 +519,19 @@ export function buildStructurePanel(
key === "inlet" && (structure === "I" || structure === "L" || structure === "U");
// 독립 기슭막이(관 숨김)의 기준벽은 관이 없어 유입·유출이라는 이름이 안 맞는다 —
// 좌·우 어느 쪽이든 "기슭막이(독립)"으로 적는다(2026-08-29 사용자).
const guardKey = key === "inlet" || key === "outlet";
title.textContent =
isOwn || (deps.hiddenPipe() && (key === "inlet" || key === "outlet"))
? L("B06_Cross_Revet_Own")
: isExtra
? L("B06_Cross_Revet_Extra").replace("{n}", String(extraIndex + 1))
: key === "outlet"
? L("B06_Cross_Revet_Outlet")
: movable
? L("B06_Cross_Revet_Inlet")
: L("B06_Cross_Struct_InletBasin");
deps.bodyGuard?.() && guardKey
? L(key === "inlet" ? "B06_Cross_Guard_Inlet" : "B06_Cross_Guard_Outlet")
: isOwn || (deps.hiddenPipe() && guardKey)
? L("B06_Cross_Revet_Own")
: isExtra
? L("B06_Cross_Revet_Extra").replace("{n}", String(extraIndex + 1))
: key === "outlet"
? L("B06_Cross_Revet_Outlet")
: movable
? L("B06_Cross_Revet_Inlet")
: L("B06_Cross_Struct_InletBasin");
// 이동 조작은 기슭막이(집수정 제외) · 집수정은 위치 십자.
dragKind = isBasin ? "basin" : movable ? "wall" : null;
moveRow.row.classList.toggle("is-hidden", !movable);
+1
View File
@@ -645,6 +645,7 @@ export function createCrossSectionCard(
appliedD: () => culvertAppliedD,
inletIsBasin: () => culvertInletIsBasin,
hiddenPipe: () => culvertHiddenPipe,
bodyGuard: () => !!(section.culvert ?? culvertLink?.source.culvert)?.body,
inletOptions: () => culvertInletOptions,
extraState: () => culvertExtraState,
basinExtraState: () => culvertBasinExtraState,
@@ -71,6 +71,8 @@ export interface StructurePanelContext {
inletIsBasin: () => boolean;
/** 기준벽이 독립 기슭막이(관 없는 숨김 세트)인가 — 조정창 걸음 분류에 쓴다. */
hiddenPipe: () => boolean;
/** 세월교 · BOX암거 흙막이 세트인가(링크 카드는 소유 측점 세트 · 79b). */
bodyGuard?: () => boolean;
inletOptions: () => { revetAllowed: boolean; basinLUAllowed: boolean };
extraState: () => { canAdd: boolean; count: number };
basinExtraState: () => { canAdd: boolean; count: number };
@@ -148,6 +150,7 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
isLinkedCard: () => ctx.isLinked,
basinExtraState: () => ctx.basinExtraState(),
hiddenPipe: () => ctx.hiddenPipe(),
bodyGuard: () => ctx.bodyGuard?.() ?? false,
setExtraCount: (count) => {
// 줄어들며 사라지는 벽이 선택 중이면 선택을 유출 벽으로 옮긴다(빈 대상 방지).
const active = ctx.activeRevet() ?? "";
@@ -77,6 +77,7 @@ function guardFit(section, list = sections) {
bottomGap: r3(bottom - end.bottom),
pureHeight: r3(w.height + 0.5),
endHeight: r3(end.top - end.bottom),
autoHeight: r3(end.height),
};
});
}
@@ -180,6 +181,15 @@ out.pipe = pipeLayout
}
: null;
// ⑥-2 손으로 x 를 넣은 관 보호공 = 종전 자리(d 기본 = 경사길이 4m 지점 · x 원점 그대로 · 79b)
const pipeX = computeCulvertLayout(
pipe,
pipe.samples,
{ outlet: { x: 1, d: null, h: null, m: null } },
"revet",
);
out.pipeStoredX = pipeX ? { x: pipeX.revetShift.outlet.x, d: r3(pipeX.revetShift.outlet.d) } : null;
// ⑦ 독립 기슭막이 · 옹벽 기본 자리 = 제일 위(d 0)
const revet = clone(at(180));
revet.revetment = {
@@ -201,6 +211,17 @@ out.revet = own
linked: own.linked,
}
: null;
// 저장 x 가 있는 벽 = 종전 자리 그대로(x 원점 = 경사길이 4m 지점 · d 빈 칸 = 종전 기본 · 79b)
const autoX = computeRevetmentLayout(revet, null).walls[0].bottomBack.offset;
const storedX = computeRevetmentLayout(revet, { x: 1, d: null });
const storedXD = computeRevetmentLayout(revet, { x: 1, d: 1 });
out.revetStoredX = {
x: storedX.appliedAdjust.x,
d: storedX.appliedAdjust.d,
// 노견 → 4m 지점(3.07) + x 1 = 제일 위 자리에서 바깥 4.07 m
shift: r3(Math.abs(storedX.walls[0].bottomBack.offset - autoX)),
shiftXD: r3(Math.abs(storedXD.walls[0].bottomBack.offset - autoX)),
};
const neighbour = clone(revet);
neighbour.chainage_m = 170;
out.revetLinked = computeRevetmentLayout(neighbour, null)?.linked ?? null;
+29 -3
View File
@@ -33,9 +33,8 @@ def shape() -> dict:
return json.loads(done.stdout)
@pytest.mark.parametrize("key", ["boxAuto", "fordAuto"])
def test_높이_빈칸은_본체_끝_높이_윗면은_본체_윗면(shape: dict, key: str) -> None:
walls = shape[key]
def test_세월교_높이_빈칸은_본체_끝_높이_윗면은_본체_윗면(shape: dict) -> None:
walls = shape["fordAuto"]
assert {w["role"] for w in walls} == {"inlet", "outlet"}
for wall in walls:
assert wall["topGap"] == pytest.approx(0, abs=1e-3)
@@ -43,6 +42,17 @@ def test_높이_빈칸은_본체_끝_높이_윗면은_본체_윗면(shape: dict,
assert wall["pureHeight"] == pytest.approx(wall["endHeight"], abs=1e-3)
def test_박스_높이_빈칸은_박스_높이_값_윗면은_본체_윗면(shape: dict) -> None:
# 2 m 박스 → 2 m(사용자 원문 숫자 · 79b) — 바닥 = 윗면 − 그 높이
walls = shape["boxAuto"]
assert {w["role"] for w in walls} == {"inlet", "outlet"}
for wall in walls:
assert wall["topGap"] == pytest.approx(0, abs=1e-3)
assert wall["pureHeight"] == pytest.approx(wall["autoHeight"], abs=1e-3)
assert wall["autoHeight"] < wall["endHeight"]
assert wall["bottomGap"] == pytest.approx(wall["endHeight"] - wall["autoHeight"], abs=1e-3)
def test_높이를_적으면_윗면은_그대로_바닥이_따라감(shape: dict) -> None:
for wall in shape["boxSet"]:
assert wall["topGap"] == pytest.approx(0, abs=1e-3)
@@ -88,6 +98,22 @@ def test_관_보호공_기본_자리는_제일_위(shape: dict) -> None:
assert float(pipe["lengthM"]).is_integer()
def test_저장_x_있는_관_보호공은_종전_자리_그대로(shape: dict) -> None:
# d 빈 칸 기본 = 경사길이 4 m 지점(3.07) — x 원점 그대로라 절대 자리 안 바뀜(79b)
stored = shape["pipeStoredX"]
assert stored["x"] == pytest.approx(1.0)
assert stored["d"] == pytest.approx(4.0 * 1.2 / (1 + 1.2**2) ** 0.5, abs=0.05)
def test_저장_x_있는_독립_기슭막이_옹벽은_종전_자리_그대로(shape: dict) -> None:
stored = shape["revetStoredX"]
assert stored["x"] == pytest.approx(1.0)
assert stored["d"] == pytest.approx(2.6) # 종전 기본 하강(3.07 / 1.2 → 0.1 눈금)
run = 4.0 * 1.2 / (1 + 1.2**2) ** 0.5
assert stored["shift"] == pytest.approx(run + 1.0, abs=1e-3)
assert stored["shiftXD"] == pytest.approx(run + 1.0, abs=1e-3)
def test_독립_기슭막이_옹벽_기본_자리는_제일_위(shape: dict) -> None:
revet = shape["revet"]
assert revet["d"] == 0
+3
View File
@@ -254,6 +254,9 @@ export const ui_locales_b2 = {
B06_Cross_Revet_Outlet: ["기슭막이(유출)", "Revetment (outlet)"],
/* 배관과 무관한 독립 기슭막이(구조물 정본 D군) — 2026-08-28. */
B06_Cross_Revet_Own: ["기슭막이(독립)", "Revetment (standalone)"],
/* 세월교 · BOX암거 유입 · 유출 흙막이 조정창(79b). */
B06_Cross_Guard_Inlet: ["흙막이(유입)", "Retaining wall (inlet)"],
B06_Cross_Guard_Outlet: ["흙막이(유출)", "Retaining wall (outlet)"],
B06_Cross_Struct_Label: ["구조물 형식", "Structure type"],
// 조정창 제목은 "기슭막이(…)" 한 형식으로 통일한다(2026-08-29 사용자). {n} = 단 번호.
B06_Cross_Revet_Extra: ["기슭막이(추가 {n})", "Revetment (extra {n})"],