fix(B06): 독립 기슭막이 좌우 로직을 성토 경로 하나로 통일한다

이관 뒤 독립 기슭막이 벽 역할이 uphill_side로 정해져, 고른 쪽이 상단이면
유입 역할(집수정 후보·placeInletWall·다단 없음), 하단이면 유출 역할
(성토부선+buildExtrasAt)을 타서 좌우 동작이 갈렸다. 새 기하 코드를 만들지
않고 이미 있는 유출 경로 인자를 유입 쪽에도 넘겨 재사용한다.

- Const: revetHeightLimit 콘크리트 4.0 → 5.0 (materialLimit과 통일)
- Geom: hidden_pipe면 basinReason null, 유입 배치에 requireCrossing 전달,
  basinExtras 채널을 유입 벽 성토부선·다단으로 사용, inletFill 생략
- Solve: placeInletWall이 requireCrossing을 통과시킨다(기본 false 유지)
- Wire: 링크 카드에서 hidden_pipe면 유입측 다단·성토부선을 벽 연장에 맞춰 유지
- Panel/View_Structure: hiddenPipe deps 추가 — 집수정 형식 숨김, 유입 벽에도
  단 수 행 표시
- Culvert: hidden_pipe의 basinFill 툴팁 이름을 "유입부"로

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-29 09:34:45 +09:00
co-authored by Claude Opus 5
parent 5142bdde9b
commit c0bebe951d
7 changed files with 68 additions and 32 deletions
+5 -2
View File
@@ -120,12 +120,15 @@ export function appendCulvertOverlay(
);
}
// 집수정(ㄴ·ㄷ형) 계류측 성토부선 — 유출측과 같은 체계(2026-08-22 사용자 확정).
// 독립 기슭막이(관 숨김)에는 집수정이 없다 — 이 채널이 **유입측 벽**의 성토부선이라
// 이름도 그렇게 적는다(2026-08-29 좌우 통일).
const basinFillName = layout.culvert.hidden_pipe ? "유입부" : "집수정 계류측";
for (const segment of layout.basinFill.segments) {
planLine(
segment.points,
segment.kind === "cut"
? `집수정 계류측 3도 절토선 — 구조물이 원지반에 묻혀 성토 불필요, 원지반 교차까지 ${segment.lengthM.toFixed(2)}m`
: `집수정 계류측 성토부선 — 사면길이 ${segment.lengthM.toFixed(2)}m` +
? `${basinFillName} 3도 절토선 — 구조물이 원지반에 묻혀 성토 불필요, 원지반 교차까지 ${segment.lengthM.toFixed(2)}m`
: `${basinFillName} 성토부선 — 사면길이 ${segment.lengthM.toFixed(2)}m` +
(segment.overLimit
? " (법정 5m 이상 — 기슭막이 의무 구간, 조정창에서 추가)"
: " (법정 5m 이내)") +
@@ -85,7 +85,7 @@ export function revetHeightLimit(form: string | null | undefined): number {
if (form.includes("메")) return 2.0;
if (form.includes("찰")) return 3.0;
if (form.includes("돌망태")) return 2.0;
if (form.includes("콘크리트")) return 4.0; // 콘크리트 흙막이 산복기초 4.0m 이하
if (form.includes("콘크리트")) return 5.0; // 프로젝트 확정값 — materialLimit과 동일
return 3.0;
}
@@ -151,7 +151,10 @@ export function computeCulvertLayout(
? "short"
: null;
const choice: InletStructureChoice = inletStructure ?? "auto";
const { reason: basinReason, shape: basinShape } = resolveBasinChoice(choice, ruleReason);
// 독립 기슭막이(관 숨김)는 집수정을 세우지 않는다 — 설치 측이 상단(유입 역할)일 때만
// 집수정이 서서 좌·우가 갈리던 원인(2026-08-29 사용자: 양쪽 다 성토 로직).
const { shape: basinShape, reason: choiceReason } = resolveBasinChoice(choice, ruleReason);
const basinReason = culvert.hidden_pipe ? null : choiceReason;
// ㄴ·ㄷ형은 사이즈 유지한 채 통째로 노견 끝점 일치 — 바닥(=관 invert)이 따라 오른다.
if (basinReason && basinShape !== "I") {
inlet.elevation = inletInfo.edge.elevation_m - basinAdjust.innerHeightM;
@@ -229,13 +232,20 @@ export function computeCulvertLayout(
defaultD: PIPE_WALL_DEFAULT_RUN_M,
groundAt,
limitOffset: inletInfo.limit,
// 독립 기슭막이는 유출 벽과 같은 자리 규칙 — 매몰-무교차 자리 금지(2026-08-29).
requireCrossing: culvert.hidden_pipe === true,
outletGuard: culvert.hidden_pipe
? undefined
: {
edge: outletInfo.edge,
outward: outletInfo.outward,
height: outletWallSpec.height,
toeOffset: slopeToeOffset(designAt, groundAt, outletInfo.edge.offset_m, outletInfo.limit),
toeOffset: slopeToeOffset(
designAt,
groundAt,
outletInfo.edge.offset_m,
outletInfo.limit,
),
limitOffset: outletInfo.limit,
},
});
@@ -400,7 +410,7 @@ export function computeCulvertLayout(
};
return wall;
};
buildWall(
const inletWall = buildWall(
culvert.inlet,
inlet,
inletInfo.outward,
@@ -583,15 +593,16 @@ export function computeCulvertLayout(
}
// 유입 기슭막이의 관 시작 접속선 — 위 0도 성토선 / 아래 0도 1m 공간 확보에서 끝
// (2026-08-23 사용자 — 절토 경사 연장 삭제).
const inletFill = !basin
? inletGroundConnector(
pipeCorners.inlet.bottom,
inletInfo.outward,
groundAt,
section.design?.cut_slope_ratio ?? 1.0,
inletInfo.limit,
)
: null;
const inletFill =
!basin && !culvert.hidden_pipe
? inletGroundConnector(
pipeCorners.inlet.bottom,
inletInfo.outward,
groundAt,
section.design?.cut_slope_ratio ?? 1.0,
inletInfo.limit,
)
: null;
// 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로).
// 유출측과 **집수정 계류측**이 같은 체계를 쓴다: 5m 넘으면 다단을 둘 수 있다.
@@ -603,14 +614,21 @@ export function computeCulvertLayout(
adjusts: (revetShift?.extras ?? []).map(adjustOf),
equalize: equalizeExtras === true,
});
const basinExtras = buildExtrasAt(basinFillStart, {
startBottomElevation: basinFillBottom,
outward: inletInfo.outward,
groundAt,
limitOffset: inletInfo.limit,
adjusts: (revetShift?.basinExtras ?? []).map(adjustOf),
equalize: false,
});
// 독립 기슭막이(관 숨김)는 집수정이 없어 이 채널이 **유입측 벽의 성토부선·다단**이다 —
// 유출측(`extras`)과 같은 시작점(관 하단 꼭짓점)·같은 `bextra` 키(2026-08-29 사용자).
const basinExtras = buildExtrasAt(
culvert.hidden_pipe ? (inletWall ? pipeCorners.inlet.bottom : null) : basinFillStart,
{
startBottomElevation: culvert.hidden_pipe
? (inletWall?.bottomBack.elevation ?? 0)
: basinFillBottom,
outward: inletInfo.outward,
groundAt,
limitOffset: inletInfo.limit,
adjusts: (revetShift?.basinExtras ?? []).map(adjustOf),
equalize: false,
},
);
// ── 성토 사면 구간 확정. 벽 자리가 굳은 뒤에 물매를 역산해야 관 길이 맞춤(벽 이동)이
// 접점을 다시 깨뜨리지 않는다 — 종전 어긋남의 직접 원인이 이 순서였다.
@@ -487,9 +487,11 @@ export function placeInletWall(input: {
toeOffset: number;
limitOffset: number;
};
/** 매몰-무교차 자리 금지(유출 벽 규칙) — 독립 기슭막이는 유입도 같은 규칙을 탄다. */
requireCrossing?: boolean;
}): PipeWallPlacement {
const place = (adjust: WallAdjust): PipeWallPlacement =>
placePipeWall({ ...input, adjust, requireCrossing: false });
placePipeWall({ ...input, adjust, requireCrossing: input.requireCrossing === true });
let placed = place(input.adjust);
const guard = input.outletGuard;
if (!guard) return placed;
@@ -253,8 +253,10 @@ function trimLinkedLayout(layout: CulvertLayout, link: CulvertLink): CulvertLayo
// 집수정은 **소유 측점 횡단도 하나에만** 선다(2026-08-24 사용자 확정) — 기슭막이와
// 달리 옆 측점으로 이어지는 구조물이 아니다. 계류측 다단·성토부선도 집수정에 딸린
// 것이라 함께 뺀다. 3D에서는 소유 측점 하나가 자기 길이(기본 2m)만큼 스윕한다.
basinExtras: [],
basinFill: empty,
// 단 독립 기슭막이(관 숨김)는 집수정이 없고 이 채널이 **유입측 벽의 다단·성토부선**
// 이라, 유출측과 같은 규칙으로 그 벽의 연장을 따른다(2026-08-29 좌우 통일).
basinExtras: spec.hidden_pipe && inletIn ? layout.basinExtras : [],
basinFill: spec.hidden_pipe && inletIn ? layout.basinFill : empty,
basin: null,
designTrim: keepTrimSides(
layout.designTrim,
@@ -47,9 +47,13 @@ export interface StructurePanelDeps {
/** 집수정 좌우 — **화면 기준**(+ = 화면 왼쪽). 부호 환산·한계 토스트는 카드가 한다. */
nudgeBasinLateral: (screenDeltaM: number) => void;
resetBasin: () => void;
/** 집수정 계류측 다단 상태·단 수 지정(2026-08-22 — 유출측과 같은 체계). */
/** 집수정 계류측 다단 상태·단 수 지정(2026-08-22 — 유출측과 같은 체계).
* 독립 기슭막이(관 숨김)에서는 집수정이 없으므로 이 채널이 **유입측 벽의 다단**이다. */
basinExtraState: () => { canAdd: boolean; count: number };
setBasinExtraCount: (count: number) => void;
/** 독립 기슭막이(관 숨김) 카드인가 — 유입·유출 벽이 같은 성토 로직이라 조정창도
* 좌우 같은 행을 준다(집수정 형식 숨김 · 유입측 단 수 행 표시 — 2026-08-29). */
hiddenPipe: () => boolean;
/** 이동 조작 가능 여부 — 집수정(자리 고정)은 숨긴다. */
canNudge: (key: RevetKey) => boolean;
/** 좌우·상하 걸음이 0.1m인가(독립·추가 기슭막이). 배관 기준벽은 false = 1m. */
@@ -505,9 +509,12 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
: movable
? L("B06_Cross_Revet_Inlet")
: L("B06_Cross_Struct_InletBasin");
// 독립 기슭막이(관 숨김)는 유입·유출이 같은 성토 벽이다 — 집수정 형식 선택을
// 숨기고 유입측에도 단 수 행을 준다(2026-08-29 사용자: 좌우 조작 통일).
const ownTiers = deps.hiddenPipe() && key === "inlet";
// 형식 선택은 유입측에서만, 이동·높이 조작은 기슭막이(집수정 제외)만.
structureRow.classList.toggle("is-hidden", key !== "inlet");
if (key === "inlet") {
structureRow.classList.toggle("is-hidden", key !== "inlet" || deps.hiddenPipe());
if (key === "inlet" && !deps.hiddenPipe()) {
// 상황에 안 맞는 선택지는 숨긴다(2026-08-22 사용자). 단 지금 선택된 값은
// 남긴다 — 숨기면 셀렉트가 빈 값이 된다.
const allow = deps.optionsFor();
@@ -528,10 +535,14 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
basinRow.row.classList.toggle("is-hidden", !isBasin);
basinReset.classList.toggle("is-hidden", !isBasin);
const basinExtra = deps.basinExtraState();
// 독립 기슭막이 유입측은 유출측과 같은 규칙 — 단 수 행이 **항상** 보인다.
basinExtraRow.classList.toggle(
"is-hidden",
!(isBasin && (basinExtra.canAdd || basinExtra.count > 0)),
!(ownTiers || (isBasin && (basinExtra.canAdd || basinExtra.count > 0))),
);
const basinExtraLabel = basinExtraRow.querySelector(".b06-structure-panel__label");
if (basinExtraLabel)
basinExtraLabel.textContent = ownTiers ? "다단 기슭막이(단)" : "집수정 계류측 기슭막이(단)";
basinCount = basinExtra.count;
basinCountValue.textContent = `${basinExtra.count}`;
// I형은 내공(높이·폭) 개념이 없다 — 9키의 **네 모서리**만 숨긴다.
@@ -138,11 +138,11 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
canNudge: (key) => key !== "inlet" || !ctx.inletIsBasin(),
// 걸음 분류(2026-08-29 사용자): **배관 기준벽만 1m**, 나머지(독립 기슭막이 기준벽·
// 배관/독립 종속 추가 벽·옛 D경로 독립 벽)는 0.1m.
fineMove: (key) =>
(key !== "inlet" && key !== "outlet") || ctx.hiddenPipe(),
fineMove: (key) => (key !== "inlet" && key !== "outlet") || ctx.hiddenPipe(),
optionsFor: () => ctx.inletOptions(),
extraState: () => ctx.extraState(),
basinExtraState: () => ctx.basinExtraState(),
hiddenPipe: () => ctx.hiddenPipe(),
setExtraCount: (count) => {
// 줄어들며 사라지는 벽이 선택 중이면 선택을 유출 벽으로 옮긴다(빈 대상 방지).
const active = ctx.activeRevet() ?? "";