Skip to content

Commit 7a9cf59

Browse files
committed
Implement stroke and fill overlays for desktop
1 parent 3754701 commit 7a9cf59

8 files changed

Lines changed: 147 additions & 58 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ impl<'a> ModifyInputsContext<'a> {
501501
};
502502

503503
let stroke_color = if let Some(color) = color { Table::new_from_element(color) } else { Table::new() };
504-
let input_connector = InputConnector::node(stroke_node_id, 1);
504+
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
505505
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), false);
506506
}
507507

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,10 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
7373
let overlay_context = OverlayContext::new(*viewport, visibility_settings);
7474

7575
if visibility_settings.all() {
76-
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
77-
7876
for provider in &self.overlay_providers {
7977
responses.add(provider(overlay_context.clone()));
8078
}
79+
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
8180
}
8281
responses.add(FrontendMessage::RenderOverlays { context: overlay_context });
8382
}

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

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ use graphene_std::table::Table;
1717
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
1818
use graphene_std::vector::click_target::ClickTargetType;
1919
use graphene_std::vector::misc::point_to_dvec2;
20-
use graphene_std::vector::style::Stroke;
20+
use graphene_std::vector::style::{PaintOrder, Stroke, StrokeAlign};
2121
use graphene_std::vector::{PointId, SegmentId, Vector};
22-
use kurbo::{self, BezPath, ParamCurve};
22+
use kurbo::{self, BezPath, ParamCurve, Shape};
2323
use kurbo::{Affine, PathSeg};
2424
use std::collections::HashMap;
2525
use std::sync::{Arc, Mutex, MutexGuard};
2626
use vello::Scene;
27-
use vello::peniko;
27+
use vello::peniko::{self, BlendMode};
2828

2929
// TODO Remove duplicated definition of this in `utility_types_web.rs`
3030
pub type OverlayProvider = fn(OverlayContext) -> Message;
@@ -1123,20 +1123,116 @@ impl OverlayContextInternal {
11231123
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
11241124
/// Used by the fill tool to show the area to be filled.
11251125
fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
1126-
let path = self.path_from_subpaths(subpaths, transform);
1127-
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color));
1126+
if let Some(stroke) = stroke {
1127+
let has_real_stroke = stroke.weight() > 0. && stroke.transform.matrix2.determinant() != 0.;
1128+
let applied_stroke_transform = if has_real_stroke { stroke.transform } else { transform };
1129+
let element_transform = if has_real_stroke { transform * stroke.transform.inverse() } else { DAffine2::IDENTITY };
1130+
1131+
let path = self.path_from_subpaths(subpaths, applied_stroke_transform);
1132+
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color));
1133+
1134+
let do_fill = |scene: &mut Scene| {
1135+
let element_transform = Affine::new(element_transform.to_cols_array());
1136+
scene.fill(peniko::Fill::NonZero, element_transform, &brush, Some(element_transform.inverse()), &path);
1137+
};
1138+
let composite_stroke_out = |scene: &mut Scene, compose_mode: peniko::Compose, stroke_scale: Option<f64>| {
1139+
let element_transform = Affine::new(element_transform.to_cols_array());
1140+
let mut stroke = stroke.to_kurbo();
1141+
if let Some(scale) = stroke_scale {
1142+
stroke.width *= scale;
1143+
}
1144+
let path_bbox = path.bounding_box().inflate(stroke.width, stroke.width);
1145+
1146+
scene.push_layer(peniko::Fill::NonZero, BlendMode::new(peniko::Mix::Normal, compose_mode), 1.0, element_transform, &path_bbox);
1147+
scene.stroke(&stroke, element_transform, &brush, Some(element_transform.inverse()), &path);
1148+
scene.pop_layer();
1149+
};
11281150

1129-
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
1151+
// For layers with open subpaths, stroke align is ignored and set to default
1152+
let is_closed_on_all = true;
1153+
let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center };
1154+
1155+
match (stroke_align, stroke.paint_order) {
1156+
(StrokeAlign::Inside, PaintOrder::StrokeAbove) => {
1157+
do_fill(&mut self.scene);
1158+
composite_stroke_out(&mut self.scene, peniko::Compose::DestOut, Some(2.0));
1159+
}
1160+
(StrokeAlign::Inside, PaintOrder::StrokeBelow) => {
1161+
do_fill(&mut self.scene);
1162+
}
1163+
(StrokeAlign::Center, PaintOrder::StrokeAbove) => {
1164+
do_fill(&mut self.scene);
1165+
composite_stroke_out(&mut self.scene, peniko::Compose::DestOut, None);
1166+
}
1167+
(StrokeAlign::Center, PaintOrder::StrokeBelow) => {
1168+
do_fill(&mut self.scene);
1169+
}
1170+
// Paint order does not affect this
1171+
(StrokeAlign::Outside, _) => {
1172+
do_fill(&mut self.scene);
1173+
}
1174+
}
1175+
} else {
1176+
let path = self.path_from_subpaths(subpaths, transform);
1177+
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color));
1178+
self.scene.fill(peniko::Fill::NonZero, Affine::IDENTITY, &brush, None, &path);
1179+
}
11301180
}
11311181

