Skip to content

Add shape fill overlays when closing a path (Pen tool) or filling it (Fill tool) #2521

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

Merged
merged 28 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
24ddd88
Make the Pen tool show a path being closed by drawing a filled overla…
c-mateo Apr 6, 2025
6e39bc2
Add to_css to color.rs
c-mateo Apr 6, 2025
affa69f
Check before unwrapping layer
c-mateo Apr 6, 2025
3a7606c
Close if in the right place
c-mateo Apr 6, 2025
54dae40
Fix typo
c-mateo Apr 6, 2025
a47cb6e
Format code
c-mateo Apr 6, 2025
96c65b4
Merge branch 'master' into 2390-show-path-being-closed
c-mateo Apr 6, 2025
198204f
Merge branch 'master' into 2390-show-path-being-closed
c-mateo Apr 9, 2025
5a801e9
Support discontinuous paths for closing preview
c-mateo Apr 9, 2025
ef59e1c
Merge branch 'master' into 2390-show-path-being-closed
Keavon Apr 14, 2025
e7066ec
Code review
Keavon Apr 14, 2025
1824450
Denser fill lines
c-mateo Apr 14, 2025
300cb21
Merge branch 'master' into 2390-show-path-being-closed
Keavon Apr 15, 2025
1dc0625
Fill tool preview with strip lines only and revert pen shape-closing …
c-mateo Apr 15, 2025
8317315
Small adjustments to fill preview
c-mateo Apr 16, 2025
00866a6
Merge branch 'master' into 2390-show-path-being-closed
Keavon Apr 16, 2025
2873bb8
Fix line width of fill preview
c-mateo Apr 17, 2025
b950441
Merge branch 'master' into 2390-show-path-being-closed
Keavon Apr 17, 2025
36c4635
Merge remote-tracking branch 'origin/master' into 2390-show-path-bein…
c-mateo Apr 18, 2025
51b5f6c
Use a pattern to preview the fill tool and fix canvas clearing
c-mateo Apr 18, 2025
7354586
Merge branch 'master' into 2390-show-path-being-closed
Keavon Apr 21, 2025
804acfd
Update pattern
c-mateo Apr 21, 2025
c56415e
Simplify code
c-mateo Apr 22, 2025
ca598cd
Format code
c-mateo Apr 22, 2025
2e79302
Merge branch 'master' into 2390-show-path-being-closed
Keavon Apr 23, 2025
b4eb2b2
Use secondary color to preview fill if shift is pressed
c-mateo Apr 23, 2025
3e7be59
Merge branch '2390-show-path-being-closed' of https://github.com/c-ma…
c-mateo Apr 23, 2025
d474312
Code review
Keavon Apr 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ ron = ["dep:ron"]
# Local dependencies
graphite-proc-macros = { path = "../proc-macros" }
graph-craft = { path = "../node-graph/graph-craft" }
interpreted-executor = { path = "../node-graph/interpreted-executor", features = ["serde"] }
interpreted-executor = { path = "../node-graph/interpreted-executor", features = [
"serde",
] }
graphene-core = { path = "../node-graph/gcore" }
graphene-std = { path = "../node-graph/gstd", features = ["serde"] }

Expand Down Expand Up @@ -58,6 +60,9 @@ web-sys = { workspace = true, features = [
"Element",
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"CanvasPattern",
"OffscreenCanvas",
"OffscreenCanvasRenderingContext2d",
"TextMetrics",
] }

Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm),
//
// FillToolMessage
entry!(PointerMove; refresh_keys=[Shift], action_dispatch=FillToolMessage::PointerMove),
entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor),
entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor),
entry!(KeyUp(MouseLeft); action_dispatch=FillToolMessage::PointerUp),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag

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

if overlays_visible {
Expand Down
63 changes: 60 additions & 3 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ use bezier_rs::{Bezier, Subpath};
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, TAU};
use glam::{DAffine2, DVec2};
use graphene_core::Color;
use graphene_core::renderer::Quad;
use graphene_std::vector::{PointId, SegmentId, VectorData};
use std::collections::HashMap;
use wasm_bindgen::JsValue;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};

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

Expand Down Expand Up @@ -447,6 +449,7 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}

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

Expand All @@ -465,6 +468,7 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}

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

Expand Down Expand Up @@ -493,7 +497,7 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}

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

self.render_context.begin_path();
Expand Down Expand Up @@ -540,10 +544,63 @@ impl OverlayContext {
}
}

self.end_dpi_aware_transform();
}

/// Used by the Select tool to outline a path selected or hovered.
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
self.push_path(subpaths, transform);

self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.stroke();
}

self.end_dpi_aware_transform();
/// Fills the area inside the path. Assumes `color` is in gamma space.
/// Used by the Pen tool to show the path being closed.
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
self.push_path(subpaths, transform);

self.render_context.set_fill_style_str(color);
self.render_context.fill();
}

/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
/// Used by the fill tool to show the area to be filled.
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
const PATTERN_WIDTH: usize = 4;
const PATTERN_HEIGHT: usize = 4;

let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas
.get_context("2d")
.ok()
.flatten()
.expect("Failed to get canvas context")
.dyn_into()
.expect("Context should be a canvas 2d context");

// 4x4 pixels, 4 components (RGBA) per pixel
let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT];

