OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

328
dev/DWGViewer/js/source.js Normal file
View File

@@ -0,0 +1,328 @@
/**
* 文件 → 统一文档模型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
}