Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dot grid #1709

Merged
merged 7 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 109 additions & 6 deletions editor/src/messages/portfolio/document/overlays/grid_overlays.rs
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -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;
Expand All @@ -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.)] {
Expand All @@ -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),
}
}
Expand All @@ -105,6 +167,23 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
}
})
};
let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| {
update_val::<ColorButton>(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::<CheckboxInput>(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()],
});
Expand Down Expand Up @@ -140,7 +219,10 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
.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(),
],
});
Expand Down Expand Up @@ -199,6 +281,27 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
});
}
}
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
}
25 changes: 25 additions & 0 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
10 changes: 9 additions & 1 deletion editor/src/messages/portfolio/document/utility_types/misc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::consts::COLOR_OVERLAY_GRAY;
use glam::DVec2;

use graphene_core::raster::Color;
use std::fmt;

#[repr(transparent)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down