Copy the vanilla engine from dwg-dxf-viewer-sample verbatim (#1): - src/engine/viewer2d/* (Viewer2D.js incl. the 2.5D view, parsers, acadrust-dwg WASM, cadSpaces, slugText, linParser/linData, fixDwgKoreanText, ...) - src/engine/{layerPanel,panelUtils,propertyInspector,optionsStore}.ts The relative layout is kept (engine/viewer2d next to the panel files), so not even import paths changed; `diff -r --strip-trailing-cr` against the sample is empty. public/ (NanumGothic, samples, licenses, module map) is copied as is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
/**
|
|
* 뷰어 옵션 영구 저장소 (IndexedDB).
|
|
*
|
|
* localStorage 대신 IndexedDB 를 쓰는 이유: 값이 문자열로 강제되지 않고(숫자/불린/
|
|
* 객체 그대로), 용량 제한이 사실상 없으며, 나중에 도면별 설정처럼 구조가 커져도
|
|
* 같은 저장소를 그대로 쓸 수 있다.
|
|
*
|
|
* DB 하나(`hmw-viewer`) 안에 key-value 스토어(`options`) 하나만 둔다.
|
|
* 실패(사생활 보호 모드, 스토리지 차단 등)해도 앱이 죽지 않도록 모든 API 는
|
|
* 조용히 기본값으로 떨어진다.
|
|
*/
|
|
|
|
const DB_NAME = 'hmw-viewer';
|
|
const DB_VERSION = 1;
|
|
const STORE = 'options';
|
|
|
|
let dbPromise: Promise<IDBDatabase | null> | null = null;
|
|
|
|
function openDb(): Promise<IDBDatabase | null> {
|
|
if (dbPromise) return dbPromise;
|
|
dbPromise = new Promise((resolve) => {
|
|
if (typeof indexedDB === 'undefined') return resolve(null);
|
|
let req: IDBOpenDBRequest;
|
|
try {
|
|
req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
} catch {
|
|
return resolve(null);
|
|
}
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result;
|
|
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
|
|
};
|
|
req.onsuccess = () => resolve(req.result);
|
|
req.onerror = () => resolve(null);
|
|
req.onblocked = () => resolve(null);
|
|
});
|
|
return dbPromise;
|
|
}
|
|
|
|
function tx<T>(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest): Promise<T | null> {
|
|
return openDb().then(
|
|
(db) =>
|
|
new Promise<T | null>((resolve) => {
|
|
if (!db) return resolve(null);
|
|
try {
|
|
const t = db.transaction(STORE, mode);
|
|
const req = run(t.objectStore(STORE));
|
|
req.onsuccess = () => resolve(req.result as T);
|
|
req.onerror = () => resolve(null);
|
|
} catch {
|
|
resolve(null);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** 저장된 옵션 하나. 없으면 `fallback`. */
|
|
export async function getOption<T>(key: string, fallback: T): Promise<T> {
|
|
const v = await tx<T>('readonly', (s) => s.get(key));
|
|
return v === null || v === undefined ? fallback : v;
|
|
}
|
|
|
|
/** 옵션 하나 저장. 저장이 막혀 있어도 예외를 던지지 않는다. */
|
|
export async function setOption(key: string, value: unknown): Promise<void> {
|
|
await tx('readwrite', (s) => s.put(value, key));
|
|
}
|
|
|
|
/** 전체 옵션을 한 번에 읽는다 (기동 시 1회 호출용). */
|
|
export async function getAllOptions(): Promise<Record<string, unknown>> {
|
|
const keys = (await tx<IDBValidKey[]>('readonly', (s) => s.getAllKeys())) ?? [];
|
|
const vals = (await tx<unknown[]>('readonly', (s) => s.getAll())) ?? [];
|
|
const out: Record<string, unknown> = {};
|
|
keys.forEach((k, i) => { out[String(k)] = vals[i]; });
|
|
return out;
|
|
}
|