mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 01:28:12 +08:00
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:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
8a3f133140
commit
e238753a35
@@ -187,7 +187,8 @@ impl PreferencesDialogMessageHandler {
|
||||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills.";
|
||||
let vector_mesh_tooltip =
|
||||
"Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle stroke joins and fills.";
|
||||
let vector_meshes = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
+25
-11
@@ -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)]
|
||||
|
||||
@@ -24,7 +24,7 @@ use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient};
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use std::vec;
|
||||
|
||||
@@ -678,6 +678,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||
if reference == "Stroke" && inputs_count == 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document.network_interface.insert_input_properties_row(node_id, 8, network_path);
|
||||
document.network_interface.insert_input_properties_row(node_id, 9, network_path);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
|
||||
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||
}
|
||||
|
||||
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
|
||||
if reference == "Splines from Points" {
|
||||
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
|
||||
|
||||
@@ -13,6 +13,7 @@ use graphene_core::raster::BlendMode;
|
||||
use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_core::vector::style::Gradient;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
@@ -258,7 +259,7 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
||||
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
|
||||
}
|
||||
|
||||
/// Get the current gradient of a layer from the closest Fill node
|
||||
/// Get the current gradient of a layer from the closest "Fill" node.
|
||||
pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
let fill_index = 1;
|
||||
|
||||
@@ -269,7 +270,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
||||
Some(gradient.clone())
|
||||
}
|
||||
|
||||
/// Get the current fill of a layer from the closest Fill node
|
||||
/// Get the current fill of a layer from the closest "Fill" node.
|
||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||
let fill_index = 1;
|
||||
|
||||
@@ -280,16 +281,16 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
||||
Some(color.to_linear_srgb())
|
||||
}
|
||||
|
||||
/// Get the current blend mode of a layer from the closest Blend Mode node
|
||||
/// Get the current blend mode of a layer from the closest "Blending" node.
|
||||
pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<BlendMode> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blend Mode")?;
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*blend_mode)
|
||||
}
|
||||
|
||||
/// Get the current opacity of a layer from the closest Opacity node.
|
||||
/// Get the current opacity of a layer from the closest "Blending" node.
|
||||
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
|
||||
/// - Multiplied with additional opacity nodes earlier in the chain
|
||||
/// - Set by an Opacity node with an exposed input value driven by another node
|
||||
@@ -298,13 +299,29 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
||||
///
|
||||
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
|
||||
pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Opacity")?;
|
||||
let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*opacity)
|
||||
}
|
||||
|
||||
pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*clip)
|
||||
}
|
||||
|
||||
pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::F64(fill) = inputs.get(3)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*fill)
|
||||
}
|
||||
|
||||
pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
|
||||
}
|
||||
@@ -356,7 +373,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
||||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let weight_node_input_index = 2;
|
||||
let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX;
|
||||
if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input("Stroke", weight_node_input_index)? {
|
||||
Some(*width)
|
||||
} else {
|
||||
|
||||
@@ -338,7 +338,15 @@ impl NodeGraphExecutor {
|
||||
fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
|
||||
// Setup rendering
|
||||
let mut render = SvgRender::new();
|
||||
let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false);
|
||||
let render_params = RenderParams {
|
||||
view_mode: ViewMode::Normal,
|
||||
culling_bounds: None,
|
||||
thumbnail: false,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
};
|
||||
|
||||
// Render SVG
|
||||
render_object.render_svg(&mut render, &render_params);
|
||||
|
||||
@@ -323,7 +323,15 @@ impl NodeRuntime {
|
||||
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY, true);
|
||||
|
||||
// Render the thumbnail from a `GraphicElement` into an SVG string
|
||||
let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false);
|
||||
let render_params = RenderParams {
|
||||
view_mode: ViewMode::Normal,
|
||||
culling_bounds: bounds,
|
||||
thumbnail: true,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
};
|
||||
let mut render = SvgRender::new();
|
||||
graphic_element.render_svg(&mut render, &render_params);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user