@flyfish-dev/dwf-viewer 0.5.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - Added WebGL-accelerated XPS/DWFx 2D vector rendering through `WebGlXpsBackend`.
6
+ - Added XPS ODTTF embedded font deobfuscation/loading for CAD-like text appearance.
7
+ - Tuned demo defaults for dense CAD overview linework and lower DPR memory pressure.
8
+ - Added curated Autodesk floor-plan DWFx demo entry with default A03 sheet.
9
+ - Coalesced viewer render requests to avoid zoom/pan render pile-ups.
10
+ - Kept Cloudflare demo assets versioned under `dist-v0.6.0` to prevent stale browser module caches.
11
+
12
+ ## 0.5.1
13
+
14
+ - Added CAD adaptive line-weight rendering for XPS FixedPage, W2D Canvas/WASM, and W2D WebGL paths.
15
+ - Added overview text LOD culling to avoid dense annotation blobs at fit-to-page while preserving text when zoomed in.
16
+ - Added embedded XPS TrueType font loading for Glyphs when browsers support the FontFace API.
17
+ - Added demo line-weight mode selector: CAD adaptive, hairline, physical.
18
+ - Versioned demo dist assets under `dist-v0.5.1` to prevent stale browser module caches.
19
+
3
20
  ## 0.5.0
4
21
 
5
- - Prepared repository for public npm release and Cloudflare Pages demo deployment.
6
- - Renamed the public package metadata to `dwf-viewer`.
7
- - Added dual npm publishing support for `dwf-viewer` and `@flyfish-dev/dwf-viewer`.
8
- - Switched the project license to `AGPL-3.0-only` and added `NOTICE`.
9
- - Normalized GitHub repository metadata for `flyfish-dev/dwf-viewer`.
22
+ - Prepared the repository for public npm release and Cloudflare Pages demo deployment.
23
+ - Published normalized packages as `dwf-viewer` and `@flyfish-dev/dwf-viewer`.
24
+ - Adopted `AGPL-3.0-only` with `NOTICE` for strict open-source distribution.
10
25
  - Added curated, de-duplicated demo example manifest.
11
26
  - Removed production-success info diagnostics from W3D/eModel pages so the Robot Arm demo renders with zero page diagnostics.
12
- - Added static demo build pipeline and package validation scripts.
27
+ - Added static demo build pipeline, package validation scripts, CI, and publish helpers.
@@ -46,3 +46,7 @@ Serve the WASM file with `application/wasm`. The Cloudflare Pages demo writes `_
46
46
  ## Unsupported semantics
47
47
 
48
48
  Unsupported historical HSF opcodes, advanced CAD display-list behavior, and full Design Review parity are tracked as incremental parser work. The rendering contract is: exact-enough supported semantics are displayed, unsupported semantics are diagnosed, and known-failing 3D geometry is not hidden by image fallback.
49
+
50
+ ## 2D production rendering update
51
+
52
+ Version 0.6.0 adds WebGL acceleration for XPS/DWFx vector geometry and keeps text/images in a Canvas overlay. This avoids CPU redraw bottlenecks when zooming dense sheets and keeps GPU memory bounded through scene cache limits. Embedded ODTTF fonts are deobfuscated and loaded through FontFace where supported, producing CAD-like text weights in the demo.
package/README.md CHANGED
@@ -1,21 +1,36 @@
1
1
  # DWF Viewer
2
2
 
3
- Pure frontend DWF/DWFx viewer for browsers. It parses DWF/DWFx packages locally in the browser and renders common 2D and 3D content without a server-side CAD conversion service.
3
+ Pure frontend DWF/DWFx CAD viewer for browsers. It parses DWF/DWFx packages locally and renders common 2D and 3D content without a server-side CAD conversion service.
4
4
 
5
- ## Status
5
+ ## Links
6
6
 
7
- This repository is structured as a publishable npm package plus a static Cloudflare Pages demo. The same build is published under both `dwf-viewer` and `@flyfish-dev/dwf-viewer`.
7
+ | Entry | URL |
8
+ |---|---|
9
+ | Online demo | https://dwf-viewer-demo.pages.dev/ |
10
+ | Repository | https://github.com/flyfish-dev/dwf-viewer |
11
+ | npm | https://www.npmjs.com/package/dwf-viewer |
12
+ | scoped npm | https://www.npmjs.com/package/@flyfish-dev/dwf-viewer |
13
+
14
+ Current version: `0.6.0`
15
+
16
+ ## Why
17
+
18
+ DWF and DWFx are still common in manufacturing, construction, field service, engineering archives, PLM, after-sales systems, and document management products. Many web systems can preview PDF, Office files, and images, but DWF often falls back to desktop viewers, server-side conversion, or static thumbnails.
19
+
20
+ DWF Viewer is built for the browser-native path: parse the package, decode the sheet/model data, and render it directly inside a web application. This keeps integration simple for private deployments and reduces conversion queues, temporary files, and infrastructure coupling.
8
21
 
9
- Supported paths:
22
+ ## Supported Paths
10
23
 
11
24
  | Format / content | Status |
12
25
  |---|---|
13
26
  | DWF 6+ ZIP container | Supported |
14
27
  | DWFx / OPC package | Supported |
15
- | XPS FixedPage 2D sheets | Supported common subset |
28
+ | XPS FixedPage 2D sheets | Supported common subset with WebGL vector acceleration and Canvas text/image overlay |
16
29
  | Classic binary WHIP!/W2D 2D sheets | Supported for core geometry/text/images used by Autodesk samples |
30
+ | Textual W2D pages | Supported for smoke tests and simple sheets |
17
31
  | W3D/HSF 3D eModel shell geometry | Supported: uncompressed, CS_TRIVIAL, and Edgebreaker shell meshes |
18
32
  | Three.js adapter | Supported |
33
+ | Built-in WebGL 3D renderer | Supported through `DwfViewer` |
19
34
  | WASM raster fallback | Supported for 2D vector rasterization |
20
35
  | eModel metadata | Materials, textures, scene tree, saved views, PMI/animation data containers |
21
36
 
@@ -31,7 +46,7 @@ npm install @flyfish-dev/dwf-viewer three
31
46
 
32
47
  `three` is an optional peer dependency. It is required only when you use `createThreeGroupFromW3d()` directly. The built-in `DwfViewer` uses its own WebGL 3D renderer.
33
48
 
34
- ## Basic browser usage
49
+ ## Basic Browser Usage
35
50
 
36
51
  ```ts
