dwf-viewer 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +235 -0
- package/NOTICE +10 -0
- package/PRODUCTION_3D_NOTES.md +48 -0
- package/README.md +203 -0
- package/dist/format/document.d.ts +186 -0
- package/dist/format/document.js +9 -0
- package/dist/format/dwf.d.ts +4 -0
- package/dist/format/dwf.js +372 -0
- package/dist/format/dwfx.d.ts +6 -0
- package/dist/format/dwfx.js +425 -0
- package/dist/format/emodelMetadata.d.ts +10 -0
- package/dist/format/emodelMetadata.js +368 -0
- package/dist/format/inflate.d.ts +4 -0
- package/dist/format/inflate.js +28 -0
- package/dist/format/opc.d.ts +28 -0
- package/dist/format/opc.js +85 -0
- package/dist/format/open.d.ts +3 -0
- package/dist/format/open.js +69 -0
- package/dist/format/types.d.ts +61 -0
- package/dist/format/types.js +6 -0
- package/dist/format/util.d.ts +18 -0
- package/dist/format/util.js +324 -0
- package/dist/format/w2dBinary.d.ts +19 -0
- package/dist/format/w2dBinary.js +629 -0
- package/dist/format/w2dText.d.ts +13 -0
- package/dist/format/w2dText.js +166 -0
- package/dist/format/w3d.d.ts +8 -0
- package/dist/format/w3d.js +826 -0
- package/dist/format/zip.d.ts +30 -0
- package/dist/format/zip.js +141 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9 -0
- package/dist/render/PageRenderer.d.ts +27 -0
- package/dist/render/PageRenderer.js +92 -0
- package/dist/render/ThreeJsSceneAdapter.d.ts +29 -0
- package/dist/render/ThreeJsSceneAdapter.js +52 -0
- package/dist/render/ThreeW3dRenderer.d.ts +24 -0
- package/dist/render/ThreeW3dRenderer.js +372 -0
- package/dist/render/W2dRenderer.d.ts +24 -0
- package/dist/render/W2dRenderer.js +198 -0
- package/dist/render/WebGlW2dBackend.d.ts +38 -0
- package/dist/render/WebGlW2dBackend.js +400 -0
- package/dist/render/XpsRenderer.d.ts +20 -0
- package/dist/render/XpsRenderer.js +310 -0
- package/dist/render/style.d.ts +16 -0
- package/dist/render/style.js +115 -0
- package/dist/render/viewport.d.ts +16 -0
- package/dist/render/viewport.js +27 -0
- package/dist/render/xpsPath.d.ts +41 -0
- package/dist/render/xpsPath.js +335 -0
- package/dist/viewer/DwfViewer.d.ts +69 -0
- package/dist/viewer/DwfViewer.js +386 -0
- package/dist/wasm/WasmRasterBackend.d.ts +21 -0
- package/dist/wasm/WasmRasterBackend.js +84 -0
- package/package.json +91 -0
- package/public/dwfv-render.wasm +0 -0
- package/styles/dwf-viewer.css +51 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { decodeUtf8, localName, parseXml } from './util.js';
|
|
2
|
+
import { diag } from './types.js';
|
|
3
|
+
export async function enrichW3dModelFromEModelResources(model, resources, opc) {
|
|
4
|
+
const diagnostics = model.diagnostics;
|
|
5
|
+
model.metadata ?? (model.metadata = {});
|
|
6
|
+
model.materials ?? (model.materials = []);
|
|
7
|
+
model.textures ?? (model.textures = []);
|
|
8
|
+
model.sceneTree ?? (model.sceneTree = []);
|
|
9
|
+
model.pmi ?? (model.pmi = []);
|
|
10
|
+
model.views ?? (model.views = []);
|
|
11
|
+
model.animations ?? (model.animations = []);
|
|
12
|
+
await collectTextures(model, resources, opc, diagnostics);
|
|
13
|
+
const descriptor = resources.find(r => /descriptor/i.test(r.role ?? '') || /descriptor\.xml$/i.test(r.path));
|
|
14
|
+
if (descriptor && opc.zip.has(descriptor.path)) {
|
|
15
|
+
try {
|
|
16
|
+
parseDescriptor(model, decodeUtf8(await opc.readBytes(descriptor.path)), descriptor.path);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
diagnostics.push(diag('warning', 'EMODEL_DESCRIPTOR_PARSE_FAILED', `Failed to parse eModel descriptor: ${String(err)}`, descriptor.path));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const contentDef = resources.find(r => /content\s+definition/i.test(r.role ?? ''));
|
|
23
|
+
const instanceInfos = contentDef && opc.zip.has(contentDef.path)
|
|
24
|
+
? parseContentDefinitionSafe(model, decodeUtf8(await opc.readBytes(contentDef.path)), contentDef.path, diagnostics)
|
|
25
|
+
: [];
|
|
26
|
+
const presentation = resources.find(r => /content\s+presentation/i.test(r.role ?? ''));
|
|
27
|
+
const labelByContentRef = new Map();
|
|
28
|
+
let instanceMaterials = new Map();
|
|
29
|
+
if (presentation && opc.zip.has(presentation.path)) {
|
|
30
|
+
try {
|
|
31
|
+
const { nodes, pmi, views, animations, materials } = parseContentPresentation(decodeUtf8(await opc.readBytes(presentation.path)), presentation.path, labelByContentRef);
|
|
32
|
+
instanceMaterials = materials;
|
|
33
|
+
model.sceneTree = nodes;
|
|
34
|
+
model.pmi = pmi;
|
|
35
|
+
model.views = mergeViews(model.views ?? [], views);
|
|
36
|
+
model.animations = animations;
|
|
37
|
+
const existing = new Map((model.materials ?? []).map(m => [m.id, m]));
|
|
38
|
+
for (const mat of materials.values())
|
|
39
|
+
existing.set(mat.id, bindTextureByName(mat, model.textures ?? []));
|
|
40
|
+
model.materials = Array.from(existing.values());
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
diagnostics.push(diag('warning', 'EMODEL_PRESENTATION_PARSE_FAILED', `Failed to parse eModel presentation: ${String(err)}`, presentation.path));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
attachSelectionMetadata(model, instanceInfos, labelByContentRef, instanceMaterials);
|
|
47
|
+
const nodeCount = countSceneNodes(model.sceneTree ?? []);
|
|
48
|
+
if ((model.views?.length ?? 0) > 0 && !model.initialView)
|
|
49
|
+
model.initialView = model.views[0];
|
|
50
|
+
model.stats.materialCount = model.materials?.length ?? 0;
|
|
51
|
+
model.stats.textureCount = model.textures?.length ?? 0;
|
|
52
|
+
model.stats.pmiCount = model.pmi?.length ?? 0;
|
|
53
|
+
model.stats.nodeCount = nodeCount;
|
|
54
|
+
}
|
|
55
|
+
async function collectTextures(model, resources, opc, diagnostics) {
|
|
56
|
+
const textures = [];
|
|
57
|
+
for (const r of resources.filter(x => /texture/i.test(x.role ?? '') || /texture/i.test(x.title ?? ''))) {
|
|
58
|
+
if (!opc.zip.has(r.path))
|
|
59
|
+
continue;
|
|
60
|
+
let width;
|
|
61
|
+
let height;
|
|
62
|
+
try {
|
|
63
|
+
const size = imageSizeFromBytes(await opc.readBytes(r.path));
|
|
64
|
+
width = size?.width;
|
|
65
|
+
height = size?.height;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
diagnostics.push(diag('warning', 'EMODEL_TEXTURE_SIZE_FAILED', `Failed to read texture size: ${String(err)}`, r.path));
|
|
69
|
+
}
|
|
70
|
+
textures.push({ id: r.path, name: cleanTitle(r.title) ?? basename(r.path), path: r.path, mediaType: r.mediaType, width, height, role: r.role });
|
|
71
|
+
}
|
|
72
|
+
model.textures = textures;
|
|
73
|
+
if (textures.length > 0) {
|
|
74
|
+
const tex = textures.find(t => /thread/i.test(t.name ?? '')) ?? textures.find(t => !/environment/i.test(t.name ?? '')) ?? textures[0];
|
|
75
|
+
if (tex) {
|
|
76
|
+
const defaultMat = { id: 'dwfx-default-textured', name: 'DWFx default material', color: [0.72, 0.74, 0.78], textureId: tex.id, roughness: 0.6, metalness: 0.03, doubleSided: true, source: tex.path };
|
|
77
|
+
model.materials = [defaultMat];
|
|
78
|
+
// Do not forcibly assign the texture to every mesh; many DWFx files use environment maps or icons.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseDescriptor(model, xml, sourcePath) {
|
|
83
|
+
const doc = parseXml(xml, sourcePath);
|
|
84
|
+
const root = doc.documentElement;
|
|
85
|
+
if (root) {
|
|
86
|
+
const units = Array.from(root.getElementsByTagName('*')).find(e => localName(e) === 'Units');
|
|
87
|
+
const unitType = units?.getAttribute('type');
|
|
88
|
+
if (unitType)
|
|
89
|
+
model.metadata.units = unitType;
|
|
90
|
+
const spaceName = root.getAttribute('name');
|
|
91
|
+
if (spaceName)
|
|
92
|
+
model.metadata.spaceName = spaceName;
|
|
93
|
+
}
|
|
94
|
+
for (const prop of Array.from(doc.getElementsByTagName('*')).filter(e => localName(e) === 'Property')) {
|
|
95
|
+
const name = prop.getAttribute('name');
|
|
96
|
+
const value = prop.getAttribute('value');
|
|
97
|
+
if (name && value)
|
|
98
|
+
model.metadata[name] = value;
|
|
99
|
+
}
|
|
100
|
+
const home = makeViewFromDescriptorProperties(model.metadata);
|
|
101
|
+
if (home) {
|
|
102
|
+
model.views = mergeViews(model.views ?? [], [home]);
|
|
103
|
+
model.initialView = home;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function makeViewFromDescriptorProperties(meta) {
|
|
107
|
+
const position = vector3(meta._ViewCubeHomeCameraPosition);
|
|
108
|
+
const target = vector3(meta._ViewCubeHomeCameraTarget);
|
|
109
|
+
const up = vector3(meta._ViewCubeHomeCameraUpVector);
|
|
110
|
+
const field = vector3(meta._ViewCubeHomeCameraField);
|
|
111
|
+
if (!position && !target && !up)
|
|
112
|
+
return undefined;
|
|
113
|
+
return { id: 'descriptor-home-view', label: 'Home', camera: { position, target, up, field, projection: meta._ViewCubeHomeCameraProjection === '1' ? 'perspective' : 'orthographic' } };
|
|
114
|
+
}
|
|
115
|
+
function parseContentDefinitionSafe(model, xml, sourcePath, diagnostics) {
|
|
116
|
+
try {
|
|
117
|
+
const doc = parseXml(xml, sourcePath);
|
|
118
|
+
const instances = [];
|
|
119
|
+
for (const el of Array.from(doc.getElementsByTagName('*')).filter(e => localName(e) === 'Instance')) {
|
|
120
|
+
const id = el.getAttribute('id') ?? '';
|
|
121
|
+
if (!id)
|
|
122
|
+
continue;
|
|
123
|
+
instances.push({ id, renderableRef: el.getAttribute('renderableRef') ?? undefined, node: el.getAttribute('node') ?? undefined, geometricVariation: el.getAttribute('geometricVariation') ?? undefined });
|
|
124
|
+
}
|
|
125
|
+
model.metadata.contentInstanceCount = String(instances.length);
|
|
126
|
+
return instances;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
diagnostics.push(diag('warning', 'EMODEL_CONTENTDEF_PARSE_FAILED', `Failed to parse eModel content definition: ${String(err)}`, sourcePath));
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function parseContentPresentation(xml, sourcePath, labelByContentRef) {
|
|
134
|
+
const doc = parseXml(xml, sourcePath);
|
|
135
|
+
const topContainers = Array.from(doc.getElementsByTagName('*')).filter(e => localName(e) === 'View');
|
|
136
|
+
const nodes = [];
|
|
137
|
+
for (const view of topContainers) {
|
|
138
|
+
const viewNodes = nodeChildrenUnder(view);
|
|
139
|
+
for (const n of viewNodes)
|
|
140
|
+
nodes.push(parseSceneNode(n, labelByContentRef));
|
|
141
|
+
}
|
|
142
|
+
if (nodes.length === 0) {
|
|
143
|
+
for (const n of Array.from(doc.getElementsByTagName('*')).filter(e => localName(e) === 'ReferenceNode' && !hasAncestorReferenceNode(e)))
|
|
144
|
+
nodes.push(parseSceneNode(n, labelByContentRef));
|
|
145
|
+
}
|
|
146
|
+
const views = [];
|
|
147
|
+
for (const modelView of Array.from(doc.getElementsByTagName('*')).filter(e => /ModelViewNode|View/i.test(localName(e)))) {
|
|
148
|
+
const id = modelView.getAttribute('id') ?? `view-${views.length + 1}`;
|
|
149
|
+
const label = modelView.getAttribute('label') ?? modelView.getAttribute('name') ?? id;
|
|
150
|
+
const camEl = directChildren(modelView).find(e => localName(e) === 'Camera') ?? Array.from(modelView.getElementsByTagName('*')).find(e => localName(e) === 'Camera');
|
|
151
|
+
const camera = camEl ? cameraFromElement(camEl) : undefined;
|
|
152
|
+
if (camera || /ModelViewNode/i.test(localName(modelView)))
|
|
153
|
+
views.push({ id, label, camera });
|
|
154
|
+
}
|
|
155
|
+
const pmi = [];
|
|
156
|
+
for (const el of Array.from(doc.getElementsByTagName('*'))) {
|
|
157
|
+
const ln = localName(el);
|
|
158
|
+
if (!/PMI|Markup|Dimension|Annotation|Callout|Leader|Note/i.test(ln))
|
|
159
|
+
continue;
|
|
160
|
+
pmi.push({ id: el.getAttribute('id') ?? `pmi-${pmi.length + 1}`, type: /Leader/i.test(ln) ? 'leader' : /Text|Note|Dimension/i.test(ln) ? 'text' : 'markup', label: el.getAttribute('label') ?? el.getAttribute('name') ?? ln, text: el.getAttribute('text') ?? el.textContent?.trim() ?? undefined, targetRef: el.getAttribute('contentElementRefs') ?? el.getAttribute('target') ?? undefined, properties: attributesToRecord(el) });
|
|
161
|
+
}
|
|
162
|
+
const animations = [];
|
|
163
|
+
for (const el of Array.from(doc.getElementsByTagName('*')).filter(e => /Animation|Sequence|Timeline/i.test(localName(e)))) {
|
|
164
|
+
animations.push({ id: el.getAttribute('id') ?? `animation-${animations.length + 1}`, name: el.getAttribute('label') ?? el.getAttribute('name') ?? localName(el), targetRef: el.getAttribute('target') ?? undefined });
|
|
165
|
+
}
|
|
166
|
+
const materials = parseInstanceMaterials(doc);
|
|
167
|
+
return { nodes, pmi, views: dedupeViews(views), animations, materials };
|
|
168
|
+
}
|
|
169
|
+
function parseInstanceMaterials(doc) {
|
|
170
|
+
const out = new Map();
|
|
171
|
+
for (const inst of Array.from(doc.getElementsByTagName('*')).filter(e => localName(e) === 'InstanceAttributes')) {
|
|
172
|
+
const id = inst.getAttribute('id');
|
|
173
|
+
if (!id)
|
|
174
|
+
continue;
|
|
175
|
+
const colorEl = directChildren(inst).find(e => localName(e) === 'Color') ?? Array.from(inst.getElementsByTagName('*')).find(e => localName(e) === 'Color');
|
|
176
|
+
if (!colorEl)
|
|
177
|
+
continue;
|
|
178
|
+
const diffuse = Array.from(colorEl.getElementsByTagName('*')).find(e => localName(e) === 'Channel' && /diffuse/i.test(e.getAttribute('type') ?? ''));
|
|
179
|
+
const env = Array.from(colorEl.getElementsByTagName('*')).find(e => localName(e) === 'Channel' && /environment/i.test(e.getAttribute('type') ?? ''));
|
|
180
|
+
const red = Number(diffuse?.getAttribute('red'));
|
|
181
|
+
const green = Number(diffuse?.getAttribute('green'));
|
|
182
|
+
const blue = Number(diffuse?.getAttribute('blue'));
|
|
183
|
+
if (![red, green, blue].every(Number.isFinite))
|
|
184
|
+
continue;
|
|
185
|
+
const gloss = Number(colorEl.getAttribute('gloss'));
|
|
186
|
+
const textureName = cleanTitle(env?.getAttribute('name') ?? undefined);
|
|
187
|
+
out.set(id, {
|
|
188
|
+
id: `material-${id}`,
|
|
189
|
+
name: `Material ${id}`,
|
|
190
|
+
color: [red, green, blue],
|
|
191
|
+
opacity: 1,
|
|
192
|
+
roughness: Number.isFinite(gloss) ? Math.max(0.12, Math.min(0.92, 1 - gloss / 128)) : 0.58,
|
|
193
|
+
metalness: 0.03,
|
|
194
|
+
textureName,
|
|
195
|
+
doubleSided: true,
|
|
196
|
+
source: 'content-presentation'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function bindTextureByName(material, textures) {
|
|
202
|
+
if (material.textureId || !material.textureName)
|
|
203
|
+
return material;
|
|
204
|
+
const norm = normalizeMaterialName(material.textureName);
|
|
205
|
+
const match = textures.find(t => normalizeMaterialName(t.name ?? '').includes(norm) || norm.includes(normalizeMaterialName(t.name ?? '')));
|
|
206
|
+
return match ? { ...material, textureId: match.id } : material;
|
|
207
|
+
}
|
|
208
|
+
function normalizeMaterialName(s) {
|
|
209
|
+
return s.replace(/"/g, '').replace(/["{}]/g, '').toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
function parseSceneNode(el, labelByContentRef) {
|
|
212
|
+
const refs = splitRefs(el.getAttribute('contentElementRefs') ?? el.getAttribute('contentRefs') ?? '');
|
|
213
|
+
const node = {
|
|
214
|
+
id: el.getAttribute('id') ?? `node-${Math.random().toString(36).slice(2)}`,
|
|
215
|
+
label: el.getAttribute('label') ?? el.getAttribute('name') ?? localName(el),
|
|
216
|
+
contentRefs: refs,
|
|
217
|
+
meshIds: [],
|
|
218
|
+
children: [],
|
|
219
|
+
properties: attributesToRecord(el)
|
|
220
|
+
};
|
|
221
|
+
for (const ref of refs)
|
|
222
|
+
labelByContentRef.set(ref, node);
|
|
223
|
+
for (const child of nodeChildrenUnder(el))
|
|
224
|
+
node.children.push(parseSceneNode(child, labelByContentRef));
|
|
225
|
+
return node;
|
|
226
|
+
}
|
|
227
|
+
function attachSelectionMetadata(model, instances, labelByContentRef, instanceMaterials) {
|
|
228
|
+
const labelForRef = new Map(labelByContentRef);
|
|
229
|
+
instances.forEach((inst, i) => {
|
|
230
|
+
const mesh = model.meshes[i];
|
|
231
|
+
if (!mesh)
|
|
232
|
+
return;
|
|
233
|
+
const refs = [inst.renderableRef, inst.id].filter(Boolean);
|
|
234
|
+
mesh.selectionRefs = refs;
|
|
235
|
+
mesh.nodeId = inst.node;
|
|
236
|
+
const mat = instanceMaterials.get(inst.id) ?? (inst.renderableRef ? instanceMaterials.get(inst.renderableRef) : undefined);
|
|
237
|
+
if (mat) {
|
|
238
|
+
mesh.materialId = mat.id;
|
|
239
|
+
mesh.color = mat.color;
|
|
240
|
+
}
|
|
241
|
+
for (const ref of refs) {
|
|
242
|
+
const node = labelForRef.get(ref);
|
|
243
|
+
if (node) {
|
|
244
|
+
mesh.name = node.label || mesh.name;
|
|
245
|
+
node.meshIds.push(mesh.id);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function cameraFromElement(el) {
|
|
252
|
+
const pos = attrsVector3(el, 'positionX', 'positionY', 'positionZ');
|
|
253
|
+
const target = attrsVector3(el, 'targetX', 'targetY', 'targetZ');
|
|
254
|
+
const up = attrsVector3(el, 'upVectorX', 'upVectorY', 'upVectorZ');
|
|
255
|
+
const field = attrsVector3(el, 'fieldWidth', 'fieldHeight', 'fieldDepth');
|
|
256
|
+
return { position: pos, target, up, field, projection: el.getAttribute('projectionType') ?? undefined };
|
|
257
|
+
}
|
|
258
|
+
function attrsVector3(el, x, y, z) {
|
|
259
|
+
const nums = [Number(el.getAttribute(x)), Number(el.getAttribute(y)), Number(el.getAttribute(z))];
|
|
260
|
+
return nums.every(Number.isFinite) ? nums : undefined;
|
|
261
|
+
}
|
|
262
|
+
function vector3(raw) {
|
|
263
|
+
if (!raw)
|
|
264
|
+
return undefined;
|
|
265
|
+
const n = raw.trim().split(/[\s,]+/).map(Number).filter(Number.isFinite);
|
|
266
|
+
return n.length >= 3 ? [n[0], n[1], n[2]] : undefined;
|
|
267
|
+
}
|
|
268
|
+
function directChildren(el) {
|
|
269
|
+
return Array.from(el.children);
|
|
270
|
+
}
|
|
271
|
+
function nodeChildrenUnder(el) {
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const child of directChildren(el)) {
|
|
274
|
+
const ln = localName(child);
|
|
275
|
+
if (ln.endsWith('Node'))
|
|
276
|
+
out.push(child);
|
|
277
|
+
else if (ln === 'Nodes' || ln === 'Views') {
|
|
278
|
+
for (const grand of directChildren(child))
|
|
279
|
+
if (localName(grand).endsWith('Node'))
|
|
280
|
+
out.push(grand);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function hasAncestorReferenceNode(el) {
|
|
286
|
+
let p = parentElementOf(el);
|
|
287
|
+
while (p) {
|
|
288
|
+
if (localName(p) === 'ReferenceNode')
|
|
289
|
+
return true;
|
|
290
|
+
p = parentElementOf(p);
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
function parentElementOf(el) {
|
|
295
|
+
return (el.parentElement ?? el.parentNode) || undefined;
|
|
296
|
+
}
|
|
297
|
+
function attributesToRecord(el) {
|
|
298
|
+
const out = {};
|
|
299
|
+
for (const a of Array.from(el.attributes))
|
|
300
|
+
out[a.localName || a.name] = a.value;
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
function splitRefs(raw) {
|
|
304
|
+
return raw.trim().split(/[\s,]+/).filter(Boolean);
|
|
305
|
+
}
|
|
306
|
+
function mergeViews(a, b) {
|
|
307
|
+
return dedupeViews([...a, ...b]);
|
|
308
|
+
}
|
|
309
|
+
function dedupeViews(views) {
|
|
310
|
+
const seen = new Set();
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const v of views) {
|
|
313
|
+
const key = v.id || v.label;
|
|
314
|
+
if (seen.has(key))
|
|
315
|
+
continue;
|
|
316
|
+
seen.add(key);
|
|
317
|
+
out.push(v);
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
function countSceneNodes(nodes) {
|
|
322
|
+
let n = 0;
|
|
323
|
+
for (const node of nodes)
|
|
324
|
+
n += 1 + countSceneNodes(node.children);
|
|
325
|
+
return n;
|
|
326
|
+
}
|
|
327
|
+
function basename(path) {
|
|
328
|
+
const i = path.lastIndexOf('/');
|
|
329
|
+
return i >= 0 ? path.slice(i + 1) : path;
|
|
330
|
+
}
|
|
331
|
+
function cleanTitle(raw) {
|
|
332
|
+
if (!raw)
|
|
333
|
+
return undefined;
|
|
334
|
+
return raw.replace(/^"|"$/g, '').replace(/"/g, '"');
|
|
335
|
+
}
|
|
336
|
+
function imageSizeFromBytes(bytes) {
|
|
337
|
+
if (bytes.length >= 24 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
|
|
338
|
+
return { width: be32(bytes, 16), height: be32(bytes, 20) };
|
|
339
|
+
}
|
|
340
|
+
if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
341
|
+
let p = 2;
|
|
342
|
+
while (p + 9 < bytes.length) {
|
|
343
|
+
if (bytes[p] !== 0xff) {
|
|
344
|
+
p++;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const marker = bytes[p + 1];
|
|
348
|
+
p += 2;
|
|
349
|
+
while (bytes[p] === 0xff)
|
|
350
|
+
p++;
|
|
351
|
+
if (marker === 0xd9 || marker === 0xda)
|
|
352
|
+
break;
|
|
353
|
+
if (p + 2 > bytes.length)
|
|
354
|
+
break;
|
|
355
|
+
const len = (bytes[p] << 8) | bytes[p + 1];
|
|
356
|
+
if (len < 2 || p + len > bytes.length)
|
|
357
|
+
break;
|
|
358
|
+
if ((marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) || (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)) {
|
|
359
|
+
return { width: (bytes[p + 5] << 8) | bytes[p + 6], height: (bytes[p + 3] << 8) | bytes[p + 4] };
|
|
360
|
+
}
|
|
361
|
+
p += len;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
function be32(bytes, offset) {
|
|
367
|
+
return ((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0;
|
|
368
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function getDecompressionStreamCtor() {
|
|
2
|
+
return globalThis.DecompressionStream;
|
|
3
|
+
}
|
|
4
|
+
export class BrowserInflateProvider {
|
|
5
|
+
async inflateRaw(data) {
|
|
6
|
+
const DS = getDecompressionStreamCtor();
|
|
7
|
+
if (!DS) {
|
|
8
|
+
throw new Error('This browser does not expose DecompressionStream. Provide a custom InflateProvider, or pre-bundle a WASM/JS inflater.');
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return await decompressWithFormat(data, 'deflate-raw', DS);
|
|
12
|
+
}
|
|
13
|
+
catch (rawError) {
|
|
14
|
+
try {
|
|
15
|
+
return await decompressWithFormat(data, 'deflate', DS);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw rawError instanceof Error ? rawError : new Error(String(rawError));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function decompressWithFormat(data, format, DS) {
|
|
24
|
+
const blobStream = new Blob([data]).stream();
|
|
25
|
+
const decompressedStream = blobStream.pipeThrough(new DS(format));
|
|
26
|
+
const ab = await new Response(decompressedStream).arrayBuffer();
|
|
27
|
+
return new Uint8Array(ab);
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResourceRef } from './types.js';
|
|
2
|
+
import { ZipPackage } from './zip.js';
|
|
3
|
+
export interface OpcRelationship {
|
|
4
|
+
id: string;
|
|
5
|
+
type: string;
|
|
6
|
+
target: string;
|
|
7
|
+
targetMode?: string;
|
|
8
|
+
source: string;
|
|
9
|
+
resolvedTarget: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ContentTypeOverride {
|
|
12
|
+
partName: string;
|
|
13
|
+
contentType: string;
|
|
14
|
+
}
|
|
15
|
+
export interface OpcPackageView {
|
|
16
|
+
zip: ZipPackage;
|
|
17
|
+
contentTypes: Map<string, string>;
|
|
18
|
+
defaults: Map<string, string>;
|
|
19
|
+
overrides: ContentTypeOverride[];
|
|
20
|
+
relationships: Map<string, OpcRelationship[]>;
|
|
21
|
+
resources: ResourceRef[];
|
|
22
|
+
readText(path: string): Promise<string>;
|
|
23
|
+
readBytes(path: string): Promise<Uint8Array>;
|
|
24
|
+
getContentType(path: string): string | undefined;
|
|
25
|
+
getRelationships(sourcePath?: string): Promise<OpcRelationship[]>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createOpcView(zip: ZipPackage): Promise<OpcPackageView>;
|
|
28
|
+
export declare function relationshipPartName(sourcePath: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { decodeUtf8, parseXml, normalizePath, resolvePart, localName } from './util.js';
|
|
2
|
+
export async function createOpcView(zip) {
|
|
3
|
+
const contentTypes = new Map();
|
|
4
|
+
const defaults = new Map();
|
|
5
|
+
const overrides = [];
|
|
6
|
+
if (zip.has('[Content_Types].xml')) {
|
|
7
|
+
const doc = parseXml(decodeUtf8(await zip.read('[Content_Types].xml')), '[Content_Types].xml');
|
|
8
|
+
for (const el of Array.from(doc.documentElement.children)) {
|
|
9
|
+
const name = localName(el);
|
|
10
|
+
if (name === 'Default') {
|
|
11
|
+
const ext = el.getAttribute('Extension')?.toLowerCase();
|
|
12
|
+
const ct = el.getAttribute('ContentType');
|
|
13
|
+
if (ext && ct)
|
|
14
|
+
defaults.set(ext, ct);
|
|
15
|
+
}
|
|
16
|
+
else if (name === 'Override') {
|
|
17
|
+
const partName = normalizePath((el.getAttribute('PartName') ?? '').replace(/^\//, ''));
|
|
18
|
+
const ct = el.getAttribute('ContentType');
|
|
19
|
+
if (partName && ct) {
|
|
20
|
+
contentTypes.set(partName, ct);
|
|
21
|
+
overrides.push({ partName, contentType: ct });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const relationships = new Map();
|
|
27
|
+
const view = {
|
|
28
|
+
zip,
|
|
29
|
+
contentTypes,
|
|
30
|
+
defaults,
|
|
31
|
+
overrides,
|
|
32
|
+
relationships,
|
|
33
|
+
resources: zip.entries.map(e => ({ path: e.name, mediaType: getContentTypeFor(contentTypes, defaults, e.name), size: e.uncompressedSize })),
|
|
34
|
+
readText: async (path) => decodeUtf8(await zip.read(normalizePath(path))),
|
|
35
|
+
readBytes: async (path) => zip.read(normalizePath(path)),
|
|
36
|
+
getContentType: (path) => getContentTypeFor(contentTypes, defaults, path),
|
|
37
|
+
getRelationships: async (sourcePath = '') => {
|
|
38
|
+
const key = normalizePath(sourcePath);
|
|
39
|
+
const existing = relationships.get(key);
|
|
40
|
+
if (existing)
|
|
41
|
+
return existing;
|
|
42
|
+
const relsPath = relationshipPartName(key);
|
|
43
|
+
if (!zip.has(relsPath)) {
|
|
44
|
+
relationships.set(key, []);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const xml = decodeUtf8(await zip.read(relsPath));
|
|
48
|
+
const doc = parseXml(xml, relsPath);
|
|
49
|
+
const rels = [];
|
|
50
|
+
for (const el of Array.from(doc.documentElement.children)) {
|
|
51
|
+
if (localName(el) !== 'Relationship')
|
|
52
|
+
continue;
|
|
53
|
+
const id = el.getAttribute('Id') ?? '';
|
|
54
|
+
const type = el.getAttribute('Type') ?? '';
|
|
55
|
+
const target = el.getAttribute('Target') ?? '';
|
|
56
|
+
const targetMode = el.getAttribute('TargetMode') ?? undefined;
|
|
57
|
+
if (!id || !target)
|
|
58
|
+
continue;
|
|
59
|
+
rels.push({ id, type, target, targetMode, source: key, resolvedTarget: targetMode === 'External' ? target : resolvePart(key, target) });
|
|
60
|
+
}
|
|
61
|
+
relationships.set(key, rels);
|
|
62
|
+
return rels;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return view;
|
|
66
|
+
}
|
|
67
|
+
export function relationshipPartName(sourcePath) {
|
|
68
|
+
const source = normalizePath(sourcePath);
|
|
69
|
+
if (!source)
|
|
70
|
+
return '_rels/.rels';
|
|
71
|
+
const slash = source.lastIndexOf('/');
|
|
72
|
+
if (slash < 0)
|
|
73
|
+
return `_rels/${source}.rels`;
|
|
74
|
+
return `${source.slice(0, slash)}/_rels/${source.slice(slash + 1)}.rels`;
|
|
75
|
+
}
|
|
76
|
+
function getContentTypeFor(overrides, defaults, path) {
|
|
77
|
+
const n = normalizePath(path);
|
|
78
|
+
const o = overrides.get(n);
|
|
79
|
+
if (o)
|
|
80
|
+
return o;
|
|
81
|
+
const dot = n.lastIndexOf('.');
|
|
82
|
+
if (dot >= 0)
|
|
83
|
+
return defaults.get(n.slice(dot + 1).toLowerCase());
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { diag } from './types.js';
|
|
2
|
+
import { looksLikeZip, ZipPackage } from './zip.js';
|
|
3
|
+
import { isProbablyDwfx, openDwfx } from './dwfx.js';
|
|
4
|
+
import { isProbablyClassicDwf, openClassicDwf } from './dwf.js';
|
|
5
|
+
import { bytesLookTextual } from './util.js';
|
|
6
|
+
import { parseW2dText } from './w2dText.js';
|
|
7
|
+
import { makeLoadedDocument } from './document.js';
|
|
8
|
+
export async function openDwfDocument(input, options = {}) {
|
|
9
|
+
const bytes = await toBytes(input);
|
|
10
|
+
const fileName = options.fileName ?? ((input instanceof File) ? input.name : undefined) ?? 'document.dwf';
|
|
11
|
+
if (looksLikeZip(bytes)) {
|
|
12
|
+
const zip = ZipPackage.open(bytes, options.inflater);
|
|
13
|
+
if (isProbablyDwfx(zip))
|
|
14
|
+
return await openDwfx(zip);
|
|
15
|
+
if (isProbablyClassicDwf(zip))
|
|
16
|
+
return await openClassicDwf(zip);
|
|
17
|
+
// Mixed or unknown ZIP: try DWFx first if fixed pages exist, then classic discovery.
|
|
18
|
+
if (zip.entries.some(e => /\.fpage$/i.test(e.name)))
|
|
19
|
+
return await openDwfx(zip);
|
|
20
|
+
return await openClassicDwf(zip);
|
|
21
|
+
}
|
|
22
|
+
if (bytesLookTextual(bytes)) {
|
|
23
|
+
const parsed = parseW2dText(bytes, fileName);
|
|
24
|
+
const pageData = [{
|
|
25
|
+
id: 'page-1',
|
|
26
|
+
name: fileName,
|
|
27
|
+
kind: 'w2d-text',
|
|
28
|
+
width: parsed.bounds ? Math.max(1, parsed.bounds.maxX - parsed.bounds.minX) : 1000,
|
|
29
|
+
height: parsed.bounds ? Math.max(1, parsed.bounds.maxY - parsed.bounds.minY) : 1000,
|
|
30
|
+
sourcePath: fileName,
|
|
31
|
+
primitives: parsed.primitives,
|
|
32
|
+
bounds: parsed.bounds,
|
|
33
|
+
diagnostics: parsed.diagnostics
|
|
34
|
+
}];
|
|
35
|
+
const base = {
|
|
36
|
+
kind: 'unknown',
|
|
37
|
+
pages: [],
|
|
38
|
+
resources: [{ path: fileName, mediaType: 'text/plain', size: bytes.byteLength }],
|
|
39
|
+
diagnostics: [diag('warning', 'RAW_TEXT_W2D_MODE', 'Input is not a DWF ZIP package; opened it as a textual W2D-like vector stream.', fileName)],
|
|
40
|
+
packageEntries: [fileName]
|
|
41
|
+
};
|
|
42
|
+
return makeLoadedDocument(base, pageData);
|
|
43
|
+
}
|
|
44
|
+
const unsupported = {
|
|
45
|
+
id: 'unsupported-raw',
|
|
46
|
+
name: fileName,
|
|
47
|
+
kind: 'unsupported',
|
|
48
|
+
width: 1000,
|
|
49
|
+
height: 1000,
|
|
50
|
+
sourcePath: fileName,
|
|
51
|
+
reason: 'Input is neither a ZIP-based DWF/DWFx package nor a supported textual W2D stream.',
|
|
52
|
+
diagnostics: [diag('error', 'UNSUPPORTED_RAW_FORMAT', 'Unsupported raw DWF input. DWF 6+/DWFx ZIP packages are required for this build.', fileName)]
|
|
53
|
+
};
|
|
54
|
+
const base = {
|
|
55
|
+
kind: 'unknown',
|
|
56
|
+
pages: [],
|
|
57
|
+
resources: [{ path: fileName, size: bytes.byteLength }],
|
|
58
|
+
diagnostics: unsupported.diagnostics,
|
|
59
|
+
packageEntries: [fileName]
|
|
60
|
+
};
|
|
61
|
+
return makeLoadedDocument(base, [unsupported]);
|
|
62
|
+
}
|
|
63
|
+
async function toBytes(input) {
|
|
64
|
+
if (input instanceof Uint8Array)
|
|
65
|
+
return input;
|
|
66
|
+
if (input instanceof ArrayBuffer)
|
|
67
|
+
return new Uint8Array(input);
|
|
68
|
+
return new Uint8Array(await input.arrayBuffer());
|
|
69
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type DiagnosticLevel = 'info' | 'warning' | 'error';
|
|
2
|
+
export interface Diagnostic {
|
|
3
|
+
level: DiagnosticLevel;
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
source?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ResourceRef {
|
|
9
|
+
path: string;
|
|
10
|
+
mediaType?: string;
|
|
11
|
+
role?: string;
|
|
12
|
+
size?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RenderViewport {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
scale: number;
|
|
18
|
+
offsetX: number;
|
|
19
|
+
offsetY: number;
|
|
20
|
+
devicePixelRatio: number;
|
|
21
|
+
}
|
|
22
|
+
export type PageKind = 'xps-fixed-page' | 'w2d-text' | 'image' | 'w3d-model' | 'unsupported';
|
|
23
|
+
export interface RenderablePage {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
kind: PageKind;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
sourcePath?: string;
|
|
30
|
+
diagnostics: Diagnostic[];
|
|
31
|
+
}
|
|
32
|
+
export interface DwfDocument {
|
|
33
|
+
kind: 'dwf' | 'dwfx' | 'zip-dwf' | 'unknown';
|
|
34
|
+
pages: RenderablePage[];
|
|
35
|
+
resources: ResourceRef[];
|
|
36
|
+
diagnostics: Diagnostic[];
|
|
37
|
+
packageEntries: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface OpenOptions {
|
|
40
|
+
fileName?: string;
|
|
41
|
+
inflater?: InflateProvider;
|
|
42
|
+
}
|
|
43
|
+
export interface InflateProvider {
|
|
44
|
+
inflateRaw(data: Uint8Array): Promise<Uint8Array>;
|
|
45
|
+
}
|
|
46
|
+
export interface PageRenderOptions {
|
|
47
|
+
pageIndex?: number;
|
|
48
|
+
preferWebgl?: boolean;
|
|
49
|
+
preferWasm?: boolean;
|
|
50
|
+
wasmUrl?: string;
|
|
51
|
+
background?: string;
|
|
52
|
+
maxGpuCacheBytes?: number;
|
|
53
|
+
maxCachedScenes?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface RenderStats {
|
|
56
|
+
backend: 'canvas2d' | 'wasm-raster' | 'webgl' | 'threejs-webgl' | 'image' | 'unsupported';
|
|
57
|
+
commands: number;
|
|
58
|
+
warnings: Diagnostic[];
|
|
59
|
+
}
|
|
60
|
+
export declare function diag(level: DiagnosticLevel, code: string, message: string, source?: string): Diagnostic;
|
|
61
|
+
export declare function actionableDiagnostics(diagnostics: readonly Diagnostic[] | undefined): Diagnostic[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function diag(level, code, message, source) {
|
|
2
|
+
return source === undefined ? { level, code, message } : { level, code, message, source };
|
|
3
|
+
}
|
|
4
|
+
export function actionableDiagnostics(diagnostics) {
|
|
5
|
+
return (diagnostics ?? []).filter(d => d.level !== 'info');
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const textDecoder: TextDecoder;
|
|
2
|
+
export declare const asciiDecoder: TextDecoder;
|
|
3
|
+
export declare function decodeUtf8(bytes: Uint8Array): string;
|
|
4
|
+
export declare function decodeUtf16Le(bytes: Uint8Array): string;
|
|
5
|
+
export declare function normalizePath(path: string): string;
|
|
6
|
+
export declare function dirname(path: string): string;
|
|
7
|
+
export declare function resolvePart(basePath: string, target: string): string;
|
|
8
|
+
export declare function stripNamespace(name: string): string;
|
|
9
|
+
export declare function localName(el: Element): string;
|
|
10
|
+
export declare function getAttr(el: Element, name: string): string | undefined;
|
|
11
|
+
export declare function childElements(el: ParentNode): Element[];
|
|
12
|
+
export declare function parseXml(xml: string, source?: string): Document;
|
|
13
|
+
export declare function bytesLookTextual(bytes: Uint8Array, sampleLength?: number): boolean;
|
|
14
|
+
export declare function extname(path: string): string;
|
|
15
|
+
export declare function mimeFromPath(path: string): string | undefined;
|
|
16
|
+
export declare function blobToImage(bytes: Uint8Array, type: string): Promise<HTMLImageElement | ImageBitmap>;
|
|
17
|
+
export declare function clamp(n: number, lo: number, hi: number): number;
|
|
18
|
+
export declare function parseNumberList(input: string): number[];
|