Remove the legacy Fill, Gradient, and PathStyle types now that paints live in attributes (#4297)

* Remove `Stroke.color`

* Remove legacy Gradient struct usage

* Fix gradient tool tests

* Remove `Fill` enum

* Fix direction of `gradient_orientation_rightward`

* Remove `Gradient` struct

* Remove `PathStyle` struct

* Fix old `VectorData` migration

* Refactor

- Remove unnecessary wrapper functions
- Use `as_ref` instead of `clone` for stroke if possible
- Comment cleanup

* Clean up stale style comments and bindings left by the paint attribute migration

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
YohYamasaki
2026-07-04 23:08:06 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 9f9899cfd0
commit 6a54dcb5da
28 changed files with 887 additions and 1334 deletions
@@ -402,11 +402,11 @@ impl MessageHandler<ClipboardMessage, ClipboardMessageContext<'_>> for Clipboard
}); });
// Add default fill and stroke to the layer // Add default fill and stroke to the layer
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(Color::WHITE) });
responses.add(GraphOperationMessage::FillSet { layer, fill });
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); let color = Some(Color::BLACK);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); let stroke = graphene_std::vector::style::Stroke::new(DEFAULT_STROKE_WIDTH);
responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
// Create new point ids and add those into the existing Vector path // Create new point ids and add those into the existing Vector path
let mut points_map = HashMap::new(); let mut points_map = HashMap::new();
@@ -13,7 +13,7 @@ use graphene_std::list::List;
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster}; use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType}; use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType};
use graphene_std::{Artboard, Color, Context, Graphic}; use graphene_std::{Artboard, Color, Context, Graphic};
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
@@ -385,61 +385,7 @@ impl TableItemLayout for Vector {
VectorTableTab::Properties => { VectorTableTab::Properties => {
table_rows.push(column_headings(&["property", "value"])); table_rows.push(column_headings(&["property", "value"]));
match self.style.fill.clone() { if let Some(stroke) = self.stroke.as_ref() {
Fill::None => table_rows.push(vec![
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoiceUI::None)
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]),
Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(color)))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]),
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").narrow(true).widget_instance(),
ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(gradient.stops)))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Type").narrow(true).widget_instance(),
TextLabel::new(gradient.gradient_type.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Start").narrow(true).widget_instance(),
TextLabel::new(format_dvec2(gradient.start)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient End").narrow(true).widget_instance(),
TextLabel::new(format_dvec2(gradient.end)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Transform").narrow(true).widget_instance(),
TextLabel::new(format_transform_matrix(gradient.transform)).narrow(true).widget_instance(),
]);
}
}
if let Some(stroke) = self.style.stroke.clone() {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![
TextLabel::new("Stroke").narrow(true).widget_instance(),
ColorInput::new(FillChoiceUI::from(&color))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
.widget_instance(),
]);
table_rows.push(vec![ table_rows.push(vec![
TextLabel::new("Stroke Weight").narrow(true).widget_instance(), TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(), TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(),
@@ -42,10 +42,10 @@ use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect; use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::dvec2_to_point; use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::style::{Fill, Gradient, RenderMode}; use graphene_std::vector::style::RenderMode;
use graphene_std::vector::{PointId, graphic_types};
use kurbo::{Affine, BezPath, Line, PathSeg}; use kurbo::{Affine, BezPath, Line, PathSeg};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf; use std::path::PathBuf;
@@ -125,7 +125,7 @@ pub struct DocumentMessageHandler {
/// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands. /// network path, the node itself, and its original relative gradient. The deferred migration removes each entry as its bake lands.
/// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement. /// Transient migration state, but persisted in the saved document so unfinished bakes retry on the next open instead of losing placement.
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, Gradient)>, pub(crate) pending_gradient_bbox_bake: Vec<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
// ============================================= // =============================================
// Fields omitted from the saved document format // Fields omitted from the saved document format
@@ -2763,30 +2763,20 @@ impl DocumentMessageHandler {
let mut resulting_layers: Vec<NodeId> = Vec::new(); let mut resulting_layers: Vec<NodeId> = Vec::new();
for layer in selected_layers { for layer in selected_layers {
let style = self.network_interface.document_metadata().layer_vector_data.get(&layer).map(|arc| arc.style.clone()); let Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else {
let Some(style) = style else {
resulting_layers.push(layer.to_node()); resulting_layers.push(layer.to_node());
continue; continue;
}; };
let stroke = vector_data.stroke.as_ref();
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer); 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 stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
// `ATTR_FILL` is the source of truth when set; fall back to the legacy `style.fill` only when no attribute is present let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list));
let has_fill = if let Some(list) = fill_graphic_list { // `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint.
is_paint_present(list) // So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth.
} else { let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent()));
!matches!(style.fill, Fill::None) let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
};
// `style.stroke` is `Some` whenever a `Stroke` node is in the chain, even with weight 0 or a transparent color.
// So `is_some()` would treat invisibly-stroked fill-only layers as having a stroke.
// `ATTR_STROKE` is the source of truth when set; fall back to `style.stroke.color` only when no attribute is present.
let stroke_visible = if let Some(list) = stroke_graphic_list {
list.element(0).is_some_and(|g| !g.is_fully_transparent())
} else {
style.stroke.as_ref().and_then(|s| s.color()).is_some_and(|c| c.a() != 0.)
};
let has_stroke = style.stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip. // No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
if !has_stroke { if !has_stroke {
@@ -4017,7 +4007,7 @@ mod document_message_handler_tests {
#[test] #[test]
fn pending_gradient_bakes_round_trip_through_serialization() { fn pending_gradient_bakes_round_trip_through_serialization() {
let document = DocumentMessageHandler { let document = DocumentMessageHandler {
pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), Gradient::default())], pending_gradient_bbox_bake: vec![(vec![NodeId(7)], NodeId(42), graphic_types::migrations::legacy::Gradient::default())],
..Default::default() ..Default::default()
}; };
@@ -10,15 +10,22 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke}; use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
use graphene_std::vector::{GradientStops, PointId, VectorModificationType}; use graphene_std::vector::{GradientStops, PointId, VectorModificationType};
#[impl_message(Message, DocumentMessage, GraphOperation)] #[impl_message(Message, DocumentMessage, GraphOperation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum GraphOperationMessage { pub enum GraphOperationMessage {
FillSet { FillColorSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
fill: Fill, color: Option<Color>,
},
FillGradientSet {
layer: LayerNodeIdentifier,
gradient: GradientStops,
gradient_type: GradientType,
spread_method: GradientSpreadMethod,
transform: DAffine2,
}, },
BlendingFillSet { BlendingFillSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
@@ -28,10 +35,9 @@ pub enum GraphOperationMessage {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
stops: GradientStops, stops: GradientStops,
}, },
GradientLineSet { GradientTransformSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
start: DVec2, transform: DAffine2,
end: DVec2,
}, },
GradientTypeSet { GradientTypeSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
@@ -54,6 +60,7 @@ pub enum GraphOperationMessage {
}, },
StrokeSet { StrokeSet {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
color: Option<Color>,
stroke: Stroke, stroke: Stroke,
}, },
TransformChange { TransformChange {
@@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput};
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color}; use graphene_std::{Artboard, Color};
#[derive(ExtractField)] #[derive(ExtractField)]
@@ -34,9 +34,20 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
let GraphOperationMessageContext { network_interface, .. } = context; let GraphOperationMessageContext { network_interface, .. } = context;
match message { match message {
GraphOperationMessage::FillSet { layer, fill } => { GraphOperationMessage::FillColorSet { layer, color } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_set(fill); modify_inputs.fill_color_set(color);
}
}
GraphOperationMessage::FillGradientSet {
layer,
gradient,
gradient_type,
spread_method,
transform,
} => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
} }
} }
GraphOperationMessage::BlendingFillSet { layer, fill } => { GraphOperationMessage::BlendingFillSet { layer, fill } => {
@@ -49,9 +60,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.gradient_stops_set(stops); modify_inputs.gradient_stops_set(stops);
} }
} }
GraphOperationMessage::GradientLineSet { layer, start, end } => { GraphOperationMessage::GradientTransformSet { layer, transform } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.gradient_line_set(start, end); modify_inputs.gradient_transform_set(transform);
} }
} }
GraphOperationMessage::GradientTypeSet { layer, gradient_type } => { GraphOperationMessage::GradientTypeSet { layer, gradient_type } => {
@@ -80,9 +91,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.clip_mode_toggle(clip_mode); modify_inputs.clip_mode_toggle(clip_mode);
} }
} }
GraphOperationMessage::StrokeSet { layer, stroke } => { GraphOperationMessage::StrokeSet { layer, color, stroke } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.stroke_set(stroke); modify_inputs.stroke_set(color, stroke);
} }
} }
GraphOperationMessage::TransformChange { GraphOperationMessage::TransformChange {
@@ -622,7 +633,7 @@ fn import_usvg_node(
usvg::Node::Text(text) => { usvg::Node::Text(text) => {
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer); modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
modify_inputs.fill_set(Fill::Solid(Color::BLACK)); modify_inputs.fill_color_set(Some(Color::BLACK));
} }
} }
} }
@@ -674,7 +685,7 @@ fn import_usvg_node_inner(
usvg::Node::Text(text) => { usvg::Node::Text(text) => {
let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string()); let font = Font::new(graphene_std::consts::DEFAULT_FONT_FAMILY.to_string(), graphene_std::consts::DEFAULT_FONT_STYLE.to_string());
modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer); modify_inputs.insert_text(text.chunks().iter().map(|chunk| chunk.text()).collect(), font, TypesettingConfig::default(), layer);
modify_inputs.fill_set(Fill::Solid(Color::BLACK)); modify_inputs.fill_color_set(Some(Color::BLACK));
0 0
} }
} }
@@ -762,27 +773,29 @@ fn set_import_child_positions(
fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) { fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsContext, transform: DAffine2) {
if let usvg::Paint::Color(color) = &stroke.paint() { if let usvg::Paint::Color(color) = &stroke.paint() {
modify_inputs.stroke_set(Stroke { modify_inputs.stroke_set(
color: Some(usvg_color(*color, stroke.opacity().get())), Some(usvg_color(*color, stroke.opacity().get())),
weight: stroke.width().get() as f64, Stroke {
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(), weight: stroke.width().get() as f64,
dash_offset: stroke.dashoffset() as f64, dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
cap: match stroke.linecap() { dash_offset: stroke.dashoffset() as f64,
usvg::LineCap::Butt => StrokeCap::Butt, cap: match stroke.linecap() {
usvg::LineCap::Round => StrokeCap::Round, usvg::LineCap::Butt => StrokeCap::Butt,
usvg::LineCap::Square => StrokeCap::Square, usvg::LineCap::Round => StrokeCap::Round,
usvg::LineCap::Square => StrokeCap::Square,
},
join: match stroke.linejoin() {
usvg::LineJoin::Miter => StrokeJoin::Miter,
usvg::LineJoin::MiterClip => StrokeJoin::Miter,
usvg::LineJoin::Round => StrokeJoin::Round,
usvg::LineJoin::Bevel => StrokeJoin::Bevel,
},
join_miter_limit: stroke.miterlimit().get() as f64,
align: StrokeAlign::Center,
paint_order: PaintOrder::StrokeAbove,
transform,
}, },
join: match stroke.linejoin() { )
usvg::LineJoin::Miter => StrokeJoin::Miter,
usvg::LineJoin::MiterClip => StrokeJoin::Miter,
usvg::LineJoin::Round => StrokeJoin::Round,
usvg::LineJoin::Bevel => StrokeJoin::Bevel,
},
join_miter_limit: stroke.miterlimit().get() as f64,
align: StrokeAlign::Center,
paint_order: PaintOrder::StrokeAbove,
transform,
})
} }
} }
@@ -795,16 +808,18 @@ fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMet
} }
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, GradientStops>) { fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, graphite_gradient_stops: &HashMap<String, GradientStops>) {
modify_inputs.fill_set(match &fill.paint() { match &fill.paint() {
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())), usvg::Paint::Color(color) => modify_inputs.fill_color_set(Some(usvg_color(*color, fill.opacity().get()))),
usvg::Paint::LinearGradient(linear) => { usvg::Paint::LinearGradient(linear) => {
let gradient_transform = usvg_transform(linear.transform()); let gradient_transform = usvg_transform(linear.transform());
let (start, end) = (DVec2::new(linear.x1() as f64, linear.y1() as f64), DVec2::new(linear.x2() as f64, linear.y2() as f64)); let (start, end) = (DVec2::new(linear.x1() as f64, linear.y1() as f64), DVec2::new(linear.x2() as f64, linear.y2() as f64));
let (start, end) = (gradient_transform.transform_point2(start), gradient_transform.transform_point2(end)); let (start, end) = (gradient_transform.transform_point2(start), gradient_transform.transform_point2(end));
let direction = end - start;
let transform = DAffine2::from_cols(direction, direction.perp(), start);
let gradient_type = GradientType::Linear; let gradient_type = GradientType::Linear;
let stops = match graphite_gradient_stops.get(linear.id()) { let gradient = match graphite_gradient_stops.get(linear.id()) {
Some(graphite_stops) => graphite_stops.clone(), Some(graphite_stops) => graphite_stops.clone(),
None => { None => {
let stops = linear.stops().iter().map(|stop| GradientStop { let stops = linear.stops().iter().map(|stop| GradientStop {
@@ -816,27 +831,19 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
} }
}; };
let spread_method = convert_spread_method(linear.spread_method()); let spread_method = convert_spread_method(linear.spread_method());
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
Fill::Gradient(Gradient {
start,
end,
gradient_type,
stops,
spread_method,
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
})
} }
usvg::Paint::RadialGradient(radial) => { usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform()); let gradient_transform = usvg_transform(radial.transform());
let center = DVec2::new(radial.cx() as f64, radial.cy() as f64); let center = DVec2::new(radial.cx() as f64, radial.cy() as f64);
let edge = center + DVec2::X * radial.r().get() as f64; let edge = center + DVec2::X * radial.r().get() as f64;
let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge)); let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge));
let direction = end - start;
let transform = DAffine2::from_cols(direction, direction.perp(), start);
let gradient_type = GradientType::Radial; let gradient_type = GradientType::Radial;
let stops = match graphite_gradient_stops.get(radial.id()) { let gradient = match graphite_gradient_stops.get(radial.id()) {
Some(graphite_stops) => graphite_stops.clone(), Some(graphite_stops) => graphite_stops.clone(),
None => { None => {
let stops = radial.stops().iter().map(|stop| GradientStop { let stops = radial.stops().iter().map(|stop| GradientStop {
@@ -849,20 +856,8 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
}; };
let spread_method = convert_spread_method(radial.spread_method()); let spread_method = convert_spread_method(radial.spread_method());
Fill::Gradient(Gradient { modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
start,
end,
gradient_type,
stops,
spread_method,
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
})
} }
usvg::Paint::Pattern(_) => { usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
warn!("SVG patterns are not currently supported"); };
return;
}
});
} }
@@ -15,7 +15,7 @@ use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{Fill, GradientSpreadMethod, GradientType, Stroke, build_transform_with_y_preservation}; use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::vector::{GradientStops, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration};
@@ -453,74 +453,59 @@ impl<'a> ModifyInputsContext<'a> {
Some(node_id) Some(node_id)
} }
pub fn fill_set(&mut self, fill: Fill) { pub fn fill_color_set(&mut self, color: Option<Color>) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else { let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return; return;
}; };
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX); let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX);
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX);
match &fill { self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
Fill::None => { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX); }
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(None), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(None), false), false); pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) {
} let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
Fill::Solid(color) => { return;
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupColorInput::INDEX); };
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), true); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Some(*color)), false), false); self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
}
Fill::Gradient(gradient) => {
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput::INDEX);
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), true);
// Skip the rerender on all but the last input so the whole update triggers a single graph run // Skip the rerender on all but the last input so the whole update triggers a single graph run
self.set_input_with_refresh( self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX), InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX),
NodeInput::value(TaggedValue::Gradient(gradient.stops.clone()), false), NodeInput::value(TaggedValue::Gradient(gradient), false),
true, true,
); );
// Reposition the gradient only when the transform is a plain value, leaving a wired transform source connected // Reposition the gradient only when the transform is a plain value, leaving a wired transform source connected
let old_transform: Option<DAffine2> = self let transform_is_value = self
.network_interface .network_interface
.document_network() .document_network()
.nodes .nodes
.get(&fill_node_id) .get(&fill_node_id)
.and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX)) .and_then(|node| node.inputs.get(graphene_std::vector::fill::TransformInput::INDEX))
.and_then(|input| input.as_value()) .is_some_and(|input| input.as_value().is_some());
.map(|value| { if transform_is_value {
if let TaggedValue::OptionalDAffine2(transform) = value { self.set_input_with_refresh(
transform.unwrap_or(DAffine2::IDENTITY) InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
} else { NodeInput::value(TaggedValue::OptionalDAffine2(Some(transform)), false),
DAffine2::IDENTITY true,
} );
});
if let Some(old_transform) = old_transform {
let new_transform = build_transform_with_y_preservation(old_transform, gradient.start, gradient.end);
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::TransformInput::INDEX),
NodeInput::value(TaggedValue::OptionalDAffine2(Some(new_transform)), false),
true,
);
}
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX),
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
true,
);
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
false,
);
}
} }
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput::INDEX),
NodeInput::value(TaggedValue::GradientType(gradient_type), false),
true,
);
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false),
false,
);
} }
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
@@ -622,10 +607,10 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
} }
/// Update the gradient line so its endpoints are at `new_start` and `new_end`. /// Update the transform to map the unit gradient ((0,0), (1, 0)) to the geometry's local space.
/// With multiple `Transform` nodes the last one (closest to the layer) is modified so the chain still composes to the target. /// With multiple `Transform` nodes the last one (closest to the layer) is modified so the chain still composes to the target.
/// With none, one is inserted unless the target is the identity. /// With none, one is inserted unless the target is the identity.
pub fn gradient_line_set(&mut self, new_start: DVec2, new_end: DVec2) { pub fn gradient_transform_set(&mut self, transform: DAffine2) {
let Some(output_layer) = self.get_output_layer() else { return }; let Some(output_layer) = self.get_output_layer() else { return };
let walk_from = if let Some(fill_input_node_id) = get_fill_input_node_id(output_layer, self.network_interface) { let walk_from = if let Some(fill_input_node_id) = get_fill_input_node_id(output_layer, self.network_interface) {
@@ -661,14 +646,9 @@ impl<'a> ModifyInputsContext<'a> {
.map_or(acc, |document_node| acc * transform_utils::get_current_transform(&document_node.inputs)) .map_or(acc, |document_node| acc * transform_utils::get_current_transform(&document_node.inputs))
}) })
}; };
let composed_old = compose(&upstream_transforms);
let prior_combined = compose(prior_transforms); let prior_combined = compose(prior_transforms);
// Rebuild the y-axis from the new x-axis using the old (parallel, perpendicular) decomposition and length ratio, let last_transform_value = transform * prior_combined.inverse();
// so the gradient's aspect ratio and skew survive an endpoint drag (so an ellipse stays the same ellipse) instead of
// the old y-axis vector remaining fixed while x changes
let new_composed = build_transform_with_y_preservation(composed_old, new_start, new_end);
let last_transform_value = new_composed * prior_combined.inverse();
let target_input = gradient_chain_target_input(output_layer, self.network_interface); let target_input = gradient_chain_target_input(output_layer, self.network_interface);
let transform_node_id = if let Some(id) = last_transform_node_id { let transform_node_id = if let Some(id) = last_transform_node_id {
@@ -730,13 +710,13 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
} }
pub fn stroke_set(&mut self, stroke: Stroke) { pub fn stroke_set(&mut self, color: Option<Color>, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else { let Some(stroke_node_id) = self.existing_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, true) else {
return; return;
}; };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke.color), false), true); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX); 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); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
@@ -2428,10 +2428,10 @@ fn root_layer_for_chain_node(node_id: NodeId, context: &mut NodePropertiesContex
/// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer /// Resolve the viewport-space orientation of a Fill node's gradient by walking downstream to its owning layer
/// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically. /// and reusing the same helper the Gradient tool uses, so canvas tilt and layer transforms behave identically.
fn gradient_orientation_in_fill_node(node_id: NodeId, start: DVec2, end: DVec2, context: &mut NodePropertiesContext) -> Option<bool> { fn gradient_orientation_in_fill_node(node_id: NodeId, gradient_transform: DAffine2, context: &mut NodePropertiesContext) -> Option<bool> {
let layer = root_layer_for_chain_node(node_id, context)?; let layer = root_layer_for_chain_node(node_id, context)?;
let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface); let transform = graph_modification_utils::gradient_space_transform(layer, context.network_interface);
Some(graph_modification_utils::gradient_orientation_rightward(start, end, transform)) Some(graph_modification_utils::gradient_orientation_rightward(transform * gradient_transform))
} }
/// Fill Node Widgets LayoutGroup /// Fill Node Widgets LayoutGroup
@@ -2659,7 +2659,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let start = transform.transform_point2(DVec2::ZERO); let start = transform.transform_point2(DVec2::ZERO);
let end = transform.transform_point2(DVec2::X); let end = transform.transform_point2(DVec2::X);
let new_transform = build_transform_with_y_preservation(transform, end, start); let new_transform = build_transform_with_y_preservation(transform, end, start);
let orientation_rightward = gradient_orientation_in_fill_node(node_id, start, end, context).unwrap_or(true); let orientation_rightward = gradient_orientation_in_fill_node(node_id, transform, context).unwrap_or(true);
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24) let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
.tooltip_label("Reverse Direction") .tooltip_label("Reverse Direction")
@@ -27,7 +27,7 @@ impl FrontendGraphDataType {
match TaggedValue::from_type_or_none(input) { match TaggedValue::from_type_or_none(input) {
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number, TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Color(_) => Self::Color, TaggedValue::Color(_) => Self::Color,
TaggedValue::FillGradient(_) | TaggedValue::Gradient(_) => Self::Gradient, TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography, TaggedValue::String(_) => Self::Typography,
// Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name. // Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name.
TaggedValue::TypeDefault(td) => match td.name.as_ref() { TaggedValue::TypeDefault(td) => match td.name.as_ref() {
@@ -61,7 +61,7 @@ impl TypeSource {
FrontendGraphDataType::Number FrontendGraphDataType::Number
} }
TaggedValue::Color(_) => FrontendGraphDataType::Color, TaggedValue::Color(_) => FrontendGraphDataType::Color,
TaggedValue::FillGradient(_) | TaggedValue::Gradient(_) => FrontendGraphDataType::Gradient, TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => FrontendGraphDataType::Gradient,
TaggedValue::String(_) => FrontendGraphDataType::Typography, TaggedValue::String(_) => FrontendGraphDataType::Typography,
// Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name. // Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name.
TaggedValue::TypeDefault(td) => match td.name.as_ref() { TaggedValue::TypeDefault(td) => match td.name.as_ref() {
@@ -14,7 +14,8 @@ use graphene_std::ProtoNodeIdentifier;
use graphene_std::text::{TextAlign, TypesettingConfig}; use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::transform::ScaleType; use graphene_std::transform::ScaleType;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use graphene_std::vector::style::{Fill, PaintOrder, StrokeAlign}; use graphene_std::vector::graphic_types;
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
use std::collections::HashMap; use std::collections::HashMap;
use std::f64::consts::PI; use std::f64::consts::PI;
use std::ops::Range; use std::ops::Range;
@@ -1583,19 +1584,19 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// Fill: a literal Fill value is decomposed, and a wired input (`List<GradientStops> / List<Color>`) is kept as-is // Fill: a literal Fill value is decomposed, and a wired input (`List<GradientStops> / List<Color>`) is kept as-is
match old_inputs[1].as_value() { match old_inputs[1].as_value() {
Some(TaggedValue::Fill(old_fill)) => { Some(TaggedValue::LegacyFill(old_fill)) => {
let exposed = old_inputs[1].is_exposed(); let exposed = old_inputs[1].is_exposed();
let fill_value = match old_fill { let fill_value = match old_fill {
Fill::None => TaggedValue::Color(None), graphic_types::migrations::legacy::Fill::None => TaggedValue::Color(None),
Fill::Solid(color) => TaggedValue::Color(Some(*color)), graphic_types::migrations::legacy::Fill::Solid(color) => TaggedValue::Color(Some(*color)),
Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()), graphic_types::migrations::legacy::Fill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
}; };
document document
.network_interface .network_interface
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path); .set_input(&InputConnector::node(*node_id, 1), NodeInput::value(fill_value, exposed), network_path);
// Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults // Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults
if let Fill::Gradient(gradient) = old_fill { if let graphic_types::migrations::legacy::Fill::Gradient(gradient) = old_fill {
document.network_interface.set_input( document.network_interface.set_input(
&InputConnector::node(*node_id, 4), &InputConnector::node(*node_id, 4),
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false), NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
@@ -1620,7 +1621,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
} }
} }
// Wired/exposed fill keeps the connection. // Wired/exposed fill keeps the connection.
// The generic paint connector accepts `List<Color>`/`List<GradientStops>` sources directly, and there were no other nodes which can generate output type that implements `From` for `Fill`. // The generic paint connector accepts the existing `List<Color>`/`List<GradientStops>` paint sources directly.
_ => { _ => {
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, 1), old_inputs[1].clone(), network_path);
} }
@@ -1630,13 +1631,18 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
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, 2), old_inputs[2].clone(), network_path);
// Gradient backup: extract stops // Gradient backup: extract stops
if let Some(TaggedValue::FillGradient(g)) = old_inputs[3].as_value() { if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() {
document document
.network_interface .network_interface
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path); .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false), network_path);
// A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore // A solid/no-fill node leaves the gradient metadata inputs unused, so seed them from the backup gradient for a later Solid -> Gradient toggle to restore
if matches!(old_inputs[1].as_value(), Some(TaggedValue::Fill(Fill::None | Fill::Solid(_)))) { if matches!(
old_inputs[1].as_value(),
Some(TaggedValue::LegacyFill(
graphic_types::migrations::legacy::Fill::None | graphic_types::migrations::legacy::Fill::Solid(_)
))
) {
document document
.network_interface .network_interface
.set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path); .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), network_path);
@@ -60,8 +60,7 @@ impl ToolColorOptions {
return; return;
} }
if let Some(FillChoice::Solid(color)) = &self.fill_choice { if let Some(FillChoice::Solid(color)) = &self.fill_choice {
let fill = graphene_std::vector::style::Fill::Solid(*color); responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(*color) });
responses.add(GraphOperationMessage::FillSet { layer, fill });
} }
} }
@@ -70,8 +69,9 @@ impl ToolColorOptions {
return; return;
} }
if let Some(FillChoice::Solid(color)) = &self.fill_choice { if let Some(FillChoice::Solid(color)) = &self.fill_choice {
let stroke = graphene_std::vector::style::Stroke::new(Some(*color), weight); let color = Some(*color);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); let stroke = graphene_std::vector::style::Stroke::new(weight);
responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
} }
} }
@@ -175,8 +175,8 @@ impl DrawingToolState {
return; return;
} }
let Some(FillChoice::Solid(color)) = &self.stroke.fill_choice else { return }; let Some(FillChoice::Solid(color)) = &self.stroke.fill_choice else { return };
let color = Some(*color);
let stroke = graphene_std::vector::style::Stroke { let stroke = graphene_std::vector::style::Stroke {
color: Some(*color),
weight: self.effective_line_weight(), weight: self.effective_line_weight(),
align: self.stroke_align.unwrap_or_default(), align: self.stroke_align.unwrap_or_default(),
cap: self.stroke_cap.unwrap_or_default(), cap: self.stroke_cap.unwrap_or_default(),
@@ -187,7 +187,7 @@ impl DrawingToolState {
dash_offset: self.dash_offset.unwrap_or(0.), dash_offset: self.dash_offset.unwrap_or(0.),
transform: glam::DAffine2::IDENTITY, transform: glam::DAffine2::IDENTITY,
}; };
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
} }
} }
@@ -14,7 +14,7 @@ use graphene_std::raster_types::{CPU, GPU, Image, Raster};
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::{Fill, FillChoice, Gradient, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box}; use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, initial_gradient_transform_for_bounding_box};
use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType}; use graphene_std::vector::{GradientSpreadMethod, GradientStops, GradientType, PointId, SegmentId, VectorModificationType};
use graphene_std::{Color, Graphic}; use graphene_std::{Color, Graphic};
use std::collections::VecDeque; use std::collections::VecDeque;
@@ -330,8 +330,8 @@ pub fn get_gradient_stops(layer: LayerNodeIdentifier, network_interface: &NodeNe
} }
/// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<GradientStops>` /// Compute the transform from a gradient's local space to viewport space for the given layer. For a `List<GradientStops>`
/// layer this is the layer's incoming footprint transform; for the legacy `Fill::Gradient` path it composes the layer's /// layer this is the layer's incoming footprint transform; for a Fill-owned gradient value it composes the layer's viewport
/// viewport transform with the [0,1]² → bounding-box mapping. /// transform with the [0,1]² → bounding-box mapping.
pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 { pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> glam::DAffine2 {
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
@@ -350,9 +350,9 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
/// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely /// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely
/// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives. /// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives.
pub fn gradient_orientation_rightward(start: glam::DVec2, end: glam::DVec2, transform: glam::DAffine2) -> bool { pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool {
let viewport_start = transform.transform_point2(start); let viewport_start = transform.transform_point2(DVec2::ZERO);
let viewport_end = transform.transform_point2(end); let viewport_end = transform.transform_point2(DVec2::X);
if (viewport_end.x - viewport_start.x).abs() > f64::EPSILON * 1e6 { if (viewport_end.x - viewport_start.x).abs() > f64::EPSILON * 1e6 {
viewport_end.x > viewport_start.x viewport_end.x > viewport_start.x
} else { } else {
@@ -617,8 +617,9 @@ pub fn set_stroke_weight_for_selected_layers(weight: f64, document: &DocumentMes
let value = TaggedValue::F64(weight); let value = TaggedValue::F64(weight);
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value }); responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
} else if weight > 0. { } else if weight > 0. {
let color = Some(Color::BLACK);
let stroke = graphene_std::vector::style::Stroke::default().with_weight(weight); let stroke = graphene_std::vector::style::Stroke::default().with_weight(weight);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
} }
} }
} }
@@ -662,32 +663,6 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
transform_is_value: transform_input.is_some(), transform_is_value: transform_input.is_some(),
}) })
} }
// TODO: Update this to return Graphic once the legacy `Fill` enum has been eliminated
/// Returns the `Fill` value from a layer's upstream Fill node.
pub fn get_fill_value(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Fill> {
let fill_node_id = NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
let fill_node = network_interface.document_network().nodes.get(&fill_node_id)?;
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? {
&TaggedValue::Color(color) => Some(color.map_or(Fill::None, Fill::Solid)),
TaggedValue::Gradient(_) => {
let gradient = read_fill_node_gradient(fill_node, || network_interface.document_metadata().nonzero_bounding_box(layer))?;
Some(Fill::Gradient(Gradient {
stops: gradient.stops,
gradient_type: gradient.gradient_type,
spread_method: gradient.spread_method,
start: gradient.transform.transform_point2(DVec2::ZERO),
end: gradient.transform.transform_point2(DVec2::X),
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
}))
}
_ => None,
}
}
/// Returns the stroke color from a layer's upstream Stroke node. /// Returns the stroke color from a layer's upstream Stroke node.
pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> { pub fn get_stroke_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Option<Color>> {
let color_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX; let color_index = graphene_std::vector::stroke::PaintInput::<List<Graphic>>::INDEX;
@@ -717,10 +692,21 @@ pub struct SelectedStrokeState {
pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> { pub fn selected_fill_state(document: &DocumentMessageHandler) -> Option<SelectedFillState> {
let selected_nodes = document.network_interface.selected_nodes(); let selected_nodes = document.network_interface.selected_nodes();
let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| { let mut per_layer = selected_nodes.selected_layers_except_artboards(&document.network_interface).map(|layer| {
if get_fill_id(layer, &document.network_interface).is_none() { let Some(fill_node_id) = get_fill_id(layer, &document.network_interface) else {
return (false, FillChoice::None); return (false, FillChoice::None);
} };
let fill_choice = get_fill_value(layer, &document.network_interface).map_or(FillChoice::None, FillChoice::from);
let fill_choice = (|| {
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
match fill_node.inputs.get(graphene_std::vector::fill::FillInput::<List<Graphic>>::INDEX)?.as_value()? {
&TaggedValue::Color(color) => Some(color.map_or(FillChoice::None, FillChoice::Solid)),
TaggedValue::Gradient(stops) => Some(FillChoice::Gradient(stops.clone())),
_ => None,
}
})()
.unwrap_or(FillChoice::None);
(true, fill_choice) (true, fill_choice)
}); });
@@ -795,12 +781,40 @@ pub fn selected_stroke_state(document: &DocumentMessageHandler) -> Option<Select
pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect(); let layers: Vec<_> = document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface).collect();
for layer in layers { for layer in layers {
let existing_gradient = get_fill_value(layer, &document.network_interface).and_then(|f| match f { match &fill_choice {
Fill::Gradient(g) => Some(g), FillChoice::None => responses.add(GraphOperationMessage::FillColorSet { layer, color: None }),
_ => None, FillChoice::Solid(color) => responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(*color) }),
}); FillChoice::Gradient(stops) => {
let fill = fill_choice.clone().to_fill(existing_gradient.as_ref()); let fill_node = NodeGraphLayer::new(layer, &document.network_interface)
responses.add(GraphOperationMessage::FillSet { layer, fill }); .upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))
.and_then(|id| document.network_interface.document_network().nodes.get(&id));
let read = |index: usize| fill_node.and_then(|node| node.inputs.get(index)).and_then(|input| input.as_value());
let gradient_type = match read(graphene_std::vector::fill::GradientTypeInput::INDEX) {
Some(TaggedValue::GradientType(value)) => *value,
_ => GradientType::default(),
};
let spread_method = match read(graphene_std::vector::fill::SpreadMethodInput::INDEX) {
Some(TaggedValue::GradientSpreadMethod(value)) => *value,
_ => GradientSpreadMethod::default(),
};
let transform = match read(graphene_std::vector::fill::TransformInput::INDEX) {
Some(TaggedValue::OptionalDAffine2(value)) => {
value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(document.network_interface.document_metadata().nonzero_bounding_box(layer)))
}
_ => DAffine2::IDENTITY,
};
responses.add(GraphOperationMessage::FillGradientSet {
layer,
gradient: stops.clone(),
gradient_type,
spread_method,
transform,
});
}
}
} }
} }
@@ -815,8 +829,8 @@ pub fn set_stroke_color_for_selected_layers(color: Option<Color>, weight: f64, d
let value = TaggedValue::Color(color); let value = TaggedValue::Color(color);
responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value }); responses.add(NodeGraphMessage::SetInputValue { node_id, input_index, value });
} else { } else {
let stroke = graphene_std::vector::style::Stroke::new(color, weight); let stroke = graphene_std::vector::style::Stroke::new(weight);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
} }
} }
} }
@@ -4,7 +4,7 @@ use crate::messages::tool::common_functionality::color_selector::solid;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::vector::style::{Fill, FillChoiceUI}; use graphene_std::vector::style::FillChoiceUI;
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct FillTool { pub struct FillTool {
@@ -161,14 +161,17 @@ impl Fsm for FillToolFsmState {
if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) { if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) {
return self; return self;
} }
let fill = match color_event { let color = match color_event {
FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color), FillToolMessage::FillPrimaryColor => global_tool_data.primary_color,
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color), FillToolMessage::FillSecondaryColor => global_tool_data.secondary_color,
_ => return self, _ => return self,
}; };
responses.add(DocumentMessage::AddTransaction); responses.add(DocumentMessage::AddTransaction);
responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); responses.add(GraphOperationMessage::FillColorSet {
layer: layer_identifier,
color: Some(color),
});
FillToolFsmState::Filling FillToolFsmState::Filling
} }
File diff suppressed because it is too large Load Diff
@@ -2837,11 +2837,11 @@ impl Fsm for PathToolFsmState {
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
// Defaults chosen because the pasted geometry has no inherent associated style // Defaults chosen because the pasted geometry has no inherent associated style
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH); let color = Some(Color::BLACK);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); let stroke = graphene_std::vector::style::Stroke::new(DEFAULT_STROKE_WIDTH);
responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE); responses.add(GraphOperationMessage::FillColorSet { layer, color: Some(Color::WHITE) });
responses.add(GraphOperationMessage::FillSet { layer, fill });
new_layers.push(layer); new_layers.push(layer);
@@ -24,7 +24,7 @@ use graphene_std::choice_type::ChoiceTypeStatic;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::renderer::Quad; use graphene_std::renderer::Quad;
use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping}; use graphene_std::text::{Font, TextAlign, TypesettingConfig, lines_clipping};
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI}; use graphene_std::vector::style::{FillChoice, FillChoiceUI};
use graphene_std::{Color, NodeInputDecleration}; use graphene_std::{Color, NodeInputDecleration};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
@@ -571,9 +571,9 @@ impl TextToolData {
parent: document.new_layer_parent(true), parent: document.new_layer_parent(true),
insert_index: 0, insert_index: 0,
}); });
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillColorSet {
layer: self.layer, layer: self.layer,
fill: editing_text.color.map_or(Fill::None, Fill::Solid), color: editing_text.color,
}); });
let transform = editing_text.transform; let transform = editing_text.transform;
self.editing_text = Some(editing_text); self.editing_text = Some(editing_text);
+6 -7
View File
@@ -14,8 +14,7 @@ use graphene_std::memo::IORecord;
use graphene_std::raster::{CPU, Raster}; use graphene_std::raster::{CPU, Raster};
use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box};
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::Vector; use graphene_std::vector::{Vector, graphic_types};
use graphene_std::vector::style::Gradient;
use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration}; use graphene_std::{ATTR_TRANSFORM, Context, Graphic, NodeInputDecleration};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
use std::any::Any; use std::any::Any;
@@ -83,7 +82,7 @@ struct ExecutionContext {
/// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing /// Set when this execution is a gradient-migration measurement run, carrying the "Fill" node (addressed by its enclosing
/// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the /// network path) and its original relative gradient. The evaluated geometry is read back from the inspect result to size the
/// gradient; such runs never touch the visible artwork. Carrying the entry keeps a stale re-dispatched response paired with the fill it measured. /// gradient; such runs never touch the visible artwork. Carrying the entry keeps a stale re-dispatched response paired with the fill it measured.
measure_fill: Option<(Vec<NodeId>, NodeId, Gradient)>, measure_fill: Option<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
@@ -93,7 +92,7 @@ struct ExecutionContext {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct GradientMigration { struct GradientMigration {
document_id: DocumentId, document_id: DocumentId,
remaining: VecDeque<(Vec<NodeId>, NodeId, Gradient)>, remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)>,
resolution: UVec2, resolution: UVec2,
scale: f64, scale: f64,
} }
@@ -493,7 +492,7 @@ impl NodeGraphExecutor {
} }
// Snapshot the queue but leave `pending_gradient_bbox_bake` populated, so subsequent render requests keep deferring here (and hit the guard above); each entry is removed from the document as its bake lands. // Snapshot the queue but leave `pending_gradient_bbox_bake` populated, so subsequent render requests keep deferring here (and hit the guard above); each entry is removed from the document as its bake lands.
let remaining: VecDeque<(Vec<NodeId>, NodeId, Gradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect(); let remaining: VecDeque<(Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient)> = document.pending_gradient_bbox_bake.iter().cloned().collect();
let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else { let Some((first_network_path, first_fill, first_gradient)) = remaining.front().cloned() else {
return false; return false;
}; };
@@ -520,7 +519,7 @@ impl NodeGraphExecutor {
document_id: DocumentId, document_id: DocumentId,
network_path: Vec<NodeId>, network_path: Vec<NodeId>,
fill_node_id: NodeId, fill_node_id: NodeId,
gradient: Gradient, gradient: graphic_types::migrations::legacy::Gradient,
resolution: UVec2, resolution: UVec2,
scale: f64, scale: f64,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
@@ -589,7 +588,7 @@ impl NodeGraphExecutor {
&mut self, &mut self,
document: &mut DocumentMessageHandler, document: &mut DocumentMessageHandler,
document_id: DocumentId, document_id: DocumentId,
bake_target: (Vec<NodeId>, NodeId, Gradient), bake_target: (Vec<NodeId>, NodeId, graphic_types::migrations::legacy::Gradient),
inspect_result: Option<InspectResult>, inspect_result: Option<InspectResult>,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) { ) {
+9 -8
View File
@@ -13,7 +13,7 @@ pub use dyn_any::StaticType;
pub use glam::{DAffine2, DVec2, IVec2, UVec2}; pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::resource::ResourceHash; use graphene_application_io::resource::ResourceHash;
use graphic_types::raster_types::{CPU, Image, Raster}; use graphic_types::raster_types::{CPU, Image, Raster};
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops}; use graphic_types::vector_types::vector::style::GradientStops;
use graphic_types::vector_types::vector::{self, ReferencePoint}; use graphic_types::vector_types::vector::{self, ReferencePoint};
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
use rendering::RenderMetadata; use rendering::RenderMetadata;
@@ -65,7 +65,7 @@ macro_rules! tagged_value {
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")] #[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
Color(Option<Color>), Color(Option<Color>),
/// Stored compactly as a `GradientStops`, materializes as a single-row `List<GradientStops>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes. /// Stored compactly as a `GradientStops`, materializes as a single-row `List<GradientStops>` at runtime via `to_dynany`/`to_any`. Aliases recover legacy on-disk shapes.
/// (Old documents that stored a full `Gradient` struct under this same `"Gradient"` tag are routed to `FillGradient` by `deserialize_tagged_value_with_legacy_migration`.) /// (Old documents that stored a full `Gradient` struct under this same `"Gradient"` tag are routed to `LegacyGradient` by `deserialize_tagged_value_with_legacy_migration`.)
#[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient_stops")] // TODO: Eventually remove this migration document upgrade code #[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_to_gradient_stops")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")] #[serde(alias = "GradientTable", alias = "GradientPositions", alias = "GradientStops")]
Gradient(GradientStops), Gradient(GradientStops),
@@ -403,7 +403,8 @@ tagged_value! {
#[serde(alias = "Affine2")] #[serde(alias = "Affine2")]
DAffine2(DAffine2), DAffine2(DAffine2),
OptionalDAffine2(Option<DAffine2>), OptionalDAffine2(Option<DAffine2>),
FillGradient(Gradient), #[serde(alias = "FillGradient")]
LegacyGradient(graphic_types::migrations::legacy::Gradient),
Font(Font), Font(Font),
Footprint(Footprint), Footprint(Footprint),
VectorModification(Box<VectorModification>), VectorModification(Box<VectorModification>),
@@ -412,7 +413,8 @@ tagged_value! {
// ========== // ==========
// ENUM TYPES // ENUM TYPES
// ========== // ==========
Fill(vector::style::Fill), #[serde(alias = "Fill")]
LegacyFill(graphic_types::migrations::legacy::Fill),
BlendMode(core_types::blending::BlendMode), BlendMode(core_types::blending::BlendMode),
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation), LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel), QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
@@ -587,7 +589,6 @@ impl TaggedValue {
// The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row // The Fill and Stroke nodes' paint connectors default to `List<Graphic>`, their first registered implementation row
() if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?, () if ty == TypeId::of::<List<Graphic>>() => to_color(string).map(|color| TaggedValue::Color(Some(color)))?,
() if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?, () if ty == TypeId::of::<List<GradientStops>>() => to_gradient(string).map(TaggedValue::Gradient)?,
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, () if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
_ => return None, _ => return None,
}; };
@@ -655,11 +656,11 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
} }
return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Vector>)))); return Ok(MemoHash::new(TaggedValue::TypeDefault(descriptor!(List<Vector>))));
} }
// The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `FillGradient`), and now carries an `Option<GradientStops>`. // The `Gradient` tag was reused: it used to carry a full `Gradient` struct (now `LegacyGradient`), and now carries an `Option<GradientStops>`.
// Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `GradientStops` has none of those (it has `position`/`midpoint`/`color`). // Disambiguate by payload shape: a Gradient struct has `start`/`end` keys; a `GradientStops` has none of those (it has `position`/`midpoint`/`color`).
"Gradient" if content.as_object().is_some_and(|c| c.contains_key("start") && c.contains_key("end")) => { "Gradient" if content.as_object().is_some_and(|c| c.contains_key("start") && c.contains_key("end")) => {
let gradient: Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?; let gradient: graphic_types::migrations::legacy::Gradient = serde_json::from_value(content.clone()).map_err(serde::de::Error::custom)?;
return Ok(MemoHash::new(TaggedValue::FillGradient(gradient))); return Ok(MemoHash::new(TaggedValue::LegacyGradient(gradient)));
} }
_ => {} _ => {}
} }
@@ -137,13 +137,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::resource::Resource]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeCap]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeJoin]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::PaintOrder]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::PointSpacingType]),
@@ -250,14 +248,12 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Vec2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => glam::f32::Affine2]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::Font]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => List<BrushStroke>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => DocumentNode]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::Footprint]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
+16 -122
View File
@@ -1,17 +1,16 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List}; use core_types::list::{ATTR_FILL, ATTR_STROKE, ItemAttributeValues, List};
use core_types::ops::{FromAnchorPosition, ListConvert}; use core_types::ops::{FromAnchorPosition, ListConvert};
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color}; use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
use std::borrow::Cow; use std::borrow::Cow;
use vector_types::GradientStops; use vector_types::GradientStops;
pub use vector_types::Vector; pub use vector_types::Vector;
use vector_types::vector::style::Fill;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax. /// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny)] #[derive(Clone, Debug, CacheHash, PartialEq, DynAny)]
@@ -191,30 +190,6 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
output output
} }
/// Converts a `Fill` enum into the `List<Graphic>` representation used as paint storage.
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
pub fn fill_to_graphic_list(fill: &Fill) -> Option<List<Graphic>> {
match fill {
Fill::None => None,
Fill::Solid(color) => Some(List::new_from_element((*color).into())),
Fill::Gradient(gradient) => {
let gradient_item = Item::new_from_element(gradient.stops.clone())
.with_attribute(ATTR_TRANSFORM, gradient.transform * gradient.to_transform())
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type)
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method);
let gradient_list = List::new_from_item(gradient_item);
Some(List::new_from_element(Graphic::Gradient(gradient_list)))
}
}
}
/// Converts a `Color` into the `List<Graphic>` representation used as paint storage.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub fn color_to_graphic_list(color: Option<Color>) -> Option<List<Graphic>> {
color.as_ref().map(|color| List::new_from_element((*color).into()))
}
/// Whether a normalized paint graphic list actually carries renderable paint. /// Whether a normalized paint graphic list actually carries renderable paint.
/// A 0-item list, or a list whose first graphic is empty, is treated as no paint. /// A 0-item list, or a list whose first graphic is empty, is treated as no paint.
pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool { pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
@@ -225,7 +200,7 @@ pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> { pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
list.attribute::<List<Graphic>>(attribute, index) list.attribute::<List<Graphic>>(attribute, index)
.map(Cow::Borrowed) .map(Cow::Borrowed)
// Treat a blank attribute as absent so consumers fall back to the legacy `style` instead of masking it. // Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
.filter(|graphic_list| is_paint_present(graphic_list)) .filter(|graphic_list| is_paint_present(graphic_list))
} }
@@ -245,86 +220,6 @@ pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, pa
list.set_attribute(key, index, paint.into_graphic_list()); list.set_attribute(key, index, paint.into_graphic_list());
} }
/// Look up the fill paint graphics for a vector item, falling back to the legacy
/// `style.fill` when the attribute is absent or empty.
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
pub fn fill_graphic_list_at(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
graphic_list_at(list, index, ATTR_FILL).or_else(|| {
let vector = list.element(index)?;
fill_to_graphic_list(vector.style.fill()).map(Cow::Owned)
})
}
/// Look up the stroke paint graphics for a vector item, falling back to the legacy
/// `style.stroke.color` when the attribute is absent or empty.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub fn stroke_graphic_list_at(list: &List<Vector>, index: usize) -> Option<Cow<'_, List<Graphic>>> {
graphic_list_at(list, index, ATTR_STROKE).or_else(|| {
let vector = list.element(index)?;
color_to_graphic_list(vector.style.stroke().and_then(|s| s.color())).map(Cow::Owned)
})
}
/// Check whether the fill paint for a vector item is fully opaque, falling back to
/// the legacy `style.fill` when the attribute is absent.
/// This avoids the `List<Graphic>` allocation that the legacy `Fill` fallback path performs.
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
pub fn is_fill_opaque_at(list: &List<Vector>, index: usize) -> bool {
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_FILL) {
return graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque());
}
let Some(vector) = list.element(index) else { return false };
match vector.style.fill() {
Fill::None => false,
Fill::Solid(color) => color.is_opaque(),
Fill::Gradient(gradient) => gradient.stops.iter().all(|stop| stop.color.is_opaque()),
}
}
/// Check whether the fill paint for a vector item is fully transparent, falling back to
/// the legacy `style.fill` when the attribute is absent.
/// This avoids the `List<Graphic>` allocation that the legacy `Fill` fallback path performs.
/// TODO: Remove once all fill paint sources flow through `List<Graphic>` directly without going through the `Fill` enum.
pub fn is_fill_fully_transparent_at(list: &List<Vector>, index: usize) -> bool {
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_FILL) {
return graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent());
}
let Some(vector) = list.element(index) else { return false };
match vector.style.fill() {
Fill::None => true,
Fill::Solid(color) => color.a() == 0.,
Fill::Gradient(gradient) => gradient.stops.iter().all(|stop| stop.color.a() == 0.),
}
}
/// Check whether the stroke paint for a vector item is fully opaque, falling back to
/// the legacy `style.stroke.color` when the attribute is absent.
/// This avoids the `List<Graphic>` allocation that the legacy `Stroke.color` fallback path performs.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub fn is_stroke_opaque_at(list: &List<Vector>, index: usize) -> bool {
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_STROKE) {
return graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque());
}
let Some(color) = list.element(index).and_then(|vector| vector.style.stroke()).and_then(|stroke| stroke.color()) else {
return false;
};
color.is_opaque()
}
/// Check whether the stroke paint for a vector item is fully transparent, falling back to
/// the legacy `style.stroke.color` when the attribute is absent.
/// This avoids the `List<Graphic>` allocation that the legacy `Stroke.color` fallback path performs.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub fn is_stroke_fully_transparent_at(list: &List<Vector>, index: usize) -> bool {
if let Some(graphic_list) = graphic_list_at(list, index, ATTR_STROKE) {
return graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent());
}
let Some(color) = list.element(index).and_then(|vector| vector.style.stroke()).and_then(|stroke| stroke.color()) else {
return true;
};
color.a() == 0.
}
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the /// Bake the provided transform into the per-item transforms of the paint graphics stored under the
/// canonical `List<Graphic>` fill and stroke attributes. /// canonical `List<Graphic>` fill and stroke attributes.
pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) { pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DAffine2) {
@@ -544,17 +439,10 @@ impl Graphic {
let Some(element) = vector.element(index) else { return false }; let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let fill_opaque_or_absent = match graphic_list_at(vector, index, ATTR_FILL) { let fill_opaque_or_absent = graphic_list_at(vector, index, ATTR_FILL).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
Some(graphic_list) => graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()),
None => element.style.fill().is_opaque(),
};
let stroke_invisible_or_transparent = element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|| if let Some(graphic_list) = graphic_list_at(vector, index, ATTR_STROKE) { || graphic_list_at(vector, index, ATTR_STROKE).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())
} else {
element.style.stroke().and_then(|stroke| stroke.color()).is_none_or(|color| color.a() == 0.)
};
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
}), }),
@@ -566,13 +454,15 @@ impl Graphic {
match self { match self {
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque), Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
Graphic::Vector(list) => { Graphic::Vector(list) => {
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()));
!list.is_empty() !list.is_empty()
&& (0..list.len()).all(|i| { && (0..list.len()).all(|i| {
let Some(vector) = list.element(i) else { return false }; let Some(vector) = list.element(i) else { return false };
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_fill_opaque_at(list, i); let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at(ATTR_FILL, i);
let stroke_opaque_or_invisible = vector.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_stroke_opaque_at(list, i); let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at(ATTR_STROKE, i);
opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible
}) })
} }
@@ -587,13 +477,16 @@ impl Graphic {
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent), Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
Graphic::Vector(list) => (0..list.len()).all(|i| { Graphic::Vector(list) => (0..list.len()).all(|i| {
let Some(vector) = list.element(i) else { return false }; let Some(vector) = list.element(i) else { return false };
let is_paint_fully_transparent_at =
|key: &str, index: usize| graphic_list_at(list, index, key).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.); let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
if opacity <= f64::EPSILON { if opacity <= f64::EPSILON {
return true; return true;
} }
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.); let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
let fill_invisible = opacity_fill <= f64::EPSILON || is_fill_fully_transparent_at(list, i); let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at(ATTR_FILL, i);
let stroke_invisible = vector.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_stroke_fully_transparent_at(list, i); let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at(ATTR_STROKE, i);
fill_invisible && stroke_invisible fill_invisible && stroke_invisible
}), }),
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.), Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
@@ -785,6 +678,7 @@ mod tests {
#[cfg(test)] #[cfg(test)]
mod graphic_is_opaque_tests { mod graphic_is_opaque_tests {
use core_types::ATTR_SPREAD_METHOD;
use vector_types::{GradientSpreadMethod, GradientStop}; use vector_types::{GradientSpreadMethod, GradientStop};
use super::*; use super::*;
+141 -17
View File
@@ -11,50 +11,174 @@ pub use artboard::Artboard;
pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector}; pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector};
pub mod migrations { pub mod migrations {
use vector_types::vector::{PathStyle, PointDomain, RegionDomain, SegmentDomain, misc::HandleId};
use crate::Vector; use crate::Vector;
// Storing legacy structs that are only used in document migration.
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code
/// Returns the first `Vector` recovered from any of the legacy on-disk shapes (a single `Vector`, the old `OldVectorData` flat struct, or any of the historical `List<Vector>` variants). pub mod legacy {
pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Vector>, D::Error> { use core_types::Color;
use serde::Deserialize; use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use vector_types::vector::{PointDomain, RegionDomain, SegmentDomain, misc::HandleId, style::Stroke};
use vector_types::{GradientStops, Vector, vector};
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: vector::style::GradientType,
pub start: DVec2,
pub end: DVec2,
#[serde(default)]
pub spread_method: vector::style::GradientSpreadMethod,
#[serde(default)]
pub absolute: bool,
#[serde(default)]
pub transform: DAffine2,
}
impl Gradient {
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> Gradient {
let start = bounding_box.transform_point2(self.start);
let end = bounding_box.transform_point2(self.end);
let direction = end - start;
// The legacy radial drew as a circle in the layer's own space; bake the adjustment that, composed with the
// endpoint frame, makes the new pipeline reproduce that circle through the (possibly non-uniform) layer transform.
let radial_invertible = self.gradient_type == vector::style::GradientType::Radial
&& layer_transform.is_finite()
&& layer_transform.matrix2.determinant().recip().is_finite()
&& direction.length_squared() > 1e-20;
let transform = if radial_invertible {
let radius = (layer_transform.matrix2 * direction).length();
let circle = DAffine2 {
matrix2: glam::DMat2::from_diagonal(DVec2::splat(radius)),
translation: layer_transform.transform_point2(start),
};
let base = DAffine2::from_cols(direction, direction.perp(), start);
(layer_transform.inverse() * circle) * base.inverse()
} else {
DAffine2::IDENTITY
};
Gradient {
start,
end,
transform,
absolute: true,
..self.clone()
}
}
/// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0, 0) -> (1, 0).
pub fn to_transform(&self) -> DAffine2 {
let direction = self.end - self.start;
DAffine2::from_cols(direction, direction.perp(), self.start)
}
}
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum Fill {
#[default]
None,
Solid(Color),
Gradient(Gradient),
}
/// The legacy `fill` field is intentionally omitted because vector payload migration only
/// recovers editable vector data. The fill/stroke paints are migrated from the the node inputs.
#[derive(serde::Deserialize)]
#[cfg_attr(test, derive(Default, serde::Serialize))]
pub(super) struct PathStyle {
pub stroke: Option<Stroke>,
}
/// Old documents stored a `Vector` flattened with list attributes (`transform`, `alpha_blending`, `upstream_graphic_group`); only the geometry fields are recovered. /// Old documents stored a `Vector` flattened with list attributes (`transform`, `alpha_blending`, `upstream_graphic_group`); only the geometry fields are recovered.
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct OldVectorData { #[cfg_attr(test, derive(Default, serde::Serialize))]
style: PathStyle, pub(super) struct VectorData {
colinear_manipulators: Vec<[HandleId; 2]>, pub style: PathStyle,
point_domain: PointDomain, pub colinear_manipulators: Vec<[HandleId; 2]>,
segment_domain: SegmentDomain, pub point_domain: PointDomain,
region_domain: RegionDomain, pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct LegacyTable { pub(super) struct Table {
#[serde(alias = "instances", alias = "instance")] #[serde(alias = "instances", alias = "instance")]
element: Vec<Vector>, pub element: Vec<Vector>,
} }
}
// TODO: Eventually remove this migration document upgrade code
/// Returns the first `Vector` recovered from any of the legacy on-disk shapes (the legacy `VectorData` flat struct, a single `Vector`, or any of the historical `List<Vector>` variants).
pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Vector>, D::Error> {
use serde::Deserialize;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
enum VectorFormat { enum VectorFormat {
// Old vector data must be tried first. Serde would otherwise ignore its `style` field and
// deserialize the missing optional `stroke` field as `None` in the current `Vector`.
OldVectorData(legacy::VectorData),
Vector(Vector), Vector(Vector),
OldVectorData(OldVectorData), List(legacy::Table),
List(LegacyTable),
} }
Ok(match VectorFormat::deserialize(deserializer)? { Ok(match VectorFormat::deserialize(deserializer)? {
VectorFormat::Vector(vector) => Some(vector),
VectorFormat::OldVectorData(old) => Some(Vector { VectorFormat::OldVectorData(old) => Some(Vector {
style: old.style, stroke: old.style.stroke,
colinear_manipulators: old.colinear_manipulators, colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain, point_domain: old.point_domain,
segment_domain: old.segment_domain, segment_domain: old.segment_domain,
region_domain: old.region_domain, region_domain: old.region_domain,
}), }),
VectorFormat::Vector(vector) => Some(vector),
VectorFormat::List(list) => list.element.into_iter().next(), VectorFormat::List(list) => list.element.into_iter().next(),
}) })
} }
#[cfg(test)]
mod migration_tests {
use super::*;
use vector_types::vector::style::Stroke;
#[test]
fn preserves_stroke_from_old_vector_data_style() {
let old_vector = legacy::VectorData {
style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) },
..Default::default()
};
let mut value = serde_json::to_value(old_vector).unwrap();
value
.as_object_mut()
.unwrap()
.get_mut("style")
.unwrap()
.as_object_mut()
.unwrap()
.insert("fill".into(), serde_json::to_value(legacy::Fill::default()).unwrap());
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
assert_eq!(migrated.stroke.unwrap().weight, 12.);
}
#[test]
fn preserves_stroke_from_current_vector_data() {
let vector = Vector {
stroke: Some(Stroke::new(12.)),
..Default::default()
};
let value = serde_json::to_value(&vector).unwrap();
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
assert_eq!(migrated.stroke.unwrap().weight, 12.);
}
}
} }
+31 -33
View File
@@ -21,12 +21,12 @@ use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper; use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource; use graphene_resource::Resource;
use graphic_types::graphic::{fill_graphic_list_at, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, stroke_graphic_list_at}; use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType}; use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts};
use num_traits::Zero; use num_traits::Zero;
@@ -459,12 +459,10 @@ pub struct RenderMetadata {
pub text_frames: HashMap<NodeId, DAffine2>, pub text_frames: HashMap<NodeId, DAffine2>,
pub clip_targets: HashSet<NodeId>, pub clip_targets: HashSet<NodeId>,
pub vector_data: HashMap<NodeId, Arc<Vector>>, pub vector_data: HashMap<NodeId, Arc<Vector>>,
/// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read paint /// Per-layer `ATTR_FILL` row attribute, exposed so message handlers can read it.
/// information that lives on the list rather than on `PathStyle.fill`.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>, pub fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
/// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read /// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it.
/// stroke paint information that lives on the list rather than on `Stroke.color`.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>, pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
pub backgrounds: Vec<Background>, pub backgrounds: Vec<Background>,
@@ -1067,7 +1065,7 @@ impl Render for List<Vector> {
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = vector.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
let applied_stroke_transform = set_stroke_transform.unwrap_or(item_transform); let applied_stroke_transform = set_stroke_transform.unwrap_or(item_transform);
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
@@ -1087,26 +1085,26 @@ impl Render for List<Vector> {
path.push_str(bezpath.to_svg().as_str()); path.push_str(bezpath.to_svg().as_str());
} }
let mask_type = if vector.style.stroke().map(|x| x.align) == Some(StrokeAlign::Inside) { let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) {
MaskType::Clip MaskType::Clip
} else { } else {
MaskType::Mask MaskType::Mask
}; };
let fill_graphic_list = fill_graphic_list_at(self, index); let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL);
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0)); let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));
let stroke_graphic_list = stroke_graphic_list_at(self, index); let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE);
let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0)); let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0));
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed()); let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
let can_draw_aligned_stroke = path_is_closed let can_draw_aligned_stroke = path_is_closed
&& vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
&& stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent()); && stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip); let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip);
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); let wants_stroke_below = vector.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
let override_paint_order = can_draw_aligned_stroke && can_use_paint_order; let override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
let use_face_fill = vector.use_face_fill(); let use_face_fill = vector.use_face_fill();
@@ -1127,7 +1125,7 @@ impl Render for List<Vector> {
let id = format!("alignment-{}", generate_uuid()); let id = format!("alignment-{}", generate_uuid());
let mut cloned_vector = vector.clone(); let mut cloned_vector = vector.clone();
cloned_vector.style.clear_stroke(); cloned_vector.stroke = None;
// The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior. // The mask must draw at full alpha so the SVG `<mask>`/`<clipPath>` fully zeroes the path interior.
// The wrapping SVG group (above) handles the user-set opacity. // The wrapping SVG group (above) handles the user-set opacity.
@@ -1167,7 +1165,7 @@ impl Render for List<Vector> {
if let Some((ref id, mask_type, ref vector_item)) = push_id { if let Some((ref id, mask_type, ref vector_item)) = push_id {
let mut svg = SvgRender::new(); let mut svg = SvgRender::new();
vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let stroke = vector.style.stroke().unwrap(); let stroke = vector.stroke.as_ref().unwrap();
// `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed` // `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed`
let (largest_scale, _) = singular_values(applied_stroke_transform); let (largest_scale, _) = singular_values(applied_stroke_transform);
let inflation = stroke.max_aabb_inflation(true) * largest_scale; let inflation = stroke.max_aabb_inflation(true) * largest_scale;
@@ -1195,8 +1193,8 @@ impl Render for List<Vector> {
render_params.override_paint_order = override_paint_order; render_params.override_paint_order = override_paint_order;
let stroke_shape_attribute = vector let stroke_shape_attribute = vector
.style .stroke
.stroke() .as_ref()
.map(|stroke| { .map(|stroke| {
if stroke_graphic_list.as_deref().is_some_and(is_paint_present) { if stroke_graphic_list.as_deref().is_some_and(is_paint_present) {
stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke) stroke.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Stroke)
@@ -1207,7 +1205,7 @@ impl Render for List<Vector> {
.unwrap_or_default(); .unwrap_or_default();
// Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback // Need to avoid generating only paint attribute, otherwise SVG uses 1px width stroke as a fallback
let stroke_visible = vector.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent()); let stroke_visible = vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
let stroke_attribute = if stroke_visible { let stroke_attribute = if stroke_visible {
stroke_graphic_list stroke_graphic_list
.as_deref() .as_deref()
@@ -1282,7 +1280,7 @@ impl Render for List<Vector> {
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.); let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let multiplied_transform = parent_transform * item_transform; let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.style.stroke().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform)); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform); let mut applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
let mut element_transform = set_stroke_transform let mut element_transform = set_stroke_transform
@@ -1306,8 +1304,8 @@ impl Render for List<Vector> {
} }
} }
let fill_graphic_list = fill_graphic_list_at(self, index); let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL);
let stroke_graphic_list = stroke_graphic_list_at(self, index); let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE);
// If we're using opacity or a blend mode, we need to push a layer // If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
@@ -1319,10 +1317,10 @@ impl Render for List<Vector> {
// Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path). // Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since // Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down. // the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
let stroke = element.style.stroke(); let stroke = element.stroke.as_ref();
let stroke_fully_transparent = stroke_graphic_list.as_ref().is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent())); let stroke_fully_transparent = stroke_graphic_list.as_ref().is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
let can_draw_aligned_stroke = let can_draw_aligned_stroke =
!stroke_fully_transparent && stroke.as_ref().is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed()); !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || blend_mode_attr != BlendMode::default() { if opacity < 1. || blend_mode_attr != BlendMode::default() {
@@ -1330,7 +1328,7 @@ impl Render for List<Vector> {
// `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`. // `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`.
// Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform. // Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform.
let (_, smallest_scale) = singular_values(applied_stroke_transform); let (_, smallest_scale) = singular_values(applied_stroke_transform);
let stroke_inflation = stroke.as_ref().map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke)); let stroke_inflation = stroke.map_or(0., |s| s.max_aabb_inflation(can_draw_aligned_stroke));
let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. }; let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. };
let quad = Quad::from_box(layer_bounds).inflate(inflate_amount); let quad = Quad::from_box(layer_bounds).inflate(inflate_amount);
let layer_bounds = quad.bounding_box(); let layer_bounds = quad.bounding_box();
@@ -1344,7 +1342,7 @@ impl Render for List<Vector> {
} }
let use_layer = can_draw_aligned_stroke; let use_layer = can_draw_aligned_stroke;
let wants_stroke_below = stroke.as_ref().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow);
let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| { let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| {
let Some(fill_graphic) = fill_graphic_list.as_deref() else { return }; let Some(fill_graphic) = fill_graphic_list.as_deref() else { return };
@@ -1401,7 +1399,7 @@ impl Render for List<Vector> {
let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| { let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| {
let Some(stroke_graphic_list) = stroke_graphic_list.as_deref() else { return }; let Some(stroke_graphic_list) = stroke_graphic_list.as_deref() else { return };
let Some(stroke) = element.style.stroke() else { return }; let Some(stroke) = stroke else { return };
for paint_index in 0..stroke_graphic_list.len() { for paint_index in 0..stroke_graphic_list.len() {
let Some(stroke_graphic) = stroke_graphic_list.element(paint_index) else { let Some(stroke_graphic) = stroke_graphic_list.element(paint_index) else {
@@ -1474,7 +1472,7 @@ impl Render for List<Vector> {
_ => { _ => {
if use_layer { if use_layer {
let mut cloned_element = element.clone(); let mut cloned_element = element.clone();
cloned_element.style.clear_stroke(); cloned_element.stroke = None;
// The mask must draw at full alpha so `SrcOut` fully zeroes the path interior. // The mask must draw at full alpha so `SrcOut` fully zeroes the path interior.
// The outer opacity/blend layer (above) handles the user-set opacity. // The outer opacity/blend layer (above) handles the user-set opacity.
@@ -1484,13 +1482,13 @@ impl Render for List<Vector> {
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
// This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed // This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
let inflation = element.style.stroke().as_ref().map_or(0., |stroke| stroke.max_aabb_inflation(true)); let inflation = stroke.map_or(0., |stroke| stroke.max_aabb_inflation(true));
let (largest_scale, _) = singular_values(applied_stroke_transform); let (largest_scale, _) = singular_values(applied_stroke_transform);
let quad = Quad::from_box(bounds).inflate(inflation * largest_scale); let quad = Quad::from_box(bounds).inflate(inflation * largest_scale);
let bounds = quad.bounding_box(); let bounds = quad.bounding_box();
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
let compose = if element.style.stroke().is_some_and(|x| x.align == StrokeAlign::Outside) { let compose = if stroke.is_some_and(|x| x.align == StrokeAlign::Outside) {
peniko::Compose::SrcOut peniko::Compose::SrcOut
} else { } else {
peniko::Compose::SrcIn peniko::Compose::SrcIn
@@ -1527,7 +1525,7 @@ impl Render for List<Vector> {
Stroke, Stroke,
} }
let order = match element.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) { let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) {
true => [Op::Stroke, Op::Fill], true => [Op::Stroke, Op::Fill],
false => [Op::Fill, Op::Stroke], // Default false => [Op::Fill, Op::Stroke], // Default
}; };
@@ -1596,9 +1594,9 @@ impl Render for List<Vector> {
accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new)); accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new));
// Source geometry (not the click-target override) so editing tools work on letterforms. // Source geometry (not the click-target override) so editing tools work on letterforms.
// Recorded together with `vector_data` from the same (first) row so `style` stays consistent with the paint. // Recorded together with `vector_data` from the same (first) row so stroke geometry stays consistent with the paint.
// Only item 0 is recorded since editing tools can only target a single item currently. // Only item 0 is recorded since editing tools can only target a single item currently.
// If that row has no paint attribute, none is recorded and consumers fall back to `style`. // If that row has no paint attribute, none is recorded.
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) { if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
e.insert(Arc::new(source.clone())); e.insert(Arc::new(source.clone()));
@@ -1668,14 +1666,14 @@ impl Render for List<Vector> {
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work /// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) { fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) {
let filled = has_paint_at(vector_list, index, ATTR_FILL) || vector_list.element(index).is_some_and(|vector| !matches!(vector.style.fill(), Fill::None)); let filled = has_paint_at(vector_list, index, ATTR_FILL);
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect(); let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side, // Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths. // so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
let stroke_width = geometry.style.stroke().map_or(0., |stroke| { let stroke_width = geometry.stroke.as_ref().map_or(0., |stroke| {
if stroke.align.is_not_centered() && all_subpaths_closed { if stroke.align.is_not_centered() && all_subpaths_closed {
stroke.weight * 2. stroke.weight * 2.
} else { } else {
@@ -2,7 +2,7 @@ use core_types::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DVec2};
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)] #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
@@ -491,163 +491,6 @@ impl GradientSpreadMethod {
} }
} }
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
#[cfg_attr(feature = "serde", serde(default))]
pub spread_method: GradientSpreadMethod,
// TODO: Eventually remove this document upgrade code
/// Whether `start`/`end` are absolute (layer-space) rather than in the legacy [0,1] bounding-box space.
/// Documents predating the gradient migration deserialize this as `false`; the deferred migration converts
/// them and sets it `true`. Once all documents are migrated, the legacy rendering path can be removed.
#[cfg_attr(feature = "serde", serde(default))]
pub absolute: bool,
// TODO: Eventually remove this document upgrade code
/// An extra frame adjustment composed onto the `start`/`end` frame (`transform * to_transform()`), letting the gradient
/// describe shapes the endpoint pair cannot, such as an elliptical radial. It defaults to identity, so existing documents
/// (which only have `start`/`end`) are unaffected; the migration stores a non-identity value only where it's needed.
#[cfg_attr(feature = "serde", serde(default))]
pub transform: DAffine2,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
spread_method: GradientSpreadMethod::Pad,
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
}
}
}
impl std::fmt::Display for Gradient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f64| (x * 1e3).round() / 1e3;
let stops = self
.stops
.iter()
.map(|stop| format!("[{}%: #{}]", round(stop.position * 100.), SRGBA8::from(stop.color).to_rgba_hex()))
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} Gradient: {stops}", self.gradient_type)
}
}
impl Gradient {
// TODO: Eventually remove this document upgrade code
/// Converts a legacy bounding-box-relative gradient (`start`/`end` in [0,1]) into an absolute one in the geometry's local space.
/// `bounding_box` maps [0,1] onto the geometry's bounding box; `layer_transform` is the layer's own transform,
/// used to bake the elliptical adjustment that reproduces the legacy isotropic radial through a non-uniform layer.
pub fn to_absolute(&self, bounding_box: DAffine2, layer_transform: DAffine2) -> Gradient {
let start = bounding_box.transform_point2(self.start);
let end = bounding_box.transform_point2(self.end);
let direction = end - start;
// The legacy radial drew as a circle in the layer's own space; bake the adjustment that, composed with the
// endpoint frame, makes the new pipeline reproduce that circle through the (possibly non-uniform) layer transform.
let radial_invertible =
self.gradient_type == GradientType::Radial && layer_transform.is_finite() && layer_transform.matrix2.determinant().recip().is_finite() && direction.length_squared() > 1e-20;
let transform = if radial_invertible {
let radius = (layer_transform.matrix2 * direction).length();
let circle = DAffine2 {
matrix2: DMat2::from_diagonal(DVec2::splat(radius)),
translation: layer_transform.transform_point2(start),
};
let base = DAffine2::from_cols(direction, direction.perp(), start);
(layer_transform.inverse() * circle) * base.inverse()
} else {
DAffine2::IDENTITY
};
Gradient {
start,
end,
transform,
// TODO: Eventually remove this document upgrade code
absolute: true,
..self.clone()
}
}
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self {
let stops = GradientStops::new([
GradientStop {
position: 0.,
midpoint: 0.5,
color: start_color,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: end_color,
},
]);
Self {
start,
end,
stops,
gradient_type,
spread_method,
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
}
}
/// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse.
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
// Calculate the new position by finding the closest point on the line
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
// Don't insert point past end of line
if !(0. ..=1.).contains(&new_position) {
return None;
}
// Compute the color of the inserted stop using evaluate (which respects midpoints)
let new_color = self.stops.evaluate(new_position);
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.stops.len() > index && self.stops.position[index] <= new_position {
index += 1;
}
// Insert the new stop, duplicating the midpoint ratio of the interval being split
let inherited_midpoint = if index > 0 { self.stops.midpoint[index - 1] } else { 0.5 };
self.stops.position.insert(index, new_position);
self.stops.midpoint.insert(index, inherited_midpoint);
self.stops.color.insert(index, new_color);
Some(index)
}
/// Builds the affine that places the gradient endpoints at `start` and `end` when applied to canonical gradient space (0, 0) -> (1, 0).
pub fn to_transform(&self) -> DAffine2 {
let direction = self.end - self.start;
DAffine2::from_cols(direction, direction.perp(), self.start)
}
}
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both /// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
/// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint /// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
/// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks. /// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
@@ -732,27 +575,3 @@ impl core_types::bounds::BoundingBox for GradientStops {
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)]) core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use glam::DVec2;
fn linear_gradient(start: DVec2, end: DVec2) -> Gradient {
Gradient { start, end, ..Default::default() }
}
#[test]
fn to_transform_roundtrip() {
let cases = [(DVec2::ZERO, DVec2::X), (DVec2::new(10., 20.), DVec2::new(50., 30.)), (DVec2::new(-5., -5.), DVec2::new(5., 3.))];
for (start, end) in cases {
let transform = linear_gradient(start, end).to_transform();
let recovered_start = transform.transform_point2(DVec2::ZERO);
let recovered_end = transform.transform_point2(DVec2::X);
assert!((recovered_start - start).length() < 1e-10);
assert!((recovered_end - end).length() < 1e-10);
}
}
}
@@ -8,7 +8,6 @@ mod vector_modification;
mod vector_types; mod vector_types;
pub use reference_point::*; pub use reference_point::*;
pub use style::PathStyle;
pub use vector_attributes::*; pub use vector_attributes::*;
pub use vector_modification::*; pub use vector_modification::*;
pub use vector_types::*; pub use vector_types::*;
@@ -1,150 +1,16 @@
//! Contains stylistic options for SVG elements. //! Contains stylistic options for SVG elements.
pub use crate::gradient::*; pub use crate::gradient::*;
use core_types::color::{Alpha, SRGBA8}; use core_types::Color;
use core_types::list::List; use core_types::color::SRGBA8;
use core_types::transform::Transform; use core_types::transform::Transform;
use core_types::{ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::DAffine2; use glam::DAffine2;
use glam::DVec2;
use std::f64::consts::{PI, TAU}; use std::f64::consts::{PI, TAU};
/// Describes the fill of a layer. /// Describes an editable fill choice, storing color or gradient stops without gradient placement metadata.
/// ///
/// Can be None, a solid [Color], or a linear/radial [Gradient]. /// Can be None, a solid [Color], or a linear/radial [GradientStops].
///
/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future.
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Fill {
#[default]
None,
Solid(Color),
Gradient(Gradient),
}
impl std::fmt::Display for Fill {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", SRGBA8::from(*color).to_rgb_hex(), color.a() * 100.),
Self::Gradient(gradient) => write!(f, "{gradient}"),
}
}
}
impl Fill {
/// Construct a new [Fill::Solid] from a [Color].
pub fn solid(color: Color) -> Self {
Self::Solid(color)
}
/// Construct a new [Fill::Solid] or [Fill::None] from an optional [Color].
pub fn solid_or_none(color: Option<Color>) -> Self {
match color {
Some(color) => Self::Solid(color),
None => Self::None,
}
}
/// Evaluate the color at some point on the fill. Doesn't currently work for Gradient.
pub fn color(&self) -> Color {
match self {
Self::None => Color::BLACK,
Self::Solid(color) => *color,
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
Self::Gradient(Gradient { stops, .. }) => {
if stops.is_empty() {
Color::BLACK
} else {
stops.color[0]
}
}
}
}
/// Extract a gradient from the fill
pub fn as_gradient(&self) -> Option<&Gradient> {
match self {
Self::Gradient(gradient) => Some(gradient),
_ => None,
}
}
/// Extract a solid color from the fill
pub fn as_solid(&self) -> Option<Color> {
match self {
Self::Solid(color) => Some(*color),
_ => None,
}
}
/// Find if fill can be represented with only opaque colors
pub fn is_opaque(&self) -> bool {
match self {
Fill::Solid(color) => color.is_opaque(),
Fill::Gradient(gradient) => gradient.stops.color.iter().all(|color| color.is_opaque()),
Fill::None => true,
}
}
/// Returns if fill is none
pub fn is_none(&self) -> bool {
*self == Self::None
}
}
impl From<Color> for Fill {
fn from(color: Color) -> Fill {
Fill::Solid(color)
}
}
impl From<Option<Color>> for Fill {
fn from(color: Option<Color>) -> Fill {
Fill::solid_or_none(color)
}
}
impl From<List<Color>> for Fill {
fn from(color: List<Color>) -> Fill {
let alpha: f64 = color.attribute_cloned_or(ATTR_OPACITY, 0, 1.);
let color = color.element(0).copied();
Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha as f32)))
}
}
impl From<List<GradientStops>> for Fill {
fn from(gradient: List<GradientStops>) -> Fill {
let gradient_type = gradient.attribute_cloned_or_default::<GradientType>(ATTR_GRADIENT_TYPE, 0);
let spread_method = gradient.attribute_cloned_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD, 0);
let transform = gradient.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0);
Fill::Gradient(Gradient {
stops: gradient.element(0).cloned().unwrap_or_default(),
gradient_type,
spread_method,
start: transform.transform_point2(DVec2::ZERO),
end: transform.transform_point2(DVec2::X),
// TODO: Eventually remove this document upgrade code
absolute: true,
transform: DAffine2::IDENTITY,
})
}
}
impl From<Gradient> for Fill {
fn from(gradient: Gradient) -> Fill {
Fill::Gradient(gradient)
}
}
/// Describes the fill of a layer, but unlike [`Fill`], this doesn't store a [`Gradient`] directly but just its [`GradientStops`].
///
/// Can be None, a solid [Color], or a linear/radial [Gradient].
/// ///
/// In the future we'll probably also add a pattern fill. /// In the future we'll probably also add a pattern fill.
/// ///
@@ -238,30 +104,6 @@ impl FillChoice {
Self::Gradient(stops) => Some(stops.to_css_linear_gradient()), Self::Gradient(stops) => Some(stops.to_css_linear_gradient()),
} }
} }
/// Convert this [`FillChoice`] to a [`Fill`] using the provided [`Gradient`] as a base for the positional information of the gradient.
/// If a gradient isn't provided, default gradient positional information is used in cases where the [`FillChoice`] is a [`Gradient`].
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill {
match self {
Self::None => Fill::None,
Self::Solid(color) => Fill::Solid(*color),
Self::Gradient(stops) => {
let mut fill = existing_gradient.cloned().unwrap_or_default();
fill.stops = stops.clone();
Fill::Gradient(fill)
}
}
}
}
impl From<Fill> for FillChoice {
fn from(fill: Fill) -> Self {
match fill {
Fill::None => FillChoice::None,
Fill::Solid(color) => FillChoice::Solid(color),
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops),
}
}
} }
/// The stroke (outline) style of an SVG element. /// The stroke (outline) style of an SVG element.
@@ -365,9 +207,6 @@ fn daffine2_identity() -> DAffine2 {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct Stroke { pub struct Stroke {
/// Deprecated, use `ATTR_STROKE` instead.
/// TODO: Remove once all stroke paint sources flow through `List<Graphic>` directly without going through `Stroke.color`.
pub color: Option<Color>,
/// Line thickness /// Line thickness
pub weight: f64, pub weight: f64,
pub dash_lengths: Vec<f64>, pub dash_lengths: Vec<f64>,
@@ -387,9 +226,8 @@ pub struct Stroke {
} }
impl Stroke { impl Stroke {
pub const fn new(color: Option<Color>, weight: f64) -> Self { pub const fn new(weight: f64) -> Self {
Self { Self {
color,
weight, weight,
dash_lengths: Vec::new(), dash_lengths: Vec::new(),
dash_offset: 0., dash_offset: 0.,
@@ -404,7 +242,6 @@ impl Stroke {
pub fn lerp(&self, other: &Self, time: f64) -> Self { pub fn lerp(&self, other: &Self, time: f64) -> Self {
Self { Self {
color: self.color.map(|color| color.lerp(&other.color.unwrap_or(color), time as f32)),
weight: self.weight + (other.weight - self.weight) * time, weight: self.weight + (other.weight - self.weight) * time,
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(), dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(),
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time, dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time,
@@ -440,11 +277,6 @@ impl Stroke {
} }
} }
/// Get the current stroke color.
pub fn color(&self) -> Option<Color> {
self.color
}
/// Get the current stroke weight. /// Get the current stroke weight.
pub fn weight(&self) -> f64 { pub fn weight(&self) -> f64 {
self.weight self.weight
@@ -510,12 +342,6 @@ impl Stroke {
self.join_miter_limit as f32 self.join_miter_limit as f32
} }
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
self.color = *color;
Some(self)
}
pub fn with_weight(mut self, weight: f64) -> Self { pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight; self.weight = weight;
self self
@@ -564,12 +390,10 @@ impl Stroke {
} }
} }
// Having an alpha of 1 to start with leads to a better experience with the properties panel
impl Default for Stroke { impl Default for Stroke {
fn default() -> Self { fn default() -> Self {
Self { Self {
weight: 0., weight: 0.,
color: Some(Color::BLACK),
dash_lengths: Vec::new(), dash_lengths: Vec::new(),
dash_offset: 0., dash_offset: 0.,
cap: StrokeCap::Butt, cap: StrokeCap::Butt,
@@ -582,129 +406,6 @@ impl Default for Stroke {
} }
} }
#[repr(C)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, PartialEq, Default, graphene_hash::CacheHash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PathStyle {
pub stroke: Option<Stroke>,
pub fill: Fill,
}
impl std::fmt::Display for PathStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fill = &self.fill;
let stroke = match &self.stroke {
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| SRGBA8::from(c).to_rgba_hex()), stroke.weight),
None => "None".to_string(),
};
write!(f, "Fill: {fill}\nStroke: {stroke}")
}
}
impl PathStyle {
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
Self { stroke, fill }
}
/// Get the current path's [Fill].
///
/// # Example
/// ```
/// # use vector_types::vector::style::{Fill, PathStyle};
/// # use core_types::Color;
/// let fill = Fill::solid(Color::RED);
/// let style = PathStyle::new(None, fill.clone());
///
/// assert_eq!(*style.fill(), fill);
/// ```
pub fn fill(&self) -> &Fill {
&self.fill
}
pub fn fill_mut(&mut self) -> &mut Fill {
&mut self.fill
}
/// Get the current path's [Stroke].
///
/// # Example
/// ```
/// # use vector_types::vector::style::{Fill, Stroke, PathStyle};
/// # use core_types::Color;
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
/// let style = PathStyle::new(Some(stroke.clone()), Fill::None);
///
/// assert_eq!(style.stroke(), Some(stroke));
/// ```
pub fn stroke(&self) -> Option<Stroke> {
self.stroke.clone()
}
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
if let Some(stroke) = &mut self.stroke {
stroke.transform = transform;
}
}
/// Replace the path's [Stroke] with a provided one.
///
/// # Example
/// ```
/// # use vector_types::vector::style::{Stroke, PathStyle};
/// # use core_types::Color;
/// let mut style = PathStyle::default();
///
/// assert_eq!(style.stroke(), None);
///
/// let stroke = Stroke::new(Some(Color::GREEN), 42.);
/// style.set_stroke(stroke.clone());
///
/// assert_eq!(style.stroke(), Some(stroke));
/// ```
pub fn set_stroke(&mut self, stroke: Stroke) {
self.stroke = Some(stroke);
}
/// Set the path's fill to None.
///
/// # Example
/// ```
/// # use vector_types::vector::style::{Fill, PathStyle};
/// # use core_types::Color;
/// let mut style = PathStyle::new(None, Fill::Solid(Color::RED));
///
/// assert_ne!(*style.fill(), Fill::None);
///
/// style.clear_fill();
///
/// assert_eq!(*style.fill(), Fill::None);
/// ```
pub fn clear_fill(&mut self) {
self.fill = Fill::None;
}
/// Set the path's stroke to None.
///
/// # Example
/// ```
/// # use vector_types::vector::style::{Fill, Stroke, PathStyle};
/// # use core_types::Color;
/// let mut style = PathStyle::new(Some(Stroke::new(Some(Color::GREEN), 42.)), Fill::None);
///
/// assert!(style.stroke().is_some());
///
/// style.clear_stroke();
///
/// assert!(!style.stroke().is_some());
/// ```
pub fn clear_stroke(&mut self) {
self.stroke = None;
}
}
/// Ways the user can choose to view the artwork in the viewport. /// Ways the user can choose to view the artwork in the viewport.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny)]
@@ -720,39 +421,3 @@ pub enum RenderMode {
/// Render a preview of how the object would be exported as an SVG. /// Render a preview of how the object would be exported as an SVG.
SvgPreview, SvgPreview,
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_from_gradient_list_preserves_attributes() {
let mut list = List::new_from_element(GradientStops::default());
list.set_attribute(ATTR_GRADIENT_TYPE, 0, GradientType::Radial);
list.set_attribute(ATTR_SPREAD_METHOD, 0, GradientSpreadMethod::Reflect);
list.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 7.)));
let Fill::Gradient(gradient) = Fill::from(list) else {
panic!("expected Fill::Gradient");
};
assert_eq!(gradient.gradient_type, GradientType::Radial);
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
assert_eq!(gradient.start, DVec2::new(5., 7.));
assert_eq!(gradient.end, DVec2::new(6., 7.));
}
#[test]
fn fill_from_empty_gradient_list_uses_defaults() {
let list = List::new_from_element(GradientStops::default());
let Fill::Gradient(gradient) = Fill::from(list) else {
panic!("expected Fill::Gradient");
};
assert_eq!(gradient.gradient_type, GradientType::default());
assert_eq!(gradient.spread_method, GradientSpreadMethod::default());
assert_eq!(gradient.start, DVec2::ZERO);
assert_eq!(gradient.end, DVec2::X);
}
}
@@ -1,12 +1,11 @@
use super::misc::dvec2_to_point; use super::misc::dvec2_to_point;
use super::style::{PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use super::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
pub use super::vector_attributes::*; pub use super::vector_attributes::*;
use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath}; use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath};
use crate::vector::click_target::{ClickTargetType, FreePoint}; use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::vector::misc::{HandleId, ManipulatorPointId}; use crate::vector::misc::{HandleId, ManipulatorPointId};
use crate::vector::vector_modification::VectorExt; use crate::vector::vector_modification::VectorExt;
use core::borrow::Borrow; use core::borrow::Borrow;
use core_types::Color;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::transform::Transform; use core_types::transform::Transform;
@@ -19,7 +18,7 @@ use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vector { pub struct Vector {
pub style: PathStyle, pub stroke: Option<Stroke>,
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another). /// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another).
/// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it). /// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it).
@@ -36,7 +35,7 @@ unsafe impl StaticType for Vector {
impl Default for Vector { impl Default for Vector {
fn default() -> Self { fn default() -> Self {
Self { Self {
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), stroke: Some(Stroke::new(0.)),
colinear_manipulators: Vec::new(), colinear_manipulators: Vec::new(),
point_domain: PointDomain::new(), point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(), segment_domain: SegmentDomain::new(),
@@ -50,7 +49,7 @@ impl graphene_hash::CacheHash for Vector {
self.point_domain.cache_hash(state); self.point_domain.cache_hash(state);
self.segment_domain.cache_hash(state); self.segment_domain.cache_hash(state);
self.region_domain.cache_hash(state); self.region_domain.cache_hash(state);
self.style.cache_hash(state); self.stroke.cache_hash(state);
self.colinear_manipulators.cache_hash(state); self.colinear_manipulators.cache_hash(state);
} }
} }
@@ -243,7 +242,7 @@ impl Vector {
pub fn stroke_inclusive_bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> { pub fn stroke_inclusive_bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let path_bounds = self.bounding_box_with_transform(transform); let path_bounds = self.bounding_box_with_transform(transform);
let Some(stroke) = self.style.stroke() else { return path_bounds }; let Some(stroke) = self.stroke.as_ref() else { return path_bounds };
// Stroke alignment is only honored by the renderer when every subpath is closed; open paths fall // Stroke alignment is only honored by the renderer when every subpath is closed; open paths fall
// back to drawing a Center-aligned `weight`-wide stroke. Match that behavior to keep bounds in sync. // back to drawing a Center-aligned `weight`-wide stroke. Match that behavior to keep bounds in sync.
let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezier_paths().all(|p| p.closed()); let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezier_paths().all(|p| p.closed());
@@ -532,10 +531,16 @@ impl Vector {
self.region_domain.concat(&additional.region_domain, transform_of_additional, &id_map); self.region_domain.concat(&additional.region_domain, transform_of_additional, &id_map);
// TODO: properly deal with fills such as gradients // TODO: properly deal with fills such as gradients
self.style = additional.style.clone(); self.stroke = additional.stroke.clone();
self.colinear_manipulators.extend(additional.colinear_manipulators.iter().copied()); self.colinear_manipulators.extend(additional.colinear_manipulators.iter().copied());
} }
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
if let Some(stroke) = &mut self.stroke {
stroke.transform = transform;
}
}
} }
impl BoundingBox for Vector { impl BoundingBox for Vector {
@@ -549,8 +554,9 @@ impl BoundingBox for Vector {
} }
// Include stroke by adding offset based on stroke width // Include stroke by adding offset based on stroke width
let stroke_width = self.style.stroke().map(|s| s.weight()).unwrap_or_default(); let stroke = self.stroke.clone();
let miter_limit = self.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.); let stroke_width = stroke.as_ref().map(|s| s.weight()).unwrap_or_default();
let miter_limit = stroke.as_ref().map(|s| s.join_miter_limit).unwrap_or(1.);
let scale = transform.scale_magnitudes(); let scale = transform.scale_magnitudes();
// Use the full line width to account for different styles of stroke caps // Use the full line width to account for different styles of stroke caps
+5 -12
View File
@@ -16,7 +16,6 @@ use linesweeper::{BinaryOp, FillRule, binary_op};
use smallvec::SmallVec; use smallvec::SmallVec;
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
pub use vector_types::vector::misc::BooleanOperation; pub use vector_types::vector::misc::BooleanOperation;
use vector_types::vector::style::Fill;
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls, // TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
// TODO: since before we used a Vec of single-item `List`s and now we use a single `List` // TODO: since before we used a Vec of single-item `List`s and now we use a single `List`
@@ -50,7 +49,7 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
let result_vector = result_vector_list.element_mut(0).unwrap(); let result_vector = result_vector_list.element_mut(0).unwrap();
Vector::transform(result_vector, transform); Vector::transform(result_vector, transform);
result_vector.style.set_stroke_transform(DAffine2::IDENTITY); result_vector.set_stroke_transform(DAffine2::IDENTITY);
// Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them // Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
// for editor click-target preservation. // for editor click-target preservation.
@@ -148,16 +147,10 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
bake_paint_transforms(&mut attributes, copy_from_transform); bake_paint_transforms(&mut attributes, copy_from_transform);
let copy_from = vector.element(index).unwrap(); let copy_from = vector.element(index).unwrap();
let mut element = Vector { let element = Vector {
style: copy_from.style.clone(), stroke: copy_from.stroke.clone(),
..Default::default() ..Default::default()
}; };
// Legacy Fill fallback: An absolute gradient lives in the geometry's space, so bake the same transform into it to track the baked points
if let Fill::Gradient(gradient) = element.style.fill_mut()
&& gradient.absolute
{
gradient.transform = copy_from_transform * gradient.transform;
}
Item::from_parts(element, attributes) Item::from_parts(element, attributes)
} else { } else {
Item::<Vector>::default() Item::<Vector>::default()
@@ -288,7 +281,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color)); set_paint_attribute(&mut attributes, ATTR_FILL, List::new_from_element(color));
let mut element = Vector::default(); let mut element = Vector::default();
element.style.set_stroke_transform(DAffine2::IDENTITY); element.set_stroke_transform(DAffine2::IDENTITY);
Item::from_parts(element, attributes) Item::from_parts(element, attributes)
}) })
@@ -311,7 +304,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint); set_paint_attribute(&mut attributes, ATTR_FILL, gradient_paint);
let mut element = Vector::default(); let mut element = Vector::default();
element.style.set_stroke_transform(DAffine2::IDENTITY); element.set_stroke_transform(DAffine2::IDENTITY);
Item::from_parts(element, attributes) Item::from_parts(element, attributes)
}) })
+33 -44
View File
@@ -13,7 +13,7 @@ use core_types::{
}; };
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::graphic::{bake_paint_transforms, fill_graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute, set_paint_attribute_at, stroke_graphic_list_at}; use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Graphic, IntoGraphicList}; use graphic_types::{Graphic, IntoGraphicList};
use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
@@ -143,7 +143,7 @@ where
if fill { if fill {
set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone()); set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone());
} }
if stroke && vector_list.element(index).and_then(|vector| vector.style.stroke()).is_some() { if stroke && vector_list.element(index).is_some_and(|vector| vector.stroke.is_some()) {
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone()); set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
} }
@@ -328,8 +328,6 @@ where
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect();
let stroke = Stroke { let stroke = Stroke {
// TODO: Remove once the deprecated `Stroke.color` field is deleted in favor of the `ATTR_STROKE` attribute
color: None,
weight, weight,
dash_lengths, dash_lengths,
dash_offset, dash_offset,
@@ -344,7 +342,7 @@ where
content.for_each_vector_mut(|vector, transform| { content.for_each_vector_mut(|vector, transform| {
let mut stroke = stroke.clone(); let mut stroke = stroke.clone();
stroke.transform *= transform; stroke.transform *= transform;
vector.style.set_stroke(stroke); vector.stroke = Some(stroke);
}); });
let paint = paint.into_graphic_list(); let paint = paint.into_graphic_list();
@@ -471,7 +469,7 @@ async fn round_corners(
let edge_length_limit = edge_length_limit * 0.005; let edge_length_limit = edge_length_limit * 0.005;
let mut result = Vector { let mut result = Vector {
style: source.style.clone(), stroke: source.stroke.clone(),
..Default::default() ..Default::default()
}; };
@@ -850,7 +848,7 @@ async fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<
}); });
} }
result.style.set_stroke_transform(DAffine2::IDENTITY); result.set_stroke_transform(DAffine2::IDENTITY);
// Add this to the `List` and reset the transform since we've applied it directly to the points // Add this to the `List` and reset the transform since we've applied it directly to the points
*row.element_mut() = result; *row.element_mut() = result;
@@ -1012,7 +1010,7 @@ async fn auto_tangents(
let source = source.element(index).unwrap(); let source = source.element(index).unwrap();
let mut result = Vector { let mut result = Vector {
style: source.style.clone(), stroke: source.stroke.clone(),
..Default::default() ..Default::default()
}; };
@@ -1162,8 +1160,8 @@ async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
}) })
.unwrap_or_default(); .unwrap_or_default();
result.style = vector.style.clone(); result.stroke = vector.stroke.clone();
result.style.set_stroke_transform(DAffine2::IDENTITY); result.set_stroke_transform(DAffine2::IDENTITY);
*row.element_mut() = result; *row.element_mut() = result;
row row
@@ -1226,10 +1224,10 @@ async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: St
let bezpaths = vector.stroke_bezpath_iter(); let bezpaths = vector.stroke_bezpath_iter();
let mut result = Vector { let mut result = Vector {
style: vector.style.clone(), stroke: vector.stroke.clone(),
..Default::default() ..Default::default()
}; };
result.style.set_stroke_transform(DAffine2::IDENTITY); result.set_stroke_transform(DAffine2::IDENTITY);
// Perform operation on all subpaths in this shape. // Perform operation on all subpaths in this shape.
for mut bezpath in bezpaths { for mut bezpath in bezpaths {
@@ -1266,10 +1264,8 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
let flattened: List<Vector> = graphic_list.clone().into_flattened_list(); let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
// A fill exists when the canonical attribute carries paint or, matching the renderer's fallback, when the legacy `style.fill` does // A fill exists when the canonical attribute carries paint
let has_fills: Vec<bool> = (0..flattened.len()) let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint_at(&flattened, index, ATTR_FILL)).collect();
.map(|index| has_paint_at(&flattened, index, ATTR_FILL) || flattened.element(index).is_some_and(|vector| !vector.style.fill().is_none()))
.collect();
let mut output: List<Vector> = flattened let mut output: List<Vector> = flattened
.into_iter() .into_iter()
@@ -1277,7 +1273,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
.flat_map(|(row, has_fill)| { .flat_map(|(row, has_fill)| {
let (mut vector, attributes) = row.into_parts(); let (mut vector, attributes) = row.into_parts();
let stroke = vector.style.stroke().clone().unwrap_or_default(); let stroke = vector.stroke.clone().unwrap_or_default();
let bezpaths = vector.stroke_bezpath_iter(); let bezpaths = vector.stroke_bezpath_iter();
let mut solidified_stroke = Vector::default(); let mut solidified_stroke = Vector::default();
@@ -1326,7 +1322,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
// If the original vector has a fill, preserve it as a separate item with the stroke cleared. // If the original vector has a fill, preserve it as a separate item with the stroke cleared.
let fill_row = has_fill.then(|| { let fill_row = has_fill.then(|| {
vector.style.clear_stroke(); vector.stroke = None;
let mut fill_attributes = attributes.clone(); let mut fill_attributes = attributes.clone();
// No stroke remains on the fill row // No stroke remains on the fill row
fill_attributes.remove::<List<Graphic>>(ATTR_STROKE); fill_attributes.remove::<List<Graphic>>(ATTR_STROKE);
@@ -1338,13 +1334,6 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL); stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL); stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
// Fall back to the legacy stroke color when no canonical stroke paint attribute was carried over
if !stroke_attributes.get::<List<Graphic>>(ATTR_FILL).is_some_and(is_paint_present)
&& let Some(color) = stroke.color
{
set_paint_attribute(&mut stroke_attributes, ATTR_FILL, List::new_from_element(color));
}
let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes);
// Ordering based on the paint order. The first item in the `List` is rendered below the second. // Ordering based on the paint order. The first item in the `List` is rendered below the second.
@@ -1389,7 +1378,7 @@ async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
return vec![row]; return vec![row];
} }
let style = row.element().style.clone(); let stroke = row.element().stroke.clone();
let (_, attributes) = row.into_parts(); let (_, attributes) = row.into_parts();
bezpaths bezpaths
@@ -1397,7 +1386,7 @@ async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
.map(|bezpath| { .map(|bezpath| {
let mut vector = Vector::default(); let mut vector = Vector::default();
vector.append_bezpath(bezpath); vector.append_bezpath(bezpath);
vector.style = style.clone(); vector.stroke = stroke.clone();
Item::from_parts(vector, attributes.clone()) Item::from_parts(vector, attributes.clone())
}) })
@@ -1464,9 +1453,9 @@ pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(Lis
let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index); let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index);
output.concat(element, source_transform, collision_hash_seed); output.concat(element, source_transform, collision_hash_seed);
// TODO: Make this instead use the first encountered style // TODO: Make this instead use the first encountered stroke
// Use the last encountered style as the output style // Use the last encountered stroke as the output stroke
output.style = element.style.clone(); output.stroke = element.stroke.clone();
primary_source = Some((index, source_transform)); primary_source = Some((index, source_transform));
} }
@@ -1532,10 +1521,10 @@ async fn sample_polyline(
segment_domain: Default::default(), segment_domain: Default::default(),
region_domain: Default::default(), region_domain: Default::default(),
colinear_manipulators: Default::default(), colinear_manipulators: Default::default(),
style: std::mem::take(&mut row.element_mut().style), stroke: std::mem::take(&mut row.element_mut().stroke),
}; };
// Transfer the stroke transform from the input vector content to the result. // Transfer the stroke transform from the input vector content to the result.
result.style.set_stroke_transform(row.attribute_cloned_or_default(ATTR_TRANSFORM)); result.set_stroke_transform(row.attribute_cloned_or_default(ATTR_TRANSFORM));
for local_bezpath in row.element().stroke_bezpath_iter() { for local_bezpath in row.element().stroke_bezpath_iter() {
// Apply the transform to compute sample locations in world space (for correct distance-based spacing) // Apply the transform to compute sample locations in world space (for correct distance-based spacing)
@@ -1606,7 +1595,7 @@ async fn simplify(
let inverse_transform = transform.inverse(); let inverse_transform = transform.inverse();
let mut result = Vector { let mut result = Vector {
style: std::mem::take(&mut row.element_mut().style), stroke: std::mem::take(&mut row.element_mut().stroke),
..Default::default() ..Default::default()
}; };
@@ -1702,7 +1691,7 @@ async fn decimate(
let inverse_transform = transform.inverse(); let inverse_transform = transform.inverse();
let mut result = Vector { let mut result = Vector {
style: std::mem::take(&mut row.element_mut().style), stroke: std::mem::take(&mut row.element_mut().stroke),
..Default::default() ..Default::default()
}; };
@@ -1781,7 +1770,7 @@ async fn cut_path(
if let Some((row_index, bezpath)) = bezpaths.get(index).cloned() { if let Some((row_index, bezpath)) = bezpaths.get(index).cloned() {
let mut result_vector = Vector { let mut result_vector = Vector {
style: content.element(row_index).unwrap().style.clone(), stroke: content.element(row_index).unwrap().stroke.clone(),
..Default::default() ..Default::default()
}; };
@@ -1991,8 +1980,8 @@ async fn scatter_points(
} }
// Transfer the style from the input vector content to the result. // Transfer the style from the input vector content to the result.
result.style = row.element().style.clone(); result.stroke = row.element().stroke.clone();
result.style.set_stroke_transform(DAffine2::IDENTITY); result.set_stroke_transform(DAffine2::IDENTITY);
*row.element_mut() = result; *row.element_mut() = result;
row row
@@ -2632,8 +2621,7 @@ async fn morph<I: IntoGraphicList>(
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes)); return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
} }
let mut vector = Vector::default(); let stroke = match (source_element.stroke.as_ref(), target_element.stroke.as_ref()) {
vector.style.stroke = match (source_element.style.stroke.as_ref(), target_element.style.stroke.as_ref()) {
(Some(a), Some(b)) => Some(a.lerp(b, time)), (Some(a), Some(b)) => Some(a.lerp(b, time)),
(Some(a), None) => { (Some(a), None) => {
if time < 0.5 { if time < 0.5 {
@@ -2651,15 +2639,16 @@ async fn morph<I: IntoGraphicList>(
} }
(None, None) => None, (None, None) => None,
}; };
let mut vector = Vector { stroke, ..Default::default() };
let fill_paint = { let fill_paint = {
let source = fill_graphic_list_at(&content, source_index); let source = graphic_list_at(&content, source_index, ATTR_FILL);
let target = fill_graphic_list_at(&content, target_index); let target = graphic_list_at(&content, target_index, ATTR_FILL);
lerp_graphic(source.as_deref(), target.as_deref(), time) lerp_graphic(source.as_deref(), target.as_deref(), time)
}; };
let stroke_paint = { let stroke_paint = {
let source = stroke_graphic_list_at(&content, source_index); let source = graphic_list_at(&content, source_index, ATTR_STROKE);
let target = stroke_graphic_list_at(&content, target_index); let target = graphic_list_at(&content, target_index, ATTR_STROKE);
lerp_graphic(source.as_deref(), target.as_deref(), time) lerp_graphic(source.as_deref(), target.as_deref(), time)
}; };
@@ -3457,7 +3446,7 @@ mod test {
let morphed = super::morph(Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()).await; let morphed = super::morph(Footprint::default(), content, 0.5, false, InterpolationDistribution::default(), List::default()).await;
let fill = fill_graphic_list_at(&morphed, 0).expect("Morph should keep the fill paint at the midpoint"); let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint");
// Interpolated color between red and blue should have >0 value on both R and B // Interpolated color between red and blue should have >0 value on both R and B
let Some(Graphic::Color(colors)) = fill.element(0) else { let Some(Graphic::Color(colors)) = fill.element(0) else {