To / From Object finds a target in another, moved component (#993)
Fixes #928. Extrude To / From Object and Revolve To Object now resolve their target in the feature's own component frame, so a face in another component whose occurrence is moved is found where it is shown instead of the extrude stopping at a flat plane through the picked point. The point is named after the nearest face, a named point keeps its named face when another component touches it there, and such features re-evaluate when a placement changes, so they follow the target when it moves later. Co-authored-by: SychPL <SychPL@users.noreply.github.com>
This commit is contained in:
1 parent
6e84c03946
commit
cf9d7772f1
5 files changed
+289
-28
No files matched your search
+104
-24
@@ -897,6 +897,15 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
}
|
||||
return hash.finish();
|
||||
}
|
||||
// To / From Object finds its target where that target's component is placed (#928).
|
||||
let to_object = match &f.kind {
|
||||
FeatureKind::Extrude { extent, .. } => extent.to.is_some() || extent.from.is_some(),
|
||||
FeatureKind::Revolve { to, .. } => to.is_some(),
|
||||
_ => false,
|
||||
};
|
||||
if to_object {
|
||||
return if doc.occurrences.is_empty() && doc.body_offsets.is_empty() { 0 } else { placements_hash(doc) };
|
||||
}
|
||||
let FeatureKind::Sketch { plane, sketch } = &f.kind else { return 0 };
|
||||
let anchor = crate::face_anchor::plane_face_anchor(plane);
|
||||
if (doc.occurrences.is_empty() && (anchor.is_none() || doc.body_offsets.is_empty()))
|
||||
@@ -904,6 +913,11 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
placements_hash(doc)
|
||||
}
|
||||
|
||||
/// Every occurrence's placement and every body offset.
|
||||
fn placements_hash(doc: &Document) -> u64 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
for o in &doc.occurrences {
|
||||
o.id.hash(&mut h);
|
||||
@@ -1105,17 +1119,18 @@ impl Model {
|
||||
FeatureKind::Hole { direction, points: None, .. } => direction.normalized(),
|
||||
_ => None,
|
||||
};
|
||||
let fst = face_point_state(doc, &state, &f.kind, f.component);
|
||||
for (k, (p, n)) in g.kind.face_points_mut().into_iter().zip(&f.face_names).enumerate() {
|
||||
if n.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match crate::naming::locate_face(&state, n, *p) {
|
||||
match crate::naming::locate_face(&fst, n, *p) {
|
||||
Some(hit) => {
|
||||
if let Some(note) = hit.note(n) {
|
||||
notes.push(note);
|
||||
}
|
||||
match (k, hole_axis) {
|
||||
(0, Some(axis)) => match hit.along(&state, *p, axis) {
|
||||
(0, Some(axis)) => match hit.along(&fst, *p, axis) {
|
||||
Some(q) => *p = q,
|
||||
// The place still lies on the face (the axis may run along it).
|
||||
None if hit.on_point => {}
|
||||
@@ -2489,15 +2504,22 @@ fn feature_tools_with_constructor(
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
// From / To Object: where the face or point lies along the normal, measured over the
|
||||
// middle of the profiles.
|
||||
// middle of the profiles. The points are in this feature's component frame, so the
|
||||
// model is seen from there: a target in another, moved component where it is shown.
|
||||
let n = ss.plane.normal();
|
||||
let tst =
|
||||
if extent.to.is_some() || extent.from.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
|
||||
// The face names follow `face_points`: To, then From.
|
||||
let name = |k: usize| f.face_names.get(k).map(String::as_str);
|
||||
let (to_name, from_name) = if extent.to.is_some() { (name(0), name(1)) } else { (None, name(0)) };
|
||||
let to_face = extent.to.and_then(|p| object_face(&tst, to_name, p));
|
||||
let centre = {
|
||||
let pts: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
|
||||
let c = pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64);
|
||||
ss.plane.to_world(c)
|
||||
};
|
||||
let from = match extent.from {
|
||||
Some(p) => object_height(st, &ss.plane, centre, p),
|
||||
Some(p) => height_at(&tst, object_face(&tst, from_name, p), &ss.plane, centre, p),
|
||||
None => 0.0,
|
||||
};
|
||||
let d = match extent.to {
|
||||
@@ -2506,7 +2528,7 @@ fn feature_tools_with_constructor(
|
||||
Some(e) => val(vals, e, Kind::Length)?,
|
||||
None => 0.0,
|
||||
};
|
||||
let h = object_height(st, &ss.plane, centre, p);
|
||||
let h = height_at(&tst, to_face, &ss.plane, centre, p);
|
||||
let start = from + extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
|
||||
// Past the face by the offset, in the direction travelled.
|
||||
let to = h + if h >= start { off } else { -off };
|
||||
@@ -2550,9 +2572,9 @@ fn feature_tools_with_constructor(
|
||||
let (mut lo, mut hi) = if a <= b { (a, b) } else { (b, a) };
|
||||
// To a curved (or slanted) face: run right through the target body, then keep what
|
||||
// lies before its surface (the tool less the body, the piece at the start).
|
||||
let curved = extent.to.and_then(|p| curved_target(st, n, p));
|
||||
let curved = curved_face(&tst, to_face, Some(n));
|
||||
if let Some(bi) = curved
|
||||
&& let Some(tb) = st.bodies.get(bi)
|
||||
&& let Some(tb) = tst.bodies.get(bi)
|
||||
{
|
||||
let m = tb.mesh();
|
||||
let hs: Vec<f64> = m.positions.iter().map(|q| (*q - ss.plane.origin).dot(n) - off).collect();
|
||||
@@ -2604,7 +2626,7 @@ fn feature_tools_with_constructor(
|
||||
tools = vec![Traced::step(one, &tools.iter().collect::<Vec<_>>())];
|
||||
}
|
||||
}
|
||||
if let Some(tb) = curved.and_then(|bi| st.bodies.get(bi)) {
|
||||
if let Some(tb) = curved.and_then(|bi| tst.bodies.get(bi)) {
|
||||
let start = if hi > 0.0 { lo + off } else { hi + off };
|
||||
tools = trim_to_body(tools, tb, |q: Vec3| ((q - ss.plane.origin).dot(n) - start).abs())?;
|
||||
}
|
||||
@@ -2662,13 +2684,15 @@ fn feature_tools_with_constructor(
|
||||
)?;
|
||||
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
|
||||
let (o, d) = revolve_axis(&ss, axis)?;
|
||||
// To Object is in this feature's component frame: the model as seen from there.
|
||||
let tst = if to.is_some() { state_in_frame(doc, st, f.component) } else { std::borrow::Cow::Borrowed(st) };
|
||||
let angles = if to.is_some() {
|
||||
(0.0, None)
|
||||
} else {
|
||||
(val(vals, angle, Kind::Angle)?, angle2.as_ref().map(|a| val(vals, a, Kind::Angle)).transpose()?)
|
||||
};
|
||||
let extent = revolve_extent_with_convention(
|
||||
st,
|
||||
&tst,
|
||||
&ss.plane,
|
||||
®ions,
|
||||
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
|
||||
@@ -2683,7 +2707,7 @@ fn feature_tools_with_constructor(
|
||||
.map(|r| {
|
||||
if *convention == crate::RevolveConvention::SketchNormal && to.is_some() {
|
||||
revolve_extent_with_convention(
|
||||
st,
|
||||
&tst,
|
||||
&ss.plane,
|
||||
std::slice::from_ref(r),
|
||||
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
|
||||
@@ -2724,8 +2748,8 @@ fn feature_tools_with_constructor(
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
// To a curved face: turn past the whole target body, then cut back to its surface
|
||||
// (the piece that starts at the profile).
|
||||
let curved = to.and_then(|p| curved_target_any(st, p));
|
||||
let curved_body = curved.and_then(|bi| st.bodies.get(bi));
|
||||
let curved = curved_face(&tst, to.and_then(|p| object_face(&tst, f.face_names.first().map(String::as_str), p)), None);
|
||||
let curved_body = curved.and_then(|bi| tst.bodies.get(bi));
|
||||
// Each region's body: its sides named by the sketch curves, its caps start and end.
|
||||
let tool = |r: &Region2, body: Body, built: Option<&Body>| crate::lineage::ProfileRoles::swept(Some(&ss), Some(r)).tool(body, built);
|
||||
let build_curved = |regions: &[Region2], angle_axis: Vec3, tb: &ModelBody| -> Result<Vec<Traced>> {
|
||||
@@ -3437,9 +3461,14 @@ mod surface;
|
||||
/// there if it is planar and parallel (its plane), else where a ray from `centre` along the
|
||||
/// normal meets the body there, else the point itself.
|
||||
pub fn object_height(st: &ModelState, plane: &Plane, centre: Vec3, p: Vec3) -> f64 {
|
||||
height_at(st, face_at(st, p), plane, centre, p)
|
||||
}
|
||||
|
||||
/// [`object_height`] of the point `p` on `face` (`(body index, face index)`, see [`object_face`]).
|
||||
fn height_at(st: &ModelState, face: Option<(usize, usize)>, plane: &Plane, centre: Vec3, p: Vec3) -> f64 {
|
||||
let n = plane.normal();
|
||||
let h = |q: Vec3| (q - plane.origin).dot(n);
|
||||
let Some((bi, fi)) = face_at(st, p) else { return h(p) };
|
||||
let Some((bi, fi)) = face else { return h(p) };
|
||||
let Some(b) = st.bodies.get(bi) else { return h(p) };
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let Ok(faces) = b.body.faces(tol) else { return h(p) };
|
||||
@@ -3477,24 +3506,37 @@ fn face_at(st: &ModelState, p: Vec3) -> Option<(usize, usize)> {
|
||||
.map(|(_, bi, fi)| (bi, fi))
|
||||
}
|
||||
|
||||
/// The body at `p` when the face there is not a plane square to `n` (To Object must then
|
||||
/// follow the face's shape).
|
||||
fn curved_target(st: &ModelState, n: Vec3, p: Vec3) -> Option<usize> {
|
||||
let (bi, fi) = face_at(st, p)?;
|
||||
/// The face a To / From Object point stands for, `(body index, face index)`: the face its
|
||||
/// stored `name` finds when the point lies on it (evaluation has moved the point onto its named
|
||||
/// face), else the nearest face at the point. The name settles a tie: in an assembly a part of
|
||||
/// another component may rest on the target and touch it at the point.
|
||||
fn object_face(st: &ModelState, name: Option<&str>, p: Vec3) -> Option<(usize, usize)> {
|
||||
if let Some(n) = name.filter(|n| !n.is_empty())
|
||||
&& let Some(hit) = crate::naming::locate_face(st, n, p)
|
||||
&& hit.on_point
|
||||
{
|
||||
return Some((hit.body, hit.face));
|
||||
}
|
||||
face_at(st, p)
|
||||
}
|
||||
|
||||
/// The body of `face` when that face is not a plane (with `n`: not a plane square to `n`), so To
|
||||
/// Object must follow its shape.
|
||||
fn curved_face(st: &ModelState, face: Option<(usize, usize)>, n: Option<Vec3>) -> Option<usize> {
|
||||
let (bi, fi) = face?;
|
||||
let b = st.bodies.get(bi)?;
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let planar =
|
||||
b.body.faces(tol).ok()?.into_iter().find(|f| f.index == fi).and_then(|f| f.plane_normal).is_some_and(|fnrm| fnrm.cross(n).len() < 1e-6);
|
||||
let normal = b.body.faces(tol).ok()?.into_iter().find(|f| f.index == fi).map(|f| f.plane_normal);
|
||||
let planar = match n {
|
||||
Some(n) => normal.flatten().is_some_and(|fnrm| fnrm.cross(n).len() < 1e-6),
|
||||
None => normal.is_some_and(|fnrm| fnrm.is_some()),
|
||||
};
|
||||
(!planar).then_some(bi)
|
||||
}
|
||||
|
||||
/// The body at `p` when the face there is not planar.
|
||||
fn curved_target_any(st: &ModelState, p: Vec3) -> Option<usize> {
|
||||
let (bi, fi) = face_at(st, p)?;
|
||||
let b = st.bodies.get(bi)?;
|
||||
let tol = (b.body.size() * 1e-3).max(1e-3);
|
||||
let planar = b.body.faces(tol).ok()?.into_iter().find(|f| f.index == fi).is_some_and(|f| f.plane_normal.is_some());
|
||||
(!planar).then_some(bi)
|
||||
curved_face(st, face_at(st, p), None)
|
||||
}
|
||||
|
||||
/// Tools run through `target`, cut back to its surface: each tool less the body, keeping the
|
||||
@@ -4427,6 +4469,44 @@ pub fn state_in_frame<'a>(doc: &Document, st: &'a ModelState, comp: u64) -> std:
|
||||
state_in_frame_at(doc, st, comp, doc.component_transform(comp))
|
||||
}
|
||||
|
||||
/// The model a feature's face points (`FeatureKind::face_points`) are found in. Extrude's To /
|
||||
/// From and Revolve's To place geometry, so they are kept in the feature's own component frame
|
||||
/// (`comp`): the model as that component sees it, a target in another, moved component where it
|
||||
/// is shown (#928). Other face points are picks, kept in the frame of the body they lie on: the
|
||||
/// model itself.
|
||||
pub fn face_point_state<'a>(doc: &Document, st: &'a ModelState, kind: &FeatureKind, comp: u64) -> std::borrow::Cow<'a, ModelState> {
|
||||
match kind {
|
||||
FeatureKind::Extrude { .. } | FeatureKind::Revolve { .. } => state_in_frame(doc, st, comp),
|
||||
_ => std::borrow::Cow::Borrowed(st),
|
||||
}
|
||||
}
|
||||
|
||||
/// The names of the faces a feature's face points lie on (empty where none), found in
|
||||
/// [`face_point_state`]. A To / From point is named after the nearest face over every body, the
|
||||
/// one `object_height` resolves it to, not the first body with a face within tolerance: a part
|
||||
/// resting a hair from the picked face would otherwise take its name.
|
||||
pub fn face_point_names(doc: &Document, st: &ModelState, kind: &FeatureKind, comp: u64) -> Vec<String> {
|
||||
let pts = kind.face_points();
|
||||
if !matches!(kind, FeatureKind::Extrude { .. } | FeatureKind::Revolve { .. }) {
|
||||
return crate::naming::face_names_at(st, &pts);
|
||||
}
|
||||
let fst = face_point_state(doc, st, kind, comp);
|
||||
pts.iter()
|
||||
.map(|p| {
|
||||
fst.bodies
|
||||
.iter()
|
||||
.filter(|b| !b.body.is_mesh())
|
||||
.filter_map(|b| {
|
||||
let (d, fi) = crate::appearance::nearest_face(b, *p)?;
|
||||
(d <= (b.body.size() * 1e-3).max(1e-3)).then_some((d, b, fi))
|
||||
})
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||||
.and_then(|(_, b, fi)| crate::naming::face_names(b).get(fi).cloned())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// As `state_in_frame`, using the exact active placement for its shared source geometry.
|
||||
pub fn state_in_frame_at<'a>(doc: &Document, st: &'a ModelState, comp: u64, frame: Mat) -> std::borrow::Cow<'a, ModelState> {
|
||||
use std::borrow::Cow;
|
||||
|
||||
Reference in new issue
Block a user