- test_551_2: 선 그리기 색 읽는 자리 = A00_Common/map/map_route_preview.ts (636-2 가 RecPreview 에서 옮김) - test_624_2: D03 도구줄에 추천(recommend) 묶음 더해짐(636-2) - vite.config.ts: server.watch.ignored = tmp · storage · docs · venv · logs · resources/tester · *.log Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
231 lines
10 KiB
Python
231 lines
10 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_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"'),
|
||
("A00_Common/map/map_route_preview.ts", '"--draw-map-sketch"'), # 636-2 선 그리기 공용
|
||
("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_RecPreview.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}")
|
||
#: 색이 아닌 그림 값 — 불투명(테마 안 따름 · 밝음 블록에만)
|
||
DRAW_ALPHAS = {"--draw-map-dim"}
|
||
|
||
|
||
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():
|
||
if name in DRAW_ALPHAS: # 불투명 값(색 아님 · 635-3)
|
||
assert 0 < float(value) < 1, (mode, name, value)
|
||
continue
|
||
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|slope-(up|down))", k)
|
||
]
|
||
assert len(names) == 10, names
|
||
for name in DRAW_ALPHAS: # 635-3 어둡게 = 밝음 블록 하나(어두움 · 시스템 어두움 덮지 않음)
|
||
assert name in blocks["light"] and all(name not in blocks[m] for m in ("dark", "auto"))
|
||
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
|
||
|
||
|
||
def test_slope_down_node_distinct_from_route_mint():
|
||
"""625-5 − 노드 색 = 진한 파랑 · 민트 노선 선(route-0)과 · 어두운 바탕과 · 검은 테와 각각 ≥ 3 : 1."""
|
||
light = _blocks(THEME.read_text(encoding="utf-8"))["light"]
|
||
blue = light["--draw-map-slope-down"]
|
||
assert blue == "#3b6bff"
|
||
for other in (light["--draw-map-route-0"], "#1f2937"):
|
||
hi, lo = sorted((_lum(blue), _lum(other)), reverse=True)
|
||
assert (hi + 0.05) / (lo + 0.05) >= 3, other
|
||
assert (_lum(blue) + 0.05) / 0.05 >= 3
|
||
|
||
|
||
def test_paint_network_uses_cased_route_lines():
|
||
src = (ROOT / "D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts").read_text(encoding="utf-8")
|
||
body = src[src.index("export function paintNetwork") : src.index("export function pointLayer")]
|
||
assert "strokeCased(context, color, width)" in body
|
||
assert "routeColor(index)" in body
|
||
|
||
|
||
def test_pick_highlight_readable_on_any_map_background():
|
||
"""613-2 고름 강조 = 노랑 줄(`--draw-map-sketch`) + 검은 테 — 바탕(위성 · 어두운 지도)이 어떤 밝기여도
|
||
줄 · 검은 테 중 큰 쪽이 ≥ 3 : 1 · 테 한쪽 4 px 이상 · 노랑 줄은 검은 테와 함께 그림."""
|
||
blocks = _blocks(THEME.read_text(encoding="utf-8"))
|
||
yellow = _lum(blocks["light"]["--draw-map-sketch"])
|
||
for step in range(201):
|
||
ground = step / 200
|
||
line = (
|
||
(yellow + 0.05) / (ground + 0.05)
|
||
if yellow > ground
|
||
else (ground + 0.05) / (yellow + 0.05)
|
||
)
|
||
casing = (ground + 0.05) / 0.05
|
||
assert max(line, casing) >= 3, ground
|
||
src = (ROOT / "D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit.ts").read_text(encoding="utf-8")
|
||
assert int(re.search(r"PICK_HALO_PX = (\d+)", src).group(1)) >= 4
|
||
assert "strokeCased(context, colors.active, PICK_LINE_PX)" in src
|
||
assert "context.strokeStyle = colors.casing;" in src
|