Files
Leantime/dev/DWGViewer/js/curves.js

323 lines
11 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.

/**
* 曲线离散化与二维仿射变换工具。
*
* 约定:
* - 变换矩阵一律用扁平数组 m = [a, b, c, d, e, f]
* 对应 x' = a*x + c*y + ey' = b*x + d*y + f与 canvas setTransform 同序)。
* - 点集一律用扁平 Float64Array [x0,y0,x1,y1,...],世界坐标,忽略 Z。
*/
export const TAU = Math.PI * 2
/** 相对弦高容差:段数按 r*EPS 的弦高计算,保证放大后仍圆滑 */
const CHORD_EPS = 1 / 2000
const MIN_SEG = 6
const MAX_SEG = 512
// ---------------------------------------------------------------- 仿射变换
export const IDENTITY = [1, 0, 0, 1, 0, 0]
/** m2 之后再作用 m1即先 m2 后 m1 的复合,等价于矩阵乘 m1·m2 */
export function mul(m1, m2) {
return [
m1[0] * m2[0] + m1[2] * m2[1],
m1[1] * m2[0] + m1[3] * m2[1],
m1[0] * m2[2] + m1[2] * m2[3],
m1[1] * m2[2] + m1[3] * m2[3],
m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
m1[1] * m2[4] + m1[3] * m2[5] + m1[5],
]
}
/** 由插入点/缩放/旋转构造矩阵DWG INSERT 的常规组合:先缩放后旋转再平移) */
export function makeInsertMatrix(tx, ty, sx, sy, rot) {
const c = Math.cos(rot), s = Math.sin(rot)
return [c * sx, s * sx, -s * sy, c * sy, tx, ty]
}
export function applyX(m, x, y) { return m[0] * x + m[2] * y + m[4] }
export function applyY(m, x, y) { return m[1] * x + m[3] * y + m[5] }
export function isIdentity(m) {
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0
}
/**
* 判断矩阵是否为「共形」变换(等比缩放 + 旋转,可含镜像)。
* 共形时圆仍是圆、圆弧角度只需整体旋转,可以交给 canvas 的 arc/ellipse 精确绘制。
*/
export function conformal(m) {
const l1 = Math.hypot(m[0], m[1]) // 列 1 长度
const l2 = Math.hypot(m[2], m[3]) // 列 2 长度
if (l1 < 1e-12 || l2 < 1e-12) return null
const dot = (m[0] * m[2] + m[1] * m[3]) / (l1 * l2)
if (Math.abs(dot) > 1e-6) return null // 两轴不正交 → 有错切
if (Math.abs(l1 - l2) > 1e-9 * Math.max(l1, l2)) return null // 非等比
const det = m[0] * m[3] - m[1] * m[2]
return { scale: l1, rotation: Math.atan2(m[1], m[0]), mirror: det < 0 }
}
/** 变换的「平均缩放」,用于线宽、文字高度等标量的换算 */
export function matrixScale(m) {
return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2])) || Math.hypot(m[0], m[1]) || 1
}
/** 原地变换扁平点集 */
export function transformPoints(pts, m) {
if (isIdentity(m)) return pts
const out = new Float64Array(pts.length)
for (let i = 0; i < pts.length; i += 2) {
const x = pts[i], y = pts[i + 1]
out[i] = m[0] * x + m[2] * y + m[4]
out[i + 1] = m[1] * x + m[3] * y + m[5]
}
return out
}
// ---------------------------------------------------------------- 离散化
/** 按弦高容差算圆弧分段数 */
export function arcSegCount(sweep) {
const step = 2 * Math.acos(Math.max(-1, 1 - CHORD_EPS))
const n = Math.ceil(Math.abs(sweep) / step)
return Math.min(MAX_SEG, Math.max(MIN_SEG, n))
}
/**
* 圆弧采样。角度为弧度,逆时针从 a0 到 a1DWG/DXF 的圆弧一律逆时针)。
*/
export function arcPoints(cx, cy, r, a0, a1) {
let sweep = a1 - a0
while (sweep <= 0) sweep += TAU
while (sweep > TAU) sweep -= TAU
const n = arcSegCount(sweep)
const out = new Float64Array((n + 1) * 2)
for (let i = 0; i <= n; i++) {
const a = a0 + (sweep * i) / n
out[i * 2] = cx + r * Math.cos(a)
out[i * 2 + 1] = cy + r * Math.sin(a)
}
return out
}
export function circlePoints(cx, cy, r) {
const n = arcSegCount(TAU)
const out = new Float64Array((n + 1) * 2)
for (let i = 0; i <= n; i++) {
const a = (TAU * i) / n
out[i * 2] = cx + r * Math.cos(a)
out[i * 2 + 1] = cy + r * Math.sin(a)
}
return out
}
/**
* 椭圆采样。majX/majY 是长轴端点相对圆心的向量ratio 为短轴/长轴。
* a0/a1 是 DXF 的「参数角」(不是几何角)。
*/
export function ellipsePoints(cx, cy, majX, majY, ratio, a0 = 0, a1 = TAU) {
const rx = Math.hypot(majX, majY)
const ry = rx * ratio
const rot = Math.atan2(majY, majX)
let sweep = a1 - a0
if (Math.abs(sweep) < 1e-12) sweep = TAU
while (sweep <= 0) sweep += TAU
while (sweep > TAU + 1e-9) sweep -= TAU
const n = arcSegCount(sweep)
const cr = Math.cos(rot), sr = Math.sin(rot)
const out = new Float64Array((n + 1) * 2)
for (let i = 0; i <= n; i++) {
const t = a0 + (sweep * i) / n
const x = rx * Math.cos(t), y = ry * Math.sin(t)
out[i * 2] = cx + x * cr - y * sr
out[i * 2 + 1] = cy + x * sr + y * cr
}
return out
}
/**
* 多段线凸度bulgebulge = tan(圆心角/4),正为逆时针。
* 返回不含起点、含终点的采样点(便于拼接)。
*/
export function bulgeArcPoints(x0, y0, x1, y1, bulge) {
const dx = x1 - x0, dy = y1 - y0
const chord = Math.hypot(dx, dy)
if (chord < 1e-12 || Math.abs(bulge) < 1e-12) return [x1, y1]
const theta = 4 * Math.atan(bulge) // 圆心角(带符号)
const r = chord / (2 * Math.sin(Math.abs(theta) / 2))
// 圆心:弦中点沿法线偏移
const h = r * Math.cos(theta / 2) // 带符号的中点到圆心距离
const mx = (x0 + x1) / 2, my = (y0 + y1) / 2
const nx = -dy / chord, ny = dx / chord
const cx = mx + nx * h, cy = my + ny * h
const a0 = Math.atan2(y0 - cy, x0 - cx)
const n = arcSegCount(theta)
const out = []
for (let i = 1; i <= n; i++) {
const a = a0 + (theta * i) / n
out.push(cx + r * Math.cos(a), cy + r * Math.sin(a))
}
return out
}
/**
* NURBS / B 样条求值de Boor 算法),支持有理(权重)样条。
* controlPoints: [{x,y,weight?}]knots: number[]
*/
export function splinePoints(degree, controlPoints, knots, closed = false) {
const n = controlPoints.length
if (n < 2) return new Float64Array(0)
if (n === 2 || degree < 1) {
const out = new Float64Array(n * 2)
controlPoints.forEach((p, i) => { out[i * 2] = p.x; out[i * 2 + 1] = p.y })
return out
}
const p = Math.min(degree, n - 1)
let kn = knots
if (!kn || kn.length !== n + p + 1) kn = uniformKnots(n, p)
// 采样密度:按控制多边形长度自适应,段数上限保证性能
let polyLen = 0
for (let i = 1; i < n; i++) {
polyLen += Math.hypot(controlPoints[i].x - controlPoints[i - 1].x, controlPoints[i].y - controlPoints[i - 1].y)
}
const steps = Math.min(MAX_SEG, Math.max(MIN_SEG * 2, (n - p) * 12))
const t0 = kn[p], t1 = kn[n]
if (!(t1 > t0)) return new Float64Array(0)
const out = new Float64Array((steps + 1) * 2)
for (let i = 0; i <= steps; i++) {
const t = t0 + ((t1 - t0) * i) / steps
const pt = deBoor(p, controlPoints, kn, i === steps ? t1 - 1e-12 : t)
out[i * 2] = pt[0]
out[i * 2 + 1] = pt[1]
}
if (closed) { /* 闭合样条由调用方补首尾 */ }
return out
}
function uniformKnots(n, p) {
const kn = []
for (let i = 0; i < n + p + 1; i++) {
if (i <= p) kn.push(0)
else if (i >= n) kn.push(n - p)
else kn.push(i - p)
}
return kn
}
function deBoor(p, cps, kn, t) {
const n = cps.length
// 定位区间
let k = p
while (k < n - 1 && kn[k + 1] <= t) k++
const dx = [], dy = [], dw = []
for (let j = 0; j <= p; j++) {
const cp = cps[k - p + j] || cps[n - 1]
const w = cp.weight == null ? 1 : cp.weight
dx.push(cp.x * w); dy.push(cp.y * w); dw.push(w)
}
for (let r = 1; r <= p; r++) {
for (let j = p; j >= r; j--) {
const i = k - p + j
const den = kn[i + p - r + 1] - kn[i]
const a = den === 0 ? 0 : (t - kn[i]) / den
dx[j] = (1 - a) * dx[j - 1] + a * dx[j]
dy[j] = (1 - a) * dy[j - 1] + a * dy[j]
dw[j] = (1 - a) * dw[j - 1] + a * dw[j]
}
}
const w = dw[p] || 1
return [dx[p] / w, dy[p] / w]
}
/** 拟合点样条:用 Catmull-Rom 过点插值近似 */
export function fitPointCurve(fitPoints, closed = false) {
const n = fitPoints.length
if (n < 2) return new Float64Array(0)
if (n === 2) return new Float64Array([fitPoints[0].x, fitPoints[0].y, fitPoints[1].x, fitPoints[1].y])
const segs = 12
const pts = []
const at = (i) => {
if (closed) return fitPoints[((i % n) + n) % n]
return fitPoints[Math.min(n - 1, Math.max(0, i))]
}
const last = closed ? n : n - 1
pts.push(fitPoints[0].x, fitPoints[0].y)
for (let i = 0; i < last; i++) {
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2)
for (let s = 1; s <= segs; s++) {
const t = s / segs, t2 = t * t, t3 = t2 * t
pts.push(
0.5 * ((2 * p1.x) + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
0.5 * ((2 * p1.y) + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
)
}
}
return new Float64Array(pts)
}
// ---------------------------------------------------------------- 包围盒
/**
* 椭圆弧的紧包围盒。
*
* 不能直接用整圆的外接方框:机械图里常见半径几万、只画几度的过渡圆弧,
* 按整圆算会把图纸范围撑大几个数量级,导致「全图」后什么都看不见。
* 这里取两个端点,再加上落在弧段内的 x/y 极值参数角。
*/
export function arcBBox(cx, cy, rx, ry, rot, a0, a1) {
const cr = Math.cos(rot), sr = Math.sin(rot)
const at = (t) => {
const x = rx * Math.cos(t), y = ry * Math.sin(t)
return [cx + x * cr - y * sr, cy + x * sr + y * cr]
}
let sweep = a1 - a0
while (sweep <= 0) sweep += TAU
if (sweep > TAU) sweep = TAU
const inArc = (t) => {
if (sweep >= TAU - 1e-9) return true
let d = t - a0
while (d < 0) d += TAU
while (d > TAU) d -= TAU
return d <= sweep
}
const b = [Infinity, Infinity, -Infinity, -Infinity]
const add = (p) => {
if (p[0] < b[0]) b[0] = p[0]
if (p[1] < b[1]) b[1] = p[1]
if (p[0] > b[2]) b[2] = p[0]
if (p[1] > b[3]) b[3] = p[1]
}
add(at(a0))
add(at(a0 + sweep))
// dx/dt = 0 与 dy/dt = 0 的参数角
const tx = Math.atan2(-ry * sr, rx * cr)
const ty = Math.atan2(ry * cr, rx * sr)
for (const t of [tx, tx + Math.PI, ty, ty + Math.PI]) {
if (inArc(t)) add(at(t))
}
return b
}
export function bboxOfPoints(pts, box) {
const b = box || [Infinity, Infinity, -Infinity, -Infinity]
for (let i = 0; i < pts.length; i += 2) {
const x = pts[i], y = pts[i + 1]
if (x < b[0]) b[0] = x
if (y < b[1]) b[1] = y
if (x > b[2]) b[2] = x
if (y > b[3]) b[3] = y
}
return b
}
export function growBox(b, o) {
if (o[0] < b[0]) b[0] = o[0]
if (o[1] < b[1]) b[1] = o[1]
if (o[2] > b[2]) b[2] = o[2]
if (o[3] > b[3]) b[3] = o[3]
return b
}