37
52
  import 'dwf-viewer/styles.css';
@@ -41,10 +56,15 @@ const viewer = new DwfViewer(document.getElementById('viewer')!, {
41
56
  wasmUrl: '/dwfv-render.wasm',
42
57
  preferWebgl: true,
43
58
  preferWasm: true,
44
- maxDevicePixelRatio: 2,
45
- maxCanvasPixels: 16_777_216,
46
- maxGpuCacheBytes: 160 * 1024 * 1024,
47
- maxCachedScenes: 2
59
+ maxDevicePixelRatio: 1.5,
60
+ maxCanvasPixels: 12_000_000,
61
+ maxGpuCacheBytes: 192 * 1024 * 1024,
62
+ maxCachedScenes: 4,
63
+ lineWeightMode: 'adaptive',
64
+ minStrokeCssPx: 0.42,
65
+ maxOverviewStrokeCssPx: 0.9,
66
+ minTextCssPx: 1.05,
67
+ minFilledAreaCssPx: 0.04
48
68
  });
49
69
 
50
70
  await viewer.load(file);
@@ -56,7 +76,47 @@ Copy the WASM asset from the package into your public assets directory:
56
76
  cp node_modules/dwf-viewer/public/dwfv-render.wasm public/dwfv-render.wasm
57
77
  ```
58
78
 
59
- ## Three.js integration
79
+ For the scoped package:
80
+
81
+ ```bash
82
+ cp node_modules/@flyfish-dev/dwf-viewer/public/dwfv-render.wasm public/dwfv-render.wasm
83
+ ```
84
+
85
+ ## WebGL 2D Rendering
86
+
87
+ Version `0.6.0` adds WebGL-accelerated XPS/DWFx 2D vector rendering through `WebGlXpsBackend`, alongside the existing W2D WebGL path.
88
+
89
+ The built-in viewer uses WebGL for Classic W2D and DWFx/XPS vector geometry when `preferWebgl` is enabled. Geometry is uploaded to GPU buffers and cached by page and zoom bucket; pan operations update shader uniforms. Text, images, and XPS image brushes stay on the transparent Canvas overlay so browser font rendering and bitmap decoding remain reliable.
90
+
91
+ For dense architectural sheets, the demo defaults are tuned for CAD review: 1.5 maximum DPR, a 12M-pixel canvas cap, adaptive thin-line overview, and render coalescing during wheel/pointer interaction.
92
+
93
+ ## CAD Line-Weight Rendering
94
+
95
+ The default 2D rendering mode is `lineWeightMode: 'adaptive'`. At fit-to-page, linework is normalized toward screen-space hairlines so dense drawings remain readable; as zoom increases, original DWF/XPS line weights return progressively.
96
+
97
+ Available modes:
98
+
99
+ | Mode | Behavior |
100
+ |---|---|
101
+ | `adaptive` | Default. Overview thin-line rendering with zoom-aware recovery of source line weights. |
102
+ | `hairline` | Force strokes to a visible CSS-pixel hairline. Useful for dense plans and review thumbnails. |
103
+ | `physical` | Preserve source stroke widths. Useful for print-fidelity comparisons; dense sheets can look heavy when zoomed out. |
104
+
105
+ Related options:
106
+
107
+ ```ts
108
+ new DwfViewer(el, {
109
+ lineWeightMode: 'adaptive',
110
+ minStrokeCssPx: 0.42,
111
+ maxOverviewStrokeCssPx: 0.9,
112
+ minTextCssPx: 1.05,
113
+ minFilledAreaCssPx: 0.04
114
+ });
115
+ ```
116
+
117
+ XPS/DWFx `Glyphs` use embedded TrueType fonts when the browser allows `FontFace` loading. Very small text is skipped in adaptive overview mode and appears normally when zoomed in; this avoids unreadable annotation blocks in dense architectural sheets.
118
+
119
+ ## Three.js Integration
60
120
 
61
121
  ```ts
