mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 12:18:11 +08:00
Add grid color customization and choice to display as dots (#1743)
* dot grid * fix warning: unreachable pattern * grid color select * add color for all grid types * Dot grid checkbox and remove prefixed Color functions * Display dot grid as grid aligned pixels * Dashed line comment * Code review and UI design widget placement updates * Isometric dotted grid * Early return when cos = 0 * Add spacing, x offset, and color to dot grids --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
1bfbe306be
commit
bc33eabc3c
Generated
+1
@@ -2376,6 +2376,7 @@ dependencies = [
|
|||||||
"graphite-proc-macros",
|
"graphite-proc-macros",
|
||||||
"image 0.24.9",
|
"image 0.24.9",
|
||||||
"interpreted-executor",
|
"interpreted-executor",
|
||||||
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"num_enum 0.6.1",
|
"num_enum 0.6.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ quantization = [
|
|||||||
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
|
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
js-sys = "0.3.67"
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use crate::consts::COLOR_OVERLAY_GRAY;
|
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
|
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::DVec2;
|
|
||||||
|
use graphene_core::raster::color::Color;
|
||||||
use graphene_core::renderer::Quad;
|
use graphene_core::renderer::Quad;
|
||||||
|
|
||||||
|
use glam::DVec2;
|
||||||
|
|
||||||
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||||
let origin = document.snapping_state.grid.origin;
|
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 {
|
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -33,12 +36,60 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
|
|||||||
} else {
|
} else {
|
||||||
DVec2::new(secondary_pos, primary_end)
|
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_string() + &grid_color.rgba_hex())),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// 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.
|
||||||
|
fn grid_overlay_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||||
|
let origin = document.snapping_state.grid.origin;
|
||||||
|
let grid_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.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();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Round to avoid floating point errors
|
||||||
|
let total_dots = ((primary_end - primary_start) / spacing.x).round();
|
||||||
|
|
||||||
|
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 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_string() + &grid_color.rgba_hex())),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) {
|
fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) {
|
||||||
|
let grid_color = document.snapping_state.grid.grid_color;
|
||||||
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
|
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
|
||||||
let origin = document.snapping_state.grid.origin;
|
let origin = document.snapping_state.grid.origin;
|
||||||
let document_to_viewport = document.metadata().document_to_viewport;
|
let document_to_viewport = document.metadata().document_to_viewport;
|
||||||
@@ -60,7 +111,12 @@ 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 x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;
|
||||||
let start = DVec2::new(x_pos, min_y);
|
let start = DVec2::new(x_pos, min_y);
|
||||||
let end = DVec2::new(x_pos, max_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_string() + &grid_color.rgba_hex())),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] {
|
for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] {
|
||||||
@@ -74,15 +130,78 @@ 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 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 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)));
|
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_string() + &grid_color.rgba_hex())),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) {
|
||||||
|
let grid_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;
|
||||||
|
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
|
||||||
|
let tan_a = angle_a.to_radians().tan();
|
||||||
|
let tan_b = angle_b.to_radians().tan();
|
||||||
|
let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing);
|
||||||
|
let Some(spacing_multiplier) = GridSnapping::compute_isometric_multiplier(y_axis_spacing, tan_a + tan_b, &document.navigation) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let isometric_spacing = spacing * spacing_multiplier;
|
||||||
|
|
||||||
|
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 spacing_x = isometric_spacing.x;
|
||||||
|
let tan = tan_a;
|
||||||
|
let multiply = -1.0;
|
||||||
|
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_y = isometric_spacing.y;
|
||||||
|
let lines = ((inverse_project(&max_y) - inverse_project(&min_y)) / spacing_y).ceil() as i32;
|
||||||
|
|
||||||
|
let cos_a = angle_a.to_radians().cos();
|
||||||
|
// If cos_a is 0 then there will be no intersections and thus no dots should be drawn
|
||||||
|
if cos_a.abs() <= 0.00001 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let x_offset = (((min_x - origin.x) / spacing_x).ceil()) * spacing_x + origin.x - min_x;
|
||||||
|
for line_index in 0..=lines {
|
||||||
|
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)));
|
||||||
|
|
||||||
|
overlay_context.line(
|
||||||
|
document_to_viewport.transform_point2(start),
|
||||||
|
document_to_viewport.transform_point2(end),
|
||||||
|
Some(&("#".to_string() + &grid_color.rgba_hex())),
|
||||||
|
Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||||
match document.snapping_state.grid.grid_type {
|
match document.snapping_state.grid.grid_type {
|
||||||
GridType::Rectangle { spacing } => grid_overlay_rectangular(document, overlay_context, spacing),
|
GridType::Rectangle { spacing } => {
|
||||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b),
|
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 } => {
|
||||||
|
if document.snapping_state.grid.dot_display {
|
||||||
|
grid_overlay_isometric_dot(document, overlay_context, y_axis_spacing, angle_a, angle_b)
|
||||||
|
} else {
|
||||||
|
grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,9 +224,65 @@ 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, color| {
|
||||||
|
if let Some(color) = color.value {
|
||||||
|
if let Some(update_color) = update(grid) {
|
||||||
|
*update_color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let update_display = |grid, update: fn(&mut GridSnapping) -> Option<&mut bool>| {
|
||||||
|
update_val::<CheckboxInput>(grid, move |grid, checkbox| {
|
||||||
|
if let Some(update) = update(grid) {
|
||||||
|
*update = checkbox.checked;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
widgets.push(LayoutGroup::Row {
|
widgets.push(LayoutGroup::Row {
|
||||||
widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()],
|
widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
widgets.push(LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
TextLabel::new("Type").table_align(true).widget_holder(),
|
||||||
|
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||||
|
RadioInput::new(vec![
|
||||||
|
RadioEntryData::new("rectangular")
|
||||||
|
.label("Rectangular")
|
||||||
|
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)),
|
||||||
|
RadioEntryData::new("isometric")
|
||||||
|
.label("Isometric")
|
||||||
|
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)),
|
||||||
|
])
|
||||||
|
.min_width(200)
|
||||||
|
.selected_index(Some(match grid.grid_type {
|
||||||
|
GridType::Rectangle { .. } => 0,
|
||||||
|
GridType::Isometric { .. } => 1,
|
||||||
|
}))
|
||||||
|
.widget_holder(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
|
ColorButton::new(Some(grid.grid_color))
|
||||||
|
.tooltip("Grid display color")
|
||||||
|
.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.push(LayoutGroup::Row {
|
||||||
widgets: vec![
|
widgets: vec![
|
||||||
TextLabel::new("Origin").table_align(true).widget_holder(),
|
TextLabel::new("Origin").table_align(true).widget_holder(),
|
||||||
@@ -127,23 +302,6 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
widgets.push(LayoutGroup::Row {
|
|
||||||
widgets: vec![
|
|
||||||
TextLabel::new("Type").table_align(true).widget_holder(),
|
|
||||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
|
||||||
RadioInput::new(vec![
|
|
||||||
RadioEntryData::new("rectangular")
|
|
||||||
.label("Rectangular")
|
|
||||||
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)),
|
|
||||||
RadioEntryData::new("isometric")
|
|
||||||
.label("Isometric")
|
|
||||||
.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 }))
|
|
||||||
.widget_holder(),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
match grid.grid_type {
|
match grid.grid_type {
|
||||||
GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row {
|
GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row {
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape
|
|||||||
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||||
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
|
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
|
||||||
let handle_position = transform.transform_point2(in_handle);
|
let handle_position = transform.transform_point2(in_handle);
|
||||||
overlay_context.line(handle_position, anchor_position, None);
|
overlay_context.line(handle_position, anchor_position, None, None);
|
||||||
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
|
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
|
||||||
}
|
}
|
||||||
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
|
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
|
||||||
let handle_position = transform.transform_point2(out_handle);
|
let handle_position = transform.transform_point2(out_handle);
|
||||||
overlay_context.line(handle_position, anchor_position, None);
|
overlay_context.line(handle_position, anchor_position, None, None);
|
||||||
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
|
overlay_context.manipulator_handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use crate::messages::prelude::Message;
|
|||||||
|
|
||||||
use bezier_rs::Subpath;
|
use bezier_rs::Subpath;
|
||||||
use graphene_core::renderer::Quad;
|
use graphene_core::renderer::Quad;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
use core::f64::consts::PI;
|
use core::f64::consts::TAU;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
pub type OverlayProvider = fn(OverlayContext) -> Message;
|
pub type OverlayProvider = fn(OverlayContext) -> Message;
|
||||||
@@ -38,10 +39,24 @@ impl OverlayContext {
|
|||||||
self.render_context.stroke();
|
self.render_context.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
|
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>) {
|
||||||
let start = start.round() - DVec2::splat(0.5);
|
let start = start.round() - DVec2::splat(0.5);
|
||||||
let end = end.round() - DVec2::splat(0.5);
|
let end = end.round() - DVec2::splat(0.5);
|
||||||
|
if let Some(dash_width) = dash_width {
|
||||||
|
let array = js_sys::Array::new();
|
||||||
|
array.push(&JsValue::from(1));
|
||||||
|
array.push(&JsValue::from(dash_width - 1.));
|
||||||
|
self.render_context
|
||||||
|
.set_line_dash(&JsValue::from(array))
|
||||||
|
.map_err(|error| log::debug!("Error drawing dashed line: {:?}", error))
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
let array = js_sys::Array::new();
|
||||||
|
self.render_context
|
||||||
|
.set_line_dash(&JsValue::from(array))
|
||||||
|
.map_err(|error| log::debug!("Error drawing dashed line: {:?}", error))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
self.render_context.begin_path();
|
self.render_context.begin_path();
|
||||||
self.render_context.move_to(start.x, start.y);
|
self.render_context.move_to(start.x, start.y);
|
||||||
self.render_context.line_to(end.x, end.y);
|
self.render_context.line_to(end.x, end.y);
|
||||||
@@ -53,7 +68,7 @@ impl OverlayContext {
|
|||||||
let position = position.round() - DVec2::splat(0.5);
|
let position = position.round() - DVec2::splat(0.5);
|
||||||
|
|
||||||
self.render_context.begin_path();
|
self.render_context.begin_path();
|
||||||
self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., PI * 2.).expect("draw circle");
|
self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU).expect("draw circle");
|
||||||
|
|
||||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||||
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
|
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
|
||||||
@@ -84,13 +99,37 @@ impl OverlayContext {
|
|||||||
self.render_context.stroke();
|
self.render_context.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pixel(&mut self, position: DVec2, color: Option<&str>) {
|
||||||
|
let size = 1.;
|
||||||
|
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 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., TAU).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) {
|
pub fn pivot(&mut self, position: DVec2) {
|
||||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||||
|
|
||||||
// Circle
|
// Circle
|
||||||
|
|
||||||
self.render_context.begin_path();
|
self.render_context.begin_path();
|
||||||
self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., PI * 2.).expect("draw circle");
|
self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., TAU).expect("draw circle");
|
||||||
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW));
|
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW));
|
||||||
self.render_context.fill();
|
self.render_context.fill();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use glam::DVec2;
|
use crate::consts::COLOR_OVERLAY_GRAY;
|
||||||
|
|
||||||
|
use graphene_core::raster::Color;
|
||||||
|
|
||||||
|
use glam::DVec2;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
@@ -86,6 +89,11 @@ impl Default for SnappingState {
|
|||||||
grid: GridSnapping {
|
grid: GridSnapping {
|
||||||
origin: DVec2::ZERO,
|
origin: DVec2::ZERO,
|
||||||
grid_type: GridType::RECTANGLE,
|
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.,
|
tolerance: 8.,
|
||||||
artboards: true,
|
artboards: true,
|
||||||
@@ -192,6 +200,8 @@ impl GridType {
|
|||||||
pub struct GridSnapping {
|
pub struct GridSnapping {
|
||||||
pub origin: DVec2,
|
pub origin: DVec2,
|
||||||
pub grid_type: GridType,
|
pub grid_type: GridType,
|
||||||
|
pub grid_color: Color,
|
||||||
|
pub dot_display: bool,
|
||||||
}
|
}
|
||||||
impl GridSnapping {
|
impl GridSnapping {
|
||||||
// Double grid size until it takes up at least 10px.
|
// Double grid size until it takes up at least 10px.
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let Gradient { start, end, positions, .. } = gradient;
|
let Gradient { start, end, positions, .. } = gradient;
|
||||||
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
|
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
|
||||||
|
|
||||||
overlay_context.line(start, end, None);
|
overlay_context.line(start, end, None, None);
|
||||||
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
|
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start));
|
||||||
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
|
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<rect width="2" height="2" x="1" y="1" />
|
||||||
|
<rect width="2" height="2" x="5" y="1" />
|
||||||
|
<rect width="2" height="2" x="9" y="1" />
|
||||||
|
<rect width="2" height="2" x="1" y="5" />
|
||||||
|
<rect width="2" height="2" x="5" y="5" />
|
||||||
|
<rect width="2" height="2" x="9" y="5" />
|
||||||
|
<rect width="2" height="2" x="1" y="9" />
|
||||||
|
<rect width="2" height="2" x="5" y="9" />
|
||||||
|
<rect width="2" height="2" x="9" y="9" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 455 B |
@@ -18,6 +18,7 @@ import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg"
|
|||||||
import Failure from "@graphite-frontend/assets/icon-12px-solid/failure.svg";
|
import Failure from "@graphite-frontend/assets/icon-12px-solid/failure.svg";
|
||||||
import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
|
import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
|
||||||
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
|
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
|
||||||
|
import GridDotted from "@graphite-frontend/assets/icon-12px-solid/grid-dotted.svg";
|
||||||
import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.svg";
|
import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.svg";
|
||||||
import Info from "@graphite-frontend/assets/icon-12px-solid/info.svg";
|
import Info from "@graphite-frontend/assets/icon-12px-solid/info.svg";
|
||||||
import KeyboardArrowDown from "@graphite-frontend/assets/icon-12px-solid/keyboard-arrow-down.svg";
|
import KeyboardArrowDown from "@graphite-frontend/assets/icon-12px-solid/keyboard-arrow-down.svg";
|
||||||
@@ -58,6 +59,7 @@ const SOLID_12PX = {
|
|||||||
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
|
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
|
||||||
FullscreenExit: { svg: FullscreenExit, size: 12 },
|
FullscreenExit: { svg: FullscreenExit, size: 12 },
|
||||||
Grid: { svg: Grid, size: 12 },
|
Grid: { svg: Grid, size: 12 },
|
||||||
|
GridDotted: { svg: GridDotted, size: 12 },
|
||||||
Info: { svg: Info, size: 12 },
|
Info: { svg: Info, size: 12 },
|
||||||
KeyboardArrowDown: { svg: KeyboardArrowDown, size: 12 },
|
KeyboardArrowDown: { svg: KeyboardArrowDown, size: 12 },
|
||||||
KeyboardArrowLeft: { svg: KeyboardArrowLeft, size: 12 },
|
KeyboardArrowLeft: { svg: KeyboardArrowLeft, size: 12 },
|
||||||
|
|||||||
Reference in New Issue
Block a user