From 8018ab15234cc54c4fce8799042434be74ca8820 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 27 Jul 2026 16:15:28 +0300 Subject: [PATCH] feat(view): implement LIMITS command Closes #478 --- AGENTS.md | 4 + COMMANDS.md | 5 +- src/app/command_driver.rs | 24 +++++ src/app/commands/dim.rs | 4 +- src/app/commands/display.rs | 56 ++++++++++++ src/app/commands/mod.rs | 9 +- src/app/update/command.rs | 6 ++ src/app/update/dynamic.rs | 6 ++ src/app/update/viewport.rs | 2 + src/app/view/mod.rs | 1 + src/modules/view/limits.rs | 98 ++++++++++++++++++++ src/modules/view/mod.rs | 1 + src/scene/limits.rs | 173 ++++++++++++++++++++++++++++++++++++ src/scene/mod.rs | 1 + src/ui/overlay.rs | 94 +++++++++++++++++++- 15 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/modules/view/limits.rs create mode 100644 src/scene/limits.rs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..b408576ea --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/COMMANDS.md b/COMMANDS.md index ea6709db7..780c90ec9 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -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). diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e6228fa7b..5b5e28ae4 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -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 = { @@ -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)); diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 37c992c15..0ed5ad2eb 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -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"); } diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 7a29318c0..84bdd6353 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -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, _> = + 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)); diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index a0966a2e7..a6e81750a 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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() diff --git a/src/app/update/command.rs b/src/app/update/command.rs index b5ca5d5d0..819914fad 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -323,6 +323,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { 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 { } } }; + 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(); diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index fdc097e01..4288c9789 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -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(); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index a29f87b71..b30938236 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -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 diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 80f24233b..d598d3a54 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -238,6 +238,7 @@ impl OpenCADStudio { ), origin, axes, + limits: tab.scene.grid_limits_for_viewport(handle), } }) .collect(); diff --git a/src/modules/view/limits.rs b/src/modules/view/limits.rs new file mode 100644 index 000000000..3f2f18258 --- /dev/null +++ b/src/modules/view/limits.rs @@ -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 { + 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 { + 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"] }); diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index a5d09e51d..067a81f46 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -4,6 +4,7 @@ mod cascade; mod file_tabs; mod hidden; mod layout_tabs; +pub mod limits; mod orbit; mod ortho; mod pan; diff --git a/src/scene/limits.rs b/src/scene/limits.rs new file mode 100644 index 000000000..b6004997f --- /dev/null +++ b/src/scene/limits.rs @@ -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; + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index daa42653e..de1c42edf 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -20,6 +20,7 @@ mod camera_ops; mod entity; mod group_layer; mod layout; +mod limits; mod modify; mod mspace; mod page_setup; diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 3dddb7490..f333049e5 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -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 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).