/** * 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 }