Skip to content

Commit feb6895

Browse files
committed
Add StrokeAlign and PaintOrder support on stroke detection
1 parent f412eb9 commit feb6895

5 files changed

Lines changed: 208 additions & 153 deletions

File tree

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ mod overlays_message;
33
mod overlays_message_handler;
44
pub mod utility_functions;
55
// Native (non‑wasm)
6-
// #[cfg(not(target_family = "wasm"))]
7-
// pub mod utility_types_native;
8-
// #[cfg(not(target_family = "wasm"))]
9-
// pub use utility_types_native as utility_types;
6+
#[cfg(not(target_family = "wasm"))]
7+
pub mod utility_types_native;
8+
#[cfg(not(target_family = "wasm"))]
9+
pub use utility_types_native as utility_types;
1010

1111
// WebAssembly
12-
// #[cfg(target_family = "wasm")]
12+
#[cfg(target_family = "wasm")]
1313
pub mod utility_types_web;
14-
// #[cfg(target_family = "wasm")]
14+
#[cfg(target_family = "wasm")]
1515
pub use utility_types_web as utility_types;
1616

1717
#[doc(inline)]

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

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,32 @@ pub fn empty_provider() -> OverlayProvider {
3838
/// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays.
3939
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
4040
pub enum OverlaysType {
41+
// =======
42+
// General
43+
// =======
4144
ArtboardName,
42-
CompassRose,
43-
QuickMeasurement,
4445
TransformMeasurement,
46+
// ===========
47+
// Select Tool
48+
// ===========
49+
QuickMeasurement,
4550
TransformCage,
51+
CompassRose,
52+
Pivot,
53+
Origin,
4654
HoverOutline,
4755
SelectionOutline,
4856
LayerOriginCross,
49-
Pivot,
50-
Origin,
57+
// ================
58+
// Pen & Path Tools
59+
// ================
5160
Path,
5261
Anchors,
5362
Handles,
63+
// =========
64+
// Fill Tool
65+
// =========
66+
FillableIndicator,
5467
}
5568

5669
// TODO Remove duplicated definition of this in `utility_types_web.rs`
@@ -59,18 +72,19 @@ pub enum OverlaysType {
5972
pub struct OverlaysVisibilitySettings {
6073
pub all: bool,
6174
pub artboard_name: bool,
62-
pub compass_rose: bool,
63-
pub quick_measurement: bool,
6475
pub transform_measurement: bool,
76+
pub quick_measurement: bool,
6577
pub transform_cage: bool,
78+
pub compass_rose: bool,
79+
pub pivot: bool,
80+
pub origin: bool,
6681
pub hover_outline: bool,
6782
pub selection_outline: bool,
6883
pub layer_origin_cross: bool,
69-
pub pivot: bool,
70-
pub origin: bool,
7184
pub path: bool,
7285
pub anchors: bool,
7386
pub handles: bool,
87+
pub fillable_indicator: bool,
7488
}
7589

7690
// TODO Remove duplicated definition of this in `utility_types_web.rs`
@@ -79,18 +93,19 @@ impl Default for OverlaysVisibilitySettings {
7993
Self {
8094
all: true,
8195
artboard_name: true,
82-
compass_rose: true,
83-
quick_measurement: true,
8496
transform_measurement: true,
97+
quick_measurement: true,
8598
transform_cage: true,
99+
compass_rose: true,
100+
pivot: true,
101+
origin: true,
86102
hover_outline: true,
87103
selection_outline: true,
88104
layer_origin_cross: true,
89-
pivot: true,
90-
origin: true,
91105
path: true,
92106
anchors: true,
93107
handles: true,
108+
fillable_indicator: true,
94109
}
95110
}
96111
}
@@ -152,6 +167,10 @@ impl OverlaysVisibilitySettings {
152167
pub fn handles(&self) -> bool {
153168
self.all && self.anchors && self.handles
154169
}
170+
171+
pub fn fillable_indicator(&self) -> bool {
172+
self.all && self.fillable_indicator
173+
}
155174
}
156175

157176
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
@@ -389,14 +408,20 @@ impl OverlayContext {
389408

390409
/// Fills the area inside the path. Assumes `color` is in gamma space.
391410
/// Used by the Pen tool to show the path being closed.
392-
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
411+
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
393412
self.internal().fill_path(subpaths, transform, color);
394413
}
395414

396-
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
415+
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
397416
/// Used by the fill tool to show the area to be filled.
398-
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
399-
self.internal().fill_path_pattern(subpaths, transform, color);
417+
pub fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
418+
self.internal().fill_overlay(subpaths, transform, color, stroke);
419+
}
420+
421+
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
422+
/// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
423+
pub fn stroke_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
424+
self.internal().stroke_overlay(subpaths, transform, color, stroke);
400425
}
401426

