mod cavities;
mod colors;
mod degenerate_coverage;
mod feature_fragments;
mod primitive_gate;
pub mod t_junctions;
use crate::{
recognize_surfaces_with_candidates, reconstruct_surface, AnalyticSurface, ComponentCandidate,
ConeSurface, CylinderSurface, Mesh, PlaneSurface, SphereSurface, TorusSurface,
MeshAnalysisOptions, PhaseTimings, RecognitionOptions, SamplingMode, SurfaceFitResult,
SurfaceHint, SurfaceRegion, SurfaceType, Vec3,
};
use brep_kernel::{
audit_step_manifold, export_step, import_step, make_cone_brep, make_cylinder_brep,
make_sphere_brep, make_torus_brep, merge_same_surface_faces, mesh_regions_to_brep,
mesh_to_faceted_brep, repair_triangle_soup, segment_mesh_faces, MeshSegmentation,
RegionCarrier, SegmentOptions,
UNASSIGNED_REGION,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::fmt::{Display, Formatter};
use web_time::Instant;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub enum ConversionPolicy {
#[default]
AllowFacetedFallback,
RequireFullyAnalytic,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct StlConversionOptions {
pub recognition: RecognitionOptions,
pub policy: ConversionPolicy,
pub try_kernel_analytic_rebuild: bool,
pub try_hybrid_rebuild: bool,
pub weld_tolerance: f64,
pub coordinate_precision_tolerance: f64,
pub kernel_deflection_angle_degrees: f64,
pub kernel_fit_tolerance: f64,
pub kernel_normal_tolerance_degrees: f64,
}
impl Default for StlConversionOptions {
fn default() -> Self {
let mut recognition = RecognitionOptions::default();
recognition.collect_phase_timings = true;
recognition.sampling = SamplingMode::Vertices;
recognition.torus_fast = crate::hypothesis_switch::torus_fast();
Self {
recognition,
policy: ConversionPolicy::AllowFacetedFallback,
try_kernel_analytic_rebuild: true,
try_hybrid_rebuild: true,
weld_tolerance: -1.0,
coordinate_precision_tolerance: 0.0,
kernel_deflection_angle_degrees: 30.0,
kernel_fit_tolerance: 1.0e-3,
kernel_normal_tolerance_degrees: 15.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversionBackend {
RansacSphere,
RansacTorus,
RansacCappedCylinder,
RansacCappedCone,
KernelAnalyticRebuild,
KernelSphericalUnion,
#[serde(alias = "hybrid_plane_cylinder_rebuild")]
HybridAnalyticRebuild,
FacetedRepair,
FacetedRepairCoplanarMerged,
Refused,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConversionRegionReport {
pub surface_type: SurfaceType,
pub surface: AnalyticSurface,
pub support_triangles: usize,
pub supported_area: f64,
pub rms_error: f64,
pub max_error: f64,
pub rms_normal_error_radians: f64,
pub max_normal_error_radians: f64,
pub confidence: f64,
pub orientation: i8,
pub reason: String,
pub phase_timings: PhaseTimings,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct ConversionTimings {
#[serde(default)]
pub segmentation_seconds: f64,
pub recognition_seconds: f64,
pub topology_build_seconds: f64,
pub step_export_seconds: f64,
pub step_validation_seconds: f64,
pub total_seconds: f64,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct HybridConversionReport {
pub total_faces: usize,
pub analytic_plane_faces: usize,
pub analytic_plane_triangles: usize,
pub analytic_cylinder_faces: usize,
pub analytic_cylinder_triangles: usize,
#[serde(default)]
pub analytic_cone_faces: usize,
#[serde(default)]
pub analytic_cone_triangles: usize,
#[serde(default)]
pub analytic_sphere_faces: usize,
#[serde(default)]
pub analytic_sphere_triangles: usize,
#[serde(default)]
pub analytic_torus_faces: usize,
#[serde(default)]
pub analytic_torus_triangles: usize,
pub faceted_faces: usize,
pub faceted_triangles: usize,
pub demoted_regions: usize,
pub demoted_triangles: usize,
pub segmentation_plane_regions: usize,
pub segmentation_cylinder_regions: usize,
#[serde(default)]
pub segmentation_cone_regions: usize,
#[serde(default)]
pub segmentation_sphere_regions: usize,
#[serde(default)]
pub segmentation_torus_regions: usize,
pub segmentation_unsupported_regions: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DemotionCause {
CylinderConeEdge,
CylinderCylinderEdge,
SphereRingEdge,
TorusRingEdge,
ConeConeEdge,
SameKindRevolveEdge,
PlaneNeighbourEdge,
FacetNeighbourEdge,
DemotedNeighbourNotRuling,
CylinderAxisSnap,
ConeSnap,
ConeSpan,
CylinderSurface,
PeriodicSeam,
ConeSurface,
SphereSurface,
TorusSurface,
ChainEdgeCurved,
ChainEdgeCone,
ChainEdgeCylinder,
ChainEdgeVertexPlacer,
CylinderFace,
ConeFace,
SphereFace,
TorusFace,
WholeBodyCheck,
Injected,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DemotedKind {
Cylinder,
Cone,
Sphere,
Torus,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct HybridDemotion {
pub region: u32,
pub kind: DemotedKind,
pub triangles: usize,
pub cause: DemotionCause,
pub root_cause: DemotionCause,
pub neighbour: Option<u32>,
pub round: usize,
pub message: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ComponentConversionReport {
pub index: usize,
pub first_triangle: usize,
pub input_vertices: usize,
pub input_triangles: usize,
pub backend: ConversionBackend,
pub backend_reason: String,
pub fallback_cause: Option<String>,
pub recognition_skipped: Option<String>,
pub recognized_regions: usize,
pub recognized_triangles: usize,
pub unresolved_triangles: usize,
pub source_closed_manifold: bool,
#[serde(default)]
pub analytic_used_repaired_mesh: bool,
pub hybrid_rebuild: Option<HybridConversionReport>,
#[serde(default)]
pub hybrid_demotions: Vec<HybridDemotion>,
pub faceted_faces_before_merge: Option<usize>,
pub faceted_faces_after_merge: Option<usize>,
pub faces: usize,
#[serde(default)]
pub refusal: Option<String>,
#[serde(default)]
pub source_name: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StlConversionReport {
pub backend: ConversionBackend,
pub backend_reason: String,
pub fallback_cause: Option<String>,
pub input_vertices: usize,
pub input_triangles: usize,
pub recognized_regions: usize,
pub recognized_triangles: usize,
pub unresolved_triangles: usize,
#[serde(default)]
pub recognition_skipped: Option<String>,
pub source_closed_manifold: bool,
#[serde(default)]
pub analytic_used_repaired_mesh: bool,
#[serde(default)]
pub requested_distance_tolerance: f64,
#[serde(default)]
pub coordinate_precision_tolerance: f64,
#[serde(default)]
pub effective_distance_tolerance: f64,
pub region_type_counts: BTreeMap<String, usize>,
pub regions: Vec<ConversionRegionReport>,
pub topology_refit: Option<ConversionRegionReport>,
#[serde(default)]
pub hybrid_rebuild: Option<HybridConversionReport>,
#[serde(default)]
pub hybrid_demotions: Vec<HybridDemotion>,
#[serde(default)]
pub components: Vec<ComponentConversionReport>,
pub exported_step_bytes: usize,
pub exported_advanced_faces: usize,
#[serde(default)]
pub faceted_faces_before_merge: Option<usize>,
#[serde(default)]
pub faceted_faces_after_merge: Option<usize>,
pub roundtrip_solids: usize,
pub manifold_audit_issues: Vec<String>,
pub timings: ConversionTimings,
#[serde(default)]
pub recognition_work: RecognitionWorkReport,
#[serde(default)]
pub whole_carrier_scan: WholeCarrierScanReport,
pub messages: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct RecognitionWorkReport {
pub hypotheses: usize,
pub rounds: usize,
pub seeded_components: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct WholeCarrierScanReport {
pub accepted: usize,
pub declined: usize,
pub abstained: usize,
}
impl WholeCarrierScanReport {
pub(crate) fn add(&mut self, verdict: crate::hybrid_whole_freeform::ScanVerdict) {
use crate::hybrid_whole_freeform::ScanVerdict;
match verdict {
ScanVerdict::Accepted => self.accepted += 1,
ScanVerdict::Declined => self.declined += 1,
ScanVerdict::Abstained => self.abstained += 1,
}
}
fn sum(self, other: Self) -> Self {
Self {
accepted: self.accepted + other.accepted,
declined: self.declined + other.declined,
abstained: self.abstained + other.abstained,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StlConversionOutput {
pub step_text: String,
pub report: StlConversionReport,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StlConversionError(pub String);
impl Display for StlConversionError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::error::Error for StlConversionError {}
impl From<String> for StlConversionError {
fn from(value: String) -> Self {
Self(value)
}
}
pub fn binary_stl_coordinate_precision_tolerance(mesh: &Mesh) -> f64 {
let maximum_absolute_coordinate = mesh
.vertices
.iter()
.flat_map(|point| [point.x.abs(), point.y.abs(), point.z.abs()])
.fold(0.0_f64, f64::max);
let coordinate_scale = maximum_absolute_coordinate.max(mesh_diagonal(mesh));
coordinate_scale * (f32::EPSILON as f64) * 0.5 * 3.0_f64.sqrt()
}
pub fn convert_stl_mesh_to_step(
mesh: &Mesh,
source_positions: &[f64],
source_indices: Option<&[u32]>,
options: &StlConversionOptions,
part_name: &str,
unit: &str,
timestamp: &str,
) -> Result<StlConversionOutput, StlConversionError> {
convert_stl_mesh_to_step_with_colors(
mesh,
source_positions,
source_indices,
None,
options,
part_name,
unit,
timestamp,
)
}
#[allow(clippy::too_many_arguments)]
pub fn convert_stl_mesh_to_step_with_colors(
mesh: &Mesh,
source_positions: &[f64],
source_indices: Option<&[u32]>,
triangle_colors: Option<&[Option<[u8; 3]>]>,
options: &StlConversionOptions,
part_name: &str,
unit: &str,
timestamp: &str,
) -> Result<StlConversionOutput, StlConversionError> {
let total_started = Instant::now();
validate_source_buffers(mesh, source_positions, source_indices)?;
if !options.coordinate_precision_tolerance.is_finite()
|| options.coordinate_precision_tolerance < 0.0
{
return Err(StlConversionError(
"coordinate_precision_tolerance must be finite and non-negative".to_owned(),
));
}
let components = triangle_components(mesh);
let count = components.len();
if count == 0 {
return Err(StlConversionError(
"the mesh contains no triangles to reconstruct".to_owned(),
));
}
let mut solids = Vec::with_capacity(count);
let mut component_ids = Vec::with_capacity(count);
let mut details = Vec::with_capacity(count);
let mut first_refusal = None;
for (index, triangles) in components.iter().enumerate() {
if count == 1 {
let built = convert_component(mesh, source_positions, source_indices, options)?;
solids.push(built.0);
component_ids.push(index);
details.push(built.1);
continue;
}
let part = component_mesh(mesh, source_positions, source_indices, triangles);
match convert_component(
&part.mesh,
&part.positions,
part.indices.as_deref(),
options,
) {
Ok(built) => {
solids.push(built.0);
component_ids.push(index);
details.push(built.1);
}
Err(error) => {
first_refusal.get_or_insert_with(|| {
format!(
"body {} of {count} (from source triangle {}, {} triangle(s)): {error}",
index + 1,
triangles[0],
triangles.len()
)
});
details.push(refused_component(&part.mesh, triangles.len(), error.0));
}
}
}
if solids.is_empty() {
let refusal = first_refusal.unwrap_or_default();
return Err(StlConversionError(if count > 1 {
format!("{refusal} (every one of the {count} bodies was refused)")
} else {
refusal
}));
}
let (mut solids, cavity_messages) = cavities::assemble(mesh, &components, solids, &component_ids)?;
let export_started = Instant::now();
let colored = triangle_colors.filter(|colors| colors.iter().any(Option::is_some));
let mut color_message = None;
let step_text = match colored {
Some(triangle_colors) => {
let assignment = colors::assign(mesh, triangle_colors, &mut solids, part_name)?;
let named: Vec<(String, &brep_kernel::BrepSolid)> =
assignment.body_names.iter().cloned().zip(solids.iter()).collect();
color_message = Some(format!(
"source colours: {} of {} bodies and {} faces styled; {} coloured source triangles matched no face",
assignment.bodies,
solids.len(),
assignment.faces,
assignment.unmatched
));
brep_kernel::export_step_report_named(
&named,
part_name,
unit,
timestamp,
None,
Some(&assignment.colors),
)
.map(|report| report.text)
}
None => export_step(&solids, part_name, unit, timestamp),
}
.map_err(|error| StlConversionError(format!("STEP export failed: {error}")))?;
let step_export_seconds = export_started.elapsed().as_secs_f64();
let validation_started = Instant::now();
let manifold_audit_issues = audit_step_manifold(&step_text);
if !manifold_audit_issues.is_empty() {
return Err(StlConversionError(format!(
"STEP manifold audit failed: {}",
manifold_audit_issues.join("; ")
)));
}
let imported = import_step(&step_text).map_err(|error| {
StlConversionError(format!("STEP round-trip validation failed: {error}"))
})?;
if let Some(issues) = imported
.iter()
.map(|solid| solid.validate())
.find(|issues| !issues.is_empty())
{
return Err(StlConversionError(format!(
"round-tripped STEP has invalid topology: {issues:?}"
)));
}
if imported.len() != solids.len() {
return Err(StlConversionError(format!(
"{} body/bodies were written but {} came back out of the STEP document",
solids.len(),
imported.len()
)));
}
let step_validation_seconds = validation_started.elapsed().as_secs_f64();
let mut report = aggregate_report(&components, details, count, options);
report.messages.extend(cavity_messages);
report.messages.extend(color_message);
report.messages.push(format!(
"STEP manifold audit and round-trip import passed for {} solid(s)",
imported.len()
));
report.messages.push(report.backend_reason.clone());
report.exported_step_bytes = step_text.len();
report.exported_advanced_faces = step_text.matches("ADVANCED_FACE(").count();
report.roundtrip_solids = imported.len();
report.manifold_audit_issues = manifold_audit_issues;
report.timings.step_export_seconds = step_export_seconds;
report.timings.step_validation_seconds = step_validation_seconds;
report.timings.total_seconds = total_started.elapsed().as_secs_f64();
Ok(StlConversionOutput { step_text, report })
}
fn refused_component(mesh: &Mesh, triangles: usize, reason: String) -> ComponentDetail {
ComponentDetail {
backend: ConversionBackend::Refused,
backend_reason: format!("refused, not in the model: {reason}"),
fallback_cause: None,
input_vertices: mesh.vertices.len(),
input_triangles: triangles,
recognized_regions: 0,
recognized_triangles: 0,
unresolved_triangles: 0,
recognition_skipped: None,
source_closed_manifold: closed_manifold(mesh),
analytic_used_repaired_mesh: false,
requested_distance_tolerance: 0.0,
effective_distance_tolerance: 0.0,
region_type_counts: BTreeMap::new(),
regions: Vec::new(),
topology_refit: None,
hybrid_rebuild: None,
hybrid_demotions: Vec::new(),
faceted_faces_before_merge: None,
faceted_faces_after_merge: None,
faces: 0,
segmentation_seconds: 0.0,
recognition_seconds: 0.0,
recognition_work: RecognitionWorkReport::default(),
whole_carrier_scan: WholeCarrierScanReport::default(),
topology_build_seconds: 0.0,
messages: vec![format!("refused, not in the model: {reason}")],
refusal: Some(reason),
}
}
struct ComponentDetail {
backend: ConversionBackend,
backend_reason: String,
fallback_cause: Option<String>,
input_vertices: usize,
input_triangles: usize,
recognized_regions: usize,
recognized_triangles: usize,
unresolved_triangles: usize,
recognition_skipped: Option<String>,
source_closed_manifold: bool,
analytic_used_repaired_mesh: bool,
requested_distance_tolerance: f64,
effective_distance_tolerance: f64,
region_type_counts: BTreeMap<String, usize>,
regions: Vec<ConversionRegionReport>,
topology_refit: Option<ConversionRegionReport>,
hybrid_rebuild: Option<HybridConversionReport>,
hybrid_demotions: Vec<HybridDemotion>,
faceted_faces_before_merge: Option<usize>,
faceted_faces_after_merge: Option<usize>,
faces: usize,
segmentation_seconds: f64,
recognition_seconds: f64,
recognition_work: RecognitionWorkReport,
whole_carrier_scan: WholeCarrierScanReport,
topology_build_seconds: f64,
messages: Vec<String>,
refusal: Option<String>,
}
fn convert_component(
mesh: &Mesh,
source_positions: &[f64],
source_indices: Option<&[u32]>,
options: &StlConversionOptions,
) -> Result<(brep_kernel::BrepSolid, ComponentDetail), StlConversionError> {
let outward = cavities::outward_component(mesh, source_positions, source_indices);
let (mesh, source_positions, source_indices) = match &outward {
Some(part) => (&part.mesh, part.positions.as_slice(), part.indices.as_deref()),
None => (mesh, source_positions, source_indices),
};
let mut recognition_options = options.recognition.clone();
recognition_options.collect_phase_timings = true;
let requested_distance_tolerance = recognition_options.distance_tolerance;
recognition_options.distance_tolerance = recognition_options
.distance_tolerance
.max(options.coordinate_precision_tolerance);
let encoding_distance_tolerance = recognition_options.distance_tolerance;
let source_closed_manifold = closed_manifold(mesh);
let input_vertices = mesh.vertices.len();
let input_triangles = mesh.triangles.len();
let repaired = if source_closed_manifold {
None
} else {
repaired_closed_mesh(source_positions, source_indices, options)
};
let (mesh, source_positions, source_indices) = match &repaired {
Some(repaired) => (
&repaired.mesh,
repaired.positions.as_slice(),
Some(repaired.indices.as_slice()),
),
None => (mesh, source_positions, source_indices),
};
let closed_for_analytics = source_closed_manifold || repaired.is_some();
let segmentation_started = Instant::now();
let segmentation = if closed_for_analytics {
gate_segmentation(source_positions, source_indices, options)
} else {
Err("the source mesh is not a closed two-manifold".to_owned())
};
let segmentation_seconds = segmentation_started.elapsed().as_secs_f64();
let initial_recognition_gate = recognition_gate(&segmentation);
let geometric_gate_rescue = initial_recognition_gate.is_err()
&& closed_for_analytics
&& segmentation.as_ref().is_ok_and(|segmentation| {
primitive_gate::has_whole_primitive_candidate(mesh, segmentation, &recognition_options)
});
let recognition_gate = if geometric_gate_rescue {
Ok(())
} else {
initial_recognition_gate
};
let measured_region_deviation = match (&recognition_gate, &segmentation) {
(Ok(()), Ok(segmentation)) => segmentation
.regions
.iter()
.map(|region| region.max_deviation)
.fold(0.0_f64, f64::max),
_ => 0.0,
};
recognition_options.distance_tolerance = recognition_options
.distance_tolerance
.max(measured_region_deviation);
let effective_distance_tolerance = recognition_options.distance_tolerance;
let recognition_started = Instant::now();
let mut recognition_work = RecognitionWorkReport::default();
let (recognition, completed_plane_regions, completed_degenerate_triangles) = if recognition_gate.is_ok() {
let candidates = match &segmentation {
Ok(segmentation) if segment_carrier_enabled() => segmentation_candidates(mesh, segmentation),
_ => Vec::new(),
};
let (mut recognition, work) =
recognize_surfaces_with_candidates(mesh, &recognition_options, &candidates)
.map_err(|error| {
StlConversionError(format!("surface recognition failed: {error}"))
})?;
recognition_work = RecognitionWorkReport {
hypotheses: work.attempts,
rounds: work.rounds,
seeded_components: work.seeded_components,
};
feature_fragments::complete(
mesh,
&recognition_options,
&mut recognition.regions,
&mut recognition.unresolved_triangles,
)?;
let completed_plane_regions = complete_small_planar_regions(
mesh,
&recognition_options,
&mut recognition.regions,
&mut recognition.unresolved_triangles,
)?;
let completed_degenerate_triangles = degenerate_coverage::complete(
mesh, &recognition_options, &mut recognition.regions,
)?;
(recognition, completed_plane_regions, completed_degenerate_triangles)
} else {
(
crate::RecognitionResult {
regions: Vec::new(),
unresolved_triangles: Vec::new(),
unresolved_diagnostics: Vec::new(),
},
0,
0,
)
};
let recognition_seconds = if recognition_gate.is_ok() {
recognition_started.elapsed().as_secs_f64()
} else {
0.0
};
let recognized_triangles = recognition
.regions
.iter()
.map(|region| region.triangle_indices.len())
.sum();
let regions = recognition
.regions
.iter()
.map(region_report)
.collect::<Vec<_>>();
let mut region_type_counts = BTreeMap::new();
for region in &recognition.regions {
*region_type_counts
.entry(region.surface.surface_type().name().to_owned())
.or_insert(0) += 1;
}
let topology_started = Instant::now();
let analytic_coverage = recognition.unresolved_triangles.is_empty()
&& recognized_triangles == mesh.triangles.len()
&& disjoint_complete_partition(&recognition.regions, mesh.triangles.len());
let spherical_union = if closed_for_analytics && options.try_kernel_analytic_rebuild {
segmentation.as_ref().ok().and_then(|seg| {
brep_kernel::reconstruct_spherical_mesh_union(
source_positions,
source_indices.unwrap_or(&[]),
seg,
&segmentation_options(source_positions, options),
)
})
} else {
None
};
let mut whole_carrier_scan = WholeCarrierScanReport::default();
let primitive = if analytic_coverage
&& closed_for_analytics
&& recognition_work.seeded_components == 0
&& whole_fits_pass_the_local_scan(mesh, &recognition.regions, encoding_distance_tolerance, &mut whole_carrier_scan)
{
direct_analytic_solid(mesh, &recognition.regions, &recognition_options)
} else {
None
};
let mut fallback_cause = None;
let mut messages = vec![match &recognition_gate {
Ok(()) => format!(
"RANSAC recognized {} region(s) covering {recognized_triangles}/{} triangles",
recognition.regions.len(),
mesh.triangles.len()
),
Err(reason) => format!("RANSAC recognition skipped: {reason}"),
}];
if geometric_gate_rescue {
messages.push("whole-primitive recognition enabled by triangle coverage on a segmented curved carrier and its possible end caps".to_owned());
}
if outward.is_some() {
messages.push("inward source shell reconstructed outward before cavity assembly".into());
}
if let Some(repaired) = &repaired {
messages.push(repaired.message.clone());
}
if encoding_distance_tolerance > requested_distance_tolerance {
messages.push(format!(
"source coordinate precision raised the absolute recognition distance from {requested_distance_tolerance:.6e} to {encoding_distance_tolerance:.6e}"
));
}
if effective_distance_tolerance > encoding_distance_tolerance {
messages.push(format!(
"the segmentation's measured vertex deviation raised the absolute recognition distance from {encoding_distance_tolerance:.6e} to {effective_distance_tolerance:.6e}"
));
}
if completed_degenerate_triangles > 0 {
messages.push(format!(
"attached {completed_degenerate_triangles} numerically degenerate triangle(s) to their surrounding fitted carrier; excluded from fit support metrics"
));
}
if completed_plane_regions > 0 {
messages.push(format!(
"a plane-only completion pass recovered {completed_plane_regions} small feature-bounded planar region(s)"
));
}
let mut hybrid_demotions = Vec::new();
let (
solid,
backend,
backend_reason,
topology_refit,
hybrid_rebuild,
faceted_faces_before_merge,
faceted_faces_after_merge,
) = if let Some(primitive) = primitive {
(
primitive.solid?,
primitive.backend,
primitive.reason,
primitive.topology_refit,
None,
None,
None,
)
} else if let Some(solid) = spherical_union {
(
solid,
ConversionBackend::KernelSphericalUnion,
"precise sphere carriers and bidirectional mesh coverage verified a primitive union; rebuilt exact analytic intersections across the mesh boolean cut strips".to_owned(),
None,
None,
None,
None,
)
} else {
let direct_cause = match &recognition_gate {
Err(reason) => format!("whole-primitive recognition skipped: {reason}"),
Ok(()) if !analytic_coverage => format!(
"RANSAC did not prove complete analytic coverage ({} unresolved triangle(s))",
recognition.unresolved_triangles.len()
),
Ok(()) if !closed_for_analytics => {
"recognized mesh is not a closed two-manifold".to_owned()
}
Ok(()) => {
"recognized regions do not match a safe complete primitive topology".to_owned()
}
};
let kernel_eligible = segmentation.as_ref().is_ok_and(|segmentation| {
segmentation_is_complete(segmentation)
&& segmentation.regions.iter().all(|region| {
matches!(
region.carrier,
RegionCarrier::Plane { .. }
| RegionCarrier::Cylinder { .. }
| RegionCarrier::Cone { .. }
)
})
});
let kernel_attempt = if closed_for_analytics
&& kernel_eligible
&& options.try_kernel_analytic_rebuild
{
try_kernel_analytic(source_positions, source_indices, options)
} else {
Err("kernel analytic rebuild requires the kernel segmentation to prove complete plane/cylinder/cone coverage".to_owned())
};
let analytic_attempt = match kernel_attempt {
Ok(solid) => {
let stitched = repaired.as_ref().is_some_and(|repaired| repaired.stitched_edges > 0);
let strips = if stitched && tjunction_strip_hybrid_enabled() {
hybrid_over_kernel_strips(&solid, source_positions, source_indices, options)
} else {
None
};
match strips {
Some(output) => {
whole_carrier_scan = whole_carrier_scan.sum(output.stats.whole_carrier_scan);
let report = hybrid_report(&output.stats);
hybrid_demotions = output.stats.demotions;
Ok((
output.solid,
ConversionBackend::HybridAnalyticRebuild,
"the kernel region rebuilder's shell carried a T-junction's chord as a plane strip; independent segmentation reconstructed and validated a fully analytic shell with fewer faces".to_owned(),
Some(report),
))
}
None => Ok((
solid,
ConversionBackend::KernelAnalyticRebuild,
"kernel segmentation proved complete plane/cylinder/cone coverage; the kernel region rebuilder then reconstructed and validated the complete analytic shell".to_owned(),
None,
)),
}
}
Err(kernel_error) => {
let hybrid_attempt = if closed_for_analytics && options.try_hybrid_rebuild {
try_hybrid_analytic(source_positions, source_indices, options)
} else if !options.try_hybrid_rebuild {
Err("mixed analytic/faceted rebuild was disabled".to_owned())
} else {
Err("mixed analytic/faceted rebuild requires a closed two-manifold".to_owned())
};
let hybrid_attempt = hybrid_attempt.and_then(|output| {
if hybrid_roundtrip_fallback_enabled() {
if let Err(error) = solid_round_trips(&output.solid) {
let joined = crate::hybrid_region_brep::hybrid_plane_cylinder_brep_before_promotion_14(
source_positions,
source_indices,
&segmentation_options(source_positions, options),
);
if let Ok(Some(joined)) = joined {
if solid_round_trips(&joined.solid).is_ok() {
return Ok(joined);
}
}
let latest = crate::hybrid_region_brep::hybrid_plane_cylinder_brep_before_promotion_13(
source_positions,
source_indices,
&segmentation_options(source_positions, options),
);
if let Ok(Some(latest)) = latest {
if solid_round_trips(&latest.solid).is_ok() {
return Ok(latest);
}
}
let newest = crate::hybrid_region_brep::hybrid_plane_cylinder_brep_before_promotion_12(
source_positions,
source_indices,
&segmentation_options(source_positions, options),
);
if let Ok(Some(newest)) = newest {
if solid_round_trips(&newest.solid).is_ok() {
return Ok(newest);
}
}
let first = crate::hybrid_region_brep::hybrid_plane_cylinder_brep_before_promotion_11(
source_positions,
source_indices,
&segmentation_options(source_positions, options),
);
if let Ok(Some(first)) = first {
if solid_round_trips(&first.solid).is_ok() {
return Ok(first);
}
}
let second = crate::hybrid_region_brep::hybrid_plane_cylinder_brep_before_promotion_9(
source_positions,
source_indices,
&segmentation_options(source_positions, options),
);
if let Ok(Some(second)) = second {
if solid_round_trips(&second.solid).is_ok() {
return Ok(second);
}
}
for with_promotion_9 in [true, false] {
let further = crate::hybrid_region_brep::hybrid_plane_cylinder_brep_before_promotion_10(
source_positions,
source_indices,
&segmentation_options(source_positions, options),
with_promotion_9,
);
if let Ok(Some(further)) = further {
if solid_round_trips(&further.solid).is_ok() {
return Ok(further);
}
}
}
return Err(format!(
"{kernel_error}; the mixed rebuild's solid failed its STEP round trip: {error}"
));
}
}
Ok(output)
});
match hybrid_attempt {
Ok(output) => {
let stats = &output.stats;
whole_carrier_scan = whole_carrier_scan.sum(stats.whole_carrier_scan);
if stats.split_freeform_regions
+ stats.split_declined_regions
+ stats.split_over_budget_regions
+ stats.split_budget_hit_regions
+ stats.split_band_declined_regions
> 0
{
messages.push(format!(
"freeform split: {} region(s) divided into carriers, {} tried and left as segmented, {} above the triangle budget and not tried, {} out of the hypothesis budget and left as segmented, {} declined by the normal-band pre-test",
stats.split_freeform_regions,
stats.split_declined_regions,
stats.split_over_budget_regions,
stats.split_budget_hit_regions,
stats.split_band_declined_regions
));
}
let hybrid_report = hybrid_report(&output.stats);
if options.policy == ConversionPolicy::RequireFullyAnalytic
&& hybrid_report.faceted_faces != 0
{
Err(format!(
"{kernel_error}; mixed rebuild retained {} faceted face(s) for {} source triangle(s)",
hybrid_report.faceted_faces, hybrid_report.faceted_triangles
))
} else {
let reason = if hybrid_report.faceted_faces == 0 {
"independent segmentation reconstructed and validated a fully analytic plane/cylinder/cone/sphere shell"
} else {
"independent segmentation reconstructed exact plane/cylinder/cone/sphere regions and retained unsupported or unsafe regions as facets"
};
hybrid_demotions = output.stats.demotions;
Ok((
output.solid,
ConversionBackend::HybridAnalyticRebuild,
reason.to_owned(),
Some(hybrid_report),
))
}
}
Err(hybrid_error) => Err(format!("{kernel_error}; {hybrid_error}")),
}
}
};
match analytic_attempt {
Ok((solid, backend, reason, hybrid)) => {
(solid, backend, reason, None, hybrid, None, None)
}
Err(analytic_error) => {
let cause = format!("{direct_cause}; {analytic_error}");
if options.policy == ConversionPolicy::RequireFullyAnalytic {
return Err(StlConversionError(format!(
"strict analytic conversion refused faceted fallback: {cause}"
)));
}
fallback_cause = Some(cause.clone());
messages.push(format!("analytic rebuild unavailable: {cause}"));
let faceted =
mesh_to_faceted_brep(source_positions, source_indices, options.weld_tolerance)
.map_err(|error| {
StlConversionError(format!("faceted mesh repair failed: {error}"))
})?;
let faces_before = solid_face_count(&faceted);
let merge_tolerance = coplanar_merge_tolerance(mesh);
match merge_same_surface_faces(&faceted, merge_tolerance) {
Ok(merged) => {
let faces_after = solid_face_count(&merged);
if faces_after < faces_before {
messages.push(format!(
"coplanar faceted merge reduced faces from {faces_before} to {faces_after} (tolerance {merge_tolerance:.3e})"
));
(
merged,
ConversionBackend::FacetedRepairCoplanarMerged,
"the complete source triangle set was repaired as a faceted BREP, then adjacent coplanar facets were merged without introducing curved replacement topology".to_owned(),
None,
None,
Some(faces_before),
Some(faces_after),
)
} else {
messages.push(format!(
"coplanar faceted merge found no mergeable faces (tolerance {merge_tolerance:.3e})"
));
(
faceted,
ConversionBackend::FacetedRepair,
"the complete source triangle set was repaired and preserved as a faceted BREP; no adjacent coplanar facets could be merged".to_owned(),
None,
None,
Some(faces_before),
Some(faces_before),
)
}
}
Err(error) => {
messages.push(format!(
"coplanar faceted merge was unavailable; retained the validated repaired mesh: {error}"
));
(
faceted,
ConversionBackend::FacetedRepair,
"the complete source triangle set was repaired and preserved as a faceted BREP; the optional coplanar merge was unavailable".to_owned(),
None,
None,
Some(faces_before),
Some(faces_before),
)
}
}
}
}
};
let topology_build_seconds = topology_started.elapsed().as_secs_f64();
let topology_issues = solid.validate();
if !topology_issues.is_empty() {
return Err(StlConversionError(format!(
"constructed BREP failed topology validation: {topology_issues:?}"
)));
}
let faces = solid_face_count(&solid);
let detail = ComponentDetail {
backend,
backend_reason,
fallback_cause,
input_vertices,
input_triangles,
recognized_regions: recognition.regions.len(),
recognized_triangles,
unresolved_triangles: recognition.unresolved_triangles.len(),
recognition_skipped: recognition_gate.err(),
source_closed_manifold,
analytic_used_repaired_mesh: repaired.is_some(),
requested_distance_tolerance,
effective_distance_tolerance,
region_type_counts,
regions,
topology_refit,
hybrid_rebuild,
hybrid_demotions,
faceted_faces_before_merge,
faceted_faces_after_merge,
faces,
segmentation_seconds,
recognition_seconds,
recognition_work,
whole_carrier_scan,
topology_build_seconds,
messages,
refusal: None,
};
Ok((solid, detail))
}
fn aggregate_report(
components: &[Vec<usize>],
details: Vec<ComponentDetail>,
count: usize,
options: &StlConversionOptions,
) -> StlConversionReport {
let mut messages = Vec::new();
if count > 1 {
messages.push(format!(
"the triangle soup holds {count} vertex-connected components; each shell was reconstructed independently before cavity assembly"
));
}
let refused = details
.iter()
.enumerate()
.filter_map(|(index, detail)| detail.refusal.as_ref().map(|reason| (index, reason)))
.collect::<Vec<_>>();
if !refused.is_empty() {
messages.push(format!(
"{} of {count} bodies could not be reconstructed and are NOT in the model: {}",
refused.len(),
refused
.iter()
.map(|(index, reason)| format!(
"body {} (from source triangle {}): {reason}",
index + 1,
components[*index][0]
))
.collect::<Vec<_>>()
.join("; ")
));
}
for (index, detail) in details.iter().enumerate() {
for message in &detail.messages {
messages.push(if count > 1 {
format!("body {}: {message}", index + 1)
} else {
message.clone()
});
}
}
let first = details
.iter()
.find(|detail| detail.refusal.is_none())
.unwrap_or(&details[0]);
let backend_reason = if count == 1 {
first.backend_reason.clone()
} else if details
.iter()
.all(|detail| detail.backend == first.backend)
{
format!(
"{count} components were reconstructed separately; each: {}",
first.backend_reason
)
} else {
format!(
"{count} components were reconstructed separately: {}",
details
.iter()
.enumerate()
.map(|(index, detail)| format!("body {} — {}", index + 1, detail.backend_reason))
.collect::<Vec<_>>()
.join("; ")
)
};
let fallback_cause = details
.iter()
.enumerate()
.find_map(|(index, detail)| {
detail.fallback_cause.as_ref().map(|cause| {
if count > 1 {
format!("body {}: {cause}", index + 1)
} else {
cause.clone()
}
})
});
let recognition_skipped = details
.iter()
.enumerate()
.map(|(index, detail)| {
detail
.recognition_skipped
.as_ref()
.map(|reason| (index, reason))
})
.collect::<Option<Vec<_>>>()
.map(|skipped| {
if count == 1 || skipped.iter().all(|(_, reason)| *reason == skipped[0].1) {
skipped[0].1.clone()
} else {
skipped
.iter()
.map(|(index, reason)| format!("body {}: {reason}", index + 1))
.collect::<Vec<_>>()
.join("; ")
}
});
let mut region_type_counts = BTreeMap::new();
for detail in &details {
for (name, count) in &detail.region_type_counts {
*region_type_counts.entry(name.clone()).or_insert(0) += count;
}
}
let hybrid_rebuild = details
.iter()
.any(|detail| detail.hybrid_rebuild.is_some())
.then(|| {
details
.iter()
.filter_map(|detail| detail.hybrid_rebuild)
.fold(HybridConversionReport::default(), add_hybrid_reports)
});
let faceted_faces_before_merge = sum_optional(&details, |detail| detail.faceted_faces_before_merge);
let faceted_faces_after_merge = sum_optional(&details, |detail| detail.faceted_faces_after_merge);
let component_reports = components
.iter()
.zip(&details)
.enumerate()
.map(|(index, (triangles, detail))| ComponentConversionReport {
index,
first_triangle: triangles[0],
input_vertices: detail.input_vertices,
input_triangles: detail.input_triangles,
backend: detail.backend,
backend_reason: detail.backend_reason.clone(),
fallback_cause: detail.fallback_cause.clone(),
recognition_skipped: detail.recognition_skipped.clone(),
recognized_regions: detail.recognized_regions,
recognized_triangles: detail.recognized_triangles,
unresolved_triangles: detail.unresolved_triangles,
source_closed_manifold: detail.source_closed_manifold,
analytic_used_repaired_mesh: detail.analytic_used_repaired_mesh,
hybrid_rebuild: detail.hybrid_rebuild,
hybrid_demotions: detail.hybrid_demotions.clone(),
faceted_faces_before_merge: detail.faceted_faces_before_merge,
faceted_faces_after_merge: detail.faceted_faces_after_merge,
faces: detail.faces,
refusal: detail.refusal.clone(),
source_name: None,
})
.collect();
StlConversionReport {
backend: first.backend,
backend_reason,
fallback_cause,
input_vertices: details.iter().map(|detail| detail.input_vertices).sum(),
input_triangles: details.iter().map(|detail| detail.input_triangles).sum(),
recognized_regions: details.iter().map(|detail| detail.recognized_regions).sum(),
recognized_triangles: details
.iter()
.map(|detail| detail.recognized_triangles)
.sum(),
unresolved_triangles: details
.iter()
.map(|detail| detail.unresolved_triangles)
.sum(),
recognition_skipped,
source_closed_manifold: details.iter().all(|detail| detail.source_closed_manifold),
analytic_used_repaired_mesh: details
.iter()
.any(|detail| detail.analytic_used_repaired_mesh),
requested_distance_tolerance: first.requested_distance_tolerance,
coordinate_precision_tolerance: options.coordinate_precision_tolerance,
effective_distance_tolerance: first.effective_distance_tolerance,
region_type_counts,
regions: details
.iter()
.flat_map(|detail| detail.regions.iter().cloned())
.collect(),
topology_refit: details
.iter()
.find_map(|detail| detail.topology_refit.clone()),
hybrid_rebuild,
hybrid_demotions: details
.iter()
.flat_map(|detail| detail.hybrid_demotions.iter().cloned())
.collect(),
components: component_reports,
exported_step_bytes: 0,
exported_advanced_faces: 0,
faceted_faces_before_merge,
faceted_faces_after_merge,
roundtrip_solids: 0,
manifold_audit_issues: Vec::new(),
timings: ConversionTimings {
segmentation_seconds: details
.iter()
.map(|detail| detail.segmentation_seconds)
.sum(),
recognition_seconds: details
.iter()
.map(|detail| detail.recognition_seconds)
.sum(),
topology_build_seconds: details
.iter()
.map(|detail| detail.topology_build_seconds)
.sum(),
step_export_seconds: 0.0,
step_validation_seconds: 0.0,
total_seconds: 0.0,
},
recognition_work: details.iter().fold(RecognitionWorkReport::default(), |sum, detail| {
RecognitionWorkReport {
hypotheses: sum.hypotheses + detail.recognition_work.hypotheses,
rounds: sum.rounds + detail.recognition_work.rounds,
seeded_components: sum.seeded_components + detail.recognition_work.seeded_components,
}
}),
whole_carrier_scan: details
.iter()
.fold(WholeCarrierScanReport::default(), |sum, detail| sum.sum(detail.whole_carrier_scan)),
messages,
}
}
fn sum_optional(
details: &[ComponentDetail],
read: impl Fn(&ComponentDetail) -> Option<usize>,
) -> Option<usize> {
details
.iter()
.any(|detail| read(detail).is_some())
.then(|| details.iter().filter_map(read).sum())
}
fn add_hybrid_reports(
total: HybridConversionReport,
body: HybridConversionReport,
) -> HybridConversionReport {
HybridConversionReport {
total_faces: total.total_faces + body.total_faces,
analytic_plane_faces: total.analytic_plane_faces + body.analytic_plane_faces,
analytic_plane_triangles: total.analytic_plane_triangles + body.analytic_plane_triangles,
analytic_cylinder_faces: total.analytic_cylinder_faces + body.analytic_cylinder_faces,
analytic_cylinder_triangles: total.analytic_cylinder_triangles
+ body.analytic_cylinder_triangles,
analytic_cone_faces: total.analytic_cone_faces + body.analytic_cone_faces,
analytic_cone_triangles: total.analytic_cone_triangles + body.analytic_cone_triangles,
analytic_sphere_faces: total.analytic_sphere_faces + body.analytic_sphere_faces,
analytic_sphere_triangles: total.analytic_sphere_triangles + body.analytic_sphere_triangles,
analytic_torus_faces: total.analytic_torus_faces + body.analytic_torus_faces,
analytic_torus_triangles: total.analytic_torus_triangles + body.analytic_torus_triangles,
faceted_faces: total.faceted_faces + body.faceted_faces,
faceted_triangles: total.faceted_triangles + body.faceted_triangles,
demoted_regions: total.demoted_regions + body.demoted_regions,
demoted_triangles: total.demoted_triangles + body.demoted_triangles,
segmentation_plane_regions: total.segmentation_plane_regions
+ body.segmentation_plane_regions,
segmentation_cylinder_regions: total.segmentation_cylinder_regions
+ body.segmentation_cylinder_regions,
segmentation_cone_regions: total.segmentation_cone_regions + body.segmentation_cone_regions,
segmentation_sphere_regions: total.segmentation_sphere_regions
+ body.segmentation_sphere_regions,
segmentation_torus_regions: total.segmentation_torus_regions
+ body.segmentation_torus_regions,
segmentation_unsupported_regions: total.segmentation_unsupported_regions
+ body.segmentation_unsupported_regions,
}
}
fn triangle_components(mesh: &Mesh) -> Vec<Vec<usize>> {
fn find(parent: &mut [usize], mut node: usize) -> usize {
while parent[node] != node {
parent[node] = parent[parent[node]];
node = parent[node];
}
node
}
let bits = |value: f64| if value == 0.0 { 0_u64 } else { value.to_bits() };
let mut position_ids = HashMap::<[u64; 3], usize>::new();
let mut vertex_position = Vec::with_capacity(mesh.vertices.len());
for point in &mesh.vertices {
let key = [bits(point.x), bits(point.y), bits(point.z)];
let next = position_ids.len();
vertex_position.push(*position_ids.entry(key).or_insert(next));
}
let mut parent = (0..position_ids.len()).collect::<Vec<_>>();
for triangle in &mesh.triangles {
let first = vertex_position[triangle[0] as usize];
for &corner in &triangle[1..] {
let (left, right) = (
find(&mut parent, first),
find(&mut parent, vertex_position[corner as usize]),
);
if left != right {
parent[left] = right;
}
}
}
let mut slots = HashMap::<usize, usize>::new();
let mut components = Vec::<Vec<usize>>::new();
for (index, triangle) in mesh.triangles.iter().enumerate() {
let root = find(&mut parent, vertex_position[triangle[0] as usize]);
let slot = match slots.get(&root) {
Some(&slot) => slot,
None => {
components.push(Vec::new());
slots.insert(root, components.len() - 1);
components.len() - 1
}
};
components[slot].push(index);
}
components
}
struct ComponentMesh {
mesh: Mesh,
positions: Vec<f64>,
indices: Option<Vec<u32>>,
}
fn component_mesh(
mesh: &Mesh,
positions: &[f64],
indices: Option<&[u32]>,
triangles: &[usize],
) -> ComponentMesh {
let mut vertex_map = HashMap::<u32, u32>::new();
let mut vertices = Vec::new();
let mut component_triangles = Vec::with_capacity(triangles.len());
for &triangle in triangles {
let mut corners = [0_u32; 3];
for (corner, &index) in mesh.triangles[triangle].iter().enumerate() {
corners[corner] = *vertex_map.entry(index).or_insert_with(|| {
vertices.push(mesh.vertices[index as usize]);
vertices.len() as u32 - 1
});
}
component_triangles.push(corners);
}
let mut component = Mesh::new(vertices, component_triangles);
if let Some(normals) = &mesh.vertex_normals {
if normals.len() == mesh.vertices.len() {
let mut component_normals = vec![Vec3::default(); component.vertices.len()];
for (&source, &target) in &vertex_map {
component_normals[target as usize] = normals[source as usize];
}
component.vertex_normals = Some(component_normals);
}
}
if !mesh.source_metadata.is_empty() {
let local = triangles
.iter()
.enumerate()
.map(|(local, &source)| (source, local))
.collect::<HashMap<_, _>>();
component.source_metadata = mesh
.source_metadata
.iter()
.filter_map(|metadata| {
let triangle_indices = metadata
.triangle_indices
.iter()
.filter_map(|source| local.get(source).copied())
.collect::<Vec<_>>();
(!triangle_indices.is_empty()).then(|| crate::SourceMetadata {
triangle_indices,
..metadata.clone()
})
})
.collect();
}
let (component_positions, component_indices) = match indices {
Some(indices) => {
let mut source_map = HashMap::<u32, u32>::new();
let mut component_positions = Vec::new();
let mut component_indices = Vec::with_capacity(triangles.len() * 3);
for &triangle in triangles {
for &index in &indices[triangle * 3..triangle * 3 + 3] {
let mapped = *source_map.entry(index).or_insert_with(|| {
let base = index as usize * 3;
component_positions.extend_from_slice(&positions[base..base + 3]);
component_positions.len() as u32 / 3 - 1
});
component_indices.push(mapped);
}
}
(component_positions, Some(component_indices))
}
None => {
let mut component_positions = Vec::with_capacity(triangles.len() * 9);
for &triangle in triangles {
component_positions.extend_from_slice(&positions[triangle * 9..triangle * 9 + 9]);
}
(component_positions, None)
}
};
ComponentMesh {
mesh: component,
positions: component_positions,
indices: component_indices,
}
}
fn validate_source_buffers(
mesh: &Mesh,
positions: &[f64],
indices: Option<&[u32]>,
) -> Result<(), StlConversionError> {
if positions.is_empty() || !positions.len().is_multiple_of(3) {
return Err(StlConversionError(
"source position buffer must contain complete non-empty xyz triples".to_owned(),
));
}
if positions.iter().any(|value| !value.is_finite()) {
return Err(StlConversionError(
"source position buffer contains a non-finite coordinate".to_owned(),
));
}
let source_triangles = match indices {
Some(indices) => {
if !indices.len().is_multiple_of(3) {
return Err(StlConversionError(
"source index buffer must contain complete triangles".to_owned(),
));
}
if indices
.iter()
.any(|&index| index as usize >= positions.len() / 3)
{
return Err(StlConversionError(
"source index buffer references a missing position".to_owned(),
));
}
indices.len() / 3
}
None => {
if !positions.len().is_multiple_of(9) {
return Err(StlConversionError(
"unindexed source positions must be a triangle soup".to_owned(),
));
}
positions.len() / 9
}
};
if source_triangles != mesh.triangles.len() {
return Err(StlConversionError(format!(
"source buffers contain {source_triangles} triangles but recognition mesh contains {}",
mesh.triangles.len()
)));
}
Ok(())
}
fn complete_small_planar_regions(
mesh: &Mesh,
options: &RecognitionOptions,
regions: &mut Vec<SurfaceRegion>,
unresolved: &mut Vec<usize>,
) -> Result<usize, StlConversionError> {
if unresolved.len() < 2 {
return Ok(0);
}
let analyzed = mesh
.analyze(&MeshAnalysisOptions {
feature_angle: options.feature_angle,
..MeshAnalysisOptions::default()
})
.map_err(|error| {
StlConversionError(format!("plane completion analysis failed: {error}"))
})?;
let allowed = unresolved.iter().copied().collect::<BTreeSet<_>>();
let mut visited = BTreeSet::new();
let mut accepted = BTreeSet::new();
let mut completed = 0;
let mut plane_options = options.clone();
plane_options.minimum_support = 2;
for &seed in unresolved.iter() {
if !visited.insert(seed) {
continue;
}
let mut component = Vec::new();
let mut queue = VecDeque::from([seed]);
while let Some(triangle) = queue.pop_front() {
component.push(triangle);
let data = &analyzed.triangles[triangle];
for edge in 0..3 {
if data.feature_edges[edge] {
continue;
}
if let Some(neighbor) = data.neighbors[edge] {
if allowed.contains(&neighbor) && visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
}
component.sort_unstable();
if component.len() < 2 {
continue;
}
let Ok(fit) = reconstruct_surface(
mesh,
&component,
&SurfaceHint::KnownType {
surface_type: SurfaceType::Plane,
},
&plane_options,
) else {
continue;
};
if fit.metrics.support_triangles != component.len()
|| !matches!(fit.surface, AnalyticSurface::Plane(_))
{
continue;
}
accepted.extend(component.iter().copied());
regions.push(SurfaceRegion {
surface: fit.surface,
orientation: fit.orientation,
triangle_indices: component,
metrics: fit.metrics,
confidence: fit.confidence,
diagnostics: fit.diagnostics,
});
completed += 1;
}
unresolved.retain(|triangle| !accepted.contains(triangle));
Ok(completed)
}
fn region_report(region: &SurfaceRegion) -> ConversionRegionReport {
fit_fields(
region.surface,
region.orientation,
®ion.metrics,
region.confidence,
®ion.diagnostics,
)
}
fn refit_report(fit: &SurfaceFitResult) -> ConversionRegionReport {
fit_fields(
fit.surface,
fit.orientation,
&fit.metrics,
fit.confidence,
&fit.diagnostics,
)
}
fn fit_fields(
surface: AnalyticSurface,
orientation: i8,
metrics: &crate::FitMetrics,
confidence: f64,
diagnostics: &crate::FitDiagnostics,
) -> ConversionRegionReport {
ConversionRegionReport {
surface_type: surface.surface_type(),
surface,
support_triangles: metrics.support_triangles,
supported_area: metrics.supported_area,
rms_error: metrics.rms_error,
max_error: metrics.max_error,
rms_normal_error_radians: metrics.rms_normal_error,
max_normal_error_radians: metrics.max_normal_error,
confidence,
orientation,
reason: diagnostics.reason.clone(),
phase_timings: diagnostics.phase_timings,
}
}
fn disjoint_complete_partition(regions: &[SurfaceRegion], triangle_count: usize) -> bool {
let mut seen = BTreeSet::new();
regions.iter().all(|region| {
region
.triangle_indices
.iter()
.all(|&triangle| triangle < triangle_count && seen.insert(triangle))
}) && seen.len() == triangle_count
}
pub(crate) struct RepairedMesh {
mesh: Mesh,
pub(crate) positions: Vec<f64>,
pub(crate) indices: Vec<u32>,
message: String,
pub(crate) stitched_edges: usize,
}
pub(crate) fn repaired_closed_mesh(
source_positions: &[f64],
source_indices: Option<&[u32]>,
options: &StlConversionOptions,
) -> Option<RepairedMesh> {
let owned_indices;
let stitched_indices = match source_indices {
Some(indices) => indices,
None => {
owned_indices = (0..source_positions.len() as u32 / 3).collect::<Vec<_>>();
&owned_indices
}
};
let stitch_tolerance = if options.weld_tolerance > 0.0 {
options.weld_tolerance
} else {
mesh_diagonal_of(source_positions) * crate::stl::DEFAULT_RELATIVE_WELD_TOLERANCE
};
let (stitched, stitch) =
t_junctions::stitch_t_junctions(source_positions, stitched_indices, stitch_tolerance);
let (positions, indices, report) =
repair_triangle_soup(source_positions, Some(&stitched), options.weld_tolerance).ok()?;
let vertices = positions
.chunks_exact(3)
.map(|point| Vec3::new(point[0], point[1], point[2]))
.collect::<Vec<_>>();
let triangles = indices
.chunks_exact(3)
.map(|triangle| [triangle[0], triangle[1], triangle[2]])
.collect::<Vec<_>>();
let mesh = Mesh::new(vertices, triangles);
if !closed_manifold(&mesh) {
return None;
}
let message = format!(
"the source mesh is not a closed two-manifold; the analytic lanes ran on the repaired mesh \
({}{} welded vertices, {} triangles: {} dropped as degenerate or duplicate, {} surplus \
sheet(s) pruned, {} reoriented to agree with their neighbours, {} boundary loop(s) \
capped with {} triangle(s))",
if stitch.stitched_edges > 0 {
format!(
"{} T-junction edge(s) split at a vertex on their interior, {} triangle(s) added; ",
stitch.stitched_edges, stitch.added_triangles
)
} else {
String::new()
},
report.welded_vertices,
report.triangles,
report.dropped_triangles,
report.pruned_triangles,
report.reoriented_triangles,
report.capped_loops,
report.cap_triangles,
);
Some(RepairedMesh {
mesh,
positions,
indices,
message,
stitched_edges: stitch.stitched_edges,
})
}
fn mesh_diagonal_of(positions: &[f64]) -> f64 {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
for point in positions.chunks_exact(3) {
for axis in 0..3 {
low[axis] = low[axis].min(point[axis]);
high[axis] = high[axis].max(point[axis]);
}
}
(0..3).map(|axis| (high[axis] - low[axis]).powi(2)).sum::<f64>().sqrt()
}
fn closed_manifold(mesh: &Mesh) -> bool {
closed_triangles(mesh.triangles.iter().copied())
}
fn closed_triangles(triangles: impl Iterator<Item = [u32; 3]>) -> bool {
let mut uses = BTreeMap::<(u32, u32), (usize, i32)>::new();
for triangle in triangles {
if triangle[0] == triangle[1] || triangle[1] == triangle[2] || triangle[2] == triangle[0] {
return false;
}
for edge in [
(triangle[0], triangle[1]),
(triangle[1], triangle[2]),
(triangle[2], triangle[0]),
] {
let key = if edge.0 < edge.1 {
(edge.0, edge.1)
} else {
(edge.1, edge.0)
};
let entry = uses.entry(key).or_default();
entry.0 += 1;
entry.1 += if edge == key { 1 } else { -1 };
}
}
!uses.is_empty()
&& uses
.values()
.all(|&(count, sense)| count == 2 && sense == 0)
}
struct DirectAnalyticBuild {
solid: Result<brep_kernel::BrepSolid, StlConversionError>,
backend: ConversionBackend,
reason: String,
topology_refit: Option<ConversionRegionReport>,
}
fn direct_analytic_solid(
mesh: &Mesh,
regions: &[SurfaceRegion],
options: &RecognitionOptions,
) -> Option<DirectAnalyticBuild> {
if regions.len() == 1 {
return match regions[0].surface {
AnalyticSurface::Sphere(sphere) => Some(DirectAnalyticBuild {
solid: make_sphere_brep(
kernel_vec(sphere.center),
sphere.radius,
kernel_vec(Vec3::new(0.0, 0.0, 1.0)),
)
.map_err(StlConversionError),
backend: ConversionBackend::RansacSphere,
reason: "RANSAC proved complete closed spherical coverage; exported an exact analytic sphere".to_owned(),
topology_refit: None,
}),
AnalyticSurface::Torus(torus) if torus.major_radius > torus.minor_radius => Some(DirectAnalyticBuild {
solid: make_torus_brep(
kernel_vec(torus.center),
kernel_vec(torus.axis),
torus.major_radius,
torus.minor_radius,
)
.map_err(StlConversionError),
backend: ConversionBackend::RansacTorus,
reason: "RANSAC proved complete closed ring-torus coverage; exported an exact analytic torus".to_owned(),
topology_refit: None,
}),
_ => None,
};
}
let curved = regions
.iter()
.filter(|region| !matches!(region.surface, AnalyticSurface::Plane(_)))
.collect::<Vec<_>>();
let planes = regions
.iter()
.filter_map(|region| match region.surface {
AnalyticSurface::Plane(plane) => Some(plane),
_ => None,
})
.collect::<Vec<_>>();
if curved.len() != 1 {
return None;
}
let topology_refit;
let mut topology_refit_report = None;
let curved = if let AnalyticSurface::Torus(torus) = curved[0].surface {
let scale = mesh_diagonal(mesh);
let collapsed = torus.major_radius.min(torus.minor_radius) > 20.0 * scale;
let forced_type = match planes.len() {
2 if collapsed => Some(SurfaceType::Cylinder),
1 if collapsed => Some(SurfaceType::Cone),
_ => None,
};
if let Some(surface_type) = forced_type {
topology_refit = reconstruct_surface(
mesh,
&curved[0].triangle_indices,
&SurfaceHint::KnownType { surface_type },
options,
)
.ok()?;
topology_refit_report = Some(refit_report(&topology_refit));
&topology_refit.surface
} else {
&curved[0].surface
}
} else {
&curved[0].surface
};
match *curved {
AnalyticSurface::Cylinder(cylinder) if planes.len() == 2 => {
let (low, high) = axial_range(mesh, cylinder.axis_origin, cylinder.axis)?;
let tolerance = conversion_tolerance(mesh, options);
if high - low <= tolerance
|| !caps_match(
&planes,
cylinder.axis_origin,
cylinder.axis,
&[low, high],
tolerance,
)
{
return None;
}
let base = cylinder.axis_origin + cylinder.axis * low;
Some(DirectAnalyticBuild {
solid: make_cylinder_brep(
kernel_vec(base),
kernel_vec(cylinder.axis),
cylinder.radius,
high - low,
)
.map_err(StlConversionError),
backend: ConversionBackend::RansacCappedCylinder,
reason: if topology_refit_report.is_some() {
"two-cap topology disambiguated a large-radius torus limit; the production known-type cylinder refit passed all gates and supplied the exact capped-cylinder parameters".to_owned()
} else {
"RANSAC proved one cylindrical wall plus two matching planar caps; exported an exact capped cylinder".to_owned()
},
topology_refit: topology_refit_report,
})
}
AnalyticSurface::Cone(cone) if planes.len() == 1 || planes.len() == 2 => {
let (low, high) = axial_range(mesh, cone.apex, cone.axis)?;
let tolerance = conversion_tolerance(mesh, options);
if low < -tolerance || high - low <= tolerance {
return None;
}
let expected_caps = if low <= tolerance {
vec![high]
} else {
vec![low, high]
};
if planes.len() != expected_caps.len()
|| !caps_match(&planes, cone.apex, cone.axis, &expected_caps, tolerance)
{
return None;
}
let radius_bottom = low.max(0.0) * cone.half_angle.tan();
let radius_top = high * cone.half_angle.tan();
if radius_top <= tolerance || (low > tolerance && radius_bottom <= tolerance) {
return None;
}
let (base, build_axis, build_bottom, build_top) = if low <= tolerance {
(
cone.apex + cone.axis * high,
cone.axis * -1.0,
radius_top,
0.0,
)
} else {
(
cone.apex + cone.axis * low,
cone.axis,
radius_bottom,
radius_top,
)
};
Some(DirectAnalyticBuild {
solid: make_cone_brep(
kernel_vec(base),
kernel_vec(build_axis),
build_bottom,
build_top,
high - low.max(0.0),
)
.map_err(StlConversionError),
backend: ConversionBackend::RansacCappedCone,
reason: if topology_refit_report.is_some() {
"cone-cap topology disambiguated a large-radius torus limit; the production known-type cone refit passed all gates and supplied the exact capped-cone parameters".to_owned()
} else {
"RANSAC proved one conical wall and matching planar cap topology; exported an exact capped cone".to_owned()
},
topology_refit: topology_refit_report,
})
}
_ => None,
}
}
fn axial_range(mesh: &Mesh, origin: Vec3, axis: Vec3) -> Option<(f64, f64)> {
let mut low = f64::INFINITY;
let mut high = f64::NEG_INFINITY;
for &point in &mesh.vertices {
let height = (point - origin).dot(axis);
low = low.min(height);
high = high.max(height);
}
(low.is_finite() && high.is_finite()).then_some((low, high))
}
fn caps_match(
planes: &[crate::PlaneSurface],
origin: Vec3,
axis: Vec3,
expected_heights: &[f64],
tolerance: f64,
) -> bool {
let mut actual = Vec::with_capacity(planes.len());
for plane in planes {
if plane.normal.dot(axis).abs() < 1.0 - 1.0e-6 {
return false;
}
actual.push((plane.origin - origin).dot(axis));
}
actual.sort_by(f64::total_cmp);
let mut expected = expected_heights.to_vec();
expected.sort_by(f64::total_cmp);
actual.len() == expected.len()
&& actual
.iter()
.zip(expected)
.all(|(actual, expected)| (*actual - expected).abs() <= tolerance)
}
fn mesh_tolerance(mesh: &Mesh) -> f64 {
let mut low = mesh.vertices[0];
let mut high = mesh.vertices[0];
for &point in &mesh.vertices[1..] {
low.x = low.x.min(point.x);
low.y = low.y.min(point.y);
low.z = low.z.min(point.z);
high.x = high.x.max(point.x);
high.y = high.y.max(point.y);
high.z = high.z.max(point.z);
}
(high - low).length().max(1.0) * 1.0e-5
}
fn conversion_tolerance(mesh: &Mesh, options: &RecognitionOptions) -> f64 {
mesh_tolerance(mesh)
.max(options.distance_tolerance + options.relative_tolerance * mesh_diagonal(mesh))
}
fn mesh_diagonal(mesh: &Mesh) -> f64 {
let mut low = mesh.vertices[0];
let mut high = mesh.vertices[0];
for &point in &mesh.vertices[1..] {
low.x = low.x.min(point.x);
low.y = low.y.min(point.y);
low.z = low.z.min(point.z);
high.x = high.x.max(point.x);
high.y = high.y.max(point.y);
high.z = high.z.max(point.z);
}
(high - low).length()
}
fn coplanar_merge_tolerance(mesh: &Mesh) -> f64 {
(mesh_diagonal(mesh) * 1.0e-9).max(1.0e-12)
}
fn solid_face_count(solid: &brep_kernel::BrepSolid) -> usize {
solid.shells.iter().map(|shell| shell.faces.len()).sum()
}
const MAX_WHOLE_PRIMITIVE_REGIONS: usize = 3;
pub(crate) fn segmentation_options(positions: &[f64], options: &StlConversionOptions) -> SegmentOptions {
SegmentOptions {
deflection_angle_deg: options.kernel_deflection_angle_degrees,
fit_tolerance: options.kernel_fit_tolerance,
normal_tolerance_deg: options.kernel_normal_tolerance_degrees,
weld_tolerance: if options.weld_tolerance > 0.0 {
options.weld_tolerance
} else {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
for point in positions.chunks_exact(3) {
for axis in 0..3 {
low[axis] = low[axis].min(point[axis]);
high[axis] = high[axis].max(point[axis]);
}
}
let diagonal = (0..3)
.map(|axis| (high[axis] - low[axis]).powi(2))
.sum::<f64>()
.sqrt();
(diagonal * crate::stl::DEFAULT_RELATIVE_WELD_TOLERANCE).max(f64::MIN_POSITIVE)
},
..SegmentOptions::default()
}
}
fn hybrid_roundtrip_fallback_enabled() -> bool {
brep_kernel::switch!(live "BREP_HYBRID_ROUNDTRIP_FALLBACK", true)
}
fn solid_round_trips(solid: &brep_kernel::BrepSolid) -> Result<(), String> {
let text = export_step(std::slice::from_ref(solid), "round-trip", "millimeter", "2026-01-01T00:00:00Z")
.map_err(|error| format!("export: {error}"))?;
let imported = import_step(&text).map_err(|error| error.to_string())?;
match imported.iter().map(|solid| solid.validate()).find(|issues| !issues.is_empty()) {
Some(issues) => Err(format!("imported solid invalid: {issues:?}")),
None => Ok(()),
}
}
fn tjunction_strip_hybrid_enabled() -> bool {
brep_kernel::switch!(live "BREP_TJUNCTION_STRIP_HYBRID", false)
}
fn hybrid_over_kernel_strips(
kernel: &brep_kernel::BrepSolid,
positions: &[f64],
indices: Option<&[u32]>,
options: &StlConversionOptions,
) -> Option<crate::hybrid_region_brep::HybridBrepOutput> {
let output = try_hybrid_analytic(positions, indices, options).ok()?;
if hybrid_report(&output.stats).faceted_faces != 0
|| solid_face_count(&output.solid) >= solid_face_count(kernel)
{
return None;
}
let (before, after) = (
brep_kernel::solid_signed_volume(kernel).ok()?,
brep_kernel::solid_signed_volume(&output.solid).ok()?,
);
if !((after - before).abs() <= 1e-2 * before.abs()) {
return None;
}
solid_round_trips(&output.solid).ok()?;
Some(output)
}
fn segment_carrier_enabled() -> bool {
brep_kernel::switch!(live "BREP_RECOG_SEGMENT_CARRIER", true)
}
fn segmentation_candidates(mesh: &Mesh, segmentation: &MeshSegmentation) -> Vec<ComponentCandidate> {
if segmentation.triangle_count != mesh.triangles.len()
|| segmentation.triangle_region_ids.len() != mesh.triangles.len()
{
return Vec::new();
}
let mut members = vec![Vec::new(); segmentation.regions.len()];
for (triangle, ®ion) in segmentation.triangle_region_ids.iter().enumerate() {
if let Some(list) = members.get_mut(region as usize) {
list.push(triangle);
}
}
segmentation
.regions
.iter()
.zip(members)
.filter_map(|(region, triangles)| {
let (surface, parameters) = carrier_surface(®ion.carrier)?;
if triangles.is_empty() || !surface.is_valid() {
return None;
}
let indices: Vec<u32> = triangles.iter().flat_map(|&t| mesh.triangles[t]).collect();
let (statistic, threshold) = crate::hybrid_whole_freeform::carrier_local_scan(
&indices,
|v| surface.signed_distance(mesh.vertices[v as usize]),
parameters,
);
if std::env::var_os("BREP_DEBUG_SPLIT").is_some() {
eprintln!(
"[candidate] region {} {} tris {} local {statistic:.2} threshold {threshold:.2} {}",
region.id,
region.carrier.kind(),
triangles.len(),
if statistic <= threshold { "offered" } else { "declined" }
);
}
(statistic <= threshold).then_some(ComponentCandidate { triangle_indices: triangles, surface })
})
.collect()
}
fn whole_fits_pass_the_local_scan(
mesh: &Mesh,
regions: &[SurfaceRegion],
requested: f64,
tally: &mut WholeCarrierScanReport,
) -> bool {
if !crate::hybrid_whole_freeform::whole_carrier_scan_enabled() {
return true;
}
let mut passes = true;
for region in regions.iter().filter(|region| region.metrics.max_error > requested) {
let surface = ®ion.surface;
let indices: Vec<u32> = region
.triangle_indices
.iter()
.flat_map(|&t| mesh.triangles[t])
.collect();
let reading = crate::hybrid_whole_freeform::carrier_scan_verdict(
&indices,
|v| surface.signed_distance(mesh.vertices[v as usize]),
analytic_parameters(surface),
);
if std::env::var_os("BREP_DEBUG_SPLIT").is_some() {
eprintln!(
"[whole-fit] {} tris {} local {:.2} threshold {:.2} independence {:.3} bound {:.3} edges {} {:?}",
surface.surface_type().name(),
region.triangle_indices.len(),
reading.statistic,
reading.threshold,
reading.independence,
reading.bound,
reading.outside_edges,
reading.verdict
);
}
tally.add(reading.verdict);
if reading.verdict == crate::hybrid_whole_freeform::ScanVerdict::Declined {
passes = false;
break;
}
}
passes
}
fn analytic_parameters(surface: &AnalyticSurface) -> usize {
match surface {
AnalyticSurface::Plane(_) => 3,
AnalyticSurface::Cylinder(_) => 5,
AnalyticSurface::Cone(_) => 6,
AnalyticSurface::Sphere(_) => 4,
AnalyticSurface::Torus(_) => 7,
}
}
fn carrier_surface(carrier: &RegionCarrier) -> Option<(AnalyticSurface, usize)> {
let v = |p: brep_kernel::Vec3| crate::brep::vec3_from_kernel(p);
Some(match *carrier {
RegionCarrier::Plane { origin, normal } => {
(AnalyticSurface::Plane(PlaneSurface { origin: v(origin), normal: v(normal) }), 3)
}
RegionCarrier::Cylinder { axis_point, axis_dir, radius, .. } => (
AnalyticSurface::Cylinder(CylinderSurface { axis_origin: v(axis_point), axis: v(axis_dir), radius }),
5,
),
RegionCarrier::Cone { apex, axis_dir, half_angle_rad, .. } => (
AnalyticSurface::Cone(ConeSurface { apex: v(apex), axis: v(axis_dir), half_angle: half_angle_rad }),
6,
),
RegionCarrier::Sphere { center, radius, .. } => {
(AnalyticSurface::Sphere(SphereSurface { center: v(center), radius }), 4)
}
RegionCarrier::Torus { center, axis_dir, major_radius, minor_radius, .. } => (
AnalyticSurface::Torus(TorusSurface { center: v(center), axis: v(axis_dir), major_radius, minor_radius }),
7,
),
RegionCarrier::Freeform => return None,
})
}
fn gate_segmentation(
positions: &[f64],
indices: Option<&[u32]>,
options: &StlConversionOptions,
) -> Result<MeshSegmentation, String> {
let owned_indices;
let indices = match indices {
Some(indices) => indices,
None => {
owned_indices = (0..positions.len() as u32 / 3).collect::<Vec<_>>();
&owned_indices
}
};
segment_mesh_faces(positions, indices, &segmentation_options(positions, options))
.map_err(|error| format!("kernel segmentation failed: {error}"))
}
fn segmentation_is_complete(segmentation: &MeshSegmentation) -> bool {
!segmentation
.triangle_region_ids
.contains(&UNASSIGNED_REGION)
&& segmentation
.regions
.iter()
.all(|region| !matches!(region.carrier, RegionCarrier::Freeform))
}
fn recognition_gate(segmentation: &Result<MeshSegmentation, String>) -> Result<(), String> {
let segmentation = segmentation.as_ref().map_err(Clone::clone)?;
let unassigned = segmentation
.triangle_region_ids
.iter()
.filter(|id| **id == UNASSIGNED_REGION)
.count();
let unsupported = segmentation
.regions
.iter()
.filter(|region| matches!(region.carrier, RegionCarrier::Freeform))
.count();
if unassigned > 0 || unsupported > 0 {
return Err(format!(
"kernel segmentation left {unassigned} triangle(s) unassigned and {unsupported} region(s) without an analytic carrier, so no whole-primitive lane can fire"
));
}
if segmentation.regions.len() > MAX_WHOLE_PRIMITIVE_REGIONS {
return Err(format!(
"kernel segmentation found {} analytic regions; a whole primitive has at most {MAX_WHOLE_PRIMITIVE_REGIONS}",
segmentation.regions.len()
));
}
Ok(())
}
fn try_kernel_analytic(
positions: &[f64],
indices: Option<&[u32]>,
options: &StlConversionOptions,
) -> Result<brep_kernel::BrepSolid, String> {
let owned_indices;
let indices = match indices {
Some(indices) => indices,
None => {
owned_indices = (0..positions.len() as u32 / 3).collect::<Vec<_>>();
&owned_indices
}
};
let segmentation_options = segmentation_options(positions, options);
let segmentation = segment_mesh_faces(positions, indices, &segmentation_options)?;
if segmentation
.triangle_region_ids
.contains(&UNASSIGNED_REGION)
{
return Err("independent kernel segmentation left triangles unassigned".to_owned());
}
if let Some(region) = segmentation.regions.iter().find(|region| {
matches!(
region.carrier,
RegionCarrier::Freeform | RegionCarrier::Sphere { .. } | RegionCarrier::Torus { .. }
)
}) {
return Err(format!(
"independent kernel segmentation produced unsupported {} region {}",
region.carrier.kind(),
region.id
));
}
mesh_regions_to_brep(positions, indices, &segmentation_options)
}
fn try_hybrid_analytic(
positions: &[f64],
indices: Option<&[u32]>,
options: &StlConversionOptions,
) -> Result<crate::hybrid_region_brep::HybridBrepOutput, String> {
crate::hybrid_region_brep::hybrid_plane_cylinder_brep(
positions,
indices,
&segmentation_options(positions, options),
)
}
fn hybrid_report(stats: &crate::hybrid_region_brep::HybridBrepStats) -> HybridConversionReport {
HybridConversionReport {
total_faces: stats.total_faces,
analytic_plane_faces: stats.analytic_plane_faces,
analytic_plane_triangles: stats.analytic_plane_triangles,
analytic_cylinder_faces: stats.analytic_cylinder_faces,
analytic_cylinder_triangles: stats.analytic_cylinder_triangles,
analytic_cone_faces: stats.analytic_cone_faces,
analytic_cone_triangles: stats.analytic_cone_triangles,
analytic_sphere_faces: stats.analytic_sphere_faces,
analytic_sphere_triangles: stats.analytic_sphere_triangles,
analytic_torus_faces: stats.analytic_torus_faces,
analytic_torus_triangles: stats.analytic_torus_triangles,
faceted_faces: stats.faceted_faces,
faceted_triangles: stats.faceted_triangles,
demoted_regions: stats.demoted_regions,
demoted_triangles: stats.demoted_triangles,
segmentation_plane_regions: stats.segmentation_plane_regions,
segmentation_cylinder_regions: stats.segmentation_cylinder_regions,
segmentation_cone_regions: stats.segmentation_cone_regions,
segmentation_sphere_regions: stats.segmentation_sphere_regions,
segmentation_torus_regions: stats.segmentation_torus_regions,
segmentation_unsupported_regions: stats.segmentation_unsupported_regions,
}
}
fn kernel_vec(value: Vec3) -> brep_kernel::Vec3 {
brep_kernel::Vec3::new(value.x, value.y, value.z)
}