#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::beam_stress::{BeamAnalysis, BeamReport, CrossSection, LoadCase};
use crate::bimaterial::{analyze_bimaterial, BimaterialReport, BimaterialSide};
use crate::bridging::{analyze_bridges, BridgeSpan, BridgingReport};
use crate::filament_db::{FilamentDb, MaterialProperties};
use crate::fillet_stress::kt_shaft_shoulder_bending;
use crate::layer_adhesion::{EffectiveStrength, PrintOrientation};
use crate::math::Fix128;
use crate::print_orientation::{optimize_analytical, LoadDirection, OrientationReport};
use crate::support_volume::{
estimate_support_volume, OverhangRegion, SupportConfig, SupportVolumeReport,
};
use crate::thermal_stress::{analyze_thermal_stress, ThermalStressReport};
use crate::warp_risk::{
analyze_warp_risk, EnvConditions, Footprint, WarpRiskCategory, WarpRiskReport,
};
#[derive(Debug, Clone, Default)]
pub struct PrintPipelineInputs {
pub beam_load: Option<(CrossSection, LoadCase)>,
pub load_direction: Option<LoadDirection>,
pub bridges: Vec<BridgeSpan>,
pub overhangs: Vec<OverhangRegion>,
pub support_config: Option<SupportConfig>,
pub fillet: Option<(Fix128, Fix128, Fix128)>,
pub secondary_material: Option<(&'static str, Fix128, Fix128)>,
pub operating_temp_c: Option<Fix128>,
}
#[derive(Debug, Clone)]
pub struct PrintSafetyReport {
pub material_name: &'static str,
pub warp: WarpRiskReport,
pub effective_strength: EffectiveStrength,
pub orientation: Option<OrientationReport>,
pub thermal_stress: ThermalStressReport,
pub beam: Option<BeamReport>,
pub bridging: Option<BridgingReport>,
pub support: Option<SupportVolumeReport>,
pub fillet_kt: Option<Fix128>,
pub bimaterial: Option<BimaterialReport>,
pub is_safe: bool,
pub messages: Vec<String>,
}
impl PrintSafetyReport {
#[cfg(feature = "std")]
pub fn print(&self) {
println!("=== Print Safety Pipeline Report ===");
println!(" Material: {}", self.material_name);
println!(
" Warp: {:?} (score {:.2})",
self.warp.category,
self.warp.score.to_f32()
);
println!(
" Strength envelope: X={:.1} Y={:.1} Z={:.1} τ_xy={:.1}",
self.effective_strength.normal_x_mpa.to_f32(),
self.effective_strength.normal_y_mpa.to_f32(),
self.effective_strength.normal_z_mpa.to_f32(),
self.effective_strength.shear_xy_mpa.to_f32(),
);
println!(
" Thermal stress: {:.2} MPa (FoS {:.1})",
self.thermal_stress.thermal_stress_mpa.to_f32(),
self.thermal_stress.factor_of_safety.to_f32(),
);
if let Some(o) = &self.orientation {
println!(
" Best orientation angle_to_Z: {:.2} rad (yield {:.1} MPa, +{:.1})",
o.angle_to_z_axis.to_f32(),
o.effective_yield_mpa.to_f32(),
o.improvement_mpa.to_f32()
);
}
if let Some(b) = &self.beam {
println!(
" Beam: σ={:.2} MPa, FoS={:.2}",
b.max_bending_stress_mpa.to_f32(),
b.factor_of_safety.to_f32()
);
}
if let Some(br) = &self.bridging {
println!(
" Bridging: {}/{} unsafe, max span {:.1} mm",
br.unsafe_count,
br.checks.len(),
br.max_length_mm.to_f32()
);
}
if let Some(sv) = &self.support {
println!(
" Support volume: {:.0} mm³, filament {:.0} mm, time {:.0} min",
sv.total_volume_mm3.to_f32(),
sv.filament_length_mm.to_f32(),
sv.estimated_time_min.to_f32()
);
}
if let Some(kt) = self.fillet_kt {
println!(" Fillet K_t: {:.2}", kt.to_f32());
}
if let Some(bm) = &self.bimaterial {
println!(
" Bimaterial: σ_residual={:.2} MPa (FoS {:.1})",
bm.thermal_residual_mpa.to_f32(),
bm.factor_of_safety.to_f32()
);
}
for m in &self.messages {
println!(" {}", m);
}
println!(
" Overall: {}",
if self.is_safe { "SAFE" } else { "UNSAFE" }
);
}
}
fn pick_material(name: &str) -> MaterialProperties {
FilamentDb::with_defaults()
.find_by_name(name)
.copied()
.unwrap_or_else(MaterialProperties::pla)
}
#[must_use]
pub fn analyze_print_pipeline(
footprint: Footprint,
material_name: &str,
inputs: &PrintPipelineInputs,
) -> PrintSafetyReport {
let material = pick_material(material_name);
let mut messages = Vec::new();
let mut is_safe = true;
let warp = analyze_warp_risk(&footprint, &material, &EnvConditions::open_air_pla());
match warp.category {
WarpRiskCategory::Critical | WarpRiskCategory::High => {
messages.push(format!("Warp {:?}: {}", warp.category, warp.recommendation));
is_safe = false;
}
WarpRiskCategory::Medium => {
messages.push(format!("Warp {:?}: {}", warp.category, warp.recommendation));
}
WarpRiskCategory::Low => {}
}
let effective_strength = EffectiveStrength::for_material(&material, PrintOrientation::XYFlat);
let orientation = inputs
.load_direction
.map(|dir| optimize_analytical(&dir, &material));
let op_temp = inputs
.operating_temp_c
.unwrap_or_else(|| Fix128::from_int(20));
let thermal_stress = analyze_thermal_stress(
&material,
Fix128::from_ratio(3, 10),
material.print_temp_c,
op_temp,
);
if thermal_stress.near_glass_transition {
messages.push("Operating temperature near Tg".to_string());
}
let beam = inputs.beam_load.map(|(section, load)| {
let analysis = BeamAnalysis::new(section, load, material);
let r = analysis.analyze();
if !r.is_safe {
messages.push(format!("Beam FoS {:.2}", r.factor_of_safety.to_f32()));
is_safe = false;
}
r
});
let bridging = if !inputs.bridges.is_empty() {
let br = analyze_bridges(&inputs.bridges, &material);
if br.has_unsafe() {
messages.push(format!(
"{} bridge spans exceed material limit",
br.unsafe_count
));
is_safe = false;
}
Some(br)
} else {
None
};
let support = if !inputs.overhangs.is_empty() {
let cfg = inputs.support_config.unwrap_or_default();
Some(estimate_support_volume(&inputs.overhangs, &cfg))
} else {
None
};
let fillet_kt = inputs
.fillet
.map(|(r, d, dbig)| kt_shaft_shoulder_bending(r, d, dbig));
if let Some(kt) = fillet_kt {
if kt > Fix128::from_int(2) {
messages.push(format!("Fillet K_t {:.2} > 2 — enlarge R", kt.to_f32()));
}
}
let bimaterial = inputs.secondary_material.map(|(name2, t1, t2)| {
let m2 = pick_material(name2);
let a = BimaterialSide::from_material(material, t1);
let b = BimaterialSide::from_material(m2, t2);
let report = analyze_bimaterial(&a, &b, material.print_temp_c, op_temp, Fix128::ZERO);
if !report.is_safe {
messages.push(format!(
"Bimaterial FoS {:.1}",
report.factor_of_safety.to_f32()
));
}
report
});
PrintSafetyReport {
material_name: material.name,
warp,
effective_strength,
orientation,
thermal_stress,
beam,
bridging,
support,
fillet_kt,
bimaterial,
is_safe,
messages,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::Vec3Fix;
fn small_footprint() -> Footprint {
Footprint {
area_mm2: Fix128::from_int(1000),
max_dimension_mm: Fix128::from_int(50),
}
}
fn large_footprint() -> Footprint {
Footprint {
area_mm2: Fix128::from_int(70_000),
max_dimension_mm: Fix128::from_int(280),
}
}
#[test]
fn small_pla_defaults_safe() {
let r = analyze_print_pipeline(small_footprint(), "PLA", &PrintPipelineInputs::default());
assert!(r.is_safe);
assert_eq!(r.material_name, "PLA");
}
#[test]
fn large_abs_flagged() {
let r = analyze_print_pipeline(large_footprint(), "ABS", &PrintPipelineInputs::default());
assert!(!r.is_safe || !r.messages.is_empty());
}
#[test]
fn beam_load_included_in_report() {
let inputs = PrintPipelineInputs {
beam_load: Some((
CrossSection::Rectangular {
width_mm: Fix128::from_int(10),
height_mm: Fix128::from_int(20),
},
LoadCase::CantileverEndPoint {
load_n: Fix128::from_int(5),
length_mm: Fix128::from_int(200),
},
)),
..Default::default()
};
let r = analyze_print_pipeline(small_footprint(), "PLA", &inputs);
assert!(r.beam.is_some());
}
#[test]
fn orientation_optimized_when_load_direction_supplied() {
let inputs = PrintPipelineInputs {
load_direction: Some(LoadDirection::axis_z()),
..Default::default()
};
let r = analyze_print_pipeline(small_footprint(), "PLA", &inputs);
assert!(r.orientation.is_some());
assert!(r.orientation.unwrap().improvement_mpa > Fix128::ZERO);
}
#[test]
fn bridging_check_flags_over_limit() {
let inputs = PrintPipelineInputs {
bridges: vec![
BridgeSpan {
start: Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ZERO),
end: Vec3Fix::new(Fix128::from_int(30), Fix128::ZERO, Fix128::ZERO),
}, ],
..Default::default()
};
let r = analyze_print_pipeline(small_footprint(), "PLA", &inputs);
assert!(r.bridging.is_some());
assert!(!r.is_safe);
}
#[test]
fn support_volume_reported() {
let inputs = PrintPipelineInputs {
overhangs: vec![OverhangRegion {
projected_area_mm2: Fix128::from_int(100),
support_height_mm: Fix128::from_int(20),
}],
..Default::default()
};
let r = analyze_print_pipeline(small_footprint(), "PLA", &inputs);
assert!(r.support.is_some());
assert!(r.support.unwrap().total_volume_mm3 > Fix128::ZERO);
}
#[test]
fn fillet_kt_reported() {
let inputs = PrintPipelineInputs {
fillet: Some((
Fix128::from_ratio(2, 10),
Fix128::from_int(20),
Fix128::from_int(40),
)),
..Default::default()
};
let r = analyze_print_pipeline(small_footprint(), "PLA", &inputs);
assert!(r.fillet_kt.is_some());
}
#[test]
fn bimaterial_reported() {
let inputs = PrintPipelineInputs {
secondary_material: Some(("PETG", Fix128::from_int(1), Fix128::from_int(1))),
..Default::default()
};
let r = analyze_print_pipeline(small_footprint(), "PLA", &inputs);
assert!(r.bimaterial.is_some());
}
#[test]
fn unknown_material_defaults_to_pla() {
let r = analyze_print_pipeline(
small_footprint(),
"Unobtainium",
&PrintPipelineInputs::default(),
);
assert_eq!(r.material_name, "PLA");
}
}