Files
Aislo/resources/tester/test_551_2_draw_tokens.py
T
eomsangdonandClaude Sonnet 5.5 26c5c0343d 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>
2026-10-30 14:37:54 +09:00

186 lines
7.9 KiB
Python

"""551-2 그림 전용 값(--draw-*) 지킴 — 화면 테마 변수(--color-*)를 바꿔도 그림 색은 그대로.
- `ui_template_theme.css` 끝 블록의 --draw-* 는 밝음 · 어두움 · 시스템 어두움 모두 글자 hex · 어두움 · 시스템 어두움은 밝음에 있는 것만 덮음.
- 그림 CSS(B05 · B06 · B08 SVG)의 stroke · fill 은 --color-* 를 쓰지 않음.
- 지도 · 3D · CAD 캔버스가 읽는 바탕 · 선 색은 --draw-*.
- 옛 변수 값을 일부러 바꿔도 그림 선언의 풀린 색이 같음(밝음 · 어두움 · 시스템 어두움).
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
THEME = ROOT / "ui_template" / "ui_template_theme.css"
DRAW_CSS = [
"B05_Profile/B05_Profile_UI_Style.css",
"B05_Profile/B05_Profile_UI_Style_RouteEdit.css",
"B05_Profile/B05_Profile_UI_Style_RouteRecommend.css",
"B05_Profile/B05_Profile_UI_Style_Table.css",
"B06_Section/B06_Section_UI_Style.css",
"B06_Section/B06_Section_UI_Style_Cross_Areas.css",
"B06_Section/B06_Section_UI_Style_Cross_Chart.css",
"B06_Section/B06_Section_UI_Style_Cross_Detail.css",
"B08_Quantity/B08_Quantity_UI_Hydro.css",
"B08_Quantity/B08_Quantity_UI_Milestone.css",
]
# 캔버스 · 3D 가 읽는 그림 색 — (파일, 꼭 있어야 할 글)
DRAW_READS = [
("B05_Profile/B05_Profile_UI_RouteEdit_Render.ts", '"--draw-surface"'),
("B05_Profile/B05_Profile_UI_RouteEdit_Render.ts", '"--draw-text-secondary"'),
("B05_Profile/B05_Profile_UI_RouteEdit_Render.ts", '"--draw-danger"'),
("B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Map.ts", '"--draw-primary"'),
("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-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);"),
(
"B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts",
"themeColor('--draw-grid'",
),
]
NO_COLOR_READS = [
"B05_Profile/B05_Profile_UI_RouteEdit_Render.ts",
"B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Map.ts",
"B05_Profile/B05_Profile_UI_Viewer.ts",
"B07_DesignDetail/openwebcad/src/drawControllers/screenCanvas.drawController.ts",
]
DRAW_NAMES_READ = {m for _, n in DRAW_READS for m in re.findall(r"--draw-[a-z-]+", n)}
DECL = re.compile(r"^\s*(stroke|fill|stop-color|--b06-tag)\s*:(.*);", re.M)
VAR = re.compile(r"var\((--[a-z0-9-]+)(?:\s*,\s*([^()]*(?:\([^()]*\))?[^()]*))?\)")
HEX = re.compile(r"#[0-9a-f]{6}")
def _blocks(css: str) -> dict[str, dict[str, str]]:
"""밝음 · 어두움 · 시스템 어두움 블록의 --변수 정의(뒤 블록이 앞을 덮음)."""
css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
out: dict[str, dict[str, str]] = {"light": {}, "dark": {}, "auto": {}}
def walk(text: str, media: bool) -> None:
i = 0
while True:
open_at = text.find("{", i)
if open_at < 0:
return
head = text[i:open_at].strip().split("}")[-1].strip()
depth, j = 1, open_at + 1
while depth:
depth += {"{": 1, "}": -1}.get(text[j], 0)
j += 1
body = text[open_at + 1 : j - 1]
if head.startswith("@media (prefers-color-scheme: dark)"):
walk(body, True)
else:
mode = None
if head == ":root":
mode = "light"
elif head == ':root[data-theme="dark"]':
mode = "dark"
elif media and head == ':root:not([data-theme="light"])':
mode = "auto"
if mode:
for name, value in re.findall(r"(--[a-z0-9-]+)\s*:([^;]*);", body):
out[mode][name] = " ".join(value.split())
i = j
walk(css, False)
return out
def _modes(css: str) -> dict[str, dict[str, str]]:
b = _blocks(css)
return {
"light": b["light"],
"dark": {**b["light"], **b["dark"]},
"auto": {**b["light"], **b["auto"]},
}
def resolve(value: str, env: dict[str, str]) -> str:
for _ in range(10):
new = VAR.sub(lambda m: env.get(m.group(1), (m.group(2) or "").strip()), value)
if new == value:
break
value = new
return " ".join(value.split()).lower()
def draw_decls() -> list[tuple[str, str]]:
out = []
for rel in DRAW_CSS:
text = re.sub(r"/\*.*?\*/", "", (ROOT / rel).read_text(encoding="utf-8"), flags=re.S)
out += [(rel, m.group(2).strip()) for m in DECL.finditer(text)]
return out
def test_draw_values_are_literal_hex():
blocks = _blocks(THEME.read_text(encoding="utf-8"))
for mode, env in blocks.items():
draws = {k: v for k, v in env.items() if k.startswith("--draw-")}
assert draws, mode
for name, value in draws.items():
assert HEX.fullmatch(value), (mode, name, value)
# 밝음 블록이 전부 정의 · 어두움 · 시스템 어두움은 그 안의 것만 덮음(고정 개수 아님)
light = {k for k in blocks["light"] if k.startswith("--draw-")}
for mode in ("dark", "auto"):
assert {k for k in blocks[mode] if k.startswith("--draw-")} <= light, mode
# 캔버스 · 3D 가 읽는 바탕 · 선 색은 세 모드 모두에서 풀림
for mode, env in _modes(THEME.read_text(encoding="utf-8")).items():
for name in DRAW_NAMES_READ:
assert name in env, (mode, name)
def test_drawing_css_uses_no_screen_tokens():
decls = draw_decls()
assert len(decls) > 100
bad = [(rel, v) for rel, v in decls if "var(--color-" in v]
assert not bad, bad
def test_canvas_and_3d_read_draw_tokens():
for rel, needle in DRAW_READS:
assert needle in (ROOT / rel).read_text(encoding="utf-8"), (rel, needle)
for rel in NO_COLOR_READS:
text = (ROOT / rel).read_text(encoding="utf-8")
assert not re.search(r"""(getPropertyValue|themeColor)\(\s*["']--color-""", text), rel
def test_changing_screen_tokens_keeps_drawing_colors():
css = THEME.read_text(encoding="utf-8")
# 옛 화면 변수를 모두 초록으로 바꾼 판 — sub1 이 테마를 갈아도 그림이 안 따라가는지
swapped = re.sub(r"(--color-[a-z0-9-]+\s*:)[^;]*;", r"\1 #00ff00;", css)
before, after = _modes(css), _modes(swapped)
decls = draw_decls()
for mode in ("light", "dark", "auto"):
for rel, value in decls:
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