Skip to content

Commit 3d37ef7

Browse files
c-mateoKeavon
andauthored
Add shape fill overlays when closing a path (Pen tool) or filling it (Fill tool) (#2521)
* Make the Pen tool show a path being closed by drawing a filled overlay when hovering the endpoint * Add to_css to color.rs * Check before unwrapping layer * Close if in the right place * Fix typo * Format code * Support discontinuous paths for closing preview * Code review * Denser fill lines * Fill tool preview with strip lines only and revert pen shape-closing opacity * Small adjustments to fill preview * Fix line width of fill preview * Use a pattern to preview the fill tool and fix canvas clearing * Update pattern * Simplify code * Format code * Use secondary color to preview fill if shift is pressed * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent a4a0e11 commit 3d37ef7

File tree

11 files changed

+350
-17
lines changed

11 files changed

+350
-17
lines changed

editor/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ ron = ["dep:ron"]
3030
# Local dependencies
3131
graphite-proc-macros = { path = "../proc-macros" }
3232
graph-craft = { path = "../node-graph/graph-craft" }
33-
interpreted-executor = { path = "../node-graph/interpreted-executor", features = ["serde"] }
33+
interpreted-executor = { path = "../node-graph/interpreted-executor", features = [
34+
"serde",
35+
] }
3436
graphene-core = { path = "../node-graph/gcore" }
3537
graphene-std = { path = "../node-graph/gstd", features = ["serde"] }
3638

@@ -58,6 +60,9 @@ web-sys = { workspace = true, features = [
5860
"Element",
5961
"HtmlCanvasElement",
6062
"CanvasRenderingContext2d",
63+
"CanvasPattern",
64+
"OffscreenCanvas",
65+
"OffscreenCanvasRenderingContext2d",
6166
"TextMetrics",
6267
] }
6368

editor/src/messages/input_mapper/input_mappings.rs

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ pub fn input_mappings() -> Mapping {
282282
entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm),
283283
//
284284
// FillToolMessage
285+
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=FillToolMessage::PointerMove),
285286
entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor),
286287
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor),
287288
entry!(KeyUp(MouseLeft); action_dispatch=FillToolMessage::PointerUp),

editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
4747

4848
let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array();
4949
let _ = context.set_transform(a, b, c, d, e, f);
50-
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
50+
context.clear_rect(0., 0., canvas.width().into(), canvas.height().into());
5151
let _ = context.reset_transform();
5252

5353
if overlays_visible {

editor/src/messages/portfolio/document/overlays/utility_types.rs

+60-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use bezier_rs::{Bezier, Subpath};
88
use core::borrow::Borrow;
99
use core::f64::consts::{FRAC_PI_2, TAU};
1010
use glam::{DAffine2, DVec2};
11+
use graphene_core::Color;
1112
use graphene_core::renderer::Quad;
1213
use graphene_std::vector::{PointId, SegmentId, VectorData};
1314
use std::collections::HashMap;
14-
use wasm_bindgen::JsValue;
15+
use wasm_bindgen::{JsCast, JsValue};
16+
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
1517

1618
pub type OverlayProvider = fn(OverlayContext) -> Message;
1719

@@ -447,6 +449,7 @@ impl OverlayContext {
447449
self.end_dpi_aware_transform();
448450
}
449451

452+
/// Used by the Pen and Path tools to outline the path of the shape.
450453
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
451454
self.start_dpi_aware_transform();
452455

@@ -465,6 +468,7 @@ impl OverlayContext {
465468
self.end_dpi_aware_transform();
466469
}
467470

471+
/// Used by the Pen tool in order to show how the bezier curve would look like.
468472
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
469473
self.start_dpi_aware_transform();
470474

@@ -493,7 +497,7 @@ impl OverlayContext {
493497
self.end_dpi_aware_transform();
494498
}
495499

496-
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
500+
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
497501
self.start_dpi_aware_transform();
498502

499503
self.render_context.begin_path();
@@ -540,10 +544,63 @@ impl OverlayContext {
540544
}
541545
}
542546

547+
self.end_dpi_aware_transform();
548+
}
549+
550+
/// Used by the Select tool to outline a path selected or hovered.
551+
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
552+
self.push_path(subpaths, transform);
553+
543554
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
544555
self.render_context.stroke();
556+
}
545557

