use crate::stdlib::hardsurface::fastener::{
counterbore, countersink, MetricSize, CLEARANCE_H2D_FDM, HEAT_SET_SINK_MARGIN,
};
use alice_sdf::SdfNode;
use glam::Vec3;
use std::sync::Arc;
pub const CAVITY_PUNCH_MARGIN: f32 = 5.0;
#[must_use]
pub fn subtract_through_cylinder(
plate: SdfNode,
hole_dia: f32,
plate_thickness: f32,
x: f32,
z: f32,
) -> SdfNode {
let cyl_len = 2.0f32.mul_add(CAVITY_PUNCH_MARGIN, plate_thickness);
let hole = SdfNode::Cylinder {
radius: hole_dia * 0.5,
half_height: cyl_len * 0.5,
};
let hole_placed = SdfNode::Translate {
child: Arc::new(hole),
offset: Vec3::new(x, 0.0, z),
};
SdfNode::Subtraction {
a: Arc::new(plate),
b: Arc::new(hole_placed),
}
}
#[must_use]
pub fn subtract_through_screw_hole(
plate: SdfNode,
m: MetricSize,
plate_thickness: f32,
x: f32,
z: f32,
) -> SdfNode {
let hole_dia = m.nominal_diameter() + CLEARANCE_H2D_FDM;
subtract_through_cylinder(plate, hole_dia, plate_thickness, x, z)
}
#[must_use]
pub fn subtract_through_counterbore(
plate: SdfNode,
m: MetricSize,
plate_thickness: f32,
x: f32,
z: f32,
) -> SdfNode {
let hole = counterbore(m, plate_thickness);
let hole_placed = SdfNode::Translate {
child: Arc::new(hole),
offset: Vec3::new(x, 0.0, z),
};
SdfNode::Subtraction {
a: Arc::new(plate),
b: Arc::new(hole_placed),
}
}
#[must_use]
pub fn subtract_through_countersink(
plate: SdfNode,
m: MetricSize,
plate_thickness: f32,
x: f32,
z: f32,
) -> SdfNode {
let hole = countersink(m, plate_thickness);
let hole_placed = SdfNode::Translate {
child: Arc::new(hole),
offset: Vec3::new(x, 0.0, z),
};
SdfNode::Subtraction {
a: Arc::new(plate),
b: Arc::new(hole_placed),
}
}
#[must_use]
pub fn subtract_blind_pocket(
plate: SdfNode,
hole_dia: f32,
plate_thickness: f32,
pocket_depth: f32,
x: f32,
z: f32,
) -> SdfNode {
SdfNode::Subtraction {
a: Arc::new(plate),
b: Arc::new(blind_pocket_cutter(
hole_dia,
plate_thickness,
pocket_depth,
x,
z,
)),
}
}
#[must_use]
pub fn blind_pocket_cutter(
hole_dia: f32,
plate_thickness: f32,
pocket_depth: f32,
x: f32,
z: f32,
) -> SdfNode {
let outer_hy = plate_thickness * 0.5;
let cyl_len = pocket_depth + CAVITY_PUNCH_MARGIN;
let hole = SdfNode::Cylinder {
radius: hole_dia * 0.5,
half_height: cyl_len * 0.5,
};
let y_offset = (CAVITY_PUNCH_MARGIN - pocket_depth).mul_add(0.5, outer_hy);
SdfNode::Translate {
child: Arc::new(hole),
offset: Vec3::new(x, y_offset, z),
}
}
#[must_use]
pub fn subtract_blind_heat_set(
plate: SdfNode,
m: MetricSize,
plate_thickness: f32,
x: f32,
z: f32,
) -> SdfNode {
SdfNode::Subtraction {
a: Arc::new(plate),
b: Arc::new(blind_heat_set_cutter(m, plate_thickness, x, z)),
}
}
#[must_use]
pub fn blind_heat_set_cutter(m: MetricSize, plate_thickness: f32, x: f32, z: f32) -> SdfNode {
let insert_dia = m.heat_set_insert_diameter() + CLEARANCE_H2D_FDM;
let pocket_depth = m.heat_set_insert_depth() + HEAT_SET_SINK_MARGIN;
blind_pocket_cutter(insert_dia, plate_thickness, pocket_depth, x, z)
}
#[must_use]
pub fn through_hole_cylinder(hole_dia: f32, plate_thickness: f32) -> SdfNode {
let cyl_len = 2.0f32.mul_add(CAVITY_PUNCH_MARGIN, plate_thickness);
SdfNode::Cylinder {
radius: hole_dia * 0.5,
half_height: cyl_len * 0.5,
}
}
#[cfg(test)]
mod tests {
use super::*;
use alice_sdf::eval;
fn plate_5mm() -> SdfNode {
SdfNode::Box3d {
half_extents: Vec3::new(30.0, 2.5, 30.0),
}
}
#[test]
fn subtract_through_cylinder_creates_visible_cavity() {
let plate = plate_5mm();
let result = subtract_through_cylinder(plate, 4.2, 5.0, 0.0, 0.0);
assert!(matches!(result, SdfNode::Subtraction { .. }));
assert!(
eval(&result, Vec3::ZERO) > 0.0,
"hole center should be empty"
);
}
#[test]
fn subtract_through_screw_hole_uses_h2d_clearance() {
let plate = plate_5mm();
let result = subtract_through_screw_hole(plate, MetricSize::M4, 5.0, 0.0, 0.0);
assert!(eval(&result, Vec3::new(0.0, 2.5, 0.0)) >= 0.0);
assert!(eval(&result, Vec3::new(2.5, 0.0, 0.0)) < 0.0);
}
#[test]
fn subtract_through_cylinder_has_5mm_margin_each_side() {
let plate = plate_5mm();
let _result = subtract_through_cylinder(plate, 4.2, 5.0, 0.0, 0.0);
let expected_hole_half_height = 2.0f32.mul_add(CAVITY_PUNCH_MARGIN, 5.0) * 0.5;
assert!((expected_hole_half_height - 7.5).abs() < 1e-6);
}
#[test]
fn subtract_blind_pocket_extends_5mm_above_plate() {
let plate = SdfNode::Box3d {
half_extents: Vec3::new(30.0, 3.0, 30.0),
};
let _result = subtract_blind_pocket(plate, 10.0, 6.0, 4.0, 0.0, 0.0);
let expected_y_offset = (CAVITY_PUNCH_MARGIN - 4.0).mul_add(0.5, 3.0);
assert!((expected_y_offset - 3.5).abs() < 1e-6);
}
#[test]
fn subtract_blind_heat_set_m3_matches_mcmaster_spec() {
let plate = SdfNode::Box3d {
half_extents: Vec3::new(30.0, 3.0, 30.0),
};
let _result = subtract_blind_heat_set(plate, MetricSize::M3, 6.0, 0.0, 0.0);
let m3_pocket_depth = MetricSize::M3.heat_set_insert_depth() + HEAT_SET_SINK_MARGIN;
assert!((m3_pocket_depth - 4.1).abs() < 1e-4);
}
#[test]
fn cavity_margin_constant_is_5mm() {
assert!((CAVITY_PUNCH_MARGIN - 5.0).abs() < 1e-6);
}
#[test]
fn through_hole_cylinder_length_is_plate_plus_10mm() {
let hole = through_hole_cylinder(4.2, 5.0);
if let SdfNode::Cylinder { half_height, .. } = hole {
assert!((half_height - 7.5).abs() < 1e-6);
} else {
panic!("expected Cylinder");
}
}
#[test]
fn all_helpers_produce_finite_sdf_at_origin() {
use alice_sdf::eval;
let plate = plate_5mm();
let hs = [
subtract_through_cylinder(plate.clone(), 4.2, 5.0, 20.0, 20.0),
subtract_through_screw_hole(plate.clone(), MetricSize::M4, 5.0, 20.0, 20.0),
subtract_through_counterbore(plate.clone(), MetricSize::M4, 5.0, 20.0, 20.0),
subtract_through_countersink(plate.clone(), MetricSize::M4, 5.0, 20.0, 20.0),
subtract_blind_pocket(plate.clone(), 10.0, 5.0, 3.0, 20.0, 20.0),
subtract_blind_heat_set(plate, MetricSize::M3, 5.0, 20.0, 20.0),
];
for (i, h) in hs.iter().enumerate() {
let d = eval(h, Vec3::new(0.1, 0.1, 0.1));
assert!(d.is_finite(), "cavity helper {i} produced non-finite SDF");
}
}
}