use crate::limits::{checked_bounded, SpecError, MAX_SKADIS_PANEL_MM};
use alice_sdf::SdfNode;
use glam::Vec3;
use std::sync::Arc;
pub const PEG_BLADE_W: f32 = 5.0;
pub const PEG_BLADE_T: f32 = 4.5;
pub const BOARD_T: f32 = 5.0;
pub const SHOULDER_DEPTH: f32 = 2.0;
pub const SHOULDER_H: f32 = 8.0;
#[must_use]
pub fn skadis_peg_and_shoulder(hook_width: f32) -> SdfNode {
let blade = SdfNode::Box3d {
half_extents: Vec3::new(BOARD_T * 0.5, PEG_BLADE_T * 0.5, hook_width * 0.5),
};
let blade_placed = SdfNode::Translate {
child: Arc::new(blade),
offset: Vec3::new(-BOARD_T * 0.5, 0.0, 0.0),
};
let shoulder = SdfNode::Box3d {
half_extents: Vec3::new(SHOULDER_DEPTH * 0.5, SHOULDER_H * 0.5, hook_width * 0.5),
};
let shoulder_placed = SdfNode::Translate {
child: Arc::new(shoulder),
offset: Vec3::new(SHOULDER_DEPTH.mul_add(-0.5, -BOARD_T), 0.0, 0.0),
};
SdfNode::Union {
a: Arc::new(blade_placed),
b: Arc::new(shoulder_placed),
}
}
fn peg_facing_back(hook_width: f32) -> SdfNode {
SdfNode::Rotate {
child: Arc::new(skadis_peg_and_shoulder(hook_width)),
rotation: crate::angle::quat_from_rotation_y(-std::f32::consts::FRAC_PI_2),
}
}
#[must_use]
pub fn capsule_polyline_sdf(pts: &[glam::Vec2], tube_radius: f32, hook_width: f32) -> SdfNode {
if pts.len() < 2 {
return SdfNode::Sphere { radius: 0.0 };
}
let strips: Vec<SdfNode> = pts
.windows(2)
.map(|pair| {
let (a, b) = (pair[0], pair[1]);
let edge = b - a;
let len = edge.length();
let puck = SdfNode::Rotate {
child: Arc::new(SdfNode::Cylinder {
radius: tube_radius,
half_height: hook_width * 0.5,
}),
rotation: crate::angle::quat_from_rotation_x(std::f32::consts::FRAC_PI_2),
};
let elongated = SdfNode::Elongate {
child: Arc::new(puck),
amount: Vec3::new(len * 0.5, 0.0, 0.0),
};
let angle = alice_det_math::atan2(edge.y, edge.x);
let rotated = SdfNode::Rotate {
child: Arc::new(elongated),
rotation: crate::angle::quat_from_rotation_z(angle),
};
let mid = (a + b) * 0.5;
SdfNode::Translate {
child: Arc::new(rotated),
offset: Vec3::new(mid.x, mid.y, 0.0),
}
})
.collect();
super::balanced_union_fold(strips).unwrap_or(SdfNode::Sphere { radius: 0.0 })
}
#[must_use]
pub fn skadis_hook_l_sdf() -> SdfNode {
let reach: f32 = 75.0;
let root_t: f32 = 7.0;
let hook_width: f32 = 8.0; let radius = root_t * 0.5;
let mut pts: Vec<glam::Vec2> =
vec![glam::Vec2::new(0.0, 0.0), glam::Vec2::new(reach - 8.0, 0.0)];
let n = 12;
for i in 1..=n {
#[allow(clippy::cast_precision_loss)]
let a = std::f32::consts::FRAC_PI_2 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
pts.push(glam::Vec2::new(
8.0f32.mul_add(sin_a, reach - 8.0),
8.0 * (1.0 - cos_a),
));
}
let peg_shoulder = skadis_peg_and_shoulder(hook_width);
let body = capsule_polyline_sdf(&pts, radius, hook_width);
SdfNode::Union {
a: Arc::new(peg_shoulder),
b: Arc::new(body),
}
}
#[must_use]
pub fn skadis_hook_j_sdf() -> SdfNode {
let reach: f32 = 25.0;
let drop: f32 = 70.0;
let root_t: f32 = 7.5;
let hook_width: f32 = 8.0;
let radius = root_t * 0.5;
let mut pts: Vec<glam::Vec2> = vec![glam::Vec2::new(0.0, 0.0)];
let n = 32;
for i in 0..=n {
#[allow(clippy::cast_precision_loss)]
let a = std::f32::consts::FRAC_PI_2 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
pts.push(glam::Vec2::new(reach * sin_a, -reach * (1.0 - cos_a)));
}
let curve_end_y = pts.last().map_or(0.0, |v| v.y);
let tip_y = curve_end_y - (drop - reach);
pts.push(glam::Vec2::new(reach, tip_y));
let tip_r: f32 = 8.0;
for i in 0..=n {
#[allow(clippy::cast_precision_loss)]
let a = -std::f32::consts::FRAC_PI_2 + std::f32::consts::PI * 0.75 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
pts.push(glam::Vec2::new(
tip_r.mul_add(cos_a, reach - tip_r),
tip_r.mul_add(sin_a, tip_y),
));
}
let peg_shoulder = skadis_peg_and_shoulder(hook_width);
let body = capsule_polyline_sdf(&pts, radius, hook_width);
SdfNode::Union {
a: Arc::new(peg_shoulder),
b: Arc::new(body),
}
}
#[must_use]
pub fn skadis_hook_s_sdf() -> SdfNode {
let reach: f32 = 22.0;
let drop: f32 = 45.0;
let root_t: f32 = 5.5;
let hook_width: f32 = 5.0; let radius = root_t * 0.5;
let mut pts: Vec<glam::Vec2> = vec![glam::Vec2::new(0.0, 0.0)];
let n = 16;
for i in 0..=n {
#[allow(clippy::cast_precision_loss)]
let a = std::f32::consts::FRAC_PI_2 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
pts.push(glam::Vec2::new(reach * sin_a, -reach * (1.0 - cos_a)));
}
let curve_end_y = pts.last().map_or(0.0, |v| v.y);
let tip_base_y = curve_end_y - (drop - reach);
pts.push(glam::Vec2::new(reach, tip_base_y));
let tip_r: f32 = 6.0;
for i in 0..=n {
#[allow(clippy::cast_precision_loss)]
let a = -std::f32::consts::FRAC_PI_2 + std::f32::consts::PI * 0.7 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
pts.push(glam::Vec2::new(
tip_r.mul_add(cos_a, reach - tip_r),
tip_r.mul_add(sin_a, tip_base_y),
));
}
let peg_shoulder = skadis_peg_and_shoulder(hook_width);
let body = capsule_polyline_sdf(&pts, radius, hook_width);
SdfNode::Union {
a: Arc::new(peg_shoulder),
b: Arc::new(body),
}
}
pub const SKADIS_PEG_W: f32 = 5.0;
pub const SKADIS_PEG_H: f32 = 15.0;
pub const SKADIS_GRID_PITCH: f32 = 40.0;
pub const SKADIS_GRID_OFFSET: f32 = 20.0;
pub const SKADIS_EDGE_MARGIN: f32 = 20.0;
pub const SKADIS_PANEL_THICKNESS: f32 = 5.0;
pub const HOLE_THROUGH_MARGIN: f32 = 0.5;
pub const SKADIS_CONN_SCREW_D: f32 = 2.7;
pub const SKADIS_CONN_INSET: f32 = 6.0;
pub const SKADIS_MOUNT_HOLE_R: f32 = 2.5;
pub const SKADIS_OUTER_FRAME: f32 = 12.0;
#[must_use]
#[allow(clippy::too_many_lines)] pub fn skadis_panel_sdf(size: f32, thickness: f32, corner_radius: f32) -> SdfNode {
checked_bounded(size, 0.0, MAX_SKADIS_PANEL_MM, "panel_size").expect(
"skadis_panel_sdf の size が検査を通らない (0 より大きく MAX_SKADIS_PANEL_MM mm 以下の有限値でなければならない): untrusted な入力は try_skadis_panel_sdf を使うこと",
);
let r = corner_radius.clamp(0.0, crate::det_math::max(size * 0.5, 0.0));
let panel_infl = SdfNode::RoundedBox {
half_extents: Vec3::new(
size.mul_add(0.5, -r),
thickness * 0.5,
size.mul_add(0.5, -r),
),
round_radius: r,
};
let y_cutter = SdfNode::Box3d {
half_extents: Vec3::new(
size.mul_add(0.5, 1.0), thickness * 0.5, size.mul_add(0.5, 1.0),
),
};
let panel = SdfNode::Intersection {
a: Arc::new(panel_infl),
b: Arc::new(y_cutter),
};
let t_pass = 2.0f32.mul_add(HOLE_THROUGH_MARGIN, thickness);
let stadium_middle_z = (SKADIS_PEG_H - SKADIS_PEG_W) * 0.5; let peg_middle = SdfNode::Box3d {
half_extents: Vec3::new(SKADIS_PEG_W * 0.5, t_pass * 0.5, stadium_middle_z),
};
let peg_end_cyl = SdfNode::Cylinder {
radius: SKADIS_PEG_W * 0.5, half_height: t_pass * 0.5,
};
let peg_end_top = SdfNode::Translate {
child: Arc::new(peg_end_cyl.clone()),
offset: Vec3::new(0.0, 0.0, stadium_middle_z),
};
let peg_end_bot = SdfNode::Translate {
child: Arc::new(peg_end_cyl),
offset: Vec3::new(0.0, 0.0, -stadium_middle_z),
};
let peg_hole = SdfNode::Union {
a: Arc::new(SdfNode::Union {
a: Arc::new(peg_middle),
b: Arc::new(peg_end_top),
}),
b: Arc::new(peg_end_bot),
};
let usable_half = size.mul_add(0.5, -SKADIS_EDGE_MARGIN);
let neg_usable_half = size.mul_add(-0.5, SKADIS_EDGE_MARGIN);
#[allow(clippy::cast_possible_truncation)]
let index_range = |offset: f32| {
let lo = ((neg_usable_half - offset) / SKADIS_GRID_PITCH).ceil() as i32;
let hi = ((usable_half - offset) / SKADIS_GRID_PITCH).floor() as i32;
lo..=hi
};
let mut hole_list: Vec<SdfNode> = Vec::new();
for (grid_x, grid_z) in [(0.0, 0.0), (SKADIS_GRID_OFFSET, SKADIS_GRID_OFFSET)] {
for ix in index_range(grid_x) {
for iz in index_range(grid_z) {
#[allow(clippy::cast_precision_loss)]
let cx = (ix as f32).mul_add(SKADIS_GRID_PITCH, grid_x);
#[allow(clippy::cast_precision_loss)]
let cz = (iz as f32).mul_add(SKADIS_GRID_PITCH, grid_z);
hole_list.push(SdfNode::Translate {
child: Arc::new(peg_hole.clone()),
offset: Vec3::new(cx, 0.0, cz),
});
}
}
}
let conn_cyl = SdfNode::Cylinder {
radius: SKADIS_CONN_SCREW_D * 0.5, half_height: t_pass * 0.5,
};
let half_size = size * 0.5;
let conn_inset_from_center = half_size - SKADIS_CONN_INSET;
let mut i = 1i32;
loop {
#[allow(clippy::cast_precision_loss)]
let pos = i as f32 * SKADIS_GRID_PITCH;
if pos >= size {
break;
}
let center_shift = pos - half_size;
hole_list.push(SdfNode::Translate {
child: Arc::new(conn_cyl.clone()),
offset: Vec3::new(center_shift, 0.0, -conn_inset_from_center),
});
hole_list.push(SdfNode::Translate {
child: Arc::new(conn_cyl.clone()),
offset: Vec3::new(center_shift, 0.0, conn_inset_from_center),
});
hole_list.push(SdfNode::Translate {
child: Arc::new(conn_cyl.clone()),
offset: Vec3::new(-conn_inset_from_center, 0.0, center_shift),
});
hole_list.push(SdfNode::Translate {
child: Arc::new(conn_cyl.clone()),
offset: Vec3::new(conn_inset_from_center, 0.0, center_shift),
});
i += 1;
}
for &(sx, sz) in &[
(-conn_inset_from_center, -conn_inset_from_center),
(conn_inset_from_center, -conn_inset_from_center),
(-conn_inset_from_center, conn_inset_from_center),
(conn_inset_from_center, conn_inset_from_center),
] {
hole_list.push(SdfNode::Translate {
child: Arc::new(conn_cyl.clone()),
offset: Vec3::new(sx, 0.0, sz),
});
}
let mount_cyl = SdfNode::Cylinder {
radius: SKADIS_MOUNT_HOLE_R, half_height: t_pass * 0.5,
};
let mount_x_positions = [60.0, 150.0, 220.0];
for &x in &mount_x_positions {
let cx = x - half_size;
hole_list.push(SdfNode::Translate {
child: Arc::new(mount_cyl.clone()),
offset: Vec3::new(cx, 0.0, -conn_inset_from_center),
});
hole_list.push(SdfNode::Translate {
child: Arc::new(mount_cyl.clone()),
offset: Vec3::new(cx, 0.0, conn_inset_from_center),
});
}
let all_holes = super::balanced_union_fold(hole_list);
match all_holes {
Some(holes) => SdfNode::Subtraction {
a: Arc::new(panel),
b: Arc::new(holes),
},
None => panel,
}
}
pub fn try_skadis_panel_sdf(
size: f32,
thickness: f32,
corner_radius: f32,
) -> Result<SdfNode, SpecError> {
checked_bounded(size, 0.0, MAX_SKADIS_PANEL_MM, "panel_size")?;
Ok(skadis_panel_sdf(size, thickness, corner_radius))
}
pub const CONTAINER_W: f32 = 65.0;
pub const CONTAINER_D: f32 = 75.0;
pub const CONTAINER_H: f32 = 70.0;
pub const CONTAINER_WALL_T: f32 = 1.6;
pub const CONTAINER_BOTTOM_T: f32 = 1.6;
#[must_use]
pub fn skadis_container_sdf() -> SdfNode {
let outer_w = 2.0f32.mul_add(CONTAINER_WALL_T, CONTAINER_W);
let outer_d = 2.0f32.mul_add(CONTAINER_WALL_T, CONTAINER_D);
let neg_outer_d = 2.0f32.mul_add(-CONTAINER_WALL_T, -CONTAINER_D);
let total_h = CONTAINER_H + CONTAINER_BOTTOM_T;
let outer = SdfNode::Box3d {
half_extents: Vec3::new(outer_w * 0.5, total_h * 0.5, outer_d * 0.5),
};
let inner_half_h = f32::midpoint(CONTAINER_H, 1.0);
let inner = SdfNode::Box3d {
half_extents: Vec3::new(CONTAINER_W * 0.5, inner_half_h, CONTAINER_D * 0.5),
};
let inner_placed = SdfNode::Translate {
child: Arc::new(inner),
offset: Vec3::new(
0.0,
total_h.mul_add(-0.5, CONTAINER_BOTTOM_T) + inner_half_h,
0.0,
),
};
let hollow = SdfNode::Subtraction {
a: Arc::new(outer),
b: Arc::new(inner_placed),
};
let peg_at = |x: f32| SdfNode::Translate {
child: Arc::new(peg_facing_back(PEG_BLADE_W)),
offset: Vec3::new(x, total_h * 0.35, neg_outer_d * 0.5),
};
let pegs = SdfNode::Union {
a: Arc::new(peg_at(-SKADIS_GRID_PITCH * 0.5)),
b: Arc::new(peg_at(SKADIS_GRID_PITCH * 0.5)),
};
SdfNode::Union {
a: Arc::new(hollow),
b: Arc::new(pegs),
}
}
pub const CLIP_WIDTH: f32 = 15.0;
pub const CLIP_LENGTH: f32 = 55.0;
pub const CLIP_BODY_T: f32 = 3.0;
pub const CLIP_SLOT_W: f32 = 1.2;
#[must_use]
pub fn skadis_clip_sdf() -> SdfNode {
let gap = CLIP_SLOT_W * 0.5;
let root = SdfNode::Box3d {
half_extents: Vec3::new(f32::midpoint(gap, CLIP_BODY_T), 4.0, CLIP_WIDTH * 0.5),
};
let root_placed = SdfNode::Translate {
child: Arc::new(root),
offset: Vec3::new(gap * 0.5, PEG_BLADE_T * 0.25, 0.0),
};
let front = SdfNode::Box3d {
half_extents: Vec3::new(CLIP_BODY_T * 0.5, CLIP_LENGTH * 0.5, CLIP_WIDTH * 0.5),
};
let front_placed = SdfNode::Translate {
child: Arc::new(front),
offset: Vec3::new(CLIP_BODY_T.mul_add(0.5, gap), -CLIP_LENGTH * 0.5, 0.0),
};
let back = SdfNode::Box3d {
half_extents: Vec3::new(CLIP_BODY_T * 0.5, CLIP_LENGTH * 0.5, CLIP_WIDTH * 0.5),
};
let back_placed = SdfNode::Translate {
child: Arc::new(back),
offset: Vec3::new(CLIP_BODY_T.mul_add(-0.5, -gap), -CLIP_LENGTH * 0.5, 0.0),
};
let tip = SdfNode::Box3d {
half_extents: Vec3::new(0.4, 2.5, CLIP_WIDTH * 0.5),
};
let tip_placed = SdfNode::Translate {
child: Arc::new(tip),
offset: Vec3::new(gap + 0.1, -CLIP_LENGTH + 2.5, 0.0),
};
let peg = skadis_peg_and_shoulder(CLIP_WIDTH);
let body_upper = SdfNode::Union {
a: Arc::new(root_placed),
b: Arc::new(peg),
};
let body_front = SdfNode::Union {
a: Arc::new(body_upper),
b: Arc::new(front_placed),
};
let body_back = SdfNode::Union {
a: Arc::new(body_front),
b: Arc::new(back_placed),
};
SdfNode::Union {
a: Arc::new(body_back),
b: Arc::new(tip_placed),
}
}
pub const SHELF_W: f32 = 260.0;
pub const SHELF_D: f32 = 80.0;
pub const SHELF_LIP_H: f32 = 20.0;
pub const SHELF_BACK_H: f32 = 25.0;
pub const SHELF_BOTTOM_T: f32 = 2.0;
pub const SHELF_PEG_SPACING: f32 = 240.0;
#[must_use]
pub fn skadis_shelf_sdf() -> SdfNode {
let bottom = SdfNode::Box3d {
half_extents: Vec3::new(SHELF_W * 0.5, SHELF_BOTTOM_T * 0.5, SHELF_D * 0.5),
};
let back = SdfNode::Box3d {
half_extents: Vec3::new(SHELF_W * 0.5, SHELF_BACK_H * 0.5, 1.6 * 0.5),
};
let back_placed = SdfNode::Translate {
child: Arc::new(back),
offset: Vec3::new(0.0, SHELF_BACK_H * 0.5, (-SHELF_D).mul_add(0.5, 0.8)),
};
let lip = SdfNode::Box3d {
half_extents: Vec3::new(SHELF_W * 0.5, SHELF_LIP_H * 0.5, 1.6 * 0.5),
};
let lip_placed = SdfNode::Translate {
child: Arc::new(lip),
offset: Vec3::new(0.0, SHELF_LIP_H * 0.5, SHELF_D.mul_add(0.5, -0.8)),
};
let peg = peg_facing_back(PEG_BLADE_W);
let peg_l = SdfNode::Translate {
child: Arc::new(peg.clone()),
offset: Vec3::new(-SHELF_PEG_SPACING * 0.5, SHELF_BACK_H * 0.7, -SHELF_D * 0.5),
};
let peg_r = SdfNode::Translate {
child: Arc::new(peg),
offset: Vec3::new(SHELF_PEG_SPACING * 0.5, SHELF_BACK_H * 0.7, -SHELF_D * 0.5),
};
let step1 = SdfNode::Union {
a: Arc::new(bottom),
b: Arc::new(back_placed),
};
let step2 = SdfNode::Union {
a: Arc::new(step1),
b: Arc::new(lip_placed),
};
let step3 = SdfNode::Union {
a: Arc::new(step2),
b: Arc::new(peg_l),
};
SdfNode::Union {
a: Arc::new(step3),
b: Arc::new(peg_r),
}
}
pub const ELASTIC_CORD_BODY_T: f32 = 3.0;
pub const ELASTIC_CORD_PEG_PITCH: f32 = 40.0;
pub const ELASTIC_CORD_HOOK_REACH: f32 = 12.0;
pub const ELASTIC_CORD_HOOK_R: f32 = 3.0;
#[must_use]
pub fn skadis_elastic_cord_sdf() -> SdfNode {
let ht = ELASTIC_CORD_BODY_T * 0.5;
let hook_width = 8.0;
let peg_top = skadis_peg_and_shoulder(hook_width);
let peg_bot = SdfNode::Translate {
child: Arc::new(skadis_peg_and_shoulder(hook_width)),
offset: Vec3::new(0.0, -ELASTIC_CORD_PEG_PITCH, 0.0),
};
let spine = SdfNode::Box3d {
half_extents: Vec3::new(ht * 0.5, f32::midpoint(ELASTIC_CORD_PEG_PITCH, 8.0), 4.0),
};
let spine_placed = SdfNode::Translate {
child: Arc::new(spine),
offset: Vec3::new(ht * 0.5, -ELASTIC_CORD_PEG_PITCH * 0.5, 0.0),
};
let mut top_pts: Vec<glam::Vec2> = vec![glam::Vec2::new(ht, -8.0)];
let n = 12;
for i in 1..=n {
#[allow(clippy::cast_precision_loss)]
let a = std::f32::consts::FRAC_PI_2 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
top_pts.push(glam::Vec2::new(
(ELASTIC_CORD_HOOK_REACH - ELASTIC_CORD_HOOK_R).mul_add(sin_a, ht),
ELASTIC_CORD_HOOK_R.mul_add(-(1.0 - cos_a), -8.0),
));
}
let top_hook = capsule_polyline_sdf(&top_pts, ht, hook_width);
let mut bot_pts: Vec<glam::Vec2> = vec![glam::Vec2::new(ht, -ELASTIC_CORD_PEG_PITCH + 8.0)];
for i in 1..=n {
#[allow(clippy::cast_precision_loss)]
let a = std::f32::consts::FRAC_PI_2 * i as f32 / n as f32;
let (sin_a, cos_a) = alice_det_math::sin_cos(a);
bot_pts.push(glam::Vec2::new(
(ELASTIC_CORD_HOOK_REACH - ELASTIC_CORD_HOOK_R).mul_add(sin_a, ht),
ELASTIC_CORD_HOOK_R.mul_add(1.0 - cos_a, -ELASTIC_CORD_PEG_PITCH + 8.0),
));
}
let bot_hook = capsule_polyline_sdf(&bot_pts, ht, hook_width);
let step1 = SdfNode::Union {
a: Arc::new(peg_top),
b: Arc::new(peg_bot),
};
let step2 = SdfNode::Union {
a: Arc::new(step1),
b: Arc::new(spine_placed),
};
let step3 = SdfNode::Union {
a: Arc::new(step2),
b: Arc::new(top_hook),
};
SdfNode::Union {
a: Arc::new(step3),
b: Arc::new(bot_hook),
}
}
#[cfg(test)]
mod tests {
use super::*;
use alice_sdf::eval;
#[test]
fn skadis_panel_sdf_returns_subtraction() {
let panel = skadis_panel_sdf(300.0, 5.0, 5.0);
assert!(matches!(panel, SdfNode::Subtraction { .. }));
}
#[test]
fn try_skadis_panel_sdf_accepts_every_size_in_use_today() {
assert!(try_skadis_panel_sdf(300.0, 5.0, 5.0).is_ok());
assert!(try_skadis_panel_sdf(MAX_SKADIS_PANEL_MM, 5.0, 5.0).is_ok());
}
#[test]
fn try_skadis_panel_sdf_never_hangs_on_a_nan_size() {
let e = try_skadis_panel_sdf(f32::NAN, 5.0, 5.0).unwrap_err();
assert!(e.to_string().contains("kind=panel_size"), "{e}");
assert!(e.to_string().contains("NaN"), "{e}");
}
#[test]
fn try_skadis_panel_sdf_never_hangs_on_an_infinite_size() {
let e = try_skadis_panel_sdf(f32::INFINITY, 5.0, 5.0).unwrap_err();
assert!(e.to_string().contains("kind=panel_size"), "{e}");
assert!(e.to_string().contains("inf"), "{e}");
let nan = try_skadis_panel_sdf(f32::NAN, 5.0, 5.0).unwrap_err();
assert_ne!(e.to_string(), nan.to_string());
}
#[test]
#[allow(clippy::float_cmp)] fn try_skadis_panel_sdf_refuses_a_huge_finite_size_too() {
let e = try_skadis_panel_sdf(1.0e9, 5.0, 5.0).unwrap_err();
let crate::limits::SpecError::FloatLimit(e) = e else {
panic!("expected FloatLimit, got {e:?}")
};
assert_eq!(e.kind, "panel_size");
assert_eq!(e.value, 1.0e9);
}
#[test]
#[should_panic(expected = "skadis_panel_sdf の size が検査を通らない")]
fn skadis_panel_sdf_panics_with_its_documented_message_on_a_nan_size() {
let _ = skadis_panel_sdf(f32::NAN, 5.0, 5.0);
}
#[test]
fn skadis_panel_material_region_is_inside() {
let panel = skadis_panel_sdf(300.0, 5.0, 5.0);
assert!(eval(&panel, Vec3::new(10.0, 0.0, 10.0)) < 0.0);
}
#[test]
fn skadis_panel_peg_center_is_hole() {
let panel = skadis_panel_sdf(300.0, 5.0, 5.0);
assert!(eval(&panel, Vec3::ZERO) > 0.0);
}
#[test]
fn skadis_panel_stagger_peg_position() {
let panel = skadis_panel_sdf(300.0, 5.0, 5.0);
assert!(eval(&panel, Vec3::new(20.0, 0.0, 20.0)) > 0.0);
}
#[test]
fn skadis_panel_outside_boundary() {
let panel = skadis_panel_sdf(300.0, 5.0, 5.0);
assert!(eval(&panel, Vec3::new(200.0, 0.0, 0.0)) > 0.0);
}
#[test]
fn skadis_panel_y_thickness_bounded_at_thickness_half() {
let panel = skadis_panel_sdf(300.0, 5.0, 6.0);
assert!(
eval(&panel, Vec3::new(10.0, 3.0, 10.0)) > 0.0,
"Y=3mm は thickness/2=2.5mm を超えるので空間になるはず"
);
assert!(
eval(&panel, Vec3::new(10.0, 2.0, 10.0)) < 0.0,
"Y=2mm + XZ=(10,10) は material 内部になるはず"
);
}
#[test]
fn skadis_panel_xz_corner_fillet_preserved() {
let panel = skadis_panel_sdf(300.0, 5.0, 6.0);
assert!(
eval(&panel, Vec3::new(156.0, 0.0, 156.0)) > 0.0,
"コーナー (156, 0, 156) は fillet で切り取られるので空間になるはず"
);
assert!(
eval(&panel, Vec3::new(110.0, 0.0, 110.0)) < 0.0,
"(110, 0, 110) は grid 外の material 内部になるはず"
);
}
#[test]
fn skadis_panel_connector_holes_are_present() {
let panel = skadis_panel_sdf(300.0, 5.0, 6.0);
for &(sx, sz) in &[
(-144.0f32, -144.0),
(144.0, -144.0),
(-144.0, 144.0),
(144.0, 144.0),
] {
assert!(
eval(&panel, Vec3::new(sx, 0.0, sz)) > 0.0,
"コーナー connector 穴 ({sx}, 0, {sz}) は空間になるはず"
);
}
for &cx in &[-110.0f32, 10.0, 130.0] {
assert!(
eval(&panel, Vec3::new(cx, 0.0, -144.0)) > 0.0,
"上辺 connector 穴 ({cx}, 0, -144) は空間になるはず"
);
}
}
#[test]
fn skadis_panel_mount_holes_are_present() {
let panel = skadis_panel_sdf(300.0, 5.0, 6.0);
for &cx in &[-90.0f32, 0.0, 70.0] {
assert!(
eval(&panel, Vec3::new(cx, 0.0, -144.0)) > 0.0,
"上辺 mount 穴 ({cx}, 0, -144) は空間になるはず"
);
assert!(
eval(&panel, Vec3::new(cx, 0.0, 144.0)) > 0.0,
"下辺 mount 穴 ({cx}, 0, 144) は空間になるはず"
);
}
}
#[test]
fn skadis_panel_frame_zone_material_between_holes() {
let panel = skadis_panel_sdf(300.0, 5.0, 6.0);
assert!(
eval(&panel, Vec3::new(-140.0, 0.0, -140.0)) < 0.0,
"frame 内 穴なし位置 (-140, 0, -140) は material 内部になるはず"
);
}
#[test]
fn skadis_panel_peg_hole_stadium_shape() {
let panel = skadis_panel_sdf(300.0, 5.0, 6.0);
assert!(
eval(&panel, Vec3::new(0.0, 0.0, 6.0)) > 0.0,
"(0, 0, 6.0) は stadium 半円 end 内なので穴になるはず"
);
assert!(
eval(&panel, Vec3::new(0.0, 0.0, 8.0)) < 0.0,
"(0, 0, 8.0) は stadium 外側で material 内部になるはず"
);
assert!(
eval(&panel, Vec3::new(3.0, 0.0, 0.0)) < 0.0,
"(3, 0, 0) は stadium X 幅 (±2.5) の外なので material 内部になるはず"
);
}
#[test]
fn skadis_peg_and_shoulder_returns_union() {
let ps = skadis_peg_and_shoulder(8.0);
assert!(matches!(ps, SdfNode::Union { .. }));
}
#[test]
fn capsule_polyline_short_input_returns_sphere() {
let empty: Vec<glam::Vec2> = vec![glam::Vec2::ZERO];
let s = capsule_polyline_sdf(&empty, 1.0, 5.0);
assert!(matches!(s, SdfNode::Sphere { .. }));
}
#[test]
fn skadis_hook_l_body_returns_union() {
let h = skadis_hook_l_sdf();
assert!(matches!(h, SdfNode::Union { .. }));
}
#[test]
fn skadis_hook_j_and_s_return_union() {
let j = skadis_hook_j_sdf();
let s = skadis_hook_s_sdf();
assert!(matches!(j, SdfNode::Union { .. }));
assert!(matches!(s, SdfNode::Union { .. }));
}
#[test]
fn hook_peg_area_is_inside_material() {
for hook in [
skadis_hook_l_sdf(),
skadis_hook_j_sdf(),
skadis_hook_s_sdf(),
] {
assert!(eval(&hook, Vec3::new(-2.5, 0.0, 0.0)) < 0.0);
}
}
#[test]
fn container_returns_union() {
let c = skadis_container_sdf();
assert!(matches!(c, SdfNode::Union { .. }));
}
#[test]
fn clip_returns_union() {
let c = skadis_clip_sdf();
assert!(matches!(c, SdfNode::Union { .. }));
}
#[test]
fn shelf_returns_union() {
let s = skadis_shelf_sdf();
assert!(matches!(s, SdfNode::Union { .. }));
}
#[test]
fn elastic_cord_returns_union() {
let e = skadis_elastic_cord_sdf();
assert!(matches!(e, SdfNode::Union { .. }));
}
#[test]
fn all_phase_5_2_accessories_produce_finite_sdf() {
let nodes = [
skadis_container_sdf(),
skadis_clip_sdf(),
skadis_shelf_sdf(),
skadis_elastic_cord_sdf(),
];
for (i, n) in nodes.iter().enumerate() {
let d = eval(n, Vec3::new(0.1, 0.1, 0.1));
assert!(d.is_finite(), "accessory {i}: non-finite SDF");
}
}
}