@flyfish-dev/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,30 @@
|
|
|
1
|
+
import type { InflateProvider } from './types.js';
|
|
2
|
+
export interface ZipEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
compressedSize: number;
|
|
5
|
+
uncompressedSize: number;
|
|
6
|
+
compressionMethod: number;
|
|
7
|
+
crc32: number;
|
|
8
|
+
localHeaderOffset: number;
|
|
9
|
+
centralHeaderOffset: number;
|
|
10
|
+
flags: number;
|
|
11
|
+
lastModTime: number;
|
|
12
|
+
lastModDate: number;
|
|
13
|
+
comment: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class ZipPackage {
|
|
16
|
+
readonly entries: ZipEntry[];
|
|
17
|
+
readonly entryMap: Map<string, ZipEntry>;
|
|
18
|
+
readonly archiveBase: number;
|
|
19
|
+
private readonly bytes;
|
|
20
|
+
private readonly inflater;
|
|
21
|
+
private readonly cache;
|
|
22
|
+
private constructor();
|
|
23
|
+
static open(bytes: Uint8Array, inflater?: InflateProvider): ZipPackage;
|
|
24
|
+
has(name: string): boolean;
|
|
25
|
+
get(name: string): ZipEntry | undefined;
|
|
26
|
+
find(predicate: (entry: ZipEntry) => boolean): ZipEntry | undefined;
|
|
27
|
+
findAll(predicate: (entry: ZipEntry) => boolean): ZipEntry[];
|
|
28
|
+
read(entryOrName: ZipEntry | string): Promise<Uint8Array>;
|
|
29
|
+
}
|
|
30
|
+
export declare function looksLikeZip(bytes: Uint8Array): boolean;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { BrowserInflateProvider } from './inflate.js';
|
|
2
|
+
import { decodeUtf8, normalizePath } from './util.js';
|
|
3
|
+
const SIG_EOCD = 0x06054b50;
|
|
4
|
+
const SIG_CENTRAL = 0x02014b50;
|
|
5
|
+
const SIG_LOCAL = 0x04034b50;
|
|
6
|
+
export class ZipPackage {
|
|
7
|
+
constructor(bytes, entries, archiveBase, inflater) {
|
|
8
|
+
this.entryMap = new Map();
|
|
9
|
+
this.cache = new Map();
|
|
10
|
+
this.bytes = bytes;
|
|
11
|
+
this.entries = entries;
|
|
12
|
+
this.archiveBase = archiveBase;
|
|
13
|
+
this.inflater = inflater ?? new BrowserInflateProvider();
|
|
14
|
+
for (const e of entries)
|
|
15
|
+
this.entryMap.set(normalizePath(e.name), e);
|
|
16
|
+
}
|
|
17
|
+
static open(bytes, inflater) {
|
|
18
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
19
|
+
const eocdOffset = findEocd(view);
|
|
20
|
+
if (eocdOffset < 0)
|
|
21
|
+
throw new Error('ZIP end-of-central-directory record was not found. This is not a DWF 6+/DWFx ZIP package.');
|
|
22
|
+
const diskNo = u16(view, eocdOffset + 4);
|
|
23
|
+
const cdDiskNo = u16(view, eocdOffset + 6);
|
|
24
|
+
if (diskNo !== 0 || cdDiskNo !== 0)
|
|
25
|
+
throw new Error('Multi-disk ZIP archives are not supported.');
|
|
26
|
+
const entryCount = u16(view, eocdOffset + 10);
|
|
27
|
+
const centralSize = u32(view, eocdOffset + 12);
|
|
28
|
+
const centralOffset = u32(view, eocdOffset + 16);
|
|
29
|
+
// ZIP offsets are relative to the beginning of the ZIP archive. DWF files may prepend a textual
|
|
30
|
+
// DWF header before the actual PK stream, so derive the base by locating the central directory.
|
|
31
|
+
let archiveBase = 0;
|
|
32
|
+
let centralAbs = centralOffset;
|
|
33
|
+
if (u32(view, centralAbs) !== SIG_CENTRAL) {
|
|
34
|
+
const maxShift = Math.max(0, eocdOffset - centralSize - 1024 * 1024);
|
|
35
|
+
let found = -1;
|
|
36
|
+
for (let p = eocdOffset - centralSize; p >= maxShift; p--) {
|
|
37
|
+
if (p >= 0 && u32(view, p) === SIG_CENTRAL) {
|
|
38
|
+
found = p;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (found < 0)
|
|
43
|
+
throw new Error('ZIP central directory could not be located.');
|
|
44
|
+
centralAbs = found;
|
|
45
|
+
archiveBase = centralAbs - centralOffset;
|
|
46
|
+
}
|
|
47
|
+
const entries = [];
|
|
48
|
+
let ptr = centralAbs;
|
|
49
|
+
for (let i = 0; i < entryCount; i++) {
|
|
50
|
+
if (u32(view, ptr) !== SIG_CENTRAL)
|
|
51
|
+
throw new Error(`Bad central directory signature at ${ptr}.`);
|
|
52
|
+
const flags = u16(view, ptr + 8);
|
|
53
|
+
const method = u16(view, ptr + 10);
|
|
54
|
+
const lastModTime = u16(view, ptr + 12);
|
|
55
|
+
const lastModDate = u16(view, ptr + 14);
|
|
56
|
+
const crc32 = u32(view, ptr + 16);
|
|
57
|
+
const compressedSize = u32(view, ptr + 20);
|
|
58
|
+
const uncompressedSize = u32(view, ptr + 24);
|
|
59
|
+
const nameLen = u16(view, ptr + 28);
|
|
60
|
+
const extraLen = u16(view, ptr + 30);
|
|
61
|
+
const commentLen = u16(view, ptr + 32);
|
|
62
|
+
const localHeaderOffset = u32(view, ptr + 42);
|
|
63
|
+
const nameBytes = bytes.subarray(ptr + 46, ptr + 46 + nameLen);
|
|
64
|
+
const commentBytes = bytes.subarray(ptr + 46 + nameLen + extraLen, ptr + 46 + nameLen + extraLen + commentLen);
|
|
65
|
+
const name = normalizePath(decodeFileName(nameBytes, flags));
|
|
66
|
+
const comment = decodeFileName(commentBytes, flags);
|
|
67
|
+
if (!name.endsWith('/')) {
|
|
68
|
+
entries.push({ name, compressedSize, uncompressedSize, compressionMethod: method, crc32, localHeaderOffset, centralHeaderOffset: ptr - archiveBase, flags, lastModTime, lastModDate, comment });
|
|
69
|
+
}
|
|
70
|
+
ptr += 46 + nameLen + extraLen + commentLen;
|
|
71
|
+
}
|
|
72
|
+
return new ZipPackage(bytes, entries, archiveBase, inflater);
|
|
73
|
+
}
|
|
74
|
+
has(name) {
|
|
75
|
+
return this.entryMap.has(normalizePath(name));
|
|
76
|
+
}
|
|
77
|
+
get(name) {
|
|
78
|
+
return this.entryMap.get(normalizePath(name));
|
|
79
|
+
}
|
|
80
|
+
find(predicate) {
|
|
81
|
+
return this.entries.find(predicate);
|
|
82
|
+
}
|
|
83
|
+
findAll(predicate) {
|
|
84
|
+
return this.entries.filter(predicate);
|
|
85
|
+
}
|
|
86
|
+
async read(entryOrName) {
|
|
87
|
+
const entry = typeof entryOrName === 'string' ? this.get(entryOrName) : entryOrName;
|
|
88
|
+
if (!entry)
|
|
89
|
+
throw new Error(`ZIP entry not found: ${entryOrName}`);
|
|
90
|
+
const cached = this.cache.get(entry.name);
|
|
91
|
+
if (cached)
|
|
92
|
+
return cached;
|
|
93
|
+
const view = new DataView(this.bytes.buffer, this.bytes.byteOffset, this.bytes.byteLength);
|
|
94
|
+
const localAbs = this.archiveBase + entry.localHeaderOffset;
|
|
95
|
+
if (u32(view, localAbs) !== SIG_LOCAL)
|
|
96
|
+
throw new Error(`Bad local header signature for ${entry.name}.`);
|
|
97
|
+
const nameLen = u16(view, localAbs + 26);
|
|
98
|
+
const extraLen = u16(view, localAbs + 28);
|
|
99
|
+
const dataStart = localAbs + 30 + nameLen + extraLen;
|
|
100
|
+
const compressed = this.bytes.subarray(dataStart, dataStart + entry.compressedSize);
|
|
101
|
+
let out;
|
|
102
|
+
if (entry.compressionMethod === 0)
|
|
103
|
+
out = compressed.slice();
|
|
104
|
+
else if (entry.compressionMethod === 8)
|
|
105
|
+
out = await this.inflater.inflateRaw(compressed);
|
|
106
|
+
else
|
|
107
|
+
throw new Error(`Unsupported ZIP compression method ${entry.compressionMethod} for ${entry.name}.`);
|
|
108
|
+
if (entry.uncompressedSize !== 0 && out.byteLength !== entry.uncompressedSize) {
|
|
109
|
+
// Some producers write zero or ZIP64 placeholders; only enforce when central directory has a normal size.
|
|
110
|
+
if (entry.uncompressedSize < 0xffffffff) {
|
|
111
|
+
throw new Error(`ZIP entry size mismatch for ${entry.name}: expected ${entry.uncompressedSize}, got ${out.byteLength}.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.cache.set(entry.name, out);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function looksLikeZip(bytes) {
|
|
119
|
+
if (bytes.byteLength < 22)
|
|
120
|
+
return false;
|
|
121
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
122
|
+
if (u32(view, 0) === SIG_LOCAL)
|
|
123
|
+
return true;
|
|
124
|
+
return findEocd(view) >= 0;
|
|
125
|
+
}
|
|
126
|
+
function findEocd(view) {
|
|
127
|
+
const min = Math.max(0, view.byteLength - 0xffff - 22);
|
|
128
|
+
for (let p = view.byteLength - 22; p >= min; p--) {
|
|
129
|
+
if (u32(view, p) === SIG_EOCD)
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
return -1;
|
|
133
|
+
}
|
|
134
|
+
function decodeFileName(bytes, flags) {
|
|
135
|
+
// Bit 11 indicates UTF-8. For legacy DWF names, ASCII-compatible decoding is usually enough.
|
|
136
|
+
if ((flags & (1 << 11)) !== 0)
|
|
137
|
+
return decodeUtf8(bytes);
|
|
138
|
+
return Array.from(bytes, b => String.fromCharCode(b)).join('');
|
|
139
|
+
}
|
|
140
|
+
function u16(view, offset) { return view.getUint16(offset, true); }
|
|
141
|
+
function u32(view, offset) { return view.getUint32(offset, true); }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { openDwfDocument } from './format/open.js';
|
|
2
|
+
export { ZipPackage, looksLikeZip } from './format/zip.js';
|
|
3
|
+
export { BrowserInflateProvider } from './format/inflate.js';
|
|
4
|
+
export { DwfViewer } from './viewer/DwfViewer.js';
|
|
5
|
+
export type { DwfViewerOptions, LoadOptions } from './viewer/DwfViewer.js';
|
|
6
|
+
export { PageRenderer } from './render/PageRenderer.js';
|
|
7
|
+
export { WasmRasterBackend } from './wasm/WasmRasterBackend.js';
|
|
8
|
+
export { WebGlW2dBackend } from './render/WebGlW2dBackend.js';
|
|
9
|
+
export { ThreeW3dRenderer } from './render/ThreeW3dRenderer.js';
|
|
10
|
+
export { createThreeGroupFromW3d } from './render/ThreeJsSceneAdapter.js';
|
|
11
|
+
export type { LoadedDwfDocument, PageData, XpsPageData, W2dTextPageData, ImagePageData, UnsupportedPageData, W3dPageData, W3dModelData, W3dMeshData, W2dPrimitive } from './format/document.js';
|
|
12
|
+
export type { DwfDocument, RenderablePage, Diagnostic, RenderStats, OpenOptions, InflateProvider } from './format/types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { openDwfDocument } from './format/open.js';
|
|
2
|
+
export { ZipPackage, looksLikeZip } from './format/zip.js';
|
|
3
|
+
export { BrowserInflateProvider } from './format/inflate.js';
|
|
4
|
+
export { DwfViewer } from './viewer/DwfViewer.js';
|
|
5
|
+
export { PageRenderer } from './render/PageRenderer.js';
|
|
6
|
+
export { WasmRasterBackend } from './wasm/WasmRasterBackend.js';
|
|
7
|
+
export { WebGlW2dBackend } from './render/WebGlW2dBackend.js';
|
|
8
|
+
export { ThreeW3dRenderer } from './render/ThreeW3dRenderer.js';
|
|
9
|
+
export { createThreeGroupFromW3d } from './render/ThreeJsSceneAdapter.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type RenderStats } from '../format/types.js';
|
|
2
|
+
import type { LoadedDwfDocument } from '../format/document.js';
|
|
3
|
+
export interface GenericRenderOptions {
|
|
4
|
+
zoom?: number;
|
|
5
|
+
panX?: number;
|
|
6
|
+
panY?: number;
|
|
7
|
+
preferWebgl?: boolean;
|
|
8
|
+
preferWasm?: boolean;
|
|
9
|
+
wasmUrl?: string;
|
|
10
|
+
background?: string;
|
|
11
|
+
maxGpuCacheBytes?: number;
|
|
12
|
+
maxCachedScenes?: number;
|
|
13
|
+
webglCanvas?: HTMLCanvasElement;
|
|
14
|
+
yaw?: number;
|
|
15
|
+
pitch?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class PageRenderer {
|
|
18
|
+
private readonly document;
|
|
19
|
+
private xps?;
|
|
20
|
+
private w2d?;
|
|
21
|
+
private w3d?;
|
|
22
|
+
constructor(document: LoadedDwfDocument);
|
|
23
|
+
render(pageIndex: number, canvas: HTMLCanvasElement, options?: GenericRenderOptions): Promise<RenderStats>;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
private renderImage;
|
|
26
|
+
private renderUnsupported;
|
|
27
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { actionableDiagnostics, diag } from '../format/types.js';
|
|
2
|
+
import { blobToImage } from '../format/util.js';
|
|
3
|
+
import { XpsRenderer } from './XpsRenderer.js';
|
|
4
|
+
import { W2dRenderer } from './W2dRenderer.js';
|
|
5
|
+
import { ThreeW3dRenderer } from './ThreeW3dRenderer.js';
|
|
6
|
+
export class PageRenderer {
|
|
7
|
+
constructor(document) {
|
|
8
|
+
this.document = document;
|
|
9
|
+
}
|
|
10
|
+
async render(pageIndex, canvas, options = {}) {
|
|
11
|
+
const page = this.document.pageData[pageIndex];
|
|
12
|
+
if (!page)
|
|
13
|
+
throw new Error(`Page index out of range: ${pageIndex}`);
|
|
14
|
+
if (page.kind !== 'w2d-text' && page.kind !== 'w3d-model' && options.webglCanvas)
|
|
15
|
+
options.webglCanvas.style.visibility = 'hidden';
|
|
16
|
+
if (page.kind === 'xps-fixed-page') {
|
|
17
|
+
this.xps ?? (this.xps = new XpsRenderer(this.document));
|
|
18
|
+
return await this.xps.render(page, canvas, options);
|
|
19
|
+
}
|
|
20
|
+
if (page.kind === 'w2d-text') {
|
|
21
|
+
this.w2d ?? (this.w2d = new W2dRenderer());
|
|
22
|
+
return await this.w2d.render(page, canvas, options);
|
|
23
|
+
}
|
|
24
|
+
if (page.kind === 'w3d-model') {
|
|
25
|
+
this.w3d ?? (this.w3d = new ThreeW3dRenderer());
|
|
26
|
+
return await this.w3d.render(page, canvas, options);
|
|
27
|
+
}
|
|
28
|
+
if (page.kind === 'image')
|
|
29
|
+
return await this.renderImage(page, canvas, options);
|
|
30
|
+
return this.renderUnsupported(page, canvas, options);
|
|
31
|
+
}
|
|
32
|
+
dispose() {
|
|
33
|
+
this.w2d?.dispose();
|
|
34
|
+
this.w3d?.dispose();
|
|
35
|
+
}
|
|
36
|
+
async renderImage(page, canvas, options) {
|
|
37
|
+
const zip = this.document.zip;
|
|
38
|
+
if (!zip)
|
|
39
|
+
throw new Error('Image page requires a ZIP package.');
|
|
40
|
+
const ctx = canvas.getContext('2d');
|
|
41
|
+
if (!ctx)
|
|
42
|
+
throw new Error('CanvasRenderingContext2D is not available.');
|
|
43
|
+
ctx.save();
|
|
44
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
45
|
+
ctx.fillStyle = options.background ?? '#ffffff';
|
|
46
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
47
|
+
ctx.restore();
|
|
48
|
+
const image = await blobToImage(await zip.read(page.sourcePath), page.mediaType);
|
|
49
|
+
const iw = 'width' in image ? image.width : image.naturalWidth;
|
|
50
|
+
const ih = 'height' in image ? image.height : image.naturalHeight;
|
|
51
|
+
const scale = Math.min(canvas.width / Math.max(1, iw), canvas.height / Math.max(1, ih)) * (options.zoom ?? 1);
|
|
52
|
+
const w = iw * scale, h = ih * scale;
|
|
53
|
+
const x = (canvas.width - w) / 2 + (options.panX ?? 0);
|
|
54
|
+
const y = (canvas.height - h) / 2 + (options.panY ?? 0);
|
|
55
|
+
ctx.drawImage(image, x, y, w, h);
|
|
56
|
+
return { backend: 'image', commands: 1, warnings: actionableDiagnostics(page.diagnostics) };
|
|
57
|
+
}
|
|
58
|
+
renderUnsupported(page, canvas, options) {
|
|
59
|
+
const ctx = canvas.getContext('2d');
|
|
60
|
+
if (!ctx)
|
|
61
|
+
throw new Error('CanvasRenderingContext2D is not available.');
|
|
62
|
+
ctx.save();
|
|
63
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
64
|
+
ctx.fillStyle = options.background ?? '#ffffff';
|
|
65
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
66
|
+
ctx.fillStyle = '#111827';
|
|
67
|
+
ctx.font = '16px sans-serif';
|
|
68
|
+
wrapText(ctx, page.name, 32, 48, canvas.width - 64, 24);
|
|
69
|
+
ctx.font = '13px sans-serif';
|
|
70
|
+
ctx.fillStyle = '#92400e';
|
|
71
|
+
wrapText(ctx, page.reason, 32, 88, canvas.width - 64, 20);
|
|
72
|
+
ctx.restore();
|
|
73
|
+
return { backend: 'unsupported', commands: 0, warnings: [...page.diagnostics, diag('warning', 'PAGE_UNSUPPORTED', page.reason, page.sourcePath)] };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
|
|
77
|
+
const words = text.split(/\s+/);
|
|
78
|
+
let line = '';
|
|
79
|
+
for (const word of words) {
|
|
80
|
+
const test = line ? `${line} ${word}` : word;
|
|
81
|
+
if (ctx.measureText(test).width > maxWidth && line) {
|
|
82
|
+
ctx.fillText(line, x, y);
|
|
83
|
+
line = word;
|
|
84
|
+
y += lineHeight;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
line = test;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (line)
|
|
91
|
+
ctx.fillText(line, x, y);
|
|
92
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { W3dMaterialData, W3dPageData, W3dModelData, W3dTextureData } from '../format/document.js';
|
|
2
|
+
export interface ThreeLikeNamespace {
|
|
3
|
+
Group: new () => any;
|
|
4
|
+
BufferGeometry: new () => any;
|
|
5
|
+
BufferAttribute: new (array: ArrayLike<number>, itemSize: number) => any;
|
|
6
|
+
MeshStandardMaterial: new (options?: Record<string, unknown>) => any;
|
|
7
|
+
MeshBasicMaterial?: new (options?: Record<string, unknown>) => any;
|
|
8
|
+
LineBasicMaterial?: new (options?: Record<string, unknown>) => any;
|
|
9
|
+
Mesh: new (geometry: any, material: any) => any;
|
|
10
|
+
LineSegments?: new (geometry: any, material: any) => any;
|
|
11
|
+
Color?: new (r: number, g: number, b: number) => any;
|
|
12
|
+
DoubleSide?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ThreeAdapterOptions {
|
|
15
|
+
doubleSide?: boolean;
|
|
16
|
+
roughness?: number;
|
|
17
|
+
metalness?: number;
|
|
18
|
+
showFeatureEdges?: boolean;
|
|
19
|
+
edgeColor?: number | string;
|
|
20
|
+
textureResolver?: (texture: W3dTextureData, material: W3dMaterialData) => unknown;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Convert a decoded DWFx/W3D model into a real THREE.Group.
|
|
24
|
+
*
|
|
25
|
+
* This helper accepts the THREE namespace as an argument so the core viewer remains
|
|
26
|
+
* buildable offline without bundling npm dependencies. Pass `textureResolver` to
|
|
27
|
+
* bind DWFx package image resources to Three.js Texture objects in your app layer.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createThreeGroupFromW3d(pageOrModel: W3dPageData | W3dModelData, THREE: ThreeLikeNamespace, options?: ThreeAdapterOptions): any;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a decoded DWFx/W3D model into a real THREE.Group.
|
|
3
|
+
*
|
|
4
|
+
* This helper accepts the THREE namespace as an argument so the core viewer remains
|
|
5
|
+
* buildable offline without bundling npm dependencies. Pass `textureResolver` to
|
|
6
|
+
* bind DWFx package image resources to Three.js Texture objects in your app layer.
|
|
7
|
+
*/
|
|
8
|
+
export function createThreeGroupFromW3d(pageOrModel, THREE, options = {}) {
|
|
9
|
+
const model = pageOrModel.model ?? pageOrModel;
|
|
10
|
+
const group = new THREE.Group();
|
|
11
|
+
group.name = model.title ?? 'DWFx W3D Model';
|
|
12
|
+
const materialById = new Map((model.materials ?? []).map(m => [m.id, m]));
|
|
13
|
+
const textureById = new Map((model.textures ?? []).map(t => [t.id, t]));
|
|
14
|
+
for (const mesh of model.meshes) {
|
|
15
|
+
const geometry = new THREE.BufferGeometry();
|
|
16
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(mesh.positions, 3));
|
|
17
|
+
if (mesh.normals)
|
|
18
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(mesh.normals, 3));
|
|
19
|
+
geometry.setIndex(new THREE.BufferAttribute(mesh.indices, 1));
|
|
20
|
+
const srcMaterial = mesh.materialId ? materialById.get(mesh.materialId) : undefined;
|
|
21
|
+
const color = srcMaterial?.color ?? mesh.color ?? [0.72, 0.74, 0.78];
|
|
22
|
+
const materialOptions = {
|
|
23
|
+
color: THREE.Color ? new THREE.Color(color[0], color[1], color[2]) : undefined,
|
|
24
|
+
roughness: srcMaterial?.roughness ?? options.roughness ?? 0.55,
|
|
25
|
+
metalness: srcMaterial?.metalness ?? options.metalness ?? 0.04,
|
|
26
|
+
transparent: srcMaterial?.opacity !== undefined && srcMaterial.opacity < 1,
|
|
27
|
+
opacity: srcMaterial?.opacity ?? 1
|
|
28
|
+
};
|
|
29
|
+
if ((options.doubleSide || srcMaterial?.doubleSided) && THREE.DoubleSide !== undefined)
|
|
30
|
+
materialOptions.side = THREE.DoubleSide;
|
|
31
|
+
const tex = srcMaterial?.textureId ? textureById.get(srcMaterial.textureId) : undefined;
|
|
32
|
+
if (tex && options.textureResolver)
|
|
33
|
+
materialOptions.map = options.textureResolver(tex, srcMaterial);
|
|
34
|
+
const material = new THREE.MeshStandardMaterial(materialOptions);
|
|
35
|
+
const obj = new THREE.Mesh(geometry, material);
|
|
36
|
+
obj.name = mesh.name;
|
|
37
|
+
obj.userData = { sourceStart: mesh.sourceStart, sourceEnd: mesh.sourceEnd, decodeKind: mesh.decodeKind, selectionRefs: mesh.selectionRefs, nodeId: mesh.nodeId, materialId: mesh.materialId };
|
|
38
|
+
group.add(obj);
|
|
39
|
+
if (options.showFeatureEdges !== false && mesh.edgeIndices && THREE.LineSegments && THREE.LineBasicMaterial) {
|
|
40
|
+
const edgeGeometry = new THREE.BufferGeometry();
|
|
41
|
+
edgeGeometry.setAttribute('position', new THREE.BufferAttribute(mesh.positions, 3));
|
|
42
|
+
edgeGeometry.setIndex(new THREE.BufferAttribute(mesh.edgeIndices, 1));
|
|
43
|
+
const edgeMaterial = new THREE.LineBasicMaterial({ color: options.edgeColor ?? 0x111111, transparent: true, opacity: 0.52 });
|
|
44
|
+
const edges = new THREE.LineSegments(edgeGeometry, edgeMaterial);
|
|
45
|
+
edges.name = `${mesh.name} feature edges`;
|
|
46
|
+
edges.userData = { sourceMeshId: mesh.id, selectionRefs: mesh.selectionRefs, nodeId: mesh.nodeId };
|
|
47
|
+
group.add(edges);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
group.userData = { bounds: model.bounds, stats: model.stats, sourcePath: model.sourcePath, sceneTree: model.sceneTree, pmi: model.pmi, views: model.views, textures: model.textures, materials: model.materials };
|
|
51
|
+
return group;
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type RenderStats } from '../format/types.js';
|
|
2
|
+
import type { W3dPageData } from '../format/document.js';
|
|
3
|
+
import type { GenericRenderOptions } from './PageRenderer.js';
|
|
4
|
+
export interface W3dCameraOptions {
|
|
5
|
+
yaw?: number;
|
|
6
|
+
pitch?: number;
|
|
7
|
+
zoom?: number;
|
|
8
|
+
panX?: number;
|
|
9
|
+
panY?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class ThreeW3dRenderer {
|
|
12
|
+
private gl?;
|
|
13
|
+
private program?;
|
|
14
|
+
private edgeProgram?;
|
|
15
|
+
private cache;
|
|
16
|
+
private currentCanvas?;
|
|
17
|
+
render(page: W3dPageData, overlayCanvas: HTMLCanvasElement, options?: GenericRenderOptions): Promise<RenderStats>;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
private ensureContext;
|
|
20
|
+
private ensureProgram;
|
|
21
|
+
private ensureEdgeProgram;
|
|
22
|
+
private evictScenes;
|
|
23
|
+
private ensureScene;
|
|
24
|
+
}
|