// 质量属性:重心和转动惯量。
//
// 对应 trimesh 的 `mesh.center_mass` 和 `mesh.moment_inertia`。
//
// 算法和体积用的是同一套:把网格拆成一堆「原点 + 三角形」的四面体,
// 用散度定理把体积分变成面积分。体积是零阶矩,重心是一阶矩,
// 转动惯量是二阶矩——同一个套路往下走而已:
//
// 体积 = Σ 有符号四面体体积
// 一阶矩 = Σ 体积 × 四面体重心
// 二阶矩 = Σ 体积 × (四面体的二阶矩公式)
//
// 为什么值得单独做:**重心偏了的模型打印时容易倒**。这不是几何上「坏」,
// 水密性检查一个字都不会说,但它是个实打实的打印问题。
//
// 前提:**网格要水密,而且朝向一致**。有破洞的话正负抵消不干净,
// 算出来的数没有意义——这一点报告里会说明,不装作没这回事。
// 以下底层计算不自动校验网格。CLI / 网页先通过
// PrintabilityReport::mass_properties_available();它仍不证明无自相交。
///|
/// 一个四面体(原点 + 三角形 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 mut volume = 0.0
let mut mx = 0.0
let mut my = 0.0
let mut mz = 0.0
for t in self.triangles {
let (a, b, c) = (t.a, t.b, t.c)
let vol = tet_volume(a, b, c)
volume = volume + vol
// 四面体的重心是四个顶点的平均,原点是 (0,0,0),
// 所以是 (a+b+c)/4。乘上体积就是这个四面体的一阶矩
mx = mx + vol * (a.x + b.x + c.x) / 4.0
my = my + vol * (a.y + b.y + c.y) / 4.0
mz = mz + vol * (a.z + b.z + c.z) / 4.0
}
// 朝向整体反了的话(体积为负),三个分量一起是负的,
// 相除仍然得到正确的位置——这里不用特殊处理
if volume == 0.0 {
return Vec3::zero()
}
Vec3::new(mx / volume, my / volume, mz / volume)
}
///|
/// 网格关于**重心**的转动惯量张量,假设密度为 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] {
// 关于原点的二阶矩。四个顶点是「原点 + a + b + c」
let mut volume = 0.0
let mut s_xx = 0.0
let mut s_yy = 0.0
let mut s_zz = 0.0
let mut s_xy = 0.0
let mut s_yz = 0.0
let mut s_zx = 0.0
for t in self.triangles {
let (a, b, c) = (t.a, t.b, t.c)
let vol = tet_volume(a, b, c)
volume = volume + 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)
s_xx = s_xx + vol * (sq_x + sx * sx) / 20.0
s_yy = s_yy + vol * (sq_y + sy * sy) / 20.0
s_zz = s_zz + vol * (sq_z + sz * sz) / 20.0
s_xy = s_xy + vol * (sq_xy + sx * sy) / 20.0
s_yz = s_yz + vol * (sq_yz + sy * sz) / 20.0
s_zx = s_zx + vol * (sq_zx + sz * sx) / 20.0
}
// 朝向整体反了的话,所有矩都是负的。翻转过来,
// 这样「顶点顺序写反了的模型」也能算出正确的物理量
if volume < 0.0 {
s_xx = -s_xx
s_yy = -s_yy
s_zz = -s_zz
s_xy = -s_xy
s_yz = -s_yz
s_zx = -s_zx
}
// 关于原点的惯性张量:I = 迹(二阶矩)·单位阵 − 二阶矩
let trace = s_xx + s_yy + s_zz
let i_xx = trace - s_xx
let i_yy = trace - s_yy
let i_zz = trace - s_zz
let i_xy = -s_xy
let i_yz = -s_yz
let i_zx = -s_zx
// 平移到重心:平行轴定理。m = |V|(密度取 1)
let center = self.center_mass()
let mass = if volume < 0.0 { -volume } else { volume }
let cc = center.dot(center)
let i_xx_c = i_xx - mass * (cc - center.x * center.x)
let i_yy_c = i_yy - mass * (cc - center.y * center.y)
let i_zz_c = i_zz - mass * (cc - center.z * center.z)
let i_xy_c = i_xy - mass * (0.0 - center.x * center.y)
let i_yz_c = i_yz - mass * (0.0 - center.y * center.z)
let i_zx_c = i_zx - mass * (0.0 - center.z * center.x)
[
i_xx_c, i_xy_c, i_zx_c,
i_xy_c, i_yy_c, i_yz_c,
i_zx_c, i_yz_c, i_zz_c,
]
}
///|
/// 惯量主轴上的三个主转动惯量(把转动惯量张量对角化之后的三个特征值)。
///
/// 对应 trimesh 的 `mesh.principal_inertia_components`。
///
/// 三个数按**从大到小**返回。物理含义是:绕着对应主轴转的时候,
/// 需要用到的力矩——最小的那个轴就是「最容易转起来的轴」。
///
/// 张量是对称的,所以特征值有闭式解(Cardano 公式),不用迭代。
pub fn Mesh::principal_moments(self : Mesh) -> FixedArray[Double] {
// 特征值用共用实现(mat3.mbt)——有向包围盒的主轴也要用同一套,
// 写两遍迟早会改漏一处
symmetric_eigenvalues(self.moment_inertia())
}