@flyfish-dev/dwf-viewer 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +235 -0
  3. package/NOTICE +10 -0
  4. package/PRODUCTION_3D_NOTES.md +48 -0
  5. package/README.md +203 -0
  6. package/dist/format/document.d.ts +186 -0
  7. package/dist/format/document.js +9 -0
  8. package/dist/format/dwf.d.ts +4 -0
  9. package/dist/format/dwf.js +372 -0
  10. package/dist/format/dwfx.d.ts +6 -0
  11. package/dist/format/dwfx.js +425 -0
  12. package/dist/format/emodelMetadata.d.ts +10 -0
  13. package/dist/format/emodelMetadata.js +368 -0
  14. package/dist/format/inflate.d.ts +4 -0
  15. package/dist/format/inflate.js +28 -0
  16. package/dist/format/opc.d.ts +28 -0
  17. package/dist/format/opc.js +85 -0
  18. package/dist/format/open.d.ts +3 -0
  19. package/dist/format/open.js +69 -0
  20. package/dist/format/types.d.ts +61 -0
  21. package/dist/format/types.js +6 -0
  22. package/dist/format/util.d.ts +18 -0
  23. package/dist/format/util.js +324 -0
  24. package/dist/format/w2dBinary.d.ts +19 -0
  25. package/dist/format/w2dBinary.js +629 -0
  26. package/dist/format/w2dText.d.ts +13 -0
  27. package/dist/format/w2dText.js +166 -0
  28. package/dist/format/w3d.d.ts +8 -0
  29. package/dist/format/w3d.js +826 -0
  30. package/dist/format/zip.d.ts +30 -0
  31. package/dist/format/zip.js +141 -0
  32. package/dist/index.d.ts +12 -0
  33. package/dist/index.js +9 -0
  34. package/dist/render/PageRenderer.d.ts +27 -0
  35. package/dist/render/PageRenderer.js +92 -0
  36. package/dist/render/ThreeJsSceneAdapter.d.ts +29 -0
  37. package/dist/render/ThreeJsSceneAdapter.js +52 -0
  38. package/dist/render/ThreeW3dRenderer.d.ts +24 -0
  39. package/dist/render/ThreeW3dRenderer.js +372 -0
  40. package/dist/render/W2dRenderer.d.ts +24 -0
  41. package/dist/render/W2dRenderer.js +198 -0
  42. package/dist/render/WebGlW2dBackend.d.ts +38 -0
  43. package/dist/render/WebGlW2dBackend.js +400 -0
  44. package/dist/render/XpsRenderer.d.ts +20 -0
  45. package/dist/render/XpsRenderer.js +310 -0
  46. package/dist/render/style.d.ts +16 -0
  47. package/dist/render/style.js +115 -0
  48. package/dist/render/viewport.d.ts +16 -0
  49. package/dist/render/viewport.js +27 -0
  50. package/dist/render/xpsPath.d.ts +41 -0
  51. package/dist/render/xpsPath.js +335 -0
  52. package/dist/viewer/DwfViewer.d.ts +69 -0
  53. package/dist/viewer/DwfViewer.js +386 -0
  54. package/dist/wasm/WasmRasterBackend.d.ts +21 -0
  55. package/dist/wasm/WasmRasterBackend.js +84 -0
  56. package/package.json +91 -0
  57. package/public/dwfv-render.wasm +0 -0
  58. package/styles/dwf-viewer.css +51 -0
