- 옵션 토글 켜짐 색을 주 액션 색으로 바꾼다(붉은 채움이 경고처럼 읽혔다). - 같은 문구 토스트가 연달아 오면 앞 토스트가 떠 있는 동안 무시한다. - 벽 내부 표현을 형태별로 나눈다: 메=돌, 찰=돌+줄눈, 콘크리트=사선 해칭, 돌망태=2열 격자, 통나무=원, 바자=말뚝+엮음. 배관용·독립용·추가용이 공용 `appendWallHatch` 하나를 쓴다(배관 오버레이의 중복 코드 제거). - 추가(다단) 기슭막이를 고르면 유입구·유출구 아래에 전용 칸이 서고 형태·높이·구간값·옵션 행이 그 칸으로 옮겨 간다. 선택이 풀리면 사라진다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
148 lines
6.6 KiB
TypeScript
148 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Adjust_Dock.ts
|
|
* 좌측 [구조물 배치] 섹션 안 **[횡단 조정] 하위 컨테이너**(2026-08-29 사용자:
|
|
* B05/B06 일원화 — 오버레이 조정창 항목을 좌측에도 보이되, 기존 폼과 구분되게
|
|
* 접이식 하위 컨테이너에 담는다. 9키·십자 위치제어는 오버레이 전용).
|
|
*
|
|
* 카드가 조정창을 만들 때 같은 deps로 **사본 창**을 하나 더 만들어 이 슬롯에
|
|
* 세운다(adopt). 값·동작이 같은 함수라 오버레이·좌측 어느 쪽을 만져도 카드
|
|
* 재렌더에서 두 창이 함께 새 값으로 그려진다 — 별도 동기화 코드가 없다.
|
|
* 카드가 여럿이어도 슬롯은 하나 — 마지막으로 고른 구조물의 사본만 선다.
|
|
* ========================================================================== */
|
|
|
|
let dockRoot: HTMLDetailsElement | null = null;
|
|
let slot: HTMLElement | null = null;
|
|
let placeholder: HTMLElement | null = null;
|
|
/** 유입구·유출구·추가 기슭막이 그룹의 이식 자리(2026-08-29 지시 4, 2026-08-30 지시 4).
|
|
* B06 패널이 등록한다. */
|
|
let sideSlots: { inlet: HTMLElement; outlet: HTMLElement; extra: HTMLElement } | null = null;
|
|
|
|
/** 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구·추가 기슭막이). */
|
|
export function setAdjustSideSlots(next: {
|
|
inlet: HTMLElement;
|
|
outlet: HTMLElement;
|
|
extra: HTMLElement;
|
|
}): void {
|
|
sideSlots = next;
|
|
}
|
|
|
|
/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값까지 전부 온다. */
|
|
const EXTRA_ROWS = [
|
|
".b06-structure-panel__struct--form",
|
|
".b06-structure-panel__struct--height",
|
|
".b06-structure-panel__struct--span",
|
|
".b06-structure-panel__struct--tier",
|
|
".b06-structure-panel__struct--options",
|
|
].join(", ");
|
|
|
|
/** 유입구·유출구는 폼이 이미 가진 항목 말고 단 수·옵션만 받는다. */
|
|
const SIDE_ROWS = [
|
|
".b06-structure-panel__struct--tier",
|
|
".b06-structure-panel__struct--options",
|
|
].join(", ");
|
|
|
|
/** 이식 자리가 지금 화면에 서 있는지 — 숨겨진 그룹(다른 시설)이면 옮기지 않는다. */
|
|
function slotUsable(target: HTMLElement | undefined): target is HTMLElement {
|
|
if (!target) return false;
|
|
const group = target.closest("fieldset");
|
|
return !!group && !group.hidden;
|
|
}
|
|
|
|
/** 옮겨 온 행과 그 **제자리 표식** — 되돌릴 때 원래 순서로 꽂기 위해 짝으로 둔다.
|
|
* 표식 없이 지우면 행 DOM이 사라져, 같은 창에서 다른 벽을 고를 때 그 행이 다시는
|
|
* 나오지 않는다(2026-08-29 사용자 보고: 단 수·옵션이 사라짐). */
|
|
const movedRows: Array<{ row: HTMLElement; anchor: Comment }> = [];
|
|
|
|
/** 옮겨 온 행을 **원래 자리로 되돌리고** 슬롯을 비운다. 그룹 강조도 끈다. */
|
|
function clearSideSlots(): void {
|
|
while (movedRows.length) {
|
|
const { row, anchor } = movedRows.pop()!;
|
|
anchor.parentNode?.replaceChild(row, anchor);
|
|
}
|
|
for (const target of [sideSlots?.inlet, sideSlots?.outlet, sideSlots?.extra]) {
|
|
target?.replaceChildren();
|
|
const group = target?.closest("fieldset");
|
|
group?.classList.remove("is-active");
|
|
// 추가 기슭막이 칸은 고른 다단 벽이 있을 때만 서 있는다 — 비면 그룹째 내린다.
|
|
if (group && target === sideSlots?.extra) group.hidden = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을
|
|
* 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다.
|
|
*/
|
|
function relocateSideRows(panelRoot: HTMLElement): void {
|
|
clearSideSlots();
|
|
const side = panelRoot.dataset.side;
|
|
if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return;
|
|
const target = sideSlots[side];
|
|
// 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 여기서 세운다.
|
|
if (side === "extra") {
|
|
const group = target.closest("fieldset");
|
|
if (!group) return;
|
|
group.hidden = false;
|
|
const legend = group.querySelector("legend");
|
|
if (legend) legend.textContent = panelRoot.dataset.extraTitle || "추가 기슭막이";
|
|
} else if (!slotUsable(target)) return;
|
|
const rows = panelRoot.querySelectorAll<HTMLElement>(side === "extra" ? EXTRA_ROWS : SIDE_ROWS);
|
|
rows.forEach((row) => {
|
|
if (row.classList.contains("is-hidden")) return;
|
|
// 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다.
|
|
const anchor = document.createComment("adjust-row");
|
|
row.parentNode?.insertBefore(anchor, row);
|
|
movedRows.push({ row, anchor });
|
|
target.append(row);
|
|
});
|
|
// 고른 구조물이 어느 쪽인지 그룹 테두리로 알린다(2026-08-29 사용자 지시 3).
|
|
target.closest("fieldset")?.classList.add("is-active");
|
|
}
|
|
|
|
function ensureDock(): void {
|
|
if (dockRoot) return;
|
|
dockRoot = document.createElement("details");
|
|
dockRoot.className = "b06-adjust-dock";
|
|
dockRoot.open = true;
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = "횡단 조정";
|
|
placeholder = document.createElement("p");
|
|
placeholder.className = "b06-adjust-dock__empty";
|
|
placeholder.textContent = "횡단도에서 구조물(벽·구체)을 고르면 조정 항목이 여기에 뜹니다.";
|
|
slot = document.createElement("div");
|
|
slot.className = "b06-adjust-dock__slot";
|
|
dockRoot.append(summary, placeholder, slot);
|
|
}
|
|
|
|
/** 하위 컨테이너 루트 — 페이지가 [구조물 배치] 섹션 본문에 붙인다. */
|
|
export function adjustDockRoot(): HTMLElement {
|
|
ensureDock();
|
|
return dockRoot!;
|
|
}
|
|
|
|
/** 페이지 진입 시 초기 상태(빈 슬롯 + 안내문)로 되돌린다. */
|
|
export function resetAdjustDock(): void {
|
|
ensureDock();
|
|
slot!.replaceChildren();
|
|
clearSideSlots();
|
|
placeholder!.hidden = false;
|
|
}
|
|
|
|
/** 조정창 사본을 슬롯에 세운다 — 이미 다른 사본이 있으면 갈아 끼운다. */
|
|
export function adoptAdjustPanel(panelRoot: HTMLElement): void {
|
|
ensureDock();
|
|
panelRoot.classList.add("b06-structure-panel--dock");
|
|
if (slot!.firstChild !== panelRoot || slot!.childNodes.length !== 1) {
|
|
slot!.replaceChildren(panelRoot);
|
|
}
|
|
placeholder!.hidden = true;
|
|
relocateSideRows(panelRoot);
|
|
}
|
|
|
|
/** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */
|
|
export function releaseAdjustPanel(panelRoot: HTMLElement): void {
|
|
if (!slot || !slot.contains(panelRoot)) return;
|
|
slot.replaceChildren();
|
|
clearSideSlots();
|
|
placeholder!.hidden = false;
|
|
}
|