From 26c5c0343dd53fa5da8e62740dafc182aab5d7f4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 30 Oct 2026 14:37:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(D=20=EC=98=88=EC=A0=95=EB=85=B8=EC=84=A0):?= =?UTF-8?q?=20=EC=A7=80=EB=8F=84=20=EC=A0=84=EC=9A=A9=20=EA=B7=B8=EB=A6=BC?= =?UTF-8?q?=20=EC=83=89=20=C2=B7=20=EC=84=A0=EB=A7=88=EB=8B=A4=20=EC=B0=A8?= =?UTF-8?q?=EB=A1=80=20=EC=83=89=20=C2=B7=20=EA=B2=80=EC=9D=80=20=ED=85=8C?= =?UTF-8?q?=20(PLAN=20600-1=20=C2=B7=20=EC=9A=94=EA=B5=AC=20463-24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 값 --draw-map-sketch · measure · warn · route-0~4 (테마 안 따름) · routeColor(index) 차례 색 함수 - planColors 가 지도 전용 색 + casing · 미리보기 · 거리재기 · 마커 · 핀 · 지점 검은 테 (strokeCased) - 시험 551_2 에 지도 전용 색 대비(어두운 바탕 · 검은 테 ≥ 3 : 1) 덧 Co-Authored-By: Claude Sonnet 5.5 --- .../D03_PlanAnalysis_UI_DrawPaint.ts | 51 ++++++++++++++----- D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts | 23 ++++++--- .../D03_PlanAnalysis_UI_Measure.ts | 31 +++++------ resources/tester/test_551_2_draw_tokens.py | 24 ++++++++- ui_template/ui_template_palette.ts | 13 +++++ ui_template/ui_template_theme.css | 8 +++ 6 files changed, 112 insertions(+), 38 deletions(-) diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts index a5ef6888a..0e81c63f5 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts @@ -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)); diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts index de02713cf..fc69c07cd 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts @@ -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)", }; } diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure.ts index fb4dbe80a..0102c9d31 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure.ts @@ -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) { diff --git a/resources/tester/test_551_2_draw_tokens.py b/resources/tester/test_551_2_draw_tokens.py index 321e7467d..04accf07d 100644 --- a/resources/tester/test_551_2_draw_tokens.py +++ b/resources/tester/test_551_2_draw_tokens.py @@ -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 diff --git a/ui_template/ui_template_palette.ts b/ui_template/ui_template_palette.ts index 93420d4dd..034218b25 100644 --- a/ui_template/ui_template_palette.ts +++ b/ui_template/ui_template_palette.ts @@ -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); +} diff --git a/ui_template/ui_template_theme.css b/ui_template/ui_template_theme.css index ebc6038fa..f5a61f805 100644 --- a/ui_template/ui_template_theme.css +++ b/ui_template/ui_template_theme.css @@ -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"] {