mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 03:28:12 +08:00
Adopt the cascading "appearance" attribute in place of Vector::stroke and the "fill"/"paint" attributes (#4433)
* Add the appearance model types and attribute constants * Dual-write the appearance attribute alongside the fill/stroke pair in all paint-writing nodes * Read paint from the appearance attribute in the renderer, analysis, metadata, and editor, cascading from ancestors * Retire the fill/stroke attribute pair and the Vector stroke field in favor of the appearance attribute * Replace the Stroke node's paint order input with the relative chain order of the Fill and Stroke nodes * Code review fixes * Re-save demo art * Stamp coverages in place and fuse the renderer's per-item appearance reads into single walks * Treat an empty appearance as the undeclared state so padded rows inherit instead of blocking the cascade * Update demo art * Treat padded appearance rows as undeclared in the boolean flatten's group recursion
This commit is contained in:
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
@@ -8,5 +8,7 @@ pub enum EventMessage {
|
|||||||
CanvasTransformed,
|
CanvasTransformed,
|
||||||
ToolAbort,
|
ToolAbort,
|
||||||
SelectionChanged,
|
SelectionChanged,
|
||||||
|
/// The document graph's nodes or wires changed, so state derived from the selection's chains may be stale
|
||||||
|
GraphChanged,
|
||||||
WorkingColorChanged,
|
WorkingColorChanged,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup,
|
|||||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessage, PathStep};
|
use crate::messages::portfolio::document::data_panel::{DataPanelMessage, PathStep};
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{format_rounded, round_away_float_noise};
|
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||||
use glam::{Affine2, DAffine2, Vec2};
|
use glam::{Affine2, DAffine2, Vec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
@@ -18,6 +18,7 @@ use graphene_std::raster::{
|
|||||||
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
||||||
};
|
};
|
||||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||||
|
use graphene_std::subpath::BezierHandles;
|
||||||
use graphene_std::text::TextAlign;
|
use graphene_std::text::TextAlign;
|
||||||
use graphene_std::text_nodes::StringCapitalization;
|
use graphene_std::text_nodes::StringCapitalization;
|
||||||
use graphene_std::transform::{ReferencePoint, ScaleType};
|
use graphene_std::transform::{ReferencePoint, ScaleType};
|
||||||
@@ -25,10 +26,10 @@ use graphene_std::vector::misc::{
|
|||||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{
|
use graphene_std::vector::style::{
|
||||||
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin,
|
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
|
||||||
use graphene_std::{Artboard, Color, Context, Graphic};
|
use graphene_std::{Appearance, Artboard, Color, Context, Cover, Coverage, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -228,7 +229,6 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
List<StrokeJoin>,
|
List<StrokeJoin>,
|
||||||
List<StrokeAlign>,
|
List<StrokeAlign>,
|
||||||
List<StrokeCap>,
|
List<StrokeCap>,
|
||||||
List<PaintOrder>,
|
|
||||||
List<MergeByDistanceAlgorithm>,
|
List<MergeByDistanceAlgorithm>,
|
||||||
List<ExtrudeJoiningAlgorithm>,
|
List<ExtrudeJoiningAlgorithm>,
|
||||||
List<PointSpacingType>,
|
List<PointSpacingType>,
|
||||||
@@ -284,7 +284,6 @@ fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'st
|
|||||||
Item<StrokeJoin>,
|
Item<StrokeJoin>,
|
||||||
Item<StrokeAlign>,
|
Item<StrokeAlign>,
|
||||||
Item<StrokeCap>,
|
Item<StrokeCap>,
|
||||||
Item<PaintOrder>,
|
|
||||||
Item<MergeByDistanceAlgorithm>,
|
Item<MergeByDistanceAlgorithm>,
|
||||||
Item<ExtrudeJoiningAlgorithm>,
|
Item<ExtrudeJoiningAlgorithm>,
|
||||||
Item<PointSpacingType>,
|
Item<PointSpacingType>,
|
||||||
@@ -513,6 +512,45 @@ impl TableItemLayout for DashPattern {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TableItemLayout for Appearance {
|
||||||
|
fn type_name() -> &'static str {
|
||||||
|
"Appearance"
|
||||||
|
}
|
||||||
|
fn identifier(&self) -> String {
|
||||||
|
"Appearance".to_string()
|
||||||
|
}
|
||||||
|
// The wrapping `Item` already contributes the breadcrumb; the inner list supplies the next level
|
||||||
|
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
|
self.value_page(data)
|
||||||
|
}
|
||||||
|
// Label the spreadsheet's element button with the inner list's identifier, like Artboard
|
||||||
|
fn value_widgets(&self, target: PathStep, data: &LayoutData) -> Vec<WidgetInstance> {
|
||||||
|
self.0.value_widgets(target, data)
|
||||||
|
}
|
||||||
|
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
|
self.0.layout_with_breadcrumb(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableItemLayout for Coverage {
|
||||||
|
fn type_name() -> &'static str {
|
||||||
|
"Coverage"
|
||||||
|
}
|
||||||
|
fn identifier(&self) -> String {
|
||||||
|
"Coverage".to_string()
|
||||||
|
}
|
||||||
|
// The wrapping row already contributes the breadcrumb; the inner item supplies the next level
|
||||||
|
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
|
self.value_page(data)
|
||||||
|
}
|
||||||
|
fn value_widgets(&self, target: PathStep, data: &LayoutData) -> Vec<WidgetInstance> {
|
||||||
|
self.0.value_widgets(target, data)
|
||||||
|
}
|
||||||
|
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||||
|
self.0.layout_with_breadcrumb(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TableItemLayout for BoxCorners {
|
impl TableItemLayout for BoxCorners {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"BoxCorners"
|
"BoxCorners"
|
||||||
@@ -581,7 +619,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:?}"))
|
||||||
@@ -593,89 +631,49 @@ 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 => {
|
VectorTableTab::Handles => {
|
||||||
table_rows.push(column_headings(&["property", "value"]));
|
table_rows.push(column_headings(&["", "colinear_manipulators[0]", "colinear_manipulators[1]"]));
|
||||||
|
table_rows.extend(self.colinear_manipulators.iter().enumerate().map(|(index, [a, b])| {
|
||||||
if let Some(stroke) = self.stroke.as_ref() {
|
vec![
|
||||||
table_rows.push(vec![
|
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
|
||||||
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
|
TextLabel::new(format!("{a}")).narrow(true).widget_instance(),
|
||||||
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(),
|
TextLabel::new(format!("{b}")).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)| {
|
||||||
let position = DVec2::new(round_away_float_noise(position.x), round_away_float_noise(position.y));
|
|
||||||
vec![
|
vec![
|
||||||
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
||||||
TextLabel::new(format!("{position}")).narrow(true).widget_instance(),
|
TextLabel::new(format_dvec2(position)).narrow(true).widget_instance(),
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
VectorTableTab::Segments => {
|
VectorTableTab::Segments => {
|
||||||
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
|
table_rows.push(column_headings(&["", "start_point", "end_point", "handles"]));
|
||||||
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
|
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
|
||||||
|
let handles = match handles {
|
||||||
|
BezierHandles::Linear => "Linear".to_string(),
|
||||||
|
BezierHandles::Quadratic { handle } => format!("Quadratic — {}", format_dvec2(handle)),
|
||||||
|
BezierHandles::Cubic { handle_start, handle_end } => format!("Cubic — start: {}, end: {}", format_dvec2(handle_start), format_dvec2(handle_end)),
|
||||||
|
};
|
||||||
vec![
|
vec![
|
||||||
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
||||||
TextLabel::new(format!("{start}")).narrow(true).widget_instance(),
|
TextLabel::new(format!("Point {start}")).narrow(true).widget_instance(),
|
||||||
TextLabel::new(format!("{end}")).narrow(true).widget_instance(),
|
TextLabel::new(format!("Point {end}")).narrow(true).widget_instance(),
|
||||||
TextLabel::new(format!("{handles:?}")).narrow(true).widget_instance(),
|
TextLabel::new(handles).narrow(true).widget_instance(),
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
VectorTableTab::Regions => {
|
VectorTableTab::Regions => {
|
||||||
table_rows.push(column_headings(&["", "segment_range", "fill"]));
|
table_rows.push(column_headings(&["", "segment_range"]));
|
||||||
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
|
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, _)| {
|
||||||
vec![
|
vec![
|
||||||
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(),
|
||||||
TextLabel::new(format!("{segment_range:?}")).narrow(true).widget_instance(),
|
TextLabel::new(format!("Segment {} – Segment {}", segment_range.start().inner(), segment_range.end().inner()))
|
||||||
TextLabel::new(format!("{}", fill.inner())).narrow(true).widget_instance(),
|
.narrow(true)
|
||||||
|
.widget_instance(),
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1019,6 +1017,7 @@ macro_rules! impl_table_item_layout_for_choice_enum {
|
|||||||
}
|
}
|
||||||
impl_table_item_layout_for_choice_enum!(
|
impl_table_item_layout_for_choice_enum!(
|
||||||
BlendMode,
|
BlendMode,
|
||||||
|
Cover,
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientSpace,
|
GradientSpace,
|
||||||
@@ -1027,7 +1026,6 @@ impl_table_item_layout_for_choice_enum!(
|
|||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
PaintOrder,
|
|
||||||
MergeByDistanceAlgorithm,
|
MergeByDistanceAlgorithm,
|
||||||
ExtrudeJoiningAlgorithm,
|
ExtrudeJoiningAlgorithm,
|
||||||
PointSpacingType,
|
PointSpacingType,
|
||||||
@@ -1231,6 +1229,9 @@ macro_rules! known_item_types {
|
|||||||
Raster<GPU>,
|
Raster<GPU>,
|
||||||
Graphic,
|
Graphic,
|
||||||
Artboard,
|
Artboard,
|
||||||
|
Appearance,
|
||||||
|
Coverage,
|
||||||
|
Cover,
|
||||||
DashPattern,
|
DashPattern,
|
||||||
BoxCorners,
|
BoxCorners,
|
||||||
BlendMode,
|
BlendMode,
|
||||||
@@ -1242,7 +1243,6 @@ macro_rules! known_item_types {
|
|||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
PaintOrder,
|
|
||||||
MergeByDistanceAlgorithm,
|
MergeByDistanceAlgorithm,
|
||||||
ExtrudeJoiningAlgorithm,
|
ExtrudeJoiningAlgorithm,
|
||||||
PointSpacingType,
|
PointSpacingType,
|
||||||
|
|||||||
@@ -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>>>,
|
appearance_attributes: HashMap<NodeId, Arc<Appearance>>,
|
||||||
},
|
|
||||||
#[serde(skip)]
|
|
||||||
UpdateStrokeAttributes {
|
|
||||||
stroke_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
|
|
||||||
},
|
},
|
||||||
Undo,
|
Undo,
|
||||||
UngroupSelectedLayers,
|
UngroupSelectedLayers,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ use graph_craft::application_io::wgpu_available;
|
|||||||
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 graph_craft::list;
|
use graph_craft::list;
|
||||||
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,21 @@ 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.element(0).is_some_and(|graphic| !graphic.is_fully_transparent()))
|
||||||
|
});
|
||||||
|
|
||||||
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
// 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,
|
||||||
},
|
},
|
||||||
|
StrokeOrderSet {
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
paint_order: PaintOrder,
|
||||||
|
},
|
||||||
TransformChange {
|
TransformChange {
|
||||||
layer: LayerNodeIdentifier,
|
layer: LayerNodeIdentifier,
|
||||||
transform: DAffine2,
|
transform: DAffine2,
|
||||||
|
|||||||
+89
-4
@@ -1,8 +1,8 @@
|
|||||||
use super::transform_utils;
|
use super::transform_utils;
|
||||||
use super::utility_types::ModifyInputsContext;
|
use super::utility_types::{ModifyInputsContext, set_stroke_paint_order};
|
||||||
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
||||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::BLEND_PATH_INPUT_INDEX;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
||||||
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
||||||
@@ -13,7 +13,7 @@ use graph_craft::document::{NodeId, NodeInput};
|
|||||||
use graph_craft::list;
|
use graph_craft::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)]
|
||||||
@@ -131,6 +131,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
|||||||
modify_inputs.stroke_set(color, stroke);
|
modify_inputs.stroke_set(color, stroke);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphOperationMessage::StrokeOrderSet { layer, paint_order } => {
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let Some(stroke_node_id) = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if set_stroke_paint_order(network_interface, &[], stroke_node_id, paint_order) {
|
||||||
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
GraphOperationMessage::TransformChange {
|
GraphOperationMessage::TransformChange {
|
||||||
layer,
|
layer,
|
||||||
transform,
|
transform,
|
||||||
@@ -944,7 +955,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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -1026,6 +1036,81 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stroke_order_set_reorders_the_fill_and_stroke_nodes() {
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::get_stroke_paint_order;
|
||||||
|
use crate::test_utils::test_prelude::*;
|
||||||
|
use graphene_std::vector::style::PaintOrder;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let document = editor.active_document();
|
||||||
|
let layer = document.metadata().all_layers().next().unwrap();
|
||||||
|
let paint_order = get_stroke_paint_order(layer, &document.network_interface);
|
||||||
|
assert_eq!(paint_order, PaintOrder::StrokeAbove, "a fresh shape should stroke above its fill");
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||||
|
layer,
|
||||||
|
paint_order: PaintOrder::StrokeBelow,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||||
|
assert_eq!(paint_order, PaintOrder::StrokeBelow, "the rewrite should move the stroke downstream of the fill");
|
||||||
|
|
||||||
|
editor
|
||||||
|
.handle_message(GraphOperationMessage::StrokeOrderSet {
|
||||||
|
layer,
|
||||||
|
paint_order: PaintOrder::StrokeAbove,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let paint_order = get_stroke_paint_order(layer, &editor.active_document().network_interface);
|
||||||
|
assert_eq!(paint_order, PaintOrder::StrokeAbove, "the rewrite should move the stroke back upstream of the fill");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_node_inserted_between_the_fill_and_stroke_makes_the_order_swap_inapplicable() {
|
||||||
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_node_type;
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::stroke_paint_order_applicable;
|
||||||
|
use crate::test_utils::test_prelude::*;
|
||||||
|
|
||||||
|
let mut editor = EditorTestUtils::create();
|
||||||
|
editor.new_document().await;
|
||||||
|
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||||
|
|
||||||
|
let layer = editor.active_document().metadata().all_layers().next().unwrap();
|
||||||
|
assert!(
|
||||||
|
stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||||
|
"a fresh shape's adjacent Fill and Stroke pair should be swappable"
|
||||||
|
);
|
||||||
|
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let stroke_node_id = ModifyInputsContext::locate_node_in_layer_chain(&stroke_reference, layer, &editor.active_document().network_interface).unwrap();
|
||||||
|
|
||||||
|
let node_template = Box::new(resolve_proto_node_type(graphene_std::ops::passthrough::IDENTIFIER).unwrap().default_node_template());
|
||||||
|
let passthrough_node_id = NodeId::new();
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::InsertNode {
|
||||||
|
node_id: passthrough_node_id,
|
||||||
|
node_template,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
editor
|
||||||
|
.handle_message(NodeGraphMessage::InsertNodeBetween {
|
||||||
|
node_id: passthrough_node_id,
|
||||||
|
input_connector: InputConnector::node_at_index(stroke_node_id, 0),
|
||||||
|
insert_node_input_index: 0,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!stroke_paint_order_applicable(layer, &editor.active_document().network_interface),
|
||||||
|
"a node between the pair should gray the order radio out"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
|
fn color_interpolation_resolves_per_gradient_with_inheritance_and_style_priority() {
|
||||||
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
|
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" color-interpolation="linearRGB">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
|
|||||||
ARTBOARD_DIMENSIONS_INPUT_INDEX, ARTBOARD_LOCATION_INPUT_INDEX, DefinitionIdentifier, resolve_document_node_type, resolve_network_node_type, resolve_proto_node_type,
|
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,
|
||||||
@@ -18,7 +18,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};
|
||||||
|
|
||||||
@@ -431,7 +431,8 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_color_set(&mut self, color: Option<Color>) {
|
pub fn fill_color_set(&mut self, color: Option<Color>) {
|
||||||
let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||||
|
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput);
|
let input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput);
|
||||||
@@ -443,10 +444,15 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
let fill_value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
let fill_value = color.map_or_else(TaggedValue::no_paint, TaggedValue::Color);
|
||||||
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
self.set_input_with_refresh(input_connector, NodeInput::value(fill_value, false), false);
|
||||||
|
|
||||||
|
if existing_fill_node_id.is_none() {
|
||||||
|
self.restore_default_stroke_order();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
|
pub fn fill_gradient_set(&mut self, gradient: Gradient, gradient_form: GradientForm, settings: GradientSettings, transform: DAffine2) {
|
||||||
let Some(fill_node_id) = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
|
let existing_fill_node_id = self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, false);
|
||||||
|
let Some(fill_node_id) = existing_fill_node_id.or_else(|| self.existing_chain_hosted_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true)) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
|
||||||
@@ -487,6 +493,21 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
NodeInput::value(TaggedValue::GradientForm(gradient_form), false),
|
NodeInput::value(TaggedValue::GradientForm(gradient_form), false),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if existing_fill_node_id.is_none() {
|
||||||
|
self.restore_default_stroke_order();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A freshly created Fill node lands at the chain start, downstream of any Stroke node, where it would
|
||||||
|
/// paint over the stroke. This hops the stroke back downstream so it keeps painting above by default.
|
||||||
|
fn restore_default_stroke_order(&mut self) {
|
||||||
|
let Some(output_layer) = self.get_output_layer() else { return };
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let Some(stroke_node_id) = Self::locate_node_in_layer_chain(&stroke_reference, output_layer, self.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
set_stroke_paint_order(self.network_interface, &[], stroke_node_id, PaintOrder::StrokeAbove);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||||
@@ -857,8 +878,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
|
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);
|
||||||
@@ -1006,3 +1025,60 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The wires feeding off a node's primary output.
|
||||||
|
fn primary_output_consumers(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], node_id: NodeId) -> Vec<InputConnector> {
|
||||||
|
network_interface
|
||||||
|
.outward_wires(network_path)
|
||||||
|
.and_then(|wires| wires.get(&OutputConnector::node(node_id, 0)).cloned())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swaps a chain's directly adjacent Stroke and Fill nodes when their order disagrees with the requested
|
||||||
|
/// paint order: both nodes append their cover, so the downstream one of the pair paints on top, following
|
||||||
|
/// the painter's algorithm. Without a fill wired directly to the stroke, nothing changes.
|
||||||
|
/// Returns whether the graph changed.
|
||||||
|
pub fn set_stroke_paint_order(network_interface: &mut NodeNetworkInterface, network_path: &[NodeId], stroke_node_id: NodeId, paint_order: PaintOrder) -> bool {
|
||||||
|
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||||
|
let is_fill = |network_interface: &NodeNetworkInterface, node_id: &NodeId| network_interface.reference(node_id, network_path).as_ref() == Some(&fill_reference);
|
||||||
|
|
||||||
|
// Find the fill wired directly to the stroke on either side; the downstream one of the pair paints on top
|
||||||
|
let stroke_primary_source = match network_interface.input_from_connector(&InputConnector::node_at_index(stroke_node_id, 0), network_path) {
|
||||||
|
Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let (fill_node_id, currently_above) = if let Some(source) = stroke_primary_source.filter(|source| is_fill(network_interface, source)) {
|
||||||
|
(source, true)
|
||||||
|
} else {
|
||||||
|
let consumers = primary_output_consumers(network_interface, network_path, stroke_node_id);
|
||||||
|
let fill_consumer = consumers.iter().find_map(|connector| match connector {
|
||||||
|
InputConnector::Node { node_id, input_index: 0 } if is_fill(network_interface, node_id) => Some(*node_id),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let Some(fill_node_id) = fill_consumer else { return false };
|
||||||
|
(fill_node_id, false)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paint_order == PaintOrder::StrokeAbove) == currently_above {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the pair in place: the downstream node takes the upstream one's source, consumers of the
|
||||||
|
// downstream node move over to the upstream one, and the wire linking the pair reverses direction
|
||||||
|
let (upstream, downstream) = if currently_above { (fill_node_id, stroke_node_id) } else { (stroke_node_id, fill_node_id) };
|
||||||
|
let Some(upstream_source) = network_interface.input_from_connector(&InputConnector::node_at_index(upstream, 0), network_path).cloned() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let downstream_consumers = primary_output_consumers(network_interface, network_path, downstream);
|
||||||
|
|
||||||
|
network_interface.set_input(&InputConnector::node_at_index(downstream, 0), upstream_source, network_path);
|
||||||
|
network_interface.set_input(&InputConnector::node_at_index(upstream, 0), NodeInput::node(downstream, 0), network_path);
|
||||||
|
for consumer in &downstream_consumers {
|
||||||
|
if matches!(consumer, InputConnector::Node { node_id, .. } if *node_id == upstream || *node_id == downstream) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
network_interface.set_input(consumer, NodeInput::node(upstream, 0), network_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|||||||
@@ -1734,6 +1734,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
|||||||
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
responses.add(NodeGraphMessage::UpdateLayerPanel);
|
||||||
responses.add(DocumentMessage::DocumentStructureChanged);
|
responses.add(DocumentMessage::DocumentStructureChanged);
|
||||||
responses.add(PropertiesPanelMessage::Refresh);
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
|
responses.add(EventMessage::GraphChanged);
|
||||||
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
|
if breadcrumb_network_path == selection_network_path && graph_view_overlay_open {
|
||||||
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
|
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
|
||||||
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
|
self.frontend_nodes = nodes.iter().map(|node| node.id).collect();
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ use graphene_std::vector::misc::{
|
|||||||
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{
|
use graphene_std::vector::style::{
|
||||||
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, PaintOrder, StrokeAlign, StrokeCap,
|
FillChoice, Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStops, StrokeAlign, StrokeCap, StrokeJoin,
|
||||||
StrokeJoin, build_transform_with_y_preservation,
|
build_transform_with_y_preservation,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||||
use graphene_std::{NodeParameter, ParameterRef};
|
use graphene_std::{NodeParameter, ParameterRef};
|
||||||
@@ -322,7 +322,6 @@ pub(crate) fn property_from_type(
|
|||||||
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<StrokeCap>(x) => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||||
Some(x) if id_is::<StrokeJoin>(x) => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<StrokeJoin>(x) => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
||||||
Some(x) if id_is::<StrokeAlign>(x) => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<StrokeAlign>(x) => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||||
Some(x) if id_is::<PaintOrder>(x) => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
|
||||||
Some(x) if id_is::<ArcType>(x) => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<ArcType>(x) => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||||
Some(x) if id_is::<RowsOrColumns>(x) => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<RowsOrColumns>(x) => enum_choice::<RowsOrColumns>().for_socket(default_info).property_row(),
|
||||||
Some(x) if id_is::<TextAlign>(x) => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
Some(x) if id_is::<TextAlign>(x) => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
|
||||||
@@ -2678,9 +2677,6 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||||||
ParameterWidgetsInfo::new(node_id, MiterLimitInput, true, context),
|
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;
|
||||||
@@ -2693,7 +2689,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,8 @@ 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 `ATTR_APPEARANCE` attribute, exposed so message handlers can read paint information that lives on the list.
|
||||||
/// information that lives on the list.
|
pub layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>,
|
||||||
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
|
|
||||||
/// Per-layer `ATTR_STROKE` attribute, exposed so message handlers can read
|
|
||||||
/// stroke paint information that lives on the list.
|
|
||||||
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>,
|
|
||||||
/// 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 +228,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,9 +40,8 @@ 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::Appearance;
|
||||||
use graphene_std::ContextDependencies;
|
use graphene_std::ContextDependencies;
|
||||||
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 `ATTR_APPEARANCE` snapshot.
|
||||||
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
|
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 `ATTR_STROKE` snapshot.
|
|
||||||
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic>>>) {
|
|
||||||
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
@@ -1807,13 +1808,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)
|
||||||
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);
|
||||||
@@ -1822,13 +1822,36 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 4), old_inputs[5].clone(), network_path);
|
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 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);
|
||||||
|
}
|
||||||
|
let dash_input = migrate_dash_input(&old_inputs[8]).unwrap_or_else(|| old_inputs[8].clone());
|
||||||
|
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), dash_input, network_path);
|
||||||
|
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 8), old_inputs[9].clone(), network_path);
|
||||||
|
inputs_count = 9;
|
||||||
|
|
||||||
|
set_stroke_paint_order(&mut document.network_interface, network_path, *node_id, paint_order);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this document upgrade code
|
|
||||||
// A legacy "no color" on a plain color connector (`TaggedValue::no_paint()` restored by the deserializer) becomes a color,
|
// A legacy "no color" on a plain color connector (`TaggedValue::no_paint()` restored by the deserializer) becomes a color,
|
||||||
// since only paint connectors keep the no-paint choice
|
// since only paint connectors keep the no-paint choice
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ pub struct DrawingToolState {
|
|||||||
pub miter_limit: Option<f64>,
|
pub miter_limit: Option<f64>,
|
||||||
/// Paint order from the selection. `None` = mixed.
|
/// Paint order from the selection. `None` = mixed.
|
||||||
pub paint_order: Option<PaintOrder>,
|
pub paint_order: Option<PaintOrder>,
|
||||||
|
/// Whether any selected layer has a lone adjacent Fill/Stroke pair for the paint order to swap.
|
||||||
|
/// Stays true with an empty selection, where the radio still sets the default for new shapes.
|
||||||
|
pub paint_order_applicable: bool,
|
||||||
/// Dash lengths from the selection. `None` = mixed.
|
/// Dash lengths from the selection. `None` = mixed.
|
||||||
pub dash_lengths: Option<Vec<f64>>,
|
pub dash_lengths: Option<Vec<f64>>,
|
||||||
/// Dash offset from the selection. `None` = mixed.
|
/// Dash offset from the selection. `None` = mixed.
|
||||||
@@ -150,6 +153,7 @@ impl DrawingToolState {
|
|||||||
stroke_join: Some(StrokeJoin::default()),
|
stroke_join: Some(StrokeJoin::default()),
|
||||||
miter_limit: Some(4.),
|
miter_limit: Some(4.),
|
||||||
paint_order: Some(PaintOrder::default()),
|
paint_order: Some(PaintOrder::default()),
|
||||||
|
paint_order_applicable: true,
|
||||||
dash_lengths: Some(Vec::new()),
|
dash_lengths: Some(Vec::new()),
|
||||||
dash_offset: Some(0.),
|
dash_offset: Some(0.),
|
||||||
last_synced_selection: Vec::new(),
|
last_synced_selection: Vec::new(),
|
||||||
@@ -183,13 +187,21 @@ 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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queues the paint order rewrite for a freshly created `layer`. The order is the relative position of the
|
||||||
|
/// Stroke and Fill nodes in the chain, so this must run after both the stroke and fill have been applied.
|
||||||
|
pub fn apply_stroke_order_to_new_layer(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||||
|
let paint_order = self.paint_order.unwrap_or_default();
|
||||||
|
if paint_order != PaintOrder::default() {
|
||||||
|
responses.add(GraphOperationMessage::StrokeOrderSet { layer, paint_order });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `FillChoice::Solid` from a color.
|
/// Builds a `FillChoice::Solid` from a color.
|
||||||
@@ -304,12 +316,23 @@ pub fn sync_drawing_state(drawing: &mut DrawingToolState, natural_fill_enabled:
|
|||||||
/// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates
|
/// Reads the stroke proto-node inputs (align, cap, join, miter limit, paint order, dash lengths, dash offset) across the selection and updates
|
||||||
/// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is.
|
/// the matching fields on `drawing`. Each field becomes `None` (mixed) when selected strokes disagree. With no selection, fields are left as-is.
|
||||||
fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool {
|
fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessageHandler) -> bool {
|
||||||
let strokes: Vec<_> = graph_modification_utils::paintable_selected_layers(document)
|
let layers = graph_modification_utils::paintable_selected_layers(document);
|
||||||
|
|
||||||
|
// The order radio only swaps a layer's lone adjacent Fill/Stroke pair, so it grays out when no selected
|
||||||
|
// layer has one; with nothing selected it still sets the default order for new shapes
|
||||||
|
let paint_order_applicable = layers.is_empty() || layers.iter().any(|&layer| graph_modification_utils::stroke_paint_order_applicable(layer, &document.network_interface));
|
||||||
|
let mut applicability_changed = false;
|
||||||
|
if drawing.paint_order_applicable != paint_order_applicable {
|
||||||
|
drawing.paint_order_applicable = paint_order_applicable;
|
||||||
|
applicability_changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let strokes: Vec<_> = layers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface))
|
.filter_map(|layer| graph_modification_utils::get_stroke_options(layer, &document.network_interface))
|
||||||
.collect();
|
.collect();
|
||||||
if strokes.is_empty() {
|
if strokes.is_empty() {
|
||||||
return false;
|
return applicability_changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unanimous<T: PartialEq + Clone>(values: impl IntoIterator<Item = T>) -> Option<T> {
|
fn unanimous<T: PartialEq + Clone>(values: impl IntoIterator<Item = T>) -> Option<T> {
|
||||||
@@ -326,7 +349,7 @@ fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessag
|
|||||||
let new_dash_lengths = unanimous(strokes.iter().map(|s| &s.dash_lengths)).cloned();
|
let new_dash_lengths = unanimous(strokes.iter().map(|s| &s.dash_lengths)).cloned();
|
||||||
let new_dash_offset = unanimous(strokes.iter().map(|s| s.dash_offset));
|
let new_dash_offset = unanimous(strokes.iter().map(|s| s.dash_offset));
|
||||||
|
|
||||||
let mut changed = false;
|
let mut changed = applicability_changed;
|
||||||
|
|
||||||
if drawing.stroke_align != new_align {
|
if drawing.stroke_align != new_align {
|
||||||
drawing.stroke_align = new_align;
|
drawing.stroke_align = new_align;
|
||||||
|
|||||||
@@ -692,10 +692,7 @@ pub fn get_stroke_options(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
|||||||
Some(TaggedValue::F64(value)) => *value,
|
Some(TaggedValue::F64(value)) => *value,
|
||||||
_ => 4.,
|
_ => 4.,
|
||||||
};
|
};
|
||||||
let paint_order = match parameters.value(stroke::PaintOrderInput) {
|
let paint_order = get_stroke_paint_order(layer, network_interface);
|
||||||
Some(TaggedValue::PaintOrder(value)) => *value,
|
|
||||||
_ => 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(),
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
@@ -721,6 +718,67 @@ pub fn get_stroke_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork
|
|||||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
|
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the paint order swap can act on the layer: exactly one Fill and one Stroke node in its chain,
|
||||||
|
/// wired directly together. Extra paint nodes make the swap unreliable, since a cover replaced in place
|
||||||
|
/// keeps the slot in the paint order that its upstream sibling established.
|
||||||
|
pub fn stroke_paint_order_applicable(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> bool {
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||||
|
|
||||||
|
let mut strokes = Vec::new();
|
||||||
|
let mut fills = Vec::new();
|
||||||
|
let node_graph_layer = NodeGraphLayer::new(layer, network_interface);
|
||||||
|
for node_id in node_graph_layer
|
||||||
|
.horizontal_layer_flow()
|
||||||
|
.take_while(|&node_id| node_id == layer.to_node() || !network_interface.is_layer(&node_id, &[]))
|
||||||
|
{
|
||||||
|
let reference = network_interface.reference(&node_id, &[]);
|
||||||
|
if reference.as_ref() == Some(&stroke_reference) {
|
||||||
|
strokes.push(node_id);
|
||||||
|
} else if reference.as_ref() == Some(&fill_reference) {
|
||||||
|
fills.push(node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (&[stroke_node_id], &[fill_node_id]) = (strokes.as_slice(), fills.as_slice()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_source = |node_id: NodeId| match network_interface.input_from_connector(&InputConnector::node_at_index(node_id, 0), &[]) {
|
||||||
|
Some(NodeInput::Node { node_id, output_index: 0, .. }) => Some(*node_id),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
primary_source(stroke_node_id) == Some(fill_node_id) || primary_source(fill_node_id) == Some(stroke_node_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The paint order of a layer's stroke, read from the chain topology: both the Fill and Stroke nodes append
|
||||||
|
/// their cover, so the more downstream of the two paints on top, following the painter's algorithm.
|
||||||
|
pub fn get_stroke_paint_order(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> PaintOrder {
|
||||||
|
let stroke_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER);
|
||||||
|
let fill_reference = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER);
|
||||||
|
|
||||||
|
// The flow iterates downstream to upstream, so the first of the pair encountered is the one painting on top
|
||||||
|
let mut stroke_position = None;
|
||||||
|
let mut fill_position = None;
|
||||||
|
let node_graph_layer = NodeGraphLayer::new(layer, network_interface);
|
||||||
|
for (position, node_id) in node_graph_layer
|
||||||
|
.horizontal_layer_flow()
|
||||||
|
.take_while(|&node_id| node_id == layer.to_node() || !network_interface.is_layer(&node_id, &[]))
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let reference = network_interface.reference(&node_id, &[]);
|
||||||
|
if reference.as_ref() == Some(&stroke_reference) {
|
||||||
|
stroke_position.get_or_insert(position);
|
||||||
|
} else if reference.as_ref() == Some(&fill_reference) {
|
||||||
|
fill_position.get_or_insert(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match stroke_position.zip(fill_position) {
|
||||||
|
Some((stroke_position, fill_position)) if fill_position < stroke_position => PaintOrder::StrokeBelow,
|
||||||
|
_ => PaintOrder::StrokeAbove,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Writes the weight back to every selected non-artboard layer's stroke. Layers with an existing stroke just have their
|
/// Writes the weight back to every selected non-artboard layer's stroke. Layers with an existing stroke just have their
|
||||||
/// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new
|
/// `WeightInput` updated; layers without one get a fresh stroke node added (defaulting to a black stroke with the new
|
||||||
/// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes.
|
/// weight) only when the new weight is nonzero, so changing back to 0 doesn't keep adding empty strokes.
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ where
|
|||||||
if has_dash {
|
if has_dash {
|
||||||
rows.push(LayoutGroup::row(dash_offset_row(drawing.dash_offset, to_message.clone())));
|
rows.push(LayoutGroup::row(dash_offset_row(drawing.dash_offset, to_message.clone())));
|
||||||
}
|
}
|
||||||
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", drawing.paint_order, false, {
|
// An inapplicable order (no Fill/Stroke pair to reorder) grays out and highlights no entry, since the synced value is only a fallback
|
||||||
|
let paint_order = drawing.paint_order.filter(|_| drawing.paint_order_applicable);
|
||||||
|
rows.push(LayoutGroup::row(enum_radio_row::<PaintOrder, _>("Order", paint_order, !drawing.paint_order_applicable, {
|
||||||
let to_message = to_message.clone();
|
let to_message = to_message.clone();
|
||||||
move |value| to_message(StrokeOptionsUpdate::PaintOrder(value))
|
move |value| to_message(StrokeOptionsUpdate::PaintOrder(value))
|
||||||
})));
|
})));
|
||||||
@@ -198,7 +200,12 @@ pub fn apply_miter_limit(drawing: &mut DrawingToolState, limit: f64, document: &
|
|||||||
|
|
||||||
pub fn apply_paint_order(drawing: &mut DrawingToolState, order: PaintOrder, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
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);
|
// A mixed selection only grays the radio out when no layer can take the swap, so skip the ones that can't
|
||||||
|
for layer in graph_modification_utils::paintable_selected_layers(document) {
|
||||||
|
if graph_modification_utils::stroke_paint_order_applicable(layer, &document.network_interface) {
|
||||||
|
responses.add(GraphOperationMessage::StrokeOrderSet { layer, paint_order: order });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
pub fn apply_dash_lengths(drawing: &mut DrawingToolState, lengths: Vec<f64>, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ impl ToolTransition for FreehandTool {
|
|||||||
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
|
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
|
||||||
tool_abort: Some(FreehandToolMessage::Abort.into()),
|
tool_abort: Some(FreehandToolMessage::Abort.into()),
|
||||||
selection_changed: Some(FreehandToolMessage::SelectionChanged.into()),
|
selection_changed: Some(FreehandToolMessage::SelectionChanged.into()),
|
||||||
|
graph_changed: Some(FreehandToolMessage::SelectionChanged.into()),
|
||||||
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
|
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -302,8 +303,9 @@ impl Fsm for FreehandToolFsmState {
|
|||||||
let nodes = vec![(NodeId(0), node)];
|
let nodes = vec![(NodeId(0), node)];
|
||||||
|
|
||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
|
||||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||||
|
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||||
|
tool_options.drawing.apply_stroke_order_to_new_layer(layer, responses);
|
||||||
tool_data.layer = Some(layer);
|
tool_data.layer = Some(layer);
|
||||||
tool_data.new_layer_viewport_start = Some(input.mouse.position);
|
tool_data.new_layer_viewport_start = Some(input.mouse.position);
|
||||||
|
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ impl ToolTransition for PenTool {
|
|||||||
EventToMessageMap {
|
EventToMessageMap {
|
||||||
tool_abort: Some(PenToolMessage::Abort.into()),
|
tool_abort: Some(PenToolMessage::Abort.into()),
|
||||||
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||||
|
graph_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||||
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
|
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
|
||||||
overlay_provider: Some(|context| PenToolMessage::Overlays { context }.into()),
|
overlay_provider: Some(|context| PenToolMessage::Overlays { context }.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -1352,8 +1353,9 @@ impl PenToolData {
|
|||||||
let parent = document.new_layer_bounding_artboard(input, viewport);
|
let parent = document.new_layer_bounding_artboard(input, viewport);
|
||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
self.current_layer = Some(layer);
|
self.current_layer = Some(layer);
|
||||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
|
||||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||||
|
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||||
|
tool_options.drawing.apply_stroke_order_to_new_layer(layer, responses);
|
||||||
self.prior_segment = None;
|
self.prior_segment = None;
|
||||||
self.prior_segments = None;
|
self.prior_segments = None;
|
||||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ impl ToolTransition for SelectTool {
|
|||||||
EventToMessageMap {
|
EventToMessageMap {
|
||||||
tool_abort: Some(SelectToolMessage::Abort.into()),
|
tool_abort: Some(SelectToolMessage::Abort.into()),
|
||||||
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
selection_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
||||||
|
graph_changed: Some(SelectToolMessage::SelectionChanged.into()),
|
||||||
working_color_changed: Some(SelectToolMessage::WorkingColorChanged.into()),
|
working_color_changed: Some(SelectToolMessage::WorkingColorChanged.into()),
|
||||||
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
|
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -710,6 +710,7 @@ impl ToolTransition for ShapeTool {
|
|||||||
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
|
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
|
||||||
tool_abort: Some(ShapeToolMessage::Abort.into()),
|
tool_abort: Some(ShapeToolMessage::Abort.into()),
|
||||||
selection_changed: Some(ShapeToolMessage::SelectionChanged.into()),
|
selection_changed: Some(ShapeToolMessage::SelectionChanged.into()),
|
||||||
|
graph_changed: Some(ShapeToolMessage::SelectionChanged.into()),
|
||||||
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
|
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@@ -1129,8 +1130,9 @@ impl Fsm for ShapeToolFsmState {
|
|||||||
skip_rerender: false,
|
skip_rerender: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
|
||||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||||
|
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||||
|
tool_options.drawing.apply_stroke_order_to_new_layer(layer, defered_responses);
|
||||||
}
|
}
|
||||||
ShapeType::Arrow => {
|
ShapeType::Arrow => {
|
||||||
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
||||||
@@ -1143,8 +1145,9 @@ impl Fsm for ShapeToolFsmState {
|
|||||||
|
|
||||||
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
tool_data.line_data.weight = tool_options.drawing.effective_line_weight();
|
||||||
tool_data.line_data.editing_layer = Some(layer);
|
tool_data.line_data.editing_layer = Some(layer);
|
||||||
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
|
||||||
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
tool_options.drawing.fill.apply_fill(layer, defered_responses);
|
||||||
|
tool_options.drawing.apply_stroke_to_new_layer(layer, defered_responses);
|
||||||
|
tool_options.drawing.apply_stroke_order_to_new_layer(layer, defered_responses);
|
||||||
}
|
}
|
||||||
ShapeType::Line => {
|
ShapeType::Line => {
|
||||||
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
let viewport_drag_start = tool_data.data.viewport_drag_start(document);
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ impl ToolTransition for SplineTool {
|
|||||||
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
|
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
|
||||||
tool_abort: Some(SplineToolMessage::Abort.into()),
|
tool_abort: Some(SplineToolMessage::Abort.into()),
|
||||||
selection_changed: Some(SplineToolMessage::SelectionChanged.into()),
|
selection_changed: Some(SplineToolMessage::SelectionChanged.into()),
|
||||||
|
graph_changed: Some(SplineToolMessage::SelectionChanged.into()),
|
||||||
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
|
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,8 +408,9 @@ impl Fsm for SplineToolFsmState {
|
|||||||
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
||||||
|
|
||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
|
||||||
tool_options.drawing.fill.apply_fill(layer, responses);
|
tool_options.drawing.fill.apply_fill(layer, responses);
|
||||||
|
tool_options.drawing.apply_stroke_to_new_layer(layer, responses);
|
||||||
|
tool_options.drawing.apply_stroke_order_to_new_layer(layer, responses);
|
||||||
tool_data.current_layer = Some(layer);
|
tool_data.current_layer = Some(layer);
|
||||||
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
tool_data.new_layer_viewport_start = Some(viewport_vec);
|
||||||
|
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ impl ToolTransition for TextTool {
|
|||||||
EventToMessageMap {
|
EventToMessageMap {
|
||||||
canvas_transformed: None,
|
canvas_transformed: None,
|
||||||
selection_changed: Some(TextToolMessage::SelectionChanged.into()),
|
selection_changed: Some(TextToolMessage::SelectionChanged.into()),
|
||||||
|
graph_changed: None,
|
||||||
tool_abort: Some(TextToolMessage::Abort.into()),
|
tool_abort: Some(TextToolMessage::Abort.into()),
|
||||||
working_color_changed: Some(TextToolMessage::WorkingColorChanged.into()),
|
working_color_changed: Some(TextToolMessage::WorkingColorChanged.into()),
|
||||||
overlay_provider: Some(|context| TextToolMessage::Overlays { context }.into()),
|
overlay_provider: Some(|context| TextToolMessage::Overlays { context }.into()),
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ impl DocumentToolData {
|
|||||||
pub struct EventToMessageMap {
|
pub struct EventToMessageMap {
|
||||||
pub canvas_transformed: Option<ToolMessage>,
|
pub canvas_transformed: Option<ToolMessage>,
|
||||||
pub selection_changed: Option<ToolMessage>,
|
pub selection_changed: Option<ToolMessage>,
|
||||||
|
/// Tools whose control bar mirrors the selection's node chains map this to the same message as
|
||||||
|
/// `selection_changed`, so a graph edit re-syncs the widgets the same way reselecting would.
|
||||||
|
pub graph_changed: Option<ToolMessage>,
|
||||||
pub tool_abort: Option<ToolMessage>,
|
pub tool_abort: Option<ToolMessage>,
|
||||||
pub working_color_changed: Option<ToolMessage>,
|
pub working_color_changed: Option<ToolMessage>,
|
||||||
pub overlay_provider: Option<OverlayProvider>,
|
pub overlay_provider: Option<OverlayProvider>,
|
||||||
@@ -178,6 +181,7 @@ pub trait ToolTransition {
|
|||||||
subscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
subscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
||||||
subscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
subscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
||||||
subscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
subscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
||||||
|
subscribe_message(event_to_tool_map.graph_changed, EventMessage::GraphChanged);
|
||||||
subscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
subscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
||||||
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
||||||
responses.add(OverlaysMessage::AddProvider { provider: overlay_provider });
|
responses.add(OverlaysMessage::AddProvider { provider: overlay_provider });
|
||||||
@@ -198,6 +202,7 @@ pub trait ToolTransition {
|
|||||||
unsubscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
unsubscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
||||||
unsubscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
unsubscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
||||||
unsubscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
unsubscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
||||||
|
unsubscribe_message(event_to_tool_map.graph_changed, EventMessage::GraphChanged);
|
||||||
unsubscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
unsubscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
||||||
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
||||||
responses.add(OverlaysMessage::RemoveProvider { provider: overlay_provider });
|
responses.add(OverlaysMessage::RemoveProvider { provider: overlay_provider });
|
||||||
|
|||||||
@@ -697,8 +697,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;
|
||||||
|
|
||||||
@@ -713,8 +712,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);
|
||||||
|
|||||||
@@ -550,7 +550,6 @@ tagged_value! {
|
|||||||
#[serde(alias = "LineJoin")]
|
#[serde(alias = "LineJoin")]
|
||||||
StrokeJoin(vector::style::StrokeJoin),
|
StrokeJoin(vector::style::StrokeJoin),
|
||||||
StrokeAlign(vector::style::StrokeAlign),
|
StrokeAlign(vector::style::StrokeAlign),
|
||||||
PaintOrder(vector::style::PaintOrder),
|
|
||||||
#[serde(alias = "GradientType")] // TODO: Eventually remove this document upgrade code
|
#[serde(alias = "GradientType")] // TODO: Eventually remove this document upgrade code
|
||||||
GradientForm(vector::style::GradientForm),
|
GradientForm(vector::style::GradientForm),
|
||||||
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
|
#[serde(alias = "GradientSpreadMethod")] // TODO: Eventually remove this document upgrade code
|
||||||
@@ -564,8 +563,9 @@ tagged_value! {
|
|||||||
TextAlign(text_nodes::TextAlign),
|
TextAlign(text_nodes::TextAlign),
|
||||||
ScaleType(core_types::transform::ScaleType),
|
ScaleType(core_types::transform::ScaleType),
|
||||||
// Legacy
|
// Legacy
|
||||||
|
PaintOrder(vector::style::PaintOrder), // TODO: Eventually remove this document upgrade code
|
||||||
#[serde(alias = "Fill")]
|
#[serde(alias = "Fill")]
|
||||||
LegacyFill(graphic_types::migrations::legacy::LegacyFill),
|
LegacyFill(graphic_types::migrations::legacy::LegacyFill), // TODO: Eventually remove this document upgrade code
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaggedValue {
|
impl TaggedValue {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use graphene_std::transform::{Footprint, ReferencePoint, ScaleType};
|
|||||||
use graphene_std::vector::misc::{
|
use graphene_std::vector::misc::{
|
||||||
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
|
||||||
};
|
};
|
||||||
use graphene_std::vector::style::{DashPattern, GradientForm, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{DashPattern, GradientForm, GradientHueDirection, GradientInterpolation, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector, VectorModification};
|
||||||
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
||||||
use node_registry_macros::async_node;
|
use node_registry_macros::async_node;
|
||||||
@@ -336,7 +336,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||||||
StrokeJoin,
|
StrokeJoin,
|
||||||
StrokeAlign,
|
StrokeAlign,
|
||||||
StrokeCap,
|
StrokeCap,
|
||||||
PaintOrder,
|
|
||||||
GradientForm,
|
GradientForm,
|
||||||
GradientSpread,
|
GradientSpread,
|
||||||
GradientSpace,
|
GradientSpace,
|
||||||
|
|||||||
@@ -77,10 +77,26 @@ pub const ATTR_POSITION: &str = "position";
|
|||||||
/// Gradient stop's `f64` midpoint (implicit default `0.5`, linear), a factor from 0 to 1 across the distance to the next
|
/// Gradient stop's `f64` midpoint (implicit default `0.5`, linear), a factor from 0 to 1 across the distance to the next
|
||||||
/// stop, on the `List<Color>` inside a `Gradient`. The final stop's midpoint is ignored if "gradient_cyclic" is false.
|
/// stop, on the `List<Color>` inside a `Gradient`. The final stop's midpoint is ignored if "gradient_cyclic" is false.
|
||||||
pub const ATTR_MIDPOINT: &str = "midpoint";
|
pub const ATTR_MIDPOINT: &str = "midpoint";
|
||||||
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
|
/// Item's ordered list of paint passes, of type `Appearance`. Earlier coverages paint first, compositing below later ones.
|
||||||
pub const ATTR_FILL: &str = "fill";
|
pub const ATTR_APPEARANCE: &str = "appearance";
|
||||||
/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.
|
// TODO: Add a "fill_rule" attribute as a sibling of "paint" on the coverage list (uniform across covers) once a FillRule type ships
|
||||||
pub const ATTR_STROKE: &str = "stroke";
|
/// Coverage's `List<Graphic>` paint (implicit default empty, painting nothing), on the
|
||||||
|
/// `List<Coverage>` inside an `Appearance`.
|
||||||
|
pub const ATTR_PAINT: &str = "paint";
|
||||||
|
/// Stroke coverage's line thickness (`f64`, implicit default `0.`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_WEIGHT: &str = "weight";
|
||||||
|
/// Stroke coverage's `DashPattern` (implicit default empty, a solid line), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_DASH_PATTERN: &str = "dash_pattern";
|
||||||
|
/// Stroke coverage's dash phase offset distance (`f64`, implicit default `0.`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_DASH_OFFSET: &str = "dash_offset";
|
||||||
|
/// Stroke coverage's `StrokeCap` (implicit default `Butt`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_CAP: &str = "cap";
|
||||||
|
/// Stroke coverage's `StrokeJoin` (implicit default `Miter`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_JOIN: &str = "join";
|
||||||
|
/// Stroke coverage's miter limit threshold (`f64`, implicit default `4.`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_JOIN_MITER_LIMIT: &str = "join_miter_limit";
|
||||||
|
/// Stroke coverage's `StrokeAlign` (implicit default `Center`), on the `Item<Cover>` inside a `Coverage`.
|
||||||
|
pub const ATTR_ALIGN: &str = "align";
|
||||||
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
|
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
|
||||||
pub const ATTR_FONT_SIZE: &str = "font_size";
|
pub const ATTR_FONT_SIZE: &str = "font_size";
|
||||||
/// Text item's font, as a `Resource` of the loaded font file.
|
/// Text item's font, as a `Resource` of the loaded font file.
|
||||||
@@ -678,6 +694,11 @@ impl ItemAttributeValues {
|
|||||||
self.0.iter().find_map(|(existing_key, value)| if existing_key == key { Some((**value).as_any()) } else { None })
|
self.0.iter().find_map(|(existing_key, value)| if existing_key == key { Some((**value).as_any()) } else { None })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over key and type-erased value pairs of all stored attributes, in insertion order.
|
||||||
|
pub fn iter_any(&self) -> impl Iterator<Item = (&str, &dyn std::any::Any)> {
|
||||||
|
self.0.iter().map(|(key, value)| (key.as_str(), (**value).as_any()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a debug-formatted string representation of the attribute value for the given key, if it exists.
|
/// Returns a debug-formatted string representation of the attribute value for the given key, if it exists.
|
||||||
/// The `overrides` function can provide custom formatting for specific type.
|
/// The `overrides` function can provide custom formatting for specific type.
|
||||||
pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option<String>) -> Option<String> {
|
pub fn display_value(&self, key: &str, overrides: fn(&dyn std::any::Any) -> Option<String>) -> Option<String> {
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
//! 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.
|
||||||
|
|
||||||
|
use crate::graphic::{Graphic, is_paint_present};
|
||||||
|
use core_types::graphene_hash::CacheHash;
|
||||||
|
use core_types::list::{ATTR_ALIGN, ATTR_APPEARANCE, ATTR_CAP, ATTR_DASH_OFFSET, ATTR_DASH_PATTERN, ATTR_JOIN, ATTR_JOIN_MITER_LIMIT, ATTR_PAINT, ATTR_TRANSFORM, ATTR_WEIGHT, Item, List};
|
||||||
|
use vector_types::vector::style::{DashPattern, 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)]
|
||||||
|
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, PartialEq, CacheHash)]
|
||||||
|
pub struct Coverage(pub Item<Cover>);
|
||||||
|
|
||||||
|
/// 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)]
|
||||||
|
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, DashPattern::from(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 {
|
||||||
|
// A single walk of the attribute pairs instead of one keyed scan per parameter, since this runs per item per render pass
|
||||||
|
let mut stroke = Stroke::default();
|
||||||
|
for (key, value) in self.0.attributes().iter_any() {
|
||||||
|
match key {
|
||||||
|
ATTR_WEIGHT => stroke.weight = value.downcast_ref().copied().unwrap_or(stroke.weight),
|
||||||
|
ATTR_DASH_PATTERN => stroke.dash_lengths = value.downcast_ref::<DashPattern>().map(DashPattern::clamped_lengths).unwrap_or(stroke.dash_lengths),
|
||||||
|
ATTR_DASH_OFFSET => stroke.dash_offset = value.downcast_ref().copied().unwrap_or(stroke.dash_offset),
|
||||||
|
ATTR_CAP => stroke.cap = value.downcast_ref().copied().unwrap_or(stroke.cap),
|
||||||
|
ATTR_JOIN => stroke.join = value.downcast_ref().copied().unwrap_or(stroke.join),
|
||||||
|
ATTR_JOIN_MITER_LIMIT => stroke.join_miter_limit = value.downcast_ref().copied().unwrap_or(stroke.join_miter_limit),
|
||||||
|
ATTR_ALIGN => stroke.align = value.downcast_ref().copied().unwrap_or(stroke.align),
|
||||||
|
ATTR_TRANSFORM => stroke.transform = value.downcast_ref().copied().unwrap_or(stroke.transform),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stroke
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an appearance row, eliding the paint attribute when it draws nothing.
|
||||||
|
fn cover_row(coverage: Coverage, paint: List<Graphic>) -> Item<Coverage> {
|
||||||
|
let mut row = Item::new_from_element(coverage);
|
||||||
|
if is_paint_present(&paint) {
|
||||||
|
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: List<Graphic>) -> 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<&List<Graphic>> {
|
||||||
|
self.0.attribute::<List<Graphic>>(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<&List<Graphic>> {
|
||||||
|
self.first_index_of(cover).and_then(|index| self.paint_at(index)).filter(|paint| is_paint_present(paint))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<&List<Graphic>>)> {
|
||||||
|
self.covers()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, coverage)| (coverage, self.paint_at(index).filter(|paint| is_paint_present(paint))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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| is_paint_present(paint));
|
||||||
|
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 [`Graphic::None`] 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(is_paint_present))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: List<Graphic>, 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 => {
|
||||||
|
let mut reordered = List::new_from_item(row);
|
||||||
|
reordered.extend(std::mem::take(&mut self.0));
|
||||||
|
self.0 = reordered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: List<Graphic>) -> 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 List<Graphic>>,
|
||||||
|
pub stroke_paint: Option<&'a List<Graphic>>,
|
||||||
|
/// 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: List<Graphic>, placement: CoverPlacement) {
|
||||||
|
item.attribute_mut_or_insert_default::<Appearance>(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) -> List<Graphic> {
|
||||||
|
List::new_from_element(Graphic::Color(List::new_from_element(color)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_color(appearance: &Appearance, index: usize) -> Option<Color> {
|
||||||
|
let paint = appearance.paint_at(index)?;
|
||||||
|
let Some(Graphic::Color(colors)) = paint.element(0) else { return None };
|
||||||
|
colors.element(0).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(), List::new_from_element(Graphic::None), 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,13 +1,15 @@
|
|||||||
|
use crate::appearance::{Appearance, Cover, Coverage};
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::graphene_hash::CacheHash;
|
use core_types::graphene_hash::CacheHash;
|
||||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, NodeIdPath};
|
use core_types::list::{ATTR_APPEARANCE, ATTR_PAINT, Item, ItemAttributeValues, List, NodeIdPath};
|
||||||
|
use core_types::math::quad::Quad;
|
||||||
use core_types::ops::FromAnchorPosition;
|
use core_types::ops::FromAnchorPosition;
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
|
use core_types::transform::Transform;
|
||||||
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use raster_types::{CPU, GPU, Raster};
|
use raster_types::{CPU, GPU, Raster};
|
||||||
use std::borrow::Cow;
|
|
||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
pub use vector_types::Vector;
|
pub use vector_types::Vector;
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ pub fn is_lone_anonymous_leaf(content: &List<Graphic>) -> bool {
|
|||||||
&& content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).is_none()
|
&& content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).is_none()
|
||||||
&& content.attribute::<f64>(ATTR_OPACITY, 0).is_none()
|
&& content.attribute::<f64>(ATTR_OPACITY, 0).is_none()
|
||||||
&& content.attribute::<f64>(ATTR_OPACITY_FILL, 0).is_none()
|
&& content.attribute::<f64>(ATTR_OPACITY_FILL, 0).is_none()
|
||||||
|
&& content.attribute::<Appearance>(ATTR_APPEARANCE, 0).is_none()
|
||||||
&& content.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).is_none()
|
&& content.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, 0).is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,13 +137,14 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
|
|
||||||
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) {
|
fn flatten_recursive<T>(output: &mut List<T>, current_graphic_list: List<Graphic>, extract_variant: fn(Graphic) -> Option<List<T>>) {
|
||||||
for current_graphic_item in current_graphic_list.into_iter() {
|
for current_graphic_item in current_graphic_list.into_iter() {
|
||||||
// Whether the parent carries each attribute: a structural fact (column presence), never a value comparison.
|
// Whether the parent carries each composed attribute: a structural fact (column presence), never a value comparison.
|
||||||
// Flattening composes a parent attribute onto its children only when the parent has it,
|
// Flattening composes a parent attribute onto its children only when the parent has it,
|
||||||
// so an absent parent attribute never invents a column the children didn't already have.
|
// so an absent parent attribute never invents a column the children didn't already have.
|
||||||
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
let parent_has_transform = current_graphic_item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some();
|
||||||
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
let parent_has_opacity = current_graphic_item.attribute::<f64>(ATTR_OPACITY).is_some();
|
||||||
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
let parent_has_fill = current_graphic_item.attribute::<f64>(ATTR_OPACITY_FILL).is_some();
|
||||||
let parent_has_layer_path = current_graphic_item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).is_some();
|
let parent_has_layer_path = current_graphic_item.attribute::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH).is_some();
|
||||||
|
let parent_appearance = current_graphic_item.attribute::<Appearance>(ATTR_APPEARANCE).and_then(Appearance::declared).cloned();
|
||||||
|
|
||||||
let layer_path: NodeIdPath = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
|
let layer_path: NodeIdPath = current_graphic_item.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
|
||||||
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
|
let current_transform: DAffine2 = current_graphic_item.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
@@ -172,6 +176,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);
|
flatten_recursive(output, sub_list, extract_variant);
|
||||||
}
|
}
|
||||||
@@ -196,6 +208,11 @@ fn flatten_graphic_list<T>(content: List<Graphic>, extract_variant: fn(Graphic)
|
|||||||
if parent_has_layer_path {
|
if parent_has_layer_path {
|
||||||
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
|
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(appearance) = &parent_appearance
|
||||||
|
&& item.attribute::<Appearance>(ATTR_APPEARANCE).and_then(Appearance::declared).is_none()
|
||||||
|
{
|
||||||
|
item.set_attribute(ATTR_APPEARANCE, appearance.clone());
|
||||||
|
}
|
||||||
|
|
||||||
output.push(item);
|
output.push(item);
|
||||||
}
|
}
|
||||||
@@ -216,32 +233,7 @@ 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 attribute for a vector item, in the canonical `List<Graphic>` form.
|
/// Bake the provided transform into the per-item transforms of the appearance's paint graphics.
|
||||||
pub fn graphic_list_at<'a>(list: &'a List<Vector>, index: usize, attribute: &str) -> Option<Cow<'a, List<Graphic>>> {
|
|
||||||
list.attribute::<List<Graphic>>(attribute, index)
|
|
||||||
.map(Cow::Borrowed)
|
|
||||||
// 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 attribute,
|
|
||||||
/// checked by borrowing without cloning the renderable list.
|
|
||||||
pub fn has_paint_at(list: &List<Vector>, index: usize, attribute: &str) -> bool {
|
|
||||||
list.attribute::<List<Graphic>>(attribute, index).is_some_and(is_paint_present)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores a paint attribute in its canonical `List<Graphic>` form, the only representation paint readers accept.
|
|
||||||
pub fn set_paint_attribute(attributes: &mut ItemAttributeValues, key: &str, paint: impl IntoGraphicList) {
|
|
||||||
attributes.insert(key, paint.into_graphic_list());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores a paint attribute at a list index in its canonical `List<Graphic>` 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, 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_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
|
fn bake_list_transform<T>(list: &mut List<T>, transform: DAffine2) {
|
||||||
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
for item_transform in list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||||
@@ -249,24 +241,26 @@ pub fn bake_paint_transforms(attributes: &mut ItemAttributeValues, transform: DA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bake_graphic_paint_transform(graphics: &mut List<Graphic>, transform: DAffine2) {
|
fn bake_graphic_transform(graphic: &mut Graphic, transform: DAffine2) {
|
||||||
for graphic in graphics.iter_element_values_mut() {
|
match graphic {
|
||||||
match graphic {
|
Graphic::None => {}
|
||||||
Graphic::None => {}
|
Graphic::Graphic(list) => bake_list_transform(list, transform),
|
||||||
Graphic::Graphic(list) => bake_list_transform(list, transform),
|
Graphic::Vector(list) => bake_list_transform(list, transform),
|
||||||
Graphic::Vector(list) => bake_list_transform(list, transform),
|
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
|
||||||
Graphic::RasterCPU(list) => bake_list_transform(list, transform),
|
Graphic::RasterGPU(list) => bake_list_transform(list, transform),
|
||||||
Graphic::RasterGPU(list) => bake_list_transform(list, transform),
|
Graphic::Gradient(list) => bake_list_transform(list, transform),
|
||||||
Graphic::Gradient(list) => bake_list_transform(list, transform),
|
Graphic::Text(list) => bake_list_transform(list, transform),
|
||||||
Graphic::Text(list) => bake_list_transform(list, transform),
|
Graphic::Color(_) => {}
|
||||||
Graphic::Color(_) => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for paint_key in [ATTR_FILL, ATTR_STROKE] {
|
if let Some(appearance) = attributes.get_mut::<Appearance>(ATTR_APPEARANCE)
|
||||||
if let Some(graphics) = attributes.get_mut::<List<Graphic>>(paint_key) {
|
&& let Some(paints) = appearance.0.iter_attribute_values_mut::<List<Graphic>>(ATTR_PAINT)
|
||||||
bake_graphic_paint_transform(graphics, transform);
|
{
|
||||||
|
for paint in paints {
|
||||||
|
for graphic in paint.iter_element_values_mut() {
|
||||||
|
bake_graphic_transform(graphic, transform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,15 +442,24 @@ impl Graphic {
|
|||||||
pub fn can_reduce_to_clip_path(&self) -> bool {
|
pub fn can_reduce_to_clip_path(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
|
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
|
||||||
let Some(element) = vector.element(index) else { return false };
|
|
||||||
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||||
|
let appearance = vector.attribute::<Appearance>(ATTR_APPEARANCE, index);
|
||||||
|
|
||||||
let fill_opaque_or_absent = graphic_list_at(vector, index, ATTR_FILL).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_opaque()));
|
let fills_opaque_or_absent = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Fill)
|
||||||
|
.all(|(_, paint)| paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_opaque)))
|
||||||
|
});
|
||||||
|
|
||||||
let stroke_invisible_or_transparent = element.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
let strokes_invisible_or_transparent = appearance.is_none_or(|appearance| {
|
||||||
|| graphic_list_at(vector, index, ATTR_STROKE).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
||||||
|
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_fully_transparent)))
|
||||||
|
});
|
||||||
|
|
||||||
opacity > 1. - f64::EPSILON && fill_opaque_or_absent && stroke_invisible_or_transparent
|
opacity > 1. - f64::EPSILON && fills_opaque_or_absent && strokes_invisible_or_transparent
|
||||||
}),
|
}),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -467,16 +470,27 @@ impl Graphic {
|
|||||||
Graphic::None => false,
|
Graphic::None => false,
|
||||||
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
Graphic::Graphic(list) => !list.is_empty() && list.iter_element_values().all(Graphic::is_opaque),
|
||||||
Graphic::Vector(list) => {
|
Graphic::Vector(list) => {
|
||||||
let is_paint_opaque_at = |key: &str, index: usize| graphic_list_at(list, index, key).is_some_and(|graphic_list| graphic_list.element(0).is_some_and(|graphic| graphic.is_opaque()));
|
|
||||||
|
|
||||||
!list.is_empty()
|
!list.is_empty()
|
||||||
&& (0..list.len()).all(|i| {
|
&& (0..list.len()).all(|i| {
|
||||||
let Some(vector) = list.element(i) else { return false };
|
|
||||||
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
||||||
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
||||||
let fill_opaque = opacity_fill >= 1. - f64::EPSILON && is_paint_opaque_at(ATTR_FILL, i);
|
let appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, i);
|
||||||
let stroke_opaque_or_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_opaque_at(ATTR_STROKE, i);
|
|
||||||
opacity >= 1. - f64::EPSILON && fill_opaque && stroke_opaque_or_invisible
|
let fill_opaque = opacity_fill >= 1. - f64::EPSILON
|
||||||
|
&& appearance.is_some_and(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.any(|(coverage, paint)| coverage.cover() == Cover::Fill && paint.is_some_and(|paint| paint.element(0).is_some_and(Graphic::is_opaque)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let strokes_opaque_or_invisible = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
||||||
|
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_some_and(|paint| paint.element(0).is_some_and(Graphic::is_opaque)))
|
||||||
|
});
|
||||||
|
|
||||||
|
opacity >= 1. - f64::EPSILON && fill_opaque && strokes_opaque_or_invisible
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()),
|
Graphic::Color(list) => list.element(0).is_some_and(|color| color.is_opaque()),
|
||||||
@@ -490,18 +504,29 @@ impl Graphic {
|
|||||||
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),
|
||||||
Graphic::Vector(list) => (0..list.len()).all(|i| {
|
Graphic::Vector(list) => (0..list.len()).all(|i| {
|
||||||
let Some(vector) = list.element(i) else { return false };
|
|
||||||
let is_paint_fully_transparent_at =
|
|
||||||
|key: &str, index: usize| graphic_list_at(list, index, key).is_none_or(|graphic_list| graphic_list.element(0).is_none_or(|graphic| graphic.is_fully_transparent()));
|
|
||||||
|
|
||||||
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
let opacity: f64 = list.attribute_cloned_or(ATTR_OPACITY, i, 1.);
|
||||||
if opacity <= f64::EPSILON {
|
if opacity <= f64::EPSILON {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
let opacity_fill: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
|
||||||
let fill_invisible = opacity_fill <= f64::EPSILON || is_paint_fully_transparent_at(ATTR_FILL, i);
|
let appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, i);
|
||||||
let stroke_invisible = vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()) || is_paint_fully_transparent_at(ATTR_STROKE, i);
|
|
||||||
fill_invisible && stroke_invisible
|
let fills_invisible = opacity_fill <= f64::EPSILON
|
||||||
|
|| appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Fill)
|
||||||
|
.all(|(_, paint)| paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_fully_transparent)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let strokes_invisible = appearance.is_none_or(|appearance| {
|
||||||
|
appearance
|
||||||
|
.covers_with_paints()
|
||||||
|
.filter(|(coverage, _)| coverage.cover() == Cover::Stroke)
|
||||||
|
.all(|(coverage, paint)| !coverage.stroke_params().has_renderable_stroke() || paint.is_none_or(|paint| paint.element(0).is_none_or(Graphic::is_fully_transparent)))
|
||||||
|
});
|
||||||
|
|
||||||
|
fills_invisible && strokes_invisible
|
||||||
}),
|
}),
|
||||||
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
|
Graphic::Color(list) => list.iter_element_values().all(|color| color.a() == 0.),
|
||||||
Graphic::Gradient(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
|
Graphic::Gradient(list) => list.iter_element_values().all(|stops| stops.iter().all(|stop| stop.color.a() == 0.)),
|
||||||
@@ -530,11 +555,47 @@ impl Graphic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Combined bounding box of a vector list's rows, inflating each row by its appearance's stroke when `include_stroke`.
|
||||||
|
/// Stroke parameters live on the row attribute, out of reach of the element-level impl.
|
||||||
|
pub fn vector_list_bounding_box(list: &List<Vector>, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
|
let mut combined_bounds: Option<[DVec2; 2]> = None;
|
||||||
|
|
||||||
|
for index in 0..list.len() {
|
||||||
|
let Some(element) = list.element(index) else { continue };
|
||||||
|
let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
let row_transform = transform * item_transform;
|
||||||
|
|
||||||
|
let Some(mut bounds) = element.bounding_box_with_transform(row_transform) else { continue };
|
||||||
|
|
||||||
|
// The full line width (not half) accounts for different styles of stroke caps
|
||||||
|
if include_stroke
|
||||||
|
&& let Some(stroke) = list
|
||||||
|
.attribute::<Appearance>(ATTR_APPEARANCE, index)
|
||||||
|
.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke))
|
||||||
|
.map(Coverage::stroke_params)
|
||||||
|
{
|
||||||
|
let scale = row_transform.scale_magnitudes();
|
||||||
|
let offset = DVec2::splat(stroke.weight() * scale.x.max(scale.y) * stroke.join_miter_limit);
|
||||||
|
bounds = [bounds[0] - offset, bounds[1] + offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
combined_bounds = Some(match combined_bounds {
|
||||||
|
Some(existing) => Quad::combine_bounds(existing, bounds),
|
||||||
|
None => bounds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match combined_bounds {
|
||||||
|
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||||
|
None => RenderBoundingBox::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BoundingBox for Graphic {
|
impl BoundingBox for Graphic {
|
||||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => RenderBoundingBox::None,
|
Graphic::None => RenderBoundingBox::None,
|
||||||
Graphic::Vector(list) => list.bounding_box(transform, include_stroke),
|
Graphic::Vector(list) => vector_list_bounding_box(list, transform, include_stroke),
|
||||||
Graphic::RasterCPU(list) => list.bounding_box(transform, include_stroke),
|
Graphic::RasterCPU(list) => list.bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterGPU(list) => list.bounding_box(transform, include_stroke),
|
Graphic::RasterGPU(list) => list.bounding_box(transform, include_stroke),
|
||||||
Graphic::Graphic(list) => list.bounding_box(transform, include_stroke),
|
Graphic::Graphic(list) => list.bounding_box(transform, include_stroke),
|
||||||
@@ -547,7 +608,7 @@ impl BoundingBox for Graphic {
|
|||||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => RenderBoundingBox::None,
|
Graphic::None => RenderBoundingBox::None,
|
||||||
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::Vector(vector) => vector_list_bounding_box(vector, transform, include_stroke),
|
||||||
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||||
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
|
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
|
||||||
@@ -717,6 +778,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_rows() {
|
||||||
|
use core_types::Color;
|
||||||
|
|
||||||
|
let solid = |color: Color| List::new_from_element(Graphic::Color(List::new_from_element(color)));
|
||||||
|
|
||||||
|
// Declaring an appearance on row 0 forces the column, padding row 1 with the empty appearance
|
||||||
|
let mut inner = List::new();
|
||||||
|
inner.push(Item::new_from_element(Vector::default()));
|
||||||
|
inner.push(Item::new_from_element(Vector::default()));
|
||||||
|
inner.set_attribute(ATTR_APPEARANCE, 0, Appearance::new_single(Coverage::new_fill(), solid(Color::BLACK)));
|
||||||
|
|
||||||
|
let mut outer = List::new_from_element(Graphic::Vector(inner));
|
||||||
|
outer.set_attribute(ATTR_APPEARANCE, 0, Appearance::new_single(Coverage::new_fill(), solid(Color::WHITE)));
|
||||||
|
|
||||||
|
let flattened: List<Vector> = outer.into_flattened_list();
|
||||||
|
let color_of = |index: usize| {
|
||||||
|
let appearance = flattened.attribute::<Appearance>(ATTR_APPEARANCE, index)?;
|
||||||
|
let Some(Graphic::Color(colors)) = appearance.paint_at(0)?.element(0) else { return None };
|
||||||
|
colors.element(0).copied()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(color_of(0), Some(Color::BLACK), "a declared row should keep its own appearance");
|
||||||
|
assert_eq!(color_of(1), Some(Color::WHITE), "a padded row should inherit the parent appearance");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod appearance;
|
||||||
pub mod artboard;
|
pub mod artboard;
|
||||||
pub mod graphic;
|
pub mod graphic;
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ 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};
|
||||||
|
|
||||||
@@ -92,10 +94,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 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +106,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,
|
||||||
@@ -135,7 +139,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,
|
||||||
@@ -186,10 +189,15 @@ pub mod migrations {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use vector_types::vector::style::Stroke;
|
use vector_types::vector::style::Stroke;
|
||||||
|
|
||||||
|
/// The legacy `style` payload (including its stroke) must still parse so the untagged format
|
||||||
|
/// disambiguation succeeds, even though the geometry is all that survives.
|
||||||
#[test]
|
#[test]
|
||||||
fn preserves_stroke_from_old_vector_data_style() {
|
fn recovers_geometry_from_old_vector_data_with_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()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,22 +210,26 @@ pub mod migrations {
|
|||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.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();
|
|
||||||
|
|
||||||
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
let migrated = migrate_to_optional_vector(value).unwrap().expect("the legacy shape should parse into a vector");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
migrated.point_domain.positions(),
|
||||||
|
[glam::DVec2::new(3., 4.)],
|
||||||
|
"the legacy geometry should survive 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.)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use core_types::{ATTR_GRADIENT_FORM, ATTR_TRANSFORM, Color};
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphic_types::Graphic;
|
use graphic_types::Graphic;
|
||||||
use graphic_types::vector_types::gradient::GradientForm;
|
use graphic_types::vector_types::gradient::GradientForm;
|
||||||
use graphic_types::vector_types::vector::style::{PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use vector_types::Gradient;
|
use vector_types::Gradient;
|
||||||
use vector_types::gradient::{GradientSettings, GradientSpread};
|
use vector_types::gradient::{GradientSettings, GradientSpread};
|
||||||
@@ -194,7 +194,6 @@ 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);
|
|
||||||
|
|
||||||
// Render the needed stroke attributes
|
// Render the needed stroke attributes
|
||||||
let mut attributes = String::new();
|
let mut attributes = String::new();
|
||||||
@@ -219,7 +218,7 @@ impl RenderExt for Stroke {
|
|||||||
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
|
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
|
||||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{stroke_join_miter_limit}""#);
|
let _ = write!(&mut attributes, r#" stroke-miterlimit="{stroke_join_miter_limit}""#);
|
||||||
}
|
}
|
||||||
if paint_order.is_some() {
|
if render_params.stroke_below {
|
||||||
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
|
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
|
||||||
}
|
}
|
||||||
attributes
|
attributes
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use core_types::bounds::RenderBoundingBox;
|
|||||||
use core_types::color::Color;
|
use core_types::color::Color;
|
||||||
use core_types::color::SRGBA8;
|
use core_types::color::SRGBA8;
|
||||||
use core_types::consts::DEFAULT_FONT_SIZE;
|
use core_types::consts::DEFAULT_FONT_SIZE;
|
||||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, List, NodeIdPath};
|
use core_types::list::ATTR_APPEARANCE;
|
||||||
|
use core_types::list::{Item, List, NodeIdPath};
|
||||||
use core_types::math::quad::Quad;
|
use core_types::math::quad::Quad;
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
@@ -21,13 +22,12 @@ use dyn_any::DynAny;
|
|||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graphene_hash::CacheHashWrapper;
|
use graphene_hash::CacheHashWrapper;
|
||||||
use graphene_resource::Resource;
|
use graphene_resource::Resource;
|
||||||
use graphic_types::graphic::{graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute};
|
|
||||||
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};
|
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
|
||||||
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::{Artboard, Graphic, Vector};
|
use graphic_types::{Appearance, Artboard, Cover, Coverage, FillAndStroke, 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};
|
||||||
@@ -224,28 +224,45 @@ 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,
|
/// Paint the stroke below the fill within the same SVG path element
|
||||||
|
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>,
|
||||||
/// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode.
|
/// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode.
|
||||||
pub viewport_zoom: f64,
|
pub viewport_zoom: f64,
|
||||||
|
/// The nearest ancestor's appearance, cascading to items that lack their own.
|
||||||
|
pub inherited_appearance: Option<Appearance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderParams {
|
impl RenderParams {
|
||||||
pub fn for_clipper(&self) -> Self {
|
pub fn for_clipper(&self) -> Self {
|
||||||
Self { for_mask: true, ..*self }
|
Self { for_mask: true, ..self.clone() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_alignment(&self, transform: DAffine2) -> Self {
|
pub fn for_alignment(&self, transform: DAffine2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
alignment_parent_transform: Some(transform),
|
alignment_parent_transform: Some(transform),
|
||||||
..*self
|
..self.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_pattern(&self) -> Self {
|
pub fn for_pattern(&self) -> Self {
|
||||||
Self { inside_pattern: true, ..*self }
|
// A paint subtree supplies its own styling, so the painted element's appearance must not cascade into it
|
||||||
|
Self {
|
||||||
|
inside_pattern: true,
|
||||||
|
inherited_appearance: None,
|
||||||
|
..self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Params for rendering a child item, cascading this item's appearance to descendants lacking their own.
|
||||||
|
/// Callers only build these when the item carries a declared appearance, so an item without one clones nothing.
|
||||||
|
pub fn for_child_item(&self, item_appearance: &Appearance) -> Self {
|
||||||
|
Self {
|
||||||
|
inherited_appearance: Some(item_appearance.clone()),
|
||||||
|
..self.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_canvas(&self) -> bool {
|
pub fn to_canvas(&self) -> bool {
|
||||||
@@ -552,12 +569,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 `ATTR_APPEARANCE` row attribute, exposed so message handlers can read it.
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic>>>,
|
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>>>,
|
|
||||||
pub backgrounds: Vec<Background>,
|
pub backgrounds: Vec<Background>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,8 +595,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());
|
||||||
@@ -593,8 +606,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 {
|
||||||
@@ -618,18 +630,19 @@ pub trait Render: BoundingBox + RenderComplexity {
|
|||||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams);
|
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams);
|
||||||
|
|
||||||
/// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection.
|
/// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection.
|
||||||
fn add_upstream_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
|
/// `inherited_appearance` is the nearest ancestor's appearance, cascading to items that lack their own, mirroring the render cascade.
|
||||||
|
fn add_upstream_click_targets(&self, _click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {}
|
||||||
|
|
||||||
/// Like `add_upstream_click_targets` but for visual outlines. `List<Vector>` overrides this to ignore `editor:click_target` so outlines reflect the actual geometry.
|
/// Like `add_upstream_click_targets` but for visual outlines. `List<Vector>` overrides this to ignore `editor:click_target` so outlines reflect the actual geometry.
|
||||||
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
self.add_upstream_click_targets(outlines);
|
self.add_upstream_click_targets(outlines, inherited_appearance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Store all click targets in a vec which contains the AABB, click target, and path
|
// TODO: Store all click targets in a vec which contains the AABB, click target, and path
|
||||||
// fn add_click_targets(&self, click_targets: &mut Vec<([DVec2; 2], ClickTarget, Vec<NodeId>)>, current_path: Option<NodeId>) {}
|
// fn add_click_targets(&self, click_targets: &mut Vec<([DVec2; 2], ClickTarget, Vec<NodeId>)>, current_path: Option<NodeId>) {}
|
||||||
|
|
||||||
/// Recursively iterate over data in the render (including nested layer stacks upstream of a vector node, in the case of a boolean operation) to collect the footprints, click targets, and vector modify.
|
/// Recursively iterate over data in the render (including nested layer stacks upstream of a vector node, in the case of a boolean operation) to collect the footprints, click targets, and vector modify.
|
||||||
fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option<NodeId>) {}
|
fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option<NodeId>, _inherited_appearance: Option<&Appearance>) {}
|
||||||
|
|
||||||
fn contains_artboard(&self) -> bool {
|
fn contains_artboard(&self) -> bool {
|
||||||
false
|
false
|
||||||
@@ -665,7 +678,7 @@ impl Render for Graphic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>, inherited_appearance: Option<&Appearance>) {
|
||||||
if let Some(element_id) = element_id {
|
if let Some(element_id) = element_id {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => {}
|
Graphic::None => {}
|
||||||
@@ -729,39 +742,39 @@ impl Render for Graphic {
|
|||||||
|
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => (),
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Graphic(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::Vector(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Vector(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::RasterCPU(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::RasterCPU(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::RasterGPU(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Color(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Gradient(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id),
|
Graphic::Text(list) => list.collect_metadata(metadata, footprint, element_id, inherited_appearance),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => (),
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Graphic(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::Vector(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Vector(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::RasterCPU(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::RasterCPU(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::RasterGPU(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::Color(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Color(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Gradient(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
Graphic::Text(list) => list.add_upstream_click_targets(click_targets),
|
Graphic::Text(list) => list.add_upstream_click_targets(click_targets, inherited_appearance),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
match self {
|
match self {
|
||||||
Graphic::None => (),
|
Graphic::None => (),
|
||||||
Graphic::Graphic(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Graphic(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::Vector(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Vector(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::RasterCPU(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::RasterCPU(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::RasterGPU(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::Color(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Color(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Gradient(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
Graphic::Text(list) => list.add_upstream_outline_targets(outlines),
|
Graphic::Text(list) => list.add_upstream_outline_targets(outlines, inherited_appearance),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,7 +903,7 @@ impl Render for List<Artboard> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option<NodeId>, inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue };
|
let Some(content) = self.element(index).map(Artboard::as_graphic_list) else { continue };
|
||||||
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
|
let (location, dimensions, _background, clip) = read_artboard_attributes(self, index);
|
||||||
@@ -912,11 +925,11 @@ impl Render for List<Artboard> {
|
|||||||
|
|
||||||
let mut child_footprint = footprint;
|
let mut child_footprint = footprint;
|
||||||
child_footprint.transform *= DAffine2::from_translation(location);
|
child_footprint.transform *= DAffine2::from_translation(location);
|
||||||
content.collect_metadata(metadata, child_footprint, None);
|
content.collect_metadata(metadata, child_footprint, None, inherited_appearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
|
let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
|
||||||
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
|
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions);
|
||||||
@@ -939,6 +952,12 @@ impl Render for List<Graphic> {
|
|||||||
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
// This item's declared appearance (if any) cascades to descendants lacking their own
|
||||||
|
let child_render_params = self
|
||||||
|
.attribute::<Appearance>(ATTR_APPEARANCE, index)
|
||||||
|
.and_then(Appearance::declared)
|
||||||
|
.map(|appearance| render_params.for_child_item(appearance));
|
||||||
|
let render_params = child_render_params.as_ref().unwrap_or(render_params);
|
||||||
|
|
||||||
let matrix = format_transform_matrix(transform);
|
let matrix = format_transform_matrix(transform);
|
||||||
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled();
|
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled();
|
||||||
@@ -1008,6 +1027,12 @@ impl Render for List<Graphic> {
|
|||||||
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
// This item's declared appearance (if any) cascades to descendants lacking their own
|
||||||
|
let child_render_params = self
|
||||||
|
.attribute::<Appearance>(ATTR_APPEARANCE, index)
|
||||||
|
.and_then(Appearance::declared)
|
||||||
|
.map(|appearance| render_params.for_child_item(appearance));
|
||||||
|
let render_params = child_render_params.as_ref().unwrap_or(render_params);
|
||||||
|
|
||||||
let mut layer = false;
|
let mut layer = false;
|
||||||
|
|
||||||
@@ -1076,21 +1101,23 @@ impl Render for List<Graphic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>, inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
||||||
let layer = layer_path.iter_element_values().next_back().copied();
|
let layer = layer_path.iter_element_values().next_back().copied();
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
// This item's appearance (if any) cascades to descendants lacking their own
|
||||||
|
let child_appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
|
|
||||||
let mut footprint = footprint;
|
let mut footprint = footprint;
|
||||||
footprint.transform *= item_transform;
|
footprint.transform *= item_transform;
|
||||||
|
|
||||||
if let Some(element_id) = layer {
|
if let Some(element_id) = layer {
|
||||||
element.collect_metadata(metadata, footprint, Some(element_id));
|
element.collect_metadata(metadata, footprint, Some(element_id), child_appearance);
|
||||||
} else {
|
} else {
|
||||||
// Recurse through anonymous wrapper items to reach nested content with editor:layer_path tags
|
// Recurse through anonymous wrapper items to reach nested content with editor:layer_path tags
|
||||||
element.collect_metadata(metadata, footprint, None);
|
element.collect_metadata(metadata, footprint, None, child_appearance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1101,9 +1128,10 @@ impl Render for List<Graphic> {
|
|||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
let child_appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
|
|
||||||
let mut new_click_targets = Vec::new();
|
let mut new_click_targets = Vec::new();
|
||||||
element.add_upstream_click_targets(&mut new_click_targets);
|
element.add_upstream_click_targets(&mut new_click_targets, child_appearance);
|
||||||
|
|
||||||
for click_target in new_click_targets.iter_mut() {
|
for click_target in new_click_targets.iter_mut() {
|
||||||
click_target.apply_transform(item_transform)
|
click_target.apply_transform(item_transform)
|
||||||
@@ -1112,7 +1140,7 @@ impl Render for List<Graphic> {
|
|||||||
all_upstream_click_targets.extend(new_click_targets);
|
all_upstream_click_targets.extend(new_click_targets);
|
||||||
|
|
||||||
let mut new_outlines = Vec::new();
|
let mut new_outlines = Vec::new();
|
||||||
element.add_upstream_outline_targets(&mut new_outlines);
|
element.add_upstream_outline_targets(&mut new_outlines, child_appearance);
|
||||||
for outline in new_outlines.iter_mut() {
|
for outline in new_outlines.iter_mut() {
|
||||||
outline.apply_transform(item_transform)
|
outline.apply_transform(item_transform)
|
||||||
}
|
}
|
||||||
@@ -1124,13 +1152,14 @@ impl Render for List<Graphic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
let child_appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
let mut new_click_targets = Vec::new();
|
let mut new_click_targets = Vec::new();
|
||||||
|
|
||||||
element.add_upstream_click_targets(&mut new_click_targets);
|
element.add_upstream_click_targets(&mut new_click_targets, child_appearance);
|
||||||
|
|
||||||
for click_target in new_click_targets.iter_mut() {
|
for click_target in new_click_targets.iter_mut() {
|
||||||
click_target.apply_transform(item_transform)
|
click_target.apply_transform(item_transform)
|
||||||
@@ -1140,13 +1169,14 @@ impl Render for List<Graphic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let element = self.element(index).unwrap();
|
let element = self.element(index).unwrap();
|
||||||
|
let child_appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
let mut new_outlines = Vec::new();
|
let mut new_outlines = Vec::new();
|
||||||
|
|
||||||
element.add_upstream_outline_targets(&mut new_outlines);
|
element.add_upstream_outline_targets(&mut new_outlines, child_appearance);
|
||||||
|
|
||||||
for outline in new_outlines.iter_mut() {
|
for outline in new_outlines.iter_mut() {
|
||||||
outline.apply_transform(item_transform)
|
outline.apply_transform(item_transform)
|
||||||
@@ -1175,16 +1205,29 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
let opacity_attr: f64 = list.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
let opacity_attr: f64 = list.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||||
let opacity_fill_attr: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
let opacity_fill_attr: f64 = list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||||
|
|
||||||
|
// The item's own declared appearance wins over one cascading down from an ancestor
|
||||||
|
let own_appearance = list.attribute::<Appearance>(ATTR_APPEARANCE, index).and_then(Appearance::declared);
|
||||||
|
let appearance = own_appearance.or(render_params.inherited_appearance.as_ref());
|
||||||
|
let FillAndStroke {
|
||||||
|
stroke: stroke_params,
|
||||||
|
fill_paint: fill_graphic_list,
|
||||||
|
stroke_paint: stroke_graphic_list,
|
||||||
|
stroke_below: wants_stroke_below,
|
||||||
|
} = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
|
||||||
|
|
||||||
// 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 = stroke_params.as_ref().filter(|stroke| stroke.weight() > 0.);
|
||||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform_is_invertible(*transform));
|
// A cascaded coverage records its stroke space in the ancestor's coordinates, so this item authors its own
|
||||||
|
let set_stroke_transform = has_real_stroke
|
||||||
|
.map(|stroke| if own_appearance.is_some() { stroke.transform } else { item_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);
|
||||||
let element_transform = set_stroke_transform.map(|stroke_transform| item_transform * stroke_transform.inverse());
|
let element_transform = set_stroke_transform.map(|stroke_transform| item_transform * stroke_transform.inverse());
|
||||||
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, stroke_params.as_ref()).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]);
|
||||||
@@ -1196,26 +1239,22 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
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 stroke_params.as_ref().map(|stroke| stroke.align) == Some(StrokeAlign::Inside) {
|
||||||
MaskType::Clip
|
MaskType::Clip
|
||||||
} else {
|
} else {
|
||||||
MaskType::Mask
|
MaskType::Mask
|
||||||
};
|
};
|
||||||
|
|
||||||
let fill_graphic_list = graphic_list_at(list, index, ATTR_FILL);
|
let fill_graphic = fill_graphic_list.and_then(|l| l.element(0));
|
||||||
let fill_graphic = fill_graphic_list.as_ref().and_then(|l| l.element(0));
|
let stroke_graphic = stroke_graphic_list.and_then(|l| l.element(0));
|
||||||
|
|
||||||
let stroke_graphic_list = graphic_list_at(list, index, ATTR_STROKE);
|
|
||||||
let stroke_graphic = stroke_graphic_list.as_ref().and_then(|l| l.element(0));
|
|
||||||
|
|
||||||
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
|
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
|
||||||
let can_draw_aligned_stroke = path_is_closed
|
let can_draw_aligned_stroke = path_is_closed
|
||||||
&& vector.stroke.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
&& stroke_params.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
||||||
&& stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
|
&& stroke_graphic.is_some_and(|graphic| !graphic.is_fully_transparent());
|
||||||
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip);
|
let can_use_paint_order = !(fill_graphic.is_none_or(|graphic| !graphic.covers_opaquely()) || mask_type == MaskType::Clip);
|
||||||
|
|
||||||
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
|
let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order;
|
||||||
let wants_stroke_below = vector.stroke.as_ref().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow);
|
|
||||||
let override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
|
let override_paint_order = can_draw_aligned_stroke && can_use_paint_order;
|
||||||
let use_face_fill = vector.use_face_fill();
|
let use_face_fill = vector.use_face_fill();
|
||||||
|
|
||||||
@@ -1223,7 +1262,7 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
emit_svg_fill_path(
|
emit_svg_fill_path(
|
||||||
render,
|
render,
|
||||||
path.clone(),
|
path.clone(),
|
||||||
fill_graphic_list.as_deref(),
|
fill_graphic_list,
|
||||||
item_transform,
|
item_transform,
|
||||||
element_transform,
|
element_transform,
|
||||||
applied_stroke_transform,
|
applied_stroke_transform,
|
||||||
@@ -1235,13 +1274,13 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
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));
|
let black_fill = List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK)));
|
||||||
|
mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill));
|
||||||
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)
|
||||||
@@ -1255,7 +1294,7 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
emit_svg_fill_path(
|
emit_svg_fill_path(
|
||||||
render,
|
render,
|
||||||
face_d,
|
face_d,
|
||||||
fill_graphic_list.as_deref(),
|
fill_graphic_list,
|
||||||
item_transform,
|
item_transform,
|
||||||
element_transform,
|
element_transform,
|
||||||
applied_stroke_transform,
|
applied_stroke_transform,
|
||||||
@@ -1276,10 +1315,9 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
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();
|
|
||||||
// `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_params.as_ref().map(|stroke| stroke.max_aabb_inflation(true)).unwrap_or_default() * largest_scale;
|
||||||
let quad = Quad::from_box(transformed_bounds).inflate(inflation);
|
let quad = Quad::from_box(transformed_bounds).inflate(inflation);
|
||||||
let (x, y) = quad.top_left().into();
|
let (x, y) = quad.top_left().into();
|
||||||
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
||||||
@@ -1301,13 +1339,12 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
|
|
||||||
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 = stroke_params
|
||||||
.stroke
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|stroke| {
|
.map(|stroke| {
|
||||||
if stroke_graphic_list.as_deref().is_some_and(is_paint_present) {
|
if stroke_graphic_list.is_some() {
|
||||||
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)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
@@ -1316,10 +1353,10 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
.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 = stroke_params.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke()) && stroke_graphic.is_some_and(|g| !g.is_fully_transparent());
|
||||||
let stroke_attribute = if stroke_visible {
|
let stroke_attribute = if stroke_visible {
|
||||||
stroke_graphic_list
|
stroke_graphic_list
|
||||||
.as_deref()
|
.as_ref()
|
||||||
.map(|list| {
|
.map(|list| {
|
||||||
// Gradient should align with the fill path bbox so that a shared gradient lines up across fill and stroke.
|
// Gradient should align with the fill path bbox so that a shared gradient lines up across fill and stroke.
|
||||||
// Only clipping-based paints need the stroke-inclusive bbox.
|
// Only clipping-based paints need the stroke-inclusive bbox.
|
||||||
@@ -1338,7 +1375,7 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
r#" fill="none""#.to_string()
|
r#" fill="none""#.to_string()
|
||||||
} else {
|
} else {
|
||||||
fill_graphic_list
|
fill_graphic_list
|
||||||
.as_deref()
|
.as_ref()
|
||||||
.map(|list| list.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Fill))
|
.map(|list| list.render(defs, item_transform, element_transform, applied_stroke_transform, bounds_matrix, &render_params, PaintTarget::Fill))
|
||||||
.unwrap_or_else(|| r#" fill="none""#.to_string())
|
.unwrap_or_else(|| r#" fill="none""#.to_string())
|
||||||
};
|
};
|
||||||
@@ -1370,7 +1407,7 @@ fn render_vector_item_svg(list: &List<Vector>, index: usize, vector: &Vector, re
|
|||||||
emit_svg_fill_path(
|
emit_svg_fill_path(
|
||||||
render,
|
render,
|
||||||
path.clone(),
|
path.clone(),
|
||||||
fill_graphic_list.as_deref(),
|
fill_graphic_list,
|
||||||
item_transform,
|
item_transform,
|
||||||
element_transform,
|
element_transform,
|
||||||
applied_stroke_transform,
|
applied_stroke_transform,
|
||||||
@@ -1425,17 +1462,35 @@ impl Render for List<Vector> {
|
|||||||
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) {
|
||||||
let mut clip_masker: Option<List<Vector>> = None;
|
let mut clip_masker: Option<List<Vector>> = None;
|
||||||
|
|
||||||
for index in 0..self.len() {
|
// A paint subtree supplies its own styling, so an element's appearance must not cascade into it
|
||||||
use graphic_types::vector_types::vector;
|
let paint_render_params = RenderParams {
|
||||||
|
inherited_appearance: None,
|
||||||
|
..render_params.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
for index in 0..self.len() {
|
||||||
let Some(element) = self.element(index) else { continue };
|
let Some(element) = self.element(index) else { continue };
|
||||||
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
|
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
|
||||||
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||||
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||||
let multiplied_transform = parent_transform * item_transform;
|
let multiplied_transform = parent_transform * item_transform;
|
||||||
let has_real_stroke = element.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));
|
// The item's own declared appearance wins over one cascading down from an ancestor
|
||||||
|
let own_appearance = self.attribute::<Appearance>(ATTR_APPEARANCE, index).and_then(Appearance::declared);
|
||||||
|
let appearance = own_appearance.or(render_params.inherited_appearance.as_ref());
|
||||||
|
let FillAndStroke {
|
||||||
|
stroke: stroke_params,
|
||||||
|
fill_paint: fill_graphic_list,
|
||||||
|
stroke_paint: stroke_graphic_list,
|
||||||
|
stroke_below: wants_stroke_below,
|
||||||
|
} = appearance.map(Appearance::fill_and_stroke).unwrap_or_default();
|
||||||
|
|
||||||
|
let has_real_stroke = stroke_params.as_ref().filter(|stroke| stroke.weight() > 0.);
|
||||||
|
// A cascaded coverage records its stroke space in the ancestor's coordinates, so this item authors its own
|
||||||
|
let set_stroke_transform = has_real_stroke
|
||||||
|
.map(|stroke| if own_appearance.is_some() { stroke.transform } else { item_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
|
||||||
.map(|stroke_transform| multiplied_transform * stroke_transform.inverse())
|
.map(|stroke_transform| multiplied_transform * stroke_transform.inverse())
|
||||||
@@ -1458,9 +1513,6 @@ impl Render for List<Vector> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fill_graphic_list = graphic_list_at(self, index, ATTR_FILL);
|
|
||||||
let stroke_graphic_list = graphic_list_at(self, index, ATTR_STROKE);
|
|
||||||
|
|
||||||
// If we're using opacity or a blend mode, we need to push a layer
|
// If we're using opacity or a blend mode, we need to push a layer
|
||||||
let blend_mode = match render_params.render_mode {
|
let blend_mode = match render_params.render_mode {
|
||||||
RenderMode::Outline => peniko::Mix::Normal,
|
RenderMode::Outline => peniko::Mix::Normal,
|
||||||
@@ -1471,8 +1523,8 @@ impl Render for List<Vector> {
|
|||||||
// Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
|
// Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
|
||||||
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
|
// Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
|
||||||
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
|
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
|
||||||
let stroke = element.stroke.as_ref();
|
let stroke = stroke_params.as_ref();
|
||||||
let stroke_fully_transparent = stroke_graphic_list.as_ref().is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
|
let stroke_fully_transparent = stroke_graphic_list.is_none_or(|l| l.element(0).is_none_or(|g| g.is_fully_transparent()));
|
||||||
let can_draw_aligned_stroke =
|
let can_draw_aligned_stroke =
|
||||||
!stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
|
!stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
|
||||||
|
|
||||||
@@ -1526,10 +1578,9 @@ impl Render for List<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);
|
|
||||||
|
|
||||||
let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| {
|
let do_fill_path = |scene: &mut Scene, context: &mut RenderContext, path: &kurbo::BezPath, fill_rule: peniko::Fill| {
|
||||||
let Some(fill_graphic) = fill_graphic_list.as_deref() else { return };
|
let Some(fill_graphic) = fill_graphic_list else { return };
|
||||||
|
|
||||||
for paint_index in 0..fill_graphic.len() {
|
for paint_index in 0..fill_graphic.len() {
|
||||||
let Some(paint) = fill_graphic.element(paint_index) else { continue };
|
let Some(paint) = fill_graphic.element(paint_index) else { continue };
|
||||||
@@ -1556,7 +1607,7 @@ impl Render for List<Vector> {
|
|||||||
}
|
}
|
||||||
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
|
Graphic::Vector(_) | Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Graphic(_) | Graphic::Text(_) => {
|
||||||
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
|
scene.push_clip_layer(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), path);
|
||||||
paint.render_to_vello(scene, multiplied_transform, context, render_params);
|
paint.render_to_vello(scene, multiplied_transform, context, &paint_render_params);
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1583,7 +1634,7 @@ impl Render for List<Vector> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| {
|
let do_stroke = |scene: &mut Scene, width_scale: f64, context: &mut RenderContext| {
|
||||||
let Some(stroke_graphic_list) = stroke_graphic_list.as_deref() else { return };
|
let Some(stroke_graphic_list) = stroke_graphic_list else { return };
|
||||||
let Some(stroke) = stroke else { return };
|
let Some(stroke) = stroke else { return };
|
||||||
|
|
||||||
for paint_index in 0..stroke_graphic_list.len() {
|
for paint_index in 0..stroke_graphic_list.len() {
|
||||||
@@ -1641,7 +1692,7 @@ impl Render for List<Vector> {
|
|||||||
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
|
let stroked = peniko::kurbo::stroke(path.iter(), &stroke, &StrokeOpts::default(), 0.01);
|
||||||
|
|
||||||
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked);
|
scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &stroked);
|
||||||
stroke_graphic.render_to_vello(scene, multiplied_transform, context, render_params);
|
stroke_graphic.render_to_vello(scene, multiplied_transform, context, &paint_render_params);
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1657,13 +1708,13 @@ impl Render for List<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));
|
let black_fill = List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK)));
|
||||||
|
mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill));
|
||||||
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);
|
||||||
@@ -1711,7 +1762,7 @@ impl Render for List<Vector> {
|
|||||||
Stroke,
|
Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
let order = match stroke.is_some_and(|stroke| !stroke.paint_order.is_default()) {
|
let order = match 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
|
||||||
};
|
};
|
||||||
@@ -1738,7 +1789,7 @@ impl Render for List<Vector> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>, inherited_appearance: Option<&Appearance>) {
|
||||||
// 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();
|
||||||
@@ -1751,6 +1802,8 @@ impl Render for List<Vector> {
|
|||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
let layer_path: List<NodeId> = self.attribute_cloned_or_default::<NodeIdPath>(ATTR_EDITOR_LAYER_PATH, index).0;
|
||||||
let layer = layer_path.iter_element_values().next_back().copied();
|
let layer = layer_path.iter_element_values().next_back().copied();
|
||||||
|
// The item's own appearance wins over one cascading down from an ancestor
|
||||||
|
let appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
|
|
||||||
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);
|
||||||
@@ -1766,12 +1819,12 @@ impl Render for List<Vector> {
|
|||||||
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, self, index, click_target_vector, item_relative_transform);
|
extend_targets_from_vector(&mut click_targets_unwrapped, appearance, 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, self, index, source, item_relative_transform);
|
extend_targets_from_vector(&mut outlines_unwrapped, appearance, source, 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.
|
||||||
@@ -1781,11 +1834,8 @@ impl Render for List<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(source.clone()));
|
e.insert(Arc::new(source.clone()));
|
||||||
|
|
||||||
if let Some(fill_graphic) = graphic_list_at(self, index, ATTR_FILL) {
|
if let Some(appearance) = appearance {
|
||||||
metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.into_owned()));
|
metadata.appearance_attributes.insert(element_id, Arc::new(appearance.clone()));
|
||||||
}
|
|
||||||
if let Some(stroke_graphic) = graphic_list_at(self, index, ATTR_STROKE) {
|
|
||||||
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.into_owned()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1802,7 +1852,8 @@ impl Render for List<Vector> {
|
|||||||
if !upstream_nested_layers.is_empty() {
|
if !upstream_nested_layers.is_empty() {
|
||||||
let mut upstream_footprint = footprint;
|
let mut upstream_footprint = footprint;
|
||||||
upstream_footprint.transform *= transform;
|
upstream_footprint.transform *= transform;
|
||||||
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None);
|
// Snapshot layers carry their own styling, so the merged result's appearance must not cascade into them
|
||||||
|
upstream_nested_layers.collect_metadata(metadata, upstream_footprint, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1824,25 +1875,27 @@ impl Render for List<Vector> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let Some(source) = self.element(index) else { continue };
|
let Some(source) = self.element(index) else { continue };
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
let appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
|
|
||||||
// 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 = self.attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source);
|
let vector = self.attribute::<Vector>(ATTR_EDITOR_CLICK_TARGET, index).unwrap_or(source);
|
||||||
|
|
||||||
extend_targets_from_vector(click_targets, self, index, vector, transform);
|
extend_targets_from_vector(click_targets, appearance, vector, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>, inherited_appearance: Option<&Appearance>) {
|
||||||
// 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..self.len() {
|
for index in 0..self.len() {
|
||||||
let Some(source) = self.element(index) else { continue };
|
let Some(source) = self.element(index) else { continue };
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
|
let appearance = Appearance::cascade(self.attribute::<Appearance>(ATTR_APPEARANCE, index), inherited_appearance);
|
||||||
|
|
||||||
extend_targets_from_vector(outlines, self, index, source, transform);
|
extend_targets_from_vector(outlines, appearance, source, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1855,15 +1908,17 @@ impl Render for List<Vector> {
|
|||||||
|
|
||||||
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
|
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work
|
||||||
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
|
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
|
||||||
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, vector_list: &List<Vector>, index: usize, geometry: &Vector, transform: DAffine2) {
|
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, appearance: Option<&Appearance>, geometry: &Vector, transform: DAffine2) {
|
||||||
let filled = has_paint_at(vector_list, index, ATTR_FILL);
|
// A coverage whose paint is `Graphic::None` exists but paints nothing, so it does not close subpaths for hit testing
|
||||||
|
let filled = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
|
||||||
|
|
||||||
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
|
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect();
|
||||||
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
|
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed());
|
||||||
|
|
||||||
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
|
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
|
||||||
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
|
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
|
||||||
let stroke_width = geometry.stroke.as_ref().map_or(0., |stroke| {
|
let stroke_width = appearance.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke)).map_or(0., |coverage| {
|
||||||
|
let stroke = coverage.stroke_params();
|
||||||
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 {
|
||||||
@@ -2048,7 +2103,7 @@ impl Render for List<Raster<CPU>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>, _inherited_appearance: Option<&Appearance>) {
|
||||||
let Some(element_id) = element_id else { return };
|
let Some(element_id) = element_id else { return };
|
||||||
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||||
|
|
||||||
@@ -2067,12 +2122,12 @@ impl Render for List<Raster<CPU>> {
|
|||||||
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
|
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
|
||||||
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, 0);
|
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, 0);
|
||||||
if !upstream_nested_layers.is_empty() {
|
if !upstream_nested_layers.is_empty() {
|
||||||
upstream_nested_layers.collect_metadata(metadata, footprint, None);
|
upstream_nested_layers.collect_metadata(metadata, footprint, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
// The unit square is the raster's own space, so its placement only exists in the item transform
|
// The unit square is the raster's own space, so its placement only exists in the item transform
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
@@ -2149,7 +2204,7 @@ impl Render for List<Raster<GPU>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>, _inherited_appearance: Option<&Appearance>) {
|
||||||
let Some(element_id) = element_id else { return };
|
let Some(element_id) = element_id else { return };
|
||||||
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||||
|
|
||||||
@@ -2168,12 +2223,12 @@ impl Render for List<Raster<GPU>> {
|
|||||||
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
|
// multiply in `transform` (which is the rasterization area, not a layer-stack transform).
|
||||||
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, 0);
|
let upstream_nested_layers = self.attribute_cloned_or_default::<List<Graphic>>(ATTR_EDITOR_MERGED_LAYERS, 0);
|
||||||
if !upstream_nested_layers.is_empty() {
|
if !upstream_nested_layers.is_empty() {
|
||||||
upstream_nested_layers.collect_metadata(metadata, footprint, None);
|
upstream_nested_layers.collect_metadata(metadata, footprint, None, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
|
||||||
@@ -2432,7 +2487,7 @@ impl Render for List<Gradient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option<NodeId>) {
|
fn collect_metadata(&self, metadata: &mut RenderMetadata, _footprint: Footprint, element_id: Option<NodeId>, _inherited_appearance: Option<&Appearance>) {
|
||||||
let Some(element_id) = element_id else { return };
|
let Some(element_id) = element_id else { return };
|
||||||
if self.is_empty() {
|
if self.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -2468,7 +2523,7 @@ impl Render for List<Gradient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
if !gradient_control_interior_is_clickable(gradient_form) {
|
if !gradient_control_interior_is_clickable(gradient_form) {
|
||||||
@@ -2482,7 +2537,7 @@ impl Render for List<Gradient> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>) {
|
fn add_upstream_outline_targets(&self, outlines: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
let gradient_form: GradientForm = self.attribute_cloned_or_default(ATTR_GRADIENT_FORM, index);
|
||||||
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||||
@@ -2813,7 +2868,7 @@ impl Render for List<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>, _inherited_appearance: Option<&Appearance>) {
|
||||||
// Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`.
|
// Click targets are baked relative to item 0's transform, which `Graphic::collect_metadata` records as `local_transforms[element_id]`.
|
||||||
let item_zero_transform: DAffine2 = if !self.is_empty() {
|
let item_zero_transform: DAffine2 = if !self.is_empty() {
|
||||||
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
self.attribute_cloned_or_default(ATTR_TRANSFORM, 0)
|
||||||
@@ -2853,7 +2908,7 @@ impl Render for List<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
|
||||||
for index in 0..self.len() {
|
for index in 0..self.len() {
|
||||||
let Some((size, transform)) = text_item_size_and_transform(self, index) else { continue };
|
let Some((size, transform)) = text_item_size_and_transform(self, index) else { continue };
|
||||||
let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
|
let subpath = Subpath::new_rectangle(DVec2::ZERO, size);
|
||||||
|
|||||||
@@ -715,7 +715,7 @@ impl SmoothPath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stepped holds each stop's color the whole way to the next stop, so the ramp jumps at stops and midpoints are inert.
|
/// Stepped holds each stop's color the whole way to the next stop, so the ramp jumps at stops and midpoints are ignored.
|
||||||
fn stepped_color(stops: &[GradientStop], t: f64, gradient_cyclic: bool) -> Color {
|
fn stepped_color(stops: &[GradientStop], t: f64, gradient_cyclic: bool) -> Color {
|
||||||
let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return Color::BLACK };
|
let (Some(first), Some(last)) = (stops.first(), stops.last()) else { return Color::BLACK };
|
||||||
|
|
||||||
|
|||||||
@@ -563,9 +563,9 @@ pub struct HandleId {
|
|||||||
impl std::fmt::Display for HandleId {
|
impl std::fmt::Display for HandleId {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self.ty {
|
match self.ty {
|
||||||
// I haven't checked if "out" and "in" are reversed, or are accurate translations of the "primary" and "end" terms used in the `HandleType` enum, so this naming is an assumption.
|
// The primary handle sits at the segment's start anchor and the end handle at its end anchor
|
||||||
HandleType::Primary => write!(f, "{} out", self.segment.inner()),
|
HandleType::Primary => write!(f, "Segment {} (start handle)", self.segment.inner()),
|
||||||
HandleType::End => write!(f, "{} in", self.segment.inner()),
|
HandleType::End => write!(f, "Segment {} (end handle)", self.segment.inner()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ impl StrokeAlign {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backs the control bar's stroke popover radio and legacy document parsing: the relative order
|
||||||
|
// of the Fill and Stroke nodes in the chain is what actually determines the paint order
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
||||||
@@ -235,8 +237,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 +250,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 +286,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 +400,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 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,10 +242,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());
|
||||||
@@ -538,40 +533,16 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The element sees only geometry; stroke inflation is applied at the row level by `vector_list_bounding_box`
|
||||||
|
// in graphic-types, which can read the appearance attribute the stroke parameters live on.
|
||||||
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, CoverPlacement, Coverage, Graphic, Vector, stamp_coverage};
|
||||||
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;
|
||||||
|
|||||||
@@ -44,7 +44,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],
|
||||||
@@ -61,7 +61,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>,
|
||||||
@@ -86,7 +86,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,
|
||||||
@@ -363,7 +363,7 @@ pub async fn render_output_cache<'a: 'n>(
|
|||||||
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(),
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
|
|||||||
|
|
||||||
let footprint = Footprint::default();
|
let footprint = Footprint::default();
|
||||||
let mut metadata = RenderMetadata::default();
|
let mut metadata = RenderMetadata::default();
|
||||||
data.collect_metadata(&mut metadata, footprint, None);
|
data.collect_metadata(&mut metadata, footprint, None, None);
|
||||||
let intermediate = match &render_params.render_output_type {
|
let intermediate = match &render_params.render_output_type {
|
||||||
RenderOutputTypeRequest::Vello => {
|
RenderOutputTypeRequest::Vello => {
|
||||||
let mut scene = vello::Scene::new();
|
let mut scene = vello::Scene::new();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use core_types::list::{Item, List, NodeIdPath};
|
use core_types::list::{ATTR_APPEARANCE, Item, List, NodeIdPath};
|
||||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx};
|
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
use graphic_types::Appearance;
|
||||||
use graphic_types::graphic::bake_paint_transforms;
|
use graphic_types::graphic::bake_paint_transforms;
|
||||||
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;
|
||||||
@@ -45,7 +46,15 @@ async fn boolean_operation<I: graphic_types::IntoGraphicList>(
|
|||||||
|
|
||||||
let result_vector = result_vector_list.element_mut(0).unwrap();
|
let result_vector = result_vector_list.element_mut(0).unwrap();
|
||||||
Vector::transform(result_vector, transform);
|
Vector::transform(result_vector, transform);
|
||||||
result_vector.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
// The geometry is baked into identity space, so a copied stroke authoring space would be stale
|
||||||
|
if let Some(appearance) = result_vector_list.attribute::<Appearance>(ATTR_APPEARANCE, 0) {
|
||||||
|
let mut appearance = appearance.clone();
|
||||||
|
for coverage in appearance.0.iter_element_values_mut() {
|
||||||
|
coverage.0.remove_attribute::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
}
|
||||||
|
result_vector_list.set_attribute(ATTR_APPEARANCE, 0, appearance);
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
|
// Snapshot the input layers as the `editor:merged_layers` attribute so the renderer can recurse into them
|
||||||
// for editor click-target preservation.
|
// for editor click-target preservation.
|
||||||
@@ -142,12 +151,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()
|
||||||
};
|
};
|
||||||
@@ -191,6 +195,7 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
let parent_opacity: f64 = graphic_list.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
let parent_opacity: f64 = graphic_list.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||||
let parent_fill: f64 = graphic_list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
let parent_fill: f64 = graphic_list.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||||
let layer_path: NodeIdPath = graphic_list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
let layer_path: NodeIdPath = graphic_list.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, index);
|
||||||
|
let parent_appearance = graphic_list.attribute::<Appearance>(ATTR_APPEARANCE, index).and_then(Appearance::declared).cloned();
|
||||||
|
|
||||||
let compose_parent = |mut item: Item<Vector>| {
|
let compose_parent = |mut item: Item<Vector>| {
|
||||||
if parent_has_transform || item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some() {
|
if parent_has_transform || item.attribute::<DAffine2>(ATTR_TRANSFORM).is_some() {
|
||||||
@@ -208,6 +213,12 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
if parent_has_layer_path {
|
if parent_has_layer_path {
|
||||||
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
|
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
|
||||||
}
|
}
|
||||||
|
// Appearance cascades into each child whose own is undeclared, since a declared child wins wholesale
|
||||||
|
if let Some(appearance) = &parent_appearance
|
||||||
|
&& item.attribute::<Appearance>(ATTR_APPEARANCE).and_then(Appearance::declared).is_none()
|
||||||
|
{
|
||||||
|
item.set_attribute(ATTR_APPEARANCE, appearance.clone());
|
||||||
|
}
|
||||||
|
|
||||||
item
|
item
|
||||||
};
|
};
|
||||||
@@ -231,6 +242,13 @@ fn flatten_vector(graphic_list: &List<Graphic>) -> List<Vector> {
|
|||||||
*fill *= parent_fill;
|
*fill *= parent_fill;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(appearance) = &parent_appearance {
|
||||||
|
for value in graphic.iter_attribute_values_mut_or_default::<Appearance>(ATTR_APPEARANCE) {
|
||||||
|
if value.is_empty() {
|
||||||
|
*value = appearance.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unioning each group lets it enter the outer operation as one region, which is why this cannot defer
|
// Unioning each group lets it enter the outer operation as one region, which is why this cannot defer
|
||||||
// to `flatten_graphic_list`. Splicing children in as sibling operands makes them subtract from each other.
|
// to `flatten_graphic_list`. Splicing children in as sibling operands makes them subtract from each other.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU};
|
|||||||
use core::hash::{Hash, Hasher};
|
use core::hash::{Hash, Hasher};
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
|
use core_types::list::{ATTR_APPEARANCE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath};
|
||||||
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
|
||||||
use core_types::transform::{Footprint, Transform};
|
use core_types::transform::{Footprint, Transform};
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
@@ -13,9 +13,9 @@ use core_types::{
|
|||||||
};
|
};
|
||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
use graphic_types::graphic::{bake_paint_transforms, graphic_list_at, has_paint_at, is_paint_present, set_paint_attribute_at};
|
use graphic_types::graphic::{bake_paint_transforms, is_paint_present};
|
||||||
use graphic_types::raster_types::{CPU, GPU, Raster};
|
use graphic_types::raster_types::{CPU, GPU, Raster};
|
||||||
use graphic_types::{Graphic, IntoGraphicList};
|
use graphic_types::{Appearance, Cover, CoverPlacement, Coverage, Graphic, IntoGraphicList, stamp_coverage};
|
||||||
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};
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
@@ -32,7 +32,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, GradientSettings, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use vector_types::vector::style::{DashPattern, Gradient, GradientSettings, 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};
|
||||||
|
|
||||||
@@ -72,8 +72,6 @@ impl VectorListIterMut for List<Vector> {
|
|||||||
/// single `Item<Vector>` or `Item<Graphic>`.
|
/// single `Item<Vector>` or `Item<Graphic>`.
|
||||||
trait VectorItemMut {
|
trait VectorItemMut {
|
||||||
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2));
|
fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2));
|
||||||
|
|
||||||
fn set_vector_paint(&mut self, key: &str, paint: List<Graphic>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VectorItemMut for Item<Vector> {
|
impl VectorItemMut for Item<Vector> {
|
||||||
@@ -81,10 +79,6 @@ impl VectorItemMut for Item<Vector> {
|
|||||||
let transform = self.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
|
let transform = self.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
f(self.element_mut(), transform);
|
f(self.element_mut(), transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_vector_paint(&mut self, key: &str, paint: List<Graphic>) {
|
|
||||||
self.set_attribute(key, paint);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VectorItemMut for Item<Graphic> {
|
impl VectorItemMut for Item<Graphic> {
|
||||||
@@ -95,13 +89,6 @@ impl VectorItemMut for Item<Graphic> {
|
|||||||
f(vector, *transform);
|
f(vector, *transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_vector_paint(&mut self, key: &str, paint: List<Graphic>) {
|
|
||||||
let Some(vector_list) = self.element_mut().as_vector_mut() else { return };
|
|
||||||
for slot in vector_list.iter_attribute_values_mut_or_default::<List<Graphic>>(key) {
|
|
||||||
*slot = paint.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Geometry counterpart to [`VectorItemMut`] for the element-wise modifier nodes, running a per-item transformation over
|
/// Geometry counterpart to [`VectorItemMut`] for the element-wise modifier nodes, running a per-item transformation over
|
||||||
@@ -252,13 +239,24 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let color = evaluator.evaluate(factor);
|
let color = evaluator.evaluate(factor);
|
||||||
let paint = List::new_from_element(color).into_graphic_list();
|
let color_paint = List::new_from_element(color).into_graphic_list();
|
||||||
|
|
||||||
if fill {
|
if fill {
|
||||||
set_paint_attribute_at(vector_list, index, ATTR_FILL, paint.clone());
|
vector_list.with_attribute_mut_or_default::<Appearance, _, _>(ATTR_APPEARANCE, index, |appearance| {
|
||||||
|
if !appearance.set_paint_of(Cover::Fill, color_paint.clone()) {
|
||||||
|
appearance.replace_or_insert(Coverage::new_fill(), color_paint.clone(), CoverPlacement::Below);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if stroke && vector_list.element(index).is_some_and(|vector| vector.stroke.is_some()) {
|
// The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
|
||||||
set_paint_attribute_at(vector_list, index, ATTR_STROKE, paint.clone());
|
if stroke
|
||||||
|
&& vector_list
|
||||||
|
.attribute::<Appearance>(ATTR_APPEARANCE, index)
|
||||||
|
.is_some_and(|appearance| appearance.has_cover(Cover::Stroke))
|
||||||
|
{
|
||||||
|
vector_list.with_attribute_mut_or_default::<Appearance, _, _>(ATTR_APPEARANCE, index, |appearance| {
|
||||||
|
appearance.set_paint_of(Cover::Stroke, color_paint.clone());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -339,7 +337,8 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content.set_vector_paint(ATTR_FILL, fill);
|
// Appending follows the painter's algorithm: the most downstream paint node in the chain paints on top
|
||||||
|
stamp_coverage(&mut content, Coverage::new_fill(), fill, CoverPlacement::Above);
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +369,6 @@ async fn stroke<V, P: IntoGraphicList + 'n + Send + 'static>(
|
|||||||
/// 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: Item<f64>,
|
miter_limit: Item<f64>,
|
||||||
// <https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty>
|
|
||||||
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
|
|
||||||
paint_order: Item<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: Item<DashPattern>,
|
dash_pattern: Item<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.
|
||||||
@@ -383,13 +379,12 @@ where
|
|||||||
Item<V>: VectorItemMut + 'n + Send,
|
Item<V>: VectorItemMut + 'n + Send,
|
||||||
{
|
{
|
||||||
let mut content = content;
|
let mut content = content;
|
||||||
let (weight, align, cap, join, miter_limit, paint_order, dash_offset) = (
|
let (weight, align, cap, join, miter_limit, dash_offset) = (
|
||||||
weight.into_element(),
|
weight.into_element(),
|
||||||
align.into_element(),
|
align.into_element(),
|
||||||
cap.into_element(),
|
cap.into_element(),
|
||||||
join.into_element(),
|
join.into_element(),
|
||||||
miter_limit.into_element(),
|
miter_limit.into_element(),
|
||||||
paint_order.into_element(),
|
|
||||||
dash_offset.into_element(),
|
dash_offset.into_element(),
|
||||||
);
|
);
|
||||||
let dash_lengths = dash_pattern.into_element().clamped_lengths();
|
let dash_lengths = dash_pattern.into_element().clamped_lengths();
|
||||||
@@ -403,17 +398,17 @@ where
|
|||||||
join_miter_limit: miter_limit,
|
join_miter_limit: miter_limit,
|
||||||
align,
|
align,
|
||||||
transform: DAffine2::IDENTITY,
|
transform: DAffine2::IDENTITY,
|
||||||
paint_order,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
content.for_each_vector_mut(|vector, transform| {
|
|
||||||
let mut stroke = stroke.clone();
|
|
||||||
stroke.transform *= transform;
|
|
||||||
vector.stroke = Some(stroke);
|
|
||||||
});
|
|
||||||
|
|
||||||
let paint = paint.into_graphic_list();
|
let paint = paint.into_graphic_list();
|
||||||
content.set_vector_paint(ATTR_STROKE, paint);
|
|
||||||
|
// The coverage records the stroke's authoring space, so the item transform is composed in. Its translation
|
||||||
|
// cancels out in every consumer, so it is cleared to let an otherwise-identity capture elide.
|
||||||
|
let mut coverage_stroke = stroke;
|
||||||
|
coverage_stroke.transform *= content.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
|
||||||
|
coverage_stroke.transform.translation = DVec2::ZERO;
|
||||||
|
|
||||||
|
stamp_coverage(&mut content, Coverage::new_stroke(&coverage_stroke), paint, CoverPlacement::Above);
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,10 +538,7 @@ async fn round_corners<V: MapVectorItems + 'n + Send>(
|
|||||||
// 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());
|
||||||
@@ -923,8 +915,6 @@ async fn box_warp<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
*row.element_mut() = result;
|
*row.element_mut() = result;
|
||||||
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY);
|
row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY);
|
||||||
@@ -1086,10 +1076,7 @@ async fn auto_tangents<V: MapVectorItems + 'n + Send>(
|
|||||||
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
|
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||||
let (source, attributes) = source.into_parts();
|
let (source, attributes) = source.into_parts();
|
||||||
|
|
||||||
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);
|
||||||
@@ -1225,7 +1212,7 @@ async fn auto_tangents<V: MapVectorItems + 'n + Send>(
|
|||||||
async fn bounding_box<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> Item<V> {
|
async fn bounding_box<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<V>) -> Item<V> {
|
||||||
V::map_vector_items(content, |content| {
|
V::map_vector_items(content, |content| {
|
||||||
let mut content = content;
|
let mut content = content;
|
||||||
let mut result = content
|
let result = content
|
||||||
.element()
|
.element()
|
||||||
.bounding_box_rect()
|
.bounding_box_rect()
|
||||||
.map(|bbox| {
|
.map(|bbox| {
|
||||||
@@ -1235,9 +1222,6 @@ async fn bounding_box<V: MapVectorItems + 'n + Send>(_: impl Ctx, #[implementati
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
result.stroke = std::mem::take(&mut content.element_mut().stroke);
|
|
||||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
|
||||||
*content.element_mut() = result;
|
*content.element_mut() = result;
|
||||||
content
|
content
|
||||||
})
|
})
|
||||||
@@ -1481,11 +1465,7 @@ async fn offset_path<V: MapVectorItems + 'n + Send>(
|
|||||||
let vector = std::mem::take(content.element_mut());
|
let vector = std::mem::take(content.element_mut());
|
||||||
|
|
||||||
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 {
|
||||||
@@ -1568,16 +1548,28 @@ fn solidify_stroke_list_with_snapshot(source: List<Vector>) -> List<Vector> {
|
|||||||
/// Replaces each item's stroke with filled outline geometry, emitting a separate fill item beside it when the
|
/// Replaces each item's stroke with filled outline geometry, emitting a separate fill item beside it when the
|
||||||
/// original carried a fill. Grows the item count by up to 2x.
|
/// original carried a fill. Grows the item count by up to 2x.
|
||||||
fn solidify_stroke_list(content: List<Vector>) -> List<Vector> {
|
fn solidify_stroke_list(content: List<Vector>) -> List<Vector> {
|
||||||
// A fill exists when the canonical attribute carries paint
|
// A fill exists when its coverage carries paint that draws something
|
||||||
let has_fills: Vec<bool> = (0..content.len()).map(|index| has_paint_at(&content, index, ATTR_FILL)).collect();
|
let has_fills: Vec<bool> = (0..content.len())
|
||||||
|
.map(|index| {
|
||||||
|
content
|
||||||
|
.attribute::<Appearance>(ATTR_APPEARANCE, index)
|
||||||
|
.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
content
|
content
|
||||||
.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>(ATTR_APPEARANCE).cloned().unwrap_or_default();
|
||||||
|
let stroke = appearance.first_coverage_of(Cover::Stroke).map(Coverage::stroke_params).unwrap_or_default();
|
||||||
|
// List order is paint order: a stroke coverage before the first fill coverage paints below it
|
||||||
|
let stroke_below = appearance
|
||||||
|
.first_index_of(Cover::Stroke)
|
||||||
|
.zip(appearance.first_index_of(Cover::Fill))
|
||||||
|
.is_some_and(|(stroke_index, fill_index)| stroke_index < fill_index);
|
||||||
let bezpaths = vector.stroke_bezpath_iter();
|
let bezpaths = vector.stroke_bezpath_iter();
|
||||||
let mut solidified_stroke = Vector::default();
|
let mut solidified_stroke = Vector::default();
|
||||||
|
|
||||||
@@ -1593,9 +1585,8 @@ fn solidify_stroke_list(content: List<Vector>) -> List<Vector> {
|
|||||||
StrokeCap::Square => kurbo::Cap::Square,
|
StrokeCap::Square => kurbo::Cap::Square,
|
||||||
};
|
};
|
||||||
let dash_offset = stroke.dash_offset;
|
let dash_offset = stroke.dash_offset;
|
||||||
let dash_pattern = stroke.dash_lengths;
|
let dash_pattern = stroke.dash_lengths.clone();
|
||||||
let miter_limit = stroke.join_miter_limit;
|
let miter_limit = stroke.join_miter_limit;
|
||||||
let paint_order = stroke.paint_order;
|
|
||||||
|
|
||||||
let stroke_style = kurbo::Stroke::new(stroke.weight)
|
let stroke_style = kurbo::Stroke::new(stroke.weight)
|
||||||
.with_caps(cap)
|
.with_caps(cap)
|
||||||
@@ -1624,26 +1615,29 @@ fn solidify_stroke_list(content: 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(mut appearance) = fill_attributes.remove::<Appearance>(ATTR_APPEARANCE) {
|
||||||
fill_attributes.remove::<List<Graphic>>(ATTR_STROKE);
|
appearance.retain_cover(Cover::Fill);
|
||||||
|
fill_attributes.insert(ATTR_APPEARANCE, appearance);
|
||||||
|
}
|
||||||
Item::from_parts(vector, fill_attributes)
|
Item::from_parts(vector, fill_attributes)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The outlined stroke geometry becomes a fill painted with the original stroke's paint
|
||||||
let mut stroke_attributes = attributes;
|
let mut stroke_attributes = attributes;
|
||||||
// Drop the original fill and use the stroke paint to fill the outlined stroke
|
if stroke_attributes.remove::<Appearance>(ATTR_APPEARANCE).is_some() {
|
||||||
stroke_attributes.remove::<List<Graphic>>(ATTR_FILL);
|
let stroke_paint = appearance.first_index_of(Cover::Stroke).and_then(|index| appearance.paint_at(index).cloned()).unwrap_or_default();
|
||||||
stroke_attributes.rename(ATTR_STROKE, ATTR_FILL);
|
stroke_attributes.insert(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), stroke_paint));
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
// 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()
|
||||||
@@ -1667,7 +1661,6 @@ async fn separate_subpaths<V: ExpandVectorItems + 'n + Send>(_: impl Ctx, #[impl
|
|||||||
return List::new_from_item(content);
|
return List::new_from_item(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
let stroke = content.element().stroke.clone();
|
|
||||||
let (_, attributes) = content.into_parts();
|
let (_, attributes) = content.into_parts();
|
||||||
|
|
||||||
bezpaths
|
bezpaths
|
||||||
@@ -1675,7 +1668,6 @@ async fn separate_subpaths<V: ExpandVectorItems + 'n + Send>(_: impl Ctx, #[impl
|
|||||||
.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())
|
||||||
})
|
})
|
||||||
@@ -1743,10 +1735,6 @@ pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
|
|||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1754,8 +1742,7 @@ pub async fn combine_paths<T: IntoGraphicList>(_: impl Ctx, #[implementations(Li
|
|||||||
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, ATTR_APPEARANCE);
|
||||||
attributes.insert_cloned_from(&source_attributes, ATTR_STROKE);
|
|
||||||
// Adopt the last input item's layer (if any) so the editor can also bucket clicks under a contributing child layer
|
// Adopt the last input item's layer (if any) so the editor can also bucket clicks under a contributing child layer
|
||||||
attributes.insert_cloned_from(&source_attributes, ATTR_EDITOR_LAYER_PATH);
|
attributes.insert_cloned_from(&source_attributes, ATTR_EDITOR_LAYER_PATH);
|
||||||
bake_paint_transforms(&mut attributes, source_transform);
|
bake_paint_transforms(&mut attributes, source_transform);
|
||||||
@@ -1806,15 +1793,7 @@ async fn sample_polyline<V: MapVectorItems + 'n + Send>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = Vector {
|
let mut result = Vector::default();
|
||||||
point_domain: Default::default(),
|
|
||||||
segment_domain: Default::default(),
|
|
||||||
region_domain: Default::default(),
|
|
||||||
colinear_manipulators: Default::default(),
|
|
||||||
stroke: std::mem::take(&mut content.element_mut().stroke),
|
|
||||||
};
|
|
||||||
// Transfer the stroke transform from the input vector content to the result.
|
|
||||||
result.set_stroke_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM));
|
|
||||||
|
|
||||||
for local_bezpath in content.element().stroke_bezpath_iter() {
|
for local_bezpath in content.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)
|
||||||
@@ -1884,10 +1863,7 @@ async fn simplify<V: MapVectorItems + 'n + Send>(
|
|||||||
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: std::mem::take(&mut item.element_mut().stroke),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for mut bezpath in item.element().stroke_bezpath_iter() {
|
for mut bezpath in item.element().stroke_bezpath_iter() {
|
||||||
bezpath.apply_affine(transform);
|
bezpath.apply_affine(transform);
|
||||||
@@ -1981,10 +1957,7 @@ async fn decimate<V: MapVectorItems + 'n + Send>(
|
|||||||
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: std::mem::take(&mut content.element_mut().stroke),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for mut bezpath in content.element().stroke_bezpath_iter() {
|
for mut bezpath in content.element().stroke_bezpath_iter() {
|
||||||
bezpath.apply_affine(transform);
|
bezpath.apply_affine(transform);
|
||||||
@@ -2060,10 +2033,7 @@ async fn cut_path<V: MapVectorItems + 'n + Send>(
|
|||||||
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(bezpath) = bezpaths.get(index).cloned() {
|
if let Some(bezpath) = bezpaths.get(index).cloned() {
|
||||||
let mut result_vector = Vector {
|
let mut result_vector = Vector::default();
|
||||||
stroke: content.element().stroke.clone(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for (_, bezpath) in bezpaths.iter().enumerate().filter(|&(i, _)| i != index) {
|
for (_, bezpath) in bezpaths.iter().enumerate().filter(|&(i, _)| i != index) {
|
||||||
result_vector.append_bezpath(bezpath.clone());
|
result_vector.append_bezpath(bezpath.clone());
|
||||||
@@ -2273,10 +2243,6 @@ async fn scatter_points<V: MapVectorItems + 'n + Send>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer the style from the input vector content to the result.
|
|
||||||
result.stroke = content.element().stroke.clone();
|
|
||||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
|
||||||
|
|
||||||
*content.element_mut() = result;
|
*content.element_mut() = result;
|
||||||
content
|
content
|
||||||
})
|
})
|
||||||
@@ -2662,6 +2628,48 @@ async fn morph<I: IntoGraphicList>(
|
|||||||
graphic.map(List::new_from_element)
|
graphic.map(List::new_from_element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
let paint = lerp_graphic(source_index.and_then(|index| a.paint_at(index)), target_index.and_then(|index| b.paint_at(index)), time).unwrap_or_default();
|
||||||
|
|
||||||
|
result.replace_or_insert(coverage, paint, CoverPlacement::Above);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
let (progression, reverse, distribution) = (progression.into_element(), reverse.into_element(), distribution.into_element());
|
let (progression, reverse, distribution) = (progression.into_element(), reverse.into_element(), distribution.into_element());
|
||||||
|
|
||||||
// Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools.
|
// Preserve original `List<Graphic>` as upstream data so this group layer's nested layers can be edited by the tools.
|
||||||
@@ -2916,35 +2924,12 @@ async fn morph<I: IntoGraphicList>(
|
|||||||
return Item::from_parts(endpoint_element.clone(), attributes);
|
return 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 = graphic_list_at(&content, source_index, ATTR_FILL);
|
let source = content.attribute::<Appearance>(ATTR_APPEARANCE, source_index);
|
||||||
let target = graphic_list_at(&content, target_index, ATTR_FILL);
|
let target = content.attribute::<Appearance>(ATTR_APPEARANCE, target_index);
|
||||||
lerp_graphic(source.as_deref(), target.as_deref(), time)
|
lerp_appearance(source, target, time)
|
||||||
};
|
|
||||||
let stroke_paint = {
|
|
||||||
let source = graphic_list_at(&content, source_index, ATTR_STROKE);
|
|
||||||
let target = graphic_list_at(&content, target_index, ATTR_STROKE);
|
|
||||||
lerp_graphic(source.as_deref(), target.as_deref(), time)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
||||||
@@ -3114,11 +3099,8 @@ async fn morph<I: IntoGraphicList>(
|
|||||||
}
|
}
|
||||||
item.set_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
|
item.set_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_list_content);
|
||||||
|
|
||||||
if let Some(fill) = fill_paint {
|
if let Some(appearance) = appearance {
|
||||||
item.set_attribute(ATTR_FILL, fill);
|
item.set_attribute(ATTR_APPEARANCE, appearance);
|
||||||
}
|
|
||||||
if let Some(stroke) = stroke_paint {
|
|
||||||
item.set_attribute(ATTR_STROKE, stroke);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item
|
item
|
||||||
@@ -3904,12 +3886,13 @@ mod test {
|
|||||||
v
|
v
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), 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, List::new_from_element(Color::RED).into_graphic_list());
|
.with_attribute(ATTR_APPEARANCE, solid_fill(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, List::new_from_element(Color::BLUE).into_graphic_list());
|
.with_attribute(ATTR_APPEARANCE, solid_fill(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);
|
||||||
@@ -3925,7 +3908,8 @@ mod test {
|
|||||||
.await;
|
.await;
|
||||||
let morphed = List::new_from_item(morphed);
|
let morphed = List::new_from_item(morphed);
|
||||||
|
|
||||||
let fill = graphic_list_at(&morphed, 0, ATTR_FILL).expect("Morph should keep the fill paint at the midpoint");
|
let appearance = morphed.attribute::<Appearance>(ATTR_APPEARANCE, 0).expect("Morph should keep the appearance at the midpoint");
|
||||||
|
let fill = appearance.first_paint_of(Cover::Fill).expect("Morph should keep the fill paint at the midpoint");
|
||||||
|
|
||||||
// Interpolated color between red and blue should have >0 value on both R and B
|
// Interpolated color between red and blue should have >0 value on both R and B
|
||||||
let Some(Graphic::Color(colors)) = fill.element(0) else {
|
let Some(Graphic::Color(colors)) = fill.element(0) else {
|
||||||
@@ -3935,6 +3919,64 @@ mod test {
|
|||||||
assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}");
|
assert!(color.r() > 0. && color.b() > 0., "Fill should be a red-to-blue blend, got {color:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn morph_pairs_appearance_coverages_by_cover() {
|
||||||
|
let rect = || {
|
||||||
|
let mut v = Vector::default();
|
||||||
|
v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY));
|
||||||
|
v
|
||||||
|
};
|
||||||
|
|
||||||
|
let paint_color = |appearance: &Appearance, cover| {
|
||||||
|
let paint = appearance.first_paint_of(cover).expect("Morph should keep both paints at the midpoint");
|
||||||
|
let Some(Graphic::Color(colors)) = paint.element(0) else {
|
||||||
|
panic!("Expected a solid color paint, got {:?}", paint.element(0));
|
||||||
|
};
|
||||||
|
*colors.element(0).expect("Color present")
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two endpoints list their covers in opposite paint orders, which pairing by position would cross
|
||||||
|
let appearance = |fill: Color, stroke: Color, stroke_placement| {
|
||||||
|
let mut appearance = Appearance::default();
|
||||||
|
appearance.replace_or_insert(Coverage::new_fill(), List::new_from_element(fill).into_graphic_list(), CoverPlacement::Above);
|
||||||
|
appearance.replace_or_insert(Coverage::new_stroke(&Stroke::new(4.)), List::new_from_element(stroke).into_graphic_list(), stroke_placement);
|
||||||
|
appearance
|
||||||
|
};
|
||||||
|
|
||||||
|
let item_a = Item::new_from_element(rect())
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY)
|
||||||
|
.with_attribute(ATTR_APPEARANCE, appearance(Color::RED, Color::BLACK, CoverPlacement::Above));
|
||||||
|
let item_b = Item::new_from_element(rect())
|
||||||
|
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into()))
|
||||||
|
.with_attribute(ATTR_APPEARANCE, appearance(Color::BLUE, Color::WHITE, CoverPlacement::Below));
|
||||||
|
|
||||||
|
let mut content = List::new_from_item(item_a);
|
||||||
|
content.push(item_b);
|
||||||
|
|
||||||
|
let morphed = super::morph(
|
||||||
|
Footprint::default(),
|
||||||
|
content,
|
||||||
|
Item::new_from_element(0.5),
|
||||||
|
Item::new_from_element(false),
|
||||||
|
Item::new_from_element(InterpolationDistribution::default()),
|
||||||
|
Item::default(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let morphed = List::new_from_item(morphed);
|
||||||
|
|
||||||
|
let appearance = morphed.attribute::<Appearance>(ATTR_APPEARANCE, 0).expect("Morph should keep the appearance at the midpoint");
|
||||||
|
assert_eq!(appearance.len(), 2, "the midpoint should hold one fill and one stroke, not a duplicated cover");
|
||||||
|
|
||||||
|
let fill = paint_color(appearance, Cover::Fill);
|
||||||
|
assert!(fill.r() > 0. && fill.b() > 0. && fill.g() == 0., "the fill should interpolate against the other fill, got {fill:?}");
|
||||||
|
|
||||||
|
let stroke = paint_color(appearance, Cover::Stroke);
|
||||||
|
assert!(
|
||||||
|
stroke.r() > 0. && stroke.r() == stroke.g() && stroke.g() == stroke.b(),
|
||||||
|
"the stroke should interpolate against the other stroke, got {stroke:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn contains_segment(vector: Vector, target: PathSeg) {
|
fn contains_segment(vector: Vector, target: PathSeg) {
|
||||||
let segments = vector.segment_iter().map(|x| x.1);
|
let segments = vector.segment_iter().map(|x| x.1);
|
||||||
|
|||||||
Reference in New Issue
Block a user