Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Footprint.ts
|
|
* 노선 편집 평면에 **평면 발자국**(도로 끝 · 비탈 끝)을 그린다(PLAN 346-2).
|
|
*
|
|
* 값 = 가게 키 `footprint`(계산 묶음 footprint 단계가 씀) · 선 갈래 = `logic/_Footprint_Strokes`.
|
|
* 가게 값이 바뀌거나 [발자국] 단추를 누르면 마지막 장면을 한 번 다시 그린다(프레임마다 한 번) —
|
|
* 편집기 본체가 그릴 때마다 `drawFootprint` 가 다시 그릴 길을 새로 받아 둔다.
|
|
* 켬끔은 이 페이지 동안만(기본 켬 · 계정 설정 아님).
|
|
* ========================================================================== */
|
|
|
|
import { draftProjectId, useDesignStore } from "../A00_Common/design/design_store";
|
|
import type { Vertex } from "./B05_Profile_UI_RouteEdit_Curve";
|
|
import {
|
|
type FootprintStrokeKind,
|
|
type FootprintValue,
|
|
footprintStrokes,
|
|
} from "./logic/B05_Profile_Footprint_Strokes";
|
|
import "./B05_Profile_UI_Style_RouteEdit_Footprint.css";
|
|
|
|
/** 갈래 → 색 토큰 · 대체 색. */
|
|
const STROKE_STYLE: Record<FootprintStrokeKind, [string, string]> = {
|
|
road: ["--b05-footprint-road", "#475569"],
|
|
cut: ["--b05-footprint-cut", "#dc2626"],
|
|
fill: ["--b05-footprint-fill", "#2563eb"],
|
|
slope: ["--b05-footprint-road", "#475569"],
|
|
};
|
|
const STROKE_WIDTH = 1.4;
|
|
|
|
let shown = true;
|
|
let redraw: (() => void) | null = null;
|
|
let frame = 0;
|
|
let listening: { projectId: string; stop: () => void } | null = null;
|
|
|
|
function scheduleRedraw(): void {
|
|
if (frame) return;
|
|
frame = requestAnimationFrame(() => {
|
|
frame = 0;
|
|
redraw?.();
|
|
});
|
|
}
|
|
|
|
function listen(projectId: string): void {
|
|
if (listening?.projectId === projectId) return;
|
|
listening?.stop();
|
|
const stop = useDesignStore(projectId, null).subscribe("footprint", scheduleRedraw);
|
|
listening = { projectId, stop };
|
|
}
|
|
|
|
/** [발자국] 단추 — 누르면 켬끔 · 다시 그림. */
|
|
export function bindFootprintToggle(button: HTMLButtonElement): void {
|
|
const paint = (): void => {
|
|
button.classList.toggle("is-active", shown);
|
|
button.setAttribute("aria-pressed", String(shown));
|
|
};
|
|
paint();
|
|
button.addEventListener("click", () => {
|
|
shown = !shown;
|
|
paint();
|
|
scheduleRedraw();
|
|
});
|
|
}
|
|
|
|
/** 발자국을 그린다(회전 · 등고선 위 · 노선 아래 자리에서 부름). `again` = 장면을 다시 그리는 길. */
|
|
export function drawFootprint(
|
|
context: CanvasRenderingContext2D,
|
|
toScreen: (vertex: Vertex) => [number, number],
|
|
style: CSSStyleDeclaration,
|
|
again: () => void,
|
|
): void {
|
|
redraw = again;
|
|
const projectId = draftProjectId();
|
|
listen(projectId);
|
|
if (!shown) return;
|
|
const strokes = footprintStrokes(
|
|
useDesignStore(projectId, null).get("footprint") as FootprintValue | null,
|
|
);
|
|
if (!strokes.length) return;
|
|
context.save();
|
|
context.setLineDash([]);
|
|
context.lineWidth = STROKE_WIDTH;
|
|
for (const { kind, points } of strokes) {
|
|
const [token, fallback] = STROKE_STYLE[kind];
|
|
context.strokeStyle = style.getPropertyValue(token).trim() || fallback;
|
|
context.beginPath();
|
|
points.forEach((vertex, index) => {
|
|
const [x, y] = toScreen(vertex);
|
|
if (index === 0) context.moveTo(x, y);
|
|
else context.lineTo(x, y);
|
|
});
|
|
context.stroke();
|
|
}
|
|
context.restore();
|
|
}
|