forked from 1b-agent/opencadstudio-upstream
feat: editable grid spacing and major grid lines in Drafting Settings
The Snap and Grid tab showed static grid values with dead checkboxes, and every grid line stroked identically. Grid X/Y spacing and Major-line-every are now editable inputs (validated 2-100 for majors, dirty-tracked, applied with an error message on bad input); Adaptive-grid and Display-beyond-Limits checkboxes are wired to live state. Every Nth line partitions into major_segments and strokes subtly brighter (1.5x alpha, 0.75px) for the reference hierarchy. GRIDMAJOR state persists to UserSettings. Covers partition, invalidation, and parse tests.
This commit is contained in:
1 parent
05f462f294
commit
4aa699e08d
9 files changed
+249
-37
No files matched your search
@@ -1180,15 +1180,18 @@ fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) {
|
||||
let origin2 = glam::DVec3::new(0.0, 0.0, 0.0);
|
||||
let axes2 = (Vec3::X, Vec3::Y, Vec3::Z);
|
||||
let limits2: Option<(glam::DVec2, glam::DVec2)> = None;
|
||||
// Both panes use a square grid, so one step per pane plus a shared
|
||||
// major-line interval covers the new step_x / step_y / major_every args.
|
||||
let major_every = 5u32;
|
||||
|
||||
// ── 1. Uncached grid geometry generation ──
|
||||
if runner.should_run("ui_grid_geometry_uncached") {
|
||||
for _ in 0..10 {
|
||||
let _ = black_box(grid_segments(
|
||||
view_rot1, eye1, bounds1, step1, step1, origin1, axes1, limits1,
|
||||
view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1,
|
||||
));
|
||||
let _ = black_box(grid_segments(
|
||||
view_rot2, eye2, bounds2, step2, step2, origin2, axes2, limits2,
|
||||
view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1199,8 +1202,8 @@ fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) {
|
||||
for _ in 0..runs {
|
||||
let t0 = Instant::now();
|
||||
for _ in 0..n {
|
||||
let g1 = grid_segments(view_rot1, eye1, bounds1, step1, step1, origin1, axes1, limits1);
|
||||
let g2 = grid_segments(view_rot2, eye2, bounds2, step2, step2, origin2, axes2, limits2);
|
||||
let g1 = grid_segments(view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1);
|
||||
let g2 = grid_segments(view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2);
|
||||
black_box(g1);
|
||||
black_box(g2);
|
||||
}
|
||||
@@ -1227,6 +1230,7 @@ fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) {
|
||||
bounds: bounds1,
|
||||
step_x: step1,
|
||||
step_y: step1,
|
||||
major_every,
|
||||
origin: origin1,
|
||||
axes: axes1,
|
||||
limits: limits1,
|
||||
@@ -1237,6 +1241,7 @@ fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) {
|
||||
bounds: bounds2,
|
||||
step_x: step2,
|
||||
step_y: step2,
|
||||
major_every,
|
||||
origin: origin2,
|
||||
axes: axes2,
|
||||
limits: limits2,
|
||||
|
||||
@@ -16,6 +16,15 @@ impl DraftingSettingsState {
|
||||
snap_x_input: crate::ui::window::drafting_settings::format_snap_spacing(snap_x),
|
||||
snap_y_input: crate::ui::window::drafting_settings::format_snap_spacing(snap_y),
|
||||
snap_equal,
|
||||
grid_x_input: crate::ui::window::drafting_settings::format_snap_spacing(
|
||||
app.grid_spacing_x,
|
||||
),
|
||||
grid_y_input: crate::ui::window::drafting_settings::format_snap_spacing(
|
||||
app.grid_spacing_y,
|
||||
),
|
||||
grid_major_input: format!("{}", app.grid_major_every),
|
||||
grid_adaptive: app.grid_adaptive,
|
||||
grid_beyond_limits: app.grid_beyond_limits,
|
||||
isometric: app.isometric_drafting,
|
||||
iso_plane: app.iso_plane,
|
||||
snap_angle_deg: app.snap_angle_deg,
|
||||
@@ -61,7 +70,22 @@ impl OpenCADStudio {
|
||||
live.snap_y_input = live.snap_x_input.clone();
|
||||
}
|
||||
}
|
||||
let gx = crate::ui::window::drafting_settings::parse_snap_spacing(&state.grid_x_input);
|
||||
let gy = crate::ui::window::drafting_settings::parse_snap_spacing(&state.grid_y_input);
|
||||
let gm = crate::ui::window::drafting_settings::parse_grid_major(&state.grid_major_input);
|
||||
let (Some(gx), Some(gy), Some(gm)) = (gx, gy, gm) else {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Grid X/Y spacings must be positive numbers and Major every 2-100.")
|
||||
.as_ref(),
|
||||
);
|
||||
return false;
|
||||
};
|
||||
self.show_grid = state.grid_on;
|
||||
self.grid_spacing_x = gx;
|
||||
self.grid_spacing_y = gy;
|
||||
self.grid_major_every = gm;
|
||||
self.grid_adaptive = state.grid_adaptive;
|
||||
self.grid_beyond_limits = state.grid_beyond_limits;
|
||||
self.snapper.grid_snap_on = state.snap_on;
|
||||
self.snapper.snap_spacing_x = sx;
|
||||
self.snapper.snap_spacing_y = sy;
|
||||
@@ -97,6 +121,11 @@ mod tests {
|
||||
snap_x_input: "10".to_string(),
|
||||
snap_y_input: "10".to_string(),
|
||||
snap_equal: true,
|
||||
grid_x_input: "10".to_string(),
|
||||
grid_y_input: "10".to_string(),
|
||||
grid_major_input: "5".to_string(),
|
||||
grid_adaptive: true,
|
||||
grid_beyond_limits: true,
|
||||
isometric: false,
|
||||
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||
snap_angle_deg: 0.0,
|
||||
@@ -133,6 +162,11 @@ mod tests {
|
||||
snap_x_input: "10".to_string(),
|
||||
snap_y_input: "10".to_string(),
|
||||
snap_equal: true,
|
||||
grid_x_input: "10".to_string(),
|
||||
grid_y_input: "10".to_string(),
|
||||
grid_major_input: "5".to_string(),
|
||||
grid_adaptive: true,
|
||||
grid_beyond_limits: true,
|
||||
isometric: false,
|
||||
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||
snap_angle_deg: 0.0,
|
||||
@@ -220,11 +254,33 @@ mod tests {
|
||||
let mut modded = base.clone();
|
||||
modded.snap_equal = false;
|
||||
assert!(modded.is_dirty(&base));
|
||||
|
||||
let mut modded = base.clone();
|
||||
modded.grid_x_input = "5".to_string();
|
||||
assert!(modded.is_dirty(&base));
|
||||
|
||||
let mut modded = base.clone();
|
||||
modded.grid_y_input = "5".to_string();
|
||||
assert!(modded.is_dirty(&base));
|
||||
|
||||
let mut modded = base.clone();
|
||||
modded.grid_major_input = "10".to_string();
|
||||
assert!(modded.is_dirty(&base));
|
||||
|
||||
let mut modded = base.clone();
|
||||
modded.grid_adaptive = false;
|
||||
assert!(modded.is_dirty(&base));
|
||||
|
||||
let mut modded = base.clone();
|
||||
modded.grid_beyond_limits = false;
|
||||
assert!(modded.is_dirty(&base));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_snap_spacing() {
|
||||
use crate::ui::window::drafting_settings::{format_snap_spacing, parse_snap_spacing};
|
||||
use crate::ui::window::drafting_settings::{
|
||||
format_snap_spacing, parse_grid_major, parse_snap_spacing,
|
||||
};
|
||||
assert_eq!(parse_snap_spacing("10"), Some(10.0));
|
||||
assert_eq!(parse_snap_spacing(" 2.5 "), Some(2.5));
|
||||
assert_eq!(parse_snap_spacing("0"), None);
|
||||
@@ -232,5 +288,9 @@ mod tests {
|
||||
assert_eq!(parse_snap_spacing("abc"), None);
|
||||
assert_eq!(parse_snap_spacing(""), None);
|
||||
assert_eq!(format_snap_spacing(10.0), "10");
|
||||
assert_eq!(parse_grid_major("5"), Some(5));
|
||||
assert_eq!(parse_grid_major("1"), None);
|
||||
assert_eq!(parse_grid_major("101"), None);
|
||||
assert_eq!(parse_grid_major("abc"), None);
|
||||
}
|
||||
}
|
||||
@@ -545,6 +545,8 @@ pub(super) struct OpenCADStudio {
|
||||
/// GRIDUNIT X/Y display spacing backing the DSettings grid-resize inputs.
|
||||
pub grid_spacing_x: f32,
|
||||
pub grid_spacing_y: f32,
|
||||
/// GRIDMAJOR: every Nth line draws as a brighter major line.
|
||||
pub grid_major_every: u32,
|
||||
/// Adaptive grid: scale GRIDUNIT up by 5x steps to stay readable.
|
||||
pub grid_adaptive: bool,
|
||||
/// Display the grid beyond LIMITS (infinite) instead of clipping to them.
|
||||
@@ -2767,6 +2769,11 @@ pub enum Message {
|
||||
DraftingSettingsToggleSnap,
|
||||
DraftingSettingsSnapXChanged(String),
|
||||
DraftingSettingsSnapYChanged(String),
|
||||
DraftingSettingsGridXChanged(String),
|
||||
DraftingSettingsGridYChanged(String),
|
||||
DraftingSettingsGridMajorChanged(String),
|
||||
DraftingSettingsToggleAdaptiveGrid,
|
||||
DraftingSettingsToggleBeyondLimits,
|
||||
DraftingSettingsToggleEqualSnap,
|
||||
DraftingSettingsToggleIsometric,
|
||||
DraftingSettingsSetIsoPlane(crate::app::settings::IsoPlane),
|
||||
@@ -3815,6 +3822,7 @@ impl OpenCADStudio {
|
||||
show_grid: false,
|
||||
grid_spacing_x: 10.0,
|
||||
grid_spacing_y: 10.0,
|
||||
grid_major_every: 5,
|
||||
grid_adaptive: true,
|
||||
grid_beyond_limits: true,
|
||||
dyn_input: true,
|
||||
|
||||
@@ -464,6 +464,9 @@ pub struct UserSettings {
|
||||
pub grid_spacing_x: f32,
|
||||
#[serde(default = "default_snap_spacing")]
|
||||
pub grid_spacing_y: f32,
|
||||
/// GRIDMAJOR: every Nth grid line is a brighter major line.
|
||||
#[serde(default = "default_grid_major")]
|
||||
pub grid_major_every: u32,
|
||||
/// Adaptive grid scaling (default on).
|
||||
#[serde(default = "default_true")]
|
||||
pub grid_adaptive: bool,
|
||||
@@ -496,10 +499,19 @@ fn default_snap_spacing() -> f32 {
|
||||
10.0
|
||||
}
|
||||
|
||||
fn default_grid_major() -> u32 {
|
||||
5
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Clamp a major-line interval to the range the dialog accepts.
|
||||
pub fn sanitize_grid_major(v: u32) -> u32 {
|
||||
v.clamp(2, 100)
|
||||
}
|
||||
|
||||
/// Clamp a snap spacing to the positive range the dialog accepts.
|
||||
pub fn sanitize_snap_spacing(v: f32) -> f32 {
|
||||
if v.is_finite() && v > 0.0 && v <= 1e9 {
|
||||
@@ -616,6 +628,7 @@ impl Default for UserSettings {
|
||||
snap_spacing_y: 10.0,
|
||||
grid_spacing_x: 10.0,
|
||||
grid_spacing_y: 10.0,
|
||||
grid_major_every: 5,
|
||||
grid_adaptive: true,
|
||||
grid_beyond_limits: true,
|
||||
block_mru: Vec::new(),
|
||||
|
||||
@@ -533,6 +533,7 @@ impl OpenCADStudio {
|
||||
snap_spacing_y: self.snapper.snap_spacing_y,
|
||||
grid_spacing_x: self.grid_spacing_x,
|
||||
grid_spacing_y: self.grid_spacing_y,
|
||||
grid_major_every: self.grid_major_every,
|
||||
grid_adaptive: self.grid_adaptive,
|
||||
grid_beyond_limits: self.grid_beyond_limits,
|
||||
block_mru: self.block_mru.clone(),
|
||||
@@ -641,6 +642,7 @@ impl OpenCADStudio {
|
||||
crate::app::settings::sanitize_snap_spacing(s.snap_spacing_y);
|
||||
self.grid_spacing_x = crate::app::settings::sanitize_snap_spacing(s.grid_spacing_x);
|
||||
self.grid_spacing_y = crate::app::settings::sanitize_snap_spacing(s.grid_spacing_y);
|
||||
self.grid_major_every = crate::app::settings::sanitize_grid_major(s.grid_major_every);
|
||||
self.grid_adaptive = s.grid_adaptive;
|
||||
self.grid_beyond_limits = s.grid_beyond_limits;
|
||||
// Block usage: clone but cap to sane sizes (MRU 20, freq map 200)
|
||||
|
||||
@@ -4596,6 +4596,36 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsGridXChanged(value) => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.grid_x_input = value;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsGridYChanged(value) => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.grid_y_input = value;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsGridMajorChanged(value) => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.grid_major_input = value;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsToggleAdaptiveGrid => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.grid_adaptive = !state.grid_adaptive;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsToggleBeyondLimits => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.grid_beyond_limits = !state.grid_beyond_limits;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsToggleEqualSnap => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.snap_equal = !state.snap_equal;
|
||||
|
||||
@@ -465,6 +465,7 @@ impl OpenCADStudio {
|
||||
bounds,
|
||||
step_x,
|
||||
step_y,
|
||||
major_every: self.grid_major_every,
|
||||
origin,
|
||||
axes,
|
||||
limits,
|
||||
|
||||
+88
-27
@@ -495,6 +495,8 @@ pub struct GridParams {
|
||||
pub step_x: f32,
|
||||
/// World-space Y spacing (GRIDUNIT Y, after adaptive scaling).
|
||||
pub step_y: f32,
|
||||
/// Draw every Nth line as a brighter major line. < 2 disables majors.
|
||||
pub major_every: u32,
|
||||
/// Grid origin in absolute world f64 and the active UCS axis directions.
|
||||
/// The grid always lies on the active UCS XY plane. Plain WCS passes
|
||||
/// `(ZERO, X, Y, Z)`.
|
||||
@@ -513,6 +515,8 @@ pub struct GridParams {
|
||||
#[doc(hidden)]
|
||||
pub struct GridGeometry {
|
||||
pub segments: Vec<(Point, Point)>,
|
||||
/// Brighter every-Nth lines (major grid). Stroked separately in `draw_grid`.
|
||||
pub major_segments: Vec<(Point, Point)>,
|
||||
pub axis_extent: f32,
|
||||
}
|
||||
|
||||
@@ -523,7 +527,7 @@ impl GridGeometry {
|
||||
/// the `None` path.
|
||||
#[doc(hidden)]
|
||||
pub fn empty() -> Self {
|
||||
Self { segments: Vec::new(), axis_extent: 0.0 }
|
||||
Self { segments: Vec::new(), major_segments: Vec::new(), axis_extent: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,6 +789,7 @@ impl canvas::Program<Message> for GridCanvas {
|
||||
gb,
|
||||
g.step_x,
|
||||
g.step_y,
|
||||
g.major_every,
|
||||
g.origin,
|
||||
g.axes,
|
||||
g.limits,
|
||||
@@ -2078,36 +2083,33 @@ fn draw_grid(
|
||||
bounds: iced::Rectangle,
|
||||
step_x: f32,
|
||||
step_y: f32,
|
||||
major_every: u32,
|
||||
grid_origin: glam::DVec3,
|
||||
grid_axes: (Vec3, Vec3, Vec3),
|
||||
limits: Option<(glam::DVec2, glam::DVec2)>,
|
||||
style: GridStyle,
|
||||
) {
|
||||
let alpha = (style.opacity as f32 / 100.0).clamp(0.02, 1.0);
|
||||
let gc = if style.bg_luminance > 0.5 {
|
||||
// Light background: subtle dark grid lines
|
||||
Color {
|
||||
r: 0.10,
|
||||
g: 0.10,
|
||||
b: 0.10,
|
||||
a: alpha,
|
||||
}
|
||||
// Major lines stay subtle: ~1.5x the minor alpha with a slightly heavier
|
||||
// stroke, matching the reference hierarchy without glaring.
|
||||
let major_alpha = (alpha * 1.5).clamp(0.03, 1.0);
|
||||
let (r, g, b) = if style.bg_luminance > 0.5 {
|
||||
(0.10, 0.10, 0.10)
|
||||
} else {
|
||||
// Dark background: subtle light grid lines
|
||||
Color {
|
||||
r: 0.80,
|
||||
g: 0.80,
|
||||
b: 0.80,
|
||||
a: alpha,
|
||||
}
|
||||
(0.80, 0.80, 0.80)
|
||||
};
|
||||
let st = canvas::Stroke {
|
||||
let minor_stroke = canvas::Stroke {
|
||||
width: 0.5,
|
||||
style: canvas::Style::Solid(gc),
|
||||
style: canvas::Style::Solid(Color { r, g, b, a: alpha }),
|
||||
..Default::default()
|
||||
};
|
||||
let major_stroke = canvas::Stroke {
|
||||
width: 0.75,
|
||||
style: canvas::Style::Solid(Color { r, g, b, a: major_alpha }),
|
||||
..Default::default()
|
||||
};
|
||||
let geometry =
|
||||
grid_segments(view_rot, eye, bounds, step_x, step_y, grid_origin, grid_axes, limits);
|
||||
grid_segments(view_rot, eye, bounds, step_x, step_y, major_every, grid_origin, grid_axes, limits);
|
||||
if !geometry.segments.is_empty() {
|
||||
let path = canvas::Path::new(|builder| {
|
||||
for (p0, p1) in &geometry.segments {
|
||||
@@ -2115,7 +2117,16 @@ fn draw_grid(
|
||||
builder.line_to(*p1);
|
||||
}
|
||||
});
|
||||
frame.stroke(&path, st);
|
||||
frame.stroke(&path, minor_stroke);
|
||||
}
|
||||
if !geometry.major_segments.is_empty() {
|
||||
let path = canvas::Path::new(|builder| {
|
||||
for (p0, p1) in &geometry.major_segments {
|
||||
builder.move_to(*p0);
|
||||
builder.line_to(*p1);
|
||||
}
|
||||
});
|
||||
frame.stroke(&path, major_stroke);
|
||||
}
|
||||
if geometry.axis_extent > 0.0 {
|
||||
let (gx, gy, gz) = grid_axes;
|
||||
@@ -2139,6 +2150,7 @@ pub fn grid_segments(
|
||||
bounds: iced::Rectangle,
|
||||
step_x: f32,
|
||||
step_y: f32,
|
||||
major_every: u32,
|
||||
grid_origin: glam::DVec3,
|
||||
grid_axes: (Vec3, Vec3, Vec3),
|
||||
limits: Option<(glam::DVec2, glam::DVec2)>,
|
||||
@@ -2283,6 +2295,9 @@ pub fn grid_segments(
|
||||
return GridGeometry::empty();
|
||||
}
|
||||
let (sx, sy) = (step_x, step_y);
|
||||
let is_major = |index: i32| {
|
||||
major_every >= 2 && (index.rem_euclid(major_every as i32) == 0)
|
||||
};
|
||||
|
||||
// Trace a family-specific visible region around the viewport perimeter.
|
||||
// When a boundary ray points through the horizon, binary-search back toward
|
||||
@@ -2581,13 +2596,15 @@ pub fn grid_segments(
|
||||
|
||||
let (min1, max1) = coordinate_range(axis1);
|
||||
let (min2, max2) = coordinate_range(axis2);
|
||||
let mut minor = Vec::new();
|
||||
let mut major = 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, sx);
|
||||
for index in start..=end {
|
||||
if let Some(segment) = clip_world_line(0, index as f32 * sx) {
|
||||
all_segments.push(segment);
|
||||
if is_major(index) { major.push(segment); } else { minor.push(segment); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2598,11 +2615,13 @@ pub fn grid_segments(
|
||||
let (start, end) = line_range(min2, max2, anchor, sy);
|
||||
for index in start..=end {
|
||||
if let Some(segment) = clip_world_line(1, index as f32 * sy) {
|
||||
all_segments.push(segment);
|
||||
if is_major(index) { major.push(segment); } else { minor.push(segment); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
all_segments.extend(minor);
|
||||
let all_major: Vec<(Point, Point)> = major;
|
||||
|
||||
// LIMITS bounds the grid, not the UCS axes. Size the axes from the
|
||||
// visible grid plane so X/Y/Z still span the viewport even when the
|
||||
@@ -2616,9 +2635,10 @@ pub fn grid_segments(
|
||||
if limits_extent > 0.0 {
|
||||
axis_extent = limits_extent;
|
||||
}
|
||||
return GridGeometry { segments: all_segments, axis_extent };
|
||||
return GridGeometry { segments: all_segments, major_segments: all_major, axis_extent };
|
||||
}
|
||||
|
||||
let mut all_major: Vec<(Point, Point)> = Vec::new();
|
||||
// Lines parallel to axis2 (varying axis1 position).
|
||||
if let Some((anchor_screen, anchor_world, gap)) = best_anchor(0) {
|
||||
if gap >= MIN_HORIZON_GRID_PX {
|
||||
@@ -2631,7 +2651,8 @@ pub fn grid_segments(
|
||||
for i in start..=end {
|
||||
let value = i as f32 * sx;
|
||||
if let Some((p0, p1)) = project_line(0, value) {
|
||||
all_segments.extend(trim_line(0, p0, p1));
|
||||
let segs = trim_line(0, p0, p1);
|
||||
if is_major(i) { all_major.extend(segs); } else { all_segments.extend(segs); }
|
||||
}
|
||||
}
|
||||
axis_extent =
|
||||
@@ -2652,7 +2673,8 @@ pub fn grid_segments(
|
||||
for i in start..=end {
|
||||
let value = i as f32 * sy;
|
||||
if let Some((p0, p1)) = project_line(1, value) {
|
||||
all_segments.extend(trim_line(1, p0, p1));
|
||||
let segs = trim_line(1, p0, p1);
|
||||
if is_major(i) { all_major.extend(segs); } else { all_segments.extend(segs); }
|
||||
}
|
||||
}
|
||||
axis_extent =
|
||||
@@ -2662,7 +2684,7 @@ pub fn grid_segments(
|
||||
}
|
||||
|
||||
let _ = gz; // gz unused after move; retained for symmetry with `draw_axes` call sites.
|
||||
GridGeometry { segments: all_segments, axis_extent }
|
||||
GridGeometry { segments: all_segments, major_segments: all_major, axis_extent }
|
||||
}
|
||||
|
||||
// ── Coloured UCS axes ──────────────────────────────────────────────────────
|
||||
@@ -3740,6 +3762,7 @@ mod grid_key_tests {
|
||||
},
|
||||
step_x: 80.0,
|
||||
step_y: 80.0,
|
||||
major_every: 5,
|
||||
origin: glam::DVec3::new(0.0, 0.0, 0.0),
|
||||
axes: (Vec3::X, Vec3::Y, Vec3::Z),
|
||||
limits: None,
|
||||
@@ -3807,6 +3830,15 @@ mod grid_key_tests {
|
||||
"step_y change must invalidate"
|
||||
);
|
||||
|
||||
// major_every: major-line frequency change
|
||||
let mut p = baseline_params();
|
||||
p.major_every = 10;
|
||||
assert_ne!(
|
||||
GridKey::from_grids(&[p], baseline_bounds, GridStyle::default()),
|
||||
baseline_key,
|
||||
"major_every change must invalidate"
|
||||
);
|
||||
|
||||
// origin: translate the UCS origin off-zero
|
||||
let mut p = baseline_params();
|
||||
p.origin = glam::DVec3::new(100.0, 0.0, 0.0);
|
||||
@@ -3962,11 +3994,39 @@ mod grid_key_tests {
|
||||
let eye = glam::DVec3::new(0.0, 0.0, 500.0);
|
||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 800.0, height: 600.0 };
|
||||
let g = grid_segments(
|
||||
view_rot, eye, bounds, 10.0, 2.0,
|
||||
view_rot, eye, bounds, 10.0, 2.0, 5,
|
||||
glam::DVec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||
);
|
||||
assert!(!g.segments.is_empty(), "grid lines expected");
|
||||
}
|
||||
|
||||
/// Every-Nth line lands in `major_segments`, the rest in `segments`.
|
||||
#[test]
|
||||
fn grid_segments_partition_major_lines() {
|
||||
// Top-down orthographic-ish view over the origin: deterministic lines.
|
||||
let view_rot = glam::camera::rh::proj::directx::orthographic(
|
||||
-400.0, 400.0, -300.0, 300.0, 0.1, 2000.0,
|
||||
) * glam::camera::rh::view::look_at_mat4(
|
||||
Vec3::new(0.0, 0.0, 500.0),
|
||||
Vec3::ZERO,
|
||||
Vec3::Y,
|
||||
);
|
||||
let eye = glam::DVec3::new(0.0, 0.0, 500.0);
|
||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: 800.0, height: 600.0 };
|
||||
let g = grid_segments(
|
||||
view_rot, eye, bounds, 10.0, 10.0, 5,
|
||||
glam::DVec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||
);
|
||||
assert!(!g.segments.is_empty(), "minor lines expected");
|
||||
assert!(!g.major_segments.is_empty(), "major lines expected");
|
||||
// major_every = 1 disables the split: everything is minor.
|
||||
let flat = grid_segments(
|
||||
view_rot, eye, bounds, 10.0, 10.0, 1,
|
||||
glam::DVec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||
);
|
||||
assert!(flat.major_segments.is_empty());
|
||||
assert!(!flat.segments.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -4001,6 +4061,7 @@ mod grid_canvas_state_tests {
|
||||
bounds,
|
||||
step_x: 80.0,
|
||||
step_y: 80.0,
|
||||
major_every: 5,
|
||||
origin: grid_origin,
|
||||
axes: grid_axes,
|
||||
limits,
|
||||
|
||||
@@ -31,6 +31,11 @@ pub struct DraftingSettingsState {
|
||||
pub snap_x_input: String,
|
||||
pub snap_y_input: String,
|
||||
pub snap_equal: bool,
|
||||
pub grid_x_input: String,
|
||||
pub grid_y_input: String,
|
||||
pub grid_major_input: String,
|
||||
pub grid_adaptive: bool,
|
||||
pub grid_beyond_limits: bool,
|
||||
pub isometric: bool,
|
||||
pub iso_plane: IsoPlane,
|
||||
pub snap_angle_deg: f32,
|
||||
@@ -62,6 +67,11 @@ impl DraftingSettingsState {
|
||||
|| self.snap_x_input != saved.snap_x_input
|
||||
|| self.snap_y_input != saved.snap_y_input
|
||||
|| self.snap_equal != saved.snap_equal
|
||||
|| self.grid_x_input != saved.grid_x_input
|
||||
|| self.grid_y_input != saved.grid_y_input
|
||||
|| self.grid_major_input != saved.grid_major_input
|
||||
|| self.grid_adaptive != saved.grid_adaptive
|
||||
|| self.grid_beyond_limits != saved.grid_beyond_limits
|
||||
|| self.isometric != saved.isometric
|
||||
|| self.iso_plane != saved.iso_plane
|
||||
|| (self.snap_angle_deg - saved.snap_angle_deg).abs() > 0.001
|
||||
@@ -98,6 +108,12 @@ pub fn parse_snap_spacing(s: &str) -> Option<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a major-line interval. Must be an integer in 2..=100.
|
||||
pub fn parse_grid_major(s: &str) -> Option<u32> {
|
||||
let v: u32 = s.trim().parse().ok()?;
|
||||
(2..=100).contains(&v).then_some(v)
|
||||
}
|
||||
|
||||
/// Helper for grouped sub-panels with a light border and header title.
|
||||
fn group<'a>(title: impl Into<String>, body: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
container(
|
||||
@@ -298,19 +314,31 @@ pub fn view_window<'a>(
|
||||
column![
|
||||
row![
|
||||
text(crate::t!("Grid X spacing:")).size(11).width(120),
|
||||
text("10.00").size(11),
|
||||
text_input("10", &state.grid_x_input)
|
||||
.on_input(Message::DraftingSettingsGridXChanged)
|
||||
.size(11)
|
||||
.padding([4, 7])
|
||||
.width(100),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![
|
||||
text(crate::t!("Grid Y spacing:")).size(11).width(120),
|
||||
text("10.00").size(11),
|
||||
text_input("10", &state.grid_y_input)
|
||||
.on_input(Message::DraftingSettingsGridYChanged)
|
||||
.size(11)
|
||||
.padding([4, 7])
|
||||
.width(100),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
row![
|
||||
text(crate::t!("Major line every:")).size(11).width(120),
|
||||
text("5").size(11),
|
||||
text_input("5", &state.grid_major_input)
|
||||
.on_input(Message::DraftingSettingsGridMajorChanged)
|
||||
.size(11)
|
||||
.padding([4, 7])
|
||||
.width(100),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
@@ -322,13 +350,17 @@ pub fn view_window<'a>(
|
||||
crate::t!("Grid behavior"),
|
||||
column![
|
||||
row![
|
||||
checkbox(true).size(14),
|
||||
checkbox(state.grid_adaptive)
|
||||
.on_toggle(|_| Message::DraftingSettingsToggleAdaptiveGrid)
|
||||
.size(14),
|
||||
text(crate::t!("Adaptive grid")).size(11),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
row![
|
||||
checkbox(true).size(14),
|
||||
checkbox(state.grid_beyond_limits)
|
||||
.on_toggle(|_| Message::DraftingSettingsToggleBeyondLimits)
|
||||
.size(14),
|
||||
text(crate::t!("Display grid beyond Limits")).size(11),
|
||||
]
|
||||
.spacing(6)
|
||||
|
||||
Reference in new issue
Block a user