11321182
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
11331183
/// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
11341184
pub fn stroke_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
1135-
let path = self.path_from_subpaths(subpaths, transform);
1136-
11371185
if let Some(stroke) = stroke {
1138-
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(&color));
1139-
self.scene.stroke(&kurbo::Stroke::new(stroke.weight), self.get_transform(), &brush, None, &path);
1186+
let has_real_stroke = stroke.weight() > 0. && stroke.transform.matrix2.determinant() != 0.;
1187+
let applied_stroke_transform = if has_real_stroke { stroke.transform } else { transform };
1188+
let element_transform = if has_real_stroke { transform * stroke.transform.inverse() } else { DAffine2::IDENTITY };
1189+
1190+
let path = self.path_from_subpaths(subpaths, applied_stroke_transform);
1191+
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color));
1192+
1193+
let do_stroke = |scene: &mut Scene, stroke_scale: Option<f64>| {
1194+
let element_transform = Affine::new(element_transform.to_cols_array());
1195+
let mut stroke = stroke.to_kurbo();
1196+
if let Some(scale) = stroke_scale {
1197+
stroke.width *= scale;
1198+
}
1199+
1200+
scene.stroke(&stroke, element_transform, &brush, Some(element_transform.inverse()), &path);
1201+
};
1202+
let composite_fill_out = |scene: &mut Scene, compose_mode: peniko::Compose, stroke_scale: Option<f64>| {
1203+
let element_transform = Affine::new(element_transform.to_cols_array());
1204+
let stroke_width = stroke.weight() * stroke_scale.map_or(1.0, |scale| scale);
1205+
let path_bbox = path.bounding_box().inflate(stroke_width, stroke_width);
1206+
1207+
scene.push_layer(peniko::Fill::NonZero, BlendMode::new(peniko::Mix::Normal, compose_mode), 1.0, element_transform, &path_bbox);
1208+
scene.fill(peniko::Fill::NonZero, element_transform, &brush, Some(element_transform.inverse()), &path);
1209+
scene.pop_layer();
1210+
};
1211+
1212+
// For layers with open subpaths, stroke align is ignored and set to default
1213+
let is_closed_on_all = true;
1214+
let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center };
1215+
1216+
match (stroke_align, stroke.paint_order) {
1217+
(StrokeAlign::Inside, PaintOrder::StrokeAbove) => {
1218+
// TODO: Fix overlay leak outside the stroke region
1219+
do_stroke(&mut self.scene, Some(2.0));
1220+
composite_fill_out(&mut self.scene, peniko::Compose::DestIn, Some(2.0));
1221+
}
1222+
(StrokeAlign::Inside, PaintOrder::StrokeBelow) => {}
1223+
(StrokeAlign::Center, PaintOrder::StrokeAbove) => {
1224+
do_stroke(&mut self.scene, None);
1225+
}
1226+
(StrokeAlign::Center, PaintOrder::StrokeBelow) => {
1227+
do_stroke(&mut self.scene, None);
1228+
composite_fill_out(&mut self.scene, peniko::Compose::DestOut, None);
1229+
}
1230+
// Paint order does not affect this
1231+
(StrokeAlign::Outside, _) => {
1232+
do_stroke(&mut self.scene, Some(2.0));
1233+
composite_fill_out(&mut self.scene, peniko::Compose::DestOut, Some(2.0));
1234+
}
1235+
}
11401236
}
11411237
}
11421238

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ impl OverlayContext {
10501050
// 4x4 pixels, 4 components (RGBA) per pixel
10511051
let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT];
10521052

1053-
let rgba = hex_to_rgba_u8(color.to_rgba_hex_srgb().as_str());
1053+
let rgba = color.to_rgba8_srgb();
10541054

10551055
// ┌▄▄┬──┬──┬──┐
10561056
// ├▀▀┼──┼──┼──┤

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3132,7 +3132,7 @@ impl NodeNetworkInterface {
31323132
return Some(modified);
31333133
}
31343134

