329 lines
11 KiB
JavaScript
329 lines
11 KiB
JavaScript
/**
|
||
* 文件 → 统一文档模型(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
|
||
}
|