Instance tables refactor part 5: unwrap GraphicGroup as multi-row Instance<GraphicElement> tables and move up transforms (#2363)

* Just group

* Partly working but without transforms

* Remove Transform/TransformMut from GraphicElement and GraphicGroupTable

* Fix layers and flattening

* Fix transform group handling on the remaining nodes

* Change collect metadata

* Add transform on vector data. TODO: Remove duplicate transform

* Small code tidying-up

* Add concatenate node?

* Remove ignore_modifications which is always false

* Improve transforms

* Mostly fix the nested transform cage angle (except leaf layers and skew)

* WIP attempt to integrate skew

* Fix nesting bounding box

* Avoid setting the transform

* Fix stroke transforms

* Renderer cleanup

* Fix tests for repeated elements not given unique point IDs

* Suppress cargo-deny warning

* Fix upgrade code for graphic group data

* Work around rendering issue in Isometric Fountain

---------

Co-authored-by: Adam <adamgerhant@gmail.com>
Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2025-03-12 01:38:36 -07:00
committed by GitHub
parent d2fc919ba6
commit a696aae044
28 changed files with 856 additions and 930 deletions

View File

@@ -147,6 +147,8 @@ impl Gradient {
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);

View File

@@ -12,6 +12,7 @@ use dyn_any::DynAny;
use core::borrow::Borrow;
use glam::{DAffine2, DVec2};
use std::collections::HashMap;
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
@@ -338,6 +339,48 @@ impl VectorData {
ManipulatorPointId::Anchor(_) => None,
}
}
pub fn concat(&mut self, other: &Self, transform: DAffine2, node_id: u64) {
let point_map = other
.point_domain
.ids()
.iter()
.filter(|id| self.point_domain.ids().contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)))
.collect::<HashMap<_, _>>();
let segment_map = other
.segment_domain
.ids()
.iter()
.filter(|id| self.segment_domain.ids().contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)))
.collect::<HashMap<_, _>>();
let region_map = other
.region_domain
.ids()
.iter()
.filter(|id| self.region_domain.ids().contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)))
.collect::<HashMap<_, _>>();
let id_map = IdMap {
point_offset: self.point_domain.ids().len(),
point_map,
segment_map,
region_map,
};
self.point_domain.concat(&other.point_domain, transform, &id_map);
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
self.region_domain.concat(&other.region_domain, transform, &id_map);
// TODO: properly deal with fills such as gradients
self.style = other.style.clone();
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
}
}
impl Default for VectorData {

View File

@@ -1,6 +1,4 @@
use crate::transform::Transform;
use crate::vector::vector_data::{HandleId, VectorData, VectorDataTable};
use crate::vector::ConcatElement;
use crate::vector::vector_data::{HandleId, VectorData};
use dyn_any::DynAny;
@@ -166,16 +164,16 @@ impl PointDomain {
self.id.iter().position(|&check_id| check_id == id)
}
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
pub fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
self.id.extend(other.id.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
self.position.extend(other.position.iter().map(|&pos| transform.transform_point2(pos)));
}
fn map_ids(&mut self, id_map: &IdMap) {
pub fn map_ids(&mut self, id_map: &IdMap) {
self.id.iter_mut().for_each(|id| *id = *id_map.point_map.get(id).unwrap_or(id));
}
fn transform(&mut self, transform: DAffine2) {
pub fn transform(&mut self, transform: DAffine2) {
for pos in &mut self.position {
*pos = transform.transform_point2(*pos);
}
@@ -364,7 +362,7 @@ impl SegmentDomain {
}
}
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
pub fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
self.id.extend(other.id.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id)));
self.start_point.extend(other.start_point.iter().map(|&index| id_map.point_offset + index));
self.end_point.extend(other.end_point.iter().map(|&index| id_map.point_offset + index));
@@ -372,11 +370,11 @@ impl SegmentDomain {
self.stroke.extend(&other.stroke);
}
fn map_ids(&mut self, id_map: &IdMap) {
pub fn map_ids(&mut self, id_map: &IdMap) {
self.id.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
}
fn transform(&mut self, transform: DAffine2) {
pub fn transform(&mut self, transform: DAffine2) {
for handles in &mut self.handles {
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
}
@@ -474,7 +472,7 @@ impl RegionDomain {
&self.fill
}
fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
pub fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
self.id.extend(other.id.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
self.segment_range.extend(
other
@@ -485,7 +483,7 @@ impl RegionDomain {
self.fill.extend(&other.fill);
}
fn map_ids(&mut self, id_map: &IdMap) {
pub fn map_ids(&mut self, id_map: &IdMap) {
self.id.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
self.segment_range
.iter_mut()
@@ -764,57 +762,10 @@ impl bezier_rs::Identifier for PointId {
}
}
impl ConcatElement for VectorData {
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
let new_ids = other
.point_domain
.id
.iter()
.filter(|id| self.point_domain.id.contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)));
let point_map = new_ids.collect::<HashMap<_, _>>();
let new_ids = other
.segment_domain
.id
.iter()
.filter(|id| self.segment_domain.id.contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)));
let segment_map = new_ids.collect::<HashMap<_, _>>();
let new_ids = other
.region_domain
.id
.iter()
.filter(|id| self.region_domain.id.contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)));
let region_map = new_ids.collect::<HashMap<_, _>>();
let id_map = IdMap {
point_offset: self.point_domain.ids().len(),
point_map,
segment_map,
region_map,
};
self.point_domain.concat(&other.point_domain, transform, &id_map);
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
self.region_domain.concat(&other.region_domain, transform, &id_map);
// TODO: properly deal with fills such as gradients
self.style = other.style.clone();
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
}
}
impl ConcatElement for VectorDataTable {
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
for (instance, other_instance) in self.instances_mut().zip(other.instances()) {
*instance.alpha_blending = *other_instance.alpha_blending;
instance.instance.concat(other_instance.instance, transform * other_instance.transform(), node_id);
}
}
}
/// Represents the conversion of ids used when concatenating vector data with conflicting ids.
struct IdMap {
point_offset: usize,
point_map: HashMap<PointId, PointId>,
segment_map: HashMap<SegmentId, SegmentId>,
region_map: HashMap<RegionId, RegionId>,
pub struct IdMap {
pub point_offset: usize,
pub point_map: HashMap<PointId, PointId>,
pub segment_map: HashMap<SegmentId, SegmentId>,
pub region_map: HashMap<RegionId, RegionId>,
}

View File

@@ -1,7 +1,7 @@
use super::misc::CentroidType;
use super::style::{Fill, Gradient, GradientStops, Stroke};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
use crate::instances::InstanceMut;
use crate::instances::{InstanceMut, Instances};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
@@ -15,55 +15,61 @@ use rand::{Rng, SeedableRng};
/// Implemented for types that can be converted to an iterator of vector data.
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
trait VectorIterMut {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)>;
trait VectorDataTableIterMut {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>>;
}
impl VectorIterMut for GraphicGroupTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
let parent_transform = self.transform();
let instance = self.one_instance_mut().instance;
impl VectorDataTableIterMut for GraphicGroupTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
// Grab only the direct children
instance.iter_mut().filter_map(|(element, _)| element.as_vector_data_mut()).map(move |vector_data| {
let transform = parent_transform * vector_data.transform();
let vector_data_instance = vector_data.one_instance_mut().instance;
(vector_data_instance, transform)
})
self.instances_mut()
.filter_map(|element| element.instance.as_vector_data_mut())
.flat_map(move |vector_data| vector_data.instances_mut())
}
}
impl VectorIterMut for VectorDataTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
self.instances_mut().map(|instance| {
let transform = instance.transform();
(instance.instance, transform)
})
impl VectorDataTableIterMut for VectorDataTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
self.instances_mut()
}
}
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T: VectorIterMut>(
async fn assign_colors<T>(
_: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable)]
#[widget(ParsedWidgetOverride::Hidden)]
/// The vector elements, or group of vector elements, to apply the fill and/or stroke style to.
mut vector_group: T,
#[default(true)] fill: bool,
#[default(true)]
/// Whether to style the fill.
fill: bool,
/// Whether to style the stroke.
stroke: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")] gradient: GradientStops,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
/// The range of colors to select from.
gradient: GradientStops,
/// Whether to reverse the gradient.
reverse: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")] randomize: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] seed: SeedValue,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32,
) -> T {
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")]
/// Whether to randomize the color selection for each element from throughout the gradient.
randomize: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")]
/// The seed used for randomization.
seed: SeedValue,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
repeat_every: u32,
) -> T
where
T: VectorDataTableIterMut + 'n + Send,
{
let length = vector_group.vector_iter_mut().count();
let gradient = if reverse { gradient.reversed() } else { gradient };
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
for (i, (vector_data, _)) in vector_group.vector_iter_mut().enumerate() {
for (i, vector_data) in vector_group.vector_iter_mut().enumerate() {
let factor = match randomize {
true => rng.random::<f64>(),
false => match repeat_every {
@@ -76,11 +82,11 @@ async fn assign_colors<T: VectorIterMut>(
let color = gradient.evalute(factor);
if fill {
vector_data.style.set_fill(Fill::Solid(color));
vector_data.instance.style.set_fill(Fill::Solid(color));
}
if stroke {
if let Some(stroke) = vector_data.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
vector_data.style.set_stroke(stroke);
if let Some(stroke) = vector_data.instance.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
vector_data.instance.style.set_stroke(stroke);
}
}
}
@@ -89,7 +95,7 @@ async fn assign_colors<T: VectorIterMut>(
}
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
async fn fill<F: Into<Fill> + 'n + Send, V>(
_: impl Ctx,
#[implementations(
VectorDataTable,
@@ -101,7 +107,8 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
GraphicGroupTable,
GraphicGroupTable
)]
mut vector_data: TargetTy,
/// The vector elements, or group of vector elements, to apply the fill to.
mut vector_data: V,
#[implementations(
Fill,
Option<Color>,
@@ -113,22 +120,33 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
Gradient,
)]
#[default(Color::BLACK)]
fill: FillTy,
/// The fill to paint the path with.
fill: F,
_backup_color: Option<Color>,
_backup_gradient: Gradient,
) -> TargetTy {
) -> V
where
V: VectorDataTableIterMut + 'n + Send,
{
let fill: Fill = fill.into();
for (target, _transform) in vector_data.vector_iter_mut() {
target.style.set_fill(fill.clone());
for vector in vector_data.vector_iter_mut() {
let mut fill = fill.clone();
if let Fill::Gradient(gradient) = &mut fill {
gradient.transform *= *vector.transform;
}
vector.instance.style.set_fill(fill);
}
vector_data
}
/// Applies a stroke style to the vector data contained in the input.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
_: impl Ctx,
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)] mut vector_data: TargetTy,
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)]
/// The vector elements, or group of vector elements, to apply the stroke to.
mut vector_data: Instances<V>,
#[implementations(
Option<Color>,
Color,
@@ -136,14 +154,26 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
Color,
)]
#[default(Color::BLACK)]
color: ColorTy,
#[default(2.)] weight: f64,
/// The stroke color.
color: C,
#[default(2.)]
/// The stroke weight.
weight: f64,
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
dash_lengths: Vec<f64>,
/// The offset distance from the starting point of the dash pattern.
dash_offset: f64,
/// The shape of the stroke at open endpoints.
line_cap: crate::vector::style::LineCap,
/// The curvature of the bent stroke at sharp corners.
line_join: LineJoin,
#[default(4.)] miter_limit: f64,
) -> TargetTy {
#[default(4.)]
/// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio.
miter_limit: f64,
) -> Instances<V>
where
Instances<V>: VectorDataTableIterMut + 'n + Send,
{
let stroke = Stroke {
color: color.into(),
weight,
@@ -154,110 +184,111 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
line_join_miter_limit: miter_limit,
transform: DAffine2::IDENTITY,
};
for (target, transform) in vector_data.vector_iter_mut() {
target.style.set_stroke(Stroke { transform, ..stroke.clone() });
for vector in vector_data.vector_iter_mut() {
let mut stroke = stroke.clone();
stroke.transform *= *vector.transform;
vector.instance.style.set_stroke(stroke);
}
vector_data
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
async fn repeat<I: 'n + Send>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
#[default(100., 100.)]
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
direction: DVec2,
angle: Angle,
#[default(4)] instances: IntegerCount,
) -> GraphicGroupTable {
let first_vector_transform = instance.transform();
) -> GraphicGroupTable
where
Instances<I>: GraphicElementRendered,
{
let angle = angle.to_radians();
let instances = instances.max(1);
let total = (instances - 1) as f64;
let mut result_table = GraphicGroupTable::default();
let result = result_table.one_instance_mut().instance;
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
let center = (bounding_box[0] + bounding_box[1]) / 2.;
for i in 0..instances {
let translation = i as f64 * direction / total;
let angle = i as f64 * angle / total;
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
new_instance.new_ids_from_hash(None);
for index in 0..instances {
let angle = index as f64 * angle / total;
let translation = index as f64 * direction / total;
let modification = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
let data_transform = new_instance.transform_mut();
*data_transform = modification * first_vector_transform;
result.push((new_instance, None));
let mut new_graphic_element = instance.to_graphic_element().clone();
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
let new_instance = result_table.push(new_graphic_element);
*new_instance.transform = modification;
}
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn circular_repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
async fn circular_repeat<I: 'n + Send>(
_: impl Ctx,
// TODO: Implement other GraphicElementRendered types.
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
angle_offset: Angle,
#[default(5)] radius: f64,
#[default(5)] instances: IntegerCount,
) -> GraphicGroupTable {
let first_vector_transform = instance.transform();
) -> GraphicGroupTable
where
Instances<I>: GraphicElementRendered,
{
let instances = instances.max(1);
let mut result_table = GraphicGroupTable::default();
let result = result_table.one_instance_mut().instance;
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
let center = (bounding_box[0] + bounding_box[1]) / 2.;
let base_transform = DVec2::new(0., radius) - center;
for i in 0..instances {
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians();
let rotation = DAffine2::from_angle(angle);
for index in 0..instances {
let rotation = DAffine2::from_angle((std::f64::consts::TAU / instances as f64) * index as f64 + angle_offset.to_radians());
let modification = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
new_instance.new_ids_from_hash(None);
let data_transform = new_instance.transform_mut();
*data_transform = modification * first_vector_transform;
result.push((new_instance, None));
let mut new_graphic_element = instance.to_graphic_element().clone();
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
let new_instance = result_table.push(new_graphic_element);
*new_instance.transform = modification;
}
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
async fn copy_to_points<I: 'n + Send>(
_: impl Ctx,
points: VectorDataTable,
#[expose]
#[implementations(VectorDataTable, GraphicGroupTable)]
instance: I,
instance: Instances<I>,
#[default(1)] random_scale_min: f64,
#[default(1)] random_scale_max: f64,
random_scale_bias: f64,
random_scale_seed: SeedValue,
random_rotation: Angle,
random_rotation_seed: SeedValue,
) -> GraphicGroupTable {
) -> GraphicGroupTable
where
Instances<I>: GraphicElementRendered,
{
let points_transform = points.transform();
let points = points.one_instance().instance;
let instance_transform = instance.transform();
let points_list = points.instances().flat_map(|element| element.instance.point_domain.positions());
let random_scale_difference = random_scale_max - random_scale_min;
let points_list = points.point_domain.positions();
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
@@ -268,9 +299,8 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
let do_rotation = random_rotation.abs() > 1e-6;
let mut result_table = GraphicGroupTable::default();
let result = result_table.one_instance_mut().instance;
for &point in points_list {
for (index, &point) in points_list.into_iter().enumerate() {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points_transform.transform_point2(point);
@@ -296,12 +326,11 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
random_scale_min
};
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
new_instance.new_ids_from_hash(None);
let mut new_graphic_element = instance.to_graphic_element().clone();
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
let data_transform = new_instance.transform_mut();
*data_transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform * instance_transform;
result.push((new_instance, None));
let new_instance = result_table.push(new_graphic_element);
*new_instance.transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform;
}
result_table
@@ -406,53 +435,40 @@ async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupT
// A node based solution to support passing through vector data could be a network node with a cache node connected to
// a flatten vector elements connected to an if else node, another connection from the cache directly
// To the if else node, and another connection from the cache to a matches type node connected to the if else node.
fn concat_group(graphic_group_table: &GraphicGroupTable, current_transform: DAffine2, result: &mut InstanceMut<VectorData>) {
for (element, reference) in graphic_group_table.one_instance().instance.iter() {
match element {
GraphicElement::VectorData(vector_data) => {
for instance in vector_data.instances() {
*result.alpha_blending = *instance.alpha_blending;
result
.instance
.concat(instance.instance, current_transform * instance.transform(), reference.map(|node_id| node_id.0).unwrap_or_default());
fn flatten_group(graphic_group_table: &GraphicGroupTable, output: &mut InstanceMut<VectorData>) {
for current_element in graphic_group_table.instances() {
match current_element.instance {
GraphicElement::VectorData(vector_data_table) => {
// Loop through every row of the VectorDataTable and concatenate each instance's subpath into the output VectorData instance.
for vector_data_instance in vector_data_table.instances() {
let other = vector_data_instance.instance;
let transform = *current_element.transform * *vector_data_instance.transform;
let node_id = current_element.source_node_id.map(|node_id| node_id.0).unwrap_or_default();
output.instance.concat(other, transform, node_id);
// Use the last encountered style as the output style
output.instance.style = vector_data_instance.instance.style.clone();
}
}
GraphicElement::GraphicGroup(graphic_group) => {
concat_group(graphic_group, current_transform * graphic_group.transform(), result);
let mut graphic_group = graphic_group.clone();
for instance in graphic_group.instances_mut() {
*instance.transform = *current_element.transform * *instance.transform;
}
flatten_group(&graphic_group, output);
}
_ => {}
}
}
}
let mut result_table = VectorDataTable::default();
let mut result_instance = result_table.one_instance_mut();
// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
result_instance.instance.style.set_stroke_transform(DAffine2::IDENTITY);
concat_group(&graphic_group_input, DAffine2::IDENTITY, &mut result_instance);
let mut output_table = VectorDataTable::default();
let Some(mut output) = output_table.instances_mut().next() else { return output_table };
result_table
}
flatten_group(&graphic_group_input, &mut output);
pub trait ConcatElement {
fn concat(&mut self, other: &Self, transform: DAffine2, node_id: u64);
}
impl ConcatElement for GraphicGroupTable {
fn concat(&mut self, other: &Self, transform: DAffine2, _node_id: u64) {
let other_transform = other.transform();
let self_group = self.one_instance_mut().instance;
let other_group = other.one_instance().instance;
// TODO: Decide if we want to keep this behavior whereby the layers are flattened
for (mut element, footprint_mapping) in other_group.iter().cloned() {
*element.transform_mut() = transform * element.transform() * other_transform;
self_group.push((element, footprint_mapping));
}
*self.one_instance_mut().alpha_blending = *other.one_instance().alpha_blending;
}
output_table
}
#[node_macro::node(category(""), path(graphene_core::vector))]
@@ -1077,7 +1093,7 @@ mod test {
let instances = 3;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = vector_data.one_instance().instance;
let vector_data = vector_data.instances().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
@@ -1089,29 +1105,32 @@ mod test {
let instances = 8;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = vector_data.one_instance().instance;
let vector_data = vector_data.instances().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn circle_repeat() {
async fn circular_repeat() {
let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = vector_data.one_instance().instance;
let vector_data = vector_data.instances().next().unwrap().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5);
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
#[tokio::test]
async fn bounding_box() {
let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await;
let bounding_box = bounding_box.one_instance().instance;
let bounding_box = bounding_box.instances().next().unwrap().instance;
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
@@ -1119,13 +1138,13 @@ mod test {
// Test a VectorData with non-zero rotation
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
let mut square = VectorDataTable::new(square);
*square.one_instance_mut().transform_mut() *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
*square.one_instance_mut().transform *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
let bounding_box = BoundingBoxNode {
vector_data: FutureWrapperNode(square),
}
.eval(Footprint::default())
.await;
let bounding_box = bounding_box.one_instance().instance;
let bounding_box = bounding_box.instances().next().unwrap().instance;
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
let sqrt2 = core::f64::consts::SQRT_2;
@@ -1136,15 +1155,19 @@ mod test {
async fn copy_to_points() {
let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flattened_copy_to_points.one_instance().instance;
let flatten_vector_elements = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flatten_vector_elements.instances().next().unwrap().instance;
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
let offset = expected_points[index];
assert_eq!(
&subpath.anchors()[..4],
&subpath.anchors(),
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]
);
}
@@ -1153,7 +1176,7 @@ mod test {
async fn sample_points() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
let sample_points = sample_points.one_instance().instance;
let sample_points = sample_points.instances().next().unwrap().instance;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
@@ -1163,7 +1186,7 @@ mod test {
async fn adaptive_spacing() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
let sample_points = sample_points.one_instance().instance;
let sample_points = sample_points.instances().next().unwrap().instance;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
@@ -1178,7 +1201,7 @@ mod test {
0,
)
.await;
let sample_points = sample_points.one_instance().instance;
let sample_points = sample_points.instances().next().unwrap().instance;
assert!(
(20..=40).contains(&sample_points.point_domain.positions().len()),
"actual len {}",
@@ -1197,7 +1220,7 @@ mod test {
#[tokio::test]
async fn spline() {
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
let spline = spline.one_instance().instance;
let spline = spline.instances().next().unwrap().instance;
assert_eq!(spline.stroke_bezier_paths().count(), 1);
assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]);
}
@@ -1206,7 +1229,7 @@ mod test {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5, 0).await;
let sample_points = sample_points.one_instance().instance;
let sample_points = sample_points.instances().next().unwrap().instance;
assert_eq!(
&sample_points.point_domain.positions()[..4],
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
@@ -1229,7 +1252,7 @@ mod test {
async fn bevel_rect() {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
let beveled = beveled.one_instance().instance;
let beveled = beveled.instances().next().unwrap().instance;
assert_eq!(beveled.point_domain.positions().len(), 8);
assert_eq!(beveled.segment_domain.ids().len(), 8);
@@ -1252,7 +1275,7 @@ mod test {
let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.);
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), curve], false);
let beveled = super::bevel((), vector_node(source), 5.);
let beveled = beveled.one_instance().instance;
let beveled = beveled.instances().next().unwrap().instance;
assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3);
@@ -1273,11 +1296,10 @@ mod test {
let vector_data = VectorData::from_subpath(source);
let mut vector_data_table = VectorDataTable::new(vector_data.clone());
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
*vector_data_table.one_instance_mut().transform_mut() = transform;
*vector_data_table.one_instance_mut().transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.);
let beveled = beveled.one_instance().instance;
let beveled = beveled.instances().next().unwrap().instance;
assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3);
@@ -1295,7 +1317,7 @@ mod test {
async fn bevel_too_high() {
let source = Subpath::from_anchors([DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)], false);
let beveled = super::bevel(Footprint::default(), vector_node(source), 999.);
let beveled = beveled.one_instance().instance;
let beveled = beveled.instances().next().unwrap().instance;
assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5);
@@ -1316,7 +1338,7 @@ mod test {
let point = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO, DVec2::ZERO);
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), point, curve], false);
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
let beveled = beveled.one_instance().instance;
let beveled = beveled.instances().next().unwrap().instance;
assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5);