use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{AddedSolid, FeatureContext, FeatureResult};
use crate::topology::{FaceRecord, LoopRecord};
use crate::{BrepSolid, NurbsCurve, NurbsSurface};
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
let mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
let distance = match ctx.number("distance") {
Ok(distance) if distance.is_finite() && distance != 0.0 => distance,
_ => return result,
};
let selections = common::unique_reference_names(ctx.param("face"));
let mut faces: Vec<(String, crate::feature_pipeline::FaceRef)> = Vec::new();
for name in &selections {
match ctx.scene.resolve_face(name) {
Some(face) => faces.push((name.clone(), face)),
None => result.unresolved.push(name.clone()),
}
}
if faces.is_empty() {
return result;
}
let feature_id = if ctx.id.is_empty() {
ctx.feature_type.clone()
} else {
ctx.id.clone()
};
let multiple = faces.len() > 1;
let width = faces.len().to_string().len().max(2);
let mut failures: Vec<String> = Vec::new();
for (index, (face_name, face)) in faces.iter().enumerate() {
let result_name = if multiple {
format!(
"{feature_id}_{:0>width$}_{}",
index + 1,
sanitize_token(face_name),
width = width,
)
} else {
feature_id.clone()
};
match thicken_face(face.handle, face.face_id, distance) {
Ok(solid) => {
let face_names = common::collect_face_names(&solid);
let edge_names = common::collect_edge_names(&solid);
let handle = crate::register_solid_value(solid);
result.added.push(AddedSolid {
handle,
name: result_name,
face_names,
edge_names,
..AddedSolid::default()
});
}
Err(error) => failures.push(format!("{face_name}: {error}")),
}
}
if result.added.is_empty() {
return ctx.fail(format!(
"Thicken failed to produce any solids: {}",
failures.join("; ")
));
}
result
}
fn sanitize_token(value: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
return "FACE".to_string();
}
let sanitized: String = trimmed
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' || ch == '-' {
ch
} else {
'_'
}
})
.collect();
if sanitized.is_empty() {
"FACE".to_string()
} else {
sanitized
}
}
fn thicken_face(handle: u32, face_id: u64, distance: f64) -> Result<BrepSolid, String> {
crate::with_registered_solid_str(handle, |solid| {
let face = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| face.id == face_id)
.ok_or_else(|| format!("thicken: face {face_id} not found on solid"))?;
let thickness = if face.same_sense { distance } else { -distance };
let loops = build_trim_loops(face)?;
crate::thicken_trimmed_sheet(&face.surface, &loops, thickness, false)
})
}
fn build_trim_loops(face: &FaceRecord) -> Result<Vec<Vec<NurbsCurve>>, String> {
if face.loops.is_empty() {
return Err("thicken: face has no trim loops".into());
}
let mut ordered: Vec<(usize, f64)> = Vec::with_capacity(face.loops.len());
for (index, loop_record) in face.loops.iter().enumerate() {
ordered.push((index, loop_signed_area(&face.surface, loop_record)?));
}
ordered.sort_by(|a, b| b.1.abs().partial_cmp(&a.1.abs()).unwrap_or(std::cmp::Ordering::Equal));
let reverse = ordered[0].1 < 0.0;
let mut loops = Vec::with_capacity(face.loops.len());
for (index, _) in &ordered {
let curves: Vec<NurbsCurve> = face.loops[*index]
.coedges
.iter()
.map(|coedge| coedge.pcurve.clone())
.collect();
loops.push(if reverse { reverse_loop(&curves)? } else { curves });
}
Ok(loops)
}
fn reverse_loop(curves: &[NurbsCurve]) -> Result<Vec<NurbsCurve>, String> {
curves.iter().rev().map(|curve| curve.reversed()).collect()
}
fn loop_signed_area(surface: &NurbsSurface, loop_record: &LoopRecord) -> Result<f64, String> {
let face = FaceRecord {
id: 0,
surface: surface.clone(),
same_sense: true,
loops: vec![loop_record.clone()],
name: None,
};
crate::parameter_space_area(&face)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.faces > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "THK",
"shortName": "THK",
"longName": "Thicken",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "Optional identifier for the thicken feature."
},
"face": {
"type": "reference_selection",
"label": "Faces",
"selectionFilter": [
"FACE"
],
"multiple": true,
"default_value": [],
"hint": "Select one or more open faces to thicken into individual solids."
},
"distance": {
"type": "number",
"default_value": 1,
"hint": "Signed thickness to apply along the face normals."
}
}
})
}