mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Flatten vector rows natively and serve solidify per lane
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::markers::{ATTR_FILL, ATTR_STROKE, Fill, Stroke};
|
||||
use core_types::attribute::{Attribute, Opacity};
|
||||
use core_types::attribute::{Attribute, EditorLayerPath, Opacity, OpacityFill, Transform};
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::lane::{LaneColumn, LaneSource};
|
||||
@@ -277,6 +277,50 @@ impl<'a, S: LaneSource> PaintColumns<'a, S> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a lane's paint reaches into the element beneath it, mirroring the
|
||||
/// legacy conversion's paint push: vector interiors directly and vector
|
||||
/// children of a nested graphic list, one level deep.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PaintReach<'a> {
|
||||
pub paint: LanePaint<'a>,
|
||||
hops: u8,
|
||||
}
|
||||
|
||||
impl<'a> PaintReach<'a> {
|
||||
pub const NONE: Self = Self { paint: LanePaint::NONE, hops: 0 };
|
||||
|
||||
/// The lane's effective reach: an inherited paint stays authoritative
|
||||
/// (lane paint below a push's origin is inert in the legacy model), an
|
||||
/// absent one reads the lane's own paint.
|
||||
pub fn for_lane<S: LaneSource>(self, columns: &PaintColumns<'a, S>, index: usize) -> Self {
|
||||
match self.paint.is_present() {
|
||||
true => self,
|
||||
false => Self { paint: columns.read(index), hops: 2 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn applies(&self) -> bool {
|
||||
self.hops > 0 && self.paint.is_present()
|
||||
}
|
||||
|
||||
/// The reach one graphic nesting level further down.
|
||||
pub fn nested(self) -> Self {
|
||||
Self {
|
||||
paint: self.paint,
|
||||
hops: self.hops.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reach entering a group's own graphic run: a spent or absent reach
|
||||
/// resets so the group's own lane paint applies at its own boundary.
|
||||
pub fn into_group_graphics(self) -> Self {
|
||||
match self.applies() {
|
||||
true => self.nested(),
|
||||
false => Self::NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A source with a lane's paint forced over its fill and stroke columns,
|
||||
/// reaching the interiors the legacy conversion's paint push reached.
|
||||
pub struct PaintOverlay<'a, S> {
|
||||
@@ -793,6 +837,217 @@ pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_t
|
||||
Some(list)
|
||||
}
|
||||
|
||||
/// One step of the vector-row walk: continue to the next row or stop early.
|
||||
pub enum RowStep {
|
||||
Continue,
|
||||
Stop,
|
||||
}
|
||||
|
||||
/// The ancestor composition a flattened row inherits: transform, opacity and
|
||||
/// fill opacity multiply down, each composing only where some ancestor
|
||||
/// carries the attribute, matching the legacy flatten.
|
||||
#[derive(Clone, Copy)]
|
||||
struct FlattenScale {
|
||||
has_transform: bool,
|
||||
transform: DAffine2,
|
||||
has_opacity: bool,
|
||||
opacity: f64,
|
||||
has_fill_opacity: bool,
|
||||
fill_opacity: f64,
|
||||
}
|
||||
|
||||
impl FlattenScale {
|
||||
const ROOT: Self = Self {
|
||||
has_transform: false,
|
||||
transform: DAffine2::IDENTITY,
|
||||
has_opacity: false,
|
||||
opacity: 1.,
|
||||
has_fill_opacity: false,
|
||||
fill_opacity: 1.,
|
||||
};
|
||||
|
||||
fn composed<S: LaneSource>(self, source: &S, lane: usize) -> Self {
|
||||
let transform = source.try_attr::<Transform>(lane);
|
||||
let opacity = source.try_attr::<Opacity>(lane);
|
||||
let fill_opacity = source.try_attr::<OpacityFill>(lane);
|
||||
Self {
|
||||
has_transform: self.has_transform || transform.is_some(),
|
||||
transform: self.transform * transform.unwrap_or(DAffine2::IDENTITY),
|
||||
has_opacity: self.has_opacity || opacity.is_some(),
|
||||
opacity: self.opacity * opacity.unwrap_or(1.),
|
||||
has_fill_opacity: self.has_fill_opacity || fill_opacity.is_some(),
|
||||
fill_opacity: self.fill_opacity * fill_opacity.unwrap_or(1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One flattened vector row served by [`walk_vector_rows`]: cheap probes
|
||||
/// first, the full row built on demand.
|
||||
pub struct VectorRow<'w> {
|
||||
source: RowSourceRef<'w>,
|
||||
scale: FlattenScale,
|
||||
layer_path: Option<&'w [NodeId]>,
|
||||
paint: LanePaint<'w>,
|
||||
}
|
||||
|
||||
enum RowSourceRef<'w> {
|
||||
Legacy(&'w List<Vector>, usize),
|
||||
Run(&'w core_types::record::RunView<'w, Vector>, &'w core_types::record::GroupItem, usize),
|
||||
}
|
||||
|
||||
impl VectorRow<'_> {
|
||||
/// The row's vector, borrowed.
|
||||
pub fn element(&self) -> &Vector {
|
||||
match &self.source {
|
||||
RowSourceRef::Legacy(list, index) => list.element(*index).expect("the walk visits held rows"),
|
||||
RowSourceRef::Run(run, _, index) => LaneSource::element(*run, *index).expect("the walk visits held lanes"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the built row will carry fill paint: the reaching lane paint,
|
||||
/// else the row's own.
|
||||
pub fn has_fill(&self) -> bool {
|
||||
if self.paint.fill.is_some() {
|
||||
return true;
|
||||
}
|
||||
match &self.source {
|
||||
RowSourceRef::Legacy(list, index) => list
|
||||
.attribute::<Option<List<Graphic>>>(ATTR_FILL, *index)
|
||||
.and_then(|paint| paint.as_ref())
|
||||
.is_some_and(is_paint_present),
|
||||
RowSourceRef::Run(run, _, index) => paint_graphics::<Fill, _>(*run, *index).is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the row at the end of `out`, applying the reach paint and the
|
||||
/// inherited composition.
|
||||
pub fn build_into(&self, out: &mut List<Vector>) {
|
||||
let index = out.len();
|
||||
match &self.source {
|
||||
RowSourceRef::Legacy(list, row) => {
|
||||
out.push(list.clone_item(*row).expect("the walk visits held rows"));
|
||||
}
|
||||
RowSourceRef::Run(run, item, lane) => {
|
||||
out.push(Item::new_from_element(LaneSource::element(*run, *lane).expect("the walk visits held lanes").clone()));
|
||||
for field in &item.layout().fields {
|
||||
// SAFETY: the offset comes from the item's own layout.
|
||||
let value = unsafe { (field.read_erased)(item.lanes().get(*lane).rec().ptr().add(field.offset)) };
|
||||
out.set_attribute_value_dyn(field.name, index, AttributeValueDyn(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, slot) in [(ATTR_FILL, self.paint.fill), (ATTR_STROKE, self.paint.stroke)] {
|
||||
if let Some(paint) = slot {
|
||||
set_paint_attribute_at(out, index, key, paint.clone());
|
||||
}
|
||||
}
|
||||
if self.scale.has_transform || out.attribute::<DAffine2>(ATTR_TRANSFORM, index).is_some() {
|
||||
let row_transform: DAffine2 = out.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
out.set_attribute(ATTR_TRANSFORM, index, self.scale.transform * row_transform);
|
||||
}
|
||||
if self.scale.has_opacity || out.attribute::<f64>(ATTR_OPACITY, index).is_some() {
|
||||
let row_opacity: f64 = out.attribute_cloned_or(ATTR_OPACITY, index, 1.);
|
||||
out.set_attribute(ATTR_OPACITY, index, self.scale.opacity * row_opacity);
|
||||
}
|
||||
if self.scale.has_fill_opacity || out.attribute::<f64>(ATTR_OPACITY_FILL, index).is_some() {
|
||||
let row_fill: f64 = out.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
|
||||
out.set_attribute(ATTR_OPACITY_FILL, index, self.scale.fill_opacity * row_fill);
|
||||
}
|
||||
if let Some(layer_path) = self.layer_path {
|
||||
out.set_attribute(ATTR_EDITOR_LAYER_PATH, index, layer_path.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_rows_of_list(list: &List<Vector>, scale: FlattenScale, layer_path: Option<&[NodeId]>, paint: LanePaint<'_>, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) -> RowStep {
|
||||
for row in 0..list.len() {
|
||||
if let RowStep::Stop = visit(VectorRow {
|
||||
source: RowSourceRef::Legacy(list, row),
|
||||
scale,
|
||||
layer_path,
|
||||
paint,
|
||||
}) {
|
||||
return RowStep::Stop;
|
||||
}
|
||||
}
|
||||
RowStep::Continue
|
||||
}
|
||||
|
||||
fn walk_rows_of_run(
|
||||
item: &core_types::record::GroupItem,
|
||||
scale: FlattenScale,
|
||||
layer_path: Option<&[NodeId]>,
|
||||
paint: LanePaint<'_>,
|
||||
visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep,
|
||||
) -> RowStep {
|
||||
let Some(run) = core_types::record::RunView::<Vector>::new(item) else {
|
||||
return RowStep::Continue;
|
||||
};
|
||||
for lane in 0..item.len() {
|
||||
if let RowStep::Stop = visit(VectorRow {
|
||||
source: RowSourceRef::Run(&run, item, lane),
|
||||
scale,
|
||||
layer_path,
|
||||
paint,
|
||||
}) {
|
||||
return RowStep::Stop;
|
||||
}
|
||||
}
|
||||
RowStep::Continue
|
||||
}
|
||||
|
||||
/// Walks a graphic level into its flattened vector rows, matching the legacy
|
||||
/// push-then-flatten lowering: lane paint threads with [`PaintReach`],
|
||||
/// ancestor transform, opacity and fill opacity compose down, the immediate
|
||||
/// parent's layer path overwrites its rows, and non-vector content is
|
||||
/// discarded.
|
||||
pub fn walk_vector_rows<S: LaneSource<Element = Graphic>>(source: &S, visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep) {
|
||||
walk_vector_rows_impl(source, FlattenScale::ROOT, PaintReach::NONE, visit);
|
||||
}
|
||||
|
||||
fn walk_vector_rows_impl<'a, S: LaneSource<Element = Graphic>>(
|
||||
source: &'a S,
|
||||
scale: FlattenScale,
|
||||
inherited: PaintReach<'a>,
|
||||
visit: &mut dyn FnMut(VectorRow<'_>) -> RowStep,
|
||||
) -> RowStep {
|
||||
let columns = PaintColumns::new(source);
|
||||
for index in 0..source.lane_count() {
|
||||
let Some(element) = source.element(index) else { continue };
|
||||
let reach = inherited.for_lane(&columns, index);
|
||||
let lane_scale = scale.composed(source, index);
|
||||
let layer_path = source.try_attr::<EditorLayerPath>(index);
|
||||
let row_paint = match reach.applies() {
|
||||
true => reach.paint,
|
||||
false => LanePaint::NONE,
|
||||
};
|
||||
let step = match element {
|
||||
Graphic::Vector(inner) => walk_rows_of_list(inner, lane_scale, layer_path, row_paint, visit),
|
||||
Graphic::Graphic(children) => walk_vector_rows_impl(children, lane_scale, reach.nested(), visit),
|
||||
Graphic::Group(group) => match core_types::record::RunView::<Graphic>::new(&group.content) {
|
||||
Some(run) => walk_vector_rows_impl(&run, lane_scale, reach.into_group_graphics(), visit),
|
||||
None => walk_rows_of_run(&group.content, lane_scale, layer_path, row_paint, visit),
|
||||
},
|
||||
_ => RowStep::Continue,
|
||||
};
|
||||
if let RowStep::Stop = step {
|
||||
return RowStep::Stop;
|
||||
}
|
||||
}
|
||||
RowStep::Continue
|
||||
}
|
||||
|
||||
/// The level's flattened vector rows as one owned list, the walk's collect
|
||||
/// form.
|
||||
pub fn flatten_vector_rows<S: LaneSource<Element = Graphic>>(source: &S) -> List<Vector> {
|
||||
let mut out = List::new();
|
||||
walk_vector_rows(source, &mut |row| {
|
||||
row.build_into(&mut out);
|
||||
RowStep::Continue
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// One typed run as the legacy list its `Render` impl consumes, nested
|
||||
/// groups converted to their legacy form.
|
||||
pub fn run_to_render_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
|
||||
@@ -1444,6 +1699,54 @@ mod run_tests {
|
||||
assert!(matches!(list.element(0), Some(Graphic::Group(_))), "the list keeps the native group form");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_vector_row_walk_matches_the_legacy_flatten() {
|
||||
let inner_vector = unit_square_at(DVec2::ZERO);
|
||||
let inner_layout = Layout::default().with_writes(0, element_write_hashed::<Vector>(), &[]);
|
||||
let mut inner_bytes = vec![0u8; inner_layout.lane_stride()];
|
||||
// SAFETY: `inner_bytes` is one lane of `inner_layout`; a parked element
|
||||
// stores its reference.
|
||||
unsafe { inner_bytes.as_mut_ptr().cast::<&Vector>().write(&inner_vector) };
|
||||
// SAFETY: `inner_bytes` holds one lane of `inner_layout` at its stride.
|
||||
let inner_item = unsafe { GroupItem::from_resident(RecordBatch::new(inner_bytes.as_ptr(), 1, &inner_layout)) };
|
||||
|
||||
let mut painted = List::new();
|
||||
painted.push(Item::new_from_element(unit_square_at(DVec2::ZERO)));
|
||||
painted.push(Item::new_from_element(unit_square_at(DVec2::ONE)));
|
||||
painted.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(1., 0.)));
|
||||
painted.set_attribute(core_types::ATTR_TRANSFORM, 1, DAffine2::from_translation(DVec2::new(0., 1.)));
|
||||
set_paint_attribute_at(&mut painted, 1, ATTR_FILL, List::new_from_element(Graphic::Color(List::new_from_element(Color::WHITE))));
|
||||
|
||||
let mut nested_child = List::new();
|
||||
nested_child.push(Item::new_from_element(unit_square_at(DVec2::new(2., 2.))));
|
||||
let mut nested = List::new_from_element(Graphic::Vector(nested_child));
|
||||
nested.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_scale(DVec2::splat(2.)));
|
||||
|
||||
let mut top = List::new();
|
||||
top.push(Item::new_from_element(Graphic::Vector(painted)));
|
||||
top.push(Item::new_from_element(Graphic::Graphic(nested)));
|
||||
top.push(Item::new_from_element(Graphic::Group(core_types::record::Group {
|
||||
row: None,
|
||||
content: inner_item,
|
||||
})));
|
||||
top.push(Item::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))));
|
||||
top.set_attribute(core_types::ATTR_TRANSFORM, 0, DAffine2::from_translation(DVec2::new(5., 5.)));
|
||||
top.set_attribute(core_types::ATTR_EDITOR_LAYER_PATH, 0, vec![core_types::uuid::NodeId(7)]);
|
||||
set_paint_attribute_at(&mut top, 0, ATTR_FILL, List::new_from_element(Graphic::Color(List::new_from_element(Color::BLACK))));
|
||||
top.set_attribute(core_types::ATTR_OPACITY, 1, 0.5);
|
||||
top.set_attribute(core_types::ATTR_TRANSFORM, 2, DAffine2::from_scale(DVec2::splat(3.)));
|
||||
|
||||
let legacy = {
|
||||
let mut list = top.clone();
|
||||
for element in list.iter_element_values_mut() {
|
||||
*element = map_groups_to_legacy(element);
|
||||
}
|
||||
push_lane_paint_into_interiors(&mut list);
|
||||
list.into_flattened_list::<Vector>()
|
||||
};
|
||||
assert_eq!(flatten_vector_rows(&top), legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_and_its_legacy_list_agree_on_bounding_boxes() {
|
||||
let vectors = [unit_square_at(DVec2::ZERO), unit_square_at(DVec2::new(4., 4.))];
|
||||
|
||||
@@ -22,7 +22,7 @@ use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_hash::CacheHashWrapper;
|
||||
use graphene_resource::Resource;
|
||||
use graphic_types::graphic::{LanePaint, PaintColumns, PaintOverlay, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
|
||||
use graphic_types::graphic::{PaintColumns, PaintOverlay, PaintReach, has_paint, is_paint_present, paint_graphics, set_paint_attribute, vector_can_reduce_to_clip_path};
|
||||
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke};
|
||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
||||
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
|
||||
@@ -614,50 +614,6 @@ impl Render for Graphic {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a lane's paint reaches into the element beneath it, mirroring the
|
||||
/// legacy conversion's paint push: vector interiors directly and vector
|
||||
/// children of a nested graphic list, one level deep.
|
||||
#[derive(Clone, Copy)]
|
||||
struct PaintReach<'a> {
|
||||
paint: LanePaint<'a>,
|
||||
hops: u8,
|
||||
}
|
||||
|
||||
impl<'a> PaintReach<'a> {
|
||||
const NONE: Self = Self { paint: LanePaint::NONE, hops: 0 };
|
||||
|
||||
/// The lane's effective reach: an inherited paint stays authoritative
|
||||
/// (lane paint below a push's origin is inert in the legacy model), an
|
||||
/// absent one reads the lane's own paint.
|
||||
fn for_lane<S: LaneSource>(self, columns: &PaintColumns<'a, S>, index: usize) -> Self {
|
||||
match self.paint.is_present() {
|
||||
true => self,
|
||||
false => Self { paint: columns.read(index), hops: 2 },
|
||||
}
|
||||
}
|
||||
|
||||
fn applies(&self) -> bool {
|
||||
self.hops > 0 && self.paint.is_present()
|
||||
}
|
||||
|
||||
/// The reach one graphic nesting level further down.
|
||||
fn nested(self) -> Self {
|
||||
Self {
|
||||
paint: self.paint,
|
||||
hops: self.hops.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reach entering a group's own graphic run: a spent or absent reach
|
||||
/// resets so the group's own lane paint applies at its own boundary.
|
||||
fn into_group_graphics(self) -> Self {
|
||||
match self.applies() {
|
||||
true => self.nested(),
|
||||
false => Self::NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_element_svg<'a>(element: &'a Graphic, reach: PaintReach<'a>, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
match element {
|
||||
Graphic::Vector(inner) if reach.applies() => render_vector_svg(&PaintOverlay::new(inner, reach.paint), render, render_params),
|
||||
|
||||
@@ -1359,15 +1359,13 @@ fn offset_path(_: impl Ctx, (vector, lane_transform): (Vector, Attr<TransformAtt
|
||||
(result, Attr(transform_attribute))
|
||||
}
|
||||
|
||||
fn solidify_stroke_core(graphic_list: List<Graphic>) -> List<Vector> {
|
||||
fn solidify_rows(flattened: List<Vector>) -> List<Vector> {
|
||||
// TODO: Make this node support stroke align, which it currently ignores
|
||||
|
||||
let flattened: List<Vector> = graphic_list.clone().into_flattened_list();
|
||||
|
||||
// A fill exists when the canonical attribute carries paint
|
||||
let has_fills: Vec<bool> = (0..flattened.len()).map(|index| has_paint::<Fill, _>(&flattened, index)).collect();
|
||||
|
||||
let mut output: List<Vector> = flattened
|
||||
let output: List<Vector> = flattened
|
||||
.into_iter()
|
||||
.zip(has_fills)
|
||||
.flat_map(|(row, has_fill)| {
|
||||
@@ -1444,31 +1442,17 @@ fn solidify_stroke_core(graphic_list: List<Graphic>) -> List<Vector> {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Snapshot the upstream content so the renderer can recurse into it for editor click-target preservation
|
||||
// and surface the original pre-solidified `Vector` to the Path tool for editing.
|
||||
if !output.is_empty() {
|
||||
// Row 0 carries a composed transform inherited from the flattened input, but the merged_layers
|
||||
// already holds the original transforms; pre-compensate by row 0's inverse so the renderer's
|
||||
// `upstream_footprint *= row_0_transform` recursion cancels out and leaves the originals intact.
|
||||
let mut graphic_list = graphic_list;
|
||||
let row_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
|
||||
let inverse = row_0_transform.inverse();
|
||||
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*transform = inverse * *transform;
|
||||
}
|
||||
}
|
||||
|
||||
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// One output lane of the solidify: the walk locates the flattened row the
|
||||
/// lane addresses (a fill-bearing row serves two lanes), builds and splits
|
||||
/// only that row, and lane 0 additionally carries the merged-layers snapshot.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn solidify_lane<'e>(
|
||||
fn solidify_native_lane<'e, S: core_types::lane::LaneSource<Element = Graphic>>(
|
||||
arena: &'e core_types::arena::Arena,
|
||||
graphic_list: List<Graphic>,
|
||||
source: &S,
|
||||
snapshot: impl FnOnce() -> List<Graphic>,
|
||||
lane: usize,
|
||||
) -> Result<
|
||||
(
|
||||
@@ -1485,7 +1469,42 @@ fn solidify_lane<'e>(
|
||||
),
|
||||
Interrupt,
|
||||
> {
|
||||
emit_legacy_lane(arena, solidify_stroke_core(graphic_list), lane)
|
||||
use graphic_types::graphic::RowStep;
|
||||
let mut remaining = lane;
|
||||
let mut located: Option<List<Vector>> = None;
|
||||
graphic_types::graphic::walk_vector_rows(source, &mut |row| {
|
||||
let parts = 1 + row.has_fill() as usize;
|
||||
if remaining >= parts {
|
||||
remaining -= parts;
|
||||
return RowStep::Continue;
|
||||
}
|
||||
let mut one = List::new();
|
||||
row.build_into(&mut one);
|
||||
located = Some(one);
|
||||
RowStep::Stop
|
||||
});
|
||||
let Some(row) = located else {
|
||||
return Err(GraphError::past_end().into());
|
||||
};
|
||||
let mut split = solidify_rows(row);
|
||||
// Snapshot the upstream content so the renderer can recurse into it for editor click-target preservation
|
||||
// and surface the original pre-solidified `Vector` to the Path tool for editing.
|
||||
if lane == 0 && !split.is_empty() {
|
||||
// Row 0 carries a composed transform inherited from the flattened input, but the merged_layers
|
||||
// already holds the original transforms; pre-compensate by row 0's inverse so the renderer's
|
||||
// `upstream_footprint *= row_0_transform` recursion cancels out and leaves the originals intact.
|
||||
let mut graphic_list = snapshot();
|
||||
let row_0_transform: DAffine2 = split.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
|
||||
let inverse = row_0_transform.inverse();
|
||||
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*transform = inverse * *transform;
|
||||
}
|
||||
}
|
||||
|
||||
split.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list));
|
||||
}
|
||||
emit_legacy_lane(arena, split, remaining)
|
||||
}
|
||||
|
||||
/// One lane of a legacy result list as the element and standard-attribute
|
||||
@@ -1584,7 +1603,10 @@ fn solidify_stroke<'e>(
|
||||
)>,
|
||||
Interrupt,
|
||||
> {
|
||||
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.index() as usize)
|
||||
// SAFETY: a materialized input's frames are arena-resident.
|
||||
let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) };
|
||||
let run = core_types::record::RunView::<Graphic>::new(&item).expect("the run holds graphic lanes");
|
||||
solidify_native_lane(ctx.arena(), &run, || legacy_graphic_list_of(content), ctx.index() as usize)
|
||||
}
|
||||
|
||||
/// A fill-bearing row splits into a fill lane and a solidified stroke lane,
|
||||
@@ -1623,7 +1645,19 @@ fn solidify_stroke_vector<'e>(
|
||||
)>,
|
||||
Interrupt,
|
||||
> {
|
||||
solidify_lane(ctx.arena(), legacy_graphic_list_of(content), ctx.index() as usize)
|
||||
// SAFETY: a materialized input's frames are arena-resident.
|
||||
let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) };
|
||||
// The wrap the legacy list collapse applied: lane 0's layer path stamps
|
||||
// every row.
|
||||
let layer_path: Vec<NodeId> = match content.len() > 0 {
|
||||
true => content.lane(0).attr::<EditorLayerPath>().to_vec(),
|
||||
false => Vec::new(),
|
||||
};
|
||||
let mut wrapper = List::new_from_element(Graphic::Group(core_types::record::Group { row: None, content: item }));
|
||||
if !layer_path.is_empty() {
|
||||
wrapper.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer_path);
|
||||
}
|
||||
solidify_native_lane(ctx.arena(), &wrapper, || legacy_graphic_list_of(content), ctx.index() as usize)
|
||||
}
|
||||
|
||||
fn solidify_stroke_vector_extent(content: ListIn<'_, Vector>, level: LevelIn) -> GPoll<Extent> {
|
||||
|
||||
Reference in New Issue
Block a user