From 77e8aa270948f73d70792fb85faf07d49b9138c9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 00:26:10 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EB=B0=95=EC=8A=A4=20=ED=9D=99?= =?UTF-8?q?=EB=A7=89=EC=9D=B4=20=EA=B8=B0=EB=B3=B8=20=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=3D=20=EB=B0=95=EC=8A=A4=20=EB=86=92=EC=9D=B4=20=EA=B0=92=20?= =?UTF-8?q?=C2=B7=20=EC=A0=80=EC=9E=A5=20x=20=EB=B2=BD=20=EC=A2=85?= =?UTF-8?q?=EC=A0=84=20=EC=9E=90=EB=A6=AC=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=C2=B7=20=ED=9D=99=EB=A7=89=EC=9D=B4=20=EC=A1=B0=EC=A0=95?= =?UTF-8?q?=EC=B0=BD=20=EC=A0=9C=EB=AA=A9=20=C2=B7=20=EB=B0=95=EC=8A=A4=20?= =?UTF-8?q?=EB=82=A0=EA=B0=9C=EB=B2=BD=20=ED=88=B4=ED=8C=81(79b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 박스 흙막이 높이 빈 칸 = 박스 높이 값(2 m 박스 → 2 m) · 윗면 = 본체 윗면 · 바닥 = 윗면 − 높이 · 세월교는 그대로 - x 원점(사면 경사길이 4 m 지점) 되돌림 — 손대지 않은 벽(x · d 빈 칸)만 성토사면 제일 위 · 손으로 x 를 넣은 독립 기슭막이 · 옹벽 · 관 보호공은 종전 자리 - 세월교 · 박스 흙막이 조정창 제목 「흙막이(유입 · 유출)」 · 박스 에이프런 툴팁 유입 · 유출 날개벽 이름 바로잡음 - 시험: test_79_guard_shape — 박스 높이 값 · 저장 x 있는 관 보호공 · 독립 기슭막이 = 종전 자리 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- A00_Common/design/design_structure_box.ts | 2 +- A00_Common/design/design_structure_ford.ts | 2 +- A00_Common/design/design_structure_guard.ts | 6 ++-- .../B06_Section_UI_Cross_Body_Guard.ts | 19 ++++++++--- B06_Section/B06_Section_UI_Cross_Box.ts | 3 +- B06_Section/B06_Section_UI_Cross_Box_Geom.ts | 6 +++- .../B06_Section_UI_Cross_Culvert_Const.ts | 19 +++++++---- .../B06_Section_UI_Cross_Culvert_Geom.ts | 6 ++-- .../B06_Section_UI_Cross_Culvert_Solve.ts | 2 +- .../B06_Section_UI_Cross_Culvert_Tiers.ts | 2 +- B06_Section/B06_Section_UI_Cross_Revetment.ts | 22 +++++++------ .../B06_Section_UI_Cross_Structure_Panel.ts | 23 +++++++------ B06_Section/B06_Section_UI_Cross_View.ts | 1 + .../B06_Section_UI_Cross_View_Structure.ts | 3 ++ resources/tester/helper_79_guard_shape.cjs | 21 ++++++++++++ resources/tester/test_79_guard_shape.py | 32 +++++++++++++++++-- ui_template/ui_template_locale_b2.ts | 3 ++ 17 files changed, 128 insertions(+), 44 deletions(-) diff --git a/A00_Common/design/design_structure_box.ts b/A00_Common/design/design_structure_box.ts index 815e4e32d..4f2b8b480 100644 --- a/A00_Common/design/design_structure_box.ts +++ b/A00_Common/design/design_structure_box.ts @@ -50,7 +50,7 @@ const guardContainer = ( { value: "흙막이", label: "흙막이" }, ], }, - ...bodyGuardItems(id), + ...bodyGuardItems(id, "박스 높이(자동)"), ], }); diff --git a/A00_Common/design/design_structure_ford.ts b/A00_Common/design/design_structure_ford.ts index 3fb88107d..4b12e1ed5 100644 --- a/A00_Common/design/design_structure_ford.ts +++ b/A00_Common/design/design_structure_ford.ts @@ -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, "본체 끝 높이(자동)"), ], }); diff --git a/A00_Common/design/design_structure_guard.ts b/A00_Common/design/design_structure_guard.ts index d5d917ffc..e1b4fbdb5 100644 --- a/A00_Common/design/design_structure_guard.ts +++ b/A00_Common/design/design_structure_guard.ts @@ -82,11 +82,11 @@ export const revetKeys = (side: "inlet" | "outlet"): Record => ( 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", diff --git a/B06_Section/B06_Section_UI_Cross_Body_Guard.ts b/B06_Section/B06_Section_UI_Cross_Body_Guard.ts index 2de0c4fa9..fcafb2c66 100644 --- a/B06_Section/B06_Section_UI_Cross_Body_Guard.ts +++ b/B06_Section/B06_Section_UI_Cross_Body_Guard.ts @@ -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` 역산. diff --git a/B06_Section/B06_Section_UI_Cross_Box.ts b/B06_Section/B06_Section_UI_Cross_Box.ts index be95b58d4..c83a1a607 100644 --- a/B06_Section/B06_Section_UI_Cross_Box.ts +++ b/B06_Section/B06_Section_UI_Cross_Box.ts @@ -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)} 투영 연장)`, ), ); } diff --git a/B06_Section/B06_Section_UI_Cross_Box_Geom.ts b/B06_Section/B06_Section_UI_Cross_Box_Geom.ts index f68d91cad..a9ae4f1b3 100644 --- a/B06_Section/B06_Section_UI_Cross_Box_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Box_Geom.ts @@ -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 }, }; }; diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts index a354164f4..e4bad48ea 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts @@ -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). 기슭막이를 자연 사면 diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index 2f556b75f..3cdce0936 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -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); diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts index 08144f921..d45f2a726 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts @@ -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; diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts index 10e0e59de..6396cc06f 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Tiers.ts @@ -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, diff --git a/B06_Section/B06_Section_UI_Cross_Revetment.ts b/B06_Section/B06_Section_UI_Cross_Revetment.ts index 45c49dad9..f73a30df1 100644 --- a/B06_Section/B06_Section_UI_Cross_Revetment.ts +++ b/B06_Section/B06_Section_UI_Cross_Revetment.ts @@ -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단 벽 전면 하단 꼭짓점을 성토부선 시작점으로. diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index c46b44c28..0ad30e724 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -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); diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index 0f6a99db3..9477defc5 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -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, diff --git a/B06_Section/B06_Section_UI_Cross_View_Structure.ts b/B06_Section/B06_Section_UI_Cross_View_Structure.ts index c65676d57..e41f661d6 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Structure.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Structure.ts @@ -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() ?? ""; diff --git a/resources/tester/helper_79_guard_shape.cjs b/resources/tester/helper_79_guard_shape.cjs index 913562993..69a30ad20 100644 --- a/resources/tester/helper_79_guard_shape.cjs +++ b/resources/tester/helper_79_guard_shape.cjs @@ -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; diff --git a/resources/tester/test_79_guard_shape.py b/resources/tester/test_79_guard_shape.py index eb3b7ed66..f33cbcafe 100644 --- a/resources/tester/test_79_guard_shape.py +++ b/resources/tester/test_79_guard_shape.py @@ -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 diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 956e9dccb..ee0678e37 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -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})"],