@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 +21 -6
- package/PRODUCTION_3D_NOTES.md +4 -0
- package/README.md +101 -36
- package/dist/format/types.d.ts +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/render/PageRenderer.d.ts +5 -0
- package/dist/render/PageRenderer.js +1 -0
- package/dist/render/W2dRenderer.d.ts +2 -1
- package/dist/render/W2dRenderer.js +16 -13
- package/dist/render/WebGlW2dBackend.d.ts +2 -1
- package/dist/render/WebGlW2dBackend.js +14 -9
- package/dist/render/WebGlXpsBackend.d.ts +38 -0
- package/dist/render/WebGlXpsBackend.js +541 -0
- package/dist/render/XpsRenderer.d.ts +16 -1
- package/dist/render/XpsRenderer.js +270 -25
- package/dist/render/cadLineStyle.d.ts +32 -0
- package/dist/render/cadLineStyle.js +59 -0
- package/dist/viewer/DwfViewer.d.ts +13 -0
- package/dist/viewer/DwfViewer.js +66 -30
- package/package.json +6 -3
|
@@ -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
|
+
`;
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
import { type RenderStats } from '../format/types.js';
|
|
2
2
|
import type { LoadedDwfDocument, XpsPageData } from '../format/document.js';
|
|
3
|
-
|
|
3
|
+
import { type CadLineStyleOptions } from './cadLineStyle.js';
|
|
4
|
+
export interface XpsRenderOptions extends CadLineStyleOptions {
|
|
4
5
|
zoom?: number;
|
|
5
6
|
panX?: number;
|
|
6
7
|
panY?: number;
|
|
8
|
+
preferWebgl?: boolean;
|
|
7
9
|
preferWasm?: boolean;
|
|
8
10
|
wasmUrl?: string;
|
|
11
|
+
webglCanvas?: HTMLCanvasElement;
|
|
12
|
+
maxGpuCacheBytes?: number;
|
|
13
|
+
maxCachedScenes?: number;
|
|
9
14
|
background?: string;
|
|
10
15
|
}
|
|
11
16
|
export declare class XpsRenderer {
|
|
12
17
|
private readonly document;
|
|
13
18
|
private wasm?;
|
|
19
|
+
private webgl?;
|
|
20
|
+
private webglCanvas?;
|
|
21
|
+
private readonly fontCache;
|
|
22
|
+
private readonly xmlCache;
|
|
14
23
|
constructor(document: LoadedDwfDocument);
|
|
15
24
|
render(page: XpsPageData, canvas: HTMLCanvasElement, options?: XpsRenderOptions): Promise<RenderStats>;
|
|
25
|
+
private getWebGlBackend;
|
|
26
|
+
private getXmlDocument;
|
|
27
|
+
dispose(): void;
|
|
16
28
|
private renderElementToCanvas;
|
|
17
29
|
private renderElementToWasm;
|
|
30
|
+
private drawGlyphs;
|
|
31
|
+
private fontFamilyForGlyphs;
|
|
32
|
+
private loadFontFace;
|
|
18
33
|
private drawImageResource;
|
|
19
34
|
private drawImageBrush;
|
|
20
35
|
}
|