feat(view): complete ZOOM options
Add command-line navigation modes and persistent wheel controls.\n\nRefs #412
This commit is contained in:
parent
3f103d2f8b
commit
adb8ced8b5
17 changed files with 380 additions and 23 deletions
|
|
@ -120,6 +120,7 @@ QS, *QSELECT
|
||||||
FI, *FILTER
|
FI, *FILTER
|
||||||
|
|
||||||
; ── View / display ────────────────────────────────────────────────────────
|
; ── View / display ────────────────────────────────────────────────────────
|
||||||
|
Z, *ZOOM
|
||||||
P, *PAN
|
P, *PAN
|
||||||
U, *UNDO
|
U, *UNDO
|
||||||
PR, *PROPERTIES
|
PR, *PROPERTIES
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ impl OpenCADStudio {
|
||||||
self.ucs_icon_hover = false;
|
self.ucs_icon_hover = false;
|
||||||
self.tabs[i].pan_mode = false;
|
self.tabs[i].pan_mode = false;
|
||||||
self.tabs[i].orbit_mode = false;
|
self.tabs[i].orbit_mode = false;
|
||||||
|
self.tabs[i].zoom_dynamic_mode = false;
|
||||||
let _ = self.on_viewport_exit();
|
let _ = self.on_viewport_exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1939,6 +1940,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
.zoom_to_window(p1.as_vec3(), p2.as_vec3());
|
||||||
|
|
|
||||||
|
|
@ -805,40 +805,94 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
"ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZEA" => {
|
"ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZOOM EA" | "ZEA" => {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.fit_all_model_viewports();
|
self.tabs[i].scene.fit_all_model_viewports();
|
||||||
self.command_line.push_output(crate::t!("Zoom Extents — All Viewports").as_ref());
|
self.command_line.push_output(crate::t!("Zoom Extents — All Viewports").as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
"ZOOM EXTENTS" | "ZOOMEXTENTS" | "ZE" => {
|
"ZOOM EXTENTS" | "ZOOM E" | "ZOOMEXTENTS" | "ZE" => {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.fit_all();
|
self.tabs[i].scene.fit_all();
|
||||||
self.command_line.push_output(crate::t!("Zoom Extents").as_ref());
|
self.command_line.push_output(crate::t!("Zoom Extents").as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
"ZOOM IN" | "ZI" => {
|
"ZOOM IN" | "ZOOM I" | "ZI" => {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.zoom_camera(1.0 / 1.5);
|
self.tabs[i].scene.zoom_camera(1.0 / 1.5);
|
||||||
self.command_line.push_output(crate::t!("Zoom In").as_ref());
|
self.command_line.push_output(crate::t!("Zoom In").as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
"ZOOM OUT" | "ZO" => {
|
"ZOOM OUT" | "ZO" => {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.zoom_camera(1.5);
|
self.tabs[i].scene.zoom_camera(1.5);
|
||||||
self.command_line.push_output(crate::t!("Zoom Out").as_ref());
|
self.command_line.push_output(crate::t!("Zoom Out").as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZOOM ALL — fit the configured drawing limits.
|
// ZOOM ALL — fit the configured drawing limits.
|
||||||
"ZOOM ALL" | "ZOOM A" | "ZA" => {
|
"ZOOM ALL" | "ZOOM A" | "ZA" => {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.fit_all_with_limits();
|
self.tabs[i].scene.fit_all_with_limits();
|
||||||
self.command_line.push_output(crate::t!("Zoom All").as_ref());
|
self.command_line.push_output(crate::t!("Zoom All").as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"ZOOM PREVIOUS" | "ZOOM P" | "ZP" => {
|
||||||
|
if self.tabs[i].scene.restore_previous_view() {
|
||||||
|
self.command_line.push_output(crate::t!("Zoom Previous").as_ref());
|
||||||
|
} else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("ZOOM: no previous view.").as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ZOOM OBJECT" | "ZOOM O" | "ZOBJ" => {
|
||||||
|
let handles: Vec<_> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selected_entities()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(handle, _)| handle)
|
||||||
|
.collect();
|
||||||
|
if handles.is_empty() {
|
||||||
|
use crate::modules::draw::select::SelectObjectsCommand;
|
||||||
|
let command = SelectObjectsCommand::new("ZOOM OBJECT");
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
} else {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
|
if self.tabs[i].scene.zoom_to_entities(&handles) {
|
||||||
|
self.command_line.push_output(crate::t!("Zoom Object").as_ref());
|
||||||
|
} else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("ZOOM: selected objects have no visible bounds.").as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ZOOM DYNAMIC" | "ZOOM D" | "ZD" => {
|
||||||
|
self.tabs[i].zoom_dynamic_mode = true;
|
||||||
|
self.clear_navigation_hover(i);
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::t!(
|
||||||
|
"ZOOM Dynamic: drag horizontally to pan and vertically to zoom. Press Esc to exit."
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ZOOM SCALE — set zoom factor (e.g. "ZOOM SCALE 2" or "ZS 0.5")
|
// ZOOM SCALE — set zoom factor (e.g. "ZOOM SCALE 2" or "ZS 0.5")
|
||||||
cmd if cmd.starts_with("ZOOM SCALE ") || cmd.starts_with("ZS ") => {
|
cmd if cmd.starts_with("ZOOM SCALE ")
|
||||||
|
|| cmd.starts_with("ZOOM S ")
|
||||||
|
|| cmd.starts_with("ZS ") =>
|
||||||
|
{
|
||||||
let rest = cmd
|
let rest = cmd
|
||||||
.split_once(' ')
|
.strip_prefix("ZOOM SCALE ")
|
||||||
.and_then(|(_, r)| r.split_once(' ').map(|(_, v)| v).or(Some(r)))
|
.or_else(|| cmd.strip_prefix("ZOOM S "))
|
||||||
|
.or_else(|| cmd.strip_prefix("ZS "))
|
||||||
.unwrap_or("1");
|
.unwrap_or("1");
|
||||||
if let Ok(factor) = rest.trim().parse::<f32>() {
|
if let Ok(factor) = rest.trim().parse::<f32>() {
|
||||||
if factor > 0.0 {
|
if factor > 0.0 {
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.tabs[i].scene.zoom_camera(1.0 / factor);
|
self.tabs[i].scene.zoom_camera(1.0 / factor);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("Zoom Scale ×{factor:.3}").as_ref());
|
.push_output(crate::tf!("Zoom Scale ×{factor:.3}").as_ref());
|
||||||
|
|
@ -866,15 +920,18 @@ impl OpenCADStudio {
|
||||||
use crate::command::KeywordCommand;
|
use crate::command::KeywordCommand;
|
||||||
let c = KeywordCommand::new(
|
let c = KeywordCommand::new(
|
||||||
"ZOOM",
|
"ZOOM",
|
||||||
"ZOOM [Window / Extents / Extents All / All / In / Out / Scale]:",
|
"ZOOM [Window / Extents / Previous / Object / All / Dynamic / Extents All / In / Out / Scale]:",
|
||||||
vec![
|
vec![
|
||||||
("Window", "WINDOW", None),
|
("Window", "W", None),
|
||||||
("Extents", "EXTENTS", None),
|
("Extents", "E", None),
|
||||||
("Extents All", "EXTENTS ALL", None),
|
("Previous", "P", None),
|
||||||
("All", "ALL", None),
|
("Object", "O", None),
|
||||||
("In", "IN", None),
|
("All", "A", None),
|
||||||
|
("Dynamic", "D", None),
|
||||||
|
("Extents All", "EA", None),
|
||||||
|
("In", "I", None),
|
||||||
("Out", "OUT", None),
|
("Out", "OUT", None),
|
||||||
("Scale", "SCALE", Some("ZOOM scale factor (e.g. 2 or 0.5):")),
|
("Scale", "S", Some("ZOOM scale factor (e.g. 2 or 0.5):")),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
self.command_line.push_info(&c.prompt());
|
self.command_line.push_info(&c.prompt());
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ impl OpenCADStudio {
|
||||||
// command arms below re-enable the selected one).
|
// command arms below re-enable the selected one).
|
||||||
self.tabs[i].pan_mode = false;
|
self.tabs[i].pan_mode = false;
|
||||||
self.tabs[i].orbit_mode = false;
|
self.tabs[i].orbit_mode = false;
|
||||||
|
self.tabs[i].zoom_dynamic_mode = false;
|
||||||
// Reset the last committed point so the first click of the new command
|
// Reset the last committed point so the first click of the new command
|
||||||
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
||||||
self.last_point = None;
|
self.last_point = None;
|
||||||
|
|
@ -445,6 +446,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||||
"FITSPLINE",
|
"FITSPLINE",
|
||||||
// System variables (typeable directly).
|
// System variables (typeable directly).
|
||||||
"MIRRTEXT",
|
"MIRRTEXT",
|
||||||
|
"ZOOMWHEEL",
|
||||||
|
"ZOOMFACTOR",
|
||||||
"ATTREQ",
|
"ATTREQ",
|
||||||
"ATTDIA",
|
"ATTDIA",
|
||||||
"DIMASSOC",
|
"DIMASSOC",
|
||||||
|
|
|
||||||
|
|
@ -866,6 +866,8 @@ impl OpenCADStudio {
|
||||||
cmd if matches!(
|
cmd if matches!(
|
||||||
cmd.split_whitespace().next().unwrap_or(""),
|
cmd.split_whitespace().next().unwrap_or(""),
|
||||||
"MIRRTEXT"
|
"MIRRTEXT"
|
||||||
|
| "ZOOMWHEEL"
|
||||||
|
| "ZOOMFACTOR"
|
||||||
| "TEXTFILL"
|
| "TEXTFILL"
|
||||||
| "ATTREQ"
|
| "ATTREQ"
|
||||||
| "ATTDIA"
|
| "ATTDIA"
|
||||||
|
|
@ -936,7 +938,7 @@ impl OpenCADStudio {
|
||||||
let value = it.next().map(|s| s.trim().to_string());
|
let value = it.next().map(|s| s.trim().to_string());
|
||||||
if name.is_empty() || name == "?" {
|
if name.is_empty() || name == "?" {
|
||||||
self.command_line.push_info(
|
self.command_line.push_info(
|
||||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ZOOMWHEEL ZOOMFACTOR ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Parse a boolean given as 0/1 or ON/OFF.
|
// Parse a boolean given as 0/1 or ON/OFF.
|
||||||
|
|
@ -1028,6 +1030,37 @@ impl OpenCADStudio {
|
||||||
.ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
|
.ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
|
||||||
None => Ok((format!("MIRRTEXT = {}", h.mirror_text as i32), false)),
|
None => Ok((format!("MIRRTEXT = {}", h.mirror_text as i32), false)),
|
||||||
},
|
},
|
||||||
|
"ZOOMWHEEL" => match &value {
|
||||||
|
Some(v) => match parse_bool(v) {
|
||||||
|
Some(reversed) => {
|
||||||
|
self.zoom_wheel_reversed = reversed;
|
||||||
|
Ok((
|
||||||
|
format!("ZOOMWHEEL = {}", reversed as i32),
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
None => Err("SETVAR: 0 or 1 required.".into()),
|
||||||
|
},
|
||||||
|
None => Ok((
|
||||||
|
format!(
|
||||||
|
"ZOOMWHEEL = {}",
|
||||||
|
self.zoom_wheel_reversed as i32
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
"ZOOMFACTOR" => match &value {
|
||||||
|
Some(v) => match v.parse::<i32>() {
|
||||||
|
Ok(factor) if (3..=100).contains(&factor) => {
|
||||||
|
self.zoom_factor = factor;
|
||||||
|
Ok((format!("ZOOMFACTOR = {factor}"), true))
|
||||||
|
}
|
||||||
|
_ => Err("SETVAR: integer from 3 to 100 required.".into()),
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false))
|
||||||
|
}
|
||||||
|
},
|
||||||
// Global (not stored in the drawing): fill vs. hollow
|
// Global (not stored in the drawing): fill vs. hollow
|
||||||
// TrueType text. The active tab re-tessellates below.
|
// TrueType text. The active tab re-tessellates below.
|
||||||
"TEXTFILL" => match &value {
|
"TEXTFILL" => match &value {
|
||||||
|
|
@ -1579,7 +1612,11 @@ impl OpenCADStudio {
|
||||||
match outcome {
|
match outcome {
|
||||||
Ok((msg, changed)) => {
|
Ok((msg, changed)) => {
|
||||||
if changed {
|
if changed {
|
||||||
self.tabs[i].dirty = true;
|
if matches!(name.as_str(), "ZOOMWHEEL" | "ZOOMFACTOR") {
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
} else {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
}
|
||||||
self.command_line.push_output(&msg);
|
self.command_line.push_output(&msg);
|
||||||
} else {
|
} else {
|
||||||
// Queried with no value (`changed == false`):
|
// Queried with no value (`changed == false`):
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,9 @@ pub(super) struct DocumentTab {
|
||||||
/// Interactive 3-D orbit mode. While active, a left-button drag follows
|
/// Interactive 3-D orbit mode. While active, a left-button drag follows
|
||||||
/// the same camera-orbit path as Shift + middle-button drag.
|
/// the same camera-orbit path as Shift + middle-button drag.
|
||||||
pub(super) orbit_mode: bool,
|
pub(super) orbit_mode: bool,
|
||||||
|
/// Interactive ZOOM Dynamic mode. A left drag pans horizontally and zooms
|
||||||
|
/// vertically until Escape or another command ends the mode.
|
||||||
|
pub(super) zoom_dynamic_mode: bool,
|
||||||
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
||||||
|
|
@ -476,6 +479,7 @@ impl DocumentTab {
|
||||||
is_start: false,
|
is_start: false,
|
||||||
pan_mode: false,
|
pan_mode: false,
|
||||||
orbit_mode: false,
|
orbit_mode: false,
|
||||||
|
zoom_dynamic_mode: false,
|
||||||
plugin_state: HashMap::new(),
|
plugin_state: HashMap::new(),
|
||||||
suspended_cmd: None,
|
suspended_cmd: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,10 @@ pub(super) struct OpenCADStudio {
|
||||||
polar_mode: bool,
|
polar_mode: bool,
|
||||||
/// Polar tracking angle increment in degrees (15 / 30 / 45 / 90).
|
/// Polar tracking angle increment in degrees (15 / 30 / 45 / 90).
|
||||||
polar_increment_deg: f32,
|
polar_increment_deg: f32,
|
||||||
|
/// Reverse the mouse-wheel zoom direction when true (ZOOMWHEEL = 1).
|
||||||
|
zoom_wheel_reversed: bool,
|
||||||
|
/// Mouse-wheel zoom sensitivity, clamped to 3..=100 (ZOOMFACTOR).
|
||||||
|
zoom_factor: i32,
|
||||||
/// Show grid lines in the viewport (F7).
|
/// Show grid lines in the viewport (F7).
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
||||||
|
|
@ -3030,6 +3034,8 @@ impl OpenCADStudio {
|
||||||
ortho_mode: false,
|
ortho_mode: false,
|
||||||
polar_mode: false,
|
polar_mode: false,
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
|
zoom_wheel_reversed: false,
|
||||||
|
zoom_factor: 60,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ pub struct UserSettings {
|
||||||
pub dyn_input: bool,
|
pub dyn_input: bool,
|
||||||
pub polar: bool,
|
pub polar: bool,
|
||||||
pub polar_increment_deg: f32,
|
pub polar_increment_deg: f32,
|
||||||
|
pub zoom_wheel_reversed: bool,
|
||||||
|
pub zoom_factor: i32,
|
||||||
pub otrack: bool,
|
pub otrack: bool,
|
||||||
// Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing —
|
// Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing —
|
||||||
// stored in the document header, not here (they used to be persisted app-
|
// stored in the document header, not here (they used to be persisted app-
|
||||||
|
|
@ -166,6 +168,8 @@ impl Default for UserSettings {
|
||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
polar: false,
|
polar: false,
|
||||||
polar_increment_deg: 45.0,
|
polar_increment_deg: 45.0,
|
||||||
|
zoom_wheel_reversed: false,
|
||||||
|
zoom_factor: 60,
|
||||||
otrack: false,
|
otrack: false,
|
||||||
default_assoc_prompted: false,
|
default_assoc_prompted: false,
|
||||||
disabled_plugins: Vec::new(),
|
disabled_plugins: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -660,16 +660,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
// drag. Orbit exits silently; PAN keeps its existing message.
|
// drag. Orbit exits silently; PAN keeps its existing message.
|
||||||
if self.tabs[self.active_tab].pan_mode
|
if self.tabs[self.active_tab].pan_mode
|
||||||
|| self.tabs[self.active_tab].orbit_mode
|
|| self.tabs[self.active_tab].orbit_mode
|
||||||
|
|| self.tabs[self.active_tab].zoom_dynamic_mode
|
||||||
{
|
{
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let was_pan = self.tabs[i].pan_mode;
|
let was_pan = self.tabs[i].pan_mode;
|
||||||
self.tabs[i].pan_mode = false;
|
self.tabs[i].pan_mode = false;
|
||||||
self.tabs[i].orbit_mode = false;
|
self.tabs[i].orbit_mode = false;
|
||||||
|
self.tabs[i].zoom_dynamic_mode = false;
|
||||||
{
|
{
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.middle_down = false;
|
sel.middle_down = false;
|
||||||
sel.middle_last_pos = None;
|
sel.middle_last_pos = None;
|
||||||
sel.orbit_pivot = None;
|
sel.orbit_pivot = None;
|
||||||
|
sel.box_anchor = None;
|
||||||
|
sel.box_anchor_world = None;
|
||||||
|
sel.box_current = None;
|
||||||
|
sel.box_crossing_locked = false;
|
||||||
}
|
}
|
||||||
if was_pan {
|
if was_pan {
|
||||||
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||||
&& self.active_modal.is_none()
|
&& self.active_modal.is_none()
|
||||||
&& !self.tabs[i].pan_mode
|
&& !self.tabs[i].pan_mode
|
||||||
&& !self.tabs[i].orbit_mode
|
&& !self.tabs[i].orbit_mode
|
||||||
|
&& !self.tabs[i].zoom_dynamic_mode
|
||||||
{
|
{
|
||||||
self.ribbon.deactivate_tool();
|
self.ribbon.deactivate_tool();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,8 @@ impl OpenCADStudio {
|
||||||
dyn_input: self.dyn_input,
|
dyn_input: self.dyn_input,
|
||||||
polar: self.polar_mode,
|
polar: self.polar_mode,
|
||||||
polar_increment_deg: self.polar_increment_deg,
|
polar_increment_deg: self.polar_increment_deg,
|
||||||
|
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
||||||
|
zoom_factor: self.zoom_factor,
|
||||||
otrack: self.snapper.otrack_enabled,
|
otrack: self.snapper.otrack_enabled,
|
||||||
default_assoc_prompted: self.default_assoc_prompted,
|
default_assoc_prompted: self.default_assoc_prompted,
|
||||||
disabled_plugins: {
|
disabled_plugins: {
|
||||||
|
|
@ -299,6 +301,8 @@ impl OpenCADStudio {
|
||||||
self.dyn_input = s.dyn_input;
|
self.dyn_input = s.dyn_input;
|
||||||
self.polar_mode = s.polar;
|
self.polar_mode = s.polar;
|
||||||
self.polar_increment_deg = s.polar_increment_deg;
|
self.polar_increment_deg = s.polar_increment_deg;
|
||||||
|
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
||||||
|
self.zoom_factor = s.zoom_factor.clamp(3, 100);
|
||||||
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
||||||
// open / tab switch), not app-global, so they are not applied here.
|
// open / tab switch), not app-global, so they are not applied here.
|
||||||
self.snapper.otrack_enabled = s.otrack;
|
self.snapper.otrack_enabled = s.otrack;
|
||||||
|
|
|
||||||
|
|
@ -2831,6 +2831,7 @@ impl OpenCADStudio {
|
||||||
Message::ViewCubeHome => {
|
Message::ViewCubeHome => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
let r_ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
let r_ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
||||||
if self.tabs[i].scene.active_viewport.is_some() {
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
@ -2847,6 +2848,7 @@ impl OpenCADStudio {
|
||||||
Message::ViewCubeRoll(cw) => {
|
Message::ViewCubeRoll(cw) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
let ang = if cw {
|
let ang = if cw {
|
||||||
std::f32::consts::FRAC_PI_2
|
std::f32::consts::FRAC_PI_2
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2873,6 +2875,7 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
if self.tabs[i].scene.active_viewport.is_some() {
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
|
||||||
|
|
@ -739,6 +739,39 @@ impl OpenCADStudio {
|
||||||
if mid_down {
|
if mid_down {
|
||||||
if let Some(last) = mid_last {
|
if let Some(last) = mid_last {
|
||||||
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
||||||
|
if self.tabs[i].zoom_dynamic_mode {
|
||||||
|
let bounds = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.active_model_tile_bounds(vp_size.0, vp_size.1);
|
||||||
|
drop(sel);
|
||||||
|
let zoom_delta = -dy * 0.03;
|
||||||
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
|
self.tabs[i].scene.pan_active_viewport(dx, 0.0, bounds);
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.zoom_active_viewport(zoom_delta, None);
|
||||||
|
} else {
|
||||||
|
let local = iced::Point {
|
||||||
|
x: p.x - bounds.x,
|
||||||
|
y: p.y - bounds.y,
|
||||||
|
};
|
||||||
|
let local_bounds = iced::Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
};
|
||||||
|
let mut camera = self.tabs[i].scene.camera.borrow_mut();
|
||||||
|
camera.pan_screen(dx, 0.0, bounds.height);
|
||||||
|
camera.zoom_about_point(local, local_bounds, zoom_delta);
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_nav_perf(crate::scene::NavPerfOp::Zoom, move_started);
|
||||||
|
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
// Shift+MMB drag orbits the model view instead of panning
|
// Shift+MMB drag orbits the model view instead of panning
|
||||||
// — the requested Zoom=wheel / Pan=MMB / Rotate=Shift+MMB
|
// — the requested Zoom=wheel / Pan=MMB / Rotate=Shift+MMB
|
||||||
// scheme (#229). Floating viewports and paper keep the
|
// scheme (#229). Floating viewports and paper keep the
|
||||||
|
|
@ -2288,11 +2321,21 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
// Interactive navigation tools reuse the middle-button movement path,
|
// Interactive navigation tools reuse the middle-button movement path,
|
||||||
// so no selection/pick logic runs while the left button drives them.
|
// so no selection/pick logic runs while the left button drives them.
|
||||||
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
|
if self.tabs[i].orbit_mode
|
||||||
|
|| self.tabs[i].pan_mode
|
||||||
|
|| self.tabs[i].zoom_dynamic_mode
|
||||||
|
{
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.middle_down = true;
|
sel.middle_down = true;
|
||||||
sel.middle_last_pos = Some(p);
|
sel.middle_last_pos = Some(p);
|
||||||
|
if self.tabs[i].zoom_dynamic_mode {
|
||||||
|
sel.box_anchor = Some(p);
|
||||||
|
sel.box_current = Some(p);
|
||||||
|
sel.box_crossing = false;
|
||||||
|
sel.box_crossing_locked = true;
|
||||||
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2460,11 +2503,18 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
// Navigation mode: end this drag but keep the tool armed for the next
|
// Navigation mode: end this drag but keep the tool armed for the next
|
||||||
// left drag (exit is Esc / another command). Mirror of the press.
|
// left drag (exit is Esc / another command). Mirror of the press.
|
||||||
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
|
if self.tabs[i].orbit_mode
|
||||||
|
|| self.tabs[i].pan_mode
|
||||||
|
|| self.tabs[i].zoom_dynamic_mode
|
||||||
|
{
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.middle_down = false;
|
sel.middle_down = false;
|
||||||
sel.middle_last_pos = None;
|
sel.middle_last_pos = None;
|
||||||
sel.orbit_pivot = None;
|
sel.orbit_pivot = None;
|
||||||
|
sel.box_anchor = None;
|
||||||
|
sel.box_anchor_world = None;
|
||||||
|
sel.box_current = None;
|
||||||
|
sel.box_crossing_locked = false;
|
||||||
drop(sel);
|
drop(sel);
|
||||||
self.arm_hover_after_navigation(i);
|
self.arm_hover_after_navigation(i);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
|
|
@ -4007,6 +4057,7 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
self.ribbon.close_dropdown();
|
self.ribbon.close_dropdown();
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let is_double = {
|
let is_double = {
|
||||||
let sel = self.tabs[i].scene.selection.borrow();
|
let sel = self.tabs[i].scene.selection.borrow();
|
||||||
|
|
@ -4035,11 +4086,16 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
||||||
let nav_started = Instant::now();
|
let nav_started = Instant::now();
|
||||||
let s = match delta {
|
let mut s = match delta {
|
||||||
mouse::ScrollDelta::Lines { y, .. } => y,
|
mouse::ScrollDelta::Lines { y, .. } => y,
|
||||||
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
||||||
};
|
};
|
||||||
|
s *= self.zoom_factor as f32 / 60.0;
|
||||||
|
if self.zoom_wheel_reversed {
|
||||||
|
s = -s;
|
||||||
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
|
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
|
||||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
|
|
@ -4204,6 +4260,7 @@ impl OpenCADStudio {
|
||||||
fn snap_view_region(&mut self, region: CubeRegion, r_ucs: glam::Mat4) -> Task<Message> {
|
fn snap_view_region(&mut self, region: CubeRegion, r_ucs: glam::Mat4) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
|
self.tabs[i].scene.remember_current_view();
|
||||||
let mut region = region;
|
let mut region = region;
|
||||||
// "Already there → flip to opposite" check: compare the
|
// "Already there → flip to opposite" check: compare the
|
||||||
// current gaze direction with the region's target gaze.
|
// current gaze direction with the region's target gaze.
|
||||||
|
|
|
||||||
|
|
@ -680,7 +680,7 @@ impl OpenCADStudio {
|
||||||
dividers,
|
dividers,
|
||||||
pane_move_rect,
|
pane_move_rect,
|
||||||
pane_drop_rect,
|
pane_drop_rect,
|
||||||
tab.pan_mode || tab.orbit_mode,
|
tab.pan_mode || tab.orbit_mode || tab.zoom_dynamic_mode,
|
||||||
self.ribbon.open_dropdown.is_some(),
|
self.ribbon.open_dropdown.is_some(),
|
||||||
hover_locked,
|
hover_locked,
|
||||||
crosshair_background(tab, is_paper),
|
crosshair_background(tab, is_paper),
|
||||||
|
|
|
||||||
|
|
@ -566,8 +566,9 @@ impl CadCommand for KeywordCommand {
|
||||||
// `None` would hand the same text to the command a second time.
|
// `None` would hand the same text to the command a second time.
|
||||||
None => {
|
None => {
|
||||||
let up = t.to_uppercase();
|
let up = t.to_uppercase();
|
||||||
let Some((_, keyword, value_prompt)) =
|
let Some((_, keyword, value_prompt)) = self.options.iter().find(|(label, k, _)| {
|
||||||
self.options.iter().find(|(_, k, _)| k.eq_ignore_ascii_case(&up))
|
k.eq_ignore_ascii_case(&up) || label.eq_ignore_ascii_case(t)
|
||||||
|
})
|
||||||
else {
|
else {
|
||||||
// Unknown verb — keep prompting rather than dispatch garbage.
|
// Unknown verb — keep prompting rather than dispatch garbage.
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
// hit-testing lives in `scene::pick::hit_test`.)
|
// hit-testing lives in `scene::pick::hit_test`.)
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DVec3> {
|
fn rendered_wire_bounds(
|
||||||
|
wires: &[WireModel],
|
||||||
|
fallback_z: f64,
|
||||||
|
) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||||
let mut min = glam::DVec3::splat(f64::INFINITY);
|
let mut min = glam::DVec3::splat(f64::INFINITY);
|
||||||
let mut max = glam::DVec3::splat(f64::NEG_INFINITY);
|
let mut max = glam::DVec3::splat(f64::NEG_INFINITY);
|
||||||
let mut include = |point: glam::DVec3| {
|
let mut include = |point: glam::DVec3| {
|
||||||
|
|
@ -55,12 +58,16 @@ fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DV
|
||||||
}
|
}
|
||||||
|
|
||||||
if min.is_finite() && max.is_finite() {
|
if min.is_finite() && max.is_finite() {
|
||||||
Some((min + max) * 0.5)
|
Some((min, max))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rendered_wire_center(wires: &[WireModel], fallback_z: f64) -> Option<glam::DVec3> {
|
||||||
|
rendered_wire_bounds(wires, fallback_z).map(|(min, max)| (min + max) * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
fn block_entity_transform(
|
fn block_entity_transform(
|
||||||
document: &CadDocument,
|
document: &CadDocument,
|
||||||
block_name: &str,
|
block_name: &str,
|
||||||
|
|
@ -189,6 +196,105 @@ impl Scene {
|
||||||
self.camera_generation += 1;
|
self.camera_generation += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save the active view immediately before a navigation operation. The
|
||||||
|
/// paper-space camera and an entered floating viewport use different state,
|
||||||
|
/// so the snapshot records whichever one currently owns navigation.
|
||||||
|
pub fn remember_current_view(&mut self) {
|
||||||
|
let layout = self.current_layout.clone();
|
||||||
|
if let Some(handle) = self.active_viewport {
|
||||||
|
if let Some(EntityType::Viewport(viewport)) = self.document.get_entity(handle) {
|
||||||
|
self.previous_view = Some(ViewSnapshot::Floating {
|
||||||
|
layout,
|
||||||
|
handle,
|
||||||
|
viewport: Box::new(viewport.clone()),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.previous_view = Some(ViewSnapshot::Main {
|
||||||
|
layout,
|
||||||
|
tile: self.active_model_tile.get(),
|
||||||
|
camera: self.camera.borrow().clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap the active view with the most recently remembered one. Keeping the
|
||||||
|
/// displaced current view makes repeated ZOOM Previous calls toggle cleanly
|
||||||
|
/// between the last two views.
|
||||||
|
pub fn restore_previous_view(&mut self) -> bool {
|
||||||
|
let Some(snapshot) = self.previous_view.take() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match snapshot {
|
||||||
|
ViewSnapshot::Main {
|
||||||
|
layout,
|
||||||
|
tile,
|
||||||
|
camera,
|
||||||
|
} if self.active_viewport.is_none()
|
||||||
|
&& self.current_layout == layout
|
||||||
|
&& (layout != "Model" || self.active_model_tile.get() == tile) =>
|
||||||
|
{
|
||||||
|
let current = ViewSnapshot::Main {
|
||||||
|
layout,
|
||||||
|
tile,
|
||||||
|
camera: self.camera.borrow().clone(),
|
||||||
|
};
|
||||||
|
*self.camera.borrow_mut() = camera;
|
||||||
|
self.previous_view = Some(current);
|
||||||
|
self.camera_generation += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
ViewSnapshot::Floating {
|
||||||
|
layout,
|
||||||
|
handle,
|
||||||
|
viewport,
|
||||||
|
} if self.current_layout == layout && self.active_viewport == Some(handle) => {
|
||||||
|
let Some(EntityType::Viewport(current)) = self.document.get_entity(handle) else {
|
||||||
|
self.previous_view = Some(ViewSnapshot::Floating {
|
||||||
|
layout,
|
||||||
|
handle,
|
||||||
|
viewport,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if current.status.locked {
|
||||||
|
self.previous_view = Some(ViewSnapshot::Floating {
|
||||||
|
layout,
|
||||||
|
handle,
|
||||||
|
viewport,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let current = Box::new(current.clone());
|
||||||
|
if let Some(EntityType::Viewport(destination)) =
|
||||||
|
self.document.get_entity_mut(handle)
|
||||||
|
{
|
||||||
|
destination.view_center = viewport.view_center;
|
||||||
|
destination.view_direction = viewport.view_direction;
|
||||||
|
destination.view_target = viewport.view_target;
|
||||||
|
destination.lens_length = viewport.lens_length;
|
||||||
|
destination.front_clip_z = viewport.front_clip_z;
|
||||||
|
destination.back_clip_z = viewport.back_clip_z;
|
||||||
|
destination.view_height = viewport.view_height;
|
||||||
|
destination.twist_angle = viewport.twist_angle;
|
||||||
|
destination.custom_scale = viewport.custom_scale;
|
||||||
|
destination.status.perspective = viewport.status.perspective;
|
||||||
|
}
|
||||||
|
self.previous_view = Some(ViewSnapshot::Floating {
|
||||||
|
layout,
|
||||||
|
handle,
|
||||||
|
viewport: current,
|
||||||
|
});
|
||||||
|
self.camera_generation += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
snapshot => {
|
||||||
|
self.previous_view = Some(snapshot);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Refresh model bounds without moving the live camera. Reusing `fit_all`
|
/// Refresh model bounds without moving the live camera. Reusing `fit_all`
|
||||||
/// keeps projection framing on the same visibility and outlier filters as
|
/// keeps projection framing on the same visibility and outlier filters as
|
||||||
/// Zoom Extents instead of accepting every finite cached AABB.
|
/// Zoom Extents instead of accepting every finite cached AABB.
|
||||||
|
|
@ -270,6 +376,52 @@ impl Scene {
|
||||||
self.camera_generation += 1;
|
self.camera_generation += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fit the active view to the rendered bounds of the supplied entities.
|
||||||
|
/// Returns false when the selection has no finite visible geometry.
|
||||||
|
pub fn zoom_to_entities(&mut self, handles: &[Handle]) -> bool {
|
||||||
|
let fallback_z = self.active_camera_target().z;
|
||||||
|
let wires = self.wire_models_for(handles);
|
||||||
|
let mut bounds = rendered_wire_bounds(&wires, fallback_z);
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
let Some(mesh) = self.meshes.get(handle) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let [x0, y0, x1, y1] = mesh.world_aabb;
|
||||||
|
let [z0, z1] = mesh.z_aabb;
|
||||||
|
let mesh_min = glam::DVec3::new(x0 as f64, y0 as f64, z0 as f64);
|
||||||
|
let mesh_max = glam::DVec3::new(x1 as f64, y1 as f64, z1 as f64);
|
||||||
|
if !mesh_min.is_finite() || !mesh_max.is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bounds = Some(match bounds {
|
||||||
|
Some((min, max)) => (min.min(mesh_min), max.max(mesh_max)),
|
||||||
|
None => (mesh_min, mesh_max),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((mut min, mut max)) = bounds else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if min == max {
|
||||||
|
min -= glam::DVec3::splat(1.0);
|
||||||
|
max += glam::DVec3::splat(1.0);
|
||||||
|
}
|
||||||
|
let min = min.as_vec3();
|
||||||
|
let max = max.as_vec3();
|
||||||
|
if !min.is_finite() || !max.is_finite() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.active_viewport.is_some() {
|
||||||
|
return self.fit_active_viewport_to_bounds(min, max);
|
||||||
|
}
|
||||||
|
let aspect = self.active_camera_aspect();
|
||||||
|
self.camera.borrow_mut().fit_to_bounds(min, max, aspect);
|
||||||
|
self.projection_bounds_epoch.set(self.geometry_epoch);
|
||||||
|
self.camera_generation += 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Centre the active camera on one rendered entity without changing zoom
|
/// Centre the active camera on one rendered entity without changing zoom
|
||||||
/// distance, orientation, or viewport scale.
|
/// distance, orientation, or viewport scale.
|
||||||
pub fn center_camera_on_entity(&mut self, handle: Handle) -> bool {
|
pub fn center_camera_on_entity(&mut self, handle: Handle) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -1123,6 +1123,20 @@ pub(crate) struct ModelTile {
|
||||||
pub(crate) snap_on: bool,
|
pub(crate) snap_on: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum ViewSnapshot {
|
||||||
|
Main {
|
||||||
|
layout: String,
|
||||||
|
tile: usize,
|
||||||
|
camera: Camera,
|
||||||
|
},
|
||||||
|
Floating {
|
||||||
|
layout: String,
|
||||||
|
handle: Handle,
|
||||||
|
viewport: Box<acadrust::entities::Viewport>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// Gap (pixels) between Model panes — the `pane_grid` spacing and the visible
|
/// Gap (pixels) between Model panes — the `pane_grid` spacing and the visible
|
||||||
/// divider width. The renderer derives tile rects through this same spacing so
|
/// divider width. The renderer derives tile rects through this same spacing so
|
||||||
/// the drawn viewports line up exactly with the pane_grid layout.
|
/// the drawn viewports line up exactly with the pane_grid layout.
|
||||||
|
|
@ -1488,6 +1502,10 @@ struct SceneLight {
|
||||||
|
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
pub camera: Rc<RefCell<Camera>>,
|
pub camera: Rc<RefCell<Camera>>,
|
||||||
|
/// View saved immediately before the latest navigation operation. ZOOM
|
||||||
|
/// Previous swaps with this snapshot, allowing the user to toggle between
|
||||||
|
/// the two most recent views without touching drawing history.
|
||||||
|
previous_view: Option<ViewSnapshot>,
|
||||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||||
/// the split buttons / VPORTS subdivide the active tile.
|
/// the split buttons / VPORTS subdivide the active tile.
|
||||||
pub(crate) model_tiles: RefCell<Vec<ModelTile>>,
|
pub(crate) model_tiles: RefCell<Vec<ModelTile>>,
|
||||||
|
|
@ -1932,6 +1950,7 @@ impl Scene {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
camera: Rc::new(RefCell::new(Camera::default())),
|
camera: Rc::new(RefCell::new(Camera::default())),
|
||||||
|
previous_view: None,
|
||||||
model_tiles: RefCell::new(vec![ModelTile {
|
model_tiles: RefCell::new(vec![ModelTile {
|
||||||
rect: iced::Rectangle {
|
rect: iced::Rectangle {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue