- 벽 사이 성토사면 = 정확히 1:1.2 — 벽 높이를 자리에서 역산(닫힌식), 0.1m 눈금 올림 제거(물매 정확 유지 우선) - 아랫단 상단은 윗단 하단(수평 기초) 관통 금지 — 안쪽 이동 한계 - 다음 단 성토선 시작 = 윗단 하단 +0.5m 수평선 × 전면 경사선(1:0.3) 교차점 (배관 벽의 관 하단 꼭짓점과 같은 자리) - 시작점이 원지반 아래(벽 0.5m 이상 매몰)면 성토 불필요 — 다단 종료 - 추가 방식 = 유출 벽 조정창의 단 수 숫자 입력(+/- 버튼 대체), 지형 허용 단 수 초과 시 토스트로 가능한 단 수 안내 후 되는 만큼만 반영 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
9.3 KiB
TypeScript
211 lines
9.3 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Structure_Panel.ts
|
|
* 횡단도 안에 뜨는 **구조물 위치 조정 오버레이 창**(2026-08-21 사용자 확정).
|
|
*
|
|
* 조정 조작구는 카드 하단이 아니라 **해당 횡단도 안에** 있어야 한다 — 어느 도면의
|
|
* 구조물을 만지는지 눈으로 붙어 있어야 하고, 카드 하단은 표시 반폭 조작구와 섞여
|
|
* 구분이 안 됐다. 앞으로 구조물 위치 조정은 모두 이 창을 통한다.
|
|
*
|
|
* 창은 그래프 영역(`.b06-cross-card__chart-wrap`) 안에 절대 위치로 얹힌다. 줌 버튼은
|
|
* 우측 상단, 면적표는 중상단이라 이 창은 **좌측 하단**에 둔다.
|
|
* ========================================================================== */
|
|
|
|
import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
|
|
import { L } from "./B06_Section_UI_Section_Common";
|
|
|
|
/** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */
|
|
export interface StructurePanelDeps {
|
|
/** 지금 이동량(m, + = 계류측 바깥). 창을 열거나 값이 바뀔 때마다 읽는다. */
|
|
shiftFor: (key: RevetKey) => number;
|
|
/** 화면 좌(+)/우(−) 방향으로 미는 양(m). 부호 환산은 카드가 한다. */
|
|
nudge: (key: RevetKey, screenDeltaM: number) => void;
|
|
/** 자동 자리로 되돌린다. */
|
|
reset: (key: RevetKey) => void;
|
|
/** 지금 관 길이(m) — 조정 단위가 관 길이 1m이라 창에 같이 적는다. */
|
|
pipeLengthM: () => number | null;
|
|
/** 창을 닫는다 = 구조물 선택 해제. */
|
|
close: () => void;
|
|
/** 유입측 구조물 형식(드롭다운 값 — 2026-08-22 사용자). */
|
|
structureFor: () => InletStructureChoice;
|
|
/** 유입측 구조물 형식 변경 — 카드를 다시 그린다. */
|
|
setStructure: (value: InletStructureChoice) => void;
|
|
/** ◀/▶ 이동 가능 여부 — 집수정(자리 고정)은 숨긴다. */
|
|
canNudge: (key: RevetKey) => boolean;
|
|
/** 상황에 안 맞는 선택지 숨김(2026-08-22 사용자) — 기하가 판정한 가용성. */
|
|
optionsFor: () => { revetAllowed: boolean; basinLUAllowed: boolean };
|
|
/** 다단 기슭막이 상태 — 유출 벽 선택 시 개수 입력 행 표시 판단에 쓴다. */
|
|
extraState: () => { canAdd: boolean; count: number };
|
|
/** 다단 기슭막이 단 수 지정(2026-08-22 사용자 — 숫자 입력). 지형이 허락하는
|
|
* 단 수보다 크면 기하가 되는 만큼만 세우고 토스트로 알린다. */
|
|
setExtraCount: (count: number) => void;
|
|
}
|
|
|
|
export interface StructurePanelHandle {
|
|
root: HTMLElement;
|
|
/** null이면 숨긴다. 값이 오면 그 구조물 기준으로 다시 그린다. */
|
|
show: (key: RevetKey | null) => void;
|
|
}
|
|
|
|
/**
|
|
* 한 걸음 이동량(m) — **관 길이 1m**에 맞춘다(2026-08-21 사용자 확정).
|
|
* 관은 m 단위로 설치하므로 0.1m씩 밀면 그 끝수를 벽 폭이 흡수하게 되어 미는 동안
|
|
* 벽 단면이 변한다. 한 걸음을 1m로 두면 벽은 자리만 옮기고 형상은 그대로다.
|
|
*/
|
|
const STEP_M = 1.0;
|
|
|
|
function makeButton(label: string, title: string, onClick: () => void): HTMLButtonElement {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "b06-structure-panel__btn";
|
|
button.textContent = label;
|
|
button.title = title;
|
|
button.addEventListener("click", (event) => {
|
|
// 카드 선택·팬으로 번지면 도면이 다시 그려져 방금 맞춘 배율이 날아간다.
|
|
event.stopPropagation();
|
|
onClick();
|
|
});
|
|
return button;
|
|
}
|
|
|
|
/** 구조물 위치 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
|
|
export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHandle {
|
|
const root = document.createElement("div");
|
|
root.className = "b06-structure-panel is-hidden";
|
|
// 창 안에서의 클릭이 카드 선택으로 번지지 않게 한 번에 막는다.
|
|
root.addEventListener("click", (event) => event.stopPropagation());
|
|
|
|
const title = document.createElement("span");
|
|
title.className = "b06-structure-panel__title";
|
|
|
|
const value = document.createElement("span");
|
|
value.className = "b06-structure-panel__value";
|
|
|
|
let current: RevetKey | null = null;
|
|
const act = (run: (key: RevetKey) => void) => () => {
|
|
if (current) run(current);
|
|
};
|
|
|
|
const buttons = document.createElement("div");
|
|
buttons.className = "b06-structure-panel__buttons";
|
|
buttons.append(
|
|
makeButton(
|
|
"◀",
|
|
L("B06_Cross_Revet_Left"),
|
|
act((key) => deps.nudge(key, STEP_M)),
|
|
),
|
|
makeButton(
|
|
"▶",
|
|
L("B06_Cross_Revet_Right"),
|
|
act((key) => deps.nudge(key, -STEP_M)),
|
|
),
|
|
makeButton(
|
|
"↺",
|
|
L("B06_Cross_Revet_Reset"),
|
|
act((key) => deps.reset(key)),
|
|
),
|
|
);
|
|
|
|
// 다단 기슭막이 단 수 입력(2026-08-22 사용자) — 유출 벽 기준 숫자 입력.
|
|
const extraRow = document.createElement("label");
|
|
extraRow.className = "b06-structure-panel__struct";
|
|
const extraLabel = document.createElement("span");
|
|
extraLabel.textContent = L("B06_Cross_Extra_Count");
|
|
const countInput = document.createElement("input");
|
|
countInput.type = "number";
|
|
countInput.min = "0";
|
|
countInput.max = "9";
|
|
countInput.step = "1";
|
|
countInput.className = "b06-structure-panel__count";
|
|
countInput.addEventListener("change", () => {
|
|
const requested = Math.max(0, Math.min(9, Math.round(Number(countInput.value) || 0)));
|
|
countInput.value = String(requested);
|
|
deps.setExtraCount(requested);
|
|
});
|
|
extraRow.append(extraLabel, countInput);
|
|
|
|
// 유입측 구조물 형식 드롭다운(2026-08-22 사용자) — 자동/기슭막이/집수정 I·ㄴ·ㄷ.
|
|
const structureRow = document.createElement("label");
|
|
structureRow.className = "b06-structure-panel__struct";
|
|
const structureLabel = document.createElement("span");
|
|
structureLabel.textContent = L("B06_Cross_Struct_Label");
|
|
const select = document.createElement("select");
|
|
select.className = "b06-structure-panel__select";
|
|
const OPTIONS: Array<[InletStructureChoice, string]> = [
|
|
["auto", L("B06_Cross_Struct_Auto")],
|
|
["revet", L("B06_Cross_Struct_Revet")],
|
|
["I", L("B06_Cross_Struct_I")],
|
|
["L", L("B06_Cross_Struct_L")],
|
|
["U", L("B06_Cross_Struct_U")],
|
|
];
|
|
for (const [optionValue, label] of OPTIONS) {
|
|
const option = document.createElement("option");
|
|
option.value = optionValue;
|
|
option.textContent = label;
|
|
select.append(option);
|
|
}
|
|
select.addEventListener("change", () => {
|
|
deps.setStructure(select.value as InletStructureChoice);
|
|
});
|
|
structureRow.append(structureLabel, select);
|
|
|
|
const closeButton = makeButton("✕", L("B06_Cross_Revet_Close"), () => deps.close());
|
|
closeButton.classList.add("b06-structure-panel__close");
|
|
|
|
const head = document.createElement("div");
|
|
head.className = "b06-structure-panel__head";
|
|
head.append(title, closeButton);
|
|
root.append(head, value, structureRow, extraRow, buttons);
|
|
|
|
return {
|
|
root,
|
|
show: (key) => {
|
|
current = key;
|
|
root.classList.toggle("is-hidden", key === null);
|
|
if (!key) return;
|
|
const isExtra = key.startsWith("extra");
|
|
title.textContent = isExtra
|
|
? `${L("B06_Cross_Revet_Extra")} ${Number(key.slice(5)) + 1}`
|
|
: key === "outlet"
|
|
? L("B06_Cross_Revet_Outlet")
|
|
: deps.canNudge("inlet")
|
|
? L("B06_Cross_Revet_Inlet")
|
|
: L("B06_Cross_Struct_InletBasin");
|
|
// 형식 선택은 유입측에서만, ◀/▶/↺는 자리를 옮길 수 있는 구조물(기슭막이)만.
|
|
structureRow.classList.toggle("is-hidden", key !== "inlet");
|
|
if (key === "inlet") {
|
|
const current = deps.structureFor();
|
|
// 상황에 안 맞는 선택지는 숨긴다(2026-08-22 사용자). 단 지금 선택된 값은
|
|
// 남긴다 — 숨기면 셀렉트가 빈 값이 된다.
|
|
const allow = deps.optionsFor();
|
|
for (const option of select.options) {
|
|
const value = option.value as InletStructureChoice;
|
|
const hidden =
|
|
(value === "revet" && !allow.revetAllowed) ||
|
|
((value === "L" || value === "U") && !allow.basinLUAllowed);
|
|
option.hidden = hidden && value !== current;
|
|
}
|
|
select.value = current;
|
|
}
|
|
buttons.classList.toggle("is-hidden", !deps.canNudge(key));
|
|
// 단 수 입력은 **유출 벽에서만** — 성토부가 5m 이상(의무)이거나 이미 단이 있을 때.
|
|
const extra = deps.extraState();
|
|
extraRow.classList.toggle(
|
|
"is-hidden",
|
|
!(key === "outlet" && (extra.canAdd || extra.count > 0)),
|
|
);
|
|
countInput.value = String(extra.count);
|
|
const shift = deps.shiftFor(key);
|
|
// 자동 자리 기준 이동량 — 0이면 "자동"으로 적어 손을 안 댔음을 바로 알린다.
|
|
// 부호(+/−)만 적으면 좌·우 벽에서 어느 쪽인지 읽히지 않아 **안/바깥**으로 적는다.
|
|
const direction = L(shift > 0 ? "B06_Cross_Revet_Outward" : "B06_Cross_Revet_Inward");
|
|
const moved =
|
|
Math.abs(shift) < 1e-9
|
|
? L("B06_Cross_Revet_Auto")
|
|
: `${direction} ${Math.abs(shift).toFixed(1)}m`;
|
|
const pipeLength = deps.pipeLengthM();
|
|
value.textContent =
|
|
pipeLength === null ? moved : `${L("B06_Cross_Revet_Pipe")} ${pipeLength}m · ${moved}`;
|
|
},
|
|
};
|
|
}
|