refactor: extract viewer2d workspace package

This commit is contained in:
2026-07-29 09:03:41 +09:00
parent 95fa6a452b
commit 5e10bb1be4
27 changed files with 1588 additions and 704 deletions
+1463 -673
View File
File diff suppressed because it is too large Load Diff
+13 -7
View File
@@ -4,20 +4,26 @@
"version": "0.1.0", "version": "0.1.0",
"description": "Standalone sample: compose acadrust-dwg + dxf-parser + Viewer2D (sibling of hmwebviewer)", "description": "Standalone sample: compose acadrust-dwg + dxf-parser + Viewer2D (sibling of hmwebviewer)",
"type": "module", "type": "module",
"workspaces": [
"packages/*"
],
"scripts": { "scripts": {
"dev": "vite --port 5173 --strictPort", "dev": "vite --port 5173 --strictPort",
"build": "tsc --noEmit && vite build", "build": "tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"test": "vitest run",
"test:unit": "vitest run tests/viewer2d.contract.test.ts"
}, },
"dependencies": { "dependencies": {
"dxf-parser": "^1.1.2", "@hmwebviewer/viewer2d": "workspace:*",
"opentype.js": "^2.0.0", "three": "0.185.1"
"three": "^0.185.1"
}, },
"devDependencies": { "devDependencies": {
"@types/three": "^0.185.1", "@playwright/test": "1.62.0",
"typescript": "^5.6.0", "@types/three": "0.185.1",
"vite": "^5.4.0" "typescript": "7.0.2",
"vite": "8.1.5",
"vitest": "4.1.10"
} }
} }
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@hmwebviewer/viewer2d",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"types": "./src/index.ts",
"dependencies": {
"dxf-parser": "1.1.2",
"opentype.js": "2.0.0"
},
"peerDependencies": {
"three": "0.185.1"
}
}
+42
View File
@@ -0,0 +1,42 @@
import { Viewer2D } from './Viewer2D.js';
import { parseDwgBuffer, type CadParseResult } from './dwgParser';
import { is2D, isDwg } from './ext2d';
import { parseDxfBuffer } from './parseDxf';
import { setDefaultFontUrl } from './slugText';
export type { CadParseResult } from './dwgParser';
export type CadInput = {
name: string;
data: ArrayBuffer | Uint8Array;
};
export type Viewer2DOptions = {
fontUrl?: string;
};
function toArrayBuffer(data: ArrayBuffer | Uint8Array): ArrayBuffer {
if (data instanceof ArrayBuffer) return data;
return data.slice().buffer as ArrayBuffer;
}
export function isCad2DFile(name: string): boolean {
return is2D(name);
}
export async function parseCad(input: CadInput): Promise<CadParseResult> {
const buffer = toArrayBuffer(input.data);
if (isDwg(input.name)) return parseDwgBuffer(new Uint8Array(buffer));
if (is2D(input.name)) return parseDxfBuffer(buffer);
const head = new TextDecoder('latin1').decode(
new Uint8Array(buffer, 0, Math.min(6, buffer.byteLength)),
);
if (head.startsWith('AC')) return parseDwgBuffer(new Uint8Array(buffer));
return parseDxfBuffer(buffer);
}
export function createViewer2D(container: HTMLElement, options: Viewer2DOptions = {}): Viewer2D {
if (options.fontUrl) setDefaultFontUrl(options.fontUrl);
return new Viewer2D(container);
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src/**/*"]
}
+17 -21
View File
@@ -20,11 +20,12 @@
*/ */
// ── ① 모듈 지도의 각 카드 (src/viewer2d/*) ──────────────────────── // ── ① 모듈 지도의 각 카드 (src/viewer2d/*) ────────────────────────
import { Viewer2D } from './viewer2d/Viewer2D.js'; import {
import { parseDwgBuffer, type CadParseResult } from './viewer2d/dwgParser'; createViewer2D,
import { parseDxfBuffer } from './viewer2d/parseDxf'; isCad2DFile,
import { is2D, isDxf, isDwg } from './viewer2d/ext2d'; parseCad,
import { setDefaultFontUrl } from './viewer2d/slugText'; type CadParseResult,
} from '@hmwebviewer/viewer2d';
// parseDxfBuffer 내부: // parseDxfBuffer 내부:
// dxf-parser → HatchHandler → dxfAdapter // dxf-parser → HatchHandler → dxfAdapter
@@ -32,7 +33,8 @@ import { setDefaultFontUrl } from './viewer2d/slugText';
// acadrustParser → acadrust-dwg WASM // acadrustParser → acadrust-dwg WASM
// ── ② 호스트 정적 자산 ────────────────────────────────────────── // ── ② 호스트 정적 자산 ──────────────────────────────────────────
setDefaultFontUrl('/fonts/NanumGothic-Regular.ttf'); const appBase = new URL(import.meta.env.BASE_URL, window.location.origin);
const publicUrl = (path: string) => new URL(path, appBase).href;
// ── ③ Viewer2D 마운트 ─────────────────────────────────────────── // ── ③ Viewer2D 마운트 ───────────────────────────────────────────
const app = document.getElementById('app')!; const app = document.getElementById('app')!;
@@ -45,7 +47,9 @@ function setStatus(msg: string, err = false) {
statusEl.classList.toggle('err', err); statusEl.classList.toggle('err', err);
} }
const viewer = new Viewer2D(app); const viewer = createViewer2D(app, {
fontUrl: publicUrl('fonts/NanumGothic-Regular.ttf'),
});
viewer.onSelect((entity: unknown) => { viewer.onSelect((entity: unknown) => {
if (!entity) { if (!entity) {
setStatus('deselected'); setStatus('deselected');
@@ -56,18 +60,8 @@ viewer.onSelect((entity: unknown) => {
}); });
// ── ④ 파서 조합 ───────────────────────────────────────────────── // ── ④ 파서 조합 ─────────────────────────────────────────────────
async function parseCad(buf: ArrayBuffer, name: string): Promise<CadParseResult> {
if (isDxf(name)) return parseDxfBuffer(buf);
if (isDwg(name)) return parseDwgBuffer(new Uint8Array(buf));
const head = new TextDecoder('latin1').decode(
new Uint8Array(buf, 0, Math.min(6, buf.byteLength)),
);
if (head.startsWith('AC')) return parseDwgBuffer(new Uint8Array(buf));
return parseDxfBuffer(buf);
}
async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseResult> { async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseResult> {
const result = await parseCad(buf, name); const result = await parseCad({ data: buf, name });
viewer.load(result); viewer.load(result);
viewer.resize(); viewer.resize();
return result; return result;
@@ -81,7 +75,7 @@ async function loadUrl(url: string, nameHint?: string) {
} }
async function loadFile(file: File) { async function loadFile(file: File) {
if (!is2D(file.name)) throw new Error(`not .dwg/.dxf: ${file.name}`); if (!isCad2DFile(file.name)) throw new Error(`not .dwg/.dxf: ${file.name}`);
return loadAndRender(await file.arrayBuffer(), file.name); return loadAndRender(await file.arrayBuffer(), file.name);
} }
@@ -129,10 +123,12 @@ async function run(label: string, job: () => Promise<CadParseResult>) {
} }
document.getElementById('sample')!.addEventListener('click', () => { document.getElementById('sample')!.addEventListener('click', () => {
void run('BasicSample.dwg', () => loadUrl('/samples/BasicSample.dwg', 'BasicSample.dwg')); void run('BasicSample.dwg', () =>
loadUrl(publicUrl('samples/BasicSample.dwg'), 'BasicSample.dwg'),
);
}); });
document.getElementById('sampleDxf')!.addEventListener('click', () => { document.getElementById('sampleDxf')!.addEventListener('click', () => {
void run('simple.dxf', () => loadUrl('/samples/simple.dxf', 'simple.dxf')); void run('simple.dxf', () => loadUrl(publicUrl('samples/simple.dxf'), 'simple.dxf'));
}); });
document.getElementById('file')!.addEventListener('change', (ev) => { document.getElementById('file')!.addEventListener('change', (ev) => {
const file = (ev.target as HTMLInputElement).files?.[0]; const file = (ev.target as HTMLInputElement).files?.[0];
+26
View File
@@ -0,0 +1,26 @@
import { readFile } from 'node:fs/promises';
import { describe, expect, it } from 'vitest';
import { isCad2DFile, parseCad } from '@hmwebviewer/viewer2d';
describe('viewer2d package interface', () => {
it('parses the sample DXF through the public package interface', async () => {
const file = await readFile(new URL('../public/samples/simple.dxf', import.meta.url));
const result = await parseCad({
name: 'simple.dxf',
data: file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength),
});
const entities = result.entities as Array<{ layer?: string; type?: string }>;
expect(isCad2DFile('drawing.DWG')).toBe(true);
expect(isCad2DFile('drawing.dxf')).toBe(true);
expect(isCad2DFile('drawing.pdf')).toBe(false);
expect(entities.map(({ type, layer }) => [type, layer])).toEqual([
['LINE', 'OUTLINE'],
['LINE', 'OUTLINE'],
['LINE', 'OUTLINE'],
['LINE', 'OUTLINE'],
['CIRCLE', '0'],
['TEXT', '0'],
]);
});
});
+1 -1
View File
@@ -11,5 +11,5 @@
"isolatedModules": true, "isolatedModules": true,
"allowJs": true "allowJs": true
}, },
"include": ["src/**/*", "vite.config.ts"] "include": ["src/**/*", "packages/*/src/**/*", "vite.config.ts"]
} }