import { importUrl } from '../shared/import-url'; import { vendored } from '../shared/vendor'; /** * DWG support: AutoCAD's own binary format, converted to DXF by libredwg-web * (a WebAssembly build of GNU LibreDWG) so the rest of the viewer reads it * exactly as it reads a DXF. * * libredwg-web is GPL-3.0, so this folder - the code that runs with it - is * published under the same licence at /source/dxf-viewer/ on this site. */ const LIBREDWG = vendored('@mlightcad/libredwg-web@0.7.14/dist/libredwg-web.js'); let libredwg: Promise | null = null; /** True for a file that starts with a DWG version string, "AC" and four digits (AC1009 for R12 up to AC1032 for 2018+). */ export function isDwg(bytes: Uint8Array): boolean { return bytes.length > 6 && bytes[0] === 0x41 && bytes[1] === 0x43 && /^AC\d{4}$/.test(String.fromCharCode(...bytes.subarray(0, 6))); } export interface LayerState { off: boolean; frozen: boolean; } /** * The DWG as DXF bytes, plus each layer's real on/off and frozen state. * * The second part exists because libredwg's DXF writer marks every layer as * switched off (a negative colour number) - checked against AutoCAD's own DXF * of the same drawings, where the same layers are on. Its typed database, read * separately, has the states right and agrees with AutoCAD's, so the viewer * takes layer visibility from there. */ export async function dwgToDxf(bytes: Uint8Array): Promise<{ dxf: Uint8Array; layers: Map }> { const dynamicImport = importUrl; libredwg ??= dynamicImport(LIBREDWG).catch((err) => { libredwg = null; throw err; }); const mod = await libredwg; // A fresh instance for every use: after one read the library's internal // state is not reliable, and the next fails with "null function" or an // abort. The WebAssembly is compiled once and cached, so a new instance // costs tens of milliseconds. const writer = await mod.LibreDwg.create(); const out: Uint8Array | null = writer.dwg_write_dxf(bytes); if (!out || !out.length) throw new Error('DWG conversion failed'); // A copy: the result is a view into the module's own memory, and a Blob made // from that view crashed the tab when it was downloaded. const dxf = out.slice(); const layers = new Map(); try { const reader = await mod.LibreDwg.create(); const db = reader.convert(reader.dwg_read_data(bytes, mod.Dwg_File_Type.DWG)); for (const l of db?.tables?.LAYER?.entries ?? []) layers.set(String(l.name), { off: !!l.off, frozen: !!l.frozen }); } catch { // Without the states every layer is shown, which beats every layer hidden. } return { dxf: fixLayerColours(dxf, layers), layers }; } /** * Rewrites each LAYER record's colour (group 62) with the sign that says what * the layer really is - negative only when it is off - so the DXF a person * downloads does not open elsewhere with every layer hidden. Done on the text * decoded as Latin-1, which maps every byte to one character and back, so * nothing else in the file changes, whatever its encoding. */ export function fixLayerColours(dxf: Uint8Array, layers: Map): Uint8Array { let text = ''; for (let i = 0; i < dxf.length; i += 8192) text += String.fromCharCode(...dxf.subarray(i, i + 8192)); const lines = text.split('\n'); const code = (i: number) => lines[i]?.trim(); let i = 0; while (i + 1 < lines.length) { if (code(i) === '0' && code(i + 1) === 'LAYER') { let j = i + 2; let name = ''; let colourLine = -1; while (j + 1 < lines.length && code(j) !== '0') { if (code(j) === '2') name = code(j + 1)!; if (code(j) === '62') colourLine = j + 1; j += 2; } if (colourLine >= 0) { const raw = lines[colourLine]; const n = Math.abs(parseInt(raw, 10)); if (!isNaN(n)) { const off = layers.get(name)?.off ?? false; const eol = raw.endsWith('\r') ? '\r' : ''; lines[colourLine] = String(off ? -n : n).padStart(raw.trim().length, ' ') + eol; } } i = j; } else { i += 2; } } const outText = lines.join('\n'); const out = new Uint8Array(outText.length); for (let k = 0; k < outText.length; k++) out[k] = outText.charCodeAt(k); return out; }