diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index cc6789a2aa..c016cfa09d 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -1,13 +1,14 @@ -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}; use crate::messages::prelude::*; use glam::DVec2; +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; + let grid_color: Color = document.snapping_state.grid.grid_color; let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else { return; }; @@ -33,12 +34,59 @@ 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(&("#".to_owned() + &grid_color.rgb_hex())), + ); + } + } +} + +//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; + 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(); let origin = document.snapping_state.grid.origin; let document_to_viewport = document.metadata().document_to_viewport; @@ -60,7 +108,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(COLOR_OVERLAY_GRAY)); + 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.)] { @@ -74,14 +126,24 @@ 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(&("#".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), } } @@ -105,6 +167,23 @@ 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; + } + } + }) + }; + 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()], }); @@ -140,7 +219,10 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { .on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)), ]) .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, + })) .widget_holder(), ], }); @@ -199,6 +281,27 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { }); } } + match grid.grid_type { + 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: _, + } => {} + } + 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/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index d8c2a98e29..b013d1db52 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -84,6 +84,31 @@ impl OverlayContext { 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 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..5bff06e160 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -1,5 +1,6 @@ +use crate::consts::COLOR_OVERLAY_GRAY; use glam::DVec2; - +use graphene_core::raster::Color; use std::fmt; #[repr(transparent)] @@ -86,6 +87,11 @@ impl Default for SnappingState { grid: GridSnapping { origin: DVec2::ZERO, grid_type: GridType::RECTANGLE, + 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, @@ -192,6 +198,8 @@ impl GridType { 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.