feat(D 예정노선): 지도 전용 그림 색 · 선마다 차례 색 · 검은 테 (PLAN 600-1 · 요구 463-24)

- 새 값 --draw-map-sketch · measure · warn · route-0~4 (테마 안 따름) · routeColor(index) 차례 색 함수
- planColors 가 지도 전용 색 + casing · 미리보기 · 거리재기 · 마커 · 핀 · 지점 검은 테 (strokeCased)
- 시험 551_2 에 지도 전용 색 대비(어두운 바탕 · 검은 테 ≥ 3 : 1) 덧

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 14:37:54 +09:00
co-authored by Claude Sonnet 5.5
parent 4cb77b2bcd
commit 26c5c0343d
6 changed files with 112 additions and 38 deletions
@@ -33,6 +33,32 @@ export const LINE_HIT_PX = 8;
/** 구간 길이 라벨을 보일 화면 길이(px). */
const LABEL_MIN_PX = 70;
/** 지도 위 그림 검은 테(600-1) — 지금 길을 검은 선 → 색 선 두 번 긋고 점선 무늬를 되돌림 · 길은 부르는 쪽이 먼저 만듦. */
export function strokeCased(
context: CanvasRenderingContext2D,
color: string,
width: number,
dash: number[] = [],
): void {
context.setLineDash(dash);
context.lineWidth = width + 3;
context.strokeStyle = planColors().casing;
context.stroke();
context.lineWidth = width;
context.strokeStyle = color;
context.stroke();
context.setLineDash([]);
}
/** 지도 위 점 — 색 채움 + 검은 테. */
function fillCased(context: CanvasRenderingContext2D, color: string): void {
context.fillStyle = color;
context.fill();
context.lineWidth = 1.5;
context.strokeStyle = planColors().casing;
context.stroke();
}
export const fmt = (value: number): string => value.toFixed(1);
const signed = (value: number): string => `${value >= 0 ? "+" : ""}${value.toFixed(1)}`;
/** 기울기 % → 각(°) — 가상 종단 기울기를 두 단위로. */
@@ -151,6 +177,9 @@ export function pointLayer(points: () => Vertex[]): PlanMapLayer {
context.closePath();
context.fillStyle = pointColor(i);
context.fill();
context.lineWidth = 5;
context.strokeStyle = planColors().casing;
context.stroke();
context.lineWidth = 2;
context.strokeStyle = "#ffffff";
context.stroke();
@@ -218,6 +247,9 @@ export function paintMark(
context.closePath();
context.fillStyle = hollow ? "#ffffff" : color;
context.fill();
context.lineWidth = hollow ? 5.5 : 4.5;
context.strokeStyle = planColors().casing;
context.stroke();
context.lineWidth = hollow ? 2.5 : 1.5;
context.strokeStyle = hollow ? color : "#ffffff";
context.stroke();
@@ -235,31 +267,24 @@ export function paintPreview(
const [ax, ay] = map.toScreen(from);
const [bx, by] = map.toScreen(preview.point);
if (compass) {
context.strokeStyle = colors.active;
context.lineWidth = 1;
context.setLineDash([3, 4]);
context.beginPath();
context.arc(ax, ay, compass.lengthM * map.pxPerMeter, 0, Math.PI * 2);
context.stroke();
context.fillStyle = colors.active;
strokeCased(context, colors.active, 1.5, [3, 4]);
for (const c of compass.candidates) {
const [cx, cy] = map.toScreen(c);
context.beginPath();
context.arc(cx, cy, 3, 0, Math.PI * 2);
context.fill();
context.arc(cx, cy, 3.5, 0, Math.PI * 2);
fillCased(context, colors.active);
}
}
context.strokeStyle = preview.blocked ? colors.warn : colors.active;
context.lineWidth = 2;
context.setLineDash([6, 4]);
const color = preview.blocked ? colors.warn : colors.active;
context.beginPath();
context.moveTo(ax, ay);
context.lineTo(bx, by);
context.stroke();
context.setLineDash([]);
strokeCased(context, color, 2.5, [6, 4]);
context.beginPath();
context.arc(bx, by, 5, 0, Math.PI * 2);
context.stroke();
strokeCased(context, color, 2.5);
const value =
`${fmt(preview.lengthM)} m · ` +
(preview.slopePct === null ? "지형 밖" : slopeText(preview.slopePct));
+16 -7
View File
@@ -17,7 +17,7 @@
* ========================================================================== */
import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools";
import { themeColor } from "@ui/ui_template_palette";
import { routeColor, themeColor } from "@ui/ui_template_palette";
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
computeMapRect,
@@ -26,7 +26,6 @@ import {
createNormalizer,
lonLatToScreen,
normalizedToScreen,
routeLineColor,
type ViewState,
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import { bindRouteEditNavigation } from "../B05_Profile/B05_Profile_UI_RouteEdit_Input";
@@ -41,12 +40,22 @@ export const YEAR_MARK_DRAG = "application/x-aislo-year-mark";
/** 측점 간격(m) — B 규칙 측점과 같음. */
export const PLAN_STATION_INTERVAL_M = 20;
/** 겹 그림 색(그림 전용 값 `--draw-*` · 밝음 · 어두움 같음) — 선 · 그리는 중 · 경고 · 글 테두리. */
export function planColors(): { line: string; active: string; warn: string; halo: string } {
/** 지도 위 그림 색(600-1) — 지도 전용 값 `--draw-map-*`(테마 안 따름 · 바탕 늘 어두움) + 검은 테 `casing`.
* `line` = 첫 선 색 · 선마다 차례 색 = `routeColor(index)`. */
export function planColors(): {
line: string;
active: string;
warn: string;
measure: string;
casing: string;
halo: string;
} {
return {
line: routeLineColor(),
active: themeColor("--draw-primary", "#6d28d9"),
warn: themeColor("--draw-danger", "#dc2626"),
line: routeColor(0),
active: themeColor("--draw-map-sketch", "#ffe94d"),
warn: themeColor("--draw-map-warn", "#ff5a4d"),
measure: themeColor("--draw-map-measure", "#ffffff"),
casing: "rgba(0, 0, 0, 0.85)",
halo: "rgba(0, 0, 0, 0.65)",
};
}
+14 -17
View File
@@ -7,6 +7,7 @@
import { groundSlopePct, segmentLength } from "./logic/D03_PlanAnalysis_Draw";
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
import { planColors, type PlanMapLayer, type ScreenMap } from "./D03_PlanAnalysis_UI_Map";
import { strokeCased } from "./D03_PlanAnalysis_UI_DrawPaint";
export interface PlanMeasure {
layer: PlanMapLayer;
@@ -40,25 +41,23 @@ export function createPlanMeasure(heightAt: () => (x: number, y: number) => numb
if (!from) return;
const colors = planColors();
const [ax, ay] = map.toScreen(from);
context.lineWidth = 2;
context.strokeStyle = colors.active;
context.fillStyle = colors.active;
context.setLineDash([6, 4]);
const dot = (x: number, y: number): void => {
context.beginPath();
context.arc(x, y, 4.5, 0, Math.PI * 2);
context.fillStyle = colors.measure;
context.fill();
context.lineWidth = 1.5;
context.strokeStyle = colors.casing;
context.stroke();
};
if (to) {
const [bx, by] = map.toScreen(to);
context.beginPath();
context.moveTo(ax, ay);
context.lineTo(bx, by);
context.stroke();
context.setLineDash([]);
for (const [x, y] of [
[ax, ay],
[bx, by],
]) {
context.beginPath();
context.arc(x, y, 4, 0, Math.PI * 2);
context.fill();
}
strokeCased(context, colors.measure, 2.5, [6, 4]);
dot(ax, ay);
dot(bx, by);
const { lengthM, slopePct, riseM } = measure(from, to);
const text =
`${fmt(lengthM)} m · ` +
@@ -75,9 +74,7 @@ export function createPlanMeasure(heightAt: () => (x: number, y: number) => numb
context.fillText(text, mx, my);
});
} else {
context.beginPath();
context.arc(ax, ay, 4, 0, Math.PI * 2);
context.fill();
dot(ax, ay);
}
},
press(px, py, map, button) {
+23 -1
View File
@@ -34,7 +34,7 @@ DRAW_READS = [
("B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Map.ts", '"--draw-text-secondary"'),
("B05_Profile/B05_Profile_UI_Viewer.ts", '"--draw-surface-raised"'),
("B04_PreProcess/B04_PreProcess_UI_Camera.ts", '"--draw-surface-raised"'),
("D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts", '"--draw-primary"'),
("D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts", '"--draw-map-sketch"'),
("B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts", '"--draw-warning"'),
("B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts", '"--draw-canvas"'),
("B07_DesignDetail/openwebcad/src/App.css", "--cad-canvas: var(--draw-canvas);"),
@@ -161,3 +161,25 @@ def test_changing_screen_tokens_keeps_drawing_colors():
a = resolve(value, before[mode])
assert "var(" not in a, (mode, rel, value)
assert a == resolve(value, after[mode]), (mode, rel, value)
def _lum(hex_color: str) -> float:
rgb = [int(hex_color[i : i + 2], 16) / 255 for i in (1, 3, 5)]
lin = [c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 for c in rgb]
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
def test_map_drawing_colors_fixed_and_readable_on_dark_map():
"""600-1 지도 위 그림 색 — 테마 안 따름(어두움 블록에 없음) · 어두운 바탕 `#1f2937` · 검은 테 둘 다 ≥ 3 : 1."""
blocks = _blocks(THEME.read_text(encoding="utf-8"))
names = [
k for k in blocks["light"] if re.fullmatch(r"--draw-map-(sketch|measure|warn|route-\d)", k)
]
assert len(names) == 8, names
ground = _lum("#1f2937")
for name in names:
for mode in ("dark", "auto"):
assert name not in blocks[mode], (mode, name)
lum = _lum(blocks["light"][name])
assert (lum + 0.05) / (ground + 0.05) >= 3, name
assert (lum + 0.05) / 0.05 >= 3, name
+13
View File
@@ -50,3 +50,16 @@ export function pointColor(index: number): string {
const [name, fallback] = POINT_COLORS[index % POINT_COLORS.length];
return themeColor(name, fallback);
}
/** 600-1 예정노선 선마다 차례 색 — 다섯 차례 돌림 · 지도 전용 값(테마 안 따름 · 겹 색과 가름) · 지도 선 · 구역 목록 색 띠가 같은 번호 같은 색 */
const ROUTE_COLORS = [
["--draw-map-route-0", "#3df5c4"],
["--draw-map-route-1", "#5aa9ff"],
["--draw-map-route-2", "#c58bff"],
["--draw-map-route-3", "#ff6ea8"],
["--draw-map-route-4", "#9dff4d"],
] as const;
export function routeColor(index: number): string {
const [name, fallback] = ROUTE_COLORS[((index % 5) + 5) % 5];
return themeColor(name, fallback);
}
+8
View File
@@ -585,6 +585,14 @@ html.is-bm-route .m01lab-src__body * {
--draw-map-zero-order: #7dd3fc; /* 0차곡(지형 추정 골짜기 머리 · 점선) */
--draw-map-forest-road-file: #ea580c; /* 올린 임도망도(실선 · 짐작 = 주황 점선) */
--draw-map-landslide-zone: #e11d48; /* 산사태취약지역 점 · 필지 테두리 */
--draw-map-sketch: #ffe94d; /* 지도 위 그림(테마 안 따름 · 검은 테와 함께) — 긋기 미리보기 · 그리는 선 · 고름 강조 · 마커 */
--draw-map-measure: #ffffff; /* 거리재기 */
--draw-map-warn: #ff5a4d; /* 긋기 막힘(경고) */
--draw-map-route-0: #3df5c4; /* 예정노선 선마다 차례 색(개발제한 · 산사태 주황과 가름) */
--draw-map-route-1: #5aa9ff;
--draw-map-route-2: #c58bff;
--draw-map-route-3: #ff6ea8;
--draw-map-route-4: #9dff4d;
}
:root[data-theme="dark"] {