From f11e98f8e9c793730085cf33370d06ffd7042ab4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 08:54:50 +0900 Subject: [PATCH] auto: 2026-08-29 08:54 (EOMSANGDON-HOME) --- B06_Section/B06_Section_UI_Cross_Culvert.ts | 5 +- .../B06_Section_UI_Cross_Culvert_Const.ts | 6 +- .../B06_Section_UI_Cross_Culvert_Extra.ts | 74 +++++---- B06_Section/B06_Section_UI_Cross_Revetment.ts | 147 ++++++++++++++---- .../B06_Section_UI_Cross_Structure_Panel.ts | 15 +- .../B06_Section_UI_Cross_View_Structure.ts | 5 +- 6 files changed, 180 insertions(+), 72 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Culvert.ts b/B06_Section/B06_Section_UI_Cross_Culvert.ts index 7540adba..bf838e93 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert.ts @@ -163,9 +163,10 @@ export function appendCulvertOverlay( const revetShape = polygon( wall.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]), "b06-chart__culvert-revet", - // 배관 벽 높이 표기 = 순수 높이(바닥~상단, 관 아래 0.5 포함 — 2026-08-23 사용자). + // 높이 표기 = 순수 높이(바닥~상단, 근입 0.5 포함). 추가 기슭막이도 같은 기준으로 + // 통일했다(2026-08-23 배관 벽 → 2026-08-29 추가 벽). `${roleLabel(wall.role)} 기슭막이 ${wall.form ?? ""} H=${( - wall.height + (wall.role === "extra" ? 0 : REVET_EMBED_DEPTH_M) + wall.height + REVET_EMBED_DEPTH_M ).toFixed(1)}m` + `(상단 = 사면선 접점, 전면 1:${REVET_LEAN_RATIO}` + `, 높이 한계 ${revetHeightLimit(wall.form).toFixed(1)}m — 교본 7-3)` + diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts index fcf58cbf..fe45f206 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts @@ -92,9 +92,9 @@ export function revetHeightLimit(form: string | null | undefined): number { /* ── 기슭막이 재질·높이 조작(2026-08-22 사용자 확정) ───────────────────── * 높이 조작이 교본 형태별 한계와 부딪히면 **재질을 바꿔야** 더 올릴 수 있다: * 메쌓기 2.0 / 찰쌓기 3.0(돌쌓기.md §1) / 콘크리트 5.0(프로젝트 기본값, - * 외부 기준 미확인). 배관 기슭막이의 높이 제어·표기·한계 기준은 **순수 높이** - * (벽 바닥~상단, 관 아래 0.5m 포함 — 2026-08-23 사용자 확정). 일반(추가) - * 기슭막이는 종전 계산용 높이(근입 0.5 위 기준선~상단) 기준을 유지한다. */ + * 외부 기준 미확인). 높이 제어·표기·한계 기준은 모두 **순수 높이**(벽 바닥~상단, + * 근입 0.5m 포함) — 배관 기슭막이 2026-08-23, 추가·독립 기슭막이 2026-08-29 + * 사용자 확정으로 기준을 하나로 맞췄다. */ export type RevetMaterial = "dry" | "wet" | "concrete"; export const REVET_MATERIALS: RevetMaterial[] = ["dry", "wet", "concrete"]; diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts index 55e96bb2..bbcd1639 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts @@ -294,10 +294,18 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult const adjust = input.adjusts[i]; const material = adjust.m ?? "dry"; const limit = materialLimit(material); + // 높이 기준 = **벽 바닥~상단**(근입 0.5 포함) — 배관 기준벽과 같은 기준으로 + // 통일했다(2026-08-29 사용자: 추가 기슭막이 높이가 하단~상단이 아니었다). const height = Math.min( - Math.max(adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M, EXTRA_WALL_MIN_HEIGHT_M), + Math.max( + adjust.h ?? EXTRA_WALL_DEFAULT_HEIGHT_M, + EXTRA_WALL_MIN_HEIGHT_M, + REVET_EMBED_DEPTH_M + 0.1, + ), limit, ); + /** 기준선(근입 위)~상단 — 도형·자리 계산은 종전대로 이 값으로 한다. */ + const exposed = height - REVET_EMBED_DEPTH_M; /** 자리 x(벽 하단 중점)에 지반 안착 + 상단이 성토선에 닿는 데 필요한 높이. */ const heightAt = (x: number): number => { @@ -318,7 +326,7 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult let previousShort = 0; // 직전 스캔점의 h 부족량(heightAt < h) — 보간용 for (let t = step; t <= Math.max(span, 0) + 1e-9; t += step) { const x = src.offset + outward * t; - const gap = heightAt(x) - height; + const gap = heightAt(x) - exposed; if (gap >= 0) { // 교차점 선형 보간 — 격자 대신 정확 자리(사면선·지반 동시 접점). const back = previousShort + gap > 1e-9 ? (gap / (previousShort + gap)) * step : 0; @@ -333,36 +341,41 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult } if (autoOffset === null) autoOffset = bestOffset; if (autoOffset === null) break; // 세울 만한 지형이 아니다 — 되는 만큼만. - // 관통 금지: 상단 ≤ 윗단 하단 → 성토선 수평거리(경사부) ≥ 1.2×(시작−윗단하단). - const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(height); - const minJointRun = FILL_SLOPE_RATIO_MIN * Math.max(src.elevation - prevBottom, 0); - // 등간격 배치: 1차는 남은 구간(끝 포함) 균등 분할 근사, 이후 라운드는 보정 d. + const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(exposed); + /** 자동 자리(선반 0·수직 0)의 벽 상단 표고 — 조작은 여기서부터 잰다. */ + const autoTop = src.elevation - autoJointRun / FILL_SLOPE_RATIO_MIN; + // 관통 금지: 상단 ≤ 윗단 하단 → 그만큼은 반드시 내려간다. + const minV = Math.ceil(Math.max(autoTop - prevBottom, 0) * 10) / 10; + // 조작 축(2026-08-29 사용자): d = **순수 수직 하강(m)**, x = **순수 수평 이동(m)**. + // 성토선 각도(1:1.2)는 그대로 두고, 윗단 앞 **수평 선반**이 길이를 흡수한다 + // 선반 = x − 1.2·d (≥ 0이어야 각도가 유지된다). + // 등간격 배치: 1차는 남은 구간(끝 포함) 균등 분할 근사, 이후 라운드는 보정값. const requestedD = plan === "user" ? (adjust.d ?? 0) : plan === "greedy" - ? (trailing.lengthM / (input.adjusts.length - i + 1)) * + ? ((trailing.lengthM / (input.adjusts.length - i + 1)) * (FILL_SLOPE_RATIO_MIN / Math.hypot(1, FILL_SLOPE_RATIO_MIN)) - - autoJointRun + autoJointRun) / + FILL_SLOPE_RATIO_MIN : (plan[i] ?? 0); const requestedX = plan === "user" ? adjust.x : 0; - const minD = Math.ceil((minJointRun - autoJointRun) * 10) / 10; /** 후보 자리의 바닥 — buildExtraWall과 같은 근입 0.5m 고정(2026-08-23). */ const bottomAt = (_anchorX: number, baseElevation: number): number => baseElevation - REVET_EMBED_DEPTH_M; // 이동 한계 = 원지반 매몰 자리 금지(2026-08-22 사용자 확정): ① 이음선 상단이 // 지반 아래(벽이 통째로 묻힘) ② 자기 성토선 시작점(실바닥+0.5 전면 교차점)이 // 지반 아래 0.05m 이상(= 0.5m 이상 매몰 → 성토 불필요 자리). - const placeable = (xShift: number, dShift: number): boolean => { - const topE = src.elevation - (autoJointRun + dShift) / FILL_SLOPE_RATIO_MIN; - const aX = autoOffset + outward * (xShift + dShift); - const jointX = aX - outward * jointRunOf(height); + const placeable = (xShift: number, vShift: number): boolean => { + const topE = autoTop - vShift; + const aX = autoOffset + outward * xShift; + const jointX = aX - outward * jointRunOf(exposed); if ( Math.max(groundAt(jointX), groundAt(jointX + outward * REVET_THICKNESS_M)) >= topE - 0.01 ) return false; - const baseE = topE - height; + const baseE = topE - exposed; const bottom = bottomAt(aX, baseE); const startE = bottom + REVET_EMBED_DEPTH_M; const topFrontX = jointX + outward * REVET_THICKNESS_M; @@ -379,13 +392,18 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult ) !== null ); }; - let appliedD = Math.max(requestedD, minD); - let appliedX = Math.max(requestedX, 0); + let appliedD = Math.max(requestedD, minV); + /** 선반 길이가 음수가 되면 각도가 깨진다 — x가 최소 1.2·d까지 따라 나간다. */ + const shelfFloor = (v: number): number => FILL_SLOPE_RATIO_MIN * v; + let appliedX = Math.max(requestedX, 0, shelfFloor(appliedD)); for (let pass = 0; pass < 400 && !placeable(appliedX, appliedD); pass += 1) { // 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우). - if (appliedD > minD + 1e-9) appliedD = Math.max(minD, appliedD - 0.05); - else if (appliedX > 1e-9) appliedX = Math.max(0, appliedX - 0.05); - else break; + if (appliedD > minV + 1e-9) { + appliedD = Math.max(minV, appliedD - 0.05); + appliedX = Math.max(appliedX, shelfFloor(appliedD)); + } else if (appliedX > shelfFloor(appliedD) + 1e-9) { + appliedX = Math.max(shelfFloor(appliedD), appliedX - 0.05); + } else break; } if (!placeable(appliedX, appliedD)) break; // 자동 자리조차 매몰 — 이 단은 불가. appliedD = Math.round(appliedD * 10) / 10; @@ -397,23 +415,23 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult m: adjust.m, }); - const slopeRun = autoJointRun + appliedD; - const topElevation = src.elevation - slopeRun / FILL_SLOPE_RATIO_MIN; - const anchorX = autoOffset + outward * (appliedX + appliedD); - const base = topElevation - height; // 근입 0.5 위 기준선 + const topElevation = autoTop - appliedD; + const anchorX = autoOffset + outward * appliedX; + const base = topElevation - exposed; // 근입 0.5 위 기준선 const wall = buildExtraWall( i, { offset: anchorX, elevation: base }, outward, - height, + exposed, material, Math.max(0, base - groundAt(anchorX)), ); walls.push(wall); // 성토선: src → (수평 선반 x>0이면 선반 끝) → 이음선 상단점. 사면길이 = 경사부. + const shelfRun = Math.max(0, appliedX - FILL_SLOPE_RATIO_MIN * appliedD); const shelf: OffsetPoint | null = - appliedX > 1e-9 - ? { offset: src.offset + outward * appliedX, elevation: src.elevation } + shelfRun > 1e-9 + ? { offset: src.offset + outward * shelfRun, elevation: src.elevation } : null; const slopeFrom = shelf ?? src; const run = Math.abs(wall.topJoint.offset - slopeFrom.offset); @@ -474,7 +492,9 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult const mean = lengths.reduce((a, b) => a + b, 0) / lengths.length; if (Math.max(...lengths.map((l) => Math.abs(l - mean))) < 0.05) break; const dNext = result.appliedAdjusts.map( - (applied, i) => (applied.d ?? 0) + (mean - (lengths[i] ?? mean)) * horizontalPerSlope, + (applied, i) => + (applied.d ?? 0) + + ((mean - (lengths[i] ?? mean)) * horizontalPerSlope) / FILL_SLOPE_RATIO_MIN, ); result = cascadeOnce(dNext); } diff --git a/B06_Section/B06_Section_UI_Cross_Revetment.ts b/B06_Section/B06_Section_UI_Cross_Revetment.ts index 8d0971cb..e860207c 100644 --- a/B06_Section/B06_Section_UI_Cross_Revetment.ts +++ b/B06_Section/B06_Section_UI_Cross_Revetment.ts @@ -4,9 +4,10 @@ * 배관만 없을 뿐, 벽 자리·이동·한계는 배관 유출 벽과 한 몸으로 푼다(2026-08-28 사용자: * "배관용에서 배관만 빼면 대부분 동일해야"). * - * · 자리: `placePipeWall`(배관 공용 4축 배치) — d=0이면 벽 상단이 노견, d로 성토 사면을 - * 1:1.2 타고 내려간다. base(=invert)는 **성토선을 따라** 정해지고(지반밀착 아님), - * 지반보다 뜨면 `floatGap`으로 잡는다. x·d 한계(노견 안쪽 금지·매몰 금지)도 여기서. + * · 자리(2026-08-29 사용자 개정): 조작은 **순수 좌우(x)·순수 상하(d)** 0.1m다. + * x = 자동 자리에서의 수평 이동, d = 노견에서 잰 수직 하강. 성토선 각도는 유지하고 + * **노견 확장 구간**이 길이를 흡수한다. 한계는 노견 안쪽 금지·확장 0(각도 유지)·매몰 금지. + * 배관 기준벽(`placePipeWall`)의 이동은 손대지 않는다. * · 실제 적용값(한계 절삭 후)은 `appliedAdjust`로 돌려주고, 화면이 조작 저장소에 되받는다 * — 눌러도 안 움직이는데 숫자만 커지는 것 방지(배관과 같은 syncApplied 규칙). * · 다단: 배관과 **같은 함수** `buildExtrasAt`. 그리기(폴리곤·이음선·돌·성토부선)는 공용 @@ -18,6 +19,8 @@ import { showToast } from "@ui/ui_template_elements"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import { + FILL_SLOPE_RATIO_MIN, + PIPE_CONNECT_GRADE, PIPE_WALL_DEFAULT_RUN_M, REVET_EMBED_DEPTH_M, REVET_LEAN_RATIO, @@ -25,12 +28,8 @@ import { revetHeightLimit, } from "./B06_Section_UI_Cross_Culvert_Const"; import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; -import { - fillWallBaseWidth, - groundInterpolator, - placePipeWall, -} from "./B06_Section_UI_Cross_Culvert_Solve"; -import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra"; +import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve"; +import { buildExtrasAt, slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra"; import { ZERO_ADJUST, type CulvertDesignTrim, @@ -80,7 +79,7 @@ export interface RevetmentTier { export interface RevetmentLayout { side: "left" | "right"; - /** 1단 벽 높이(m) — 조정창 높이 표시·조작의 기준값. */ + /** 1단 벽 높이(m) — **바닥~상단**. 조정창 높이 표시·조작의 기준값. */ heightM: number; /** 이 단면의 누가거리(m) — 부족 안내를 측점 단위로 센다(재렌더 중복 방지). */ chainageM: number; @@ -119,6 +118,29 @@ function sampleLimit(section: CrossSection, outward: number): number | null { return outward > 0 ? Math.max(...offsets) : Math.min(...offsets); } +/** + * 노면 횡단물매를 **바깥 방향 1m당 표고 변화**로 돌려준다(하향이면 음수). + * 노면은 측구 쪽으로 내려가는 단일 평면이라(엔진 `road_z`), 노견을 밖으로 연장하면 + * 그 평면을 그대로 타고 가야 각도가 맞는다(2026-08-29 사용자). + * 1순위는 **실제 그려진 노견 구간**(차도 끝 → 노면 끝)의 기울기 — 부호를 추측하지 + * 않는다. 노견 폭이 0이면 `cross_slope_pct`를 측구 방향 부호로 쓴다. + */ +function roadSlopePerOutward( + design: NonNullable, + side: "left" | "right", + outward: number, +): number { + const edge = design.road_edges?.[side]; + const inner = design.carriageway_edges?.[side]; + if (edge && inner) { + const run = (edge.offset_m - inner.offset_m) * outward; + if (run > 1e-6) return (edge.elevation_m - inner.elevation_m) / run; + } + const pct = Number(design.cross_slope_pct); + if (!Number.isFinite(pct)) return 0; + return (pct / 100) * (design.ditch_side === side ? -1 : 1); +} + /** * 독립 기슭막이 단면 — 제원·높이·설계선이 없거나 자리를 못 찾으면 null. * 1단 벽 = 배관 유출 벽과 같은 `placePipeWall`+`buildRevetWallGeometry`, @@ -141,31 +163,65 @@ export function computeRevetmentLayout( const limit = sampleLimit(section, outward); if (!edge || !groundAt || limit === null) return null; - // 재질 한계로 높이를 자른다(배관 벽과 같은 규칙). - const height = Math.min(requestedHeight, revetHeightLimit(spec.form)); + // 높이 기준 = **벽 바닥~상단**(근입 0.5 포함 — 배관·추가 벽과 같은 기준, + // 2026-08-29 사용자). 재질(형태) 한계도 이 기준으로 자른다. + const pureHeight = Math.min( + Math.max(requestedHeight, REVET_EMBED_DEPTH_M + 0.1), + revetHeightLimit(spec.form), + ); + /** 기준선(근입 위)~상단 — 도형 계산은 종전대로 이 값으로 한다. */ + const height = pureHeight - REVET_EMBED_DEPTH_M; - // 배관 유출 벽과 **같은 4축 배치**. 기준(d=0) = 벽 상단이 노견인 자리. - const autoOffset = edge.offset_m + outward * (fillWallBaseWidth(height) / 2 - REVET_THICKNESS_M / 2); - const placed = placePipeWall({ - autoOffset, - outward, - height, - baseElevation0: edge.elevation_m - height, // d=0에서 벽 상단 = 노견 - edgeElevation: edge.elevation_m, - invertCap: Number.POSITIVE_INFINITY, // 관 없음 — 위 한계는 d≥0(노견)만 - adjust: { x: adjust?.x ?? 0, d: adjust?.d ?? null, h: null, m: null }, - defaultD: PIPE_WALL_DEFAULT_RUN_M, - groundAt, - limitOffset: limit, - requireCrossing: true, // 유출 벽처럼 매몰(원지반 아래 daylight 없음) 금지 - }); - const baseElevation = placed.invert; - const floatGapM = Math.max(0, baseElevation - groundAt(placed.anchorOffset)); + const roadSlope = roadSlopePerOutward(design, side, outward); + const autoOffset = + edge.offset_m + outward * (fillWallBaseWidth(height) / 2 - REVET_THICKNESS_M / 2); + // 자동 자리 = 성토사면(1:1.2) 위 기본 지점 — 배관 유출 벽과 같은 기준값. + const autoRun = PIPE_WALL_DEFAULT_RUN_M; + /** + * 조작 축(2026-08-29 사용자): x = **순수 좌우(m)**, d = **노견에서 잰 순수 수직 + * 하강(m)**. 성토선 각도는 그대로 두고 **노견 확장 구간**이 길이를 흡수한다 + * (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다). + */ + const defaultDrop = autoRun / FILL_SLOPE_RATIO_MIN; + /** 그 자리에 벽을 세울 수 있나 — 매몰(원지반 아래 daylight 없음) 금지. */ + const placeable = (xShift: number, drop: number): boolean => { + const anchor = autoOffset + outward * (autoRun + xShift); + const invert = edge.elevation_m - drop - height; + return ( + invert >= groundAt(anchor) - 0.01 || + slopedCrossing( + { offset: anchor, elevation: invert }, + outward, + -PIPE_CONNECT_GRADE, + groundAt, + limit, + ) !== null + ); + }; + 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); + let appliedDrop = Math.max(adjust?.d ?? defaultDrop, 0); + let appliedX = Math.max(requestedX, xFloor(appliedDrop)); + for (let pass = 0; pass < 400 && !placeable(appliedX, appliedDrop); pass += 1) { + // 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우). + if (appliedDrop > 1e-9) { + appliedDrop = Math.max(0, appliedDrop - 0.05); + appliedX = Math.max(Math.min(appliedX, requestedX), xFloor(appliedDrop)); + } else if (appliedX > 1e-9) appliedX = Math.max(0, appliedX - 0.05); + else break; + } + appliedX = Math.round(appliedX * 10) / 10; + appliedDrop = Math.round(appliedDrop * 10) / 10; + + const anchorOffset = autoOffset + outward * (autoRun + appliedX); + const baseElevation = edge.elevation_m - appliedDrop - height; + const floatGapM = Math.max(0, baseElevation - groundAt(anchorOffset)); const material = materialOfForm(spec.form); const lengthM = Number.isFinite(spec.end_m - spec.start_m) ? spec.end_m - spec.start_m : null; const tier1 = buildRevetWallGeometry({ - anchor: { offset: placed.anchorOffset, elevation: baseElevation }, + anchor: { offset: anchorOffset, elevation: baseElevation }, outward, height, material, @@ -191,8 +247,31 @@ export function computeRevetmentLayout( const walls: WallLayout[] = [tier1, ...extras.walls]; const jt = tier1.topJoint; - // 노견 → 벽 이음선 상단점 성토선(1:1.2, 벽이 밖으로 밀리면 그만큼 노견이 연장된다). - const slope = { points: [{ offset: edge.offset_m, elevation: edge.elevation_m }, jt] }; + // 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고, + // 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정). + // D = 노견에서 벽 상단까지 바깥 방향 거리, R0 = 노견과 벽 상단의 낙차 + // (D − t) = 1.2 × (R0 + roadSlope × t) → t = (D − 1.2·R0) / (1 + 1.2·roadSlope) + const edgePoint = { offset: edge.offset_m, elevation: edge.elevation_m }; + const D = (jt.offset - edge.offset_m) * outward; + const R0 = edge.elevation_m - jt.elevation; + const denominator = 1 + FILL_SLOPE_RATIO_MIN * roadSlope; + const breakRun = + R0 > 1e-6 && D > 1e-6 && Math.abs(denominator) > 1e-9 + ? Math.min(D, Math.max(0, (D - FILL_SLOPE_RATIO_MIN * R0) / denominator)) + : 0; + const slope = { + points: + breakRun > 1e-6 + ? [ + edgePoint, + { + offset: edge.offset_m + outward * breakRun, + elevation: edge.elevation_m + roadSlope * breakRun, + }, + jt, + ] + : [edgePoint, jt], + }; const designTrim: CulvertDesignTrim = outward > 0 ? { minOffset: -Infinity, maxOffset: jt.offset, maxElevation: jt.elevation, maxSlope: slope } @@ -200,14 +279,14 @@ export function computeRevetmentLayout( return { side, - heightM: height, + heightM: pureHeight, chainageM: Number(section.chainage_m) || 0, requestedTiers, top: jt, walls, fillSegments: extras.segments, designTrim, - appliedAdjust: { x: placed.x, d: placed.d, h: adjust?.h ?? null, m: null }, + appliedAdjust: { x: appliedX, d: appliedDrop, h: adjust?.h ?? null, m: null }, tiers: walls.map((wall) => ({ polygon: wall.points })), span: { beforeM: Math.max(spec.anchor_m - spec.start_m, 0), diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index 8e86f65d..af24c896 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -86,6 +86,13 @@ export interface StructurePanelHandle { * 높이는 0.1m 눈금(2026-08-22 사용자 확정 2~3m·0.5~3m 구간 0.1 단위 제어). */ const STEP_M = 1.0; +/** + * 독립·추가 기슭막이의 좌우·상하 한 걸음(m) — **0.1m**(2026-08-29 사용자). + * 배관 기준벽(유입·유출)만 관 길이에 맞춘 1m 걸음을 그대로 쓴다. + */ +const OWN_STEP_M = 0.1; +const moveStepOf = (key: RevetKey): number => + key === "inlet" || key === "outlet" ? STEP_M : OWN_STEP_M; const HEIGHT_STEP_M = 0.1; const BASIN_STEP_M = 0.1; /** 전/후 한 걸음(m) — 집수정 기본이 2m·1/1이라 1m 눈금은 너무 거칠다. */ @@ -162,13 +169,13 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan "▲", "up", L("B06_Cross_Revet_Up"), - act((key) => deps.nudgeSlope(key, -STEP_M)), + act((key) => deps.nudgeSlope(key, -moveStepOf(key))), ), dpad( "◀", "left", L("B06_Cross_Revet_Left"), - act((key) => deps.nudge(key, STEP_M)), + act((key) => deps.nudge(key, moveStepOf(key))), ), dpad( "↺", @@ -180,13 +187,13 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan "▶", "right", L("B06_Cross_Revet_Right"), - act((key) => deps.nudge(key, -STEP_M)), + act((key) => deps.nudge(key, -moveStepOf(key))), ), dpad( "▼", "down", L("B06_Cross_Revet_Down"), - act((key) => deps.nudgeSlope(key, STEP_M)), + act((key) => deps.nudgeSlope(key, moveStepOf(key))), ), dpad("≡", "equal", L("B06_Cross_Extra_Equalize"), () => deps.equalizeExtras()), ); diff --git a/B06_Section/B06_Section_UI_Cross_View_Structure.ts b/B06_Section/B06_Section_UI_Cross_View_Structure.ts index 60babcc5..8af50e22 100644 --- a/B06_Section/B06_Section_UI_Cross_View_Structure.ts +++ b/B06_Section/B06_Section_UI_Cross_View_Structure.ts @@ -195,8 +195,9 @@ export function culvertCardState(layout: CulvertLayout): { const wallSpecs = new Map(); const specOf = (wall: CulvertLayout["walls"][number], key: RevetKey): void => { wallSpecs.set(key, { - // 배관 벽은 순수 높이(바닥~상단 = 계산용 + 근입 0.5 — 2026-08-23 사용자). - height: wall.role === "extra" ? wall.height : wall.height + REVET_EMBED_DEPTH_M, + // 조정창 높이 = 순수 높이(바닥~상단 = 계산용 + 근입 0.5). 추가 기슭막이도 + // 같은 기준이다(2026-08-23 배관 벽 → 2026-08-29 추가 벽). + height: wall.height + REVET_EMBED_DEPTH_M, material: wall.material, }); };