use serde::{Deserialize, Serialize};
use crate::scope::{Scope, Vec3};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Material {
pub id: String,
pub density: Option<f64>,
}
impl Material {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
density: None,
}
}
pub fn with_density(id: impl Into<String>, density: f64) -> Self {
Self {
id: id.into(),
density: Some(density),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SnapPlane {
pub point: Vec3,
pub normal: Vec3,
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MassProperties {
pub mass: f64,
pub centroid: Vec3,
pub inertia: Option<glam::DMat3>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FaceProfile {
Rectangle,
Taper(f64),
Triangle { peak_offset: f64 },
Trapezoid { top_width: f64, offset_x: f64 },
Polygon(Vec<glam::DVec2>),
}
impl FaceProfile {
pub fn is_rectangle(&self) -> bool {
matches!(self, Self::Rectangle)
}
pub fn taper_coeff(&self) -> Option<f64> {
match self {
Self::Taper(t) => Some(*t),
Self::Rectangle => Some(0.0),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Terminal {
pub scope: Scope,
pub mesh_id: String,
pub face_profile: FaceProfile,
pub material: Option<Material>,
pub mass_properties: Option<MassProperties>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl Terminal {
pub fn new(scope: Scope, mesh_id: impl Into<String>) -> Self {
Self {
scope,
mesh_id: mesh_id.into(),
face_profile: FaceProfile::Rectangle,
material: None,
mass_properties: None,
label: None,
}
}
pub fn new_with_taper(scope: Scope, mesh_id: impl Into<String>, taper: f64) -> Self {
let face_profile = taper_to_profile(taper);
Self {
scope,
mesh_id: mesh_id.into(),
face_profile,
material: None,
mass_properties: None,
label: None,
}
}
pub fn new_profiled(
scope: Scope,
mesh_id: impl Into<String>,
face_profile: FaceProfile,
material: Option<Material>,
) -> Self {
let mass_properties = material
.as_ref()
.and_then(|m| m.density)
.and_then(|rho| compute_mass_properties(&scope, &face_profile, rho));
Self {
scope,
mesh_id: mesh_id.into(),
face_profile,
material,
mass_properties,
label: None,
}
}
}
pub fn taper_to_profile(taper: f64) -> FaceProfile {
if taper <= 0.0 {
FaceProfile::Rectangle
} else if (taper - 1.0).abs() < 1e-9 {
FaceProfile::Triangle { peak_offset: 0.5 }
} else {
FaceProfile::Taper(taper.clamp(0.0, 1.0))
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ShapeModel {
pub terminals: Vec<Terminal>,
pub snap_planes: Vec<SnapPlane>,
}
impl ShapeModel {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, terminal: Terminal) {
self.terminals.push(terminal);
}
pub fn len(&self) -> usize {
self.terminals.len()
}
pub fn is_empty(&self) -> bool {
self.terminals.is_empty()
}
}
pub fn compute_mass_properties(
scope: &Scope,
profile: &FaceProfile,
density: f64,
) -> Option<MassProperties> {
if !density.is_finite() || density <= 0.0 {
return None;
}
if !scope.size.is_finite() {
return None;
}
match profile {
FaceProfile::Rectangle => box_mass_properties(scope, density),
FaceProfile::Taper(t) => taper_mass_properties(scope, *t, density),
FaceProfile::Triangle { peak_offset } => {
let sx = scope.size.x;
let sy = scope.size.y;
let verts = vec![
glam::DVec2::new(0.0, 0.0),
glam::DVec2::new(sx, 0.0),
glam::DVec2::new(peak_offset.clamp(0.0, 1.0) * sx, sy),
];
prism_mass_properties_xy(&verts, scope, density)
}
FaceProfile::Trapezoid {
top_width,
offset_x,
} => {
let sx = scope.size.x;
let sy = scope.size.y;
let tw = top_width.clamp(0.0, 1.0);
let ox = offset_x.clamp(0.0, 1.0);
let verts = vec![
glam::DVec2::new(0.0, 0.0),
glam::DVec2::new(sx, 0.0),
glam::DVec2::new((ox + tw) * sx, sy),
glam::DVec2::new(ox * sx, sy),
];
prism_mass_properties_xy(&verts, scope, density)
}
FaceProfile::Polygon(verts) => prism_mass_properties_xz(verts, scope, density),
}
}
fn box_mass_properties(scope: &Scope, density: f64) -> Option<MassProperties> {
let sx = scope.size.x;
let sy = scope.size.y;
let sz = scope.size.z;
if sx <= 0.0 || sy <= 0.0 || sz <= 0.0 {
return None;
}
let volume = sx * sy * sz;
if !volume.is_finite() {
return None;
}
let mass = density * volume;
if !mass.is_finite() {
return None;
}
let local_centroid = Vec3::new(sx * 0.5, sy * 0.5, sz * 0.5);
let centroid = scope.position + scope.rotation * local_centroid;
if !centroid.is_finite() {
return None;
}
let i_xx = mass * (sy * sy + sz * sz) / 12.0;
let i_yy = mass * (sx * sx + sz * sz) / 12.0;
let i_zz = mass * (sx * sx + sy * sy) / 12.0;
let i_local = glam::DMat3::from_diagonal(glam::DVec3::new(i_xx, i_yy, i_zz));
let inertia = rotate_inertia(i_local, scope.rotation);
Some(MassProperties {
mass,
centroid,
inertia: Some(inertia),
})
}
fn taper_mass_properties(scope: &Scope, t: f64, density: f64) -> Option<MassProperties> {
let sx = scope.size.x;
let sy = scope.size.y;
let sz = scope.size.z;
if sx <= 0.0 || sy <= 0.0 || sz <= 0.0 {
return None;
}
let r = (1.0 - t.clamp(0.0, 1.0)).max(0.0);
let volume = sx * sy * sz * (1.0 + r * r + r) / 3.0;
if !volume.is_finite() || volume <= 0.0 {
return None;
}
let mass = density * volume;
if !mass.is_finite() {
return None;
}
let denom = 1.0 + r + r * r;
let cy = if denom > 1e-12 {
sy * 0.25 * (1.0 + 2.0 * r + 3.0 * r * r) / denom
} else {
sy * 0.25
};
let local_centroid = Vec3::new(sx * 0.5, cy, sz * 0.5);
let centroid = scope.position + scope.rotation * local_centroid;
if !centroid.is_finite() {
return None;
}
Some(MassProperties {
mass,
centroid,
inertia: None,
})
}
fn prism_mass_properties_xy(
verts: &[glam::DVec2],
scope: &Scope,
density: f64,
) -> Option<MassProperties> {
let depth = scope.size.z;
if depth <= 0.0 {
return None;
}
let m = polygon_moments(verts)?;
let volume = m.area * depth;
if !volume.is_finite() || volume <= 0.0 {
return None;
}
let mass = density * volume;
if !mass.is_finite() {
return None;
}
let j = (m.ixx_origin - m.area * m.cx * m.cx) / m.area;
let k = (m.iyy_origin - m.area * m.cy * m.cy) / m.area;
let p = (m.ixy_origin - m.area * m.cx * m.cy) / m.area;
let local_centroid = Vec3::new(m.cx, m.cy, depth * 0.5);
let centroid = scope.position + scope.rotation * local_centroid;
if !centroid.is_finite() {
return None;
}
let d2_12 = depth * depth / 12.0;
let i_xx = mass * (d2_12 + k);
let i_yy = mass * (d2_12 + j);
let i_zz = mass * (j + k);
let i_xy = -mass * p;
let i_local = glam::DMat3::from_cols_array(&[
i_xx, i_xy, 0.0, i_xy, i_yy, 0.0, 0.0, 0.0, i_zz, ]);
let inertia = rotate_inertia(i_local, scope.rotation);
Some(MassProperties {
mass,
centroid,
inertia: Some(inertia),
})
}
fn prism_mass_properties_xz(
verts: &[glam::DVec2],
scope: &Scope,
density: f64,
) -> Option<MassProperties> {
let height = scope.size.y;
if height <= 0.0 {
return None;
}
let m = polygon_moments(verts)?;
let volume = m.area * height;
if !volume.is_finite() || volume <= 0.0 {
return None;
}
let mass = density * volume;
if !mass.is_finite() {
return None;
}
let j = (m.ixx_origin - m.area * m.cx * m.cx) / m.area;
let k = (m.iyy_origin - m.area * m.cy * m.cy) / m.area;
let p = (m.ixy_origin - m.area * m.cx * m.cy) / m.area;
let local_centroid = Vec3::new(m.cx, height * 0.5, m.cy);
let centroid = scope.position + scope.rotation * local_centroid;
if !centroid.is_finite() {
return None;
}
let h2_12 = height * height / 12.0;
let i_xx = mass * (h2_12 + k);
let i_yy = mass * (j + k);
let i_zz = mass * (h2_12 + j);
let i_xz = -mass * p;
let i_local = glam::DMat3::from_cols_array(&[
i_xx, 0.0, i_xz, 0.0, i_yy, 0.0, i_xz, 0.0, i_zz, ]);
let inertia = rotate_inertia(i_local, scope.rotation);
Some(MassProperties {
mass,
centroid,
inertia: Some(inertia),
})
}
struct PolygonMoments {
area: f64,
cx: f64,
cy: f64,
ixx_origin: f64,
iyy_origin: f64,
ixy_origin: f64,
}
fn polygon_moments(verts: &[glam::DVec2]) -> Option<PolygonMoments> {
if verts.len() < 3 {
return None;
}
let n = verts.len();
let mut a2 = 0.0_f64; let mut cx6 = 0.0_f64;
let mut cy6 = 0.0_f64;
let mut ixx12 = 0.0_f64;
let mut iyy12 = 0.0_f64;
let mut ixy24 = 0.0_f64;
for i in 0..n {
let p = verts[i];
let q = verts[(i + 1) % n];
let cross = p.x * q.y - q.x * p.y;
a2 += cross;
cx6 += (p.x + q.x) * cross;
cy6 += (p.y + q.y) * cross;
ixx12 += (p.y * p.y + p.y * q.y + q.y * q.y) * cross;
iyy12 += (p.x * p.x + p.x * q.x + q.x * q.x) * cross;
ixy24 += (p.x * q.y + 2.0 * p.x * p.y + 2.0 * q.x * q.y + q.x * p.y) * cross;
}
let area = a2 * 0.5;
if !area.is_finite() || area.abs() < 1e-15 {
return None;
}
let cx = cx6 / (6.0 * area);
let cy = cy6 / (6.0 * area);
let abs_area = area.abs();
let area_sign = if area >= 0.0 { 1.0 } else { -1.0 };
Some(PolygonMoments {
area: abs_area,
cx,
cy,
ixx_origin: (ixx12 / 12.0) * area_sign,
iyy_origin: (iyy12 / 12.0) * area_sign,
ixy_origin: (ixy24 / 24.0) * area_sign,
})
}
fn rotate_inertia(i_local: glam::DMat3, rotation: crate::scope::Quat) -> glam::DMat3 {
let r = glam::DMat3::from_quat(rotation);
r * i_local * r.transpose()
}