164 lines
5.6 KiB
JavaScript
164 lines
5.6 KiB
JavaScript
/**
|
||
* MTEXT / TEXT 的文字内容解析。
|
||
*
|
||
* MTEXT 的正文里混着 AutoCAD 的格式码(\P 换行、\H 字高、\S 堆叠、{} 分组……),
|
||
* 直接画出来会满屏乱码,所以这里把它拆成「行 → 文字段(run)」的结构,
|
||
* 每个 run 带自己的字高倍数、宽度因子、倾斜、粗斜体、颜色和下划线。
|
||
*/
|
||
|
||
/** %%d 之类的控制码(TEXT 与 MTEXT 通用) */
|
||
function replaceSpecial(s) {
|
||
return s
|
||
.replace(/%%[dD]/g, '°') // 度
|
||
.replace(/%%[cC]/g, '∅') // 直径 ⌀
|
||
.replace(/%%[pP]/g, '±') // 正负
|
||
.replace(/%%%/g, '%')
|
||
}
|
||
|
||
/** \U+00B0 形式的 Unicode 转义 */
|
||
function replaceUnicode(s) {
|
||
return s.replace(/\\U\+([0-9A-Fa-f]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)))
|
||
}
|
||
|
||
const BASE_STYLE = {
|
||
hFactor: 1, // 相对于实体 textHeight 的倍数
|
||
wFactor: 1,
|
||
oblique: 0,
|
||
bold: false,
|
||
italic: false,
|
||
font: null,
|
||
color: null, // ACI 索引或 0xRRGGBB(带 trueColor 标记)
|
||
underline: false,
|
||
overline: false,
|
||
strike: false,
|
||
rise: 0, // 相对基线的抬升(堆叠分数用),单位为字高倍数
|
||
}
|
||
|
||
/**
|
||
* 解析 MTEXT 正文。
|
||
* @returns {{lines: Array<Array<Run>>, columnBreaks: number[]}}
|
||
*/
|
||
export function parseMText(raw) {
|
||
const lines = [[]]
|
||
if (!raw) return { lines }
|
||
const text = replaceUnicode(raw)
|
||
const stack = []
|
||
let st = { ...BASE_STYLE }
|
||
let buf = ''
|
||
|
||
const flush = () => {
|
||
if (!buf) return
|
||
lines[lines.length - 1].push({ ...st, text: replaceSpecial(buf) })
|
||
buf = ''
|
||
}
|
||
const newLine = () => { flush(); lines.push([]) }
|
||
|
||
// 读到分号为止的参数
|
||
let i = 0
|
||
const readArg = () => {
|
||
let out = ''
|
||
while (i < text.length && text[i] !== ';') { out += text[i]; i++ }
|
||
i++ // 吃掉 ;
|
||
return out
|
||
}
|
||
|
||
while (i < text.length) {
|
||
const ch = text[i]
|
||
if (ch === '\\') {
|
||
const c = text[i + 1]
|
||
i += 2
|
||
switch (c) {
|
||
case 'P': newLine(); break
|
||
case 'X': newLine(); break
|
||
case '~': buf += ' '; break
|
||
case '\\': buf += '\\'; break
|
||
case '{': buf += '{'; break
|
||
case '}': buf += '}'; break
|
||
case 'L': flush(); st = { ...st, underline: true }; break
|
||
case 'l': flush(); st = { ...st, underline: false }; break
|
||
case 'O': flush(); st = { ...st, overline: true }; break
|
||
case 'o': flush(); st = { ...st, overline: false }; break
|
||
case 'K': flush(); st = { ...st, strike: true }; break
|
||
case 'k': flush(); st = { ...st, strike: false }; break
|
||
case 'H': {
|
||
flush()
|
||
const a = readArg()
|
||
const rel = /x$/i.test(a)
|
||
const v = parseFloat(a)
|
||
if (isFinite(v)) st = { ...st, hFactor: rel ? st.hFactor * v : v }
|
||
break
|
||
}
|
||
case 'W': { flush(); const v = parseFloat(readArg()); if (isFinite(v) && v > 0) st = { ...st, wFactor: v }; break }
|
||
case 'Q': { flush(); const v = parseFloat(readArg()); if (isFinite(v)) st = { ...st, oblique: (v * Math.PI) / 180 }; break }
|
||
case 'T': { readArg(); break } // 字间距,忽略
|
||
case 'A': { readArg(); break } // 对齐,交由整体对齐处理
|
||
case 'p': { readArg(); break } // 段落属性,忽略
|
||
case 'C': { flush(); const v = parseInt(readArg(), 10); if (isFinite(v)) st = { ...st, color: { aci: v } }; break }
|
||
case 'c': { flush(); const v = parseInt(readArg(), 10); if (isFinite(v)) st = { ...st, color: { rgb: bgrToRgb(v) } }; break }
|
||
case 'F':
|
||
case 'f': {
|
||
flush()
|
||
const a = readArg()
|
||
const parts = a.split('|')
|
||
const next = { ...st, font: parts[0] || null }
|
||
for (const p of parts.slice(1)) {
|
||
if (/^b1$/i.test(p)) next.bold = true
|
||
else if (/^b0$/i.test(p)) next.bold = false
|
||
else if (/^i1$/i.test(p)) next.italic = true
|
||
else if (/^i0$/i.test(p)) next.italic = false
|
||
}
|
||
st = next
|
||
break
|
||
}
|
||
case 'S': {
|
||
// 堆叠分数: 上^下; 上/下; 上#下;
|
||
flush()
|
||
let a = ''
|
||
while (i < text.length && text[i] !== ';') { a += text[i]; i++ }
|
||
i++
|
||
const m = a.match(/^(.*?)([\^\/#])(.*)$/)
|
||
if (m) {
|
||
const upper = replaceSpecial(m[1]), lower = replaceSpecial(m[3])
|
||
const small = { ...st, hFactor: st.hFactor * 0.65 }
|
||
const arr = lines[lines.length - 1]
|
||
if (upper) arr.push({ ...small, text: upper, rise: 0.55 })
|
||
if (m[2] !== '^' && upper && lower) arr.push({ ...small, text: '/', rise: 0 })
|
||
if (lower) arr.push({ ...small, text: lower, rise: -0.25 })
|
||
} else if (a) {
|
||
buf += replaceSpecial(a)
|
||
}
|
||
break
|
||
}
|
||
default:
|
||
// 未知转义,原样保留字符
|
||
if (c != null) buf += c
|
||
}
|
||
} else if (ch === '{') {
|
||
flush(); stack.push({ ...st }); i++
|
||
} else if (ch === '}') {
|
||
flush(); if (stack.length) st = stack.pop(); i++
|
||
} else if (ch === '\n') {
|
||
newLine(); i++
|
||
} else if (ch === '\r') {
|
||
i++
|
||
} else {
|
||
buf += ch; i++
|
||
}
|
||
}
|
||
flush()
|
||
return { lines }
|
||
}
|
||
|
||
/** DXF 的 true color 在 MTEXT 里是 BGR 顺序 */
|
||
function bgrToRgb(v) {
|
||
const b = (v >> 16) & 0xff, g = (v >> 8) & 0xff, r = v & 0xff
|
||
return (r << 16) | (g << 8) | b
|
||
}
|
||
|
||
/** 单行 TEXT / ATTRIB 的内容处理 */
|
||
export function parseSimpleText(raw) {
|
||
if (!raw) return ''
|
||
return replaceSpecial(replaceUnicode(String(raw)))
|
||
.replace(/%%[uUoO]/g, '') // 下划线/上划线开关,画不出来就去掉标记
|
||
}
|