mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 11:08:11 +08:00
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:
co-authored by
Dennis Kobert
parent
28e5d1bdc9
commit
a41b362fe2
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
File diff suppressed because one or more lines are too long
Generated
+1
-1
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,
|
||||||
|
|||||||
+9
-2
@@ -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>) {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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""#),
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user