use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{Axis, FeatureContext, FeatureResult, SketchProfile};
use crate::revolve_profile_brep_named;
use crate::Vec3;
use serde_json::Value;
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 name = common::normalize_profile_alias(
common::first_reference_name(ctx.param("profile"))
.ok_or("revolve: missing `profile` reference selection")?,
);
let mut face_profile_storage: Option<SketchProfile> = None;
let (profile, from_face): (&SketchProfile, bool) = match ctx.scene.resolve_profile(&name) {
Some(profile) => (profile, false),
None => match ctx.scene.resolve_face(&name) {
Some(face) => {
let built = super::face_profile::face_profile(face)
.map_err(|error| format!("revolve: {error}"))?;
(&*face_profile_storage.insert(built), true)
}
None => {
return Err(format!(
"revolve: profile '{name}' not found (no sketch profile or resident face)"
));
}
},
};
let axis_name = common::first_reference_name(ctx.param("axis"))
.ok_or("revolve: missing `axis` reference selection")?;
let axis = resolve_axis(ctx, &axis_name)?;
let oriented = orient_axis_toward_profile_front(profile, &axis);
let degrees = ctx.number("angle")?;
let magnitude = degrees.abs().min(360.0);
let radians = (magnitude * std::f64::consts::PI / 180.0).max(1e-8);
let direction = if degrees < 0.0 {
oriented.scale(-1.0)
} else {
oriented
};
let caps = common::CapNames::new(&ctx.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("revolve: profile has no outer loop")?;
if outer.curves.len() < 2 {
return Err(format!(
"revolve: outer loop needs >= 2 curves, got {}",
outer.curves.len()
));
}
for (index, hole) in region.iter().enumerate().skip(1) {
reject_axis_touching_hole(profile, hole, &common::hole_key(hole, index), &axis)?;
}
let side_names: Vec<Option<String>> = outer
.edge_names
.iter()
.map(|edge| edge.as_ref().map(|name| format!("{name}_RV")))
.collect();
let (start, end) = &caps.per_loop[region_index];
let cap_names = [Some(start.clone()), Some(end.clone())];
let mut solid = revolve_profile_brep_named(
&outer.curves,
axis.point,
direction,
radians,
&side_names,
&cap_names,
)?;
if region.len() > 1 {
solid = common::subtract_region_holes(
solid,
profile,
region,
"revolve",
&ctx.id,
None,
&mut |loop_index, _depth| {
let hole = ®ion[loop_index];
let hole_side_names: Vec<Option<String>> = hole
.edge_names
.iter()
.map(|edge| edge.as_ref().map(|name| format!("{name}_RV")))
.collect();
revolve_profile_brep_named(
&hole.curves,
axis.point,
direction,
radians,
&hole_side_names,
&[],
)
},
)?;
}
region_solids.push(solid);
}
let solid = common::union_region_solids(region_solids)
.map_err(|error| format!("revolve: {error}"))?;
common::stamp_sweep_roles_multi(&solid, "_RV", &caps.starts(), &caps.ends());
let mut result = common::finalize_solid_grouped(ctx, solid, &ctx.id, &caps.containers());
if result.error.is_some() {
return Ok(result);
}
if !from_face && consume_profile_sketch(ctx) {
let sketch_name = name.strip_suffix(":PROFILE").unwrap_or(&name).to_string();
if !result.removed.contains(&sketch_name) {
result.removed.push(sketch_name);
}
}
Ok(result)
}
fn reject_axis_touching_hole(
profile: &SketchProfile,
hole: &crate::feature_pipeline::ProfileLoop,
key: &str,
axis: &Axis,
) -> Result<(), String> {
let side = profile
.z_axis
.cross(axis.direction)
.normalized()
.map_err(|_| "revolve: axis is perpendicular to the profile plane".to_string())?;
let mut min_abs = f64::INFINITY;
let mut has_positive = false;
let mut has_negative = false;
for curve in &hole.curves {
for cp in &curve.control_points {
let point = cp.point()?;
let signed = point.sub(axis.point).dot(side);
if signed > 0.0 {
has_positive = true;
}
if signed < 0.0 {
has_negative = true;
}
min_abs = min_abs.min(signed.abs());
}
}
const AXIS_TOLERANCE: f64 = 1e-9;
if (has_positive && has_negative) || min_abs <= AXIS_TOLERANCE {
return Err(format!(
"revolve: hole loop {key} touches or crosses the revolve axis \
(min radial distance {min_abs:.3e}) — revolving it would self-intersect; \
a hole must stay strictly on one side of the axis"
));
}
Ok(())
}
fn orient_axis_toward_profile_front(profile: &SketchProfile, axis: &Axis) -> Vec3 {
let mut sum = Vec3::new(0.0, 0.0, 0.0);
let mut count = 0usize;
if let Some(outer) = profile.regions.first().and_then(|region| region.first()) {
for curve in &outer.curves {
for cp in &curve.control_points {
let w = if cp.w.abs() > 1e-12 { cp.w } else { 1.0 };
sum = sum.add(Vec3::new(cp.x / w, cp.y / w, cp.z / w));
count += 1;
}
}
}
if count == 0 {
return axis.direction;
}
let center = sum.scale(1.0 / count as f64);
let offset = center.sub(axis.point);
let radial = offset.sub(axis.direction.scale(offset.dot(axis.direction)));
if radial.dot(radial) <= 1e-18 {
return axis.direction;
}
if axis.direction.cross(radial).dot(profile.z_axis) < 0.0 {
axis.direction.scale(-1.0)
} else {
axis.direction
}
}
fn resolve_axis(ctx: &FeatureContext, name: &str) -> Result<Axis, String> {
if let Some(axis) = ctx.scene.resolve_axis(name) {
return Ok(axis);
}
if let Some(edge) = ctx.scene.resolve_edge(name) {
return common::edge_axis(edge);
}
Err(format!(
"revolve: axis '{name}' not found (no sketch line or resident edge)"
))
}
fn consume_profile_sketch(ctx: &FeatureContext) -> bool {
!matches!(ctx.param("consumeProfileSketch"), Some(Value::Bool(false)))
}
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": "R",
"shortName": "R",
"longName": "Revolve",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the revolve feature"
},
"profile": {
"type": "reference_selection",
"selectionFilter": [
"SKETCH",
"FACE"
],
"multiple": false,
"default_value": null,
"hint": "Select the profile (face) to revolve"
},
"consumeProfileSketch": {
"type": "boolean",
"default_value": true,
"hint": "Remove the referenced sketch after creating the revolve. Turn off to keep it in the scene."
},
"axis": {
"type": "reference_selection",
"selectionFilter": [
"EDGE"
],
"multiple": false,
"default_value": null,
"hint": "Select the axis to revolve about"
},
"angle": {
"type": "number",
"default_value": 360,
"hint": "Revolve angle"
},
"boolean": common::optional_boolean_schema()
}
})
}