mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* 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:
@@ -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);
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user