62
122
  import * as THREE from 'three';
@@ -69,7 +129,6 @@ if (page?.kind === 'w3d-model') {
69
129
  const group = createThreeGroupFromW3d(page, THREE, {
70
130
  showFeatureEdges: true,
71
131
  textureResolver(texture) {
72
- // Return a THREE.Texture if your app wants to bind DWFx texture resources.
73
132
  return undefined;
74
133
  }
75
134
  });
@@ -77,7 +136,7 @@ if (page?.kind === 'w3d-model') {
77
136
  }
78
137
  ```
79
138
 
80
- ## Local development
139
+ ## Local Development
81
140
 
82
141
  ```bash
83
142
  npm install
@@ -92,14 +151,15 @@ Then open:
92
151
  http://127.0.0.1:8080/
93
152
  ```
94
153
 
95
- ## Example set
154
+ ## Example Set
96
155
 
97
156
  The demo examples are listed in `examples/manifest.json` and are intentionally de-duplicated:
98
157
 
99
158
  | Example | Purpose |
100
159
  |---|---|
101
- | `robot-arm.dwfx` | 3D W3D/HSF eModel with shell meshes, scene tree, materials, textures, saved views |
102
- | `blocks-and-tables.dwf` | Binary WHIP!/W2D 2D ePlot sample |
160
+ | `autodesk-floor-plans.dwfx` | Multi-page architectural DWFx/XPS sample, defaulting to A03 First Floor Plan for WebGL XPS, embedded font, and thin-line overview validation |
161
+ | `robot-arm.dwfx` | 3D W3D/HSF eModel with shell meshes, scene tree, materials, textures, and saved views |
162
+ | `blocks-and-tables.dwf` | Binary WHIP!/W2D ePlot sample |
103
163
  | `minimal-xps.dwfx` | Small DWFx/XPS FixedPage sample |
104
164
  | `text-w2d.dwf` | Textual W2D smoke-test sample |
105
165
 
@@ -109,27 +169,24 @@ Run:
109
169
  npm run check:examples
110
170
  ```
111
171
 
112
- ## NPM publishing checklist
172
+ ## NPM Publishing Checklist
113
173
 
114
174
  ```bash
115
175
  npm run clean
176
+ npm run build
177
+ npm run validate:production
178
+ npm run check:package
116
179
  npm run publish:all
117
180
  ```
118
181
 
119
- `publish:all` builds once, validates the production examples, checks the package tarball, then publishes both `dwf-viewer` and `@flyfish-dev/dwf-viewer`. Add npm options when needed:
182
+ The package is published as both `dwf-viewer` and `@flyfish-dev/dwf-viewer`.
120
183
 
121
- ```bash
122
- npm run publish:all -- --dry-run
123
- npm run publish:all -- --otp=123456
124
- ```
125
-
126
- GitHub release publishing can use provenance through the included workflow and `NPM_TOKEN`.
127
-
128
- ## Cloudflare Pages demo
184
+ ## Cloudflare Pages Demo
129
185
 
130
186
  The repository includes `wrangler.toml` with:
131
187
 
132
188
  ```toml
189
+ name = "dwf-viewer-demo"
133
190
  pages_build_output_dir = "./demo-dist"
134
191
  ```
135
192
 
@@ -144,11 +201,10 @@ Root directory: /
144
201
  Direct upload:
145
202
 
146
203
  ```bash
147
- npm run build:demo
148
- npx wrangler pages deploy demo-dist
204
+ npm run deploy:pages
149
205
  ```
150
206
 
151
- `build:demo` produces a static directory containing only demo HTML/JS, `dist`, `public/dwfv-render.wasm`, `styles`, and the curated examples.
207
+ `build:demo` produces a static directory containing demo HTML/JS, a versioned `dist-v*` directory, `public/dwfv-render.wasm`, `styles`, and curated examples. Versioned dist assets prevent stale browser module caches after releases.
152
208
 
153
209
  ## Public API
154
210
 
@@ -159,6 +215,7 @@ openDwfDocument(input, options?)
159
215
  DwfViewer
160
216
  PageRenderer
161
217
  WebGlW2dBackend
218
+ WebGlXpsBackend
162
219
  ThreeW3dRenderer
163
220
  createThreeGroupFromW3d(page, THREE, options?)
164
221
  ```
@@ -168,6 +225,7 @@ Types:
168
225
  ```ts
169
226
  LoadedDwfDocument
170
227
  PageData
228
+ XpsPageData
171
229
  W3dPageData
172
230
  W3dModelData
173
231
  W3dMeshData
@@ -176,15 +234,20 @@ Diagnostic
176
234
  RenderStats
177
235
  ```
178
236
 
179
- ## Production behavior
237
+ ## Production Behavior
180
238
 
181
- The production validation target is strict for the bundled Robot Arm eModel:
239
+ The production validation target is strict for the bundled samples:
182
240
 
183
241
  ```text
242
+ Robot Arm:
184
243
  page kind: w3d-model
185
244
  meshes >= 30
186
245
  triangles >= 40,000
187
- page diagnostics: 0
246
+ non-info diagnostics: 0
247
+
248
+ Autodesk Floor Plans:
249
+ page kind: xps-fixed-page
250
+ pages >= 18
188
251
  non-info diagnostics: 0
189
252
  ```
190
253
 
@@ -194,10 +257,12 @@ Run:
194
257
  npm run validate:production
195
258
  ```
196
259
 
197
- ## Known boundaries
260
+ ## License
261
+
262
+ DWF Viewer is licensed under `AGPL-3.0-only`. See `LICENSE` and `NOTICE`.
198
263
 
199
- This is a pure frontend implementation, not Autodesk Design Review or HOOPS Exchange. The parser is intentionally fail-closed for unsupported historical HSF/W2D opcode semantics: it should show explicit diagnostics instead of silently drawing incorrect geometry. Current production coverage includes the core 2D/3D rendering paths needed by the bundled samples and the extension points for materials, textures, PMI, animation and selection tree metadata.
264
+ Commercial use, private forks, and second-development integrations must comply with the license and preserve attribution. Contributions are welcome through Issues and Pull Requests.
200
265
 
201
- ## License
266
+ ## Known Boundaries
202
267
 
203
- AGPL-3.0-only. See `LICENSE` and `NOTICE`.
268
+ This is a pure frontend implementation and is not affiliated with Autodesk Design Review or HOOPS Exchange. The parser is intentionally fail-closed for unsupported historical HSF/W2D opcode semantics: it should show explicit diagnostics instead of silently drawing guessed geometry. Current production coverage includes the core 2D/3D rendering paths needed by the bundled samples and extension points for materials, textures, PMI, animation, and selection tree metadata.
@@ -45,6 +45,11 @@ export interface InflateProvider {
45
45
  }
46
46
  export interface PageRenderOptions {
47
47
  pageIndex?: number;
48
+ lineWeightMode?: 'adaptive' | 'physical' | 'hairline';
49
+ minStrokeCssPx?: number;
50
+ maxOverviewStrokeCssPx?: number;
51
+ minTextCssPx?: number;
52
+ minFilledAreaCssPx?: number;
48
53
  preferWebgl?: boolean;
49
54
  preferWasm?: boolean;
50
55
  wasmUrl?: string;
@@ -53,7 +58,7 @@ export interface PageRenderOptions {
53
58
  maxCachedScenes?: number;
54
59
  }
55
60
  export interface RenderStats {
56
- backend: 'canvas2d' | 'wasm-raster' | 'webgl' | 'threejs-webgl' | 'image' | 'unsupported';
61
+ backend: 'canvas2d' | 'wasm-raster' | 'webgl' | 'webgl-xps' | 'threejs-webgl' | 'image' | 'unsupported';
57
62
  commands: number;
58
63
  warnings: Diagnostic[];
59
64
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export type { DwfViewerOptions, LoadOptions } from './viewer/DwfViewer.js';
6
6
  export { PageRenderer } from './render/PageRenderer.js';
7
7
  export { WasmRasterBackend } from './wasm/WasmRasterBackend.js';
8
8
  export { WebGlW2dBackend } from './render/WebGlW2dBackend.js';
9
+ export { WebGlXpsBackend } from './render/WebGlXpsBackend.js';
9
10
  export { ThreeW3dRenderer } from './render/ThreeW3dRenderer.js';
10
11
  export { createThreeGroupFromW3d } from './render/ThreeJsSceneAdapter.js';
11
12
  export type { LoadedDwfDocument, PageData, XpsPageData, W2dTextPageData, ImagePageData, UnsupportedPageData, W3dPageData, W3dModelData, W3dMeshData, W2dPrimitive } from './format/document.js';
package/dist/index.js CHANGED
@@ -5,5 +5,6 @@ export { DwfViewer } from './viewer/DwfViewer.js';
5
5
  export { PageRenderer } from './render/PageRenderer.js';
6
6
  export { WasmRasterBackend } from './wasm/WasmRasterBackend.js';
7
7
  export { WebGlW2dBackend } from './render/WebGlW2dBackend.js';
8
+ export { WebGlXpsBackend } from './render/WebGlXpsBackend.js';
8
9
  export { ThreeW3dRenderer } from './render/ThreeW3dRenderer.js';
9
10
  export { createThreeGroupFromW3d } from './render/ThreeJsSceneAdapter.js';
@@ -2,6 +2,11 @@ import { type RenderStats } from '../format/types.js';
2
2
  import type { LoadedDwfDocument } from '../format/document.js';
3
3
  export interface GenericRenderOptions {
4
4
  zoom?: number;
5
+ lineWeightMode?: 'adaptive' | 'physical' | 'hairline';
6
+ minStrokeCssPx?: number;
7
+ maxOverviewStrokeCssPx?: number;
8
+ minTextCssPx?: number;
9
+ minFilledAreaCssPx?: number;
5
10
  panX?: number;
6
11
  panY?: number;
7
12
  preferWebgl?: boolean;
@@ -30,6 +30,7 @@ export class PageRenderer {
30
30
  return this.renderUnsupported(page, canvas, options);
31
31
  }
32
32
  dispose() {
33
+ this.xps?.dispose();
33
34
  this.w2d?.dispose();
34
35
  this.w3d?.dispose();
35
36
  }
@@ -1,6 +1,7 @@
1
1
  import { type RenderStats } from '../format/types.js';
2
2
  import type { W2dTextPageData } from '../format/document.js';
3
- export interface W2dRenderOptions {
3
+ import { type CadLineStyleOptions } from './cadLineStyle.js';
4
+ export interface W2dRenderOptions extends CadLineStyleOptions {
4
5
  zoom?: number;
5
6
  panX?: number;
6
7
  panY?: number;
@@ -1,6 +1,7 @@
1
1
  import { actionableDiagnostics, diag } from '../format/types.js';
2
2
  import { applyPathToCanvas, flattenPath } from './xpsPath.js';
3
3
  import { multiplyMatrix, parseBrushColor, transformPoint } from './style.js';
4
+ import { adaptiveStrokeUserWidth, canvasDpr, estimateMatrixScale, shouldDrawTextByPixelSize } from './cadLineStyle.js';
4
5
  import { matrixForW2d } from './viewport.js';
5
6
  import { WasmRasterBackend } from '../wasm/WasmRasterBackend.js';
6
7
  import { WebGlW2dBackend } from './WebGlW2dBackend.js';
@@ -30,6 +31,7 @@ export class W2dRenderer {
30
31
  if (!ctx)
31
32
  throw new Error('CanvasRenderingContext2D is not available.');
32
33
  const pageMatrix = matrixForW2d(page, canvas.width, canvas.height, options.zoom, options.panX, options.panY);
34
+ const runtime = { dpr: canvasDpr(canvas), zoom: options.zoom ?? 1 };
33
35
  let commands = 0;
34
36
  if (options.preferWasm) {
35
37
  try {
@@ -37,11 +39,11 @@ export class W2dRenderer {
37
39
  await this.wasm.init();
38
40
  this.wasm.begin(canvas.width, canvas.height, bg);
39
41
  for (const p of page.primitives)
40
- commands += this.drawPrimitiveWasm(p, pageMatrix);
42
+ commands += this.drawPrimitiveWasm(p, pageMatrix, options, runtime);
41
43
  ctx.setTransform(1, 0, 0, 1, 0, 0);
42
44
  ctx.putImageData(this.wasm.toImageData(), 0, 0);
43
45
  for (const p of page.primitives.filter(p => p.type === 'text'))
44
- commands += this.drawPrimitiveCanvas(ctx, p, pageMatrix);
46
+ commands += this.drawPrimitiveCanvas(ctx, p, pageMatrix, options, runtime);
45
47
  return { backend: 'wasm-raster', commands, warnings };
46
48
  }
47
49
  catch (err) {
@@ -54,7 +56,7 @@ export class W2dRenderer {
54
56
  ctx.fillRect(0, 0, canvas.width, canvas.height);
55
57
  ctx.restore();
56
58
  for (const p of page.primitives)
57
- commands += this.drawPrimitiveCanvas(ctx, p, pageMatrix);
59
+ commands += this.drawPrimitiveCanvas(ctx, p, pageMatrix, options, runtime);
58
60
  return { backend: 'canvas2d', commands, warnings };
59
61
  }
60
62
  dispose() {
@@ -70,14 +72,14 @@ export class W2dRenderer {
70
72
  }
71
73
  return this.webgl;
72
74
  }
73
- drawPrimitiveCanvas(ctx, p, pageMatrix) {
75
+ drawPrimitiveCanvas(ctx, p, pageMatrix, options, runtime) {
74
76
  const matrix = multiplyMatrix(pageMatrix, p.matrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
75
77
  const stroke = parseBrushColor(p.stroke ?? '#000000') ?? '#000000';
76
78
  const fill = parseBrushColor(p.fill);
77
79
  if (p.type === 'text') {
78
80
  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
+ const screenSize = Math.max(1, Math.abs((p.size ?? 12) * estimateScale(matrix)));
82
+ if (!shouldDrawTextByPixelSize(p.size ?? 12, matrix, options, runtime) || x > ctx.canvas.width + 64 || y > ctx.canvas.height + 64 || x < -ctx.canvas.width || y < -ctx.canvas.height)
81
83
  return 0;
82
84
  ctx.save();
83
85
  ctx.setTransform(1, 0, 0, 1, 0, 0);
@@ -91,7 +93,7 @@ export class W2dRenderer {
91
93
  }
92
94
  ctx.save();
93
95
  ctx.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
94
- ctx.lineWidth = Math.max(0.1, (p.lineWidth ?? 1));
96
+ ctx.lineWidth = adaptiveStrokeUserWidth(p.lineWidth ?? 1, matrix, options, runtime);
95
97
  if (p.type === 'polyline') {
96
98
  if (p.points.length >= 4) {
97
99
  ctx.beginPath();
@@ -144,25 +146,26 @@ export class W2dRenderer {
144
146
  ctx.restore();
145
147
  return 1;
146
148
  }
147
- drawPrimitiveWasm(p, pageMatrix) {
149
+ drawPrimitiveWasm(p, pageMatrix, options, runtime) {
148
150
  if (!this.wasm || p.type === 'text')
149
151
  return 0;
150
152
  const matrix = multiplyMatrix(pageMatrix, p.matrix ?? { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
151
- const scale = estimateScale(matrix);
153
+ const scale = estimateMatrixScale(matrix);
154
+ const screenStroke = (w) => adaptiveStrokeUserWidth(w ?? 1, matrix, options, runtime) * scale;
152
155
  if (p.type === 'polyline') {
153
- this.wasm.drawPolyline(p.points, matrix, parseBrushColor(p.stroke ?? '#000000'), (p.lineWidth ?? 1) * scale);
156
+ this.wasm.drawPolyline(p.points, matrix, parseBrushColor(p.stroke ?? '#000000'), screenStroke(p.lineWidth));
154
157
  }
155
158
  else if (p.type === 'polygon') {
156
159
  this.wasm.drawPolygon(p.points, matrix, parseBrushColor(p.fill));
157
160
  if (p.stroke)
158
- this.wasm.drawPolyline(closePoints(p.points), matrix, parseBrushColor(p.stroke), (p.lineWidth ?? 1) * scale);
161
+ this.wasm.drawPolyline(closePoints(p.points), matrix, parseBrushColor(p.stroke), screenStroke(p.lineWidth));
159
162
  }
160
163
  else if (p.type === 'rect') {
161
164
  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
165
  if (p.fill)
163
166
  this.wasm.drawPolygon(pts, matrix, parseBrushColor(p.fill));
164
167
  if (p.stroke)
165
- this.wasm.drawPolyline(pts, matrix, parseBrushColor(p.stroke), (p.lineWidth ?? 1) * scale);
168
+ this.wasm.drawPolyline(pts, matrix, parseBrushColor(p.stroke), screenStroke(p.lineWidth));
166
169
  }
167
170
  else if (p.type === 'path') {
168
171
  const subs = flattenPath(p.commands, 0.5);
@@ -174,7 +177,7 @@ export class W2dRenderer {
174
177
  this.wasm.drawPolygon(s.points, matrix, fill);
175
178
  if (stroke)
176
179
  for (const s of subs)
177
- this.wasm.drawPolyline(s.points, matrix, stroke, (p.lineWidth ?? 1) * scale);
180
+ this.wasm.drawPolyline(s.points, matrix, stroke, screenStroke(p.lineWidth));
178
181
  }
179
182
  return 1;
180
183
  }
@@ -1,6 +1,7 @@
1
1
  import type { Diagnostic } from '../format/types.js';
2
2
  import type { W2dTextPageData } from '../format/document.js';
3
- export interface WebGlW2dRenderOptions {
3
+ import { type CadLineStyleOptions } from './cadLineStyle.js';
4
+ export interface WebGlW2dRenderOptions extends CadLineStyleOptions {
4
5
  zoom?: number;
5
6
  panX?: number;
6
7
  panY?: number;
@@ -1,6 +1,7 @@
1
1
  import { diag } from '../format/types.js';
2
2
  import { flattenPath } from './xpsPath.js';
3
3
  import { colorToRgba32, multiplyMatrix, transformPoint } from './style.js';
4
+ import { adaptiveStrokeUserWidth, canvasDpr, estimateMatrixScale } from './cadLineStyle.js';
4
5
  import { matrixForW2d } from './viewport.js';
5
6
  const VERTEX_STRIDE = 12;
6
7
  const DEFAULT_MAX_GPU_CACHE_BYTES = 96 * 1024 * 1024;
@@ -38,11 +39,13 @@ export class WebGlW2dBackend {
38
39
  return { commands: 0, warnings, gpuBytes: this.gpuBytes, vertexCount: 0, textCount: 0, cacheHit: true };
39
40
  }
40
41
  this.resize(targetCanvas.width, targetCanvas.height);
41
- const key = sceneKey(page);
42
+ const pageMatrix = matrixForW2d(page, this.canvas.width, this.canvas.height, options.zoom, options.panX, options.panY);
43
+ const runtime = { dpr: canvasDpr(targetCanvas), zoom: options.zoom ?? 1 };
44
+ const key = sceneKey(page, pageMatrix, options);
42
45
  let scene = this.scenes.get(key);
43
46
  const cacheHit = !!scene;
44
47
  if (!scene) {
45
- scene = this.compileScene(page, key, options);
48
+ scene = this.compileScene(page, key, options, pageMatrix, runtime);
46
49
  this.scenes.set(key, scene);
47
50
  this.gpuBytes += scene.gpuBytes;
48
51
  this.evictIfNeeded(options);
@@ -59,7 +62,6 @@ export class WebGlW2dBackend {
59
62
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
60
63
  gl.clearColor(bg[0], bg[1], bg[2], bg[3]);
61
64
  gl.clear(gl.COLOR_BUFFER_BIT);
62
- const pageMatrix = matrixForW2d(page, this.canvas.width, this.canvas.height, options.zoom, options.panX, options.panY);
63
65
  gl.useProgram(this.program);
64
66
  gl.bindBuffer(gl.ARRAY_BUFFER, scene.buffer);
65
67
  gl.enableVertexAttribArray(this.aPos);
@@ -103,7 +105,7 @@ export class WebGlW2dBackend {
103
105
  this.canvas.height = height;
104
106
  }
105
107
  }
106
- compileScene(page, key, options) {
108
+ compileScene(page, key, options, pageMatrix, runtime) {
107
109
  const writer = new VertexWriter();
108
110
  let primitiveCount = 0;
109
111
  let textCount = 0;
@@ -113,7 +115,7 @@ export class WebGlW2dBackend {
113
115
  continue;
114
116
  }
115
117
  primitiveCount++;
116
- appendPrimitive(writer, p);
118
+ appendPrimitive(writer, p, pageMatrix, options, runtime);
117
119
  }
118
120
  const bufferBytes = writer.byteLength;
119
121
  const maxBytes = options.maxGpuCacheBytes ?? DEFAULT_MAX_GPU_CACHE_BYTES;
@@ -187,8 +189,10 @@ export class WebGlW2dBackend {
187
189
  }
188
190
  }
189
191
  const IDENTITY_MATRIX = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
190
- function sceneKey(page) {
191
- return `${page.id}|${page.sourcePath}|${page.primitives.length}`;
192
+ function sceneKey(page, pageMatrix, options) {
193
+ const mode = options.lineWeightMode ?? 'adaptive';
194
+ const scaleBucket = mode === 'physical' ? 'physical' : String(Math.round(Math.log2(Math.max(1e-12, estimateMatrixScale(pageMatrix))) * 8));
195
+ return `${page.id}|${page.sourcePath}|${page.primitives.length}|lw:${mode}:${scaleBucket}`;
192
196
  }
193
197
  class VertexWriter {
194
198
  constructor() {
@@ -226,10 +230,11 @@ class VertexWriter {
226
230
  this.view = new DataView(this.buffer);
227
231
  }
228
232
  }
229
- function appendPrimitive(writer, p) {
233
+ function appendPrimitive(writer, p, pageMatrix, options, runtime) {
230
234
  const m = p.matrix ?? IDENTITY_MATRIX;
231
235
  const matrixScale = estimateScale(m);
232
- const lineWidth = Math.max(0.1, (p.lineWidth ?? 1) * matrixScale);
236
+ const fullMatrix = multiplyMatrix(pageMatrix, m);
237
+ const lineWidth = Math.max(0.01, adaptiveStrokeUserWidth(p.lineWidth ?? 1, fullMatrix, options, runtime) * matrixScale);
233
238
  if (p.type === 'polyline') {
234
239
  const color = rgbaBytes(p.stroke ?? '#000000');
235
240
  appendPolyline(writer, transformPointsArray(p.points, m), lineWidth, color);
@@ -0,0 +1,38 @@
1
+ import type { Diagnostic } from '../format/types.js';
2
+ import type { XpsPageData } from '../format/document.js';
3
+ import { type CadLineStyleOptions } from './cadLineStyle.js';
4
+ export interface WebGlXpsRenderOptions extends CadLineStyleOptions {
5
+ zoom?: number;
6
+ panX?: number;
7
+ panY?: number;
8
+ background?: string;
9
+ maxGpuCacheBytes?: number;
10
+ maxCachedScenes?: number;
11
+ compositeToTarget?: boolean;
12
+ }
13
+ export interface WebGlXpsRenderResult {
14
+ commands: number;
15
+ warnings: Diagnostic[];
16
+ gpuBytes: number;
17
+ vertexCount: number;
18
+ pathCount: number;
19
+ cacheHit: boolean;
20
+ }
21
+ export declare class WebGlXpsBackend {
22
+ private readonly canvas;
23
+ private readonly gl;
24
+ private readonly program;
25
+ private readonly aPos;
26
+ private readonly aColor;
27
+ private readonly uMatrix;
28
+ private readonly uViewport;
29
+ private readonly scenes;
30
+ private gpuBytes;
31
+ private tick;
32
+ constructor(canvas?: HTMLCanvasElement);
33
+ render(page: XpsPageData, root: Element, targetCanvas: HTMLCanvasElement, options?: WebGlXpsRenderOptions): WebGlXpsRenderResult;
34
+ dispose(): void;
35
+ private resize;
36
+ private compileScene;
37
+ private evictIfNeeded;
38
+ }