266 lines
9.0 KiB
JavaScript
266 lines
9.0 KiB
JavaScript
/**
|
||
* DXF 的 HATCH 补丁解析器。
|
||
*
|
||
* dxf-parser(MIT)不认 HATCH,而机械图纸里剖面线随处可见,缺了整张图会显得很空。
|
||
* 这里独立扫一遍 DXF 组码,只挑 HATCH 出来,产出与 libredwg 一致的结构,
|
||
* 这样 hatch.js / flatten.js 两边共用同一套渲染逻辑。
|
||
*
|
||
* 角度统一转成弧度(DXF 里 50/51/53 都是度)。
|
||
*/
|
||
|
||
const RAD = Math.PI / 180
|
||
|
||
/**
|
||
* @param {string} text DXF 原文
|
||
* @param {Doc} doc 已归一化的文档,会就地把 HATCH 追加进模型空间/对应块
|
||
*/
|
||
export function parseDxfHatches(text, doc) {
|
||
const pairs = tokenize(text)
|
||
let section = null
|
||
let block = null
|
||
let count = 0
|
||
|
||
for (let i = 0; i < pairs.length; i++) {
|
||
const [code, value] = pairs[i]
|
||
if (code !== 0) continue
|
||
if (value === 'SECTION') {
|
||
const nx = pairs[i + 1]
|
||
section = nx && nx[0] === 2 ? nx[1] : null
|
||
continue
|
||
}
|
||
if (value === 'ENDSEC') { section = null; block = null; continue }
|
||
if (section !== 'ENTITIES' && section !== 'BLOCKS') continue
|
||
|
||
if (value === 'BLOCK') { block = findValue(pairs, i + 1, 2); continue }
|
||
if (value === 'ENDBLK') { block = null; continue }
|
||
if (value !== 'HATCH') continue
|
||
|
||
// 收集到下一个 0 组码为止
|
||
let j = i + 1
|
||
const body = []
|
||
while (j < pairs.length && pairs[j][0] !== 0) { body.push(pairs[j]); j++ }
|
||
const ent = parseHatch(body)
|
||
if (ent && ent.boundaryPaths.length) {
|
||
const target = block ? doc.blocks.get(block) : null
|
||
if (target) target.entities.push(ent)
|
||
else if (!block) doc.modelEntities.push(ent)
|
||
count++
|
||
}
|
||
i = j - 1
|
||
}
|
||
return count
|
||
}
|
||
|
||
function findValue(pairs, from, code) {
|
||
for (let i = from; i < pairs.length && i < from + 20; i++) {
|
||
if (pairs[i][0] === 0) return null
|
||
if (pairs[i][0] === code) return pairs[i][1]
|
||
}
|
||
return null
|
||
}
|
||
|
||
/** DXF 是「一行组码、一行值」的纯文本 */
|
||
function tokenize(text) {
|
||
const lines = text.split(/\r\n|\r|\n/)
|
||
const out = []
|
||
for (let i = 0; i + 1 < lines.length; i += 2) {
|
||
const c = parseInt(lines[i], 10)
|
||
if (!isFinite(c)) { i -= 1; continue } // 行错位时向前挪一行重新对齐
|
||
out.push([c, lines[i + 1]])
|
||
}
|
||
return out
|
||
}
|
||
|
||
function parseHatch(p) {
|
||
const ent = {
|
||
type: 'HATCH', layer: '0', colorIndex: 256, lineTypeScale: 1, isVisible: true,
|
||
patternName: '', solidFill: 0, boundaryPaths: [], definitionLines: [], xdata: [],
|
||
}
|
||
let i = 0
|
||
while (i < p.length) {
|
||
const [c, v] = p[i]
|
||
switch (c) {
|
||
case 8: ent.layer = v; i++; break
|
||
case 62: ent.colorIndex = int(v); i++; break
|
||
case 420: ent.trueColor = int(v) & 0xffffff; i++; break
|
||
case 6: ent.lineType = v; i++; break
|
||
case 370: ent.lineweight = int(v); i++; break
|
||
case 48: ent.lineTypeScale = num(v) || 1; i++; break
|
||
case 60: ent.isVisible = int(v) === 0; i++; break
|
||
case 2: ent.patternName = v; i++; break
|
||
case 70: ent.solidFill = int(v); i++; break
|
||
case 71: ent.associativity = int(v); i++; break
|
||
case 75: ent.hatchStyle = int(v); i++; break
|
||
case 76: ent.patternType = int(v); i++; break
|
||
case 52: ent.patternAngle = num(v) * RAD; i++; break
|
||
case 41: ent.patternScale = num(v); i++; break
|
||
case 91: i = readPaths(p, i + 1, int(v), ent); break
|
||
case 78: i = readDefLines(p, i + 1, int(v), ent); break
|
||
default: i++
|
||
}
|
||
}
|
||
return ent
|
||
}
|
||
|
||
const num = (v) => { const n = parseFloat(v); return isFinite(n) ? n : 0 }
|
||
const int = (v) => { const n = parseInt(v, 10); return isFinite(n) ? n : 0 }
|
||
|
||
function seek(p, i, code) {
|
||
while (i < p.length && p[i][0] !== code) i++
|
||
return i
|
||
}
|
||
|
||
function readPaths(p, i, n, ent) {
|
||
for (let k = 0; k < n; k++) {
|
||
i = seek(p, i, 92)
|
||
if (i >= p.length) break
|
||
const flag = int(p[i][1]); i++
|
||
if (flag & 2) {
|
||
// 多段线边界
|
||
const path = { boundaryPathTypeFlag: flag, hasBulge: false, isClosed: false, numberOfVertices: 0, vertices: [] }
|
||
let guard = 0
|
||
while (i < p.length && guard++ < 8) {
|
||
const c = p[i][0]
|
||
if (c === 72) { path.hasBulge = int(p[i][1]) !== 0; i++ }
|
||
else if (c === 73) { path.isClosed = int(p[i][1]) !== 0; i++ }
|
||
else if (c === 93) {
|
||
const nv = int(p[i][1]); i++
|
||
for (let t = 0; t < nv && i < p.length; t++) {
|
||
i = seek(p, i, 10)
|
||
if (i >= p.length) break
|
||
const x = num(p[i][1]); i++
|
||
const y = p[i] && p[i][0] === 20 ? num(p[i][1]) : 0
|
||
if (p[i] && p[i][0] === 20) i++
|
||
let bulge = 0
|
||
if (p[i] && p[i][0] === 42) { bulge = num(p[i][1]); i++ }
|
||
path.vertices.push({ x, y, bulge })
|
||
}
|
||
break
|
||
} else i++
|
||
}
|
||
path.numberOfVertices = path.vertices.length
|
||
if (path.vertices.length >= 2) ent.boundaryPaths.push(path)
|
||
} else {
|
||
// 边(直线/圆弧/椭圆弧/样条)组成的边界
|
||
i = seek(p, i, 93)
|
||
if (i >= p.length) break
|
||
const ne = int(p[i][1]); i++
|
||
const path = { boundaryPathTypeFlag: flag, numberOfEdges: ne, edges: [] }
|
||
for (let t = 0; t < ne && i < p.length; t++) {
|
||
i = seek(p, i, 72)
|
||
if (i >= p.length) break
|
||
const et = int(p[i][1]); i++
|
||
const r = readEdge(p, i, et)
|
||
i = r.i
|
||
if (r.edge) path.edges.push(r.edge)
|
||
}
|
||
if (path.edges.length) ent.boundaryPaths.push(path)
|
||
}
|
||
// 跳过尾部的源对象引用(97 + 330...)
|
||
}
|
||
return i
|
||
}
|
||
|
||
/** 顺序读取若干组码,遇到不属于本边的组码就停 */
|
||
function readEdge(p, i, type) {
|
||
const g = {}
|
||
const take = new Set(
|
||
type === 1 ? [10, 20, 11, 21]
|
||
: type === 2 ? [10, 20, 40, 50, 51, 73]
|
||
: type === 3 ? [10, 20, 11, 21, 40, 50, 51, 73]
|
||
: [94, 73, 74, 95, 96, 40, 10, 20, 42, 97, 11, 21, 12, 22, 13, 23],
|
||
)
|
||
if (type === 4) return readSplineEdge(p, i)
|
||
while (i < p.length && take.has(p[i][0])) {
|
||
const [c, v] = p[i]
|
||
if (g[c] !== undefined && (c === 10 || c === 40)) break // 下一条边开始了
|
||
g[c] = v
|
||
i++
|
||
}
|
||
switch (type) {
|
||
case 1:
|
||
return { i, edge: { type: 1, start: { x: num(g[10]), y: num(g[20]) }, end: { x: num(g[11]), y: num(g[21]) } } }
|
||
case 2:
|
||
return {
|
||
i,
|
||
edge: {
|
||
type: 2, center: { x: num(g[10]), y: num(g[20]) }, radius: num(g[40]),
|
||
startAngle: num(g[50]) * RAD, endAngle: num(g[51]) * RAD, isCCW: g[73] == null || int(g[73]) !== 0,
|
||
},
|
||
}
|
||
case 3:
|
||
return {
|
||
i,
|
||
edge: {
|
||
type: 3, center: { x: num(g[10]), y: num(g[20]) }, end: { x: num(g[11]), y: num(g[21]) },
|
||
lengthOfMinorAxis: num(g[40]), startAngle: num(g[50]) * RAD, endAngle: num(g[51]) * RAD,
|
||
isCCW: g[73] == null || int(g[73]) !== 0,
|
||
},
|
||
}
|
||
default:
|
||
return { i, edge: null }
|
||
}
|
||
}
|
||
|
||
function readSplineEdge(p, i) {
|
||
const edge = { type: 4, degree: 3, knots: [], controlPoints: [], fitDatum: [], numberOfKnots: 0, numberOfControlPoints: 0, numberOfFitData: 0 }
|
||
// 94 degree, 73 rational, 74 periodic, 95 numKnots, 96 numCtrl
|
||
let rational = false
|
||
while (i < p.length && [94, 73, 74, 95, 96].includes(p[i][0])) {
|
||
const [c, v] = p[i]; i++
|
||
if (c === 94) edge.degree = int(v)
|
||
else if (c === 73) rational = int(v) !== 0
|
||
else if (c === 95) edge.numberOfKnots = int(v)
|
||
else if (c === 96) edge.numberOfControlPoints = int(v)
|
||
}
|
||
for (let k = 0; k < edge.numberOfKnots && i < p.length; k++) {
|
||
if (p[i][0] !== 40) break
|
||
edge.knots.push(num(p[i][1])); i++
|
||
}
|
||
for (let k = 0; k < edge.numberOfControlPoints && i < p.length; k++) {
|
||
i = seek(p, i, 10)
|
||
if (i >= p.length) break
|
||
const x = num(p[i][1]); i++
|
||
const y = p[i] && p[i][0] === 20 ? num(p[i][1]) : 0
|
||
if (p[i] && p[i][0] === 20) i++
|
||
let w
|
||
if (rational && p[i] && p[i][0] === 42) { w = num(p[i][1]); i++ }
|
||
edge.controlPoints.push(w != null ? { x, y, weight: w } : { x, y })
|
||
}
|
||
if (i < p.length && p[i][0] === 97) {
|
||
const nf = int(p[i][1]); i++
|
||
for (let k = 0; k < nf && i < p.length; k++) {
|
||
if (p[i][0] !== 11) break
|
||
const x = num(p[i][1]); i++
|
||
const y = p[i] && p[i][0] === 21 ? num(p[i][1]) : 0
|
||
if (p[i] && p[i][0] === 21) i++
|
||
edge.fitDatum.push({ x, y })
|
||
}
|
||
edge.numberOfFitData = edge.fitDatum.length
|
||
}
|
||
return { i, edge }
|
||
}
|
||
|
||
function readDefLines(p, i, n, ent) {
|
||
for (let k = 0; k < n; k++) {
|
||
i = seek(p, i, 53)
|
||
if (i >= p.length) break
|
||
const dl = { angle: num(p[i][1]) * RAD, base: { x: 0, y: 0 }, offset: { x: 0, y: 0 }, numberOfDashLengths: 0, dashLengths: [] }
|
||
i++
|
||
while (i < p.length && [43, 44, 45, 46, 79].includes(p[i][0])) {
|
||
const [c, v] = p[i]; i++
|
||
if (c === 43) dl.base.x = num(v)
|
||
else if (c === 44) dl.base.y = num(v)
|
||
else if (c === 45) dl.offset.x = num(v)
|
||
else if (c === 46) dl.offset.y = num(v)
|
||
else if (c === 79) {
|
||
const nd = int(v)
|
||
for (let t = 0; t < nd && i < p.length && p[i][0] === 49; t++) { dl.dashLengths.push(num(p[i][1])); i++ }
|
||
}
|
||
}
|
||
dl.numberOfDashLengths = dl.dashLengths.length
|
||
ent.definitionLines.push(dl)
|
||
}
|
||
return i
|
||
}
|