Flat pattern gets its own pan and zoom through the shared navigation (#1484)
The flat pattern now keeps a 2D view of its own, driven by navigation::handle with orbit off, so pan, zoom to the cursor and reverse zoom follow the navigation preset. Input over it no longer reaches the Design camera, and Fit and Home frame the pattern. Closes #1483.
This commit is contained in:
1 parent
6ad391fd75
commit
e20cf1858a
5 files changed
+297
-17
No files matched your search
@@ -147,7 +147,7 @@ fn shallow_pocket_overlay_labels_depth_without_drawing_a_through_void() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("flat-pocket-test")));
|
||||
assert!(overlay(&app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
assert!(overlay(&mut app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let texts: Vec<&str> = output
|
||||
@@ -200,7 +200,7 @@ fn flat_pattern_islands_fit_before_the_actual_command_panel() {
|
||||
ui.label("Flat size: 50 × 10 × 2.5 mm");
|
||||
});
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Background, egui::Id::new("island-fit")));
|
||||
assert!(overlay(&app, &painter, Rect::from_min_max(pos2(250.0, 126.0), pos2(1400.0, 832.0)), &dialog));
|
||||
assert!(overlay(&mut app, &painter, Rect::from_min_max(pos2(250.0, 126.0), pos2(1400.0, 832.0)), &dialog));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
@@ -239,7 +239,7 @@ fn coincident_top_and_bottom_pocket_depths_share_a_readable_annotation() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("coincident-pocket-test")));
|
||||
assert!(overlay(&app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
assert!(overlay(&mut app, &painter, Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0)), &dialog));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let labels: Vec<_> = output
|
||||
@@ -472,3 +472,208 @@ fn convert_takes_the_fixed_face_and_the_bend_edges() {
|
||||
assert_eq!(bends(&mut app), Some(4));
|
||||
assert!(app.session.model.results.iter().all(|r| r.error.is_none()), "{:?}", app.session.model.results);
|
||||
}
|
||||
|
||||
// ---- flat pattern navigation ----
|
||||
|
||||
fn flat_app(preset: &str) -> SolveApp {
|
||||
let mut app = plate();
|
||||
app.run("sheet.flange", json!({"sketch": "Base"})).unwrap();
|
||||
let flat = app.session.execute("sheet.flat_pattern", &json!({})).unwrap();
|
||||
app.start("sheet.flat_pattern");
|
||||
let Some(Kind::Sheet(Sm::FlatPattern { data, .. })) = app.dialog.as_mut().map(|d| &mut d.kind) else { panic!("flat dialog") };
|
||||
*data = Some(Ok(flat));
|
||||
app.viewport.no_pixels = true;
|
||||
app.preferences.nav = preset.into();
|
||||
app.preferences.zoom_to_cursor = true;
|
||||
app
|
||||
}
|
||||
|
||||
/// Same view in the flat plane: the Design viewport may anchor a zoom on the model's surface
|
||||
/// (a little out of the plane), the flat view anchors on its plane.
|
||||
fn same_view(a: &solidcraft_engine::render::Camera, b: &solidcraft_engine::render::Camera) -> bool {
|
||||
(a.target.x - b.target.x).abs() < 1e-3
|
||||
&& (a.target.y - b.target.y).abs() < 1e-3
|
||||
&& (a.distance - b.distance).abs() < 1e-9
|
||||
&& a.yaw == b.yaw
|
||||
&& a.pitch == b.pitch
|
||||
}
|
||||
|
||||
fn frame(app: &mut SolveApp, ctx: &egui::Context, time: &mut f64, mods: egui::Modifiers, events: Vec<egui::Event>) {
|
||||
*time += 1.0 / 60.0;
|
||||
let events = std::iter::once(egui::Event::ModifiersChanged(mods)).chain(events).collect();
|
||||
let raw = egui::RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))),
|
||||
time: Some(*time),
|
||||
focused: true,
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = ctx.run_ui(raw, |ui| crate::viewport::show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
/// The same pan and zoom input moves the flat pattern's view exactly as it moves the Design
|
||||
/// viewport's camera, under every navigation preset, with and without reverse zoom and zoom to the
|
||||
/// cursor. The flat view never orbits.
|
||||
#[test]
|
||||
fn flat_pattern_pans_and_zooms_like_the_design_viewport() {
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
let none = Modifiers::NONE;
|
||||
let chords = [none, Modifiers::SHIFT, Modifiers::CTRL, Modifiers::ALT, Modifiers::SHIFT | Modifiers::ALT];
|
||||
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
|
||||
for (reverse, to_cursor) in [(false, true), (true, true), (false, false)] {
|
||||
let (flat_ctx, design_ctx) = (egui::Context::default(), egui::Context::default());
|
||||
let mut flat = flat_app(preset);
|
||||
let mut design = flat_app(preset);
|
||||
design.dialog = None;
|
||||
for app in [&mut flat, &mut design] {
|
||||
app.preferences.zoom_reverse = reverse;
|
||||
app.preferences.zoom_to_cursor = to_cursor;
|
||||
}
|
||||
let (mut ft, mut dt) = (0.0, 0.0);
|
||||
frame(&mut flat, &flat_ctx, &mut ft, none, vec![]);
|
||||
frame(&mut flat, &flat_ctx, &mut ft, none, vec![]);
|
||||
assert!(flat.flat_view.fitted);
|
||||
// The Design viewport starts from the flat view's camera: top-down, orthographic.
|
||||
design.cam = flat.flat_view.cam;
|
||||
design.ui.perspective = false;
|
||||
let design_before = flat.cam;
|
||||
let (mut panned, mut zoomed) = (0, 0);
|
||||
let at = pos2(300.0, 250.0);
|
||||
for button in [PointerButton::Primary, PointerButton::Middle, PointerButton::Secondary] {
|
||||
for mods in chords {
|
||||
let mode = crate::prefs::nav_mode(&flat, button.into_nav(), mods);
|
||||
if !matches!(mode, Some(crate::viewport::NavMode::Pan | crate::viewport::NavMode::Zoom)) {
|
||||
continue;
|
||||
}
|
||||
let before = flat.flat_view.cam;
|
||||
let mut both = |events: Vec<Event>, mods: Modifiers, flat: &mut SolveApp, design: &mut SolveApp| {
|
||||
frame(flat, &flat_ctx, &mut ft, mods, events.clone());
|
||||
frame(design, &design_ctx, &mut dt, mods, events);
|
||||
};
|
||||
both(vec![Event::PointerMoved(at)], mods, &mut flat, &mut design);
|
||||
both(vec![Event::PointerButton { pos: at, button, pressed: true, modifiers: mods }], mods, &mut flat, &mut design);
|
||||
let mut p = at;
|
||||
for _ in 0..5 {
|
||||
p += vec2(14.0, -9.0);
|
||||
both(vec![Event::PointerMoved(p)], mods, &mut flat, &mut design);
|
||||
}
|
||||
both(vec![Event::PointerButton { pos: p, button, pressed: false, modifiers: mods }], mods, &mut flat, &mut design);
|
||||
for _ in 0..10 {
|
||||
both(vec![], none, &mut flat, &mut design);
|
||||
}
|
||||
assert!(same_view(&flat.flat_view.cam, &design.cam), "{preset}: {button:?} drag with {mods:?} ({mode:?})");
|
||||
assert_ne!(flat.flat_view.cam, before, "{preset}: {button:?} drag with {mods:?} moves the flat view");
|
||||
if mode == Some(crate::viewport::NavMode::Pan) {
|
||||
panned += 1
|
||||
} else {
|
||||
zoomed += 1
|
||||
}
|
||||
flat.flat_view.cam = before;
|
||||
design.cam = before;
|
||||
}
|
||||
}
|
||||
assert!(panned + zoomed >= 1, "{preset}: the preset has a pan or zoom drag");
|
||||
// The wheel zooms (to the cursor when asked), identically.
|
||||
let before = flat.flat_view.cam;
|
||||
let wheel = Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: vec2(0.0, 3.0), modifiers: none, phase: egui::TouchPhase::Move };
|
||||
for (app, ctx, t) in [(&mut flat, &flat_ctx, &mut ft), (&mut design, &design_ctx, &mut dt)] {
|
||||
frame(app, ctx, t, none, vec![Event::PointerMoved(at)]);
|
||||
frame(app, ctx, t, none, vec![wheel.clone()]);
|
||||
for _ in 0..30 {
|
||||
frame(app, ctx, t, none, vec![]);
|
||||
}
|
||||
}
|
||||
assert_ne!(flat.flat_view.cam.distance, before.distance, "{preset}: the wheel zooms the flat view");
|
||||
assert!(same_view(&flat.flat_view.cam, &design.cam), "{preset} reverse {reverse} cursor {to_cursor}: wheel zoom");
|
||||
assert_eq!(flat.flat_view.cam.distance < before.distance, !reverse, "{preset}: reverse zoom flips the wheel");
|
||||
if to_cursor {
|
||||
assert!(flat.flat_view.cam.target.dist(before.target) > 1e-6, "{preset}: zoom to the cursor moves the target");
|
||||
}
|
||||
assert_eq!(flat.flat_view.cam.pitch, before.pitch);
|
||||
assert_eq!(flat.flat_view.cam.yaw, before.yaw);
|
||||
assert_eq!(flat.cam, design_before, "{preset}: the Design camera is untouched");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoNav {
|
||||
fn into_nav(self) -> crate::prefs::NavButton;
|
||||
}
|
||||
impl IntoNav for egui::PointerButton {
|
||||
fn into_nav(self) -> crate::prefs::NavButton {
|
||||
match self {
|
||||
egui::PointerButton::Middle => crate::prefs::NavButton::Middle,
|
||||
egui::PointerButton::Secondary => crate::prefs::NavButton::Right,
|
||||
_ => crate::prefs::NavButton::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Input over the flat pattern, including orbit bindings, wheel and pinch, leaves the Design
|
||||
/// camera and selection alone.
|
||||
#[test]
|
||||
fn design_camera_is_unchanged_while_the_flat_pattern_has_focus() {
|
||||
use egui::{Event, Modifiers, PointerButton};
|
||||
let none = Modifiers::NONE;
|
||||
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = flat_app(preset);
|
||||
let mut t = 0.0;
|
||||
frame(&mut app, &ctx, &mut t, none, vec![]);
|
||||
let cam = app.cam;
|
||||
let at = pos2(300.0, 250.0);
|
||||
for button in [PointerButton::Primary, PointerButton::Middle, PointerButton::Secondary] {
|
||||
for mods in [none, Modifiers::SHIFT, Modifiers::CTRL, Modifiers::ALT, Modifiers::SHIFT | Modifiers::ALT] {
|
||||
frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerMoved(at)]);
|
||||
frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerButton { pos: at, button, pressed: true, modifiers: mods }]);
|
||||
for i in 1..6 {
|
||||
frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerMoved(at + vec2(14.0, -9.0) * i as f32)]);
|
||||
}
|
||||
frame(&mut app, &ctx, &mut t, mods, vec![Event::PointerButton { pos: at, button, pressed: false, modifiers: mods }]);
|
||||
}
|
||||
}
|
||||
let wheel = Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta: vec2(0.0, 3.0), modifiers: none, phase: egui::TouchPhase::Move };
|
||||
frame(&mut app, &ctx, &mut t, none, vec![wheel]);
|
||||
frame(&mut app, &ctx, &mut t, none, vec![Event::Zoom(1.3)]);
|
||||
for _ in 0..30 {
|
||||
frame(&mut app, &ctx, &mut t, none, vec![]);
|
||||
}
|
||||
assert_eq!(app.cam, cam, "{preset}: the Design camera stays put under the flat pattern");
|
||||
assert!(app.session.selection.is_empty(), "{preset}: nothing underneath is selected");
|
||||
assert_eq!(app.flat_view.cam.pitch, app.flat_view.cam.pitch.clamp(1.5, 2.0), "{preset}: still looking straight down");
|
||||
}
|
||||
}
|
||||
|
||||
/// Fit and Home frame the flat pattern (not the model), and leave the Design camera alone.
|
||||
#[test]
|
||||
fn fit_and_home_frame_the_flat_pattern() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = flat_app("fusion");
|
||||
let mut t = 0.0;
|
||||
let none = egui::Modifiers::NONE;
|
||||
frame(&mut app, &ctx, &mut t, none, vec![]);
|
||||
let fitted = app.flat_view.cam;
|
||||
// The 100 x 60 pattern sits inside the view with room for the command panel.
|
||||
let scale = 600.0 / (2.0 * fitted.half_height());
|
||||
assert!(100.0 * scale < 800.0 && 60.0 * scale < 600.0 && 100.0 * scale > 200.0, "fits: {scale}");
|
||||
let design = app.cam;
|
||||
for view in ["fit", "home"] {
|
||||
app.flat_view.cam.pan(120.0, -80.0, 600.0);
|
||||
app.flat_view.cam.zoom_at(0.3, None);
|
||||
assert_ne!(app.flat_view.cam, fitted);
|
||||
app.animate_view(view);
|
||||
frame(&mut app, &ctx, &mut t, none, vec![]);
|
||||
assert!(app.flat_view.cam.target.dist(fitted.target) < 1e-9 && (app.flat_view.cam.distance - fitted.distance).abs() < 1e-9, "{view} refits");
|
||||
assert_eq!(app.cam, design, "{view} leaves the Design camera alone");
|
||||
}
|
||||
app.flat_view.cam.pan(50.0, 50.0, 600.0);
|
||||
app.fit_view();
|
||||
frame(&mut app, &ctx, &mut t, none, vec![]);
|
||||
assert!(app.flat_view.cam.target.dist(fitted.target) < 1e-9, "fit_view refits");
|
||||
// Closing the dialog returns the viewport to the Design camera, and reopening refits.
|
||||
app.flat_view.cam.pan(50.0, 50.0, 600.0);
|
||||
app.dialog = None;
|
||||
frame(&mut app, &ctx, &mut t, none, vec![]);
|
||||
assert!(!app.flat_view.fitted);
|
||||
}
|
||||
Reference in new issue
Block a user