Skip to content

Commit d1612e7

Browse files
committed
Implement fill tool on strokes
1 parent b433ddd commit d1612e7

12 files changed

Lines changed: 352 additions & 102 deletions

File tree

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
10631063
responses.add(PortfolioMessage::UpdateDocumentWidgets);
10641064
}
10651065
OverlaysType::Handles => visibility_settings.handles = visible,
1066+
OverlaysType::FillableIndicator => visibility_settings.fillable_indicator = visible,
10661067
}
10671068

10681069
responses.add(EventMessage::ToolAbort);
@@ -2319,7 +2320,7 @@ impl DocumentMessageHandler {
23192320
widgets: {
23202321
let checkbox_id = CheckboxId::new();
23212322
vec![
2322-
CheckboxInput::new(self.overlays_visibility_settings.pivot)
2323+
CheckboxInput::new(self.overlays_visibility_settings.origin)
23232324
.on_update(|optional_input: &CheckboxInput| {
23242325
DocumentMessage::SetOverlaysVisibility {
23252326
visible: optional_input.checked,
@@ -2466,6 +2467,27 @@ impl DocumentMessageHandler {
24662467
]
24672468
},
24682469
},
2470+
LayoutGroup::Row {
2471+
widgets: vec![TextLabel::new("Fill Tool").widget_instance()],
2472+
},
2473+
LayoutGroup::Row {
2474+
widgets: {
2475+
let checkbox_id = CheckboxId::new();
2476+
vec![
2477+
CheckboxInput::new(self.overlays_visibility_settings.fillable_indicator)
2478+
.on_update(|optional_input: &CheckboxInput| {
2479+
DocumentMessage::SetOverlaysVisibility {
2480+
visible: optional_input.checked,
2481+
overlays_type: Some(OverlaysType::FillableIndicator),
2482+
}
2483+
.into()
2484+
})
2485+
.for_label(checkbox_id.clone())
2486+
.widget_instance(),
2487+
TextLabel::new("Fillable Indicator".to_string()).for_checkbox(checkbox_id).widget_instance(),
2488+
]
2489+
},
2490+
},
24692491
]))
24702492
.widget_instance(),
24712493
Separator::new(SeparatorStyle::Related).widget_instance(),

editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
44
use crate::messages::prelude::*;
55
use glam::{DAffine2, IVec2};
66
use graph_craft::document::NodeId;
7-
use graphene_std::Artboard;
87
use graphene_std::brush::brush_stroke::BrushStroke;
98
use graphene_std::raster::BlendMode;
109
use graphene_std::raster_types::{CPU, Raster};
@@ -14,6 +13,7 @@ use graphene_std::text::{Font, TypesettingConfig};
1413
use graphene_std::vector::PointId;
1514
use graphene_std::vector::VectorModificationType;
1615
use graphene_std::vector::style::{Fill, Stroke};
16+
use graphene_std::{Artboard, Color};
1717

1818
#[impl_message(Message, DocumentMessage, GraphOperation)]
1919
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -41,6 +41,10 @@ pub enum GraphOperationMessage {
4141
layer: LayerNodeIdentifier,
4242
stroke: Stroke,
4343
},
44+
StrokeColorSet {
45+
layer: LayerNodeIdentifier,
46+
stroke_color: Color,
47+
},
4448
TransformChange {
4549
layer: LayerNodeIdentifier,
4650
transform: DAffine2,

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
6565
modify_inputs.stroke_set(stroke);
6666
}
6767
}
68+
GraphOperationMessage::StrokeColorSet { layer, stroke_color } => {
69+
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
70+
modify_inputs.stroke_color_set(Some(stroke_color));
71+
}
72+
}
6873
GraphOperationMessage::TransformChange {
6974
layer,
7075
transform,

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use glam::{DAffine2, IVec2};
77
use graph_craft::document::value::TaggedValue;
88
use graph_craft::document::{NodeId, NodeInput};
99
use graph_craft::{ProtoNodeIdentifier, concrete};
10-
use graphene_std::Artboard;
1110
use graphene_std::brush::brush_stroke::BrushStroke;
1211
use graphene_std::raster::BlendMode;
1312
use graphene_std::raster_types::{CPU, Raster};
@@ -17,7 +16,7 @@ use graphene_std::text::{Font, TypesettingConfig};
1716
use graphene_std::vector::Vector;
1817
use graphene_std::vector::style::{Fill, Stroke};
1918
use graphene_std::vector::{PointId, VectorModificationType};
20-
use graphene_std::{Graphic, NodeInputDecleration};
19+
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
2120

2221
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
2322
pub enum TransformIn {
@@ -423,6 +422,16 @@ impl<'a> ModifyInputsContext<'a> {
423422
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
424423
}
425424

425+
pub fn stroke_color_set(&mut self, color: Option<Color>) {
426+
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, false) else {
427+
return;
428+
};
429+
430+
let stroke_color = if let Some(color) = color { Table::new_from_element(color) } else { Table::new() };
431+
let input_connector = InputConnector::node(stroke_node_id, 1);
432+
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), false);
433+
}
434+
426435
/// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform.
427436
/// A new Transform node is created if one does not exist, unless it would be given the identity transform.
428437
pub fn transform_change_with_parent(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) {

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,20 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
5050
canvas_context.clear_rect(0., 0., width, height);
5151

5252
if visibility_settings.all() {
53-
responses.add(DocumentMessage::GridOverlays {
54-
context: OverlayContext {
55-
render_context: canvas_context.clone(),
56-
visibility_settings: visibility_settings.clone(),
57-
viewport: *viewport,
58-
},
59-
});
6053
for provider in &self.overlay_providers {
6154
responses.add(provider(OverlayContext {
6255
render_context: canvas_context.clone(),
6356
visibility_settings: visibility_settings.clone(),
6457
viewport: *viewport,
6558
}));
6659
}
60+
responses.add(DocumentMessage::GridOverlays {
61+
context: OverlayContext {
62+
render_context: canvas_context.clone(),
63+
visibility_settings: visibility_settings.clone(),
64+
viewport: *viewport,
65+
},
66+
});
6767
}
6868
}
6969
#[cfg(all(not(target_family = "wasm"), not(test)))]

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

Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use graphene_std::math::quad::Quad;
1515
use graphene_std::subpath::Subpath;
1616
use graphene_std::vector::click_target::ClickTargetType;
1717
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
18+
use graphene_std::vector::style::Stroke;
1819
use graphene_std::vector::{PointId, SegmentId, Vector};
1920
use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg};
2021
use std::collections::HashMap;
@@ -30,57 +31,72 @@ pub fn empty_provider() -> OverlayProvider {
3031
/// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays.
3132
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
3233
pub enum OverlaysType {
34+
// =======
35+
// General
36+
// =======
3337
ArtboardName,
34-
CompassRose,
35-
QuickMeasurement,
3638
TransformMeasurement,
39+
// ===========
40+
// Select Tool
41+
// ===========
42+
QuickMeasurement,
3743
TransformCage,
44+
CompassRose,
45+
Pivot,
46+
Origin,
3847
HoverOutline,
3948
SelectionOutline,
4049
LayerOriginCross,
41-
Pivot,
42-
Origin,
50+
// ================
51+
// Pen & Path Tools
52+
// ================
4353
Path,
4454
Anchors,
4555
Handles,
56+
// =========
57+
// Fill Tool
58+
// =========
59+
FillableIndicator,
4660
}
4761

4862
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
49-
#[serde(default)]
63+
#[serde(default = "OverlaysVisibilitySettings::default")]
5064
pub struct OverlaysVisibilitySettings {
5165
pub all: bool,
5266
pub artboard_name: bool,
53-
pub compass_rose: bool,
54-
pub quick_measurement: bool,
5567
pub transform_measurement: bool,
68+
pub quick_measurement: bool,
5669
pub transform_cage: bool,
70+
pub compass_rose: bool,
71+
pub pivot: bool,
72+
pub origin: bool,
5773
pub hover_outline: bool,
5874
pub selection_outline: bool,
5975
pub layer_origin_cross: bool,
60-
pub pivot: bool,
61-
pub origin: bool,
6276
pub path: bool,
6377
pub anchors: bool,
6478
pub handles: bool,
79+
pub fillable_indicator: bool,
6580
}
6681

6782
impl Default for OverlaysVisibilitySettings {
6883
fn default() -> Self {
6984
Self {
7085
all: true,
7186
artboard_name: true,
72-
compass_rose: true,
73-
quick_measurement: true,
7487
transform_measurement: true,
88+
quick_measurement: true,
7589
transform_cage: true,
90+
compass_rose: true,
91+
pivot: true,
92+
origin: true,
7693
hover_outline: true,
7794
selection_outline: true,
7895
layer_origin_cross: true,
79-
pivot: true,
80-
origin: true,
8196
path: true,
8297
anchors: true,
8398
handles: true,
99+
fillable_indicator: true,
84100
}
85101
}
86102
}
@@ -141,6 +157,10 @@ impl OverlaysVisibilitySettings {
141157
pub fn handles(&self) -> bool {
142158
self.all && self.anchors && self.handles
143159
}
160+
161+
pub fn fillable_indicator(&self) -> bool {
162+
self.all && self.fillable_indicator
163+
}
144164
}
145165

146166
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -805,8 +825,7 @@ impl OverlayContext {
805825
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
806826
}
807827

808-
/// Used by the Pen and Path tools to outline the path of the shape.
809-
pub fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) {
828+
pub fn draw_path_from_vector_data(&mut self, vector: &Vector, transform: DAffine2) {
810829
self.start_dpi_aware_transform();
811830

812831
self.render_context.begin_path();
@@ -818,6 +837,12 @@ impl OverlayContext {
818837
self.bezier_command(bezier, transform, move_to);
819838
}
820839

840+
self.end_dpi_aware_transform();
841+
}
842+
843+
/// Used by the Pen and Path tools to outline the path of the shape.
844+
pub fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) {
845+
self.draw_path_from_vector_data(vector, transform);
821846
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
822847
self.render_context.stroke();
823848

@@ -882,7 +907,7 @@ impl OverlayContext {
882907
self.end_dpi_aware_transform();
883908
}
884909

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

888913
self.render_context.begin_path();
@@ -943,7 +968,7 @@ impl OverlayContext {
943968
});
944969

