Implement clipping masks, stroke align, and stroke paint order (#2644)

* refactor: opacity + blend_mode -> blend_style

* Add code for clipping

* Add alt-click masking

* Clip to all colors. Fill option

* Fix undo not working. Fix strokes not being white

* Allow clipped to be grouped or raster

* Switch to alpha mode in mask-type

* add plumbing to know if clipped in frontend and add fill slider

* Attempt at document upgrade code

* Fix fill slider

* Add clipped styling and Alt-click layer border

* Use mask attr judiciously by using clip when possible

* Fix breaking documents and upgrade code

* Fix fixes

* No-op toggle if last child of parent and don't show clip UI if last element

* Fix mouse styles by plumbing clippable to frontend

* Fix Clip detection by disallowed groups as clipPath according to SVG spec doesn't allow <g>

* Add opacity to clippers can_use_clip check

* Fix issue with clipping not working nicely with strokes by using masks

* Add vello code

* cleanup

* Add stroke alignment hacks to SVG renderer

* svg: Fix mask bounds in vector data

* vello: Implement mask hacks to support stroke alignment

* Move around alignment and doc upgrade code

* rename Line X -> X

* An attempt at fixing names not updating

* svg: add stroke order with svg

* vello: add stroke order with by calling one before the other explicitly

* fix merge

* fix svg renderer messing up transform det

* Code review; reorder and rename parameters (TODO: fix tools)

* Fixes to previous

* Formatting

* fix bug 3

* some moving around (not fixed)

* fix issue 1

* fix vello

* Final code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mTvare
2025-06-19 19:11:01 -07:00
committed by Keavon Chambers
co-authored by Keavon Chambers
parent 8a3f133140
commit e238753a35
28 changed files with 1025 additions and 311 deletions
@@ -121,6 +121,9 @@ pub enum DocumentMessage {
SelectedLayersReorder {
relative_index_offset: isize,
},
ClipLayer {
id: NodeId,
},
SelectLayer {
id: NodeId,
ctrl: bool,
@@ -142,6 +145,9 @@ pub enum DocumentMessage {
SetOpacityForSelectedLayers {
opacity: f64,
},
SetFillForSelectedLayers {
fill: f64,
},
SetOverlaysVisibility {
visible: bool,
overlays_type: Option<OverlaysType>,
@@ -20,7 +20,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
@@ -1083,6 +1083,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
DocumentMessage::SelectedLayersReorder { relative_index_offset } => {
self.selected_layers_reorder(relative_index_offset, responses);
}
DocumentMessage::ClipLayer { id } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
responses.add(DocumentMessage::AddTransaction);
responses.add(GraphOperationMessage::ClipModeToggle { layer });
}
DocumentMessage::SelectLayer { id, ctrl, shift } => {
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
@@ -1177,6 +1183,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
responses.add(GraphOperationMessage::OpacitySet { layer, opacity });
}
}
DocumentMessage::SetFillForSelectedLayers { fill } => {
let fill = fill.clamp(0., 1.);
for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) {
responses.add(GraphOperationMessage::BlendingFillSet { layer, fill });
}
}
DocumentMessage::SetOverlaysVisibility { visible, overlays_type } => {
let visibility_settings = &mut self.overlays_visibility_settings;
let overlays_type = match overlays_type {
@@ -2533,38 +2545,47 @@ impl DocumentMessageHandler {
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
let mut opacity_and_blend_mode = selected_layers_except_artboards.map(|layer| {
let mut blending_options = selected_layers_except_artboards.map(|layer| {
(
get_opacity(layer, &self.network_interface).unwrap_or(100.),
get_fill(layer, &self.network_interface).unwrap_or(100.),
get_blend_mode(layer, &self.network_interface).unwrap_or_default(),
)
});
let first_opacity_and_blend_mode = opacity_and_blend_mode.next();
let result_opacity_and_blend_mode = opacity_and_blend_mode;
let first_blending_options = blending_options.next();
let result_blending_options = blending_options;
// If there are no selected layers, disable the opacity and blend mode widgets.
let disabled = first_opacity_and_blend_mode.is_none();
let disabled = first_blending_options.is_none();
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
// If identical, we display the value in the widget. If not, we display a dash indicating dissimilarity.
let (opacity, blend_mode) = first_opacity_and_blend_mode
.map(|(first_opacity, first_blend_mode)| {
let (opacity, fill, blend_mode) = first_blending_options
.map(|(first_opacity, first_fill, first_blend_mode)| {
let mut opacity_identical = true;
let mut fill_identical = true;
let mut blend_mode_identical = true;
for (opacity, blend_mode) in result_opacity_and_blend_mode {
for (opacity, fill, blend_mode) in result_blending_options {
if (opacity - first_opacity).abs() > (f64::EPSILON * 100.) {
opacity_identical = false;
}
if (fill - first_fill).abs() > (f64::EPSILON * 100.) {
fill_identical = false;
}
if blend_mode != first_blend_mode {
blend_mode_identical = false;
}
}
(opacity_identical.then_some(first_opacity), blend_mode_identical.then_some(first_blend_mode))
(
opacity_identical.then_some(first_opacity),
fill_identical.then_some(first_fill),
blend_mode_identical.then_some(first_blend_mode),
)
})
.unwrap_or((None, None));
.unwrap_or((None, None, None));
let blend_mode_menu_entries = BlendMode::list_svg_subset()
.iter()
@@ -2623,6 +2644,28 @@ impl DocumentMessageHandler {
.max_width(100)
.tooltip("Opacity")
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
NumberInput::new(fill)
.label("Fill")
.unit("%")
.display_decimal_places(0)
.disabled(disabled)
.min(0.)
.max(100.)
.range_min(Some(0.))
.range_max(Some(100.))
.mode_range()
.on_update(|number_input: &NumberInput| {
if let Some(value) = number_input.value {
DocumentMessage::SetFillForSelectedLayers { fill: value / 100. }.into()
} else {
Message::NoOp
}
})
.on_commit(|_| DocumentMessage::AddTransaction.into())
.max_width(100)
.tooltip("Fill")
.widget_holder(),
];
let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
@@ -21,6 +21,10 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
fill: Fill,
},
BlendingFillSet {
layer: LayerNodeIdentifier,
fill: f64,
},
OpacitySet {
layer: LayerNodeIdentifier,
opacity: f64,
@@ -29,6 +33,9 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier,
blend_mode: BlendMode,
},
ClipModeToggle {
layer: LayerNodeIdentifier,
},
StrokeSet {
layer: LayerNodeIdentifier,
stroke: Stroke,
@@ -5,12 +5,13 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
use graphene_core::renderer::Quad;
use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke};
use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::convert_usvg_path;
#[derive(Debug, Clone)]
@@ -41,6 +42,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.fill_set(fill);
}
}
GraphOperationMessage::BlendingFillSet { layer, fill } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.blending_fill_set(fill);
}
}
GraphOperationMessage::OpacitySet { layer, opacity } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.opacity_set(opacity);
@@ -51,6 +57,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
modify_inputs.blend_mode_set(blend_mode);
}
}
GraphOperationMessage::ClipModeToggle { layer } => {
let clip_mode = get_clip_mode(layer, network_interface);
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.clip_mode_toggle(clip_mode);
}
}
GraphOperationMessage::StrokeSet { layer, stroke } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.stroke_set(stroke);
@@ -376,18 +388,20 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
weight: stroke.width().get() as f64,
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
dash_offset: stroke.dashoffset() as f64,
line_cap: match stroke.linecap() {
usvg::LineCap::Butt => LineCap::Butt,
usvg::LineCap::Round => LineCap::Round,
usvg::LineCap::Square => LineCap::Square,
cap: match stroke.linecap() {
usvg::LineCap::Butt => StrokeCap::Butt,
usvg::LineCap::Round => StrokeCap::Round,
usvg::LineCap::Square => StrokeCap::Square,
},
line_join: match stroke.linejoin() {
usvg::LineJoin::Miter => LineJoin::Miter,
usvg::LineJoin::MiterClip => LineJoin::Miter,
usvg::LineJoin::Round => LineJoin::Round,
usvg::LineJoin::Bevel => LineJoin::Bevel,
join: match stroke.linejoin() {
usvg::LineJoin::Miter => StrokeJoin::Miter,
usvg::LineJoin::MiterClip => StrokeJoin::Miter,
usvg::LineJoin::Round => StrokeJoin::Round,
usvg::LineJoin::Bevel => StrokeJoin::Bevel,
},
line_join_miter_limit: stroke.miterlimit().get() as f64,
join_miter_limit: stroke.miterlimit().get() as f64,
align: StrokeAlign::Center,
paint_order: PaintOrder::StrokeAbove,
transform,
non_scaling: false,
})
@@ -15,8 +15,8 @@ use graphene_core::text::{Font, TypesettingConfig};
use graphene_core::vector::brush_stroke::BrushStroke;
use graphene_core::vector::style::{Fill, Stroke};
use graphene_core::vector::{PointId, VectorModificationType};
use graphene_std::GraphicGroupTable;
use graphene_std::vector::{VectorData, VectorDataTable};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum TransformIn {
@@ -58,13 +58,13 @@ impl<'a> ModifyInputsContext<'a> {
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
/// -----> Post node
/// | if insert_index == 0, return (Post node, Some(Layer1))
/// -> Layer1
/// -> Layer1
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
/// -> Layer2
/// -> Layer2
/// ↑
/// -> NonLayerNode
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
/// -> Layer3
/// -> Layer3
/// if insert_index == 3, return (Layer3, None)
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
@@ -333,37 +333,52 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
}
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
}
pub fn opacity_set(&mut self, opacity: f64) {
let Some(opacity_node_id) = self.existing_node_id("Opacity", true) else { return };
let input_connector = InputConnector::node(opacity_node_id, 1);
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 2);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
}
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
let Some(blend_mode_node_id) = self.existing_node_id("Blend Mode", true) else {
return;
};
let input_connector = InputConnector::node(blend_mode_node_id, 1);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
pub fn blending_fill_set(&mut self, fill: f64) {
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(blend_node_id, 3);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
}
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
let clip = !clip_mode.unwrap_or(false);
let Some(clip_node_id) = self.existing_node_id("Blending", true) else { return };
let input_connector = InputConnector::node(clip_node_id, 4);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
}
pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
let input_connector = InputConnector::node(stroke_node_id, 1);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Option<graphene_std::Color>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
let input_connector = InputConnector::node(stroke_node_id, 2);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, 3);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::VecF64(stroke.dash_lengths), false), true);
let input_connector = InputConnector::node(stroke_node_id, 4);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
let input_connector = InputConnector::node(stroke_node_id, 5);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineCap(stroke.line_cap), false), true);
let input_connector = InputConnector::node(stroke_node_id, 6);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineJoin(stroke.line_join), false), true);
let input_connector = InputConnector::node(stroke_node_id, 7);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false);
}
/// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform.
@@ -15,6 +15,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::{DAffine2, DVec2, IVec2};
@@ -2442,6 +2443,7 @@ impl NodeGraphMessageHandler {
}
});
let clippable = layer.can_be_clipped(network_interface.document_metadata());
let data = LayerPanelEntry {
id: node_id,
alias: network_interface.display_name(&node_id, &[]),
@@ -2461,6 +2463,8 @@ impl NodeGraphMessageHandler {
selected: selected_layers.contains(&node_id),
ancestor_of_selected: ancestors_of_selected.contains(&node_id),
descendant_of_selected: descendants_of_selected.contains(&node_id),
clipped: get_clip_mode(layer, network_interface).unwrap_or(false) && clippable,
clippable,
};
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
}
@@ -20,7 +20,7 @@ use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
use graphene_core::text::Font;
use graphene_core::vector::generator_nodes::grid;
use graphene_core::vector::misc::CentroidType;
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
use graphene_core::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::animation::RealTimeMode;
use graphene_std::ops::XY;
use graphene_std::transform::{Footprint, ReferencePoint};
@@ -233,8 +233,10 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<DomainWarpType>() => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
Some(x) if x == TypeId::of::<RelativeAbsolute>() => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
Some(x) if x == TypeId::of::<GridType>() => enum_choice::<GridType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LineCap>() => enum_choice::<LineCap>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<LineJoin>() => enum_choice::<LineJoin>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<StrokeCap>() => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<StrokeJoin>() => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
@@ -1679,20 +1681,43 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
return Vec::new();
}
};
let color_index = 1;
let weight_index = 2;
let dash_lengths_index = 3;
let dash_offset_index = 4;
let line_cap_index = 5;
let line_join_index = 6;
let miter_limit_index = 7;
let color_index = graphene_std::vector::stroke::ColorInput::<Option<Color>>::INDEX;
let weight_index = graphene_std::vector::stroke::WeightInput::INDEX;
let align_index = graphene_std::vector::stroke::AlignInput::INDEX;
let cap_index = graphene_std::vector::stroke::CapInput::INDEX;
let join_index = graphene_std::vector::stroke::JoinInput::INDEX;
let miter_limit_index = graphene_std::vector::stroke::MiterLimitInput::INDEX;
let paint_order_index = graphene_std::vector::stroke::PaintOrderInput::INDEX;
let dash_lengths_index = graphene_std::vector::stroke::DashLengthsInput::INDEX;
let dash_offset_index = graphene_std::vector::stroke::DashOffsetInput::INDEX;
let color = color_widget(ParameterWidgetsInfo::from_index(document_node, node_id, color_index, true, context), ColorInput::default());
let weight = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, weight_index, true, context),
NumberInput::default().unit(" px").min(0.),
);
let align = enum_choice::<StrokeAlign>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, align_index, true, context))
.property_row();
let cap = enum_choice::<StrokeCap>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, cap_index, true, context))
.property_row();
let join = enum_choice::<StrokeJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context))
.property_row();
let miter_limit = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context),
NumberInput::default().min(0.).disabled({
let join_value = match &document_node.inputs[join_index].as_value() {
Some(TaggedValue::StrokeJoin(x)) => x,
_ => &StrokeJoin::Miter,
};
join_value != &StrokeJoin::Miter
}),
);
let paint_order = enum_choice::<PaintOrder>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, paint_order_index, true, context))
.property_row();
let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() {
Some(TaggedValue::VecF64(x)) => x,
_ => &vec![],
@@ -1703,29 +1728,17 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
);
let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty());
let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input);
let line_cap = enum_choice::<LineCap>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context))
.property_row();
let line_join = enum_choice::<LineJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context))
.property_row();
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
Some(TaggedValue::LineJoin(x)) => x,
_ => &LineJoin::Miter,
};
let miter_limit = number_widget(
ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context),
NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter),
);
vec![
color,
LayoutGroup::Row { widgets: weight },
align,
cap,
join,
LayoutGroup::Row { widgets: miter_limit },
paint_order,
LayoutGroup::Row { widgets: dash_lengths },
LayoutGroup::Row { widgets: dash_offset },
line_cap,
line_join,
LayoutGroup::Row { widgets: miter_limit },
]
}
@@ -1737,25 +1750,27 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte
return Vec::new();
}
};
let distance_index = 1;
let line_join_index = 2;
let miter_limit_index = 3;
let distance_index = graphene_std::vector::offset_path::DistanceInput::INDEX;
let join_index = graphene_std::vector::offset_path::JoinInput::INDEX;
let miter_limit_index = graphene_std::vector::offset_path::MiterLimitInput::INDEX;
let number_input = NumberInput::default().unit(" px");
let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input);
let line_join = enum_choice::<LineJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context))
let join = enum_choice::<StrokeJoin>()
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context))
.property_row();
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
Some(TaggedValue::LineJoin(x)) => x,
_ => &LineJoin::Miter,
};
let number_input = NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter);
let number_input = NumberInput::default().min(0.).disabled({
let join_val = match &document_node.inputs[join_index].as_value() {
Some(TaggedValue::StrokeJoin(x)) => x,
_ => &StrokeJoin::Miter,
};
join_val != &StrokeJoin::Miter
});
let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), number_input);
vec![LayoutGroup::Row { widgets: distance }, line_join, LayoutGroup::Row { widgets: miter_limit }]
vec![LayoutGroup::Row { widgets: distance }, join, LayoutGroup::Row { widgets: miter_limit }]
}
pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
@@ -287,6 +287,12 @@ impl LayerNodeIdentifier {
child.ancestors(metadata).any(|ancestor| ancestor == self)
}
/// Is the layer last child of parent group? Used for clipping
pub fn can_be_clipped(self, metadata: &DocumentMetadata) -> bool {
self.parent(metadata)
.map_or(false, |layer| layer.last_child(metadata).expect("Parent accessed via child should have children") != self)
}
/// Iterator over all direct children (excluding self and recursive children)
pub fn children(self, metadata: &DocumentMetadata) -> AxisIter {
AxisIter {
@@ -1160,6 +1160,13 @@ impl NodeNetworkInterface {
.and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index))
}
pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId]) {
let row = ("", "TODO").into();
let _ = self
.node_metadata_mut(node_id, network_path)
.map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row));
}
pub fn input_metadata(&self, node_id: &NodeId, index: usize, field: &str, network_path: &[NodeId]) -> Option<&Value> {
let Some(input_row) = self.input_properties_row(node_id, index, network_path) else {
log::error!("Could not get input_row in get_input_metadata");
@@ -55,6 +55,8 @@ pub struct LayerPanelEntry {
pub ancestor_of_selected: bool,
#[serde(rename = "descendantOfSelected")]
pub descendant_of_selected: bool,
pub clipped: bool,
pub clippable: bool,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]