402427
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
@@ -1050,26 +1075,31 @@ impl OverlayContextInternal {
10501075

10511076
/// Fills the area inside the path. Assumes `color` is in gamma space.
10521077
/// Used by the Pen tool to show the path being closed.
1053-
fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
1078+
fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
10541079
let path = self.path_from_subpaths(subpaths, transform);
10551080

1056-
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
1081+
let color_str = format!("#{}", color.to_rgba_hex_srgb());
1082+
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color_str.as_str()), None, &path);
10571083
}
10581084

1059-
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
1085+
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
10601086
/// Used by the fill tool to show the area to be filled.
1061-
fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
1087+
fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
10621088
let path = self.path_from_subpaths(subpaths, transform);
10631089
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color));
10641090

10651091
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
10661092
}
10671093

1068-
pub fn fill_stroke(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, overlay_stroke: &Stroke) {
1094+
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
1095+
/// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
1096+
pub fn stroke_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
10691097
let path = self.path_from_subpaths(subpaths, transform);
1070-
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(&overlay_stroke.color.expect("Color should be set for fill_stroke()")));
10711098

1072-
self.scene.stroke(&kurbo::Stroke::new(overlay_stroke.weight), self.get_transform(), &brush, None, &path);
1099+
if let Some(stroke) = stroke {
1100+
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(&color));
1101+
self.scene.stroke(&kurbo::Stroke::new(stroke.weight), self.get_transform(), &brush, None, &path);
1102+
}
10731103
}
10741104

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

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

Lines changed: 57 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub enum OverlaysType {
6161
}
6262

