Adopt the cascading "appearance" attribute in place of Vector::stroke and the "fill"/"paint" attributes (#4433)

Reimplemented on the record model in place of upstream d117c3eace.

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers
2026-09-15 14:49:57 +02:00
committed by Dennis Kobert
co-authored by Dennis Kobert
parent 28e5d1bdc9
commit a41b362fe2
44 changed files with 2087 additions and 1021 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -37,8 +37,8 @@ pub enum PathStep {
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorTableTab { pub enum VectorTableTab {
#[default] #[default]
Properties,
Points, Points,
Segments, Segments,
Regions, Regions,
Handles,
} }
@@ -585,7 +585,7 @@ impl TableItemLayout for Vector {
) )
} }
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> { fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions] let table_tab_entries = [VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions, VectorTableTab::Handles]
.into_iter() .into_iter()
.map(|tab| { .map(|tab| {
RadioEntryData::new(format!("{tab:?}")) RadioEntryData::new(format!("{tab:?}"))
@@ -597,61 +597,6 @@ impl TableItemLayout for Vector {
let mut table_rows = Vec::new(); let mut table_rows = Vec::new();
match data.vector_table_tab { match data.vector_table_tab {
VectorTableTab::Properties => {
table_rows.push(column_headings(&["property", "value"]));
if let Some(stroke) = self.stroke.as_ref() {
table_rows.push(vec![
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").narrow(true).widget_instance(),
TextLabel::new(if stroke.dash_lengths.is_empty() {
"-".to_string()
} else {
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
})
.narrow(true)
.widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Offset").narrow(true).widget_instance(),
TextLabel::new(format!("{}", stroke.dash_offset)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Cap").narrow(true).widget_instance(),
TextLabel::new(stroke.cap.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Join").narrow(true).widget_instance(),
TextLabel::new(stroke.join.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").narrow(true).widget_instance(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Align").narrow(true).widget_instance(),
TextLabel::new(stroke.align.to_string()).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").narrow(true).widget_instance(),
TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").narrow(true).widget_instance(),
TextLabel::new(stroke.paint_order.to_string()).narrow(true).widget_instance(),
]);
}
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
table_rows.push(vec![
TextLabel::new("Colinear Handle IDs").narrow(true).widget_instance(),
TextLabel::new(colinear).narrow(true).widget_instance(),
]);
}
VectorTableTab::Points => { VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"])); table_rows.push(column_headings(&["", "position"]));
table_rows.extend(self.point_domain.iter().map(|(id, position)| { table_rows.extend(self.point_domain.iter().map(|(id, position)| {
@@ -683,6 +628,16 @@ impl TableItemLayout for Vector {
] ]
})); }));
} }
VectorTableTab::Handles => {
table_rows.push(column_headings(&["", "colinear_manipulators[0]", "colinear_manipulators[1]"]));
table_rows.extend(self.colinear_manipulators.iter().enumerate().map(|(index, [a, b])| {
vec![
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
TextLabel::new(format!("{a}")).narrow(true).widget_instance(),
TextLabel::new(format!("{b}")).narrow(true).widget_instance(),
]
}));
}
} }
vec![LayoutGroup::row(table_tabs), LayoutGroup::table(table_rows, false)] vec![LayoutGroup::row(table_tabs), LayoutGroup::table(table_rows, false)]
@@ -11,9 +11,8 @@ use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use glam::{DAffine2, IVec2}; use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Appearance;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
@@ -245,14 +244,10 @@ pub enum DocumentMessage {
vector_data: HashMap<NodeId, Arc<Vector>>, vector_data: HashMap<NodeId, Arc<Vector>>,
}, },
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it. // `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point. // `UpdateAppearanceAttributes` is produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
#[serde(skip)] #[serde(skip)]
UpdateFillAttributes { UpdateAppearanceAttributes {
fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>, appearance_attributes: HashMap<NodeId, Arc<Appearance>>,
},
#[serde(skip)]
UpdateStrokeAttributes {
stroke_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
}, },
Undo, Undo,
UngroupSelectedLayers, UngroupSelectedLayers,
@@ -37,7 +37,7 @@ use graph_craft::application_io::wgpu_available;
use graph_craft::descriptor; use graph_craft::descriptor;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::graphic::is_paint_present; use graphene_std::Cover;
use graphene_std::math::quad::Quad; 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;
@@ -1502,9 +1502,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
.collect(); .collect();
self.network_interface.update_vector_data(layer_vector_data); self.network_interface.update_vector_data(layer_vector_data);
} }
DocumentMessage::UpdateFillAttributes { fill_attributes } => { DocumentMessage::UpdateAppearanceAttributes { appearance_attributes } => {
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers // Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
let layer_fill_attributes = fill_attributes let layer_appearance_attributes = appearance_attributes
.into_iter() .into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id)) .filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, attrs)| { .filter_map(|(node_id, attrs)| {
@@ -1514,21 +1514,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
}) })
}) })
.collect(); .collect();
self.network_interface.update_fill_attributes(layer_fill_attributes); self.network_interface.update_appearance_attributes(layer_appearance_attributes);
}
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
let layer_stroke_attributes = stroke_attributes
.into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, attrs)| {
self.network_interface.is_layer(&node_id, &[]).then(|| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
(layer, attrs)
})
})
.collect();
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
} }
DocumentMessage::Undo => { DocumentMessage::Undo => {
if self.network_interface.transaction_status() != TransactionStatus::Finished { if self.network_interface.transaction_status() != TransactionStatus::Finished {
@@ -2763,20 +2749,19 @@ 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 Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else { if !self.network_interface.document_metadata().layer_vector_data.contains_key(&layer) {
resulting_layers.push(layer.to_node()); 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 appearance = self.network_interface.document_metadata().layer_appearance_attributes.get(&layer);
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list)); let has_fill = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
// `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint. // A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
// So stroke visibility must be checked from `ATTR_STROKE`, the paint source of truth. let has_stroke = appearance.is_some_and(|appearance| {
let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent())); appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible; && appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_fully_transparent())
});
// 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 {
@@ -10,7 +10,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::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
use graphene_std::vector::{Gradient, PointId, VectorModificationType}; use graphene_std::vector::{Gradient, PointId, VectorModificationType};
#[impl_message(Message, DocumentMessage, GraphOperation)] #[impl_message(Message, DocumentMessage, GraphOperation)]
@@ -93,6 +93,10 @@ pub enum GraphOperationMessage {
color: Option<Color>, color: Option<Color>,
stroke: Stroke, stroke: Stroke,
}, },
StrokePaintOrderSet {
layer: LayerNodeIdentifier,
paint_order: PaintOrder,
},
TransformChange { TransformChange {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
transform: DAffine2, transform: DAffine2,
@@ -14,7 +14,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::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{Gradient, GradientForm, GradientSettings, GradientSpace, GradientSpread, GradientStop, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color}; use graphene_std::{Artboard, Color};
#[derive(ExtractField)] #[derive(ExtractField)]
@@ -132,6 +132,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
modify_inputs.stroke_set(color, stroke); modify_inputs.stroke_set(color, stroke);
} }
} }
GraphOperationMessage::StrokePaintOrderSet { layer, paint_order } => {
if let Some(stroke_node_id) = crate::messages::tool::common_functionality::graph_modification_utils::get_stroke_id(layer, network_interface)
&& super::utility_types::set_stroke_paint_order(network_interface, &[], stroke_node_id, paint_order)
{
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
GraphOperationMessage::TransformChange { GraphOperationMessage::TransformChange {
layer, layer,
transform, transform,
@@ -945,7 +953,6 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
}, },
join_miter_limit: stroke.miterlimit().get() as f64, join_miter_limit: stroke.miterlimit().get() as f64,
align: StrokeAlign::Center, align: StrokeAlign::Center,
paint_order: PaintOrder::StrokeAbove,
transform, transform,
}, },
) )
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type, ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type,
}; };
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface}; use crate::messages::portfolio::document::utility_types::network_interface::{self, FlowType, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{ use crate::messages::tool::common_functionality::graph_modification_utils::{
ReplaceablePaintChain, get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain, ReplaceablePaintChain, get_fill_input_node_id, get_upstream_gradient_value_node_id, gradient_chain_target_input, replaceable_paint_chain,
@@ -19,7 +19,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::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, Stroke}; use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType}; use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic}; use graphene_std::{Artboard, Color, Graphic};
@@ -858,8 +858,6 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashPatternInput); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashPatternInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::DashPattern(stroke.dash_lengths), false), true); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::DashPattern(stroke.dash_lengths), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput); let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput);
@@ -1007,3 +1005,63 @@ impl<'a> ModifyInputsContext<'a> {
} }
} }
} }
/// The wires feeding off a node's primary output.
fn primary_output_consumers(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], node_id: NodeId) -> Vec<InputConnector> {
network_interface
.outward_wires(network_path)
.and_then(|wires| wires.get(&OutputConnector::node(node_id, 0)).cloned())
.unwrap_or_default()
}
/// The fill wired directly to the stroke on either side, and whether the stroke currently paints above it.
/// The downstream node of the pair appends its cover last, so it paints on top.
pub fn adjacent_fill_of_stroke(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId) -> Option<(NodeId, bool)> {
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
let is_fill = |network_interface: &NodeNetworkInterface, node_id: &NodeId| network_interface.reference(node_id, network_path) == Some(fill_reference.clone());
let stroke_primary_source = match network_interface.input_from_connector(&InputConnector::node_at_index(stroke_node_id, 0), network_path) {
Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id),
_ => None,
};
if let Some(source) = stroke_primary_source.filter(|source| is_fill(network_interface, source)) {
return Some((source, true));
}
let consumers = primary_output_consumers(network_interface, network_path, stroke_node_id);
let fill_consumer = consumers.iter().find_map(|connector| match connector {
InputConnector::Node { node_id, input_index: 0 } if is_fill(network_interface, node_id) => Some(*node_id),
_ => None,
});
fill_consumer.map(|fill_node_id| (fill_node_id, false))
}
/// Swaps a chain's directly adjacent Stroke and Fill nodes when their order disagrees with the requested
/// paint order: both nodes append their cover, so the downstream one of the pair paints on top, following
/// the painter's algorithm. Without a fill wired directly to the stroke, nothing changes.
/// Returns whether the graph changed.
pub fn set_stroke_paint_order(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId, paint_order: PaintOrder) -> bool {
let Some((fill_node_id, currently_above)) = adjacent_fill_of_stroke(network_interface, network_path, stroke_node_id) else {
return false;
};
if (paint_order == PaintOrder::StrokeAbove) == currently_above {
return false;
}
let (upstream, downstream) = if currently_above { (fill_node_id, stroke_node_id) } else { (stroke_node_id, fill_node_id) };
let Some(upstream_source) = network_interface.input_from_connector(&InputConnector::node_at_index(upstream, 0), network_path).cloned() else {
return false;
};
let downstream_consumers = primary_output_consumers(network_interface, network_path, downstream);
network_interface.set_input(&InputConnector::node_at_index(downstream, 0), upstream_source, network_path);
network_interface.set_input(&InputConnector::node_at_index(upstream, 0), NodeInput::node(downstream, 0), network_path);
for consumer in &downstream_consumers {
if matches!(consumer, InputConnector::Node { node_id, .. } if *node_id == upstream || *node_id == downstream) {
continue;
}
network_interface.set_input(consumer, NodeInput::node(upstream, 0), network_path);
}
true
}
@@ -2680,9 +2680,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
ParameterWidgetsInfo::new(node_id, MiterLimitInput, true, context), ParameterWidgetsInfo::new(node_id, MiterLimitInput, true, context),
NumberInput::default().min(0.).disabled(miter_limit_disabled), NumberInput::default().min(0.).disabled(miter_limit_disabled),
); );
let paint_order = enum_choice::<PaintOrder>()
.for_socket(ParameterWidgetsInfo::new(node_id, PaintOrderInput, true, context))
.property_row();
let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths); let disabled_number_input = NumberInput::default().unit(" px").disabled(has_dash_lengths);
let dash_lengths = dash_pattern_widget(ParameterWidgetsInfo::new(node_id, DashPatternInput, true, context), TextInput::default().centered(true)); let dash_lengths = dash_pattern_widget(ParameterWidgetsInfo::new(node_id, DashPatternInput, true, context), TextInput::default().centered(true));
let number_input = disabled_number_input; let number_input = disabled_number_input;
@@ -2695,7 +2692,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
cap, cap,
join, join,
LayoutGroup::row(miter_limit), LayoutGroup::row(miter_limit),
paint_order,
LayoutGroup::row(dash_lengths), LayoutGroup::row(dash_lengths),
LayoutGroup::row(dash_offset), LayoutGroup::row(dash_offset),
] ]
@@ -6,8 +6,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Graphic; use graphene_std::Appearance;
use graphene_std::list::List;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath; use graphene_std::subpath;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
@@ -41,12 +40,9 @@ pub struct DocumentMetadata {
/// Vector data keyed by layer ID, used as fallback when no Path node exists. /// Vector data keyed by layer ID, used as fallback when no Path node exists.
/// This provides accurate SegmentIds for layers without explicit Path nodes. /// This provides accurate SegmentIds for layers without explicit Path nodes.
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>, pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>,
/// Per-layer `ATTR_FILL` attribute, exposed so message handlers can read paint /// Per-layer resolved appearance snapshot, exposed so message handlers can
/// information that lives on the list. /// read the paint and stroke parameters that live on the coverage rows.
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>, pub layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>,
/// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read
/// stroke paint information that lives on the list.
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>,
/// Transform from document space to viewport space. /// Transform from document space to viewport space.
pub document_to_viewport: DAffine2, pub document_to_viewport: DAffine2,
} }
@@ -233,10 +229,15 @@ impl DocumentMetadata {
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based /// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
/// bounds for non-vector layers (groups, raster, text, color, gradient). /// bounds for non-vector layers (groups, raster, text, color, gradient).
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> { pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
if let Some(vector) = self.layer_vector_data.get(&layer) if let Some(vector) = self.layer_vector_data.get(&layer) {
&& let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer)) let stroke = self
{ .layer_appearance_attributes
return Some(bounds); .get(&layer)
.and_then(|appearance| appearance.first_coverage_of(graphene_std::Cover::Stroke))
.map(graphene_std::Coverage::stroke_params);
if let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer), stroke.as_ref()) {
return Some(bounds);
}
} }
self.bounding_box_document(layer) self.bounding_box_document(layer)
} }
@@ -40,8 +40,6 @@ use graph_craft::Type;
use graph_craft::application_io::resource::ResourceId; use graph_craft::application_io::resource::ResourceId;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graphene_std::Graphic;
use graphene_std::list::List;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
@@ -183,13 +183,8 @@ impl NodeNetworkInterface {
self.document_metadata.layer_vector_data = new_layer_vector_data; self.document_metadata.layer_vector_data = new_layer_vector_data;
} }
/// Update the per-layer `ATTR_FILL` snapshot. /// Update the per-layer resolved appearance snapshot.
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>) { pub fn update_appearance_attributes(&mut self, new_layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<graphene_std::Appearance>>) {
self.document_metadata.layer_fill_attributes = new_layer_fill_attributes; self.document_metadata.layer_appearance_attributes = new_layer_appearance_attributes;
}
/// Update the per-layer `ATTR_STROKE` snapshot.
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>) {
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
} }
} }
@@ -1,6 +1,7 @@
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
// This file contains lots of hacky code for upgrading old documents to the new format // This file contains lots of hacky code for upgrading old documents to the new format
use crate::messages::portfolio::document::graph_operation::utility_types::set_stroke_paint_order;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type}; use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, NodeTemplateImplementation, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, NodeTemplateImplementation, OutputConnector};
@@ -1857,13 +1858,12 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
inputs_count = 7; inputs_count = 7;
} }
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644) // Upgrade Stroke node to reorder parameters and add "Align" (#2644), landing on the shape without the retired "Paint Order" input
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 { if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 8 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false); let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
@@ -1872,10 +1872,33 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 4), old_inputs[5].clone(), network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 4), old_inputs[5].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 5), old_inputs[6].clone(), network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 5), old_inputs[6].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 6), old_inputs[7].clone(), network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 6), old_inputs[7].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), paint_order_input, network_path);
let dash_input = migrate_dash_input(&old_inputs[3]).unwrap_or_else(|| old_inputs[3].clone()); let dash_input = migrate_dash_input(&old_inputs[3]).unwrap_or_else(|| old_inputs[3].clone());
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), dash_input, network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), dash_input, network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 9), old_inputs[4].clone(), network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), old_inputs[4].clone(), network_path);
inputs_count = 9;
}
// The Stroke node's "Paint Order" input was retired in favor of the relative order of the Fill and Stroke
// nodes in the chain, so the stored value becomes a topology rewrite that reorders the two nodes.
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER) && inputs_count == 10 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
// A wired paint order input cannot be evaluated statically, so it degrades to the default and leaves its source disconnected
let paint_order = match old_inputs.get(7).and_then(|input| input.as_value()) {
Some(&TaggedValue::PaintOrder(value)) => value,
_ => PaintOrder::StrokeAbove,
};
for (index, input) in old_inputs.iter().enumerate().take(7) {
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
}
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), old_inputs[8].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), old_inputs[9].clone(), network_path);
inputs_count = 9;
set_stroke_paint_order(&mut document.network_interface, network_path, *node_id, paint_order);
} }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
@@ -183,12 +183,18 @@ impl DrawingToolState {
cap: self.stroke_cap.unwrap_or_default(), cap: self.stroke_cap.unwrap_or_default(),
join: self.stroke_join.unwrap_or_default(), join: self.stroke_join.unwrap_or_default(),
join_miter_limit: self.miter_limit.unwrap_or(4.), join_miter_limit: self.miter_limit.unwrap_or(4.),
paint_order: self.paint_order.unwrap_or_default(),
dash_lengths: self.effective_dash_lengths(), dash_lengths: self.effective_dash_lengths(),
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, color, stroke }); responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke });
// The paint order is the chain order of the Fill and Stroke nodes, so a below choice reorders the new pair
if self.paint_order.unwrap_or_default() == graphene_std::vector::style::PaintOrder::StrokeBelow {
responses.add(GraphOperationMessage::StrokePaintOrderSet {
layer,
paint_order: graphene_std::vector::style::PaintOrder::StrokeBelow,
});
}
} }
} }
@@ -692,9 +692,22 @@ pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNe
Some(TaggedValue::F64(value)) => *value, Some(TaggedValue::F64(value)) => *value,
_ => 4., _ => 4.,
}; };
let paint_order = match parameters.value(stroke::PaintOrderInput) { // The paint order is the chain order of the Fill and Stroke nodes: the downstream one of an adjacent pair paints on top
Some(TaggedValue::PaintOrder(value)) => *value, let paint_order = {
_ => PaintOrder::default(), let stroke_node_id = get_stroke_id(layer, network_interface);
let primary_source = |node_id: Option<NodeId>| {
node_id.and_then(|node_id| match network_interface.input_from_connector(&InputConnector::node_at_index(node_id, 0), &[]) {
Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id),
_ => None,
})
};
if primary_source(stroke_node_id).is_some_and(|source| Some(source) == get_fill_id(layer, network_interface)) {
PaintOrder::StrokeAbove
} else if primary_source(get_fill_id(layer, network_interface)).is_some_and(|source| Some(source) == stroke_node_id) {
PaintOrder::StrokeBelow
} else {
PaintOrder::default()
}
}; };
let dash_lengths = match parameters.value(stroke::DashPatternInput) { let dash_lengths = match parameters.value(stroke::DashPatternInput) {
Some(TaggedValue::DashPattern(lengths)) => lengths.clone(), Some(TaggedValue::DashPattern(lengths)) => lengths.clone(),
@@ -198,7 +198,10 @@ pub fn apply_miter_limit(drawing: &mut DrawingToolState, limit: f64, document: &
pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
drawing.paint_order = Some(order); drawing.paint_order = Some(order);
graph_modification_utils::set_parameter_for_selected_layers(document, graphene_std::vector::stroke::PaintOrderInput, TaggedValue::PaintOrder(order), responses); // The paint order is the chain order of the Fill and Stroke nodes, so applying it reorders the pair
for layer in document.network_interface.selected_nodes().selected_layers_except_artboards(&document.network_interface) {
responses.add(GraphOperationMessage::StrokePaintOrderSet { layer, paint_order: order });
}
} }
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
+2 -4
View File
@@ -712,8 +712,7 @@ impl NodeGraphExecutor {
text_frames, text_frames,
clip_targets, clip_targets,
vector_data, vector_data,
fill_attributes, appearance_attributes,
stroke_attributes,
backgrounds: _, backgrounds: _,
} = render_output.metadata; } = render_output.metadata;
@@ -728,8 +727,7 @@ impl NodeGraphExecutor {
responses.add(DocumentMessage::UpdateTextFrames { text_frames }); responses.add(DocumentMessage::UpdateTextFrames { text_frames });
responses.add(DocumentMessage::UpdateClipTargets { clip_targets }); responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
responses.add(DocumentMessage::UpdateVectorData { vector_data }); responses.add(DocumentMessage::UpdateVectorData { vector_data });
responses.add(DocumentMessage::UpdateFillAttributes { fill_attributes }); responses.add(DocumentMessage::UpdateAppearanceAttributes { appearance_attributes });
responses.add(DocumentMessage::UpdateStrokeAttributes { stroke_attributes });
responses.add(DocumentMessage::RenderScrollbars); responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers); responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
@@ -0,0 +1,417 @@
//! The appearance model: an ordered list of paint passes ("coverages") stored in the `ATTR_APPEARANCE` attribute.
//! Data uniform across all covers (the paint) rides the outer `List<Coverage>` so columnar presence holds,
//! while cover-specific data rides the inner `Item<Cover>`, reusing `ATTR_TRANSFORM` for the stroke-authoring space.
//!
//! The interior is `'static`: the paint column stores `Graphic<'static>`.
use crate::Graphic;
use crate::markers::ATTR_PAINT;
use core_types::ATTR_TRANSFORM;
use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List};
use vector_types::markers::{ATTR_ALIGN, ATTR_CAP, ATTR_DASH_OFFSET, ATTR_DASH_PATTERN, ATTR_JOIN, ATTR_JOIN_MITER_LIMIT, ATTR_WEIGHT};
use vector_types::vector::style::Stroke;
/// The geometry-to-region operator a coverage applies before painting:
/// the interior of the geometry (fill) or the region swept along its outline (stroke).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, CacheHash, dyn_any::DynAny)]
pub enum Cover {
#[default]
Fill,
Stroke,
}
impl std::fmt::Display for Cover {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Fill => write!(f, "Fill"),
Self::Stroke => write!(f, "Stroke"),
}
}
}
/// One paint pass of an [`Appearance`]: a [`Cover`] plus its cover-specific parameters, carried as
/// attributes on the inner item. Attributes for stroke parameters are ignored on fill coverages.
#[derive(Clone, Debug, Default, dyn_any::DynAny)]
pub struct Coverage(pub Item<Cover>);
// Item equality ignores attributes, but the stroke parameters live there.
impl PartialEq for Coverage {
fn eq(&self, other: &Self) -> bool {
self.0.element() == other.0.element()
&& self
.0
.attributes()
.iter()
.map(|(key, value)| (key, value.display_string()))
.eq(other.0.attributes().iter().map(|(key, value)| (key, value.display_string())))
}
}
impl CacheHash for Coverage {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.element().cache_hash(state);
for (key, value) in self.0.attributes().iter() {
std::hash::Hash::hash(key, state);
std::hash::Hash::hash(&value.display_string(), state);
}
}
}
/// An item's ordered list of paint passes, stored in the `ATTR_APPEARANCE` attribute cell.
/// Earlier coverages paint first, compositing below later ones. Each row's paint is the
/// `ATTR_PAINT` attribute beside it.
///
/// The empty appearance is its elided attribute default form, and the state in which it is
/// replaced by an inherited appearance from an outer level of the cascade.
#[derive(Clone, Debug, Default, PartialEq, CacheHash, dyn_any::DynAny)]
pub struct Appearance(pub List<Coverage>);
/// Where a newly inserted coverage lands in the paint order when no same-cover coverage exists to replace.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoverPlacement {
/// The front of the list, painting first (below every existing pass).
Below,
/// The back of the list, painting last (above every existing pass).
Above,
}
impl Coverage {
/// Creates a fill coverage with no parameters beyond its cover.
pub fn new_fill() -> Self {
Self(Item::new_from_element(Cover::Fill))
}
/// Creates a stroke coverage, stamping only the parameters that differ from their implicit defaults.
pub fn new_stroke(stroke: &Stroke) -> Self {
let defaults = Stroke::default();
let mut item = Item::new_from_element(Cover::Stroke);
if stroke.weight != defaults.weight {
item.set_attribute(ATTR_WEIGHT, stroke.weight);
}
if !stroke.dash_lengths.is_empty() {
item.set_attribute(ATTR_DASH_PATTERN, stroke.dash_lengths.clone());
}
if stroke.dash_offset != defaults.dash_offset {
item.set_attribute(ATTR_DASH_OFFSET, stroke.dash_offset);
}
if stroke.cap != defaults.cap {
item.set_attribute(ATTR_CAP, stroke.cap);
}
if stroke.join != defaults.join {
item.set_attribute(ATTR_JOIN, stroke.join);
}
if stroke.join_miter_limit != defaults.join_miter_limit {
item.set_attribute(ATTR_JOIN_MITER_LIMIT, stroke.join_miter_limit);
}
if stroke.align != defaults.align {
item.set_attribute(ATTR_ALIGN, stroke.align);
}
if stroke.transform != defaults.transform {
item.set_attribute(ATTR_TRANSFORM, stroke.transform);
}
Self(item)
}
/// This coverage's cover.
pub fn cover(&self) -> Cover {
*self.0.element()
}
/// Extracts the stroke parameters into a [`Stroke`], falling back to the default for any absent attribute.
/// Dash lengths are clamped to non-negative, matching what rendering accepts.
pub fn stroke_params(&self) -> Stroke {
let mut stroke = Stroke::default();
for (key, value) in self.0.attributes().iter() {
match key {
ATTR_WEIGHT => stroke.weight = value.as_any().downcast_ref().copied().unwrap_or(stroke.weight),
ATTR_DASH_PATTERN => {
stroke.dash_lengths = value
.as_any()
.downcast_ref::<Vec<f64>>()
.map(|lengths| lengths.iter().map(|length| length.max(0.)).collect())
.unwrap_or(stroke.dash_lengths)
}
ATTR_DASH_OFFSET => stroke.dash_offset = value.as_any().downcast_ref().copied().unwrap_or(stroke.dash_offset),
ATTR_CAP => stroke.cap = value.as_any().downcast_ref().copied().unwrap_or(stroke.cap),
ATTR_JOIN => stroke.join = value.as_any().downcast_ref().copied().unwrap_or(stroke.join),
ATTR_JOIN_MITER_LIMIT => stroke.join_miter_limit = value.as_any().downcast_ref().copied().unwrap_or(stroke.join_miter_limit),
ATTR_ALIGN => stroke.align = value.as_any().downcast_ref().copied().unwrap_or(stroke.align),
ATTR_TRANSFORM => stroke.transform = value.as_any().downcast_ref().copied().unwrap_or(stroke.transform),
_ => {}
}
}
stroke
}
}
/// Builds an appearance row, eliding the paint attribute when it is the default none-paint.
fn cover_row(coverage: Coverage, paint: Graphic<'static>) -> Item<Coverage> {
let mut row = Item::new_from_element(coverage);
if paint != Graphic::default() {
row.set_attribute(ATTR_PAINT, paint);
}
row
}
impl Appearance {
/// Creates an appearance holding a single coverage with the given paint.
pub fn new_single(coverage: Coverage, paint: Graphic<'static>) -> Self {
Self(List::new_from_item(cover_row(coverage, paint)))
}
/// The number of coverages in this appearance.
pub fn len(&self) -> usize {
self.0.len()
}
/// Whether this appearance holds no coverages, the undeclared state that defers to the cascade.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// This appearance if it declares any coverages, or `None` for the undeclared (empty) state.
pub fn declared(&self) -> Option<&Self> {
(!self.is_empty()).then_some(self)
}
/// Resolves the cascade for one item: its own declared appearance wins, while an undeclared
/// (absent or empty) cell inherits the nearest ancestor's declared appearance.
pub fn cascade<'a>(own: Option<&'a Self>, inherited: Option<&'a Self>) -> Option<&'a Self> {
own.and_then(Self::declared).or(inherited)
}
/// Iterates the coverages in paint order.
pub fn covers(&self) -> impl Iterator<Item = &Coverage> {
self.0.iter_element_values()
}
/// The coverage at the given index in paint order.
pub fn cover_at(&self, index: usize) -> Option<&Coverage> {
self.0.element(index)
}
/// The paint of the coverage at the given index, or `None` if the paint attribute is absent.
pub fn paint_at(&self, index: usize) -> Option<&Graphic<'static>> {
self.0.attribute::<Graphic<'static>>(ATTR_PAINT, index)
}
/// The index of the first coverage of the given cover in paint order.
pub fn first_index_of(&self, cover: Cover) -> Option<usize> {
self.covers().position(|coverage| coverage.cover() == cover)
}
/// The first coverage of the given cover in paint order.
pub fn first_coverage_of(&self, cover: Cover) -> Option<&Coverage> {
self.first_index_of(cover).and_then(|index| self.cover_at(index))
}
/// The paint of the first coverage of the given cover, filtered to paint that draws something.
pub fn first_paint_of(&self, cover: Cover) -> Option<&Graphic<'static>> {
self.first_index_of(cover).and_then(|index| self.paint_at(index)).filter(|paint| !paint.is_empty())
}
/// Iterates the coverages in paint order together with their paint, which is `None` when absent or drawing nothing.
pub fn covers_with_paints(&self) -> impl Iterator<Item = (&Coverage, Option<&Graphic<'static>>)> {
self.covers().enumerate().map(|(index, coverage)| (coverage, self.paint_at(index).filter(|paint| !paint.is_empty())))
}
/// Gathers the renderer's per-item reads in one walk of the coverage list.
pub fn fill_and_stroke(&self) -> FillAndStroke<'_> {
let mut first_fill = None;
let mut first_stroke = None;
for (index, coverage) in self.covers().enumerate() {
match coverage.cover() {
Cover::Fill if first_fill.is_none() => first_fill = Some(index),
Cover::Stroke if first_stroke.is_none() => first_stroke = Some((index, coverage)),
_ => {}
}
}
let painted = |index| self.paint_at(index).filter(|paint| !paint.is_empty());
FillAndStroke {
stroke: first_stroke.map(|(_, coverage)| coverage.stroke_params()),
fill_paint: first_fill.and_then(painted),
stroke_paint: first_stroke.and_then(|(index, _)| painted(index)),
stroke_below: first_stroke.zip(first_fill).is_some_and(|((stroke_index, _), fill_index)| stroke_index < fill_index),
}
}
/// Whether any coverage of the given cover exists, regardless of whether its paint draws anything.
pub fn has_cover(&self, cover: Cover) -> bool {
self.first_index_of(cover).is_some()
}
/// Whether any coverage of the given cover has paint that draws something, i.e. paint that is
/// present and not empty. A coverage whose paint is the none-paint exists but paints nothing.
pub fn has_painted_cover(&self, cover: Cover) -> bool {
self.covers()
.enumerate()
.any(|(index, coverage)| coverage.cover() == cover && self.paint_at(index).is_some_and(|paint| !paint.is_empty()))
}
/// Replaces the first coverage of the incoming cover in place (keeping its position in the paint order),
/// or inserts a new row at the requested end of the paint order if none exists.
pub fn replace_or_insert(&mut self, coverage: Coverage, paint: Graphic<'static>, placement: CoverPlacement) {
if let Some(index) = self.first_index_of(coverage.cover()) {
if let Some(element) = self.0.element_mut(index) {
*element = coverage;
}
self.0.set_attribute(ATTR_PAINT, index, paint);
return;
}
let row = cover_row(coverage, paint);
match placement {
CoverPlacement::Above => self.0.push(row),
CoverPlacement::Below => {
self.0 = std::iter::once(row).chain(std::mem::take(&mut self.0)).collect();
}
}
}
/// Sets the paint of the first coverage of the given cover, leaving its other parameters untouched.
/// Returns `false` without changing anything if no coverage of that cover exists.
pub fn set_paint_of(&mut self, cover: Cover, paint: Graphic<'static>) -> bool {
let Some(index) = self.first_index_of(cover) else { return false };
self.0.set_attribute(ATTR_PAINT, index, paint);
true
}
/// Discards every coverage that is not of the given cover, preserving the survivors' paint order.
pub fn retain_cover(&mut self, cover: Cover) {
self.0 = std::mem::take(&mut self.0).into_iter().filter(|row| row.element().cover() == cover).collect();
}
}
/// The first fill and stroke of an appearance in the form rendering consumes: the stroke's parameters,
/// each cover's first paint (filtered to paint that draws something), and their relative paint order.
#[derive(Debug, Default)]
pub struct FillAndStroke<'a> {
pub stroke: Option<Stroke>,
pub fill_paint: Option<&'a Graphic<'static>>,
pub stroke_paint: Option<&'a Graphic<'static>>,
/// Whether the first stroke coverage sits before the first fill in the paint order, painting below it.
pub stroke_below: bool,
}
/// Stamps a coverage into the item's `ATTR_APPEARANCE` cell, creating the attribute if absent.
/// The coverage replaces the first same-cover one in place, or lands at the placement end of the paint order.
pub fn stamp_coverage<T>(item: &mut Item<T>, coverage: Coverage, paint: Graphic<'static>, placement: CoverPlacement) {
item.attribute_mut_or_insert_default::<Appearance>(crate::markers::ATTR_APPEARANCE)
.replace_or_insert(coverage, paint, placement);
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::Color;
use glam::{DAffine2, DVec2};
use vector_types::vector::style::{StrokeAlign, StrokeCap, StrokeJoin};
fn solid_paint(color: Color) -> Graphic<'static> {
Graphic::Color(color)
}
fn paint_color(appearance: &Appearance, index: usize) -> Option<Color> {
let paint = appearance.paint_at(index)?;
let Graphic::Color(color) = paint else { return None };
Some(*color)
}
#[test]
fn stroke_params_survive_the_attribute_round_trip() {
let stroke = Stroke {
weight: 3.,
dash_lengths: vec![4., -2.],
dash_offset: 1.5,
cap: StrokeCap::Round,
join: StrokeJoin::Bevel,
join_miter_limit: 7.,
align: StrokeAlign::Inside,
transform: DAffine2::from_scale(DVec2::new(2., 3.)),
};
let coverage = Coverage::new_stroke(&stroke);
assert_eq!(coverage.cover(), Cover::Stroke);
let extracted = coverage.stroke_params();
assert_eq!(extracted.weight, 3.);
assert_eq!(extracted.dash_lengths, vec![4., 0.], "negative dash lengths should clamp to zero on extraction");
assert_eq!(extracted.dash_offset, 1.5);
assert_eq!(extracted.cap, StrokeCap::Round);
assert_eq!(extracted.join, StrokeJoin::Bevel);
assert_eq!(extracted.join_miter_limit, 7.);
assert_eq!(extracted.align, StrokeAlign::Inside);
assert_eq!(extracted.transform, DAffine2::from_scale(DVec2::new(2., 3.)));
}
#[test]
fn empty_appearance_is_undeclared_and_defers_to_the_cascade() {
let inherited = Appearance::new_single(Coverage::new_fill(), solid_paint(Color::BLACK));
let empty = Appearance::default();
assert!(empty.declared().is_none(), "an empty appearance should be undeclared");
assert!(inherited.declared().is_some(), "an appearance with a coverage should be declared");
assert_eq!(Appearance::cascade(Some(&empty), Some(&inherited)), Some(&inherited));
assert_eq!(Appearance::cascade(None, Some(&inherited)), Some(&inherited));
assert_eq!(Appearance::cascade(Some(&inherited), None), Some(&inherited));
assert_eq!(Appearance::cascade(Some(&empty), None), None);
assert_eq!(Appearance::cascade(None, None), None);
}
#[test]
fn default_valued_stroke_parameters_elide_to_absence() {
let coverage = Coverage::new_stroke(&Stroke::default());
assert_eq!(coverage.0.attributes().keys().count(), 0, "default parameters should stay absent");
assert_eq!(coverage.stroke_params(), Stroke::default(), "absent attributes should read back as the defaults");
let coverage = Coverage::new_stroke(&Stroke::new(2.));
let keys: Vec<_> = coverage.0.attributes().keys().collect();
assert_eq!(keys, vec![ATTR_WEIGHT], "only the non-default weight should be stamped");
assert_eq!(coverage.stroke_params().weight, 2.);
}
#[test]
fn replace_keeps_position_and_the_other_rows_paint() {
let mut appearance = Appearance::default();
appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::RED), CoverPlacement::Above);
appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(2.)), solid_paint(Color::BLACK), CoverPlacement::Above);
appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::BLUE), CoverPlacement::Above);
assert_eq!(appearance.len(), 2, "replacement should not add a row");
assert_eq!(appearance.cover_at(0).map(Coverage::cover), Some(Cover::Fill), "the fill should keep its position");
assert_eq!(paint_color(&appearance, 0), Some(Color::BLUE));
assert_eq!(paint_color(&appearance, 1), Some(Color::BLACK), "the stroke row's paint should be untouched");
}
#[test]
fn below_insertion_prepends_and_preserves_paint_columns() {
let mut appearance = Appearance::default();
appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(2.)), solid_paint(Color::BLACK), CoverPlacement::Above);
appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::RED), CoverPlacement::Below);
let covers: Vec<_> = appearance.covers().map(Coverage::cover).collect();
assert_eq!(covers, vec![Cover::Fill, Cover::Stroke], "a below-placed fill should paint before the stroke");
assert_eq!(paint_color(&appearance, 0), Some(Color::RED));
assert_eq!(paint_color(&appearance, 1), Some(Color::BLACK), "the existing row's paint should survive the reorder");
}
#[test]
fn painted_cover_distinguishes_none_paint_from_absence() {
let mut appearance = Appearance::default();
appearance.replace_or_insert(Coverage::new_fill(), Graphic::default(), CoverPlacement::Above);
assert!(appearance.has_cover(Cover::Fill), "a none-painted coverage still exists");
assert!(!appearance.has_painted_cover(Cover::Fill), "a none-painted coverage draws nothing");
assert!(!appearance.has_cover(Cover::Stroke));
assert!(!appearance.has_painted_cover(Cover::Stroke));
appearance.replace_or_insert(Coverage::new_fill(), solid_paint(Color::RED), CoverPlacement::Above);
assert!(appearance.has_painted_cover(Cover::Fill));
}
}
@@ -1,6 +1,7 @@
//! The record-crossing glue: group interiors carried between the owned, resident, and persistent regions. //! The record-crossing glue: group interiors carried between the owned, resident, and persistent regions.
use super::Graphic; use super::Graphic;
use crate::appearance::Appearance;
use core_types::Color; use core_types::Color;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
@@ -110,12 +111,18 @@ fn attribute_keys(list: &List<Graphic>) -> Vec<String> {
pub(crate) fn map_attribute_groups_to_owned(list: &mut List<Graphic<'_>>) { pub(crate) fn map_attribute_groups_to_owned(list: &mut List<Graphic<'_>>) {
for key in attribute_keys(list) { for key in attribute_keys(list) {
// A column of a type that cannot hold groups is skipped whole. // A column of a type that cannot hold groups is skipped whole.
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) else { continue }; if let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) {
for value in values.flatten() { for value in values.flatten() {
for element in value.iter_element_values_mut() { for element in value.iter_element_values_mut() {
*element = map_groups_to_owned(element); *element = map_groups_to_owned(element);
}
map_attribute_groups_to_owned(value);
} }
map_attribute_groups_to_owned(value); continue;
}
let Some(appearances) = list.iter_attribute_values_mut::<Appearance>(&key) else { continue };
for appearance in appearances {
map_appearance_groups_to_owned(appearance);
} }
} }
} }
@@ -125,15 +132,21 @@ pub(crate) fn map_attribute_groups_to_owned(list: &mut List<Graphic<'_>>) {
pub(crate) fn map_attribute_groups_to_resident(list: &mut List<Graphic<'_>>, arena: &core_types::arena::Arena) -> Option<()> { pub(crate) fn map_attribute_groups_to_resident(list: &mut List<Graphic<'_>>, arena: &core_types::arena::Arena) -> Option<()> {
for key in attribute_keys(list) { for key in attribute_keys(list) {
// A column of a type that cannot hold groups is skipped whole. // A column of a type that cannot hold groups is skipped whole.
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) else { continue }; if let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) {
for value in values.flatten() { for value in values.flatten() {
for element in value.iter_element_values_mut() { for element in value.iter_element_values_mut() {
let resident = map_groups_to_resident(element, arena)?; let resident = map_groups_to_resident(element, arena)?;
// SAFETY: the attribute store is erased, and the replay serves as // SAFETY: the attribute store is erased, and the replay serves as
// long as `arena`, which the store's reader outlives. // long as `arena`, which the store's reader outlives.
*element = unsafe { core_types::record::erase_static(resident) }; *element = unsafe { core_types::record::erase_static(resident) };
}
map_attribute_groups_to_resident(value, arena)?;
} }
map_attribute_groups_to_resident(value, arena)?; continue;
}
let Some(appearances) = list.iter_attribute_values_mut::<Appearance>(&key) else { continue };
for appearance in appearances {
map_appearance_groups_to_resident(appearance, arena)?;
} }
} }
Some(()) Some(())
@@ -145,15 +158,21 @@ pub(crate) fn map_attribute_groups_to_resident(list: &mut List<Graphic<'_>>, are
pub(crate) fn map_attribute_groups_to_persistent(list: &mut List<Graphic<'_>>, promotion: &core_types::record::Promotion<'_>) -> Option<()> { pub(crate) fn map_attribute_groups_to_persistent(list: &mut List<Graphic<'_>>, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
for key in attribute_keys(list) { for key in attribute_keys(list) {
// A column of a type that cannot hold groups is skipped whole. // A column of a type that cannot hold groups is skipped whole.
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) else { continue }; if let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(&key) {
for value in values.flatten() { for value in values.flatten() {
for element in value.iter_element_values_mut() { for element in value.iter_element_values_mut() {
let promoted = map_groups_to_persistent(element, promotion)?; let promoted = map_groups_to_persistent(element, promotion)?;
// SAFETY: the attribute store is erased, and persistent content // SAFETY: the attribute store is erased, and persistent content
// outlives the evaluation. // outlives the evaluation.
*element = unsafe { core_types::record::erase_static(promoted) }; *element = unsafe { core_types::record::erase_static(promoted) };
}
map_attribute_groups_to_persistent(value, promotion)?;
} }
map_attribute_groups_to_persistent(value, promotion)?; continue;
}
let Some(appearances) = list.iter_attribute_values_mut::<Appearance>(&key) else { continue };
for appearance in appearances {
map_appearance_groups_to_persistent(appearance, promotion)?;
} }
} }
Some(()) Some(())
@@ -232,13 +251,14 @@ pub(crate) fn list_contains_groups(list: &List<Graphic>) -> bool {
/// Whether any group hides in the list's item attribute values. Only the /// Whether any group hides in the list's item attribute values. Only the
/// group-capable columns are scanned: those are the value types the deep field /// group-capable columns are scanned: those are the value types the deep field
/// glue is registered for, today `Option<List<Graphic>>` alone. A list with no /// glue is registered for, `Option<List<Graphic>>` and `Appearance`. A list
/// attribute columns costs nothing, and a column of any other type is decided /// with no attribute columns costs nothing, and a column of any other type is
/// by its one downcast rather than per value. /// decided by its one downcast rather than per value.
fn attribute_values_contain_groups(list: &List<Graphic>) -> bool { fn attribute_values_contain_groups(list: &List<Graphic>) -> bool {
list.attribute_keys().any(|key| { list.attribute_keys().any(|key| {
list.iter_attribute_values::<Option<List<Graphic>>>(key) list.iter_attribute_values::<Option<List<Graphic>>>(key)
.is_some_and(|mut values| values.any(|value| value.as_ref().is_some_and(list_contains_groups))) .is_some_and(|mut values| values.any(|value| value.as_ref().is_some_and(list_contains_groups)))
|| list.iter_attribute_values::<Appearance>(key).is_some_and(|mut values| values.any(appearance_contains_groups))
}) })
} }
@@ -332,12 +352,143 @@ unsafe fn promote_graphic_list(src: *const u8, dst: *mut u8, promotion: &core_ty
Some(()) Some(())
} }
/// The coverage list's attribute keys, owned so the columns can be walked mutably.
fn appearance_attribute_keys(appearance: &Appearance) -> Vec<String> {
appearance.0.attribute_keys().map(str::to_string).collect()
}
/// Whether any group hides in an appearance. Groups can only arrive through the
/// `Graphic`-typed columns of the coverage list, today the paint column alone;
/// a coverage item's own attributes hold plain stroke parameters.
fn appearance_contains_groups(appearance: &Appearance) -> bool {
appearance
.0
.attribute_keys()
.any(|key| appearance.0.iter_attribute_values::<Graphic>(key).is_some_and(|mut values| values.any(graphic_contains_groups)))
}
/// The heap an appearance's paints own, group interiors excluded as
/// [`graphic_retained_heap`] excludes them.
fn appearance_retained_heap(appearance: &Appearance) -> usize {
appearance
.0
.attribute_keys()
.map(|key| appearance.0.iter_attribute_values::<Graphic>(key).map_or(0, |values| values.map(graphic_retained_heap).sum()))
.sum()
}
/// Every group held in an appearance's paint columns, deep-copied to its owned form.
fn map_appearance_groups_to_owned(appearance: &mut Appearance) {
for key in appearance_attribute_keys(appearance) {
// A column of a type that cannot hold groups is skipped whole.
let Some(values) = appearance.0.iter_attribute_values_mut::<Graphic>(&key) else { continue };
for value in values {
*value = map_groups_to_owned(value);
}
}
}
/// Every owned group held in an appearance's paint columns, re-parked into
/// `arena`. `None` reports arena exhaustion.
fn map_appearance_groups_to_resident(appearance: &mut Appearance, arena: &core_types::arena::Arena) -> Option<()> {
for key in appearance_attribute_keys(appearance) {
// A column of a type that cannot hold groups is skipped whole.
let Some(values) = appearance.0.iter_attribute_values_mut::<Graphic>(&key) else { continue };
for value in values {
let resident = map_groups_to_resident(value, arena)?;
// SAFETY: the attribute store is erased, and the replay serves as
// long as `arena`, which the store's reader outlives.
*value = unsafe { core_types::record::erase_static(resident) };
}
}
Some(())
}
/// The deep copy-out for appearance field values (the appearance marker's bare
/// owned form): content groups leave any paint column in their owned form.
/// Declines (`None`) for group-free content, which already owns everything.
fn deep_clone_appearance(value: &dyn core_types::list::AnyAttributeValue) -> Option<Box<dyn core_types::list::AnyAttributeValue>> {
let appearance = value.as_any().downcast_ref::<Appearance>().expect("an appearance field deep-copies at its own type");
appearance_contains_groups(appearance).then_some(())?;
let mut appearance = appearance.clone();
map_appearance_groups_to_owned(&mut appearance);
Some(Box::new(appearance))
}
/// The deep replay for appearance field values: owned content groups replay
/// into the serving arena before the field re-parks. `Some(None)` declines for
/// group-free content; `None` reports arena exhaustion.
fn deep_repark_appearance(value: &dyn core_types::list::AnyAttributeValue, arena: &core_types::arena::Arena) -> Option<Option<Box<dyn core_types::list::AnyAttributeValue>>> {
let appearance = value.as_any().downcast_ref::<Appearance>().expect("an appearance field replays at its own type");
if !appearance_contains_groups(appearance) {
return Some(None);
}
let mut appearance = appearance.clone();
map_appearance_groups_to_resident(&mut appearance, arena)?;
Some(Some(Box::new(appearance)))
}
/// Every group held in an appearance's paint columns, promoted into the
/// persistent region on the same Cow dispatch the elements take. `None`
/// reports arena exhaustion.
fn map_appearance_groups_to_persistent(appearance: &mut Appearance, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
for key in appearance_attribute_keys(appearance) {
// A column of a type that cannot hold groups is skipped whole.
let Some(values) = appearance.0.iter_attribute_values_mut::<Graphic>(&key) else { continue };
for value in values {
let promoted = map_groups_to_persistent(value, promotion)?;
// SAFETY: the attribute store is erased, and persistent content
// outlives the evaluation.
*value = unsafe { core_types::record::erase_static(promoted) };
}
}
Some(())
}
/// The promote for appearance fields, retargeting the paint list promote onto
/// the coverage container: the header moves where no group is reachable from
/// any paint column, and otherwise clones into a fresh persistent park, with
/// content groups taking [`map_groups_to_persistent`]'s Cow dispatch one level
/// inside, exactly as [`promote_graphic_list`] dispatches for the paint lists.
///
/// # Safety
/// `src` must point at a live parked appearance field, and `dst` at the field
/// slot the promoted reference is written to.
unsafe fn promote_appearance(src: *const u8, dst: *mut u8, promotion: &core_types::record::Promotion<'_>) -> Option<()> {
// SAFETY: the caller's contract; the slot holds one optional reference.
let Some(appearance) = (unsafe { src.cast::<Option<&Appearance>>().read() }) else {
// SAFETY: as above, into the promoted image's own field slot.
unsafe { dst.cast::<Option<&Appearance>>().write(None) };
return Some(());
};
let retained = appearance_retained_heap(appearance);
if !appearance_contains_groups(appearance) {
// SAFETY: an appearance no group is reachable from owns all of its
// content, and the arena declines a reference that is not a park at the
// appearance's own address and size.
if let Some(moved) = unsafe { promotion.move_park::<Appearance>(std::ptr::from_ref(appearance).cast(), retained) } {
// SAFETY: the move published a live appearance in the persistent region.
unsafe { dst.cast::<Option<&Appearance>>().write(Some(&*moved)) };
return Some(());
}
}
let mut promoted = appearance.clone();
map_appearance_groups_to_persistent(&mut promoted, promotion)?;
let (parked, _) = promotion.persistent().alloc_sized(promoted, retained)?;
// SAFETY: the slot holds one optional reference.
unsafe { dst.cast::<Option<&Appearance>>().write(Some(parked)) };
Some(())
}
const _: () = { const _: () = {
fn register_all() { fn register_all() {
core_types::record::register_deep_element_clone::<Graphic>(deep_clone_graphic, deep_repark_graphic); core_types::record::register_deep_element_clone::<Graphic>(deep_clone_graphic, deep_repark_graphic);
core_types::record::register_deep_field_value::<Option<List<Graphic>>>(deep_clone_graphic_list, deep_repark_graphic_list); core_types::record::register_deep_field_value::<Option<List<Graphic>>>(deep_clone_graphic_list, deep_repark_graphic_list);
core_types::record::register_field_promote::<Option<&'static List<Graphic<'static>>>>(promote_graphic_list); core_types::record::register_field_promote::<Option<&'static List<Graphic<'static>>>>(promote_graphic_list);
core_types::record::register_element_promote::<Graphic>(promote_graphic); core_types::record::register_element_promote::<Graphic>(promote_graphic);
core_types::record::register_deep_field_value::<Appearance>(deep_clone_appearance, deep_repark_appearance);
core_types::record::register_field_promote::<Option<&'static Appearance>>(promote_appearance);
core_types::record::register_retained_heap::<Appearance>(|value| value.downcast_ref::<Appearance>().map_or(0, appearance_retained_heap));
core_types::record::register_retained_heap::<Graphic>(|value| value.downcast_ref::<Graphic>().map_or(0, graphic_retained_heap)); core_types::record::register_retained_heap::<Graphic>(|value| value.downcast_ref::<Graphic>().map_or(0, graphic_retained_heap));
core_types::record::register_retained_heap::<Vector>(|value| value.downcast_ref::<Vector>().map_or(0, vector_retained_heap)); core_types::record::register_retained_heap::<Vector>(|value| value.downcast_ref::<Vector>().map_or(0, vector_retained_heap));
core_types::record::register_retained_heap::<Raster<CPU>>(|value| value.downcast_ref::<Raster<CPU>>().map_or(0, |raster| raster.data.len() * size_of::<Color>())); core_types::record::register_retained_heap::<Raster<CPU>>(|value| value.downcast_ref::<Raster<CPU>>().map_or(0, |raster| raster.data.len() * size_of::<Color>()));
@@ -362,7 +513,7 @@ mod run_tests {
use super::*; use super::*;
use crate::graphic::test_support::{native_group_paint, unit_square_at}; use crate::graphic::test_support::{native_group_paint, unit_square_at};
use crate::graphic::{group_to_legacy_list, map_groups_to_legacy}; use crate::graphic::{group_to_legacy_list, map_groups_to_legacy};
use crate::markers::{Fill, Stroke}; use crate::markers::EditorMergedLayers;
use core_types::attribute::Attribute; use core_types::attribute::Attribute;
use core_types::lane::LaneSource; use core_types::lane::LaneSource;
use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed}; use core_types::record::{FieldWrite, RunBuilder, RunView, element_write_hashed};
@@ -374,9 +525,9 @@ mod run_tests {
let vector = unit_square_at(DVec2::ZERO); let vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap(); let source = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<EditorMergedLayers>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap(); let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<EditorMergedLayers>(lane, Some(&paint));
let group = core_types::record::Group { row: None, content: builder.finish() }; let group = core_types::record::Group { row: None, content: builder.finish() };
let expected = group_to_legacy_list(&group); let expected = group_to_legacy_list(&group);
let owned = map_groups_to_owned(&Graphic::Group(group)); let owned = map_groups_to_owned(&Graphic::Group(group));
@@ -418,9 +569,9 @@ mod run_tests {
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) }; let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
let vector = unit_square_at(DVec2::new(4., 4.)); let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<EditorMergedLayers>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap(); let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<EditorMergedLayers>(lane, Some(&paint));
let item = builder.finish(); let item = builder.finish();
let owned = item.copy_out(); let owned = item.copy_out();
let expected = map_groups_to_legacy(paint.element(0).unwrap()); let expected = map_groups_to_legacy(paint.element(0).unwrap());
@@ -431,7 +582,7 @@ mod run_tests {
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let replayed = owned.replay(&arena).expect("the arena holds the replay"); let replayed = owned.replay(&arena).expect("the arena holds the replay");
let run = RunView::<Vector>::new(&replayed).expect("the run holds vector elements"); let run = RunView::<Vector>::new(&replayed).expect("the run holds vector elements");
let served = run.attr::<Fill>(0).expect("the fill replays present"); let served = run.attr::<EditorMergedLayers>(0).expect("the fill replays present");
assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected); assert_eq!(map_groups_to_legacy(served.element(0).unwrap()), expected);
} }
@@ -444,12 +595,12 @@ mod run_tests {
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) }; let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
let vector = unit_square_at(DVec2::new(4., 4.)); let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<EditorMergedLayers>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap(); let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<EditorMergedLayers>(lane, Some(&paint));
let item = builder.finish(); let item = builder.finish();
let layout = item.layout().clone(); let layout = item.layout().clone();
let offset = layout.offset_of(Fill::NAME, 0).unwrap(); let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap();
// SAFETY: the item's lane is a live record of `layout`. // SAFETY: the item's lane is a live record of `layout`.
let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) }; let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) };
let expected = map_groups_to_legacy(paint.element(0).unwrap()); let expected = map_groups_to_legacy(paint.element(0).unwrap());
@@ -479,8 +630,8 @@ mod run_tests {
) -> (core_types::record::Layout, core_types::record::MaterializedSpan, Vec<u64>) { ) -> (core_types::record::Layout, core_types::record::MaterializedSpan, Vec<u64>) {
use core_types::record::{Layout, MaterializedSpan, Promotion, element_write, write_field}; use core_types::record::{Layout, MaterializedSpan, Promotion, element_write, write_field};
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<Fill>(0)]); let layout = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<EditorMergedLayers>(0)]);
let offset = layout.offset_of(Fill::NAME, 0).unwrap(); let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap();
let stride = layout.lane_stride(); let stride = layout.lane_stride();
let mut buffer = vec![0u64; (lanes * stride).div_ceil(8)]; let mut buffer = vec![0u64; (lanes * stride).div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>(); let base = buffer.as_mut_ptr().cast::<u8>();
@@ -503,7 +654,7 @@ mod run_tests {
/// The promoted paint of one lane, at the layout the promote published. /// The promoted paint of one lane, at the layout the promote published.
fn promoted_paint<'p>(span: &core_types::record::MaterializedSpan, layout: &core_types::record::Layout, lane: usize, persistent: &'p core_types::arena::Arena) -> &'p List<Graphic<'p>> { fn promoted_paint<'p>(span: &core_types::record::MaterializedSpan, layout: &core_types::record::Layout, lane: usize, persistent: &'p core_types::arena::Arena) -> &'p List<Graphic<'p>> {
let offset = layout.offset_of(Fill::NAME, 0).unwrap(); let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap();
let batch = span.batch(persistent, layout).expect("the span resolves in its own region"); let batch = span.batch(persistent, layout).expect("the span resolves in its own region");
// SAFETY: the promote wrote a record of `layout` into every lane. // SAFETY: the promote wrote a record of `layout` into every lane.
unsafe { batch.get(lane).rec().read::<Option<&List<Graphic>>>(offset) }.expect("the paint promotes present") unsafe { batch.get(lane).rec().read::<Option<&List<Graphic>>>(offset) }.expect("the paint promotes present")
@@ -583,7 +734,7 @@ mod run_tests {
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(4., 4.)))); let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(4., 4.))));
// SAFETY: the erased native list serves only while `transient` is live; the // SAFETY: the erased native list serves only while `transient` is live; the
// promote under test replaces its borrows. // promote under test replaces its borrows.
paint.set_attribute::<Option<List<Graphic>>>(Stroke::NAME, 0, Some(unsafe { core_types::record::erase_static(native) })); paint.set_attribute::<Option<List<Graphic>>>("probe:paint", 0, Some(unsafe { core_types::record::erase_static(native) }));
let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap(); let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap();
let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent); let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent);
@@ -593,7 +744,7 @@ mod run_tests {
transient.reset(); transient.reset();
let served = promoted_paint(&span, &layout, 0, &persistent); let served = promoted_paint(&span, &layout, 0, &persistent);
let held = served.attribute::<Option<List<Graphic>>>(Stroke::NAME, 0).expect("the stroke attribute rides the promoted list"); let held = served.attribute::<Option<List<Graphic>>>("probe:paint", 0).expect("the stroke attribute rides the promoted list");
let held = held.as_ref().expect("the stroke is present"); let held = held.as_ref().expect("the stroke is present");
assert_eq!( assert_eq!(
map_groups_to_legacy(held.element(0).unwrap()), map_groups_to_legacy(held.element(0).unwrap()),
@@ -612,7 +763,7 @@ mod run_tests {
// whose value holds none: neither denies the move. // whose value holds none: neither denies the move.
paint.set_attribute::<f64>("opacity", 0, 0.5); paint.set_attribute::<f64>("opacity", 0, 0.5);
paint.set_attribute::<Color>("probe:color", 0, Color::BLACK); paint.set_attribute::<Color>("probe:color", 0, Color::BLACK);
paint.set_attribute::<Option<List<Graphic>>>(Stroke::NAME, 0, Some(List::new_from_element(Graphic::Color(Color::WHITE)))); paint.set_attribute::<Option<List<Graphic>>>("probe:paint", 0, Some(List::new_from_element(Graphic::Color(Color::WHITE))));
let heap = { let heap = {
let Some(Graphic::Vector(vector)) = paint.element(0) else { panic!("the paint carries a vector") }; let Some(Graphic::Vector(vector)) = paint.element(0) else { panic!("the paint carries a vector") };
vector.point_domain.positions().as_ptr() vector.point_domain.positions().as_ptr()
@@ -647,15 +798,15 @@ mod run_tests {
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.)))); let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.))));
// SAFETY: the erased native list serves only while `source` is live; the // SAFETY: the erased native list serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam. // deep glue under test replaces its borrows at the copy-out seam.
paint.set_attribute::<Option<List<Graphic>>>(Stroke::NAME, 0, Some(unsafe { core_types::record::erase_static(native) })); paint.set_attribute::<Option<List<Graphic>>>("probe:paint", 0, Some(unsafe { core_types::record::erase_static(native) }));
let vector = unit_square_at(DVec2::new(4., 4.)); let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<EditorMergedLayers>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap(); let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<EditorMergedLayers>(lane, Some(&paint));
let item = builder.finish(); let item = builder.finish();
let layout = item.layout().clone(); let layout = item.layout().clone();
let offset = layout.offset_of(Fill::NAME, 0).unwrap(); let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap();
// SAFETY: the item's lane is a live record of `layout`. // SAFETY: the item's lane is a live record of `layout`.
let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) }; let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) };
drop(item); drop(item);
@@ -670,8 +821,244 @@ mod run_tests {
let value = unsafe { slot.finish() }; let value = unsafe { slot.finish() };
// SAFETY: the replay wrote a record of `layout`. // SAFETY: the replay wrote a record of `layout`.
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present"); let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the fill replays present");
let held = served.attribute::<Option<List<Graphic>>>(Stroke::NAME, 0).expect("the stroke attribute rides the replayed list"); let held = served.attribute::<Option<List<Graphic>>>("probe:paint", 0).expect("the stroke attribute rides the replayed list");
let held = held.as_ref().expect("the stroke is present"); let held = held.as_ref().expect("the stroke is present");
assert_eq!(map_groups_to_legacy(held.element(0).unwrap()), expected, "the attribute-held group replayed into the serving arena"); assert_eq!(map_groups_to_legacy(held.element(0).unwrap()), expected, "the attribute-held group replayed into the serving arena");
} }
use crate::appearance::{Cover, CoverPlacement, Coverage};
use crate::markers::Appearance as AppearanceMarker;
/// A `lanes`-long frame promoted out of `transient` into `persistent`, with
/// `appearance` written into the appearance field of every lane. The frame
/// buffer comes back so it outlives the promote's reads.
fn promote_appearance_field(
appearance: Option<&Appearance>,
lanes: usize,
transient: &core_types::arena::Arena,
persistent: &core_types::arena::Arena,
) -> (core_types::record::Layout, core_types::record::MaterializedSpan, Vec<u64>) {
use core_types::record::{Layout, MaterializedSpan, Promotion, element_write, write_field};
let layout = Layout::default().with_writes(0, element_write::<f64>(), &[FieldWrite::of::<AppearanceMarker>(0)]);
let offset = layout.offset_of(AppearanceMarker::NAME, 0).unwrap();
let stride = layout.lane_stride();
let mut buffer = vec![0u64; (lanes * stride).div_ceil(8)];
let base = buffer.as_mut_ptr().cast::<u8>();
let bounds = (base as usize, buffer.len() * 8);
for lane in 0..lanes {
// SAFETY: the frame is this layout's, written at the element slot and
// at the appearance field's own offset.
unsafe {
base.add(lane * stride).cast::<f64>().write(lane as f64);
write_field::<Option<&Appearance>>(base.add(lane * stride), offset, appearance);
}
}
// SAFETY: the frames hold `lanes` live records of `layout`.
let batch = unsafe { core_types::node::RecordBatch::new(base.cast_const(), lanes, &layout) };
let promotion = Promotion::new(transient, bounds, persistent);
// SAFETY: as above.
let span = unsafe { MaterializedSpan::to_persistent(&batch, &promotion) }.expect("the region holds the promote");
(layout, span, buffer)
}
/// The promoted appearance of one lane, at the layout the promote published.
fn promoted_appearance<'p>(span: &core_types::record::MaterializedSpan, layout: &core_types::record::Layout, lane: usize, persistent: &'p core_types::arena::Arena) -> &'p Appearance {
let offset = layout.offset_of(AppearanceMarker::NAME, 0).unwrap();
let batch = span.batch(persistent, layout).expect("the span resolves in its own region");
// SAFETY: the promote wrote a record of `layout` into every lane.
unsafe { batch.get(lane).rec().read::<Option<&Appearance>>(offset) }.expect("the appearance promotes present")
}
#[test]
fn a_promoted_appearance_shares_persistent_interiors() {
let inner_vector = unit_square_at(DVec2::ZERO);
let transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
// The interior an upstream promote already published, named by a paint
// the evaluation parked in a coverage's paint column.
let published = native_group_paint(&inner_vector, &persistent);
let Some(Graphic::Group(group)) = published.element(0) else {
panic!("the paint carries a native group")
};
let interior = group.content.lanes().get(0).rec().ptr();
// SAFETY: the group serves only while `persistent` is live, and the
// promote under test replaces every borrow it carries.
let paint = unsafe { core_types::record::erase_static(Graphic::Group(group.clone())) };
let appearance = Appearance::new_single(Coverage::new_fill(), paint);
let (appearance, _) = transient.alloc_sized_keyed(appearance, 0).unwrap();
let (layout, span, _frames) = promote_appearance_field(Some(appearance), 1, &transient, &persistent);
let served = promoted_appearance(&span, &layout, 0, &persistent);
let Some(Graphic::Group(group)) = served.paint_at(0) else {
panic!("the promote keeps the group form")
};
assert_eq!(group.content.lanes().get(0).rec().ptr(), interior, "a persistent interior is shared pointer for pointer");
}
#[test]
fn a_group_free_appearance_moves_its_parked_header() {
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
let appearance = Appearance::new_single(Coverage::new_fill(), Graphic::Vector(unit_square_at(DVec2::ZERO)));
let heap = {
let Some(Graphic::Vector(vector)) = appearance.paint_at(0) else {
panic!("the paint carries a vector")
};
vector.point_domain.positions().as_ptr()
};
let (appearance, _) = transient.alloc_sized_keyed(appearance, 0).unwrap();
let (layout, span, _frames) = promote_appearance_field(Some(appearance), 2, &transient, &persistent);
let served = promoted_appearance(&span, &layout, 0, &persistent);
let Some(Graphic::Vector(vector)) = served.paint_at(0) else {
panic!("the promote keeps the vector")
};
assert_eq!(
vector.point_domain.positions().as_ptr(),
heap,
"the promote moved the header, so the served paint names the pre-promote heap"
);
assert!(std::ptr::eq(served, promoted_appearance(&span, &layout, 1, &persistent)), "an appearance two lanes share moves once");
transient.reset();
let served = promoted_appearance(&span, &layout, 0, &persistent);
assert!(matches!(served.paint_at(0), Some(Graphic::Vector(_))), "the moved appearance survives the transient reset");
}
#[test]
fn an_appearance_whose_paint_holds_groups_never_moves() {
let inner_vector = unit_square_at(DVec2::ZERO);
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
let native = native_group_paint(&inner_vector, &transient);
let Some(native_group) = native.element(0) else { panic!("the paint carries a group") };
let expected = map_groups_to_legacy(native_group);
// SAFETY: the erased native group serves only while `transient` is live;
// the promote under test replaces its borrows.
let paint = unsafe { core_types::record::erase_static(native_group.clone()) };
let appearance = Appearance::new_single(Coverage::new_fill(), paint);
let (appearance, _) = transient.alloc_sized_keyed(appearance, 0).unwrap();
let (layout, span, _frames) = promote_appearance_field(Some(appearance), 1, &transient, &persistent);
let served = promoted_appearance(&span, &layout, 0, &persistent);
let moved = std::ptr::eq(std::ptr::from_ref(served).cast::<u8>(), std::ptr::from_ref(appearance).cast::<u8>());
assert!(!moved, "a paint-held group denies the move, so the promote parks a header of its own");
transient.reset();
let served = promoted_appearance(&span, &layout, 0, &persistent);
let held = served.paint_at(0).expect("the paint rides the promoted appearance");
assert_eq!(map_groups_to_legacy(held), expected, "the paint-held group serves from persistent storage after the reset");
}
#[test]
fn a_group_free_appearance_moves_past_its_stroke_columns() {
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
// Stroke parameters on the coverage and a group-free paint: neither denies the move.
let mut appearance = Appearance::new_single(Coverage::new_stroke(&vector_types::vector::style::Stroke::new(2.)), Graphic::Vector(unit_square_at(DVec2::ZERO)));
appearance.replace_or_insert(Coverage::new_fill(), Graphic::Color(Color::WHITE), CoverPlacement::Below);
let heap = {
let Some(Graphic::Vector(vector)) = appearance.paint_at(1) else {
panic!("the stroke paint carries a vector")
};
vector.point_domain.positions().as_ptr()
};
let (appearance, _) = transient.alloc_sized_keyed(appearance, 0).unwrap();
let (layout, span, _frames) = promote_appearance_field(Some(appearance), 1, &transient, &persistent);
let served = promoted_appearance(&span, &layout, 0, &persistent);
let Some(Graphic::Vector(vector)) = served.paint_at(1) else {
panic!("the promote keeps the vector")
};
assert_eq!(
vector.point_domain.positions().as_ptr(),
heap,
"the promote moved the header, so the served paint names the pre-promote heap"
);
transient.reset();
let served = promoted_appearance(&span, &layout, 0, &persistent);
assert_eq!(
served.first_coverage_of(Cover::Stroke).map(|coverage| coverage.stroke_params().weight),
Some(2.),
"the stroke parameters survive the move"
);
}
#[test]
fn a_paint_field_whose_appearance_column_holds_groups_never_moves() {
let inner_vector = unit_square_at(DVec2::ZERO);
let mut transient = core_types::arena::Arena::new(1 << 16).unwrap();
let persistent = core_types::arena::Arena::new(1 << 16).unwrap();
// Group-free elements, with the resident group hidden in an item's appearance paint cell.
let native = native_group_paint(&inner_vector, &transient);
let Some(native_group) = native.element(0) else { panic!("the paint carries a group") };
let expected = map_groups_to_legacy(native_group);
// SAFETY: the erased native group serves only while `transient` is live;
// the promote under test replaces its borrows.
let paint_cell = unsafe { core_types::record::erase_static(native_group.clone()) };
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(4., 4.))));
paint.set_attribute(crate::markers::ATTR_APPEARANCE, 0, Appearance::new_single(Coverage::new_fill(), paint_cell));
let (paint, _) = transient.alloc_sized_keyed(paint, 0).unwrap();
let (layout, span, _frames) = promote_paint_field(Some(paint), 1, &transient, &persistent);
let served = promoted_paint(&span, &layout, 0, &persistent);
let moved = std::ptr::eq(std::ptr::from_ref(served).cast::<u8>(), std::ptr::from_ref(paint).cast::<u8>());
assert!(!moved, "an appearance-held group denies the move, so the promote parks a header of its own");
transient.reset();
let served = promoted_paint(&span, &layout, 0, &persistent);
let held = served.attribute::<Appearance>(crate::markers::ATTR_APPEARANCE, 0).expect("the appearance rides the promoted list");
let held = held.paint_at(0).expect("the fill coverage keeps its paint");
assert_eq!(map_groups_to_legacy(held), expected, "the appearance-held group serves from persistent storage after the reset");
}
#[test]
fn an_owned_record_deep_copies_appearance_column_groups() {
let inner_vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap();
let native = native_group_paint(&inner_vector, &source);
let Some(native_group) = native.element(0) else { panic!("the paint carries a group") };
let expected = map_groups_to_legacy(native_group);
// The field's elements are group-free; the group rides an item's appearance
// paint cell, which the shallow read alone would leave borrowing `source`.
// SAFETY: the erased native group serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam.
let paint_cell = unsafe { core_types::record::erase_static(native_group.clone()) };
let mut paint = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.))));
paint.set_attribute(crate::markers::ATTR_APPEARANCE, 0, Appearance::new_single(Coverage::new_fill(), paint_cell));
let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<EditorMergedLayers>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap();
builder.attr::<EditorMergedLayers>(lane, Some(&paint));
let item = builder.finish();
let layout = item.layout().clone();
let offset = layout.offset_of(EditorMergedLayers::NAME, 0).unwrap();
// SAFETY: the item's lane is a live record of `layout`.
let owned = unsafe { core_types::record::OwnedRecord::copy_out(&layout, item.lanes().get(0).rec()) };
drop(item);
drop(paint);
drop(source);
let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let frames = core_types::record::test_frames(layout.frame_bytes());
let mut slot = frames.claim(&layout);
owned.replay_into(&mut slot, &arena).expect("the arena holds the replay");
// SAFETY: the replay completes the record in the claimed frame.
let value = unsafe { slot.finish() };
// SAFETY: the replay wrote a record of `layout`.
let served = unsafe { layout.rec(&value).read::<Option<&List<Graphic>>>(offset) }.expect("the field replays present");
let held = served.attribute::<Appearance>(crate::markers::ATTR_APPEARANCE, 0).expect("the appearance rides the replayed list");
let held = held.paint_at(0).expect("the fill coverage keeps its paint");
assert_eq!(map_groups_to_legacy(held), expected, "the appearance-held group replayed into the serving arena");
}
} }
@@ -1,8 +1,8 @@
//! The legacy bridge: record-backed groups rebuilt as owned legacy lists. //! The legacy bridge: record-backed groups rebuilt as owned legacy lists.
use super::walk::push_lane_paint_into_interiors;
use super::{Graphic, detable_items}; use super::{Graphic, detable_items};
use crate::markers::{ATTR_FILL, ATTR_STROKE}; use crate::appearance::Appearance;
use crate::markers::{ATTR_APPEARANCE, ATTR_EDITOR_MERGED_LAYERS, ATTR_PAINT};
use core_types::Color; use core_types::Color;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
@@ -15,11 +15,19 @@ pub fn run_to_list<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(item: &cor
core_types::record::run_to_owned_list(item) core_types::record::run_to_owned_list(item)
} }
/// Converts the group content of the list's paint attribute values to legacy /// Converts the group content the list's attribute values reach to legacy
/// form, so a legacy product owns everything its attributes reach. /// form, so a legacy product owns everything its attributes hold: the paint
/// cells inside each appearance, and the merged-layers snapshot.
pub fn map_paint_attrs_to_legacy<T>(list: &mut List<T>) { pub fn map_paint_attrs_to_legacy<T>(list: &mut List<T>) {
for key in [ATTR_FILL, ATTR_STROKE, crate::markers::ATTR_EDITOR_MERGED_LAYERS] { if let Some(appearances) = list.iter_attribute_values_mut::<Appearance>(ATTR_APPEARANCE) {
let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(key) else { continue }; for appearance in appearances {
let Some(cells) = appearance.0.iter_attribute_values_mut::<Graphic>(ATTR_PAINT) else { continue };
for cell in cells {
*cell = map_groups_to_legacy(cell);
}
}
}
if let Some(values) = list.iter_attribute_values_mut::<Option<List<Graphic>>>(ATTR_EDITOR_MERGED_LAYERS) {
for value in values.flatten() { for value in values.flatten() {
for element in value.iter_element_values_mut() { for element in value.iter_element_values_mut() {
*element = map_groups_to_legacy(element); *element = map_groups_to_legacy(element);
@@ -87,7 +95,6 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List<Graphic<'
for element in list.iter_element_values_mut() { for element in list.iter_element_values_mut() {
*element = map_groups_to_legacy(element); *element = map_groups_to_legacy(element);
} }
push_lane_paint_into_interiors(&mut list);
return list; return list;
} }
None.or_else(|| run_to_legacy_list::<Vector>(item).map(|list| detable_items(list, Graphic::Vector))) None.or_else(|| run_to_legacy_list::<Vector>(item).map(|list| detable_items(list, Graphic::Vector)))
@@ -102,24 +109,25 @@ pub fn group_to_legacy_list(group: &core_types::record::Group) -> List<Graphic<'
#[cfg(test)] #[cfg(test)]
mod run_tests { mod run_tests {
use super::*; use super::*;
use crate::appearance::Coverage;
use crate::graphic::test_support::{native_group_paint, unit_square_at}; use crate::graphic::test_support::{native_group_paint, unit_square_at};
use crate::markers::Fill; use crate::markers::Appearance as AppearanceMarker;
use core_types::attribute::Attribute;
use core_types::record::{FieldWrite, RunBuilder, element_write_hashed}; use core_types::record::{FieldWrite, RunBuilder, element_write_hashed};
use glam::DVec2; use glam::DVec2;
#[test] #[test]
fn a_legacy_list_owns_its_paint_attr_content() { fn a_legacy_list_owns_its_appearance_paint_content() {
let inner_vector = unit_square_at(DVec2::ZERO); let inner_vector = unit_square_at(DVec2::ZERO);
let source = core_types::arena::Arena::new(1 << 16).unwrap(); let source = core_types::arena::Arena::new(1 << 16).unwrap();
// SAFETY: the erased native list serves only while `source` is live; the // SAFETY: the erased native list serves only while `source` is live; the
// deep glue under test replaces its borrows at the copy-out seam. // deep glue under test replaces its borrows at the copy-out seam.
let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) }; let paint = unsafe { core_types::record::erase_static(native_group_paint(&inner_vector, &source)) };
let appearance = Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint.clone()));
let vector = unit_square_at(DVec2::new(4., 4.)); let vector = unit_square_at(DVec2::new(4., 4.));
let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let mut builder = RunBuilder::new(&source, element_write_hashed::<Vector>(), &[FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap(); let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let item = builder.finish(); let item = builder.finish();
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list"); let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
let expected = map_groups_to_legacy(paint.element(0).unwrap()); let expected = map_groups_to_legacy(paint.element(0).unwrap());
@@ -127,9 +135,10 @@ mod run_tests {
drop(paint); drop(paint);
drop(source); drop(source);
let served = legacy.attribute::<Option<List<Graphic>>>(Fill::NAME, 0).expect("the fill attribute rides the list"); let served = legacy.attribute::<Appearance>(ATTR_APPEARANCE, 0).expect("the appearance rides the list");
let served = served.as_ref().expect("the fill is present"); let cell = served.paint_at(0).expect("the fill coverage keeps its paint");
assert_eq!(served.element(0).unwrap(), &expected); let Graphic::Graphic(cell_rows) = cell else { panic!("the paint cell keeps the list form") };
assert_eq!(cell_rows.element(0).unwrap(), &expected);
} }
#[test] #[test]
@@ -10,13 +10,12 @@ pub(crate) use glue::{list_contains_groups, map_attribute_groups_to_owned, map_a
pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resident}; pub use glue::{map_groups_to_owned, map_groups_to_persistent, map_groups_to_resident};
pub(crate) use legacy::run_to_legacy_list; pub(crate) use legacy::run_to_legacy_list;
pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list}; pub use legacy::{group_to_legacy_graphic, group_to_legacy_list, map_groups_to_legacy, map_paint_attrs_to_legacy, run_to_list};
pub use paint::{ pub use paint::{PaintColumns, PaintReach, bake_paint_transforms, is_paint_present, paint_cell_rows, vector_can_reduce_to_clip_path, vector_lane_can_reduce_to_clip_path};
LanePaint, PaintColumns, PaintOverlay, PaintOverlayColumn, PaintReach, bake_paint_transforms, has_paint, is_paint_present, paint_graphics, set_paint_attribute, set_paint_attribute_at,
vector_can_reduce_to_clip_path, vector_lane_can_reduce_to_clip_path,
};
pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows}; pub use walk::{GraphicLevel, GraphicLevelColumn, RowStep, VectorRow, direct_vector_len, flatten_vector_rows, group_is_empty, lane_attributes, run_lane_attributes, walk_vector_rows};
use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity}; use walk::{group_all_clipped, group_bounding_box, group_is_fully_transparent, group_is_opaque, group_render_complexity};
use crate::appearance::Appearance;
use crate::markers::ATTR_APPEARANCE;
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::{Item, List}; use core_types::list::{Item, List};
@@ -195,6 +194,7 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.); let current_opacity: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY, 1.);
let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); let current_fill: f64 = current_graphic_item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
let lane_layer_path: Option<Vec<NodeId>> = current_graphic_item.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH).cloned(); let lane_layer_path: Option<Vec<NodeId>> = current_graphic_item.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH).cloned();
let parent_appearance = current_graphic_item.attribute::<Appearance>(ATTR_APPEARANCE).and_then(Appearance::declared).cloned();
let (element, attributes) = current_graphic_item.into_parts(); let (element, attributes) = current_graphic_item.into_parts();
match element { match element {
@@ -222,6 +222,14 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
*v *= current_fill; *v *= current_fill;
} }
} }
// Appearance cascades into each child whose own is undeclared, since a declared child wins wholesale
if let Some(appearance) = &parent_appearance {
for v in sub_list.iter_attribute_values_mut_or_default::<Appearance>(ATTR_APPEARANCE) {
if v.is_empty() {
*v = appearance.clone();
}
}
}
flatten_recursive(output, sub_list, extract_variant, lane_layer_path.as_deref()); flatten_recursive(output, sub_list, extract_variant, lane_layer_path.as_deref());
} }
@@ -418,9 +426,9 @@ impl<'e> Graphic<'e> {
} }
} }
pub fn can_reduce_to_clip_path(&self) -> bool { pub fn can_reduce_to_clip_path(&self, inherited_appearance: Option<&Appearance>) -> bool {
match self { match self {
Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector)), Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&core_types::lane::Single(vector), inherited_appearance),
_ => false, _ => false,
} }
} }
@@ -443,9 +451,8 @@ impl<'e> Graphic<'e> {
match self { match self {
Graphic::None => true, Graphic::None => true,
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent), Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
// A bare leaf carries no paint attribute, so only an unstroked // A bare vector leaf carries no paint or stroke of its own, so it is invisible on its own
// vector is invisible on its own. Graphic::Vector(_) => true,
Graphic::Vector(vector) => vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()),
Graphic::Color(color) => color.a() == 0., Graphic::Color(color) => color.a() == 0.,
Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.a() == 0.), Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.a() == 0.),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false, Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
@@ -674,6 +681,33 @@ mod tests {
let flattened: List<Vector> = group.into_flattened_list(); let flattened: List<Vector> = group.into_flattened_list();
assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5); assert_eq!(flattened.attribute_cloned_or_default::<f64>(ATTR_OPACITY, 0), 0.5);
} }
// A padded (empty) appearance cell is undeclared, so the parent's appearance cascades into it while a declared sibling keeps its own
#[test]
fn flatten_cascades_into_padded_empty_appearance_items() {
use crate::appearance::Coverage;
let single = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::Color(color));
// Declaring an appearance on item 0 forces the attribute, padding item 1 with the empty appearance
let mut inner = List::new();
inner.push(Item::new_from_element(vector_graphic()));
inner.push(Item::new_from_element(vector_graphic()));
inner.set_attribute(ATTR_APPEARANCE, 0, single(Color::BLACK));
let mut outer = List::new_from_element(Graphic::Graphic(inner));
outer.set_attribute(ATTR_APPEARANCE, 0, single(Color::WHITE));
let flattened: List<Vector> = outer.into_flattened_list();
let color_of = |index: usize| {
let appearance = flattened.attribute::<Appearance>(ATTR_APPEARANCE, index)?;
let Graphic::Color(color) = appearance.paint_at(0)? else { return None };
Some(*color)
};
assert_eq!(color_of(0), Some(Color::BLACK), "a declared item should keep its own appearance");
assert_eq!(color_of(1), Some(Color::WHITE), "a padded item should inherit the parent appearance");
}
} }
#[cfg(test)] #[cfg(test)]
@@ -1,9 +1,10 @@
//! The paint column level: fill and stroke read as lane columns and threaded down to the elements they reach. //! The appearance cascade level: the declared appearance read as a lane column and threaded down to the elements it reaches.
use super::{Graphic, IntoGraphicList}; use super::Graphic;
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke}; use crate::appearance::Appearance;
use crate::markers::Appearance as AppearanceMarker;
use core_types::ATTR_TRANSFORM; use core_types::ATTR_TRANSFORM;
use core_types::attribute::{Attribute, Opacity}; use core_types::attribute::Opacity;
use core_types::lane::{LaneColumn, LaneSource}; use core_types::lane::{LaneColumn, LaneSource};
use core_types::list::{ItemAttributeValues, List}; use core_types::list::{ItemAttributeValues, List};
use glam::DAffine2; use glam::DAffine2;
@@ -15,216 +16,87 @@ pub fn is_paint_present(graphic_list: &List<Graphic>) -> bool {
graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty()) graphic_list.element(0).is_some_and(|graphic| !graphic.is_empty())
} }
/// Look up the paint graphics stored under the marker `A`, in the canonical `List<Graphic>` form.
pub fn paint_graphics<'a, A, S>(source: &'a S, index: usize) -> Option<&'a List<Graphic<'static>>>
where
S: LaneSource,
A: Attribute<Value<'a> = Option<&'a List<Graphic<'static>>>>,
{
source
.attr::<A>(index)
// Treat a blank paint attribute as absent so an empty attribute doesn't count as painted
.filter(|graphic_list| is_paint_present(graphic_list))
}
/// Whether the item carries a non-blank canonical `List<Graphic>` paint under the marker `A`,
/// checked by borrowing without cloning the renderable list.
pub fn has_paint<'a, A, S>(source: &'a S, index: usize) -> bool
where
S: LaneSource,
A: Attribute<Value<'a> = Option<&'a List<Graphic<'static>>>>,
{
paint_graphics::<A, S>(source, index).is_some()
}
/// Whether one lane of a vector source draws as a plain clip path: fully /// Whether one lane of a vector source draws as a plain clip path: fully
/// opaque, fill absent or opaque, stroke invisible or fully transparent. /// opaque, fill absent or opaque, stroke invisible or fully transparent.
pub fn vector_lane_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S, index: usize) -> bool { pub fn vector_lane_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S, index: usize, inherited_appearance: Option<&Appearance>) -> bool {
let Some(element) = source.element(index) else { return false }; if source.element(index).is_none() {
return false;
}
let opacity: f64 = source.attr::<Opacity>(index); let opacity: f64 = source.attr::<Opacity>(index);
let fill_opaque_or_absent = paint_graphics::<Fill, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque())); let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) let fill_opaque_or_absent = resolved
|| paint_graphics::<Stroke, _>(source, index).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent())); .fill_paint
.and_then(paint_cell_rows)
.is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
let stroke_invisible_or_transparent = resolved.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|| resolved
.stroke_paint
.and_then(paint_cell_rows)
.is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
} }
/// Whether every lane of a vector source draws as a plain clip path. /// Whether every lane of a vector source draws as a plain clip path.
pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S) -> bool { pub fn vector_can_reduce_to_clip_path<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>) -> bool {
(0..source.lane_count()).all(|index| vector_lane_can_reduce_to_clip_path(source, index)) (0..source.lane_count()).all(|index| vector_lane_can_reduce_to_clip_path(source, index, inherited_appearance))
} }
/// The paint a lane carries for its interiors, in the reference form /// A source's declared appearance column, resolved once for per-lane reads.
/// [`PaintOverlay`] threads down.
#[derive(Clone, Copy, Default)]
pub struct LanePaint<'a> {
pub fill: Option<&'a List<Graphic<'static>>>,
pub stroke: Option<&'a List<Graphic<'static>>>,
}
impl<'a> LanePaint<'a> {
pub const NONE: Self = Self { fill: None, stroke: None };
pub fn is_present(&self) -> bool {
self.fill.is_some() || self.stroke.is_some()
}
}
/// A source's fill and stroke columns, resolved once for per-lane reads.
pub struct PaintColumns<'a, S: LaneSource + 'a> { pub struct PaintColumns<'a, S: LaneSource + 'a> {
fill: S::Column<'a, Fill>, appearance: S::Column<'a, AppearanceMarker>,
stroke: S::Column<'a, Stroke>,
} }
impl<'a, S: LaneSource> PaintColumns<'a, S> { impl<'a, S: LaneSource> PaintColumns<'a, S> {
pub fn new(source: &'a S) -> Self { pub fn new(source: &'a S) -> Self {
Self { Self {
fill: source.column::<Fill>(), appearance: source.column::<AppearanceMarker>(),
stroke: source.column::<Stroke>(),
} }
} }
/// The lane's present, non-blank paint. /// The lane's own declared appearance; an absent or empty cell is undeclared.
pub fn read(&self, lane: usize) -> LanePaint<'a> { pub fn read_appearance(&self, lane: usize) -> Option<&'a Appearance> {
let present = |value: Option<Option<&'a List<Graphic<'static>>>>| value.flatten().filter(|list| is_paint_present(list)); self.appearance.try_get(lane).flatten().and_then(Appearance::declared)
LanePaint {
fill: present(self.fill.try_get(lane)),
stroke: present(self.stroke.try_get(lane)),
}
} }
} }
/// How far a lane's paint reaches into the element beneath it, mirroring the /// The appearance cascade threading down the graphic levels: a lane's own
/// legacy conversion's paint push: vector interiors directly and vector /// declared appearance wins wholesale, an undeclared lane inherits the
/// children of a nested graphic list, one level deep. /// nearest ancestor's, at any depth. Only a fresh entry (a pattern's own
/// render, or any standalone render root) starts without an inherited one.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct PaintReach<'a> { pub struct PaintReach<'a> {
pub paint: LanePaint<'a>, /// The cascade's resolved appearance: the nearest declared one at or above this lane.
hops: u8, pub appearance: Option<&'a Appearance>,
} }
impl<'a> PaintReach<'a> { impl<'a> PaintReach<'a> {
pub const NONE: Self = Self { paint: LanePaint::NONE, hops: 0 }; pub const NONE: Self = Self { appearance: None };
/// The lane's effective reach: an inherited paint stays authoritative /// The lane's effective reach: its own declared appearance wins over the inherited one.
/// (lane paint below a push's origin is inert in the legacy model), an
/// absent one reads the lane's own paint.
pub fn for_lane<S: LaneSource>(self, columns: &PaintColumns<'a, S>, index: usize) -> Self { pub fn for_lane<S: LaneSource>(self, columns: &PaintColumns<'a, S>, index: usize) -> Self {
match self.paint.is_present() {
true => self,
false => Self { paint: columns.read(index), hops: 2 },
}
}
pub fn applies(&self) -> bool {
self.hops > 0 && self.paint.is_present()
}
/// The reach one graphic nesting level further down.
pub fn nested(self) -> Self {
Self { Self {
paint: self.paint, appearance: Appearance::cascade(columns.read_appearance(index), self.appearance),
hops: self.hops.saturating_sub(1),
}
}
/// The reach entering a group's own graphic run: a spent or absent reach
/// resets so the group's own lane paint applies at its own boundary.
pub fn into_group_graphics(self) -> Self {
match self.applies() {
true => self.nested(),
false => Self::NONE,
} }
} }
} }
/// A source with a lane's paint forced over its fill and stroke columns, /// The paint a coverage row's cell holds, in the canonical `List<Graphic>` form the paint
/// reaching the interiors the legacy conversion's paint push reached. /// renderers consume: this crate's writers carry the list as one graphic cell, and a bare
pub struct PaintOverlay<'a, S> { /// cell of any other form is treated as paint that draws nothing.
inner: &'a S, pub fn paint_cell_rows<'a>(cell: &'a Graphic<'static>) -> Option<&'a List<Graphic<'static>>> {
paint: LanePaint<'a>, match cell {
} Graphic::Graphic(list) => Some(list).filter(|list| is_paint_present(list)),
impl<'a, S> PaintOverlay<'a, S> {
pub fn new(inner: &'a S, paint: LanePaint<'a>) -> Self {
Self { inner, paint }
}
}
pub struct PaintOverlayColumn<'a, S: LaneSource + 'a, A: Attribute> {
inner: S::Column<'a, A>,
forced: Option<A::Value<'a>>,
}
impl<'a, S: LaneSource, A: Attribute> LaneColumn<'a, A> for PaintOverlayColumn<'a, S, A> {
fn try_get(&self, lane: usize) -> Option<A::Value<'a>> {
match self.forced {
Some(forced) => Some(forced),
None => self.inner.try_get(lane),
}
}
}
/// The forced value for the marker `A`: the lane paint where `A` is this
/// crate's fill or stroke marker, absent otherwise.
fn forced_paint<'a, A: Attribute>(paint: LanePaint<'a>) -> Option<A::Value<'a>> {
let slot = match A::NAME {
name if name == Fill::NAME => paint.fill,
name if name == Stroke::NAME => paint.stroke,
_ => None, _ => None,
}?;
assert_eq!(
std::any::TypeId::of::<A::Value<'static>>(),
std::any::TypeId::of::<Option<&'static List<Graphic<'static>>>>(),
"attribute `{}` is declared at another value type than this crate's paint form",
A::NAME
);
assert_eq!(
size_of::<A::Value<'a>>(),
size_of::<Option<&'a List<Graphic<'a>>>>(),
"the paint value form must span the marker's value"
);
// SAFETY: the census admits one value type per attribute name, so a `fill` or `stroke` marker carries this crate's `Option<&List<Graphic>>` at the asserted size.
Some(unsafe { std::mem::transmute_copy::<Option<&'a List<Graphic>>, A::Value<'a>>(&Some(slot)) })
}
impl<'a, S: LaneSource> LaneSource for PaintOverlay<'a, S> {
type Element = S::Element;
type Column<'b, A: Attribute>
= PaintOverlayColumn<'b, S, A>
where
Self: 'b;
fn lane_count(&self) -> usize {
self.inner.lane_count()
}
fn element(&self, lane: usize) -> Option<&S::Element> {
self.inner.element(lane)
}
fn column<A: Attribute>(&self) -> PaintOverlayColumn<'_, S, A> {
PaintOverlayColumn {
inner: self.inner.column::<A>(),
forced: forced_paint::<A>(self.paint),
}
} }
} }
/// Stores a paint attribute in the paint marker's owned form, the only representation paint readers accept. /// Bake the provided transform into the per-item transforms of the paint
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) { /// graphics inside the item's appearance coverage cells.
attributes.insert(key, Some(paint.into_graphic_list()));
}
/// Stores a paint attribute at a list index in the paint marker's owned form, the only representation paint readers accept.
pub fn set_paint_attribute_at<T>(list: &mut List<T>, index: usize, key: &str, paint: impl IntoGraphicList) {
list.set_attribute(key, index, Some(paint.into_graphic_list()));
}
/// Bake the provided transform into the per-item transforms of the paint graphics stored under the
/// 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) {
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) { fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
for item_transform in graphics.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) { for item_transform in graphics.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
@@ -237,9 +109,13 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
} }
} }
for paint_key in [ATTR_FILL, ATTR_STROKE] { if let Some(appearance) = attributes.get_mut::<Appearance>(crate::markers::ATTR_APPEARANCE)
if let Some(Some(graphics)) = attributes.get_mut::<Option<List<Graphic>>>(paint_key) { && let Some(cells) = appearance.0.iter_attribute_values_mut::<Graphic>(crate::markers::ATTR_PAINT)
bake_graphic_paint_transform(graphics, transform); {
for cell in cells {
if let Graphic::Graphic(list) = cell {
bake_graphic_paint_transform(list, transform);
}
} }
} }
} }
@@ -254,22 +130,44 @@ mod run_tests {
use glam::DVec2; use glam::DVec2;
#[test] #[test]
fn a_run_serves_the_parked_paint_reference() { fn a_run_serves_the_parked_appearance_reference() {
let paint = List::new_from_element(Graphic::Color(Color::BLACK)); use crate::appearance::Coverage;
use core_types::lane::LaneSource;
let appearance = Appearance::new_single(Coverage::new_fill(), Graphic::Color(Color::BLACK));
let vector = unit_square_at(DVec2::ZERO); let vector = unit_square_at(DVec2::ZERO);
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(vector.clone()).unwrap(); let lane = builder.push(vector.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let item = builder.finish(); let item = builder.finish();
let run = RunView::<Vector>::new(&item).expect("the run holds vector elements"); let run = RunView::<Vector>::new(&item).expect("the run holds vector elements");
assert_eq!(run.attr::<Fill>(0), Some(&paint)); assert_eq!(run.attr::<AppearanceMarker>(0), Some(&appearance));
assert_eq!(paint_graphics::<Fill, _>(&run, 0), Some(&paint));
assert_eq!(paint_graphics::<Stroke, _>(&run, 0), None);
let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list"); let legacy = run_to_legacy_list::<Vector>(&item).expect("the run lowers to a legacy vector list");
assert_eq!(paint_graphics::<Fill, _>(&legacy, 0), paint_graphics::<Fill, _>(&run, 0)); assert_eq!(legacy.attr::<AppearanceMarker>(0), Some(&appearance));
}
#[test]
fn reach_cascades_the_appearance_with_own_wins_arbitration() {
use crate::appearance::Coverage;
use crate::markers::ATTR_APPEARANCE;
let own = Appearance::new_single(Coverage::new_fill(), Graphic::Color(Color::BLACK));
let inherited = Appearance::new_single(Coverage::new_fill(), Graphic::Color(Color::WHITE));
// Lane 0 declares its own appearance, lane 1 is padded with the empty (undeclared) one
let mut list: List<Graphic<'static>> = List::new_from_element(Graphic::Vector(Vector::default()));
list.push(core_types::list::Item::new_from_element(Graphic::Vector(Vector::default())));
list.set_attribute(ATTR_APPEARANCE, 0, own.clone());
let columns = PaintColumns::new(&list);
let ancestor = PaintReach { appearance: Some(&inherited) };
assert_eq!(ancestor.for_lane(&columns, 0).appearance, Some(&own), "a declared lane wins over the inherited appearance");
assert_eq!(ancestor.for_lane(&columns, 1).appearance, Some(&inherited), "a padded lane inherits");
assert_eq!(PaintReach::NONE.for_lane(&columns, 1).appearance, None, "no ancestor leaves an undeclared lane bare");
} }
} }
@@ -1,8 +1,9 @@
//! The native-content walk: vector rows reached through a graphic's own storage, with no legacy conversion. //! The native-content walk: vector rows reached through a graphic's own storage, with no legacy conversion.
use super::Graphic; use super::Graphic;
use super::paint::{LanePaint, PaintColumns, PaintReach, is_paint_present, paint_graphics, set_paint_attribute_at}; use super::paint::{PaintColumns, PaintReach};
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill}; use crate::appearance::Appearance;
use crate::markers::ATTR_APPEARANCE;
use core_types::attribute::{Attribute, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform}; use core_types::attribute::{Attribute, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform};
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::lane::LaneSource; use core_types::lane::LaneSource;
@@ -251,7 +252,7 @@ pub struct VectorRow<'w> {
source: RowSourceRef<'w>, source: RowSourceRef<'w>,
scale: FlattenScale, scale: FlattenScale,
layer_path: Option<&'w [NodeId]>, layer_path: Option<&'w [NodeId]>,
paint: LanePaint<'w>, appearance: Option<&'w Appearance>,
/// The lane of the walk's OWN top level whose subtree produced this row. /// The lane of the walk's OWN top level whose subtree produced this row.
/// A leaf at any depth reports the top-level row it descends from, which is /// A leaf at any depth reports the top-level row it descends from, which is
/// the lane whose columns a consumer carries onto it. /// the lane whose columns a consumer carries onto it.
@@ -283,16 +284,14 @@ impl VectorRow<'_> {
} }
} }
/// Whether the built row will carry fill paint: the reaching lane paint, /// Whether the built row will carry fill paint: a painted fill coverage on
/// else the row's own. /// the row's resolved appearance.
pub fn has_fill(&self) -> bool { pub fn has_fill(&self) -> bool {
if self.paint.fill.is_some() { let own = match &self.source {
return true; RowSourceRef::Lane(level, index) => level.attr::<crate::markers::Appearance>(*index),
} RowSourceRef::Run(run, _, index) => LaneSource::attr::<crate::markers::Appearance>(*run, *index),
match &self.source { };
RowSourceRef::Lane(level, index) => paint_graphics::<Fill, _>(level, *index).is_some(), Appearance::cascade(own, self.appearance).is_some_and(|appearance| appearance.has_painted_cover(crate::appearance::Cover::Fill))
RowSourceRef::Run(run, _, index) => paint_graphics::<Fill, _>(*run, *index).is_some(),
}
} }
/// Builds the row at the end of `out`, applying the reach paint and the /// Builds the row at the end of `out`, applying the reach paint and the
@@ -309,11 +308,6 @@ impl VectorRow<'_> {
out.push(Item::from_parts(vector, run_lane_attributes(item, *lane))); out.push(Item::from_parts(vector, run_lane_attributes(item, *lane)));
} }
} }
for (key, slot) in [(ATTR_FILL, self.paint.fill), (ATTR_STROKE, self.paint.stroke)] {
if let Some(paint) = slot {
set_paint_attribute_at(out, index, key, paint.clone());
}
}
if self.scale.has_transform || out.attribute::<DAffine2>(ATTR_TRANSFORM, index).is_some() { if self.scale.has_transform || out.attribute::<DAffine2>(ATTR_TRANSFORM, index).is_some() {
let row_transform: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index); let row_transform: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index);
out.set_attribute(ATTR_TRANSFORM, index, self.scale.transform * row_transform); out.set_attribute(ATTR_TRANSFORM, index, self.scale.transform * row_transform);
@@ -329,6 +323,12 @@ impl VectorRow<'_> {
if let Some(layer_path) = self.layer_path { if let Some(layer_path) = self.layer_path {
out.set_attribute(ATTR_EDITOR_LAYER_PATH, index, layer_path.to_vec()); out.set_attribute(ATTR_EDITOR_LAYER_PATH, index, layer_path.to_vec());
} }
// The cascade's resolved appearance lands on a row whose own is undeclared, since a declared row wins wholesale
if let Some(appearance) = self.appearance
&& out.attribute::<Appearance>(ATTR_APPEARANCE, index).and_then(Appearance::declared).is_none()
{
out.set_attribute(ATTR_APPEARANCE, index, appearance.clone());
}
} }
} }
@@ -338,7 +338,7 @@ fn walk_rows_of_run(
item: &core_types::record::GroupItem, item: &core_types::record::GroupItem,
scale: FlattenScale, scale: FlattenScale,
layer_path: Option<&[NodeId]>, layer_path: Option<&[NodeId]>,
paint: LanePaint<'_>, appearance: Option<&Appearance>,
top_lane: Option<usize>, top_lane: Option<usize>,
visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep,
) -> RowStep { ) -> RowStep {
@@ -350,7 +350,7 @@ fn walk_rows_of_run(
source: RowSourceRef::Run(&run, item, lane), source: RowSourceRef::Run(&run, item, lane),
scale, scale,
layer_path, layer_path,
paint, appearance,
top_lane: top_lane.unwrap_or(lane), top_lane: top_lane.unwrap_or(lane),
}) { }) {
return RowStep::Stop; return RowStep::Stop;
@@ -360,9 +360,9 @@ fn walk_rows_of_run(
} }
/// Walks a graphic level into its flattened vector rows, matching the legacy /// Walks a graphic level into its flattened vector rows, matching the legacy
/// push-then-flatten lowering: lane paint threads with [`PaintReach`], /// flatten lowering: the appearance cascades with [`PaintReach`], ancestor
/// ancestor transform, opacity and fill opacity compose down, the containing /// transform, opacity and fill opacity compose down, the containing level's
/// level's parent layer path overwrites its rows, and non-vector content is /// parent layer path overwrites its rows, and non-vector content is
/// discarded. A de-tabled leaf's row is its lane, attributes included. /// discarded. A de-tabled leaf's row is its lane, attributes included.
pub fn walk_vector_rows(level: GraphicLevel<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) { pub fn walk_vector_rows(level: GraphicLevel<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) {
walk_vector_rows_impl(level, FlattenScale::ROOT, None, PaintReach::NONE, None, visit); walk_vector_rows_impl(level, FlattenScale::ROOT, None, PaintReach::NONE, None, visit);
@@ -382,21 +382,13 @@ fn walk_vector_rows_impl<'a>(
if let GraphicLevel::Run(item) = level { if let GraphicLevel::Run(item) = level {
// A vector-typed run is already its rows. // A vector-typed run is already its rows.
if item.typed_lanes::<Vector>().is_some() { if item.typed_lanes::<Vector>().is_some() {
let paint = match inherited.applies() { return walk_rows_of_run(item, scale, parent_layer_path, inherited.appearance, top_lane, visit);
true => inherited.paint,
false => LanePaint::NONE,
};
return walk_rows_of_run(item, scale, parent_layer_path, paint, top_lane, visit);
} }
} }
let columns = PaintColumns::new(&level); let columns = PaintColumns::new(&level);
for index in 0..level.lane_count() { for index in 0..level.lane_count() {
let Some(element) = level.element(index) else { continue }; let Some(element) = level.element(index) else { continue };
let reach = inherited.for_lane(&columns, index); let reach = inherited.for_lane(&columns, index);
let row_paint = match reach.applies() {
true => reach.paint,
false => LanePaint::NONE,
};
// At the top level this lane IS the row every leaf under it reports. // At the top level this lane IS the row every leaf under it reports.
let row_top = top_lane.unwrap_or(index); let row_top = top_lane.unwrap_or(index);
let step = match element { let step = match element {
@@ -404,27 +396,27 @@ fn walk_vector_rows_impl<'a>(
source: RowSourceRef::Lane(level, index), source: RowSourceRef::Lane(level, index),
scale, scale,
layer_path: parent_layer_path, layer_path: parent_layer_path,
paint: row_paint, appearance: reach.appearance,
top_lane: row_top, top_lane: row_top,
}), }),
Graphic::Graphic(children) => walk_vector_rows_impl( Graphic::Graphic(children) => walk_vector_rows_impl(
GraphicLevel::Legacy(children), GraphicLevel::Legacy(children),
scale.composed(&level, index), scale.composed(&level, index),
level.try_attr::<EditorLayerPath>(index), level.try_attr::<EditorLayerPath>(index),
reach.nested(), reach,
Some(row_top), Some(row_top),
visit, visit,
), ),
Graphic::Group(group) => { Graphic::Group(group) => {
let item = &group.content; let item = &group.content;
if item.typed_lanes::<Vector>().is_some() { if item.typed_lanes::<Vector>().is_some() {
walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::<EditorLayerPath>(index), row_paint, Some(row_top), visit) walk_rows_of_run(item, scale.composed(&level, index), level.try_attr::<EditorLayerPath>(index), reach.appearance, Some(row_top), visit)
} else if item.typed_lanes::<Graphic>().is_some() { } else if item.typed_lanes::<Graphic>().is_some() {
walk_vector_rows_impl( walk_vector_rows_impl(
GraphicLevel::Run(item), GraphicLevel::Run(item),
scale.composed(&level, index), scale.composed(&level, index),
level.try_attr::<EditorLayerPath>(index), level.try_attr::<EditorLayerPath>(index),
reach.into_group_graphics(), reach,
Some(row_top), Some(row_top),
visit, visit,
) )
@@ -452,26 +444,6 @@ pub fn flatten_vector_rows(level: GraphicLevel<'_>) -> List<Vector> {
out out
} }
/// The transitional paint placement: a lane-level fill or stroke paint
/// attribute moves onto the vector interiors the legacy paint readers
/// inspect, reaching as far as the pre-flip broadcast did.
pub(in crate::graphic) fn push_lane_paint_into_interiors(list: &mut List<Graphic>) {
for index in 0..list.len() {
for key in [ATTR_FILL, ATTR_STROKE] {
let stored = list.attribute::<Option<List<Graphic>>>(key, index).and_then(|optional| optional.as_ref());
let Some(paint) = stored.filter(|paint| is_paint_present(paint)).cloned() else {
continue;
};
let Some(Graphic::Graphic(children)) = list.element_mut(index) else { continue };
for child in 0..children.len() {
if matches!(children.element(child), Some(Graphic::Vector(_))) {
set_paint_attribute_at(children, child, key, paint.clone());
}
}
}
}
}
/// The count [`map_groups_to_legacy`] would expose through [`Graphic::as_vector`], /// The count [`map_groups_to_legacy`] would expose through [`Graphic::as_vector`],
/// read from the run's lanes instead of materializing the legacy list. Mirrors /// read from the run's lanes instead of materializing the legacy list. Mirrors
/// [`group_to_legacy_graphic`]'s typed-run path, where `Vector` is tried first. /// [`group_to_legacy_graphic`]'s typed-run path, where `Vector` is tried first.
@@ -544,6 +516,10 @@ mod run_tests {
#[test] #[test]
fn the_vector_row_walk_matches_the_legacy_flatten() { fn the_vector_row_walk_matches_the_legacy_flatten() {
use crate::appearance::Coverage;
let single = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::Color(color));
let inner_vector = unit_square_at(DVec2::ZERO); let inner_vector = unit_square_at(DVec2::ZERO);
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[], 1).unwrap(); let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[], 1).unwrap();
@@ -555,7 +531,7 @@ mod run_tests {
painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE)))); painted.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE))));
painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.))); painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.)));
painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.))); painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.)));
set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE))); painted.set_attribute(ATTR_APPEARANCE, 1, single(Color::WHITE));
let mut nested = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.)))); let mut nested = List::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(2., 2.))));
nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.))); nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.)));
@@ -568,12 +544,12 @@ mod run_tests {
top.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(6., 0.))))); top.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::new(6., 0.)))));
top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.))); top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.)));
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]); top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]);
set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(Color::BLACK))); top.set_attribute(ATTR_APPEARANCE, 0, single(Color::BLACK));
top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5); top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5);
top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.))); top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.)));
top.set_attribute(core_types::ATTR_TRANSFORM, 4, DAffine2::from_translation(DVec2::new(0., 7.))); top.set_attribute(core_types::ATTR_TRANSFORM, 4, DAffine2::from_translation(DVec2::new(0., 7.)));
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 4, vec![core_types::uuid::NodeId(9)]); top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 4, vec![core_types::uuid::NodeId(9)]);
set_paint_attribute_at(&mut top, 4, ATTR_FILL, List::new_from_element(Graphic::Color(Color::WHITE))); top.set_attribute(ATTR_APPEARANCE, 4, single(Color::WHITE));
let legacy = { let legacy = {
let mut list = List::new(); let mut list = List::new();
@@ -581,7 +557,6 @@ mod run_tests {
let (element, attributes) = item.into_parts(); let (element, attributes) = item.into_parts();
list.push(Item::from_parts(map_groups_to_legacy(&element), attributes)); list.push(Item::from_parts(map_groups_to_legacy(&element), attributes));
} }
push_lane_paint_into_interiors(&mut list);
list.into_flattened_list::<Vector>() list.into_flattened_list::<Vector>()
}; };
let native = flatten_vector_rows(GraphicLevel::Legacy(&top)); let native = flatten_vector_rows(GraphicLevel::Legacy(&top));
@@ -603,9 +578,9 @@ mod run_tests {
"layer path, row {row}" "layer path, row {row}"
); );
assert_eq!( assert_eq!(
native.attribute::<Option<List<Graphic>>>(ATTR_FILL, row), native.attribute::<Appearance>(ATTR_APPEARANCE, row),
legacy.attribute::<Option<List<Graphic>>>(ATTR_FILL, row), legacy.attribute::<Appearance>(ATTR_APPEARANCE, row),
"fill, row {row}" "appearance, row {row}"
); );
} }
assert_eq!(native, legacy); assert_eq!(native, legacy);
@@ -634,4 +609,33 @@ mod run_tests {
assert_eq!(run.thumbnail_bounding_box(outer, include_stroke), legacy.thumbnail_bounding_box(outer, include_stroke)); assert_eq!(run.thumbnail_bounding_box(outer, include_stroke), legacy.thumbnail_bounding_box(outer, include_stroke));
} }
} }
#[test]
fn the_walk_cascades_appearance_like_the_legacy_flatten() {
use crate::appearance::Coverage;
let single = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::Color(color));
let mut inner = List::new();
inner.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ZERO))));
inner.push(Item::new_from_element(Graphic::Vector(unit_square_at(DVec2::ONE))));
inner.set_attribute(ATTR_APPEARANCE, 0, single(Color::BLACK));
let mut top = List::new_from_element(Graphic::Graphic(inner));
top.set_attribute(ATTR_APPEARANCE, 0, single(Color::WHITE));
let walked = flatten_vector_rows(GraphicLevel::Legacy(&top));
let legacy: List<Vector> = top.clone().into_flattened_list();
let color_of = |list: &List<Vector>, index: usize| {
let appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, index)?;
let Graphic::Color(color) = appearance.paint_at(0)? else { return None };
Some(*color)
};
for list in [&walked, &legacy] {
assert_eq!(color_of(list, 0), Some(Color::BLACK), "a declared row keeps its own appearance");
assert_eq!(color_of(list, 1), Some(Color::WHITE), "an undeclared row inherits the level's appearance");
}
}
} }
+15 -10
View File
@@ -1,3 +1,4 @@
pub mod appearance;
pub mod artboard; pub mod artboard;
pub mod boundary; pub mod boundary;
pub mod graphic; pub mod graphic;
@@ -9,9 +10,10 @@ pub use raster_types;
pub use vector_types; pub use vector_types;
// Re-export commonly used types at the crate root // Re-export commonly used types at the crate root
pub use appearance::{Appearance, Cover, CoverPlacement, Coverage, FillAndStroke, stamp_coverage};
pub use artboard::Artboard; pub use artboard::Artboard;
pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector}; pub use graphic::{Graphic, IntoGraphicList, TryFromGraphic, Vector};
pub use markers::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE}; pub use markers::{ATTR_APPEARANCE, ATTR_EDITOR_MERGED_LAYERS, ATTR_PAINT};
pub mod migrations { pub mod migrations {
use crate::Vector; use crate::Vector;
@@ -99,6 +101,7 @@ pub mod migrations {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[cfg_attr(test, derive(Default, serde::Serialize))] #[cfg_attr(test, derive(Default, serde::Serialize))]
pub(super) struct PathStyle { pub(super) struct PathStyle {
#[allow(dead_code)]
pub stroke: Option<Stroke>, pub stroke: Option<Stroke>,
} }
@@ -106,6 +109,7 @@ pub mod migrations {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[cfg_attr(test, derive(Default, serde::Serialize))] #[cfg_attr(test, derive(Default, serde::Serialize))]
pub(super) struct VectorData { pub(super) struct VectorData {
#[allow(dead_code)]
pub style: PathStyle, pub style: PathStyle,
pub colinear_manipulators: Vec<[HandleId; 2]>, pub colinear_manipulators: Vec<[HandleId; 2]>,
pub point_domain: PointDomain, pub point_domain: PointDomain,
@@ -138,7 +142,6 @@ pub mod migrations {
Ok(match VectorFormat::deserialize(deserializer)? { Ok(match VectorFormat::deserialize(deserializer)? {
VectorFormat::OldVectorData(old) => Some(Vector { VectorFormat::OldVectorData(old) => Some(Vector {
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,
@@ -190,9 +193,12 @@ pub mod migrations {
use vector_types::vector::style::Stroke; use vector_types::vector::style::Stroke;
#[test] #[test]
fn preserves_stroke_from_old_vector_data_style() { fn recovers_geometry_from_old_vector_data_style() {
use core_types::ops::FromAnchorPosition;
let old_vector = legacy::VectorData { let old_vector = legacy::VectorData {
style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) }, style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) },
point_domain: Vector::from_anchor_position(glam::DVec2::new(3., 4.)).point_domain,
..Default::default() ..Default::default()
}; };
@@ -207,20 +213,19 @@ pub mod migrations {
.insert("fill".into(), serde_json::to_value(legacy::LegacyFill::default()).unwrap()); .insert("fill".into(), serde_json::to_value(legacy::LegacyFill::default()).unwrap());
let migrated = migrate_to_optional_vector(value).unwrap().unwrap(); let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
assert_eq!(migrated.stroke.unwrap().weight, 12.); assert_eq!(migrated.point_domain.positions(), [glam::DVec2::new(3., 4.)], "the geometry survives alongside the discarded style");
} }
#[test] #[test]
fn preserves_stroke_from_current_vector_data() { fn recovers_geometry_from_current_vector_data() {
let vector = Vector { use core_types::ops::FromAnchorPosition;
stroke: Some(Stroke::new(12.)),
..Default::default() let vector = Vector::from_anchor_position(glam::DVec2::new(3., 4.));
};
let value = serde_json::to_value(&vector).unwrap(); let value = serde_json::to_value(&vector).unwrap();
let migrated = migrate_to_optional_vector(value).unwrap().unwrap(); let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
assert_eq!(migrated.stroke.unwrap().weight, 12.); assert_eq!(migrated.point_domain.positions(), [glam::DVec2::new(3., 4.)]);
} }
} }
} }
@@ -1,30 +1,104 @@
//! Attribute markers whose value types live in this crate, with their name //! Attribute markers whose value types live in this crate, with their name
//! constants for the string-keyed legacy readers and writers. //! constants for the string-keyed legacy readers and writers.
//! //!
//! The list-valued markers may carry native [`Graphic::Group`] content: the //! The markers may carry native [`Graphic::Group`] content: the registered
//! registered deep field glue owns it across persistence seams, and legacy //! deep field glue owns it across persistence seams, and legacy products
//! products convert it through [`crate::graphic::map_paint_attrs_to_legacy`]. //! convert it through [`crate::graphic::map_paint_attrs_to_legacy`].
use crate::Graphic; use crate::Graphic;
use core_types::attribute::Attribute; use core_types::attribute::Attribute;
use core_types::list::List; use core_types::list::List;
core_types::attribute! { core_types::attribute! {
/// Vector graphics object's filled area paint, a graphic list in the canonical paint form.
/// An absent value means no fill.
pub Fill("fill"): Option<&List<Graphic<'static>>>;
/// Vector graphics object's stroke paint, a graphic list in the canonical paint form.
/// An absent value means no stroke paint.
pub Stroke("stroke"): Option<&List<Graphic<'static>>>;
/// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation, /// Snapshot of the upstream content that fed into a destructive merge (Boolean Operation,
/// Rasterize, etc.), so the editor can still surface click targets for the original child /// Rasterize, etc.), so the editor can still surface click targets for the original child
/// layers after their content has been collapsed. /// layers after their content has been collapsed.
pub EditorMergedLayers("editor:merged_layers"): Option<&List<Graphic<'static>>>; pub EditorMergedLayers("editor:merged_layers"): Option<&List<Graphic<'static>>>;
} }
pub const ATTR_FILL: &str = Fill::NAME; /// The item's ordered list of paint passes. An absent or empty value is the undeclared
pub const ATTR_STROKE: &str = Stroke::NAME; /// state that inherits the nearest ancestor's appearance through the cascade, so both
/// read as `None`. The stored form is the bare [`crate::appearance::Appearance`], the
/// shape the appearance writers use, which the `attribute!` macro's optional-reference
/// arm cannot express.
pub struct Appearance;
// SAFETY: `read_erased` produces the bare owned appearance `from_stored` reads, `REPARK`
// re-parks that same form, and the empty appearance collapses to the `None` default at
// every read seam.
unsafe impl Attribute for Appearance {
const NAME: &'static str = "appearance";
type Value<'e> = Option<&'e crate::appearance::Appearance>;
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>> {
stored.downcast_ref::<crate::appearance::Appearance>().map(crate::appearance::Appearance::declared)
}
unsafe fn read_erased(ptr: *const u8) -> Box<dyn core_types::list::AnyAttributeValue> {
Box::new(unsafe { ptr.cast::<Option<&crate::appearance::Appearance>>().read() }.cloned().unwrap_or_default())
}
const REPARK: Option<core_types::list::ReparkFn> = {
unsafe fn repark(value: &dyn core_types::list::AnyAttributeValue, dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> {
let owned: &crate::appearance::Appearance = value.as_any().downcast_ref().expect("an appearance attribute replays its bare owned clone");
let parked = match owned.is_empty() {
true => None,
false => {
let (parked, _) = arena.alloc(owned.clone())?;
Some(parked)
}
};
// SAFETY: the slot is a live field of this marker's value type.
unsafe { dst.cast::<Option<&crate::appearance::Appearance>>().write(parked) };
Some(())
}
Some(repark)
};
}
core_types::attribute!(@register Appearance);
/// One coverage row's paint, a bare graphic riding the coverage list as a column. Absent
/// or empty paint draws nothing, so both read as `None`; the stored form is the bare
/// [`Graphic`], the shape [`crate::appearance`]'s row writers use.
pub struct Paint;
// SAFETY: as for `Appearance`, at the bare `Graphic` stored form.
unsafe impl Attribute for Paint {
const NAME: &'static str = "paint";
type Value<'e> = Option<&'e Graphic<'static>>;
fn from_stored<'a>(stored: &'a dyn std::any::Any) -> Option<Self::Value<'a>> {
stored.downcast_ref::<Graphic<'static>>().map(|paint| (!paint.is_empty()).then_some(paint))
}
unsafe fn read_erased(ptr: *const u8) -> Box<dyn core_types::list::AnyAttributeValue> {
Box::new(unsafe { ptr.cast::<Option<&Graphic<'static>>>().read() }.cloned().unwrap_or_default())
}
const REPARK: Option<core_types::list::ReparkFn> = {
unsafe fn repark(value: &dyn core_types::list::AnyAttributeValue, dst: *mut u8, arena: &core_types::arena::Arena) -> Option<()> {
let owned: &Graphic<'static> = value.as_any().downcast_ref().expect("a paint attribute replays its bare owned clone");
let parked = match owned.is_empty() {
true => None,
false => {
let (parked, _) = arena.alloc(owned.clone())?;
Some(parked)
}
};
// SAFETY: the slot is a live field of this marker's value type.
unsafe { dst.cast::<Option<&Graphic<'static>>>().write(parked) };
Some(())
}
Some(repark)
};
}
core_types::attribute!(@register Paint);
pub const ATTR_EDITOR_MERGED_LAYERS: &str = EditorMergedLayers::NAME; pub const ATTR_EDITOR_MERGED_LAYERS: &str = EditorMergedLayers::NAME;
pub const ATTR_APPEARANCE: &str = Appearance::NAME;
pub const ATTR_PAINT: &str = Paint::NAME;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -34,25 +108,13 @@ mod tests {
#[test] #[test]
fn the_census_carries_this_crates_names() { fn the_census_carries_this_crates_names() {
for name in ["fill", "stroke", "editor:merged_layers"] { assert_eq!(info("editor:merged_layers").unwrap().value_type, TypeId::of::<Option<&'static List<Graphic>>>());
assert_eq!(info(name).unwrap().value_type, TypeId::of::<Option<&'static List<Graphic>>>()); assert_eq!(info("appearance").unwrap().value_type, TypeId::of::<Option<&'static crate::appearance::Appearance>>());
} assert_eq!(info("paint").unwrap().value_type, TypeId::of::<Option<&'static Graphic>>());
} }
#[test] #[test]
fn an_absent_paint_defaults_to_none() { fn an_absent_paint_defaults_to_none() {
assert_eq!(<Fill as Attribute>::default(), None); assert_eq!(<Paint as Attribute>::default(), None);
}
#[test]
fn a_paint_marker_reads_back_what_the_paint_writer_stored() {
use core_types::lane::LaneSource;
let paint = List::new_from_element(Graphic::default());
let mut list = List::new_from_element(Graphic::default());
crate::graphic::set_paint_attribute_at(&mut list, 0, ATTR_FILL, paint.clone());
assert_eq!(list.attr::<Fill>(0), Some(&paint));
assert_eq!(list.attr::<Stroke>(0), None);
} }
} }
@@ -185,3 +185,60 @@ impl Display for BlendMode {
} }
} }
} }
/// Mixes the two colors by the blend mode's own formula, leaving the alpha compositing to the caller.
pub fn apply_blend_mode(foreground: crate::color::Color, background: crate::color::Color, blend_mode: BlendMode) -> crate::color::Color {
use crate::color::Color;
match blend_mode {
// Normal group
BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal),
// Darken group
BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken),
BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply),
BlendMode::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn),
BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn),
BlendMode::DarkerColor => background.blend_darker_color(foreground),
// Lighten group
BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten),
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
BlendMode::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge),
BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge),
BlendMode::LighterColor => background.blend_lighter_color(foreground),
// Contrast group
BlendMode::Overlay => background.blend_rgb(foreground, Color::blend_overlay),
BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight),
BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight),
BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light),
BlendMode::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light),
BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light),
BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix),
// Inversion group
BlendMode::Difference => background.blend_rgb(foreground, Color::blend_difference),
BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion),
BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract),
BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide),
// Component group
BlendMode::Hue => background.blend_hue(foreground),
BlendMode::Saturation => background.blend_saturation(foreground),
BlendMode::Color => background.blend_color(foreground),
BlendMode::Luminosity => background.blend_luminosity(foreground),
// The alpha-only utility modes mix no color, so the foreground passes through for the caller to composite
BlendMode::Erase | BlendMode::Restore | BlendMode::MultiplyAlpha => foreground,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
#[test]
fn overlay_is_hard_light_with_swapped_operands() {
let a = Color::from_rgbaf32_unchecked(0.8, 0.3, 0.6, 1.);
let b = Color::from_rgbaf32_unchecked(0.2, 0.7, 0.4, 1.);
let overlay = apply_blend_mode(a, b, BlendMode::Overlay);
let swapped_hard_light = apply_blend_mode(b, a, BlendMode::HardLight);
assert_eq!(overlay, swapped_hard_light);
}
}
@@ -750,6 +750,11 @@ impl Color {
} }
} }
/// Per-channel "Overlay" blend: hard light with its operands swapped.
pub fn blend_overlay(c_b: f32, c_s: f32) -> f32 {
Self::blend_hardlight(c_s, c_b)
}
/// Per-channel "Hard Light" blend. /// Per-channel "Hard Light" blend.
pub fn blend_hardlight(c_b: f32, c_s: f32) -> f32 { pub fn blend_hardlight(c_b: f32, c_s: f32) -> f32 {
if c_s <= 0.5 { if c_s <= 0.5 {
@@ -1,4 +1,6 @@
use crate::renderer::{ClearGuardPlacement, RenderParams, format_transform_matrix, gradient_placement, spread_adjusted_samples, transform_is_invertible}; use crate::renderer::{
ClearGuardPlacement, RenderParams, composite_paint_colors, format_transform_matrix, gradient_placement, paint_faded_samples, paint_lane_opacity, spread_adjusted_samples, transform_is_invertible,
};
use crate::{Render, RenderSvgSegmentList, SvgRender}; use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::Color; use core_types::Color;
use core_types::attribute::Transform; use core_types::attribute::Transform;
@@ -52,13 +54,13 @@ pub trait RenderExt {
) -> Self::Output; ) -> Self::Output;
} }
/// The color paint attribute over any color lane source. /// The color paint attribute for a composited paint color.
pub fn render_color_paint<S: core_types::lane::LaneSource<Element = Color>>(source: &S, target: PaintTarget) -> String { pub fn render_color_paint(color: Option<Color>, target: PaintTarget) -> String {
let Some(color) = source.element(0) else { let Some(color) = color else {
return format!(r#" {}="none""#, target.paint_attr()); return format!(r#" {}="none""#, target.paint_attr());
}; };
let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(*color).to_rgb_hex()); let mut result = format!(r##" {}="#{}""##, target.paint_attr(), SRGBA8::from(color).to_rgb_hex());
if color.a() < 1. { if color.a() < 1. {
let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.); let _ = write!(result, r#" {}="{}""#, target.opacity_attr(), (color.a() * 1000.).round() / 1000.);
} }
@@ -76,10 +78,10 @@ impl RenderExt for List<Color> {
_element_transform: DAffine2, _element_transform: DAffine2,
_stroke_transform: DAffine2, _stroke_transform: DAffine2,
_bounds: DAffine2, _bounds: DAffine2,
_render_params: &RenderParams, render_params: &RenderParams,
target: PaintTarget, target: PaintTarget,
) -> Self::Output { ) -> Self::Output {
render_color_paint(self, target) render_color_paint(composite_paint_colors(self, |color| Some(*color), render_params.for_mask), target)
} }
} }
@@ -94,16 +96,16 @@ impl RenderExt for List<Gradient> {
element_transform: DAffine2, element_transform: DAffine2,
_stroke_transform: DAffine2, _stroke_transform: DAffine2,
_bounds: DAffine2, _bounds: DAffine2,
_render_params: &RenderParams, render_params: &RenderParams,
_target: PaintTarget, _target: PaintTarget,
) -> Self::Output { ) -> Self::Output {
render_gradient_paint(self, svg_defs, item_transform, element_transform) render_gradient_paint(self, svg_defs, item_transform, element_transform, render_params.for_mask)
} }
} }
/// Adds the gradient def through mutating `svg_defs`, returning the gradient /// Adds the gradient def through mutating `svg_defs`, returning the gradient
/// ID, over any gradient lane source. /// ID, over any gradient lane source.
pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>>(source: &S, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2) -> u64 { pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>>(source: &S, svg_defs: &mut String, item_transform: DAffine2, element_transform: DAffine2, for_mask: bool) -> u64 {
let mut stop = String::new(); let mut stop = String::new();
{ {
@@ -113,6 +115,7 @@ pub fn render_gradient_paint<S: core_types::lane::LaneSource<Element = Gradient>
let settings = GradientSettings::from_lane_attributes(source, 0); let settings = GradientSettings::from_lane_attributes(source, 0);
let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder); let (samples, _) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
let samples = paint_faded_samples(samples, paint_lane_opacity(source, 0, for_mask));
for (position, color, original_midpoint) in samples { for (position, color, original_midpoint) in samples {
stop.push_str("<stop"); stop.push_str("<stop");
@@ -209,7 +212,7 @@ impl RenderExt for Stroke {
let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join); let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join);
let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit); let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit);
let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align); let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align);
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow); let paint_order = render_params.stroke_below.then_some(PaintOrder::StrokeBelow);
// Render the needed stroke attributes // Render the needed stroke attributes
let mut attributes = String::new(); let mut attributes = String::new();
@@ -258,9 +261,13 @@ impl RenderExt for List<Graphic<'_>> {
let paint_attr = target.paint_attr(); let paint_attr = target.paint_attr();
match fill_graphic { match fill_graphic {
Some(Graphic::Color(color)) => render_color_paint(&core_types::lane::LeafLane::new(self, 0, color), target), Some(Graphic::Color(_)) => {
// The whole color stack collapses to the one composited color the fast path emits
let composited = composite_paint_colors(self, |graphic| if let Graphic::Color(color) = graphic { Some(*color) } else { None }, render_params.for_mask);
render_color_paint(composited, target)
}
Some(Graphic::Gradient(gradient)) => { Some(Graphic::Gradient(gradient)) => {
let gradient_id = render_gradient_paint(&core_types::lane::LeafLane::new(self, 0, gradient), svg_defs, item_transform, element_transform); let gradient_id = render_gradient_paint(&core_types::lane::LeafLane::new(self, 0, gradient), svg_defs, item_transform, element_transform, render_params.for_mask);
format!(r##" {paint_attr}="url(#{gradient_id})""##) format!(r##" {paint_attr}="url(#{gradient_id})""##)
} }
Some(Graphic::None) => format!(r#" {paint_attr}="none""#), Some(Graphic::None) => format!(r#" {paint_attr}="none""#),
+319 -137
View File
@@ -23,17 +23,16 @@ 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::{ use graphic_types::appearance::{Appearance, Coverage};
PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path, vector_lane_can_reduce_to_clip_path, use graphic_types::graphic::{PaintColumns, PaintReach, is_paint_present, paint_cell_rows, vector_can_reduce_to_clip_path, vector_lane_can_reduce_to_clip_path};
}; use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientSettings}; use graphic_types::vector_types::gradient::{Gradient, GradientForm, GradientSettings};
use graphic_types::vector_types::markers::GradientForm as GradientFormAttr; use graphic_types::vector_types::markers::GradientForm as GradientFormAttr;
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::{PaintOrder, RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{ATTR_FILL, 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;
use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::instance::{LocationRef, NormalizedCoord, Size};
@@ -209,6 +208,98 @@ pub struct RenderContext {
pub resource_overrides: Vec<(peniko::ImageBrush, Texture)>, pub resource_overrides: Vec<(peniko::ImageBrush, Texture)>,
} }
/// The single black-fill appearance a mask clone paints with, at full alpha so the mask fully covers the interior.
fn black_fill_appearance() -> Appearance {
Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(List::new_from_element(Graphic::Color(Color::BLACK))))
}
/// The alpha multiplier a paint row's opacity attributes apply when it serves as a paint.
/// Fill opacity fades a paint just as opacity does, but a masker drops it so it cannot reach the content clipped to it.
pub(crate) fn paint_row_opacity<T>(list: &List<T>, index: usize, for_mask: bool) -> f32 {
let opacity_fill = if for_mask {
1.
} else {
list.attribute_cloned_or::<f64>(core_types::ATTR_OPACITY_FILL, index, 1.)
};
(list.attribute_cloned_or::<f64>(core_types::ATTR_OPACITY, index, 1.) * opacity_fill) as f32
}
/// [`paint_row_opacity`] over a lane source, for the gradient paths that hold one.
pub(crate) fn paint_lane_opacity<S: LaneSource>(source: &S, index: usize, for_mask: bool) -> f32 {
let opacity_fill: f64 = if for_mask { 1. } else { source.attr::<OpacityFill>(index) };
(source.attr::<Opacity>(index) * opacity_fill) as f32
}
/// Fades a gradient's renderer samples by the paint row's own opacity, applied after
/// interpolation so the ramp's interpolation space is the one the stops were sampled in.
/// Transparent `Clear` guards are unaffected, their alpha already being zero.
pub(crate) fn paint_faded_samples(samples: GradientSamples, paint_opacity: f32) -> GradientSamples {
if paint_opacity >= 1. {
return samples;
}
samples
.into_iter()
.map(|(position, color, midpoint)| (position, color.with_alpha(color.a() * paint_opacity), midpoint))
.collect()
}
/// Composites one paint color over the stack beneath it, mixing by the blend mode and then source-over in straight alpha.
fn composite_paint_over(over: Color, under: Color, blend_mode: BlendMode) -> Color {
let (over_alpha, under_alpha) = (over.a(), under.a());
// These modes only move the backdrop's alpha, leaving its color alone
match blend_mode {
BlendMode::Erase => return under.with_alpha((under_alpha - over_alpha).clamp(0., 1.)),
BlendMode::Restore => return under.with_alpha((under_alpha + over_alpha).clamp(0., 1.)),
BlendMode::MultiplyAlpha => return under.with_alpha(under_alpha * over_alpha),
_ => {}
}
let result_alpha = over_alpha + under_alpha * (1. - over_alpha);
if result_alpha <= 0. {
return Color::TRANSPARENT;
}
// The blend formulas read their backdrop premultiplied
let premultiplied_under = Color::from_rgbaf32_unchecked(under.r() * under_alpha, under.g() * under_alpha, under.b() * under_alpha, under_alpha);
let mixed = core_types::blending::apply_blend_mode(over, premultiplied_under, blend_mode);
// The mode only mixes where the backdrop has coverage, so its alpha interpolates each source channel from the raw color to the mixed color
let source_channel = |over_channel: f32, mixed_channel: f32| over_channel * (1. - under_alpha) + mixed_channel * under_alpha;
let channel =
|mixed_channel: f32, over_channel: f32, under_channel: f32| (source_channel(over_channel, mixed_channel) * over_alpha + under_channel * under_alpha * (1. - over_alpha)) / result_alpha;
Color::from_rgbaf32_unchecked(
channel(mixed.r(), over.r(), under.r()),
channel(mixed.g(), over.g(), under.g()),
channel(mixed.b(), over.b(), under.b()),
result_alpha,
)
}
/// Flattens a color paint into the single color the fast path emits, stacking the rows in paint order.
/// `element_color` reads a row's color, `None` skipping rows of another element type.
pub(crate) fn composite_paint_colors<T>(list: &List<T>, element_color: impl Fn(&T) -> Option<Color>, for_mask: bool) -> Option<Color> {
let mut composited = None;
for index in 0..list.len() {
let Some(color) = list.element(index).and_then(&element_color) else { continue };
let faded = color.with_alpha(color.a() * paint_row_opacity(list, index, for_mask));
composited = Some(match composited {
// The lowest paint has nothing beneath it, so its blend mode has nothing to act on
None => faded,
Some(under) => composite_paint_over(faded, under, list.attribute_cloned_or::<BlendMode>(core_types::ATTR_BLEND_MODE, index, BlendMode::default())),
});
}
composited
}
#[derive(Default, Clone, Copy, Hash, graphene_hash::CacheHash)] #[derive(Default, Clone, Copy, Hash, graphene_hash::CacheHash)]
pub enum RenderOutputType { pub enum RenderOutputType {
#[default] #[default]
@@ -232,7 +323,7 @@ pub struct RenderParams {
/// Are we generating a mask for alignment? Used to prevent unnecessary transforms in masks /// Are we generating a mask for alignment? Used to prevent unnecessary transforms in masks
pub alignment_parent_transform: Option<DAffine2>, pub alignment_parent_transform: Option<DAffine2>,
pub aligned_strokes: bool, pub aligned_strokes: bool,
pub override_paint_order: bool, pub stroke_below: bool,
/// Are we rendering for a pattern content /// Are we rendering for a pattern content
pub inside_pattern: bool, pub inside_pattern: bool,
pub artboard_background: Option<Color>, pub artboard_background: Option<Color>,
@@ -504,7 +595,7 @@ fn peniko_extend(gradient_spread: GradientSpread) -> peniko::Extend {
} }
} }
fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2) -> Option<(peniko::Brush, DAffine2)> { fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list: &S, multiplied_transform: &DAffine2, for_mask: bool) -> Option<(peniko::Brush, DAffine2)> {
let stops = gradient_list.element(0)?; let stops = gradient_list.element(0)?;
let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0); let gradient_form: GradientForm = gradient_list.attr::<GradientFormAttr>(0);
@@ -512,6 +603,7 @@ fn create_peniko_gradient_brush<S: LaneSource<Element = Gradient>>(gradient_list
let settings = GradientSettings::from_lane_attributes(gradient_list, 0); let settings = GradientSettings::from_lane_attributes(gradient_list, 0);
let (samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels); let (samples, span) = spread_adjusted_samples(stops, settings, gradient_form, ClearGuardPlacement::VelloRampTexels);
let samples = paint_faded_samples(samples, paint_lane_opacity(gradient_list, 0, for_mask));
let peniko_stops = peniko_color_stops(&samples); let peniko_stops = peniko_color_stops(&samples);
@@ -560,12 +652,9 @@ 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 it. /// Per-layer resolved appearance snapshot, exposed so message handlers can read the paint.
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>, pub appearance_attributes: HashMap<NodeId, Arc<Appearance>>,
/// Per-layer `ATTR_STROKE` row attribute, exposed so message handlers can read it.
#[cfg_attr(feature = "serde", serde(skip))]
pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
pub backgrounds: Vec<Background>, pub backgrounds: Vec<Background>,
} }
@@ -589,8 +678,7 @@ impl RenderMetadata {
text_frames, text_frames,
clip_targets, clip_targets,
vector_data, vector_data,
fill_attributes, appearance_attributes,
stroke_attributes,
backgrounds, backgrounds,
} = self; } = self;
upstream_footprints.extend(other.upstream_footprints.iter()); upstream_footprints.extend(other.upstream_footprints.iter());
@@ -601,8 +689,7 @@ impl RenderMetadata {
text_frames.extend(other.text_frames.iter()); text_frames.extend(other.text_frames.iter());
clip_targets.extend(other.clip_targets.iter()); clip_targets.extend(other.clip_targets.iter());
vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone()))); vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone())));
fill_attributes.extend(other.fill_attributes.iter().map(|(id, data)| (*id, data.clone()))); appearance_attributes.extend(other.appearance_attributes.iter().map(|(id, data)| (*id, data.clone())));
stroke_attributes.extend(other.stroke_attributes.iter().map(|(id, data)| (*id, data.clone())));
// TODO: Find a better non O(n^2) way to merge backgrounds // TODO: Find a better non O(n^2) way to merge backgrounds
for background in &other.backgrounds { for background in &other.backgrounds {
@@ -651,7 +738,7 @@ impl Render for Graphic<'_> {
match self { match self {
Graphic::None => (), Graphic::None => (),
Graphic::Graphic(list) => list.render_svg(render, render_params), Graphic::Graphic(list) => list.render_svg(render, render_params),
Graphic::Vector(vector) => render_vector_svg(&Single(vector), render, render_params), Graphic::Vector(vector) => render_vector_svg(&Single(vector), None, render, render_params),
Graphic::RasterCPU(raster) => render_raster_cpu_svg(&Single(raster), render, render_params), Graphic::RasterCPU(raster) => render_raster_cpu_svg(&Single(raster), render, render_params),
Graphic::RasterGPU(_) => (), Graphic::RasterGPU(_) => (),
Graphic::Color(color) => render_color_svg(&Single(color), render, render_params), Graphic::Color(color) => render_color_svg(&Single(color), render, render_params),
@@ -665,7 +752,7 @@ impl Render for Graphic<'_> {
match self { match self {
Graphic::None => (), Graphic::None => (),
Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params), Graphic::Graphic(list) => list.render_to_vello(scene, transform, context, render_params),
Graphic::Vector(vector) => render_vector_vello(&Single(vector), scene, transform, context, render_params), Graphic::Vector(vector) => render_vector_vello(&Single(vector), None, scene, transform, context, render_params),
Graphic::RasterCPU(raster) => render_raster_cpu_vello(&Single(raster), scene, transform, render_params), Graphic::RasterCPU(raster) => render_raster_cpu_vello(&Single(raster), scene, transform, render_params),
Graphic::RasterGPU(raster) => render_raster_gpu_vello(&Single(raster), scene, transform, context, render_params), Graphic::RasterGPU(raster) => render_raster_gpu_vello(&Single(raster), scene, transform, context, render_params),
Graphic::Color(color) => render_color_vello(&Single(color), scene, render_params), Graphic::Color(color) => render_color_vello(&Single(color), scene, render_params),
@@ -707,8 +794,8 @@ impl Render for Graphic<'_> {
fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
match element { match element {
Graphic::Vector(vector) if reach.applies() => render_vector_svg(&PaintOverlay::new(&Single(vector), reach.paint), render, render_params), Graphic::Vector(vector) => render_vector_svg(&Single(vector), reach.appearance, render, render_params),
Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach.nested(), render, render_params), Graphic::Graphic(inner) => render_graphic_svg_with(inner, reach, render, render_params),
Graphic::Group(group) => render_group_svg(group, reach, render, render_params), Graphic::Group(group) => render_group_svg(group, reach, render, render_params),
_ => element.render_svg(render, render_params), _ => element.render_svg(render, render_params),
} }
@@ -716,8 +803,8 @@ fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &
fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match element { match element {
Graphic::Vector(vector) if reach.applies() => render_vector_vello(&PaintOverlay::new(&Single(vector), reach.paint), scene, transform, context, render_params), Graphic::Vector(vector) => render_vector_vello(&Single(vector), reach.appearance, scene, transform, context, render_params),
Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach.nested(), scene, transform, context, render_params), Graphic::Graphic(inner) => render_graphic_vello_with(inner, reach, scene, transform, context, render_params),
Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params), Graphic::Group(group) => render_group_vello(group, reach, scene, transform, context, render_params),
_ => element.render_to_vello(scene, transform, context, render_params), _ => element.render_to_vello(scene, transform, context, render_params),
} }
@@ -725,13 +812,12 @@ fn render_element_vello<'a>(element: &'a Graphic, reach: PaintReach<'a>, scene:
fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool { fn element_can_reduce_to_clip_path<'a>(element: &'a Graphic, reach: PaintReach<'a>) -> bool {
match element { match element {
Graphic::Vector(vector) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&Single(vector), reach.paint)), Graphic::Vector(vector) => vector_can_reduce_to_clip_path(&Single(vector), reach.appearance),
Graphic::Group(group) => match RunView::<Vector>::new(&group.content) { Graphic::Group(group) => match RunView::<Vector>::new(&group.content) {
Some(run) if reach.applies() => vector_can_reduce_to_clip_path(&PaintOverlay::new(&run, reach.paint)), Some(run) => vector_can_reduce_to_clip_path(&run, reach.appearance),
Some(run) => vector_can_reduce_to_clip_path(&run),
None => false, None => false,
}, },
_ => element.can_reduce_to_clip_path(), _ => element.can_reduce_to_clip_path(reach.appearance),
} }
} }
@@ -762,9 +848,8 @@ fn collect_element_metadata<'a>(
match element { match element {
Graphic::None => {} Graphic::None => {}
Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach.nested(), metadata, footprint, element_id), Graphic::Graphic(list) => collect_graphic_metadata_with(list, reach, metadata, footprint, element_id),
Graphic::Vector(vector) if reach.applies() => collect_vector_metadata(&PaintOverlay::new(&Single(vector), reach.paint), metadata, footprint, element_id), Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), reach.appearance, metadata, footprint, element_id),
Graphic::Vector(vector) => collect_vector_metadata(&Single(vector), metadata, footprint, element_id),
Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), Graphic::RasterCPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id), Graphic::RasterGPU(raster) => collect_raster_metadata(&Single(raster), metadata, footprint, element_id),
Graphic::Color(_) => {} Graphic::Color(_) => {}
@@ -806,9 +891,8 @@ fn collect_group_row_metadata(group: &Group, metadata: &mut RenderMetadata, elem
fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) { fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
match element { match element {
Graphic::None => (), Graphic::None => (),
Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach.nested(), click_targets), Graphic::Graphic(list) => add_graphic_upstream_click_targets_with(list, reach, click_targets),
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_click_targets(&PaintOverlay::new(&Single(vector), reach.paint), click_targets), Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), reach.appearance, click_targets),
Graphic::Vector(vector) => add_vector_upstream_click_targets(&Single(vector), click_targets),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(click_targets),
Graphic::Color(_) => {} Graphic::Color(_) => {}
Graphic::Gradient(gradient) => click_targets.extend(gradient_control_targets(&Single(gradient), |transform| transform, true)), Graphic::Gradient(gradient) => click_targets.extend(gradient_control_targets(&Single(gradient), |transform| transform, true)),
@@ -820,9 +904,8 @@ fn add_element_upstream_click_targets<'a>(element: &'a Graphic, reach: PaintReac
fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) { fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
match element { match element {
Graphic::None => (), Graphic::None => (),
Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach.nested(), outlines), Graphic::Graphic(list) => add_graphic_upstream_outline_targets_with(list, reach, outlines),
Graphic::Vector(vector) if reach.applies() => add_vector_upstream_outline_targets(&PaintOverlay::new(&Single(vector), reach.paint), outlines), Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), reach.appearance, outlines),
Graphic::Vector(vector) => add_vector_upstream_outline_targets(&Single(vector), outlines),
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines), Graphic::RasterCPU(_) | Graphic::RasterGPU(_) => add_raster_upstream_click_targets(outlines),
Graphic::Color(_) => {} Graphic::Color(_) => {}
Graphic::Gradient(gradient) => outlines.extend(gradient_control_targets(&Single(gradient), |transform| transform, false)), Graphic::Gradient(gradient) => outlines.extend(gradient_control_targets(&Single(gradient), |transform| transform, false)),
@@ -836,12 +919,9 @@ fn add_element_upstream_outline_targets<'a>(element: &'a Graphic, reach: PaintRe
fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) { fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
let item = &group.content; let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) { if let Some(run) = RunView::<Graphic>::new(item) {
render_graphic_svg_with(&run, reach.into_group_graphics(), render, render_params) render_graphic_svg_with(&run, reach, render, render_params)
} else if let Some(run) = RunView::<Vector>::new(item) { } else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() { render_vector_svg(&run, reach.appearance, render, render_params)
true => render_vector_svg(&PaintOverlay::new(&run, reach.paint), render, render_params),
false => render_vector_svg(&run, render, render_params),
}
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) { } else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
render_raster_cpu_svg(&run, render, render_params) render_raster_cpu_svg(&run, render, render_params)
} else if item.typed_lanes::<Raster<GPU>>().is_some() { } else if item.typed_lanes::<Raster<GPU>>().is_some() {
@@ -857,12 +937,9 @@ fn render_group_svg<'a>(group: &'a Group, reach: PaintReach<'a>, render: &mut Sv
fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
let item = &group.content; let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) { if let Some(run) = RunView::<Graphic>::new(item) {
render_graphic_vello_with(&run, reach.into_group_graphics(), scene, transform, context, render_params) render_graphic_vello_with(&run, reach, scene, transform, context, render_params)
} else if let Some(run) = RunView::<Vector>::new(item) { } else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() { render_vector_vello(&run, reach.appearance, scene, transform, context, render_params)
true => render_vector_vello(&PaintOverlay::new(&run, reach.paint), scene, transform, context, render_params),
false => render_vector_vello(&run, scene, transform, context, render_params),
}
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) { } else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
render_raster_cpu_vello(&run, scene, transform, render_params) render_raster_cpu_vello(&run, scene, transform, render_params)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) { } else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
@@ -882,12 +959,9 @@ fn render_group_vello<'a>(group: &'a Group, reach: PaintReach<'a>, scene: &mut S
fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let item = &group.content; let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) { if let Some(run) = RunView::<Graphic>::new(item) {
collect_graphic_metadata_with(&run, reach.into_group_graphics(), metadata, footprint, element_id) collect_graphic_metadata_with(&run, reach, metadata, footprint, element_id)
} else if let Some(run) = RunView::<Vector>::new(item) { } else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() { collect_vector_metadata(&run, reach.appearance, metadata, footprint, element_id)
true => collect_vector_metadata(&PaintOverlay::new(&run, reach.paint), metadata, footprint, element_id),
false => collect_vector_metadata(&run, metadata, footprint, element_id),
}
} else if let Some(run) = RunView::<Raster<CPU>>::new(item) { } else if let Some(run) = RunView::<Raster<CPU>>::new(item) {
collect_raster_metadata(&run, metadata, footprint, element_id) collect_raster_metadata(&run, metadata, footprint, element_id)
} else if let Some(run) = RunView::<Raster<GPU>>::new(item) { } else if let Some(run) = RunView::<Raster<GPU>>::new(item) {
@@ -903,12 +977,9 @@ fn collect_group_metadata<'a>(group: &'a Group, reach: PaintReach<'a>, metadata:
fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) { fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>, click_targets: &mut Vec<ClickTarget>) {
let item = &group.content; let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) { if let Some(run) = RunView::<Graphic>::new(item) {
add_graphic_upstream_click_targets_with(&run, reach.into_group_graphics(), click_targets) add_graphic_upstream_click_targets_with(&run, reach, click_targets)
} else if let Some(run) = RunView::<Vector>::new(item) { } else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() { add_vector_upstream_click_targets(&run, reach.appearance, click_targets)
true => add_vector_upstream_click_targets(&PaintOverlay::new(&run, reach.paint), click_targets),
false => add_vector_upstream_click_targets(&run, click_targets),
}
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() { } else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(click_targets) add_raster_upstream_click_targets(click_targets)
} else if let Some(run) = RunView::<Gradient>::new(item) { } else if let Some(run) = RunView::<Gradient>::new(item) {
@@ -921,12 +992,9 @@ fn add_group_upstream_click_targets<'a>(group: &'a Group, reach: PaintReach<'a>,
fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) { fn add_group_upstream_outline_targets<'a>(group: &'a Group, reach: PaintReach<'a>, outlines: &mut Vec<ClickTarget>) {
let item = &group.content; let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) { if let Some(run) = RunView::<Graphic>::new(item) {
add_graphic_upstream_outline_targets_with(&run, reach.into_group_graphics(), outlines) add_graphic_upstream_outline_targets_with(&run, reach, outlines)
} else if let Some(run) = RunView::<Vector>::new(item) { } else if let Some(run) = RunView::<Vector>::new(item) {
match reach.applies() { add_vector_upstream_outline_targets(&run, reach.appearance, outlines)
true => add_vector_upstream_outline_targets(&PaintOverlay::new(&run, reach.paint), outlines),
false => add_vector_upstream_outline_targets(&run, outlines),
}
} else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() { } else if item.typed_lanes::<Raster<CPU>>().is_some() || item.typed_lanes::<Raster<GPU>>().is_some() {
add_raster_upstream_click_targets(outlines) add_raster_upstream_click_targets(outlines)
} else if let Some(run) = RunView::<Gradient>::new(item) { } else if let Some(run) = RunView::<Gradient>::new(item) {
@@ -1397,14 +1465,19 @@ impl Render for List<Graphic<'_>> {
} }
/// Emits one lane of a vector source as SVG, with no wrapping group of its own. /// Emits one lane of a vector source as SVG, with no wrapping group of its own.
fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: usize, vector: &Vector, render: &mut SvgRender, render_params: &RenderParams) { fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: usize, vector: &Vector, inherited_appearance: Option<&Appearance>, render: &mut SvgRender, render_params: &RenderParams) {
let item_transform: DAffine2 = source.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
// The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
let element_stroke = resolved.stroke.as_ref();
// 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.stroke.as_ref().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = element_stroke.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);
@@ -1412,7 +1485,7 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
let layer_bounds = vector.bounding_box().unwrap_or_default(); let layer_bounds = vector.bounding_box().unwrap_or_default();
let transformed_bounds = vector.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default(); let transformed_bounds = vector.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY).unwrap_or(layer_bounds); let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY, element_stroke).unwrap_or(layer_bounds);
let bounds_matrix = DAffine2::from_scale_angle_translation(layer_bounds[1] - layer_bounds[0], 0., layer_bounds[0]); let bounds_matrix = DAffine2::from_scale_angle_translation(layer_bounds[1] - layer_bounds[0], 0., layer_bounds[0]);
let stroke_bounds_matrix = DAffine2::from_scale_angle_translation(stroke_layer_bounds[1] - stroke_layer_bounds[0], 0., stroke_layer_bounds[0]); let stroke_bounds_matrix = DAffine2::from_scale_angle_translation(stroke_layer_bounds[1] - stroke_layer_bounds[0], 0., stroke_layer_bounds[0]);
@@ -1424,26 +1497,27 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
path.push_str(bezpath.to_svg().as_str()); path.push_str(bezpath.to_svg().as_str());
} }
let mask_type = if vector.stroke.as_ref().map(|x| x.align) == Some(StrokeAlign::Inside) { let mask_type = if element_stroke.map(|x| x.align) == Some(StrokeAlign::Inside) {
MaskType::Clip MaskType::Clip
} else { } else {
MaskType::Mask MaskType::Mask
}; };
let fill_graphic_list = paint_graphics::<Fill, _>(source, index); let fill_graphic_list = resolved.fill_paint.and_then(paint_cell_rows);
let fill_graphic = fill_graphic_list.and_then(|l| l.element(0)); let fill_graphic = fill_graphic_list.and_then(|l| l.element(0));
let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index); let stroke_graphic_list = resolved.stroke_paint.and_then(paint_cell_rows);
let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0)); let stroke_graphic = stroke_graphic_list.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.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && element_stroke.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.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); // The paint order rides the coverage list's row order
let wants_stroke_below = resolved.stroke_below;
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();
@@ -1463,13 +1537,12 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
let push_id = needs_separate_alignment_fill.then_some({ let push_id = needs_separate_alignment_fill.then_some({
let id = format!("alignment-{}", generate_uuid()); let id = format!("alignment-{}", generate_uuid());
let mut cloned_vector = vector.clone(); let cloned_vector = vector.clone();
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.
let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform); let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); mask_item.set_attribute(graphic_types::ATTR_APPEARANCE, black_fill_appearance());
let vector_item = List::new_from_item(mask_item); let vector_item = List::new_from_item(mask_item);
(id, mask_type, vector_item) (id, mask_type, vector_item)
@@ -1504,7 +1577,7 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
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.stroke.as_ref().unwrap(); let stroke = element_stroke.expect("push_id is only set when an aligned stroke can draw");
// `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;
@@ -1529,11 +1602,9 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
let mut render_params = render_params.clone(); let mut render_params = render_params.clone();
render_params.aligned_strokes = can_draw_aligned_stroke; render_params.aligned_strokes = can_draw_aligned_stroke;
render_params.override_paint_order = override_paint_order; render_params.stroke_below = override_paint_order || wants_stroke_below;
let stroke_shape_attribute = vector let stroke_shape_attribute = element_stroke
.stroke
.as_ref()
.map(|stroke| { .map(|stroke| {
if stroke_graphic_list.is_some_and(is_paint_present) { if stroke_graphic_list.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)
@@ -1544,7 +1615,7 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
.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.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent()); let stroke_visible = element_stroke.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
.map(|list| { .map(|list| {
@@ -1606,7 +1677,7 @@ fn render_vector_item_svg<S: LaneSource<Element = Vector>>(source: &S, index: us
} }
} }
fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut SvgRender, render_params: &RenderParams) { fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>, render: &mut SvgRender, render_params: &RenderParams) {
let mut clip_mask_state: Option<(u64, MaskType)> = None; let mut clip_mask_state: Option<(u64, MaskType)> = None;
for index in 0..source.lane_count() { for index in 0..source.lane_count() {
@@ -1617,11 +1688,15 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
let mut masked_by = None; let mut masked_by = None;
if next_clips && clip_mask_state.is_none() { if next_clips && clip_mask_state.is_none() {
let mask_type = if vector_lane_can_reduce_to_clip_path(source, index) { MaskType::Clip } else { MaskType::Mask }; let mask_type = if vector_lane_can_reduce_to_clip_path(source, index, inherited_appearance) {
MaskType::Clip
} else {
MaskType::Mask
};
let uuid = generate_uuid(); let uuid = generate_uuid();
let mut masker_svg = SvgRender::new(); let mut masker_svg = SvgRender::new();
render_vector_item_svg(source, index, vector, &mut masker_svg, &render_params.for_clipper()); render_vector_item_svg(source, index, vector, inherited_appearance, &mut masker_svg, &render_params.for_clipper());
render.svg_defs.push_str(&masker_svg.svg_defs); render.svg_defs.push_str(&masker_svg.svg_defs);
mask_type.write_to_defs(&mut render.svg_defs, uuid, masker_svg.svg.to_svg_string()); mask_type.write_to_defs(&mut render.svg_defs, uuid, masker_svg.svg.to_svg_string());
@@ -1639,9 +1714,9 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
Some((attribute, selector)) => render.parent_tag( Some((attribute, selector)) => render.parent_tag(
"g", "g",
|attributes| attributes.push(attribute, selector), |attributes| attributes.push(attribute, selector),
|render| render_vector_item_svg(source, index, vector, render, render_params), |render| render_vector_item_svg(source, index, vector, inherited_appearance, render, render_params),
), ),
None => render_vector_item_svg(source, index, vector, render, render_params), None => render_vector_item_svg(source, index, vector, inherited_appearance, render, render_params),
} }
} }
} }
@@ -1649,24 +1724,31 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, render: &mut S
/// Emits one lane of a vector source to Vello. `clip_masker` names the lane whose /// Emits one lane of a vector source to Vello. `clip_masker` names the lane whose
/// paint masks this one; its layers are pushed inside this item's blend layer so the /// paint masks this one; its layers are pushed inside this item's blend layer so the
/// mask cuts this item's own paint rather than the composited result. /// mask cuts this item's own paint rather than the composited result.
#[expect(clippy::too_many_arguments, reason = "one lane's full render context: source and index, the cascade, the vello sink, and the masking lane")]
fn render_vector_item_vello<S: LaneSource<Element = Vector>>( fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
source: &S, source: &S,
index: usize, index: usize,
inherited_appearance: Option<&Appearance>,
scene: &mut Scene, scene: &mut Scene,
parent_transform: DAffine2, parent_transform: DAffine2,
context: &mut RenderContext, context: &mut RenderContext,
render_params: &RenderParams, render_params: &RenderParams,
clip_masker: Option<usize>, clip_masker: Option<usize>,
) { ) {
use graphic_types::vector_types::vector;
let Some(element) = source.element(index) else { return }; let Some(element) = source.element(index) else { return };
let item_transform: DAffine2 = source.attr::<Transform>(index); let item_transform: DAffine2 = source.attr::<Transform>(index);
let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index); let blend_mode_attr: BlendMode = source.attr::<BlendModeAttr>(index);
let opacity_attr: f64 = source.attr::<Opacity>(index); let opacity_attr: f64 = source.attr::<Opacity>(index);
let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index); let opacity_fill_attr: f64 = source.attr::<OpacityFill>(index);
let multiplied_transform = parent_transform * item_transform; let multiplied_transform = parent_transform * item_transform;
let has_real_stroke = element.stroke.as_ref().filter(|stroke| stroke.weight() > 0.);
// The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
let fill_graphic_list = resolved.fill_paint.and_then(paint_cell_rows);
let stroke_graphic_list = resolved.stroke_paint.and_then(paint_cell_rows);
let has_real_stroke = resolved.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
@@ -1690,9 +1772,6 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
} }
} }
let fill_graphic_list = paint_graphics::<Fill, _>(source, index);
let stroke_graphic_list = paint_graphics::<Stroke, _>(source, index);
// 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 {
RenderMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
@@ -1703,7 +1782,7 @@ fn render_vector_item_vello<S: LaneSource<Element = 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.stroke.as_ref(); let stroke = resolved.stroke.as_ref();
let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent())); let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
let can_draw_aligned_stroke = !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 can_draw_aligned_stroke = !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());
@@ -1736,7 +1815,7 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
&& let Some((layer_affine, layer_rect)) = layer_geometry && let Some((layer_affine, layer_rect)) = layer_geometry
{ {
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., layer_affine, &layer_rect); scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., layer_affine, &layer_rect);
render_vector_item_vello(source, masker_index, scene, parent_transform, context, &render_params.for_clipper(), None); render_vector_item_vello(source, masker_index, inherited_appearance, scene, parent_transform, context, &render_params.for_clipper(), None);
scene.push_layer( scene.push_layer(
peniko::Fill::NonZero, peniko::Fill::NonZero,
peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn), peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::SrcIn),
@@ -1748,7 +1827,8 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
} }
let use_layer = can_draw_aligned_stroke; let use_layer = can_draw_aligned_stroke;
let wants_stroke_below = stroke.is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); // The paint order rides the coverage list's row order
let wants_stroke_below = resolved.stroke_below;
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 else { return }; let Some(fill_graphic) = fill_graphic_list else { return };
@@ -1758,11 +1838,13 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
match paint { match paint {
Graphic::None => continue, Graphic::None => continue,
Graphic::Color(color) => { Graphic::Color(color) => {
let fill = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color()); // The row's own opacity fades the pass, matching the composited SVG fast path
let color = color.with_alpha(color.a() * paint_row_opacity(fill_graphic, paint_index, render_params.for_mask));
let fill = peniko::Brush::Solid(SRGBA8::from(color).to_peniko_color());
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
} }
Graphic::Gradient(gradient) => { Graphic::Gradient(gradient) => {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(fill_graphic, paint_index, gradient), &multiplied_transform) else { let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(fill_graphic, paint_index, gradient), &multiplied_transform, render_params.for_mask) else {
continue; continue;
}; };
@@ -1839,12 +1921,15 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
match stroke_graphic { match stroke_graphic {
Graphic::None => continue, Graphic::None => continue,
Graphic::Color(color) => { Graphic::Color(color) => {
let brush = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color()); // The row's own opacity fades the pass, matching the composited SVG fast path
let color = color.with_alpha(color.a() * paint_row_opacity(stroke_graphic_list, paint_index, render_params.for_mask));
let brush = peniko::Brush::Solid(SRGBA8::from(color).to_peniko_color());
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path); scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), &brush, None, &path);
} }
Graphic::Gradient(gradient) => { Graphic::Gradient(gradient) => {
let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(stroke_graphic_list, paint_index, gradient), &multiplied_transform) else { let Some((brush, gradient_to_device)) = create_peniko_gradient_brush(&LeafLane::new(stroke_graphic_list, paint_index, gradient), &multiplied_transform, render_params.for_mask)
else {
continue; continue;
}; };
let inverse_element_transform = if transform_is_invertible(element_transform) { let inverse_element_transform = if transform_is_invertible(element_transform) {
@@ -1876,13 +1961,12 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
} }
_ => { _ => {
if use_layer { if use_layer {
let mut cloned_element = element.clone(); let cloned_element = element.clone();
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.
let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform); let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform);
set_paint_attribute(mask_item.attributes_mut(), ATTR_FILL, List::new_from_element(Color::BLACK)); mask_item.set_attribute(graphic_types::ATTR_APPEARANCE, black_fill_appearance());
let vector_list = List::new_from_item(mask_item); let vector_list = List::new_from_item(mask_item);
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);
@@ -1930,7 +2014,7 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
Stroke, Stroke,
} }
let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) { let order = match stroke.is_some() && wants_stroke_below {
true => [Op::Stroke, Op::Fill], true => [Op::Stroke, Op::Fill],
false => [Op::Fill, Op::Stroke], // Default false => [Op::Fill, Op::Stroke], // Default
}; };
@@ -1956,7 +2040,14 @@ fn render_vector_item_vello<S: LaneSource<Element = Vector>>(
} }
} }
fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_vector_vello<S: LaneSource<Element = Vector>>(
source: &S,
inherited_appearance: Option<&Appearance>,
scene: &mut Scene,
parent_transform: DAffine2,
context: &mut RenderContext,
render_params: &RenderParams,
) {
let mut clip_masker: Option<usize> = None; let mut clip_masker: Option<usize> = None;
for index in 0..source.lane_count() { for index in 0..source.lane_count() {
@@ -1964,7 +2055,16 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
let next_clips = index + 1 < source.lane_count() && source.attr::<ClippingMask>(index + 1); let next_clips = index + 1 < source.lane_count() && source.attr::<ClippingMask>(index + 1);
let becomes_masker = next_clips && clip_masker.is_none(); let becomes_masker = next_clips && clip_masker.is_none();
render_vector_item_vello(source, index, scene, parent_transform, context, render_params, if becomes_masker { None } else { clip_masker }); render_vector_item_vello(
source,
index,
inherited_appearance,
scene,
parent_transform,
context,
render_params,
if becomes_masker { None } else { clip_masker },
);
if becomes_masker { if becomes_masker {
clip_masker = Some(index); clip_masker = Some(index);
@@ -1974,7 +2074,13 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(source: &S, scene: &mut
} }
} }
fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) { fn collect_vector_metadata<S: LaneSource<Element = Vector>>(
source: &S,
inherited_appearance: Option<&Appearance>,
metadata: &mut RenderMetadata,
footprint: Footprint,
caller_element_id: Option<NodeId>,
) {
// Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph. // Aggregate all items' targets per element_id so multi-item lists (e.g. the "Text to Vector Glyphs" node) produce hit areas for every glyph.
// Targets are baked relative to the first item carrying each element_id, since that is the transform recorded as its `local_transforms` entry. // Targets are baked relative to the first item carrying each element_id, since that is the transform recorded as its `local_transforms` entry.
let mut reference_transforms: HashMap<NodeId, DAffine2> = HashMap::new(); let mut reference_transforms: HashMap<NodeId, DAffine2> = HashMap::new();
@@ -1988,6 +2094,10 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index); let layer_path: &[NodeId] = source.attr::<EditorLayerPath>(index);
let layer = layer_path.last().copied(); let layer = layer_path.last().copied();
// The lane's paint: its own declared appearance, or the nearest ancestor's through the cascade
let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
if let Some(element_id) = caller_element_id.or(layer) { if let Some(element_id) = caller_element_id.or(layer) {
let reference_transform = *reference_transforms.entry(element_id).or_insert(transform); let reference_transform = *reference_transforms.entry(element_id).or_insert(transform);
let reference_inverse = if transform_is_invertible(reference_transform) { let reference_inverse = if transform_is_invertible(reference_transform) {
@@ -2002,12 +2112,12 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
let item_relative_transform = reference_inverse * transform; let item_relative_transform = reference_inverse * transform;
let mut click_targets_unwrapped = Vec::new(); let mut click_targets_unwrapped = Vec::new();
extend_targets_from_vector(&mut click_targets_unwrapped, source, index, click_target_vector, item_relative_transform); extend_targets_from_vector(&mut click_targets_unwrapped, &resolved, click_target_vector, item_relative_transform);
accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new)); accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new));
// Outlines always use source geometry so the visual outline reflects actual letterforms // Outlines always use source geometry so the visual outline reflects actual letterforms
let mut outlines_unwrapped = Vec::new(); let mut outlines_unwrapped = Vec::new();
extend_targets_from_vector(&mut outlines_unwrapped, source, index, element, item_relative_transform); extend_targets_from_vector(&mut outlines_unwrapped, &resolved, element, item_relative_transform);
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.
@@ -2017,11 +2127,8 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
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(element.clone())); e.insert(Arc::new(element.clone()));
if let Some(fill_graphic) = source.attr::<Fill>(index).filter(|list| is_paint_present(list)) { if let Some(appearance) = appearance {
metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.clone())); metadata.appearance_attributes.insert(element_id, Arc::new(appearance.clone()));
}
if let Some(stroke_graphic) = source.attr::<Stroke>(index).filter(|list| is_paint_present(list)) {
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone()));
} }
} }
@@ -2059,7 +2166,7 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(source: &S, metadata
} }
} }
fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S, click_targets: &mut Vec<ClickTarget>) { fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>, click_targets: &mut Vec<ClickTarget>) {
for index in 0..source.lane_count() { for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue }; let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
@@ -2067,39 +2174,45 @@ fn add_vector_upstream_click_targets<S: LaneSource<Element = Vector>>(source: &S
// Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes)
let vector = source.attr::<EditorClickTarget>(index).unwrap_or(element); let vector = source.attr::<EditorClickTarget>(index).unwrap_or(element);
extend_targets_from_vector(click_targets, source, index, vector, transform); let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
extend_targets_from_vector(click_targets, &resolved, vector, transform);
} }
} }
fn add_vector_upstream_outline_targets<S: LaneSource<Element = Vector>>(source: &S, outlines: &mut Vec<ClickTarget>) { fn add_vector_upstream_outline_targets<S: LaneSource<Element = Vector>>(source: &S, inherited_appearance: Option<&Appearance>, outlines: &mut Vec<ClickTarget>) {
// Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms // Source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms
for index in 0..source.lane_count() { for index in 0..source.lane_count() {
let Some(element) = source.element(index) else { continue }; let Some(element) = source.element(index) else { continue };
let transform: DAffine2 = source.attr::<Transform>(index); let transform: DAffine2 = source.attr::<Transform>(index);
extend_targets_from_vector(outlines, source, index, element, transform); let appearance = Appearance::cascade(source.attr::<AppearanceMarker>(index), inherited_appearance);
let resolved = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
extend_targets_from_vector(outlines, &resolved, element, transform);
} }
} }
impl Render for List<Vector> { impl Render for List<Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params) render_vector_svg(self, None, render, render_params)
} }
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_vector_vello(self, scene, parent_transform, context, render_params) render_vector_vello(self, None, scene, parent_transform, context, render_params)
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_vector_metadata(self, metadata, footprint, caller_element_id) collect_vector_metadata(self, None, metadata, footprint, caller_element_id)
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets) add_vector_upstream_click_targets(self, None, click_targets)
} }
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) { fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_vector_upstream_outline_targets(self, outlines) add_vector_upstream_outline_targets(self, None, outlines)
} }
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
@@ -2111,15 +2224,15 @@ 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<S: LaneSource<Element = Vector>>(targets: &mut Vec<ClickTarget>, source: &S, index: usize, geometry: &Vector, transform: DAffine2) { fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, resolved: &graphic_types::appearance::FillAndStroke<'_>, geometry: &Vector, transform: DAffine2) {
let filled = has_paint::<Fill, _>(source, index); let filled = resolved.fill_paint.and_then(paint_cell_rows).is_some();
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.stroke.as_ref().map_or(0., |stroke| { let stroke_width = resolved.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 {
@@ -2560,6 +2673,7 @@ fn render_gradient_svg<S: LaneSource<Element = Gradient>>(source: &S, render: &m
} }
let (samples, _) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::SvgStopOrder); let (samples, _) = spread_adjusted_samples(gradient, settings, gradient_form, ClearGuardPlacement::SvgStopOrder);
let samples = paint_faded_samples(samples, paint_lane_opacity(source, index, render_params.for_mask));
let mut stop_string = String::new(); let mut stop_string = String::new();
for (position, color, original_midpoint) in samples { for (position, color, original_midpoint) in samples {
@@ -3169,23 +3283,23 @@ impl Render for RunView<'_, Graphic<'_>> {
impl Render for RunView<'_, Vector> { impl Render for RunView<'_, Vector> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
render_vector_svg(self, render, render_params) render_vector_svg(self, None, render, render_params)
} }
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
render_vector_vello(self, scene, parent_transform, context, render_params) render_vector_vello(self, None, scene, parent_transform, context, render_params)
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, caller_element_id: Option<NodeId>) {
collect_vector_metadata(self, metadata, footprint, caller_element_id) collect_vector_metadata(self, None, metadata, footprint, caller_element_id)
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
add_vector_upstream_click_targets(self, click_targets) add_vector_upstream_click_targets(self, None, click_targets)
} }
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) { fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
add_vector_upstream_outline_targets(self, outlines) add_vector_upstream_outline_targets(self, None, outlines)
} }
} }
@@ -3336,7 +3450,6 @@ impl SvgRenderAttrs<'_> {
mod group_walk_tests { mod group_walk_tests {
use super::*; use super::*;
use core_types::record::{FieldWrite, RunBuilder, element_write_hashed}; use core_types::record::{FieldWrite, RunBuilder, element_write_hashed};
use graphic_types::markers::Fill;
use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::PointId;
fn unit_square_at(corner: DVec2) -> Vector { fn unit_square_at(corner: DVec2) -> Vector {
@@ -3347,6 +3460,11 @@ mod group_walk_tests {
List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap())) List::new_from_element(Graphic::Color(Color::from_rgbaf32(0.8, 0.2, 0.33, 1.).unwrap()))
} }
/// The appearance the fill node stamps, so test content mirrors node output.
fn fill_appearance(paint: &List<Graphic<'static>>) -> Appearance {
Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint.clone()))
}
fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) { fn rendered_svg(render: impl FnOnce(&mut SvgRender)) -> (String, String) {
let mut svg_render = SvgRender::new(); let mut svg_render = SvgRender::new();
render(&mut svg_render); render(&mut svg_render);
@@ -3359,9 +3477,10 @@ mod group_walk_tests {
let paint = color_paint(); let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))]; let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(3., 1.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 2).unwrap(); let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<AppearanceMarker>(0)], 2).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap(); let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<AppearanceMarker>(lane, Some(&appearance));
builder.push(vectors[1].clone()).unwrap(); builder.push(vectors[1].clone()).unwrap();
let item = builder.finish(); let item = builder.finish();
let group = Group { row: None, content: item }; let group = Group { row: None, content: item };
@@ -3378,9 +3497,10 @@ mod group_walk_tests {
let paint = color_paint(); let paint = color_paint();
let inner = Graphic::Vector(unit_square_at(DVec2::ZERO)); let inner = Graphic::Vector(unit_square_at(DVec2::ZERO));
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Graphic>(), &[FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(inner.clone()).unwrap(); let lane = builder.push(inner.clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let item = builder.finish(); let item = builder.finish();
let group = Group { row: None, content: item }; let group = Group { row: None, content: item };
@@ -3397,9 +3517,10 @@ mod group_walk_tests {
let paint = color_paint(); let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO)]; let vectors = [unit_square_at(DVec2::ZERO)];
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 1).unwrap(); let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<AppearanceMarker>(0)], 1).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap(); let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<AppearanceMarker>(lane, Some(&appearance));
let item = builder.finish(); let item = builder.finish();
let group = Group { row: None, content: item }; let group = Group { row: None, content: item };
@@ -3414,7 +3535,14 @@ mod group_walk_tests {
assert!(native.local_transforms.contains_key(&caller)); assert!(native.local_transforms.contains_key(&caller));
assert!(native.upstream_footprints.contains_key(&caller)); assert!(native.upstream_footprints.contains_key(&caller));
assert_eq!(native.vector_data.get(&caller).map(|vector| vector.as_ref()), Some(&vectors[0])); assert_eq!(native.vector_data.get(&caller).map(|vector| vector.as_ref()), Some(&vectors[0]));
assert!(native.fill_attributes.get(&caller).is_some_and(|fill| matches!(fill.element(0), Some(Graphic::Color(_))))); assert!(
native
.appearance_attributes
.get(&caller)
.and_then(|appearance| appearance.first_paint_of(graphic_types::appearance::Cover::Fill))
.and_then(paint_cell_rows)
.is_some_and(|fill| matches!(fill.element(0), Some(Graphic::Color(_))))
);
} }
#[test] #[test]
@@ -3422,9 +3550,10 @@ mod group_walk_tests {
let paint = color_paint(); let paint = color_paint();
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(2., 2.))]; let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(2., 2.))];
let arena = core_types::arena::Arena::new(1 << 16).unwrap(); let arena = core_types::arena::Arena::new(1 << 16).unwrap();
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<Fill>(0)], 2).unwrap(); let appearance = fill_appearance(&paint);
let mut builder = RunBuilder::new(&arena, element_write_hashed::<Vector>(), &[FieldWrite::of::<AppearanceMarker>(0)], 2).unwrap();
let lane = builder.push(vectors[0].clone()).unwrap(); let lane = builder.push(vectors[0].clone()).unwrap();
builder.attr::<Fill>(lane, Some(&paint)); builder.attr::<AppearanceMarker>(lane, Some(&appearance));
builder.push(vectors[1].clone()).unwrap(); builder.push(vectors[1].clone()).unwrap();
let item = builder.finish(); let item = builder.finish();
let group = Group { row: None, content: item }; let group = Group { row: None, content: item };
@@ -3441,6 +3570,59 @@ mod group_walk_tests {
Graphic::Group(group).add_upstream_outline_targets(&mut native_outlines); Graphic::Group(group).add_upstream_outline_targets(&mut native_outlines);
assert_eq!(native_outlines, legacy); assert_eq!(native_outlines, legacy);
} }
#[test]
fn stacked_paint_colors_composite_in_straight_alpha() {
// A half-transparent red over an opaque blue lands halfway between the two
let mut list = List::new();
list.push(Item::new_from_element(Color::from_rgbaf32_unchecked(0., 0., 1., 1.)));
list.push(Item::new_from_element(Color::from_rgbaf32_unchecked(1., 0., 0., 0.5)));
let composited = composite_paint_colors(&list, |color| Some(*color), false).expect("a non-empty paint list composites to a color");
assert!((composited.r() - 0.5).abs() < 1e-5, "red was {}", composited.r());
assert!((composited.g() - 0.).abs() < 1e-5, "green was {}", composited.g());
assert!((composited.b() - 0.5).abs() < 1e-5, "blue was {}", composited.b());
assert!((composited.a() - 1.).abs() < 1e-5, "alpha was {}", composited.a());
}
#[test]
fn stacked_paint_blending_interpolates_by_backdrop_coverage() {
// Multiply over half-covering black only half-multiplies the red
let mut list = List::new();
list.push(Item::new_from_element(Color::from_rgbaf32_unchecked(0., 0., 0., 0.5)));
list.push(Item::new_from_element(Color::from_rgbaf32_unchecked(1., 0., 0., 1.)).with_attribute(core_types::ATTR_BLEND_MODE, BlendMode::Multiply));
let composited = composite_paint_colors(&list, |color| Some(*color), false).expect("a non-empty paint list composites to a color");
assert!((composited.r() - 0.5).abs() < 1e-5, "red was {}", composited.r());
assert!((composited.a() - 1.).abs() < 1e-5, "alpha was {}", composited.a());
// Multiply over no backdrop at all leaves the source color untouched
let mut list = List::new();
list.push(Item::new_from_element(Color::TRANSPARENT));
list.push(Item::new_from_element(Color::from_rgbaf32_unchecked(1., 0., 0., 1.)).with_attribute(core_types::ATTR_BLEND_MODE, BlendMode::Multiply));
let composited = composite_paint_colors(&list, |color| Some(*color), false).expect("a non-empty paint list composites to a color");
assert!((composited.r() - 1.).abs() < 1e-5, "red was {}", composited.r());
assert!((composited.a() - 1.).abs() < 1e-5, "alpha was {}", composited.a());
}
#[test]
fn a_paint_rows_own_opacity_fades_its_color() {
let mut list = List::new();
list.push(Item::new_from_element(Color::from_rgbaf32_unchecked(1., 0., 0., 1.)));
list.set_attribute(core_types::ATTR_OPACITY, 0, 0.5_f64);
list.set_attribute(core_types::ATTR_OPACITY_FILL, 0, 0.5_f64);
let composited = composite_paint_colors(&list, |color| Some(*color), false).expect("a non-empty paint list composites to a color");
assert!((composited.a() - 0.25).abs() < 1e-5, "both opacities fade the paint, alpha was {}", composited.a());
// A masker drops the fill opacity so it cannot reach the content clipped to it
let masked = composite_paint_colors(&list, |color| Some(*color), true).expect("a non-empty paint list composites to a color");
assert!((masked.a() - 0.5).abs() < 1e-5, "the mask keeps only the plain opacity, alpha was {}", masked.a());
}
} }
#[cfg(test)] #[cfg(test)]
@@ -22,6 +22,20 @@ core_types::attribute! {
/// by clicking anywhere within their bounds, not just the filled letterform. An absent /// by clicking anywhere within their bounds, not just the filled letterform. An absent
/// value means the item's own geometry is the click target. /// value means the item's own geometry is the click target.
pub EditorClickTarget("editor:click_target"): Option<&crate::Vector>; pub EditorClickTarget("editor:click_target"): Option<&crate::Vector>;
/// Stroke coverage's line thickness. Absent when equal to the stroke default.
pub Weight("weight"): f64;
/// Stroke coverage's dash lengths, alternating dash and gap. Absent when the pattern is solid.
pub DashPattern("dash_pattern"): Option<&Vec<f64>>;
/// Stroke coverage's phase offset into the dash pattern. Absent when equal to the stroke default.
pub DashOffset("dash_offset"): f64;
/// Stroke coverage's shape at open endpoints. Absent when equal to the stroke default.
pub Cap("cap"): crate::vector::style::StrokeCap;
/// Stroke coverage's corner curvature. Absent when equal to the stroke default.
pub Join("join"): crate::vector::style::StrokeJoin;
/// Stroke coverage's miter-to-bevel conversion threshold. Absent when equal to the stroke default.
pub JoinMiterLimit("join_miter_limit"): f64 = 4.;
/// Stroke coverage's alignment to the path centerline. Absent when equal to the stroke default.
pub Align("align"): crate::vector::style::StrokeAlign;
} }
// The value types a name-generic attribute can name here, so a compile-time // The value types a name-generic attribute can name here, so a compile-time
@@ -41,6 +55,13 @@ pub const ATTR_GRADIENT_SPACE: &str = GradientSpace::NAME;
pub const ATTR_GRADIENT_HUE_DIRECTION: &str = GradientHueDirection::NAME; pub const ATTR_GRADIENT_HUE_DIRECTION: &str = GradientHueDirection::NAME;
pub const ATTR_GRADIENT_INTERPOLATION: &str = GradientInterpolation::NAME; pub const ATTR_GRADIENT_INTERPOLATION: &str = GradientInterpolation::NAME;
pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME; pub const ATTR_EDITOR_CLICK_TARGET: &str = EditorClickTarget::NAME;
pub const ATTR_WEIGHT: &str = Weight::NAME;
pub const ATTR_DASH_PATTERN: &str = DashPattern::NAME;
pub const ATTR_DASH_OFFSET: &str = DashOffset::NAME;
pub const ATTR_CAP: &str = Cap::NAME;
pub const ATTR_JOIN: &str = Join::NAME;
pub const ATTR_JOIN_MITER_LIMIT: &str = JoinMiterLimit::NAME;
pub const ATTR_ALIGN: &str = Align::NAME;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -53,6 +74,24 @@ mod tests {
assert_eq!(info("gradient_form").unwrap().value_type, TypeId::of::<crate::gradient::GradientForm>()); assert_eq!(info("gradient_form").unwrap().value_type, TypeId::of::<crate::gradient::GradientForm>());
assert_eq!(info("gradient_spread").unwrap().value_type, TypeId::of::<crate::gradient::GradientSpread>()); assert_eq!(info("gradient_spread").unwrap().value_type, TypeId::of::<crate::gradient::GradientSpread>());
assert_eq!(info("editor:click_target").unwrap().value_type, TypeId::of::<Option<&'static crate::Vector>>()); assert_eq!(info("editor:click_target").unwrap().value_type, TypeId::of::<Option<&'static crate::Vector>>());
assert_eq!(info("weight").unwrap().value_type, TypeId::of::<f64>());
assert_eq!(info("dash_pattern").unwrap().value_type, TypeId::of::<Option<&'static Vec<f64>>>());
assert_eq!(info("dash_offset").unwrap().value_type, TypeId::of::<f64>());
assert_eq!(info("cap").unwrap().value_type, TypeId::of::<crate::vector::style::StrokeCap>());
assert_eq!(info("join").unwrap().value_type, TypeId::of::<crate::vector::style::StrokeJoin>());
assert_eq!(info("join_miter_limit").unwrap().value_type, TypeId::of::<f64>());
assert_eq!(info("align").unwrap().value_type, TypeId::of::<crate::vector::style::StrokeAlign>());
}
#[test]
fn stroke_parameter_defaults_match_the_stroke_struct() {
let defaults = crate::vector::style::Stroke::default();
assert_eq!(<Weight as Attribute>::default(), defaults.weight);
assert_eq!(<DashOffset as Attribute>::default(), defaults.dash_offset);
assert_eq!(<JoinMiterLimit as Attribute>::default(), defaults.join_miter_limit);
assert_eq!(<Cap as Attribute>::default(), defaults.cap);
assert_eq!(<Join as Attribute>::default(), defaults.join);
assert_eq!(<Align as Attribute>::default(), defaults.align);
} }
#[test] #[test]
@@ -235,8 +235,6 @@ pub struct Stroke {
pub align: StrokeAlign, pub align: StrokeAlign,
#[cfg_attr(feature = "serde", serde(default = "daffine2_identity"))] #[cfg_attr(feature = "serde", serde(default = "daffine2_identity"))]
pub transform: DAffine2, pub transform: DAffine2,
#[cfg_attr(feature = "serde", serde(default))]
pub paint_order: PaintOrder,
} }
impl Stroke { impl Stroke {
@@ -250,7 +248,6 @@ impl Stroke {
join_miter_limit: 4., join_miter_limit: 4.,
align: StrokeAlign::Center, align: StrokeAlign::Center,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
paint_order: PaintOrder::StrokeAbove,
} }
} }
@@ -287,7 +284,6 @@ impl Stroke {
let skew = DAffine2::from_cols_array(&[1., 0., lerp(s_skew, t_skew), 1., 0., 0.]); let skew = DAffine2::from_cols_array(&[1., 0., lerp(s_skew, t_skew), 1., 0., 0.]);
trs * skew trs * skew
}, },
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order },
} }
} }
@@ -402,7 +398,6 @@ impl Default for Stroke {
join_miter_limit: 4., join_miter_limit: 4.,
align: StrokeAlign::Center, align: StrokeAlign::Center,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
paint_order: PaintOrder::default(),
} }
} }
} }
@@ -8,7 +8,6 @@ use crate::vector::vector_modification::VectorExt;
use core::borrow::Borrow; use core::borrow::Borrow;
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 glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use kurbo::{Affine, BezPath, Rect, Shape}; use kurbo::{Affine, BezPath, Rect, Shape};
use std::collections::HashMap; use std::collections::HashMap;
@@ -17,8 +16,6 @@ use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, dyn_any::DynAny)] #[derive(Clone, Debug, PartialEq, dyn_any::DynAny)]
#[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 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).
pub colinear_manipulators: Vec<[HandleId; 2]>, pub colinear_manipulators: Vec<[HandleId; 2]>,
@@ -31,7 +28,6 @@ pub struct Vector {
impl Default for Vector { impl Default for Vector {
fn default() -> Self { fn default() -> Self {
Self { Self {
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(),
@@ -45,7 +41,6 @@ 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.stroke.cache_hash(state);
self.colinear_manipulators.cache_hash(state); self.colinear_manipulators.cache_hash(state);
} }
} }
@@ -251,10 +246,10 @@ impl Vector {
/// identity (`Inside` = 0, `Outside` = 2×weight): the renderer masks half of a centered double-width /// identity (`Inside` = 0, `Outside` = 2×weight): the renderer masks half of a centered double-width
/// stroke, so its AABB matches the unmasked centered stroke's. For open paths the renderer always /// stroke, so its AABB matches the unmasked centered stroke's. For open paths the renderer always
/// draws a centered `weight`-wide stroke regardless of the align attribute, so we mirror that here. /// draws a centered `weight`-wide stroke regardless of the align attribute, so we mirror that here.
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, stroke: Option<&Stroke>) -> 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.stroke.as_ref() else { return path_bounds }; let Some(stroke) = stroke 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());
@@ -542,40 +537,14 @@ impl Vector {
self.segment_domain.concat(&additional.segment_domain, transform_of_additional, &id_map); self.segment_domain.concat(&additional.segment_domain, transform_of_additional, &id_map);
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
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 {
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
if !include_stroke {
// Just use the path bounds without stroke
return match self.bounding_box_with_transform(transform) {
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
None => RenderBoundingBox::None,
};
}
// Include stroke by adding offset based on stroke width
let stroke = self.stroke.clone();
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();
// Use the full line width to account for different styles of stroke caps
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
match self.bounding_box_with_transform(transform) { match self.bounding_box_with_transform(transform) {
Some([a, b]) => RenderBoundingBox::Rectangle([a - offset, b + offset]), Some(bounds) => RenderBoundingBox::Rectangle(bounds),
None => RenderBoundingBox::None, None => RenderBoundingBox::None,
} }
} }
+2 -16
View File
@@ -11,9 +11,9 @@ use core_types::registry::types::Angle;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, ExtractIndex, InjectIndex}; use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, ExtractIndex, InjectIndex};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::Vector;
use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, is_lone_anonymous_leaf, walk_vector_rows}; use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, is_lone_anonymous_leaf, walk_vector_rows};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr}; use graphic_types::markers::EditorMergedLayers;
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
use vector_types::gradient::{GradientForm as GradientFormValue, GradientHueDirection, GradientSpace, GradientSpread}; use vector_types::gradient::{GradientForm as GradientFormValue, GradientHueDirection, GradientSpace, GradientSpread};
use vector_types::markers::GradientCyclic; use vector_types::markers::GradientCyclic;
@@ -424,8 +424,6 @@ fn merged_layers_snapshot<'e>(arena: &'e Arena, mut snapshot: List<Graphic<'stat
type FlattenedVectorRow<'a, 'e> = ( type FlattenedVectorRow<'a, 'e> = (
Lane<'a, Vector>, Lane<'a, Vector>,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
@@ -436,14 +434,6 @@ type FlattenedVectorRow<'a, 'e> = (
/// walk's composition, paint and layer path overriding, and `snapshot` parked /// walk's composition, paint and layer path overriding, and `snapshot` parked
/// as the merged layers where given. /// as the merged layers where given.
fn emit_vector_row<'a, 'e>(arena: &'e Arena, carrier: Lane<'a, Graphic<'static>>, row: List<Vector>, snapshot: Option<List<Graphic<'static>>>) -> Result<FlattenedVectorRow<'a, 'e>, Interrupt> { fn emit_vector_row<'a, 'e>(arena: &'e Arena, carrier: Lane<'a, Graphic<'static>>, row: List<Vector>, snapshot: Option<List<Graphic<'static>>>) -> Result<FlattenedVectorRow<'a, 'e>, Interrupt> {
let park_paint = |paint: Option<&Option<List<Graphic<'static>>>>| {
paint
.and_then(|paint| paint.as_ref())
.map(|paint| arena.alloc_sized_keyed(paint.clone(), 0).map(|(parked, _)| parked).ok_or_else(arena_exhausted))
.transpose()
};
let fill = park_paint(row.attribute(ATTR_FILL, 0))?;
let stroke = park_paint(row.attribute(ATTR_STROKE, 0))?;
let layer_path: Vec<NodeId> = row.attribute(ATTR_EDITOR_LAYER_PATH, 0).cloned().unwrap_or_default(); let layer_path: Vec<NodeId> = row.attribute(ATTR_EDITOR_LAYER_PATH, 0).cloned().unwrap_or_default();
let (layer_path, _) = arena.alloc(layer_path).ok_or_else(arena_exhausted)?; let (layer_path, _) = arena.alloc(layer_path).ok_or_else(arena_exhausted)?;
@@ -454,8 +444,6 @@ fn emit_vector_row<'a, 'e>(arena: &'e Arena, carrier: Lane<'a, Graphic<'static>>
Ok(( Ok((
carrier.map_element(element), carrier.map_element(element),
Attr(transform), Attr(transform),
Attr(fill),
Attr(stroke),
Attr(row.attribute_cloned_or(ATTR_OPACITY, 0, 1.)), Attr(row.attribute_cloned_or(ATTR_OPACITY, 0, 1.)),
Attr(row.attribute_cloned_or(ATTR_OPACITY_FILL, 0, 1.)), Attr(row.attribute_cloned_or(ATTR_OPACITY_FILL, 0, 1.)),
Attr(layer_path.as_slice()), Attr(layer_path.as_slice()),
@@ -474,8 +462,6 @@ pub fn flatten_vector<'e>(
IList<( IList<(
Lane<Vector>, Lane<Vector>,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
+1 -1
View File
@@ -12,7 +12,7 @@ pub use graphene_application_io as application_io;
pub use graphene_core; pub use graphene_core;
pub use graphene_core::debug; pub use graphene_core::debug;
pub use graphic_nodes; pub use graphic_nodes;
pub use graphic_types::{Artboard, Graphic, Vector}; pub use graphic_types::{Appearance, Artboard, Cover, Coverage, Graphic, Vector};
pub use math_nodes; pub use math_nodes;
pub use path_bool_nodes; pub use path_bool_nodes;
pub use raster_nodes; pub use raster_nodes;
+4 -4
View File
@@ -43,7 +43,7 @@ pub struct CacheKey {
pub for_mask: bool, pub for_mask: bool,
pub thumbnail: bool, pub thumbnail: bool,
pub aligned_strokes: bool, pub aligned_strokes: bool,
pub override_paint_order: bool, pub stroke_below: bool,
pub animation_time_ms: i64, pub animation_time_ms: i64,
pub real_time_ms: i64, pub real_time_ms: i64,
pub pointer: [u8; 16], pub pointer: [u8; 16],
@@ -60,7 +60,7 @@ impl CacheKey {
for_mask: bool, for_mask: bool,
thumbnail: bool, thumbnail: bool,
aligned_strokes: bool, aligned_strokes: bool,
override_paint_order: bool, stroke_below: bool,
animation_time: f64, animation_time: f64,
real_time: f64, real_time: f64,
pointer: Option<DVec2>, pointer: Option<DVec2>,
@@ -85,7 +85,7 @@ impl CacheKey {
for_mask, for_mask,
thumbnail, thumbnail,
aligned_strokes, aligned_strokes,
override_paint_order, stroke_below,
animation_time_ms: (animation_time * 1000.).round() as i64, animation_time_ms: (animation_time * 1000.).round() as i64,
real_time_ms: (real_time * 1000.).round() as i64, real_time_ms: (real_time * 1000.).round() as i64,
pointer: pointer_bytes, pointer: pointer_bytes,
@@ -360,7 +360,7 @@ pub fn render_output_cache(
render_params.for_mask, render_params.for_mask,
render_params.thumbnail, render_params.thumbnail,
render_params.aligned_strokes, render_params.aligned_strokes,
render_params.override_paint_order, render_params.stroke_below,
ctx.try_animation_time().unwrap_or(0.), ctx.try_animation_time().unwrap_or(0.),
ctx.try_real_time().unwrap_or(0.), ctx.try_real_time().unwrap_or(0.),
ctx.try_pointer_position(), ctx.try_pointer_position(),
+44 -46
View File
@@ -3,12 +3,13 @@ use core_types::list::{Item, List};
use core_types::node::Lane; use core_types::node::Lane;
use core_types::{ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx}; use core_types::{ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::graphic::{GraphicLevel, PaintColumns, PaintReach, bake_paint_transforms, is_paint_present, set_paint_attribute_at}; use graphic_types::appearance::Appearance;
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke}; use graphic_types::graphic::{GraphicLevel, PaintColumns, PaintReach, bake_paint_transforms};
use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers};
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::PointId;
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use graphic_types::{ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList, Vector}; use graphic_types::{Graphic, IntoGraphicList, Vector};
use linesweeper::topology::Topology; use linesweeper::topology::Topology;
use linesweeper::{BinaryOp, FillRule, binary_op}; use linesweeper::{BinaryOp, FillRule, binary_op};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -25,7 +26,7 @@ fn boolean_core<'e>(
flattened: List<Vector>, flattened: List<Vector>,
snapshot: List<Graphic<'static>>, snapshot: List<Graphic<'static>>,
operation: BooleanOperation, operation: BooleanOperation,
) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, Stroke>, Attr<'e, EditorMergedLayers>), core_types::gpoll::Interrupt> { ) -> Result<(Vector, Attr<'e, TransformAttr>, Attr<'e, AppearanceMarker>, Attr<'e, EditorMergedLayers>), core_types::gpoll::Interrupt> {
// The first index is the bottom of the stack // The first index is the bottom of the stack
let mut result_vector_list = boolean_operation_on_vector_list(&flattened, operation); let mut result_vector_list = boolean_operation_on_vector_list(&flattened, operation);
@@ -36,7 +37,6 @@ fn boolean_core<'e>(
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.set_stroke_transform(DAffine2::IDENTITY);
// Clean up the boolean operation result by merging duplicated points // Clean up the boolean operation result by merging duplicated points
let merge_transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let merge_transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
@@ -49,17 +49,12 @@ fn boolean_core<'e>(
trace: Vec::new(), trace: Vec::new(),
}) })
}; };
let park_paint = |paint: Option<List<Graphic<'static>>>| -> Result<Option<&'e List<Graphic>>, core_types::gpoll::Interrupt> {
match paint {
Some(list) => Ok(Some(arena.alloc_sized_keyed(list, 0).ok_or_else(exhausted)?.0)),
None => Ok(None),
}
};
let element = result_vector_list.element(0).cloned().unwrap_or_default(); let element = result_vector_list.element(0).cloned().unwrap_or_default();
use core_types::lane::LaneSource; use core_types::lane::LaneSource;
let fill = park_paint(result_vector_list.attr::<Fill>(0).filter(|paint| is_paint_present(paint)).cloned())?; let appearance = match result_vector_list.attr::<AppearanceMarker>(0).cloned() {
let stroke = park_paint(result_vector_list.attr::<Stroke>(0).filter(|paint| is_paint_present(paint)).cloned())?; Some(appearance) => Some(arena.alloc_sized_keyed(appearance, 0).ok_or_else(exhausted)?.0),
None => None,
};
// Snapshot the input layers so the renderer can recurse into them for // Snapshot the input layers so the renderer can recurse into them for
// editor click-target preservation. // editor click-target preservation.
let merged_layers = arena.alloc_sized_keyed(snapshot, 0).ok_or_else(exhausted)?.0; let merged_layers = arena.alloc_sized_keyed(snapshot, 0).ok_or_else(exhausted)?.0;
@@ -69,8 +64,7 @@ fn boolean_core<'e>(
Ok(( Ok((
element, element,
Attr(result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)), Attr(result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0)),
Attr(fill), Attr(appearance),
Attr(stroke),
Attr(Some(merged_layers)), Attr(Some(merged_layers)),
)) ))
} }
@@ -88,17 +82,17 @@ fn boolean_operation<'e>(
/// Intersection cuts away all but the overlapping areas shared by every path. /// Intersection cuts away all but the overlapping areas shared by every path.
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas. /// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
operation: BooleanOperation, operation: BooleanOperation,
) -> Result<(Lane<Vector>, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, Stroke>, Attr<'e, EditorMergedLayers>), core_types::gpoll::Interrupt> { ) -> Result<(Lane<Vector>, Attr<'e, TransformAttr>, Attr<'e, AppearanceMarker>, Attr<'e, EditorMergedLayers>), core_types::gpoll::Interrupt> {
if content.is_empty() { if content.is_empty() {
return Err(core_types::gpoll::GraphError::past_end().into()); return Err(core_types::gpoll::GraphError::past_end().into());
} }
let item = content.as_group_item(); let item = content.as_group_item();
let flattened = flatten_vector_run(GraphicLevel::Run(&item), Ancestors::NONE, PaintReach::NONE); let flattened = flatten_vector_run(GraphicLevel::Run(&item), Ancestors::NONE, PaintReach::NONE);
let snapshot = graphic_types::graphic::run_to_list::<Graphic>(&item).expect("the run holds the row's element type").into_graphic_list(); let snapshot = graphic_types::graphic::run_to_list::<Graphic>(&item).expect("the run holds the row's element type").into_graphic_list();
let (element, transform, fill, stroke, merged) = boolean_core(ctx.arena(), flattened, snapshot, operation)?; let (element, transform, appearance, merged) = boolean_core(ctx.arena(), flattened, snapshot, operation)?;
// The merge presents the bottom-of-stack lane's blending, clipping and layer // The merge presents the bottom-of-stack lane's blending, clipping and layer
// path, carried rather than re-read: one named lane instead of four copies. // path, carried rather than re-read: one named lane instead of four copies.
Ok((content.lane(0).map_element(element), transform, fill, stroke, merged)) Ok((content.lane(0).map_element(element), transform, appearance, merged))
} }
/// The boolean operation over a plain vector level, as [`boolean_operation`]. /// The boolean operation over a plain vector level, as [`boolean_operation`].
@@ -107,15 +101,15 @@ fn boolean_operation_vector<'e>(
ctx: impl Ctx + ExtractArena<'e> + core_types::InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + core_types::InjectIndex + Copy,
content: IList<Vector>, content: IList<Vector>,
operation: BooleanOperation, operation: BooleanOperation,
) -> Result<(Lane<Vector>, Attr<'e, TransformAttr>, Attr<'e, Fill>, Attr<'e, Stroke>, Attr<'e, EditorMergedLayers>), core_types::gpoll::Interrupt> { ) -> Result<(Lane<Vector>, Attr<'e, TransformAttr>, Attr<'e, AppearanceMarker>, Attr<'e, EditorMergedLayers>), core_types::gpoll::Interrupt> {
if content.is_empty() { if content.is_empty() {
return Err(core_types::gpoll::GraphError::past_end().into()); return Err(core_types::gpoll::GraphError::past_end().into());
} }
let item = content.as_group_item(); let item = content.as_group_item();
let flattened = graphic_types::graphic::run_to_list::<Vector>(&item).expect("the run holds vector lanes"); let flattened = graphic_types::graphic::run_to_list::<Vector>(&item).expect("the run holds vector lanes");
let snapshot = graphic_types::graphic::run_to_list::<Vector>(&item).expect("the run holds the row's element type").into_graphic_list(); let snapshot = graphic_types::graphic::run_to_list::<Vector>(&item).expect("the run holds the row's element type").into_graphic_list();
let (element, transform, fill, stroke, merged) = boolean_core(ctx.arena(), flattened, snapshot, operation)?; let (element, transform, appearance, merged) = boolean_core(ctx.arena(), flattened, snapshot, operation)?;
Ok((content.lane(0).map_element(element), transform, fill, stroke, merged)) Ok((content.lane(0).map_element(element), transform, appearance, merged))
} }
pub use _boolean_operation_vector_mod::boolean_operation_vector_entries; pub use _boolean_operation_vector_mod::boolean_operation_vector_entries;
@@ -203,12 +197,7 @@ 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(); Item::from_parts(Vector::default(), attributes)
let element = Vector {
stroke: copy_from.stroke.clone(),
..Default::default()
};
Item::from_parts(element, attributes)
} else { } else {
Item::<Vector>::default() Item::<Vector>::default()
}; };
@@ -312,13 +301,7 @@ impl Ancestors {
fn push_leaf_vector_row(out: &mut List<Vector>, level: GraphicLevel<'_>, index: usize, vector: &Vector, ancestors: Ancestors, reach: PaintReach<'_>) { fn push_leaf_vector_row(out: &mut List<Vector>, level: GraphicLevel<'_>, index: usize, vector: &Vector, ancestors: Ancestors, reach: PaintReach<'_>) {
let out_index = out.len(); let out_index = out.len();
out.push(Item::from_parts(vector.clone(), graphic_types::graphic::lane_attributes(level, index))); out.push(Item::from_parts(vector.clone(), graphic_types::graphic::lane_attributes(level, index)));
if reach.applies() { stamp_inherited_appearance(out, out_index, reach.appearance);
for (key, slot) in [(ATTR_FILL, reach.paint.fill), (ATTR_STROKE, reach.paint.stroke)] {
if let Some(paint) = slot {
set_paint_attribute_at(out, out_index, key, paint.clone());
}
}
}
ancestors.compose(out, out_index); ancestors.compose(out, out_index);
} }
@@ -327,13 +310,7 @@ fn push_vector_rows(out: &mut List<Vector>, rows: &List<Vector>, composed: Ances
let Some(item) = rows.clone_item(row) else { continue }; let Some(item) = rows.clone_item(row) else { continue };
let index = out.len(); let index = out.len();
out.push(item); out.push(item);
if reach.applies() { stamp_inherited_appearance(out, index, reach.appearance);
for (key, slot) in [(ATTR_FILL, reach.paint.fill), (ATTR_STROKE, reach.paint.stroke)] {
if let Some(paint) = slot {
set_paint_attribute_at(out, index, key, paint.clone());
}
}
}
composed.compose(out, index); composed.compose(out, index);
} }
} }
@@ -347,6 +324,15 @@ fn push_text_rows(out: &mut List<Vector>, text: &List<String>, composed: Ancesto
} }
} }
/// The cascade's resolved appearance lands on a row whose own is undeclared, since a declared row wins wholesale.
fn stamp_inherited_appearance(out: &mut List<Vector>, index: usize, inherited: Option<&Appearance>) {
if let Some(appearance) = inherited
&& out.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, index).and_then(Appearance::declared).is_none()
{
out.set_attribute(graphic_types::ATTR_APPEARANCE, index, appearance.clone());
}
}
fn push_union(out: &mut List<Vector>, flattened: List<Vector>) { fn push_union(out: &mut List<Vector>, flattened: List<Vector>) {
// The union emits one blank operand even from an empty list, which would fabricate a region out of nothing // The union emits one blank operand even from an empty list, which would fabricate a region out of nothing
if flattened.len() == 0 { if flattened.len() == 0 {
@@ -375,7 +361,7 @@ fn flatten_vector_run_into<'a>(out: &mut List<Vector>, level: GraphicLevel<'a>,
let composed = ancestors.through(&level, index); let composed = ancestors.through(&level, index);
match element { match element {
Graphic::Vector(vector) => push_leaf_vector_row(out, level, index, vector, ancestors, reach), Graphic::Vector(vector) => push_leaf_vector_row(out, level, index, vector, ancestors, reach),
Graphic::Graphic(children) => push_union(out, flatten_vector_run(GraphicLevel::Legacy(children), composed, reach.nested())), Graphic::Graphic(children) => push_union(out, flatten_vector_run(GraphicLevel::Legacy(children), composed, reach)),
Graphic::Group(group) => flatten_group(out, group, composed, reach), Graphic::Group(group) => flatten_group(out, group, composed, reach),
Graphic::Text(text) => { Graphic::Text(text) => {
let one = List::new_from_item(Item::from_parts(text.clone(), graphic_types::graphic::lane_attributes(level, index))); let one = List::new_from_item(Item::from_parts(text.clone(), graphic_types::graphic::lane_attributes(level, index)));
@@ -395,7 +381,7 @@ fn flatten_group(out: &mut List<Vector>, group: &core_types::record::Group, comp
if let Some(rows) = graphic_types::graphic::run_to_list::<Vector>(item) { if let Some(rows) = graphic_types::graphic::run_to_list::<Vector>(item) {
push_vector_rows(out, &rows, composed, reach); push_vector_rows(out, &rows, composed, reach);
} else if core_types::record::RunView::<Graphic>::new(item).is_some() { } else if core_types::record::RunView::<Graphic>::new(item).is_some() {
push_union(out, flatten_vector_run(GraphicLevel::Run(item), composed, reach.into_group_graphics())); push_union(out, flatten_vector_run(GraphicLevel::Run(item), composed, reach));
} else if let Some(text) = graphic_types::graphic::run_to_list::<String>(item) { } else if let Some(text) = graphic_types::graphic::run_to_list::<String>(item) {
push_text_rows(out, &text, composed); push_text_rows(out, &text, composed);
} }
@@ -502,6 +488,12 @@ mod tests {
List::new_from_element(Graphic::Color(Color::BLACK)) List::new_from_element(Graphic::Color(Color::BLACK))
} }
/// The single-fill appearance a built row paints with.
fn fill_appearance(paint: List<Graphic<'static>>) -> Appearance {
use graphic_types::appearance::Coverage;
Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(paint))
}
#[test] #[test]
fn the_native_flatten_reads_lanes_groups_and_reach() { fn the_native_flatten_reads_lanes_groups_and_reach() {
let inner_vector = square(DVec2::ZERO); let inner_vector = square(DVec2::ZERO);
@@ -515,7 +507,7 @@ mod tests {
top.push(Item::new_from_element(Graphic::Color(Color::BLACK))); top.push(Item::new_from_element(Graphic::Color(Color::BLACK)));
top.push(Item::new_from_element(Graphic::Group(Group { row: None, content: inner_item }))); top.push(Item::new_from_element(Graphic::Group(Group { row: None, content: inner_item })));
top.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.))); top.set_attribute(ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.)));
set_paint_attribute_at(&mut top, 0, ATTR_FILL, black_paint()); top.set_attribute(graphic_types::ATTR_APPEARANCE, 0, fill_appearance(black_paint()));
top.set_attribute(ATTR_OPACITY, 1, 0.5); top.set_attribute(ATTR_OPACITY, 1, 0.5);
top.set_attribute(ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.))); top.set_attribute(ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.)));
@@ -523,10 +515,16 @@ mod tests {
// The color lane bounds no region, so it serves no operand and the group's row lands at 1 // The color lane bounds no region, so it serves no operand and the group's row lands at 1
assert_eq!(rows.len(), 2); assert_eq!(rows.len(), 2);
let fill_of = |index: usize| {
rows.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, index)
.and_then(|appearance| appearance.first_paint_of(graphic_types::appearance::Cover::Fill))
.and_then(graphic_types::graphic::paint_cell_rows)
};
// Lane 0: the leaf row keeps its lane attributes, with the lane fill // Lane 0: the leaf row keeps its lane attributes, with the lane fill
// present and the ancestor composition the identity. // present and the ancestor composition the identity.
assert_eq!(rows.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0), DAffine2::from_translation(DVec2::new(5., 5.))); assert_eq!(rows.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, 0), DAffine2::from_translation(DVec2::new(5., 5.)));
assert!(graphic_types::graphic::paint_graphics::<Fill, _>(&rows, 0).is_some()); assert!(fill_of(0).is_some());
// Lane 1: the group's vector run serves its row under the lane // Lane 1: the group's vector run serves its row under the lane
// transform. // transform.
+238 -263
View File
@@ -17,10 +17,11 @@ use core_types::transform::Transform;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex};
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::graphic::{MapVectorContent, bake_paint_transforms, has_paint, is_paint_present, set_paint_attribute_at}; use graphic_types::appearance::{Appearance, Cover, CoverPlacement, Coverage};
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr}; use graphic_types::graphic::{MapVectorContent, bake_paint_transforms, is_paint_present};
use graphic_types::markers::{Appearance as AppearanceMarker, EditorMergedLayers};
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_FILL, ATTR_STROKE, Graphic, IntoGraphicList}; use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; use graphic_types::{Artboard, Vector};
use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::simplify::{SimplifyOptions, simplify_bezpath};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape};
@@ -39,7 +40,7 @@ use vector_types::vector::misc::{
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles, bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
}; };
use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{DashPattern, Gradient, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::vector::{PointDomain, RegionDomain}; use vector_types::vector::{PointDomain, RegionDomain};
@@ -91,18 +92,18 @@ fn assign_colors<'e>(
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once. /// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] #[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
repeat_every: u32, repeat_every: u32,
) -> Result<IList<(Lane<Vector>, Attr<'e, Fill>, Attr<'e, StrokeAttr>)>, Interrupt> { ) -> Result<IList<(Lane<Vector>, Attr<'e, AppearanceMarker>)>, Interrupt> {
let lane = ctx.index() as usize; let lane = ctx.index() as usize;
if lane >= content.len() { if lane >= content.len() {
return Err(GraphError::past_end().into()); return Err(GraphError::past_end().into());
} }
let element = content.element_ref(lane).clone(); let element = content.element_ref(lane).clone();
let park_existing = |paint: Option<&List<Graphic<'static>>>| -> Result<Option<&'e List<Graphic>>, Interrupt> { paint.map(|paint| park_paint(ctx.arena(), paint.clone())).transpose() }; let existing_appearance = content.lane(lane).attr::<AppearanceMarker>().cloned();
let existing_fill = park_existing(content.lane(lane).attr::<Fill>())?; let park_appearance_attr = |appearance: Option<Appearance>| -> Result<Option<&'e Appearance>, Interrupt> { appearance.map(|appearance| park_appearance(ctx.arena(), appearance)).transpose() };
let existing_stroke = park_existing(content.lane(lane).attr::<StrokeAttr>())?;
if gradient.is_empty() { if gradient.is_empty() {
return Ok((content.lane(lane).map_element(element), Attr(existing_fill), Attr(existing_stroke))); let parked_appearance = park_appearance_attr(existing_appearance)?;
return Ok((content.lane(lane).map_element(element), Attr(parked_appearance)));
} }
let settings = vector_types::GradientSettings { let settings = vector_types::GradientSettings {
spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(), spread: gradient.lane(0).attr::<vector_types::markers::GradientSpread>(),
@@ -123,17 +124,20 @@ fn assign_colors<'e>(
let color = assign_color_at(gradient_element, settings, lane, content.len(), randomize, seed, repeat_every); let color = assign_color_at(gradient_element, settings, lane, content.len(), randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list(); let paint = List::new_from_element(color).into_graphic_list();
let parked = park_paint(ctx.arena(), paint)?;
let fill_attr = match fill { // The recolor lands on the appearance's coverage paints
true => Some(parked), let mut appearance = existing_appearance.unwrap_or_default();
false => existing_fill, let paint_cell = Graphic::Graphic(paint);
}; if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
let stroke_attr = match stroke && element.stroke.is_some() { appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Below);
true => Some(parked), }
false => existing_stroke, // The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
}; if stroke {
Ok((content.lane(lane).map_element(element), Attr(fill_attr), Attr(stroke_attr))) appearance.set_paint_of(Cover::Stroke, paint_cell);
}
let parked_appearance = park_appearance_attr(Some(appearance))?;
Ok((content.lane(lane).map_element(element), Attr(parked_appearance)))
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -230,15 +234,20 @@ fn assign_colors_graphic<'e>(
let element = match rows { let element = match rows {
Some(mut rows) => { Some(mut rows) => {
for row in 0..rows.len() { for row in 0..rows.len() {
let has_stroke = rows.element(row).is_some_and(|vector| vector.stroke.is_some());
let color = assign_color_at(gradient_element, settings, position + row, length, randomize, seed, repeat_every); let color = assign_color_at(gradient_element, settings, position + row, length, randomize, seed, repeat_every);
let paint = List::new_from_element(color).into_graphic_list(); let paint = List::new_from_element(color).into_graphic_list();
if fill {
set_paint_attribute_at(&mut rows, row, ATTR_FILL, paint.clone()); // The recolor lands on the row's appearance coverage paints
let mut appearance = rows.attribute_cloned_or_default::<Appearance>(graphic_types::ATTR_APPEARANCE, row);
let paint_cell = Graphic::Graphic(paint);
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Below);
} }
if stroke && has_stroke { // The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
set_paint_attribute_at(&mut rows, row, ATTR_STROKE, paint.clone()); if stroke {
appearance.set_paint_of(Cover::Stroke, paint_cell);
} }
rows.set_attribute(graphic_types::ATTR_APPEARANCE, row, appearance);
} }
let content = core_types::record::GroupItem::from_list(rows, ctx.arena()).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?; let content = core_types::record::GroupItem::from_list(rows, ctx.arena()).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?;
Graphic::Group(core_types::record::Group { row: None, content }) Graphic::Group(core_types::record::Group { row: None, content })
@@ -278,16 +287,27 @@ fn assign_colors_graphic_extent(
pub use _assign_colors_graphic_mod::assign_colors_graphic_entries; pub use _assign_colors_graphic_mod::assign_colors_graphic_entries;
/// Keyed, so a group-free paint's promote moves this header rather than /// Keyed, so a group-free appearance's promote moves this header rather than
/// cloning the content it owns. /// cloning the content it owns.
fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List<Graphic<'static>>) -> Result<&'e List<Graphic<'static>>, Interrupt> { /// Keyed so a group-free appearance's promote moves this header.
let (parked, _) = arena.alloc_sized_keyed(paint, 0).ok_or(GraphError { fn park_appearance(arena: &core_types::arena::Arena, appearance: Appearance) -> Result<&Appearance, Interrupt> {
let (parked, _) = arena.alloc_sized_keyed(appearance, 0).ok_or(GraphError {
kind: core_types::gpoll::ErrorKind::ArenaExhausted, kind: core_types::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(), trace: Vec::new(),
})?; })?;
Ok(parked) Ok(parked)
} }
/// Appends one coverage to the content's appearance following the painter's algorithm:
/// the most downstream paint node in the chain paints on top unless it asks for the
/// below placement. The coverage's paint is the canonical paint list carried as one
/// graphic cell, the input lane's own envelope dropped.
fn stamped_appearance(content_appearance: Option<&Appearance>, coverage: Coverage, paint: &List<Graphic<'static>>, placement: CoverPlacement) -> Appearance {
let mut appearance = content_appearance.cloned().unwrap_or_default();
appearance.replace_or_insert(coverage, Graphic::Graphic(paint.clone()), placement);
appearance
}
/// The gradient defaulting the legacy fill performed, applied to the nested /// The gradient defaulting the legacy fill performed, applied to the nested
/// stops list the paint table wraps. /// stops list the paint table wraps.
fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>, gradient_type: GradientForm, transform: Option<DAffine2>) { fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>, gradient_type: GradientForm, transform: Option<DAffine2>) {
@@ -329,7 +349,7 @@ fn paint_table(paint: core_types::node::List<'_, Graphic<'_>>) -> List<Graphic<'
fn fill<'e>( fn fill<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The content with vector paths to apply the fill style to. /// The content with vector paths to apply the fill style to.
(element, _content_fill): (Vector, Attr<Fill>), (element, content_appearance): (Vector, Attr<AppearanceMarker>),
/// The fill to paint the path with. /// The fill to paint the path with.
#[default(Color::BLACK)] #[default(Color::BLACK)]
fill: IList<Graphic<'static>>, fill: IList<Graphic<'static>>,
@@ -338,35 +358,36 @@ fn fill<'e>(
_gradient_form: GradientForm, _gradient_form: GradientForm,
_has_transform: bool, _has_transform: bool,
_transform: DAffine2, _transform: DAffine2,
) -> Result<(Vector, Attr<'e, Fill>), Interrupt> { ) -> Result<(Vector, Attr<'e, AppearanceMarker>), Interrupt> {
let mut paint = paint_table(fill); let mut paint = paint_table(fill);
default_gradient_paint(&mut paint, element.bounding_box(), _gradient_form, _has_transform.then_some(_transform)); default_gradient_paint(&mut paint, element.bounding_box(), _gradient_form, _has_transform.then_some(_transform));
let parked = park_paint(ctx.arena(), paint)?; let appearance = stamped_appearance(*content_appearance, Coverage::new_fill(), &paint, CoverPlacement::Above);
Ok((element, Attr(Some(parked)))) let parked_appearance = park_appearance(ctx.arena(), appearance)?;
Ok((element, Attr(Some(parked_appearance))))
} }
/// The fill over graphic lanes: the marker parks on the lane and the render /// The fill over graphic lanes: the appearance parks on the lane and cascades
/// boundary moves it onto the interior vector lists the legacy paint readers /// to the vectors beneath it. Registered under the fill's identifier.
/// inspect. Registered under the fill's identifier.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
fn fill_graphic_leveled<'e>( fn fill_graphic_leveled<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, _content_fill): (Graphic<'static>, Attr<Fill>), (element, content_appearance): (Graphic<'static>, Attr<AppearanceMarker>),
#[default(Color::BLACK)] fill: IList<Graphic<'static>>, #[default(Color::BLACK)] fill: IList<Graphic<'static>>,
_backup_color: IList<Color>, _backup_color: IList<Color>,
_backup_gradient: IList<Gradient>, _backup_gradient: IList<Gradient>,
_gradient_form: GradientForm, _gradient_form: GradientForm,
_has_transform: bool, _has_transform: bool,
_transform: DAffine2, _transform: DAffine2,
) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> { ) -> Result<(Graphic<'static>, Attr<'e, AppearanceMarker>), Interrupt> {
let bounds = match BoundingBox::bounding_box(&element, DAffine2::IDENTITY, false) { let bounds = match BoundingBox::bounding_box(&element, DAffine2::IDENTITY, false) {
RenderBoundingBox::Rectangle(bounds) => Some(bounds), RenderBoundingBox::Rectangle(bounds) => Some(bounds),
_ => None, _ => None,
}; };
let mut paint = paint_table(fill); let mut paint = paint_table(fill);
default_gradient_paint(&mut paint, bounds, _gradient_form, _has_transform.then_some(_transform)); default_gradient_paint(&mut paint, bounds, _gradient_form, _has_transform.then_some(_transform));
let parked = park_paint(ctx.arena(), paint)?; let appearance = stamped_appearance(*content_appearance, Coverage::new_fill(), &paint, CoverPlacement::Above);
Ok((element, Attr(Some(parked)))) let parked_appearance = park_appearance(ctx.arena(), appearance)?;
Ok((element, Attr(Some(parked_appearance))))
} }
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
@@ -374,7 +395,7 @@ fn fill_graphic_leveled<'e>(
fn stroke<'e>( fn stroke<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The content with vector paths to apply the stroke style to. /// The content with vector paths to apply the stroke style to.
(element, content_transform): (Vector, Attr<TransformAttr>), (element, content_transform, content_appearance): (Vector, Attr<TransformAttr>, Attr<AppearanceMarker>),
/// The stroke paint. /// The stroke paint.
#[default(Color::BLACK)] #[default(Color::BLACK)]
paint: IList<Graphic<'static>>, paint: IList<Graphic<'static>>,
@@ -391,72 +412,12 @@ fn stroke<'e>(
/// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio. /// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio.
#[default(4.)] #[default(4.)]
miter_limit: f64, miter_limit: f64,
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
paint_order: PaintOrder,
/// The stroke dash pattern. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed. /// The stroke dash pattern. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
dash_pattern: DashPattern, dash_pattern: DashPattern,
/// The phase offset distance from the starting point of the dash pattern. /// The phase offset distance from the starting point of the dash pattern.
#[unit(" px")] #[unit(" px")]
dash_offset: f64, dash_offset: f64,
) -> Result<(Vector, Attr<TransformAttr>, Attr<'e, StrokeAttr>), Interrupt> { ) -> Result<(Vector, Attr<TransformAttr>, Attr<'e, AppearanceMarker>), Interrupt> {
let dash_lengths = dash_pattern.clamped_lengths();
let mut stroke = Stroke {
weight,
dash_lengths,
dash_offset,
cap,
join,
join_miter_limit: miter_limit,
align,
transform: DAffine2::IDENTITY,
paint_order,
};
stroke.transform *= *content_transform;
let mut element = element;
element.stroke = Some(stroke);
let paint = paint_table(paint);
let parked = park_paint(ctx.arena(), paint)?;
Ok((element, Attr(*content_transform), Attr(Some(parked))))
}
/// The vector items of a graphic lane's interior, one wrap level deep, the
/// reach of the pre-flip broadcast over a legacy list.
fn for_each_interior_vector_mut(element: &mut Graphic, mut f: impl FnMut(&mut Vector, DAffine2)) {
match element {
Graphic::Vector(vector) => f(vector, DAffine2::IDENTITY),
Graphic::Graphic(children) => {
for index in 0..children.len() {
let transform: DAffine2 = children.attribute_cloned_or_default(ATTR_TRANSFORM, index);
if let Some(Graphic::Vector(vector)) = children.element_mut(index) {
f(vector, transform);
}
}
}
_ => {}
}
}
/// The stroke over graphic lanes: the style applies to the interior vectors,
/// the paint marker parks on the lane for the render boundary to place.
/// Registered under the stroke's identifier.
#[node_macro::node(category(""))]
fn stroke_graphic_leveled<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, content_transform): (Graphic<'static>, Attr<TransformAttr>),
#[default(Color::BLACK)] paint: IList<Graphic<'static>>,
#[unit(" px")]
#[default(2.)]
weight: f64,
align: StrokeAlign,
cap: StrokeCap,
join: StrokeJoin,
#[default(4.)] miter_limit: f64,
paint_order: PaintOrder,
dash_pattern: DashPattern,
#[unit(" px")] dash_offset: f64,
) -> Result<(Graphic<'static>, Attr<TransformAttr>, Attr<'e, StrokeAttr>), Interrupt> {
let dash_lengths = dash_pattern.clamped_lengths(); let dash_lengths = dash_pattern.clamped_lengths();
let stroke = Stroke { let stroke = Stroke {
weight, weight,
@@ -467,19 +428,57 @@ fn stroke_graphic_leveled<'e>(
join_miter_limit: miter_limit, join_miter_limit: miter_limit,
align, align,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
paint_order,
}; };
let mut element = element; // The coverage records the stroke's authoring space: the item transform, translation included
for_each_interior_vector_mut(&mut element, |vector, transform| { let mut coverage_stroke = stroke;
let mut stroke = stroke.clone(); coverage_stroke.transform *= *content_transform;
stroke.transform *= transform;
vector.stroke = Some(stroke);
});
let paint = paint_table(paint); let paint = paint_table(paint);
let parked = park_paint(ctx.arena(), paint)?; // A below stroke is the chain running the stroke node before the fill, so the coverage appends above
Ok((element, Attr(*content_transform), Attr(Some(parked)))) let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, CoverPlacement::Above);
let parked_appearance = park_appearance(ctx.arena(), appearance)?;
Ok((element, Attr(*content_transform), Attr(Some(parked_appearance))))
}
/// The stroke over graphic lanes: the appearance parks on the lane and cascades
/// to the vectors beneath it. Registered under the stroke's identifier.
#[node_macro::node(category(""))]
fn stroke_graphic_leveled<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
(element, content_transform, content_appearance): (Graphic<'static>, Attr<TransformAttr>, Attr<AppearanceMarker>),
#[default(Color::BLACK)] paint: IList<Graphic<'static>>,
#[unit(" px")]
#[default(2.)]
weight: f64,
align: StrokeAlign,
cap: StrokeCap,
join: StrokeJoin,
#[default(4.)] miter_limit: f64,
dash_pattern: DashPattern,
#[unit(" px")] dash_offset: f64,
) -> Result<(Graphic<'static>, Attr<TransformAttr>, Attr<'e, AppearanceMarker>), Interrupt> {
let dash_lengths = dash_pattern.clamped_lengths();
let stroke = Stroke {
weight,
dash_lengths,
dash_offset,
cap,
join,
join_miter_limit: miter_limit,
align,
transform: DAffine2::IDENTITY,
};
// The coverage records the stroke's authoring space: the lane transform, translation included
let mut coverage_stroke = stroke;
coverage_stroke.transform *= *content_transform;
let paint = paint_table(paint);
// A below stroke is the chain running the stroke node before the fill, so the coverage appends above
let appearance = stamped_appearance(*content_appearance, Coverage::new_stroke(&coverage_stroke), &paint, CoverPlacement::Above);
let parked_appearance = park_appearance(ctx.arena(), appearance)?;
Ok((element, Attr(*content_transform), Attr(Some(parked_appearance))))
} }
pub use _fill_graphic_leveled_mod::fill_graphic_leveled_entries; pub use _fill_graphic_leveled_mod::fill_graphic_leveled_entries;
@@ -630,10 +629,7 @@ fn round_corners<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'st
// Convert 0-100 to 0-0.5 // Convert 0-100 to 0-0.5
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 { ..Default::default() };
stroke: source.stroke.clone(),
..Default::default()
};
// Grab the initial point ID as a stable starting point // Grab the initial point ID as a stable starting point
let mut initial_point_id = source.point_domain.ids().first().copied().unwrap_or(PointId::generate()); let mut initial_point_id = source.point_domain.ids().first().copied().unwrap_or(PointId::generate());
@@ -1014,8 +1010,6 @@ fn box_warp<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'static>
}); });
} }
result.set_stroke_transform(DAffine2::IDENTITY);
// Reset the transform since we've applied it directly to the points // Reset the transform since we've applied it directly to the points
(result, DAffine2::IDENTITY) (result, DAffine2::IDENTITY)
})?; })?;
@@ -1163,10 +1157,7 @@ fn auto_tangents<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'st
preserve_existing: bool, preserve_existing: bool,
) -> Result<(V::Live<'e>, Attr<TransformAttr>), Interrupt> { ) -> Result<(V::Live<'e>, Attr<TransformAttr>), Interrupt> {
let (source, transform) = map_vectors(ctx.arena(), source, *lane_transform, |source, transform| { let (source, transform) = map_vectors(ctx.arena(), source, *lane_transform, |source, transform| {
let mut result = Vector { let mut result = Vector { ..Default::default() };
stroke: source.stroke.clone(),
..Default::default()
};
for mut subpath in source.stroke_bezier_paths() { for mut subpath in source.stroke_bezier_paths() {
subpath.apply_transform(transform); subpath.apply_transform(transform);
@@ -1305,7 +1296,7 @@ fn bounding_box<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'sta
#[implementations(Graphic, Vector)] content: V, #[implementations(Graphic, Vector)] content: V,
) -> Result<V::Live<'e>, Interrupt> { ) -> Result<V::Live<'e>, Interrupt> {
let (content, _) = map_vectors(ctx.arena(), content, DAffine2::IDENTITY, |vector, transform| { let (content, _) = map_vectors(ctx.arena(), content, DAffine2::IDENTITY, |vector, transform| {
let mut result = vector let result = vector
.bounding_box_rect() .bounding_box_rect()
.map(|bbox| { .map(|bbox| {
let mut vector = Vector::default(); let mut vector = Vector::default();
@@ -1314,9 +1305,6 @@ fn bounding_box<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'sta
}) })
.unwrap_or_default(); .unwrap_or_default();
result.stroke = vector.stroke.clone();
result.set_stroke_transform(DAffine2::IDENTITY);
(result, transform) (result, transform)
})?; })?;
@@ -1556,11 +1544,7 @@ fn offset_path<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'stat
let transform = Affine::new(transform_attribute.to_cols_array()); let transform = Affine::new(transform_attribute.to_cols_array());
let bezpaths = vector.stroke_bezpath_iter(); let bezpaths = vector.stroke_bezpath_iter();
let mut result = Vector { let mut result = Vector { ..Default::default() };
stroke: vector.stroke.clone(),
..Default::default()
};
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 {
@@ -1593,16 +1577,23 @@ fn offset_path<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'stat
fn solidify_rows(flattened: List<Vector>) -> List<Vector> { fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
// TODO: Make this node support stroke align, which it currently ignores // TODO: Make this node support stroke align, which it currently ignores
// A fill exists when the canonical attribute carries paint // A fill exists when the row's appearance carries a painted fill coverage
let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint::<Fill, _>(&flattened, index)).collect(); let has_fills: Vec<bool> = (0..flattened.len())
.map(|index| {
flattened
.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, index)
.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill))
})
.collect();
let output: List<Vector> = flattened let output: List<Vector> = flattened
.into_iter() .into_iter()
.zip(has_fills) .zip(has_fills)
.flat_map(|(row, has_fill)| { .flat_map(|(row, has_fill)| {
let (mut vector, attributes) = row.into_parts(); let (vector, attributes) = row.into_parts();
let stroke = vector.stroke.clone().unwrap_or_default(); let appearance = attributes.get::<Appearance>(graphic_types::ATTR_APPEARANCE).cloned().unwrap_or_default();
let stroke = appearance.first_coverage_of(Cover::Stroke).map(Coverage::stroke_params).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();
@@ -1620,7 +1611,11 @@ fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
let dash_offset = stroke.dash_offset; let dash_offset = stroke.dash_offset;
let dash_pattern = stroke.dash_lengths; let dash_pattern = stroke.dash_lengths;
let miter_limit = stroke.join_miter_limit; let miter_limit = stroke.join_miter_limit;
let paint_order = stroke.paint_order; // The paint order rides the row's coverage order
let stroke_below = attributes
.get::<Appearance>(graphic_types::ATTR_APPEARANCE)
.map(Appearance::fill_and_stroke)
.is_some_and(|resolved| resolved.stroke_below);
let stroke_style = kurbo::Stroke::new(stroke.weight) let stroke_style = kurbo::Stroke::new(stroke.weight)
.with_caps(cap) .with_caps(cap)
@@ -1649,26 +1644,32 @@ fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
solidified_stroke.append_bezpath(solidified); solidified_stroke.append_bezpath(solidified);
} }
// 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 coverages dropped.
let fill_row = has_fill.then(|| { let fill_row = has_fill.then(|| {
vector.stroke = None;
let mut fill_attributes = attributes.clone(); let mut fill_attributes = attributes.clone();
// No stroke remains on the fill row if let Some(appearance) = fill_attributes.get_mut::<Appearance>(graphic_types::ATTR_APPEARANCE) {
fill_attributes.remove::<Option<List<Graphic>>>(ATTR_STROKE); appearance.retain_cover(Cover::Fill);
}
Item::from_parts(vector, fill_attributes) Item::from_parts(vector, fill_attributes)
}); });
let mut stroke_attributes = attributes; let mut stroke_attributes = attributes;
// Drop the original fill and use the stroke paint to fill the outlined stroke // Drop the original fill and use the stroke paint to fill the outlined stroke
stroke_attributes.remove::<Option<List<Graphic>>>(ATTR_FILL); if let Some(appearance) = stroke_attributes.get_mut::<Appearance>(graphic_types::ATTR_APPEARANCE) {
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL); // The outlined stroke is filled with the stroke coverage's paint
let stroke_paint = appearance.first_paint_of(Cover::Stroke).cloned();
*appearance = Appearance::default();
if let Some(paint) = stroke_paint {
appearance.replace_or_insert(Coverage::new_fill(), paint, CoverPlacement::Above);
}
}
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 coverage order. The first item in the `List` is rendered below the second.
match paint_order { match stroke_below {
PaintOrder::StrokeAbove => fill_row.into_iter().chain(std::iter::once(stroke_row)).collect::<Vec<_>>(), false => fill_row.into_iter().chain(std::iter::once(stroke_row)).collect::<Vec<_>>(),
PaintOrder::StrokeBelow => std::iter::once(stroke_row).chain(fill_row).collect::<Vec<_>>(), true => std::iter::once(stroke_row).chain(fill_row).collect::<Vec<_>>(),
} }
}) })
.collect(); .collect();
@@ -1689,8 +1690,7 @@ fn solidify_native_lane<'e>(
( (
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -1749,8 +1749,7 @@ fn emit_legacy_lane<'e>(
( (
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -1771,15 +1770,10 @@ fn emit_legacy_lane<'e>(
}; };
let element = output.element(lane).cloned().unwrap_or_default(); let element = output.element(lane).cloned().unwrap_or_default();
let fill = output let appearance = output
.attribute::<Option<List<Graphic>>>(ATTR_FILL, lane) .attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, lane)
.and_then(|paint| paint.as_ref()) .cloned()
.map(|paint| park_paint(arena, paint.clone())) .map(|appearance| park_appearance(arena, appearance))
.transpose()?;
let stroke = output
.attribute::<Option<List<Graphic>>>(ATTR_STROKE, lane)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?; .transpose()?;
let layer_path: Vec<NodeId> = output.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, lane).cloned().unwrap_or_default(); let layer_path: Vec<NodeId> = output.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, lane).cloned().unwrap_or_default();
let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0;
@@ -1792,8 +1786,7 @@ fn emit_legacy_lane<'e>(
Ok(( Ok((
element, element,
Attr(output.attribute_cloned_or_default(ATTR_TRANSFORM, lane)), Attr(output.attribute_cloned_or_default(ATTR_TRANSFORM, lane)),
Attr(fill), Attr(appearance),
Attr(stroke),
Attr(output.attribute_cloned_or_default(ATTR_BLEND_MODE, lane)), Attr(output.attribute_cloned_or_default(ATTR_BLEND_MODE, lane)),
Attr(output.attribute_cloned_or(ATTR_OPACITY, lane, 1.)), Attr(output.attribute_cloned_or(ATTR_OPACITY, lane, 1.)),
Attr(output.attribute_cloned_or(ATTR_OPACITY_FILL, lane, 1.)), Attr(output.attribute_cloned_or(ATTR_OPACITY_FILL, lane, 1.)),
@@ -1839,8 +1832,7 @@ fn solidify_stroke<'e>(
IList<( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -1879,8 +1871,7 @@ fn solidify_stroke_vector<'e>(
IList<( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -1925,7 +1916,6 @@ fn separate_subpaths_core(content: List<Vector>) -> List<Vector> {
return vec![row]; return vec![row];
} }
let stroke = row.element().stroke.clone();
let (_, attributes) = row.into_parts(); let (_, attributes) = row.into_parts();
bezpaths bezpaths
@@ -1933,7 +1923,6 @@ fn separate_subpaths_core(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.stroke = stroke.clone();
Item::from_parts(vector, attributes.clone()) Item::from_parts(vector, attributes.clone())
}) })
@@ -1951,8 +1940,7 @@ fn separate_subpaths<'e>(
IList<( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -2007,8 +1995,7 @@ fn map_points<'e>(
IList<( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -2063,8 +2050,7 @@ fn flatten_path_core<'e>(
( (
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>, Attr<'e, EditorMergedLayers>,
Option<usize>, Option<usize>,
@@ -2087,35 +2073,19 @@ fn flatten_path_core<'e>(
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 stroke
// Use the last encountered stroke as the output stroke
output.stroke = element.stroke.clone();
primary_source = Some((index, source_transform)); primary_source = Some((index, source_transform));
} }
let mut fill = None; // The primary row's appearance carries over whole, its paint transforms baked
let mut stroke = None; let mut appearance = None;
let mut layer_path = Vec::new(); let mut layer_path = Vec::new();
if let Some((primary, source_transform)) = primary_source { if let Some((primary, source_transform)) = primary_source {
let source_attributes = flattened.clone_item_attributes(primary); let source_attributes = flattened.clone_item_attributes(primary);
let mut attributes = ItemAttributeValues::new(); let mut attributes = ItemAttributeValues::new();
attributes.insert_cloned_from(&source_attributes, ATTR_FILL); attributes.insert_cloned_from(&source_attributes, graphic_types::ATTR_APPEARANCE);
attributes.insert_cloned_from(&source_attributes, ATTR_STROKE);
bake_paint_transforms(&mut attributes, source_transform); bake_paint_transforms(&mut attributes, source_transform);
appearance = attributes.remove::<Appearance>(graphic_types::ATTR_APPEARANCE).and_then(|appearance| appearance.declared().cloned());
let carrier = List::new_from_item(Item::from_parts(Vector::default(), attributes));
fill = carrier
.attribute::<Option<List<Graphic>>>(ATTR_FILL, 0)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?;
stroke = carrier
.attribute::<Option<List<Graphic>>>(ATTR_STROKE, 0)
.and_then(|paint| paint.as_ref())
.map(|paint| park_paint(arena, paint.clone()))
.transpose()?;
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer // Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
layer_path = flattened.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, primary); layer_path = flattened.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, primary);
@@ -2131,11 +2101,12 @@ fn flatten_path_core<'e>(
// editor click-target preservation, as the boolean operation does. // editor click-target preservation, as the boolean operation does.
let merged_layers = arena.alloc_sized_keyed(snapshot, 0).ok_or_else(exhausted)?.0; let merged_layers = arena.alloc_sized_keyed(snapshot, 0).ok_or_else(exhausted)?.0;
let appearance = appearance.map(|appearance| park_appearance(arena, appearance)).transpose()?;
Ok(( Ok((
output, output,
Attr(DAffine2::IDENTITY), Attr(DAffine2::IDENTITY),
Attr(fill), Attr(appearance),
Attr(stroke),
Attr(layer_path.as_slice()), Attr(layer_path.as_slice()),
Attr(Some(merged_layers)), Attr(Some(merged_layers)),
primary_source.map(|(index, _)| index), primary_source.map(|(index, _)| index),
@@ -2151,8 +2122,7 @@ pub fn combine_paths<'e>(
( (
Lane<Vector>, Lane<Vector>,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>, Attr<'e, EditorMergedLayers>,
), ),
@@ -2164,12 +2134,12 @@ pub fn combine_paths<'e>(
let item = content.as_group_item(); let item = content.as_group_item();
let (flattened, tops) = flatten_rows_with_top_lanes(graphic_types::graphic::GraphicLevel::Run(&item)); let (flattened, tops) = flatten_rows_with_top_lanes(graphic_types::graphic::GraphicLevel::Run(&item));
let snapshot = graphic_types::graphic::run_to_list::<Graphic>(&item).expect("the run holds the row's element type"); let snapshot = graphic_types::graphic::run_to_list::<Graphic>(&item).expect("the run holds the row's element type");
let (element, transform, fill, stroke, layer_path, merged, primary) = flatten_path_core(ctx.arena(), flattened, snapshot)?; let (element, transform, appearance, layer_path, merged, primary) = flatten_path_core(ctx.arena(), flattened, snapshot)?;
// The merge presents the blending of the top-level row its last contributing // The merge presents the blending of the top-level row its last contributing
// path came from, carried rather than re-read. The layer path stays an // path came from, carried rather than re-read. The layer path stays an
// override: it deliberately names the contributing CHILD layer, not the row. // override: it deliberately names the contributing CHILD layer, not the row.
let carrier = primary.and_then(|row| tops.get(row).copied()).unwrap_or(0); let carrier = primary.and_then(|row| tops.get(row).copied()).unwrap_or(0);
Ok((content.lane(carrier).map_element(element), transform, fill, stroke, layer_path, merged)) Ok((content.lane(carrier).map_element(element), transform, appearance, layer_path, merged))
} }
/// The path flattening over a plain vector level, as [`combine_paths`]. /// The path flattening over a plain vector level, as [`combine_paths`].
@@ -2182,8 +2152,7 @@ pub fn combine_paths_vector<'e>(
( (
Lane<Vector>, Lane<Vector>,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, EditorLayerPath>, Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>, Attr<'e, EditorMergedLayers>,
), ),
@@ -2195,12 +2164,12 @@ pub fn combine_paths_vector<'e>(
let wrapper = wrap_vector_level(content); let wrapper = wrap_vector_level(content);
let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Legacy(&wrapper)); let flattened = graphic_types::graphic::flatten_vector_rows(graphic_types::graphic::GraphicLevel::Legacy(&wrapper));
let snapshot = legacy_graphic_list_of(content); let snapshot = legacy_graphic_list_of(content);
let (element, transform, fill, stroke, layer_path, merged, primary) = flatten_path_core(ctx.arena(), flattened, snapshot)?; let (element, transform, appearance, layer_path, merged, primary) = flatten_path_core(ctx.arena(), flattened, snapshot)?;
// `top_lane` is degenerate here - the wrapper is one graphic lane holding the // `top_lane` is degenerate here - the wrapper is one graphic lane holding the
// whole vector run, so every row reports 0. The rows ARE the input lanes in // whole vector run, so every row reports 0. The rows ARE the input lanes in
// order though, so the contributing row index names the lane directly. // order though, so the contributing row index names the lane directly.
let carrier = primary.filter(|row| *row < content.len()).unwrap_or(0); let carrier = primary.filter(|row| *row < content.len()).unwrap_or(0);
Ok((content.lane(carrier).map_element(element), transform, fill, stroke, layer_path, merged)) Ok((content.lane(carrier).map_element(element), transform, appearance, layer_path, merged))
} }
pub use _combine_paths_vector_mod::combine_paths_vector_entries; pub use _combine_paths_vector_mod::combine_paths_vector_entries;
@@ -2235,16 +2204,13 @@ fn sample_polyline<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + '
} }
}; };
let mut element = element;
let mut result = Vector { let mut result = Vector {
point_domain: Default::default(), point_domain: Default::default(),
segment_domain: Default::default(), segment_domain: Default::default(),
region_domain: Default::default(), region_domain: Default::default(),
colinear_manipulators: Default::default(), colinear_manipulators: Default::default(),
stroke: std::mem::take(&mut element.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.set_stroke_transform(transform);
for local_bezpath in element.stroke_bezpath_iter() { for local_bezpath in 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)
@@ -2311,10 +2277,7 @@ fn simplify<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'static>
let transform = Affine::new(transform_attribute.to_cols_array()); let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse(); let inverse_transform = transform.inverse();
let mut result = Vector { let mut result = Vector { ..Default::default() };
stroke: content.stroke.clone(),
..Default::default()
};
for mut bezpath in content.stroke_bezpath_iter() { for mut bezpath in content.stroke_bezpath_iter() {
bezpath.apply_affine(transform); bezpath.apply_affine(transform);
@@ -2405,10 +2368,7 @@ fn decimate<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'static>
let transform = Affine::new(transform_attribute.to_cols_array()); let transform = Affine::new(transform_attribute.to_cols_array());
let inverse_transform = transform.inverse(); let inverse_transform = transform.inverse();
let mut result = Vector { let mut result = Vector { ..Default::default() };
stroke: content.stroke.clone(),
..Default::default()
};
for mut bezpath in content.stroke_bezpath_iter() { for mut bezpath in content.stroke_bezpath_iter() {
bezpath.apply_affine(transform); bezpath.apply_affine(transform);
@@ -2485,10 +2445,7 @@ fn cut_path_core(mut content: List<Vector>, progression: f64, reverse: bool, par
let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize }; let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize };
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::default();
stroke: content.element(row_index).unwrap().stroke.clone(),
..Default::default()
};
for (_, (_, bezpath)) in bezpaths.iter().enumerate().filter(|(i, (ri, _))| *i != index && *ri == row_index) { for (_, (_, bezpath)) in bezpaths.iter().enumerate().filter(|(i, (ri, _))| *i != index && *ri == row_index) {
result_vector.append_bezpath(bezpath.clone()); result_vector.append_bezpath(bezpath.clone());
@@ -2527,8 +2484,7 @@ fn cut_path<'e>(
IList<( IList<(
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -2738,8 +2694,6 @@ fn scatter_points<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 's
} }
// Transfer the style from the input vector content to the result. // Transfer the style from the input vector content to the result.
result.stroke = element.stroke.clone();
result.set_stroke_transform(DAffine2::IDENTITY);
(result, lane) (result, lane)
})?; })?;
@@ -2933,7 +2887,6 @@ fn offset_points<'e, V: MapVectorContent + Clone + Send + Sync + CacheHash + 'st
/// ///
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments. /// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progression: f64, reverse: bool, distribution: InterpolationDistribution, path: List<Vector>) -> List<Vector> { fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progression: f64, reverse: bool, distribution: InterpolationDistribution, path: List<Vector>) -> List<Vector> {
use core_types::lane::LaneSource;
/// Promotes a segment's handle pair to cubic-equivalent Bézier control points. /// Promotes a segment's handle pair to cubic-equivalent Bézier control points.
/// For linear segments (both None), handles are placed at their respective anchors (zero-length) /// For linear segments (both None), handles are placed at their respective anchors (zero-length)
/// so that interpolation against another zero-length cubic doesn't introduce unwanted curvature. /// so that interpolation against another zero-length cubic doesn't introduce unwanted curvature.
@@ -3092,6 +3045,51 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
} }
} }
/// Lerps two appearances pairing coverages by cover, so a fill and a stroke never interpolate into each other.
/// Stroke parameter pairs interpolate; other coverage pairings and the paint order step at the midpoint.
fn lerp_appearance(a: Option<&Appearance>, b: Option<&Appearance>, time: f64) -> Option<Appearance> {
if a.is_none() && b.is_none() {
return None;
}
let empty = Appearance::default();
let (a, b) = (a.unwrap_or(&empty), b.unwrap_or(&empty));
// The side holding the paint order at this time leads, so covers only the other side has follow behind it
let (leading, trailing) = if time < 0.5 { (a, b) } else { (b, a) };
let mut covers: Vec<Cover> = Vec::new();
for cover in leading.covers().chain(trailing.covers()).map(Coverage::cover) {
if !covers.contains(&cover) {
covers.push(cover);
}
}
let mut result = Appearance::default();
for cover in covers {
let (source_index, target_index) = (a.first_index_of(cover), b.first_index_of(cover));
// An unmatched stroke steps out at the midpoint, matching the stroke geometry, while an unmatched fill persists and fades
let coverage = match (source_index.and_then(|index| a.cover_at(index)), target_index.and_then(|index| b.cover_at(index))) {
(Some(source), Some(target)) if cover == Cover::Stroke => Coverage::new_stroke(&source.stroke_params().lerp(&target.stroke_params(), time)),
(Some(source), Some(target)) => (if time < 0.5 { source } else { target }).clone(),
(Some(_), None) if cover == Cover::Stroke && time >= 0.5 => continue,
(None, Some(_)) if cover == Cover::Stroke && time < 0.5 => continue,
(Some(source), None) => source.clone(),
(None, Some(target)) => target.clone(),
(None, None) => continue,
};
// An unmatched side falls to `None` here, which `lerp_graphic` fades against transparent.
// The paint cell carries its graphic list as one wrapped cell, so the lerp works on the unwrapped rows.
let source_paint = source_index.and_then(|index| a.paint_at(index)).and_then(graphic_types::graphic::paint_cell_rows);
let target_paint = target_index.and_then(|index| b.paint_at(index)).and_then(graphic_types::graphic::paint_cell_rows);
let paint = lerp_graphic(source_paint, target_paint, time).map(Graphic::Graphic).unwrap_or_default();
result.replace_or_insert(coverage, paint, CoverPlacement::Above);
}
Some(result)
}
// Preserve the original legacy snapshot as upstream data so this group layer's nested layers can be edited by the tools. // Preserve the original legacy snapshot as upstream data so this group layer's nested layers can be edited by the tools.
let mut graphic_list_content = snapshot; let mut graphic_list_content = snapshot;
@@ -3349,35 +3347,12 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
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 stroke = match (source_element.stroke.as_ref(), target_element.stroke.as_ref()) { let mut vector = Vector::default();
(Some(a), Some(b)) => Some(a.lerp(b, time)),
(Some(a), None) => {
if time < 0.5 {
Some(a.clone())
} else {
None
}
}
(None, Some(b)) => {
if time < 0.5 {
None
} else {
Some(b.clone())
}
}
(None, None) => None,
};
let mut vector = Vector { stroke, ..Default::default() };
let fill_paint = { let appearance = {
let source = content.attr::<Fill>(source_index).filter(|paint| is_paint_present(paint)); let source = content.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, source_index);
let target = content.attr::<Fill>(target_index).filter(|paint| is_paint_present(paint)); let target = content.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, target_index);
lerp_graphic(source, target, time) lerp_appearance(source, target, time)
};
let stroke_paint = {
let source = content.attr::<StrokeAttr>(source_index).filter(|paint| is_paint_present(paint));
let target = content.attr::<StrokeAttr>(target_index).filter(|paint| is_paint_present(paint));
lerp_graphic(source, target, time)
}; };
// Work directly with manipulator groups, bypassing the BezPath intermediate representation. // Work directly with manipulator groups, bypassing the BezPath intermediate representation.
@@ -3537,11 +3512,8 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content)); .with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content));
if let Some(fill) = fill_paint { if let Some(appearance) = appearance {
item.set_attribute(ATTR_FILL, Some(fill)); item.set_attribute(graphic_types::ATTR_APPEARANCE, appearance);
}
if let Some(stroke) = stroke_paint {
item.set_attribute(ATTR_STROKE, Some(stroke));
} }
List::new_from_item(item) List::new_from_item(item)
@@ -3562,8 +3534,7 @@ fn morph_lane<'e>(
( (
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -3602,8 +3573,7 @@ fn morph<'e>(
( (
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -3634,8 +3604,7 @@ fn morph_vector<'e>(
( (
Vector, Vector,
Attr<'e, TransformAttr>, Attr<'e, TransformAttr>,
Attr<'e, Fill>, Attr<'e, AppearanceMarker>,
Attr<'e, StrokeAttr>,
Attr<'e, BlendModeAttr>, Attr<'e, BlendModeAttr>,
Attr<'e, Opacity>, Attr<'e, Opacity>,
Attr<'e, OpacityFill>, Attr<'e, OpacityFill>,
@@ -4081,7 +4050,6 @@ fn centroid(_: impl Ctx, vector: IList<Vector>, centroid_type: CentroidType) ->
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use graphic_types::graphic::paint_graphics;
use kurbo::{CubicBez, Ellipse, Point, Rect}; use kurbo::{CubicBez, Ellipse, Point, Rect};
use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg}; use vector_types::vector::algorithms::bezpath_algorithms::{TValue, trim_pathseg};
use vector_types::vector::misc::pathseg_abs_diff_eq; use vector_types::vector::misc::pathseg_abs_diff_eq;
@@ -4376,12 +4344,13 @@ mod test {
v v
}; };
let fill_appearance = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::Graphic(List::new_from_element(color).into_graphic_list()));
let item_a = Item::new_from_element(rect()) let item_a = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
.with_attribute(ATTR_FILL, Some(List::new_from_element(Color::RED).into_graphic_list())); .with_attribute(graphic_types::ATTR_APPEARANCE, fill_appearance(Color::RED));
let item_b = Item::new_from_element(rect()) let item_b = Item::new_from_element(rect())
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into())) .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into()))
.with_attribute(ATTR_FILL, Some(List::new_from_element(Color::BLUE).into_graphic_list())); .with_attribute(graphic_types::ATTR_APPEARANCE, fill_appearance(Color::BLUE));
let mut content = List::new_from_item(item_a); let mut content = List::new_from_item(item_a);
content.push(item_b); content.push(item_b);
@@ -4389,7 +4358,13 @@ mod test {
let snapshot = content.into_graphic_list(); let snapshot = content.into_graphic_list();
let morphed = super::morph_core(snapshot.clone().into_flattened_list(), snapshot, 0.5, false, InterpolationDistribution::default(), List::default()); let morphed = super::morph_core(snapshot.clone().into_flattened_list(), snapshot, 0.5, false, InterpolationDistribution::default(), List::default());
let fill = paint_graphics::<Fill, _>(&morphed, 0).expect("Morph should keep the fill paint at the midpoint"); let appearance = morphed
.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, 0)
.expect("Morph should keep the appearance at the midpoint");
let fill = appearance
.first_paint_of(Cover::Fill)
.and_then(graphic_types::graphic::paint_cell_rows)
.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(color)) = fill.element(0) else { let Some(Graphic::Color(color)) = fill.element(0) else {