use std::collections::BTreeMap;
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::sheet_metal::{self, Edge, Flat, SheetTree};
use crate::feature_pipeline::{FeatureContext, FeatureResult, ProfileLoop, SketchProfile};
use crate::NurbsCurve;
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("sheet-metal tab: 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!("sheet-metal tab: {error}"))?;
(&*face_profile_storage.insert(built), true)
}
None => {
return Err(format!(
"sheet-metal tab: profile '{name}' not found (no sketch profile or resident face)"
));
}
},
};
if profile.regions.len() > 1 {
return Err(
"sheet-metal tab: profile has multiple disjoint regions; a tab roots one \
sheet-metal body — sketch one tab per region"
.into(),
);
}
let region = profile
.regions
.first()
.ok_or("sheet-metal tab: profile has no outer loop")?;
let outer = region.first().ok_or("sheet-metal tab: profile has no outer loop")?;
let (outline, edges, outline_curves) = outline_from_outer_loop(&ctx.id, profile, outer)?;
let thickness = ctx.number("thickness")?;
if !(thickness > 0.0) {
return Err(format!("sheet-metal tab: thickness must be positive, got {thickness}"));
}
let bend_radius = ctx.number("bendRadius").unwrap_or(0.0).max(0.0);
let k_factor = ctx.number("neutralFactor").unwrap_or(0.5).clamp(0.0, 1.0);
let mode = ctx
.string("placementMode")
.unwrap_or_else(|| "forward".to_string());
let origin = profile.origin;
let x_axis = profile.x_axis;
let y_axis = profile.y_axis;
let normal = profile.z_axis;
let half_t = thickness * 0.5;
let shift = match mode.as_str() {
"reverse" => -half_t,
"midplane" => 0.0,
_ => half_t, };
let placed_origin = origin.add(normal.scale(shift));
let root_transform = [
[placed_origin.x, placed_origin.y, placed_origin.z],
[x_axis.x, x_axis.y, x_axis.z],
[y_axis.x, y_axis.y, y_axis.z],
[normal.x, normal.y, normal.z],
];
let holes = region
.iter()
.skip(1)
.map(|hole| common::profile_loop_uv_curves(profile, hole).map(sheet_metal::Hole::through))
.collect::<Result<Vec<_>, _>>()?;
let tree = SheetTree {
thickness,
root: Flat {
id: format!("{}:flat_root", ctx.id),
outline,
edges,
holes,
hole_bends: Vec::new(),
outline_curves,
},
default_inside_radius: bend_radius,
default_k_factor: k_factor,
root_transform,
};
let added = sheet_metal::register_folded(tree, &ctx.id)?;
let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
result.added.push(added);
if !from_face {
common::consume_sketch_always(&name, &mut result);
}
Ok(result)
}
fn outline_from_outer_loop(
feature_id: &str,
profile: &SketchProfile,
outer: &ProfileLoop,
) -> Result<(Vec<[f64; 2]>, Vec<Edge>, BTreeMap<String, NurbsCurve>), String> {
let mut curves = common::profile_loop_uv_curves(profile, outer)?;
if curves.is_empty() {
return Err("sheet-metal tab: profile outer loop has no curves".into());
}
let area = sampled_loop_area(&curves)?;
if !(area.abs() > 1e-12) {
return Err("sheet-metal tab: profile outer loop encloses no area".into());
}
if area < 0.0 {
curves = curves
.iter()
.rev()
.map(NurbsCurve::reversed)
.collect::<Result<_, _>>()?;
}
while curves.len() < 3 {
let mut split = Vec::with_capacity(curves.len() * 2);
for curve in &curves {
let [t0, t1] = curve.domain()?;
let (first, second) = curve.split((t0 + t1) * 0.5)?;
split.push(first);
split.push(second);
}
curves = split;
}
let mut outline = Vec::with_capacity(curves.len());
let mut edges = Vec::with_capacity(curves.len());
let mut outline_curves = BTreeMap::new();
for (i, curve) in curves.iter().enumerate() {
let domain = curve.domain()?;
let start = curve.evaluate(domain[0])?;
outline.push([start.x, start.y]);
let id = format!("{feature_id}:e{i}");
if !curve_is_straight(curve)? {
outline_curves.insert(id.clone(), curve.clone());
}
edges.push(Edge { id, bend: None });
}
Ok((outline, edges, outline_curves))
}
fn sampled_loop_area(curves: &[NurbsCurve]) -> Result<f64, String> {
let mut points = Vec::with_capacity(curves.len() * 64);
for curve in curves {
let [t0, t1] = curve.domain()?;
for step in 0..64 {
let p = curve.evaluate(t0 + (t1 - t0) * step as f64 / 64.0)?;
points.push([p.x, p.y]);
}
}
let mut area2 = 0.0;
for i in 0..points.len() {
let a = points[i];
let b = points[(i + 1) % points.len()];
area2 += a[0] * b[1] - b[0] * a[1];
}
Ok(area2 * 0.5)
}
fn curve_is_straight(curve: &NurbsCurve) -> Result<bool, String> {
let domain = curve.domain()?;
let start = curve.evaluate(domain[0])?;
let end = curve.evaluate(domain[1])?;
let chord = end.sub(start);
let chord_len = chord.length();
for step in 1..8 {
let sample = curve.evaluate(domain[0] + (domain[1] - domain[0]) * step as f64 / 8.0)?;
if chord_len <= 1e-9 {
if sample.sub(start).length() > 1e-9 {
return Ok(false);
}
continue;
}
let t = sample.sub(start).dot(chord) / (chord_len * chord_len);
let projection = start.add(chord.scale(t));
if sample.sub(projection).length() > 1e-6 * chord_len.max(1.0) {
return Ok(false);
}
}
Ok(true)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.has_profile()
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "SM.TAB",
"shortName": "SM.TAB",
"longName": "SM Tab",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "Unique identifier for the sheet metal tab"
},
"profile": {
"type": "reference_selection",
"selectionFilter": [
"FACE",
"SKETCH"
],
"multiple": false,
"default_value": null,
"hint": "Closed sketch or face defining the tab footprint"
},
"thickness": {
"type": "number",
"default_value": 1,
"min": 0,
"hint": "Sheet metal thickness. Also used as the tab extrusion distance."
},
"placementMode": {
"type": "options",
"options": [
"forward",
"reverse",
"midplane"
],
"default_value": "forward",
"hint": "Controls whether material is added forward, backward, or split about the sketch plane."
},
"bendRadius": {
"type": "number",
"default_value": 0.125,
"min": 0,
"hint": "Default bend radius captured with the sheet-metal base feature."
},
"neutralFactor": {
"type": "number",
"default_value": 0.5,
"min": 0,
"max": 1,
"step": 0.01,
"hint": "Neutral factor used for flat pattern bend allowance (0-1)."
}
}
})
}