mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
Retire the Vector stroke field into the appearance coverage
Stroke parameters live only on the appearance's stroke coverage now. Vector loses its stroke field, transform normalization, and concat stroke adoption; solidify, morph, combine, and the recolor read the coverage instead, with morph gaining the cover-paired appearance lerp. The stroke-inclusive bounds take the stroke as a parameter, the plain vector bounds ignore include_stroke, the editor's metadata channel carries one resolved appearance snapshot per layer, and the data panel's Vector table drops its stroke properties tab for a handles tab. Legacy vector payloads parse their stroke solely to validate the shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -419,9 +419,8 @@ impl<'e> Graphic<'e> {
|
||||
pub fn is_fully_transparent(&self) -> bool {
|
||||
match self {
|
||||
Graphic::Graphic(list) => list.iter_element_values().all(Graphic::is_fully_transparent),
|
||||
// A bare leaf carries no paint attribute, so only an unstroked
|
||||
// vector is invisible on its own.
|
||||
Graphic::Vector(vector) => vector.stroke.as_ref().is_none_or(|stroke| !stroke.has_renderable_stroke()),
|
||||
// A bare vector leaf carries no paint or stroke of its own, so it is invisible on its own
|
||||
Graphic::Vector(_) => true,
|
||||
Graphic::Color(color) => color.a() == 0.,
|
||||
Graphic::Gradient(stops) => stops.iter().all(|stop| stop.color.a() == 0.),
|
||||
Graphic::RasterCPU(_) | Graphic::RasterGPU(_) | Graphic::Text(_) => false,
|
||||
|
||||
@@ -93,10 +93,11 @@ pub mod migrations {
|
||||
}
|
||||
|
||||
/// The legacy `fill` field is intentionally omitted because vector payload migration only
|
||||
/// recovers editable vector data. The fill/stroke paints are migrated from the the node inputs.
|
||||
/// recovers editable vector data. The stroke parses solely to validate the legacy shape.
|
||||
#[derive(serde::Deserialize)]
|
||||
#[cfg_attr(test, derive(Default, serde::Serialize))]
|
||||
pub(super) struct PathStyle {
|
||||
#[allow(dead_code)]
|
||||
pub stroke: Option<Stroke>,
|
||||
}
|
||||
|
||||
@@ -104,6 +105,7 @@ pub mod migrations {
|
||||
#[derive(serde::Deserialize)]
|
||||
#[cfg_attr(test, derive(Default, serde::Serialize))]
|
||||
pub(super) struct VectorData {
|
||||
#[allow(dead_code)]
|
||||
pub style: PathStyle,
|
||||
pub colinear_manipulators: Vec<[HandleId; 2]>,
|
||||
pub point_domain: PointDomain,
|
||||
@@ -136,7 +138,6 @@ pub mod migrations {
|
||||
|
||||
Ok(match VectorFormat::deserialize(deserializer)? {
|
||||
VectorFormat::OldVectorData(old) => Some(Vector {
|
||||
stroke: old.style.stroke,
|
||||
colinear_manipulators: old.colinear_manipulators,
|
||||
point_domain: old.point_domain,
|
||||
segment_domain: old.segment_domain,
|
||||
@@ -153,9 +154,12 @@ pub mod migrations {
|
||||
use vector_types::vector::style::Stroke;
|
||||
|
||||
#[test]
|
||||
fn preserves_stroke_from_old_vector_data_style() {
|
||||
fn recovers_geometry_from_old_vector_data_style() {
|
||||
use core_types::ops::FromAnchorPosition;
|
||||
|
||||
let old_vector = legacy::VectorData {
|
||||
style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) },
|
||||
point_domain: Vector::from_anchor_position(glam::DVec2::new(3., 4.)).point_domain,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -168,22 +172,21 @@ pub mod migrations {
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("fill".into(), serde_json::to_value(legacy::Fill::default()).unwrap());
|
||||
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
||||
let migrated = migrate_to_optional_vector(value).unwrap().expect("the legacy shape parses into a vector");
|
||||
|
||||
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
||||
assert_eq!(migrated.point_domain.positions(), [glam::DVec2::new(3., 4.)], "the geometry survives alongside the discarded style");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_stroke_from_current_vector_data() {
|
||||
let vector = Vector {
|
||||
stroke: Some(Stroke::new(12.)),
|
||||
..Default::default()
|
||||
};
|
||||
fn recovers_geometry_from_current_vector_data() {
|
||||
use core_types::ops::FromAnchorPosition;
|
||||
|
||||
let vector = Vector::from_anchor_position(glam::DVec2::new(3., 4.));
|
||||
|
||||
let value = serde_json::to_value(&vector).unwrap();
|
||||
let migrated = migrate_to_optional_vector(value).unwrap().unwrap();
|
||||
|
||||
assert_eq!(migrated.stroke.unwrap().weight, 12.);
|
||||
assert_eq!(migrated.point_domain.positions(), [glam::DVec2::new(3., 4.)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,12 +540,9 @@ pub struct RenderMetadata {
|
||||
pub text_frames: HashMap<NodeId, DAffine2>,
|
||||
pub clip_targets: HashSet<NodeId>,
|
||||
pub vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||
/// Per-layer fill paint snapshot from the resolved appearance, exposed so message handlers can read it.
|
||||
/// Per-layer resolved appearance snapshot, exposed so message handlers can read the paint.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
||||
/// Per-layer stroke paint snapshot from the resolved appearance, exposed so message handlers can read it.
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub stroke_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
||||
pub appearance_attributes: HashMap<NodeId, Arc<Appearance>>,
|
||||
pub backgrounds: Vec<Background>,
|
||||
}
|
||||
|
||||
@@ -569,8 +566,7 @@ impl RenderMetadata {
|
||||
text_frames,
|
||||
clip_targets,
|
||||
vector_data,
|
||||
fill_attributes,
|
||||
stroke_attributes,
|
||||
appearance_attributes,
|
||||
backgrounds,
|
||||
} = self;
|
||||
upstream_footprints.extend(other.upstream_footprints.iter());
|
||||
@@ -581,8 +577,7 @@ impl RenderMetadata {
|
||||
text_frames.extend(other.text_frames.iter());
|
||||
clip_targets.extend(other.clip_targets.iter());
|
||||
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())));
|
||||
stroke_attributes.extend(other.stroke_attributes.iter().map(|(id, data)| (*id, data.clone())));
|
||||
appearance_attributes.extend(other.appearance_attributes.iter().map(|(id, data)| (*id, data.clone())));
|
||||
|
||||
// TODO: Find a better non O(n^2) way to merge backgrounds
|
||||
for background in &other.backgrounds {
|
||||
@@ -1352,7 +1347,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, inherited_appe
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = vector.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = vector.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY).unwrap_or(layer_bounds);
|
||||
let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY, element_stroke).unwrap_or(layer_bounds);
|
||||
|
||||
let bounds_matrix = DAffine2::from_scale_angle_translation(layer_bounds[1] - layer_bounds[0], 0., layer_bounds[0]);
|
||||
let stroke_bounds_matrix = DAffine2::from_scale_angle_translation(stroke_layer_bounds[1] - stroke_layer_bounds[0], 0., stroke_layer_bounds[0]);
|
||||
@@ -1404,8 +1399,7 @@ fn render_vector_svg<S: LaneSource<Element = Vector>>(source: &S, inherited_appe
|
||||
let push_id = needs_separate_alignment_fill.then_some({
|
||||
let id = format!("alignment-{}", generate_uuid());
|
||||
|
||||
let mut cloned_vector = vector.clone();
|
||||
cloned_vector.stroke = None;
|
||||
let cloned_vector = vector.clone();
|
||||
|
||||
// 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.
|
||||
@@ -1759,8 +1753,7 @@ fn render_vector_vello<S: LaneSource<Element = Vector>>(
|
||||
}
|
||||
_ => {
|
||||
if use_layer {
|
||||
let mut cloned_element = element.clone();
|
||||
cloned_element.stroke = None;
|
||||
let cloned_element = element.clone();
|
||||
|
||||
// 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.
|
||||
@@ -1894,11 +1887,8 @@ fn collect_vector_metadata<S: LaneSource<Element = Vector>>(
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) {
|
||||
e.insert(Arc::new(element.clone()));
|
||||
|
||||
if let Some(fill_graphic) = resolved.fill_paint.and_then(paint_cell_rows) {
|
||||
metadata.fill_attributes.insert(element_id, Arc::new(fill_graphic.clone()));
|
||||
}
|
||||
if let Some(stroke_graphic) = resolved.stroke_paint.and_then(paint_cell_rows) {
|
||||
metadata.stroke_attributes.insert(element_id, Arc::new(stroke_graphic.clone()));
|
||||
if let Some(appearance) = appearance {
|
||||
metadata.appearance_attributes.insert(element_id, Arc::new(appearance.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3212,7 +3202,14 @@ mod group_walk_tests {
|
||||
assert!(native.local_transforms.contains_key(&caller));
|
||||
assert!(native.upstream_footprints.contains_key(&caller));
|
||||
assert_eq!(native.vector_data.get(&caller).map(|vector| vector.as_ref()), Some(&vectors[0]));
|
||||
assert!(native.fill_attributes.get(&caller).is_some_and(|fill| matches!(fill.element(0), Some(Graphic::Color(_)))));
|
||||
assert!(
|
||||
native
|
||||
.appearance_attributes
|
||||
.get(&caller)
|
||||
.and_then(|appearance| appearance.first_paint_of(graphic_types::appearance::Cover::Fill))
|
||||
.and_then(paint_cell_rows)
|
||||
.is_some_and(|fill| matches!(fill.element(0), Some(Graphic::Color(_))))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::vector::vector_modification::VectorExt;
|
||||
use core::borrow::Borrow;
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::render_complexity::RenderComplexity;
|
||||
use core_types::transform::Transform;
|
||||
use dyn_any::StaticType;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
@@ -18,8 +17,6 @@ use std::collections::HashMap;
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
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).
|
||||
/// 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]>,
|
||||
@@ -35,7 +32,6 @@ unsafe impl StaticType for Vector {
|
||||
impl Default for Vector {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stroke: Some(Stroke::new(0.)),
|
||||
colinear_manipulators: Vec::new(),
|
||||
point_domain: PointDomain::new(),
|
||||
segment_domain: SegmentDomain::new(),
|
||||
@@ -49,7 +45,6 @@ impl graphene_hash::CacheHash for Vector {
|
||||
self.point_domain.cache_hash(state);
|
||||
self.segment_domain.cache_hash(state);
|
||||
self.region_domain.cache_hash(state);
|
||||
self.stroke.cache_hash(state);
|
||||
self.colinear_manipulators.cache_hash(state);
|
||||
}
|
||||
}
|
||||
@@ -239,10 +234,10 @@ impl Vector {
|
||||
/// identity (`Inside` = 0, `Outside` = 2×weight): the renderer masks half of a centered double-width
|
||||
/// 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.
|
||||
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 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
|
||||
// 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());
|
||||
@@ -530,40 +525,14 @@ impl Vector {
|
||||
self.segment_domain.concat(&additional.segment_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());
|
||||
}
|
||||
|
||||
pub fn set_stroke_transform(&mut self, transform: DAffine2) {
|
||||
if let Some(stroke) = &mut self.stroke {
|
||||
stroke.transform = transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Vector {
|
||||
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);
|
||||
|
||||
fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
match self.bounding_box_with_transform(transform) {
|
||||
Some([a, b]) => RenderBoundingBox::Rectangle([a - offset, b + offset]),
|
||||
Some(bounds) => RenderBoundingBox::Rectangle(bounds),
|
||||
None => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub use graphene_application_io as application_io;
|
||||
pub use graphene_core;
|
||||
pub use graphene_core::debug;
|
||||
pub use graphic_nodes;
|
||||
pub use graphic_types::{Artboard, Graphic, Vector};
|
||||
pub use graphic_types::{Appearance, Artboard, Cover, Coverage, Graphic, Vector};
|
||||
pub use math_nodes;
|
||||
pub use path_bool_nodes;
|
||||
pub use raster_nodes;
|
||||
|
||||
@@ -54,7 +54,6 @@ fn boolean_core<'e>(
|
||||
|
||||
let result_vector = result_vector_list.element_mut(0).unwrap();
|
||||
Vector::transform(result_vector, transform);
|
||||
result_vector.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Clean up the boolean operation result by merging duplicated points
|
||||
let merge_transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
@@ -236,12 +235,7 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
|
||||
|
||||
bake_paint_transforms(&mut attributes, copy_from_transform);
|
||||
|
||||
let copy_from = vector.element(index).unwrap();
|
||||
let element = Vector {
|
||||
stroke: copy_from.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
Item::from_parts(element, attributes)
|
||||
Item::from_parts(Vector::default(), attributes)
|
||||
} else {
|
||||
Item::<Vector>::default()
|
||||
};
|
||||
@@ -308,10 +302,7 @@ fn fill_appearance(paint: List<Graphic<'static>>) -> Appearance {
|
||||
fn color_paint_row(color: Color, mut attributes: core_types::list::ItemAttributeValues) -> Item<Vector> {
|
||||
attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(List::new_from_element(Graphic::Color(color))));
|
||||
|
||||
let mut element = Vector::default();
|
||||
element.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
Item::from_parts(element, attributes)
|
||||
Item::from_parts(Vector::default(), attributes)
|
||||
}
|
||||
|
||||
/// A gradient row: an empty vector carrying the stops as its fill paint, the
|
||||
@@ -329,10 +320,7 @@ fn gradient_paint_row(stops: GradientStops, mut attributes: core_types::list::It
|
||||
}
|
||||
attributes.insert(graphic_types::ATTR_APPEARANCE, fill_appearance(gradient_paint));
|
||||
|
||||
let mut element = Vector::default();
|
||||
element.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
Item::from_parts(element, attributes)
|
||||
Item::from_parts(Vector::default(), attributes)
|
||||
}
|
||||
|
||||
/// A text lane's rows: the shaped glyph vectors under the composed transform.
|
||||
|
||||
@@ -132,10 +132,11 @@ fn assign_colors<'e>(
|
||||
let mut appearance = existing_appearance.unwrap_or_default();
|
||||
let paint_cell = Graphic::Graphic(paint);
|
||||
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
|
||||
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above);
|
||||
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Below);
|
||||
}
|
||||
if stroke && element.stroke.is_some() && !appearance.set_paint_of(Cover::Stroke, paint_cell.clone()) {
|
||||
appearance.replace_or_insert(Coverage::new_stroke(&element.stroke.clone().unwrap_or_default()), paint_cell, CoverPlacement::Above);
|
||||
// The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
|
||||
if stroke {
|
||||
appearance.set_paint_of(Cover::Stroke, paint_cell);
|
||||
}
|
||||
let parked_appearance = park_appearance_attr(Some(appearance))?;
|
||||
|
||||
@@ -230,7 +231,6 @@ fn assign_colors_graphic<'e>(
|
||||
let element = match rows {
|
||||
Some(mut rows) => {
|
||||
for row in 0..rows.len() {
|
||||
let row_stroke = rows.element(row).and_then(|vector| vector.stroke.clone());
|
||||
let color = assign_color_at(gradient_element, position + row, length, randomize, seed, repeat_every);
|
||||
let paint = List::new_from_element(color).into_graphic_list();
|
||||
|
||||
@@ -238,10 +238,11 @@ fn assign_colors_graphic<'e>(
|
||||
let mut appearance = rows.attribute_cloned_or_default::<Appearance>(graphic_types::ATTR_APPEARANCE, row);
|
||||
let paint_cell = Graphic::Graphic(paint);
|
||||
if fill && !appearance.set_paint_of(Cover::Fill, paint_cell.clone()) {
|
||||
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Above);
|
||||
appearance.replace_or_insert(Coverage::new_fill(), paint_cell.clone(), CoverPlacement::Below);
|
||||
}
|
||||
if stroke && row_stroke.is_some() && !appearance.set_paint_of(Cover::Stroke, paint_cell.clone()) {
|
||||
appearance.replace_or_insert(Coverage::new_stroke(&row_stroke.unwrap_or_default()), paint_cell, CoverPlacement::Above);
|
||||
// The stroke recolor is gated on an existing stroke coverage, since restyling never adds a stroke
|
||||
if stroke {
|
||||
appearance.set_paint_of(Cover::Stroke, paint_cell);
|
||||
}
|
||||
rows.set_attribute(graphic_types::ATTR_APPEARANCE, row, appearance);
|
||||
}
|
||||
@@ -421,7 +422,7 @@ fn stroke<'e>(
|
||||
dash_offset: f64,
|
||||
) -> Result<(Vector, Attr<TransformAttr>, Attr<'e, AppearanceMarker>), Interrupt> {
|
||||
let dash_lengths: Vec<f64> = (0..dash_lengths.len()).map(|index| dash_lengths.get(index).max(0.)).collect();
|
||||
let mut stroke = Stroke {
|
||||
let stroke = Stroke {
|
||||
weight,
|
||||
dash_lengths,
|
||||
dash_offset,
|
||||
@@ -434,14 +435,9 @@ fn stroke<'e>(
|
||||
|
||||
// The coverage records the stroke's authoring space, so the item transform is composed in, translation
|
||||
// included so the render consumers see the exact legacy stroke space.
|
||||
let mut coverage_stroke = stroke.clone();
|
||||
let mut coverage_stroke = stroke;
|
||||
coverage_stroke.transform *= *content_transform;
|
||||
|
||||
stroke.transform *= *content_transform;
|
||||
|
||||
let mut element = element;
|
||||
element.stroke = Some(stroke);
|
||||
|
||||
let paint = paint_table(paint);
|
||||
// The paint order is the coverage row order: appending above follows the painter's algorithm, and a
|
||||
// below stroke is expressed by the chain running the stroke node before the fill
|
||||
@@ -450,23 +446,6 @@ fn stroke<'e>(
|
||||
Ok((element, Attr(*content_transform), Attr(Some(parked_appearance))))
|
||||
}
|
||||
|
||||
/// The vector items of a graphic lane's interior, one wrap level deep, the
|
||||
/// reach of the pre-flip broadcast over a legacy list.
|
||||
fn for_each_interior_vector_mut(element: &mut Graphic, mut f: impl FnMut(&mut Vector, DAffine2)) {
|
||||
match element {
|
||||
Graphic::Vector(vector) => f(vector, DAffine2::IDENTITY),
|
||||
Graphic::Graphic(children) => {
|
||||
for index in 0..children.len() {
|
||||
let transform: DAffine2 = children.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
if let Some(Graphic::Vector(vector)) = children.element_mut(index) {
|
||||
f(vector, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The stroke over graphic lanes: the style applies to the interior vectors,
|
||||
/// the paint marker parks on the lane for the render boundary to place.
|
||||
/// Registered under the stroke's identifier.
|
||||
@@ -498,16 +477,9 @@ fn stroke_graphic_leveled<'e>(
|
||||
};
|
||||
|
||||
// The coverage records the stroke's authoring space at the lane, composing the lane transform as in `stroke` above.
|
||||
let mut coverage_stroke = stroke.clone();
|
||||
let mut coverage_stroke = stroke;
|
||||
coverage_stroke.transform *= *content_transform;
|
||||
|
||||
let mut element = element;
|
||||
for_each_interior_vector_mut(&mut element, |vector, transform| {
|
||||
let mut stroke = stroke.clone();
|
||||
stroke.transform *= transform;
|
||||
vector.stroke = Some(stroke);
|
||||
});
|
||||
|
||||
let paint = paint_table(paint);
|
||||
// The paint order is the coverage row order: appending above follows the painter's algorithm, and a
|
||||
// below stroke is expressed by the chain running the stroke node before the fill
|
||||
@@ -654,10 +626,7 @@ fn round_corners(
|
||||
// Convert 0-100 to 0-0.5
|
||||
let edge_length_limit = edge_length_limit * 0.005;
|
||||
|
||||
let mut result = Vector {
|
||||
stroke: source.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut result = Vector::default();
|
||||
|
||||
// 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());
|
||||
@@ -1016,8 +985,6 @@ fn box_warp(_: impl Ctx, (vector, transform): (Vector, Attr<TransformAttr>), #[e
|
||||
});
|
||||
}
|
||||
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Reset the transform since we've applied it directly to the points
|
||||
(result, Attr(DAffine2::IDENTITY))
|
||||
}
|
||||
@@ -1163,10 +1130,7 @@ fn auto_tangents(
|
||||
) -> (Vector, Attr<TransformAttr>) {
|
||||
let transform: DAffine2 = *lane_transform;
|
||||
|
||||
let mut result = Vector {
|
||||
stroke: source.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut result = Vector::default();
|
||||
|
||||
for mut subpath in source.stroke_bezier_paths() {
|
||||
subpath.apply_transform(transform);
|
||||
@@ -1298,19 +1262,14 @@ fn auto_tangents(
|
||||
|
||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||
fn bounding_box(_: impl Ctx, vector: Vector) -> Vector {
|
||||
let mut result = vector
|
||||
vector
|
||||
.bounding_box_rect()
|
||||
.map(|bbox| {
|
||||
let mut vector = Vector::default();
|
||||
vector.append_bezpath(bbox.to_path(DEFAULT_ACCURACY));
|
||||
vector
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
result.stroke = vector.stroke.clone();
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
result
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
|
||||
@@ -1361,11 +1320,7 @@ fn offset_path(_: impl Ctx, (vector, lane_transform): (Vector, Attr<TransformAtt
|
||||
let transform = Affine::new(transform_attribute.to_cols_array());
|
||||
|
||||
let bezpaths = vector.stroke_bezpath_iter();
|
||||
let mut result = Vector {
|
||||
stroke: vector.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
let mut result = Vector::default();
|
||||
|
||||
// Perform operation on all subpaths in this shape.
|
||||
for mut bezpath in bezpaths {
|
||||
@@ -1408,9 +1363,10 @@ fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
|
||||
.into_iter()
|
||||
.zip(has_fills)
|
||||
.flat_map(|(row, has_fill)| {
|
||||
let (mut vector, attributes) = row.into_parts();
|
||||
let (vector, attributes) = row.into_parts();
|
||||
|
||||
let stroke = vector.stroke.clone().unwrap_or_default();
|
||||
let appearance = attributes.get::<Appearance>(graphic_types::ATTR_APPEARANCE).cloned().unwrap_or_default();
|
||||
let stroke = appearance.first_coverage_of(Cover::Stroke).map(Coverage::stroke_params).unwrap_or_default();
|
||||
let bezpaths = vector.stroke_bezpath_iter();
|
||||
let mut solidified_stroke = Vector::default();
|
||||
|
||||
@@ -1461,11 +1417,9 @@ fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
|
||||
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(|| {
|
||||
vector.stroke = None;
|
||||
let mut fill_attributes = attributes.clone();
|
||||
// No stroke remains on the fill row
|
||||
if let Some(appearance) = fill_attributes.get_mut::<Appearance>(graphic_types::ATTR_APPEARANCE) {
|
||||
appearance.retain_cover(Cover::Fill);
|
||||
}
|
||||
@@ -1735,7 +1689,6 @@ fn separate_subpaths_core(content: List<Vector>) -> List<Vector> {
|
||||
return vec![row];
|
||||
}
|
||||
|
||||
let stroke = row.element().stroke.clone();
|
||||
let (_, attributes) = row.into_parts();
|
||||
|
||||
bezpaths
|
||||
@@ -1743,7 +1696,6 @@ fn separate_subpaths_core(content: List<Vector>) -> List<Vector> {
|
||||
.map(|bezpath| {
|
||||
let mut vector = Vector::default();
|
||||
vector.append_bezpath(bezpath);
|
||||
vector.stroke = stroke.clone();
|
||||
|
||||
Item::from_parts(vector, attributes.clone())
|
||||
})
|
||||
@@ -1869,15 +1821,11 @@ fn flatten_path_core<'e>(
|
||||
let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
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));
|
||||
}
|
||||
|
||||
let mut fill_cell = None;
|
||||
let mut stroke_cell = None;
|
||||
// The primary row's appearance carries over whole, its paint transforms baked
|
||||
let mut appearance = None;
|
||||
let mut layer_path = Vec::new();
|
||||
if let Some((primary, source_transform)) = primary_source {
|
||||
let source_attributes = flattened.clone_item_attributes(primary);
|
||||
@@ -1885,11 +1833,7 @@ fn flatten_path_core<'e>(
|
||||
|
||||
attributes.insert_cloned_from(&source_attributes, graphic_types::ATTR_APPEARANCE);
|
||||
bake_paint_transforms(&mut attributes, source_transform);
|
||||
|
||||
if let Some(appearance) = attributes.get::<Appearance>(graphic_types::ATTR_APPEARANCE) {
|
||||
fill_cell = appearance.first_paint_of(Cover::Fill).filter(|cell| !cell.is_empty()).cloned();
|
||||
stroke_cell = appearance.first_paint_of(Cover::Stroke).filter(|cell| !cell.is_empty()).cloned();
|
||||
}
|
||||
appearance = attributes.remove::<Appearance>(graphic_types::ATTR_APPEARANCE).and_then(|appearance| appearance.declared().cloned());
|
||||
|
||||
// Adopt the last input item's layer so the editor can also bucket clicks under a contributing child layer
|
||||
layer_path = flattened.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, primary);
|
||||
@@ -1905,21 +1849,7 @@ fn flatten_path_core<'e>(
|
||||
// editor click-target preservation, as the boolean operation does.
|
||||
let merged_layers = arena.alloc_sized_keyed(snapshot, 0).ok_or_else(exhausted)?.0;
|
||||
|
||||
// The carried paints land on the appearance, the stroke coverage recording the carried stroke's parameters
|
||||
let appearance = {
|
||||
let mut appearance = Appearance::default();
|
||||
if let Some(cell) = fill_cell {
|
||||
appearance.replace_or_insert(Coverage::new_fill(), cell, CoverPlacement::Above);
|
||||
}
|
||||
if let Some(cell) = stroke_cell {
|
||||
let coverage = Coverage::new_stroke(&output.stroke.clone().unwrap_or_default());
|
||||
appearance.replace_or_insert(coverage, cell, CoverPlacement::Above);
|
||||
}
|
||||
match appearance.declared().is_some() {
|
||||
true => Some(park_appearance(arena, appearance)?),
|
||||
false => None,
|
||||
}
|
||||
};
|
||||
let appearance = appearance.map(|appearance| park_appearance(arena, appearance)).transpose()?;
|
||||
|
||||
Ok((output, Attr(DAffine2::IDENTITY), Attr(appearance), Attr(layer_path.as_slice()), Attr(Some(merged_layers))))
|
||||
}
|
||||
@@ -1980,16 +1910,7 @@ fn sample_polyline(
|
||||
}
|
||||
};
|
||||
|
||||
let mut element = element;
|
||||
let mut result = Vector {
|
||||
point_domain: Default::default(),
|
||||
segment_domain: Default::default(),
|
||||
region_domain: Default::default(),
|
||||
colinear_manipulators: Default::default(),
|
||||
stroke: std::mem::take(&mut element.stroke),
|
||||
};
|
||||
// Transfer the stroke transform from the input vector content to the result.
|
||||
result.set_stroke_transform(*transform);
|
||||
let mut result = Vector::default();
|
||||
|
||||
for local_bezpath in element.stroke_bezpath_iter() {
|
||||
// Apply the transform to compute sample locations in world space (for correct distance-based spacing)
|
||||
@@ -2052,10 +1973,7 @@ fn simplify(
|
||||
let transform = Affine::new(transform_attribute.to_cols_array());
|
||||
let inverse_transform = transform.inverse();
|
||||
|
||||
let mut result = Vector {
|
||||
stroke: content.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut result = Vector::default();
|
||||
|
||||
for mut bezpath in content.stroke_bezpath_iter() {
|
||||
bezpath.apply_affine(transform);
|
||||
@@ -2142,10 +2060,7 @@ fn decimate(
|
||||
let transform = Affine::new(transform_attribute.to_cols_array());
|
||||
let inverse_transform = transform.inverse();
|
||||
|
||||
let mut result = Vector {
|
||||
stroke: content.stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut result = Vector::default();
|
||||
|
||||
for mut bezpath in content.stroke_bezpath_iter() {
|
||||
bezpath.apply_affine(transform);
|
||||
@@ -2219,10 +2134,7 @@ fn cut_path_core(mut content: List<Vector>, progression: f64, reverse: bool, par
|
||||
let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize };
|
||||
|
||||
if let Some((row_index, bezpath)) = bezpaths.get(index).cloned() {
|
||||
let mut result_vector = Vector {
|
||||
stroke: content.element(row_index).unwrap().stroke.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut result_vector = Vector::default();
|
||||
|
||||
for (_, (_, bezpath)) in bezpaths.iter().enumerate().filter(|(i, (ri, _))| *i != index && *ri == row_index) {
|
||||
result_vector.append_bezpath(bezpath.clone());
|
||||
@@ -2463,8 +2375,6 @@ fn scatter_points(
|
||||
}
|
||||
|
||||
// Transfer the style from the input vector content to the result.
|
||||
result.stroke = element.stroke.clone();
|
||||
result.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
result
|
||||
}
|
||||
@@ -2800,6 +2710,51 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
|
||||
}
|
||||
}
|
||||
|
||||
// Lerp between two appearances, pairing coverages by cover so a fill and a stroke never interpolate into each other.
|
||||
// Stroke parameter pairs interpolate; other coverage pairings and the paint order step at the midpoint.
|
||||
fn lerp_appearance(a: Option<&Appearance>, b: Option<&Appearance>, time: f64) -> Option<Appearance> {
|
||||
if a.is_none() && b.is_none() {
|
||||
return None;
|
||||
}
|
||||
let empty = Appearance::default();
|
||||
let (a, b) = (a.unwrap_or(&empty), b.unwrap_or(&empty));
|
||||
|
||||
// The side holding the paint order at this time leads, so covers only the other side has follow behind it
|
||||
let (leading, trailing) = if time < 0.5 { (a, b) } else { (b, a) };
|
||||
let mut covers: Vec<Cover> = Vec::new();
|
||||
for cover in leading.covers().chain(trailing.covers()).map(Coverage::cover) {
|
||||
if !covers.contains(&cover) {
|
||||
covers.push(cover);
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = Appearance::default();
|
||||
for cover in covers {
|
||||
let (source_index, target_index) = (a.first_index_of(cover), b.first_index_of(cover));
|
||||
|
||||
// An unmatched stroke steps out at the midpoint, matching the stroke geometry, while an unmatched fill persists and fades
|
||||
let coverage = match (source_index.and_then(|index| a.cover_at(index)), target_index.and_then(|index| b.cover_at(index))) {
|
||||
(Some(source), Some(target)) if cover == Cover::Stroke => Coverage::new_stroke(&source.stroke_params().lerp(&target.stroke_params(), time)),
|
||||
(Some(source), Some(target)) => (if time < 0.5 { source } else { target }).clone(),
|
||||
(Some(_), None) if cover == Cover::Stroke && time >= 0.5 => continue,
|
||||
(None, Some(_)) if cover == Cover::Stroke && time < 0.5 => continue,
|
||||
(Some(source), None) => source.clone(),
|
||||
(None, Some(target)) => target.clone(),
|
||||
(None, None) => continue,
|
||||
};
|
||||
|
||||
// An unmatched side falls to `None` here, which `lerp_graphic` fades against transparent.
|
||||
// The paint cell carries its graphic list as one wrapped cell, so the lerp works on the unwrapped rows.
|
||||
let source_paint = source_index.and_then(|index| a.paint_at(index)).and_then(graphic_types::graphic::paint_cell_rows);
|
||||
let target_paint = target_index.and_then(|index| b.paint_at(index)).and_then(graphic_types::graphic::paint_cell_rows);
|
||||
let paint = lerp_graphic(source_paint, target_paint, time).map(Graphic::Graphic).unwrap_or_default();
|
||||
|
||||
result.replace_or_insert(coverage, paint, CoverPlacement::Above);
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
// Preserve the original legacy snapshot as upstream data so this group layer's nested layers can be edited by the tools.
|
||||
let mut graphic_list_content = snapshot;
|
||||
|
||||
@@ -3057,34 +3012,13 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
|
||||
return List::new_from_item(Item::from_parts(endpoint_element.clone(), attributes));
|
||||
}
|
||||
|
||||
let stroke = match (source_element.stroke.as_ref(), target_element.stroke.as_ref()) {
|
||||
(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 mut vector = Vector::default();
|
||||
|
||||
let coverage_paint = |index: usize, cover: Cover| {
|
||||
content
|
||||
.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, index)
|
||||
.and_then(|appearance| appearance.first_paint_of(cover))
|
||||
.and_then(graphic_types::graphic::paint_cell_rows)
|
||||
let appearance = {
|
||||
let source = content.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, source_index);
|
||||
let target = content.attribute::<Appearance>(graphic_types::ATTR_APPEARANCE, target_index);
|
||||
lerp_appearance(source, target, time)
|
||||
};
|
||||
let fill_paint = lerp_graphic(coverage_paint(source_index, Cover::Fill), coverage_paint(target_index, Cover::Fill), time);
|
||||
let stroke_paint = lerp_graphic(coverage_paint(source_index, Cover::Stroke), coverage_paint(target_index, Cover::Stroke), time);
|
||||
|
||||
// Work directly with manipulator groups, bypassing the BezPath intermediate representation.
|
||||
// This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame.
|
||||
@@ -3243,16 +3177,7 @@ fn morph_core(flattened: List<Vector>, snapshot: List<Graphic<'static>>, progres
|
||||
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
|
||||
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, Some(graphic_list_content));
|
||||
|
||||
// The lerped paints land on the appearance, the stroke coverage recording the lerped stroke's parameters
|
||||
let mut appearance = Appearance::default();
|
||||
if let Some(fill) = fill_paint {
|
||||
appearance.replace_or_insert(Coverage::new_fill(), Graphic::Graphic(fill), CoverPlacement::Above);
|
||||
}
|
||||
if let Some(stroke) = stroke_paint {
|
||||
let coverage = Coverage::new_stroke(&item.element().stroke.clone().unwrap_or_default());
|
||||
appearance.replace_or_insert(coverage, Graphic::Graphic(stroke), CoverPlacement::Above);
|
||||
}
|
||||
if appearance.declared().is_some() {
|
||||
if let Some(appearance) = appearance {
|
||||
item.set_attribute(graphic_types::ATTR_APPEARANCE, appearance);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user