From eae7aa1d383624d6b69ba608ad8f6ca05253ff64 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 27 Mar 2024 13:17:06 -0700 Subject: [PATCH 1/7] dot grid --- .../document/overlays/grid_overlays.rs | 57 ++++++++++++++++++- .../document/overlays/utility_types.rs | 13 ++++- .../portfolio/document/utility_types/misc.rs | 8 +++ .../snapping/grid_snapper.rs | 1 + 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index cc6789a2aa..0400a32d86 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -79,10 +79,34 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m } } +fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { + let origin = document.snapping_state.grid.origin; + let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { + return; + }; + let document_to_viewport = document.metadata().document_to_viewport; + let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]); + + let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); + let min_x = bounds.0.iter().map(|&corner| corner.x).min_by(cmp).unwrap_or_default(); + let max_x = bounds.0.iter().map(|&corner| corner.x).max_by(cmp).unwrap_or_default(); + 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(); + for line_index in 0..=((max_x - min_x) / spacing.x).ceil() as i32 { + let x_pos = (((min_x - origin.x) / spacing.x).ceil() + line_index as f64) * spacing.x + origin.x; + for line_index in 0..=((max_y - min_y) / spacing.y).ceil() as i32 { + let y_pos = (((min_y - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; + let circle_pos = DVec2::new(x_pos, y_pos); + overlay_context.circle(document_to_viewport.transform_point2(circle_pos), 1., Some(COLOR_OVERLAY_GRAY), Some(COLOR_OVERLAY_GRAY)); + } + } +} + pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match document.snapping_state.grid.grid_type { GridType::Rectangle { spacing } => grid_overlay_rectangular(document, overlay_context, spacing), GridType::Isometric { y_axis_spacing, angle_a, angle_b } => grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b), + GridType::Dot { spacing } => grid_overlay_dot(document, overlay_context, spacing), } } @@ -138,9 +162,17 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { RadioEntryData::new("isometric") .label("Isometric") .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)), + RadioEntryData::new("dot") + .label("Dot") + .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::DOT)), ]) .min_width(200) - .selected_index(Some(if matches!(grid.grid_type, GridType::Rectangle { .. }) { 0 } else { 1 })) + .selected_index(Some(match grid.grid_type { + GridType::Rectangle { .. } => 0, + GridType::Isometric { .. } => 1, + GridType::Dot { .. } => 2, + _ => 0, + })) .widget_holder(), ], }); @@ -197,7 +229,28 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .widget_holder(), ], }); - } + }, + GridType::Dot { 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.dot_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.dot_spacing().map(|spacing| &mut spacing.y))) + .widget_holder(), + ], + }), } widgets diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index d8c2a98e29..4cb8b14590 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -83,7 +83,18 @@ impl OverlayContext { self.render_context.fill(); self.render_context.stroke(); } - + pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { + //let radius = radius.unwrap_or(DEFAULT_RADIUS); //DEFAULT_RADIUS has to be added to consts in order to use an option + let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let position = position.round(); + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0.0, 2.0 * PI).expect("draw circle"); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); + self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke)); + self.render_context.fill(); + self.render_context.stroke(); + } pub fn pivot(&mut self, position: DVec2) { let (x, y) = (position.round() - DVec2::splat(0.5)).into(); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 1faf4d3daf..78e68ba2da 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -155,6 +155,7 @@ pub struct OptionPointSnapping { pub enum GridType { Rectangle { spacing: DVec2 }, Isometric { y_axis_spacing: f64, angle_a: f64, angle_b: f64 }, + Dot { spacing: DVec2 }, } impl GridType { pub const RECTANGLE: Self = GridType::Rectangle { spacing: DVec2::ONE }; @@ -163,6 +164,7 @@ impl GridType { angle_a: 30., angle_b: 30., }; + pub const DOT: Self = GridType::Dot { spacing: DVec2::ONE }; pub fn rect_spacing(&mut self) -> Option<&mut DVec2> { match self { Self::Rectangle { spacing } => Some(spacing), @@ -187,6 +189,12 @@ impl GridType { _ => None, } } + pub fn dot_spacing(&mut self) -> Option<&mut DVec2> { + match self { + Self::Dot { spacing } => Some(spacing), + _ => None, + } + } } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub struct GridSnapping { 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 061265ec7a..ee1ee93427 100644 --- a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs @@ -93,6 +93,7 @@ impl GridSnapper { match snap_data.document.snapping_state.grid.grid_type { GridType::Rectangle { 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), + GridType::Dot { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing), } } From 2b1606e52ea291c8bff2e8d51bd80ec4d38fd7b9 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 27 Mar 2024 15:43:53 -0700 Subject: [PATCH 2/7] fix warning: unreachable pattern --- 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 0400a32d86..ee4ea65674 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -171,7 +171,6 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { GridType::Rectangle { .. } => 0, GridType::Isometric { .. } => 1, GridType::Dot { .. } => 2, - _ => 0, })) .widget_holder(), ], From 023e0fe277f02f6d335cea8b1610c761c62c4f2d Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 27 Mar 2024 16:36:40 -0700 Subject: [PATCH 3/7] grid color select --- .../document/overlays/grid_overlays.rs | 22 ++++++- .../portfolio/document/utility_types/misc.rs | 5 +- node-graph/gcore/src/raster/color.rs | 66 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index ee4ea65674..a95af19e87 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -5,9 +5,11 @@ use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, Gr use crate::messages::prelude::*; use glam::DVec2; use graphene_core::renderer::Quad; +use graphene_core::raster::color::Color; fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; + let grid_color: Color = document.snapping_state.grid.grid_color; let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { return; }; @@ -33,7 +35,7 @@ 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(COLOR_OVERLAY_GRAY)); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color.rgb_hex_prefixed())); } } } @@ -129,6 +131,15 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { } }) }; + let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| { + update_val::(grid, move |grid, val| { + if let Some(val) = val.value { + if let Some(update) = update(grid) { + *update = val; + } + } + }) + }; widgets.push(LayoutGroup::Row { widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()], }); @@ -252,5 +263,14 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { }), } + widgets.push(LayoutGroup::Row{ + widgets: vec![ + TextLabel::new("Color").table_align(true).widget_holder(), + Separator::new(SeparatorType::Unrelated).widget_holder(), + ColorButton::new(Some(grid.grid_color)) + .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) + .widget_holder(), + ]}); + widgets } diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 78e68ba2da..363f5398f4 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,6 +1,7 @@ use glam::DVec2; - use std::fmt; +use crate::consts::COLOR_OVERLAY_GRAY; +use graphene_core::raster::Color; #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)] @@ -86,6 +87,7 @@ impl Default for SnappingState { grid: GridSnapping { origin: DVec2::ZERO, grid_type: GridType::RECTANGLE, + grid_color: Color::from_rgb_str_prefixed(COLOR_OVERLAY_GRAY.to_uppercase().as_str()).expect("color"), }, tolerance: 8., artboards: true, @@ -200,6 +202,7 @@ impl GridType { pub struct GridSnapping { pub origin: DVec2, pub grid_type: GridType, + pub grid_color: Color, } impl GridSnapping { // Double grid size until it takes up at least 10px. diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 670d0ac1b8..46dbd344a5 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -749,6 +749,24 @@ impl Color { (self.a() * 255.) as u8, ) } + /// Return an 8-character RGBA hex string (with a # prefix). + /// + /// # Examples + /// ``` + /// use graphene_core::raster::color::Color; + /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); + /// assert_eq!("#3240A261", color.rgba_hex_prefixed()) + /// ``` + #[cfg(feature = "std")] + pub fn rgba_hex_prefixed(&self) -> String { + format!( + "#{:02X?}{:02X?}{:02X?}{:02X?}", + (self.r() * 255.) as u8, + (self.g() * 255.) as u8, + (self.b() * 255.) as u8, + (self.a() * 255.) as u8, + ) + } /// Return a 6-character RGB hex string (without a # prefix). /// ``` @@ -761,6 +779,17 @@ impl Color { format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8) } + /// Return a 6-character RGB hex string (with a # prefix). + /// ``` + /// use graphene_core::raster::color::Color; + /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); + /// assert_eq!("#3240A2", color.rgb_hex_prefixed()) + /// ``` + #[cfg(feature = "std")] + pub fn rgb_hex_prefixed(&self) -> String { + format!("#{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8) + } + /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. /// /// # Examples @@ -831,6 +860,26 @@ impl Color { Some(Color::from_rgba8_srgb(r, g, b, a)) } + /// Creates a color from a 8-character RGBA hex string (with a # prefix). + /// + /// # Examples + /// ``` + /// use graphene_core::raster::color::Color; + /// let color = Color::from_rgba_str_prefixed("#7C67FA61").unwrap(); + /// ``` + pub fn from_rgba_str_prefixed(color_str: &str) -> Option { + if color_str.len() != 9 { + return None; + } + let color_str = &color_str[1..]; + let r = u8::from_str_radix(&color_str[0..2], 16).ok()?; + let g = u8::from_str_radix(&color_str[2..4], 16).ok()?; + let b = u8::from_str_radix(&color_str[4..6], 16).ok()?; + let a = u8::from_str_radix(&color_str[6..8], 16).ok()?; + + Some(Color::from_rgba8_srgb(r, g, b, a)) + } + /// Creates a color from a 6-character RGB hex string (without a # prefix). /// ``` /// use graphene_core::raster::color::Color; @@ -847,6 +896,23 @@ impl Color { Some(Color::from_rgb8_srgb(r, g, b)) } + /// Creates a color from a 6-character RGB hex string (with a # prefix). + /// ``` + /// use graphene_core::raster::color::Color; + /// let color = Color::from_rgb_str_prefixed("7C67FA").unwrap(); + /// ``` + pub fn from_rgb_str_prefixed(color_str: &str) -> Option { + if color_str.len() != 7 { + return None; + } + let color_str = &color_str[1..]; + let r = u8::from_str_radix(&color_str[0..2], 16).ok()?; + let g = u8::from_str_radix(&color_str[2..4], 16).ok()?; + let b = u8::from_str_radix(&color_str[4..6], 16).ok()?; + + Some(Color::from_rgb8_srgb(r, g, b)) + } + /// Linearly interpolates between two colors based on t. /// /// T must be between 0 and 1. From 642be116164d8c55d7664b2bc71bd193fbfccbaa Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 27 Mar 2024 16:45:45 -0700 Subject: [PATCH 4/7] add color for all grid types --- .../portfolio/document/overlays/grid_overlays.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index a95af19e87..86e7479740 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -1,4 +1,3 @@ -use crate::consts::COLOR_OVERLAY_GRAY; 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}; @@ -41,6 +40,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: } fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { + let grid_color: Color = document.snapping_state.grid.grid_color; let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); let origin = document.snapping_state.grid.origin; let document_to_viewport = document.metadata().document_to_viewport; @@ -62,7 +62,7 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m 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(COLOR_OVERLAY_GRAY)); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color.rgb_hex_prefixed())); } for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] { @@ -76,13 +76,14 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m 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(COLOR_OVERLAY_GRAY)); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color.rgb_hex_prefixed())); } } } fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; + let grid_color: Color = document.snapping_state.grid.grid_color; let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { return; }; @@ -99,7 +100,7 @@ fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut Ove for line_index in 0..=((max_y - min_y) / spacing.y).ceil() as i32 { let y_pos = (((min_y - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; let circle_pos = DVec2::new(x_pos, y_pos); - overlay_context.circle(document_to_viewport.transform_point2(circle_pos), 1., Some(COLOR_OVERLAY_GRAY), Some(COLOR_OVERLAY_GRAY)); + overlay_context.circle(document_to_viewport.transform_point2(circle_pos), 1., Some(&grid_color.rgb_hex_prefixed()), Some(&grid_color.rgb_hex_prefixed())); } } } @@ -271,6 +272,6 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) .widget_holder(), ]}); - + widgets } From 2fec162e6e3114f8472e6855da82681cccda9d35 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 3 Apr 2024 13:18:46 -0700 Subject: [PATCH 5/7] Dot grid checkbox and remove prefixed Color functions --- .../document/overlays/grid_overlays.rs | 89 +++++++++++-------- .../portfolio/document/utility_types/misc.rs | 19 ++-- .../snapping/grid_snapper.rs | 1 - node-graph/gcore/src/raster/color.rs | 66 -------------- 4 files changed, 59 insertions(+), 116 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 86e7479740..129e7f463f 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -3,8 +3,8 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType}; use crate::messages::prelude::*; use glam::DVec2; -use graphene_core::renderer::Quad; use graphene_core::raster::color::Color; +use graphene_core::renderer::Quad; fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; @@ -34,7 +34,11 @@ 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.rgb_hex_prefixed())); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_owned() + &grid_color.rgb_hex())), + ); } } } @@ -62,7 +66,11 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m 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.rgb_hex_prefixed())); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_owned() + &grid_color.rgb_hex())), + ); } for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] { @@ -76,7 +84,11 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m 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.rgb_hex_prefixed())); + overlay_context.line( + document_to_viewport.transform_point2(start), + document_to_viewport.transform_point2(end), + Some(&("#".to_owned() + &grid_color.rgb_hex())), + ); } } } @@ -99,17 +111,27 @@ fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut Ove let x_pos = (((min_x - origin.x) / spacing.x).ceil() + line_index as f64) * spacing.x + origin.x; for line_index in 0..=((max_y - min_y) / spacing.y).ceil() as i32 { let y_pos = (((min_y - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; - let circle_pos = DVec2::new(x_pos, y_pos); - overlay_context.circle(document_to_viewport.transform_point2(circle_pos), 1., Some(&grid_color.rgb_hex_prefixed()), Some(&grid_color.rgb_hex_prefixed())); + let circle_pos = DVec2::new(x_pos, y_pos); + overlay_context.circle( + document_to_viewport.transform_point2(circle_pos), + 1., + Some(&("#".to_owned() + &grid_color.rgb_hex())), + Some(&("#".to_owned() + &grid_color.rgb_hex())), + ); } - } + } } pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match document.snapping_state.grid.grid_type { - GridType::Rectangle { spacing } => grid_overlay_rectangular(document, overlay_context, spacing), + GridType::Rectangle { spacing } => { + if document.snapping_state.grid.dot_display { + grid_overlay_dot(document, overlay_context, spacing) + } else { + grid_overlay_rectangular(document, overlay_context, spacing) + } + } GridType::Isometric { y_axis_spacing, angle_a, angle_b } => grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b), - GridType::Dot { spacing } => grid_overlay_dot(document, overlay_context, spacing), } } @@ -141,6 +163,14 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { } }) }; + let update_display = |grid, update: fn(&mut GridSnapping) -> Option<&mut bool>| { + update_val::(grid, move |grid, val| { + if let Some(update) = update(grid) { + *update = val.checked; + } + }) + }; + widgets.push(LayoutGroup::Row { widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()], }); @@ -174,15 +204,11 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { RadioEntryData::new("isometric") .label("Isometric") .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)), - RadioEntryData::new("dot") - .label("Dot") - .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::DOT)), ]) .min_width(200) .selected_index(Some(match grid.grid_type { GridType::Rectangle { .. } => 0, GridType::Isometric { .. } => 1, - GridType::Dot { .. } => 2, })) .widget_holder(), ], @@ -240,38 +266,25 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .widget_holder(), ], }); - }, - GridType::Dot { spacing } => widgets.push(LayoutGroup::Row { + } + } + match grid.grid_type { + GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row { widgets: vec![ - TextLabel::new("Spacing").table_align(true).widget_holder(), + TextLabel::new("Dot display").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.dot_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.dot_spacing().map(|spacing| &mut spacing.y))) - .widget_holder(), + CheckboxInput::new(grid.dot_display).on_update(update_display(grid, |grid| Some(&mut grid.dot_display))).widget_holder(), ], }), + GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {} } - - widgets.push(LayoutGroup::Row{ - widgets: vec![ + widgets.push(LayoutGroup::Row { + widgets: vec![ TextLabel::new("Color").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), - ColorButton::new(Some(grid.grid_color)) - .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) - .widget_holder(), - ]}); + ColorButton::new(Some(grid.grid_color)).on_update(update_color(grid, |grid| Some(&mut grid.grid_color))).widget_holder(), + ], + }); widgets } diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 363f5398f4..73fd101e22 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,7 +1,7 @@ -use glam::DVec2; -use std::fmt; use crate::consts::COLOR_OVERLAY_GRAY; +use glam::DVec2; use graphene_core::raster::Color; +use std::{fmt, path::Display}; #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)] @@ -87,7 +87,11 @@ impl Default for SnappingState { grid: GridSnapping { origin: DVec2::ZERO, grid_type: GridType::RECTANGLE, - grid_color: Color::from_rgb_str_prefixed(COLOR_OVERLAY_GRAY.to_uppercase().as_str()).expect("color"), + grid_color: COLOR_OVERLAY_GRAY + .strip_prefix("#") + .and_then(|value| Color::from_rgb_str(value)) + .expect("Should create Color from prefixed hex string"), + dot_display: false, }, tolerance: 8., artboards: true, @@ -157,7 +161,6 @@ pub struct OptionPointSnapping { pub enum GridType { Rectangle { spacing: DVec2 }, Isometric { y_axis_spacing: f64, angle_a: f64, angle_b: f64 }, - Dot { spacing: DVec2 }, } impl GridType { pub const RECTANGLE: Self = GridType::Rectangle { spacing: DVec2::ONE }; @@ -166,7 +169,6 @@ impl GridType { angle_a: 30., angle_b: 30., }; - pub const DOT: Self = GridType::Dot { spacing: DVec2::ONE }; pub fn rect_spacing(&mut self) -> Option<&mut DVec2> { match self { Self::Rectangle { spacing } => Some(spacing), @@ -191,18 +193,13 @@ impl GridType { _ => None, } } - pub fn dot_spacing(&mut self) -> Option<&mut DVec2> { - match self { - Self::Dot { spacing } => Some(spacing), - _ => None, - } - } } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub struct GridSnapping { pub origin: DVec2, pub grid_type: GridType, pub grid_color: Color, + pub dot_display: bool, } impl GridSnapping { // Double grid size until it takes up at least 10px. 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 ee1ee93427..061265ec7a 100644 --- a/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/grid_snapper.rs @@ -93,7 +93,6 @@ impl GridSnapper { match snap_data.document.snapping_state.grid.grid_type { GridType::Rectangle { 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), - GridType::Dot { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing), } } diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 46dbd344a5..670d0ac1b8 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -749,24 +749,6 @@ impl Color { (self.a() * 255.) as u8, ) } - /// Return an 8-character RGBA hex string (with a # prefix). - /// - /// # Examples - /// ``` - /// use graphene_core::raster::color::Color; - /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); - /// assert_eq!("#3240A261", color.rgba_hex_prefixed()) - /// ``` - #[cfg(feature = "std")] - pub fn rgba_hex_prefixed(&self) -> String { - format!( - "#{:02X?}{:02X?}{:02X?}{:02X?}", - (self.r() * 255.) as u8, - (self.g() * 255.) as u8, - (self.b() * 255.) as u8, - (self.a() * 255.) as u8, - ) - } /// Return a 6-character RGB hex string (without a # prefix). /// ``` @@ -779,17 +761,6 @@ impl Color { format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8) } - /// Return a 6-character RGB hex string (with a # prefix). - /// ``` - /// use graphene_core::raster::color::Color; - /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); - /// assert_eq!("#3240A2", color.rgb_hex_prefixed()) - /// ``` - #[cfg(feature = "std")] - pub fn rgb_hex_prefixed(&self) -> String { - format!("#{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8) - } - /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. /// /// # Examples @@ -860,26 +831,6 @@ impl Color { Some(Color::from_rgba8_srgb(r, g, b, a)) } - /// Creates a color from a 8-character RGBA hex string (with a # prefix). - /// - /// # Examples - /// ``` - /// use graphene_core::raster::color::Color; - /// let color = Color::from_rgba_str_prefixed("#7C67FA61").unwrap(); - /// ``` - pub fn from_rgba_str_prefixed(color_str: &str) -> Option { - if color_str.len() != 9 { - return None; - } - let color_str = &color_str[1..]; - let r = u8::from_str_radix(&color_str[0..2], 16).ok()?; - let g = u8::from_str_radix(&color_str[2..4], 16).ok()?; - let b = u8::from_str_radix(&color_str[4..6], 16).ok()?; - let a = u8::from_str_radix(&color_str[6..8], 16).ok()?; - - Some(Color::from_rgba8_srgb(r, g, b, a)) - } - /// Creates a color from a 6-character RGB hex string (without a # prefix). /// ``` /// use graphene_core::raster::color::Color; @@ -896,23 +847,6 @@ impl Color { Some(Color::from_rgb8_srgb(r, g, b)) } - /// Creates a color from a 6-character RGB hex string (with a # prefix). - /// ``` - /// use graphene_core::raster::color::Color; - /// let color = Color::from_rgb_str_prefixed("7C67FA").unwrap(); - /// ``` - pub fn from_rgb_str_prefixed(color_str: &str) -> Option { - if color_str.len() != 7 { - return None; - } - let color_str = &color_str[1..]; - let r = u8::from_str_radix(&color_str[0..2], 16).ok()?; - let g = u8::from_str_radix(&color_str[2..4], 16).ok()?; - let b = u8::from_str_radix(&color_str[4..6], 16).ok()?; - - Some(Color::from_rgb8_srgb(r, g, b)) - } - /// Linearly interpolates between two colors based on t. /// /// T must be between 0 and 1. From 51d0e28205ecb8ee0ae5bce58da85108b0eae81a Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 6 May 2024 19:23:25 -0700 Subject: [PATCH 6/7] Display dot grid as grid aligned pixels --- .../document/overlays/grid_overlays.rs | 75 +++++++++++-------- .../document/overlays/utility_types.rs | 28 +++++-- .../portfolio/document/utility_types/misc.rs | 2 +- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 129e7f463f..53f5eccf2b 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -43,6 +43,44 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: } } +//TODO: Potentially create an image and render the image onto the canvas a single time +fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { + let origin = document.snapping_state.grid.origin; + let grid_color: Color = document.snapping_state.grid.grid_color; + let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { + return; + }; + let document_to_viewport = document.metadata().document_to_viewport; + let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]); + + let min = bounds.0.iter().map(|&corner| corner[1]).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let max = bounds.0.iter().map(|&corner| corner[1]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let mut primary_start = bounds.0.iter().map(|&corner| corner[0]).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + let mut primary_end = bounds.0.iter().map(|&corner| corner[0]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default(); + + primary_start = (primary_start / spacing.x).ceil() * spacing.x; + primary_end = (primary_end / spacing.x).ceil() * spacing.x; + + let spacing = spacing[0]; + + let total_dots = ((primary_end - primary_start) / spacing).ceil(); + + for line_index in 0..=((max - min) / spacing).ceil() as i32 { + let secondary_pos = (((min - origin[1]) / spacing).ceil() + line_index as f64) * spacing + origin[1]; + let start = DVec2::new(primary_start, secondary_pos); + let end = DVec2::new(primary_end, secondary_pos); + + 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(&("#".to_owned() + &grid_color.rgb_hex())), + ) + } + } +} + fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { let grid_color: Color = document.snapping_state.grid.grid_color; let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); @@ -93,35 +131,6 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m } } -fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { - let origin = document.snapping_state.grid.origin; - let grid_color: Color = document.snapping_state.grid.grid_color; - let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { - return; - }; - let document_to_viewport = document.metadata().document_to_viewport; - let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]); - - let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); - let min_x = bounds.0.iter().map(|&corner| corner.x).min_by(cmp).unwrap_or_default(); - let max_x = bounds.0.iter().map(|&corner| corner.x).max_by(cmp).unwrap_or_default(); - 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(); - for line_index in 0..=((max_x - min_x) / spacing.x).ceil() as i32 { - let x_pos = (((min_x - origin.x) / spacing.x).ceil() + line_index as f64) * spacing.x + origin.x; - for line_index in 0..=((max_y - min_y) / spacing.y).ceil() as i32 { - let y_pos = (((min_y - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y; - let circle_pos = DVec2::new(x_pos, y_pos); - overlay_context.circle( - document_to_viewport.transform_point2(circle_pos), - 1., - Some(&("#".to_owned() + &grid_color.rgb_hex())), - Some(&("#".to_owned() + &grid_color.rgb_hex())), - ); - } - } -} - pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match document.snapping_state.grid.grid_type { GridType::Rectangle { spacing } => { @@ -269,14 +278,18 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { } } match grid.grid_type { - GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row { + GridType::Rectangle { .. } => widgets.push(LayoutGroup::Row { widgets: vec![ TextLabel::new("Dot display").table_align(true).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), CheckboxInput::new(grid.dot_display).on_update(update_display(grid, |grid| Some(&mut grid.dot_display))).widget_holder(), ], }), - GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {} + GridType::Isometric { + y_axis_spacing: _, + angle_a: _, + angle_b: _, + } => {} } widgets.push(LayoutGroup::Row { widgets: vec![ diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 4cb8b14590..b013d1db52 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -83,17 +83,31 @@ impl OverlayContext { self.render_context.fill(); self.render_context.stroke(); } + + pub fn pixel(&mut self, position: DVec2, color: Option<&str>) { + let size = 1.0; + let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE); + + let position = position.round() - DVec2::splat(0.5); + let corner = position - DVec2::splat(size) / 2.; + + self.render_context.begin_path(); + self.render_context.rect(corner.x, corner.y, size, size); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); + self.render_context.fill(); + } + pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { //let radius = radius.unwrap_or(DEFAULT_RADIUS); //DEFAULT_RADIUS has to be added to consts in order to use an option - let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); - let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); let position = position.round(); self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0.0, 2.0 * PI).expect("draw circle"); - self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); - self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke)); - self.render_context.fill(); - self.render_context.stroke(); + self.render_context.arc(position.x, position.y, radius, 0.0, 2.0 * PI).expect("draw circle"); + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); + self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke)); + self.render_context.fill(); + self.render_context.stroke(); } pub fn pivot(&mut self, position: DVec2) { let (x, y) = (position.round() - DVec2::splat(0.5)).into(); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 73fd101e22..5bff06e160 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,7 +1,7 @@ use crate::consts::COLOR_OVERLAY_GRAY; use glam::DVec2; use graphene_core::raster::Color; -use std::{fmt, path::Display}; +use std::fmt; #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)] From 40312d63d26d868c832e7a3e0d5fa1ee4260acd9 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 6 May 2024 22:26:22 -0700 Subject: [PATCH 7/7] Dashed line comment --- .../src/messages/portfolio/document/overlays/grid_overlays.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 53f5eccf2b..c016cfa09d 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -44,6 +44,10 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: } //TODO: Potentially create an image and render the image onto the canvas a single time +//TODO: Implement this with a dashed line (`set_line_dash`), with integer spacing which is continuously adjusted to correct the accumulated error. +// In the best case, where the x distance/total dots is an integer, this will reduce draw requests from the current m(horizontal dots)*n(vertical dots) to m(horizontal lines) * 1(line changes). +// In the worst case, where the x distance/total dots is an integer+0.5, then each pixel will require a new line, and requests will be m(horizontal lines)*n(line changes = horizontal dots) +// The draw dashed line method will also be not grid aligned for tilted grids fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { let origin = document.snapping_state.grid.origin; let grid_color: Color = document.snapping_state.grid.grid_color;