use alice_sdf::SdfNode;
use glam::Vec3;
use std::sync::Arc;
pub const SKADIS_PEG_W: f32 = 5.0;
pub const SKADIS_PEG_H: f32 = 15.0;
pub const SKADIS_PEG_R: f32 = 2.5;
pub const FDM_CLEARANCE: f32 = 0.2;
pub const PROFILE_2020_SIZE: f32 = 20.0;
pub const PROFILE_3030_SIZE: f32 = 30.0;
pub const PROFILE_2020_CENTER_BORE: f32 = 5.2;
pub const PROFILE_3030_CENTER_BORE: f32 = 6.2;
pub const PROFILE_SLOT_OPENING_WIDTH: f32 = 6.0;
pub const PROFILE_SLOT_NECK_DEPTH: f32 = 1.8;
pub const PROFILE_SLOT_DEPTH: f32 = 6.1;
pub const PROFILE_SLOT_CAVITY_WIDTH: f32 = 11.0;
const SMOOTH_UNION_K_PER_FILLET_RADIUS: f32 = 4.0 - 2.0 * std::f32::consts::SQRT_2;
#[must_use]
pub fn bracket_l(
horizontal_length: f32,
vertical_height: f32,
thickness: f32,
depth: f32,
fillet_radius: f32,
) -> SdfNode {
let horizontal = SdfNode::Box3d {
half_extents: Vec3::new(horizontal_length * 0.5, thickness * 0.5, depth * 0.5),
};
let vertical_raw = SdfNode::Box3d {
half_extents: Vec3::new(thickness * 0.5, vertical_height * 0.5, depth * 0.5),
};
let vertical = SdfNode::Translate {
child: Arc::new(vertical_raw),
offset: Vec3::new(
-(horizontal_length - thickness) * 0.5,
f32::midpoint(vertical_height, thickness),
0.0,
),
};
if fillet_radius > 0.0 {
SdfNode::SmoothUnion {
a: Arc::new(horizontal),
b: Arc::new(vertical),
k: fillet_radius * SMOOTH_UNION_K_PER_FILLET_RADIUS,
}
} else {
SdfNode::Union {
a: Arc::new(horizontal),
b: Arc::new(vertical),
}
}
}
#[must_use]
pub fn flange_circular(
od: f32,
center_bore_dia: f32,
thickness: f32,
bolt_pcd: f32,
bolt_count: u32,
bolt_dia: f32,
) -> SdfNode {
let disc = SdfNode::Cylinder {
radius: od * 0.5,
half_height: thickness * 0.5,
};
let single_bolt = SdfNode::Cylinder {
radius: bolt_dia * 0.5,
half_height: thickness.mul_add(0.5, 5.0),
};
let bolt_offset = SdfNode::Translate {
child: Arc::new(single_bolt),
offset: Vec3::new(bolt_pcd * 0.5, 0.0, 0.0),
};
let bolt_ring = SdfNode::PolarRepeat {
child: Arc::new(bolt_offset),
count: bolt_count,
};
let with_bolts = SdfNode::Subtraction {
a: Arc::new(disc),
b: Arc::new(bolt_ring),
};
if center_bore_dia > 0.0 {
let center_bore = SdfNode::Cylinder {
radius: center_bore_dia * 0.5,
half_height: thickness.mul_add(0.5, 5.0),
};
SdfNode::Subtraction {
a: Arc::new(with_bolts),
b: Arc::new(center_bore),
}
} else {
with_bolts
}
}
#[must_use]
pub fn rack_shelf(
length: f32,
thickness: f32,
width: f32,
notch_pitch: f32,
notch_dia: f32,
notch_count: u32,
) -> SdfNode {
let plate = SdfNode::Box3d {
half_extents: Vec3::new(length * 0.5, thickness * 0.5, width * 0.5),
};
let notch = SdfNode::Cylinder {
radius: notch_dia * 0.5,
half_height: thickness.mul_add(0.5, 5.0),
};
let notch_row = SdfNode::RepeatFinite {
child: Arc::new(notch),
count: [notch_count.saturating_mul(2), 0, 0],
spacing: Vec3::new(notch_pitch, 1.0, 1.0),
};
SdfNode::Subtraction {
a: Arc::new(plate),
b: Arc::new(notch_row),
}
}
#[must_use]
pub fn skadis_peg_compat(board_thickness: f32) -> SdfNode {
let peg_w = SKADIS_PEG_W - FDM_CLEARANCE;
let half = Vec3::new(peg_w * 0.5, SKADIS_PEG_H * 0.5, board_thickness * 0.5);
let r = crate::det_math::max(crate::det_math::min(SKADIS_PEG_R, half.min_element()), 0.0);
SdfNode::RoundedBox {
half_extents: half - Vec3::splat(r),
round_radius: r,
}
}
fn profile_t_slot(length: f32) -> SdfNode {
let half_len = length.mul_add(0.5, 5.0);
let neck = PROFILE_SLOT_NECK_DEPTH;
let depth = PROFILE_SLOT_DEPTH;
let open_half = PROFILE_SLOT_OPENING_WIDTH * 0.5;
let cavity_half = PROFILE_SLOT_CAVITY_WIDTH * 0.5;
let boxed = |x_center: f32, half_x: f32, half_z: f32| SdfNode::Translate {
child: Arc::new(SdfNode::Box3d {
half_extents: Vec3::new(half_x, half_len, half_z),
}),
offset: Vec3::new(x_center, 0.0, 0.0),
};
let neck_box = boxed(-neck * 0.5, neck * 0.5, open_half);
let punch = boxed(2.5, 2.5, open_half);
let cavity_box = boxed(-(neck + depth) * 0.5, (depth - neck) * 0.5, cavity_half);
let inv_sqrt2 = std::f32::consts::FRAC_1_SQRT_2;
let limit = (cavity_half + neck) * inv_sqrt2;
let plane_pos = SdfNode::Plane {
normal: Vec3::new(-inv_sqrt2, 0.0, inv_sqrt2),
distance: limit,
};
let plane_neg = SdfNode::Plane {
normal: Vec3::new(-inv_sqrt2, 0.0, -inv_sqrt2),
distance: limit,
};
let cavity = SdfNode::Intersection {
a: Arc::new(SdfNode::Intersection {
a: Arc::new(cavity_box),
b: Arc::new(plane_pos),
}),
b: Arc::new(plane_neg),
};
SdfNode::Union {
a: Arc::new(SdfNode::Union {
a: Arc::new(neck_box),
b: Arc::new(punch),
}),
b: Arc::new(cavity),
}
}
fn extrusion_profile(size: f32, length: f32, center_bore_dia: f32) -> SdfNode {
let outer = SdfNode::Box3d {
half_extents: Vec3::new(size * 0.5, length * 0.5, size * 0.5),
};
let t_slot = profile_t_slot(length);
let face_offset = size * 0.5;
let mut with_slots = outer;
for i in 0..4_u8 {
let angle = std::f32::consts::FRAC_PI_2 * f32::from(i);
let rotated = SdfNode::Rotate {
child: Arc::new(t_slot.clone()),
rotation: crate::angle::quat_from_rotation_y(angle),
};
let (sin_a, cos_a) = alice_det_math::sin_cos(angle);
let offset = Vec3::new(face_offset * cos_a, 0.0, (-face_offset).mul_add(sin_a, 0.0));
let placed = SdfNode::Translate {
child: Arc::new(rotated),
offset,
};
with_slots = SdfNode::Subtraction {
a: Arc::new(with_slots),
b: Arc::new(placed),
};
}
if center_bore_dia > 0.0 {
let bore = SdfNode::Cylinder {
radius: center_bore_dia * 0.5,
half_height: length.mul_add(0.5, 5.0),
};
SdfNode::Subtraction {
a: Arc::new(with_slots),
b: Arc::new(bore),
}
} else {
with_slots
}
}
#[must_use]
pub fn profile_2020(length: f32) -> SdfNode {
extrusion_profile(PROFILE_2020_SIZE, length, PROFILE_2020_CENTER_BORE)
}
#[must_use]
pub fn profile_3030(length: f32) -> SdfNode {
extrusion_profile(PROFILE_3030_SIZE, length, PROFILE_3030_CENTER_BORE)
}
#[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 extrusion_profile_i0_t_slot_offset_has_no_negative_zero() {
let node = profile_2020(300.0);
let text = crate::emit::to_lol(&node).unwrap();
assert!(
!text.contains("-0.0"),
"profile_2020's emitted text has a -0.0: {text:?}"
);
}
#[test]
fn bracket_l_with_fillet_returns_smooth_union() {
let b = bracket_l(60.0, 40.0, 4.0, 40.0, 3.0);
let want = 3.0 * 2.0_f32.mul_add(-std::f32::consts::SQRT_2, 4.0);
assert!(matches!(b, SdfNode::SmoothUnion { k, .. } if approx_eq(k, want)));
}
#[test]
fn bracket_l_without_fillet_returns_plain_union() {
let b = bracket_l(60.0, 40.0, 4.0, 40.0, 0.0);
assert!(matches!(b, SdfNode::Union { .. }));
}
#[test]
fn bracket_l_horizontal_center_inside() {
let b = bracket_l(60.0, 40.0, 4.0, 40.0, 3.0);
assert!(eval(&b, Vec3::ZERO) < 0.0);
}
#[test]
fn flange_circular_has_bolt_holes() {
let f = flange_circular(60.0, 20.0, 6.0, 45.0, 4, 4.2);
assert!(eval(&f, Vec3::ZERO) > 0.0);
assert!(eval(&f, Vec3::new(22.5, 0.0, 0.0)) > 0.0);
assert!(eval(&f, Vec3::new(28.0, 0.0, 0.0)) < 0.0);
}
#[test]
fn flange_circular_no_center_bore() {
let f = flange_circular(60.0, 0.0, 6.0, 45.0, 4, 4.2);
assert!(eval(&f, Vec3::ZERO) < 0.0);
}
#[test]
fn rack_shelf_has_periodic_notches() {
let r = rack_shelf(200.0, 5.0, 30.0, 25.0, 6.0, 3);
assert!(eval(&r, Vec3::ZERO) > 0.0);
assert!(eval(&r, Vec3::new(12.5, 0.0, 0.0)) < 0.0);
}
#[test]
fn skadis_peg_dimensions_match_pipeline_spec() {
let p = skadis_peg_compat(5.0);
let extent = |dir: Vec3| {
let (mut lo, mut hi) = (0.0_f32, 30.0_f32);
for _ in 0..60 {
let mid = f32::midpoint(lo, hi);
if eval(&p, dir * mid) <= 0.0 {
lo = mid;
} else {
hi = mid;
}
}
lo * 2.0
};
assert!(
(extent(Vec3::X) - 4.8).abs() < 1e-3,
"幅 {}",
extent(Vec3::X)
);
assert!(
(extent(Vec3::Y) - 15.0).abs() < 1e-3,
"高さ {}",
extent(Vec3::Y)
);
assert!(
(extent(Vec3::Z) - 5.0).abs() < 1e-3,
"厚さ {}",
extent(Vec3::Z)
);
}
#[test]
fn profile_2020_outer_dimensions() {
let p = profile_2020(100.0);
assert!(eval(&p, Vec3::new(9.0, 0.0, 9.0)) < 0.0);
assert!(eval(&p, Vec3::new(15.0, 0.0, 0.0)) > 0.0);
}
#[test]
fn profile_3030_center_bore_is_hollow() {
let p = profile_3030(100.0);
assert!(eval(&p, Vec3::ZERO) > 0.0);
assert!(eval(&p, Vec3::new(13.0, 0.0, 13.0)) < 0.0);
}
#[test]
fn all_mount_primitives_produce_finite_sdf() {
let nodes = [
bracket_l(60.0, 40.0, 4.0, 40.0, 3.0),
flange_circular(60.0, 20.0, 6.0, 45.0, 4, 4.2),
rack_shelf(200.0, 5.0, 30.0, 25.0, 6.0, 3),
skadis_peg_compat(5.0),
profile_2020(300.0),
profile_3030(500.0),
];
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}");
}
}
}