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,372 @@
1
+ import { actionableDiagnostics } from '../format/types.js';
2
+ export class ThreeW3dRenderer {
3
+ constructor() {
4
+ this.cache = new Map();
5
+ }
6
+ async render(page, overlayCanvas, options = {}) {
7
+ const canvas = options.webglCanvas ?? overlayCanvas;
8
+ if (options.webglCanvas)
9
+ options.webglCanvas.style.visibility = 'visible';
10
+ clearOverlay(overlayCanvas);
11
+ const gl = this.ensureContext(canvas);
12
+ const program = this.ensureProgram(gl);
13
+ const edgeProgram = this.ensureEdgeProgram(gl);
14
+ const scene = this.ensureScene(gl, page);
15
+ this.evictScenes(gl, options.maxCachedScenes ?? 2, options.maxGpuCacheBytes ?? 160 * 1024 * 1024);
16
+ gl.viewport(0, 0, canvas.width, canvas.height);
17
+ gl.enable(gl.DEPTH_TEST);
18
+ // DWF eModel sources may mix right/left handed shell orientation.
19
+ // Keep CAD preview robust by rendering shells double-sided.
20
+ gl.disable(gl.CULL_FACE);
21
+ gl.clearColor(0.97, 0.98, 1.0, 1.0);
22
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
23
+ gl.useProgram(program.program);
24
+ const camera = makeCamera(page, canvas, options);
25
+ gl.uniformMatrix4fv(program.uMvp, false, camera.mvp);
26
+ gl.uniformMatrix4fv(program.uModel, false, identity4());
27
+ gl.uniform3f(program.uLight0, 0.35, 0.65, 0.68);
28
+ gl.uniform3f(program.uLight1, -0.75, 0.4, -0.5);
29
+ gl.uniform1f(program.uAmbient, 0.28);
30
+ let drawCalls = 0;
31
+ gl.enable(gl.POLYGON_OFFSET_FILL);
32
+ gl.polygonOffset(1, 1);
33
+ for (const gm of scene.meshes) {
34
+ const color = gm.mesh.color ?? [0.72, 0.74, 0.78];
35
+ gl.uniform3f(program.uColor, color[0], color[1], color[2]);
36
+ if (gm.vao && 'bindVertexArray' in gl) {
37
+ gl.bindVertexArray(gm.vao);
38
+ }
39
+ else {
40
+ bindMesh(gl, program, gm);
41
+ }
42
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gm.index);
43
+ gl.drawElements(gl.TRIANGLES, gm.indexCount, gm.indexType, 0);
44
+ drawCalls++;
45
+ }
46
+ gl.disable(gl.POLYGON_OFFSET_FILL);
47
+ if (isWebGL2(gl))
48
+ gl.bindVertexArray(null);
49
+ gl.useProgram(edgeProgram.program);
50
+ gl.uniformMatrix4fv(edgeProgram.uMvp, false, camera.mvp);
51
+ gl.uniform3f(edgeProgram.uColor, 0.035, 0.04, 0.045);
52
+ gl.enable(gl.DEPTH_TEST);
53
+ for (const gm of scene.meshes) {
54
+ if (!gm.edgeIndex || !gm.edgeIndexCount)
55
+ continue;
56
+ gl.bindBuffer(gl.ARRAY_BUFFER, gm.pos);
57
+ gl.enableVertexAttribArray(edgeProgram.aPosition);
58
+ gl.vertexAttribPointer(edgeProgram.aPosition, 3, gl.FLOAT, false, 0, 0);
59
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gm.edgeIndex);
60
+ gl.drawElements(gl.LINES, gm.edgeIndexCount, gm.edgeIndexType ?? gm.indexType, 0);
61
+ drawCalls++;
62
+ }
63
+ return { backend: 'threejs-webgl', commands: drawCalls, warnings: actionableDiagnostics(page.diagnostics) };
64
+ }
65
+ dispose() {
66
+ if (this.gl) {
67
+ for (const scene of this.cache.values())
68
+ disposeScene(this.gl, scene);
69
+ }
70
+ this.cache.clear();
71
+ this.program = undefined;
72
+ this.edgeProgram = undefined;
73
+ }
74
+ ensureContext(canvas) {
75
+ if (this.gl && this.currentCanvas === canvas)
76
+ return this.gl;
77
+ const gl = (canvas.getContext('webgl2', { antialias: true, alpha: false })
78
+ ?? canvas.getContext('webgl', { antialias: true, alpha: false }));
79
+ if (!gl)
80
+ throw new Error('WebGL is not available for 3D W3D rendering.');
81
+ if (this.gl && this.currentCanvas !== canvas) {
82
+ for (const scene of this.cache.values())
83
+ disposeScene(this.gl, scene);
84
+ this.cache.clear();
85
+ }
86
+ this.gl = gl;
87
+ this.currentCanvas = canvas;
88
+ this.program = undefined;
89
+ this.edgeProgram = undefined;
90
+ return gl;
91
+ }
92
+ ensureProgram(gl) {
93
+ if (this.program)
94
+ return this.program;
95
+ const vs = compileShader(gl, gl.VERTEX_SHADER, `
96
+ attribute vec3 a_position;
97
+ attribute vec3 a_normal;
98
+ uniform mat4 u_mvp;
99
+ uniform mat4 u_model;
100
+ varying vec3 v_normal;
101
+ void main() {
102
+ v_normal = normalize((u_model * vec4(a_normal, 0.0)).xyz);
103
+ gl_Position = u_mvp * vec4(a_position, 1.0);
104
+ }
105
+ `);
106
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, `
107
+ precision mediump float;
108
+ varying vec3 v_normal;
109
+ uniform vec3 u_color;
110
+ uniform vec3 u_light0;
111
+ uniform vec3 u_light1;
112
+ uniform float u_ambient;
113
+ void main() {
114
+ vec3 n = normalize(v_normal);
115
+ float d0 = max(dot(n, normalize(u_light0)), 0.0);
116
+ float d1 = max(dot(n, normalize(u_light1)), 0.0) * 0.45;
117
+ vec3 color = u_color * (u_ambient + d0 * 0.78 + d1);
118
+ gl_FragColor = vec4(color, 1.0);
119
+ }
120
+ `);
121
+ const program = linkProgram(gl, vs, fs);
122
+ const info = {
123
+ program,
124
+ aPosition: gl.getAttribLocation(program, 'a_position'),
125
+ aNormal: gl.getAttribLocation(program, 'a_normal'),
126
+ uMvp: mustUniform(gl, program, 'u_mvp'),
127
+ uModel: mustUniform(gl, program, 'u_model'),
128
+ uColor: mustUniform(gl, program, 'u_color'),
129
+ uLight0: mustUniform(gl, program, 'u_light0'),
130
+ uLight1: mustUniform(gl, program, 'u_light1'),
131
+ uAmbient: mustUniform(gl, program, 'u_ambient')
132
+ };
133
+ this.program = info;
134
+ return info;
135
+ }
136
+ ensureEdgeProgram(gl) {
137
+ if (this.edgeProgram)
138
+ return this.edgeProgram;
139
+ const vs = compileShader(gl, gl.VERTEX_SHADER, `
140
+ attribute vec3 a_position;
141
+ uniform mat4 u_mvp;
142
+ void main() {
143
+ gl_Position = u_mvp * vec4(a_position, 1.0);
144
+ }
145
+ `);
146
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, `
147
+ precision mediump float;
148
+ uniform vec3 u_color;
149
+ void main() {
150
+ gl_FragColor = vec4(u_color, 0.9);
151
+ }
152
+ `);
153
+ const program = linkProgram(gl, vs, fs);
154
+ this.edgeProgram = {
155
+ program,
156
+ aPosition: gl.getAttribLocation(program, 'a_position'),
157
+ uMvp: mustUniform(gl, program, 'u_mvp'),
158
+ uColor: mustUniform(gl, program, 'u_color')
159
+ };
160
+ return this.edgeProgram;
161
+ }
162
+ evictScenes(gl, maxScenes, maxBytes) {
163
+ let bytes = Array.from(this.cache.values()).reduce((sum, s) => sum + s.bytes, 0);
164
+ while (this.cache.size > Math.max(1, maxScenes) || bytes > maxBytes) {
165
+ const first = this.cache.entries().next().value;
166
+ if (!first)
167
+ break;
168
+ this.cache.delete(first[0]);
169
+ disposeScene(gl, first[1]);
170
+ bytes -= first[1].bytes;
171
+ }
172
+ }
173
+ ensureScene(gl, page) {
174
+ const cached = this.cache.get(page);
175
+ if (cached)
176
+ return cached;
177
+ const meshes = [];
178
+ let bytes = 0;
179
+ const program = this.ensureProgram(gl);
180
+ const hasVao = typeof gl.createVertexArray === 'function';
181
+ for (const mesh of page.model.meshes) {
182
+ const pos = createBuffer(gl, gl.ARRAY_BUFFER, mesh.positions);
183
+ const normals = mesh.normals ?? new Float32Array(mesh.positions.length);
184
+ const normal = createBuffer(gl, gl.ARRAY_BUFFER, normals);
185
+ const index = createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, mesh.indices);
186
+ const edgeIndex = mesh.edgeIndices ? createBuffer(gl, gl.ELEMENT_ARRAY_BUFFER, mesh.edgeIndices) : undefined;
187
+ const gm = {
188
+ mesh,
189
+ pos,
190
+ normal,
191
+ index,
192
+ edgeIndex,
193
+ indexType: mesh.indices instanceof Uint32Array ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT,
194
+ edgeIndexType: mesh.edgeIndices instanceof Uint32Array ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT,
195
+ indexCount: mesh.indices.length,
196
+ edgeIndexCount: mesh.edgeIndices?.length,
197
+ bytes: mesh.positions.byteLength + normals.byteLength + mesh.indices.byteLength + (mesh.edgeIndices?.byteLength ?? 0)
198
+ };
199
+ if ((mesh.indices instanceof Uint32Array || mesh.edgeIndices instanceof Uint32Array) && !isWebGL2(gl)) {
200
+ const ext = gl.getExtension('OES_element_index_uint');
201
+ if (!ext)
202
+ throw new Error('This WebGL implementation does not support 32-bit element indices.');
203
+ }
204
+ if (hasVao) {
205
+ const gl2 = gl;
206
+ const vao = gl2.createVertexArray();
207
+ gl2.bindVertexArray(vao);
208
+ bindMesh(gl, program, gm);
209
+ gl2.bindVertexArray(null);
210
+ gm.vao = vao;
211
+ }
212
+ meshes.push(gm);
213
+ bytes += gm.bytes;
214
+ }
215
+ const scene = { page, meshes, bytes };
216
+ this.cache.set(page, scene);
217
+ return scene;
218
+ }
219
+ }
220
+ function disposeScene(gl, scene) {
221
+ for (const gm of scene.meshes) {
222
+ gl.deleteBuffer(gm.pos);
223
+ gl.deleteBuffer(gm.normal);
224
+ gl.deleteBuffer(gm.index);
225
+ if (gm.edgeIndex)
226
+ gl.deleteBuffer(gm.edgeIndex);
227
+ if (gm.vao && isWebGL2(gl))
228
+ gl.deleteVertexArray(gm.vao);
229
+ }
230
+ }
231
+ function createBuffer(gl, target, data) {
232
+ const buf = gl.createBuffer();
233
+ if (!buf)
234
+ throw new Error('Failed to create WebGL buffer.');
235
+ gl.bindBuffer(target, buf);
236
+ gl.bufferData(target, data, gl.STATIC_DRAW);
237
+ return buf;
238
+ }
239
+ function bindMesh(gl, program, gm) {
240
+ gl.bindBuffer(gl.ARRAY_BUFFER, gm.pos);
241
+ gl.enableVertexAttribArray(program.aPosition);
242
+ gl.vertexAttribPointer(program.aPosition, 3, gl.FLOAT, false, 0, 0);
243
+ gl.bindBuffer(gl.ARRAY_BUFFER, gm.normal);
244
+ gl.enableVertexAttribArray(program.aNormal);
245
+ gl.vertexAttribPointer(program.aNormal, 3, gl.FLOAT, false, 0, 0);
246
+ }
247
+ function clearOverlay(canvas) {
248
+ const ctx = canvas.getContext('2d');
249
+ if (!ctx)
250
+ return;
251
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
252
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
253
+ }
254
+ function compileShader(gl, type, source) {
255
+ const shader = gl.createShader(type);
256
+ if (!shader)
257
+ throw new Error('Failed to create WebGL shader.');
258
+ gl.shaderSource(shader, source);
259
+ gl.compileShader(shader);
260
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
261
+ const log = gl.getShaderInfoLog(shader) ?? 'unknown shader error';
262
+ gl.deleteShader(shader);
263
+ throw new Error(log);
264
+ }
265
+ return shader;
266
+ }
267
+ function linkProgram(gl, vs, fs) {
268
+ const program = gl.createProgram();
269
+ if (!program)
270
+ throw new Error('Failed to create WebGL program.');
271
+ gl.attachShader(program, vs);
272
+ gl.attachShader(program, fs);
273
+ gl.linkProgram(program);
274
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
275
+ const log = gl.getProgramInfoLog(program) ?? 'unknown program link error';
276
+ gl.deleteProgram(program);
277
+ throw new Error(log);
278
+ }
279
+ return program;
280
+ }
281
+ function mustUniform(gl, program, name) {
282
+ const loc = gl.getUniformLocation(program, name);
283
+ if (!loc)
284
+ throw new Error(`Missing shader uniform ${name}.`);
285
+ return loc;
286
+ }
287
+ function makeCamera(page, canvas, options) {
288
+ const b = page.model.bounds;
289
+ const aspect = Math.max(1e-6, canvas.width / Math.max(1, canvas.height));
290
+ const yaw = options.yaw ?? -0.78;
291
+ const pitch = clamp(options.pitch ?? 0.55, -1.45, 1.45);
292
+ const zoom = Math.max(0.05, Math.min(100, options.zoom ?? 1));
293
+ const radius = Math.max(1e-4, b.radius);
294
+ const distance = radius * 2.55 / zoom;
295
+ const cp = Math.cos(pitch);
296
+ const dir = [Math.sin(yaw) * cp, Math.sin(pitch), Math.cos(yaw) * cp];
297
+ const baseCenter = b.center;
298
+ const right = [Math.cos(yaw), 0, -Math.sin(yaw)];
299
+ const camUp = [
300
+ right[1] * dir[2] - right[2] * dir[1],
301
+ right[2] * dir[0] - right[0] * dir[2],
302
+ right[0] * dir[1] - right[1] * dir[0]
303
+ ];
304
+ const panScale = (radius * 2.0) / Math.max(1, Math.min(canvas.width, canvas.height)) / Math.max(0.1, zoom);
305
+ const px = (options.panX ?? 0) * panScale;
306
+ const py = (options.panY ?? 0) * panScale;
307
+ const center = [
308
+ baseCenter[0] - right[0] * px + camUp[0] * py,
309
+ baseCenter[1] - right[1] * px + camUp[1] * py,
310
+ baseCenter[2] - right[2] * px + camUp[2] * py
311
+ ];
312
+ const eye = [center[0] + dir[0] * distance, center[1] + dir[1] * distance, center[2] + dir[2] * distance];
313
+ const view = lookAt(eye, center, [0, 1, 0]);
314
+ const proj = perspective(Math.PI / 4, aspect, Math.max(0.001, distance - radius * 2.2), distance + radius * 3.2);
315
+ return { mvp: multiply4(proj, view) };
316
+ }
317
+ function identity4() {
318
+ return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
319
+ }
320
+ function perspective(fovy, aspect, near, far) {
321
+ const f = 1 / Math.tan(fovy / 2);
322
+ const nf = 1 / (near - far);
323
+ return new Float32Array([
324
+ f / aspect, 0, 0, 0,
325
+ 0, f, 0, 0,
326
+ 0, 0, (far + near) * nf, -1,
327
+ 0, 0, 2 * far * near * nf, 0
328
+ ]);
329
+ }
330
+ function lookAt(eye, target, up) {
331
+ let zx = eye[0] - target[0], zy = eye[1] - target[1], zz = eye[2] - target[2];
332
+ let len = Math.hypot(zx, zy, zz) || 1;
333
+ zx /= len;
334
+ zy /= len;
335
+ zz /= len;
336
+ let xx = up[1] * zz - up[2] * zy;
337
+ let xy = up[2] * zx - up[0] * zz;
338
+ let xz = up[0] * zy - up[1] * zx;
339
+ len = Math.hypot(xx, xy, xz) || 1;
340
+ xx /= len;
341
+ xy /= len;
342
+ xz /= len;
343
+ const yx = zy * xz - zz * xy;
344
+ const yy = zz * xx - zx * xz;
345
+ const yz = zx * xy - zy * xx;
346
+ return new Float32Array([
347
+ xx, yx, zx, 0,
348
+ xy, yy, zy, 0,
349
+ xz, yz, zz, 0,
350
+ -(xx * eye[0] + xy * eye[1] + xz * eye[2]),
351
+ -(yx * eye[0] + yy * eye[1] + yz * eye[2]),
352
+ -(zx * eye[0] + zy * eye[1] + zz * eye[2]),
353
+ 1
354
+ ]);
355
+ }
356
+ function multiply4(a, b) {
357
+ const out = new Float32Array(16);
358
+ for (let col = 0; col < 4; col++) {
359
+ for (let row = 0; row < 4; row++) {
360
+ out[col * 4 + row] =
361
+ a[0 * 4 + row] * b[col * 4 + 0] +
362
+ a[1 * 4 + row] * b[col * 4 + 1] +
363
+ a[2 * 4 + row] * b[col * 4 + 2] +
364
+ a[3 * 4 + row] * b[col * 4 + 3];
365
+ }
366
+ }
367
+ return out;
368
+ }
369
+ function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
370
+ function isWebGL2(gl) {
371
+ return typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
372
+ }
@@ -0,0 +1,24 @@
1
+ import { type RenderStats } from '../format/types.js';
2
+ import type { W2dTextPageData } from '../format/document.js';
3
+ export interface W2dRenderOptions {
4
+ zoom?: number;
5
+ panX?: number;
6
+ panY?: number;
7
+ preferWebgl?: boolean;
8
+ preferWasm?: boolean;
9
+ wasmUrl?: string;
10
+ background?: string;
11
+ maxGpuCacheBytes?: number;
12
+ maxCachedScenes?: number;
13
+ webglCanvas?: HTMLCanvasElement;
14
+ }
15
+ export declare class W2dRenderer {
16
+ private wasm?;
17
+ private webgl?;
18
+ private webglCanvas?;
19
+ render(page: W2dTextPageData, canvas: HTMLCanvasElement, options?: W2dRenderOptions): Promise<RenderStats>;
20
+ dispose(): void;
21
+ private getWebGlBackend;
22
+ private drawPrimitiveCanvas;
23
+ private drawPrimitiveWasm;
24
+ }
@@ -0,0 +1,198 @@
1
+ import { actionableDiagnostics, diag } from '../format/types.js';
2
+ import { applyPathToCanvas, flattenPath } from './xpsPath.js';
3
+ import { multiplyMatrix, parseBrushColor, transformPoint } from './style.js';
4
+ import { matrixForW2d } from './viewport.js';
5
+ import { WasmRasterBackend } from '../wasm/WasmRasterBackend.js';
6
+ import { WebGlW2dBackend } from './WebGlW2dBackend.js';
7
+ export class W2dRenderer {
8
+ async render(page, canvas, options = {}) {
9
+ const warnings = actionableDiagnostics(page.diagnostics);
10
+ const bg = options.background ?? '#ffffff';
11
+ if (options.preferWebgl ?? true) {
12
+ try {
13
+ const backend = this.getWebGlBackend(options.webglCanvas);
14
+ if (options.webglCanvas)
15
+ options.webglCanvas.style.visibility = 'visible';
16
+ const stats = backend.render(page, canvas, { ...options, compositeToTarget: !options.webglCanvas });
17
+ const mem = formatBytes(stats.gpuBytes);
18
+ const cacheText = stats.cacheHit ? 'cache hit' : 'cache upload';
19
+ warnings.push(diag('info', 'WEBGL_W2D_RENDERED', `WebGL rendered ${stats.vertexCount} vertices, ${stats.textCount} text item(s), GPU cache ${mem}, ${cacheText}.`, page.sourcePath));
20
+ warnings.push(...stats.warnings);
21
+ return { backend: 'webgl', commands: stats.commands, warnings };
22
+ }
23
+ catch (err) {
24
+ warnings.push(diag('warning', 'WEBGL_BACKEND_FALLBACK', `WebGL vector path failed, falling back to ${options.preferWasm ? 'WASM raster' : 'Canvas2D'}: ${String(err)}`, page.sourcePath));
25
+ }
26
+ }
27
+ if (options.webglCanvas)
28
+ options.webglCanvas.style.visibility = 'hidden';
29
+ const ctx = canvas.getContext('2d');
30
+ if (!ctx)
31
+ throw new Error('CanvasRenderingContext2D is not available.');
32
+ const pageMatrix = matrixForW2d(page, canvas.width, canvas.height, options.zoom, options.panX, options.panY);
33
+ let commands = 0;
34
+ if (options.preferWasm) {
35
+ try {
36
+ this.wasm ?? (this.wasm = new WasmRasterBackend({ wasmUrl: options.wasmUrl }));
37
+ await this.wasm.init();
38
+ this.wasm.begin(canvas.width, canvas.height, bg);
39
+ for (const p of page.primitives)
40
+ commands += this.drawPrimitiveWasm(p, pageMatrix);
41
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
42
+ ctx.putImageData(this.wasm.toImageData(), 0, 0);
43
+ for (const p of page.primitives.filter(p => p.type === 'text'))
44
+ commands += this.drawPrimitiveCanvas(ctx, p, pageMatrix);
45
+ return { backend: 'wasm-raster', commands, warnings };
46
+ }
47
+ catch (err) {
48
+ warnings.push(diag('warning', 'WASM_BACKEND_FALLBACK', `WASM raster path failed, falling back to Canvas2D: ${String(err)}`, page.sourcePath));
49
+ }
50
+ }
51
+ ctx.save();
52
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
53
+ ctx.fillStyle = bg;
54
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
55
+ ctx.restore();
56
+ for (const p of page.primitives)
57
+ commands += this.drawPrimitiveCanvas(ctx, p, pageMatrix);
58
+ return { backend: 'canvas2d', commands, warnings };
59
+ }
60
+ dispose() {
61
+ this.webgl?.dispose();
62
+ this.webgl = undefined;
63
+ this.webglCanvas = undefined;
64
+ }
65
+ getWebGlBackend(canvas) {
66
+ if (!this.webgl || this.webglCanvas !== canvas) {
67
+ this.webgl?.dispose();
68
+ this.webgl = new WebGlW2dBackend(canvas);
69
+ this.webglCanvas = canvas;
70
+ }
71
+ return this.webgl;
72
+ }
73
+ drawPrimitiveCanvas(ctx, p, pageMatrix) {
74
+ const matrix = multiplyMatrix(pageMatrix, p.matrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
75
+ const stroke = parseBrushColor(p.stroke ?? '#000000') ?? '#000000';
76
+ const fill = parseBrushColor(p.fill);
77
+ if (p.type === 'text') {
78
+ const [x, y] = transformPoint(matrix, p.x, p.y);
79
+ const screenSize = Math.max(4, Math.abs((p.size ?? 12) * estimateScale(matrix)));
80
+ if (screenSize < 2.5 || x > ctx.canvas.width + 64 || y > ctx.canvas.height + 64 || x < -ctx.canvas.width || y < -ctx.canvas.height)
81
+ return 0;
82
+ ctx.save();
83
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
84
+ ctx.fillStyle = fill ?? stroke;
85
+ ctx.font = `${Math.min(768, screenSize)}px sans-serif`;
86
+ ctx.textBaseline = 'alphabetic';
87
+ for (const [i, line] of p.text.split(/\n/).entries())
88
+ ctx.fillText(line, x, y + i * screenSize * 1.15);
89
+ ctx.restore();
90
+ return 1;
91
+ }
92
+ ctx.save();
93
+ ctx.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
94
+ ctx.lineWidth = Math.max(0.1, (p.lineWidth ?? 1));
95
+ if (p.type === 'polyline') {
96
+ if (p.points.length >= 4) {
97
+ ctx.beginPath();
98
+ ctx.moveTo(p.points[0], p.points[1]);
99
+ for (let i = 2; i + 1 < p.points.length; i += 2)
100
+ ctx.lineTo(p.points[i], p.points[i + 1]);
101
+ ctx.strokeStyle = stroke;
102
+ ctx.stroke();
103
+ }
104
+ }
105
+ else if (p.type === 'polygon') {
106
+ if (p.points.length >= 6) {
107
+ ctx.beginPath();
108
+ ctx.moveTo(p.points[0], p.points[1]);
109
+ for (let i = 2; i + 1 < p.points.length; i += 2)
110
+ ctx.lineTo(p.points[i], p.points[i + 1]);
111
+ ctx.closePath();
112
+ if (fill) {
113
+ ctx.fillStyle = fill;
114
+ ctx.fill();
115
+ }
116
+ if (p.stroke) {
117
+ ctx.strokeStyle = stroke;
118
+ ctx.stroke();
119
+ }
120
+ }
121
+ }
122
+ else if (p.type === 'rect') {
123
+ if (fill) {
124
+ ctx.fillStyle = fill;
125
+ ctx.fillRect(p.x, p.y, p.width, p.height);
126
+ }
127
+ if (p.stroke) {
128
+ ctx.strokeStyle = stroke;
129
+ ctx.strokeRect(p.x, p.y, p.width, p.height);
130
+ }
131
+ }
132
+ else if (p.type === 'path') {
133
+ ctx.beginPath();
134
+ applyPathToCanvas(ctx, p.commands);
135
+ if (fill) {
136
+ ctx.fillStyle = fill;
137
+ ctx.fill();
138
+ }
139
+ if (p.stroke) {
140
+ ctx.strokeStyle = stroke;
141
+ ctx.stroke();
142
+ }
143
+ }
144
+ ctx.restore();
145
+ return 1;
146
+ }
147
+ drawPrimitiveWasm(p, pageMatrix) {
148
+ if (!this.wasm || p.type === 'text')
149
+ return 0;
150
+ const matrix = multiplyMatrix(pageMatrix, p.matrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
151
+ const scale = estimateScale(matrix);
152
+ if (p.type === 'polyline') {
153
+ this.wasm.drawPolyline(p.points, matrix, parseBrushColor(p.stroke ?? '#000000'), (p.lineWidth ?? 1) * scale);
154
+ }
155
+ else if (p.type === 'polygon') {
156
+ this.wasm.drawPolygon(p.points, matrix, parseBrushColor(p.fill));
157
+ if (p.stroke)
158
+ this.wasm.drawPolyline(closePoints(p.points), matrix, parseBrushColor(p.stroke), (p.lineWidth ?? 1) * scale);
159
+ }
160
+ else if (p.type === 'rect') {
161
+ const pts = [p.x, p.y, p.x + p.width, p.y, p.x + p.width, p.y + p.height, p.x, p.y + p.height, p.x, p.y];
162
+ if (p.fill)
163
+ this.wasm.drawPolygon(pts, matrix, parseBrushColor(p.fill));
164
+ if (p.stroke)
165
+ this.wasm.drawPolyline(pts, matrix, parseBrushColor(p.stroke), (p.lineWidth ?? 1) * scale);
166
+ }
167
+ else if (p.type === 'path') {
168
+ const subs = flattenPath(p.commands, 0.5);
169
+ const fill = parseBrushColor(p.fill);
170
+ const stroke = parseBrushColor(p.stroke);
171
+ if (fill)
172
+ for (const s of subs)
173
+ if (s.closed || s.points.length >= 6)
174
+ this.wasm.drawPolygon(s.points, matrix, fill);
175
+ if (stroke)
176
+ for (const s of subs)
177
+ this.wasm.drawPolyline(s.points, matrix, stroke, (p.lineWidth ?? 1) * scale);
178
+ }
179
+ return 1;
180
+ }
181
+ }
182
+ function closePoints(points) {
183
+ if (points.length < 4)
184
+ return points;
185
+ const x0 = points[0], y0 = points[1];
186
+ const xn = points[points.length - 2], yn = points[points.length - 1];
187
+ return x0 === xn && y0 === yn ? points : [...points, x0, y0];
188
+ }
189
+ function estimateScale(m) {
190
+ return Math.max(1e-9, (Math.hypot(m.a, m.b) + Math.hypot(m.c, m.d)) * 0.5);
191
+ }
192
+ function formatBytes(bytes) {
193
+ if (bytes < 1024)
194
+ return `${bytes} B`;
195
+ if (bytes < 1024 * 1024)
196
+ return `${(bytes / 1024).toFixed(1)} KiB`;
197
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
198
+ }
@@ -0,0 +1,38 @@
1
+ import type { Diagnostic } from '../format/types.js';
2
+ import type { W2dTextPageData } from '../format/document.js';
3
+ export interface WebGlW2dRenderOptions {
4
+ zoom?: number;
5
+ panX?: number;
6
+ panY?: number;
7
+ background?: string;
8
+ maxGpuCacheBytes?: number;
9
+ maxCachedScenes?: number;
10
+ compositeToTarget?: boolean;
11
+ }
12
+ export interface WebGlW2dRenderResult {
13
+ commands: number;
14
+ warnings: Diagnostic[];
15
+ gpuBytes: number;
16
+ vertexCount: number;
17
+ textCount: number;
18
+ cacheHit: boolean;
19
+ }
20
+ export declare class WebGlW2dBackend {
21
+ private readonly canvas;
22
+ private readonly gl;
23
+ private readonly program;
24
+ private readonly aPos;
25
+ private readonly aColor;
26
+ private readonly uMatrix;
27
+ private readonly uViewport;
28
+ private readonly scenes;
29
+ private gpuBytes;
30
+ private tick;
31
+ constructor(canvas?: HTMLCanvasElement);
32
+ render(page: W2dTextPageData, targetCanvas: HTMLCanvasElement, options?: WebGlW2dRenderOptions): WebGlW2dRenderResult;
33
+ dispose(): void;
34
+ private resize;
35
+ private compileScene;
36
+ private evictIfNeeded;
37
+ private drawTextOverlay;
38
+ }