use std::collections::HashSet;
use crate::feature_pipeline::features::common;
use crate::feature_pipeline::{AddedSolid, FeatureContext, FeatureRefusal, FeatureResult};
use crate::{move_faces, AnalyticSurface, BrepSolid, Vec3};
pub fn execute(ctx: &FeatureContext) -> FeatureResult {
match build(ctx) {
Ok(result) => result,
Err(error) => ctx.fail(error),
}
}
fn build(ctx: &FeatureContext) -> Result<FeatureResult, FeatureRefusal> {
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 mut resolved: Vec<(u32, u64)> = Vec::new();
for name in &names {
match ctx.scene.resolve_face(name) {
Some(face_ref) => resolved.push((face_ref.handle, face_ref.face_id)),
None => result.unresolved.push(name.clone()),
}
}
if resolved.is_empty() {
return Ok(result); }
let handles: HashSet<u32> = resolved.iter().map(|(handle, _)| *handle).collect();
if handles.len() > 1 {
return Ok(result); }
let target_handle = resolved[0].0;
let distance = match ctx.param("distance") {
None | Some(serde_json::Value::Null) => return Ok(result),
Some(_) => ctx.number("distance")?,
};
if !distance.is_finite() || distance.abs() <= 1e-12 {
return Ok(result);
}
result.note_partial_resolution(&names);
let mut seen: HashSet<u64> = HashSet::new();
let face_ids: Vec<u64> = resolved
.iter()
.map(|(_, face_id)| *face_id)
.filter(|face_id| seen.insert(*face_id))
.collect();
let target_name = ctx
.scene
.solids
.iter()
.find(|(_, handle)| **handle == target_handle)
.map(|(name, _)| name.clone())
.ok_or("push_face: target solid has no scene-map name")?;
let mut solid = crate::with_registered_solid_str(target_handle, |solid| Ok(solid.clone()))?;
let face_ids = merge_selected_ruled_pieces(&mut solid, &face_ids)?;
for face_id in &face_ids {
match face_carrier(&solid, *face_id)? {
FaceCarrier::Planar => {
let normal = planar_face_normal(&solid, *face_id)?;
solid = move_faces(&solid, &[*face_id], normal.scale(distance))?;
}
FaceCarrier::Ruled => {
solid = match crate::offset_ruled_face(&solid, *face_id, distance) {
Ok(pushed) => pushed,
Err(refusal) => return Err(name_unselected_sibling(&solid, *face_id, refusal).into()),
};
}
FaceCarrier::Sphere => {
solid = crate::offset_sphere_face(&solid, *face_id, distance)?;
}
FaceCarrier::Torus => {
solid = crate::offset_torus_face(&solid, *face_id, distance)?;
}
FaceCarrier::Revolution => {
solid = crate::offset_revolution_face(&solid, *face_id, distance)?;
}
FaceCarrier::Freeform => {
solid = crate::offset_freeform_face(&solid, *face_id, distance)?;
}
}
}
let solid = crate::accept_sound(solid, "pushFace")?;
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: target_name.clone(),
face_names,
edge_names,
..AddedSolid::default()
});
result.removed.push(target_name);
Ok(result)
}
fn merge_selected_ruled_pieces(solid: &mut BrepSolid, face_ids: &[u64]) -> Result<Vec<u64>, crate::KernelRefusal> {
let ruled: Vec<u64> = face_ids
.iter()
.copied()
.filter(|id| matches!(face_carrier(solid, *id), Ok(FaceCarrier::Ruled)))
.collect();
if ruled.len() < 2 {
return Ok(face_ids.to_vec());
}
let (merged, carried_by) = crate::merge_faces_within(solid, PIECE_MERGE_TOLERANCE, &ruled)?;
*solid = merged;
let mut seen: HashSet<u64> = HashSet::new();
Ok(face_ids
.iter()
.map(|id| {
carried_by
.iter()
.find(|(face, _)| face == id)
.map(|(_, carrier)| *carrier)
.unwrap_or(*id)
})
.filter(|id| seen.insert(*id))
.collect())
}
const PIECE_MERGE_TOLERANCE: f64 = 1e-7;
fn name_unselected_sibling(solid: &BrepSolid, face_id: u64, refusal: crate::KernelRefusal) -> crate::KernelRefusal {
let edges_of = |id: u64| -> HashSet<u64> {
solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| face.id == id)
.flat_map(|face| &face.loops)
.flat_map(|loop_record| &loop_record.coedges)
.map(|coedge| coedge.edge_id)
.collect()
};
let own = edges_of(face_id);
let siblings: Vec<&crate::FaceRecord> = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.filter(|face| {
face.id != face_id
&& matches!(face.surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. }))
&& !edges_of(face.id).is_disjoint(&own)
})
.collect();
for sibling in siblings {
let Ok((_, carried_by)) = crate::merge_faces_within(solid, PIECE_MERGE_TOLERANCE, &[face_id, sibling.id]) else {
continue;
};
if carried_by.iter().all(|(_, carrier)| *carrier == carried_by[0].1) {
let name = sibling.name.clone().unwrap_or_else(|| format!("face {}", sibling.id));
return crate::KernelRefusal::input(
crate::KernelStage::Collect,
"split_carrier_piece",
format!(
"push_face: face {face_id} is one piece of a cylinder/cone split into several faces; \
select its sibling `{name}` too so the whole carrier is pushed ({})",
refusal.message
),
);
}
}
refusal
}
enum FaceCarrier {
Planar,
Ruled,
Sphere,
Torus,
Revolution,
Freeform,
}
fn face_carrier(solid: &BrepSolid, face_id: u64) -> Result<FaceCarrier, String> {
for shell in &solid.shells {
for face in &shell.faces {
if face.id != face_id {
continue;
}
return match face.surface.analytic() {
Some(AnalyticSurface::Plane { .. }) => Ok(FaceCarrier::Planar),
Some(AnalyticSurface::RuledRevolution { .. }) => Ok(FaceCarrier::Ruled),
Some(AnalyticSurface::Sphere { .. }) => Ok(FaceCarrier::Sphere),
Some(AnalyticSurface::Torus { .. }) => Ok(FaceCarrier::Torus),
Some(AnalyticSurface::Revolution { .. }) => Ok(FaceCarrier::Revolution),
None => Ok(FaceCarrier::Freeform),
};
}
}
Err(format!("push_face: face id {face_id} not found"))
}
fn planar_face_normal(solid: &BrepSolid, face_id: u64) -> Result<Vec3, String> {
for shell in &solid.shells {
for face in &shell.faces {
if face.id != face_id {
continue;
}
if !matches!(face.surface.analytic(), Some(AnalyticSurface::Plane { .. })) {
return Err(
"push_face: only planar faces are migrated (cylindrical radius-delta \
and other curved carriers are deferred)"
.into(),
);
}
let [u0, u1] = face.surface.domain_u()?;
let [v0, v1] = face.surface.domain_v()?;
let (um, vm) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
let mut normal = face.surface.normal(um, vm)?;
if !face.same_sense {
normal = normal.scale(-1.0);
}
return Ok(normal);
}
}
Err(format!("push_face: face id {face_id} not found"))
}
pub fn context_applicable(probe: &crate::feature_pipeline::SelectionProbe) -> bool {
probe.faces > 0
}
pub fn schema() -> serde_json::Value {
serde_json::json!({
"type": "PF",
"shortName": "PF",
"longName": "Push Face",
"ribbonPath": "Home/Synchronous Modeling/Push Face",
"commandSize": "Compact",
"displayBuilder": false,
"inputParamsSchema": {
"id": {
"type": "string",
"default_value": null,
"hint": "Optional identifier for the push 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 push"
},
"distance": {
"type": "number",
"default_value": 1,
"hint": "Signed distance to push the selected faces along their normals"
}
}
})
}