parent
a4235a1243
commit
8018ab1523
15 changed files with 478 additions and 6 deletions
4
AGENTS.md
Normal file
4
AGENTS.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
- Never run `cargo fmt`, `rustfmt`, or any automatic source-formatting command in this repository.
|
||||||
|
- Preserve the existing formatting and make only targeted edits.
|
||||||
|
|
@ -241,6 +241,7 @@ Status of every standard CAD command in Open CAD Studio:
|
||||||
| Command | Alias | Description | Status |
|
| Command | Alias | Description | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `ZOOM` | Z | Zoom | ✅ |
|
| `ZOOM` | Z | Zoom | ✅ |
|
||||||
|
| `LIMITS` | — | Drawing/grid limits and point checking | ✅ |
|
||||||
| `PAN` | P | Pan | ✅ |
|
| `PAN` | P | Pan | ✅ |
|
||||||
| `ORBIT` | 3DO | 3D orbit | ✅ |
|
| `ORBIT` | 3DO | 3D orbit | ✅ |
|
||||||
| `VPORTS` | — | Viewport configuration | ✅ |
|
| `VPORTS` | — | Viewport configuration | ✅ |
|
||||||
|
|
@ -356,11 +357,11 @@ Status of every standard CAD command in Open CAD Studio:
|
||||||
| Layer | 19 | 17 | 0 | 2 |
|
| Layer | 19 | 17 | 0 | 2 |
|
||||||
| Block & Reference | 23 | 18 | 2 | 3 |
|
| Block & Reference | 23 | 18 | 2 | 3 |
|
||||||
| 3D Modeling | 27 | 25 | 0 | 2 |
|
| 3D Modeling | 27 | 25 | 0 | 2 |
|
||||||
| View & Navigation | 32 | 15 | 8 | 9 |
|
| View & Navigation | 33 | 16 | 8 | 9 |
|
||||||
| Inquiry | 12 | 12 | 0 | 0 |
|
| Inquiry | 12 | 12 | 0 | 0 |
|
||||||
| File & Plot | 17 | 16 | 0 | 1 |
|
| File & Plot | 17 | 16 | 0 | 1 |
|
||||||
| Manage & Customize | 22 | 13 | 3 | 6 |
|
| Manage & Customize | 22 | 13 | 3 | 6 |
|
||||||
| **Total** | **262** | **219** | **15** | **28** |
|
| **Total** | **263** | **220** | **15** | **28** |
|
||||||
|
|
||||||
> Counts include commands listed under more than one category (e.g. `SLICE`, `HELIX`,
|
> Counts include commands listed under more than one category (e.g. `SLICE`, `HELIX`,
|
||||||
> `MINSERT`, `SUBTRACT`/`UNION`/`INTERSECT` appear in both their 2D and 3D groups).
|
> `MINSERT`, `SUBTRACT`/`UNION`/`INTERSECT` appear in both their 2D and 3D groups).
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,22 @@ use acadrust::Handle;
|
||||||
use iced::Task;
|
use iced::Task;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
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
|
/// Drive the active command's step machine with one [`StepInput`], then
|
||||||
/// apply the result. This is the single entry point every input source
|
/// apply the result. This is the single entry point every input source
|
||||||
/// (command line, headless, dynamic input, plugin API, viewport) funnels
|
/// (command line, headless, dynamic input, plugin API, viewport) funnels
|
||||||
|
|
@ -19,6 +35,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
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 ctrl = self.ctrl_down;
|
||||||
let shift = self.shift_down;
|
let shift = self.shift_down;
|
||||||
let result: Option<CmdResult> = {
|
let result: Option<CmdResult> = {
|
||||||
|
|
@ -229,6 +250,9 @@ impl OpenCADStudio {
|
||||||
None => coord,
|
None => coord,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if !self.command_point_allowed(i, wcs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.last_point = Some(wcs);
|
self.last_point = Some(wcs);
|
||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
let _ = self.feed_command(StepInput::Point(wcs));
|
let _ = self.feed_command(StepInput::Point(wcs));
|
||||||
|
|
|
||||||
|
|
@ -692,9 +692,9 @@ impl OpenCADStudio {
|
||||||
self.command_line.push_output("Zoom Out");
|
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" => {
|
"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");
|
self.command_line.push_output("Zoom All");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,62 @@ impl OpenCADStudio {
|
||||||
return Some(Task::done(Message::ToggleViewCube));
|
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 — toggle Properties panel visibility ──────────────────
|
||||||
"PROPERTIES" | "PROPS" => {
|
"PROPERTIES" | "PROPS" => {
|
||||||
return Some(Task::done(Message::ToggleProperties));
|
return Some(Task::done(Message::ToggleProperties));
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,14 @@ impl OpenCADStudio {
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.right_click_entered = false;
|
.right_click_entered = false;
|
||||||
// A fresh command starts at the polar/cartesian default — clear
|
// 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.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:") {
|
if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") {
|
||||||
let path = PathBuf::from(path_str);
|
let path = PathBuf::from(path_str);
|
||||||
|
|
@ -208,6 +214,7 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.tabs[i].active_cmd.is_some() {
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
self.tabs[i].last_cmd = Some(cmd.to_string());
|
self.tabs[i].last_cmd = Some(cmd.to_string());
|
||||||
|
self.sync_dyn_fields();
|
||||||
self.focus_cmd_input()
|
self.focus_cmd_input()
|
||||||
} else {
|
} else {
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -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((base, dir)) = self.otrack_active {
|
||||||
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
|
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
|
||||||
let pt = base + dir * dist;
|
let pt = base + dir * dist;
|
||||||
|
if !self.command_point_allowed(i, pt) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.last_point = Some(pt);
|
self.last_point = Some(pt);
|
||||||
self.dyn_user_reshaped = false;
|
self.dyn_user_reshaped = false;
|
||||||
self.sync_dyn_fields();
|
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.last_point = Some(wcs_pt);
|
||||||
self.dyn_user_reshaped = false;
|
self.dyn_user_reshaped = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
|
|
|
||||||
|
|
@ -484,6 +484,9 @@ impl OpenCADStudio {
|
||||||
{
|
{
|
||||||
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
|
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
|
||||||
let pt = base + dir * dist;
|
let pt = base + dir * dist;
|
||||||
|
if !self.command_point_allowed(i, pt) {
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
self.last_point = Some(pt);
|
self.last_point = Some(pt);
|
||||||
for f in self.tabs[i].dyn_fields.iter_mut() {
|
for f in self.tabs[i].dyn_fields.iter_mut() {
|
||||||
f.buffer = None;
|
f.buffer = None;
|
||||||
|
|
@ -547,6 +550,9 @@ impl OpenCADStudio {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let pt = self.dyn_resolve_point()?;
|
let pt = self.dyn_resolve_point()?;
|
||||||
|
if !self.command_point_allowed(i, pt) {
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
self.last_point = Some(pt);
|
self.last_point = Some(pt);
|
||||||
self.dyn_user_reshaped = false;
|
self.dyn_user_reshaped = false;
|
||||||
self.sync_dyn_fields();
|
self.sync_dyn_fields();
|
||||||
|
|
|
||||||
|
|
@ -2383,6 +2383,8 @@ impl OpenCADStudio {
|
||||||
self.command_line.push_info("Select a tangent object.");
|
self.command_line.push_info("Select a tangent object.");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
} else if !self.command_point_allowed(i, world_pt) {
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
// A scalar typed into the dynamic-input box but not
|
// A scalar typed into the dynamic-input box but not
|
||||||
// yet confirmed with Enter is applied before the
|
// yet confirmed with Enter is applied before the
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,7 @@ impl OpenCADStudio {
|
||||||
),
|
),
|
||||||
origin,
|
origin,
|
||||||
axes,
|
axes,
|
||||||
|
limits: tab.scene.grid_limits_for_viewport(handle),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
98
src/modules/view/limits.rs
Normal file
98
src/modules/view/limits.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
use glam::{DVec2, DVec3};
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||||
|
|
||||||
|
enum LimitsStep {
|
||||||
|
FirstCorner,
|
||||||
|
OppositeCorner(DVec2),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interactive front-end for LIMITS. The command itself only gathers input;
|
||||||
|
/// the dispatched form mutates the active drawing/layout in one central place.
|
||||||
|
pub struct LimitsCommand {
|
||||||
|
step: LimitsStep,
|
||||||
|
current_min: DVec2,
|
||||||
|
current_max: DVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LimitsCommand {
|
||||||
|
pub fn new(current_min: DVec2, current_max: DVec2) -> Self {
|
||||||
|
Self {
|
||||||
|
step: LimitsStep::FirstCorner,
|
||||||
|
current_min,
|
||||||
|
current_max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_text(point: DVec2) -> String {
|
||||||
|
format!("{:.17} {:.17}", point.x, point.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for LimitsCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"LIMITS"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.step {
|
||||||
|
LimitsStep::FirstCorner => format!(
|
||||||
|
"LIMITS Specify first corner or [On / Off] <{:.4},{:.4}>:",
|
||||||
|
self.current_min.x, self.current_min.y
|
||||||
|
),
|
||||||
|
LimitsStep::OppositeCorner(_) => format!(
|
||||||
|
"LIMITS Specify opposite corner <{:.4},{:.4}>:",
|
||||||
|
self.current_max.x, self.current_max.y
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
LimitsStep::FirstCorner => vec![CmdOption::new("On", "ON"), CmdOption::new("Off", "OFF")],
|
||||||
|
LimitsStep::OppositeCorner(_) => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
if !matches!(self.step, LimitsStep::FirstCorner) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match text.trim().to_ascii_uppercase().as_str() {
|
||||||
|
"ON" => Some(CmdResult::Dispatch("LIMITS ON".to_string())),
|
||||||
|
"OFF" => Some(CmdResult::Dispatch("LIMITS OFF".to_string())),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
let point = point.truncate();
|
||||||
|
match self.step {
|
||||||
|
LimitsStep::FirstCorner => {
|
||||||
|
self.step = LimitsStep::OppositeCorner(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
LimitsStep::OppositeCorner(first) => CmdResult::Dispatch(format!(
|
||||||
|
"LIMITS SET {} {}",
|
||||||
|
Self::point_text(first),
|
||||||
|
Self::point_text(point)
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
LimitsStep::FirstCorner => {
|
||||||
|
self.step = LimitsStep::OppositeCorner(self.current_min);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
LimitsStep::OppositeCorner(first) => CmdResult::Dispatch(format!(
|
||||||
|
"LIMITS SET {} {}",
|
||||||
|
Self::point_text(first),
|
||||||
|
Self::point_text(self.current_max)
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["LIMITS"] });
|
||||||
|
|
@ -4,6 +4,7 @@ mod cascade;
|
||||||
mod file_tabs;
|
mod file_tabs;
|
||||||
mod hidden;
|
mod hidden;
|
||||||
mod layout_tabs;
|
mod layout_tabs;
|
||||||
|
pub mod limits;
|
||||||
mod orbit;
|
mod orbit;
|
||||||
mod ortho;
|
mod ortho;
|
||||||
mod pan;
|
mod pan;
|
||||||
|
|
|
||||||
173
src/scene/limits.rs
Normal file
173
src/scene/limits.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
fn model_limits(&self) -> Option<(glam::DVec2, glam::DVec2)> {
|
||||||
|
let min = self.document.header.model_space_limits_min;
|
||||||
|
let max = self.document.header.model_space_limits_max;
|
||||||
|
Self::valid_limits(
|
||||||
|
glam::DVec2::new(min.x, min.y),
|
||||||
|
glam::DVec2::new(max.x, max.y),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paper_layout_limits(&self) -> Option<(glam::DVec2, glam::DVec2)> {
|
||||||
|
self.document.objects.values().find_map(|object| {
|
||||||
|
let ObjectType::Layout(layout) = object else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
(layout.name == self.current_layout).then(|| {
|
||||||
|
Self::valid_limits(
|
||||||
|
glam::DVec2::new(layout.min_limits.0, layout.min_limits.1),
|
||||||
|
glam::DVec2::new(layout.max_limits.0, layout.max_limits.1),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_limits(min: glam::DVec2, max: glam::DVec2) -> Option<(glam::DVec2, glam::DVec2)> {
|
||||||
|
const SANE_LIMIT: f64 = 1.0e16;
|
||||||
|
(min.is_finite()
|
||||||
|
&& max.is_finite()
|
||||||
|
&& min.x < max.x
|
||||||
|
&& min.y < max.y
|
||||||
|
&& min.abs().max_element() < SANE_LIMIT
|
||||||
|
&& max.abs().max_element() < SANE_LIMIT)
|
||||||
|
.then_some((min, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active input space is model space on the Model tab and while editing
|
||||||
|
/// through a floating paper-space viewport (MSPACE).
|
||||||
|
pub fn input_uses_model_space(&self) -> bool {
|
||||||
|
self.current_layout == "Model" || self.active_viewport.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LIMITS rectangle for the active point-input space.
|
||||||
|
pub fn current_drawing_limits(&self) -> Option<(glam::DVec2, glam::DVec2)> {
|
||||||
|
if self.input_uses_model_space() {
|
||||||
|
self.model_limits()
|
||||||
|
} else {
|
||||||
|
self.paper_layout_limits().or_else(|| {
|
||||||
|
let min = self.document.header.paper_space_limits_min;
|
||||||
|
let max = self.document.header.paper_space_limits_max;
|
||||||
|
Self::valid_limits(
|
||||||
|
glam::DVec2::new(min.x, min.y),
|
||||||
|
glam::DVec2::new(max.x, max.y),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LIMITS rectangle belonging to a rendered grid viewport. Floating
|
||||||
|
/// viewports display model space; the sheet viewport displays paper space.
|
||||||
|
pub fn grid_limits_for_viewport(&self, viewport: Handle) -> Option<(glam::DVec2, glam::DVec2)> {
|
||||||
|
if self.current_layout == "Model" {
|
||||||
|
return self.model_limits();
|
||||||
|
}
|
||||||
|
let sheet = self.current_layout_sheet_viewport_handle();
|
||||||
|
if viewport.is_valid() && viewport != sheet {
|
||||||
|
self.model_limits()
|
||||||
|
} else {
|
||||||
|
self.paper_layout_limits()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drawing_limit_check_enabled(&self) -> bool {
|
||||||
|
if self.input_uses_model_space() {
|
||||||
|
self.document.header.limit_check
|
||||||
|
} else {
|
||||||
|
self.document.header.paper_space_limit_check
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn point_inside_drawing_limits(&self, point: glam::DVec3) -> bool {
|
||||||
|
let Some((min, max)) = self.current_drawing_limits() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
point.x >= min.x && point.x <= max.x && point.y >= min.y && point.y <= max.y
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_drawing_limit_check(&mut self, enabled: bool) {
|
||||||
|
if self.input_uses_model_space() {
|
||||||
|
self.document.header.limit_check = enabled;
|
||||||
|
} else {
|
||||||
|
self.document.header.paper_space_limit_check = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_drawing_limits(&mut self, min: glam::DVec2, max: glam::DVec2) {
|
||||||
|
if self.input_uses_model_space() {
|
||||||
|
self.document.header.model_space_limits_min =
|
||||||
|
acadrust::types::Vector2::new(min.x, min.y);
|
||||||
|
self.document.header.model_space_limits_max =
|
||||||
|
acadrust::types::Vector2::new(max.x, max.y);
|
||||||
|
} else {
|
||||||
|
self.document.header.paper_space_limits_min =
|
||||||
|
acadrust::types::Vector2::new(min.x, min.y);
|
||||||
|
self.document.header.paper_space_limits_max =
|
||||||
|
acadrust::types::Vector2::new(max.x, max.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the current Layout object synchronized with the header values.
|
||||||
|
// DWG stores per-layout limits here as well as the current-space header.
|
||||||
|
for object in self.document.objects.values_mut() {
|
||||||
|
if let ObjectType::Layout(layout) = object {
|
||||||
|
if layout.name == self.current_layout {
|
||||||
|
layout.min_limits = (min.x, min.y);
|
||||||
|
layout.max_limits = (max.x, max.y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ZOOM All frames the configured drawing limits. Object-only framing
|
||||||
|
/// remains the responsibility of ZOOM Extents.
|
||||||
|
pub fn fit_all_with_limits(&mut self) {
|
||||||
|
let Some((limit_min, limit_max)) = self.current_drawing_limits() else {
|
||||||
|
self.fit_all();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let min = glam::Vec3::new(limit_min.x as f32, limit_min.y as f32, 0.0);
|
||||||
|
let max = glam::Vec3::new(limit_max.x as f32, limit_max.y as f32, 0.0);
|
||||||
|
|
||||||
|
// MSPACE owns a camera encoded on the active viewport entity.
|
||||||
|
if let Some(viewport_handle) = self.active_viewport {
|
||||||
|
let (width, height, locked) = match self.document.get_entity(viewport_handle) {
|
||||||
|
Some(EntityType::Viewport(viewport)) => {
|
||||||
|
(viewport.width, viewport.height, viewport.status.locked)
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
if locked {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let aspect = (width / height.max(1e-9)) as f32;
|
||||||
|
let mut camera = match self.viewport_edit_frame(self.selection.borrow().vp_size) {
|
||||||
|
Some((camera, _)) => camera,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
camera.fit_to_bounds(min, max, aspect.max(0.01));
|
||||||
|
if let Some(EntityType::Viewport(viewport)) =
|
||||||
|
self.document.get_entity_mut(viewport_handle)
|
||||||
|
{
|
||||||
|
viewport.view_target.x = camera.target.x;
|
||||||
|
viewport.view_target.y = camera.target.y;
|
||||||
|
viewport.view_target.z = camera.target.z;
|
||||||
|
viewport.view_center.x = 0.0;
|
||||||
|
viewport.view_center.y = 0.0;
|
||||||
|
viewport.view_height = camera.ortho_size() as f64 * 2.0;
|
||||||
|
if viewport.view_height > 1e-9 {
|
||||||
|
viewport.custom_scale = viewport.height / viewport.view_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.camera_generation += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.camera
|
||||||
|
.borrow_mut()
|
||||||
|
.fit_to_bounds(min, max, self.last_render_aspect.get().max(0.01));
|
||||||
|
self.camera_generation += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ mod camera_ops;
|
||||||
mod entity;
|
mod entity;
|
||||||
mod group_layer;
|
mod group_layer;
|
||||||
mod layout;
|
mod layout;
|
||||||
|
mod limits;
|
||||||
mod modify;
|
mod modify;
|
||||||
mod mspace;
|
mod mspace;
|
||||||
mod page_setup;
|
mod page_setup;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ pub struct GridParams {
|
||||||
/// `(ZERO, X, Y, Z)`.
|
/// `(ZERO, X, Y, Z)`.
|
||||||
pub origin: glam::DVec3,
|
pub origin: glam::DVec3,
|
||||||
pub axes: (Vec3, Vec3, Vec3),
|
pub axes: (Vec3, Vec3, Vec3),
|
||||||
|
/// WCS XY drawing limits. When present, grid lines stop at this rectangle
|
||||||
|
/// instead of extending across the full viewport.
|
||||||
|
pub limits: Option<(glam::DVec2, glam::DVec2)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the adaptive grid step size (world units) from camera zoom.
|
/// Compute the adaptive grid step size (world units) from camera zoom.
|
||||||
|
|
@ -191,7 +194,7 @@ impl canvas::Program<Message> for GridCanvas {
|
||||||
height: cy1 - cy0,
|
height: cy1 - cy0,
|
||||||
};
|
};
|
||||||
frame.with_clip(clip, |f| {
|
frame.with_clip(clip, |f| {
|
||||||
draw_grid(f, g.view_rot, g.eye, gb, g.step, g.origin, g.axes)
|
draw_grid(f, g.view_rot, g.eye, gb, g.step, g.origin, g.axes, g.limits)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1163,6 +1166,7 @@ fn draw_grid(
|
||||||
step: f32,
|
step: f32,
|
||||||
grid_origin: glam::DVec3,
|
grid_origin: glam::DVec3,
|
||||||
grid_axes: (Vec3, Vec3, Vec3),
|
grid_axes: (Vec3, Vec3, Vec3),
|
||||||
|
limits: Option<(glam::DVec2, glam::DVec2)>,
|
||||||
) {
|
) {
|
||||||
if bounds.width <= 0.0 || bounds.height <= 0.0 {
|
if bounds.width <= 0.0 || bounds.height <= 0.0 {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1558,6 +1562,94 @@ fn draw_grid(
|
||||||
frame.stroke(&path, st.clone());
|
frame.stroke(&path, st.clone());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A finite LIMITS rectangle replaces the usual viewport/horizon extent.
|
||||||
|
// Clip each UCS grid line analytically against the WCS XY rectangle, then
|
||||||
|
// project only that finite segment. This keeps the grid bounded even when
|
||||||
|
// the active UCS is rotated.
|
||||||
|
if let Some((limit_min, limit_max)) = limits {
|
||||||
|
let corners = [
|
||||||
|
glam::DVec3::new(limit_min.x, limit_min.y, grid_origin.z),
|
||||||
|
glam::DVec3::new(limit_max.x, limit_min.y, grid_origin.z),
|
||||||
|
glam::DVec3::new(limit_max.x, limit_max.y, grid_origin.z),
|
||||||
|
glam::DVec3::new(limit_min.x, limit_max.y, grid_origin.z),
|
||||||
|
];
|
||||||
|
let coordinate_range = |axis: Vec3| {
|
||||||
|
corners
|
||||||
|
.iter()
|
||||||
|
.fold((f32::INFINITY, f32::NEG_INFINITY), |(min, max), corner| {
|
||||||
|
let value = (*corner - grid_origin).as_vec3().dot(axis);
|
||||||
|
(min.min(value), max.max(value))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let clip_world_line = |family: usize, value: f32| -> Option<(Point, Point)> {
|
||||||
|
let (base, direction) = if family == 0 {
|
||||||
|
(grid_origin + (axis1 * value).as_dvec3(), axis2.as_dvec3())
|
||||||
|
} else {
|
||||||
|
(grid_origin + (axis2 * value).as_dvec3(), axis1.as_dvec3())
|
||||||
|
};
|
||||||
|
let (mut t0, mut t1) = (f64::NEG_INFINITY, f64::INFINITY);
|
||||||
|
let mut clip_axis = |origin: f64, delta: f64, low: f64, high: f64| {
|
||||||
|
if delta.abs() < 1e-12 {
|
||||||
|
return origin >= low && origin <= high;
|
||||||
|
}
|
||||||
|
let a = (low - origin) / delta;
|
||||||
|
let b = (high - origin) / delta;
|
||||||
|
t0 = t0.max(a.min(b));
|
||||||
|
t1 = t1.min(a.max(b));
|
||||||
|
t0 <= t1
|
||||||
|
};
|
||||||
|
if !clip_axis(base.x, direction.x, limit_min.x, limit_max.x)
|
||||||
|
|| !clip_axis(base.y, direction.y, limit_min.y, limit_max.y)
|
||||||
|
|| !t0.is_finite()
|
||||||
|
|| !t1.is_finite()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let p0 = project(base + direction * t0)?;
|
||||||
|
let p1 = project(base + direction * t1)?;
|
||||||
|
let local_bounds = iced::Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
};
|
||||||
|
clip_seg(p0, p1, local_bounds).map(|(p0, p1)| {
|
||||||
|
(
|
||||||
|
Point::new(p0.x + bounds.x, p0.y + bounds.y),
|
||||||
|
Point::new(p1.x + bounds.x, p1.y + bounds.y),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let (min1, max1) = coordinate_range(axis1);
|
||||||
|
let (min2, max2) = coordinate_range(axis2);
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
if let Some((_, anchor_world, gap)) = best_anchor(0) {
|
||||||
|
if gap >= MIN_HORIZON_GRID_PX {
|
||||||
|
let anchor = (anchor_world - grid_origin).as_vec3().dot(axis1);
|
||||||
|
let (start, end) = line_range(min1, max1, anchor);
|
||||||
|
for index in start..=end {
|
||||||
|
if let Some(segment) = clip_world_line(0, index as f32 * s) {
|
||||||
|
segments.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((_, anchor_world, gap)) = best_anchor(1) {
|
||||||
|
if gap >= MIN_HORIZON_GRID_PX {
|
||||||
|
let anchor = (anchor_world - grid_origin).as_vec3().dot(axis2);
|
||||||
|
let (start, end) = line_range(min2, max2, anchor);
|
||||||
|
for index in start..=end {
|
||||||
|
if let Some(segment) = clip_world_line(1, index as f32 * s) {
|
||||||
|
segments.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw_segments(frame, &segments);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut axis_extent = 0.0_f32;
|
let mut axis_extent = 0.0_f32;
|
||||||
|
|
||||||
// Lines parallel to axis2 (varying axis1 position).
|
// Lines parallel to axis2 (varying axis1 position).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue