From 5a547a96d3b7018c194fa898cbc3ad5076bbb65b Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 19 Oct 2025 16:33:26 -0400 Subject: [PATCH 1/6] Add rectangular major grid lines (broken impl) --- editor/src/consts.rs | 1 + .../document/overlays/grid_overlays.rs | 231 ++++++++++++++---- .../portfolio/document/utility_types/misc.rs | 32 ++- .../snapping/grid_snapper.rs | 2 +- frontend/src/utility-functions/icons.ts | 2 + 5 files changed, 218 insertions(+), 50 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index b17a8621cc..396e29a724 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -146,6 +146,7 @@ pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b"; pub const COLOR_OVERLAY_GREEN: &str = "#63ce63"; pub const COLOR_OVERLAY_RED: &str = "#ef5454"; pub const COLOR_OVERLAY_GRAY: &str = "#cccccc"; +pub const COLOR_OVERLAY_GRAY_DARK: &str = "#555555"; pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40"; pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 9155e7bce2..55cf2197d2 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -10,6 +10,7 @@ use graphene_std::vector::style::FillChoice; fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb(); + let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { return; }; @@ -36,7 +37,28 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: } else { DVec2::new(secondary_pos, primary_end) }; - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color), None); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + is_major_line( + line_index, + if primary == 0 { + document.snapping_state.grid.rectangular_major_interval_along_x + } else { + document.snapping_state.grid.rectangular_major_interval_along_y + }, + ) + .then_some(&if document.snapping_state.grid.major_is_thick { &grid_color } else { &grid_color_minor }), + is_major_line( + line_index, + if primary == 0 { + document.snapping_state.grid.rectangular_major_interval_along_x + } else { + document.snapping_state.grid.rectangular_major_interval_along_y + }, + ) + .then_some(if document.snapping_state.grid.major_is_thick { 3. } else { 1. }), + ); } } } @@ -49,6 +71,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb(); + let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { return; }; @@ -174,9 +197,17 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context } } +fn is_major_line(line_index: i32, major_interval: u32) -> bool { + line_index % major_interval as i32 == 0 +} + +fn line_is_thick(line_index: i32, major_interval: u32, major_is_thick: bool) -> bool { + major_is_thick && is_major_line(line_index, major_interval) +} + pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match document.snapping_state.grid.grid_type { - GridType::Rectangular { spacing } => { + GridType::Rectangular { spacing, .. } => { if document.snapping_state.grid.dot_display { grid_overlay_rectangular_dot(document, overlay_context, spacing) } else { @@ -205,10 +236,8 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { } let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| { update_val::(grid, move |grid, val| { - if let Some(val) = val.value { - if let Some(update) = update(grid) { - *update = val; - } + if let (Some(val), Some(update)) = (val.value, update(grid)) { + *update = val; } }) }; @@ -219,7 +248,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { } }) }; - let update_display = |grid, update: fn(&mut GridSnapping) -> Option<&mut bool>| { + let _update_display = |grid, update: fn(&mut GridSnapping) -> Option<&mut bool>| { update_val::(grid, move |grid, checkbox| { if let Some(update) = update(grid) { *update = checkbox.checked; @@ -230,7 +259,54 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { widgets.push(LayoutGroup::Row { widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()], }); + let mut color_widgets = vec![TextLabel::new("Color").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; + color_widgets.push( + ColorInput::new(FillChoice::Solid(grid.grid_color.to_gamma_srgb())) + .tooltip("Grid display color") + .allow_none(false) + .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) + .widget_holder(), + ); + if grid.has_minor_lines() { + color_widgets.push(Separator::new(SeparatorType::Related).widget_holder()); + color_widgets.push( + ColorInput::new(FillChoice::Solid(grid.grid_color_minor.to_gamma_srgb())) + .tooltip("Minor grid line display color") + .allow_none(false) + .on_update(update_color(grid, |grid| Some(&mut grid.grid_color_minor))) + .widget_holder(), + ); + } + widgets.push(LayoutGroup::Row { widgets: color_widgets }); + widgets.push(LayoutGroup::Row { + widgets: vec![ + TextLabel::new("Display").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + RadioInput::new(vec![ + RadioEntryData::new("small").icon("Dot").on_update(update_val(grid, |grid, _| { + grid.major_is_thick = false; + })), + RadioEntryData::new("large").icon("DotLarge").on_update(update_val(grid, |grid, _| { + grid.major_is_thick = true; + })), + ]) + .selected_index(Some(if grid.major_is_thick { 1 } else { 0 })) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + RadioInput::new(vec![ + RadioEntryData::new("lines").label("Lines").icon("Grid").on_update(update_val(grid, |grid, _| { + grid.dot_display = false; + })), + RadioEntryData::new("dots").label("Dots").icon("GridDotted").on_update(update_val(grid, |grid, _| { + grid.dot_display = true; + })), + ]) + // .min_width(200) + .selected_index(Some(if grid.dot_display { 1 } else { 0 })) + .widget_holder(), + ], + }); widgets.push(LayoutGroup::Row { widgets: vec![ TextLabel::new("Type").table_align(true).widget_holder(), @@ -245,7 +321,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { grid.grid_type = GridType::Rectangular { spacing: grid.rectangular_spacing }; })), RadioEntryData::new("isometric").label("Isometric").on_update(update_val(grid, |grid, _| { - if let GridType::Rectangular { spacing } = grid.grid_type { + if let GridType::Rectangular { spacing, .. } = grid.grid_type { grid.rectangular_spacing = spacing; } grid.grid_type = GridType::Isometric { @@ -264,24 +340,6 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { ], }); - let mut color_widgets = vec![TextLabel::new("Display").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()]; - color_widgets.extend([ - CheckboxInput::new(grid.dot_display) - .icon("GridDotted") - .tooltip("Display as dotted grid") - .on_update(update_display(grid, |grid| Some(&mut grid.dot_display))) - .widget_holder(), - Separator::new(SeparatorType::Related).widget_holder(), - ]); - color_widgets.push( - ColorInput::new(FillChoice::Solid(grid.grid_color.to_gamma_srgb())) - .tooltip("Grid display color") - .allow_none(false) - .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) - .widget_holder(), - ); - widgets.push(LayoutGroup::Row { widgets: color_widgets }); - widgets.push(LayoutGroup::Row { widgets: vec![ TextLabel::new("Origin").table_align(true).widget_holder(), @@ -303,27 +361,58 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { }); match grid.grid_type { - GridType::Rectangular { spacing } => widgets.push(LayoutGroup::Row { - widgets: vec![ - TextLabel::new("Spacing").table_align(true).widget_holder(), - Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(spacing.x)) - .label("X") - .unit(" px") - .min(0.) - .min_width(98) - .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x))) - .widget_holder(), - Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(spacing.y)) - .label("Y") - .unit(" px") - .min(0.) - .min_width(98) - .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y))) - .widget_holder(), - ], - }), + GridType::Rectangular { spacing, .. } => { + widgets.push(LayoutGroup::Row { + widgets: vec![ + TextLabel::new("Spacing").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(spacing.x)) + .label("X") + .unit(" px") + .min(0.) + .min_width(98) + .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x))) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(spacing.y)) + .label("Y") + .unit(" px") + .min(0.) + .min_width(98) + .on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y))) + .widget_holder(), + ], + }); + widgets.push(LayoutGroup::Row { + widgets: vec![ + TextLabel::new("Mark Every").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(grid.rectangular_major_interval_along_x as f64)) + .unit(" col") + .int() + .min(1.) + .min_width(98) + .on_update(update_val(grid, |grid, val: &NumberInput| { + if let Some(val) = val.value { + grid.rectangular_major_interval_along_x = val as u32; + } + })) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(grid.rectangular_major_interval_along_y as f64)) + .unit(" row") + .int() + .min(1.) + .min_width(98) + .on_update(update_val(grid, |grid, val: &NumberInput| { + if let Some(val) = val.value { + grid.rectangular_major_interval_along_y = val as u32; + } + })) + .widget_holder(), + ], + }); + } GridType::Isometric { y_axis_spacing, angle_a, angle_b } => { widgets.push(LayoutGroup::Row { widgets: vec![ @@ -342,18 +431,66 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { TextLabel::new("Angles").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), NumberInput::new(Some(angle_a)) + .label("A") .unit("°") .min_width(98) .on_update(update_origin(grid, |grid| grid.grid_type.angle_a())) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), NumberInput::new(Some(angle_b)) + .label("B") .unit("°") .min_width(98) .on_update(update_origin(grid, |grid| grid.grid_type.angle_b())) .widget_holder(), ], }); + widgets.push(LayoutGroup::Row { + widgets: vec![ + TextLabel::new("Mark Every").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(grid.isometric_major_interval_along_a as f64)) + .label("A") + .int() + .min(1.) + .min_width(98) + .on_update(update_val(grid, |grid, val: &NumberInput| { + if let Some(val) = val.value { + grid.isometric_major_interval_along_a = val as u32; + } + })) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(grid.isometric_major_interval_along_b as f64)) + .label("B") + .int() + .min(1.) + .min_width(98) + .on_update(update_val(grid, |grid, val: &NumberInput| { + if let Some(val) = val.value { + grid.isometric_major_interval_along_b = val as u32; + } + })) + .widget_holder(), + ], + }); + widgets.push(LayoutGroup::Row { + widgets: vec![ + TextLabel::new("").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + NumberInput::new(Some(grid.isometric_major_interval_along_x as f64)) + .label("X") + .int() + .min(1.) + .min_width(200) + .on_update(update_val(grid, |grid, val: &NumberInput| { + if let Some(val) = val.value { + grid.isometric_major_interval_along_x = val as u32; + } + })) + .widget_holder(), + ], + }); } } diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index ae8fd73532..f0c9efb4b4 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,4 +1,4 @@ -use crate::consts::COLOR_OVERLAY_GRAY; +use crate::consts::COLOR_OVERLAY_GRAY_DARK; use glam::DVec2; use graphene_std::raster::Color; use std::fmt; @@ -213,10 +213,20 @@ pub struct GridSnapping { pub origin: DVec2, pub grid_type: GridType, pub rectangular_spacing: DVec2, + pub rectangular_major_interval_along_x: u32, + pub rectangular_major_interval_along_y: u32, pub isometric_y_spacing: f64, pub isometric_angle_a: f64, pub isometric_angle_b: f64, + /// Interval between major y-axis lines + pub isometric_major_interval_along_x: u32, + /// Interval between major angle a lines + pub isometric_major_interval_along_b: u32, + /// Interval between major angle b lines + pub isometric_major_interval_along_a: u32, pub grid_color: Color, + pub grid_color_minor: Color, + pub major_is_thick: bool, pub dot_display: bool, } @@ -226,10 +236,17 @@ impl Default for GridSnapping { origin: DVec2::ZERO, grid_type: Default::default(), rectangular_spacing: DVec2::ONE, + rectangular_major_interval_along_x: 1, + rectangular_major_interval_along_y: 1, isometric_y_spacing: 1., isometric_angle_a: 30., isometric_angle_b: 30., - grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY.strip_prefix('#').unwrap()).unwrap(), + isometric_major_interval_along_x: 1, + isometric_major_interval_along_b: 1, + isometric_major_interval_along_a: 1, + grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY_DARK.strip_prefix('#').unwrap()).unwrap().with_alpha(0.4), + grid_color_minor: Color::from_rgb_str(COLOR_OVERLAY_GRAY_DARK.strip_prefix('#').unwrap()).unwrap().with_alpha(0.2), + major_is_thick: true, dot_display: false, } } @@ -264,6 +281,17 @@ impl GridSnapping { } Some(multiplier) } + + pub fn has_minor_lines(&self) -> bool { + match self.grid_type { + GridType::Rectangular { .. } => self.rectangular_major_interval_along_x > 1 || self.rectangular_major_interval_along_y > 1, + GridType::Isometric { .. } => { + self.isometric_major_interval_along_x > 1 + || self.isometric_major_interval_along_a > 1 + || self.isometric_major_interval_along_b > 1 + } + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs index 1c339d4354..02d790f4f8 100644 --- a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs @@ -90,7 +90,7 @@ impl GridSnapper { fn get_snap_lines(&self, document_point: DVec2, snap_data: &mut SnapData) -> Vec { match snap_data.document.snapping_state.grid.grid_type { - GridType::Rectangular { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing), + GridType::Rectangular { spacing, .. } => self.get_snap_lines_rectangular(document_point, snap_data, spacing), GridType::Isometric { y_axis_spacing, angle_a, angle_b } => self.get_snap_lines_isometric(document_point, snap_data, y_axis_spacing, angle_a, angle_b), } } diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index bdbde00238..ca862de266 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -11,6 +11,7 @@ import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg"; import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg"; import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg"; import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg"; +import DotLarge from "@graphite-frontend/assets/icon-12px-solid/dot-large.svg"; import Dot from "@graphite-frontend/assets/icon-12px-solid/dot.svg"; import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg"; import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg"; @@ -58,6 +59,7 @@ const SOLID_12PX = { Clipped: { svg: Clipped, size: 12 }, CloseX: { svg: CloseX, size: 12 }, Delay: { svg: Delay, size: 12 }, + DotLarge: { svg: DotLarge, size: 12 }, Dot: { svg: Dot, size: 12 }, DropdownArrow: { svg: DropdownArrow, size: 12 }, Edit12px: { svg: Edit12px, size: 12 }, From 94c26cc8b3715b6822193535f8c44177cb3719d6 Mon Sep 17 00:00:00 2001 From: blue linden Date: Mon, 20 Oct 2025 09:06:47 -0400 Subject: [PATCH 2/6] add large dot icon and remove unnecessary ref --- .../src/messages/portfolio/document/overlays/grid_overlays.rs | 2 +- frontend/assets/icon-12px-solid/dot-large.svg | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 frontend/assets/icon-12px-solid/dot-large.svg diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 55cf2197d2..8c8e161e76 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -48,7 +48,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: document.snapping_state.grid.rectangular_major_interval_along_y }, ) - .then_some(&if document.snapping_state.grid.major_is_thick { &grid_color } else { &grid_color_minor }), + .then_some(if document.snapping_state.grid.major_is_thick { &grid_color } else { &grid_color_minor }), is_major_line( line_index, if primary == 0 { diff --git a/frontend/assets/icon-12px-solid/dot-large.svg b/frontend/assets/icon-12px-solid/dot-large.svg new file mode 100644 index 0000000000..054d0838ee --- /dev/null +++ b/frontend/assets/icon-12px-solid/dot-large.svg @@ -0,0 +1,3 @@ + + + From 1b0ef1ba25d7b6920bf0ad1eacea1bb5d6c78d21 Mon Sep 17 00:00:00 2001 From: blue linden Date: Fri, 31 Oct 2025 11:15:47 -0400 Subject: [PATCH 3/6] Work on rect grid lines --- .../document/overlays/grid_overlays.rs | 37 ++++++++++--------- .../portfolio/document/utility_types/misc.rs | 10 ++--- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 8c8e161e76..010bf7f090 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -25,7 +25,16 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: let primary_start = bounds.0.iter().map(|&corner| corner[primary]).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); let primary_end = bounds.0.iter().map(|&corner| corner[primary]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); let spacing = spacing[secondary]; + let first_index = ((min - origin[secondary]) / spacing).ceil() as i32; for line_index in 0..=((max - min) / spacing).ceil() as i32 { + let is_major = is_major_line( + line_index + first_index, + if primary == 1 { + document.snapping_state.grid.rectangular_major_interval_along_x + } else { + document.snapping_state.grid.rectangular_major_interval_along_y + }, + ); let secondary_pos = (((min - origin[secondary]) / spacing).ceil() + line_index as f64) * spacing + origin[secondary]; let start = if primary == 0 { DVec2::new(primary_start, secondary_pos) @@ -40,24 +49,16 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: overlay_context.line( document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), - is_major_line( - line_index, - if primary == 0 { - document.snapping_state.grid.rectangular_major_interval_along_x - } else { - document.snapping_state.grid.rectangular_major_interval_along_y - }, - ) - .then_some(if document.snapping_state.grid.major_is_thick { &grid_color } else { &grid_color_minor }), - is_major_line( - line_index, - if primary == 0 { - document.snapping_state.grid.rectangular_major_interval_along_x - } else { - document.snapping_state.grid.rectangular_major_interval_along_y - }, - ) - .then_some(if document.snapping_state.grid.major_is_thick { 3. } else { 1. }), + if is_major { + Some(&grid_color) + } else { + Some(&grid_color_minor) + }, + if is_major && document.snapping_state.grid.major_is_thick { + Some(3.) + } else { + Some(1.) + }, ); } } diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index f0c9efb4b4..fc5142faed 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -246,7 +246,7 @@ impl Default for GridSnapping { isometric_major_interval_along_a: 1, grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY_DARK.strip_prefix('#').unwrap()).unwrap().with_alpha(0.4), grid_color_minor: Color::from_rgb_str(COLOR_OVERLAY_GRAY_DARK.strip_prefix('#').unwrap()).unwrap().with_alpha(0.2), - major_is_thick: true, + major_is_thick: false, dot_display: false, } } @@ -281,15 +281,11 @@ impl GridSnapping { } Some(multiplier) } - + pub fn has_minor_lines(&self) -> bool { match self.grid_type { GridType::Rectangular { .. } => self.rectangular_major_interval_along_x > 1 || self.rectangular_major_interval_along_y > 1, - GridType::Isometric { .. } => { - self.isometric_major_interval_along_x > 1 - || self.isometric_major_interval_along_a > 1 - || self.isometric_major_interval_along_b > 1 - } + GridType::Isometric { .. } => self.isometric_major_interval_along_x > 1 || self.isometric_major_interval_along_a > 1 || self.isometric_major_interval_along_b > 1, } } } From f10583974f58e2c5125f39df2fa49a02f626e9c6 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 2 Nov 2025 21:48:00 -0800 Subject: [PATCH 4/6] rectilinear grid work --- .../document/overlays/grid_overlays.rs | 41 +++++++------- .../portfolio/document/utility_types/misc.rs | 53 +++++++++---------- .../snapping/grid_snapper.rs | 4 +- 3 files changed, 46 insertions(+), 52 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 010bf7f090..6cb9e7929a 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -2,7 +2,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType}; use crate::messages::prelude::*; -use glam::DVec2; +use glam::{DVec2,UVec2}; use graphene_std::raster::color::Color; use graphene_std::renderer::Quad; use graphene_std::vector::style::FillChoice; @@ -11,9 +11,10 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: let origin = document.snapping_state.grid.origin; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb(); let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); - let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { + let Some(scaled_spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.snapping_state.grid.rectangular_major_interval, &document.document_ptz) else { return; }; + let scale_is_adjusted = scaled_spacing != spacing; let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]); @@ -24,17 +25,17 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: let max = bounds.0.iter().map(|&corner| corner[secondary]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); let primary_start = bounds.0.iter().map(|&corner| corner[primary]).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); let primary_end = bounds.0.iter().map(|&corner| corner[primary]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let spacing = spacing[secondary]; + let spacing = scaled_spacing[secondary]; let first_index = ((min - origin[secondary]) / spacing).ceil() as i32; for line_index in 0..=((max - min) / spacing).ceil() as i32 { let is_major = is_major_line( line_index + first_index, if primary == 1 { - document.snapping_state.grid.rectangular_major_interval_along_x + document.snapping_state.grid.rectangular_major_interval.x } else { - document.snapping_state.grid.rectangular_major_interval_along_y + document.snapping_state.grid.rectangular_major_interval.y }, - ); + ) || scale_is_adjusted; let secondary_pos = (((min - origin[secondary]) / spacing).ceil() + line_index as f64) * spacing + origin[secondary]; let start = if primary == 0 { DVec2::new(primary_start, secondary_pos) @@ -47,7 +48,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: DVec2::new(secondary_pos, primary_end) }; overlay_context.line( - document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), if is_major { Some(&grid_color) @@ -73,7 +74,7 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte let origin = document.snapping_state.grid.origin; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb(); let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); - let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { + let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.snapping_state.grid.rectangular_major_interval, &document.document_ptz) else { return; }; let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); @@ -202,10 +203,6 @@ fn is_major_line(line_index: i32, major_interval: u32) -> bool { line_index % major_interval as i32 == 0 } -fn line_is_thick(line_index: i32, major_interval: u32, major_is_thick: bool) -> bool { - major_is_thick && is_major_line(line_index, major_interval) -} - pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match document.snapping_state.grid.grid_type { GridType::Rectangular { spacing, .. } => { @@ -388,26 +385,26 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { widgets: vec![ TextLabel::new("Mark Every").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(grid.rectangular_major_interval_along_x as f64)) + NumberInput::new(Some(grid.rectangular_major_interval.x as f64)) .unit(" col") .int() .min(1.) .min_width(98) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.rectangular_major_interval_along_x = val as u32; + grid.rectangular_major_interval.x = val as u32; } })) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(grid.rectangular_major_interval_along_y as f64)) + NumberInput::new(Some(grid.rectangular_major_interval.y as f64)) .unit(" row") .int() .min(1.) .min_width(98) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.rectangular_major_interval_along_y = val as u32; + grid.rectangular_major_interval.y = val as u32; } })) .widget_holder(), @@ -450,26 +447,26 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { widgets: vec![ TextLabel::new("Mark Every").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(grid.isometric_major_interval_along_a as f64)) + NumberInput::new(Some(grid.isometric_major_interval.z as f64)) .label("A") .int() .min(1.) .min_width(98) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval_along_a = val as u32; + grid.isometric_major_interval.z = val as u32; } })) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(grid.isometric_major_interval_along_b as f64)) + NumberInput::new(Some(grid.isometric_major_interval.y as f64)) .label("B") .int() .min(1.) .min_width(98) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval_along_b = val as u32; + grid.isometric_major_interval.y = val as u32; } })) .widget_holder(), @@ -479,14 +476,14 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { widgets: vec![ TextLabel::new("").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), - NumberInput::new(Some(grid.isometric_major_interval_along_x as f64)) + NumberInput::new(Some(grid.isometric_major_interval.x as f64)) .label("X") .int() .min(1.) .min_width(200) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval_along_x = val as u32; + grid.isometric_major_interval.x = val as u32; } })) .widget_holder(), diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index fc5142faed..4255861e21 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,5 +1,5 @@ use crate::consts::COLOR_OVERLAY_GRAY_DARK; -use glam::DVec2; +use glam::{DVec2,UVec2, UVec3}; use graphene_std::raster::Color; use std::fmt; @@ -213,17 +213,12 @@ pub struct GridSnapping { pub origin: DVec2, pub grid_type: GridType, pub rectangular_spacing: DVec2, - pub rectangular_major_interval_along_x: u32, - pub rectangular_major_interval_along_y: u32, + pub rectangular_major_interval: UVec2, pub isometric_y_spacing: f64, pub isometric_angle_a: f64, pub isometric_angle_b: f64, - /// Interval between major y-axis lines - pub isometric_major_interval_along_x: u32, - /// Interval between major angle a lines - pub isometric_major_interval_along_b: u32, - /// Interval between major angle b lines - pub isometric_major_interval_along_a: u32, + /// X is the major interval along the X axis, Y is the major interval along the B axis, Z is the major interval along the A axis. + pub isometric_major_interval: UVec3, pub grid_color: Color, pub grid_color_minor: Color, pub major_is_thick: bool, @@ -236,14 +231,11 @@ impl Default for GridSnapping { origin: DVec2::ZERO, grid_type: Default::default(), rectangular_spacing: DVec2::ONE, - rectangular_major_interval_along_x: 1, - rectangular_major_interval_along_y: 1, + rectangular_major_interval: UVec2::ONE, isometric_y_spacing: 1., isometric_angle_a: 30., isometric_angle_b: 30., - isometric_major_interval_along_x: 1, - isometric_major_interval_along_b: 1, - isometric_major_interval_along_a: 1, + isometric_major_interval: UVec3::ONE, grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY_DARK.strip_prefix('#').unwrap()).unwrap().with_alpha(0.4), grid_color_minor: Color::from_rgb_str(COLOR_OVERLAY_GRAY_DARK.strip_prefix('#').unwrap()).unwrap().with_alpha(0.2), major_is_thick: false, @@ -254,18 +246,23 @@ impl Default for GridSnapping { impl GridSnapping { // Double grid size until it takes up at least 10px. - pub fn compute_rectangle_spacing(mut size: DVec2, navigation: &PTZ) -> Option { - let mut iterations = 0; - size = size.abs(); - while (size * navigation.zoom()).cmplt(DVec2::splat(10.)).any() { - if iterations > 100 { - return None; + pub fn compute_rectangle_spacing(mut size: DVec2, major_interval: &UVec2, navigation: &PTZ) -> Option { + let mut iterations = 0; + size = size.abs(); + while (size.x * navigation.zoom() < 10.) || (size.y * navigation.zoom() < 10.) { + if iterations > 100 { + return None; + } + if size.x * navigation.zoom() < 10. { + size.x *= if major_interval.x != 1 {major_interval.x as f64} else {2.}; + } + if size.y * navigation.zoom() < 10. { + size.y *= if major_interval.y != 1 {major_interval.y as f64} else {2.}; + } + iterations += 1; } - size *= 2.; - iterations += 1; + Some(size) } - Some(size) - } // Double grid size until it takes up at least 10px. pub fn compute_isometric_multiplier(length: f64, divisor: f64, navigation: &PTZ) -> Option { @@ -283,11 +280,11 @@ impl GridSnapping { } pub fn has_minor_lines(&self) -> bool { - match self.grid_type { - GridType::Rectangular { .. } => self.rectangular_major_interval_along_x > 1 || self.rectangular_major_interval_along_y > 1, - GridType::Isometric { .. } => self.isometric_major_interval_along_x > 1 || self.isometric_major_interval_along_a > 1 || self.isometric_major_interval_along_b > 1, + match self.grid_type { + GridType::Rectangular { .. } => self.rectangular_major_interval.x > 1 || self.rectangular_major_interval.y > 1, + GridType::Isometric { .. } => self.isometric_major_interval.x > 1 || self.isometric_major_interval.z > 1 || self.isometric_major_interval.y > 1, + } } - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs index 02d790f4f8..38947d11a0 100644 --- a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs @@ -1,6 +1,6 @@ use super::*; use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, GridSnapping, GridType, SnapTarget}; -use glam::DVec2; +use glam::{DVec2,UVec2}; use graphene_std::renderer::Quad; struct Line { @@ -18,7 +18,7 @@ impl GridSnapper { let document = snap_data.document; let mut lines = Vec::new(); - let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { + let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &UVec2::ONE, &document.document_ptz) else { return lines; }; let origin = document.snapping_state.grid.origin; From 489ca9549c806d5fe3033ddec6accef9c860abf1 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 2 Nov 2025 21:58:51 -0800 Subject: [PATCH 5/6] fmt --- .../document/overlays/grid_overlays.rs | 18 +++------ .../portfolio/document/utility_types/misc.rs | 38 +++++++++---------- .../snapping/grid_snapper.rs | 2 +- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 6cb9e7929a..01a2ead266 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -2,7 +2,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType}; use crate::messages::prelude::*; -use glam::{DVec2,UVec2}; +use glam::{DVec2, UVec2}; use graphene_std::raster::color::Color; use graphene_std::renderer::Quad; use graphene_std::vector::style::FillChoice; @@ -48,18 +48,10 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: DVec2::new(secondary_pos, primary_end) }; overlay_context.line( - document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), - if is_major { - Some(&grid_color) - } else { - Some(&grid_color_minor) - }, - if is_major && document.snapping_state.grid.major_is_thick { - Some(3.) - } else { - Some(1.) - }, + if is_major { Some(&grid_color) } else { Some(&grid_color_minor) }, + if is_major && document.snapping_state.grid.major_is_thick { Some(3.) } else { Some(1.) }, ); } } @@ -74,7 +66,7 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte let origin = document.snapping_state.grid.origin; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb(); let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); - let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.snapping_state.grid.rectangular_major_interval, &document.document_ptz) else { + let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.snapping_state.grid.rectangular_major_interval, &document.document_ptz) else { return; }; let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 4255861e21..8a3d159d1e 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,5 +1,5 @@ use crate::consts::COLOR_OVERLAY_GRAY_DARK; -use glam::{DVec2,UVec2, UVec3}; +use glam::{DVec2, UVec2, UVec3}; use graphene_std::raster::Color; use std::fmt; @@ -247,22 +247,22 @@ impl Default for GridSnapping { impl GridSnapping { // Double grid size until it takes up at least 10px. pub fn compute_rectangle_spacing(mut size: DVec2, major_interval: &UVec2, navigation: &PTZ) -> Option { - let mut iterations = 0; - size = size.abs(); - while (size.x * navigation.zoom() < 10.) || (size.y * navigation.zoom() < 10.) { - if iterations > 100 { - return None; - } - if size.x * navigation.zoom() < 10. { - size.x *= if major_interval.x != 1 {major_interval.x as f64} else {2.}; - } - if size.y * navigation.zoom() < 10. { - size.y *= if major_interval.y != 1 {major_interval.y as f64} else {2.}; - } - iterations += 1; + let mut iterations = 0; + size = size.abs(); + while (size.x * navigation.zoom() < 10.) || (size.y * navigation.zoom() < 10.) { + if iterations > 100 { + return None; + } + if size.x * navigation.zoom() < 10. { + size.x *= if major_interval.x != 1 { major_interval.x as f64 } else { 2. }; + } + if size.y * navigation.zoom() < 10. { + size.y *= if major_interval.y != 1 { major_interval.y as f64 } else { 2. }; } - Some(size) + iterations += 1; } + Some(size) + } // Double grid size until it takes up at least 10px. pub fn compute_isometric_multiplier(length: f64, divisor: f64, navigation: &PTZ) -> Option { @@ -280,11 +280,11 @@ impl GridSnapping { } pub fn has_minor_lines(&self) -> bool { - match self.grid_type { - GridType::Rectangular { .. } => self.rectangular_major_interval.x > 1 || self.rectangular_major_interval.y > 1, - GridType::Isometric { .. } => self.isometric_major_interval.x > 1 || self.isometric_major_interval.z > 1 || self.isometric_major_interval.y > 1, - } + match self.grid_type { + GridType::Rectangular { .. } => self.rectangular_major_interval.x > 1 || self.rectangular_major_interval.y > 1, + GridType::Isometric { .. } => self.isometric_major_interval.x > 1 || self.isometric_major_interval.z > 1 || self.isometric_major_interval.y > 1, } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs index 38947d11a0..c37cc36ce1 100644 --- a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs @@ -1,6 +1,6 @@ use super::*; use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, GridSnapping, GridType, SnapTarget}; -use glam::{DVec2,UVec2}; +use glam::{DVec2, UVec2}; use graphene_std::renderer::Quad; struct Line { From 7732aca3997f681db9c967151a9823195bab161a Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 2 Nov 2025 22:12:42 -0800 Subject: [PATCH 6/6] scale if either goes below ten (for keavon) --- .../messages/portfolio/document/utility_types/misc.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 8a3d159d1e..86ae6e4955 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -249,16 +249,12 @@ impl GridSnapping { pub fn compute_rectangle_spacing(mut size: DVec2, major_interval: &UVec2, navigation: &PTZ) -> Option { let mut iterations = 0; size = size.abs(); - while (size.x * navigation.zoom() < 10.) || (size.y * navigation.zoom() < 10.) { + while (size * navigation.zoom()).cmplt(DVec2::splat(10.)).any() { if iterations > 100 { return None; } - if size.x * navigation.zoom() < 10. { - size.x *= if major_interval.x != 1 { major_interval.x as f64 } else { 2. }; - } - if size.y * navigation.zoom() < 10. { - size.y *= if major_interval.y != 1 { major_interval.y as f64 } else { 2. }; - } + size.x *= if major_interval.x != 1 { major_interval.x as f64 } else { 2. }; + size.y *= if major_interval.y != 1 { major_interval.y as f64 } else { 2. }; iterations += 1; } Some(size)