// 质量属性:重心和转动惯量。
//
// 对应 trimesh 的 `mesh.center_mass` 和 `mesh.moment_inertia`。
//
// 算法和体积用的是同一套:把网格拆成一堆「局部参考点 + 三角形」的四面体,
// 用散度定理把体积分变成面积分。体积是零阶矩,重心是一阶矩,
// 转动惯量是二阶矩——同一个套路往下走而已:
//
// 体积 = Σ 有符号四面体体积
// 一阶矩 = Σ 体积 × 四面体重心
// 二阶矩 = Σ 体积 × (四面体的二阶矩公式)
//
// 为什么值得单独做:**重心偏了的模型打印时容易倒**。这不是几何上「坏」,
// 水密性检查一个字都不会说,但它是个实打实的打印问题。
//
// 前提:**网格要水密,而且朝向一致**。有破洞的话正负抵消不干净,
// 算出来的数没有意义——这一点报告里会说明,不装作没这回事。
// 以下底层计算不自动校验网格。CLI / 网页先通过
// PrintabilityReport::mass_properties_available();它仍不证明无自相交。
///|
// Neumaier compensated summation, including cancellation between signed tetrahedra.
priv struct MomentSum {
mut sum : Double
mut correction : Double
}
///|
fn MomentSum::new() -> MomentSum {
{ sum: 0.0, correction: 0.0, }
}
///|
fn MomentSum::add(self : MomentSum, value : Double) -> Unit {
let next = self.sum + value
self.correction += if self.sum.abs() >= value.abs() {
self.sum - next + value
} else {
value - next + self.sum
}
self.sum = next
}
///|
fn MomentSum::value(self : MomentSum) -> Double {
self.sum + self.correction
}
///|
fn mass_reference(mesh : Mesh) -> Vec3 {
match mesh.triangles {
[] => Vec3::zero()
[first, ..] => first.a
}
}
///|
/// 一个四面体(原点 + 三角形 a b c)的有符号体积。
fn tet_volume(a : Vec3, b : Vec3, c : Vec3) -> Double {
a.dot(b.cross(c)) / 6.0
}
///|
/// 网格的重心(质心)。假设材料密度均匀。
/// 不自动验证几何前提;调用方应先检查闭合、退化性和绕向 / 分量朝向。
///
/// 返回的是绝对坐标。网格不水密时这个数不可信——一阶矩和体积一样,
/// 靠各面的正负抵消算出来,有破洞就抵消不干净。
pub fn Mesh::center_mass(self : Mesh) -> Vec3 {
let origin = mass_reference(self)
let volume = MomentSum::new()
let mx = MomentSum::new()
let my = MomentSum::new()
let mz = MomentSum::new()
for t in self.triangles {
let (a, b, c) = (t.a.sub(origin), t.b.sub(origin), t.c.sub(origin))
let vol = tet_volume(a, b, c)
volume.add(vol)
// 四面体的重心是四个顶点的平均,原点是 (0,0,0),
// 所以是 (a+b+c)/4。乘上体积就是这个四面体的一阶矩
mx.add(vol * (a.x + b.x + c.x) / 4.0)
my.add(vol * (a.y + b.y + c.y) / 4.0)
mz.add(vol * (a.z + b.z + c.z) / 4.0)
}
// 朝向整体反了的话(体积为负),三个分量一起是负的,
// 相除仍然得到正确的位置——这里不用特殊处理
if volume.value() == 0.0 {
return Vec3::zero()
}
Vec3::new(
origin.x + mx.value() / volume.value(),
origin.y + my.value() / volume.value(),
origin.z + mz.value() / volume.value(),
)
}
///|
/// 网格关于**重心**的转动惯量张量,假设密度为 1。
/// 不自动验证几何前提,与 center_mass() 的调用责任相同。
///
/// 返回 9 个数,按行排列:
///
/// ```text
/// [0] [1] [2] Ixx Ixy Ixz
/// [3] [4] [5] = Iyx Iyy Iyz
/// [6] [7] [8] Izx Izy Izz
/// ```
///
/// 张量是对称的(Ixy = Iyx),所以实际只有 6 个独立分量,
/// 另外 3 个是重复的——保留完整 3×3 是为了和 trimesh 的 `moment_inertia`
/// 对得上,用起来不用想「哪个下标是哪个」。
///
/// 对应 trimesh 的 `mesh.moment_inertia`(`density=1` 时的结果)。
pub fn Mesh::moment_inertia(self : Mesh) -> FixedArray[Double] {
// 直接以重心为参考积分,避免绝对坐标二阶矩减去巨大的平行轴项。
let origin = self.center_mass()
let volume_sum = MomentSum::new()
let xx = MomentSum::new()
let yy = MomentSum::new()
let zz = MomentSum::new()
let xy = MomentSum::new()
let yz = MomentSum::new()
let zx = MomentSum::new()
for t in self.triangles {
let (a, b, c) = (t.a.sub(origin), t.b.sub(origin), t.c.sub(origin))
let vol = tet_volume(a, b, c)
volume_sum.add(vol)
// 四个顶点各分量之和。原点是 0,所以只要 a+b+c
let sx = a.x + b.x + c.x
let sy = a.y + b.y + c.y
let sz = a.z + b.z + c.z
// 平方和与两两乘积:原点是 (0,0,0),所以这四项都只算 a/b/c 三个顶点
let sq_x = a.x * a.x + b.x * b.x + c.x * c.x
let sq_y = a.y * a.y + b.y * b.y + c.y * c.y
let sq_z = a.z * a.z + b.z * b.z + c.z * c.z
let sq_xy = a.x * a.y + b.x * b.y + c.x * c.y
let sq_yz = a.y * a.z + b.y * b.z + c.y * c.z
let sq_zx = a.z * a.x + b.z * b.x + c.z * c.x
// 单纯形上的多项式积分(四面体是 n=3 的单纯形):
//
// ∫ x_i x_j dV = V/20 · [ Σ_k v_{k,i}·v_{k,j} + (Σ_k v_{k,i})(Σ_k v_{k,j}) ]
//
// **两项都要**。第二项是两个和的乘积、不是小数,漏掉的话结果会明显偏小;
// 而且它对角和非对角一视同仁,不用分情况。
//
// (一开始我推错了,写成「对角 V/10、非对角 V/20」,那只有在
// 「原点 + 坐标轴上的三个点」这种特殊四面体上才凑巧对得上。
// 用长方体一测就露馅了:算出来 12/60,正确答案是 20/60)
xx.add(vol * (sq_x + sx * sx) / 20.0)
yy.add(vol * (sq_y + sy * sy) / 20.0)
zz.add(vol * (sq_z + sz * sz) / 20.0)
xy.add(vol * (sq_xy + sx * sy) / 20.0)
yz.add(vol * (sq_yz + sy * sz) / 20.0)
zx.add(vol * (sq_zx + sz * sx) / 20.0)
}
let sign = if volume_sum.value() < 0.0 { -1.0 } else { 1.0 }
let ixx = sign * (yy.value() + zz.value())
let iyy = sign * (xx.value() + zz.value())
let izz = sign * (xx.value() + yy.value())
let ixy = -sign * xy.value()
let iyz = -sign * yz.value()
let izx = -sign * zx.value()
[ixx, ixy, izx, ixy, iyy, iyz, izx, iyz, izz]
}
///|
fn inertia_numeric_valid(tensor : FixedArray[Double]) -> Bool {
if tensor.any(not_finite) ||
tensor[0] < 0.0 ||
tensor[4] < 0.0 ||
tensor[8] < 0.0 {
return false
}
let mut scale = 0.0
for value in tensor {
scale = scale.max(value.abs())
}
if scale == 0.0 {
return false
}
let normalized : FixedArray[Double] = [
tensor[0] / scale,
tensor[1] / scale,
tensor[2] / scale,
tensor[3] / scale,
tensor[4] / scale,
tensor[5] / scale,
tensor[6] / scale,
tensor[7] / scale,
tensor[8] / scale,
]
let eigenvalues = symmetric_eigenvalues(normalized)
!eigenvalues.any(not_finite) &&
eigenvalues[2] >= -1.0e-12 &&
eigenvalues[0] <= eigenvalues[1] + eigenvalues[2] + 1.0e-12
}
///|
/// Necessary numeric checks for mass properties: positive finite volume,
/// finite centroid, and physically admissible inertia. This does not prove
/// geometric validity or bound the numerical error; check the report's
/// mass_properties_available() separately on the same mesh.
pub fn Mesh::mass_properties_numeric_valid(self : Mesh) -> Bool {
let volume = self.volume()
!not_finite(volume) &&
volume > 0.0 &&
finite_vertex(self.center_mass()) &&
inertia_numeric_valid(self.moment_inertia())
}
///|
/// 惯量主轴上的三个主转动惯量(把转动惯量张量对角化之后的三个特征值)。
///
/// 对应 trimesh 的 `mesh.principal_inertia_components`。
///
/// 三个数按**从大到小**返回。物理含义是:绕着对应主轴转的时候,
/// 需要用到的力矩——最小的那个轴就是「最容易转起来的轴」。
///
/// 张量是对称的,所以特征值有闭式解(Cardano 公式),不用迭代。
pub fn Mesh::principal_moments(self : Mesh) -> FixedArray[Double] {
// 特征值用共用实现(mat3.mbt)——有向包围盒的主轴也要用同一套,
// 写两遍迟早会改漏一处
symmetric_eigenvalues(self.moment_inertia())
}