mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-04 11:58:11 +08:00
Retire the Vector stroke field into the appearance coverage
Stroke parameters live only on the appearance's stroke coverage now. Vector loses its stroke field, transform normalization, and concat stroke adoption; solidify, morph, combine, and the recolor read the coverage instead, with morph gaining the cover-paired appearance lerp. The stroke-inclusive bounds take the stroke as a parameter, the plain vector bounds ignore include_stroke, the editor's metadata channel carries one resolved appearance snapshot per layer, and the data panel's Vector table drops its stroke properties tab for a handles tab. Legacy vector payloads parse their stroke solely to validate the shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d278fd666d
commit
ff074b2a1c
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,7 +376,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:?}"))
|
||||||
@@ -388,57 +388,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(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)| {
|
||||||
@@ -469,6 +418,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 the stroke paint snapshot, 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 {
|
||||||
|
|||||||
@@ -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 fill paint snapshot, 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 stroke paint snapshot, 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ 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::Appearance;
|
||||||
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;
|
||||||
@@ -3439,14 +3438,9 @@ 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 fill paint 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<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 stroke paint 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -715,8 +715,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;
|
||||||
|
|
||||||
@@ -731,8 +730,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);
|
||||||
|
|||||||
@@ -419,9 +419,8 @@ impl<'e> Graphic<'e> {
|
|||||||
pub fn is_fully_transparent(&self) -> bool {
|
pub fn is_fully_transparent(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
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,
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ pub mod migrations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The legacy `fill` field is intentionally omitted because vector payload migration only
|
/// The legacy `fill` field is intentionally omitted because vector payload migration only
|
||||||
/// recovers editable vector data. The fill/stroke paints are migrated from the the node inputs.
|
/// recovers editable vector data. The stroke parses solely to validate the legacy shape.
|
||||||
#[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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +105,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,
|
||||||
@@ -136,7 +138,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,
|
||||||
@@ -153,9 +154,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()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,22 +172,21 @@ pub mod migrations {
|
|||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert("fill".into(), serde_json::to_value(legacy::Fill::default()).unwrap());
|
.insert("fill".into(), serde_json::to_value(legacy::Fill::default()).unwrap());
|
||||||
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
let migrated = migrate_to_optional_vector(value).unwrap().expect("the legacy shape parses into a vector");
|
||||||
|
|
||||||
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.)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -540,12 +540,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 fill paint snapshot from the resolved appearance, 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 stroke paint snapshot from the resolved appearance, 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,8 +566,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());
|
||||||
@@ -581,8 +577,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 {
|
||||||
@@ -1352,7 +1347,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, inherited_appe
|
|||||||
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]);
|
||||||
@@ -1404,8 +1399,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, inherited_appe
|
|||||||
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.
|
||||||
@@ -1759,8 +1753,7 @@ fn render_vector_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.
|
||||||
@@ -1894,11 +1887,8 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(
|
|||||||
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) = resolved.fill_paint.and_then(paint_cell_rows) {
|
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) = resolved.stroke_paint.and_then(paint_cell_rows) {
|
|
||||||
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3212,7 +3202,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]
|
||||||
|
|||||||
@@ -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 dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||||
@@ -18,8 +17,6 @@ use std::collections::HashMap;
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Vector {
|
pub struct Vector {
|
||||||
pub 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]>,
|
||||||
@@ -35,7 +32,6 @@ unsafe impl StaticType for 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(),
|
||||||
@@ -49,7 +45,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,10 +234,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());
|
||||||
@@ -530,40 +525,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -54,7 +54,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);
|
||||||
@@ -236,12 +235,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()
|
||||||
};
|
};
|
||||||
@@ -308,10 +302,7 @@ fn fill_appearance(paint: List<Graphic<'static>>) -> Appearance {
|
|||||||
fn color_paint_row(color: Color, mut attributes: core_types::list::ItemAttributeValues) -> Item<Vector> {
|
fn color_paint_row(color: Color, mut attributes: core_types::list::ItemAttributeValues) -> Item<Vector> {
|
||||||
attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(List::new_from_element(Graphic::Color(color))));
|
attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(List::new_from_element(Graphic::Color(color))));
|
||||||
|
|
||||||
let mut element = Vector::default();
|
Item::from_parts(Vector::default(), attributes)
|
||||||
element.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
|
||||||
Item::from_parts(element, attributes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A gradient row: an empty vector carrying the stops as its fill paint, the
|
/// A gradient row: an empty vector carrying the stops as its fill paint, the
|
||||||
@@ -329,10 +320,7 @@ fn gradient_paint_row(stops: GradientStops, mut attributes: core_types::list::It
|
|||||||
}
|
}
|
||||||
attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(gradient_paint));
|
attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(gradient_paint));
|
||||||
|
|
||||||
let mut element = Vector::default();
|
Item::from_parts(Vector::default(), attributes)
|
||||||
element.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
|
||||||
Item::from_parts(element, attributes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A text lane's rows: the shaped glyph vectors under the composed transform.
|
/// A text lane's rows: the shaped glyph vectors under the composed transform.
|
||||||
|
|||||||
@@ -132,10 +132,11 @@ fn assign_colors<'e>(
|
|||||||
let mut appearance = existing_appearance.unwrap_or_default();
|
let mut appearance = existing_appearance.unwrap_or_default();
|
||||||
let paint_cell = Graphic::Graphic(paint);
|
let paint_cell = Graphic::Graphic(paint);
|
||||||
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
|
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
|
||||||
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above);
|
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Below);
|
||||||
}
|
}
|
||||||
if stroke && element.stroke.is_some() && !appearance.set_paint_of(Cover::Stroke, paint_cell.clone()) {
|
// The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
|
||||||
appearance.replace_or_insert(Coverage::new_stroke(&element.stroke.clone().unwrap_or_default()), paint_cell, CoverPlacement::Above);
|
if stroke {
|
||||||
|
appearance.set_paint_of(Cover::Stroke, paint_cell);
|
||||||
}
|
}
|
||||||
let parked_appearance = park_appearance_attr(Some(appearance))?;
|
let parked_appearance = park_appearance_attr(Some(appearance))?;
|
||||||
|
|
||||||
@@ -230,7 +231,6 @@ 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 row_stroke = rows.element(row).and_then(|vector| vector.stroke.clone());
|
|
||||||
let color = assign_color_at(gradient_element, position + row, length, randomize, seed, repeat_every);
|
let color = assign_color_at(gradient_element, 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();
|
||||||
|
|
||||||
@@ -238,10 +238,11 @@ fn assign_colors_graphic<'e>(
|
|||||||
let mut appearance = rows.attribute_cloned_or_default::<Appearance>(graphic_types::ATTR_APPEARANCE, row);
|
let mut appearance = rows.attribute_cloned_or_default::<Appearance>(graphic_types::ATTR_APPEARANCE, row);
|
||||||
let paint_cell = Graphic::Graphic(paint);
|
let paint_cell = Graphic::Graphic(paint);
|
||||||
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
|
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
|
||||||
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above);
|
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Below);
|
||||||
}
|
}
|
||||||
if stroke && row_stroke.is_some() && !appearance.set_paint_of(Cover::Stroke, paint_cell.clone()) {
|
// The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
|
||||||
appearance.replace_or_insert(Coverage::new_stroke(&row_stroke.unwrap_or_default()), paint_cell, CoverPlacement::Above);
|
if stroke {
|
||||||
|
appearance.set_paint_of(Cover::Stroke, paint_cell);
|
||||||
}
|
}
|
||||||
rows.set_attribute(graphic_types::ATTR_APPEARANCE, row, appearance);
|
rows.set_attribute(graphic_types::ATTR_APPEARANCE, row, appearance);
|
||||||
}
|
}
|
||||||
@@ -421,7 +422,7 @@ fn stroke<'e>(
|
|||||||
dash_offset: f64,
|
dash_offset: f64,
|
||||||
) -> Result<(Vector, Attr<TransformAttr>, Attr<'e, AppearanceMarker>), Interrupt> {
|
) -> Result<(Vector, Attr<TransformAttr>, Attr<'e, AppearanceMarker>), Interrupt> {
|
||||||
let dash_lengths: Vec<f64> = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect();
|
let dash_lengths: Vec<f64> = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect();
|
||||||
let mut stroke = Stroke {
|
let stroke = Stroke {
|
||||||
weight,
|
weight,
|
||||||
dash_lengths,
|
dash_lengths,
|
||||||
dash_offset,
|
dash_offset,
|
||||||
@@ -434,14 +435,9 @@ fn stroke<'e>(
|
|||||||
|
|
||||||
// The coverage records the stroke's authoring space, so the item transform is composed in, translation
|
// The coverage records the stroke's authoring space, so the item transform is composed in, translation
|
||||||
// included so the render consumers see the exact legacy stroke space.
|
// included so the render consumers see the exact legacy stroke space.
|
||||||
let mut coverage_stroke = stroke.clone();
|
let mut coverage_stroke = stroke;
|
||||||
coverage_stroke.transform *= *content_transform;
|
coverage_stroke.transform *= *content_transform;
|
||||||
|
|
||||||
stroke.transform *= *content_transform;
|
|
||||||
|
|
||||||
let mut element = element;
|
|
||||||
element.stroke = Some(stroke);
|
|
||||||
|
|
||||||
let paint = paint_table(paint);
|
let paint = paint_table(paint);
|
||||||
// The paint order is the coverage row order: appending above follows the painter's algorithm, and a
|
// The paint order is the coverage row order: appending above follows the painter's algorithm, and a
|
||||||
// below stroke is expressed by the chain running the stroke node before the fill
|
// below stroke is expressed by the chain running the stroke node before the fill
|
||||||
@@ -450,23 +446,6 @@ fn stroke<'e>(
|
|||||||
Ok((element, Attr(*content_transform), Attr(Some(parked_appearance))))
|
Ok((element, Attr(*content_transform), Attr(Some(parked_appearance))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 stroke over graphic lanes: the style applies to the interior vectors,
|
||||||
/// the paint marker parks on the lane for the render boundary to place.
|
/// the paint marker parks on the lane for the render boundary to place.
|
||||||
/// Registered under the stroke's identifier.
|
/// Registered under the stroke's identifier.
|
||||||
@@ -498,16 +477,9 @@ fn stroke_graphic_leveled<'e>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The coverage records the stroke's authoring space at the lane, composing the lane transform as in `stroke` above.
|
// The coverage records the stroke's authoring space at the lane, composing the lane transform as in `stroke` above.
|
||||||
let mut coverage_stroke = stroke.clone();
|
let mut coverage_stroke = stroke;
|
||||||
coverage_stroke.transform *= *content_transform;
|
coverage_stroke.transform *= *content_transform;
|
||||||
|
|
||||||
let mut element = element;
|
|
||||||
for_each_interior_vector_mut(&mut element, |vector, transform| {
|
|
||||||
let mut stroke = stroke.clone();
|
|
||||||
stroke.transform *= transform;
|
|
||||||
vector.stroke = Some(stroke);
|
|
||||||
});
|
|
||||||
|
|
||||||
let paint = paint_table(paint);
|
let paint = paint_table(paint);
|
||||||
// The paint order is the coverage row order: appending above follows the painter's algorithm, and a
|
// The paint order is the coverage row order: appending above follows the painter's algorithm, and a
|
||||||
// below stroke is expressed by the chain running the stroke node before the fill
|
// below stroke is expressed by the chain running the stroke node before the fill
|
||||||
@@ -654,10 +626,7 @@ fn round_corners(
|
|||||||
// 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();
|
||||||
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());
|
||||||
@@ -1016,8 +985,6 @@ fn box_warp(_: impl Ctx, (vector, transform): (Vector, Attr<TransformAttr>), #[e
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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, Attr(DAffine2::IDENTITY))
|
(result, Attr(DAffine2::IDENTITY))
|
||||||
}
|
}
|
||||||
@@ -1163,10 +1130,7 @@ fn auto_tangents(
|
|||||||
) -> (Vector, Attr<TransformAttr>) {
|
) -> (Vector, Attr<TransformAttr>) {
|
||||||
let transform: DAffine2 = *lane_transform;
|
let transform: DAffine2 = *lane_transform;
|
||||||
|
|
||||||
let mut result = Vector {
|
let mut result = Vector::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);
|
||||||
@@ -1298,19 +1262,14 @@ fn auto_tangents(
|
|||||||
|
|
||||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||||
fn bounding_box(_: impl Ctx, vector: Vector) -> Vector {
|
fn bounding_box(_: impl Ctx, vector: Vector) -> Vector {
|
||||||
let mut result = vector
|
vector
|
||||||
.bounding_box_rect()
|
.bounding_box_rect()
|
||||||
.map(|bbox| {
|
.map(|bbox| {
|
||||||
let mut vector = Vector::default();
|
let mut vector = Vector::default();
|
||||||
vector.append_bezpath(bbox.to_path(DEFAULT_ACCURACY));
|
vector.append_bezpath(bbox.to_path(DEFAULT_ACCURACY));
|
||||||
vector
|
vector
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default()
|
||||||
|
|
||||||
result.stroke = vector.stroke.clone();
|
|
||||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||||
@@ -1361,11 +1320,7 @@ fn offset_path(_: impl Ctx, (vector, lane_transform): (Vector, Attr<TransformAtt
|
|||||||
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();
|
||||||
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 {
|
||||||
@@ -1408,9 +1363,10 @@ fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
|
|||||||
.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();
|
||||||
|
|
||||||
@@ -1461,11 +1417,9 @@ 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) {
|
if let Some(appearance) = fill_attributes.get_mut::<Appearance>(graphic_types::ATTR_APPEARANCE) {
|
||||||
appearance.retain_cover(Cover::Fill);
|
appearance.retain_cover(Cover::Fill);
|
||||||
}
|
}
|
||||||
@@ -1735,7 +1689,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
|
||||||
@@ -1743,7 +1696,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())
|
||||||
})
|
})
|
||||||
@@ -1869,15 +1821,11 @@ 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_cell = None;
|
// The primary row's appearance carries over whole, its paint transforms baked
|
||||||
let mut stroke_cell = 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);
|
||||||
@@ -1885,11 +1833,7 @@ fn flatten_path_core<'e>(
|
|||||||
|
|
||||||
attributes.insert_cloned_from(&source_attributes, graphic_types::ATTR_APPEARANCE);
|
attributes.insert_cloned_from(&source_attributes, graphic_types::ATTR_APPEARANCE);
|
||||||
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());
|
||||||
if let Some(appearance) = attributes.get::<Appearance>(graphic_types::ATTR_APPEARANCE) {
|
|
||||||
fill_cell = appearance.first_paint_of(Cover::Fill).filter(|cell| !cell.is_empty()).cloned();
|
|
||||||
stroke_cell = appearance.first_paint_of(Cover::Stroke).filter(|cell| !cell.is_empty()).cloned();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -1905,21 +1849,7 @@ 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;
|
||||||
|
|
||||||
// The carried paints land on the appearance, the stroke coverage recording the carried stroke's parameters
|
let appearance = appearance.map(|appearance| park_appearance(arena, appearance)).transpose()?;
|
||||||
let appearance = {
|
|
||||||
let mut appearance = Appearance::default();
|
|
||||||
if let Some(cell) = fill_cell {
|
|
||||||
appearance.replace_or_insert(Coverage::new_fill(), cell, CoverPlacement::Above);
|
|
||||||
}
|
|
||||||
if let Some(cell) = stroke_cell {
|
|
||||||
let coverage = Coverage::new_stroke(&output.stroke.clone().unwrap_or_default());
|
|
||||||
appearance.replace_or_insert(coverage, cell, CoverPlacement::Above);
|
|
||||||
}
|
|
||||||
match appearance.declared().is_some() {
|
|
||||||
true => Some(park_appearance(arena, appearance)?),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((output, Attr(DAffine2::IDENTITY), Attr(appearance), Attr(layer_path.as_slice()), Attr(Some(merged_layers))))
|
Ok((output, Attr(DAffine2::IDENTITY), Attr(appearance), Attr(layer_path.as_slice()), Attr(Some(merged_layers))))
|
||||||
}
|
}
|
||||||
@@ -1980,16 +1910,7 @@ fn sample_polyline(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut element = element;
|
let mut result = Vector::default();
|
||||||
let mut result = Vector {
|
|
||||||
point_domain: Default::default(),
|
|
||||||
segment_domain: Default::default(),
|
|
||||||
region_domain: 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.
|
|
||||||
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)
|
||||||
@@ -2052,10 +1973,7 @@ fn simplify(
|
|||||||
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();
|
||||||
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);
|
||||||
@@ -2142,10 +2060,7 @@ fn decimate(
|
|||||||
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();
|
||||||
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);
|
||||||
@@ -2219,10 +2134,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());
|
||||||
@@ -2463,8 +2375,6 @@ fn scatter_points(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transfer the style from the input vector content to the result.
|
// Transfer the style from the input vector content to the result.
|
||||||
result.stroke = element.stroke.clone();
|
|
||||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -2800,6 +2710,51 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lerp between 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;
|
||||||
|
|
||||||
@@ -3057,34 +3012,13 @@ 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 coverage_paint = |index: usize, cover: Cover| {
|
let appearance = {
|
||||||
content
|
let source = content.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, source_index);
|
||||||
.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, index)
|
let target = content.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, target_index);
|
||||||
.and_then(|appearance| appearance.first_paint_of(cover))
|
lerp_appearance(source, target, time)
|
||||||
.and_then(graphic_types::graphic::paint_cell_rows)
|
|
||||||
};
|
};
|
||||||
let fill_paint = lerp_graphic(coverage_paint(source_index, Cover::Fill), coverage_paint(target_index, Cover::Fill), time);
|
|
||||||
let stroke_paint = lerp_graphic(coverage_paint(source_index, Cover::Stroke), coverage_paint(target_index, Cover::Stroke), time);
|
|
||||||
|
|
||||||
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
||||||
// This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame.
|
// This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame.
|
||||||
@@ -3243,16 +3177,7 @@ 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));
|
||||||
|
|
||||||
// The lerped paints land on the appearance, the stroke coverage recording the lerped stroke's parameters
|
if let Some(appearance) = appearance {
|
||||||
let mut appearance = Appearance::default();
|
|
||||||
if let Some(fill) = fill_paint {
|
|
||||||
appearance.replace_or_insert(Coverage::new_fill(), Graphic::Graphic(fill), CoverPlacement::Above);
|
|
||||||
}
|
|
||||||
if let Some(stroke) = stroke_paint {
|
|
||||||
let coverage = Coverage::new_stroke(&item.element().stroke.clone().unwrap_or_default());
|
|
||||||
appearance.replace_or_insert(coverage, Graphic::Graphic(stroke), CoverPlacement::Above);
|
|
||||||
}
|
|
||||||
if appearance.declared().is_some() {
|
|
||||||
item.set_attribute(graphic_types::ATTR_APPEARANCE, appearance);
|
item.set_attribute(graphic_types::ATTR_APPEARANCE, appearance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user