From 5a547a96d3b7018c194fa898cbc3ad5076bbb65b Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 19 Oct 2025 16:33:26 -0400 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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 05/16] 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 06/16] 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) From 12c8877f24fbec6e94262c11734eb8079a3e32c8 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sat, 15 Nov 2025 22:33:48 -0500 Subject: [PATCH 07/16] more dotted grid work --- editor/src/consts.rs | 1 + .../document/overlays/grid_overlays.rs | 88 +++++++++++++++---- .../document/overlays/utility_types.rs | 64 ++++++++++++++ .../document/overlays/utility_types_vello.rs | 79 +++++++++++++++++ .../portfolio/document/utility_types/misc.rs | 4 +- 5 files changed, 216 insertions(+), 20 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 396e29a724..2c268d5edc 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -150,6 +150,7 @@ 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"; +pub const COLOR_OVERLAY_TRANSPARENT: &str = "#00000000"; // DOCUMENT pub const FILE_EXTENSION: &str = "graphite"; diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 01a2ead266..d8d4d74ed6 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -1,3 +1,4 @@ +use crate::consts::COLOR_OVERLAY_TRANSPARENT; 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}; @@ -66,35 +67,86 @@ 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(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]); - let min = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let max = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + // Draw horizontal dotted lines + let min_y = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let max_y = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let mut primary_start = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let mut primary_end = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let primary_start_x = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let primary_end_x = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - primary_start = (primary_start / spacing.x).floor() * spacing.x + origin.x % spacing.x; - primary_end = (primary_end / spacing.x).floor() * spacing.x + origin.x % spacing.x; + let first_index_y = ((min_y - origin.y) / scaled_spacing.y).ceil() as i32; - // Round to avoid floating point errors - let total_dots = ((primary_end - primary_start) / spacing.x).round(); + for line_index in 0..=((max_y - min_y) / scaled_spacing.y).ceil() as i32 { + let y_is_major = is_major_line(line_index + first_index_y, document.snapping_state.grid.rectangular_major_interval.y); + let is_major = y_is_major || scale_is_adjusted; + let is_thick = is_major && document.snapping_state.grid.major_is_thick; - for line_index in 0..=((max - min) / spacing.y).ceil() as i32 { - let secondary_pos = (((min - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; - let start = DVec2::new(primary_start, secondary_pos); - let end = DVec2::new(primary_end, secondary_pos); + let secondary_pos = (((min_y - origin.y) / scaled_spacing.y).ceil() + line_index as f64) * scaled_spacing.y + origin.y; - let x_per_dot = (end.x - start.x) / total_dots; - for dot_index in 0..=total_dots as usize { - let exact_x = x_per_dot * dot_index as f64; - overlay_context.pixel(document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(), Some(&grid_color)) - } + // Align horizontal line endpoints to the grid in the x direction + let aligned_start_x = ((primary_start_x - origin.x) / scaled_spacing.x).floor() * scaled_spacing.x + origin.x; + let aligned_end_x = ((primary_end_x - origin.x) / scaled_spacing.x).ceil() * scaled_spacing.x + origin.x; + + let start = DVec2::new(aligned_start_x, secondary_pos); + let end = DVec2::new(aligned_end_x, secondary_pos); + + let dot_size = 3.; + let gap_size = scaled_spacing.x * document_to_viewport.matrix2.x_axis.length() - dot_size; + + overlay_context.pixel_snapped_dashed_line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(if is_major { &grid_color } else { &grid_color_minor }), + Some(if is_thick { 3.0 } else { 1.0 }), + Some(dot_size), + Some(gap_size), + Some(2.0), + ); + } + + // Draw vertical dotted lines + let min_x = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let max_x = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + let primary_start_y = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let primary_end_y = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + let first_index_x = ((min_x - origin.x) / scaled_spacing.x).ceil() as i32; + + for line_index in 0..=((max_x - min_x) / scaled_spacing.x).ceil() as i32 { + let x_is_major = is_major_line(line_index + first_index_x, document.snapping_state.grid.rectangular_major_interval.x); + let is_major = x_is_major || scale_is_adjusted; + let is_thick = is_major && document.snapping_state.grid.major_is_thick; + + let secondary_pos = (((min_x - origin.x) / scaled_spacing.x).ceil() + line_index as f64) * scaled_spacing.x + origin.x; + + // Align vertical line endpoints to the grid in the y direction + let aligned_start_y = ((primary_start_y - origin.y) / scaled_spacing.y).floor() * scaled_spacing.y + origin.y; + let aligned_end_y = ((primary_end_y - origin.y) / scaled_spacing.y).ceil() * scaled_spacing.y + origin.y; + + let start = DVec2::new(secondary_pos, aligned_start_y); + let end = DVec2::new(secondary_pos, aligned_end_y); + + let dot_size = 3.; + let gap_size = scaled_spacing.y * document_to_viewport.matrix2.y_axis.length() - dot_size; + + overlay_context.pixel_snapped_dashed_line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(if is_major { &grid_color } else { &grid_color_minor }), + Some(if is_thick { 3.0 } else { 1.0 }), + Some(dot_size), + Some(gap_size), + Some(2.0), + ); } } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index f02372ff11..a970801552 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -297,6 +297,70 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + pub fn pixel_snapped_dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + // Check if the line is horizontal or vertical + let is_horizontal = (start.y - end.y).abs() < f64::EPSILON; + let is_vertical = (start.x - end.x).abs() < f64::EPSILON; + + if !is_horizontal && !is_vertical { + // Fall back to regular dashed line for diagonal lines + self.dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); + return; + } + + self.start_dpi_aware_transform(); + + // Set the dash pattern + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + + let (draw_start, draw_end) = if is_horizontal { + // For horizontal lines, snap to the pixel grid and offset by 0.5 for crisp lines + let y = start.y.round() - 0.5; + (DVec2::new(start.x, y), DVec2::new(end.x, y)) + } else { + // For vertical lines, snap to the pixel grid and offset by 0.5 for crisp lines + let x = start.x.round() - 0.5; + (DVec2::new(x, start.y), DVec2::new(x, end.y)) + }; + + self.render_context.begin_path(); + self.render_context.move_to(draw_start.x, draw_start.y); + self.render_context.line_to(draw_end.x, draw_end.y); + self.render_context.set_line_width(thickness.unwrap_or(1.)); + self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE)); + self.render_context.stroke(); + self.render_context.set_line_width(1.); + + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + + self.end_dpi_aware_transform(); + } + #[allow(clippy::too_many_arguments)] pub fn dashed_ellipse( &mut self, diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index c188d3b574..cf47dd27f6 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -264,6 +264,12 @@ impl OverlayContext { self.internal().dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); } + /// Creates a dashed line with pixel-perfect snapping for crisp rendering + #[allow(clippy::too_many_arguments)] + pub fn pixel_snapped_dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.internal().pixel_snapped_dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); + } + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { self.internal().hover_manipulator_handle(position, selected); } @@ -541,6 +547,79 @@ impl OverlayContextInternal { self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); } + /// Creates a dashed line with pixel-perfect snapping for crisp rendering + /// Each dash segment is individually pixel-aligned while maintaining accuracy to input FP values + #[allow(clippy::too_many_arguments)] + fn pixel_snapped_dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + let transform = self.get_transform(); + let thickness = thickness.unwrap_or(1.0).round().max(1.0); + + // If no dashing is specified, fall back to regular pixel-snapped line + let dash_width = match dash_width { + Some(width) => width, + None => { + let start = start.round() - DVec2::splat(0.5); + let end = end.round() - DVec2::splat(0.5); + + let mut path = BezPath::new(); + path.move_to(kurbo::Point::new(start.x, start.y)); + path.line_to(kurbo::Point::new(end.x, end.y)); + + let stroke = kurbo::Stroke::new(thickness); + self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); + return; + } + }; + + let dash_gap = dash_gap_width.unwrap_or(1.0); + let dash_offset = dash_offset.unwrap_or(0.0); + + // Calculate the line vector and length + let line_vec = end - start; + let line_length = line_vec.length(); + + if line_length < 0.001 { + return; // Line too short to render + } + + let line_unit = line_vec / line_length; + + // Calculate dash pattern cycle length + let dash_cycle = dash_width + dash_gap; + if dash_cycle <= 0.0 { + return; + } + + let mut path = BezPath::new(); + let mut current_distance = -dash_offset.rem_euclid(dash_cycle); + + while current_distance < line_length { + let dash_start_distance = current_distance.max(0.0); + let dash_end_distance = (current_distance + dash_width).min(line_length); + + if dash_start_distance < dash_end_distance { + // Calculate actual positions along the line + let dash_start_pos = start + line_unit * dash_start_distance; + let dash_end_pos = start + line_unit * dash_end_distance; + + // Snap each dash segment to pixel boundaries + let snapped_start = dash_start_pos.round() - DVec2::splat(0.5); + let snapped_end = dash_end_pos.round() - DVec2::splat(0.5); + + // Only add the dash if it has meaningful length after snapping + if (snapped_end - snapped_start).length() >= 0.5 { + path.move_to(kurbo::Point::new(snapped_start.x, snapped_start.y)); + path.line_to(kurbo::Point::new(snapped_end.x, snapped_end.y)); + } + } + + current_distance += dash_cycle; + } + + let stroke = kurbo::Stroke::new(thickness); + self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); + } + fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { let transform = self.get_transform(); let position = position.round() - DVec2::splat(0.5); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 86ae6e4955..f149da6403 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -253,8 +253,8 @@ impl GridSnapping { if iterations > 100 { return None; } - 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. }; + size.x *= if iterations == 0 { major_interval.x as f64 } else { 2. }; + size.y *= if iterations == 0 { major_interval.y as f64 } else { 2. }; iterations += 1; } Some(size) From a488bf98ad1ffae4d8a318fa8504c295685d1db8 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sat, 15 Nov 2025 22:38:44 -0500 Subject: [PATCH 08/16] Add major/minor grid lines to rect. dotted grids --- editor/src/consts.rs | 1 + .../document/overlays/grid_overlays.rs | 91 +++++++++++++++---- .../document/overlays/utility_types.rs | 64 +++++++++++++ .../document/overlays/utility_types_vello.rs | 79 ++++++++++++++++ .../portfolio/document/utility_types/misc.rs | 3 +- 5 files changed, 220 insertions(+), 18 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index b17a8621cc..cee1d6bfb3 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -149,6 +149,7 @@ pub const COLOR_OVERLAY_GRAY: &str = "#cccccc"; pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40"; pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; +pub const COLOR_OVERLAY_TRANSPARENT: &str = "#00000000"; // DOCUMENT pub const FILE_EXTENSION: &str = "graphite"; diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index d54a5f2ad0..92edbeb14b 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -1,3 +1,4 @@ +use crate::consts::COLOR_OVERLAY_TRANSPARENT; 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}; @@ -57,31 +58,87 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte let document_to_viewport = document .navigation_handler .calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz); + let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); + 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.viewport.size().into()]); - let min = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let max = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + // Draw horizontal dotted lines + let min_y = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let max_y = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let mut primary_start = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - let mut primary_end = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let primary_start_x = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let primary_end_x = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); - primary_start = (primary_start / spacing.x).floor() * spacing.x + origin.x % spacing.x; - primary_end = (primary_end / spacing.x).floor() * spacing.x + origin.x % spacing.x; + let first_index_y = ((min_y - origin.y) / scaled_spacing.y).ceil() as i32; - // Round to avoid floating point errors - let total_dots = ((primary_end - primary_start) / spacing.x).round(); + for line_index in 0..=((max_y - min_y) / scaled_spacing.y).ceil() as i32 { + let y_is_major = is_major_line(line_index + first_index_y, document.snapping_state.grid.rectangular_major_interval.y); + let is_major = y_is_major || scale_is_adjusted; + let is_thick = is_major && document.snapping_state.grid.major_is_thick; - for line_index in 0..=((max - min) / spacing.y).ceil() as i32 { - let secondary_pos = (((min - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; - let start = DVec2::new(primary_start, secondary_pos); - let end = DVec2::new(primary_end, secondary_pos); + let secondary_pos = (((min_y - origin.y) / scaled_spacing.y).ceil() + line_index as f64) * scaled_spacing.y + origin.y; - let x_per_dot = (end.x - start.x) / total_dots; - for dot_index in 0..=total_dots as usize { - let exact_x = x_per_dot * dot_index as f64; - overlay_context.pixel(document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(), Some(&grid_color)) - } + // Align horizontal line endpoints to the grid in the x direction + let aligned_start_x = ((primary_start_x - origin.x) / scaled_spacing.x).floor() * scaled_spacing.x + origin.x; + let aligned_end_x = ((primary_end_x - origin.x) / scaled_spacing.x).ceil() * scaled_spacing.x + origin.x; + + let start = DVec2::new(aligned_start_x, secondary_pos); + let end = DVec2::new(aligned_end_x, secondary_pos); + + let dot_size = 3.; + let gap_size = scaled_spacing.x * document_to_viewport.matrix2.x_axis.length() - dot_size; + + overlay_context.pixel_snapped_dashed_line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(if is_major { &grid_color } else { &grid_color_minor }), + Some(if is_thick { 3.0 } else { 1.0 }), + Some(dot_size), + Some(gap_size), + Some(2.0), + ); + } + + // Draw vertical dotted lines + let min_x = bounds.0.iter().map(|corner| corner.x).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let max_x = bounds.0.iter().map(|corner| corner.x).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + let primary_start_y = bounds.0.iter().map(|corner| corner.y).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let primary_end_y = bounds.0.iter().map(|corner| corner.y).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + let first_index_x = ((min_x - origin.x) / scaled_spacing.x).ceil() as i32; + + for line_index in 0..=((max_x - min_x) / scaled_spacing.x).ceil() as i32 { + let x_is_major = is_major_line(line_index + first_index_x, document.snapping_state.grid.rectangular_major_interval.x); + let is_major = x_is_major || scale_is_adjusted; + let is_thick = is_major && document.snapping_state.grid.major_is_thick; + + let secondary_pos = (((min_x - origin.x) / scaled_spacing.x).ceil() + line_index as f64) * scaled_spacing.x + origin.x; + + // Align vertical line endpoints to the grid in the y direction + let aligned_start_y = ((primary_start_y - origin.y) / scaled_spacing.y).floor() * scaled_spacing.y + origin.y; + let aligned_end_y = ((primary_end_y - origin.y) / scaled_spacing.y).ceil() * scaled_spacing.y + origin.y; + + let start = DVec2::new(secondary_pos, aligned_start_y); + let end = DVec2::new(secondary_pos, aligned_end_y); + + let dot_size = 3.; + let gap_size = scaled_spacing.y * document_to_viewport.matrix2.y_axis.length() - dot_size; + + overlay_context.pixel_snapped_dashed_line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(if is_major { &grid_color } else { &grid_color_minor }), + Some(if is_thick { 3.0 } else { 1.0 }), + Some(dot_size), + Some(gap_size), + Some(2.0), + ); } } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index e950c2b185..fea47b37fe 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -295,6 +295,70 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + pub fn pixel_snapped_dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + // Check if the line is horizontal or vertical + let is_horizontal = (start.y - end.y).abs() < f64::EPSILON; + let is_vertical = (start.x - end.x).abs() < f64::EPSILON; + + if !is_horizontal && !is_vertical { + // Fall back to regular dashed line for diagonal lines + self.dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); + return; + } + + self.start_dpi_aware_transform(); + + // Set the dash pattern + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + + let (draw_start, draw_end) = if is_horizontal { + // For horizontal lines, snap to the pixel grid and offset by 0.5 for crisp lines + let y = start.y.round() - 0.5; + (DVec2::new(start.x, y), DVec2::new(end.x, y)) + } else { + // For vertical lines, snap to the pixel grid and offset by 0.5 for crisp lines + let x = start.x.round() - 0.5; + (DVec2::new(x, start.y), DVec2::new(x, end.y)) + }; + + self.render_context.begin_path(); + self.render_context.move_to(draw_start.x, draw_start.y); + self.render_context.line_to(draw_end.x, draw_end.y); + self.render_context.set_line_width(thickness.unwrap_or(1.)); + self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE)); + self.render_context.stroke(); + self.render_context.set_line_width(1.); + + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + + self.end_dpi_aware_transform(); + } + #[allow(clippy::too_many_arguments)] pub fn dashed_ellipse( &mut self, diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 202fec98a8..6cee30938b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -256,6 +256,12 @@ impl OverlayContext { self.internal().dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); } + /// Creates a dashed line with pixel-perfect snapping for crisp rendering + #[allow(clippy::too_many_arguments)] + pub fn pixel_snapped_dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + self.internal().pixel_snapped_dashed_line(start, end, color, thickness, dash_width, dash_gap_width, dash_offset); + } + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { self.internal().hover_manipulator_handle(position, selected); } @@ -540,6 +546,79 @@ impl OverlayContextInternal { self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); } + /// Creates a dashed line with pixel-perfect snapping for crisp rendering + /// Each dash segment is individually pixel-aligned while maintaining accuracy to input FP values + #[allow(clippy::too_many_arguments)] + fn pixel_snapped_dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option, dash_width: Option, dash_gap_width: Option, dash_offset: Option) { + let transform = self.get_transform(); + let thickness = thickness.unwrap_or(1.0).round().max(1.0); + + // If no dashing is specified, fall back to regular pixel-snapped line + let dash_width = match dash_width { + Some(width) => width, + None => { + let start = start.round() - DVec2::splat(0.5); + let end = end.round() - DVec2::splat(0.5); + + let mut path = BezPath::new(); + path.move_to(kurbo::Point::new(start.x, start.y)); + path.line_to(kurbo::Point::new(end.x, end.y)); + + let stroke = kurbo::Stroke::new(thickness); + self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); + return; + } + }; + + let dash_gap = dash_gap_width.unwrap_or(1.0); + let dash_offset = dash_offset.unwrap_or(0.0); + + // Calculate the line vector and length + let line_vec = end - start; + let line_length = line_vec.length(); + + if line_length < 0.001 { + return; // Line too short to render + } + + let line_unit = line_vec / line_length; + + // Calculate dash pattern cycle length + let dash_cycle = dash_width + dash_gap; + if dash_cycle <= 0.0 { + return; + } + + let mut path = BezPath::new(); + let mut current_distance = -dash_offset.rem_euclid(dash_cycle); + + while current_distance < line_length { + let dash_start_distance = current_distance.max(0.0); + let dash_end_distance = (current_distance + dash_width).min(line_length); + + if dash_start_distance < dash_end_distance { + // Calculate actual positions along the line + let dash_start_pos = start + line_unit * dash_start_distance; + let dash_end_pos = start + line_unit * dash_end_distance; + + // Snap each dash segment to pixel boundaries + let snapped_start = dash_start_pos.round() - DVec2::splat(0.5); + let snapped_end = dash_end_pos.round() - DVec2::splat(0.5); + + // Only add the dash if it has meaningful length after snapping + if (snapped_end - snapped_start).length() >= 0.5 { + path.move_to(kurbo::Point::new(snapped_start.x, snapped_start.y)); + path.line_to(kurbo::Point::new(snapped_end.x, snapped_end.y)); + } + } + + current_distance += dash_cycle; + } + + let stroke = kurbo::Stroke::new(thickness); + self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path); + } + fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { let transform = self.get_transform(); let position = position.round() - DVec2::splat(0.5); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index ae8fd73532..83e8263611 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -244,7 +244,8 @@ impl GridSnapping { if iterations > 100 { return None; } - size *= 2.; + size.x *= if iterations == 0 { major_interval.x as f64 } else { 2. }; + size.y *= if iterations == 0 { major_interval.y as f64 } else { 2. }; iterations += 1; } Some(size) From 1ae49217f87c9be73506b53f696ec95be0278891 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 16 Nov 2025 15:04:35 -0500 Subject: [PATCH 09/16] fix document_to_viewport --- .../src/messages/portfolio/document/overlays/grid_overlays.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 6fd1ff24bd..274fec0e51 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -81,7 +81,9 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte 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 document_to_viewport = document + .navigation_handler + .calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz); let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]); From ec954f6ca0998b7c6534e72710f4911c66e26d60 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 16 Nov 2025 17:12:27 -0500 Subject: [PATCH 10/16] add iso major/minor --- .../document/overlays/grid_overlays.rs | 66 ++++++++++++------- 1 file changed, 41 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 274fec0e51..ece2660bb5 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -164,6 +164,7 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { 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 cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); let origin = document.snapping_state.grid.origin; let document_to_viewport = document @@ -184,31 +185,49 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m let min_y = bounds.0.iter().map(|&corner| corner.y).min_by(cmp).unwrap_or_default(); let max_y = bounds.0.iter().map(|&corner| corner.y).max_by(cmp).unwrap_or_default(); let spacing = isometric_spacing.x; + let first_index = ((min_x - origin.x) / spacing).ceil() as i32; for line_index in 0..=((max_x - min_x) / spacing).ceil() as i32 { + let is_major = is_major_line(line_index + first_index, document.snapping_state.grid.isometric_major_interval.x) || spacing_multiplier != 1.0; let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x; let start = DVec2::new(x_pos, min_y); let end = DVec2::new(x_pos, max_y); - 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), + 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.) }, + ); } - for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] { + for (tan, multiply, major_interval) in [ + (tan_a, -1., document.snapping_state.grid.isometric_major_interval.z), + (tan_b, 1., document.snapping_state.grid.isometric_major_interval.y), + ] { let project = |corner: &DVec2| corner.y + multiply * tan * (corner.x - origin.x); let inverse_project = |corner: &DVec2| corner.y - tan * multiply * (corner.x - origin.x); let min_y = bounds.0.into_iter().min_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default(); let max_y = bounds.0.into_iter().max_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default(); let spacing = isometric_spacing.y; let lines = ((inverse_project(&max_y) - inverse_project(&min_y)) / spacing).ceil() as i32; + let first_index = ((inverse_project(&min_y) - origin.y) / spacing).ceil() as i32; for line_index in 0..=lines { + let is_major = is_major_line(line_index + first_index, major_interval) || spacing_multiplier != 1.0; let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y; let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos))); let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos))); - 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), + 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.) }, + ); } } } fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { 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 cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); let origin = document.snapping_state.grid.origin; let document_to_viewport = document @@ -242,7 +261,9 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context return; } let x_offset = (((min_x - origin.x) / spacing_x).ceil()) * spacing_x + origin.x - min_x; + let first_index = ((inverse_project(&min_y) - origin.y) / spacing_y).ceil() as i32; for line_index in 0..=lines { + let is_major = is_major_line(line_index + first_index, document.snapping_state.grid.isometric_major_interval.z) || spacing_multiplier != 1.0; let y_pos = (((inverse_project(&min_y) - origin.y) / spacing_y).ceil() + line_index as f64) * spacing_y + origin.y; let start = DVec2::new(min_x + x_offset, project(&DVec2::new(min_x + x_offset, y_pos))); let end = DVec2::new(max_x + x_offset, project(&DVec2::new(max_x + x_offset, y_pos))); @@ -250,11 +271,11 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context overlay_context.dashed_line( document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), - Some(&grid_color), - None, + 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.) }, + Some(3.), + Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() - 3.), Some(1.), - Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() - 1.), - None, ); } } @@ -507,43 +528,38 @@ 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.z as f64)) - .label("A") + NumberInput::new(Some(grid.isometric_major_interval.x as f64)) + .label("X") .int() .min(1.) - .min_width(98) + .min_width(64) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval.z = val as u32; + grid.isometric_major_interval.x = val as u32; } })) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(grid.isometric_major_interval.y as f64)) - .label("B") + NumberInput::new(Some(grid.isometric_major_interval.z as f64)) + .label("A") .int() .min(1.) - .min_width(98) + .min_width(64) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval.y = val as u32; + grid.isometric_major_interval.z = 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.x as f64)) - .label("X") + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(Some(grid.isometric_major_interval.y as f64)) + .label("B") .int() .min(1.) - .min_width(200) + .min_width(64) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval.x = val as u32; + grid.isometric_major_interval.y = val as u32; } })) .widget_holder(), From eb1a4602f430a839fb483564aedc2d35d4376323 Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 16 Nov 2025 17:21:06 -0500 Subject: [PATCH 11/16] delete duplicate variable defs --- .../portfolio/document/overlays/grid_overlays.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index ece2660bb5..d7e97eb24a 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -1,9 +1,8 @@ -use crate::consts::COLOR_OVERLAY_TRANSPARENT; 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}; use graphene_std::raster::color::Color; use graphene_std::renderer::Quad; use graphene_std::vector::style::FillChoice; @@ -73,13 +72,6 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte let Some(scaled_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.viewport.center_in_viewport_space().into(), &document.document_ptz); - let grid_color_minor = "#".to_string() + &document.snapping_state.grid.grid_color_minor.to_rgba_hex_srgb(); - 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 From 25eb40796207b8100906e69ae995b8878f18857c Mon Sep 17 00:00:00 2001 From: blue linden Date: Sun, 16 Nov 2025 17:25:41 -0500 Subject: [PATCH 12/16] format --- .../src/messages/portfolio/document/overlays/grid_overlays.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index d7e97eb24a..e65d778c3e 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; use graphene_std::raster::color::Color; use graphene_std::renderer::Quad; use graphene_std::vector::style::FillChoice; From 00501c2b79228359e95a837561bbdbb4fb9171e5 Mon Sep 17 00:00:00 2001 From: blue linden Date: Mon, 17 Nov 2025 00:20:07 -0500 Subject: [PATCH 13/16] swap the places of A and B intervals in the isometric dialog --- .../portfolio/document/overlays/grid_overlays.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index e65d778c3e..a638e9e44c 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -532,26 +532,26 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { })) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(grid.isometric_major_interval.z as f64)) - .label("A") + NumberInput::new(Some(grid.isometric_major_interval.y as f64)) + .label("B") .int() .min(1.) .min_width(64) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval.z = val as u32; + grid.isometric_major_interval.y = val as u32; } })) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(Some(grid.isometric_major_interval.y as f64)) - .label("B") + NumberInput::new(Some(grid.isometric_major_interval.z as f64)) + .label("A") .int() .min(1.) .min_width(64) .on_update(update_val(grid, |grid, val: &NumberInput| { if let Some(val) = val.value { - grid.isometric_major_interval.y = val as u32; + grid.isometric_major_interval.z = val as u32; } })) .widget_holder(), From 73f1049f4d8c7c4caa37854072b2010a157c5b28 Mon Sep 17 00:00:00 2001 From: blue linden Date: Mon, 17 Nov 2025 13:30:36 -0500 Subject: [PATCH 14/16] fix layout_widget diffing --- .../layout/utility_types/layout_widget.rs | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 446b09ec0d..bb806407b4 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -508,20 +508,23 @@ impl WidgetHolder { /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec) { - if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut self.widget, &new.widget) - && button1.disabled == button2.disabled - && button1.style == button2.style - && button1.menu_direction == button2.menu_direction - && button1.icon == button2.icon - && button1.tooltip == button2.tooltip - && button1.tooltip_shortcut == button2.tooltip_shortcut - && button1.popover_min_width == button2.popover_min_width - { - let mut new_widget_path = widget_path.to_vec(); - for (i, (a, b)) in button1.popover_layout.iter_mut().zip(button2.popover_layout.iter()).enumerate() { - new_widget_path.push(i); - a.diff(b.clone(), &mut new_widget_path, widget_diffs); - new_widget_path.pop(); + if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut self.widget, &new.widget) { + if button1.disabled == button2.disabled + && button1.style == button2.style + && button1.menu_direction == button2.menu_direction + && button1.icon == button2.icon + && button1.tooltip == button2.tooltip + && button1.tooltip_shortcut == button2.tooltip_shortcut + && button1.popover_min_width == button2.popover_min_width + && button1.popover_layout.len() == button2.popover_layout.len() + { + let mut new_widget_path = widget_path.to_vec(); + for (i, (a, b)) in button1.popover_layout.iter_mut().zip(button2.popover_layout.iter()).enumerate() { + new_widget_path.push(i); + a.diff(b.clone(), &mut new_widget_path, widget_diffs); + new_widget_path.pop(); + } + return; } return; } From e5e3e5a5b1ebc4f71b11811d20c9eb5b77eb1fca Mon Sep 17 00:00:00 2001 From: blue linden Date: Mon, 17 Nov 2025 13:24:51 -0500 Subject: [PATCH 15/16] remove errant comment --- editor/src/messages/portfolio/document/overlays/grid_overlays.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 5c947008e8..e42ecbbf44 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -375,7 +375,6 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { grid.dot_display = true; })), ]) - // .min_width(200) .selected_index(Some(if grid.dot_display { 1 } else { 0 })) .widget_holder(), ], From c560ba6ccb8130b990951ab4bb896aec8dbddeb2 Mon Sep 17 00:00:00 2001 From: blue linden Date: Mon, 17 Nov 2025 14:02:48 -0500 Subject: [PATCH 16/16] undo accidental if let change --- .../layout/utility_types/layout_widget.rs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index bb806407b4..4a52a56167 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -508,23 +508,21 @@ impl WidgetHolder { /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec) { - if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut self.widget, &new.widget) { - if button1.disabled == button2.disabled - && button1.style == button2.style - && button1.menu_direction == button2.menu_direction - && button1.icon == button2.icon - && button1.tooltip == button2.tooltip - && button1.tooltip_shortcut == button2.tooltip_shortcut - && button1.popover_min_width == button2.popover_min_width - && button1.popover_layout.len() == button2.popover_layout.len() - { - let mut new_widget_path = widget_path.to_vec(); - for (i, (a, b)) in button1.popover_layout.iter_mut().zip(button2.popover_layout.iter()).enumerate() { - new_widget_path.push(i); - a.diff(b.clone(), &mut new_widget_path, widget_diffs); - new_widget_path.pop(); - } - return; + if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut self.widget, &new.widget) + && button1.disabled == button2.disabled + && button1.style == button2.style + && button1.menu_direction == button2.menu_direction + && button1.icon == button2.icon + && button1.tooltip == button2.tooltip + && button1.tooltip_shortcut == button2.tooltip_shortcut + && button1.popover_min_width == button2.popover_min_width + && button1.popover_layout.len() == button2.popover_layout.len() + { + let mut new_widget_path = widget_path.to_vec(); + for (i, (a, b)) in button1.popover_layout.iter_mut().zip(button2.popover_layout.iter()).enumerate() { + new_widget_path.push(i); + a.diff(b.clone(), &mut new_widget_path, widget_diffs); + new_widget_path.pop(); } return; }