546-
self.end_dpi_aware_transform();
558+
/// Fills the area inside the path. Assumes `color` is in gamma space.
559+
/// Used by the Pen tool to show the path being closed.
560+
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
561+
self.push_path(subpaths, transform);
562+
563+
self.render_context.set_fill_style_str(color);
564+
self.render_context.fill();
565+
}
566+
567+
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
568+
/// Used by the fill tool to show the area to be filled.
569+
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
570+
const PATTERN_WIDTH: usize = 4;
571+
const PATTERN_HEIGHT: usize = 4;
572+
573+
let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
574+
let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas
575+
.get_context("2d")
576+
.ok()
577+
.flatten()
578+
.expect("Failed to get canvas context")
579+
.dyn_into()
580+
.expect("Context should be a canvas 2d context");
581+
582+
// 4x4 pixels, 4 components (RGBA) per pixel
583+
let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT];
584+
585+
// ┌▄▄┬──┬──┬──┐
586+
// ├▀▀┼──┼──┼──┤
587+
// ├──┼──┼▄▄┼──┤
588+
// ├──┼──┼▀▀┼──┤
589+
// └──┴──┴──┴──┘
590+
let pixels = [(0, 0), (2, 2)];
591+
for &(x, y) in &pixels {
592+
let index = (x + y * PATTERN_WIDTH as usize) * 4;
593+
data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb());
594+
}
595+
596+
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
597+
pattern_context.put_image_data(&image_data, 0., 0.).unwrap();
598+
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
599+
600+
self.push_path(subpaths, transform);
601+
602+
self.render_context.set_fill_style_canvas_pattern(&pattern);
603+
self.render_context.fill();
547604
}
548605

