use std::collections::HashSet;
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{AddedSolid, FeatureContext, FeatureResult, SceneMap};
use crate::{move_faces, rotate_faces, BrepSolid, Vec3};
pub const CENTRE_SAMPLES: usize = 64;
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn resolve_selection(
scene: &SceneMap,
names: &[String],
unresolved: &mut Vec<String>,
) -> Option<(u32, Vec<u64>)> {
let mut resolved: Vec<(u32, u64)> = Vec::new();
for name in names {
match scene.resolve_face(name) {
Some(face_ref) => resolved.push((face_ref.handle, face_ref.face_id)),
None => unresolved.push(name.clone()),
}
}
let &(target, _) = resolved.first()?;
if resolved.iter().any(|(handle, _)| *handle != target) {
return None;
}
let mut seen: HashSet<u64> = HashSet::new();
let face_ids = resolved
.iter()
.map(|(_, face_id)| *face_id)
.filter(|face_id| seen.insert(*face_id))
.collect();
Some((target, face_ids))
}
pub fn selection_centre(solid: &BrepSolid, face_ids: &[u64]) -> Option<Vec3> {
let selected: HashSet<u64> = face_ids.iter().copied().collect();
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
let mut include = |point: Vec3| {
let point = [point.x, point.y, point.z];
if point.iter().all(|c| c.is_finite()) {
for axis in 0..3 {
low[axis] = low[axis].min(point[axis]);
high[axis] = high[axis].max(point[axis]);
}
}
};
let mut edge_ids: HashSet<u64> = HashSet::new();
let mut edgeless = Vec::new();
for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
if !selected.contains(&face.id) {
continue;
}
let before = edge_ids.len();
for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
edge_ids.insert(coedge.edge_id);
}
if edge_ids.len() == before && face.loops.iter().all(|l| l.coedges.is_empty()) {
edgeless.push(face);
}
}
let vertex_ids: HashSet<u64> = solid
.edges
.iter()
.filter(|edge| edge_ids.contains(&edge.id))
.flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
.collect();
for vertex in &solid.vertices {
if vertex_ids.contains(&vertex.id) {
include(vertex.point);
}
}
for edge in solid.edges.iter().filter(|edge| edge_ids.contains(&edge.id)) {
if edge.degenerate {
continue;
}
for station in 0..=CENTRE_SAMPLES {
let t = edge.t0 + (edge.t1 - edge.t0) * station as f64 / CENTRE_SAMPLES as f64;
if let Ok(point) = edge.curve.evaluate(t) {
include(point);
}
}
}
for face in edgeless {
for control in face.surface.control_points.iter().flatten() {
if let Ok(point) = control.point() {
include(point);
}
}
}
if !low[0].is_finite() {
return None;
}
Some(Vec3::new(
0.5 * (low[0] + high[0]),
0.5 * (low[1] + high[1]),
0.5 * (low[2] + high[2]),
))
}
pub fn face_transform_pivot(prefix: &[FeatureResult], names: &[String]) -> Option<[f64; 3]> {
let mut scene = SceneMap::default();
for result in prefix {
scene.apply(result);
}
let (handle, face_ids) = resolve_selection(&scene, names, &mut Vec::new())?;
let centre =
crate::with_registered_solid_str(handle, |solid| Ok(selection_centre(solid, &face_ids)))
.ok()??;
Some([centre.x, centre.y, centre.z])
}
pub fn rotation_axis_angle(rotation_deg: [f64; 3]) -> Option<(Vec3, f64)> {
if rotation_deg == [0.0; 3] {
return None;
}
let radians = rotation_deg.map(f64::to_radians);
let m = common::compose_trs_matrix([0.0; 3], radians, [1.0; 3], [0.0; 3]);
let at = |row: usize, col: usize| m[row * 4 + col];
let (m00, m11, m22) = (at(0, 0), at(1, 1), at(2, 2));
let trace = m00 + m11 + m22;
let q = if trace >= m00 && trace >= m11 && trace >= m22 {
let w = 0.5 * (1.0 + trace).max(0.0).sqrt();
let k = 0.25 / w;
[(at(2, 1) - at(1, 2)) * k, (at(0, 2) - at(2, 0)) * k, (at(1, 0) - at(0, 1)) * k, w]
} else if m00 >= m11 && m00 >= m22 {
let x = 0.5 * (1.0 + m00 - m11 - m22).max(0.0).sqrt();
let k = 0.25 / x;
[x, (at(0, 1) + at(1, 0)) * k, (at(0, 2) + at(2, 0)) * k, (at(2, 1) - at(1, 2)) * k]
} else if m11 >= m22 {
let y = 0.5 * (1.0 - m00 + m11 - m22).max(0.0).sqrt();
let k = 0.25 / y;
[(at(0, 1) + at(1, 0)) * k, y, (at(1, 2) + at(2, 1)) * k, (at(0, 2) - at(2, 0)) * k]
} else {
let z = 0.5 * (1.0 - m00 - m11 + m22).max(0.0).sqrt();
let k = 0.25 / z;
[(at(0, 2) + at(2, 0)) * k, (at(1, 2) + at(2, 1)) * k, z, (at(1, 0) - at(0, 1)) * k]
};
let sign = if q[3] < 0.0 { -1.0 } else { 1.0 };
let axis = Vec3::new(q[0] * sign, q[1] * sign, q[2] * sign);
let half_sine = axis.length();
let angle = 2.0 * half_sine.atan2(q[3] * sign);
if angle <= 1e-12 {
return None;
}
Some((axis.scale(1.0 / half_sine), angle))
}
fn transform_vec3(ctx: &FeatureContext, key: &str, default: [f64; 3]) -> Result<[f64; 3], String> {
common::vec3_from_value(
ctx.env,
ctx.param("transform").and_then(|transform| transform.get(key)),
&format!("transform.{key}"),
default,
)
}
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("faces"));
if names.is_empty() {
return Ok(result); }
let Some((target_handle, face_ids)) =
resolve_selection(ctx.scene, &names, &mut result.unresolved)
else {
return Ok(result); };
let scale = transform_vec3(ctx, "scale", [1.0; 3])?;
if scale != [1.0; 3] {
return Err(format!(
"transformFace: `transform.scale` is {scale:?}, but a face transform is a RIGID \
motion — a scaled carrier is a different surface, not a moved one — so scale must \
be [1, 1, 1]; refusing"
));
}
let position = transform_vec3(ctx, "position", [0.0; 3])?;
let rotation_deg = transform_vec3(ctx, "rotationEuler", [0.0; 3])?;
let translation = Vec3::new(position[0], position[1], position[2]);
let translates = translation.length() > 1e-12;
let rotation = rotation_axis_angle(rotation_deg);
if !translates && rotation.is_none() {
return Ok(result); }
let target_name = ctx
.scene
.solids
.iter()
.find(|(_, handle)| **handle == target_handle)
.map(|(name, _)| name.clone())
.ok_or("transformFace: target solid has no scene-map name")?;
let solid = crate::with_registered_solid_str(target_handle, |solid| Ok(solid.clone()))?;
let turn = match rotation {
None => None,
Some((axis, angle)) => {
let pivot = match ctx.param("pivot") {
None | Some(serde_json::Value::Null) => selection_centre(&solid, &face_ids)
.ok_or("transformFace: the selected faces carry no geometry to centre a pivot on")?,
Some(value) => {
let [x, y, z] = common::vec3_from_value(ctx.env, Some(value), "pivot", [0.0; 3])?;
Vec3::new(x, y, z)
}
};
Some((pivot, axis, angle))
}
};
let moved = match turn {
None => move_faces(&solid, &face_ids, translation)?,
Some((pivot, axis, angle)) => {
if !translates {
rotate_faces(&solid, &face_ids, pivot, axis, angle)?
} else {
let turned = rotate_faces(&solid, &face_ids, pivot, axis, angle).map_err(|error| {
format!(
"transformFace: the ROTATION step of this rotate-then-translate motion \
refused — {error}"
)
})?;
move_faces(&turned, &face_ids, translation).map_err(|error| {
format!(
"transformFace: the TRANSLATION step of this rotate-then-translate \
motion, applied to the faces as the rotation left them, refused — {error}"
)
})?
}
}
};
let moved = match crate::accept_sound(moved, "transformFace") {
Ok(body) => body,
Err(refusal) => {
match crate::recut_moved_planes_rigid(&solid, &face_ids, turn, translation) {
Ok(body) => {
result.notes.push(format!(
"transformFace: the moved carriers could not be re-solved onto the body \
they left ({refusal}), so the selection was RE-CUT from the original \
body instead — the result is the boolean this motion defines, and a \
face whose carrier left the body is no longer on it"
));
body
}
Err(reason) => {
return Err(format!(
"{refusal}. The re-cut road was offered it too and declined: {reason}"
))
}
}
}
};
let face_names = common::collect_face_names(&moved);
let edge_names = common::collect_edge_names(&moved);
let handle = crate::register_solid_value(moved);
result.added.push(AddedSolid {
handle,
name: target_name.clone(),
face_names,
edge_names,
..AddedSolid::default()
});
result.removed.push(target_name);
Ok(result)
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.faces > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "TF",
"shortName": "TF",
"longName": "Transform Face",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "Optional identifier for the transform face feature"
},
"faces": {
"type": "reference_selection",
"selectionFilter": [
"FACE"
],
"timestampDependency": "parentSolid",
"multiple": true,
"default_value": [],
"hint": "Select one or more faces on a single solid to move and turn"
},
"pivot": {
"type": "vec3",
"label": "Pivot",
"default_value": null,
"hint": "The point the rotation turns about, in world millimetres; set to the centre of the selection when the faces are picked"
},
"transform": {
"type": "transform",
"default_value": {
"position": [0, 0, 0],
"rotationEuler": [0, 0, 0]
},
"hint": "Rotate the faces about the pivot, then move them; drag the gizmo or type the values"
}
}
})
}