// ┌▄▄┬──┬──┬──┐
// ├▀▀┼──┼──┼──┤
// ├──┼──┼▄▄┼──┤
// ├──┼──┼▀▀┼──┤
// └──┴──┴──┴──┘
let pixels = [(0, 0), (2, 2)];
for &(x, y) in &pixels {
let index = (x + y * PATTERN_WIDTH as usize) * 4;
data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb());
}

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();
pattern_context.put_image_data(&image_data, 0., 0.).unwrap();
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();

self.push_path(subpaths, transform);

self.render_context.set_fill_style_canvas_pattern(&pattern);
self.render_context.fill();
}

pub fn get_width(&self, text: &str) -> f64 {
Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/tool/tool_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
let document_data = &mut self.tool_state.document_tool_data;
document_data.primary_color = color;

self.tool_state.document_tool_data.update_working_colors(responses); // TODO: Make this an event
document_data.update_working_colors(responses); // TODO: Make this an event
}
ToolMessage::SelectRandomPrimaryColor => {
// Select a random primary color (rgba) based on an UUID
Expand Down
28 changes: 27 additions & 1 deletion editor/src/messages/tool/tool_messages/fill_tool.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use graphene_core::vector::style::Fill;

#[derive(Default)]
pub struct FillTool {
fsm_state: FillToolFsmState,
}

#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum FillToolMessage {
// Standard messages
Abort,
WorkingColorChanged,
Overlays(OverlayContext),

// Tool-specific messages
PointerMove,
PointerUp,
FillPrimaryColor,
FillSecondaryColor,
Expand Down Expand Up @@ -45,8 +50,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for FillToo
FillToolFsmState::Ready => actions!(FillToolMessageDiscriminant;
FillPrimaryColor,
FillSecondaryColor,
PointerMove,
),
FillToolFsmState::Filling => actions!(FillToolMessageDiscriminant;
PointerMove,
PointerUp,
Abort,
),
Expand All @@ -58,6 +65,8 @@ impl ToolTransition for FillTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
tool_abort: Some(FillToolMessage::Abort.into()),
working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()),
overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()),
..Default::default()
}
}
Expand All @@ -82,6 +91,23 @@ impl Fsm for FillToolFsmState {

let ToolMessage::Fill(event) = event else { return self };
match (self, event) {
(_, FillToolMessage::Overlays(mut overlay_context)) => {
// Choose the working color to preview
let use_secondary = input.keyboard.get(Key::Shift as usize);
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };

// Get the layer the user is hovering over
if let Some(layer) = document.click(input) {
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color);
}

self
}
(_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => {
// Generate the hover outline
responses.add(OverlaysMessage::Draw);
self
}
(FillToolFsmState::Ready, color_event) => {
let Some(layer_identifier) = document.click(input) else {
return self;
Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ impl Fsm for PathToolFsmState {

match self {
Self::Drawing { selection_shape } => {
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.05)
.to_rgba_hex_srgb();
Expand Down
52 changes: 50 additions & 2 deletions editor/src/messages/tool/tool_messages/pen_tool.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::tool_prelude::*;
use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
Expand All @@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId;
use graphene_core::Color;
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, VectorData};
use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData};

#[derive(Default)]
pub struct PenTool {
Expand Down Expand Up @@ -1614,6 +1614,54 @@ impl Fsm for PenToolFsmState {
overlay_context.manipulator_anchor(next_anchor, false, None);
}

// Display a filled overlay of the shape if the new point closes the path
if let Some(latest_point) = tool_data.latest_point() {
let handle_start = latest_point.handle_start;
let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start);
let next_point = tool_data.next_point;
let start = latest_point.id;

if let Some(layer) = layer {
let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap();

let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| {
vector_data.point_domain.position_from_id(id).map_or(false, |pos| {
let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2)
})
});

// We have the point. Join the 2 vertices and check if any path is closed.
if let Some(end) = closest_point {
let segment_id = SegmentId::generate();
vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO);

let grouped_segments = vector_data.auto_join_paths();
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id));

let subpaths: Vec<_> = closed_paths
.filter_map(|path| {
let segments = path.edges.iter().filter_map(|edge| {
vector_data
.segment_domain
.iter()
.find(|(id, _, _, _)| id == &edge.id)
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) })
});
vector_data.subpath_from_segments_ignore_discontinuities(segments)
})
.collect();

let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.05)
.to_rgba_hex_srgb();
fill_color.insert(0, '#');
overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str());
}
}
}

// Draw the overlays that visualize current snapping
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);

Expand Down
6 changes: 3 additions & 3 deletions editor/src/messages/tool/tool_messages/select_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use super::tool_prelude::*;
use crate::consts::{
COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE,
SELECTION_TOLERANCE,
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT,
SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE,
};
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
Expand Down Expand Up @@ -759,7 +759,7 @@ impl Fsm for SelectToolFsmState {
}

// Update the selection box
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.05)
.to_rgba_hex_srgb();
Expand Down
4 changes: 2 additions & 2 deletions editor/src/messages/tool/tool_messages/text_tool.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![allow(clippy::too_many_arguments)]

use super::tool_prelude::*;
use crate::consts::{COLOR_OVERLAY_RED, DRAG_THRESHOLD};
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_RED, DRAG_THRESHOLD};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
Expand Down Expand Up @@ -456,7 +456,7 @@ impl Fsm for TextToolFsmState {
font_cache,
..
} = transition_data;
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.05)
.to_rgba_hex_srgb();
Expand Down
Loading
Loading