Files
Leantime/dev/DWGViewer/js/source.js

329 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件 → 统一文档模型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.0LibreDWG',
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: 'MITdxf-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
}