Restore Browser and source-body context during Extrude editing (#5573) (#5576)

* claim: #5573

* Keep Browser and source bodies in the Extrude edit context (#5573)

* fix: keep Browser feature rows after manual rollback

---------

Co-authored-by: Kirill Frolov <k.a.frolov@vk.team>
This commit is contained in:
fkirill
2026-10-30 22:03:03 +00:00
committed by GitHub
parent 8d22b3a41a
commit 1b927421f6
11 changed files with 199 additions and 20 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ pub fn map(app: &SolveApp) -> Option<AccessMap> {
return Some(m);
}
let st = app.session.world_state();
let meshes: Vec<_> = st.bodies.iter().filter(|b| app.session.body_visible(&b.name)).map(|b| b.mesh()).collect();
let meshes: Vec<_> = st.bodies.iter().filter(|b| app.body_visible(&b.name)).map(|b| b.mesh()).collect();
// Every new map gets a new version (the GPU uploads it once).
let version = VERSION.with(|v| {
v.set(v.get() + 1);
+2 -1
View File
@@ -1246,6 +1246,7 @@ fn model_feature_indices(app: &SolveApp, component: u64, sketches: bool) -> Vec<
.doc
.features
.iter()
.take(app.session.doc.marker.unwrap_or(app.session.doc.features.len()))
.enumerate()
.filter_map(|(i, f)| {
let kind = if sketches {
@@ -2260,7 +2261,7 @@ impl Entry {
label: b.name.clone(),
icon,
tip: Some(tip),
visible: !app.session.visibility.hidden_bodies.contains(&world),
visible: app.body_visible(&world),
selected: app.session.selection.iter().any(|s| matches!(s, Sel::Body { name } if name == &world)),
color: None,
sketch_state: None,
+53 -2
View File
@@ -53,17 +53,25 @@ pub(crate) fn reveal_in_history(app: &mut SolveApp, id: u64) {
app.tree.reveal_feature = Some(Reveal { id, path: Vec::new() });
}
/// Feature folders hide later rows only while editing; history always lists every feature.
fn shown_features(app: &SolveApp, compact: bool) -> usize {
let n = app.session.doc.features.len();
if !compact && app.dialog.as_ref().is_some_and(|d| d.editing.is_some()) { app.session.doc.marker.unwrap_or(n) } else { n }
}
pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path: &[u64], depth: usize, acts: &mut Actions) {
// Compact history: every feature of the component, sketches too, in history order with the
// marker, in place of the timeline.
let compact = app.preferences.history_in_browser;
let shown = shown_features(app, compact);
let has_features = match app.tree.model.as_ref().filter(|m| m.matches(&app.session) && !compact) {
Some(model) => model.features.get(&component).is_some_and(|features| !features.is_empty()),
Some(model) => model.features.get(&component).is_some_and(|features| features.iter().any(|&index| index < shown)),
None => app
.session
.doc
.features
.iter()
.take(shown)
.any(|feature| feature.component == component && (compact || !matches!(feature.kind, FeatureKind::Sketch { .. }))),
};
if !has_features {
@@ -102,6 +110,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
.doc
.features
.iter()
.take(shown)
.enumerate()
.filter_map(|(i, f)| (f.component == component && (compact || !matches!(f.kind, FeatureKind::Sketch { .. }))).then_some(i))
.collect(),
@@ -116,7 +125,7 @@ pub(super) fn rows(app: &mut SolveApp, ui: &mut egui::Ui, component: u64, path:
if id.is_some_and(|id| lit.contains(&id) || from_view.contains(&Some(id))) { Mark::Here } else { Mark::None }
};
if !compact {
for index in indices {
for index in indices.into_iter().take_while(|&index| index < shown) {
let _ = feature_row(app, ui, acts, &here, index, depth + 1, None, mark(app, index), None);
}
return;
@@ -419,6 +428,48 @@ mod tests {
assert!(h.app.session.visibility.hidden_bodies.is_empty());
}
#[test]
fn browser_feature_rows_stay_listed_after_manual_rollback() {
let mut h = Harness::new();
for name in ["Before", "After"] {
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"name":name})).unwrap();
}
h.app.run("timeline.roll_to", json!({"position":1})).unwrap();
for cache in ["current", "missing", "stale"] {
assert_eq!(feature_rows(&mut h, cache), ["Features", "Before", "After"], "{cache}");
}
h.app.run("timeline.roll_to", json!({})).unwrap();
for cache in ["current", "missing", "stale"] {
assert_eq!(feature_rows(&mut h, cache), ["Features", "Before", "After"], "{cache}");
}
}
#[test]
fn browser_feature_rows_follow_the_editor_and_restore_the_manual_marker() {
let mut h = Harness::new();
let mut ids = Vec::new();
for name in ["Before", "Edited", "After"] {
ids.push(h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"name":name})).unwrap()["feature"].as_u64().unwrap());
}
h.app.run("timeline.roll_to", json!({"position":2})).unwrap();
h.app.edit_feature(ids[1]);
assert!(h.app.dialog.as_ref().is_some_and(|d| d.editing.is_some()));
for cache in ["current", "missing", "stale"] {
assert_eq!(feature_rows(&mut h, cache), ["Features", "Before"], "{cache}");
assert_eq!(shown_features(&h.app, true), 3, "history keeps all rows during editing");
}
crate::dialogs::cancel(&mut h.app);
assert_eq!(h.app.session.doc.marker, Some(2));
for cache in ["current", "missing", "stale"] {
assert_eq!(feature_rows(&mut h, cache), ["Features", "Before", "Edited", "After"], "{cache}");
}
h.app.edit_feature(ids[0]);
for cache in ["current", "missing", "stale"] {
assert!(feature_rows(&mut h, cache).is_empty(), "{cache}: no feature precedes the editor");
}
crate::dialogs::cancel(&mut h.app);
}
#[test]
fn browser_feature_sketch_only_has_one_sketch_row_and_no_features_folder() {
let mut h = Harness::new();
+3
View File
@@ -38,6 +38,9 @@ impl Model {
if !matches!(feature.kind, FeatureKind::Sketch { .. }) {
features.entry(feature.component).or_default().push(i);
}
if session.doc.marker.is_some_and(|marker| i >= marker) {
continue;
}
let index = match feature.kind {
FeatureKind::Sketch { .. } => &mut sketches,
FeatureKind::ConstructionPlane { .. } | FeatureKind::ConstructionAxis { .. } | FeatureKind::ConstructionPoint { .. } => &mut planes,
+29
View File
@@ -410,6 +410,35 @@ fn browser_rows_still_fold_or_select_when_move_copy_is_not_open() {
assert!(h.app.tree.collapsed.contains("c0/bodies"));
}
#[test]
fn feature_editor_browser_rows_follow_the_marker_and_return_after_cancel() {
let mut h = crate::scenario::Harness::new();
h.app.run("sketch.create", json!({"plane":"XY","name":"Base"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[10,10]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("solid.extrude", json!({"sketch":"Base","distance":10,"name":"Depth"})).unwrap();
let feature = h.app.session.doc.find_feature("Depth").unwrap().id;
h.app.run("sketch.create", json!({"plane":"YZ","name":"Later sketch"})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("construct.plane.offset", json!({"base":"XY","offset":20,"name":"Later plane"})).unwrap();
super::prepare_model(&mut h.app);
assert_eq!(super::entries(&h.app, 0, "sketches").len(), 2);
assert_eq!(super::entries(&h.app, 0, "construction").len(), 1);
h.app.edit_feature(feature);
// Both the fallback queries and the cached index must follow rollback.
assert_eq!(super::entries(&h.app, 0, "sketches").iter().map(|e| e.label.as_str()).collect::<Vec<_>>(), ["Base"]);
assert!(super::entries(&h.app, 0, "construction").is_empty());
assert!(super::entries(&h.app, 0, "bodies").is_empty());
super::prepare_model(&mut h.app);
assert_eq!(super::entries(&h.app, 0, "sketches").len(), 1);
assert!(super::entries(&h.app, 0, "construction").is_empty());
crate::dialogs::cancel(&mut h.app);
super::prepare_model(&mut h.app);
assert_eq!(super::entries(&h.app, 0, "sketches").len(), 2);
assert_eq!(super::entries(&h.app, 0, "construction").len(), 1);
assert_eq!(super::entries(&h.app, 0, "bodies").len(), 1);
}
/// The sketch being edited is marked in the user's language, not with an English "(editing)",
/// and its default name reads as the browser shows it elsewhere.
#[test]
+27
View File
@@ -464,6 +464,9 @@ pub struct Dialog {
pub error: Option<String>,
/// Editing an existing feature (its id, and the timeline marker to restore afterwards).
pub editing: Option<(u64, Option<usize>)>,
/// Hidden face-profile sources shown only for this editor's lifetime. Visibility itself is
/// unchanged, so OK, Cancel, failed apply and replacing the dialog cannot leak an override.
pub(crate) edit_source_bodies: Vec<String>,
/// A user pick or clear makes the profile selector explicit, even if all are reselected.
pub profile_selection_changed: bool,
/// Give the on-canvas value box the keyboard (and select its text) on the next frame.
@@ -736,6 +739,7 @@ impl Dialog {
active: 0,
error: None,
editing: None,
edit_source_bodies: Vec::new(),
profile_selection_changed: false,
focus: true,
value_focus: None,
@@ -6499,6 +6503,29 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
crate::length_input::extra(&mut d.extra, |e| crate::length_input::stored(&s.doc, e));
}
if let FeatureKind::Extrude { sketch, .. } = &f.kind
&& let Some(FeatureKind::Sketch { plane, .. }) = s.doc.feature(sketch.0).map(|f| &f.kind)
{
let mut plane = plane;
// Plane nesting comes from design files: bound the walk just like other reference walks.
for _ in 0..64 {
match plane {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } | PlaneRef::Flipped { base, .. } => plane = base,
PlaneRef::Face { anchor: Some(anchor), .. } => {
let model = s.model.state();
if let Some(body) = model.body(&anchor.target.body) {
let owner = s.doc.model_body_component(body);
let name = s.doc.world_body_name(&model, &body.name, owner, &anchor.path);
if st.body(&name).is_some() && !s.body_visible(&name) {
d.edit_source_bodies.push(name);
}
}
break;
}
_ => break,
}
}
}
d.editing = Some((id, marker));
d.active = 0;
d.error = None;
+52
View File
@@ -521,3 +521,55 @@ fn without_value_boxes_in_the_view_the_distance_is_typed_in_the_window() {
assert!((dialog_distance(&h) - 30.0).abs() < 0.5, "{}", dialog_distance(&h));
assert!(h.ctx.memory(|m| m.area_rect(egui::Id::new("sc_canvas_area"))).is_none(), "a value box in the view");
}
#[test]
fn editing_face_extrusion_temporarily_shows_its_hidden_source_body() {
for finish in ["Cancel", "OK"] {
let mut h = Harness::new();
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Source"})).unwrap();
h.app.run("solid.box", json!({"length":2,"width":2,"height":2,"corner":[30,0,0],"body_name":"Other"})).unwrap();
h.app
.run("solid.extrude", json!({"face":[5,5,10],"face_body":"Source","distance":5,"operation":"new","body_name":"Extension","name":"Depth"}))
.unwrap();
h.app.session.visibility.hidden_bodies.push("Source".into());
let visibility = h.app.session.visibility.clone();
let undo = h.app.session.undo.len();
open(&mut h);
assert!(h.app.body_visible("Source"));
assert!(!h.app.session.body_visible("Source"), "the editor must not change persistent visibility");
assert!(h.app.body_visible("Other"));
assert!(h.app.session.world_state().body("Extension").is_none(), "the feature's result is rolled off");
assert_eq!(h.app.session.undo.len(), undo);
h.app.session.visibility.hidden_bodies.push("Other".into());
h.step(&json!({"widget":finish})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.session.log);
assert!(!h.app.body_visible("Source"));
assert!(!h.app.body_visible("Other"), "unrelated visibility edits are preserved");
assert_eq!(h.app.session.visibility.hidden_sketches, visibility.hidden_sketches);
assert_eq!(h.app.session.visibility.shown_sketches, visibility.shown_sketches);
assert!(h.app.session.world_state().body("Extension").is_some());
}
}
#[test]
fn failed_face_extrusion_edit_keeps_context_until_escape_and_preserves_visibility() {
let mut h = Harness::new();
let owner = h.app.run("component.create", json!({"name":"Part"})).unwrap();
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Source"})).unwrap();
h.app.run("solid.extrude", json!({"face":[5,5,10],"face_body":"Source","distance":5,"operation":"new","name":"Depth"})).unwrap();
let path = vec![owner["occurrence"].as_u64().unwrap()];
h.app.session.visibility.hidden_occurrences.push(path);
let visibility = h.app.session.visibility.clone();
assert!(!h.app.body_visible("Source"));
open(&mut h);
assert!(h.app.body_visible("Source"), "a hidden parent must not hide the source during editing");
h.step(&json!({"key":"A","cmd":true})).unwrap();
h.step(&json!({"text":"missing_length"})).unwrap();
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_some());
assert!(h.app.body_visible("Source"));
h.step(&json!({"key":"Escape"})).unwrap();
assert!(h.app.dialog.is_none());
assert!(!h.app.body_visible("Source"));
assert_eq!(h.app.session.visibility, visibility);
}
+6
View File
@@ -2123,6 +2123,12 @@ impl SolveApp {
let _ = self.run_heavy("timeline.roll_to", marker.map(|m| json!({ "position": m })).unwrap_or_else(|| json!({})));
}
/// Visibility in the editor's scene: hidden face-profile sources are temporarily shown
/// without changing the session's visibility or either history stack.
pub(crate) fn body_visible(&self, name: &str) -> bool {
self.session.body_visible(name) || self.dialog.as_ref().is_some_and(|d| d.edit_source_bodies.iter().any(|source| source == name))
}
/// Everything shown as selected: the selection plus the open dialog's inputs.
pub fn highlighted(&self) -> Vec<solidcraft_engine::Sel> {
let mut v = self.session.selection.clone();
+3 -3
View File
@@ -206,7 +206,7 @@ fn face_candidates(
) -> Vec<Cand> {
let mut out = Vec::new();
for (i, b) in st.bodies.iter().enumerate() {
if !app.session.body_visible(&b.name) || replaced.contains(&b.name) {
if !app.body_visible(&b.name) || replaced.contains(&b.name) {
continue;
}
let ghosted = crate::viewport::body_faded(app, b, active_bodies);
@@ -329,7 +329,7 @@ pub(crate) fn body_cut_at(
) -> Option<String> {
let walk_eps = 1e-6 * (1.0 + crate::viewport::scene_radius(app));
for (i, b) in st.bodies.iter().enumerate() {
if !app.session.body_visible(&b.name) || replaced.contains(&b.name) {
if !app.body_visible(&b.name) || replaced.contains(&b.name) {
continue;
}
if !include_ghosted && crate::viewport::body_faded(app, b, active_bodies) {
@@ -390,7 +390,7 @@ fn near_edges(
let mut best_e: Option<EdgeHit> = None;
let mut best_v: Option<VertexHit> = None;
for (i, b) in st.bodies.iter().enumerate() {
if !app.session.body_visible(&b.name) || replaced.contains(&b.name) {
if !app.body_visible(&b.name) || replaced.contains(&b.name) {
continue;
}
if !include_ghosted && crate::viewport::body_faded(app, b, active_bodies) {
+16 -13
View File
@@ -569,6 +569,7 @@ fn scene_key(app: &SolveApp) -> u64 {
app.cam.up.hash(&mut h);
(app.cam.pitch > 0.0).hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.dialog.as_ref().map(|d| &d.edit_source_bodies).hash(&mut h);
app.session.visibility.hidden_occurrences.hash(&mut h);
app.session.visibility.hidden_sketches.hash(&mut h);
app.session.visibility.shown_sketches.hash(&mut h);
@@ -865,7 +866,7 @@ fn update_refinement(app: &mut SolveApp, ctx: &egui::Context, proj: &Proj) {
if b.name != *name || b.body.is_mesh() {
continue;
}
let shown = app.session.body_visible(&b.name) && !app.preview.effective_replaced().contains(&b.name);
let shown = app.body_visible(&b.name) && !app.preview.effective_replaced().contains(&b.name);
let on_screen = match screen_box(proj, bx, 0.0).filter(|_| shown) {
Some(r) => r.intersects(proj.rect).then(|| f64::from(r.width().max(r.height())) * ppp),
// A corner behind the eye: the view is inside or right by the box.
@@ -1426,7 +1427,7 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
let markers = app.preferences.vertex_markers;
let hidden_col = c4(tk.text.gamma_multiply(0.35));
let shown: Vec<&solidcraft_engine::doc::ModelBody> =
st.bodies.iter().filter(|b| app.session.body_visible(&b.name) && !app.preview.effective_replaced().contains(&b.name)).collect();
st.bodies.iter().filter(|b| app.body_visible(&b.name) && !app.preview.effective_replaced().contains(&b.name)).collect();
let mut arena = app.viewport.palette.lock().unwrap_or_else(|e| e.into_inner());
let shown_names: Vec<&str> = shown.iter().map(|b| b.name.as_str()).collect();
arena.sync(shown_names.iter().copied());
@@ -1619,7 +1620,7 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
let mut kept = app.viewport.section_index.lock().map(|mut k| std::mem::take(&mut *k)).unwrap_or_default();
let mut made = std::collections::HashMap::with_capacity(kept.len());
let normal = [n.x.to_bits(), n.y.to_bits(), n.z.to_bits()];
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name)) {
for b in st.bodies.iter().filter(|b| app.body_visible(&b.name)) {
let mesh = b.mesh();
let key = (std::sync::Arc::as_ptr(&mesh) as usize, normal);
let index = match kept.remove(&key).filter(|(m, i)| std::sync::Arc::ptr_eq(m, &mesh) && i.normal() == n) {
@@ -1808,7 +1809,7 @@ fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &std::sync::Arc<solidcra
let mut b = solidcraft_engine::geom::Aabb3::EMPTY;
let boxes = body_bounds(app, st);
for (x, (_, bb)) in st.bodies.iter().zip(boxes.iter()) {
if bb.is_empty() || !app.session.body_visible(&x.name) {
if bb.is_empty() || !app.body_visible(&x.name) {
continue;
}
for i in 0..8 {
@@ -2233,6 +2234,7 @@ pub(crate) fn pick_cached(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit
crate::hole_place::snapping(app).hash(&mut h);
crate::drag_snap::suppressed().hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.dialog.as_ref().map(|d| &d.edit_source_bodies).hash(&mut h);
app.session.visibility.hidden_occurrences.hash(&mut h);
origin_point_shown(app).hash(&mut h);
app.view_fov().to_bits().hash(&mut h);
@@ -2522,7 +2524,7 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
// Bounds retain the state's body order. Looking each one up by name made both passes
// quadratic for assemblies with many small parts.
for (i, b) in st.bodies.iter().enumerate() {
if !through && !app.session.body_visible(&b.name) {
if !through && !app.body_visible(&b.name) {
continue;
}
// Bodies whose box the ray misses (or enters behind the best hit so far) are skipped.
@@ -2590,7 +2592,7 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
let mut bestv = Near::new(all);
let mut beste = Near::new(all);
for (i, b) in st.bodies.iter().enumerate() {
if !through && !app.session.body_visible(&b.name) {
if !through && !app.body_visible(&b.name) {
continue;
}
let mut body_slack = slack;
@@ -2776,7 +2778,7 @@ fn feature_parts(app: &SolveApp, id: u64, shown_only: bool) -> Vec<Sel> {
let st = crate::open_meshing::shown_world(app);
let mut out = Vec::new();
for b in &st.bodies {
if shown_only && (!app.session.body_visible(&b.name) || app.preview.effective_replaced().contains(&b.name)) {
if shown_only && (!app.body_visible(&b.name) || app.preview.effective_replaced().contains(&b.name)) {
continue;
}
if b.feature == id {
@@ -2824,7 +2826,7 @@ fn displayed_selection(app: &SolveApp, item: &Sel) -> Vec<Sel> {
.occurrence_bodies(path, true)
.unwrap_or_default()
.into_iter()
.filter(|name| app.session.body_visible(name))
.filter(|name| app.body_visible(name))
.map(|name| Sel::Body { name })
.collect(),
_ => grouped_selection(&app.session, item),
@@ -3418,6 +3420,7 @@ fn highlight_geometry_key(app: &SolveApp) -> u64 {
app.preferences.reference_geometry_through_bodies.hash(&mut h);
app.session.visibility.hidden_origin.hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.dialog.as_ref().map(|d| &d.edit_source_bodies).hash(&mut h);
app.session.visibility.hidden_occurrences.hash(&mut h);
app.session.visibility.hidden_folders.hash(&mut h);
// How strongly print faces are tinted (Preferences › General › Slicer).
@@ -3810,14 +3813,14 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
match x {
Sel::Face { body, index, .. } => {
// A live preview stands in for the body: its old faces would fight with it.
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !replaced(body)) {
if let Some(b) = st.body(body).filter(|_| app.body_visible(body) && !replaced(body)) {
face_tris(&mut feedback, &shown(b), *index, c4(t.sel_face), false);
hidden_face_outline(&mut feedback, &drawn(b), *index, t.sel_edge);
}
}
Sel::Body { name } => {
if let Some(b) = st.body(name)
&& app.session.body_visible(name)
&& app.body_visible(name)
{
let lit = WholeBody { selected: true, colour: c4(t.body_selected) };
if !light_whole_body(app, b, &active_bodies, lit, &body_slots, &mut feedback, &mut selected_instances) {
@@ -3829,7 +3832,7 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
}
Sel::Edge { body, index, point } => {
if let Some(b) = st.body(body)
&& app.session.body_visible(body)
&& app.body_visible(body)
&& !moving(body)
{
let m = drawn(b);
@@ -3923,7 +3926,7 @@ pub(crate) fn build_highlight(app: &SolveApp) -> GpuScene {
selected_faces.entry(body.as_str()).or_default().insert(*index);
}
}
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name) && !app.preview.effective_replaced().contains(&b.name)) {
for b in st.bodies.iter().filter(|b| app.body_visible(&b.name) && !app.preview.effective_replaced().contains(&b.name)) {
let whole = hovered_whole.contains(b.name.as_str());
let faces: Vec<usize> =
if whole { (0..b.body.face_count()).collect() } else { hovered_faces.get(b.name.as_str()).cloned().unwrap_or_default() };
@@ -5874,7 +5877,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
let active_bodies =
app.session.active_occurrence_path.as_ref().filter(|_| scoped_bodies).and_then(|p| app.session.occurrence_bodies(p, true).ok());
for b in &st.bodies {
if !app.session.body_visible(&b.name) || (scoped_bodies && body_faded(app, b, &active_bodies)) {
if !app.body_visible(&b.name) || (scoped_bodies && body_faded(app, b, &active_bodies)) {
continue;
}
let m = b.mesh();
+7
View File
@@ -236,6 +236,13 @@ across). Large bodies are sampled more coarsely, at about `max_points` (eight mi
each; `sampling.spacing_mm` says what was used, and a larger `max_points` (up to 50 million)
samples them finer, at the cost of time.
When editing an Extrude in the UI, the Browser's bodies, sketches, construction objects and
feature rows follow the history marker before that feature. Later objects return when editing
ends. Rolling back manually keeps feature rows listed, so they remain available to edit.
A hidden body supplying the profile's face is shown temporarily during the edit, including
when a folder or component hides it. OK, Cancel and Escape end that display override; the body's
saved visibility and unrelated visibility changes are preserved.
## Editing a feature from a script
For `solid.pattern.circular`, `axis` accepts `X`, `Y`, `Z`, `{origin, dir}`, or an edge or