- 새 logic/B05_Profile_RouteEdit_Fit(unionArea · fitAreaToView) — 옛 _Visible 자리 - _RouteEdit_Cover 지움 · View · CurveBar · CSS 의 덮인 높이 따라가기 걷음(345-5 덧 · 덧 2) - 글판 · 도움말 접기(덧 3)는 그대로 - 시험: test_345_5b_plan_visible_area 3 고쳐 씀 · test_345_5c 자리 줄 고침 · 관련 86 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
210 lines
12 KiB
TypeScript
210 lines
12 KiB
TypeScript
import { createButton } from "@ui/ui_template_elements";
|
|
import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint";
|
|
|
|
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Chrome.ts
|
|
* 계획노선 편집(평면)의 **뼈대** — 칸·단추·오버레이 판을 만들고 자주 쓰는 요소를 집어 준다.
|
|
*
|
|
* `B05_Profile_UI_RouteEdit.ts` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-12). 생김새만 있고
|
|
* 동작은 없다 — 배선은 본체와 각 조각(`_Apply`·`_Rotate`·`_History`…)이 한다.
|
|
*
|
|
* **배치**(2026-09-12 사용자 지시 ⑩~⑬·⑱) — 아래 정보행을 없애고 지도 위 오버레이로 옮겼다.
|
|
* 단추는 제목행 오른쪽, 조작 설명은 지도 왼쪽 위 2열, 상태·범례는 왼쪽 아래, 잰 값은 오른쪽
|
|
* 아래. 메인 창은 왼쪽으로 밀고 오른쪽 세로 칸에 횡단 두 판이 앉는다.
|
|
*
|
|
* 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 ·
|
|
* 닫기 단추는 걷었다 · [취소] → [저장 전으로] · [확인] · [노선 반영]은 없앰(346-3 — 노선은 페이지 [저장]이
|
|
* 맨 앞에서 보냄) · 오른쪽 횡단 칸은 접는 칸.
|
|
* 머리 `tools` 자리에 부른 쪽 단추([3D])가 앉는다. [노선 추천] · 현재 추천 · [초기화]는 좌측
|
|
* 「노선 추천」 칸으로 옮겼다(345-4 · `_Panel_Recommend`).
|
|
* ========================================================================== */
|
|
|
|
/** 회전 단추 아이콘 — **반만 도는 화살표**(2026-09-12 사용자 지시 ㉙). 한 바퀴를 다 그린
|
|
* 기호(`↺`·`↻`)는 「한 바퀴 돈다」로 읽혀 한 칸씩 도는 동작과 안 맞았다. */
|
|
export const HALF_TURN_ICON = {
|
|
ccw: `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none"
|
|
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M13 8a5 5 0 0 0-10 0" /><path d="M3 8 1.2 5.6" /><path d="M3 8 5.4 6.6" /></svg>`,
|
|
cw: `<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none"
|
|
stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 8a5 5 0 0 1 10 0" /><path d="M13 8 14.8 5.6" /><path d="M13 8 10.6 6.6" /></svg>`,
|
|
};
|
|
|
|
export interface RouteEditChrome {
|
|
overlay: HTMLElement;
|
|
canvas: HTMLCanvasElement;
|
|
status: HTMLElement;
|
|
busy: HTMLElement;
|
|
measureBox: HTMLElement;
|
|
measureText: HTMLElement;
|
|
measureButton: HTMLButtonElement;
|
|
minorContourButton: HTMLButtonElement;
|
|
/** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */
|
|
stationSnapButton: HTMLButtonElement;
|
|
/** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */
|
|
terrainSelect: HTMLSelectElement;
|
|
}
|
|
|
|
// 글판 접기 기억(345-5 덧 3) — 페이지 동안만(모듈 값) · 다시 열어도 그대로.
|
|
let hintOpen = false;
|
|
let infoCollapsed = false;
|
|
|
|
/** 뼈대를 만들어 `parent` 에 붙이고, 자주 쓰는 요소를 집어 돌려준다. */
|
|
export function createRouteEditChrome(
|
|
parent: HTMLElement,
|
|
tools: HTMLElement[] = [],
|
|
): RouteEditChrome {
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "b05-routeedit";
|
|
overlay.innerHTML = `
|
|
<div class="b05-routeedit__box" aria-label="계획노선 편집">
|
|
<div class="b05-routeedit__head">
|
|
<h3>계획노선</h3>
|
|
<span class="b05-routeedit__actions">
|
|
<span data-slot="tools"></span>
|
|
<i class="b05-routeedit__divider" aria-hidden="true"></i>
|
|
<label class="b05-routeedit__terrain" hidden
|
|
title="편집 창 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
|
|
<select class="ui-select ui-select--sm" data-act="terrain">
|
|
<option value="las">라이다</option>
|
|
<option value="sheet">수치지형도</option>
|
|
</select>
|
|
</label>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="fit"
|
|
title="등고 영역(등고선 범위 · 노선)을 평면 가운데 알맞게 맞춥니다 (회전은 풂)">맞춤</button>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="contour-minor"
|
|
aria-pressed="false" disabled
|
|
title="1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)">1m 등고선</button>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="measure"
|
|
title="노선 위 두 점을 눌러 거리·기울기를 잽니다 (Shift+클릭도 같음)">거리 재기</button>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="station-snap"
|
|
aria-pressed="false"
|
|
title="곡선(호) 안 측점 하나를 곡선 중점(MC)으로 옮깁니다 — [저장] 때 저장">측점 스냅</button>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="footprint"
|
|
title="도로 끝 · 비탈 끝(깎기 붉음 · 쌓기 파랑)을 평면에 겹칩니다">발자국</button>
|
|
<i class="b05-routeedit__divider" aria-hidden="true"></i>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="undo"
|
|
title="되돌리기 (Ctrl+Z)" disabled>↶ 되돌리기</button>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="redo"
|
|
title="다시하기 (Ctrl+Y)" disabled>↷ 다시하기</button>
|
|
<button type="button" class="ui-btn ui-btn--ghost" data-act="cancel"
|
|
title="마지막 [저장] 뒤 고친 노선을 버리고 그때 노선으로 되돌립니다 — 되돌리기로 다시 살릴 수 있습니다">저장 전으로</button>
|
|
</span>
|
|
</div>
|
|
<div class="b05-routeedit__canvas-wrap">
|
|
<canvas class="b05-routeedit__canvas"></canvas>
|
|
<div class="b05-routeedit__hint">
|
|
<button type="button" class="ui-btn ui-btn--glass b05-routeedit__hint-toggle"
|
|
data-act="hint-toggle" aria-expanded="false" title="조작 도움말 펼치기 · 접기">?</button>
|
|
<div class="b05-routeedit__hint-body">
|
|
<span>노드 끌기 = 옮기기</span><span>노드 클릭 = R 라벨</span>
|
|
<span>선 두 번 클릭 = 노드 추가</span><span>노드 오른쪽 클릭 = 삭제</span>
|
|
<span>측점 눈금 클릭 = 횡단 미리보기</span><span>Shift+클릭 = 거리·기울기</span>
|
|
<span>가운데(휠) 버튼 끌기 = 지도 이동</span><span>휠 = 확대</span>
|
|
</div>
|
|
</div>
|
|
<div class="b05-routeedit__spin">
|
|
<button type="button" class="ui-btn ui-btn--glass" data-act="rotate-ccw"
|
|
title="반시계로 돌리기" aria-label="반시계로 돌리기">${HALF_TURN_ICON.ccw}</button>
|
|
<button type="button" class="ui-btn ui-btn--glass" data-act="rotate-cw"
|
|
title="시계로 돌리기" aria-label="시계로 돌리기">${HALF_TURN_ICON.cw}</button>
|
|
</div>
|
|
<div class="b05-routeedit__measure" hidden>
|
|
<span class="b05-routeedit__measure-text"></span>
|
|
<span data-slot="measure-close"></span>
|
|
</div>
|
|
<div class="b05-routeedit__info">
|
|
<span class="b05-routeedit__status">노선을 읽는 중…</span>
|
|
<span class="b05-routeedit__legend">
|
|
<span><i class="is-expected"></i> 예상노선(원본)</span>
|
|
<span><i class="is-planned"></i> 계획노선</span>
|
|
</span>
|
|
<span class="b05-routeedit__summary"></span>
|
|
<button type="button" class="b05-routeedit__info-toggle" data-act="info-toggle"
|
|
aria-expanded="true" title="상태 · 범례 글판 접기 · 펴기">▾</button>
|
|
</div>
|
|
</div>
|
|
<div class="b05-routeedit__busy" hidden><span></span></div>
|
|
</div>
|
|
<aside class="b05-routeedit__side-wrap">
|
|
<button type="button" class="ui-btn ui-btn--ghost b05-routeedit__side-toggle"
|
|
data-act="side-toggle" aria-expanded="true" title="오른쪽 횡단 미리보기 칸 접기 · 펴기">횡단</button>
|
|
<div class="b05-routeedit__side"></div>
|
|
</aside>`;
|
|
parent.append(overlay);
|
|
overlay.querySelector('[data-slot="tools"]')!.replaceWith(...tools);
|
|
|
|
// 횡단 미리보기 = 접는 칸 — 좁은 화면은 접힌 채 시작(지도가 먼저다).
|
|
const sideWrap = overlay.querySelector<HTMLElement>(".b05-routeedit__side-wrap")!;
|
|
const sideToggle = overlay.querySelector<HTMLButtonElement>('[data-act="side-toggle"]')!;
|
|
const setSide = (open: boolean): void => {
|
|
sideWrap.classList.toggle("is-collapsed", !open);
|
|
sideToggle.setAttribute("aria-expanded", String(open));
|
|
};
|
|
setSide(window.innerWidth >= 1500);
|
|
sideToggle.addEventListener("click", () => setSide(sideWrap.classList.contains("is-collapsed")));
|
|
|
|
// 도움말 = 기본 접힘 · [?] 한 번에 펼침(345-5 덧 3).
|
|
const hint = overlay.querySelector<HTMLElement>(".b05-routeedit__hint")!;
|
|
const hintToggle = hint.querySelector<HTMLButtonElement>('[data-act="hint-toggle"]')!;
|
|
const setHint = (open: boolean): void => {
|
|
hintOpen = open;
|
|
hint.classList.toggle("is-open", open);
|
|
hintToggle.setAttribute("aria-expanded", String(open));
|
|
};
|
|
setHint(hintOpen);
|
|
hintToggle.addEventListener("click", () => setHint(!hintOpen));
|
|
|
|
// 상태 · 범례 접기 — 접으면 한 줄 「계획노선 N m」(상태 글에서 읽음 · 345-5 덧 3).
|
|
const info = overlay.querySelector<HTMLElement>(".b05-routeedit__info")!;
|
|
const infoToggle = info.querySelector<HTMLButtonElement>('[data-act="info-toggle"]')!;
|
|
const summary = info.querySelector<HTMLElement>(".b05-routeedit__summary")!;
|
|
const setInfo = (collapsed: boolean): void => {
|
|
infoCollapsed = collapsed;
|
|
info.classList.toggle("is-collapsed", collapsed);
|
|
infoToggle.textContent = collapsed ? "▸" : "▾";
|
|
infoToggle.setAttribute("aria-expanded", String(!collapsed));
|
|
};
|
|
setInfo(infoCollapsed);
|
|
infoToggle.addEventListener("click", () => setInfo(!infoCollapsed));
|
|
|
|
const measureCloseButton = createButton({ label: "✕", variant: "ghost" });
|
|
measureCloseButton.classList.add("b05-routeedit__measure-close");
|
|
measureCloseButton.setAttribute("aria-label", "닫기");
|
|
measureCloseButton.title = "닫기";
|
|
overlay.querySelector('[data-slot="measure-close"]')!.replaceWith(measureCloseButton);
|
|
|
|
const canvas = overlay.querySelector<HTMLCanvasElement>(".b05-routeedit__canvas")!;
|
|
const status = overlay.querySelector<HTMLElement>(".b05-routeedit__status")!;
|
|
const syncSummary = (): void => {
|
|
const text = status.textContent ?? "";
|
|
const length = /계획노선 ([\d.]+)m/.exec(text)?.[1];
|
|
summary.textContent = length ? `계획노선 ${length} m` : text;
|
|
};
|
|
new MutationObserver(syncSummary).observe(status, { childList: true, characterData: true });
|
|
syncSummary();
|
|
const measureBox = overlay.querySelector<HTMLElement>(".b05-routeedit__measure")!;
|
|
const measureText = overlay.querySelector<HTMLElement>(".b05-routeedit__measure-text")!;
|
|
const measureButton = overlay.querySelector<HTMLButtonElement>('[data-act="measure"]')!;
|
|
const busy = overlay.querySelector<HTMLElement>(".b05-routeedit__busy")!;
|
|
const minorContourButton = overlay.querySelector<HTMLButtonElement>(
|
|
'[data-act="contour-minor"]',
|
|
)!;
|
|
const terrainSelect = overlay.querySelector<HTMLSelectElement>('[data-act="terrain"]')!;
|
|
const stationSnapButton = overlay.querySelector<HTMLButtonElement>('[data-act="station-snap"]')!;
|
|
bindFootprintToggle(overlay.querySelector<HTMLButtonElement>('[data-act="footprint"]')!);
|
|
|
|
return {
|
|
overlay,
|
|
canvas,
|
|
status,
|
|
busy,
|
|
measureBox,
|
|
measureText,
|
|
measureButton,
|
|
minorContourButton,
|
|
stationSnapButton,
|
|
terrainSelect,
|
|
};
|
|
}
|