diff --git a/resources/tester/test_552_4_frames.py b/resources/tester/test_552_4_frames.py index 96e40e4ad..44502598e 100644 --- a/resources/tester/test_552_4_frames.py +++ b/resources/tester/test_552_4_frames.py @@ -25,6 +25,7 @@ PARTS = { "float-panel": ("ui_template_float_panel", "createFloatPanel", "ui-float-panel", (32,)), "drawer": ("ui_template_side_panels", "createSidePanels", "ui-side-panels", ()), "toolbar": ("ui_template_toolbar", "createToolbar", "ui-toolbar", (24, 28, 32)), + "map-tools": ("ui_template_map_tools", "createMapTools", "ui-map-tools", ()), "steps": ("ui_template_workflow_layout", "createStepBar", "ui-workflow-layout__step", (28, 40)), "panel-head": ("ui_template_panel_head", "createPanelHead", "ui-panel-head", (32,)), "list-row": ("ui_template_list_row", "createListRow", "ui-list-row", (28, 32, 36)), diff --git a/resources/tester/test_568_1_map_tools.py b/resources/tester/test_568_1_map_tools.py new file mode 100644 index 000000000..2017121ee --- /dev/null +++ b/resources/tester/test_568_1_map_tools.py @@ -0,0 +1,81 @@ +"""568-1 지도 도구줄 부품 지킴 시험 — 화면 확인은 부품 모음(`#/b01-account?view=ui-kit`) · ORCA. + +· 단추 목록의 아이콘이 모두 아이콘 묶음에 있음 · 새 아이콘 여덟(도구 넷 · 겹 넷) +· 켬끔 단추 = aria-pressed · [?] = data-act="hint-toggle" + aria-expanded · 안내 판 기본 접힘 +./venv/Scripts/python.exe -m pytest resources/tester/test_568_1_map_tools.py -q +""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +UI = ROOT / "ui_template" +TS = (UI / "ui_template_map_tools.ts").read_text(encoding="utf-8") +ICONS = (UI / "ui_template_icons.ts").read_text(encoding="utf-8") +CSS = (UI / "ui_template_map_tools.css").read_text(encoding="utf-8") + + +def _icon_names() -> set[str]: + body = ICONS[ICONS.index("const PATHS") : ICONS.index("} as const")] + return set(re.findall(r"^ (\w+):", body, re.M)) + + +def test_새_아이콘_여덟(): + names = _icon_names() + for n in ( + "ripple", + "tallymarks", + "percentage", + "magnet", + "satellite", + "droplet", + "borderAll", + "angle", + ): + assert n in names, n + + +def test_단추_목록_아이콘_차례(): + table = TS[TS.index("export const MAP_TOOLS") : TS.index("satisfies")] + rows = re.findall( + r'^ "?([\w-]+)"?: \{ icon: "(\w+)", title: "[^"]+"(, toggle: true)? \}', table, re.M + ) + acts = [a for a, _, _ in rows] + assert acts == [ + "undo", + "redo", + "view3d", + "rotate-ccw", + "rotate-cw", + "contour-minor", + "stations", + "slopes", + "footprint", + "measure", + "station-snap", + ] + names = _icon_names() + assert all(icon in names for _, icon, _ in rows) + toggles = {a for a, _, t in rows if t} + assert toggles == { + "contour-minor", + "stations", + "slopes", + "footprint", + "measure", + "station-snap", + } + + +def test_켬끔_안내_판(): + assert "pressed: tool.toggle ? (opts.pressed ?? false) : undefined" in TS + assert "button.dataset.act = act;" in TS + assert 'toggle.dataset.act = "hint-toggle";' in TS + assert 'toggle.setAttribute("aria-expanded", String(open));' in TS + assert "setOpen(opts.hint.open ?? false);" in TS # 기본 접힘 + hint = CSS[CSS.index(".ui-map-tools__hint {") :] + assert "display: none;" in hint[: hint.index("}")] + assert "display: grid;" in CSS[CSS.index(".ui-map-tools__hint.is-open {") :] + assert "pointer-events: none;" in CSS # 빈 자리 · 글판은 지도 조작 통과 diff --git a/ui_template/ui_template_icons.ts b/ui_template/ui_template_icons.ts index 6385499b0..7ac90773f 100644 --- a/ui_template/ui_template_icons.ts +++ b/ui_template/ui_template_icons.ts @@ -123,6 +123,21 @@ const PATHS = { mail: "M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10|M3 7l9 6l9 -6", inbox: "M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12|M4 13h3l3 3h4l3 -3h3", + // 568-1 — 지도 도구(1m 등고선 · 측점 · 기울기 · 측점 스냅) + ripple: "M3 7c3 -2 6 -2 9 0s6 2 9 0|M3 17c3 -2 6 -2 9 0s6 2 9 0|M3 12c3 -2 6 -2 9 0s6 2 9 0", + tallymarks: "M6 5l0 14|M10 5l0 14|M14 5l0 14|M18 5l0 14|M3 17l18 -10", + percentage: "M16 17a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M6 7a1 1 0 1 0 2 0a1 1 0 0 0 -2 0|M6 18l12 -12", + magnet: + "M4 13v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a2 2 0 0 0 6 0v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a8 8 0 0 1 -16 0|M4 8l5 0|M15 8l4 0", + // 568-1 — 지도 겹(위성 · 세류선 · 지적도 · 급경사) + satellite: + "M3.707 6.293l2.586 -2.586a1 1 0 0 1 1.414 0l5.586 5.586a1 1 0 0 1 0 1.414l-2.586 2.586a1 1 0 0 1 -1.414 0l-5.586 -5.586a1 1 0 0 1 0 -1.414z|M6 10l-3 3l3 3l3 -3|M10 6l3 -3l3 3l-3 3|M12 12l1.5 1.5|M14.5 17a2.5 2.5 0 0 0 2.5 -2.5|M15 21a6 6 0 0 0 6 -6", + droplet: + "M7.502 19.423c2.602 2.105 6.395 2.105 8.996 0c2.602 -2.105 3.262 -5.708 1.566 -8.546l-4.89 -7.26c-.42 -.625 -1.287 -.803 -1.936 -.397a1.376 1.376 0 0 0 -.41 .397l-4.893 7.26c-1.695 2.838 -1.035 6.441 1.567 8.546z", + borderAll: + "M4 6a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z|M4 12l16 0|M12 4l0 16", + angle: + "M21 19h-18l9 -15|M20.615 15.171h.015|M19.515 11.771h.015|M17.715 8.671h.015|M15.415 5.971h.015", } as const; export type IconName = keyof typeof PATHS; diff --git a/ui_template/ui_template_map_tools.css b/ui_template/ui_template_map_tools.css new file mode 100644 index 000000000..29457cbfe --- /dev/null +++ b/ui_template/ui_template_map_tools.css @@ -0,0 +1,33 @@ +/* 지도 화면 제어 도구줄(568-1) — 도구줄 + 아래 [?] 안내 판 · 자리는 페이지가 줌 · 빈 자리는 지도 조작 통과 */ +.ui-map-tools { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-8); + pointer-events: none; +} + +.ui-map-tools > .ui-toolbar { + max-width: 100%; + pointer-events: auto; +} + +/* 안내 판 — 기본 접힘 · 2열 · 글판은 클릭 통과 */ +.ui-map-tools__hint { + display: none; + grid-template-columns: auto auto; + gap: 2px var(--spacing-16); + max-width: 100%; + padding: var(--spacing-8) var(--spacing-12); + border: 1px solid color-mix(in srgb, var(--color-border) 65%, transparent); + border-radius: var(--radius-cards); + background: color-mix(in srgb, var(--color-surface-raised) 72%, transparent); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + color: var(--color-text-secondary); + font-size: var(--text-caption); +} + +.ui-map-tools__hint.is-open { + display: grid; +} diff --git a/ui_template/ui_template_map_tools.ts b/ui_template/ui_template_map_tools.ts new file mode 100644 index 000000000..1c2496a6f --- /dev/null +++ b/ui_template/ui_template_map_tools.ts @@ -0,0 +1,119 @@ +/* ============================================================================= + * ui_template_map_tools.ts + * 지도 화면 제어 도구줄(568-1) — 평면 지도 위 아이콘 단추 한 벌 · B05 평면 · D03 평면이 같이 씀. + * 꼴 = 유리 도구줄(`createToolbar` glass · 32) 한 줄 · 묶음 사이 나눔선 · 단추는 모두 아이콘(마우스 올리면 이름) + * [?] 안내 판 = 도구줄 아래 2열 글판(기본 접힘 · 클릭은 지도로 통과) + * 크기 = 단 32 하나(지도 위 도구) + * 상태 = 켬끔 단추는 `aria-pressed`(눌림 꼴) · 막힘 `disabled` · [?] 는 `aria-expanded` + * 쓰는 법 = createMapTools({ label: "평면 도구", groups: [["undo", "redo"], ["rotate-ccw", "rotate-cw", "measure"]], + * hint: { lines: ["휠 = 확대"] } }) → { root, bar, hint, button(act) } + * 페이지 문구가 다르면 `mapToolButton("measure", { title })` 를 묶음에 그대로 끼움 · 고르기 · 칩도 요소로 끼움 + * 누름 동작은 페이지가 붙임(`onClick` 또는 `[data-act]` 로 찾아 붙임) · 자리는 페이지 CSS(root) + * 대신하는 옛 변종 = B05 `b05-route__view-controls` 글자 유리 단추 줄 · `b05-routeedit__hint` 안내 판 · D03 `tool()` 지역 함수 + * ========================================================================== */ + +import { createIconButton } from "./ui_template_button"; +import type { IconName } from "./ui_template_icons"; +import { createToolbar } from "./ui_template_toolbar"; +import "./ui_template_map_tools.css"; + +/** 단추 목록 — 차례 = 도구줄 차례 · toggle = 켬끔 단추(aria-pressed) */ +export const MAP_TOOLS = { + undo: { icon: "undo", title: "되돌리기 (Ctrl+Z)" }, + redo: { icon: "redo", title: "다시하기 (Ctrl+Y)" }, + view3d: { icon: "cube", title: "3D 보기" }, + "rotate-ccw": { icon: "rotateCcw", title: "반시계로 돌리기" }, + "rotate-cw": { icon: "rotateCw", title: "시계로 돌리기" }, + "contour-minor": { icon: "ripple", title: "1m 등고선", toggle: true }, + stations: { icon: "tallymarks", title: "측점", toggle: true }, + slopes: { icon: "percentage", title: "기울기", toggle: true }, + footprint: { icon: "road", title: "절/성토선", toggle: true }, + measure: { icon: "ruler", title: "거리 재기", toggle: true }, + "station-snap": { icon: "magnet", title: "측점 스냅", toggle: true }, +} satisfies Record; + +export type MapToolAct = keyof typeof MAP_TOOLS; + +export interface MapToolButtonOptions { + /** 마우스 글 — 없으면 목록 이름 */ + title?: string; + /** 켬끔 단추 처음 값(기본 false) */ + pressed?: boolean; + disabled?: boolean; + onClick?: (ev: MouseEvent) => void; +} + +/** 아이콘 단추 하나(`data-act` = act) */ +export function mapToolButton(act: MapToolAct, opts: MapToolButtonOptions = {}): HTMLButtonElement { + const tool: { icon: IconName; title: string; toggle?: boolean } = MAP_TOOLS[act]; + const button = createIconButton({ + icon: tool.icon, + title: opts.title ?? tool.title, + size: 32, + pressed: tool.toggle ? (opts.pressed ?? false) : undefined, + disabled: opts.disabled, + onClick: opts.onClick, + }); + button.dataset.act = act; + return button; +} + +export interface MapToolsOptions { + /** 묶음들 — 사이에 나눔선 · act 이름 또는 페이지가 만든 요소 */ + groups: (MapToolAct | HTMLElement)[][]; + /** 보조기기 이름 */ + label: string; + /** [?] 안내 판(맨 앞 묶음) — 줄마다 한 칸 · 2열 */ + hint?: { lines: string[]; open?: boolean; onToggle?: (open: boolean) => void }; +} + +export interface MapTools { + root: HTMLElement; + bar: HTMLElement; + hint: HTMLElement | null; + button(act: MapToolAct): HTMLButtonElement | null; +} + +export function createMapTools(opts: MapToolsOptions): MapTools { + const root = document.createElement("div"); + root.className = "ui-map-tools"; + const groups = opts.groups.map((group) => + group.map((item) => (typeof item === "string" ? mapToolButton(item) : item)), + ); + let hint: HTMLElement | null = null; + if (opts.hint) { + const { lines, onToggle } = opts.hint; + const toggle = createIconButton({ icon: "help", title: "조작 도움말 펼치기 · 접기", size: 32 }); + toggle.dataset.act = "hint-toggle"; + const panel = document.createElement("div"); + panel.className = "ui-map-tools__hint"; + for (const line of lines) + panel.append(Object.assign(document.createElement("span"), { textContent: line })); + const setOpen = (open: boolean): void => { + panel.classList.toggle("is-open", open); + toggle.setAttribute("aria-expanded", String(open)); + }; + setOpen(opts.hint.open ?? false); + toggle.addEventListener("click", () => { + const open = !panel.classList.contains("is-open"); + setOpen(open); + onToggle?.(open); + }); + groups.unshift([toggle]); + hint = panel; + } + const bar = createToolbar({ + glass: true, + size: 32, + label: opts.label, + items: groups.flatMap((group, i) => (i ? ["divider" as const, ...group] : group)), + }); + root.append(bar); + if (hint) root.append(hint); + return { + root, + bar, + hint, + button: (act) => root.querySelector(`[data-act="${act}"]`), + }; +} diff --git a/ui_template/ui_template_ui_kit_frames.ts b/ui_template/ui_template_ui_kit_frames.ts index 5b5c4326f..b1e9558a3 100644 --- a/ui_template/ui_template_ui_kit_frames.ts +++ b/ui_template/ui_template_ui_kit_frames.ts @@ -10,6 +10,7 @@ import { createFloatPanel } from "./ui_template_float_panel"; import { createFormGrid, createPageFrame } from "./ui_template_general_layout"; import { createInputField } from "./ui_template_input"; import { createListRow } from "./ui_template_list_row"; +import { createMapTools, mapToolButton } from "./ui_template_map_tools"; import { createWorkflowPanelHandle } from "./ui_template_overlay"; import { createPanelHead } from "./ui_template_panel_head"; import { createPublicFrame } from "./ui_template_public_layout"; @@ -218,6 +219,27 @@ function toolbar(): HTMLElement { return box; } +function mapTools(): HTMLElement { + const tools = createMapTools({ + label: "평면 도구", + hint: { lines: ["휠 = 확대", "가운데 끌기 = 이동", "Ctrl+휠 = 돌리기", "Esc = 그리기 끝"] }, + groups: [ + ["undo", "redo"], + ["view3d", "rotate-ccw", "rotate-cw"], + [ + "contour-minor", + "stations", + mapToolButton("slopes", { pressed: true }), + "footprint", + "measure", + ], + ["station-snap"], + ], + }); + tools.button("contour-minor")!.disabled = true; + return tools.root; +} + /** 열쇠 → 견본(상태별 · null = 그 상태 없음) */ export const FRAME_DEMOS: Record HTMLElement | null> = { "work-frame": (s) => (s === "default" ? workFrame() : null), @@ -226,6 +248,7 @@ export const FRAME_DEMOS: Record HTMLElement | null "float-panel": (s) => (s === "default" ? floatPanel() : null), drawer: (s) => (s === "default" ? drawer() : null), toolbar: (s) => (s === "default" ? toolbar() : null), + "map-tools": (s) => (s === "default" ? mapTools() : null), steps, "panel-head": (s) => (s === "default" ? panelHead() : null), "list-row": listRow, diff --git a/ui_template/ui_template_ui_kit_parts.ts b/ui_template/ui_template_ui_kit_parts.ts index 9d46124da..84d426c78 100644 --- a/ui_template/ui_template_ui_kit_parts.ts +++ b/ui_template/ui_template_ui_kit_parts.ts @@ -363,6 +363,8 @@ export const KIT_PARTS: KitPart[] = [ demo: FRAME_DEMOS["drawer"] }, { key: "toolbar", name: "도구줄", group: "frame", kinds: "단추 묶음 · 나눔선 · 밀기 · 유리 · 24 · 28 · 32", file: "ui_template_toolbar.ts", demo: FRAME_DEMOS["toolbar"] }, + { key: "map-tools", name: "지도 도구줄", group: "frame", kinds: "아이콘 단추 한 벌 · 묶음 나눔선 · 켬끔(aria-pressed) · [?] 안내 판 · 페이지가 쓸 단추만", file: "ui_template_map_tools.ts", + demo: FRAME_DEMOS["map-tools"] }, { key: "steps", name: "단계 줄", group: "frame", kinds: "가로 막대 · compact(28) · 세로 목록(40 · ☰ 서랍)", file: "ui_template_workflow_layout.ts", demo: FRAME_DEMOS.steps }, { key: "panel-head", name: "패널 머리", group: "frame", kinds: "제목 · 부제 · 오른쪽 단추 · 밑줄", file: "ui_template_panel_head.ts",