Files
Leantime/dev/DWGViewer/js/flatten.js

518 lines
19 KiB
JavaScript
Raw Permalink 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展平成渲染用的「显示列表」。
*
* 展平只做一次(加载时),之后缩放平移都不再重算几何。
* 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)
}