/** * 文件 → 统一文档模型(Doc)。 * * 两条互不依赖的解析路径: * .dxf → dxf-parser (MIT,纯 JS,随页面一起加载) * .dwg → libredwg-web WASM (GPL-3.0,只有真的打开 dwg 时才动态 import, * 约 10MB,不打开 dwg 的用户完全不会下载) * * 这样拆分是为了让 GPL 依赖是可替换的一块:将来换成商业 SDK 或服务端转换, * 只需要替换 loadDwg(),其余代码不受影响。 * * 统一约定:Doc 里所有角度都是**弧度**。 */ import { parseDxfHatches } from './dxfHatch.js' const RAD = Math.PI / 180 /** @typedef {{name:string, format:string, layers:Map, lineTypes:Map, styles:Map, blocks:Map, modelEntities:Array, layouts:Array, header:object, license:string}} Doc */ export function extOf(name) { const m = /\.([a-z0-9]+)$/i.exec(name || '') return m ? m[1].toLowerCase() : '' } /** * @param {string} name 文件名(用于判断格式) * @param {ArrayBuffer} buffer * @param {(msg:string)=>void} [onProgress] */ export async function loadDrawing(name, buffer, onProgress = () => {}) { const ext = extOf(name) if (ext === 'dwg') return loadDwg(name, buffer, onProgress) if (ext === 'dxf') return loadDxf(name, buffer, onProgress) // 没有扩展名时按内容嗅探:DWG 以 "AC10xx" 开头 const head = new TextDecoder('latin1').decode(new Uint8Array(buffer, 0, Math.min(6, buffer.byteLength))) if (/^AC10\d\d/.test(head)) return loadDwg(name, buffer, onProgress) return loadDxf(name, buffer, onProgress) } // ---------------------------------------------------------------- DWG let libredwgPromise = null /** 动态加载 GPL 模块;只在第一次打开 dwg 时触发 */ function getLibreDwg() { if (!libredwgPromise) { libredwgPromise = import('../libs/libredwg/dist/libredwg-web.js').then(async (mod) => { const inst = await mod.LibreDwg.create() return { mod, inst } }) } return libredwgPromise } async function loadDwg(name, buffer, onProgress) { onProgress('正在加载 DWG 解析模块(首次约 10MB)...') const { mod, inst } = await getLibreDwg() onProgress('正在解析 DWG...') const dwg = inst.dwg_read_data(buffer, mod.Dwg_File_Type.DWG) if (!dwg) throw new Error('DWG 解析失败:文件可能已损坏或版本过新') let db try { db = inst.convert(dwg) } finally { try { inst.dwg_free(dwg) } catch { /* 释放失败不影响已转换的数据 */ } } return normalizeDwg(name, db) } function tableEntries(db, key) { const t = db.tables && db.tables[key] if (!t) return [] const e = t.entries if (Array.isArray(e)) return e if (e && typeof e === 'object') return Object.values(e) return [] } function normalizeDwg(name, db) { const layers = new Map() for (const l of tableEntries(db, 'LAYER')) { layers.set(l.name, { name: l.name, colorIndex: l.colorIndex, trueColor: colorFieldToTrueColor(l), lineType: l.lineType, lineweight: l.lineweight, off: !!l.off || (typeof l.colorIndex === 'number' && l.colorIndex < 0), frozen: !!l.frozen, locked: !!l.locked, plot: l.plotFlag !== 0, }) } if (!layers.has('0')) layers.set('0', { name: '0', colorIndex: 7, lineType: 'Continuous', off: false, frozen: false }) const lineTypes = new Map() for (const lt of tableEntries(db, 'LTYPE')) { lineTypes.set(lt.name, { name: lt.name, patternLength: lt.totalPatternLength || 0, pattern: normalizeLtPattern(lt.pattern), }) } const styles = new Map() for (const s of tableEntries(db, 'STYLE')) { styles.set(s.name, { name: s.name, font: s.font, bigFont: s.bigFont, widthFactor: s.widthFactor || 1, obliqueAngle: s.obliqueAngle || 0, fixedTextHeight: s.fixedTextHeight || 0, }) } // 块表:*Model_Space / *Paper_Space 是布局,其余是可被 INSERT 引用的块 const blocks = new Map() let modelEntities = [] const layouts = [] for (const br of tableEntries(db, 'BLOCK_RECORD')) { const ents = br.entities || [] const nm = br.name || '' if (/^\*model_space$/i.test(nm)) modelEntities = ents else if (/^\*paper_space/i.test(nm)) layouts.push({ name: nm, entities: ents }) blocks.set(nm, { name: nm, basePoint: br.basePoint || { x: 0, y: 0 }, entities: ents }) } // 有些文件模型空间实体只出现在顶层 entities 里 if (!modelEntities.length && Array.isArray(db.entities)) modelEntities = db.entities return { name, format: 'DWG', license: 'GPL-3.0(LibreDWG)', layers, lineTypes, styles, blocks, modelEntities, layouts, header: db.header || {}, } } /** libredwg 用 0xFFFFFF 表示「没有真彩色」 */ function colorFieldToTrueColor(o) { if (typeof o.trueColor === 'number') return o.trueColor & 0xffffff if (typeof o.color === 'number' && o.color !== 16777215 && o.color !== -1) return o.color & 0xffffff return undefined } function normalizeLtPattern(pattern) { if (!Array.isArray(pattern)) return [] return pattern.map((p) => { if (typeof p === 'number') return p if (p && typeof p === 'object') return p.length != null ? p.length : (p.dashLength != null ? p.dashLength : 0) return 0 }).filter((n) => isFinite(n)) } // ---------------------------------------------------------------- DXF async function loadDxf(name, buffer, onProgress) { const bytes = new Uint8Array(buffer) const head = new TextDecoder('latin1').decode(bytes.subarray(0, 22)) if (head.startsWith('AutoCAD Binary DXF')) { throw new Error('这是二进制 DXF,请在 CAD 里另存为 ASCII DXF,或直接用 DWG') } onProgress('正在解析 DXF...') const text = decodeDxfText(bytes) const { default: DxfParser } = await import('../libs/dxf-parser/index.js') const parsed = new DxfParser().parseSync(text) const doc = normalizeDxf(name, parsed) // dxf-parser 不支持 HATCH,用独立的扫描器补上剖面线 try { parseDxfHatches(text, doc) } catch (e) { console.warn('HATCH 解析失败,已跳过:', e) } return doc } /** DXF 可能是 UTF-8,也可能是 GBK(国内图纸常见)。用 $DWGCODEPAGE 之外的启发式判断 */ function decodeDxfText(bytes) { const utf8 = new TextDecoder('utf-8', { fatal: false }).decode(bytes) // U+FFFD 过多说明不是 UTF-8,退回 GBK let bad = 0 for (let i = 0; i < utf8.length; i += 97) if (utf8.charCodeAt(i) === 0xfffd) bad++ if (bad > 2) { try { return new TextDecoder('gbk').decode(bytes) } catch { /* 浏览器不支持 gbk 就用 utf8 */ } } return utf8 } function normalizeDxf(name, dxf) { const layers = new Map() const src = (dxf.tables && dxf.tables.layer && dxf.tables.layer.layers) || {} for (const [k, l] of Object.entries(src)) { layers.set(k, { name: k, colorIndex: l.colorIndex, trueColor: typeof l.color === 'number' ? l.color : undefined, lineType: l.lineType || l.lineTypeName, lineweight: l.lineweight, off: l.visible === false || (typeof l.colorIndex === 'number' && l.colorIndex < 0), frozen: !!(l.frozen || (l.flags & 1)), locked: !!(l.flags & 4), plot: true, }) } if (!layers.has('0')) layers.set('0', { name: '0', colorIndex: 7, lineType: 'Continuous', off: false, frozen: false }) const lineTypes = new Map() const lts = (dxf.tables && dxf.tables.lineType && dxf.tables.lineType.lineTypes) || {} for (const [k, lt] of Object.entries(lts)) { lineTypes.set(k, { name: k, patternLength: lt.patternLength || 0, pattern: Array.isArray(lt.pattern) ? lt.pattern.filter((n) => typeof n === 'number') : [], }) } const styles = new Map() const sts = (dxf.tables && dxf.tables.style && dxf.tables.style.styles) || {} for (const [k, s] of Object.entries(sts)) { styles.set(k, { name: k, font: s.fontFileName || s.font, bigFont: s.bigFontFileName, widthFactor: s.widthFactor || 1, obliqueAngle: (s.obliqueAngle || 0) * RAD, fixedTextHeight: s.fixedTextHeight || 0, }) } const blocks = new Map() for (const [k, b] of Object.entries(dxf.blocks || {})) { const ents = (b.entities || []).map(dxfEntity) blocks.set(k, { name: k, basePoint: b.position || { x: 0, y: 0 }, entities: ents }) } const modelEntities = (dxf.entities || []).map(dxfEntity) return { name, format: 'DXF', license: 'MIT(dxf-parser)', layers, lineTypes, styles, blocks, modelEntities, layouts: [], header: dxf.header || {}, } } /** * dxf-parser 的实体 → 内部约定。 * 主要做三件事:字段改名、角度统一成弧度、顶点结构统一。 */ function dxfEntity(e) { const o = { ...e } o.colorIndex = e.colorIndex != null ? e.colorIndex : 256 if (typeof e.color === 'number' && e.colorIndex == null) o.trueColor = e.color o.lineType = e.lineType o.lineweight = e.lineweight o.lineTypeScale = e.lineTypeScale || 1 o.isVisible = e.visible !== false switch (e.type) { case 'LINE': o.startPoint = e.vertices && e.vertices[0] o.endPoint = e.vertices && e.vertices[1] break case 'LWPOLYLINE': case 'POLYLINE': o.flag = (e.shape ? 1 : 0) | (e.flag || 0) o.vertices = e.vertices || [] break case 'SPLINE': o.degree = e.degreeOfSplineCurve o.knots = e.knotValues o.controlPoints = e.controlPoints o.fitPoints = e.fitPoints o.flag = e.flag || (e.closed ? 1 : 0) break case 'ELLIPSE': // dxf-parser 已把 startAngle/endAngle 转成弧度 break case 'ARC': case 'CIRCLE': // 同上 break case 'TEXT': case 'ATTRIB': case 'ATTDEF': o.startPoint = e.startPoint o.endPoint = e.endPoint o.textHeight = e.textHeight o.rotation = (e.rotation || 0) * RAD o.obliqueAngle = (e.obliqueAngle || 0) * RAD o.xScale = e.xScale || 1 o.styleName = e.styleName || e.textStyle o.generationFlag = e.textGenerationFlag || 0 break case 'MTEXT': o.insertionPoint = e.position o.textHeight = e.height o.rectWidth = e.width o.rotation = (e.rotation || 0) * RAD o.attachmentPoint = e.attachmentPoint o.styleName = e.styleName || e.textStyle o.direction = e.directionVector break case 'INSERT': o.insertionPoint = e.position o.rotation = (e.rotation || 0) * RAD o.xScale = e.xScale == null ? 1 : e.xScale o.yScale = e.yScale == null ? 1 : e.yScale o.columnCount = e.columnCount o.rowCount = e.rowCount o.columnSpacing = e.columnSpacing o.rowSpacing = e.rowSpacing o.attribs = (e.attributes || e.attribs || []).map(dxfEntity) break case 'DIMENSION': o.name = e.block // 尺寸的实际图形所在的匿名块 o.rotation = (e.rotation || 0) * RAD break case 'SOLID': case 'TRACE': case '3DFACE': { const p = e.points || e.vertices || [] o.corner1 = p[0]; o.corner2 = p[1]; o.corner3 = p[2]; o.corner4 = p[3] break } case 'POINT': o.position = e.position break default: break } return o }