@@ -0,0 +1,386 @@
1
+ import { openDwfDocument } from '../format/open.js';
2
+ import { transformPoint } from '../render/style.js';
3
+ import { fitPageMatrix, matrixForW2d } from '../render/viewport.js';
4
+ import { PageRenderer } from '../render/PageRenderer.js';
5
+ export class DwfViewer {
6
+ constructor(container, options = {}) {
7
+ this.pageIndex = 0;
8
+ this.zoom = 1;
9
+ this.panX = 0;
10
+ this.panY = 0;
11
+ this.yaw = -0.78;
12
+ this.pitch = 0.55;
13
+ this.renderRaf = 0;
14
+ this.renderSeq = 0;
15
+ this.currentDpr = 1;
16
+ this.preferWebgl = options.preferWebgl ?? true;
17
+ this.preferWasm = options.preferWasm ?? true;
18
+ this.wasmUrl = options.wasmUrl;
19
+ this.background = options.background ?? '#ffffff';
20
+ this.maxDevicePixelRatio = options.maxDevicePixelRatio ?? 2;
21
+ this.maxCanvasPixels = options.maxCanvasPixels ?? 16777216;
22
+ this.maxGpuCacheBytes = options.maxGpuCacheBytes;
23
+ this.maxCachedScenes = options.maxCachedScenes;
24
+ this.root = document.createElement('div');
25
+ this.root.className = 'dwfv-root';
26
+ const toolbar = document.createElement('div');
27
+ toolbar.className = 'dwfv-toolbar';
28
+ this.pageSelect = document.createElement('select');
29
+ this.zoomOutButton = button('−');
30
+ this.zoomInButton = button('+');
31
+ this.resetButton = button('适应');
32
+ this.status = document.createElement('span');
33
+ this.status.className = 'dwfv-status';
34
+ toolbar.append('页: ', this.pageSelect, this.zoomOutButton, this.zoomInButton, this.resetButton, this.status);
35
+ const workspace = document.createElement('div');
36
+ workspace.className = 'dwfv-workspace';
37
+ this.treePanel = document.createElement('div');
38
+ this.treePanel.className = 'dwfv-tree';
39
+ this.treePanel.style.display = 'none';
40
+ const stage = document.createElement('div');
41
+ stage.className = 'dwfv-stage';
42
+ this.webglCanvas = document.createElement('canvas');
43
+ this.webglCanvas.className = 'dwfv-canvas dwfv-webgl-canvas';
44
+ this.webglCanvas.style.visibility = 'hidden';
45
+ this.canvas = document.createElement('canvas');
46
+ this.canvas.className = 'dwfv-canvas dwfv-overlay-canvas';
47
+ this.canvas.style.touchAction = 'none';
48
+ stage.append(this.webglCanvas, this.canvas);
49
+ workspace.append(this.treePanel, stage);
50
+ this.root.append(toolbar, workspace);
51
+ container.replaceChildren(this.root);
52
+ this.pageSelect.addEventListener('change', () => { this.pageIndex = this.pageSelect.selectedIndex; this.resetView(); this.populateModelTree(); this.requestRender(); });
53
+ this.zoomOutButton.addEventListener('click', () => { this.zoomAtCenter(0.8); this.requestRender(); });
54
+ this.zoomInButton.addEventListener('click', () => { this.zoomAtCenter(1.25); this.requestRender(); });
55
+ this.resetButton.addEventListener('click', () => { this.resetView(); this.requestRender(); });
56
+ this.canvas.addEventListener('wheel', (e) => this.onWheel(e), { passive: false });
57
+ this.canvas.addEventListener('pointerdown', (e) => this.onPointerDown(e));
58
+ window.addEventListener('pointermove', (e) => this.onPointerMove(e));
59
+ window.addEventListener('pointerup', () => { this.drag = undefined; });
60
+ this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
61
+ new ResizeObserver(() => this.requestRender()).observe(stage);
62
+ this.setStatus('选择 .dwf/.dwfx 文件');
63
+ }
64
+ setPreferWebgl(value) {
65
+ this.preferWebgl = value;
66
+ this.requestRender();
67
+ }
68
+ setPreferWasm(value) {
69
+ this.preferWasm = value;
70
+ this.requestRender();
71
+ }
72
+ async load(input, options = {}) {
73
+ this.setStatus('解析文件中…');
74
+ this.renderer?.dispose();
75
+ this.doc = await openDwfDocument(input, { fileName: options.fileName });
76
+ this.renderer = new PageRenderer(this.doc);
77
+ this.pageIndex = options.pageIndex ?? 0;
78
+ this.zoom = 1;
79
+ this.panX = this.panY = 0;
80
+ this.yaw = -0.78;
81
+ this.pitch = 0.55;
82
+ this.preferWebgl = options.preferWebgl ?? this.preferWebgl;
83
+ this.preferWasm = options.preferWasm ?? this.preferWasm;
84
+ this.wasmUrl = options.wasmUrl ?? this.wasmUrl;
85
+ this.background = options.background ?? this.background;
86
+ this.maxGpuCacheBytes = options.maxGpuCacheBytes ?? this.maxGpuCacheBytes;
87
+ this.maxCachedScenes = options.maxCachedScenes ?? this.maxCachedScenes;
88
+ this.populatePages();
89
+ this.populateModelTree();
90
+ await this.render();
91
+ }
92
+ async render() {
93
+ if (!this.renderer || !this.doc)
94
+ return undefined;
95
+ this.resizeCanvasToDisplaySize();
96
+ const page = this.doc.pageData[this.pageIndex];
97
+ if (!page)
98
+ return undefined;
99
+ const seq = ++this.renderSeq;
100
+ const task = this.renderer.render(this.pageIndex, this.canvas, {
101
+ zoom: this.zoom,
102
+ panX: this.panX,
103
+ panY: this.panY,
104
+ preferWebgl: this.preferWebgl,
105
+ preferWasm: this.preferWasm,
106
+ wasmUrl: this.wasmUrl,
107
+ background: this.background,
108
+ maxGpuCacheBytes: this.maxGpuCacheBytes,
109
+ maxCachedScenes: this.maxCachedScenes,
110
+ webglCanvas: this.webglCanvas,
111
+ yaw: this.yaw,
112
+ pitch: this.pitch
113
+ });
114
+ this.pendingRender = task;
115
+ try {
116
+ const stats = await task;
117
+ if (this.pendingRender === task && seq === this.renderSeq) {
118
+ const warnCount = stats.warnings.filter(w => w.level !== 'info').length;
119
+ const dprText = this.currentDpr > 1 ? ` · DPR ${this.currentDpr.toFixed(2)}` : '';
120
+ this.setStatus(`${this.doc.kind.toUpperCase()} · ${page.kind} · ${stats.backend} · ${stats.commands} ops${dprText}${warnCount ? ` · ${warnCount} 警告` : ''}`, warnCount > 0);
121
+ }
122
+ return stats;
123
+ }
124
+ catch (err) {
125
+ if (seq === this.renderSeq)
126
+ this.setStatus(`渲染失败:${String(err)}`, true);
127
+ throw err;
128
+ }
129
+ }
130
+ getDocument() {
131
+ return this.doc;
132
+ }
133
+ fit() {
134
+ this.resetView();
135
+ this.requestRender();
136
+ }
137
+ dispose() {
138
+ if (this.renderRaf)
139
+ cancelAnimationFrame(this.renderRaf);
140
+ this.renderRaf = 0;
141
+ this.renderer?.dispose();
142
+ this.renderer = undefined;
143
+ this.doc = undefined;
144
+ this.root.replaceChildren();
145
+ }
146
+ requestRender() {
147
+ if (this.renderRaf)
148
+ return;
149
+ this.renderRaf = requestAnimationFrame(() => {
150
+ this.renderRaf = 0;
151
+ void this.render();
152
+ });
153
+ }
154
+ populatePages() {
155
+ this.pageSelect.replaceChildren();
156
+ const pages = this.doc?.pageData ?? [];
157
+ for (const [i, p] of pages.entries()) {
158
+ const opt = document.createElement('option');
159
+ opt.value = String(i);
160
+ opt.textContent = `${i + 1}. ${p.name} (${p.kind})`;
161
+ this.pageSelect.append(opt);
162
+ }
163
+ this.pageSelect.selectedIndex = Math.max(0, Math.min(this.pageIndex, pages.length - 1));
164
+ }
165
+ populateModelTree() {
166
+ const page = this.doc?.pageData[this.pageIndex];
167
+ if (!page || page.kind !== 'w3d-model' || !(page.model.sceneTree?.length)) {
168
+ this.treePanel.style.display = 'none';
169
+ this.treePanel.replaceChildren();
170
+ return;
171
+ }
172
+ this.treePanel.style.display = '';
173
+ const header = document.createElement('div');
174
+ header.className = 'dwfv-tree-header';
175
+ header.textContent = `模型结构 · ${page.model.stats.nodeCount ?? 0} 节点`;
176
+ const stats = document.createElement('div');
177
+ stats.className = 'dwfv-tree-stats';
178
+ stats.textContent = `${page.model.stats.meshCount} meshes · ${page.model.stats.triangleCount} triangles · ${(page.model.stats.textureCount ?? 0)} textures`;
179
+ const content = document.createElement('div');
180
+ content.className = 'dwfv-tree-content';
181
+ for (const node of page.model.sceneTree)
182
+ content.append(renderTreeNode(node));
183
+ this.treePanel.replaceChildren(header, stats, content);
184
+ }
185
+ resizeCanvasToDisplaySize() {
186
+ const rect = this.canvas.getBoundingClientRect();
187
+ const cssW = Math.max(1, rect.width);
188
+ const cssH = Math.max(1, rect.height);
189
+ let dpr = Math.max(1, Math.min(this.maxDevicePixelRatio, window.devicePixelRatio || 1));
190
+ const pixels = cssW * cssH * dpr * dpr;
191
+ if (pixels > this.maxCanvasPixels)
192
+ dpr *= Math.sqrt(this.maxCanvasPixels / pixels);
193
+ this.currentDpr = dpr;
194
+ const w = Math.max(1, Math.floor(cssW * dpr));
195
+ const h = Math.max(1, Math.floor(cssH * dpr));
196
+ if (this.canvas.width !== w || this.canvas.height !== h) {
197
+ this.canvas.width = w;
198
+ this.canvas.height = h;
199
+ }
200
+ if (this.webglCanvas.width !== w || this.webglCanvas.height !== h) {
201
+ this.webglCanvas.width = w;
202
+ this.webglCanvas.height = h;
203
+ }
204
+ }
205
+ resetView() {
206
+ this.zoom = 1;
207
+ this.panX = 0;
208
+ this.panY = 0;
209
+ this.yaw = -0.78;
210
+ this.pitch = 0.55;
211
+ const page = this.doc?.pageData[this.pageIndex];
212
+ if (page?.kind === 'w3d-model') {
213
+ const cam = page.model.initialView?.camera;
214
+ const pos = cam?.position;
215
+ const target = cam?.target;
216
+ if (pos && target) {
217
+ const dx = pos[0] - target[0];
218
+ const dy = pos[1] - target[1];
219
+ const dz = pos[2] - target[2];
220
+ const dist = Math.hypot(dx, dy, dz);
221
+ if (dist > 1e-6) {
222
+ this.pitch = Math.max(-1.45, Math.min(1.45, Math.asin(dy / dist)));
223
+ this.yaw = Math.atan2(dx, dz);
224
+ const radius = Math.max(1e-6, page.model.bounds.radius);
225
+ this.zoom = Math.max(0.05, Math.min(100, radius * 2.55 / dist));
226
+ }
227
+ }
228
+ }
229
+ }
230
+ zoomAtCenter(factor) {
231
+ this.resizeCanvasToDisplaySize();
232
+ if (this.is3dPage()) {
233
+ this.zoom = Math.max(0.05, Math.min(100, this.zoom * factor));
234
+ return;
235
+ }
236
+ const cx = this.canvas.width / 2;
237
+ const cy = this.canvas.height / 2;
238
+ this.zoomAroundPoint(factor, cx, cy);
239
+ }
240
+ zoomAroundPoint(factor, cx, cy) {
241
+ const oldZoom = this.zoom;
242
+ const nextZoom = Math.max(0.05, Math.min(64, oldZoom * factor));
243
+ if (nextZoom === oldZoom)
244
+ return;
245
+ // Keep the drawing coordinate under the cursor fixed. A simple
246
+ // `pan = cursor - (cursor - pan) * ratio` is wrong for fit-to-page
247
+ // transforms, because the fit center also changes with zoom.
248
+ const anchoredPoint = this.pagePointAtCanvasPoint(cx, cy, oldZoom, this.panX, this.panY);
249
+ if (anchoredPoint) {
250
+ const baseMatrix = this.pageMatrixAt(nextZoom, 0, 0);
251
+ if (baseMatrix) {
252
+ const [sx, sy] = transformPoint(baseMatrix, anchoredPoint.x, anchoredPoint.y);
253
+ this.panX = cx - sx;
254
+ this.panY = cy - sy;
255
+ this.zoom = nextZoom;
256
+ return;
257
+ }
258
+ }
259
+ // Generic fallback for unsupported page kinds.
260
+ const ratio = nextZoom / oldZoom;
261
+ this.panX = cx - (cx - this.panX) * ratio;
262
+ this.panY = cy - (cy - this.panY) * ratio;
263
+ this.zoom = nextZoom;
264
+ }
265
+ onWheel(e) {
266
+ if (!this.doc)
267
+ return;
268
+ e.preventDefault();
269
+ this.resizeCanvasToDisplaySize();
270
+ const rect = this.canvas.getBoundingClientRect();
271
+ const cx = (e.clientX - rect.left) * this.currentDpr;
272
+ const cy = (e.clientY - rect.top) * this.currentDpr;
273
+ const delta = e.deltaMode === WheelEvent.DOM_DELTA_LINE
274
+ ? e.deltaY * 16
275
+ : e.deltaMode === WheelEvent.DOM_DELTA_PAGE
276
+ ? e.deltaY * Math.max(1, rect.height)
277
+ : e.deltaY;
278
+ const factor = Math.exp(-delta * 0.0015);
279
+ if (this.is3dPage())
280
+ this.zoom = Math.max(0.05, Math.min(100, this.zoom * factor));
281
+ else
282
+ this.zoomAroundPoint(factor, cx, cy);
283
+ this.requestRender();
284
+ }
285
+ onPointerDown(e) {
286
+ this.canvas.setPointerCapture?.(e.pointerId);
287
+ if (this.is3dPage())
288
+ e.preventDefault();
289
+ const mode = this.is3dPage() ? ((e.button === 2 || e.shiftKey) ? 'pan3d' : 'rotate3d') : 'pan2d';
290
+ this.drag = { x: e.clientX, y: e.clientY, panX: this.panX, panY: this.panY, yaw: this.yaw, pitch: this.pitch, mode };
291
+ }
292
+ onPointerMove(e) {
293
+ if (!this.drag)
294
+ return;
295
+ const dx = e.clientX - this.drag.x;
296
+ const dy = e.clientY - this.drag.y;
297
+ if (this.drag.mode === 'rotate3d') {
298
+ this.yaw = this.drag.yaw + dx * 0.008;
299
+ this.pitch = Math.max(-1.45, Math.min(1.45, this.drag.pitch + dy * 0.008));
300
+ }
301
+ else {
302
+ this.panX = this.drag.panX + dx * this.currentDpr;
303
+ this.panY = this.drag.panY + dy * this.currentDpr;
304
+ }
305
+ this.requestRender();
306
+ }
307
+ is3dPage() {
308
+ const page = this.doc?.pageData[this.pageIndex];
309
+ return page?.kind === 'w3d-model';
310
+ }
311
+ pagePointAtCanvasPoint(cx, cy, zoom, panX, panY) {
312
+ const m = this.pageMatrixAt(zoom, panX, panY);
313
+ if (!m)
314
+ return undefined;
315
+ const det = m.a * m.d - m.b * m.c;
316
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12)
317
+ return undefined;
318
+ const dx = cx - m.e;
319
+ const dy = cy - m.f;
320
+ return {
321
+ x: (m.d * dx - m.c * dy) / det,
322
+ y: (-m.b * dx + m.a * dy) / det
323
+ };
324
+ }
325
+ pageMatrixAt(zoom, panX, panY) {
326
+ const page = this.doc?.pageData[this.pageIndex];
327
+ if (!page)
328
+ return undefined;
329
+ const canvasWidth = Math.max(1, this.canvas.width);
330
+ const canvasHeight = Math.max(1, this.canvas.height);
331
+ if (page.kind === 'w2d-text')
332
+ return matrixForW2d(page, canvasWidth, canvasHeight, zoom, panX, panY);
333
+ if (page.kind === 'xps-fixed-page') {
334
+ return fitPageMatrix({
335
+ canvasWidth,
336
+ canvasHeight,
337
+ pageWidth: Math.max(1, page.width),
338
+ pageHeight: Math.max(1, page.height),
339
+ zoom,
340
+ panX,
341
+ panY
342
+ });
343
+ }
344
+ if (page.kind === 'image') {
345
+ return fitPageMatrix({
346
+ canvasWidth,
347
+ canvasHeight,
348
+ pageWidth: Math.max(1, page.width),
349
+ pageHeight: Math.max(1, page.height),
350
+ zoom,
351
+ panX,
352
+ panY,
353
+ margin: 0
354
+ });
355
+ }
356
+ return undefined;
357
+ }
358
+ setStatus(text, warn = false) {
359
+ this.status.textContent = text;
360
+ this.status.classList.toggle('dwfv-warn', warn);
361
+ }
362
+ }
363
+ function button(text) {
364
+ const b = document.createElement('button');
365
+ b.type = 'button';
366
+ b.textContent = text;
367
+ return b;
368
+ }
369
+ function renderTreeNode(node) {
370
+ const details = document.createElement('details');
371
+ details.open = node.children.length > 0 && node.children.length < 20;
372
+ const summary = document.createElement('summary');
373
+ summary.textContent = node.label || node.id;
374
+ if (node.meshIds.length > 0)
375
+ summary.title = `${node.meshIds.length} mesh(es)`;
376
+ details.append(summary);
377
+ if (node.contentRefs.length > 0) {
378
+ const meta = document.createElement('div');
379
+ meta.className = 'dwfv-tree-meta';
380
+ meta.textContent = node.contentRefs.slice(0, 3).join(', ');
381
+ details.append(meta);
382
+ }
383
+ for (const child of node.children)
384
+ details.append(renderTreeNode(child));
385
+ return details;
386
+ }
@@ -0,0 +1,21 @@
1
+ import { type Matrix2D } from '../render/style.js';
2
+ export interface WasmRasterOptions {
3
+ wasmUrl?: string;
4
+ }
5
+ export declare class WasmRasterBackend {
6
+ private readonly wasmUrl;
7
+ private exports?;
8
+ private fbPtr;
9
+ private fbBytes;
10
+ private width;
11
+ private height;
12
+ constructor(options?: WasmRasterOptions);
13
+ init(): Promise<void>;
14
+ begin(width: number, height: number, backgroundCss?: string): void;
15
+ drawPolyline(points: number[], matrix: Matrix2D, strokeCss: string | undefined, thickness?: number): void;
16
+ drawPolygon(points: number[], matrix: Matrix2D, fillCss: string | undefined): void;
17
+ toImageData(): ImageData;
18
+ private allocF32;
19
+ private ensureMemory;
20
+ private requireExports;
21
+ }
@@ -0,0 +1,84 @@
1
+ import { colorToRgba32, transformPoint } from '../render/style.js';
2
+ export class WasmRasterBackend {
3
+ constructor(options = {}) {
4
+ this.fbPtr = 0;
5
+ this.fbBytes = 0;
6
+ this.width = 0;
7
+ this.height = 0;
8
+ this.wasmUrl = options.wasmUrl ?? './public/dwfv-render.wasm';
9
+ }
10
+ async init() {
11
+ if (this.exports)
12
+ return;
13
+ const response = await fetch(this.wasmUrl);
14
+ if (!response.ok)
15
+ throw new Error(`Failed to load WASM raster backend: ${response.status} ${response.statusText}`);
16
+ const bytes = await response.arrayBuffer();
17
+ const instance = await WebAssembly.instantiate(bytes, {});
18
+ this.exports = instance.instance.exports;
19
+ }
20
+ begin(width, height, backgroundCss = '#ffffff') {
21
+ const e = this.requireExports();
22
+ this.width = Math.max(1, Math.floor(width));
23
+ this.height = Math.max(1, Math.floor(height));
24
+ this.fbBytes = this.width * this.height * 4;
25
+ e.dwfv_reset_heap();
26
+ this.fbPtr = e.dwfv_alloc(this.fbBytes);
27
+ this.ensureMemory(this.fbPtr + this.fbBytes);
28
+ e.dwfv_clear(this.fbPtr, this.width, this.height, colorToRgba32(backgroundCss, 0xffffffff));
29
+ }
30
+ drawPolyline(points, matrix, strokeCss, thickness = 1) {
31
+ const e = this.requireExports();
32
+ if (points.length < 4 || !strokeCss)
33
+ return;
34
+ const count = Math.floor(points.length / 2);
35
+ const ptr = this.allocF32(count * 2);
36
+ const heap = new Float32Array(e.memory.buffer, ptr, count * 2);
37
+ for (let i = 0; i < count; i++) {
38
+ const [x, y] = transformPoint(matrix, points[i * 2], points[i * 2 + 1]);
39
+ heap[i * 2] = x;
40
+ heap[i * 2 + 1] = y;
41
+ }
42
+ e.dwfv_draw_polyline(this.fbPtr, this.width, this.height, ptr, count, colorToRgba32(strokeCss, 0xff000000), Math.max(1, thickness));
43
+ }
44
+ drawPolygon(points, matrix, fillCss) {
45
+ const e = this.requireExports();
46
+ if (points.length < 6 || !fillCss)
47
+ return;
48
+ const count = Math.floor(points.length / 2);
49
+ const ptr = this.allocF32(count * 2);
50
+ const heap = new Float32Array(e.memory.buffer, ptr, count * 2);
51
+ for (let i = 0; i < count; i++) {
52
+ const [x, y] = transformPoint(matrix, points[i * 2], points[i * 2 + 1]);
53
+ heap[i * 2] = x;
54
+ heap[i * 2 + 1] = y;
55
+ }
56
+ e.dwfv_draw_polygon(this.fbPtr, this.width, this.height, ptr, count, colorToRgba32(fillCss, 0xff000000));
57
+ }
58
+ toImageData() {
59
+ const e = this.requireExports();
60
+ const src = new Uint8ClampedArray(e.memory.buffer, this.fbPtr, this.fbBytes);
61
+ // Clone out of wasm memory because future allocations can invalidate the buffer view.
62
+ return new ImageData(new Uint8ClampedArray(src), this.width, this.height);
63
+ }
64
+ allocF32(floatCount) {
65
+ const e = this.requireExports();
66
+ const byteCount = floatCount * 4;
67
+ const ptr = e.dwfv_alloc(byteCount);
68
+ this.ensureMemory(ptr + byteCount);
69
+ return ptr;
70
+ }
71
+ ensureMemory(requiredBytes) {
72
+ const e = this.requireExports();
73
+ const page = 65536;
74
+ const current = e.memory.buffer.byteLength;
75
+ if (requiredBytes > current) {
76
+ e.memory.grow(Math.ceil((requiredBytes - current) / page));
77
+ }
78
+ }
79
+ requireExports() {
80
+ if (!this.exports)
81
+ throw new Error('WASM backend is not initialized. Call init() first.');
82
+ return this.exports;
83
+ }
84
+ }
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@flyfish-dev/dwf-viewer",
3
+ "version": "0.5.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Pure frontend DWF/DWFx viewer with W2D WebGL rendering, W3D/HSF Three.js 3D rendering, DWFx/XPS support, and optional WASM raster fallback.",
7
+ "license": "AGPL-3.0-only",
8
+ "author": "flyfish-dev",
9
+ "homepage": "https://github.com/flyfish-dev/dwf-viewer#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/flyfish-dev/dwf-viewer.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/flyfish-dev/dwf-viewer/issues"
16
+ },
17
+ "keywords": [
18
+ "dwf",
19
+ "dwfx",
20
+ "w2d",
21
+ "w3d",
22
+ "hsf",
23
+ "xps",
24
+ "cad",
25
+ "viewer",
26
+ "webgl",
27
+ "threejs",
28
+ "wasm"
29
+ ],
30
+ "sideEffects": [
31
+ "styles/dwf-viewer.css"
32
+ ],
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ },
41
+ "./styles.css": "./styles/dwf-viewer.css",
42
+ "./wasm/dwfv-render.wasm": "./public/dwfv-render.wasm",
43
+ "./package.json": "./package.json"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "public/dwfv-render.wasm",
48
+ "styles/dwf-viewer.css",
49
+ "README.md",
50
+ "LICENSE",
51
+ "NOTICE",
52
+ "CHANGELOG.md",
53
+ "PRODUCTION_3D_NOTES.md"
54
+ ],
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "packageManager": "npm@11.12.1",
62
+ "scripts": {
63
+ "build:wasm": "bash scripts/build-wasm.sh",
64
+ "build:ts": "tsc -p tsconfig.json",
65
+ "build": "npm run build:wasm && npm run build:ts",
66
+ "build:demo": "npm run build && node scripts/build-demo.mjs",
67
+ "clean": "rm -rf dist demo-dist public/dwfv-render.wasm",
68
+ "inspect:dwfx": "node scripts/inspect-dwfx.mjs",
69
+ "validate:production": "node scripts/validate-production.mjs",
70
+ "check:examples": "node scripts/check-examples.mjs",
71
+ "check:package": "node scripts/package-check.mjs",
72
+ "prepublishOnly": "npm run build && npm run validate:production && npm run check:package",
73
+ "pack:dry": "npm pack --dry-run",
74
+ "publish:unscoped": "node scripts/publish-npm.mjs dwf-viewer",
75
+ "publish:scoped": "node scripts/publish-npm.mjs @flyfish-dev/dwf-viewer",
76
+ "publish:all": "node scripts/publish-npm.mjs dwf-viewer @flyfish-dev/dwf-viewer",
77
+ "demo:serve": "npm run build:demo && python3 -m http.server 8080 -d demo-dist",
78
+ "deploy:pages": "npm run build:demo && npx wrangler pages deploy demo-dist"
79
+ },
80
+ "devDependencies": {
81
+ "typescript": "^4.9.5"
82
+ },
83
+ "peerDependencies": {
84
+ "three": ">=0.150.0"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "three": {
88
+ "optional": true
89
+ }
90
+ }
91
+ }
Binary file
@@ -0,0 +1,51 @@
1
+ .dwfv-root {
2
+ height: 100%;
3
+ min-height: 0;
4
+ display: grid;
5
+ grid-template-rows: auto 1fr;
6
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
7
+ color: #111827;
8
+ }
9
+ .dwfv-toolbar {
10
+ display: flex;
11
+ gap: 8px;
12
+ align-items: center;
13
+ padding: 8px 12px;
14
+ background: #fff;
15
+ border-bottom: 1px solid #e5e7eb;
16
+ }
17
+ .dwfv-toolbar button,
18
+ .dwfv-toolbar select { font: inherit; }
19
+ .dwfv-workspace {
20
+ min-height: 0;
21
+ display: grid;
22
+ grid-template-columns: minmax(220px, 300px) 1fr;
23
+ }
24
+ .dwfv-tree {
25
+ overflow: auto;
26
+ border-right: 1px solid #d6d8dd;
27
+ background: #fff;
28
+ font-size: 12px;
29
+ padding: 8px;
30
+ }
31
+ .dwfv-tree[style*="display: none"] + .dwfv-stage { grid-column: 1 / -1; }
32
+ .dwfv-tree-header { font-weight: 700; margin-bottom: 4px; }
33
+ .dwfv-tree-stats { color: #6b7280; margin-bottom: 8px; }
34
+ .dwfv-tree details { margin-left: 8px; }
35
+ .dwfv-tree summary { cursor: pointer; padding: 2px 0; }
36
+ .dwfv-tree-meta {
37
+ color: #6b7280;
38
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
39
+ font-size: 10px;
40
+ padding-left: 14px;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ white-space: nowrap;
44
+ }
45
+ .dwfv-stage { position: relative; overflow: hidden; background: #e5e7eb; }
46
+ .dwfv-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
47
+ .dwfv-webgl-canvas { pointer-events: none; }
48
+ .dwfv-overlay-canvas { pointer-events: auto; touch-action: none; cursor: grab; }
49
+ .dwfv-overlay-canvas:active { cursor: grabbing; }
50
+ .dwfv-status { margin-left: auto; font-size: 12px; color: #4b5563; }
51
+ .dwfv-warn { color: #92400e; }