use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{FeatureContext, FeatureResult, SketchProfile};
use crate::sweep_profile_along_chain;
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, String> {
let profile_name = common::normalize_profile_alias(
common::first_reference_name(ctx.param("profile"))
.ok_or("path sweep: missing `profile` reference selection")?,
);
let mut face_profile_storage: Option<SketchProfile> = None;
let (profile, from_face): (&SketchProfile, bool) =
match ctx.scene.resolve_profile(&profile_name) {
Some(profile) => (profile, false),
None => match ctx.scene.resolve_face(&profile_name) {
Some(face) => {
let built = super::face_profile::face_profile(face)
.map_err(|error| format!("path sweep: {error}"))?;
(&*face_profile_storage.insert(built), true)
}
None => {
return Err(format!(
"path sweep: profile '{profile_name}' not found (no sketch profile or resident face)"
));
}
},
};
let first_outer = profile
.regions
.first()
.and_then(|region| region.first())
.ok_or("path sweep: profile has no outer loop")?;
let path_names = common::reference_names(ctx.param("path"));
if path_names.is_empty() {
return Err("path sweep: requires a path edge selection".into());
}
let segments = common::resolve_path_chain(ctx, &path_names)
.map_err(|error| format!("path sweep: {error}"))?;
let path_curves: Vec<_> = segments.iter().map(|segment| segment.curve.clone()).collect();
let path_segment_names: Vec<String> =
segments.iter().map(|segment| segment.name.clone()).collect();
let feature_id = if ctx.id.is_empty() {
"PathSweep"
} else {
ctx.id.as_str()
};
let twist_degrees = common::number_or_default(ctx, "twistAngle", 0.0);
let twist_radians = twist_degrees * std::f64::consts::PI / 180.0;
let anchor = crate::profile_anchor(&first_outer.curves)
.map_err(|error| format!("path sweep: {error}"))?;
let caps = common::CapNames::new(feature_id, &profile.regions);
let mut region_solids = Vec::with_capacity(profile.regions.len());
for (region_index, region) in profile.regions.iter().enumerate() {
let outer = region.first().ok_or("path sweep: profile has no outer loop")?;
if outer.curves.len() < 2 {
return Err(format!(
"path sweep: requires a closed profile with at least two boundary curves, got {}",
outer.curves.len()
));
}
let mut solid = sweep_profile_along_chain(
&outer.curves,
&path_curves,
&path_segment_names,
twist_radians,
Some(feature_id),
(region_index != 0).then_some(anchor),
crate::SectionPlacement::Transplant,
"Use Sweep (SW) with `orientationMode: translate` for a cornered path — it builds one portion per segment, which is what gives a corner a mitre.",
)
.map_err(|error| format!("path sweep: {error}"))?;
let side_count = outer.curves.len();
let mut face_names: Vec<String> = Vec::with_capacity(side_count + 2);
for index in 0..side_count {
let base = outer
.edge_names
.get(index)
.cloned()
.flatten()
.filter(|name| !name.trim().is_empty())
.unwrap_or_else(|| "EDGE".to_string());
face_names.push(format!("{base}_SWP"));
}
let (start, end) = &caps.per_loop[region_index];
face_names.push(start.clone());
face_names.push(end.clone());
let faces = &mut solid
.shells
.get_mut(0)
.ok_or("path sweep: builder produced no shell")?
.faces;
if faces.len() != face_names.len() {
return Err(format!(
"path sweep: builder produced {} faces, expected {} ({} sides + 2 caps)",
faces.len(),
face_names.len(),
side_count
));
}
for (face, name) in faces.iter_mut().zip(&face_names) {
face.name = Some(name.clone());
}
if region.len() > 1 {
solid = common::subtract_region_holes(
solid,
profile,
region,
"path sweep",
feature_id,
None,
&mut |loop_index, _depth| {
let hole_curves = ®ion[loop_index].curves;
sweep_profile_along_chain(
hole_curves,
&path_curves,
&path_segment_names,
twist_radians,
None,
Some(anchor),
crate::SectionPlacement::Transplant,
"Use Sweep (SW) with `orientationMode: translate` for a cornered path — it builds one portion per segment, which is what gives a corner a mitre.",
)
},
)?;
}
region_solids.push(solid);
}
let solid = common::union_region_solids(region_solids)
.map_err(|error| format!("path sweep: {error}"))?;
common::stamp_sweep_roles_multi(&solid, "_SWP", &caps.starts(), &caps.ends());
let mut result = common::finalize_solid_grouped(ctx, solid, feature_id, &caps.containers());
if result.error.is_some() {
return Ok(result);
}
if !from_face {
common::consume_sketch(ctx, &profile_name, &mut result);
}
Ok(result)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.has_profile() || probe.edges > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "SWP",
"shortName": "SWP",
"longName": "Path Sweep",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the path sweep feature"
},
"profile": {
"type": "reference_selection",
"selectionFilter": [
"SKETCH",
"FACE"
],
"multiple": false,
"default_value": null,
"hint": "Select a single closed planar profile (sketch or face) to sweep"
},
"consumeProfileSketch": {
"type": "boolean",
"default_value": true,
"hint": "Remove the referenced sketch after creating the sweep. Turn off to keep it in the scene."
},
"path": {
"type": "reference_selection",
"selectionFilter": [
"SKETCH",
"EDGE"
],
"multiple": true,
"default_value": null,
"hint": "Sweep path: pick a whole sketch, or one or more connected edges — they are chained head-to-tail, with the FIRST pick setting the direction the sweep runs. The joints must be smooth (tangent-continuous); for a path with corners use Sweep (SW)."
},
"twistAngle": {
"type": "number",
"default_value": 0,
"label": "Twist (degrees)",
"hint": "Rotate the profile about the path tangent, linearly in arc length, by this total angle (§5.7). 0 = plain sweep."
},
"boolean": common::optional_boolean_schema()
}
})
}