/** * 호스트 앱(Aislo) 테마를 CAD iframe에 그대로 따라 붙인다. * * iframe은 호스트와 **같은 오리진**이라 `localStorage["frd_theme"]`를 공유한다. * 부모가 토글하며 값을 쓰면 이 문서에는 `storage` 이벤트가 오므로, 부모 쪽에 * 별도 전달 코드(postMessage)를 넣지 않는다. 값이 없으면 부모 문서의 `data-theme` * (호스트 index.html 기본값 light)를 따른다 — 시스템 설정을 따로 따르면 호스트와 어긋난다. * 부모가 없을 때(단독 실행)만 `prefers-color-scheme` 폴백이 먹는다. */ import { bumpSceneVersion } from './helpers/scene-version.ts'; const THEME_STORAGE_KEY = 'frd_theme'; /** 테마가 바뀔 때마다 비우는 색 캐시 (CSS 변수 조회·휘도 보정 결과). */ let cssVarCache = new Map(); let paintCache = new Map(); function applyTheme(value: string | null): void { if (value === 'dark' || value === 'light') { document.documentElement.setAttribute('data-theme', value); } else { document.documentElement.removeAttribute('data-theme'); } cssVarCache = new Map(); paintCache = new Map(); // 정적 장면 비트맵은 배경·선 색까지 구워 두므로 테마가 바뀌면 다시 굽는다. bumpSceneVersion(); } /** 부모(호스트) 문서의 테마 — 부모가 없거나 못 읽으면 null. */ function parentTheme(): string | null { try { return window.parent === window ? null : window.parent.document.documentElement.getAttribute('data-theme'); } catch { return null; } } /** 부팅 시 1회 호출 — 현재 테마 반영 + 이후 변경 구독. */ export function syncThemeFromHost(): void { applyTheme(localStorage.getItem(THEME_STORAGE_KEY) ?? parentTheme()); window.addEventListener('storage', (event) => { if (event.key === THEME_STORAGE_KEY) applyTheme(event.newValue ?? parentTheme()); }); // 시스템 설정 폴백으로 도는 중이면 OS 전환도 따라간다. window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (!document.documentElement.hasAttribute('data-theme')) applyTheme(null); }); } /** 토큰 값 조회 (테마 바뀔 때까지 캐시). */ export function themeColor(varName: string, fallback: string): string { const cached = cssVarCache.get(varName); if (cached !== undefined) return cached; const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || fallback; cssVarCache.set(varName, value); return value; } export function isLightTheme(): boolean { const attr = document.documentElement.getAttribute('data-theme'); if (attr === 'dark') return false; if (attr === 'light') return true; return !window.matchMedia('(prefers-color-scheme: dark)').matches; } function relativeLuminance(r: number, g: number, b: number): number { return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; } /** * 자동색 — 오토캐드 색 번호 7 처럼 바탕을 따라 흰/검정으로 그리는 색. * 검정 · 흰색(DXF 색 7) · 기본색(`#f5f7fa` = 새 개체 · DXF ByLayer 가져온 색). */ const AUTO_COLORS = new Set(['#000000', '#ffffff', '#f5f7fa', 'black', 'white']); function normalizeHex(color: string): string { const value = color.trim().toLowerCase(); const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(value); return short ? `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}` : value; } export function isAutoColor(color: string): boolean { return AUTO_COLORS.has(normalizeHex(color)); } /** * 도면 색 보정 — **그릴 때만** 바꾼다(저장 데이터는 그대로). * - 자동색: 어두운 바탕 흰색 · 밝은 바탕 검정. * - 밝은 바탕: 흰 종이에 묻히는 밝은 색은 휘도를 낮춰 색조를 살린 채 대비를 맞춘다. * - 어두운 바탕: 자동색 밖은 그대로(어두운 색을 밝히지 않음). */ export function paintColor(color: string): string { const light = isLightTheme(); if (isAutoColor(color)) return light ? '#000000' : '#ffffff'; if (!light) return color; const cached = paintCache.get(color); if (cached !== undefined) return cached; const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color); let result = color; if (hex) { const full = normalizeHex(color).slice(1); const r = Number.parseInt(full.slice(0, 2), 16); const g = Number.parseInt(full.slice(2, 4), 16); const b = Number.parseInt(full.slice(4, 6), 16); const luminance = relativeLuminance(r, g, b); // 흰 배경에서 묻히는 밝기(0.58 초과)만 목표 휘도 0.3으로 눌러 준다. if (luminance > 0.58) { const scale = 0.3 / luminance; const to = (v: number) => Math.round(Math.min(255, v * scale)) .toString(16) .padStart(2, '0'); result = `#${to(r)}${to(g)}${to(b)}`; } } paintCache.set(color, result); return result; }