Skip to content

Add fill tool support on strokes #2664

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

Draft
wants to merge 34 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2c3d8ac
Add initial support for fill tool on strokes
seam0s-dev May 22, 2025
b3a0be3
Merge branch 'GraphiteEditor:master' into 2615-fill-tool-on-strokes
seam0s-dev May 22, 2025
6046565
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev May 25, 2025
9073095
Add stroke detection on click
seam0s-dev May 26, 2025
5355e06
Fix Rust formatting
seam0s-dev May 26, 2025
e91a305
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev May 26, 2025
42ca0b0
Update transformation on subpath from document to viewport space
seam0s-dev May 27, 2025
433bdb6
Minor fix
seam0s-dev May 27, 2025
48b192c
Add stroke detection support for fill tool overlays
seam0s-dev May 28, 2025
5b8f59b
Refactor stroke detection logic
seam0s-dev May 28, 2025
602122e
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev May 28, 2025
1a64e29
Refactor push_path and add scaling to stroke overlay
seam0s-dev Jun 1, 2025
ddf2526
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev Jun 1, 2025
5ce0177
Refine scaling on stroke overlay
seam0s-dev Jun 2, 2025
33849ff
Refactor stroke and fill canvas pattern and fix stroke overlays on op…
seam0s-dev Jun 2, 2025
c196aa1
Prevent the dotted overlays from extending into the stroke area
c-mateo Jun 4, 2025
b190c1e
Fix crash when closing path and display filled overlay of shape first
c-mateo Jun 4, 2025
b3fc8fd
Add support to sync Stroke node inputs with stroke fill overlays
seam0s-dev Jun 5, 2025
f05ff1b
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev Jun 5, 2025
4e193b9
Add option in Overlays popover to switch fillable indicator state
seam0s-dev Jun 6, 2025
4e64f82
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev Jun 7, 2025
cb6fa2c
Refactor fill tool overlays to use VectorData to obtain subpaths
seam0s-dev Jun 8, 2025
7f70952
Add logic to check for the presence of Stroke node
seam0s-dev Jun 8, 2025
3716236
Refine stroke detection threshold
seam0s-dev Jun 8, 2025
cc54f0f
Add serde default to OverlaysVisibilitySettings
seam0s-dev Jun 8, 2025
255f5e8
Revert changes on stroke detection threshold
seam0s-dev Jun 8, 2025
037a1b1
Merge branch 'master' into 2615-fill-tool-on-strokes
Keavon Jun 9, 2025
1df00b2
Use better approach to fill strokes
c-mateo Jun 9, 2025
5e80c66
Merge branch 'master' into 2615-fill-tool-on-strokes
seam0s-dev Jun 9, 2025
de70263
Limit the stroke distance to half width
c-mateo Jun 9, 2025
0e535b1
Temporary fix to adjust stroke overlays on shapes with multiple upstr…
seam0s-dev Jun 11, 2025
a6b0f52
Replace transform_scale for PTZ zoom and add checks for node visibili…
seam0s-dev Jun 15, 2025
6a4a7c6
Merge remote-tracking branch 'origin/master' into 2615-fill-tool-on-s…
seam0s-dev Jun 15, 2025
d8e5691
Send fill_stroke subpaths instead of vector_data
seam0s-dev Jun 15, 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
Original file line number Diff line number Diff line change
Expand Up @@ -1202,8 +1202,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
OverlaysType::Handles => visibility_settings.handles = visible,
OverlaysType::FillableIndicator => visibility_settings.fillable_indicator = visible,
}

responses.add(BroadcastEvent::ToolAbort);
responses.add(OverlaysMessage::Draw);
}
Expand Down Expand Up @@ -2358,6 +2358,27 @@ impl DocumentMessageHandler {
]
},
},
LayoutGroup::Row {
widgets: vec![TextLabel::new("Fill Tool").widget_holder()],
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
vec![
CheckboxInput::new(self.overlays_visibility_settings.fillable_indicator)
.on_update(|optional_input: &CheckboxInput| {
DocumentMessage::SetOverlaysVisibility {
visible: optional_input.checked,
overlays_type: Some(OverlaysType::FillableIndicator),
}
.into()
})
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Fillable Indicator".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
]
},
},
])
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
stroke: Stroke,
},
StrokeColorSet {
layer: LayerNodeIdentifier,
stroke_color: Color,
},
TransformChange {
layer: LayerNodeIdentifier,
transform: DAffine2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.stroke_set(stroke);
}
}
GraphOperationMessage::StrokeColorSet { layer, stroke_color } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.stroke_color_set(Some(stroke_color));
}
}
GraphOperationMessage::TransformChange {
layer,
transform,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,13 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false);
}

pub fn stroke_color_set(&mut self, color: Option<Color>) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", false) else { return };

let input_connector = InputConnector::node(stroke_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(color), false), false);
}

/// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform.
/// A new Transform node is created if one does not exist, unless it would be given the identity transform.
pub fn transform_change_with_parent(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
let _ = context.reset_transform();

if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays(OverlayContext {
render_context: context.clone(),
size: size.as_dvec2(),
device_pixel_ratio,
visibility_settings: visibility_settings.clone(),
}));
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext {
render_context: context.clone(),
Expand All @@ -65,6 +59,12 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
visibility_settings: visibility_settings.clone(),
}));
}
responses.add(DocumentMessage::GridOverlays(OverlayContext {
render_context: context.clone(),
size: size.as_dvec2(),
device_pixel_ratio,
visibility_settings: visibility_settings.clone(),
}));
}
}
#[cfg(not(target_arch = "wasm32"))]
Expand Down
119 changes: 85 additions & 34 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use glam::{DAffine2, DVec2};
use graphene_core::Color;
use graphene_core::renderer::Quad;
use graphene_std::renderer::ClickTargetType;
use graphene_std::vector::style::Stroke;
use graphene_std::vector::{PointId, SegmentId, VectorData};
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
Expand All @@ -25,33 +26,48 @@ pub fn empty_provider() -> OverlayProvider {
// Types of overlays used by DocumentMessage to enable/disable select group of overlays in the frontend
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum OverlaysType {
// =======
// General
// =======
ArtboardName,
CompassRose,
QuickMeasurement,
TransformMeasurement,
// ===========
// Select Tool
// ===========
QuickMeasurement,
TransformCage,
CompassRose,
Pivot,
HoverOutline,
SelectionOutline,
Pivot,
// ================
// Pen & Path Tools
// ================
Path,
Anchors,
Handles,
// =========
// Fill Tool
// =========
FillableIndicator,
}

#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(default = "OverlaysVisibilitySettings::default")]
pub struct OverlaysVisibilitySettings {
pub all: bool,
pub artboard_name: bool,
pub compass_rose: bool,
pub quick_measurement: bool,
pub transform_measurement: bool,
pub quick_measurement: bool,
pub transform_cage: bool,
pub compass_rose: bool,
pub pivot: bool,
pub hover_outline: bool,
pub selection_outline: bool,
pub pivot: bool,
pub path: bool,
pub anchors: bool,
pub handles: bool,
pub fillable_indicator: bool,
}

impl Default for OverlaysVisibilitySettings {
Expand All @@ -69,6 +85,7 @@ impl Default for OverlaysVisibilitySettings {
path: true,
anchors: true,
handles: true,
fillable_indicator: true,
}
}
}
Expand Down Expand Up @@ -121,6 +138,10 @@ impl OverlaysVisibilitySettings {
pub fn handles(&self) -> bool {
self.all && self.anchors && self.handles
}

pub fn fillable_indicator(&self) -> bool {
self.all && self.fillable_indicator
}
}

#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
Expand Down Expand Up @@ -550,8 +571,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) {
fn draw_path_from_vector_data(&mut self, vector_data: &VectorData, transform: DAffine2) {
self.start_dpi_aware_transform();

self.render_context.begin_path();
Expand All @@ -563,10 +583,14 @@ impl OverlayContext {
self.bezier_command(bezier, transform, move_to);
}

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.draw_path_from_vector_data(vector_data, transform);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.stroke();

self.end_dpi_aware_transform();
}

/// Used by the Pen tool in order to show how the bezier curve would look like.
Expand Down Expand Up @@ -598,7 +622,7 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}

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

self.render_context.begin_path();
Expand All @@ -612,28 +636,29 @@ impl OverlayContext {

self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
for curve in curves {
let splat_value = 0.5;
match curve.handles {
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let a = a.round() - DVec2::splat(splat_value);

self.render_context.line_to(a.x, a.y)
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let a = transform.transform_point2(handle);
let b = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let a = a.round() - DVec2::splat(splat_value);
let b = b.round() - DVec2::splat(splat_value);

self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let a = transform.transform_point2(handle_start);
let b = transform.transform_point2(handle_end);
let c = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);
let a = a.round() - DVec2::splat(splat_value);
let b = b.round() - DVec2::splat(splat_value);
let c = c.round() - DVec2::splat(splat_value);

self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
}
Expand All @@ -660,26 +685,16 @@ impl OverlayContext {
});

if !subpaths.is_empty() {
self.push_path(subpaths.iter(), transform);
self.draw_path_from_subpaths(subpaths.iter(), transform);

let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.render_context.set_stroke_style_str(color);
self.render_context.stroke();
}
}

/// 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) {
/// Default canvas pattern used for filling stroke or fill of a path.
fn fill_canvas_pattern(&self, color: &Color) -> web_sys::CanvasPattern {
const PATTERN_WIDTH: usize = 4;
const PATTERN_HEIGHT: usize = 4;

Expand Down Expand Up @@ -708,12 +723,48 @@ impl OverlayContext {

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();
return self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
}

self.push_path(subpaths, transform);
/// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space.
/// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern.
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, with_pattern: bool, stroke_width: Option<f64>) {
self.render_context.save();
self.render_context.set_line_width(stroke_width.unwrap_or(1.));
self.draw_path_from_subpaths(subpaths, transform);

self.render_context.set_fill_style_canvas_pattern(&pattern);
self.render_context.fill();
if with_pattern {
self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color));
self.render_context.fill();

// Make the stroke transparent and erase the fill area overlapping the stroke.
self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation");
self.render_context.set_stroke_style_str(&"#000000");
self.render_context.stroke();
} else {
let color_str = format!("#{:?}", color.to_rgba_hex_srgb());
self.render_context.set_fill_style_str(&color_str.as_str());
self.render_context.fill();
}

self.render_context.restore();
}

pub fn fill_stroke(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, overlay_stroke: &Stroke) {
self.render_context.save();

// debug!("overlay_stroke.weight * ptz.zoom(): {:?}", overlay_stroke.weight);
self.render_context.set_line_width(overlay_stroke.weight);
self.draw_path_from_subpaths(subpaths, overlay_stroke.transform);

self.render_context
.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(&overlay_stroke.color.expect("Color should be set for fill_stroke()")));
self.render_context.set_line_cap(overlay_stroke.line_cap.html_canvas_name().as_str());
self.render_context.set_line_join(overlay_stroke.line_join.html_canvas_name().as_str());
self.render_context.set_miter_limit(overlay_stroke.line_join_miter_limit);
self.render_context.stroke();

self.render_context.restore();
}

pub fn get_width(&self, text: &str) -> f64 {
Expand Down
Loading
Loading