OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
272
dev/DWGViewer/js/hatch.js
Normal file
272
dev/DWGViewer/js/hatch.js
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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 arc;end 是长轴端点相对圆心的向量
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user