Skip to content
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ web-sys = { version = "=0.3.77", features = [
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"CanvasPattern",
"DomMatrix",
"SvgMatrix",
"OffscreenCanvas",
"OffscreenCanvasRenderingContext2d",
"TextMetrics",
Expand Down
18 changes: 18 additions & 0 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
OverlaysType::Handles => visibility_settings.handles = visible,
OverlaysType::FillableIndicator => visibility_settings.fillable_indicator = visible,
}

responses.add(EventMessage::ToolAbort);
Expand Down Expand Up @@ -2733,6 +2734,23 @@ impl DocumentMessageHandler {
.widget_instance(),
]
}),
LayoutGroup::row(vec![TextLabel::new("Fill Tool").widget_instance()]),
LayoutGroup::row({
let checkbox_id = CheckboxId::new();
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)
.widget_instance(),
TextLabel::new("Fillable Indicator".to_string()).for_checkbox(checkbox_id).widget_instance(),
]
}),
]))
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 @@ -66,6 +66,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
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 @@ -495,6 +495,16 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
}

pub fn stroke_color_set(&mut self, color: Option<Color>) {
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, false) else {
return;
};

let stroke_color = if let Some(color) = color { Table::new_from_element(color) } else { Table::new() };
let input_connector = InputConnector::node(stroke_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_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 @@ -50,20 +50,20 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
canvas_context.clear_rect(0., 0., width, height);

if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays {
context: OverlayContext {
render_context: canvas_context.clone(),
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
},
});
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext {
render_context: canvas_context.clone(),
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
}));
}
responses.add(DocumentMessage::GridOverlays {
context: OverlayContext {
render_context: canvas_context.clone(),
visibility_settings: visibility_settings.clone(),
viewport: *viewport,
},
});
}
}
#[cfg(all(not(target_family = "wasm"), not(test)))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::table::Table;
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::style::Stroke;
use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, BezPath, ParamCurve};
use kurbo::{Affine, PathSeg};
Expand Down Expand Up @@ -45,19 +47,32 @@
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum OverlaysType {
// =======
// General
// =======
ArtboardName,
CompassRose,
QuickMeasurement,
TransformMeasurement,
// ===========
// Select Tool
// ===========
QuickMeasurement,
TransformCage,
CompassRose,
Pivot,
Origin,
HoverOutline,
SelectionOutline,
LayerOriginCross,
Pivot,
Origin,
// ================
// Pen & Path Tools
// ================
Path,
Anchors,
Handles,
// =========
// Fill Tool
// =========
FillableIndicator,
}

// TODO Remove duplicated definition of this in `utility_types_web.rs`
Expand All @@ -67,18 +82,19 @@
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 origin: bool,
pub hover_outline: bool,
pub selection_outline: bool,
pub layer_origin_cross: bool,
pub pivot: bool,
pub origin: bool,
pub path: bool,
pub anchors: bool,
pub handles: bool,
pub fillable_indicator: bool,
}

// TODO Remove duplicated definition of this in `utility_types_web.rs`
Expand All @@ -87,18 +103,19 @@
Self {
all: true,
artboard_name: true,
compass_rose: true,
quick_measurement: true,
transform_measurement: true,
quick_measurement: true,
transform_cage: true,
compass_rose: true,
pivot: true,
origin: true,
hover_outline: true,
selection_outline: true,
layer_origin_cross: true,
pivot: true,
origin: true,
path: true,
anchors: true,
handles: true,
fillable_indicator: true,
}
}
}
Expand Down Expand Up @@ -160,6 +177,10 @@
pub fn handles(&self) -> bool {
self.all && self.anchors && self.handles
}

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

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
Expand Down Expand Up @@ -405,10 +426,16 @@
self.internal().fill_path(subpaths, transform, color);
}

/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
/// Fills the shape's fill region with a pattern of the given color. 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: &str) {
self.internal().fill_path_pattern(subpaths, transform, color);
pub fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
self.internal().fill_overlay(subpaths, transform, color, stroke);
}

/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
/// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
pub fn stroke_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
self.internal().stroke_overlay(subpaths, transform, color, stroke);
}

pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
Expand Down Expand Up @@ -975,7 +1002,7 @@
path.push(bezier.as_path_el());
}

fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
fn path_from_subpaths(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
let mut path = BezPath::new();

for subpath in subpaths {
Expand Down Expand Up @@ -1038,31 +1065,21 @@
}

if !subpaths.is_empty() {
let path = self.push_path(subpaths.iter(), transform);
let path = self.path_from_subpaths(subpaths.iter(), transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);

self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path);
}
}

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

self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
}

/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
/// Used by the fill tool to show the area to be filled.
fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
pub fn fill_canvas_pattern_image(&self, color: &Color) -> peniko::ImageBrush {
const PATTERN_WIDTH: u32 = 4;
const PATTERN_HEIGHT: u32 = 4;

// Create a 4x4 pixel pattern with colored pixels at (0,0) and (2,2)
// This matches the Canvas2D checkerboard pattern
let mut data = vec![0u8; (PATTERN_WIDTH * PATTERN_HEIGHT * 4) as usize];
let rgba = hex_to_rgba_u8(color);
let rgba = color.to_rgba8_srgb();

// ┌▄▄┬──┬──┬──┐
// ├▀▀┼──┼──┼──┤
Expand Down Expand Up @@ -1092,12 +1109,37 @@
},
};

let path = self.push_path(subpaths, transform);
let brush = peniko::Brush::Image(image);
image
}

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

self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
}

/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
/// Used by the fill tool to show the area to be filled.
fn fill_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {

Check failure on line 1125 in editor/src/messages/portfolio/document/overlays/utility_types_native.rs

View workflow job for this annotation

GitHub Actions / test

unused variable: `stroke`
let path = self.path_from_subpaths(subpaths, transform);
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color));

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

/// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space.
/// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
pub fn stroke_overlay(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color, stroke: Option<Stroke>) {
let path = self.path_from_subpaths(subpaths, transform);

if let Some(stroke) = stroke {
let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(&color));
self.scene.stroke(&kurbo::Stroke::new(stroke.weight), self.get_transform(), &brush, None, &path);
}
}

fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
// Use the proper text-to-path system for accurate text rendering
const FONT_SIZE: f64 = 12.;
Expand Down
Loading
Loading