Rename the Group type to Graphic everywhere (#3009)

This commit is contained in:
Keavon Chambers
2025-08-05 20:55:15 -07:00
committed by GitHub
parent 309a64340b
commit 0f638314dc
44 changed files with 267 additions and 277 deletions

View File

@@ -14,7 +14,7 @@ use std::hash::Hash;
/// Some [`ArtboardData`] with some optional clipping bounds that can be exported.
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct Artboard {
pub group: Table<Graphic>,
pub content: Table<Graphic>,
pub label: String,
pub location: IVec2,
pub dimensions: IVec2,
@@ -31,7 +31,7 @@ impl Default for Artboard {
impl Artboard {
pub fn new(location: IVec2, dimensions: IVec2) -> Self {
Self {
group: Table::new(),
content: Table::new(),
label: "Artboard".to_string(),
location: location.min(location + dimensions),
dimensions: dimensions.abs(),
@@ -47,7 +47,7 @@ impl BoundingBox for Artboard {
if self.clip {
Some(artboard_bounds)
} else {
[self.group.bounding_box(transform, include_stroke), Some(artboard_bounds)]
[self.content.bounding_box(transform, include_stroke), Some(artboard_bounds)]
.into_iter()
.flatten()
.reduce(Quad::combine_bounds)
@@ -56,7 +56,7 @@ impl BoundingBox for Artboard {
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Artboard>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Default, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
@@ -84,7 +84,7 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
}
table
}
EitherFormat::ArtboardTable(artboard_group_table) => artboard_group_table,
EitherFormat::ArtboardTable(artboard_table) => artboard_table,
})
}
@@ -119,7 +119,7 @@ async fn create_artboard<T: Into<Table<Graphic>> + 'n>(
footprint.translate(location.as_dvec2());
new_ctx = new_ctx.with_footprint(footprint);
}
let group = content.eval(new_ctx.into_context()).await.into();
let content = content.eval(new_ctx.into_context()).await.into();
let dimensions = dimensions.as_ivec2().max(IVec2::ONE);
@@ -128,7 +128,7 @@ async fn create_artboard<T: Into<Table<Graphic>> + 'n>(
let dimensions = dimensions.abs();
Table::new_from_element(Artboard {
group,
content,
label,
location,
dimensions,

View File

@@ -13,7 +13,7 @@ use std::hash::Hash;
/// The possible forms of graphical content that can be rendered by the Render node into either an image or SVG syntax.
#[derive(Clone, Debug, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum Graphic {
Group(Table<Graphic>),
Graphic(Table<Graphic>),
Vector(Table<Vector>),
RasterCPU(Table<Raster<CPU>>),
RasterGPU(Table<Raster<GPU>>),
@@ -21,14 +21,14 @@ pub enum Graphic {
impl Default for Graphic {
fn default() -> Self {
Self::Group(Default::default())
Self::Graphic(Default::default())
}
}
// Group
// Graphic
impl From<Table<Graphic>> for Graphic {
fn from(group: Table<Graphic>) -> Self {
Graphic::Group(group)
fn from(graphic: Table<Graphic>) -> Self {
Graphic::Graphic(graphic)
}
}
@@ -111,16 +111,16 @@ impl From<DAffine2> for Table<Graphic> {
}
impl Graphic {
pub fn as_group(&self) -> Option<&Table<Graphic>> {
pub fn as_graphic(&self) -> Option<&Table<Graphic>> {
match self {
Graphic::Group(group) => Some(group),
Graphic::Graphic(graphic) => Some(graphic),
_ => None,
}
}
pub fn as_group_mut(&mut self) -> Option<&mut Table<Graphic>> {
pub fn as_graphic_mut(&mut self) -> Option<&mut Table<Graphic>> {
match self {
Graphic::Group(group) => Some(group),
Graphic::Graphic(graphic) => Some(graphic),
_ => None,
}
}
@@ -156,7 +156,7 @@ impl Graphic {
pub fn had_clip_enabled(&self) -> bool {
match self {
Graphic::Vector(vector) => vector.iter().all(|row| row.alpha_blending.clip),
Graphic::Group(group) => group.iter().all(|row| row.alpha_blending.clip),
Graphic::Graphic(graphic) => graphic.iter().all(|row| row.alpha_blending.clip),
Graphic::RasterCPU(raster) => raster.iter().all(|row| row.alpha_blending.clip),
Graphic::RasterGPU(raster) => raster.iter().all(|row| row.alpha_blending.clip),
}
@@ -180,7 +180,7 @@ impl BoundingBox for Graphic {
Graphic::Vector(vector) => vector.bounding_box(transform, include_stroke),
Graphic::RasterCPU(raster) => raster.bounding_box(transform, include_stroke),
Graphic::RasterGPU(raster) => raster.bounding_box(transform, include_stroke),
Graphic::Group(group) => group.bounding_box(transform, include_stroke),
Graphic::Graphic(graphic) => graphic.bounding_box(transform, include_stroke),
}
}
}
@@ -295,7 +295,7 @@ async fn flatten_graphic(_: impl Ctx, content: Table<Graphic>, fully_flatten: bo
match current_element {
// If we're allowed to recurse, flatten any graphics we encounter
Graphic::Group(mut current_element) if recurse => {
Graphic::Graphic(mut current_element) if recurse => {
// Apply the parent graphic's transform to all child elements
for graphic in current_element.iter_mut() {
*graphic.transform = *current_row.transform * *graphic.transform;
@@ -332,7 +332,7 @@ async fn flatten_vector(_: impl Ctx, content: Table<Graphic>) -> Table<Vector> {
match current_graphic {
// If we're allowed to recurse, flatten any tables we encounter
Graphic::Group(mut current_graphic_table) => {
Graphic::Graphic(mut current_graphic_table) => {
// Apply the parent graphic's transform to all child elements
for graphic in current_graphic_table.iter_mut() {
*graphic.transform = *current_graphic_row.transform * *graphic.transform;
@@ -418,7 +418,7 @@ impl<T: Clone> AtIndex for Table<T> {
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DynAny, Default, serde::Serialize, serde::Deserialize)]
@@ -441,24 +441,24 @@ pub fn migrate_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::OldGraphicGroup(old) => {
let mut group_table = Table::new();
let mut graphic_table = Table::new();
for (graphic, source_node_id) in old.elements {
group_table.push(TableRow {
graphic_table.push(TableRow {
element: graphic,
transform: old.transform,
alpha_blending: old.alpha_blending,
source_node_id,
});
}
group_table
graphic_table
}
EitherFormat::Table(value) => {
// Try to deserialize as either table format
if let Ok(old_table) = serde_json::from_value::<Table<GraphicGroup>>(value.clone()) {
let mut group_table = Table::new();
let mut graphic_table = Table::new();
for row in old_table.iter() {
for (graphic, source_node_id) in &row.element.elements {
group_table.push(TableRow {
graphic_table.push(TableRow {
element: graphic.clone(),
transform: *row.transform,
alpha_blending: *row.alpha_blending,
@@ -466,7 +466,7 @@ pub fn migrate_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul
});
}
}
group_table
graphic_table
} else if let Ok(new_table) = serde_json::from_value::<Table<Graphic>>(value) {
new_table
} else {

View File

@@ -356,7 +356,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
GraphicElement::RasterFrame(RasterFrame::ImageFrame(image)) => Self {
image: image.iter().next().unwrap().element.clone(),
},
_ => panic!("Expected Image, found {:?}", element),
_ => panic!("Expected Image, found {element:?}"),
}
}
}

View File

@@ -18,14 +18,14 @@ impl<T: RenderComplexity> RenderComplexity for Table<T> {
impl RenderComplexity for Artboard {
fn render_complexity(&self) -> usize {
self.group.render_complexity()
self.content.render_complexity()
}
}
impl RenderComplexity for Graphic {
fn render_complexity(&self) -> usize {
match self {
Self::Group(table) => table.render_complexity(),
Self::Graphic(table) => table.render_complexity(),
Self::Vector(table) => table.render_complexity(),
Self::RasterCPU(table) => table.render_complexity(),
Self::RasterGPU(table) => table.render_complexity(),

View File

@@ -122,7 +122,7 @@ impl ClickTarget {
}
// Check if shape is entirely within selection
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
let any_point_from_subpath = subpath.manipulator_groups().first().map(|manipulators| manipulators.anchor);
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
}
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,

View File

@@ -188,9 +188,9 @@ pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup
ManipulatorGroup::new(point_to_dvec2(point2), Some(point_to_dvec2(point1)), None)
}
kurbo::PathEl::ClosePath => {
if let Some(last_group) = manipulator_groups.pop() {
if let Some(first_group) = manipulator_groups.first_mut() {
first_group.out_handle = last_group.in_handle;
if let Some(last_manipulators) = manipulator_groups.pop() {
if let Some(first_manipulators) = manipulator_groups.first_mut() {
first_manipulators.out_handle = last_manipulators.in_handle;
}
}
is_closed = true;

View File

@@ -811,13 +811,13 @@ impl Vector {
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut last: Option<(usize, BezierHandles)> = None;
for (handle, start, end) in segments {
first_point = Some(first_point.unwrap_or(start));
groups.push(ManipulatorGroup {
manipulators_list.push(ManipulatorGroup {
anchor: self.point_domain.positions()[start],
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
@@ -827,13 +827,13 @@ impl Vector {
last = Some((end, handle));
}
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
let closed = manipulators_list.len() > 1 && last.map(|(point, _)| point) == first_point;
if let Some((end, last_handle)) = last {
if closed {
groups[0].in_handle = last_handle.end();
manipulators_list[0].in_handle = last_handle.end();
} else {
groups.push(ManipulatorGroup {
manipulators_list.push(ManipulatorGroup {
anchor: self.point_domain.positions()[end],
in_handle: last_handle.end(),
out_handle: None,
@@ -842,7 +842,7 @@ impl Vector {
}
}
Some(bezier_rs::Subpath::new(groups, closed))
Some(bezier_rs::Subpath::new(manipulators_list, closed))
}
/// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions.
@@ -905,7 +905,7 @@ impl Vector {
/// Construct a [`bezier_rs::Bezier`] curve for stroke.
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = bezier_rs::Subpath<PointId>> {
self.build_stroke_path_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed))
self.build_stroke_path_iter().map(|(manipulators_list, closed)| bezier_rs::Subpath::new(manipulators_list, closed))
}
/// Construct and return an iterator of Vec of `(bezier_rs::ManipulatorGroup<PointId>], bool)` for stroke.
@@ -916,15 +916,15 @@ impl Vector {
/// Construct a [`kurbo::BezPath`] curve for stroke.
pub fn stroke_bezpath_iter(&self) -> impl Iterator<Item = kurbo::BezPath> {
self.build_stroke_path_iter().map(|(group, closed)| {
self.build_stroke_path_iter().map(|(manipulators_list, closed)| {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
let Some(first) = group.first() else { return bezpath };
let Some(first) = manipulators_list.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.anchor));
out_handle = first.out_handle;
for manipulator in group.iter().skip(1) {
for manipulator in manipulators_list.iter().skip(1) {
match (out_handle, manipulator.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
@@ -954,7 +954,7 @@ impl Vector {
pub fn manipulator_group_id(&self, id: impl Into<PointId>) -> Option<ManipulatorGroup<PointId>> {
let id = id.into();
self.manipulator_groups().find(|group| group.id == id)
self.manipulator_groups().find(|manipulators| manipulators.id == id)
}
pub fn transform(&mut self, transform: DAffine2) {
@@ -1047,13 +1047,13 @@ impl Iterator for StrokePathIter<'_> {
// There will always be one (seeing as we checked above)
let mut point_index = current_start;
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut in_handle = None;
let mut closed = false;
loop {
let Some(val) = self.points[point_index].take_first() else {
// Dead end
groups.push(ManipulatorGroup {
manipulators_list.push(ManipulatorGroup {
anchor: self.vector.point_domain.positions()[point_index],
in_handle,
out_handle: None,
@@ -1072,7 +1072,7 @@ impl Iterator for StrokePathIter<'_> {
} else {
self.vector.segment_domain.end_point()[val.segment_index]
};
groups.push(ManipulatorGroup {
manipulators_list.push(ManipulatorGroup {
anchor: self.vector.point_domain.positions()[point_index],
in_handle,
out_handle: handles.start(),
@@ -1085,12 +1085,12 @@ impl Iterator for StrokePathIter<'_> {
self.points[next_point_index].take_eq(val.flipped());
if next_point_index == current_start {
closed = true;
groups[0].in_handle = in_handle;
manipulators_list[0].in_handle = in_handle;
break;
}
}
Some((groups, closed))
Some((manipulators_list, closed))
}
}

View File

@@ -50,7 +50,7 @@ async fn assign_colors<T>(
_: impl Ctx,
#[implementations(Table<Graphic>, Table<Vector>)]
#[widget(ParsedWidgetOverride::Hidden)]
/// The vector elements, or group of vector elements, to apply the fill and/or stroke style to.
/// The content with vector paths to apply the fill and/or stroke style to.
mut content: T,
#[default(true)]
/// Whether to style the fill.
@@ -118,7 +118,7 @@ async fn fill<F: Into<Fill> + 'n + Send, V>(
Table<Graphic>,
Table<Graphic>
)]
/// The vector elements, or group of vector elements, to apply the fill to.
/// The content with vector paths to apply the fill style to.
mut content: V,
#[implementations(
Fill,
@@ -156,7 +156,7 @@ where
async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
_: impl Ctx,
#[implementations(Table<Vector>, Table<Vector>, Table<Graphic>, Table<Graphic>)]
/// The vector elements, or group of vector elements, to apply the stroke to.
/// The content with vector paths to apply the stroke style to.
mut content: Table<V>,
#[implementations(
Option<Color>,
@@ -447,7 +447,7 @@ async fn round_corners(
let source_node_id = source.source_node_id;
let source = source.element;
let upstream_group = source.upstream_group.clone();
let upstream_nested_layers = source.upstream_nested_layers.clone();
// Flip the roundness to help with user intuition
let roundness = 1. - roundness;
@@ -532,7 +532,7 @@ async fn round_corners(
result.append_bezpath(rounded_subpath);
}
result.upstream_group = upstream_group;
result.upstream_nested_layers = upstream_nested_layers;
TableRow {
element: result,
@@ -687,18 +687,18 @@ async fn auto_tangents(
for mut subpath in source.stroke_bezier_paths() {
subpath.apply_transform(transform);
let groups = subpath.manipulator_groups();
if groups.len() < 2 {
let manipulators_list = subpath.manipulator_groups();
if manipulators_list.len() < 2 {
// Not enough points for softening or handle removal
result.append_subpath(subpath, true);
continue;
}
let mut new_groups = Vec::with_capacity(groups.len());
let mut new_manipulators_list = Vec::with_capacity(manipulators_list.len());
let is_closed = subpath.closed();
for i in 0..groups.len() {
let curr = &groups[i];
for i in 0..manipulators_list.len() {
let curr = &manipulators_list[i];
if preserve_existing {
// Check if this point has handles that are meaningfully different from the anchor
@@ -706,15 +706,15 @@ async fn auto_tangents(
|| (curr.out_handle.is_some() && !curr.out_handle.unwrap().abs_diff_eq(curr.anchor, 1e-5));
// If the point already has handles, or if it's an endpoint of an open path, keep it as is.
if has_handles || (!is_closed && (i == 0 || i == groups.len() - 1)) {
new_groups.push(*curr);
if has_handles || (!is_closed && (i == 0 || i == manipulators_list.len() - 1)) {
new_manipulators_list.push(*curr);
continue;
}
}
// If spread is 0, remove handles for this point, making it a sharp corner.
if spread == 0. {
new_groups.push(ManipulatorGroup {
new_manipulators_list.push(ManipulatorGroup {
anchor: curr.anchor,
in_handle: None,
out_handle: None,
@@ -724,12 +724,12 @@ async fn auto_tangents(
}
// Get previous and next points for auto-tangent calculation
let prev_idx = if i == 0 { if is_closed { groups.len() - 1 } else { i } } else { i - 1 };
let next_idx = if i == groups.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 };
let prev_idx = if i == 0 { if is_closed { manipulators_list.len() - 1 } else { i } } else { i - 1 };
let next_idx = if i == manipulators_list.len() - 1 { if is_closed { 0 } else { i } } else { i + 1 };
let prev = groups[prev_idx].anchor;
let prev = manipulators_list[prev_idx].anchor;
let curr_pos = curr.anchor;
let next = groups[next_idx].anchor;
let next = manipulators_list[next_idx].anchor;
// Calculate directions from current point to adjacent points
let dir_prev = (prev - curr_pos).normalize_or_zero();
@@ -738,7 +738,7 @@ async fn auto_tangents(
// Check if we have valid directions (e.g., points are not coincident)
if dir_prev.length_squared() < 1e-5 || dir_next.length_squared() < 1e-5 {
// Fallback: keep the original manipulator group (which has no active handles here)
new_groups.push(*curr);
new_manipulators_list.push(*curr);
continue;
}
@@ -758,7 +758,7 @@ async fn auto_tangents(
let out_length = (next - curr_pos).length() / 3. * spread;
// Create new manipulator group with calculated auto-tangents
new_groups.push(ManipulatorGroup {
new_manipulators_list.push(ManipulatorGroup {
anchor: curr_pos,
in_handle: Some(curr_pos + handle_dir * in_length),
out_handle: Some(curr_pos - handle_dir * out_length),
@@ -766,7 +766,7 @@ async fn auto_tangents(
});
}
let mut softened_bezpath = bezpath_from_manipulator_groups(&new_groups, is_closed);
let mut softened_bezpath = bezpath_from_manipulator_groups(&new_manipulators_list, is_closed);
softened_bezpath.apply_affine(Affine::new(transform.inverse().to_cols_array()));
result.append_bezpath(softened_bezpath);
}
@@ -985,7 +985,7 @@ where
output.element.style = row.element.style.clone();
}
}
Graphic::Group(graphic) => {
Graphic::Graphic(graphic) => {
let mut graphic = graphic.clone();
for row in graphic.iter_mut() {
*row.transform = *current_element.transform * *row.transform;
@@ -1032,7 +1032,7 @@ async fn sample_polyline(
region_domain: Default::default(),
colinear_manipulators: Default::default(),
style: std::mem::take(&mut row.element.style),
upstream_group: std::mem::take(&mut row.element.upstream_group),
upstream_nested_layers: std::mem::take(&mut row.element.upstream_nested_layers),
};
// Transfer the stroke transform from the input vector content to the result.
result.style.set_stroke_transform(row.transform);
@@ -1343,7 +1343,7 @@ async fn spline(_: impl Ctx, content: Table<Vector>) -> Table<Vector> {
let mut segment_domain = SegmentDomain::default();
for (manipulator_groups, closed) in row.element.stroke_manipulator_groups() {
let positions = manipulator_groups.iter().map(|group| group.anchor).collect::<Vec<_>>();
let positions = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::<Vec<_>>();
let closed = closed && positions.len() > 2;
// Compute control point handles for Bezier spline.
@@ -2075,7 +2075,14 @@ mod test {
let bounding_box = super::bounding_box((), vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await;
let bounding_box = bounding_box.iter().next().unwrap().element;
assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
let manipulator_groups_anchors = bounding_box.region_manipulator_groups().next().unwrap().1.iter().map(|group| group.anchor).collect::<Vec<DVec2>>();
let manipulator_groups_anchors = bounding_box
.region_manipulator_groups()
.next()
.unwrap()
.1
.iter()
.map(|manipulators| manipulators.anchor)
.collect::<Vec<DVec2>>();
assert_eq!(&manipulator_groups_anchors[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
@@ -2086,7 +2093,14 @@ mod test {
let bounding_box = BoundingBoxNode { content: FutureWrapperNode(square) }.eval(Footprint::default()).await;
let bounding_box = bounding_box.iter().next().unwrap().element;
assert_eq!(bounding_box.region_manipulator_groups().count(), 1);
let manipulator_groups_anchors = bounding_box.region_manipulator_groups().next().unwrap().1.iter().map(|group| group.anchor).collect::<Vec<DVec2>>();
let manipulator_groups_anchors = bounding_box
.region_manipulator_groups()
.next()
.unwrap()
.1
.iter()
.map(|manipulators| manipulators.anchor)
.collect::<Vec<DVec2>>();
let expected_bounding_box = [DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.)];
for i in 0..4 {
@@ -2108,7 +2122,7 @@ mod test {
for (index, (_, manipulator_groups)) in flattened_copy_to_points.region_manipulator_groups().enumerate() {
let offset = expected_points[index];
let manipulator_groups_anchors = manipulator_groups.iter().map(|group| group.anchor).collect::<Vec<DVec2>>();
let manipulator_groups_anchors = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::<Vec<DVec2>>();
assert_eq!(
&manipulator_groups_anchors,
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]

View File

@@ -29,8 +29,10 @@ pub struct Vector {
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
// Used to store the upstream group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_group: Option<Table<Graphic>>,
/// Used to store the upstream group/folder of nested layers during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved for the child layers.
/// Without this, the tools would be working with a collapsed version of the data which has no reference to the original child layers that were booleaned together, resulting in the inner layers not being editable.
#[serde(alias = "upstream_group")]
pub upstream_nested_layers: Option<Table<Graphic>>,
}
impl Default for Vector {
@@ -41,7 +43,7 @@ impl Default for Vector {
point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::new(),
upstream_group: None,
upstream_nested_layers: None,
}
}
}
@@ -468,15 +470,12 @@ pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resu
pub style: PathStyle,
/// 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]>,
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
// Used to store the upstream group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<Table<Graphic>>,
}
@@ -498,7 +497,7 @@ pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resu
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
upstream_group: old.upstream_graphic_group,
upstream_nested_layers: old.upstream_graphic_group,
});
*vector_table.iter_mut().next().unwrap().transform = old.transform;
*vector_table.iter_mut().next().unwrap().alpha_blending = old.alpha_blending;

View File

@@ -34,9 +34,9 @@ pub enum BooleanOperation {
#[node_macro::node(category(""))]
async fn boolean_operation<I: Into<Table<Graphic>> + 'n + Send + Clone>(
_: impl Ctx,
/// The group of paths to perform the boolean operation on. Nested groups are automatically flattened.
/// The table of vector paths to perform the boolean operation on. Nested tables are automatically flattened.
#[implementations(Table<Graphic>, Table<Vector>)]
group_of_paths: I,
content: I,
/// Which boolean operation to perform on the paths.
///
/// Union combines all paths while cutting out overlapping areas (even the interiors of a single path).
@@ -45,10 +45,10 @@ async fn boolean_operation<I: Into<Table<Graphic>> + 'n + Send + Clone>(
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
operation: BooleanOperation,
) -> Table<Vector> {
let group_of_paths = group_of_paths.into();
let content = content.into();
// The first index is the bottom of the stack
let mut result_vector_table = boolean_operation_on_vector_table(flatten_vector(&group_of_paths).iter(), operation);
let mut result_vector_table = boolean_operation_on_vector_table(flatten_vector(&content).iter(), operation);
// Replace the transformation matrix with a mutation of the vector points themselves
if let Some(result_vector) = result_vector_table.iter_mut().next() {
@@ -57,7 +57,7 @@ async fn boolean_operation<I: Into<Table<Graphic>> + 'n + Send + Clone>(
Vector::transform(result_vector.element, transform);
result_vector.element.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector.element.upstream_group = Some(group_of_paths.clone());
result_vector.element.upstream_nested_layers = Some(content.clone());
// Clean up the boolean operation result by merging duplicated points
result_vector.element.merge_by_distance_spatial(*result_vector.transform, 0.0001);
@@ -221,13 +221,13 @@ fn difference<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector
boolean_operation_on_vector_table(union.iter().chain(std::iter::once(any_intersection.as_ref())), BooleanOperation::SubtractFront)
}
fn flatten_vector(group_table: &Table<Graphic>) -> Table<Vector> {
group_table
fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
graphic_table
.iter()
.flat_map(|element| {
match element.element.clone() {
Graphic::Vector(vector) => {
// Apply the parent group's transform to each element of the vector table
// Apply the parent graphic's transform to each element of the vector table
vector
.into_iter()
.map(|mut sub_vector| {
@@ -250,7 +250,7 @@ fn flatten_vector(group_table: &Table<Graphic>) -> Table<Vector> {
TableRow { element, ..Default::default() }
};
// Apply the parent group's transform to each raster element
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
Graphic::RasterGPU(image) => {
@@ -266,17 +266,17 @@ fn flatten_vector(group_table: &Table<Graphic>) -> Table<Vector> {
TableRow { element, ..Default::default() }
};
// Apply the parent group's transform to each raster element
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
Graphic::Group(mut group) => {
// Apply the parent group's transform to each element of inner group
for sub_element in group.iter_mut() {
Graphic::Graphic(mut graphic) => {
// Apply the parent graphic's transform to each element of inner table
for sub_element in graphic.iter_mut() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
// Recursively flatten the inner group into the vector table
let unioned = boolean_operation_on_vector_table(flatten_vector(&group).iter(), BooleanOperation::Union);
// Recursively flatten the inner table into the output vector table
let unioned = boolean_operation_on_vector_table(flatten_vector(&graphic).iter(), BooleanOperation::Union);
unioned.into_iter().collect::<Vec<_>>()
}
@@ -329,7 +329,7 @@ fn from_path(path_data: &[Path]) -> Vector {
for path in path_data.iter().filter(|path| !path.is_empty()) {
let cubics: Vec<[DVec2; 4]> = path.iter().map(|segment| segment.to_cubic()).collect();
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut current_start = None;
for (index, cubic) in cubics.iter().enumerate() {
@@ -337,27 +337,27 @@ fn from_path(path_data: &[Path]) -> Vector {
if current_start.is_none() || !is_close(start, current_start.unwrap()) {
// Start a new subpath
if !groups.is_empty() {
all_subpaths.push(Subpath::new(std::mem::take(&mut groups), true));
if !manipulators_list.is_empty() {
all_subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
}
// Use the correct in-handle (None) and out-handle for the start point
groups.push(ManipulatorGroup::new(start, None, Some(handle1)));
manipulators_list.push(ManipulatorGroup::new(start, None, Some(handle1)));
} else {
// Update the out-handle of the previous point
if let Some(last) = groups.last_mut() {
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(handle1);
}
}
// Add the end point with the correct in-handle and out-handle (None)
groups.push(ManipulatorGroup::new(end, Some(handle2), None));
manipulators_list.push(ManipulatorGroup::new(end, Some(handle2), None));
current_start = Some(end);
// Check if this is the last segment
if index == cubics.len() - 1 {
all_subpaths.push(Subpath::new(groups, true));
groups = Vec::new(); // Reset groups for the next path
all_subpaths.push(Subpath::new(manipulators_list, true));
manipulators_list = Vec::new(); // Reset manipulators for the next path
}
}
}

View File

@@ -390,37 +390,13 @@ impl NodeInput {
}
}
// TODO: Eventually remove this document upgrade code
#[derive(Clone, Debug, DynAny, serde::Serialize, serde::Deserialize)]
/// Represents the implementation of a node, which can be a nested [`NodeNetwork`], a proto [`ProtoNodeIdentifier`], or `Extract`.
pub enum OldDocumentNodeImplementation {
/// This describes a (document) node built out of a subgraph of other (document) nodes.
///
/// A nested [`NodeNetwork`] that is flattened by the [`NodeNetwork::flatten`] function.
Network(OldNodeNetwork),
/// This describes a (document) node implemented as a proto node.
///
/// A proto node identifier which can be found in `node_registry.rs`.
#[serde(alias = "Unresolved")] // TODO: Eventually remove this alias document upgrade code
#[serde(alias = "Unresolved")]
ProtoNode(ProtoNodeIdentifier),
/// The Extract variant is a tag which tells the compilation process to do something special. It invokes language-level functionality built for use by the ExtractNode to enable metaprogramming.
/// When the ExtractNode is compiled, it gets replaced by a value node containing a representation of the source code for the function/lambda of the document node that's fed into the ExtractNode
/// (but only that one document node, not upstream nodes).
///
/// This is explained in more detail here: <https://www.youtube.com/watch?v=72KJa3jQClo>
///
/// Currently we use it for GPU execution, where a node has to get "extracted" to its source code representation and stored as a value that can be given to the GpuCompiler node at runtime
/// (to become a compute shader). Future use could involve the addition of an InjectNode to convert the source code form back into an executable node, enabling metaprogramming in the node graph.
/// We would use an assortment of nodes that operate on Graphene source code (just data, no different from any other data flowing through the graph) to make graph transformations.
///
/// We use this for dealing with macros in a syntactic way of modifying the node graph from within the graph itself. Just like we often deal with lambdas to represent a whole group of
/// operations/code/logic, this allows us to basically deal with a lambda at a meta/source-code level, because we need to pass the GPU SPIR-V compiler the source code for a lambda,
/// not the executable logic of a lambda.
///
/// This is analogous to how Rust macros operate at the level of source code, not executable code. When we speak of source code, that represents Graphene's source code in the form of a
/// DocumentNode network, not the text form of Rust's source code. (Analogous to the token stream/AST of a Rust macro.)
///
/// `DocumentNode`s with a `DocumentNodeImplementation::Extract` are converted into a `ClonedNode` that returns the `DocumentNode` specified by the single `NodeInput::Node`. The referenced node
/// (specified by the single `NodeInput::Node`) is removed from the network, and any `NodeInput::Node`s used by the referenced node are replaced with a generically typed network input.
Extract,
}
@@ -915,6 +891,7 @@ impl NodeNetwork {
warn!("The node which was supposed to be flattened does not exist in the network, id {node_id} network {self:#?}");
return;
};
// If the node is hidden, replace it with an identity node
let identity_node = DocumentNodeImplementation::ProtoNode("graphene_core::ops::IdentityNode".into());
if !node.visible && node.implementation != identity_node {

View File

@@ -187,10 +187,10 @@ tagged_value! {
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::raster::image::migrate_image_frame"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ImageFrame", alias = "RasterData")]
Raster(Table<Raster<CPU>>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::graphic::migrate_group"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GraphicGroup")]
Group(Table<Graphic>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::artboard::migrate_artboard_group"))] // TODO: Eventually remove this migration document upgrade code
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::graphic::migrate_graphic"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GraphicGroup", alias = "Group")]
Graphic(Table<Graphic>),
#[cfg_attr(target_family = "wasm", serde(deserialize_with = "graphene_core::artboard::migrate_artboard"))] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ArtboardGroup")]
Artboard(Table<Artboard>),
// ============

View File

@@ -4,7 +4,7 @@ use graphene_core::vector::PointId;
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
let mut subpaths = Vec::new();
let mut groups = Vec::new();
let mut manipulators_list = Vec::new();
let mut points = path.data().points().iter();
let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64);
@@ -12,36 +12,36 @@ pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
for verb in path.data().verbs() {
match verb {
usvg::tiny_skia_path::PathVerb::Move => {
subpaths.push(Subpath::new(std::mem::take(&mut groups), false));
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false));
let Some(start) = points.next().map(to_vec) else { continue };
groups.push(ManipulatorGroup::new(start, Some(start), Some(start)));
manipulators_list.push(ManipulatorGroup::new(start, Some(start), Some(start)));
}
usvg::tiny_skia_path::PathVerb::Line => {
let Some(end) = points.next().map(to_vec) else { continue };
groups.push(ManipulatorGroup::new(end, Some(end), Some(end)));
manipulators_list.push(ManipulatorGroup::new(end, Some(end), Some(end)));
}
usvg::tiny_skia_path::PathVerb::Quad => {
let Some(handle) = points.next().map(to_vec) else { continue };
let Some(end) = points.next().map(to_vec) else { continue };
if let Some(last) = groups.last_mut() {
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor));
}
groups.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
manipulators_list.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
}
usvg::tiny_skia_path::PathVerb::Cubic => {
let Some(first_handle) = points.next().map(to_vec) else { continue };
let Some(second_handle) = points.next().map(to_vec) else { continue };
let Some(end) = points.next().map(to_vec) else { continue };
if let Some(last) = groups.last_mut() {
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(first_handle);
}
groups.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
manipulators_list.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
}
usvg::tiny_skia_path::PathVerb::Close => {
subpaths.push(Subpath::new(std::mem::take(&mut groups), true));
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
}
}
}
subpaths.push(Subpath::new(groups, false));
subpaths.push(Subpath::new(manipulators_list, false));
subpaths
}

View File

@@ -224,7 +224,7 @@ pub trait Render: BoundingBox + RenderComplexity {
// 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>) {}
/// Recursively iterate over data in the render (including groups upstream from vector data 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 contains_artboard(&self) -> bool {
@@ -365,7 +365,7 @@ impl Render for Table<Graphic> {
}
}
if let Some(group_id) = element_id {
if let Some(element_id) = element_id {
let mut all_upstream_click_targets = Vec::new();
for row in self.iter() {
@@ -379,7 +379,7 @@ impl Render for Table<Graphic> {
all_upstream_click_targets.extend(new_click_targets);
}
metadata.click_targets.insert(group_id, all_upstream_click_targets);
metadata.click_targets.insert(element_id, all_upstream_click_targets);
}
}
@@ -764,9 +764,9 @@ impl Render for Table<Vector> {
metadata.click_targets.entry(element_id).or_insert(click_targets);
}
if let Some(upstream_group) = &vector.upstream_group {
if let Some(upstream_nested_layers) = &vector.upstream_nested_layers {
footprint.transform *= transform;
upstream_group.collect_metadata(metadata, footprint, None);
upstream_nested_layers.collect_metadata(metadata, footprint, None);
}
}
}
@@ -813,6 +813,7 @@ impl Render for Table<Vector> {
impl Render for Artboard {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
// Rectangle for the artboard
if !render_params.hide_artboards {
// Background
render.leaf_tag("rect", |attributes| {
@@ -827,7 +828,7 @@ impl Render for Artboard {
});
}
// Content group (includes the artwork but not the background)
// Artwork
render.parent_tag(
// SVG group tag
"g",
@@ -851,9 +852,9 @@ impl Render for Artboard {
attributes.push("clip-path", selector);
}
},
// Artboard content
// Artwork content
|render| {
self.group.render_svg(render, render_params);
self.content.render_svg(render, render_params);
},
);
}
@@ -875,9 +876,9 @@ impl Render for Artboard {
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
}
// Since the group's transform is right multiplied in when rendering the group, we just need to right multiply by the offset here.
// Since the content's transform is right multiplied in when rendering the content, we just need to right multiply by the artboard offset here.
let child_transform = transform * DAffine2::from_translation(self.location.as_dvec2());
self.group.render_to_vello(scene, child_transform, context, render_params);
self.content.render_to_vello(scene, child_transform, context, render_params);
if self.clip {
scene.pop_layer();
}
@@ -894,7 +895,7 @@ impl Render for Artboard {
}
}
footprint.transform *= self.transform();
self.group.collect_metadata(metadata, footprint, None);
self.content.collect_metadata(metadata, footprint, None);
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
@@ -1140,27 +1141,27 @@ impl Render for Table<Raster<GPU>> {
impl Render for Graphic {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
Graphic::Graphic(graphic) => graphic.render_svg(render, render_params),
Graphic::Vector(vector) => vector.render_svg(render, render_params),
Graphic::RasterCPU(raster) => raster.render_svg(render, render_params),
Graphic::RasterGPU(_) => (),
Graphic::Group(group) => group.render_svg(render, render_params),
}
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
match self {
Graphic::Graphic(graphic) => graphic.render_to_vello(scene, transform, context, render_params),
Graphic::Vector(vector) => vector.render_to_vello(scene, transform, context, render_params),
Graphic::RasterCPU(raster) => raster.render_to_vello(scene, transform, context, render_params),
Graphic::RasterGPU(raster) => raster.render_to_vello(scene, transform, context, render_params),
Graphic::Group(group) => group.render_to_vello(scene, transform, context, render_params),
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
match self {
Graphic::Group(_) => {
Graphic::Graphic(_) => {
metadata.upstream_footprints.insert(element_id, footprint);
}
Graphic::Vector(vector) => {
@@ -1191,26 +1192,26 @@ impl Render for Graphic {
}
match self {
Graphic::Graphic(graphic) => graphic.collect_metadata(metadata, footprint, element_id),
Graphic::Vector(vector) => vector.collect_metadata(metadata, footprint, element_id),
Graphic::RasterCPU(raster) => raster.collect_metadata(metadata, footprint, element_id),
Graphic::RasterGPU(raster) => raster.collect_metadata(metadata, footprint, element_id),
Graphic::Group(group) => group.collect_metadata(metadata, footprint, element_id),
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
match self {
Graphic::Graphic(graphic) => graphic.add_upstream_click_targets(click_targets),
Graphic::Vector(vector) => vector.add_upstream_click_targets(click_targets),
Graphic::RasterCPU(raster) => raster.add_upstream_click_targets(click_targets),
Graphic::RasterGPU(raster) => raster.add_upstream_click_targets(click_targets),
Graphic::Group(group) => group.add_upstream_click_targets(click_targets),
}
}
fn contains_artboard(&self) -> bool {
match self {
Graphic::Graphic(graphic) => graphic.contains_artboard(),
Graphic::Vector(vector) => vector.contains_artboard(),
Graphic::Group(group) => group.contains_artboard(),
Graphic::RasterCPU(raster) => raster.contains_artboard(),
Graphic::RasterGPU(raster) => raster.contains_artboard(),
}
@@ -1218,8 +1219,8 @@ impl Render for Graphic {
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
match self {
Graphic::Graphic(graphic) => graphic.new_ids_from_hash(reference),
Graphic::Vector(vector) => vector.new_ids_from_hash(reference),
Graphic::Group(group) => group.new_ids_from_hash(reference),
Graphic::RasterCPU(_) => (),
Graphic::RasterGPU(_) => (),
}