1 parent
180e62c981
commit
487aa64e10
21 files changed
+1624
-78
No files matched your search
@@ -84,6 +84,10 @@ type FacePick = (std::sync::Arc<Mesh>, u32, f64, String);
|
||||
/// equally near (bodies touching flush), the pick is ambiguous: `Err` names those bodies.
|
||||
fn face_at_in(s: &Session, p: Vec3, only: Option<&str>) -> std::result::Result<Option<FacePick>, Vec<String>> {
|
||||
let st = s.model.state();
|
||||
face_at_state(&st, p, only)
|
||||
}
|
||||
|
||||
pub(super) fn face_at_state(st: &solidcraft_doc::ModelState, p: Vec3, only: Option<&str>) -> std::result::Result<Option<FacePick>, Vec<String>> {
|
||||
let mut best: Option<FacePick> = None;
|
||||
let mut tied: Vec<String> = Vec::new();
|
||||
for b in st.bodies.iter().filter(|b| only.is_none_or(|n| b.name == n)) {
|
||||
@@ -127,6 +131,80 @@ fn xy(v: Vec2) -> Value {
|
||||
json!([v.x, v.y])
|
||||
}
|
||||
|
||||
/// Coplanar faces share one linked profile sketch and one Extrude feature. Resolve every
|
||||
/// pick before editing the document, so later picks never search an already modified body.
|
||||
pub(super) fn sketch_of_faces(s: &mut Session, points: &[Vec3], cmd: &str, face_body: Option<&str>) -> Result<(u64, Vec<Vec2>, String)> {
|
||||
let first = points.first().copied().ok_or_else(|| bad(cmd, "faces must not be empty"))?;
|
||||
if points.len() > 1024 {
|
||||
return Err(bad(cmd, "faces exceed the 1024-face limit"));
|
||||
}
|
||||
let state = s.model.state();
|
||||
let mut picked = Vec::new();
|
||||
let mut common: Option<(String, solidcraft_geom::Plane)> = None;
|
||||
for &point in points {
|
||||
if !point.is_finite() {
|
||||
return Err(bad(cmd, "faces must be finite points"));
|
||||
}
|
||||
let (mesh, index, distance, name) = face_at_in(s, point, face_body)
|
||||
.map_err(|_| bad(cmd, "ambiguous face; specify face_body"))?
|
||||
.ok_or_else(|| bad(cmd, "no body face at a selected point"))?;
|
||||
let body = state.body(&name).ok_or_else(|| bad(cmd, "selected body disappeared"))?;
|
||||
let plane = body.body.planar_face(index as usize).ok_or_else(|| bad(cmd, "faces must be planar"))?;
|
||||
let tolerance = (body.body.size() * 1e-9).max(1e-8);
|
||||
if distance > (mesh.bounds().diagonal() * 1e-3).max(1e-3) * 10.0 {
|
||||
return Err(bad(cmd, "no body face at a selected point"));
|
||||
}
|
||||
if let Some((previous, base)) = &common {
|
||||
if *previous != name || base.normal().dot(plane.normal()) < 1.0 - 1e-9 || base.height(plane.origin).abs() > tolerance {
|
||||
return Err(bad(cmd, "faces must be coplanar, equally oriented faces of one body"));
|
||||
}
|
||||
} else {
|
||||
common = Some((name.clone(), plane));
|
||||
}
|
||||
if picked.iter().any(|(face, _)| *face == index as usize) {
|
||||
continue;
|
||||
}
|
||||
let inside = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.zip(&mesh.tri_face)
|
||||
.filter(|(_, face)| **face == index)
|
||||
.filter_map(|(triangle, _)| mesh.tri(triangle))
|
||||
.max_by(|[a, b, c], [x, y, z]| ((*b - *a).cross(*c - *a).len()).total_cmp(&((*y - *x).cross(*z - *x).len())))
|
||||
.map(|[a, b, c]| (a + b + c) / 3.0)
|
||||
.ok_or_else(|| bad(cmd, "degenerate face"))?;
|
||||
picked.push((index as usize, inside));
|
||||
}
|
||||
let (id, first_inside, _) = sketch_of_face(s, first, cmd, face_body)?;
|
||||
let (body, _) = common.ok_or_else(|| bad(cmd, "no faces selected"))?;
|
||||
let doc = s.placed_document();
|
||||
let source = state.body(&body).ok_or_else(|| bad(cmd, "source body disappeared"))?;
|
||||
let component = doc.model_body_component(source);
|
||||
let path = if component == s.active_component {
|
||||
s.active_occurrence_path.clone().unwrap_or_else(|| doc.first_path(component))
|
||||
} else {
|
||||
let placements = super::component::properties::placements(&doc, cmd)?;
|
||||
if placements.iter().filter(|(owner, _, _)| *owner == component).count() != 1 {
|
||||
return Err(bad(cmd, "activate the selected occurrence before extruding its faces"));
|
||||
}
|
||||
doc.first_path(component)
|
||||
};
|
||||
let placement = EmbossFace::component_frame(&doc, component, &path)?;
|
||||
let placement = doc.model_body_offset(source).map(|offset| mat_mul(&placement, offset)).unwrap_or(placement);
|
||||
let author_path = s.active_occurrence_path.clone().unwrap_or_else(|| doc.first_path(s.active_component));
|
||||
let author = EmbossFace::component_frame(&doc, s.active_component, &author_path)?;
|
||||
let inverse = mat_inverse(&author).ok_or_else(|| bad(cmd, "invalid face profile author frame"))?;
|
||||
let into_author = mat_mul(&inverse, &placement);
|
||||
drop(doc);
|
||||
let indices: Vec<_> = picked.iter().skip(1).map(|(index, _)| *index).collect();
|
||||
super::sketch_project::project_internal_faces(s, id, &body, &indices)?;
|
||||
s.refresh();
|
||||
let plane = s.model.state().sketch(id).map(|sketch| sketch.plane).ok_or_else(|| bad(cmd, "face profile sketch disappeared"))?;
|
||||
let mut selected = vec![first_inside];
|
||||
selected.extend(picked.iter().skip(1).map(|(_, point)| plane.to_local(apply_point(&into_author, *point))));
|
||||
Ok((id, selected, body))
|
||||
}
|
||||
|
||||
/// Start a sketch on the planar face at `point` holding a copy of the face boundary. Returns
|
||||
/// the sketch id, a point inside the face (sketch coordinates), and the picked body's name.
|
||||
/// The sketch is left active.
|
||||
|
||||
@@ -18,7 +18,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("extrude")
|
||||
.key("E")
|
||||
.params("distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [{curves: [ids]}] (the profile whose outer loop is made of exactly these curves, or pieces of them; found again on every recompute, so it follows parameter changes; islands inside are its holes; an error when no profile or several match) | [{point:[x,y]}] (index and point picks are stored by their curves when that finds the same profile); face?: [x,y,z] (extrude a planar body face instead of a sketch profile); face_body?: name (whose face, where faces of several bodies meet at the point); direction?: positive|negative|symmetric (distance is total thickness, split evenly around the start plane); convention?: total_distance|legacy_per_side (default total_distance; legacy preserves older symmetric extents); distance2?; start_offset?: expr (along the sketch normal, also with direction: negative); to?: [x,y,z] (To Object: a face, vertex or point; to_offset?: expr past it); from?: [x,y,z] (From Object: start at that face or point); operation?: new|join|cut|intersect|auto (cut into a body, otherwise join existing solids; new in an empty component); targets?: [body] (Join from a face, or from a sketch on a body's face, defaults to that face's body; explicit targets or participants override); name?; body_name?; thin?: true (Thin: a wall along sketch curves instead of the profiles) with curves?: [curve ids] (open chains and closed loops, joined where they meet end to end; default: every drawn curve of the sketch), thickness: expr, side?: center|inside|outside|two_sides (a closed loop's inside is the area it encloses, an open chain's the side it bends toward, a straight line's the left of its first curve as drawn; default center), thickness2?: expr (two_sides: the thickness outside); corners are mitred, crossing walls are one"),
|
||||
.params("distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [{curves: [ids]}] (the profile whose outer loop is made of exactly these curves, or pieces of them; found again on every recompute, so it follows parameter changes; islands inside are its holes; an error when no profile or several match) | [{point:[x,y]}] (index and point picks are stored by their curves when that finds the same profile); face?: [x,y,z] (extrude a planar body face instead of a sketch profile); faces?: [[x,y,z],…] (coplanar faces of one body); face_body?: name (whose face, where faces of several bodies meet at the point); direction?: positive|negative|symmetric (distance is total thickness, split evenly around the start plane); convention?: total_distance|legacy_per_side (default total_distance; legacy preserves older symmetric extents); distance2?; start_offset?: expr (along the sketch normal, also with direction: negative); to?: [x,y,z] (To Object: a face, vertex or point; to_offset?: expr past it); from?: [x,y,z] (From Object: start at that face or point); operation?: new|join|cut|intersect|auto (cut into a body, otherwise join existing solids; new in an empty component); targets?: [body] (Join from a face, or from a sketch on a body's face, defaults to that face's body; explicit targets or participants override); name?; body_name?; thin?: true (Thin: a wall along sketch curves instead of the profiles) with curves?: [curve ids] (open chains and closed loops, joined where they meet end to end; default: every drawn curve of the sketch), thickness: expr, side?: center|inside|outside|two_sides (a closed loop's inside is the area it encloses, an open chain's the side it bends toward, a straight line's the left of its first curve as drawn; default center), thickness2?: expr (two_sides: the thickness outside); corners are mitred, crossing walls are one"),
|
||||
CommandSpec::new("solid.revolve", "Revolve", revolve)
|
||||
.at("SOLID", "CREATE")
|
||||
.icon("revolve")
|
||||
@@ -118,7 +118,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
/// Where an extrude starts and which way is positive: a point inside the (first) profile or
|
||||
/// face, and the unit normal.
|
||||
fn extrude_base(s: &Session, p: &Value) -> Option<(Vec3, Vec3)> {
|
||||
if let Some(fp) = p.get("face").and_then(vec3) {
|
||||
if let Some(fp) = p.get("face").or_else(|| p.get("faces").and_then(Value::as_array).and_then(|faces| faces.first())).and_then(vec3) {
|
||||
return Some((fp, super::face::face_normal_in(s, fp, str_(p, "face_body"))?));
|
||||
}
|
||||
let sid = feature_sketch(s, p, "Extrude").ok()?;
|
||||
@@ -167,19 +167,38 @@ const START_PROBE_MM: f64 = 0.1;
|
||||
/// direction it grows, or halfway along it. With the halfway point alone, a cut dragged far
|
||||
/// past a body's far side read as a Join (#1257).
|
||||
pub fn auto_operation(s: &Session, p: &Value) -> Option<&'static str> {
|
||||
let d = extrude_reach(s, p)?;
|
||||
auto_operation_in(s, p, true)
|
||||
}
|
||||
|
||||
fn auto_operation_in(s: &Session, p: &Value, world_faces: bool) -> Option<&'static str> {
|
||||
let symmetric = p.get("to").is_none() && str_(p, "direction").is_some_and(|d| d.eq_ignore_ascii_case("symmetric"));
|
||||
let (base, n) = extrude_base(s, p)?;
|
||||
let st = s.model.state();
|
||||
// Imported face groups can belong to a child of an empty active component.
|
||||
// Probe in the same coordinate frame as the face picks.
|
||||
let grouped = p.get("faces").and_then(Value::as_array).and_then(|faces| faces.first()).and_then(vec3);
|
||||
let st = if grouped.is_some() && world_faces { s.world_state() } else { s.model.state() };
|
||||
let (base, n) = if let Some(point) = grouped {
|
||||
let (mesh, index, distance, name) = super::face::face_at_state(&st, point, str_(p, "face_body")).ok().flatten()?;
|
||||
if distance > (mesh.bounds().diagonal() * 1e-3).max(1e-3) * 10.0 {
|
||||
return None;
|
||||
}
|
||||
let normal = st.body(&name)?.body.planar_face(index as usize)?.normal();
|
||||
(point, normal)
|
||||
} else {
|
||||
extrude_base(s, p)?
|
||||
};
|
||||
let d = if grouped.is_some() && p.get("to").is_some() { (p.get("to").and_then(vec3)? - base).dot(n) } else { extrude_reach(s, p)? };
|
||||
if st.bodies.is_empty() || !d.is_finite() {
|
||||
return Some("new");
|
||||
}
|
||||
// Only the active component's bodies take part (or the ones named as participants).
|
||||
// Match the evaluator's implicit participants: a grouped cut from an empty
|
||||
// assembly component considers every body; otherwise keep the active scope.
|
||||
// Explicit participants override either default.
|
||||
let parts = string_list(p, "participants");
|
||||
let implicit_all = grouped.is_some() && !st.bodies.iter().any(|body| s.doc.model_body_component(body) == s.active_component);
|
||||
let bodies: Vec<_> = st
|
||||
.bodies
|
||||
.iter()
|
||||
.filter(|b| if parts.is_empty() { s.doc.model_body_component(b) == s.active_component } else { parts.contains(&b.name) })
|
||||
.filter(|b| if parts.is_empty() { implicit_all || s.doc.model_body_component(b) == s.active_component } else { parts.contains(&b.name) })
|
||||
.collect();
|
||||
if bodies.is_empty() {
|
||||
return Some("new");
|
||||
@@ -470,7 +489,8 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
|
||||
// takes part in every body instead, which it changes only where it meets them.
|
||||
let participants = if participants.is_empty()
|
||||
&& !primitive
|
||||
&& matches!(str_(p, "operation").and_then(Operation::parse), Some(Operation::Cut | Operation::Intersect))
|
||||
&& (matches!(str_(p, "operation").and_then(Operation::parse), Some(Operation::Cut | Operation::Intersect))
|
||||
|| p.get("faces").is_some() && matches!(&kind, FeatureKind::Extrude { operation: Operation::Cut | Operation::Intersect, .. }))
|
||||
&& {
|
||||
let st = s.model.state();
|
||||
!st.bodies.is_empty() && !st.bodies.iter().any(|b| s.doc.model_body_component(b) == s.active_component)
|
||||
@@ -545,13 +565,31 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
|
||||
|
||||
fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.extrude";
|
||||
let grouped = if let Some(value) = p.get("faces") {
|
||||
if p.get("face").is_some() || p.get("sketch").is_some() || p.get("profiles").is_some() {
|
||||
return Err(bad(cmd, "faces cannot be combined with face, sketch or profiles"));
|
||||
}
|
||||
let faces = value
|
||||
.as_array()
|
||||
.filter(|faces| !faces.is_empty() && faces.len() <= 1024)
|
||||
.ok_or_else(|| bad(cmd, "faces must contain between 1 and 1024 points"))?;
|
||||
let points = faces.iter().map(|point| vec3(point).ok_or_else(|| bad(cmd, "faces must be points [x,y,z]"))).collect::<Result<Vec<_>>>()?;
|
||||
let (id, points, body) = super::face::sketch_of_faces(s, &points, cmd, str_(p, "face_body"))?;
|
||||
Some((id, Some(ProfileSel::Points { points }), Some(body)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A planar body face extrudes like a profile: its boundary is copied into a sketch on it.
|
||||
let (sketch, face_profile, face_body) = match p.get("face").and_then(vec3) {
|
||||
Some(fp) => {
|
||||
let (id, inside, body) = super::face::sketch_of_face(s, fp, cmd, str_(p, "face_body"))?;
|
||||
(id, Some(ProfileSel::Points { points: vec![inside] }), Some(body))
|
||||
let (sketch, face_profile, face_body) = if let Some(grouped) = grouped {
|
||||
grouped
|
||||
} else {
|
||||
match p.get("face").and_then(vec3) {
|
||||
Some(fp) => {
|
||||
let (id, inside, body) = super::face::sketch_of_face(s, fp, cmd, str_(p, "face_body"))?;
|
||||
(id, Some(ProfileSel::Points { points: vec![inside] }), Some(body))
|
||||
}
|
||||
None => (feature_sketch(s, p, cmd)?, None, None),
|
||||
}
|
||||
None => (feature_sketch(s, p, cmd)?, None, None),
|
||||
};
|
||||
let extent = extent_param(s, p, cmd)?;
|
||||
let thin = match bool_(p, "thin") {
|
||||
@@ -565,7 +603,7 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => None,
|
||||
};
|
||||
let operation = match str_(p, "operation") {
|
||||
Some(o) if o.eq_ignore_ascii_case("auto") => Operation::parse(auto_operation(s, p).unwrap_or("new")).unwrap_or(Operation::NewBody),
|
||||
Some(o) if o.eq_ignore_ascii_case("auto") => Operation::parse(auto_operation_in(s, p, false).unwrap_or("new")).unwrap_or(Operation::NewBody),
|
||||
_ => operation(p, cmd)?,
|
||||
};
|
||||
let mut targets = string_list(p, "targets");
|
||||
|
||||
@@ -2,6 +2,63 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn grouped_face_auto_operation_keeps_implicit_and_explicit_participants() {
|
||||
let mut s = Session::default();
|
||||
let part = run(&mut s, "component.create", json!({"name":"Part"}));
|
||||
run(&mut s, "solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Stock"}));
|
||||
run(&mut s, "component.activate", json!({"component":0}));
|
||||
for offset in [0., 30.] {
|
||||
if offset != 0. {
|
||||
run(&mut s, "occurrence.move", json!({"occurrence":part["occurrence"],"translate":[offset,0,0]}));
|
||||
}
|
||||
// To Object follows the target's side, even when the distance and
|
||||
// direction fields still describe the opposite extrusion.
|
||||
for (height, expected) in [(8., "cut"), (12., "join")] {
|
||||
let params = json!({"faces":[[offset+2.,2,10],[offset+8.,8,10]],"face_body":"Stock",
|
||||
"to":[offset+5.,5,height],"direction":"negative"});
|
||||
assert_eq!(crate::auto_operation(&s, ¶ms), Some(expected));
|
||||
}
|
||||
for distance in [-2., 2.] {
|
||||
let mut params = json!({"faces":[[offset+2.,2,10],[offset+8.,8,10]],"distance":distance});
|
||||
let expected = if distance < 0. { "cut" } else { "join" };
|
||||
assert_eq!(crate::auto_operation(&s, ¶ms), Some(expected));
|
||||
params["face_body"] = json!("Stock");
|
||||
assert_eq!(crate::auto_operation(&s, ¶ms), Some(expected));
|
||||
let mut actual = s.scratch();
|
||||
if offset != 0. {
|
||||
run(&mut actual, "component.activate", json!({"component":part["component"]}));
|
||||
}
|
||||
params["operation"] = json!("auto");
|
||||
let made = run(&mut actual, "solid.extrude", params);
|
||||
let body = actual.model.state().body("Stock").unwrap().body.clone();
|
||||
let volume = solidcraft_kernel::measure(&body).unwrap().volume;
|
||||
assert!(
|
||||
(volume - (1000. + 100. * distance)).abs() < 1e-6,
|
||||
"offset={offset}, distance={distance}, volume={volume}, feature={:?}",
|
||||
actual.doc.feature(made["feature"].as_u64().unwrap())
|
||||
);
|
||||
}
|
||||
if offset != 0. {
|
||||
// Outside the edited component, grouping keeps the original explicit
|
||||
// face command's coordinate and participant behavior.
|
||||
let mut grouped = s.scratch();
|
||||
let mut single = s.scratch();
|
||||
run(&mut grouped, "solid.extrude", json!({"faces":[[32,2,10],[38,8,10]],"face_body":"Stock","distance":-2,"operation":"cut"}));
|
||||
run(&mut single, "solid.extrude", json!({"face":[32,2,10],"face_body":"Stock","distance":-2,"operation":"cut"}));
|
||||
let volume = |session: &Session| solidcraft_kernel::measure(&session.model.state().body("Stock").unwrap().body).unwrap().volume;
|
||||
assert!((volume(&grouped) - volume(&single)).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
run(&mut s, "solid.box", json!({"corner":[100,0,0],"length":10,"width":10,"height":10,"body_name":"Other"}));
|
||||
let mut params = json!({"faces":[[32,2,10],[38,8,10]],"face_body":"Stock","distance":-2});
|
||||
assert_eq!(crate::auto_operation(&s, ¶ms), Some("join"), "an existing active-component body keeps its original participant scope");
|
||||
params["participants"] = json!(["Stock"]);
|
||||
assert_eq!(crate::auto_operation(&s, ¶ms), Some("cut"));
|
||||
params["participants"] = json!(["Other"]);
|
||||
assert_eq!(crate::auto_operation(&s, ¶ms), Some("join"));
|
||||
}
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
@@ -28,6 +85,43 @@ fn volume(s: &mut Session) -> f64 {
|
||||
measure(s)["total"]["volume_mm3"].as_f64().unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separated_profile_cut_preserves_participants_and_removes_only_consumed_bodies() {
|
||||
for explicit in [false, true] {
|
||||
let mut s = Session::default();
|
||||
for (name, x) in [("A", 0), ("B", 10), ("C", 30)] {
|
||||
run(&mut s, "solid.box", json!({"length":5,"width":5,"height":10,"corner":[x,0,0],"body_name":name}));
|
||||
}
|
||||
run(&mut s, "sketch.create", json!({"plane":"XY","name":"Cutters"}));
|
||||
for (a, b) in [([-1, -1], [6, 6]), ([12, 2], [14, 4]), ([32, 2], [34, 4])] {
|
||||
run(&mut s, "sketch.rectangle.two_point", json!({"p0":a,"p1":b}));
|
||||
}
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let before = s.doc.to_json();
|
||||
let mut params = json!({"sketch":"Cutters","profiles":"all","distance":10,"operation":"cut"});
|
||||
if explicit {
|
||||
params["participants"] = json!(["A", "B"]);
|
||||
}
|
||||
run(&mut s, "solid.extrude", params);
|
||||
let check = |session: &Session| {
|
||||
let state = session.model.state();
|
||||
assert_eq!(state.bodies.len(), 2);
|
||||
assert!(state.body("A").is_none());
|
||||
for (name, expected) in [("B", 210.), ("C", if explicit { 250. } else { 210. })] {
|
||||
let body = &state.body(name).unwrap().body;
|
||||
assert!((solidcraft_kernel::measure(body).unwrap().volume - expected).abs() < 1e-6);
|
||||
assert!(body.validity().is_empty());
|
||||
}
|
||||
};
|
||||
check(&s);
|
||||
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc.to_json(), before);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
check(&s);
|
||||
}
|
||||
}
|
||||
|
||||
fn rel(a: f64, b: f64) -> f64 {
|
||||
(a - b).abs() / b.abs().max(1e-12)
|
||||
}
|
||||
|
||||
@@ -520,21 +520,34 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
||||
/// Private compatibility projection for generated face profiles. Named legacy links follow
|
||||
/// edits, but cannot provide the construction certificate required by public attachments.
|
||||
pub(super) fn project_internal_face(s: &mut Session, id: u64, body: &str, index: usize) -> Result<String> {
|
||||
project_internal_faces(s, id, body, &[index])?.pop().ok_or_else(|| bad("face profile", "empty face projection"))
|
||||
}
|
||||
|
||||
/// Capture individual replay names together, against the same unchanged source body.
|
||||
pub(super) fn project_internal_faces(s: &mut Session, id: u64, body: &str, indices: &[usize]) -> Result<Vec<String>> {
|
||||
if indices.len() > 1024 {
|
||||
return Err(bad("face profile", "too many faces"));
|
||||
}
|
||||
let mut doc = (*s.doc).clone();
|
||||
let raw = s.model.state_before(id);
|
||||
let component = doc.feature(id).map(|feature| feature.component).unwrap_or(0);
|
||||
let state = solidcraft_doc::state_in_frame_at(&doc, &raw, component, owner_frame(s, &doc, component));
|
||||
let plane = sketch_plane(&doc, id, &state)?;
|
||||
let resolved = solidcraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
|
||||
let face_name = state.body(body).and_then(|body| solidcraft_doc::naming::face_names(body).get(index).cloned());
|
||||
let names = state.body(body).map(solidcraft_doc::naming::face_names).unwrap_or_default();
|
||||
|
||||
let mut sk = doc.sketch(id)?.clone();
|
||||
let link = sk.add_link_with_wires(LinkKind::Project, resolved.source, &resolved.geom, &resolved.wires)?;
|
||||
if let Some(entry) = sk.links.iter_mut().find(|entry| entry.id == link) {
|
||||
entry.face_name = face_name;
|
||||
let mut links = Vec::with_capacity(indices.len());
|
||||
for &index in indices {
|
||||
let resolved = solidcraft_doc::project::resolve_face_indices(&state, &plane, LinkKind::Project, body, &[index])?;
|
||||
let link = sk.add_link_with_wires(LinkKind::Project, resolved.source, &resolved.geom, &resolved.wires)?;
|
||||
if let Some(entry) = sk.links.iter_mut().find(|entry| entry.id == link) {
|
||||
entry.face_name = names.get(index).cloned();
|
||||
}
|
||||
links.push(link);
|
||||
}
|
||||
*doc.sketch_mut(id)? = sk;
|
||||
*s.doc_mut() = doc;
|
||||
Ok(link)
|
||||
Ok(links)
|
||||
}
|
||||
|
||||
/// Project the edges of the face a new sketch was created on (Fusion's default).
|
||||
|
||||
Reference in new issue
Block a user