Extrude: a symmetric distance is the total thickness (#1260)
A symmetric solid extrude of 10 mm is now 10 mm thick, 5 mm each side of the sketch plane, and switching from One Side to Symmetric keeps the thickness entered. Under format 21 the extent records its convention; designs saved without it keep the per-side reading, so their geometry and parameters don't change. Fixes #1236. Co-authored-by: Karl-VonRichter <Karl-VonRichter@users.noreply.github.com>
This commit is contained in:
1 parent
faeeff1c64
commit
1ec33fcdc7
19 files changed
+323
-18
No files matched your search
@@ -338,6 +338,16 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
||||
_ => 1.0,
|
||||
};
|
||||
let symmetric = to_span.is_none() && matches!(d.kind, Kind::Extrude { direction: 2, .. });
|
||||
let symmetric_scale = if symmetric {
|
||||
d.extra
|
||||
.get("convention")
|
||||
.cloned()
|
||||
.and_then(|v| serde_json::from_value::<solidcraft_engine::doc::ExtrudeConvention>(v).ok())
|
||||
.unwrap_or(solidcraft_engine::doc::ExtrudeConvention::TotalDistance)
|
||||
.symmetric_half_distance(1.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// Extrudes and moves go either way; radii and thicknesses stay positive.
|
||||
let signed = matches!(
|
||||
d.kind,
|
||||
@@ -416,17 +426,19 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
||||
if let (Some(n), Some(l)) = (dir, len.filter(|_| kind == ValueKind::Length)) {
|
||||
let n = n * sign;
|
||||
let baseline = to_span.map(f64::abs).unwrap_or(0.0);
|
||||
let mut shown_value = format_len(l + baseline);
|
||||
// Through All already supplies a reach per side; dragging switches to an entered distance.
|
||||
let half = if all_span.is_some() { l } else { l * symmetric_scale };
|
||||
let mut shown_value = format_len(half + baseline);
|
||||
let original_shown = shown_value.clone();
|
||||
let dragged = if to_span.is_some() { &mut shown_value } else { &mut *value };
|
||||
if let Some(ts) = drag_arrow(ui, painter, proj, base, bs, n, l + baseline, signed, egui::Id::new("sc_manipulator"), half_height, dragged) {
|
||||
if to_span.is_some()
|
||||
let dragged = if to_span.is_some() || symmetric_scale != 1.0 { &mut shown_value } else { &mut *value };
|
||||
if let Some(ts) = drag_arrow(ui, painter, proj, base, bs, n, half + baseline, signed, egui::Id::new("sc_manipulator"), half_height, dragged) {
|
||||
if (to_span.is_some() || symmetric_scale != 1.0)
|
||||
&& shown_value != original_shown
|
||||
&& let Ok(v) = app.session.doc.eval(&shown_value, ValueKind::Length)
|
||||
{
|
||||
*value = format_len(v - baseline);
|
||||
*value = format_len((v - baseline) / symmetric_scale);
|
||||
}
|
||||
if symmetric && let Some(bt) = proj.to_screen(base - n * l) {
|
||||
if symmetric && let Some(bt) = proj.to_screen(base - n * half) {
|
||||
arrow(painter, bs, bt, t.manipulator);
|
||||
}
|
||||
painter.circle(bs, 3.5, t.manipulator, Stroke::new(1.0, Color32::WHITE));
|
||||
|
||||
@@ -2087,6 +2087,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
combo(ui, "ex_dir", &["One Side", "Two Sides", "Symmetric"], &mut pos);
|
||||
if pos != order.iter().position(|o| o == direction).unwrap_or(0) {
|
||||
*direction = order.get(pos).copied().unwrap_or(0);
|
||||
// Selecting Symmetric applies the current total-distance semantics.
|
||||
d.extra.insert("convention".into(), json!(solidcraft_engine::doc::ExtrudeConvention::TotalDistance));
|
||||
// An explicit direction ends the automatic choices (#1257).
|
||||
smart.manual_dir = true;
|
||||
*auto_op = false;
|
||||
@@ -4194,6 +4196,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
wall_side: thin.as_ref().map_or(0, |t| WALL_SIDES.iter().position(|w| ThinSide::parse(w) == Some(t.side)).unwrap_or(0)),
|
||||
wall2: thin.as_ref().and_then(|t| t.thickness2.clone()).unwrap_or_else(|| "1 mm".into()),
|
||||
};
|
||||
d.extra.insert("convention".into(), json!(extent.convention));
|
||||
// Extent references belong to the extrude's authoring frame, including free
|
||||
// points. They are not body-local picks (a reference may be on another body).
|
||||
let frame = feature_frame(s, f.component);
|
||||
|
||||
@@ -603,7 +603,14 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
|
||||
Direction::Symmetric => 1,
|
||||
Direction::Negative => 2,
|
||||
};
|
||||
(Surf::Extrude { distance: extent.distance.clone(), direction }, vec![sels])
|
||||
let k = Surf::Extrude { distance: extent.distance.clone(), direction };
|
||||
let mut ins = inputs(&k);
|
||||
if let Some(input) = ins.first_mut() {
|
||||
input.items = sels;
|
||||
}
|
||||
let mut extra = Map::new();
|
||||
extra.insert("convention".into(), json!(extent.convention));
|
||||
return Some((Kind::Surface(k), ins, extra));
|
||||
}
|
||||
FeatureKind::Patch { sketch, edges, .. } => {
|
||||
let mut sels: Vec<Sel> = edges.iter().filter_map(edge).collect();
|
||||
|
||||
@@ -46,6 +46,7 @@ fn unchanged_actual_editor_preserves_all_extents_and_parameter_identities() {
|
||||
json!({"direction":"negative","distance":"10"}),
|
||||
json!({"direction":"negative","distance":"-10 mm"}),
|
||||
json!({"direction":"symmetric","distance":"5 mm"}),
|
||||
json!({"direction":"symmetric","distance":"5 mm","convention":"legacy_per_side"}),
|
||||
json!({"distance":"10 mm","distance2":"3 mm"}),
|
||||
json!({"from":[0,0,5],"start_offset":"0 mm"}),
|
||||
json!({"to":[0,0,10],"to_offset":"3 mm"}),
|
||||
@@ -295,3 +296,68 @@ fn dragging_to_update_value_reselects_whole_value_box_text() {
|
||||
assert_eq!(distance, "15 mm");
|
||||
assert_eq!(selection(&h), (0, 5), "the drag grew the text length; the selection must cover all of it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetric_total_distance_preview_handle_drag_apply_and_reopen_agree() {
|
||||
for (convention, cap, total_after_drag) in [("total_distance", 50.0, 120.0), ("legacy_per_side", 100.0, 60.0)] {
|
||||
let mut h = make(json!({"direction":"symmetric","distance":"100 mm","convention":convention}));
|
||||
h.call("ui.view", json!({"view":"fit"}));
|
||||
h.frames(4);
|
||||
open(&mut h);
|
||||
let expected_tip = h.at(&json!({"world":[5,5,cap]})).unwrap();
|
||||
let handle = h.at(&json!({"handle":"arrow"})).unwrap();
|
||||
assert!((handle[0] - expected_tip[0]).hypot(handle[1] - expected_tip[1]) < 1.0, "{convention}: {handle:?} expected {expected_tip:?}");
|
||||
h.step(&json!({"drag":[{"handle":"arrow"},{"world":[5,5,60]}]})).unwrap();
|
||||
let d = h.app.dialog.as_ref().unwrap();
|
||||
let crate::dialogs::Kind::Extrude { distance, .. } = &d.kind else { panic!("extrude") };
|
||||
assert_eq!(h.app.session.doc.eval(distance, solidcraft_engine::doc::expr::Kind::Length).unwrap(), total_after_drag);
|
||||
let commands = crate::dialogs::apply_commands(&h.app, d).unwrap();
|
||||
let preview = h.app.session.preview(&commands).unwrap();
|
||||
let preview_bounds = preview.after.body("Main").unwrap().mesh().bounds();
|
||||
assert!((preview_bounds.min.z + 60.0).abs() < 1e-7 && (preview_bounds.max.z - 60.0).abs() < 1e-7);
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), preview_bounds);
|
||||
let path = h.dir.join("symmetric.solidcraft");
|
||||
h.app.run("file.save_as", json!({"path":path})).unwrap();
|
||||
h.app.run("doc.open", json!({"path":path})).unwrap();
|
||||
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), preview_bounds);
|
||||
open(&mut h);
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), preview_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_one_side_to_symmetric_in_actual_editor_keeps_total_thickness() {
|
||||
for convention in ["total_distance", "legacy_per_side"] {
|
||||
let mut h = make(json!({"convention":convention}));
|
||||
open(&mut h);
|
||||
h.step(&json!({"widget":"One Side"})).unwrap();
|
||||
h.step(&json!({"widget":"Symmetric"})).unwrap();
|
||||
let commands = crate::dialogs::apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
|
||||
let preview = h.app.session.preview(&commands).unwrap();
|
||||
let bounds = preview.after.body("Main").unwrap().mesh().bounds();
|
||||
assert!((bounds.min.z + 5.0).abs() < 1e-7 && (bounds.max.z - 5.0).abs() < 1e-7);
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
assert_eq!(h.app.session.world_state().body("Main").unwrap().mesh().bounds(), bounds);
|
||||
assert_eq!(extent(&h).convention, solidcraft_engine::doc::ExtrudeConvention::TotalDistance);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetric_through_all_handle_drag_switches_to_total_distance() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":100,"corner":[0,0,-50],"body_name":"Reference"})).unwrap();
|
||||
let mut h = make_in(h, json!({"direction":"symmetric","through_all":true}));
|
||||
h.call("ui.view", json!({"view":"fit"}));
|
||||
h.frames(4);
|
||||
open(&mut h);
|
||||
h.step(&json!({"drag":[{"handle":"arrow"},{"world":[5,5,60]}]})).unwrap();
|
||||
let d = h.app.dialog.as_ref().unwrap();
|
||||
let crate::dialogs::Kind::Extrude { distance, all, .. } = &d.kind else { panic!("extrude") };
|
||||
assert!(!all);
|
||||
assert_eq!(h.app.session.doc.eval(distance, solidcraft_engine::doc::expr::Kind::Length).unwrap(), 120.0);
|
||||
h.step(&json!({"widget":"OK"})).unwrap();
|
||||
let bounds = h.app.session.world_state().body("Main").unwrap().mesh().bounds();
|
||||
assert!((bounds.min.z + 60.0).abs() < 1e-7 && (bounds.max.z - 60.0).abs() < 1e-7);
|
||||
}
|
||||
Reference in new issue
Block a user