mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Adopt the cascading "appearance" attribute in place of Vector::stroke and the "fill"/"paint" attributes (#4433)
* Add the appearance model types and attribute constants * Dual-write the appearance attribute alongside the fill/stroke pair in all paint-writing nodes * Read paint from the appearance attribute in the renderer, analysis, metadata, and editor, cascading from ancestors * Retire the fill/stroke attribute pair and the Vector stroke field in favor of the appearance attribute * Replace the Stroke node's paint order input with the relative chain order of the Fill and Stroke nodes * Code review fixes * Re-save demo art * Stamp coverages in place and fuse the renderer's per-item appearance reads into single walks * Treat an empty appearance as the undeclared state so padded rows inherit instead of blocking the cascade * Update demo art * Treat padded appearance rows as undeclared in the boolean flatten's group recursion
This commit is contained in:
@@ -8,5 +8,7 @@ pub enum EventMessage {
|
||||
CanvasTransformed,
|
||||
ToolAbort,
|
||||
SelectionChanged,
|
||||
/// The document graph's nodes or wires changed, so state derived from the selection's chains may be stale
|
||||
GraphChanged,
|
||||
WorkingColorChanged,
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ pub enum PathStep {
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorTableTab {
|
||||
#[default]
|
||||
Properties,
|
||||
Points,
|
||||
Segments,
|
||||
Regions,
|
||||
Handles,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup,
|
||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessage, PathStep};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{format_rounded, round_away_float_noise};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::{Affine2, DAffine2, Vec2};
|
||||
use graph_craft::document::NodeId;
|
||||
@@ -18,6 +18,7 @@ use graphene_std::raster::{
|
||||
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
||||
};
|
||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||
use graphene_std::subpath::BezierHandles;
|
||||
use graphene_std::text::TextAlign;
|
||||
use graphene_std::text_nodes::StringCapitalization;
|
||||
use graphene_std::transform::{ReferencePoint, ScaleType};
|
||||
@@ -25,10 +26,10 @@ use graphene_std::vector::misc::{
|
||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||
};
|
||||
use graphene_std::vector::style::{
|
||||
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin,
|
||||
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin,
|
||||
};
|
||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
||||
use graphene_std::{Artboard, Color, Context, Graphic};
|
||||
use graphene_std::{Appearance, Artboard, Color, Context, Cover, Coverage, Graphic};
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -228,7 +229,6 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
List<StrokeJoin>,
|
||||
List<StrokeAlign>,
|
||||
List<StrokeCap>,
|
||||
List<PaintOrder>,
|
||||
List<MergeByDistanceAlgorithm>,
|
||||
List<ExtrudeJoiningAlgorithm>,
|
||||
List<PointSpacingType>,
|
||||
@@ -284,7 +284,6 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
||||
Item<StrokeJoin>,
|
||||
Item<StrokeAlign>,
|
||||
Item<StrokeCap>,
|
||||
Item<PaintOrder>,
|
||||
Item<MergeByDistanceAlgorithm>,
|
||||
Item<ExtrudeJoiningAlgorithm>,
|
||||
Item<PointSpacingType>,
|
||||
@@ -513,6 +512,45 @@ impl TableItemLayout for DashPattern {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for Appearance {
|
||||
fn type_name() -> &'static str {
|
||||
"Appearance"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Appearance".to_string()
|
||||
}
|
||||
// The wrapping `Item` already contributes the breadcrumb; the inner list supplies the next level
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.value_page(data)
|
||||
}
|
||||
// Label the spreadsheet's element button with the inner list's identifier, like Artboard
|
||||
fn value_widgets(&self, target: PathStep, data: &LayoutData) -> Vec<WidgetInstance> {
|
||||
self.0.value_widgets(target, data)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.0.layout_with_breadcrumb(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for Coverage {
|
||||
fn type_name() -> &'static str {
|
||||
"Coverage"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Coverage".to_string()
|
||||
}
|
||||
// The wrapping row already contributes the breadcrumb; the inner item supplies the next level
|
||||
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.value_page(data)
|
||||
}
|
||||
fn value_widgets(&self, target: PathStep, data: &LayoutData) -> Vec<WidgetInstance> {
|
||||
self.0.value_widgets(target, data)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
self.0.layout_with_breadcrumb(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl TableItemLayout for BoxCorners {
|
||||
fn type_name() -> &'static str {
|
||||
"BoxCorners"
|
||||
@@ -581,7 +619,7 @@ impl TableItemLayout for Vector {
|
||||
)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions]
|
||||
let table_tab_entries = [VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions, VectorTableTab::Handles]
|
||||
.into_iter()
|
||||
.map(|tab| {
|
||||
RadioEntryData::new(format!("{tab:?}"))
|
||||
@@ -593,89 +631,49 @@ impl TableItemLayout for Vector {
|
||||
|
||||
let mut table_rows = Vec::new();
|
||||
match data.vector_table_tab {
|
||||
VectorTableTab::Properties => {
|
||||
table_rows.push(column_headings(&["property", "value"]));
|
||||
|
||||
if let Some(stroke) = self.stroke.as_ref() {
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Dash Lengths").narrow(true).widget_instance(),
|
||||
TextLabel::new(if stroke.dash_lengths.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
|
||||
})
|
||||
.narrow(true)
|
||||
.widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Dash Offset").narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{}", stroke.dash_offset)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Cap").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.cap.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Join").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.join.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Join Miter Limit").narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{}", stroke.join_miter_limit)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Align").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.align.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Transform").narrow(true).widget_instance(),
|
||||
TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.paint_order.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
}
|
||||
|
||||
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
|
||||
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Colinear Handle IDs").narrow(true).widget_instance(),
|
||||
TextLabel::new(colinear).narrow(true).widget_instance(),
|
||||
]);
|
||||
VectorTableTab::Handles => {
|
||||
table_rows.push(column_headings(&["", "colinear_manipulators[0]", "colinear_manipulators[1]"]));
|
||||
table_rows.extend(self.colinear_manipulators.iter().enumerate().map(|(index, [a, b])| {
|
||||
vec![
|
||||
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{a}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{b}")).narrow(true).widget_instance(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
VectorTableTab::Points => {
|
||||
table_rows.push(column_headings(&["", "position"]));
|
||||
table_rows.extend(self.point_domain.iter().map(|(id, position)| {
|
||||
let position = DVec2::new(round_away_float_noise(position.x), round_away_float_noise(position.y));
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{position}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format_dvec2(position)).narrow(true).widget_instance(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
VectorTableTab::Segments => {
|
||||
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
|
||||
table_rows.push(column_headings(&["", "start_point", "end_point", "handles"]));
|
||||
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
|
||||
let handles = match handles {
|
||||
BezierHandles::Linear => "Linear".to_string(),
|
||||
BezierHandles::Quadratic { handle } => format!("Quadratic — {}", format_dvec2(handle)),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => format!("Cubic — start: {}, end: {}", format_dvec2(handle_start), format_dvec2(handle_end)),
|
||||
};
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{start}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{end}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{handles:?}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("Point {start}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("Point {end}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(handles).narrow(true).widget_instance(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
VectorTableTab::Regions => {
|
||||
table_rows.push(column_headings(&["", "segment_range", "fill"]));
|
||||
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
|
||||
table_rows.push(column_headings(&["", "segment_range"]));
|
||||
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, _)| {
|
||||
vec![
|
||||
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{segment_range:?}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{}", fill.inner())).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("Segment {} – Segment {}", segment_range.start().inner(), segment_range.end().inner()))
|
||||
.narrow(true)
|
||||
.widget_instance(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
@@ -1019,6 +1017,7 @@ macro_rules! impl_table_item_layout_for_choice_enum {
|
||||
}
|
||||
impl_table_item_layout_for_choice_enum!(
|
||||
BlendMode,
|
||||
Cover,
|
||||
GradientForm,
|
||||
GradientSpread,
|
||||
GradientSpace,
|
||||
@@ -1027,7 +1026,6 @@ impl_table_item_layout_for_choice_enum!(
|
||||
StrokeJoin,
|
||||
StrokeAlign,
|
||||
StrokeCap,
|
||||
PaintOrder,
|
||||
MergeByDistanceAlgorithm,
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
@@ -1231,6 +1229,9 @@ macro_rules! known_item_types {
|
||||
Raster<GPU>,
|
||||
Graphic,
|
||||
Artboard,
|
||||
Appearance,
|
||||
Coverage,
|
||||
Cover,
|
||||
DashPattern,
|
||||
BoxCorners,
|
||||
BlendMode,
|
||||
@@ -1242,7 +1243,6 @@ macro_rules! known_item_types {
|
||||
StrokeJoin,
|
||||
StrokeAlign,
|
||||
StrokeCap,
|
||||
PaintOrder,
|
||||
MergeByDistanceAlgorithm,
|
||||
ExtrudeJoiningAlgorithm,
|
||||
PointSpacingType,
|
||||
|
||||
@@ -11,9 +11,8 @@ use crate::messages::portfolio::utility_types::PanelType;
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Appearance;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::transform::Footprint;
|
||||
@@ -245,14 +244,10 @@ pub enum DocumentMessage {
|
||||
vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||
},
|
||||
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
|
||||
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
|
||||
// `UpdateAppearanceAttributes` is produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
|
||||
#[serde(skip)]
|
||||
UpdateFillAttributes {
|
||||
fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
|
||||
},
|
||||
#[serde(skip)]
|
||||
UpdateStrokeAttributes {
|
||||
stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
|
||||
UpdateAppearanceAttributes {
|
||||
appearance_attributes: HashMap<NodeId, Arc<Appearance>>,
|
||||
},
|
||||
Undo,
|
||||
UngroupSelectedLayers,
|
||||
|
||||
@@ -37,7 +37,7 @@ use graph_craft::application_io::wgpu_available;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||
use graph_craft::list;
|
||||
use graphene_std::graphic::is_paint_present;
|
||||
use graphene_std::Cover;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||
use graphene_std::raster::BlendMode;
|
||||
@@ -1502,9 +1502,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
.collect();
|
||||
self.network_interface.update_vector_data(layer_vector_data);
|
||||
}
|
||||
DocumentMessage::UpdateFillAttributes { fill_attributes } => {
|
||||
DocumentMessage::UpdateAppearanceAttributes { appearance_attributes } => {
|
||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||
let layer_fill_attributes = fill_attributes
|
||||
let layer_appearance_attributes = appearance_attributes
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, attrs)| {
|
||||
@@ -1514,21 +1514,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_fill_attributes(layer_fill_attributes);
|
||||
}
|
||||
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
|
||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||
let layer_stroke_attributes = stroke_attributes
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, attrs)| {
|
||||
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
||||
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
||||
(layer, attrs)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
|
||||
self.network_interface.update_appearance_attributes(layer_appearance_attributes);
|
||||
}
|
||||
DocumentMessage::Undo => {
|
||||
if self.network_interface.transaction_status() != TransactionStatus::Finished {
|
||||
@@ -2763,20 +2749,21 @@ impl DocumentMessageHandler {
|
||||
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
||||
|
||||
for layer in selected_layers {
|
||||
let Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else {
|
||||
if !self.network_interface.document_metadata().layer_vector_data.contains_key(&layer) {
|
||||
resulting_layers.push(layer.to_node());
|
||||
continue;
|
||||
};
|
||||
let stroke = vector_data.stroke.as_ref();
|
||||
}
|
||||
|
||||
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
||||
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
||||
let appearance = self.network_interface.document_metadata().layer_appearance_attributes.get(&layer);
|
||||
|
||||
let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list));
|
||||
// `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint.
|
||||
// So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth.
|
||||
let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent()));
|
||||
let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
|
||||
let has_fill = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
|
||||
// A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
|
||||
let has_stroke = appearance.is_some_and(|appearance| {
|
||||
appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
|
||||
&& appearance
|
||||
.first_paint_of(Cover::Stroke)
|
||||
.is_some_and(|paint| paint.element(0).is_some_and(|graphic| !graphic.is_fully_transparent()))
|
||||
});
|
||||
|
||||
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
||||
if !has_stroke {
|
||||
|
||||
@@ -10,7 +10,7 @@ use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke};
|
||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
||||
use graphene_std::vector::{Gradient, PointId, VectorModificationType};
|
||||
|
||||
#[impl_message(Message, DocumentMessage, GraphOperation)]
|
||||
@@ -93,6 +93,10 @@ pub enum GraphOperationMessage {
|
||||
color: Option<Color>,
|
||||
stroke: Stroke,
|
||||
},
|
||||
StrokeOrderSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
paint_order: PaintOrder,
|
||||
},
|
||||
TransformChange {
|
||||
layer: LayerNodeIdentifier,
|
||||
transform: DAffine2,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::transform_utils;
|
||||
use super::utility_types::ModifyInputsContext;
|
||||
use super::utility_types::{ModifyInputsContext, set_stroke_paint_order};
|
||||
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::BLEND_PATH_INPUT_INDEX;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
||||
@@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput};
|
||||
use graph_craft::list;
|
||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::{Artboard, Color};
|
||||
|
||||
#[derive(ExtractField)]
|
||||
@@ -131,6 +131,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.stroke_set(color, stroke);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::StrokeOrderSet { layer, paint_order } => {
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let Some(stroke_node_id) = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, network_interface) else {
|
||||
return;
|
||||
};
|
||||
if set_stroke_paint_order(network_interface, &[], stroke_node_id, paint_order) {
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::TransformChange {
|
||||
layer,
|
||||
transform,
|
||||
@@ -944,7 +955,6 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
|
||||
},
|
||||
join_miter_limit: stroke.miterlimit().get() as f64,
|
||||
align: StrokeAlign::Center,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
transform,
|
||||
},
|
||||
)
|
||||
@@ -1026,6 +1036,81 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn stroke_order_set_reorders_the_fill_and_stroke_nodes() {
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_stroke_paint_order;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use graphene_std::vector::style::PaintOrder;
|
||||
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let layer = document.metadata().all_layers().next().unwrap();
|
||||
let paint_order = get_stroke_paint_order(layer, &document.network_interface);
|
||||
assert_eq!(paint_order, PaintOrder::StrokeAbove, "a fresh shape should stroke above its fill");
|
||||
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||
layer,
|
||||
paint_order: PaintOrder::StrokeBelow,
|
||||
})
|
||||
.await;
|
||||
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||
assert_eq!(paint_order, PaintOrder::StrokeBelow, "the rewrite should move the stroke downstream of the fill");
|
||||
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||
layer,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
})
|
||||
.await;
|
||||
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||
assert_eq!(paint_order, PaintOrder::StrokeAbove, "the rewrite should move the stroke back upstream of the fill");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_node_inserted_between_the_fill_and_stroke_makes_the_order_swap_inapplicable() {
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_node_type;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::stroke_paint_order_applicable;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||
assert!(
|
||||
stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||
"a fresh shape's adjacent Fill and Stroke pair should be swappable"
|
||||
);
|
||||
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let stroke_node_id = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, &editor.active_document().network_interface).unwrap();
|
||||
|
||||
let node_template = Box::new(resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).unwrap().default_node_template());
|
||||
let passthrough_node_id = NodeId::new();
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::InsertNode {
|
||||
node_id: passthrough_node_id,
|
||||
node_template,
|
||||
})
|
||||
.await;
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::InsertNodeBetween {
|
||||
node_id: passthrough_node_id,
|
||||
input_connector: InputConnector::node_at_index(stroke_node_id, 0),
|
||||
insert_node_input_index: 0,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
!stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||
"a node between the pair should gray the order radio out"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
|
||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
||||
ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{
|
||||
ReplaceablePaintChain, get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain,
|
||||
@@ -18,7 +18,7 @@ use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke};
|
||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
||||
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
|
||||
use graphene_std::{Artboard, Color, Graphic};
|
||||
|
||||
@@ -431,7 +431,8 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
|
||||
pub fn fill_color_set(&mut self, color: Option<Color>) {
|
||||
let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||
return;
|
||||
};
|
||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput);
|
||||
@@ -443,10 +444,15 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
let fill_value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
||||
|
||||
if existing_fill_node_id.is_none() {
|
||||
self.restore_default_stroke_order();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
|
||||
let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
||||
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||
return;
|
||||
};
|
||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
||||
@@ -487,6 +493,21 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
NodeInput::value(TaggedValue::GradientForm(gradient_form), false),
|
||||
false,
|
||||
);
|
||||
|
||||
if existing_fill_node_id.is_none() {
|
||||
self.restore_default_stroke_order();
|
||||
}
|
||||
}
|
||||
|
||||
/// A freshly created Fill node lands at the chain start, downstream of any Stroke node, where it would
|
||||
/// paint over the stroke. This hops the stroke back downstream so it keeps painting above by default.
|
||||
fn restore_default_stroke_order(&mut self) {
|
||||
let Some(output_layer) = self.get_output_layer() else { return };
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let Some(stroke_node_id) = Self::locate_node_in_layer_chain(&stroke_reference, output_layer, self.network_interface) else {
|
||||
return;
|
||||
};
|
||||
set_stroke_paint_order(self.network_interface, &[], stroke_node_id, PaintOrder::StrokeAbove);
|
||||
}
|
||||
|
||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||
@@ -857,8 +878,6 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
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);
|
||||
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);
|
||||
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::DashPatternInput);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::DashPattern(stroke.dash_lengths), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput);
|
||||
@@ -1006,3 +1025,60 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The wires feeding off a node's primary output.
|
||||
fn primary_output_consumers(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], node_id: NodeId) -> Vec<InputConnector> {
|
||||
network_interface
|
||||
.outward_wires(network_path)
|
||||
.and_then(|wires| wires.get(&OutputConnector::node(node_id, 0)).cloned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Swaps a chain's directly adjacent Stroke and Fill nodes when their order disagrees with the requested
|
||||
/// paint order: both nodes append their cover, so the downstream one of the pair paints on top, following
|
||||
/// the painter's algorithm. Without a fill wired directly to the stroke, nothing changes.
|
||||
/// Returns whether the graph changed.
|
||||
pub fn set_stroke_paint_order(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId, paint_order: PaintOrder) -> bool {
|
||||
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||
let is_fill = |network_interface: &NodeNetworkInterface, node_id: &NodeId| network_interface.reference(node_id, network_path).as_ref() == Some(&fill_reference);
|
||||
|
||||
// Find the fill wired directly to the stroke on either side; the downstream one of the pair paints on top
|
||||
let stroke_primary_source = match network_interface.input_from_connector(&InputConnector::node_at_index(stroke_node_id, 0), network_path) {
|
||||
Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id),
|
||||
_ => None,
|
||||
};
|
||||
let (fill_node_id, currently_above) = if let Some(source) = stroke_primary_source.filter(|source| is_fill(network_interface, source)) {
|
||||
(source, true)
|
||||
} else {
|
||||
let consumers = primary_output_consumers(network_interface, network_path, stroke_node_id);
|
||||
let fill_consumer = consumers.iter().find_map(|connector| match connector {
|
||||
InputConnector::Node { node_id, input_index: 0 } if is_fill(network_interface, node_id) => Some(*node_id),
|
||||
_ => None,
|
||||
});
|
||||
let Some(fill_node_id) = fill_consumer else { return false };
|
||||
(fill_node_id, false)
|
||||
};
|
||||
|
||||
if (paint_order == PaintOrder::StrokeAbove) == currently_above {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Swap the pair in place: the downstream node takes the upstream one's source, consumers of the
|
||||
// downstream node move over to the upstream one, and the wire linking the pair reverses direction
|
||||
let (upstream, downstream) = if currently_above { (fill_node_id, stroke_node_id) } else { (stroke_node_id, fill_node_id) };
|
||||
let Some(upstream_source) = network_interface.input_from_connector(&InputConnector::node_at_index(upstream, 0), network_path).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let downstream_consumers = primary_output_consumers(network_interface, network_path, downstream);
|
||||
|
||||
network_interface.set_input(&InputConnector::node_at_index(downstream, 0), upstream_source, network_path);
|
||||
network_interface.set_input(&InputConnector::node_at_index(upstream, 0), NodeInput::node(downstream, 0), network_path);
|
||||
for consumer in &downstream_consumers {
|
||||
if matches!(consumer, InputConnector::Node { node_id, .. } if *node_id == upstream || *node_id == downstream) {
|
||||
continue;
|
||||
}
|
||||
network_interface.set_input(consumer, NodeInput::node(upstream, 0), network_path);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1734,6 +1734,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
responses.add(EventMessage::GraphChanged);
|
||||
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
|
||||
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
|
||||
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
|
||||
|
||||
@@ -33,8 +33,8 @@ use graphene_std::vector::misc::{
|
||||
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||
};
|
||||
use graphene_std::vector::style::{
|
||||
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap,
|
||||
StrokeJoin, build_transform_with_y_preservation,
|
||||
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, StrokeAlign, StrokeCap, StrokeJoin,
|
||||
build_transform_with_y_preservation,
|
||||
};
|
||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||
use graphene_std::{NodeParameter, ParameterRef};
|
||||
@@ -322,7 +322,6 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeJoin>(x) => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<StrokeAlign>(x) => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<PaintOrder>(x) => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<ArcType>(x) => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<RowsOrColumns>(x) => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
|
||||
Some(x) if id_is::<TextAlign>(x) => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
||||
@@ -2678,9 +2677,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
ParameterWidgetsInfo::new(node_id, MiterLimitInput, true, context),
|
||||
NumberInput::default().min(0.).disabled(miter_limit_disabled),
|
||||
);
|
||||
let paint_order = enum_choice::<PaintOrder>()
|
||||
.for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput, true, context))
|
||||
.property_row();
|
||||
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
|
||||
let dash_lengths = dash_pattern_widget(ParameterWidgetsInfo::new(node_id, DashPatternInput, true, context), TextInput::default().centered(true));
|
||||
let number_input = disabled_number_input;
|
||||
@@ -2693,7 +2689,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
cap,
|
||||
join,
|
||||
LayoutGroup::row(miter_limit),
|
||||
paint_order,
|
||||
LayoutGroup::row(dash_lengths),
|
||||
LayoutGroup::row(dash_offset),
|
||||
]
|
||||
|
||||
@@ -6,8 +6,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::Appearance;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
@@ -41,12 +40,8 @@ pub struct DocumentMetadata {
|
||||
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
|
||||
/// This provides accurate SegmentIds for layers without explicit Path nodes.
|
||||
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>,
|
||||
/// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint
|
||||
/// information that lives on the list.
|
||||
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
|
||||
/// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read
|
||||
/// stroke paint information that lives on the list.
|
||||
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
|
||||
/// Per-layer `ATTR_APPEARANCE` attribute, exposed so message handlers can read paint information that lives on the list.
|
||||
pub layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>,
|
||||
/// Transform from document space to viewport space.
|
||||
pub document_to_viewport: DAffine2,
|
||||
}
|
||||
@@ -233,10 +228,15 @@ impl DocumentMetadata {
|
||||
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
|
||||
/// bounds for non-vector layers (groups, raster, text, color, gradient).
|
||||
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
||||
if let Some(vector) = self.layer_vector_data.get(&layer)
|
||||
&& let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer))
|
||||
{
|
||||
return Some(bounds);
|
||||
if let Some(vector) = self.layer_vector_data.get(&layer) {
|
||||
let stroke = self
|
||||
.layer_appearance_attributes
|
||||
.get(&layer)
|
||||
.and_then(|appearance| appearance.first_coverage_of(graphene_std::Cover::Stroke))
|
||||
.map(graphene_std::Coverage::stroke_params);
|
||||
if let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer), stroke.as_ref()) {
|
||||
return Some(bounds);
|
||||
}
|
||||
}
|
||||
self.bounding_box_document(layer)
|
||||
}
|
||||
|
||||
@@ -40,9 +40,8 @@ use graph_craft::Type;
|
||||
use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
|
||||
use graphene_std::Appearance;
|
||||
use graphene_std::ContextDependencies;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
|
||||
@@ -183,13 +183,8 @@ impl NodeNetworkInterface {
|
||||
self.document_metadata.layer_vector_data = new_layer_vector_data;
|
||||
}
|
||||
|
||||
/// Update the per-layer `ATTR_FILL` snapshot.
|
||||
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
|
||||
self.document_metadata.layer_fill_attributes = new_layer_fill_attributes;
|
||||
}
|
||||
|
||||
/// Update the per-layer `ATTR_STROKE` snapshot.
|
||||
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
|
||||
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
|
||||
/// Update the per-layer `ATTR_APPEARANCE` snapshot.
|
||||
pub fn update_appearance_attributes(&mut self, new_layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>) {
|
||||
self.document_metadata.layer_appearance_attributes = new_layer_appearance_attributes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// This file contains lots of hacky code for upgrading old documents to the new format
|
||||
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::set_stroke_paint_order;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, NodeTemplateImplementation, OutputConnector};
|
||||
@@ -1807,13 +1808,12 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
inputs_count = 7;
|
||||
}
|
||||
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" (#2644)
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
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_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
@@ -1822,13 +1822,36 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 5), old_inputs[6].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 6), old_inputs[7].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), paint_order_input, network_path);
|
||||
let dash_input = migrate_dash_input(&old_inputs[3]).unwrap_or_else(|| old_inputs[3].clone());
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), dash_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), dash_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), old_inputs[4].clone(), network_path);
|
||||
|
||||
inputs_count = 9;
|
||||
}
|
||||
|
||||
// The Stroke node's "Paint Order" input was retired in favor of the relative order of the Fill and Stroke
|
||||
// nodes in the chain, so the stored value becomes a topology rewrite that reorders the two nodes.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 10 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
// A wired paint order input cannot be evaluated statically, so it degrades to the default and leaves its source disconnected
|
||||
let paint_order = match old_inputs.get(7).and_then(|input| input.as_value()) {
|
||||
Some(&TaggedValue::PaintOrder(value)) => value,
|
||||
_ => PaintOrder::StrokeAbove,
|
||||
};
|
||||
|
||||
for (index, input) in old_inputs.iter().enumerate().take(7) {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
|
||||
}
|
||||
let dash_input = migrate_dash_input(&old_inputs[8]).unwrap_or_else(|| old_inputs[8].clone());
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), dash_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), old_inputs[9].clone(), network_path);
|
||||
inputs_count = 9;
|
||||
|
||||
set_stroke_paint_order(&mut document.network_interface, network_path, *node_id, paint_order);
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// A legacy "no color" on a plain color connector (`TaggedValue::no_paint()` restored by the deserializer) becomes a color,
|
||||
// since only paint connectors keep the no-paint choice
|
||||
{
|
||||
|
||||
@@ -125,6 +125,9 @@ pub struct DrawingToolState {
|
||||
pub miter_limit: Option<f64>,
|
||||
/// Paint order from the selection. `None` = mixed.
|
||||
pub paint_order: Option<PaintOrder>,
|
||||
/// Whether any selected layer has a lone adjacent Fill/Stroke pair for the paint order to swap.
|
||||
/// Stays true with an empty selection, where the radio still sets the default for new shapes.
|
||||
pub paint_order_applicable: bool,
|
||||
/// Dash lengths from the selection. `None` = mixed.
|
||||
pub dash_lengths: Option<Vec<f64>>,
|
||||
/// Dash offset from the selection. `None` = mixed.
|
||||
@@ -150,6 +153,7 @@ impl DrawingToolState {
|
||||
stroke_join: Some(StrokeJoin::default()),
|
||||
miter_limit: Some(4.),
|
||||
paint_order: Some(PaintOrder::default()),
|
||||
paint_order_applicable: true,
|
||||
dash_lengths: Some(Vec::new()),
|
||||
dash_offset: Some(0.),
|
||||
last_synced_selection: Vec::new(),
|
||||
@@ -183,13 +187,21 @@ impl DrawingToolState {
|
||||
cap: self.stroke_cap.unwrap_or_default(),
|
||||
join: self.stroke_join.unwrap_or_default(),
|
||||
join_miter_limit: self.miter_limit.unwrap_or(4.),
|
||||
paint_order: self.paint_order.unwrap_or_default(),
|
||||
dash_lengths: self.effective_dash_lengths(),
|
||||
dash_offset: self.dash_offset.unwrap_or(0.),
|
||||
transform: glam::DAffine2::IDENTITY,
|
||||
};
|
||||
responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
|
||||
}
|
||||
|
||||
/// Queues the paint order rewrite for a freshly created `layer`. The order is the relative position of the
|
||||
/// Stroke and Fill nodes in the chain, so this must run after both the stroke and fill have been applied.
|
||||
pub fn apply_stroke_order_to_new_layer(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
let paint_order = self.paint_order.unwrap_or_default();
|
||||
if paint_order != PaintOrder::default() {
|
||||
responses.add(GraphOperationMessage::StrokeOrderSet { layer, paint_order });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `FillChoice::Solid` from a color.
|
||||
@@ -304,12 +316,23 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled:
|
||||
/// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates
|
||||
/// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is.
|
||||
fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool {
|
||||
let strokes: Vec<_> = graph_modification_utils::paintable_selected_layers(document)
|
||||
let layers = graph_modification_utils::paintable_selected_layers(document);
|
||||
|
||||
// The order radio only swaps a layer's lone adjacent Fill/Stroke pair, so it grays out when no selected
|
||||
// layer has one; with nothing selected it still sets the default order for new shapes
|
||||
let paint_order_applicable = layers.is_empty() || layers.iter().any(|&layer| graph_modification_utils::stroke_paint_order_applicable(layer, &document.network_interface));
|
||||
let mut applicability_changed = false;
|
||||
if drawing.paint_order_applicable != paint_order_applicable {
|
||||
drawing.paint_order_applicable = paint_order_applicable;
|
||||
applicability_changed = true;
|
||||
}
|
||||
|
||||
let strokes: Vec<_> = layers
|
||||
.into_iter()
|
||||
.filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface))
|
||||
.collect();
|
||||
if strokes.is_empty() {
|
||||
return false;
|
||||
return applicability_changed;
|
||||
}
|
||||
|
||||
fn unanimous<T: PartialEq + Clone>(values: impl IntoIterator<Item = T>) -> Option<T> {
|
||||
@@ -326,7 +349,7 @@ fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessag
|
||||
let new_dash_lengths = unanimous(strokes.iter().map(|s| &s.dash_lengths)).cloned();
|
||||
let new_dash_offset = unanimous(strokes.iter().map(|s| s.dash_offset));
|
||||
|
||||
let mut changed = false;
|
||||
let mut changed = applicability_changed;
|
||||
|
||||
if drawing.stroke_align != new_align {
|
||||
drawing.stroke_align = new_align;
|
||||
|
||||
@@ -692,10 +692,7 @@ pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
||||
Some(TaggedValue::F64(value)) => *value,
|
||||
_ => 4.,
|
||||
};
|
||||
let paint_order = match parameters.value(stroke::PaintOrderInput) {
|
||||
Some(TaggedValue::PaintOrder(value)) => *value,
|
||||
_ => PaintOrder::default(),
|
||||
};
|
||||
let paint_order = get_stroke_paint_order(layer, network_interface);
|
||||
let dash_lengths = match parameters.value(stroke::DashPatternInput) {
|
||||
Some(TaggedValue::DashPattern(lengths)) => lengths.clone(),
|
||||
_ => Vec::new(),
|
||||
@@ -721,6 +718,67 @@ pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
|
||||
}
|
||||
|
||||
/// Whether the paint order swap can act on the layer: exactly one Fill and one Stroke node in its chain,
|
||||
/// wired directly together. Extra paint nodes make the swap unreliable, since a cover replaced in place
|
||||
/// keeps the slot in the paint order that its upstream sibling established.
|
||||
pub fn stroke_paint_order_applicable(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||
|
||||
let mut strokes = Vec::new();
|
||||
let mut fills = Vec::new();
|
||||
let node_graph_layer = NodeGraphLayer::new(layer, network_interface);
|
||||
for node_id in node_graph_layer
|
||||
.horizontal_layer_flow()
|
||||
.take_while(|&node_id| node_id == layer.to_node() || !network_interface.is_layer(&node_id, &[]))
|
||||
{
|
||||
let reference = network_interface.reference(&node_id, &[]);
|
||||
if reference.as_ref() == Some(&stroke_reference) {
|
||||
strokes.push(node_id);
|
||||
} else if reference.as_ref() == Some(&fill_reference) {
|
||||
fills.push(node_id);
|
||||
}
|
||||
}
|
||||
let (&[stroke_node_id], &[fill_node_id]) = (strokes.as_slice(), fills.as_slice()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let primary_source = |node_id: NodeId| match network_interface.input_from_connector(&InputConnector::node_at_index(node_id, 0), &[]) {
|
||||
Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id),
|
||||
_ => None,
|
||||
};
|
||||
primary_source(stroke_node_id) == Some(fill_node_id) || primary_source(fill_node_id) == Some(stroke_node_id)
|
||||
}
|
||||
|
||||
/// The paint order of a layer's stroke, read from the chain topology: both the Fill and Stroke nodes append
|
||||
/// their cover, so the more downstream of the two paints on top, following the painter's algorithm.
|
||||
pub fn get_stroke_paint_order(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> PaintOrder {
|
||||
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||
|
||||
// The flow iterates downstream to upstream, so the first of the pair encountered is the one painting on top
|
||||
let mut stroke_position = None;
|
||||
let mut fill_position = None;
|
||||
let node_graph_layer = NodeGraphLayer::new(layer, network_interface);
|
||||
for (position, node_id) in node_graph_layer
|
||||
.horizontal_layer_flow()
|
||||
.take_while(|&node_id| node_id == layer.to_node() || !network_interface.is_layer(&node_id, &[]))
|
||||
.enumerate()
|
||||
{
|
||||
let reference = network_interface.reference(&node_id, &[]);
|
||||
if reference.as_ref() == Some(&stroke_reference) {
|
||||
stroke_position.get_or_insert(position);
|
||||
} else if reference.as_ref() == Some(&fill_reference) {
|
||||
fill_position.get_or_insert(position);
|
||||
}
|
||||
}
|
||||
|
||||
match stroke_position.zip(fill_position) {
|
||||
Some((stroke_position, fill_position)) if fill_position < stroke_position => PaintOrder::StrokeBelow,
|
||||
_ => PaintOrder::StrokeAbove,
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the weight back to every selected non-artboard layer's stroke. Layers with an existing stroke just have their
|
||||
/// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new
|
||||
/// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes.
|
||||
|
||||
@@ -64,7 +64,9 @@ where
|
||||
if has_dash {
|
||||
rows.push(LayoutGroup::row(dash_offset_row(drawing.dash_offset, to_message.clone())));
|
||||
}
|
||||
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", drawing.paint_order, false, {
|
||||
// An inapplicable order (no Fill/Stroke pair to reorder) grays out and highlights no entry, since the synced value is only a fallback
|
||||
let paint_order = drawing.paint_order.filter(|_| drawing.paint_order_applicable);
|
||||
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", paint_order, !drawing.paint_order_applicable, {
|
||||
let to_message = to_message.clone();
|
||||
move |value| to_message(StrokeOptionsUpdate::PaintOrder(value))
|
||||
})));
|
||||
@@ -198,7 +200,12 @@ pub fn apply_miter_limit(drawing: &mut DrawingToolState, limit: f64, document: &
|
||||
|
||||
pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
drawing.paint_order = Some(order);
|
||||
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::PaintOrderInput, TaggedValue::PaintOrder(order), responses);
|
||||
// A mixed selection only grays the radio out when no layer can take the swap, so skip the ones that can't
|
||||
for layer in graph_modification_utils::paintable_selected_layers(document) {
|
||||
if graph_modification_utils::stroke_paint_order_applicable(layer, &document.network_interface) {
|
||||
responses.add(GraphOperationMessage::StrokeOrderSet { layer, paint_order: order });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -216,6 +216,7 @@ impl ToolTransition for FreehandTool {
|
||||
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
|
||||
tool_abort: Some(FreehandToolMessage::Abort.into()),
|
||||
selection_changed: Some(FreehandToolMessage::SelectionChanged.into()),
|
||||
graph_changed: Some(FreehandToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -302,8 +303,9 @@ impl Fsm for FreehandToolFsmState {
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||
tool_options.drawing.apply_stroke_order_to_new_layer(layer, responses);
|
||||
tool_data.layer = Some(layer);
|
||||
tool_data.new_layer_viewport_start = Some(input.mouse.position);
|
||||
|
||||
|
||||
@@ -314,6 +314,7 @@ impl ToolTransition for PenTool {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(PenToolMessage::Abort.into()),
|
||||
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||
graph_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|context| PenToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
@@ -1352,8 +1353,9 @@ impl PenToolData {
|
||||
let parent = document.new_layer_bounding_artboard(input, viewport);
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
self.current_layer = Some(layer);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||
tool_options.drawing.apply_stroke_order_to_new_layer(layer, responses);
|
||||
self.prior_segment = None;
|
||||
self.prior_segments = None;
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||
|
||||
@@ -448,6 +448,7 @@ impl ToolTransition for SelectTool {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(SelectToolMessage::Abort.into()),
|
||||
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
||||
graph_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(SelectToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
|
||||
@@ -710,6 +710,7 @@ impl ToolTransition for ShapeTool {
|
||||
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
|
||||
tool_abort: Some(ShapeToolMessage::Abort.into()),
|
||||
selection_changed: Some(ShapeToolMessage::SelectionChanged.into()),
|
||||
graph_changed: Some(ShapeToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -1129,8 +1130,9 @@ impl Fsm for ShapeToolFsmState {
|
||||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||
tool_options.drawing.apply_stroke_order_to_new_layer(layer, defered_responses);
|
||||
}
|
||||
ShapeType::Arrow => {
|
||||
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
||||
@@ -1143,8 +1145,9 @@ impl Fsm for ShapeToolFsmState {
|
||||
|
||||
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
||||
tool_data.line_data.editing_layer = Some(layer);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||
tool_options.drawing.apply_stroke_order_to_new_layer(layer, defered_responses);
|
||||
}
|
||||
ShapeType::Line => {
|
||||
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
||||
|
||||
@@ -233,6 +233,7 @@ impl ToolTransition for SplineTool {
|
||||
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
|
||||
tool_abort: Some(SplineToolMessage::Abort.into()),
|
||||
selection_changed: Some(SplineToolMessage::SelectionChanged.into()),
|
||||
graph_changed: Some(SplineToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
|
||||
}
|
||||
}
|
||||
@@ -407,8 +408,9 @@ impl Fsm for SplineToolFsmState {
|
||||
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||
tool_options.drawing.apply_stroke_order_to_new_layer(layer, responses);
|
||||
tool_data.current_layer = Some(layer);
|
||||
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
||||
|
||||
|
||||
@@ -403,6 +403,7 @@ impl ToolTransition for TextTool {
|
||||
EventToMessageMap {
|
||||
canvas_transformed: None,
|
||||
selection_changed: Some(TextToolMessage::SelectionChanged.into()),
|
||||
graph_changed: None,
|
||||
tool_abort: Some(TextToolMessage::Abort.into()),
|
||||
working_color_changed: Some(TextToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|context| TextToolMessage::Overlays { context }.into()),
|
||||
|
||||
@@ -156,6 +156,9 @@ impl DocumentToolData {
|
||||
pub struct EventToMessageMap {
|
||||
pub canvas_transformed: Option<ToolMessage>,
|
||||
pub selection_changed: Option<ToolMessage>,
|
||||
/// Tools whose control bar mirrors the selection's node chains map this to the same message as
|
||||
/// `selection_changed`, so a graph edit re-syncs the widgets the same way reselecting would.
|
||||
pub graph_changed: Option<ToolMessage>,
|
||||
pub tool_abort: Option<ToolMessage>,
|
||||
pub working_color_changed: Option<ToolMessage>,
|
||||
pub overlay_provider: Option<OverlayProvider>,
|
||||
@@ -178,6 +181,7 @@ pub trait ToolTransition {
|
||||
subscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
||||
subscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
||||
subscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
||||
subscribe_message(event_to_tool_map.graph_changed, EventMessage::GraphChanged);
|
||||
subscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
||||
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
||||
responses.add(OverlaysMessage::AddProvider { provider: overlay_provider });
|
||||
@@ -198,6 +202,7 @@ pub trait ToolTransition {
|
||||
unsubscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
||||
unsubscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
||||
unsubscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
||||
unsubscribe_message(event_to_tool_map.graph_changed, EventMessage::GraphChanged);
|
||||
unsubscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
||||
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
||||
responses.add(OverlaysMessage::RemoveProvider { provider: overlay_provider });
|
||||
|
||||
@@ -697,8 +697,7 @@ impl NodeGraphExecutor {
|
||||
text_frames,
|
||||
clip_targets,
|
||||
vector_data,
|
||||
fill_attributes,
|
||||
stroke_attributes,
|
||||
appearance_attributes,
|
||||
backgrounds: _,
|
||||
} = render_output.metadata;
|
||||
|
||||
@@ -713,8 +712,7 @@ impl NodeGraphExecutor {
|
||||
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
responses.add(DocumentMessage::UpdateVectorData { vector_data });
|
||||
responses.add(DocumentMessage::UpdateFillAttributes { fill_attributes });
|
||||
responses.add(DocumentMessage::UpdateStrokeAttributes { stroke_attributes });
|
||||
responses.add(DocumentMessage::UpdateAppearanceAttributes { appearance_attributes });
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
Reference in New Issue
Block a user