@flyfish-dev/dwf-viewer 0.5.1 → 0.6.1
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 +21 -8
- package/PRODUCTION_3D_NOTES.md +4 -0
- package/README.md +80 -63
- package/dist/format/types.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/render/PageRenderer.js +1 -0
- package/dist/render/WebGlXpsBackend.d.ts +38 -0
- package/dist/render/WebGlXpsBackend.js +541 -0
- package/dist/render/XpsRenderer.d.ts +10 -0
- package/dist/render/XpsRenderer.js +118 -9
- package/dist/viewer/DwfViewer.d.ts +2 -0
- package/dist/viewer/DwfViewer.js +51 -35
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
- Changed the public demo default sample to `Blocks and Tables · Binary W2D DWF` for a faster and clearer first load.
|
|
6
|
+
- Kept Autodesk Floor Plans as an optional XPS WebGL validation sample instead of loading the large DWFx file by default.
|
|
7
|
+
- Updated demo asset versioning to `dist-v0.6.1`.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
- Added WebGL-accelerated XPS/DWFx 2D vector rendering through `WebGlXpsBackend`.
|
|
12
|
+
- Added XPS ODTTF embedded font deobfuscation/loading for CAD-like text appearance.
|
|
13
|
+
- Tuned demo defaults for dense CAD overview linework and lower DPR memory pressure.
|
|
14
|
+
- Added curated Autodesk floor-plan DWFx demo entry with default A03 sheet.
|
|
15
|
+
- Coalesced viewer render requests to avoid zoom/pan render pile-ups.
|
|
16
|
+
- Kept Cloudflare demo assets versioned under `dist-v0.6.0` to prevent stale browser module caches.
|
|
17
|
+
|
|
3
18
|
## 0.5.1
|
|
4
19
|
|
|
5
|
-
- Published as `dwf-viewer` and `@flyfish-dev/dwf-viewer` with AGPL-3.0-only package metadata.
|
|
6
20
|
- Added CAD adaptive line-weight rendering for XPS FixedPage, W2D Canvas/WASM, and W2D WebGL paths.
|
|
7
|
-
- Added overview text LOD culling to avoid
|
|
21
|
+
- Added overview text LOD culling to avoid dense annotation blobs at fit-to-page while preserving text when zoomed in.
|
|
8
22
|
- Added embedded XPS TrueType font loading for Glyphs when browsers support the FontFace API.
|
|
9
23
|
- Added demo line-weight mode selector: CAD adaptive, hairline, physical.
|
|
24
|
+
- Versioned demo dist assets under `dist-v0.5.1` to prevent stale browser module caches.
|
|
10
25
|
|
|
11
26
|
## 0.5.0
|
|
12
27
|
|
|
13
|
-
- Prepared repository for public npm release and Cloudflare Pages demo deployment.
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- Switched the project license to `AGPL-3.0-only` and added `NOTICE`.
|
|
17
|
-
- Normalized GitHub repository metadata for `flyfish-dev/dwf-viewer`.
|
|
28
|
+
- Prepared the repository for public npm release and Cloudflare Pages demo deployment.
|
|
29
|
+
- Published normalized packages as `dwf-viewer` and `@flyfish-dev/dwf-viewer`.
|
|
30
|
+
- Adopted `AGPL-3.0-only` with `NOTICE` for strict open-source distribution.
|
|
18
31
|
- Added curated, de-duplicated demo example manifest.
|
|
19
32
|
- Removed production-success info diagnostics from W3D/eModel pages so the Robot Arm demo renders with zero page diagnostics.
|
|
20
|
-
- Added static demo build pipeline
|
|
33
|
+
- Added static demo build pipeline, package validation scripts, CI, and publish helpers.
|
package/PRODUCTION_3D_NOTES.md
CHANGED
|
@@ -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,31 +1,36 @@
|
|
|
1
1
|
# DWF Viewer
|
|
2
2
|
|
|
3
|
-
Pure frontend DWF/DWFx viewer for browsers. It parses DWF/DWFx packages locally
|
|
4
|
-
|
|
5
|
-
## Status
|
|
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`.
|
|
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.
|
|
8
4
|
|
|
9
5
|
## Links
|
|
10
6
|
|
|
11
|
-
|
|
|
7
|
+
| Entry | URL |
|
|
12
8
|
|---|---|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
| npm
|
|
17
|
-
|
|
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.1`
|
|
15
|
+
|
|
16
|
+
## Why
|
|
18
17
|
|
|
19
|
-
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Supported Paths
|
|
20
23
|
|
|
21
24
|
| Format / content | Status |
|
|
22
25
|
|---|---|
|
|
23
26
|
| DWF 6+ ZIP container | Supported |
|
|
24
27
|
| DWFx / OPC package | Supported |
|
|
25
|
-
| XPS FixedPage 2D sheets | Supported common subset |
|
|
28
|
+
| XPS FixedPage 2D sheets | Supported common subset with WebGL vector acceleration and Canvas text/image overlay |
|
|
26
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 |
|
|
27
31
|
| W3D/HSF 3D eModel shell geometry | Supported: uncompressed, CS_TRIVIAL, and Edgebreaker shell meshes |
|
|
28
32
|
| Three.js adapter | Supported |
|
|
33
|
+
| Built-in WebGL 3D renderer | Supported through `DwfViewer` |
|
|
29
34
|
| WASM raster fallback | Supported for 2D vector rasterization |
|
|
30
35
|
| eModel metadata | Materials, textures, scene tree, saved views, PMI/animation data containers |
|
|
31
36
|
|
|
@@ -41,7 +46,7 @@ npm install @flyfish-dev/dwf-viewer three
|
|
|
41
46
|
|
|
42
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.
|
|
43
48
|
|
|
44
|
-
## Basic
|
|
49
|
+
## Basic Browser Usage
|
|
45
50
|
|
|
46
51
|
```ts
|
|
47
52
|
import 'dwf-viewer/styles.css';
|
|
@@ -51,14 +56,15 @@ const viewer = new DwfViewer(document.getElementById('viewer')!, {
|
|
|
51
56
|
wasmUrl: '/dwfv-render.wasm',
|
|
52
57
|
preferWebgl: true,
|
|
53
58
|
preferWasm: true,
|
|
54
|
-
maxDevicePixelRatio:
|
|
55
|
-
maxCanvasPixels:
|
|
56
|
-
maxGpuCacheBytes:
|
|
57
|
-
maxCachedScenes:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
62
68
|
});
|
|
63
69
|
|
|
64
70
|
await viewer.load(file);
|
|
@@ -70,32 +76,47 @@ Copy the WASM asset from the package into your public assets directory:
|
|
|
70
76
|
cp node_modules/dwf-viewer/public/dwfv-render.wasm public/dwfv-render.wasm
|
|
71
77
|
```
|
|
72
78
|
|
|
73
|
-
|
|
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.x` adds WebGL-accelerated XPS/DWFx 2D vector rendering through `WebGlXpsBackend`, alongside the existing W2D WebGL path.
|
|
74
88
|
|
|
75
|
-
The
|
|
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.
|
|
76
96
|
|
|
77
97
|
Available modes:
|
|
78
98
|
|
|
79
99
|
| Mode | Behavior |
|
|
80
100
|
|---|---|
|
|
81
101
|
| `adaptive` | Default. Overview thin-line rendering with zoom-aware recovery of source line weights. |
|
|
82
|
-
| `hairline` | Force
|
|
83
|
-
| `physical` | Preserve source stroke widths
|
|
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. |
|
|
84
104
|
|
|
85
105
|
Related options:
|
|
86
106
|
|
|
87
107
|
```ts
|
|
88
108
|
new DwfViewer(el, {
|
|
89
109
|
lineWeightMode: 'adaptive',
|
|
90
|
-
minStrokeCssPx: 0.
|
|
91
|
-
maxOverviewStrokeCssPx:
|
|
92
|
-
minTextCssPx:
|
|
110
|
+
minStrokeCssPx: 0.42,
|
|
111
|
+
maxOverviewStrokeCssPx: 0.9,
|
|
112
|
+
minTextCssPx: 1.05,
|
|
113
|
+
minFilledAreaCssPx: 0.04
|
|
93
114
|
});
|
|
94
115
|
```
|
|
95
116
|
|
|
96
|
-
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
|
|
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.
|
|
97
118
|
|
|
98
|
-
## Three.js
|
|
119
|
+
## Three.js Integration
|
|
99
120
|
|
|
100
121
|
```ts
|
|
101
122
|
import * as THREE from 'three';
|
|
@@ -108,7 +129,6 @@ if (page?.kind === 'w3d-model') {
|
|
|
108
129
|
const group = createThreeGroupFromW3d(page, THREE, {
|
|
109
130
|
showFeatureEdges: true,
|
|
110
131
|
textureResolver(texture) {
|
|
111
|
-
// Return a THREE.Texture if your app wants to bind DWFx texture resources.
|
|
112
132
|
return undefined;
|
|
113
133
|
}
|
|
114
134
|
});
|
|
@@ -116,7 +136,7 @@ if (page?.kind === 'w3d-model') {
|
|
|
116
136
|
}
|
|
117
137
|
```
|
|
118
138
|
|
|
119
|
-
## Local
|
|
139
|
+
## Local Development
|
|
120
140
|
|
|
121
141
|
```bash
|
|
122
142
|
npm install
|
|
@@ -131,14 +151,15 @@ Then open:
|
|
|
131
151
|
http://127.0.0.1:8080/
|
|
132
152
|
```
|
|
133
153
|
|
|
134
|
-
## Example
|
|
154
|
+
## Example Set
|
|
135
155
|
|
|
136
156
|
The demo examples are listed in `examples/manifest.json` and are intentionally de-duplicated:
|
|
137
157
|
|
|
138
158
|
| Example | Purpose |
|
|
139
159
|
|---|---|
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
160
|
+
| `blocks-and-tables.dwf` | Default demo. Binary WHIP!/W2D ePlot sample with fast loading and a clear first impression |
|
|
161
|
+
| `autodesk-floor-plans.dwfx` | Multi-page architectural DWFx/XPS sample, using A03 First Floor Plan for WebGL XPS, embedded font, and thin-line overview validation |
|
|
162
|
+
| `robot-arm.dwfx` | 3D W3D/HSF eModel with shell meshes, scene tree, materials, textures, and saved views |
|
|
142
163
|
| `minimal-xps.dwfx` | Small DWFx/XPS FixedPage sample |
|
|
143
164
|
| `text-w2d.dwf` | Textual W2D smoke-test sample |
|
|
144
165
|
|
|
@@ -148,30 +169,19 @@ Run:
|
|
|
148
169
|
npm run check:examples
|
|
149
170
|
```
|
|
150
171
|
|
|
151
|
-
## NPM
|
|
172
|
+
## NPM Publishing Checklist
|
|
152
173
|
|
|
153
174
|
```bash
|
|
154
175
|
npm run clean
|
|
155
|
-
npm run
|
|
176
|
+
npm run build
|
|
177
|
+
npm run validate:production
|
|
178
|
+
npm run check:package
|
|
156
179
|
npm run publish:all
|
|
157
180
|
```
|
|
158
181
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
npm run publish:all -- --dry-run
|
|
163
|
-
npm run publish:all -- --otp=123456
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
GitHub release publishing can use provenance through the included workflow and `NPM_TOKEN`.
|
|
167
|
-
|
|
168
|
-
## Cloudflare Pages demo
|
|
182
|
+
The package is published as both `dwf-viewer` and `@flyfish-dev/dwf-viewer`.
|
|
169
183
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
```text
|
|
173
|
-
https://dwf-viewer-demo.pages.dev/
|
|
174
|
-
```
|
|
184
|
+
## Cloudflare Pages Demo
|
|
175
185
|
|
|
176
186
|
The repository includes `wrangler.toml` with:
|
|
177
187
|
|
|
@@ -194,9 +204,7 @@ Direct upload:
|
|
|
194
204
|
npm run deploy:pages
|
|
195
205
|
```
|
|
196
206
|
|
|
197
|
-
`build:demo` produces a static directory containing
|
|
198
|
-
|
|
199
|
-
GitHub Actions builds the demo for every pull request. On pushes to `main`, it deploys to Cloudflare Pages when the repository secrets `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are present.
|
|
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.
|
|
200
208
|
|
|
201
209
|
## Public API
|
|
202
210
|
|
|
@@ -207,6 +215,7 @@ openDwfDocument(input, options?)
|
|
|
207
215
|
DwfViewer
|
|
208
216
|
PageRenderer
|
|
209
217
|
WebGlW2dBackend
|
|
218
|
+
WebGlXpsBackend
|
|
210
219
|
ThreeW3dRenderer
|
|
211
220
|
createThreeGroupFromW3d(page, THREE, options?)
|
|
212
221
|
```
|
|
@@ -216,6 +225,7 @@ Types:
|
|
|
216
225
|
```ts
|
|
217
226
|
LoadedDwfDocument
|
|
218
227
|
PageData
|
|
228
|
+
XpsPageData
|
|
219
229
|
W3dPageData
|
|
220
230
|
W3dModelData
|
|
221
231
|
W3dMeshData
|
|
@@ -224,15 +234,20 @@ Diagnostic
|
|
|
224
234
|
RenderStats
|
|
225
235
|
```
|
|
226
236
|
|
|
227
|
-
## Production
|
|
237
|
+
## Production Behavior
|
|
228
238
|
|
|
229
|
-
The production validation target is strict for the bundled
|
|
239
|
+
The production validation target is strict for the bundled samples:
|
|
230
240
|
|
|
231
241
|
```text
|
|
242
|
+
Robot Arm:
|
|
232
243
|
page kind: w3d-model
|
|
233
244
|
meshes >= 30
|
|
234
245
|
triangles >= 40,000
|
|
235
|
-
|
|
246
|
+
non-info diagnostics: 0
|
|
247
|
+
|
|
248
|
+
Autodesk Floor Plans:
|
|
249
|
+
page kind: xps-fixed-page
|
|
250
|
+
pages >= 18
|
|
236
251
|
non-info diagnostics: 0
|
|
237
252
|
```
|
|
238
253
|
|
|
@@ -242,10 +257,12 @@ Run:
|
|
|
242
257
|
npm run validate:production
|
|
243
258
|
```
|
|
244
259
|
|
|
245
|
-
##
|
|
260
|
+
## License
|
|
246
261
|
|
|
247
|
-
|
|
262
|
+
DWF Viewer is licensed under `AGPL-3.0-only`. See `LICENSE` and `NOTICE`.
|
|
248
263
|
|
|
249
|
-
|
|
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.
|
|
265
|
+
|
|
266
|
+
## Known Boundaries
|
|
250
267
|
|
|
251
|
-
|
|
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.
|
package/dist/format/types.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface PageRenderOptions {
|
|
|
58
58
|
maxCachedScenes?: number;
|
|
59
59
|
}
|
|
60
60
|
export interface RenderStats {
|
|
61
|
-
backend: 'canvas2d' | 'wasm-raster' | 'webgl' | 'threejs-webgl' | 'image' | 'unsupported';
|
|
61
|
+
backend: 'canvas2d' | 'wasm-raster' | 'webgl' | 'webgl-xps' | 'threejs-webgl' | 'image' | 'unsupported';
|
|
62
62
|
commands: number;
|
|
63
63
|
warnings: Diagnostic[];
|
|
64
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';
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { diag } from '../format/types.js';
|
|
2
|
+
import { childElements, getAttr, localName, parseNumberList } from '../format/util.js';
|
|
3
|
+
import { flattenPath, parsePathData } from './xpsPath.js';
|
|
4
|
+
import { colorToRgba32, multiplyMatrix, parseBrushColor, parseMatrix, transformPoint } from './style.js';
|
|
5
|
+
import { adaptiveStrokeUserWidth, canvasDpr, estimateMatrixScale, shouldDrawFilledBounds } from './cadLineStyle.js';
|
|
6
|
+
import { fitPageMatrix } from './viewport.js';
|
|
7
|
+
const VERTEX_STRIDE = 12;
|
|
8
|
+
const DEFAULT_MAX_GPU_CACHE_BYTES = 128 * 1024 * 1024;
|
|
9
|
+
const DEFAULT_MAX_CACHED_SCENES = 4;
|
|
10
|
+
const IDENTITY_MATRIX = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
11
|
+
export class WebGlXpsBackend {
|
|
12
|
+
constructor(canvas) {
|
|
13
|
+
this.scenes = new Map();
|
|
14
|
+
this.gpuBytes = 0;
|
|
15
|
+
this.tick = 0;
|
|
16
|
+
this.canvas = canvas ?? document.createElement('canvas');
|
|
17
|
+
const gl = this.canvas.getContext('webgl', {
|
|
18
|
+
alpha: false,
|
|
19
|
+
antialias: true,
|
|
20
|
+
depth: false,
|
|
21
|
+
stencil: false,
|
|
22
|
+
preserveDrawingBuffer: true,
|
|
23
|
+
powerPreference: 'high-performance'
|
|
24
|
+
});
|
|
25
|
+
if (!gl)
|
|
26
|
+
throw new Error('WebGLRenderingContext is not available.');
|
|
27
|
+
this.gl = gl;
|
|
28
|
+
this.program = createProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
|
|
29
|
+
this.aPos = gl.getAttribLocation(this.program, 'a_pos');
|
|
30
|
+
this.aColor = gl.getAttribLocation(this.program, 'a_color');
|
|
31
|
+
const matrix = gl.getUniformLocation(this.program, 'u_matrix');
|
|
32
|
+
const viewport = gl.getUniformLocation(this.program, 'u_viewport');
|
|
33
|
+
if (this.aPos < 0 || this.aColor < 0 || !matrix || !viewport)
|
|
34
|
+
throw new Error('Failed to resolve WebGL shader locations.');
|
|
35
|
+
this.uMatrix = matrix;
|
|
36
|
+
this.uViewport = viewport;
|
|
37
|
+
}
|
|
38
|
+
render(page, root, targetCanvas, options = {}) {
|
|
39
|
+
const warnings = [];
|
|
40
|
+
if (targetCanvas.width <= 0 || targetCanvas.height <= 0) {
|
|
41
|
+
return { commands: 0, warnings, gpuBytes: this.gpuBytes, vertexCount: 0, pathCount: 0, cacheHit: true };
|
|
42
|
+
}
|
|
43
|
+
this.resize(targetCanvas.width, targetCanvas.height);
|
|
44
|
+
const pageMatrix = fitPageMatrix({
|
|
45
|
+
canvasWidth: this.canvas.width,
|
|
46
|
+
canvasHeight: this.canvas.height,
|
|
47
|
+
pageWidth: page.width,
|
|
48
|
+
pageHeight: page.height,
|
|
49
|
+
zoom: options.zoom,
|
|
50
|
+
panX: options.panX,
|
|
51
|
+
panY: options.panY
|
|
52
|
+
});
|
|
53
|
+
const runtime = { dpr: canvasDpr(targetCanvas), zoom: options.zoom ?? 1 };
|
|
54
|
+
const key = sceneKey(page, pageMatrix, options);
|
|
55
|
+
let scene = this.scenes.get(key);
|
|
56
|
+
const cacheHit = !!scene;
|
|
57
|
+
if (!scene) {
|
|
58
|
+
const vectors = collectVectorPaths(root);
|
|
59
|
+
scene = this.compileScene(page, key, vectors, pageMatrix, options, runtime);
|
|
60
|
+
this.scenes.set(key, scene);
|
|
61
|
+
this.gpuBytes += scene.gpuBytes;
|
|
62
|
+
this.evictIfNeeded(options);
|
|
63
|
+
scene = this.scenes.get(key) ?? scene;
|
|
64
|
+
}
|
|
65
|
+
scene.lastUsed = ++this.tick;
|
|
66
|
+
const gl = this.gl;
|
|
67
|
+
const bg = rgba01(options.background ?? '#ffffff');
|
|
68
|
+
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
69
|
+
gl.disable(gl.DEPTH_TEST);
|
|
70
|
+
gl.disable(gl.CULL_FACE);
|
|
71
|
+
gl.enable(gl.BLEND);
|
|
72
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
73
|
+
gl.clearColor(bg[0], bg[1], bg[2], bg[3]);
|
|
74
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
75
|
+
gl.useProgram(this.program);
|
|
76
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, scene.buffer);
|
|
77
|
+
gl.enableVertexAttribArray(this.aPos);
|
|
78
|
+
gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, VERTEX_STRIDE, 0);
|
|
79
|
+
gl.enableVertexAttribArray(this.aColor);
|
|
80
|
+
gl.vertexAttribPointer(this.aColor, 4, gl.UNSIGNED_BYTE, true, VERTEX_STRIDE, 8);
|
|
81
|
+
gl.uniform4f(this.uMatrix, pageMatrix.a, pageMatrix.b, pageMatrix.c, pageMatrix.d);
|
|
82
|
+
gl.uniform4f(this.uViewport, pageMatrix.e, pageMatrix.f, this.canvas.width, this.canvas.height);
|
|
83
|
+
gl.drawArrays(gl.TRIANGLES, 0, scene.vertexCount);
|
|
84
|
+
if (options.compositeToTarget ?? true) {
|
|
85
|
+
const ctx = targetCanvas.getContext('2d');
|
|
86
|
+
if (!ctx)
|
|
87
|
+
throw new Error('CanvasRenderingContext2D is not available for WebGL compositing.');
|
|
88
|
+
ctx.save();
|
|
89
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
90
|
+
ctx.clearRect(0, 0, targetCanvas.width, targetCanvas.height);
|
|
91
|
+
ctx.drawImage(this.canvas, 0, 0);
|
|
92
|
+
ctx.restore();
|
|
93
|
+
}
|
|
94
|
+
if (scene.vertexCount === 0) {
|
|
95
|
+
warnings.push(diag('warning', 'WEBGL_XPS_EMPTY_SCENE', 'WebGL XPS scene contained no drawable vector geometry; Canvas/WASM fallback may be required.', page.sourcePath));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
commands: scene.pathCount,
|
|
99
|
+
warnings,
|
|
100
|
+
gpuBytes: this.gpuBytes,
|
|
101
|
+
vertexCount: scene.vertexCount,
|
|
102
|
+
pathCount: scene.pathCount,
|
|
103
|
+
cacheHit
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
dispose() {
|
|
107
|
+
for (const scene of this.scenes.values())
|
|
108
|
+
this.gl.deleteBuffer(scene.buffer);
|
|
109
|
+
this.scenes.clear();
|
|
110
|
+
this.gpuBytes = 0;
|
|
111
|
+
}
|
|
112
|
+
resize(width, height) {
|
|
113
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
114
|
+
this.canvas.width = width;
|
|
115
|
+
this.canvas.height = height;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
compileScene(page, key, vectors, pageMatrix, options, runtime) {
|
|
119
|
+
const writer = new VertexWriter();
|
|
120
|
+
let pathCount = 0;
|
|
121
|
+
for (const vector of vectors) {
|
|
122
|
+
pathCount++;
|
|
123
|
+
appendVectorPath(writer, vector, pageMatrix, options, runtime);
|
|
124
|
+
}
|
|
125
|
+
const bufferBytes = writer.byteLength;
|
|
126
|
+
const maxBytes = options.maxGpuCacheBytes ?? DEFAULT_MAX_GPU_CACHE_BYTES;
|
|
127
|
+
if (bufferBytes > maxBytes) {
|
|
128
|
+
throw new Error(`WebGL XPS scene buffer would require ${formatBytes(bufferBytes)}, exceeding maxGpuCacheBytes=${formatBytes(maxBytes)}.`);
|
|
129
|
+
}
|
|
130
|
+
const gl = this.gl;
|
|
131
|
+
const buffer = gl.createBuffer();
|
|
132
|
+
if (!buffer)
|
|
133
|
+
throw new Error('Failed to allocate WebGLBuffer.');
|
|
134
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
135
|
+
gl.bufferData(gl.ARRAY_BUFFER, writer.toArrayBuffer(), gl.STATIC_DRAW);
|
|
136
|
+
const err = gl.getError();
|
|
137
|
+
if (err !== gl.NO_ERROR) {
|
|
138
|
+
gl.deleteBuffer(buffer);
|
|
139
|
+
throw new Error(`WebGL XPS buffer upload failed: 0x${err.toString(16)} for ${page.sourcePath}.`);
|
|
140
|
+
}
|
|
141
|
+
return { key, buffer, vertexCount: writer.vertexCount, gpuBytes: bufferBytes, pathCount, lastUsed: ++this.tick };
|
|
142
|
+
}
|
|
143
|
+
evictIfNeeded(options) {
|
|
144
|
+
const maxBytes = options.maxGpuCacheBytes ?? DEFAULT_MAX_GPU_CACHE_BYTES;
|
|
145
|
+
const maxScenes = options.maxCachedScenes ?? DEFAULT_MAX_CACHED_SCENES;
|
|
146
|
+
while (this.scenes.size > Math.max(1, maxScenes) || this.gpuBytes > maxBytes) {
|
|
147
|
+
let oldest;
|
|
148
|
+
for (const scene of this.scenes.values())
|
|
149
|
+
if (!oldest || scene.lastUsed < oldest.lastUsed)
|
|
150
|
+
oldest = scene;
|
|
151
|
+
if (!oldest)
|
|
152
|
+
break;
|
|
153
|
+
this.scenes.delete(oldest.key);
|
|
154
|
+
this.gl.deleteBuffer(oldest.buffer);
|
|
155
|
+
this.gpuBytes -= oldest.gpuBytes;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function collectVectorPaths(root) {
|
|
160
|
+
const out = [];
|
|
161
|
+
walk(root, IDENTITY_MATRIX, 1, out);
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
function walk(el, matrix, opacity, out) {
|
|
165
|
+
const name = localName(el);
|
|
166
|
+
const local = elementMatrix(el);
|
|
167
|
+
const composed = multiplyMatrix(matrix, local);
|
|
168
|
+
const ownOpacity = opacity * parseOpacity(getAttr(el, 'Opacity'));
|
|
169
|
+
if (name === 'Path') {
|
|
170
|
+
const commands = extractPathCommands(el);
|
|
171
|
+
if (commands.length > 0) {
|
|
172
|
+
const fill = extractBrush(el, 'Fill', ownOpacity);
|
|
173
|
+
const stroke = extractBrush(el, 'Stroke', ownOpacity);
|
|
174
|
+
const thickness = Number(getAttr(el, 'StrokeThickness') ?? 1);
|
|
175
|
+
if (fill || (stroke && thickness > 0)) {
|
|
176
|
+
out.push({
|
|
177
|
+
commands,
|
|
178
|
+
matrix: composed,
|
|
179
|
+
opacity: ownOpacity,
|
|
180
|
+
fill,
|
|
181
|
+
stroke,
|
|
182
|
+
thickness: Number.isFinite(thickness) ? thickness : 1,
|
|
183
|
+
lineStyle: extractLineStyle(el),
|
|
184
|
+
fillRule: fillRule(el),
|
|
185
|
+
bounds: pathBounds(commands)
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const child of childElements(el)) {
|
|
191
|
+
const childName = localName(child);
|
|
192
|
+
if (childName.includes('.'))
|
|
193
|
+
continue;
|
|
194
|
+
walk(child, composed, ownOpacity, out);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function appendVectorPath(writer, vector, pageMatrix, options, runtime) {
|
|
198
|
+
const fullMatrix = multiplyMatrix(pageMatrix, vector.matrix);
|
|
199
|
+
const localScale = estimateMatrixScale(vector.matrix);
|
|
200
|
+
const sourceWidth = Math.max(0.000001, vector.thickness || 1);
|
|
201
|
+
const width = Math.max(0.01, adaptiveStrokeUserWidth(sourceWidth, fullMatrix, options, runtime) * localScale);
|
|
202
|
+
const subs = flattenPath(vector.commands, 0.5);
|
|
203
|
+
const fill = vector.fill ? rgbaBytes(vector.fill) : undefined;
|
|
204
|
+
const stroke = vector.stroke ? rgbaBytes(vector.stroke) : undefined;
|
|
205
|
+
if (fill && shouldDrawFilledBounds(vector.bounds, fullMatrix, options, runtime)) {
|
|
206
|
+
for (const sub of subs) {
|
|
207
|
+
if (sub.closed || sub.points.length >= 6)
|
|
208
|
+
appendPolygonFan(writer, transformPointsArray(sub.points, vector.matrix), fill);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (stroke && vector.thickness > 0) {
|
|
212
|
+
for (const sub of subs) {
|
|
213
|
+
const pts = transformPointsArray(sub.points, vector.matrix);
|
|
214
|
+
const drawPts = sub.closed ? closePoints(pts) : pts;
|
|
215
|
+
if (vector.lineStyle.dash.length > 0)
|
|
216
|
+
appendDashedPolyline(writer, drawPts, width, vector.lineStyle, stroke);
|
|
217
|
+
else
|
|
218
|
+
appendPolyline(writer, drawPts, width, stroke);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
class VertexWriter {
|
|
223
|
+
constructor() {
|
|
224
|
+
this.buffer = new ArrayBuffer(64 * 1024);
|
|
225
|
+
this.view = new DataView(this.buffer);
|
|
226
|
+
this.vertexCount = 0;
|
|
227
|
+
}
|
|
228
|
+
get byteLength() { return this.vertexCount * VERTEX_STRIDE; }
|
|
229
|
+
push(x, y, color) {
|
|
230
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
231
|
+
return;
|
|
232
|
+
const offset = this.byteLength;
|
|
233
|
+
this.ensure(VERTEX_STRIDE);
|
|
234
|
+
this.view.setFloat32(offset, x, true);
|
|
235
|
+
this.view.setFloat32(offset + 4, y, true);
|
|
236
|
+
this.view.setUint8(offset + 8, color.r);
|
|
237
|
+
this.view.setUint8(offset + 9, color.g);
|
|
238
|
+
this.view.setUint8(offset + 10, color.b);
|
|
239
|
+
this.view.setUint8(offset + 11, color.a);
|
|
240
|
+
this.vertexCount++;
|
|
241
|
+
}
|
|
242
|
+
toArrayBuffer() { return this.buffer.slice(0, this.byteLength); }
|
|
243
|
+
ensure(extraBytes) {
|
|
244
|
+
const needed = this.byteLength + extraBytes;
|
|
245
|
+
if (needed <= this.buffer.byteLength)
|
|
246
|
+
return;
|
|
247
|
+
let next = this.buffer.byteLength;
|
|
248
|
+
while (next < needed)
|
|
249
|
+
next *= 2;
|
|
250
|
+
const newBuffer = new ArrayBuffer(next);
|
|
251
|
+
new Uint8Array(newBuffer).set(new Uint8Array(this.buffer, 0, this.byteLength));
|
|
252
|
+
this.buffer = newBuffer;
|
|
253
|
+
this.view = new DataView(this.buffer);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function appendPolygonFan(writer, pts, color) {
|
|
257
|
+
if (pts.length < 3)
|
|
258
|
+
return;
|
|
259
|
+
const p0 = pts[0];
|
|
260
|
+
for (let i = 1; i + 1 < pts.length; i++) {
|
|
261
|
+
const p1 = pts[i];
|
|
262
|
+
const p2 = pts[i + 1];
|
|
263
|
+
if (triangleAreaAbs(p0, p1, p2) < 1e-6)
|
|
264
|
+
continue;
|
|
265
|
+
writer.push(p0.x, p0.y, color);
|
|
266
|
+
writer.push(p1.x, p1.y, color);
|
|
267
|
+
writer.push(p2.x, p2.y, color);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function appendPolyline(writer, pts, width, color) {
|
|
271
|
+
if (pts.length < 2)
|
|
272
|
+
return;
|
|
273
|
+
const half = Math.max(0.05, width * 0.5);
|
|
274
|
+
for (let i = 0; i + 1 < pts.length; i++)
|
|
275
|
+
appendLineSegment(writer, pts[i], pts[i + 1], half, color);
|
|
276
|
+
}
|
|
277
|
+
function appendDashedPolyline(writer, pts, width, style, color) {
|
|
278
|
+
if (pts.length < 2 || style.dash.length === 0)
|
|
279
|
+
return;
|
|
280
|
+
const pattern = style.dash.map(v => Math.max(0.01, v * width));
|
|
281
|
+
const total = pattern.reduce((a, b) => a + b, 0);
|
|
282
|
+
if (total <= 0.01) {
|
|
283
|
+
appendPolyline(writer, pts, width, color);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
let patternIndex = 0;
|
|
287
|
+
let remaining = pattern[0];
|
|
288
|
+
let draw = true;
|
|
289
|
+
let offset = ((style.dashOffset * width) % total + total) % total;
|
|
290
|
+
while (offset > remaining) {
|
|
291
|
+
offset -= remaining;
|
|
292
|
+
patternIndex = (patternIndex + 1) % pattern.length;
|
|
293
|
+
remaining = pattern[patternIndex];
|
|
294
|
+
draw = !draw;
|
|
295
|
+
}
|
|
296
|
+
remaining -= offset;
|
|
297
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
298
|
+
const a = pts[i];
|
|
299
|
+
const b = pts[i + 1];
|
|
300
|
+
const dx = b.x - a.x;
|
|
301
|
+
const dy = b.y - a.y;
|
|
302
|
+
const len = Math.hypot(dx, dy);
|
|
303
|
+
if (len <= 1e-9)
|
|
304
|
+
continue;
|
|
305
|
+
let consumed = 0;
|
|
306
|
+
while (consumed < len - 1e-9) {
|
|
307
|
+
const step = Math.min(remaining, len - consumed);
|
|
308
|
+
if (draw && step > 1e-9) {
|
|
309
|
+
const t0 = consumed / len;
|
|
310
|
+
const t1 = (consumed + step) / len;
|
|
311
|
+
appendLineSegment(writer, { x: a.x + dx * t0, y: a.y + dy * t0 }, { x: a.x + dx * t1, y: a.y + dy * t1 }, Math.max(0.05, width * 0.5), color);
|
|
312
|
+
}
|
|
313
|
+
consumed += step;
|
|
314
|
+
remaining -= step;
|
|
315
|
+
if (remaining <= 1e-9) {
|
|
316
|
+
patternIndex = (patternIndex + 1) % pattern.length;
|
|
317
|
+
remaining = pattern[patternIndex];
|
|
318
|
+
draw = !draw;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function appendLineSegment(writer, p0, p1, half, color) {
|
|
324
|
+
const dx = p1.x - p0.x;
|
|
325
|
+
const dy = p1.y - p0.y;
|
|
326
|
+
const len = Math.hypot(dx, dy);
|
|
327
|
+
if (len <= 1e-9)
|
|
328
|
+
return;
|
|
329
|
+
const nx = -dy / len * half;
|
|
330
|
+
const ny = dx / len * half;
|
|
331
|
+
const ax = p0.x + nx, ay = p0.y + ny;
|
|
332
|
+
const bx = p0.x - nx, by = p0.y - ny;
|
|
333
|
+
const cx = p1.x - nx, cy = p1.y - ny;
|
|
334
|
+
const dx2 = p1.x + nx, dy2 = p1.y + ny;
|
|
335
|
+
writer.push(ax, ay, color);
|
|
336
|
+
writer.push(bx, by, color);
|
|
337
|
+
writer.push(cx, cy, color);
|
|
338
|
+
writer.push(ax, ay, color);
|
|
339
|
+
writer.push(cx, cy, color);
|
|
340
|
+
writer.push(dx2, dy2, color);
|
|
341
|
+
}
|
|
342
|
+
function transformPointsArray(points, m) {
|
|
343
|
+
const out = [];
|
|
344
|
+
for (let i = 0; i + 1 < points.length; i += 2) {
|
|
345
|
+
const [x, y] = transformPoint(m, points[i] ?? 0, points[i + 1] ?? 0);
|
|
346
|
+
out.push({ x, y });
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
function closePoints(pts) {
|
|
351
|
+
if (pts.length < 2)
|
|
352
|
+
return pts;
|
|
353
|
+
const first = pts[0];
|
|
354
|
+
const last = pts[pts.length - 1];
|
|
355
|
+
if (first.x === last.x && first.y === last.y)
|
|
356
|
+
return pts;
|
|
357
|
+
return [...pts, { x: first.x, y: first.y }];
|
|
358
|
+
}
|
|
359
|
+
function triangleAreaAbs(a, b, c) {
|
|
360
|
+
return Math.abs((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x));
|
|
361
|
+
}
|
|
362
|
+
function sceneKey(page, pageMatrix, options) {
|
|
363
|
+
const mode = options.lineWeightMode ?? 'adaptive';
|
|
364
|
+
const scaleBucket = mode === 'physical' ? 'physical' : String(Math.round(Math.log2(Math.max(1e-12, estimateMatrixScale(pageMatrix))) * 8));
|
|
365
|
+
return `${page.id}|${page.sourcePath}|${page.width}x${page.height}|lw:${mode}:${scaleBucket}`;
|
|
366
|
+
}
|
|
367
|
+
function elementMatrix(el) {
|
|
368
|
+
let m = parseMatrix(getAttr(el, 'RenderTransform') ?? getAttr(el, 'Transform'));
|
|
369
|
+
for (const child of childElements(el)) {
|
|
370
|
+
const name = localName(child);
|
|
371
|
+
if (name.endsWith('.RenderTransform') || name.endsWith('.Transform')) {
|
|
372
|
+
const matrixEl = childElements(child).find(c => localName(c) === 'MatrixTransform');
|
|
373
|
+
if (matrixEl)
|
|
374
|
+
m = multiplyMatrix(m, parseMatrix(getAttr(matrixEl, 'Matrix')));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const left = Number(getAttr(el, 'Canvas.Left') ?? 0);
|
|
378
|
+
const top = Number(getAttr(el, 'Canvas.Top') ?? 0);
|
|
379
|
+
if (left || top)
|
|
380
|
+
m = multiplyMatrix({ a: 1, b: 0, c: 0, d: 1, e: left, f: top }, m);
|
|
381
|
+
return m;
|
|
382
|
+
}
|
|
383
|
+
function extractPathCommands(pathEl) {
|
|
384
|
+
const data = getAttr(pathEl, 'Data');
|
|
385
|
+
if (data)
|
|
386
|
+
return parsePathData(data);
|
|
387
|
+
for (const prop of childElements(pathEl)) {
|
|
388
|
+
if (localName(prop) !== 'Path.Data')
|
|
389
|
+
continue;
|
|
390
|
+
for (const geom of childElements(prop)) {
|
|
391
|
+
const figures = getAttr(geom, 'Figures');
|
|
392
|
+
if (figures)
|
|
393
|
+
return parsePathData(figures);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
function extractBrush(el, prop, opacity) {
|
|
399
|
+
const direct = getAttr(el, prop);
|
|
400
|
+
if (direct)
|
|
401
|
+
return parseBrushColor(direct, opacity);
|
|
402
|
+
const solid = findPropertyBrush(el, prop, 'SolidColorBrush');
|
|
403
|
+
if (solid)
|
|
404
|
+
return parseBrushColor(getAttr(solid, 'Color'), opacity * parseOpacity(getAttr(solid, 'Opacity')));
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
function findPropertyBrush(el, prop, brushLocalName) {
|
|
408
|
+
const propName = `${localName(el)}.${prop}`;
|
|
409
|
+
for (const child of childElements(el)) {
|
|
410
|
+
if (localName(child) !== propName)
|
|
411
|
+
continue;
|
|
412
|
+
return childElements(child).find(c => localName(c) === brushLocalName);
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
function parseOpacity(value) {
|
|
417
|
+
if (!value)
|
|
418
|
+
return 1;
|
|
419
|
+
const n = Number(value);
|
|
420
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1;
|
|
421
|
+
}
|
|
422
|
+
function fillRule(el) {
|
|
423
|
+
const data = getAttr(el, 'Data') ?? '';
|
|
424
|
+
return /F0/.test(data) ? 'nonzero' : 'evenodd';
|
|
425
|
+
}
|
|
426
|
+
function extractLineStyle(el) {
|
|
427
|
+
const start = (getAttr(el, 'StrokeStartLineCap') ?? '').toLowerCase();
|
|
428
|
+
const end = (getAttr(el, 'StrokeEndLineCap') ?? '').toLowerCase();
|
|
429
|
+
const dashCap = (getAttr(el, 'StrokeDashCap') ?? '').toLowerCase();
|
|
430
|
+
const cap = start === 'round' || end === 'round' || dashCap === 'round'
|
|
431
|
+
? 'round'
|
|
432
|
+
: start === 'square' || end === 'square' || dashCap === 'square'
|
|
433
|
+
? 'square'
|
|
434
|
+
: 'butt';
|
|
435
|
+
const joinAttr = (getAttr(el, 'StrokeLineJoin') ?? '').toLowerCase();
|
|
436
|
+
const join = joinAttr === 'round' ? 'round' : joinAttr === 'bevel' ? 'bevel' : 'miter';
|
|
437
|
+
const miter = Number(getAttr(el, 'StrokeMiterLimit') ?? 10);
|
|
438
|
+
return {
|
|
439
|
+
cap,
|
|
440
|
+
join,
|
|
441
|
+
miterLimit: Number.isFinite(miter) && miter > 0 ? miter : 10,
|
|
442
|
+
dash: parseNumberList(getAttr(el, 'StrokeDashArray') ?? '').filter(v => Number.isFinite(v) && v > 0),
|
|
443
|
+
dashOffset: Number(getAttr(el, 'StrokeDashOffset') ?? 0) || 0
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function pathBounds(commands) {
|
|
447
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
448
|
+
const add = (x, y) => {
|
|
449
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
450
|
+
return;
|
|
451
|
+
minX = Math.min(minX, x);
|
|
452
|
+
minY = Math.min(minY, y);
|
|
453
|
+
maxX = Math.max(maxX, x);
|
|
454
|
+
maxY = Math.max(maxY, y);
|
|
455
|
+
};
|
|
456
|
+
for (const c of commands) {
|
|
457
|
+
if (c.type === 'M' || c.type === 'L')
|
|
458
|
+
add(c.x, c.y);
|
|
459
|
+
else if (c.type === 'C') {
|
|
460
|
+
add(c.x1, c.y1);
|
|
461
|
+
add(c.x2, c.y2);
|
|
462
|
+
add(c.x, c.y);
|
|
463
|
+
}
|
|
464
|
+
else if (c.type === 'Q') {
|
|
465
|
+
add(c.x1, c.y1);
|
|
466
|
+
add(c.x, c.y);
|
|
467
|
+
}
|
|
468
|
+
else if (c.type === 'A') {
|
|
469
|
+
add(c.x - c.rx, c.y - c.ry);
|
|
470
|
+
add(c.x + c.rx, c.y + c.ry);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : undefined;
|
|
474
|
+
}
|
|
475
|
+
function rgbaBytes(css) {
|
|
476
|
+
const packed = colorToRgba32(css, 0xff000000);
|
|
477
|
+
return { r: packed & 255, g: (packed >>> 8) & 255, b: (packed >>> 16) & 255, a: (packed >>> 24) & 255 };
|
|
478
|
+
}
|
|
479
|
+
function rgba01(css) {
|
|
480
|
+
const c = rgbaBytes(css);
|
|
481
|
+
return [c.r / 255, c.g / 255, c.b / 255, c.a / 255];
|
|
482
|
+
}
|
|
483
|
+
function formatBytes(bytes) {
|
|
484
|
+
if (bytes < 1024)
|
|
485
|
+
return `${bytes} B`;
|
|
486
|
+
if (bytes < 1024 * 1024)
|
|
487
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
488
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
489
|
+
}
|
|
490
|
+
function createProgram(gl, vertexSource, fragmentSource) {
|
|
491
|
+
const vertex = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
|
|
492
|
+
const fragment = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
|
493
|
+
const program = gl.createProgram();
|
|
494
|
+
if (!program)
|
|
495
|
+
throw new Error('Failed to create WebGLProgram.');
|
|
496
|
+
gl.attachShader(program, vertex);
|
|
497
|
+
gl.attachShader(program, fragment);
|
|
498
|
+
gl.linkProgram(program);
|
|
499
|
+
gl.deleteShader(vertex);
|
|
500
|
+
gl.deleteShader(fragment);
|
|
501
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
502
|
+
const log = gl.getProgramInfoLog(program) ?? 'unknown link error';
|
|
503
|
+
gl.deleteProgram(program);
|
|
504
|
+
throw new Error(`WebGL program link failed: ${log}`);
|
|
505
|
+
}
|
|
506
|
+
return program;
|
|
507
|
+
}
|
|
508
|
+
function compileShader(gl, type, source) {
|
|
509
|
+
const shader = gl.createShader(type);
|
|
510
|
+
if (!shader)
|
|
511
|
+
throw new Error('Failed to create WebGLShader.');
|
|
512
|
+
gl.shaderSource(shader, source);
|
|
513
|
+
gl.compileShader(shader);
|
|
514
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
515
|
+
const log = gl.getShaderInfoLog(shader) ?? 'unknown shader compile error';
|
|
516
|
+
gl.deleteShader(shader);
|
|
517
|
+
throw new Error(`WebGL shader compile failed: ${log}`);
|
|
518
|
+
}
|
|
519
|
+
return shader;
|
|
520
|
+
}
|
|
521
|
+
const VERTEX_SHADER = `
|
|
522
|
+
attribute vec2 a_pos;
|
|
523
|
+
attribute vec4 a_color;
|
|
524
|
+
uniform vec4 u_matrix;
|
|
525
|
+
uniform vec4 u_viewport;
|
|
526
|
+
varying vec4 v_color;
|
|
527
|
+
void main() {
|
|
528
|
+
float x = u_matrix.x * a_pos.x + u_matrix.z * a_pos.y + u_viewport.x;
|
|
529
|
+
float y = u_matrix.y * a_pos.x + u_matrix.w * a_pos.y + u_viewport.y;
|
|
530
|
+
vec2 clip = vec2((x / u_viewport.z) * 2.0 - 1.0, 1.0 - (y / u_viewport.w) * 2.0);
|
|
531
|
+
gl_Position = vec4(clip, 0.0, 1.0);
|
|
532
|
+
v_color = a_color;
|
|
533
|
+
}
|
|
534
|
+
`;
|
|
535
|
+
const FRAGMENT_SHADER = `
|
|
536
|
+
precision mediump float;
|
|
537
|
+
varying vec4 v_color;
|
|
538
|
+
void main() {
|
|
539
|
+
gl_FragColor = v_color;
|
|
540
|
+
}
|
|
541
|
+
`;
|
|
@@ -5,16 +5,26 @@ export interface XpsRenderOptions extends CadLineStyleOptions {
|
|
|
5
5
|
zoom?: number;
|
|
6
6
|
panX?: number;
|
|
7
7
|
panY?: number;
|
|
8
|
+
preferWebgl?: boolean;
|
|
8
9
|
preferWasm?: boolean;
|
|
9
10
|
wasmUrl?: string;
|
|
11
|
+
webglCanvas?: HTMLCanvasElement;
|
|
12
|
+
maxGpuCacheBytes?: number;
|
|
13
|
+
maxCachedScenes?: number;
|
|
10
14
|
background?: string;
|
|
11
15
|
}
|
|
12
16
|
export declare class XpsRenderer {
|
|
13
17
|
private readonly document;
|
|
14
18
|
private wasm?;
|
|
19
|
+
private webgl?;
|
|
20
|
+
private webglCanvas?;
|
|
15
21
|
private readonly fontCache;
|
|
22
|
+
private readonly xmlCache;
|
|
16
23
|
constructor(document: LoadedDwfDocument);
|
|
17
24
|
render(page: XpsPageData, canvas: HTMLCanvasElement, options?: XpsRenderOptions): Promise<RenderStats>;
|
|
25
|
+
private getWebGlBackend;
|
|
26
|
+
private getXmlDocument;
|
|
27
|
+
dispose(): void;
|
|
18
28
|
private renderElementToCanvas;
|
|
19
29
|
private renderElementToWasm;
|
|
20
30
|
private drawGlyphs;
|
|
@@ -5,18 +5,19 @@ import { multiplyMatrix, parseBrushColor, parseMatrix } from './style.js';
|
|
|
5
5
|
import { adaptiveStrokeUserWidth, canvasDpr, shouldDrawFilledBounds, shouldDrawTextByPixelSize } from './cadLineStyle.js';
|
|
6
6
|
import { fitPageMatrix } from './viewport.js';
|
|
7
7
|
import { WasmRasterBackend } from '../wasm/WasmRasterBackend.js';
|
|
8
|
+
import { WebGlXpsBackend } from './WebGlXpsBackend.js';
|
|
8
9
|
export class XpsRenderer {
|
|
9
10
|
constructor(document) {
|
|
10
11
|
this.document = document;
|
|
11
12
|
this.fontCache = new Map();
|
|
13
|
+
this.xmlCache = new Map();
|
|
12
14
|
}
|
|
13
15
|
async render(page, canvas, options = {}) {
|
|
14
16
|
const opc = this.document.opc;
|
|
15
17
|
if (!opc)
|
|
16
18
|
throw new Error('XPS page requires an OPC package view.');
|
|
17
19
|
const warnings = actionableDiagnostics(page.diagnostics);
|
|
18
|
-
const
|
|
19
|
-
const doc = parseXml(xml, page.sourcePath);
|
|
20
|
+
const doc = await this.getXmlDocument(page.sourcePath);
|
|
20
21
|
const root = doc.documentElement;
|
|
21
22
|
const ctx = canvas.getContext('2d');
|
|
22
23
|
if (!ctx)
|
|
@@ -25,6 +26,29 @@ export class XpsRenderer {
|
|
|
25
26
|
const runtime = { dpr: canvasDpr(canvas), zoom: options.zoom ?? 1 };
|
|
26
27
|
const pageMatrix = fitPageMatrix({ canvasWidth: canvas.width, canvasHeight: canvas.height, pageWidth: page.width, pageHeight: page.height, zoom: options.zoom, panX: options.panX, panY: options.panY });
|
|
27
28
|
let commands = 0;
|
|
29
|
+
if (options.preferWebgl ?? true) {
|
|
30
|
+
try {
|
|
31
|
+
const backend = this.getWebGlBackend(options.webglCanvas);
|
|
32
|
+
if (options.webglCanvas)
|
|
33
|
+
options.webglCanvas.style.visibility = 'visible';
|
|
34
|
+
const stats = backend.render(page, root, canvas, { ...options, compositeToTarget: !options.webglCanvas });
|
|
35
|
+
ctx.save();
|
|
36
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
37
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
38
|
+
ctx.restore();
|
|
39
|
+
commands += stats.commands;
|
|
40
|
+
commands += await this.renderElementToCanvas(root, ctx, page.sourcePath, pageMatrix, 1, warnings, { vectors: false, overlays: true }, options, runtime);
|
|
41
|
+
warnings.push(...stats.warnings);
|
|
42
|
+
return { backend: 'webgl-xps', commands, warnings };
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (options.webglCanvas)
|
|
46
|
+
options.webglCanvas.style.visibility = 'hidden';
|
|
47
|
+
warnings.push(diag('warning', 'WEBGL_XPS_BACKEND_FALLBACK', `WebGL XPS vector path failed, falling back to ${options.preferWasm ? 'WASM raster' : 'Canvas2D'}: ${String(err)}`, page.sourcePath));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (options.webglCanvas)
|
|
51
|
+
options.webglCanvas.style.visibility = 'hidden';
|
|
28
52
|
if (options.preferWasm) {
|
|
29
53
|
try {
|
|
30
54
|
this.wasm ?? (this.wasm = new WasmRasterBackend({ wasmUrl: options.wasmUrl }));
|
|
@@ -48,6 +72,30 @@ export class XpsRenderer {
|
|
|
48
72
|
commands += await this.renderElementToCanvas(root, ctx, page.sourcePath, pageMatrix, 1, warnings, { vectors: true, overlays: true }, options, runtime);
|
|
49
73
|
return { backend: 'canvas2d', commands, warnings };
|
|
50
74
|
}
|
|
75
|
+
getWebGlBackend(canvas) {
|
|
76
|
+
if (!this.webgl || this.webglCanvas !== canvas) {
|
|
77
|
+
this.webgl?.dispose();
|
|
78
|
+
this.webgl = new WebGlXpsBackend(canvas);
|
|
79
|
+
this.webglCanvas = canvas;
|
|
80
|
+
}
|
|
81
|
+
return this.webgl;
|
|
82
|
+
}
|
|
83
|
+
getXmlDocument(part) {
|
|
84
|
+
let cached = this.xmlCache.get(part);
|
|
85
|
+
if (!cached) {
|
|
86
|
+
const opc = this.document.opc;
|
|
87
|
+
cached = opc.readText(part).then(xml => parseXml(xml, part));
|
|
88
|
+
this.xmlCache.set(part, cached);
|
|
89
|
+
}
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
dispose() {
|
|
93
|
+
this.webgl?.dispose();
|
|
94
|
+
this.webgl = undefined;
|
|
95
|
+
this.webglCanvas = undefined;
|
|
96
|
+
this.xmlCache.clear();
|
|
97
|
+
this.fontCache.clear();
|
|
98
|
+
}
|
|
51
99
|
async renderElementToCanvas(el, ctx, pagePath, matrix, opacity, warnings, mode, options, runtime) {
|
|
52
100
|
const name = localName(el);
|
|
53
101
|
const local = elementMatrix(el);
|
|
@@ -192,8 +240,9 @@ export class XpsRenderer {
|
|
|
192
240
|
if (!uri)
|
|
193
241
|
return undefined;
|
|
194
242
|
const part = resolvePart(pagePath, uri.replace(/^\//, ''));
|
|
195
|
-
|
|
196
|
-
|
|
243
|
+
// XPS/DWFx often stores embedded TrueType fonts as ODTTF.
|
|
244
|
+
// Deobfuscate and load them so overview text matches CAD viewers instead
|
|
245
|
+
// of falling back to thick system fonts.
|
|
197
246
|
let cached = this.fontCache.get(part);
|
|
198
247
|
if (!cached) {
|
|
199
248
|
cached = this.loadFontFace(part).catch(err => {
|
|
@@ -209,14 +258,26 @@ export class XpsRenderer {
|
|
|
209
258
|
const fontSet = document.fonts;
|
|
210
259
|
if (!FontFaceCtor || !fontSet)
|
|
211
260
|
return undefined;
|
|
212
|
-
|
|
261
|
+
let bytes = await this.document.opc.readBytes(part);
|
|
262
|
+
let mime = mimeFromPath(part) ?? 'font/ttf';
|
|
263
|
+
if (/\.odttf$/i.test(part)) {
|
|
264
|
+
bytes = deobfuscateOdttf(part, bytes);
|
|
265
|
+
mime = 'font/ttf';
|
|
266
|
+
}
|
|
213
267
|
const family = `dwfv_xps_${hashString(part)}`;
|
|
214
|
-
const blob = new Blob([bytes], { type:
|
|
268
|
+
const blob = new Blob([bytes], { type: mime });
|
|
215
269
|
const url = URL.createObjectURL(blob);
|
|
216
270
|
const face = new FontFaceCtor(family, `url("${url}")`);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
271
|
+
try {
|
|
272
|
+
await face.load();
|
|
273
|
+
fontSet.add(face);
|
|
274
|
+
URL.revokeObjectURL(url);
|
|
275
|
+
return family;
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
URL.revokeObjectURL(url);
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
220
281
|
}
|
|
221
282
|
async drawImageResource(ctx, pagePath, source, matrix, opacity, el) {
|
|
222
283
|
const opc = this.document.opc;
|
|
@@ -330,6 +391,54 @@ function drawGlyphRunWithIndices(ctx, text, indices, x, y, emSize) {
|
|
|
330
391
|
if (charIndex < text.length)
|
|
331
392
|
ctx.fillText(text.slice(charIndex), cursor, y);
|
|
332
393
|
}
|
|
394
|
+
function deobfuscateOdttf(part, bytes) {
|
|
395
|
+
const name = part.split('/').pop() ?? '';
|
|
396
|
+
const guid = name.replace(/\.odttf$/i, '').replace(/[^0-9a-fA-F]/g, '');
|
|
397
|
+
if (guid.length !== 32 || bytes.length < 32)
|
|
398
|
+
return bytes;
|
|
399
|
+
const key = new Uint8Array(16);
|
|
400
|
+
for (let i = 0; i < 16; i++)
|
|
401
|
+
key[i] = parseInt(guid.slice(i * 2, i * 2 + 2), 16);
|
|
402
|
+
// XPS font obfuscation uses the GUID bytes in reverse order, repeated over
|
|
403
|
+
// the first 32 bytes of the font payload.
|
|
404
|
+
const out = new Uint8Array(bytes);
|
|
405
|
+
for (let i = 0; i < Math.min(32, out.length); i++)
|
|
406
|
+
out[i] = (out[i] ?? 0) ^ key[15 - (i % 16)];
|
|
407
|
+
if (!looksLikeSfnt(out)) {
|
|
408
|
+
// A few producers use the GUID binary/little-endian representation. Try it
|
|
409
|
+
// as a guarded fallback rather than silently returning a broken font.
|
|
410
|
+
const altKey = guidLittleEndianBytes(guid);
|
|
411
|
+
const alt = new Uint8Array(bytes);
|
|
412
|
+
for (let i = 0; i < Math.min(32, alt.length); i++)
|
|
413
|
+
alt[i] = (alt[i] ?? 0) ^ altKey[15 - (i % 16)];
|
|
414
|
+
if (looksLikeSfnt(alt))
|
|
415
|
+
return alt;
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
function guidLittleEndianBytes(hex) {
|
|
420
|
+
const b = new Uint8Array(16);
|
|
421
|
+
const raw = new Uint8Array(16);
|
|
422
|
+
for (let i = 0; i < 16; i++)
|
|
423
|
+
raw[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
424
|
+
b[0] = raw[3];
|
|
425
|
+
b[1] = raw[2];
|
|
426
|
+
b[2] = raw[1];
|
|
427
|
+
b[3] = raw[0];
|
|
428
|
+
b[4] = raw[5];
|
|
429
|
+
b[5] = raw[4];
|
|
430
|
+
b[6] = raw[7];
|
|
431
|
+
b[7] = raw[6];
|
|
432
|
+
for (let i = 8; i < 16; i++)
|
|
433
|
+
b[i] = raw[i];
|
|
434
|
+
return b;
|
|
435
|
+
}
|
|
436
|
+
function looksLikeSfnt(bytes) {
|
|
437
|
+
if (bytes.length < 4)
|
|
438
|
+
return false;
|
|
439
|
+
const tag = String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
440
|
+
return tag === 'OTTO' || tag === 'true' || tag === 'typ1' || tag === 'ttcf' || (bytes[0] === 0 && bytes[1] === 1 && bytes[2] === 0 && bytes[3] === 0);
|
|
441
|
+
}
|
|
333
442
|
function hashString(s) {
|
|
334
443
|
let h = 2166136261;
|
|
335
444
|
for (let i = 0; i < s.length; i++) {
|
package/dist/viewer/DwfViewer.js
CHANGED
|
@@ -10,6 +10,8 @@ export class DwfViewer {
|
|
|
10
10
|
this.panY = 0;
|
|
11
11
|
this.yaw = -0.78;
|
|
12
12
|
this.pitch = 0.55;
|
|
13
|
+
this.rendering = false;
|
|
14
|
+
this.renderAgain = false;
|
|
13
15
|
this.renderRaf = 0;
|
|
14
16
|
this.renderSeq = 0;
|
|
15
17
|
this.currentDpr = 1;
|
|
@@ -107,44 +109,58 @@ export class DwfViewer {
|
|
|
107
109
|
async render() {
|
|
108
110
|
if (!this.renderer || !this.doc)
|
|
109
111
|
return undefined;
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const task = this.renderer.render(this.pageIndex, this.canvas, {
|
|
116
|
-
zoom: this.zoom,
|
|
117
|
-
panX: this.panX,
|
|
118
|
-
panY: this.panY,
|
|
119
|
-
preferWebgl: this.preferWebgl,
|
|
120
|
-
preferWasm: this.preferWasm,
|
|
121
|
-
wasmUrl: this.wasmUrl,
|
|
122
|
-
background: this.background,
|
|
123
|
-
maxGpuCacheBytes: this.maxGpuCacheBytes,
|
|
124
|
-
maxCachedScenes: this.maxCachedScenes,
|
|
125
|
-
webglCanvas: this.webglCanvas,
|
|
126
|
-
yaw: this.yaw,
|
|
127
|
-
pitch: this.pitch,
|
|
128
|
-
lineWeightMode: this.lineWeightMode,
|
|
129
|
-
minStrokeCssPx: this.minStrokeCssPx,
|
|
130
|
-
maxOverviewStrokeCssPx: this.maxOverviewStrokeCssPx,
|
|
131
|
-
minTextCssPx: this.minTextCssPx,
|
|
132
|
-
minFilledAreaCssPx: this.minFilledAreaCssPx
|
|
133
|
-
});
|
|
134
|
-
this.pendingRender = task;
|
|
112
|
+
if (this.rendering) {
|
|
113
|
+
this.renderAgain = true;
|
|
114
|
+
return this.pendingRender;
|
|
115
|
+
}
|
|
116
|
+
this.rendering = true;
|
|
135
117
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
118
|
+
this.resizeCanvasToDisplaySize();
|
|
119
|
+
const page = this.doc.pageData[this.pageIndex];
|
|
120
|
+
if (!page)
|
|
121
|
+
return undefined;
|
|
122
|
+
const seq = ++this.renderSeq;
|
|
123
|
+
const task = this.renderer.render(this.pageIndex, this.canvas, {
|
|
124
|
+
zoom: this.zoom,
|
|
125
|
+
panX: this.panX,
|
|
126
|
+
panY: this.panY,
|
|
127
|
+
preferWebgl: this.preferWebgl,
|
|
128
|
+
preferWasm: this.preferWasm,
|
|
129
|
+
wasmUrl: this.wasmUrl,
|
|
130
|
+
background: this.background,
|
|
131
|
+
maxGpuCacheBytes: this.maxGpuCacheBytes,
|
|
132
|
+
maxCachedScenes: this.maxCachedScenes,
|
|
133
|
+
webglCanvas: this.webglCanvas,
|
|
134
|
+
yaw: this.yaw,
|
|
135
|
+
pitch: this.pitch,
|
|
136
|
+
lineWeightMode: this.lineWeightMode,
|
|
137
|
+
minStrokeCssPx: this.minStrokeCssPx,
|
|
138
|
+
maxOverviewStrokeCssPx: this.maxOverviewStrokeCssPx,
|
|
139
|
+
minTextCssPx: this.minTextCssPx,
|
|
140
|
+
minFilledAreaCssPx: this.minFilledAreaCssPx
|
|
141
|
+
});
|
|
142
|
+
this.pendingRender = task;
|
|
143
|
+
try {
|
|
144
|
+
const stats = await task;
|
|
145
|
+
if (this.pendingRender === task && seq === this.renderSeq) {
|
|
146
|
+
const warnCount = stats.warnings.filter(w => w.level !== 'info').length;
|
|
147
|
+
const dprText = this.currentDpr > 1 ? ` · DPR ${this.currentDpr.toFixed(2)}` : '';
|
|
148
|
+
this.setStatus(`${this.doc.kind.toUpperCase()} · ${page.kind} · ${stats.backend} · ${stats.commands} ops${dprText}${warnCount ? ` · ${warnCount} 警告` : ''}`, warnCount > 0);
|
|
149
|
+
}
|
|
150
|
+
return stats;
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (seq === this.renderSeq)
|
|
154
|
+
this.setStatus(`渲染失败:${String(err)}`, true);
|
|
155
|
+
throw err;
|
|
141
156
|
}
|
|
142
|
-
return stats;
|
|
143
157
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
finally {
|
|
159
|
+
this.rendering = false;
|
|
160
|
+
if (this.renderAgain) {
|
|
161
|
+
this.renderAgain = false;
|
|
162
|
+
this.requestRender();
|
|
163
|
+
}
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
166
|
getDocument() {
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flyfish-dev/dwf-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Pure frontend DWF/DWFx viewer with W2D
|
|
6
|
+
"description": "Pure frontend DWF/DWFx CAD viewer with WebGL-accelerated XPS/W2D 2D rendering, W3D/HSF 3D rendering, embedded XPS fonts, and optional WASM fallback.",
|
|
7
7
|
"license": "AGPL-3.0-only",
|
|
8
8
|
"author": "flyfish-dev",
|
|
9
9
|
"homepage": "https://github.com/flyfish-dev/dwf-viewer#readme",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"cad",
|
|
25
25
|
"lineweight",
|
|
26
26
|
"line-width",
|
|
27
|
+
"xps-webgl",
|
|
27
28
|
"viewer",
|
|
28
29
|
"webgl",
|
|
29
30
|
"threejs",
|