Files
Leantime/dev/DWGViewer/js/hatch.js

273 lines
10 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.

/**
* HATCH填充/剖面线)的边界提取与图案线生成。
*
* 机械图纸里剖面线占比很高,只画边界会看着很空,所以这里按 AutoCAD 的
* 图案定义线definitionLines真实生成一族平行线再用奇偶规则裁剪到边界内。
*/
import { arcPoints, bulgeArcPoints, ellipsePoints, splinePoints } from './curves.js'
/** 单个填充最多生成多少条图案线(防止比例异常的图案把页面拖死) */
const MAX_PATTERN_LINES = 1200
const MAX_SEGMENTS = 20000
/**
* 内置的 AutoCAD 预定义图案表(摘自 acad.pat单位为英寸、角度为度
*
* DWG/DXF 对 ANSI31 这类预定义图案只存名字 + 角度 + 比例,定义线要靠 acad.pat 现场展开。
* 机械图的剖面线基本都是 ANSI31没有这张表的话整个剖面区域会是空的。
* 每条定义线:[角度, 基点x, 基点y, 沿线偏移, 行距, ...虚线段]
*/
const BUILTIN_PATTERNS = {
ANSI31: [[45, 0, 0, 0, 0.125]],
ANSI32: [[45, 0, 0, 0, 0.375], [45, 0.176776695, 0, 0, 0.375]],
ANSI33: [[45, 0, 0, 0, 0.25], [45, 0.176776695, 0, 0, 0.25, 0.125, -0.0625]],
ANSI34: [[45, 0, 0, 0, 0.75], [45, 0.176776695, 0, 0, 0.75],
[45, 0.353553391, 0, 0, 0.75], [45, 0.530330086, 0, 0, 0.75]],
ANSI35: [[45, 0, 0, 0, 0.25], [45, 0.176776695, 0, 0, 0.25, 0.3125, -0.0625, 0, -0.0625]],
ANSI36: [[45, 0, 0, 0.21875, 0.125, 0.3125, -0.0625, 0, -0.0625]],
ANSI37: [[45, 0, 0, 0, 0.125], [135, 0, 0, 0, 0.125]],
ANSI38: [[45, 0, 0, 0, 0.125], [135, 0, 0, 0, 0.125, 0.3125, -0.1875]],
LINE: [[0, 0, 0, 0, 0.125]],
NET: [[0, 0, 0, 0, 0.125], [90, 0, 0, 0, 0.125]],
CROSS: [[0, 0, 0, 0.125, 0.125, 0.0625, -0.0625], [90, 0, 0, 0.125, 0.125, 0.0625, -0.0625]],
DOTS: [[0, 0, 0, 0.03125, 0.0625, 0, -0.0625]],
STEEL: [[45, 0, 0, 0, 0.125], [45, 0.0883883476, 0, 0, 0.125]],
GRASS: [[0, 0, 0, 0, 0.25]],
EARTH: [[0, 0, 0, 0.25, 0.25, 0.25, -0.125], [0, 0, 0.0625, 0.25, 0.25, 0.25, -0.125]],
}
const RAD = Math.PI / 180
/**
* 按图案名 + 角度 + 比例展开出定义线。
* @returns {Array|null} 与文件里 definitionLines 同构的数组
*/
export function builtinDefinitionLines(name, angle = 0, scale = 1) {
if (!name) return null
const key = String(name).toUpperCase().replace(/^[*_]/, '')
const def = BUILTIN_PATTERNS[key] || (key === 'SOLID' ? null : BUILTIN_PATTERNS.ANSI31)
if (!def) return null
const s = scale > 0 ? scale : 1
const ca = Math.cos(angle), sa = Math.sin(angle)
return def.map((d) => {
const [a, bx, by, dx, dy, ...dashes] = d
const la = a * RAD + angle // 该定义线的最终角度
// 基点按图案角度旋转;偏移量按定义线角度旋转 —— 与 AutoCAD 写进
// DXF 43/44、45/46 的值保持一致(都是世界坐标系下的量)
const sx = bx * s, sy = by * s
const cl = Math.cos(la), sl = Math.sin(la)
const ox = dx * s, oy = dy * s
return {
angle: la,
base: { x: sx * ca - sy * sa, y: sx * sa + sy * ca },
offset: { x: ox * cl - oy * sl, y: ox * sl + oy * cl },
numberOfDashLengths: dashes.length,
dashLengths: dashes.map((v) => v * s),
}
})
}
/**
* 把 HATCH 的边界路径解析成一组闭合环(实体自身坐标系下的扁平点数组)。
*/
export function hatchLoops(ent) {
const loops = []
for (const path of ent.boundaryPaths || []) {
if (!path) continue
const pts = []
if (path.vertices && path.vertices.length) {
// 多段线型边界
const vs = path.vertices
for (let i = 0; i < vs.length; i++) {
const v = vs[i]
if (!v) continue
if (i === 0) pts.push(v.x, v.y)
const nx = vs[i + 1] || (path.isClosed ? vs[0] : null)
if (!nx) break
if (path.hasBulge && v.bulge) pts.push(...bulgeArcPoints(v.x, v.y, nx.x, nx.y, v.bulge))
else pts.push(nx.x, nx.y)
}
} else if (path.edges) {
// libredwg 偶尔会给出带空洞的 edges 数组,逐个防一手
for (const e of path.edges) {
if (e && e.type != null) appendEdge(pts, e)
}
}
if (pts.length >= 6) loops.push(new Float64Array(pts))
}
return loops
}
function appendEdge(pts, e) {
const push = (arr) => {
// 去掉与上一点重复的首点,避免断裂
let s = 0
if (pts.length >= 2 && arr.length >= 2 &&
Math.abs(pts[pts.length - 2] - arr[0]) < 1e-9 && Math.abs(pts[pts.length - 1] - arr[1]) < 1e-9) s = 2
for (let i = s; i < arr.length; i++) pts.push(arr[i])
}
switch (e.type) {
case 1: // Line
if (!e.start || !e.end) break
push([e.start.x, e.start.y, e.end.x, e.end.y])
break
case 2: { // Circular arc角度已在解析层统一为弧度
if (!e.center || !(e.radius > 0)) break
const p = arcPoints(e.center.x, e.center.y, e.radius, e.startAngle || 0, e.endAngle || 0)
push(e.isCCW === false ? reverse(p) : p)
break
}
case 3: { // Elliptic arcend 是长轴端点相对圆心的向量
if (!e.center || !e.end) break
const p = ellipsePoints(e.center.x, e.center.y, e.end.x, e.end.y,
e.lengthOfMinorAxis, e.startAngle || 0, e.endAngle || 0)
push(e.isCCW === false ? reverse(p) : p)
break
}
case 4: { // Spline
const cps = (e.controlPoints || []).map((c) => ({ x: c.x, y: c.y, weight: c.weight }))
if (cps.length) push(splinePoints(e.degree || 3, cps, e.knots))
else if (e.fitDatum && e.fitDatum.length) {
const arr = []
for (const p of e.fitDatum) arr.push(p.x, p.y)
push(arr)
}
break
}
default:
break
}
}
function reverse(pts) {
const out = new Float64Array(pts.length)
for (let i = 0, n = pts.length / 2; i < n; i++) {
out[i * 2] = pts[(n - 1 - i) * 2]
out[i * 2 + 1] = pts[(n - 1 - i) * 2 + 1]
}
return out
}
/** 环集合的包围盒 */
export function loopsBBox(loops) {
const b = [Infinity, Infinity, -Infinity, -Infinity]
for (const l of loops) {
for (let i = 0; i < l.length; i += 2) {
if (l[i] < b[0]) b[0] = l[i]
if (l[i + 1] < b[1]) b[1] = l[i + 1]
if (l[i] > b[2]) b[2] = l[i]
if (l[i + 1] > b[3]) b[3] = l[i + 1]
}
}
return b
}
/**
* 生成图案线,返回扁平线段数组 [x0,y0,x1,y1, x0,y0,x1,y1, ...]。
* 用奇偶规则裁剪:把每条候选直线与所有边界求交,排序后取奇数区间。
*/
export function hatchPatternSegments(loops, defLines) {
const out = []
if (!loops.length || !defLines || !defLines.length) return out
const bb = loopsBBox(loops)
if (!isFinite(bb[0])) return out
const diag = Math.hypot(bb[2] - bb[0], bb[3] - bb[1])
if (!(diag > 0)) return out
for (const dl of defLines) {
const ang = dl.angle || 0
const ux = Math.cos(ang), uy = Math.sin(ang)
const px = -uy, py = ux // 垂直方向
const ox = dl.base ? dl.base.x : 0
const oy = dl.base ? dl.base.y : 0
// offset 是世界坐标系下「相邻两条线的位移向量」,不是定义线自身坐标系的分量
const offX = dl.offset ? dl.offset.x : 0
const offY = dl.offset ? dl.offset.y : 0
const perp = offX * px + offY * py // 位移在法线方向的分量 = 真正的行距
if (Math.abs(perp) < 1e-9) continue // 位移与线平行,画不出一族线
const spacing = Math.abs(perp)
if (diag / spacing > MAX_PATTERN_LINES) continue // 行距过密,跳过(多半是比例异常)
// 计算需要覆盖的 n 范围:把包围盒四角投影到法线方向
let nMin = Infinity, nMax = -Infinity
for (const [cx, cy] of [[bb[0], bb[1]], [bb[2], bb[1]], [bb[0], bb[3]], [bb[2], bb[3]]]) {
const n = ((cx - ox) * px + (cy - oy) * py) / perp
if (n < nMin) nMin = n
if (n > nMax) nMax = n
}
nMin = Math.floor(nMin) - 1
nMax = Math.ceil(nMax) + 1
if (!isFinite(nMin) || !isFinite(nMax)) continue
const dashes = (dl.dashLengths || []).filter((d) => isFinite(d))
const patLen = dashes.reduce((s, d) => s + Math.abs(d), 0)
for (let n = nMin; n <= nMax; n++) {
// 该条线上的一个点
const bx = ox + n * offX
const by = oy + n * offY
const hits = lineLoopIntersections(loops, bx, by, ux, uy)
if (hits.length < 2) continue
hits.sort((a, b) => a - b)
for (let k = 0; k + 1 < hits.length; k += 2) {
const t0 = hits[k], t1 = hits[k + 1]
if (t1 - t0 < 1e-9) continue
if (patLen > 1e-9) emitDashed(out, bx, by, ux, uy, t0, t1, dashes, patLen)
else out.push(bx + ux * t0, by + uy * t0, bx + ux * t1, by + uy * t1)
if (out.length > MAX_SEGMENTS * 4) return out
}
}
}
return out
}
/** 在 [t0,t1] 区间内按虚线定义切段(负值为空白) */
function emitDashed(out, bx, by, ux, uy, t0, t1, dashes, patLen) {
// 从图案原点对齐:找到 t0 所在的图案相位
let t = t0 - ((t0 % patLen) + patLen) % patLen
let guard = 0
while (t < t1 && guard++ < 4000) {
for (const d of dashes) {
const len = Math.abs(d) < 1e-9 ? 1e-9 : Math.abs(d)
const a = t, b = t + len
if (d >= 0) {
const s = Math.max(a, t0), e = Math.min(b, t1)
if (e > s) out.push(bx + ux * s, by + uy * s, bx + ux * e, by + uy * e)
}
t = b
if (t > t1) break
}
}
}
/** 直线 (b + t*u) 与所有边界环的交点参数 t */
function lineLoopIntersections(loops, bx, by, ux, uy) {
const ts = []
const px = -uy, py = ux
for (const l of loops) {
const n = l.length / 2
for (let i = 0; i < n; i++) {
const j = (i + 1) % n
const x0 = l[i * 2], y0 = l[i * 2 + 1]
const x1 = l[j * 2], y1 = l[j * 2 + 1]
// 到直线的有符号距离
const d0 = (x0 - bx) * px + (y0 - by) * py
const d1 = (x1 - bx) * px + (y1 - by) * py
if ((d0 > 0 && d1 > 0) || (d0 < 0 && d1 < 0)) continue
if (d0 === 0 && d1 === 0) continue // 共线,忽略
// 顶点正好落在线上时只算一次(用半开区间规则避免重复计数)
if (d1 === 0) continue
if (d0 === 0) {
ts.push((x0 - bx) * ux + (y0 - by) * uy)
continue
}
const s = d0 / (d0 - d1)
const ix = x0 + (x1 - x0) * s
const iy = y0 + (y1 - y0) * s
ts.push((ix - bx) * ux + (iy - by) * uy)
}
}
return ts
}