6363
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
64-
#[serde(default = "OverlaysVisibilitySettings::default")]
64+
#[serde(default)]
6565
pub struct OverlaysVisibilitySettings {
6666
pub all: bool,
6767
pub artboard_name: bool,
@@ -910,7 +910,7 @@ impl OverlayContext {
910910

911911
pub fn draw_path_from_subpaths(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, stroke_transform: DAffine2) -> bool {
912912
// Subpaths on a layer is considered "closed" only if all subpaths are closed.
913-
let mut is_closed = true;
913+
let mut is_closed_on_all = true;
914914
self.render_context.begin_path();
915915
for subpath in subpaths {
916916
let subpath = subpath.borrow().clone();
@@ -954,10 +954,10 @@ impl OverlayContext {
954954
if subpath.closed() {
955955
self.render_context.close_path();
956956
} else {
957-
is_closed = false;
957+
is_closed_on_all = false;
958958
}
959959
}
960-
return is_closed;
960+
return is_closed_on_all;
961961
}
962962

963963
/// Used by the Select tool to outline a path or a free point when selected or hovered.
@@ -1031,17 +1031,18 @@ impl OverlayContext {
10311031
return pattern;
10321032
}
10331033

1034-
/// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space.
1035-
/// 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.
1034+
/// Used by the Pen tool to show the path being closed.
10361035
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
10371036
self.draw_path_from_subpaths(subpaths, transform);
10381037

1039-
let color_str = format!("#{:?}", color.to_rgba_hex_srgb());
1040-
self.render_context.set_fill_style_str(&color_str.as_str());
1038+
let color_str = format!("#{}", color.to_rgba_hex_srgb());
1039+
self.render_context.set_fill_style_str(&color_str);
10411040
self.render_context.fill();
10421041
}
10431042

1044-
pub fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, layer_to_viewport: DAffine2, color: &Color, stroke: Option<Stroke>) {
1043+
/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
1044+
/// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
1045+
pub fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
10451046
// Render for elements with fill
10461047
// Render for elements with fill only
10471048
// Render for elements with fill and stroke
@@ -1053,13 +1054,15 @@ impl OverlayContext {
10531054

10541055
if let Some(stroke) = stroke {
10551056
let has_real_stroke = stroke.weight() > 0. && stroke.transform.matrix2.determinant() != 0.;
1056-
let applied_stroke_transform = if has_real_stroke { stroke.transform } else { layer_to_viewport };
1057-
let element_transform = if has_real_stroke { layer_to_viewport * stroke.transform.inverse() } else { DAffine2::IDENTITY };
1057+
let applied_stroke_transform = if has_real_stroke { stroke.transform } else { transform };
1058+
let element_transform = if has_real_stroke { transform * stroke.transform.inverse() } else { DAffine2::IDENTITY };
10581059

10591060
let [a, b, c, d, e, f] = element_transform.to_cols_array();
10601061
self.render_context.transform(a, b, c, d, e, f).expect("element_transform should be set to render stroke properly");
10611062

1062-
let is_closed = self.draw_path_from_subpaths(subpaths, applied_stroke_transform);
1063+
// For layers with open subpaths, stroke align is ignored and set to default
1064+
let is_closed_on_all = self.draw_path_from_subpaths(subpaths, applied_stroke_transform);
1065+
let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center };
10631066

10641067
let do_fill = || {
10651068
self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color));
@@ -1076,37 +1079,24 @@ impl OverlayContext {
10761079
.expect("Failed to set global composite operation");
10771080
};
10781081

1079-
if is_closed {
1080-
match (stroke.align, stroke.paint_order) {
1081-
(StrokeAlign::Inside, PaintOrder::StrokeAbove) => {
1082-
do_fill();
1083-
composite_mode("destination-out");
1084-
do_stroke(stroke.weight() * 2.);
1085-
}
1086-
(StrokeAlign::Inside, PaintOrder::StrokeBelow) => do_fill(),
1087-
(StrokeAlign::Center, PaintOrder::StrokeAbove) => {
1088-
do_fill();
1089-
composite_mode("destination-out");
1090-
do_stroke(stroke.weight());
1091-
}
1092-
(StrokeAlign::Center, PaintOrder::StrokeBelow) => {
1093-
do_fill();
1094-
}
1095-
// Paint order does not affect this
1096-
(StrokeAlign::Outside, _) => {
1097-
do_fill();
1098-
}
1082+
match (stroke_align, stroke.paint_order) {
1083+
(StrokeAlign::Inside, PaintOrder::StrokeAbove) => {
1084+
do_fill();
1085+
composite_mode("destination-out");
1086+
do_stroke(stroke.weight() * 2.);
10991087
}
1100-
} else {
1101-
match stroke.paint_order {
1102-
PaintOrder::StrokeAbove => {
1103-
do_fill();
1104-
composite_mode("destination-out");
1105-
do_stroke(stroke.weight());
1106-
}
1107-
PaintOrder::StrokeBelow => {
1108-
do_fill();
1109-
}
1088+
(StrokeAlign::Inside, PaintOrder::StrokeBelow) => do_fill(),
1089+
(StrokeAlign::Center, PaintOrder::StrokeAbove) => {
1090+
do_fill();
1091+
composite_mode("destination-out");
1092+
do_stroke(stroke.weight());
1093+
}
1094+
(StrokeAlign::Center, PaintOrder::StrokeBelow) => {
1095+
do_fill();
1096+
}
1097+
// Paint order does not affect this
1098+
(StrokeAlign::Outside, _) => {
1099+
do_fill();
11101100
}
11111101
}
11121102
} else {
@@ -1118,8 +1108,9 @@ impl OverlayContext {
11181108
self.render_context.restore();
11191109
}
11201110

1121-
// WARN: Don't use source-in, destination-atop, destination-in, copy
1122-
// on the main canvas as it will erase the existing overlays
1111+
/// Fills the shape's stroke region with a pattern of the given color. Assumes `color` is in gamma space.
1112+
/// WARN: Don't use source-in, destination-atop, destination-in, copy
1113+
/// on the main canvas as it will erase the existing overlays
11231114
pub fn stroke_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, layer_to_viewport: DAffine2, color: &Color, stroke: Option<Stroke>) {
11241115
// Render for elements with stroke
11251116
//----StrokeAlign
@@ -1138,7 +1129,9 @@ impl OverlayContext {
11381129
let [a, b, c, d, e, f] = element_transform.to_cols_array();
11391130
self.render_context.transform(a, b, c, d, e, f).expect("element_transform should be set to render stroke properly");
11401131

1141-
let is_closed = self.draw_path_from_subpaths(subpaths, applied_stroke_transform);
1132+
// For layers with open subpaths, stroke align is ignored and set to default
1133+
let is_closed_on_all = self.draw_path_from_subpaths(subpaths, applied_stroke_transform);
1134+
let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center };
11421135

11431136
let do_stroke = |stroke_weight: f64| {
11441137
self.render_context.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(color));
@@ -1158,39 +1151,26 @@ impl OverlayContext {
11581151
.expect("Failed to set global composite operation");
11591152
};
11601153

1161-
if is_closed {
1162-
match (stroke.align, stroke.paint_order) {
1163-
(StrokeAlign::Inside, PaintOrder::StrokeAbove) => {
1164-
// Clips away the stroke lying outside the path drawn from the subpaths
1165-
self.render_context.clip();
1166-
do_stroke(stroke.weight() * 2.);
1167-
}
1168-
(StrokeAlign::Inside, PaintOrder::StrokeBelow) => {}
1169-
(StrokeAlign::Center, PaintOrder::StrokeAbove) => {
1170-
do_stroke(stroke.weight());
1171-
}
1172-
(StrokeAlign::Center, PaintOrder::StrokeBelow) => {
1173-
do_stroke(stroke.weight());
1174-
composite_mode("destination-out");
1175-
do_fill();
1176-
}
1177-
// Paint order does not affect this
1178-
(StrokeAlign::Outside, _) => {
1179-
do_stroke(stroke.weight() * 2.);
1180-
composite_mode("destination-out");
1181-
do_fill();
1182-
}
1154+
match (stroke_align, stroke.paint_order) {
1155+
(StrokeAlign::Inside, PaintOrder::StrokeAbove) => {
1156+
// Clips away the stroke lying outside the path drawn from the subpaths
1157+
self.render_context.clip();
1158+
do_stroke(stroke.weight() * 2.);
11831159
}
1184-
} else {
1185-
match stroke.paint_order {
1186-
PaintOrder::StrokeAbove => {
1187-
do_stroke(stroke.weight());
1188-
}
1189-
PaintOrder::StrokeBelow => {
1190-
do_stroke(stroke.weight());
1191-
composite_mode("destination-out");
1192-
do_fill();
1193-
}
1160+
(StrokeAlign::Inside, PaintOrder::StrokeBelow) => {}
1161+
(StrokeAlign::Center, PaintOrder::StrokeAbove) => {
1162+
do_stroke(stroke.weight());
1163+
}
1164+
(StrokeAlign::Center, PaintOrder::StrokeBelow) => {
1165+
do_stroke(stroke.weight());
1166+
composite_mode("destination-out");
1167+
do_fill();
1168+
}
1169+
// Paint order does not affect this
1170+
(StrokeAlign::Outside, _) => {
1171+
do_stroke(stroke.weight() * 2.);
1172+
composite_mode("destination-out");
1173+
do_fill();
11941174
}
11951175
}
11961176
}

0 commit comments

Comments
 (0)