945970
if !subpaths.is_empty() {
946-
self.push_path(subpaths.iter(), transform);
971+
self.draw_path_from_subpaths(subpaths.iter(), transform);
947972

948973
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
949974
self.render_context.set_stroke_style_str(color);
@@ -952,18 +977,8 @@ impl OverlayContext {
952977
}
953978
}
954979

955-
/// Fills the area inside the path. Assumes `color` is in gamma space.
956-
/// Used by the Pen tool to show the path being closed.
957-
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
958-
self.push_path(subpaths, transform);
959-
960-
self.render_context.set_fill_style_str(color);
961-
self.render_context.fill();
962-
}
963-
964-
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
965-
/// Used by the fill tool to show the area to be filled.
966-
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
980+
/// Default canvas pattern used for filling stroke or fill of a path.
981+
fn fill_canvas_pattern(&self, color: &Color) -> web_sys::CanvasPattern {
967982
const PATTERN_WIDTH: usize = 4;
968983
const PATTERN_HEIGHT: usize = 4;
969984

@@ -992,12 +1007,61 @@ impl OverlayContext {
9921007

9931008
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
9941009
pattern_context.put_image_data(&image_data, 0., 0.).unwrap();
995-
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
996-
997-
self.push_path(subpaths, transform);
1010+
return self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
1011+
}
9981012

999-
self.render_context.set_fill_style_canvas_pattern(&pattern);
1013+
/// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space.
1014+
/// 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.
1015+
pub fn fill_path(
1016+
&mut self,
1017+
subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>,
1018+
transform: DAffine2,
1019+
color: &Color,
1020+
with_pattern: bool,
1021+
clear_stroke_part: bool,
1022+
stroke_width: Option<f64>,
1023+
) {
1024+
self.render_context.save();
1025+
self.render_context.set_line_width(stroke_width.unwrap_or(1.));
1026+
self.draw_path_from_subpaths(subpaths, transform);
1027+
1028+
if with_pattern {
1029+
self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color));
1030+
} else {
1031+
let color_str = format!("#{:?}", color.to_rgba_hex_srgb());
1032+
self.render_context.set_fill_style_str(&color_str.as_str());
1033+
}
10001034
self.render_context.fill();
1035+
1036+
// Make the stroke transparent and erase the fill area overlapping the stroke.
1037+
if clear_stroke_part {
1038+
self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation");
1039+
self.render_context.set_stroke_style_str(&"#000000");
1040+
self.render_context.stroke();
1041+
}
1042+
1043+
self.render_context.restore();
1044+
}
1045+
1046+
pub fn fill_stroke(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, overlay_stroke: &Stroke) {
1047+
self.render_context.save();
1048+
1049+
// debug!("overlay_stroke.weight * ptz.zoom(): {:?}", overlay_stroke.weight);
1050+
self.render_context.set_line_width(overlay_stroke.weight);
1051+
self.draw_path_from_subpaths(subpaths, overlay_stroke.transform);
1052+
1053+
self.render_context
1054+
.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(&overlay_stroke.color.expect("Color should be set for fill_stroke()")));
1055+
self.render_context.set_line_cap(overlay_stroke.cap.html_canvas_name().as_str());
1056+
self.render_context.set_line_join(overlay_stroke.join.html_canvas_name().as_str());
1057+
self.render_context.set_miter_limit(overlay_stroke.join_miter_limit);
1058+
self.render_context.stroke();
1059+
1060+
self.render_context.restore();
1061+
}
1062+
1063+
pub fn get_width(&self, text: &str) -> f64 {
1064+
self.render_context.measure_text(text).expect("Failed to measure text dimensions").width()
10011065
}
10021066

10031067
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {

0 commit comments

Comments
 (0)