use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{AddedSolid, FeatureContext, FeatureResult};
use crate::{transform_brep, AffineTransform, BrepSolid};
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 mut result = FeatureResult::empty(ctx.id.clone(), ctx.feature_type.clone());
let names = common::reference_name_array(ctx.param("solids"));
if names.is_empty() {
return Ok(result);
}
let resolved = common::resolve_solid_names(ctx.scene, &names, &mut result.unresolved);
if resolved.is_empty() {
return Ok(result);
}
let translate = vec3_param(ctx, "position", [0.0, 0.0, 0.0])?;
let rotate_deg = vec3_param(ctx, "rotationEuler", [0.0, 0.0, 0.0])?;
let scale = vec3_param(ctx, "scale", [1.0, 1.0, 1.0])?;
let rotate_rad = [
rotate_deg[0].to_radians(),
rotate_deg[1].to_radians(),
rotate_deg[2].to_radians(),
];
let pivot_bbox = matches!(
ctx.param("pivot").and_then(|value| value.as_str()),
Some(mode) if mode.eq_ignore_ascii_case("BBOX_CENTER")
);
let copy = ctx
.param("copy")
.and_then(|value| value.as_bool())
.unwrap_or(false);
for (source_name, handle) in &resolved {
let mut transformed = crate::with_registered_solid_str(*handle, |solid| {
let pivot = if pivot_bbox {
bbox_center(solid)
} else {
[0.0, 0.0, 0.0]
};
let matrix = common::compose_trs_matrix(translate, rotate_rad, scale, pivot);
let transform = AffineTransform::new(matrix)?;
transform_brep(solid, transform, false)
})?;
let name = if copy {
common::namespace_copy_names(&mut transformed, &ctx.id);
format!("{}_{}", source_name, ctx.id)
} else {
source_name.clone()
};
let face_names = common::collect_face_names(&transformed);
let edge_names = common::collect_edge_names(&transformed);
let new_handle = crate::register_solid_value(transformed);
result.added.push(AddedSolid {
handle: new_handle,
name,
face_names,
edge_names,
..AddedSolid::default()
});
}
if !copy {
result.removed = resolved.into_iter().map(|(name, _)| name).collect();
}
Ok(result)
}
pub(crate) fn bbox_center(solid: &BrepSolid) -> [f64; 3] {
let mut min = [f64::INFINITY; 3];
let mut max = [f64::NEG_INFINITY; 3];
for vertex in &solid.vertices {
let p = [vertex.point.x, vertex.point.y, vertex.point.z];
for axis in 0..3 {
min[axis] = min[axis].min(p[axis]);
max[axis] = max[axis].max(p[axis]);
}
}
if !min[0].is_finite() {
return [0.0, 0.0, 0.0];
}
[
0.5 * (min[0] + max[0]),
0.5 * (min[1] + max[1]),
0.5 * (min[2] + max[2]),
]
}
fn vec3_param(
ctx: &FeatureContext,
key: &str,
default: [f64; 3],
) -> Result<[f64; 3], String> {
common::vec3_from_value(ctx.env, ctx.param("transform").and_then(|t| t.get(key)), &format!("transform.{key}"), default)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.solids > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "XFORM",
"shortName": "XFORM",
"longName": "Transform",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "unique identifier for the transform feature"
},
"solids": {
"type": "reference_selection",
"selectionFilter": [
"SOLID"
],
"multiple": true,
"default_value": [],
"hint": "Select solids to transform"
},
"pivot": {
"type": "options",
"options": [
"ORIGIN",
"BBOX_CENTER"
],
"default_value": "ORIGIN",
"hint": "Pivot point for rotation/scale"
},
"transform": {
"type": "transform",
"default_value": {
"position": [0, 0, 0],
"rotationEuler": [0, 0, 0],
"scale": [1, 1, 1]
},
"hint": "Move, rotate and scale selected solids"
},
"copy": {
"type": "boolean",
"default_value": false,
"label": "Copy",
"hint": "Create a new transformed copy; keep the original"
}
}
})
}