feat(view): implement LIMITS command

Closes #478
This commit is contained in:
Hakan Seven 2026-07-27 16:15:28 +03:00
commit 8018ab1523
15 changed files with 478 additions and 6 deletions

View file

@ -4,6 +4,22 @@ use acadrust::Handle;
use iced::Task;
impl OpenCADStudio {
/// Apply LIMCHECK/PLIMCHECK to a point before an interactive command
/// consumes it. LIMITS itself must be able to redefine a rectangle beyond
/// the old boundary, so it is the sole bypass.
pub(super) fn command_point_allowed(&mut self, i: usize, point: glam::DVec3) -> bool {
let checks_limits = self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.name() != "LIMITS")
&& self.tabs[i].scene.drawing_limit_check_enabled();
if checks_limits && !self.tabs[i].scene.point_inside_drawing_limits(point) {
self.command_line.push_error("Outside limits.");
return false;
}
true
}
/// Drive the active command's step machine with one [`StepInput`], then
/// apply the result. This is the single entry point every input source
/// (command line, headless, dynamic input, plugin API, viewport) funnels
@ -19,6 +35,11 @@ impl OpenCADStudio {
}
}
let i = self.active_tab;
if let StepInput::Point(point) = &input {
if !self.command_point_allowed(i, *point) {
return Task::none();
}
}
let ctrl = self.ctrl_down;
let shift = self.shift_down;
let result: Option<CmdResult> = {
@ -229,6 +250,9 @@ impl OpenCADStudio {
None => coord,
},
};
if !self.command_point_allowed(i, wcs) {
return;
}
self.last_point = Some(wcs);
self.push_ucs_to_cmd(i);
let _ = self.feed_command(StepInput::Point(wcs));

View file

@ -692,9 +692,9 @@ impl OpenCADStudio {
self.command_line.push_output("Zoom Out");
}
// ZOOM ALL — fit all entities (same as EXTENTS for now)
// ZOOM ALL — fit the configured drawing limits.
"ZOOM ALL" | "ZOOM A" | "ZA" => {
self.tabs[i].scene.fit_all();
self.tabs[i].scene.fit_all_with_limits();
self.command_line.push_output("Zoom All");
}

View file

@ -142,6 +142,62 @@ impl OpenCADStudio {
return Some(Task::done(Message::ToggleViewCube));
}
// ── LIMITS — drawing/grid boundary for the active space ─────────────
"LIMITS" => {
use crate::modules::view::limits::LimitsCommand;
let (min, max) = self.tabs[i]
.scene
.current_drawing_limits()
.unwrap_or((glam::DVec2::ZERO, glam::DVec2::new(12.0, 9.0)));
let command = LimitsCommand::new(min, max);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"LIMITS ON" | "LIMITS OFF" => {
let enabled = cmd.ends_with("ON");
if self.tabs[i].scene.drawing_limit_check_enabled() != enabled {
self.push_undo_snapshot(i, "LIMITS");
self.tabs[i].scene.set_drawing_limit_check(enabled);
self.tabs[i].dirty = true;
}
self.command_line.push_output(if enabled {
"Limits checking ON."
} else {
"Limits checking OFF."
});
}
cmd if cmd.starts_with("LIMITS SET ") => {
let tokens: Vec<&str> = cmd["LIMITS SET ".len()..].split_whitespace().collect();
let values: Result<Vec<f64>, _> =
tokens.iter().map(|value| value.parse()).collect();
let Ok(values) = values else {
self.command_line
.push_error("LIMITS: four numeric coordinates required.");
return Some(Task::none());
};
if tokens.len() != 4 || !values.iter().all(|value| value.is_finite()) {
self.command_line
.push_error("LIMITS: four finite numeric coordinates required.");
} else {
let first = glam::DVec2::new(values[0], values[1]);
let opposite = glam::DVec2::new(values[2], values[3]);
let min = first.min(opposite);
let max = first.max(opposite);
if min.x == max.x || min.y == max.y {
self.command_line
.push_error("LIMITS: corners must define a non-zero area.");
} else {
self.push_undo_snapshot(i, "LIMITS");
self.tabs[i].scene.set_current_drawing_limits(min, max);
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"Drawing limits: {:.4},{:.4} to {:.4},{:.4}.",
min.x, min.y, max.x, max.y
));
}
}
}
// ── PROPERTIES — toggle Properties panel visibility ──────────────────
"PROPERTIES" | "PROPS" => {
return Some(Task::done(Message::ToggleProperties));

View file

@ -90,8 +90,14 @@ impl OpenCADStudio {
.borrow_mut()
.right_click_entered = false;
// A fresh command starts at the polar/cartesian default — clear
// any `,`-driven reshape from a previous command (#35).
// any `,`-driven reshape and locked dynamic-input values from a
// previous command. Otherwise a bare Enter on the first point prompt
// can commit that stale coordinate instead of accepting the command's
// default (LIMITS then compares an unintended lower-left point with
// the displayed default upper-right).
self.dyn_user_reshaped = false;
self.tabs[i].dyn_fields.clear();
self.tabs[i].dyn_active = 0;
if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") {
let path = PathBuf::from(path_str);
@ -208,6 +214,7 @@ impl OpenCADStudio {
let i = self.active_tab;
if self.tabs[i].active_cmd.is_some() {
self.tabs[i].last_cmd = Some(cmd.to_string());
self.sync_dyn_fields();
self.focus_cmd_input()
} else {
Task::none()

View file

@ -323,6 +323,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if let Some((base, dir)) = self.otrack_active {
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
let pt = base + dir * dist;
if !self.command_point_allowed(i, pt) {
return Task::none();
}
self.last_point = Some(pt);
self.dyn_user_reshaped = false;
self.sync_dyn_fields();
@ -365,6 +368,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
}
};
if !self.command_point_allowed(i, wcs_pt) {
return Task::none();
}
self.last_point = Some(wcs_pt);
self.dyn_user_reshaped = false;
self.sync_dyn_fields();

View file

@ -484,6 +484,9 @@ impl OpenCADStudio {
{
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
let pt = base + dir * dist;
if !self.command_point_allowed(i, pt) {
return Some(Task::none());
}
self.last_point = Some(pt);
for f in self.tabs[i].dyn_fields.iter_mut() {
f.buffer = None;
@ -547,6 +550,9 @@ impl OpenCADStudio {
});
}
let pt = self.dyn_resolve_point()?;
if !self.command_point_allowed(i, pt) {
return Some(Task::none());
}
self.last_point = Some(pt);
self.dyn_user_reshaped = false;
self.sync_dyn_fields();

View file

@ -2383,6 +2383,8 @@ impl OpenCADStudio {
self.command_line.push_info("Select a tangent object.");
None
}
} else if !self.command_point_allowed(i, world_pt) {
None
} else {
// A scalar typed into the dynamic-input box but not
// yet confirmed with Enter is applied before the

View file

@ -238,6 +238,7 @@ impl OpenCADStudio {
),
origin,
axes,
limits: tab.scene.grid_limits_for_viewport(handle),
}
})
.collect();