3135-
self.document_metadata.layer_vector_data.get(&layer).map(|arc| arc.as_ref().clone())
3135+
self.vector_data_from_layer(layer)
31363136
}
31373137

31383138
pub fn vector_data_from_layer(&self, layer: LayerNodeIdentifier) -> Option<Vector> {

node-graph/libraries/rendering/src/renderer.rs

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,7 @@ impl Render for Table<Vector> {
885885
let mut path = String::new();
886886

887887
for mut bezpath in row.element.stroke_bezpath_iter() {
888-
// Only seems to affect upstream-transformed (from stroke node) layers ~~with row.transform~~
888+
// Only affects layers with upstream transforms (from stroke node)
889889
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
890890
path.push_str(bezpath.to_svg().as_str());
891891
}
@@ -904,7 +904,6 @@ impl Render for Table<Vector> {
904904
let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
905905

906906
if needs_separate_alignment_fill && !wants_stroke_below {
907-
log::info!("Entering needs_separate_alignment_fill && !wants_stroke_below");
908907
render.leaf_tag("path", |attributes| {
909908
attributes.push("d", path.clone());
910909
let matrix = format_transform_matrix(element_transform);
@@ -944,7 +943,6 @@ impl Render for Table<Vector> {
944943

945944
let use_face_fill = vector.use_face_fill();
946945
if use_face_fill {
947-
log::info!("Entering vector.is_branching()");
948946
for mut face_path in vector.construct_faces().filter(|face| face.area() >= 0.) {
949947
face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
950948

@@ -972,16 +970,14 @@ impl Render for Table<Vector> {
972970

973971
render.leaf_tag("path", |attributes| {
974972
attributes.push("d", path.clone());
975-
// Only seem to affect layers with downstream-transformed layers (from stroke node) with ~~row.transform*stroke_transform.inverse()~~
976-
// and affect layers with upstream-transformed (from stroke node) layers ~~with IDENTITY~~
973+
// Affects all layers regardless of upstream/downstream transforms (from the stroke node)
977974
let matrix = format_transform_matrix(element_transform);
978975
if !matrix.is_empty() {
979976
attributes.push("transform", matrix);
980977
}
981978

982979
let defs = &mut attributes.0.svg_defs;
983980
if let Some((ref id, mask_type, ref vector_row)) = push_id {
984-
log::info!("Entering Some(p) = push_id");
985981
let mut svg = SvgRender::new();
986982
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
987983
let stroke = row.element.style.stroke().unwrap();
@@ -1015,13 +1011,11 @@ impl Render for Table<Vector> {
10151011
}
10161012

10171013
let fill_and_stroke = style.render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params);
1018-
// log::info!("file_and_stroke: {:?}", fill_and_stroke);
10191014

10201015
if let Some((id, mask_type, _)) = push_id {
10211016
let selector = format!("url(#{id})");
10221017
attributes.push(mask_type.to_attribute(), selector);
10231018
}
1024-
// Look here to see how to adjust the stroke
10251019
attributes.push_val(fill_and_stroke);
10261020

10271021
if vector.is_branching() && !use_face_fill {
@@ -1040,7 +1034,6 @@ impl Render for Table<Vector> {
10401034

10411035
// When splitting passes and stroke is below, draw the fill after the stroke.
10421036
if needs_separate_alignment_fill && wants_stroke_below {
1043-
log::info!("Entering needs_separate_alignment_fill && wants_stroke_below");
10441037
render.leaf_tag("path", |attributes| {
10451038
attributes.push("d", path);
10461039
let matrix = format_transform_matrix(element_transform);
@@ -1064,7 +1057,7 @@ impl Render for Table<Vector> {
10641057
}
10651058

10661059
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
1067-
use graphic_types::vector_types::vector::style::{GradientType, StrokeCap, StrokeJoin};
1060+
use graphic_types::vector_types::vector::style::GradientType;
10681061

10691062
for row in self.iter() {
10701063
use graphic_types::vector_types::vector;
@@ -1214,23 +1207,13 @@ impl Render for Table<Vector> {
12141207
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
12151208
None => peniko::Color::TRANSPARENT,
12161209
};
1217-
let cap = match stroke.cap {
1218-
StrokeCap::Butt => Cap::Butt,
1219-
StrokeCap::Round => Cap::Round,
1220-
StrokeCap::Square => Cap::Square,
1221-
};
1222-
let join = match stroke.join {
1223-
StrokeJoin::Miter => Join::Miter,
1224-
StrokeJoin::Bevel => Join::Bevel,
1225-
StrokeJoin::Round => Join::Round,
1226-
};
12271210
let dash_pattern = stroke.dash_lengths.iter().map(|l| l.max(0.)).collect();
12281211
let stroke = kurbo::Stroke {
12291212
width: stroke.weight * width_scale,
12301213
miter_limit: stroke.join_miter_limit,
1231-
join,
1232-
start_cap: cap,
1233-
end_cap: cap,
1214+
join: stroke.join.to_kurbo(),
1215+
start_cap: stroke.cap.to_kurbo(),
1216+
end_cap: stroke.cap.to_kurbo(),
12341217
dash_pattern,
12351218
dash_offset: stroke.dash_offset,
12361219
};

node-graph/libraries/vector-types/src/vector/style.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ impl StrokeCap {
245245
StrokeCap::Square => String::from("square"),
246246
}
247247
}
248+
249+
pub fn to_kurbo(&self) -> kurbo::Cap {
250+
match self {
251+
StrokeCap::Butt => kurbo::Cap::Butt,
252+
StrokeCap::Round => kurbo::Cap::Round,
253+
StrokeCap::Square => kurbo::Cap::Square,
254+
}
255+
}
248256
}
249257

250258
#[repr(C)]
@@ -274,6 +282,14 @@ impl StrokeJoin {
274282
StrokeJoin::Round => String::from("round"),
275283
}
276284
}
285+
286+
pub fn to_kurbo(&self) -> kurbo::Join {
287+
match self {
288+
StrokeJoin::Bevel => kurbo::Join::Bevel,
289+
StrokeJoin::Miter => kurbo::Join::Miter,
290+
StrokeJoin::Round => kurbo::Join::Round,
291+
}
292+
}
277293
}
278294

279295
#[repr(C)]
@@ -372,6 +388,18 @@ impl Stroke {
372388
}
373389
}
374390

391+
pub fn to_kurbo(&self) -> kurbo::Stroke {
392+
kurbo::Stroke {
393+
width: self.weight,
394+
join: self.join.to_kurbo(),
395+
miter_limit: self.join_miter_limit,
396+
start_cap: self.cap.to_kurbo(),
397+
end_cap: self.cap.to_kurbo(),
398+
dash_offset: self.dash_offset,
399+
..Default::default()
400+
}
401+
}
402+
375403
pub fn lerp(&self, other: &Self, time: f64) -> Self {
376404
Self {
377405
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)),

node-graph/nodes/vector/src/vector_nodes.rs

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,16 +1123,7 @@ async fn offset_path(_: impl Ctx, content: Table<Vector>, distance: f64, join: S
11231123
bezpath.apply_affine(transform);
11241124

11251125
// Taking the existing stroke data and passing it to Kurbo to generate new paths.
1126-
let mut bezpath_out = offset_bezpath(
1127-
&bezpath,
1128-
-distance,
1129-
match join {
1130-
StrokeJoin::Miter => kurbo::Join::Miter,
1131-
StrokeJoin::Bevel => kurbo::Join::Bevel,
1132-
StrokeJoin::Round => kurbo::Join::Round,
1133-
},
1134-
Some(miter_limit),
1135-
);
1126+
let mut bezpath_out = offset_bezpath(&bezpath, -distance, join.to_kurbo(), Some(miter_limit));
11361127

11371128
bezpath_out.apply_affine(transform.inverse());
11381129

@@ -1163,16 +1154,8 @@ async fn solidify_stroke(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
11631154
let mut solidified_stroke = Vector::default();
11641155

11651156
// Taking the existing stroke data and passing it to kurbo::stroke to generate new fill paths.
1166-
let join = match stroke.join {
1167-
StrokeJoin::Miter => kurbo::Join::Miter,
1168-
StrokeJoin::Bevel => kurbo::Join::Bevel,
1169-
StrokeJoin::Round => kurbo::Join::Round,
1170-
};
1171-
let cap = match stroke.cap {
1172-
StrokeCap::Butt => kurbo::Cap::Butt,
1173-
StrokeCap::Round => kurbo::Cap::Round,
1174-
StrokeCap::Square => kurbo::Cap::Square,
1175-
};
1157+
let join = stroke.join.to_kurbo();
1158+
let cap = stroke.cap.to_kurbo();
11761159
let dash_offset = stroke.dash_offset;
11771160
let dash_pattern = stroke.dash_lengths;
11781161
let miter_limit = stroke.join_miter_limit;

0 commit comments

Comments
 (0)