feat(B06): 횡단도 안 조정창 = 제목 · 단추만 · 그림 끌기 두 축 함께 + 놓을 때 패널 걸음 맞춤 · 미리 보기 = 맞춘 자리(108-1 · 108-2)

- 조정창(오버레이): 읽기 줄 · 줄 이름 · 머리줄 집수정 ↺ 지움 · 끄는 중 미리 보기 글만 · 좌측 사본 그대로
- 끌기: 벽 · 집수정 · 세월교 = 수평 + 1:1.2 사면 · 다단 · 독립 벽 = 수평 + 수직 · BOX = 길이 + 표고
- 한계(±10 m · 노견 안쪽 · 노면 위 · 다단 선반 · BOX 길이 0)면 가장 가까운 허용 자리 · 반영 한 번
- 시험 test_108_drag_box_hit(끌기 · 걸음 · 미리 보기 = placePipeWall 결과 · 한계 · BOX 판정면)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-08 16:22:21 +09:00
co-authored by Claude Opus 5.5
parent 90383cab29
commit 42d0d725a5
10 changed files with 654 additions and 69 deletions
@@ -8,10 +8,13 @@
import type { CrossSection } from "./B06_Section_Api_Fetch";
import type { RevetKey } from "./B06_Section_UI_Cross_Culvert";
import {
FILL_SLOPE_RATIO_MIN,
PIPE_WALL_DEFAULT_RUN_M,
REVET_EMBED_DEPTH_M,
REVET_FORM_DEFAULT,
WALL_MIN_PURE_HEIGHT_M,
} from "./B06_Section_UI_Cross_Culvert_Const";
import { clampAxis, type AxisDragSpec } from "./B06_Section_UI_Cross_Panel_Base";
import type { CulvertLayout } from "./B06_Section_UI_Cross_Culvert_Types";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
import type { StructurePanelDeps } from "./B06_Section_UI_Cross_Structure_Panel";
@@ -149,6 +152,7 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
// 배관/독립 종속 추가 벽·옛 D경로 독립 벽)는 0.1m. 세월교 흙막이는 안이 관이라 1m(80).
fineMove: (key) =>
(key !== "inlet" && key !== "outlet") || (ctx.hiddenPipe() && !ctx.fordGuard?.()),
dragAxes: (key, basin) => wallDragAxes(ctx, key, basin),
extraState: () => ctx.extraState(),
isLinkedCard: () => ctx.isLinked,
basinExtraState: () => ctx.basinExtraState(),
@@ -222,6 +226,70 @@ export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDe
};
}
/** 끌기 한계 ±10 m — 제어기 `clampMove`(`_Page_Station_Controls`)와 같다. */
const DRAG_LIMIT_M = 10;
const round1 = (value: number): number => Math.round(value * 10) / 10;
/**
* 벽 · 집수정 그림 끌기 두 축(108-2) — 축 뜻 · 한계 · 한 번 반영. 한계는 저장 · 기하가 거는 것 중
* 미리 알 수 있는 것(±10 m · 안쪽 금지 · 노면 위 금지 · 다단 선반 1.2·d) — 넘으면 가장 가까운 허용 자리.
* · 배관 기준벽 · 집수정 = ▲▼ 1:1.2 사면(`placePipeWall` · `_Culvert_Basin` 사면 이동)
* · 다단 · 독립 벽 = ▲▼ 순수 수직(`_Culvert_Extra` · `_Revetment` 축)
*/
function wallDragAxes(
ctx: StructurePanelContext,
key: RevetKey,
basin: boolean,
): Omit<AxisDragSpec, "stepM"> | null {
if (!ctx.revetOffset) return null;
const outward = ctx.outwardOf(basin ? "inlet" : key);
if (basin) {
const from = ctx.inletStructure?.adjustFor(ctx.section);
if (!from) return null;
const lateralMove = (left: number): number =>
clampAxis(from.lateralM, left * outward, 0, DRAG_LIMIT_M);
return {
slopeOutward: outward,
clamp: (left, down) => [
lateralMove(left) * outward,
clampAxis(from.slopeM, down, 0, DRAG_LIMIT_M),
],
apply: (left, down) =>
ctx.inletStructure?.updateAdjust(ctx.adjustChainage(), {
...from,
lateralM: round1(from.lateralM + left * outward),
slopeM: round1(from.slopeM + down),
}),
};
}
const adjust = ctx.adjustOf(key);
const d0 = adjust.d ?? ctx.appliedD().get(key) ?? 0;
const diagonal = key === "inlet" || key === "outlet";
// x 하한 — 배관 기준벽 = 안쪽 금지(흙막이는 본체 끝이 0점이라 안 막음 · 73-4) · 다단 = 선반 ≥ 0 ·
// 독립 = 성토선 각도(1.2·d − 기본 run)
const xLow = (d: number): number =>
isTierKey(key)
? Math.max(0, FILL_SLOPE_RATIO_MIN * d)
: key === "own"
? FILL_SLOPE_RATIO_MIN * d - PIPE_WALL_DEFAULT_RUN_M
: ctx.bodyGuard?.()
? -DRAG_LIMIT_M
: 0;
return {
slopeOutward: diagonal ? outward : null,
clamp: (left, down) => {
const dMove = clampAxis(d0, down, 0, DRAG_LIMIT_M);
const low = Math.max(xLow(d0 + dMove), -DRAG_LIMIT_M);
return [clampAxis(adjust.x, left * outward, low, DRAG_LIMIT_M) * outward, dMove];
},
apply: (left, down) =>
ctx.revetOffset?.update(ctx.adjustChainage(), key, {
x: round1(adjust.x + left * outward),
d: round1(d0 + down),
}),
};
}
/**
* 조정창이 쓸 **카드 상태**를 마지막 기하 결과에서 뽑는다(2026-08-25 분리 — 카드
* 렌더러 700줄). 값 계산은 하지 않고 레이아웃 결과를 조정창 키 체계로 옮기기만 한다.