use alice_sdf::SdfNode;
use glam::Vec3;
use std::sync::Arc;
pub const SCREW_BOSS_OD_RATIO: f32 = 2.2;
pub const BOSS_TAP_ACCURACY: f32 = 0.1;
#[must_use]
pub fn rib(length: f32, height: f32, thickness: f32) -> SdfNode {
SdfNode::Box3d {
half_extents: Vec3::new(length * 0.5, height * 0.5, thickness * 0.5),
}
}
#[must_use]
pub fn boss(screw_dia: f32, height: f32) -> SdfNode {
let outer_r = screw_dia * SCREW_BOSS_OD_RATIO * 0.5;
let tap_r = screw_dia.mul_add(0.85, 2.0 * BOSS_TAP_ACCURACY) * 0.5;
let barrel = SdfNode::Cylinder {
radius: outer_r,
half_height: height * 0.5,
};
let hole = SdfNode::Cylinder {
radius: tap_r,
half_height: height.mul_add(0.5, 5.0),
};
SdfNode::Subtraction {
a: Arc::new(barrel),
b: Arc::new(hole),
}
}
#[must_use]
pub fn fillet(a: SdfNode, b: SdfNode, radius: f32) -> SdfNode {
SdfNode::SmoothUnion {
a: Arc::new(a),
b: Arc::new(b),
k: radius,
}
}
#[must_use]
pub fn chamfer(a: SdfNode, b: SdfNode, size: f32) -> SdfNode {
SdfNode::ChamferUnion {
a: Arc::new(a),
b: Arc::new(b),
r: size,
}
}
#[must_use]
pub fn honeycomb_infill(
container: SdfNode,
cell_size: f32,
wall_thickness: f32,
count: u32,
) -> SdfNode {
let hex_r = cell_size / 3.0_f32.sqrt();
let cell_effective_r = crate::det_math::max(wall_thickness.mul_add(-0.5, hex_r), 0.0);
let hex = SdfNode::HexPrism {
hex_radius: cell_effective_r,
half_height: 1000.0, };
let repeated = SdfNode::RepeatFinite {
child: Arc::new(hex),
count: [count, 0, count],
spacing: Vec3::new(cell_size * 3.0_f32.sqrt(), 1.0, cell_size * 1.5),
};
SdfNode::Subtraction {
a: Arc::new(container),
b: Arc::new(repeated),
}
}
#[must_use]
pub fn gyroid_infill(container: SdfNode, cell_scale: f32, wall_thickness: f32) -> SdfNode {
let (scale, thickness) = crate::limits::checked_tpms_fields(cell_scale, wall_thickness * 0.5)
.unwrap_or_else(|e| panic!("gyroid_infill: {e}"));
let gyroid = SdfNode::Gyroid { scale, thickness };
SdfNode::Intersection {
a: Arc::new(container),
b: Arc::new(gyroid),
}
}
#[cfg(feature = "physics")]
#[must_use]
pub fn material_elastic_modulus_gpa(name: &str) -> Option<f32> {
use alice_physics::filament_db::MaterialProperties;
let props = match name.to_ascii_lowercase().as_str() {
"pla" => MaterialProperties::pla(),
"petg" => MaterialProperties::petg(),
"abs" => MaterialProperties::abs(),
_ => return None,
};
Some(props.youngs_modulus_gpa.to_f32())
}
#[cfg(feature = "physics")]
#[must_use]
pub fn fillet_kt_shaft_shoulder(
fillet_radius_mm: f32,
small_dia_mm: f32,
large_dia_mm: f32,
) -> f32 {
use alice_physics::fillet_stress::kt_shaft_shoulder_bending;
use alice_physics::math::Fix128;
let kt = kt_shaft_shoulder_bending(
Fix128::from_f32(fillet_radius_mm),
Fix128::from_f32(small_dia_mm),
Fix128::from_f32(large_dia_mm),
);
kt.to_f32()
}
#[cfg(feature = "physics")]
#[must_use]
pub fn recommended_fillet_radius_mm(small_dia_mm: f32, large_dia_mm: f32, kt_target: f32) -> f32 {
use alice_physics::fillet_stress::recommended_fillet_radius_mm as inner;
use alice_physics::math::Fix128;
inner(
Fix128::from_f32(small_dia_mm),
Fix128::from_f32(large_dia_mm),
Fix128::from_f32(kt_target),
)
.to_f32()
}
#[cfg(test)]
mod tests {
use super::*;
use alice_sdf::eval;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn rib_is_box3d_with_expected_dimensions() {
let node = rib(30.0, 5.0, 1.6);
match node {
SdfNode::Box3d { half_extents } => {
assert!(approx_eq(half_extents.x, 15.0));
assert!(approx_eq(half_extents.y, 2.5));
assert!(approx_eq(half_extents.z, 0.8));
}
_ => panic!("expected Box3d"),
}
}
#[test]
fn boss_outer_diameter_uses_pipeline_screw_boss_od_ratio() {
let node = boss(3.0, 8.0);
match &node {
SdfNode::Subtraction { a, b: _ } => match &**a {
SdfNode::Cylinder { radius, .. } => assert!(approx_eq(*radius, 3.3)),
_ => panic!("expected outer Cylinder"),
},
_ => panic!("expected Subtraction"),
}
}
#[test]
fn boss_inner_hole_uses_tap_formula() {
let node = boss(3.0, 8.0);
match &node {
SdfNode::Subtraction { a: _, b } => match &**b {
SdfNode::Cylinder { radius, .. } => assert!(approx_eq(*radius, 1.375)),
_ => panic!("expected inner Cylinder"),
},
_ => panic!("expected Subtraction"),
}
}
#[test]
fn fillet_returns_smooth_union() {
let a = SdfNode::Sphere { radius: 1.0 };
let b = SdfNode::Sphere { radius: 1.0 };
let f = fillet(a, b, 0.3);
match f {
SdfNode::SmoothUnion { k, .. } => assert!(approx_eq(k, 0.3)),
_ => panic!("expected SmoothUnion"),
}
}
#[test]
fn chamfer_returns_chamfer_union() {
let a = SdfNode::Sphere { radius: 1.0 };
let b = SdfNode::Sphere { radius: 1.0 };
let c = chamfer(a, b, 0.5);
match c {
SdfNode::ChamferUnion { r, .. } => assert!(approx_eq(r, 0.5)),
_ => panic!("expected ChamferUnion"),
}
}
#[test]
fn honeycomb_infill_returns_subtraction() {
let container = SdfNode::Box3d {
half_extents: Vec3::new(50.0, 5.0, 50.0),
};
let node = honeycomb_infill(container, 8.0, 1.2, 6);
assert!(matches!(node, SdfNode::Subtraction { .. }));
}
#[test]
fn gyroid_infill_returns_intersection_with_gyroid() {
let container = SdfNode::Box3d {
half_extents: Vec3::new(20.0, 20.0, 20.0),
};
let node = gyroid_infill(container, 3.0, 0.4);
match &node {
SdfNode::Intersection { a: _, b } => {
assert!(matches!(&**b, SdfNode::Gyroid { .. }));
}
_ => panic!("expected Intersection with Gyroid"),
}
}
#[test]
#[should_panic(expected = "gyroid_infill")]
fn gyroid_infill_panics_on_a_degenerate_cell_scale() {
let container = SdfNode::Box3d {
half_extents: Vec3::new(20.0, 20.0, 20.0),
};
let _ = gyroid_infill(container, -0.0, 0.4);
}
#[test]
#[should_panic(expected = "gyroid_infill")]
fn gyroid_infill_panics_on_a_non_finite_wall_thickness() {
let container = SdfNode::Box3d {
half_extents: Vec3::new(20.0, 20.0, 20.0),
};
let _ = gyroid_infill(container, 3.0, f32::NAN);
}
#[test]
fn gyroid_infill_accepts_any_finite_positive_cell_scale() {
for cell_scale in [1e-45_f32, 5e-7] {
let container = SdfNode::Box3d {
half_extents: Vec3::new(20.0, 20.0, 20.0),
};
let node = gyroid_infill(container, cell_scale, 0.4);
assert!(
matches!(node, SdfNode::Intersection { .. }),
"cell_scale={cell_scale} should build an Intersection node"
);
}
}
#[test]
fn boss_evaluation_at_origin_is_inside_tap_hole() {
let node = boss(3.0, 8.0);
assert!(eval(&node, Vec3::ZERO) > 0.0);
}
#[test]
fn boss_evaluation_between_tap_and_outer_is_inside_material() {
let node = boss(3.0, 8.0);
assert!(eval(&node, Vec3::new(2.0, 0.0, 0.0)) < 0.0);
}
#[test]
fn rib_evaluation_at_center_is_inside() {
let node = rib(30.0, 5.0, 1.6);
assert!(eval(&node, Vec3::ZERO) < 0.0);
}
#[test]
fn all_reinforcement_primitives_produce_finite_sdf() {
let container = SdfNode::Box3d {
half_extents: Vec3::new(20.0, 20.0, 20.0),
};
let nodes = [
rib(30.0, 5.0, 1.6),
boss(3.0, 8.0),
fillet(
SdfNode::Sphere { radius: 1.0 },
SdfNode::Sphere { radius: 1.0 },
0.3,
),
chamfer(
SdfNode::Sphere { radius: 1.0 },
SdfNode::Sphere { radius: 1.0 },
0.5,
),
honeycomb_infill(container.clone(), 8.0, 1.2, 6),
gyroid_infill(container, 3.0, 0.4),
];
for (i, node) in nodes.iter().enumerate() {
let d = eval(node, Vec3::new(0.1, 0.1, 0.1));
assert!(d.is_finite(), "primitive {i} produced non-finite SDF: {d}");
}
}
#[cfg(feature = "physics")]
#[test]
fn physics_material_elastic_modulus_pla_matches_hardcode() {
let e = material_elastic_modulus_gpa("pla").expect("PLA registered");
assert!((3.0..=4.0).contains(&e), "PLA elastic modulus = {e} GPa");
}
#[cfg(feature = "physics")]
#[test]
fn physics_fillet_kt_decreases_with_larger_radius() {
let kt_small = fillet_kt_shaft_shoulder(0.2, 10.0, 20.0);
let kt_large = fillet_kt_shaft_shoulder(2.0, 10.0, 20.0);
assert!(
kt_large < kt_small,
"kt_large ({kt_large}) should be < kt_small ({kt_small})"
);
}
#[cfg(feature = "physics")]
#[test]
fn physics_recommended_fillet_radius_positive() {
let r = recommended_fillet_radius_mm(10.0, 20.0, 1.5);
assert!(r > 0.0, "recommended R = {r}");
}
#[cfg(feature = "physics")]
#[test]
fn physics_unknown_material_returns_none() {
assert!(material_elastic_modulus_gpa("unknown_polymer").is_none());
}
}