auto: 2026-08-29 08:54 (EOMSANGDON-HOME)

This commit is contained in:
2026-08-29 08:54:50 +09:00
parent 23285dc016
commit f11e98f8e9
6 changed files with 180 additions and 72 deletions
+3 -2
View File
@@ -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)` +
@@ -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"];
@@ -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);
}
+113 -34
View File
@@ -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<CrossSection["design"]>,
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),
@@ -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()),
);
@@ -195,8 +195,9 @@ export function culvertCardState(layout: CulvertLayout): {
const wallSpecs = new Map<RevetKey, { height: number; material: RevetMaterial }>();
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,
});
};