- 겹 표(map_layer_list) = 그룹1 22 줄 표 차례 · title = 호버제목 · group(수계망도 · 도로 · 토지·건물 · 제한·재해) · pages · 숨김 표 걷음 · 도로300m 한 줄(「도로」 묶음 · 기능 남김) - 겹 줄(map_layer_bar) = 단독 넷 | 묶음 넷(여럿 고르기 메뉴 · 켠 수 배지 · 안 닫힘) | 그래픽 카드 · sync 가 단추 · 메뉴 줄 막힘 · 글을 맞춤(why = 페이지 까닭) - 도구 표(map_toolbar) = 그룹2 17 줄 title · mapToolGroups 가 글을 표로 맞춤 · 기본 단추 생성 · 지형 칩 막힘 「수치지형도/라이다 — 까닭」 - 띠 = 한 자료 · 겹 둘(GL fillEdge 거름 · 옛 캔버스 · 범례 줄 따로) · B05 띠 받기(D03 과 같은 서버 길 · bandCollection 공용) - D03 = 겹 줄 · 도구줄 한 행(createMapToolRow) · 222px 기댐 · 줄바꿈 CSS 걷음 · 측점 · 기울기 호버 덮어쓰기 걷음 · LAS 올리기 호버 걷음 - B05 = 세로 두 줄 → 한 행 · 호버 덮어쓰기 걷음(도구줄 · 1m 등고선 · 3D · 추천) · 겹 막힘 querySelector 덮어쓰기 → 공용 sync · 임도(업로드) 보임 + 「받은 임도 자료 없음」 막힘 · 스냅 간격 날 select → 고르기 메뉴(하나) - 켬끔 기억 열쇠 · 측점 · 기울기 처음 값 · 그래픽 카드 취향 열쇠 둘 그대로 - 낡은 시험 고침(568_1 · 622_2 · 624_2 · 624_3 · 553_4 · 625_draw · b05_routeedit_initial · 626_3 · 390_4 · 346_2 · 353_1 · 581_2 · 584_1 · 584_3) — 표 꼴 row() · 호버 글 = 표 · 한 행 · 고르기 메뉴로 바뀐 글을 잡음 · 새 지킴 634_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
291 lines
15 KiB
TypeScript
291 lines
15 KiB
TypeScript
import { createIconButton } from "@ui/ui_template_elements";
|
|
import { createPickMenu, type PickMenu } from "@ui/ui_template_context_menu";
|
|
import { createMapToolRow, createMapTools, mapToolButton } from "@ui/ui_template_map_tools";
|
|
import { mapToolGroups } from "../A00_Common/map/map_toolbar";
|
|
import { createSidePanels } from "@ui/ui_template_side_panels";
|
|
import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint";
|
|
import { bindMarkToggle } from "./B05_Profile_UI_RouteEdit_Marks";
|
|
import { createRouteEditGl, type RouteEditGl } from "./B05_Profile_UI_RouteEdit_Gl";
|
|
import { createRouteEditLayers, type RouteEditLayers } from "./B05_Profile_UI_RouteEdit_Layers";
|
|
import { createRecPreview, type RecPreview } from "./B05_Profile_UI_RouteEdit_RecPreview";
|
|
import { createRouteTerrain, type RouteTerrain } from "./B05_Profile_UI_RouteEdit_Terrain";
|
|
import { bottomRowNarrow, infoLiftPx } from "./logic/B05_Profile_RouteEdit_InfoLift";
|
|
|
|
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Chrome.ts
|
|
* 계획노선 편집(평면)의 **뼈대** — 칸·단추·오버레이 판을 만들고 자주 쓰는 요소를 집어 준다.
|
|
*
|
|
* `B05_Profile_UI_RouteEdit.ts` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-12). 생김새만 있고
|
|
* 동작은 없다 — 배선은 본체와 각 조각(`_Apply`·`_Rotate`·`_History`…)이 한다.
|
|
*
|
|
* **배치**(352-1) — 지도가 칸 전체 · 머리 줄 · 테두리 없음. 도구는 3D 와 같은 떠 있는 유리 단추
|
|
* (지도 왼쪽 위 · ☰ 비킴), [?] 조작 설명은 도구줄 아래 2열, 상태·범례는 왼쪽 아래, 잰 값은 오른쪽
|
|
* 아래. 횡단 두 판은 지도 오른쪽을 덮는 판(아래 종단 판이 그 위).
|
|
* 한 행(634-3 · `createMapToolRow`) = 왼쪽 공용 겹 줄(`_Layers` · 끝 = 그래픽 카드 단추) · 오른쪽 도구줄 — 영역마다 가로 밀기.
|
|
* 도구줄 차례 = 공용 표(624-3 · `A00_Common/map/map_toolbar` 「b05」) — [?] | ↶ ↷ | 3D ⟲ ⟳ | 1m 등고선 · 측점 · 기울기 ·
|
|
* 절/성토선 · 거리 재기 | 측점 스냅 · 간격(고르기 메뉴) | 예정노선 변경 · 원본(626-3) | 추천 노선 보기(626-1) | 설계 지형 칩(626-1).
|
|
* 범례 = 상태 글판 위. 마우스 글 = 공용 표 호버제목(634-2 · 페이지 덮어쓰기 없음).
|
|
*
|
|
* 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 ·
|
|
* 닫기 단추는 걷었다 · [취소] · [확인] · [노선 반영] · [저장 전으로]는 없앰(346-3 · 352-2 — 노선은 페이지
|
|
* [저장]이 맨 앞에서 보냄) · 오른쪽 횡단 칸은 접는 칸.
|
|
* 머리 `tools` 자리에 부른 쪽 단추([3D])가 앉는다. 옛 [노선 추천] 모달 · 좌측 「노선 추천」 칸은 걷음(626-1 ·
|
|
* 추천 = 도구줄 미리보기 · 지형 바꿈 = 지형 칩).
|
|
* ========================================================================== */
|
|
|
|
/** 회전 단추 아이콘 — **반만 도는 화살표**(2026-09-12 사용자 지시 ㉙). 한 바퀴를 다 그린
|
|
* 기호(`↺`·`↻`)는 「한 바퀴 돈다」로 읽혀 한 칸씩 도는 동작과 안 맞았다. */
|
|
export const HALF_TURN_ICON = {
|
|
ccw: `<span class="ui-btn__icon"><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></span>`,
|
|
cw: `<span class="ui-btn__icon"><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></span>`,
|
|
};
|
|
|
|
/** 되돌리기 · 다시하기 아이콘 — **곧은 꼬리 + 갈고리**(↩ ↪ · 요구 241-9). 둥근 화살표(`↶`·`↷`)는
|
|
* 회전 단추와 헷갈렸다. 종단 도구줄도 같은 그림. */
|
|
export const HISTORY_ICON = {
|
|
undo: `<span class="ui-btn__icon"><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 6h7.5a3.5 3.5 0 0 1 0 7H7" /><path d="M6 3 3 6l3 3" /></svg></span>`,
|
|
redo: `<span class="ui-btn__icon"><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 6H5.5a3.5 3.5 0 0 0 0 7H9" /><path d="M10 3l3 3-3 3" /></svg></span>`,
|
|
};
|
|
|
|
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;
|
|
/** 스냅 간격 고르기 메뉴(하나 · 634-3) — 누름 배선 = `onPick`(`_SnapStep`). */
|
|
snapStep: PickMenu & { onPick: (fn: (value: string | null) => void) => void };
|
|
/** 예정노선 그리기 켬끔(626-3 · 배선 = `B05_Profile_UI_ExpectedDraw`). */
|
|
expectedDrawButton: HTMLButtonElement;
|
|
/** 예정노선(원본 · 파일입력) 옅은 점선 켬끔(626-3 · 535-1). */
|
|
expectedOriginalButton: HTMLButtonElement;
|
|
/** 설계 지형 칩(626-1) — 지형 읽은 뒤 `load`. */
|
|
terrain: RouteTerrain;
|
|
/** 추천 노선 미리보기(626-1) — 그리기 안에서 `draw`. */
|
|
preview: RecPreview;
|
|
/** 공용 지도 겹(624-3). */
|
|
layers: RouteEditLayers;
|
|
/** 장면 새로 그리기(겹 켬끔 · 추천 보기 뒤) — 편집기가 건다(없으면 GL 마지막 장면). */
|
|
onRedraw: (draw: () => void) => void;
|
|
}
|
|
|
|
/** 손잡이 자리(24) + 틈(8) — 접혀도 남는 폭(`.ui-side-panels` margin-left). */
|
|
const SIDE_HANDLE_PX = 32;
|
|
|
|
// 글판 접기 기억(345-5 덧 3) — 페이지 동안만(모듈 값) · 다시 열어도 그대로.
|
|
let hintOpen = false;
|
|
const HINT_LINES = [
|
|
"노드 끌기 = 옮기기",
|
|
"노드 클릭 = R 라벨",
|
|
"선 두 번 클릭 = 노드 추가",
|
|
"노드 오른쪽 클릭 = 삭제",
|
|
"측점 눈금 클릭 = 횡단 미리보기",
|
|
"Shift+클릭 = 거리·기울기",
|
|
"가운데(휠) 버튼 끌기 = 지도 이동",
|
|
"휠 = 확대",
|
|
"Ctrl+휠 = 돌리기(한 칸 5°)",
|
|
];
|
|
let infoCollapsed = false;
|
|
|
|
/** 뼈대를 만들어 `parent` 에 붙이고, 자주 쓰는 요소를 집어 돌려준다. */
|
|
export function createRouteEditChrome(
|
|
parent: HTMLElement,
|
|
projectId: string,
|
|
tools: HTMLElement[] = [],
|
|
host?: { onDispose: (fn: () => void) => void },
|
|
): RouteEditChrome {
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "b05-routeedit";
|
|
overlay.innerHTML = `
|
|
<div class="b05-routeedit__canvas-wrap" aria-label="계획노선 편집">
|
|
<canvas class="b05-routeedit__canvas"></canvas>
|
|
<div data-slot="map-tools"></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="ui-legend ui-legend--vertical b05-routeedit__legend">
|
|
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--dash is-expected"
|
|
style="--legend-color: var(--color-text-secondary)"></i>예상노선(원본)</span>
|
|
<span class="ui-legend__item"><i class="ui-legend__mark ui-legend__mark--line is-planned"
|
|
style="--legend-color: var(--map-route)"></i>계획노선</span>
|
|
</span>
|
|
<span class="b05-routeedit__summary"></span>
|
|
<button type="button" data-size="18" class="ui-icon-btn ui-icon-btn--ghost b05-routeedit__info-toggle" data-act="info-toggle"
|
|
aria-expanded="true" title="상태 · 범례 글판 접기 · 펴기">▾</button>
|
|
</div>
|
|
</div>
|
|
<div class="b05-routeedit__busy" hidden><span></span></div>
|
|
<aside class="b05-routeedit__side-wrap"></aside>`;
|
|
parent.append(overlay);
|
|
// 스냅 간격 = 고르기 메뉴 하나(634-3 · 요구 546) — 누름 배선은 `_SnapStep`.
|
|
let pickSnapStep: (value: string | null) => void = () => {};
|
|
const snapStep = createPickMenu({
|
|
icon: "spacing",
|
|
title: "스냅 간격",
|
|
items: [
|
|
{ value: "10", label: "10 m" },
|
|
{ value: "station", label: "측점 No." },
|
|
],
|
|
onPick: (value) => pickSnapStep(value),
|
|
});
|
|
snapStep.root.dataset.act = "snap-step";
|
|
snapStep.root.classList.add("b05-routeedit__snap-step");
|
|
const terrain = createRouteTerrain(projectId);
|
|
let gl: RouteEditGl | null = null;
|
|
let redrawScene: (() => void) | null = null;
|
|
const redraw = (): void => (redrawScene ? redrawScene() : gl?.redraw());
|
|
const preview = createRecPreview({
|
|
projectId,
|
|
terrain: terrain.design,
|
|
redraw,
|
|
});
|
|
const mapTools = createMapTools({
|
|
label: "평면 도구",
|
|
hint: { lines: HINT_LINES, open: hintOpen, onToggle: (open) => (hintOpen = open) },
|
|
// 마우스 글 = 공용 표 호버제목(634-2) · 막힘 까닭만 페이지(`mapToolTip`)
|
|
groups: mapToolGroups("b05", {
|
|
undo: mapToolButton("undo", { disabled: true }),
|
|
redo: mapToolButton("redo", { disabled: true }),
|
|
...(tools[0] ? { view3d: tools[0] } : {}), // 부른 쪽 단추([3D])
|
|
"contour-minor": mapToolButton("contour-minor", { disabled: true }),
|
|
"snap-step": snapStep.root,
|
|
recommend: preview.root,
|
|
terrain: terrain.root,
|
|
}),
|
|
});
|
|
mapTools.hint!.classList.add("b05-routeedit__hint"); // 가이드 촬영 · 엿보기 숨김이 잡음
|
|
// 공용 지도 겹(624-3) — 한 행 왼쪽(634-3 · 도구줄은 오른쪽) · 범례는 상태 글판 맨 위
|
|
const wrap = overlay.querySelector<HTMLElement>(".b05-routeedit__canvas-wrap")!;
|
|
const layers = createRouteEditLayers({
|
|
projectId,
|
|
wrap,
|
|
onGl: () => gl?.toggle(),
|
|
redraw,
|
|
});
|
|
const toolRows = createMapToolRow(layers.bar, mapTools.root);
|
|
toolRows.classList.add("b05-routeedit__tools");
|
|
overlay.querySelector('[data-slot="map-tools"]')!.replaceWith(toolRows);
|
|
|
|
// 횡단 미리보기 = 패널 양식(382-1 · 손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 자리는 지도 위 덮개 그대로 ·
|
|
// 기본 접힘(393-1 · 키 -v2 = 옛 열림 기억 한 번 버림) · 열림 · 닫힘은 손잡이만(513-2) · 잰 값 창이 판을 비킴(--b05-side-open).
|
|
const side = document.createElement("div");
|
|
side.className = "b05-routeedit__side";
|
|
const setSideOpen = (): void =>
|
|
overlay.style.setProperty(
|
|
"--b05-side-open",
|
|
`${(panels.current() ? panels.width() : 0) + SIDE_HANDLE_PX}px`,
|
|
);
|
|
const panels = createSidePanels({
|
|
prefKey: "b05-routeedit-cross-v2",
|
|
panels: [{ id: "cross", title: "횡단", body: side }],
|
|
defaultOpen: null,
|
|
defaultWidth: 460,
|
|
follow: false,
|
|
onChange: setSideOpen,
|
|
onResize: setSideOpen,
|
|
});
|
|
overlay.querySelector(".b05-routeedit__side-wrap")!.append(panels.root);
|
|
setSideOpen();
|
|
|
|
// 상태 · 범례 접기 — 접으면 한 줄 「계획노선 N m」(상태 글에서 읽음 · 345-5 덧 3).
|
|
const info = overlay.querySelector<HTMLElement>(".b05-routeedit__info")!;
|
|
info.prepend(layers.legend);
|
|
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));
|
|
|
|
// 뜬 종단 판(이웃 칸)이 지도 아래를 덮으면 상태 · 범례를 그 위로(408-2) — 판 높이 끌기 · 접기 ·
|
|
// 창 분할(판이 다른 창으로 감 = 못 찾음)을 판 크기 관찰로 따라감. 횡단 판이 넓어 아래 줄이 좁으면 잰 값 창을
|
|
// 상태 글판 위로 쌓음(408-4 · 횡단 판 · 글판 크기도 관찰).
|
|
let watchedProfile: Element | null = null;
|
|
const liftInfo = (): void => {
|
|
if (!overlay.isConnected) return; // 붙기 전 첫 알림 · 걷힌 뒤 — 판 관찰은 판과 함께 사라짐
|
|
const profile =
|
|
overlay.closest(".design-page__main")?.querySelector(".b05-route-profile") ?? null;
|
|
if (profile && profile !== watchedProfile) lift.observe((watchedProfile = profile));
|
|
const map = overlay.getBoundingClientRect();
|
|
const top = profile?.isConnected ? profile.getBoundingClientRect().top : null;
|
|
overlay.style.setProperty(
|
|
"--b05-profile-cover",
|
|
`${infoLiftPx(map.bottom, map.height, top)}px`,
|
|
);
|
|
const sideOpen = parseFloat(overlay.style.getPropertyValue("--b05-side-open")) || 0;
|
|
overlay.classList.toggle("is-narrow-bottom", bottomRowNarrow(map.width, sideOpen));
|
|
overlay.style.setProperty(
|
|
"--b05-info-h",
|
|
`${Math.round(info.getBoundingClientRect().height)}px`,
|
|
);
|
|
};
|
|
const lift = new ResizeObserver(liftInfo);
|
|
lift.observe(overlay);
|
|
lift.observe(info);
|
|
lift.observe(overlay.querySelector(".b05-routeedit__side-wrap")!);
|
|
|
|
const measureCloseButton = createIconButton({ glyph: "✕", title: "닫기", size: 24 });
|
|
measureCloseButton.classList.add("b05-routeedit__measure-close");
|
|
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 = mapTools.button("measure")!;
|
|
const busy = overlay.querySelector<HTMLElement>(".b05-routeedit__busy")!;
|
|
const minorContourButton = mapTools.button("contour-minor")!;
|
|
gl = createRouteEditGl(canvas, layers, host); // GL 바탕(589-2 · 그리기가 `routeGlOf` 로 찾음)
|
|
Object.assign(canvas, { mapProbe: { layers: layers.state, preview: preview.state } }); // 검증 창구
|
|
const stationSnapButton = mapTools.button("station-snap")!;
|
|
bindFootprintToggle(mapTools.button("footprint")!);
|
|
bindMarkToggle(mapTools.button("stations")!, "stations");
|
|
bindMarkToggle(mapTools.button("slopes")!, "slopes");
|
|
|
|
return {
|
|
overlay,
|
|
canvas,
|
|
status,
|
|
busy,
|
|
measureBox,
|
|
measureText,
|
|
measureButton,
|
|
minorContourButton,
|
|
stationSnapButton,
|
|
snapStep: Object.assign(snapStep, { onPick: (fn: typeof pickSnapStep) => (pickSnapStep = fn) }),
|
|
expectedDrawButton: mapTools.button("expected-draw")!,
|
|
expectedOriginalButton: mapTools.button("expected-original")!,
|
|
terrain,
|
|
preview,
|
|
layers,
|
|
onRedraw: (draw) => (redrawScene = draw),
|
|
};
|
|
}
|