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

517
dev/DWGViewer/js/flatten.js Normal file
View File

@@ -0,0 +1,517 @@
/**
* 把归一化后的图纸文档Doc展平成渲染用的「显示列表」。
*
* 展平只做一次(加载时),之后缩放平移都不再重算几何。
* INSERT 会递归展开并把变换烘焙进坐标,因此显示列表里的坐标全是世界坐标。
*/
import ACI from '../libs/dxf-parser/AutoCadColorIndex.js'
import {
applyX, applyY, arcBBox, arcPoints, bboxOfPoints, bulgeArcPoints, circlePoints, conformal,
ellipsePoints, fitPointCurve, growBox, IDENTITY, makeInsertMatrix, matrixScale, mul,
splinePoints, TAU, transformPoints,
} from './curves.js'
import { builtinDefinitionLines, hatchLoops, hatchPatternSegments } from './hatch.js'
import { parseMText, parseSimpleText } from './mtext.js'
const MAX_INSERT_DEPTH = 12
const MAX_ARRAY_CELLS = 2000
/** DWG 线宽枚举(值 = 1/100 mm其余为特殊值 */
const LW_TABLE = new Set([0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211])
const LW_BYLAYER = -1, LW_BYBLOCK = -2, LW_DEFAULT = -3
function normLineweight(v) {
if (v == null) return LW_BYLAYER
if (v < 0) return v >= -3 ? v : LW_BYLAYER
if (v === 28) return LW_BYLAYER
if (v === 29) return LW_BYBLOCK
if (v === 30 || v === 31) return LW_DEFAULT
return LW_TABLE.has(v) ? v : LW_DEFAULT
}
/** ACI 索引 → 0xRRGGBB */
export function aciColor(i) {
if (i == null || i < 0 || i > 255) return 0x000000
const c = ACI[i]
return typeof c === 'number' ? c : 0x000000
}
/**
* 解析实体最终颜色。
* @returns {number} 0xRRGGBB
*/
function resolveColor(ent, layer, inherited) {
if (typeof ent.trueColor === 'number') return ent.trueColor & 0xffffff
const ci = ent.colorIndex
if (ci === 0) return inherited != null ? inherited : (layer ? layerColor(layer) : 0x000000) // BYBLOCK
if (ci == null || ci === 256) return layer ? layerColor(layer) : 0x000000 // BYLAYER
if (ci > 0 && ci < 256) return aciColor(ci)
return 0x000000
}
function layerColor(layer) {
if (typeof layer.trueColor === 'number') return layer.trueColor & 0xffffff
if (layer.colorIndex != null && layer.colorIndex !== 0) return aciColor(Math.abs(layer.colorIndex))
if (typeof layer.color === 'number' && layer.color !== 16777215) return layer.color & 0xffffff
return 0x000000
}
// ---------------------------------------------------------------- 主入口
/**
* @param {Doc} doc 归一化文档
* @param {object} opt { space: 'model' | layout 名 }
* @returns {{shapes: Array, bbox: number[], stats: object}}
*/
export function flatten(doc, opt = {}) {
const shapes = []
const bbox = [Infinity, Infinity, -Infinity, -Infinity]
const stats = { entities: 0, shapes: 0, skipped: 0, byType: {} }
const ctx = { doc, shapes, bbox, stats, blockStack: new Set() }
const entities = opt.entities || doc.modelEntities || []
for (const ent of entities) {
emitEntity(ctx, ent, IDENTITY, null, null, 0)
}
stats.shapes = shapes.length
return { shapes, bbox, stats }
}
function emitEntity(ctx, ent, m, inheritColor, inheritLayer, depth) {
if (!ent || ent.isVisible === false) return
const type = ent.type
ctx.stats.entities++
ctx.stats.byType[type] = (ctx.stats.byType[type] || 0) + 1
// 图层:块内实体用 "0" 图层时继承外层图层AutoCAD 规则)
let layerName = ent.layer || '0'
if (layerName === '0' && inheritLayer) layerName = inheritLayer
const layer = ctx.doc.layers.get(layerName)
const base = {
layer: layerName,
color: resolveColor(ent, layer, inheritColor),
lw: normLineweight(ent.lineweight),
lt: resolveLineType(ent, layer),
ltScale: ent.lineTypeScale || 1,
ent,
}
switch (type) {
case 'LINE': {
const p = ent.startPoint, q = ent.endPoint
if (!p || !q) return
addPoly(ctx, base, new Float64Array([
applyX(m, p.x, p.y), applyY(m, p.x, p.y),
applyX(m, q.x, q.y), applyY(m, q.x, q.y),
]), false)
break
}
case 'CIRCLE':
addCircleLike(ctx, base, m, ent.center, ent.radius, ent.radius, 0, 0, TAU)
break
case 'ARC': {
const a0 = ang(ent.startAngle), a1 = ang(ent.endAngle)
addCircleLike(ctx, base, m, ent.center, ent.radius, ent.radius, 0, a0, a1)
break
}
case 'ELLIPSE': {
const mj = ent.majorAxisEndPoint || { x: 1, y: 0 }
const rx = Math.hypot(mj.x, mj.y)
const ry = rx * (ent.axisRatio || 1)
const rot = Math.atan2(mj.y, mj.x)
let a0 = ent.startAngle || 0, a1 = ent.endAngle
if (a1 == null || Math.abs((a1 - a0) % TAU) < 1e-9) { a0 = 0; a1 = TAU }
addCircleLike(ctx, base, m, ent.center, rx, ry, rot, a0, a1)
break
}
case 'LWPOLYLINE': {
const pts = lwPolyPoints(ent)
if (pts.length >= 4) addPoly(ctx, base, transformPoints(pts, m), isClosed(ent.flag))
break
}
case 'POLYLINE':
case 'POLYLINE2D':
case 'POLYLINE3D': {
const pts = polyPoints(ent)
if (pts.length >= 4) addPoly(ctx, base, transformPoints(pts, m), isClosed(ent.flag))
break
}
case 'SPLINE': {
const pts = splineEntityPoints(ent)
if (pts.length >= 4) addPoly(ctx, base, transformPoints(pts, m), !!(ent.flag & 1))
break
}
case 'POINT': {
const p = ent.position || ent.startPoint || ent.center
if (!p) return
push(ctx, { ...base, kind: 'point', x: applyX(m, p.x, p.y), y: applyY(m, p.x, p.y) },
[applyX(m, p.x, p.y), applyY(m, p.x, p.y), applyX(m, p.x, p.y), applyY(m, p.x, p.y)])
break
}
case 'SOLID':
case 'TRACE': {
const cs = [ent.corner1, ent.corner2, ent.corner4, ent.corner3].filter(Boolean)
if (cs.length < 3) return
const pts = new Float64Array(cs.length * 2)
cs.forEach((c, i) => { pts[i * 2] = applyX(m, c.x, c.y); pts[i * 2 + 1] = applyY(m, c.x, c.y) })
push(ctx, { ...base, kind: 'fill', loops: [pts] }, bboxOfPoints(pts))
break
}
case '3DFACE': {
const cs = [ent.corner1 || ent.vertices?.[0], ent.corner2 || ent.vertices?.[1],
ent.corner3 || ent.vertices?.[2], ent.corner4 || ent.vertices?.[3]].filter(Boolean)
if (cs.length < 3) return
const pts = new Float64Array(cs.length * 2)
cs.forEach((c, i) => { pts[i * 2] = applyX(m, c.x, c.y); pts[i * 2 + 1] = applyY(m, c.x, c.y) })
addPoly(ctx, base, pts, true)
break
}
case 'HATCH':
emitHatch(ctx, base, ent, m)
break
case 'TEXT':
case 'ATTRIB':
case 'ATTDEF':
emitText(ctx, base, ent, m)
break
case 'MTEXT':
emitMText(ctx, base, ent, m)
break
case 'DIMENSION':
case 'ARC_DIMENSION':
emitBlockRef(ctx, base, ent, ent.name, m, inheritColor, layerName, depth, ent.definitionPoint)
break
case 'LEADER':
emitLeader(ctx, base, ent, m)
break
case 'MULTILEADER':
case 'MLEADER':
emitMLeader(ctx, base, ent, m)
break
case 'INSERT':
emitInsert(ctx, base, ent, m, inheritColor, layerName, depth)
break
case 'VIEWPORT':
case 'SEQEND':
case 'VERTEX':
case 'BLOCK':
case 'ENDBLK':
break
default:
ctx.stats.skipped++
break
}
}
// ---------------------------------------------------------------- 各类实体
function resolveLineType(ent, layer) {
let lt = ent.lineType || ent.lineTypeName
if (!lt || lt === 'ByLayer' || lt === 'BYLAYER') lt = layer ? layer.lineType : null
if (!lt || lt === 'ByBlock' || lt === 'BYBLOCK') return null
if (/^continuous$/i.test(lt)) return null
return lt
}
const isClosed = (flag) => !!(flag & 1)
/**
* 文档内的角度约定:一律弧度。
* libredwg 本身就给弧度dxf-parser 混用圆弧是弧度、rotation 是度),
* 已在 source.js 的归一化里统一,这里不再换算。
*/
const ang = (a) => a || 0
function lwPolyPoints(ent) {
const vs = ent.vertices || []
if (!vs.length) return new Float64Array(0)
const out = []
const closed = isClosed(ent.flag)
const n = vs.length
out.push(vs[0].x, vs[0].y)
for (let i = 0; i < (closed ? n : n - 1); i++) {
const a = vs[i], b = vs[(i + 1) % n]
if (a.bulge) out.push(...bulgeArcPoints(a.x, a.y, b.x, b.y, a.bulge))
else out.push(b.x, b.y)
}
return new Float64Array(out)
}
function polyPoints(ent) {
const vs = (ent.vertices || []).filter((v) => v && v.x != null)
if (!vs.length) return new Float64Array(0)
// 曲线拟合的多段线flag & 4 = spline-fit顶点已是拟合点直接连
const out = []
const closed = isClosed(ent.flag)
const n = vs.length
out.push(vs[0].x, vs[0].y)
for (let i = 0; i < (closed ? n : n - 1); i++) {
const a = vs[i], b = vs[(i + 1) % n]
if (a.bulge) out.push(...bulgeArcPoints(a.x, a.y, b.x, b.y, a.bulge))
else out.push(b.x, b.y)
}
return new Float64Array(out)
}
function splineEntityPoints(ent) {
const cps = ent.controlPoints || []
if (cps.length >= 2) {
const weights = ent.weights
const pts = cps.map((c, i) => ({ x: c.x, y: c.y, weight: weights ? weights[i] : c.weight }))
return splinePoints(ent.degree || 3, pts, ent.knots, !!(ent.flag & 1))
}
const fps = ent.fitPoints || []
if (fps.length >= 2) return fitPointCurve(fps, !!(ent.flag & 1))
return new Float64Array(0)
}
function addPoly(ctx, base, pts, closed) {
if (pts.length < 4) return
push(ctx, { ...base, kind: 'poly', pts, closed }, bboxOfPoints(pts))
}
/**
* 圆/圆弧/椭圆:变换若是共形的(等比缩放+旋转),保留解析形式交给
* canvas 的 ellipse() 精确绘制;否则退化成折线。
*/
function addCircleLike(ctx, base, m, center, rx, ry, rot, a0, a1) {
if (!center) return
if (!(rx > 0)) return
const full = Math.abs(a1 - a0) >= TAU - 1e-9
const cf = conformal(m)
if (cf) {
// 共形变换下圆仍是圆、椭圆仍是椭圆,保留解析形式让 canvas 画出无锯齿的曲线。
// 镜像会让参数角反向a → -a且弧的走向翻转故起止角对调。
const s = cf.scale, phi = cf.rotation
const R = cf.mirror ? phi - rot : rot + phi
const A0 = cf.mirror ? -a1 : a0
const A1 = cf.mirror ? -a0 : a1
const cx = applyX(m, center.x, center.y)
const cy = applyY(m, center.x, center.y)
const RX = rx * s, RY = ry * s
push(ctx, { ...base, kind: 'arc', cx, cy, rx: RX, ry: RY, rot: R, a0: A0, a1: A1 },
arcBBox(cx, cy, RX, RY, R, A0, A1))
return
}
const pts = rx === ry
? (full ? circlePoints(center.x, center.y, rx) : arcPoints(center.x, center.y, rx, a0, a1))
: ellipsePoints(center.x, center.y, rx * Math.cos(rot), rx * Math.sin(rot), ry / rx, a0, a1)
addPoly(ctx, base, transformPoints(pts, m), full)
}
function emitHatch(ctx, base, ent, m) {
const loops = hatchLoops(ent)
if (!loops.length) return
const solid = ent.solidFill === 1 || /^solid$/i.test(ent.patternName || '')
if (solid) {
const world = loops.map((l) => transformPoints(l, m))
const bb = [Infinity, Infinity, -Infinity, -Infinity]
world.forEach((l) => growBox(bb, bboxOfPoints(l)))
push(ctx, { ...base, kind: 'fill', loops: world }, bb)
return
}
// 文件里没带定义线时(预定义图案只存名字),从内置图案表展开
let defs = ent.definitionLines
if (!defs || !defs.length) {
defs = builtinDefinitionLines(ent.patternName, ent.patternAngle || 0, ent.patternScale || 1)
}
const segs = hatchPatternSegments(loops, defs)
if (!segs.length) {
// 没有图案定义线(例如未知图案):至少把边界画出来
for (const l of loops) addPoly(ctx, base, transformPoints(l, m), true)
return
}
const arr = transformPoints(new Float64Array(segs), m)
push(ctx, { ...base, kind: 'segs', pts: arr }, bboxOfPoints(arr))
}
function emitText(ctx, base, ent, m) {
const raw = ent.text
if (raw == null || raw === '') return
const txt = parseSimpleText(raw)
if (!txt) return
const style = ctx.doc.styles.get(ent.styleName) || null
const sc = matrixScale(m)
const h = (ent.textHeight || (style && style.fixedTextHeight) || 2.5) * sc
if (!(h > 0)) return
// TEXT 的对齐点halign/valign 非默认时用 endPoint第二对齐点
const useAlt = (ent.halign && ent.halign !== 0) || (ent.valign && ent.valign !== 0)
const p = (useAlt && ent.endPoint && (ent.endPoint.x || ent.endPoint.y)) ? ent.endPoint : (ent.startPoint || ent.position)
if (!p) return
const cf = conformal(m)
const rot = ang(ent.rotation) + (cf ? cf.rotation : 0)
push(ctx, {
...base,
kind: 'text',
x: applyX(m, p.x, p.y),
y: applyY(m, p.x, p.y),
h,
rot,
wFactor: ent.xScale || (style && style.widthFactor) || 1,
oblique: ang(ent.obliqueAngle || (style && style.obliqueAngle) || 0),
halign: ent.halign || 0,
valign: ent.valign || 0,
mirrorX: !!(ent.generationFlag & 2),
mirrorY: !!(ent.generationFlag & 4),
font: style && style.font,
runs: [{ text: txt, hFactor: 1, wFactor: 1, rise: 0 }],
plain: txt,
}, textBox(applyX(m, p.x, p.y), applyY(m, p.x, p.y),
charWidth(txt) * h * (ent.xScale || 1), h,
ent.halign === 1 || ent.halign === 4 ? 0.5 : ent.halign === 2 ? 1 : 0,
ent.valign === 3 ? 1 : ent.valign === 2 || ent.halign === 4 ? 0.5 : 0,
rot))
}
/** 估算字符串宽度(以字高为单位):中日韩按 1 个字宽,西文按 0.55 */
function charWidth(t) {
let w = 0
for (const c of t) w += c.charCodeAt(0) > 0x2e80 ? 1 : 0.55
return w
}
/**
* 文字包围盒。
* @param ax 0=左 0.5=中 1=右ay 0=基线/底 0.5=中 1=顶
*/
function textBox(x, y, w, h, ax, ay, rot) {
const x0 = -w * ax, x1 = x0 + w
const y1 = h * (1 - ay) + h * 0.25 // 多留一点给下伸部
const y0 = y1 - h * 1.25
const c = Math.cos(rot || 0), s = Math.sin(rot || 0)
const b = [Infinity, Infinity, -Infinity, -Infinity]
for (const [lx, ly] of [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]) {
const px = x + lx * c - ly * s
const py = y + lx * s + ly * c
if (px < b[0]) b[0] = px
if (py < b[1]) b[1] = py
if (px > b[2]) b[2] = px
if (py > b[3]) b[3] = py
}
return b
}
/** MTEXT 的 9 种对齐点 */
const MT_H = { 1: 0, 2: 0.5, 3: 1, 4: 0, 5: 0.5, 6: 1, 7: 0, 8: 0.5, 9: 1 }
const MT_V = { 1: 1, 2: 1, 3: 1, 4: 0.5, 5: 0.5, 6: 0.5, 7: 0, 8: 0, 9: 0 }
function emitMText(ctx, base, ent, m) {
const { lines } = parseMText(ent.text)
const plain = lines.map((l) => l.map((r) => r.text).join('')).join('\n')
if (!plain.trim()) return
const style = ctx.doc.styles.get(ent.styleName) || null
const sc = matrixScale(m)
const h = (ent.textHeight || 2.5) * sc
const p = ent.insertionPoint
if (!p || !(h > 0)) return
const cf = conformal(m)
let rot = 0
if (ent.direction && (ent.direction.x || ent.direction.y)) rot = Math.atan2(ent.direction.y, ent.direction.x)
else rot = ang(ent.rotation)
rot += cf ? cf.rotation : 0
const ap = ent.attachmentPoint || 1
const lineGap = (ent.lineSpacing || 1) * 1.667 // AutoCAD 默认行距系数
const ax = MT_H[ap] ?? 0
const ay = MT_V[ap] ?? 1
const width = (ent.rectWidth || 0) * sc
const X = applyX(m, p.x, p.y), Y = applyY(m, p.x, p.y)
// 包围盒:有 rectWidth 就按框宽算(正文会在这个宽度内折行),
// 否则按最长一行估。行数同样按折行后估,否则长段落的盒子会短一截。
const longest = Math.max(...lines.map((l) => charWidth(l.map((r) => r.text).join(''))), 1)
const boxW = width > 0 ? width : longest * h
const nLines = width > 0
? lines.reduce((n, l) => n + Math.max(1, Math.ceil((charWidth(l.map((r) => r.text).join('')) * h) / width)), 0)
: lines.length
const boxH = Math.max(1, nLines) * h * lineGap
push(ctx, {
...base, kind: 'mtext', x: X, y: Y, h, rot, lines, lineGap, ax, ay, width,
font: style && style.font, plain,
}, textBox(X, Y, boxW, boxH, ax, ay, rot))
}
function emitLeader(ctx, base, ent, m) {
const vs = ent.vertices || ent.points || []
if (vs.length < 2) return
const pts = new Float64Array(vs.length * 2)
vs.forEach((v, i) => { pts[i * 2] = applyX(m, v.x, v.y); pts[i * 2 + 1] = applyY(m, v.x, v.y) })
addPoly(ctx, base, pts, false)
}
function emitMLeader(ctx, base, ent, m) {
const ctxData = ent.contextData || ent.context || {}
const leaders = ctxData.leaders || ent.leaders || []
for (const ld of leaders) {
for (const line of ld.leaderLines || ld.lines || []) {
const vs = line.vertices || line.points || []
if (vs.length < 2) continue
const pts = new Float64Array(vs.length * 2)
vs.forEach((v, i) => { pts[i * 2] = applyX(m, v.x, v.y); pts[i * 2 + 1] = applyY(m, v.x, v.y) })
addPoly(ctx, base, pts, false)
}
}
const t = ctxData.text || ctxData.textLabel
if (t && ctxData.textLocation) {
emitMText(ctx, base, {
text: t, insertionPoint: ctxData.textLocation, textHeight: ctxData.textHeight || ent.textHeight,
rotation: 0, attachmentPoint: ctxData.textAttachmentPoint || 1, styleName: ctxData.textStyleName,
}, m)
}
}
/** DIMENSION真正的图形放在一个匿名块里直接展开那个块 */
function emitBlockRef(ctx, base, ent, blockName, m, inheritColor, layerName, depth, fallbackPt) {
const blk = blockName && ctx.doc.blocks.get(blockName)
if (!blk) return
if (depth > MAX_INSERT_DEPTH) return
const bp = blk.basePoint || { x: 0, y: 0 }
const bm = mul(m, [1, 0, 0, 1, -bp.x, -bp.y])
for (const e of blk.entities || []) {
emitEntity(ctx, e, bm, base.color, layerName, depth + 1)
}
}
function emitInsert(ctx, base, ent, m, inheritColor, layerName, depth) {
if (depth > MAX_INSERT_DEPTH) return
const blk = ctx.doc.blocks.get(ent.name)
// ATTRIB 无论块在不在都要画
for (const a of ent.attribs || []) {
if (a && a.isVisible !== false) emitEntity(ctx, a, m, base.color, layerName, depth + 1)
}
if (!blk) { ctx.stats.skipped++; return }
const p = ent.insertionPoint || { x: 0, y: 0 }
const sx = ent.xScale === 0 || ent.xScale == null ? 1 : ent.xScale
const sy = ent.yScale === 0 || ent.yScale == null ? 1 : ent.yScale
const rot = ang(ent.rotation)
const bp = blk.basePoint || { x: 0, y: 0 }
const cols = Math.max(1, ent.columnCount || 1)
const rows = Math.max(1, ent.rowCount || 1)
const cSp = ent.columnSpacing || 0
const rSp = ent.rowSpacing || 0
if (cols * rows > MAX_ARRAY_CELLS) { ctx.stats.skipped++; return }
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const local = makeInsertMatrix(p.x + c * cSp, p.y + r * rSp, sx, sy, rot)
// 先把块基点移到原点,再套插入变换
const bm = mul(mul(m, local), [1, 0, 0, 1, -bp.x, -bp.y])
for (const e of blk.entities || []) {
emitEntity(ctx, e, bm, base.color, layerName, depth + 1)
}
}
}
}
// ---------------------------------------------------------------- 输出
function push(ctx, shape, bb) {
if (!bb || !isFinite(bb[0]) || !isFinite(bb[2])) return
shape.bbox = bb
shape.i = ctx.shapes.length
ctx.shapes.push(shape)
growBox(ctx.bbox, bb)
}