fix(B06): 관 끝점 기준 = 실제 바닥+0.5 · 추가 벽 매몰 이동 차단 · 기본 높이 2.0 통일

- 유출 관 하단 끝점·다음 단 성토선 시작점의 기준을 기준선(anchor)이 아니라
  **실제 바닥(전면 근입 심화 반영) +0.5m**로 통일 — 가파른 지반에서 바닥만
  깊어지고 관이 벽 위쪽에 떠 보이던 문제 해소(2026-08-22 사용자 지적)
- 추가 기슭막이 이동 한계 = 원지반 매몰 자리 금지: 이음선 상단이 지반 아래
  (통째 매몰)이거나 자기 성토선 시작점이 지반 아래 0.05m 이상이면 그 앞에서
  멈춤(상하 먼저, 다음 좌우 되돌림) — 자동 자리조차 매몰이면 그 단은 불가
- 배관 기슭막이 기본 높이 = 재질 무관 2.0m(찰·콘크리트 기본 2.5 → 2.0 정정)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 15:06:31 +09:00
co-authored by Claude Opus 5
parent 5666ef0dd4
commit 63bd922711
3 changed files with 66 additions and 13 deletions
@@ -112,8 +112,8 @@ export function materialFromForm(form: string | null | undefined): RevetMaterial
/** 배관 기슭막이 높이 조작 하한(m) — 2~3m 구간 지시(2026-08-22)의 하한. */
export const PIPE_WALL_MIN_HEIGHT_M = 2.0;
/** 배관 기슭막이 기본 높이(m, 2026-08-22 지시 2.5) — 재질 한계로 잘린다(메→2.0). */
export const PIPE_WALL_DEFAULT_HEIGHT_M = 2.5;
/** 배관 기슭막이 기본 높이(m) — 재질과 무관하게 2.0(2026-08-22 사용자 정정). */
export const PIPE_WALL_DEFAULT_HEIGHT_M = 2.0;
/** 일반(추가) 기슭막이 높이 조작 하한(m). */
export const EXTRA_WALL_MIN_HEIGHT_M = 0.5;
/** 일반(추가) 기슭막이 기본 높이(m). */
@@ -217,8 +217,52 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
// 관통 금지: 상단 ≤ 윗단 하단 → 성토선 수평거리(경사부) ≥ 1.2×(시작−윗단하단).
const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(height);
const minJointRun = FILL_SLOPE_RATIO_MIN * Math.max(src.elevation - prevBottom, 0);
const appliedD = Math.max(adjust.d, Math.ceil((minJointRun - autoJointRun) * 10) / 10);
const appliedX = Math.max(adjust.x, 0);
const minD = Math.ceil((minJointRun - autoJointRun) * 10) / 10;
/** 후보 자리의 바닥(전면 근입 심화 반영) — buildExtraWall과 같은 계산. */
const bottomAt = (anchorX: number, baseElevation: number): number => {
const baseWidth = REVET_THICKNESS_M * 1.5 + REVET_LEAN_RATIO * height;
const jointX = anchorX - outward * (baseWidth / 2 - REVET_THICKNESS_M / 2);
const topFrontX = jointX + outward * REVET_THICKNESS_M;
const topElevation2 = baseElevation + height;
const frontXAt = (e: number): number =>
topFrontX + outward * REVET_LEAN_RATIO * (topElevation2 - e);
let bottom = baseElevation - REVET_EMBED_DEPTH_M;
if (baseElevation - groundAt(anchorX) <= 1e-6) {
for (let pass = 0; pass < 6; pass += 1) {
const toe = Math.min(groundAt(frontXAt(bottom)), baseElevation);
if (toe - REVET_EMBED_DEPTH_M >= bottom - 1e-6) break;
bottom = toe - REVET_EMBED_DEPTH_M;
}
}
return bottom;
};
// 이동 한계 = 원지반 매몰 자리 금지(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);
if (Math.max(groundAt(jointX), groundAt(jointX + outward * REVET_THICKNESS_M)) >= topE - 0.01)
return false;
const baseE = topE - height;
const bottom = bottomAt(aX, baseE);
const startE = bottom + REVET_EMBED_DEPTH_M;
const topFrontX = jointX + outward * REVET_THICKNESS_M;
const startX = topFrontX + outward * REVET_LEAN_RATIO * (topE - startE);
return groundAt(startX) < startE + 0.05;
};
let appliedD = Math.max(adjust.d, minD);
let appliedX = Math.max(adjust.x, 0);
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 (!placeable(appliedX, appliedD)) break; // 자동 자리조차 매몰 — 이 단은 불가.
appliedD = Math.round(appliedD * 10) / 10;
appliedX = Math.round(appliedX * 10) / 10;
appliedAdjusts.push({
x: appliedX,
d: appliedD,
@@ -254,10 +298,14 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult
ratio: rise > 1e-9 ? run / rise : FILL_SLOPE_RATIO_MIN,
overLimit: Math.hypot(run, rise) >= FILL_SLOPE_MAX_LENGTH_M - 1e-6,
});
// 다음 단 시작점 = 이 벽 하단 +0.5m(기준선) 수평선과 전면 경사선(1:0.3)의 교차점.
// 다음 단 시작점 = 이 벽 **실제 바닥**(근입 심화 반영) +0.5m 수평선과 전면
// 경사선(1:0.3)의 교차점(2026-08-22 사용자 — 관 끝점 규칙과 동일 기준).
const startElevation = wall.bottomBack.elevation + REVET_EMBED_DEPTH_M;
src = {
offset: wall.points[2].offset + outward * REVET_LEAN_RATIO * height,
elevation: base,
offset:
wall.points[2].offset +
outward * REVET_LEAN_RATIO * (wall.topJoint.elevation - startElevation),
elevation: startElevation,
};
prevBottom = wall.bottomBack.elevation;
}
@@ -461,12 +461,17 @@ export function computeCulvertLayout(
const pipeStart = basinPipeEnd ?? inlet;
/** 유출 관 하단 끝점 목표 = 벽 바닥 기준 0.5m 상단(기준선)과 **전면 경사선의
* 교차점**(2026-08-22 사용자 ① — 성토부선 시작 규칙과 같은 자리). */
const outletPipeEnd = (wall: WallLayout): OffsetPoint => ({
offset:
wall.points[2].offset +
wall.outward * REVET_LEAN_RATIO * (wall.topJoint.elevation - wall.base),
elevation: wall.base,
});
const outletPipeEnd = (wall: WallLayout): OffsetPoint => {
// 기준은 **실제 바닥**(전면 근입으로 깊어진 값) +0.5 — 기준선(anchor)을 쓰면
// 가파른 지반에서 바닥만 내려가고 관이 벽 위쪽에 떠 보인다(2026-08-22 사용자 ②).
const reference = wall.bottomBack.elevation + REVET_EMBED_DEPTH_M;
return {
offset:
wall.points[2].offset +
wall.outward * REVET_LEAN_RATIO * (wall.topJoint.elevation - reference),
elevation: reference,
};
};
// 유출 벽이 사면 끝과 다른 자리면 관 끝도 그 자리 기준으로 맞춘다(올림 연장분은
// 유출 쪽 — 2026-08-20 확정). 관 하단선은 전면 기준선 교차점을 지나 정수 길이로.
if (outletWall) {