diff --git a/A00_Common/map/map_route_preview.ts b/A00_Common/map/map_route_preview.ts
index a2676335c..50d3796f9 100644
--- a/A00_Common/map/map_route_preview.ts
+++ b/A00_Common/map/map_route_preview.ts
@@ -1,7 +1,8 @@
/* =============================================================================
* map_route_preview.ts
* 공용 「추천노선」 미리보기(PLAN 626-1 → 636-2 공용 · 요구 530 · 546) — B05 평면 · 예정노선(D03)이 같이 씀.
- * 메뉴 = 도구줄 아이콘 하나(compass · 호버 「추천노선」) + 아래 판(항목 하나만 · 맨 끝 「끔」 · 부품 `createPickMenu`).
+ * 메뉴 = 도구줄 아이콘 하나(compass · 호버 「추천노선」) + 아래 판(여럿 고르기 · 줄마다 색 점 · 「끔」 줄 없음 ·
+ * 다 끄면 꺼짐 · 부품 `createPickMenu({ multi })` · 589-12).
* 그리기 = 지도 위 선(노랑 `--draw-map-sketch` · 검은 테 · 기본 점선) — 위성 · 어두운 바탕 모두 보임.
* 항목 · 선 셈은 페이지 몫: B05 = 노선 추천 저장본 넷(`B05_Profile_UI_RouteEdit_RecPreview`) ·
* 예정노선 = 가이드 선(`D03_PlanAnalysis_UI_Guide`).
@@ -12,17 +13,17 @@ import { themeColor } from "@ui/ui_template_palette";
type Vertex = [number, number] | [number, number, number];
-/** 「추천노선」 메뉴 — 고름 = `onPick(값)` · 「끔」 = `onPick(null)`. */
+/** 「추천노선」 메뉴 — 줄 누름 = `onPick(값)`(켬끔 바뀜 · 지금 켠 줄은 `menu.values()`). */
export function createRoutePreviewMenu(opts: {
items: PickMenuItem[];
- onPick: (value: string | null) => void;
+ onPick: (value: string) => void;
}): PickMenu {
const menu = createPickMenu({
+ multi: true,
icon: "compass",
title: "추천노선", // 마우스 글 = 공용 표 호버제목(634-2 · `mapToolGroups` 가 맞춤)
items: opts.items,
- offLabel: "끔",
- onPick: opts.onPick,
+ onPick: (value) => value && opts.onPick(value),
});
menu.root.dataset.act = "recommend";
return menu;
diff --git a/A00_Common/map/map_toolbar.ts b/A00_Common/map/map_toolbar.ts
index 031d60e95..60286a277 100644
--- a/A00_Common/map/map_toolbar.ts
+++ b/A00_Common/map/map_toolbar.ts
@@ -12,7 +12,7 @@
import { mapToolButton, mapToolTip, type MapToolAct } from "@ui/ui_template_map_tools";
import type { MapPage } from "./map_layer_list";
-export type MapToolKey = MapToolAct | "hint-toggle" | "snap-step" | "recommend" | "terrain";
+export type MapToolKey = MapToolAct | "hint-toggle" | "recommend" | "terrain";
export interface MapToolRow {
key: MapToolKey;
@@ -52,16 +52,15 @@ export const MAP_TOOLBAR: readonly (readonly MapToolRow[])[] = [
{
key: "station-snap",
title: "스냅",
- does: "곡선 가운데(MC)를 간격 배수에 맞춤",
+ does: "곡선 가운데(MC)를 간격 배수에 맞춤 · 고르기 메뉴 하나(측점 No. · 10 m · 끔 · 589-4)",
pages: B05,
},
- { key: "snap-step", title: "스냅 간격", does: "스냅 간격 고르기(고르기 메뉴)", pages: B05 },
],
[
{
key: "recommend",
title: "추천노선",
- does: "B05 = 노선 추천 저장본 선 하나만 · 예정노선 = 가이드 선(지점 잇기 · 636-2) · 끔",
+ does: "B05 = 노선 추천 저장본 선 여럿 고르기(색 구분) · 예정노선 = 가이드 선(지점 잇기 · 636-2)",
pages: BOTH,
},
],
@@ -75,7 +74,7 @@ export const mapToolTitle = (key: MapToolKey): string =>
ROWS.find((row) => row.key === key)?.title ?? key;
const isAct = (key: MapToolKey): key is MapToolAct =>
- key !== "hint-toggle" && key !== "snap-step" && key !== "recommend" && key !== "terrain";
+ key !== "hint-toggle" && key !== "station-snap" && key !== "recommend" && key !== "terrain";
/** 그 페이지 도구줄 묶음 — 넘긴 요소 · 기본 단추(단추 열쇠만) · 마우스 글 = 표 · 빈 묶음 뺌. */
export function mapToolGroups(
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts
index 829023d60..20c5f1a06 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts
@@ -31,6 +31,7 @@ import {
type RouteEditContours,
type SheetLayers,
} from "./B05_Profile_UI_RouteEdit_Contour";
+import { contourBandLine, editingExpected } from "./B05_Profile_UI_RouteEdit_Input";
import { createContourLock } from "./B05_Profile_UI_RouteEdit_ContourLock";
import {
fetchRoutePlan,
@@ -75,7 +76,7 @@ import {
} from "./B05_Profile_UI_RouteEdit_History";
import type { DesignPageHost } from "../A00_Common/design/design_page";
import * as draftLink from "./B05_Profile_UI_RouteEdit_Draft";
-import { bindStationSnap, paintStationSnap } from "./B05_Profile_UI_RouteEdit_SnapStep";
+import { bindStationSnap } from "./B05_Profile_UI_RouteEdit_SnapStep";
import * as fit from "./logic/B05_Profile_RouteEdit_StationFit";
import { calcChecked } from "../A00_Common/design/design_calc_server";
import "./B05_Profile_UI_Style_RouteEdit.css";
@@ -116,7 +117,7 @@ export async function mountRouteEditor(
const stationIntervalM = options.stationIntervalM ?? 20;
const chrome = createRouteEditChrome(parent, projectId, options.tools, host);
const { overlay, canvas, status, measureBox, measureText, measureButton } = chrome;
- const { minorContourButton, stationSnapButton } = chrome;
+ const { minorContourButton } = chrome;
chrome.onRedraw(() => draw()); // 겹 켬끔 · 추천 보기 = 장면 새로(624-3)
const context = canvas.getContext("2d")!;
const expectedDraw = createExpectedDraw({
@@ -129,7 +130,7 @@ export async function mountRouteEditor(
pxPerMeter: () => pxPerMeter(),
view: () => view,
frame: () => ({ meta, setView: (next: ViewState) => (view = next) }),
- redraw: () => draw(),
+ redraw: () => (draw(), sheetFollow?.poke()), // 켠 동안 예정노선이 도엽 밖으로 가면 다시 받음(589-10)
onOpen: () => ((picked = -1), syncCurveBar()),
onFollow: (flat, line) =>
restoreSnapshot(
@@ -168,14 +169,13 @@ export async function mountRouteEditor(
/** [측점 스냅](224-2 · 366-3) — 켬 = 곡선 MC 를 고른 간격 배수에 · 켬끔 자체는 되돌리기 밖 · [저장] 때 저장. */
let stationSnap = false;
/** 간격 · 저장 비교 · 편집 창 측점(끔이면 null · 그리기 · 누름 · 기울기가 봄)은 `_SnapStep` 몫(390-2 · 5). */
- const snap = bindStationSnap(chrome, projectId, stationIntervalM, (toggle) => {
- if (toggle) setStationSnap(!stationSnap);
+ const snap = bindStationSnap(chrome, stationIntervalM, () => {
+ stationSnap = snap.on();
if (stationSnap && history) applyEdit(snap.text()); // 전체 · 한 걸음
tellDraft(); // 끔 · 간격 바꿈도 계산 줄에 바로(430)
draw();
});
const stations = () => snap.stations(plannedLine.length ? plannedLine : planned, curveInfo);
- const setStationSnap = (on: boolean) => paintStationSnap(stationSnapButton, (stationSnap = on));
/** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */
let history: RouteEditHistory | null = null;
let meta: VWorldMeta | null = null;
@@ -599,7 +599,13 @@ export async function mountRouteEditor(
try {
const [plan, drainage, sources] = await Promise.all([
fetchRoutePlan(projectId),
- fetchRouteSheets(projectId, () => (planned.length ? planned : plannedLine)), // 띠 = 편집 노드
+ fetchRouteSheets(projectId, () =>
+ contourBandLine(
+ planned.length ? planned : plannedLine,
+ expectedDraw.line(),
+ editingExpected({ overAll: expectedDraw }),
+ ),
+ ), // 띠 = 편집 노드(+ 켠 동안 고치는 예정노선 · 589-10)
fetchTerrainSources(projectId).catch(() => null),
]);
if (closed) return;
@@ -612,7 +618,7 @@ export async function mountRouteEditor(
minRadiusM = plan.min_radius_m ?? 0;
limitRadiusM = plan.limit_radius_m ?? 0;
limitArcM = plan.limit_curve_length_m ?? 0;
- setStationSnap(snap.load(plan));
+ stationSnap = snap.load(plan);
// 곡선 성분을 편집할 수 있는 꼴로 편다 — 셈은 `_Edits` 몫(까닭도 그쪽에 적었다).
const flat = edits.flattenServerPlan(nodes, plan.curves ?? [], plan.node_edits);
({ planned, curveOn, curveRadius, curveLock, curveArc } = flat);
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts
index ec60d45b6..7ca580e44 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts
@@ -65,10 +65,8 @@ export interface RouteEditChrome {
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 };
+ /** 스냅 고르기 메뉴 하나(요구 224-2 · 589-4) — 측점 No. · 10 m · 끔(끔 = 스냅 꺼짐) · 배선 = `onPick`(`_SnapStep`). */
+ snap: PickMenu & { onPick: (fn: (value: string | null) => void) => void };
/** 설계 지형 칩(626-1) — 지형 읽은 뒤 `load`. */
terrain: RouteTerrain;
/** 추천 노선 미리보기(626-1) — 그리기 안에서 `draw`. */
@@ -130,19 +128,19 @@ export function createRouteEditChrome(
`;
parent.append(overlay);
- // 스냅 간격 = 고르기 메뉴 하나(634-3 · 요구 546) — 누름 배선은 `_SnapStep`.
- let pickSnapStep: (value: string | null) => void = () => {};
- const snapStep = createPickMenu({
- icon: "spacing",
- title: "스냅 간격",
+ // 스냅 = 고르기 메뉴 하나(634-3 · 589-4 · 켬끔 + 간격) — 누름 배선은 `_SnapStep`.
+ let pickSnap: (value: string | null) => void = () => {};
+ const snap = createPickMenu({
+ icon: "magnet",
+ title: "스냅",
items: [
{ value: "10", label: "10 m" },
{ value: "station", label: "측점 No." },
],
- onPick: (value) => pickSnapStep(value),
+ offLabel: "끔",
+ onPick: (value) => pickSnap(value),
});
- snapStep.root.dataset.act = "snap-step";
- snapStep.root.classList.add("b05-routeedit__snap-step");
+ snap.root.dataset.act = "station-snap";
const terrain = createRouteTerrain(projectId);
let gl: RouteEditGl | null = null;
let redrawScene: (() => void) | null = null;
@@ -161,7 +159,7 @@ export function createRouteEditChrome(
redo: mapToolButton("redo", { disabled: true }),
...(tools[0] ? { view3d: tools[0] } : {}), // 부른 쪽 단추([3D])
"contour-minor": mapToolButton("contour-minor", { disabled: true }),
- "snap-step": snapStep.root,
+ "station-snap": snap.root,
recommend: preview.root,
terrain: terrain.root,
}),
@@ -261,7 +259,6 @@ export function createRouteEditChrome(
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");
@@ -275,8 +272,7 @@ export function createRouteEditChrome(
measureText,
measureButton,
minorContourButton,
- stationSnapButton,
- snapStep: Object.assign(snapStep, { onPick: (fn: typeof pickSnapStep) => (pickSnapStep = fn) }),
+ snap: Object.assign(snap, { onPick: (fn: typeof pickSnap) => (pickSnap = fn) }),
terrain,
preview,
layers,
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Gl.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Gl.ts
index 19c94f3d4..b27be11f5 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Gl.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Gl.ts
@@ -25,7 +25,7 @@ import {
} from "../B04_PreProcess/logic/B04_PreProcess_UI_MapGl_Frame";
import type { RouteEditContours } from "./B05_Profile_UI_RouteEdit_Contour";
import type { RouteEditLayers } from "./B05_Profile_UI_RouteEdit_Layers";
-import { contourBandRect } from "./B05_Profile_UI_RouteEdit_Input";
+import { sceneContourBand } from "./B05_Profile_UI_RouteEdit_Input";
import type { RouteEditScene } from "./B05_Profile_UI_RouteEdit_Render";
import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve";
@@ -179,7 +179,7 @@ export function createRouteEditGl(
layers.follow(meta, normalizer, toScreen, view.width, view.height);
// 노선 둘레 띠만(캔버스 자르기와 같은 네모 · 돌림 · 칸 안 px)
const line = (scene.plannedLine.length ? scene.plannedLine : scene.planned) as Vertex[];
- const band = scene.hasMeta ? contourBandRect(line, scene.toScreen, bandM) : null;
+ const band = sceneContourBand(scene, line, scene.toScreen, bandM);
const corners: [number, number][] = band
? [
[band.x, band.y],
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts
index be18299bc..74c67a03e 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts
@@ -504,6 +504,38 @@ function stationTickAtScreen(
return best;
}
+type XY = ReadonlyArray;
+
+/** 예정노선 수정 켬인가 — 아래 노선 그림이 흐려진 동안(`overAll.dim` < 1 · 589-10). */
+export const editingExpected = (scene: { overAll?: { dim: () => number } }): boolean =>
+ (scene.overAll?.dim() ?? 1) < 1;
+
+/** 등고선 띠를 잴 선 — 켠 동안은 계획선 + 고치는 예정노선(계획선만 보면 앞 토막 지움에 띠가 줄어듦 · 589-10). */
+export const contourBandLine = (plan: XY, expected: XY, editing: boolean): XY =>
+ editing ? [...plan, ...expected] : plan;
+
+/** 화면에서 등고선을 자를 사각형 — 켠 동안 LAS 는 자르지 않음(전체 · null) · 도엽은 띠를 예정노선까지 넓힘. */
+export function sceneContourBand(
+ scene: {
+ hasMeta: boolean;
+ contours: { source: string } | null;
+ overAll?: { dim: () => number };
+ expected: XY;
+ },
+ line: XY,
+ toScreen: ScreenOf,
+ bandM: number,
+): { x: number; y: number; width: number; height: number } | null {
+ if (!scene.hasMeta) return null;
+ const editing = editingExpected(scene);
+ if (editing && scene.contours?.source === "las") return null;
+ return contourBandRect(
+ contourBandLine(line, scene.expected, editing) as Array<[number, number]>,
+ toScreen,
+ bandM,
+ );
+}
+
/** 등고선을 보일 화면 사각형 — 노선 경계에 `bandM` 를 두른 것. 노선이 없으면 null.
*
* **매 프레임 다시 잰다** — 창 크기·배율·이동이 바뀌어도 띠가 노선을 따라간다. 띠 자체는
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts b/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts
index a21b72860..def720ed0 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts
@@ -1,7 +1,8 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_RecPreview.ts
* 평면 노선 추천 미리보기(PLAN 626-1 · 요구 530 · 결정 D1 · D7 · D8) — 평면 도구줄 아이콘 하나 + 아래 판
- * (영선 · 토공 최소 · 기울기 최소 · 비중 계산 · 끔) · 하나만 보임 · 선만(위 캔버스 · 노선 아래 그림 · 집기 없음).
+ * (영선 · 토공 최소 · 기울기 최소 · 비중 계산) · 여럿 고름(589-12 · 줄마다 색 · 하나도 안 고르면 꺼짐) ·
+ * 선만(위 캔버스 · 노선 아래 그림 · 집기 없음).
* 선 = 노선 추천 저장본(`B05_Profile/route_recommend/` · 지금 설계 지형 · 체인이 미리 셈) 그대로 — 다시 셈 · 쓰기 없음 ·
* 저장본 없으면(409) 알림 · 끔. 옛 추천 모달 · 좌측 「노선 추천」 칸 · [적용](`as_initial`) 은 걷음(서버 길 · 시험은 남김).
* 부르는 곳 = `_Chrome`(도구줄 `recommend` 칸) · `_RouteEdit`(그리기 `overStations` 앞).
@@ -9,6 +10,7 @@
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
+import { themeColor } from "@ui/ui_template_palette";
import { createRoutePreviewMenu, paintRoutePreview } from "../A00_Common/map/map_route_preview";
import type { AnalysisTerrain } from "./B05_Profile_Api_Replan";
import { fetchRouteInitial, type RouteInitialLogic } from "./B05_Profile_Api_RouteInitial";
@@ -24,12 +26,23 @@ const MODES = [
"weighted",
] as const satisfies readonly RouteInitialLogic[];
+/** 갈래별 선 색(`--draw-map-rec-1~4`) · 점선 꼴(색만으로 못 가르는 눈도 가름). */
+const REC_VARS = ["--draw-map-rec-1", "--draw-map-rec-2", "--draw-map-rec-3", "--draw-map-rec-4"];
+const REC_FALLBACK = ["#ffe94d", "#d58bff", "#ff7ab8", "#a3e635"];
+const REC_COLORS = REC_VARS.map((name, index) => `var(${name}, ${REC_FALLBACK[index]})`);
+const REC_DASHES = [
+ [10, 6],
+ [3, 6],
+ [14, 4, 3, 4],
+ [6, 3],
+];
+
export interface RecPreview {
root: HTMLElement;
- /** 노선 그리기 안(돌림 안) — 고른 선 하나. */
+ /** 노선 그리기 안(돌림 안) — 고른 선들(갈래마다 색). */
draw: (context: CanvasRenderingContext2D, toScreen: (vertex: Vertex) => [number, number]) => void;
- /** 검증 창구 — 고른 갈래 · 선 점 수. */
- state: () => { mode: string | null; points: number };
+ /** 검증 창구 — 고른 갈래들 · 보이는 선 점 수(갈래별). */
+ state: () => { modes: string[]; points: Record };
}
export function createRecPreview(opts: {
@@ -39,18 +52,23 @@ export function createRecPreview(opts: {
redraw: () => void;
}): RecPreview {
const lines = new Map();
- let shown: Vertex[] | null = null;
- let turn = 0;
+ const shown = new Map();
+ const turns = new Map();
const menu = createRoutePreviewMenu({
- items: MODES.map((mode) => ({ value: mode, label: LOGIC_LABEL[mode] })),
- onPick: (mode) => void pick(mode as RouteInitialLogic | null),
+ items: MODES.map((mode, index) => ({
+ value: mode,
+ label: LOGIC_LABEL[mode],
+ color: REC_COLORS[index],
+ })),
+ onPick: (mode) => void pick(mode as RouteInitialLogic),
});
- async function pick(mode: RouteInitialLogic | null): Promise {
- const mine = ++turn;
- shown = null;
+ async function pick(mode: RouteInitialLogic): Promise {
+ const mine = (turns.get(mode) ?? 0) + 1;
+ turns.set(mode, mine);
+ shown.delete(mode);
opts.redraw();
- if (!mode) return;
+ if (!menu.values().includes(mode)) return;
const key = `${mode}|${opts.terrain()}`;
try {
if (!lines.has(key)) {
@@ -63,12 +81,12 @@ export function createRecPreview(opts: {
if (planned.length < 2) throw new Error("추천 선이 비었음");
lines.set(key, planned);
}
- if (mine !== turn) return;
- shown = lines.get(key)!;
+ if (mine !== turns.get(mode)) return;
+ shown.set(mode, lines.get(key)!);
opts.redraw();
} catch (error) {
- if (mine !== turn) return;
- menu.set(null);
+ if (mine !== turns.get(mode)) return;
+ menu.setItem(mode, { checked: false });
const text = error instanceof Error ? error.message : String(error);
showToast(`추천 노선 「${LOGIC_LABEL[mode]}」 못 봄 — ${text}`, "info");
}
@@ -77,8 +95,18 @@ export function createRecPreview(opts: {
return {
root: menu.root,
draw(context, toScreen) {
- if (shown) paintRoutePreview(context, shown, toScreen);
+ MODES.forEach((mode, index) => {
+ const line = shown.get(mode);
+ if (line)
+ paintRoutePreview(context, line, toScreen, {
+ color: themeColor(REC_VARS[index], REC_FALLBACK[index]),
+ dash: REC_DASHES[index],
+ });
+ });
},
- state: () => ({ mode: menu.value(), points: shown?.length ?? 0 }),
+ state: () => ({
+ modes: menu.values(),
+ points: Object.fromEntries([...shown].map(([mode, line]) => [mode, line.length])),
+ }),
};
}
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts
index 0264527ed..e7037b266 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts
@@ -25,7 +25,7 @@ import {
type StationLabelBox,
} from "../B04_PreProcess/B04_PreProcess_UI_MapOverlays";
import type { EditedCurve, EditedNode, Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve";
-import { contourBandRect, setStationLabelBoxes } from "./B05_Profile_UI_RouteEdit_Input";
+import { sceneContourBand, setStationLabelBoxes } from "./B05_Profile_UI_RouteEdit_Input";
import { drawPickHover, drawStationNormal } from "./B05_Profile_UI_RouteEdit_Hover";
import { drawPickedCurve, traceNodeTriangle } from "./B05_Profile_UI_RouteEdit_Curve_Pick";
import type { CrossWatch } from "./B05_Profile_UI_RouteEdit_Cross";
@@ -198,7 +198,7 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
context.save();
// 등고선은 **노선 둘레 300m 안**에서만 그린다 — 노선과 상관없는 산줄기까지 다 그리면
// 화면이 등고선으로 덮여 노선이 안 보인다(2026-09-07 사용자 지시 ⑥).
- const band = scene.hasMeta ? contourBandRect(line as Vertex[], toScreen, CONTOUR_BAND_M) : null;
+ const band = sceneContourBand(scene, line, toScreen, CONTOUR_BAND_M);
if (band) {
context.beginPath();
context.rect(band.x, band.y, band.width, band.height);
@@ -264,7 +264,11 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
style.getPropertyValue("--draw-text-secondary") || "#9ca3af",
1.6,
);
- scene.underRoute?.(context); // 추천 노선 보기(626-1)
+ // 추천 노선 보기(626-1) — 예정노선 수정 켬(dim)에도 진한 색 그대로(589-2)
+ context.save();
+ context.globalAlpha = 1;
+ scene.underRoute?.(context);
+ context.restore();
// 곡선 시작·끝점 — 잡아서 직선 각도와 R 을 함께 바꾸는 손잡이(2026-09-07 사용자 지시).
// **속을 비우고 테두리를 굵게** 그린다 — 선·노드와 색이 같으면 눈에도 안 띄고 집기도 어렵다.
// 예상노선 뒤 · 노선 앞(414-2) — 노선이 네모 위를 지나감 · 고른 곡선은 `drawPickedCurve` 가 위에.
diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts b/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts
index a21ab3fa9..b27ae27c9 100644
--- a/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts
+++ b/B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts
@@ -1,17 +1,15 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_SnapStep.ts
- * 노선 편집 [측점 스냅] 단추 · 스냅 간격 고르기(390-2 · 요구 242-11).
+ * 노선 편집 스냅 고르기 하나(390-2 · 요구 242-11 · 589-4) — 측점 No. · 10 m · 끔(끔 = 스냅 꺼짐).
*
* 간격 = 10 m · 측점 No.(편집 창 측점 간격 · 기본 · 280-4) — 곡선 MC 를 그 간격 배수에 맞춤.
- * 고른 값은 프로젝트별 화면 기억(pref `routeedit-snap-step`) · 스냅 켬이면 저장 노선에도 같이
- * (`station_snap_step` · 390-5 · 서버 측점 셈이 MC 측점 값 자릿수로 씀 · 옛 노선 = 측점 No.).
+ * 켬이면 저장 노선에도 같이(`station_snap_step` · 390-5 · 서버 측점 셈이 MC 측점 값 자릿수로 씀 · 옛 노선 = 측점 No.).
* 저장 노선이 스냅 켬이면 열 때 그 간격으로 고르기를 맞춤. 추천 · 서버 초기 노선은 측점 No. 그대로.
- * 고르기 = 도구줄 고르기 메뉴 하나(634-3 · `chrome.snapStep`) · 스냅 끔이면 흐림(CSS) · 고를 수는 있음.
+ * 고르기 = 도구줄 고르기 메뉴 하나(634-3 · `chrome.snap`) · 처음 = 끔.
* 상태줄 글은 고른 간격 따라(390-4) — 측점 No. = 「일반측점에」 · 10 m = 「10 m 배수에」.
* 평면 눈금 = 일반측점(스냅이 옮기지 않음 · 280-4) + 곡선 측점(MC 에 측점 없을 때 · 252-2 · `ticks`).
* ========================================================================== */
-import { readState, writeState } from "../A00_Common/b_page_state";
import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types";
import type { RouteEditChrome } from "./B05_Profile_UI_RouteEdit_Chrome";
import type { EditedCurve, Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve";
@@ -25,12 +23,6 @@ export type SnapSetting = Pick;
type SavedSnap = { station_snap?: boolean; station_snap_step?: number | null };
-/** [측점 스냅] 단추 켬끔 모양(`is-active` · `aria-pressed`). */
-export function paintStationSnap(button: HTMLButtonElement, on: boolean): void {
- button.classList.toggle("is-active", on);
- button.setAttribute("aria-pressed", String(on));
-}
-
/** 스냅 뒤 상태줄 글 — `step` = 고르기 값("10" · "station"). */
export function snapFitText(step: string): string {
const where = step === "station" ? "일반측점" : `${step} m 배수`;
@@ -40,6 +32,8 @@ export function snapFitText(step: string): string {
export type SnapStep = {
/** 지금 스냅 간격(m) — 측점 No. 면 측점 간격. */
(): number;
+ /** 스냅 켬인가(고르기가 끔이 아님). */
+ on: () => boolean;
/** 스냅 뒤 상태줄 글. */
text: () => string;
/** 저장 · 초안 두 칸 — 끔이면 간격 null. */
@@ -57,24 +51,17 @@ export type SnapStep = {
) => { stations: number[] | null; curveStations: number[] };
};
-/** 단추 · 고르기 배선 — `onChange(true)` = 단추 누름(켬끔 바꿈) · `onChange(false)` = 간격 바꿈. */
+/** 고르기 배선 — 고름(켬 · 간격 · 끔) 때 `onChange()`. */
export function bindStationSnap(
- chrome: Pick,
- projectId: string,
+ chrome: Pick,
stationIntervalM: number,
- onChange: (toggle: boolean) => void,
+ onChange: () => void,
): SnapStep {
- // 고르기 메뉴 하나(634-3 · 옛 날