549606
pub fn get_width(&self, text: &str) -> f64 {

editor/src/messages/tool/tool_message_handler.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
218218
let document_data = &mut self.tool_state.document_tool_data;
219219
document_data.primary_color = color;
220220

221-
self.tool_state.document_tool_data.update_working_colors(responses); // TODO: Make this an event
221+
document_data.update_working_colors(responses); // TODO: Make this an event
222222
}
223223
ToolMessage::SelectRandomPrimaryColor => {
224224
// Select a random primary color (rgba) based on an UUID

editor/src/messages/tool/tool_messages/fill_tool.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
use super::tool_prelude::*;
2+
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
23
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
34
use graphene_core::vector::style::Fill;
5+
46
#[derive(Default)]
57
pub struct FillTool {
68
fsm_state: FillToolFsmState,
79
}
810

911
#[impl_message(Message, ToolMessage, Fill)]
10-
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
12+
#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
1113
pub enum FillToolMessage {
1214
// Standard messages
1315
Abort,
16+
WorkingColorChanged,
17+
Overlays(OverlayContext),
1418

1519
// Tool-specific messages
20+
PointerMove,
1621
PointerUp,
1722
FillPrimaryColor,
1823
FillSecondaryColor,
@@ -45,8 +50,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for FillToo
4550
FillToolFsmState::Ready => actions!(FillToolMessageDiscriminant;
4651
FillPrimaryColor,
4752
FillSecondaryColor,
53+
PointerMove,
4854
),
4955
FillToolFsmState::Filling => actions!(FillToolMessageDiscriminant;
56+
PointerMove,
5057
PointerUp,
5158
Abort,
5259
),
@@ -58,6 +65,8 @@ impl ToolTransition for FillTool {
5865
fn event_to_message_map(&self) -> EventToMessageMap {
5966
EventToMessageMap {
6067
tool_abort: Some(FillToolMessage::Abort.into()),
68+
working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()),
69+
overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()),
6170
..Default::default()
6271
}
6372
}
@@ -82,6 +91,23 @@ impl Fsm for FillToolFsmState {
8291

8392
let ToolMessage::Fill(event) = event else { return self };
8493
match (self, event) {
94+
(_, FillToolMessage::Overlays(mut overlay_context)) => {
95+
// Choose the working color to preview
96+
let use_secondary = input.keyboard.get(Key::Shift as usize);
97+
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };
98+
99+
// Get the layer the user is hovering over
100+
if let Some(layer) = document.click(input) {
101+
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color);
102+
}
103+
104+
self
105+
}
106+
(_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => {
107+
// Generate the hover outline
108+
responses.add(OverlaysMessage::Draw);
109+
self
110+
}
85111
(FillToolFsmState::Ready, color_event) => {
86112
let Some(layer_identifier) = document.click(input) else {
87113
return self;

editor/src/messages/tool/tool_messages/path_tool.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ impl Fsm for PathToolFsmState {
10301030

10311031
match self {
10321032
Self::Drawing { selection_shape } => {
1033-
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
1033+
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
10341034
.unwrap()
10351035
.with_alpha(0.05)
10361036
.to_rgba_hex_srgb();

editor/src/messages/tool/tool_messages/pen_tool.rs

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::tool_prelude::*;
2-
use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
2+
use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
33
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
44
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
55
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
@@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles};
1515
use graph_craft::document::NodeId;
1616
use graphene_core::Color;
1717
use graphene_core::vector::{PointId, VectorModificationType};
18-
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, VectorData};
18+
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData};
1919

2020
#[derive(Default)]
2121
pub struct PenTool {
@@ -1614,6 +1614,54 @@ impl Fsm for PenToolFsmState {
16141614
overlay_context.manipulator_anchor(next_anchor, false, None);
16151615
}
16161616

1617+
// Display a filled overlay of the shape if the new point closes the path
1618+
if let Some(latest_point) = tool_data.latest_point() {
1619+
let handle_start = latest_point.handle_start;
1620+
let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start);
1621+
let next_point = tool_data.next_point;
1622+
let start = latest_point.id;
1623+
1624+
if let Some(layer) = layer {
1625+
let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
1626+
1627+
let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| {
1628+
vector_data.point_domain.position_from_id(id).map_or(false, |pos| {
1629+
let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
1630+
dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2)
1631+
})
1632+
});
1633+
1634+
// We have the point. Join the 2 vertices and check if any path is closed.
1635+
if let Some(end) = closest_point {
1636+
let segment_id = SegmentId::generate();
1637+
vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO);
1638+
1639+
let grouped_segments = vector_data.auto_join_paths();
1640+
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id));
1641+
1642+
let subpaths: Vec<_> = closed_paths
1643+
.filter_map(|path| {
1644+
let segments = path.edges.iter().filter_map(|edge| {
1645+
vector_data
1646+
.segment_domain
1647+
.iter()
1648+
.find(|(id, _, _, _)| id == &edge.id)
1649+
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) })
1650+
});
1651+
vector_data.subpath_from_segments_ignore_discontinuities(segments)
1652+
})
1653+
.collect();
1654+
1655+
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
1656+
.unwrap()
1657+
.with_alpha(0.05)
1658+
.to_rgba_hex_srgb();
1659+
fill_color.insert(0, '#');
1660+
overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str());
1661+
}
1662+
}
1663+
}
1664+
16171665
// Draw the overlays that visualize current snapping
16181666
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
16191667

editor/src/messages/tool/tool_messages/select_tool.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
use super::tool_prelude::*;
44
use crate::consts::{
5-
COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE,
6-
SELECTION_TOLERANCE,
5+
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT,
6+
SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE,
77
};
88
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
99
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
@@ -759,7 +759,7 @@ impl Fsm for SelectToolFsmState {
759759
}
760760

761761
// Update the selection box
762-
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
762+
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
763763
.unwrap()
764764
.with_alpha(0.05)
765765
.to_rgba_hex_srgb();

editor/src/messages/tool/tool_messages/text_tool.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(clippy::too_many_arguments)]
22

33
use super::tool_prelude::*;
4-
use crate::consts::{COLOR_OVERLAY_RED, DRAG_THRESHOLD};
4+
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_RED, DRAG_THRESHOLD};
55
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
66
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
77
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -456,7 +456,7 @@ impl Fsm for TextToolFsmState {
456456
font_cache,
457457
..
458458
} = transition_data;
459-
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
459+
let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
460460
.unwrap()
461461
.with_alpha(0.05)
462462
.to_rgba_hex_srgb();

0 commit comments

Comments
 (0)