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 |
|
||||
|---|---|---|---|
|
||||
| `ZOOM` | Z | Zoom | ✅ |
|
||||
| `LIMITS` | — | Drawing/grid limits and point checking | ✅ |
|
||||
| `PAN` | P | Pan | ✅ |
|
||||
| `ORBIT` | 3DO | 3D orbit | ✅ |
|
||||
| `VPORTS` | — | Viewport configuration | ✅ |
|
||||
|
|
@ -356,11 +357,11 @@ Status of every standard CAD command in Open CAD Studio:
|
|||
| Layer | 19 | 17 | 0 | 2 |
|
||||
| Block & Reference | 23 | 18 | 2 | 3 |
|
||||
| 3D Modeling | 27 | 25 | 0 | 2 |
|
||||
| View & Navigation | 32 | 15 | 8 | 9 |
|
||||
| View & Navigation | 33 | 16 | 8 | 9 |
|
||||
| Inquiry | 12 | 12 | 0 | 0 |
|
||||
| File & Plot | 17 | 16 | 0 | 1 |
|
||||
| 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`,
|
||||
> `MINSERT`, `SUBTRACT`/`UNION`/`INTERSECT` appear in both their 2D and 3D groups).
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ impl OpenCADStudio {
|
|||
),
|
||||
origin,
|
||||
axes,
|
||||
limits: tab.scene.grid_limits_for_viewport(handle),
|
||||
}
|
||||
})
|
||||
.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 hidden;
|
||||
mod layout_tabs;
|
||||
pub mod limits;
|
||||
mod orbit;
|
||||
mod ortho;
|
||||
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 group_layer;
|
||||
mod layout;
|
||||
mod limits;
|
||||
mod modify;
|
||||
mod mspace;
|
||||
mod page_setup;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ pub struct GridParams {
|
|||
/// `(ZERO, X, Y, Z)`.
|
||||
pub origin: glam::DVec3,
|
||||
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.
|
||||
|
|
@ -191,7 +194,7 @@ impl canvas::Program<Message> for GridCanvas {
|
|||
height: cy1 - cy0,
|
||||
};
|
||||
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,
|
||||
grid_origin: glam::DVec3,
|
||||
grid_axes: (Vec3, Vec3, Vec3),
|
||||
limits: Option<(glam::DVec2, glam::DVec2)>,
|
||||
) {
|
||||
if bounds.width <= 0.0 || bounds.height <= 0.0 {
|
||||
return;
|
||||
|
|
@ -1558,6 +1562,94 @@ fn draw_grid(
|
|||
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;
|
||||
|
||||
// Lines parallel to axis2 (varying axis1 position).
|
||||
|
|
|
|||
Loading…
Reference in a new issue