OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

322
dev/DWGViewer/js/curves.js Normal file
View File

@@ -0,0 +1,322 @@
/**
* 曲线离散化与二维仿射变换工具。
*
* 约定:
* - 变换矩阵一律用扁平数组 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
}