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..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; +use glam::{DVec2, UVec2}; use graphene_std::raster::color::Color; use graphene_std::renderer::Quad; use graphene_std::vector::style::FillChoice; @@ -10,9 +10,11 @@ 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 Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { + 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.size]); @@ -23,8 +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.x + } else { + 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) @@ -36,7 +47,12 @@ 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), + 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.) }, + ); } } } @@ -49,7 +65,8 @@ 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 Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { + 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 { return; }; let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); @@ -174,9 +191,13 @@ 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 +} + 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 +226,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 +238,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 +249,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 +311,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 +330,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 +351,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.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.x = val as u32; + } + })) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + 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.y = val as u32; + } + })) + .widget_holder(), + ], + }); + } GridType::Isometric { y_axis_spacing, angle_a, angle_b } => { widgets.push(LayoutGroup::Row { widgets: vec![ @@ -342,18 +421,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.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.z = val as u32; + } + })) + .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + 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.y = 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") + .int() + .min(1.) + .min_width(200) + .on_update(update_val(grid, |grid, val: &NumberInput| { + if let Some(val) = val.value { + 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 ae8fd73532..86ae6e4955 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; -use glam::DVec2; +use crate::consts::COLOR_OVERLAY_GRAY_DARK; +use glam::{DVec2, UVec2, UVec3}; use graphene_std::raster::Color; use std::fmt; @@ -213,10 +213,15 @@ pub struct GridSnapping { pub origin: DVec2, pub grid_type: GridType, pub rectangular_spacing: DVec2, + pub rectangular_major_interval: UVec2, pub isometric_y_spacing: f64, pub isometric_angle_a: f64, pub isometric_angle_b: f64, + /// 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, pub dot_display: bool, } @@ -226,10 +231,14 @@ impl Default for GridSnapping { origin: DVec2::ZERO, grid_type: Default::default(), rectangular_spacing: DVec2::ONE, + rectangular_major_interval: UVec2::ONE, 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: 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, dot_display: false, } } @@ -237,14 +246,15 @@ 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 { + pub fn compute_rectangle_spacing(mut size: DVec2, major_interval: &UVec2, navigation: &PTZ) -> Option { let mut iterations = 0; size = size.abs(); while (size * navigation.zoom()).cmplt(DVec2::splat(10.)).any() { if iterations > 100 { return None; } - size *= 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) @@ -264,6 +274,13 @@ impl GridSnapping { } Some(multiplier) } + + 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, + } + } } #[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..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; +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; @@ -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/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 @@ + + + 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 },