Make Copy to Points and (Circular) Repeat and nodes output group data, and add flattening nodes (#2011)

* Output group from repeat, add flatten vector elements

* Fix tests

* Fix demo artwork

* Output group from copy to points, add repeat for graphic groups, fix editor freeze on render fail

* Restore painted dreams

* WIP: Fix demo artwork

* Fix demo artwork, add ungroup node

* Incorrect scaling

* fix test

* Fix demo art

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant 2024-10-14 12:39:28 -07:00 committed by GitHub
parent b028bbb8cc
commit e09f5ecaec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 346 additions and 158 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -239,18 +239,28 @@ impl<'a> ModifyInputsContext<'a> {
// Gets the node id of a node with a specific reference that is upstream from the layer node, and creates it if it does not exist
pub fn existing_node_id(&mut self, reference: &'static str) -> Option<NodeId> {
// Start from the layer node or export
let node_id = self.get_output_layer().map_or(Vec::new(), |output_layer| vec![output_layer.to_node()]);
let output_layer = self.get_output_layer()?;
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type();
let upstream = self.network_interface.upstream_flow_back_from_nodes(node_id, &[], network_interface::FlowType::HorizontalFlow);
let is_traversal_start = |node_id: NodeId| {
self.layer_node.map(|layer| layer.to_node()) == Some(node_id) || self.network_interface.network(&[]).unwrap().exports.iter().any(|export| export.as_node() == Some(node_id))
};
let upstream = self
.network_interface
.upstream_flow_back_from_nodes(vec![output_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow);
// Take until another layer node is found (but not the first layer node)
let existing_node_id = upstream
.take_while(|node_id| is_traversal_start(*node_id) || !self.network_interface.is_layer(node_id, &[]))
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|node_reference| node_reference == reference));
let mut existing_node_id = None;
for upstream_node in upstream.collect::<Vec<_>>() {
let upstream_node_input_type = self.network_interface.input_type(&InputConnector::node(upstream_node, 0), &[]).0.nested_type();
let is_traversal_start = |node_id: NodeId| {
self.layer_node.map(|layer| layer.to_node()) == Some(node_id) || self.network_interface.network(&[]).unwrap().exports.iter().any(|export| export.as_node() == Some(node_id))
};
if !is_traversal_start(upstream_node) && (self.network_interface.is_layer(&upstream_node, &[]) || upstream_node_input_type != layer_input_type) {
break;
}
if self.network_interface.reference(&upstream_node, &[]).is_some_and(|node_reference| node_reference == reference) {
existing_node_id = Some(upstream_node);
break;
}
}
// Create a new node if the node does not exist and update its inputs
existing_node_id.or_else(|| {

View File

@ -752,8 +752,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
return;
};
responses.add(DocumentMessage::EndTransaction);
if let Some(preview_node) = self.preview_on_mouse_up {
responses.add(NodeGraphMessage::TogglePreview { node_id: preview_node });
self.preview_on_mouse_up = None;
@ -992,6 +990,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
self.box_selection_start = None;
self.wire_in_progress_from_connector = None;
self.wire_in_progress_to_connector = None;
responses.add(DocumentMessage::EndTransaction);
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None });
responses.add(FrontendMessage::UpdateBox { box_selection: None })
}

View File

@ -3603,7 +3603,7 @@ impl NodeNetworkInterface {
};
// Check whether the being-deleted node's first (primary) input is a node
let mut reconnect_to_input = network.nodes.get(node_id).and_then(|node| {
let reconnect_to_input = network.nodes.get(node_id).and_then(|node| {
node.inputs
.iter()
.find(|input| input.is_exposed_to_frontend(network_path.is_empty()))
@ -3625,15 +3625,22 @@ impl NodeNetworkInterface {
let mut reconnect_node = None;
// Don't reconnect if the upstream node is a layer and there are multiple downstream inputs to reconnect
let multiple_disconnect_and_upstream_is_layer = downstream_inputs_to_disconnect.len() > 1
&& reconnect_to_input
.as_ref()
.is_some_and(|upstream_input| upstream_input.as_node().map_or(false, |node_id| self.is_layer(&node_id, network_path)));
for downstream_input in &downstream_inputs_to_disconnect {
self.disconnect_input(downstream_input, network_path);
// Prevent reconnecting export to import until https://github.com/GraphiteEditor/Graphite/issues/1762 is solved
if !(matches!(reconnect_to_input, Some(NodeInput::Network { .. })) && matches!(downstream_input, InputConnector::Export(_))) {
if let Some(reconnect_input) = reconnect_to_input.take() {
// Get the reconnect node position only if it is in a stack
if let Some(reconnect_input) = &reconnect_to_input {
reconnect_node = reconnect_input.as_node().and_then(|node_id| if self.is_stack(&node_id, network_path) { Some(node_id) } else { None });
self.disconnect_input(&InputConnector::node(*node_id, 0), network_path);
self.set_input(downstream_input, reconnect_input, network_path);
if !multiple_disconnect_and_upstream_is_layer {
self.set_input(downstream_input, reconnect_input.clone(), network_path);
}
}
}
}

View File

@ -963,6 +963,7 @@ impl PortfolioMessageHandler {
/text>"#
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
.to_string();
responses.add(Message::EndBuffer(graphene_std::renderer::RenderMetadata::default()));
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
}
result

View File

@ -15,7 +15,7 @@ use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_core::text::FontCache;
use graphene_core::transform::Footprint;
use graphene_core::vector::style::ViewMode;
use graphene_std::renderer::format_transform_matrix;
use graphene_std::renderer::{format_transform_matrix, RenderMetadata};
use graphene_std::vector::VectorData;
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
@ -630,6 +630,7 @@ impl NodeGraphExecutor {
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
let mut render_output_metadata = RenderMetadata::default();
match node_graph_output {
TaggedValue::RenderOutput(render_output) => {
match render_output.data {
@ -651,10 +652,7 @@ impl NodeGraphExecutor {
}
}
responses.add(Message::EndBuffer(render_output.metadata));
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw);
render_output_metadata = render_output.metadata;
}
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),
@ -669,6 +667,10 @@ impl NodeGraphExecutor {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
}
};
responses.add(Message::EndBuffer(render_output_metadata));
responses.add(DocumentMessage::RenderScrollbars);
responses.add(DocumentMessage::RenderRulers);
responses.add(OverlaysMessage::Draw);
Ok(())
}
}

View File

@ -315,6 +315,48 @@ async fn to_group<F: 'n + Send, Data: Into<GraphicGroup> + 'n>(
element.eval(footprint).await.into()
}
#[node_macro::node(category("General"))]
async fn flatten_group<F: 'n + Send>(
#[implementations(
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> GraphicGroup,
Footprint -> GraphicGroup,
)]
group: impl Node<F, Output = GraphicGroup>,
fully_flatten: bool,
) -> GraphicGroup {
let nested_group = group.eval(footprint).await;
let mut flat_group = GraphicGroup::EMPTY;
fn flatten_group(result_group: &mut GraphicGroup, current_group: GraphicGroup, fully_flatten: bool) {
let mut collection_group = GraphicGroup::EMPTY;
for (element, reference) in current_group.elements {
if let GraphicElement::GraphicGroup(mut nested_group) = element {
nested_group.transform *= current_group.transform;
let mut sub_group = GraphicGroup::EMPTY;
if fully_flatten {
flatten_group(&mut sub_group, nested_group, fully_flatten);
} else {
for (collection_element, _) in &mut nested_group.elements {
*collection_element.transform_mut() = nested_group.transform * collection_element.transform();
}
sub_group = nested_group;
}
collection_group.append(&mut sub_group.elements);
} else {
collection_group.push((element, reference));
}
}
result_group.append(&mut collection_group.elements);
}
flatten_group(&mut flat_group, nested_group, fully_flatten);
flat_group
}
#[node_macro::node(category(""))]
async fn to_artboard<F: 'n + Send + ApplyTransform, Data: Into<GraphicGroup> + 'n>(
#[implementations(

View File

@ -266,7 +266,7 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
// TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes.
// This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets.
#[derive(Debug, Clone, PartialEq, DynAny)]
#[derive(Debug, Default, Clone, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderMetadata {
pub footprints: HashMap<NodeId, (Footprint, DAffine2)>,
@ -301,6 +301,12 @@ pub trait GraphicElementRendered {
fn contains_artboard(&self) -> bool {
false
}
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {}
fn to_graphic_element(&self) -> GraphicElement {
GraphicElement::default()
}
}
impl GraphicElementRendered for GraphicGroup {
@ -393,6 +399,16 @@ impl GraphicElementRendered for GraphicGroup {
fn contains_artboard(&self) -> bool {
self.iter().any(|(element, _)| element.contains_artboard())
}
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
for (element, node_id) in self.elements.iter_mut() {
element.new_ids_from_hash(*node_id);
}
}
fn to_graphic_element(&self) -> GraphicElement {
GraphicElement::GraphicGroup(self.clone())
}
}
impl GraphicElementRendered for VectorData {
@ -582,6 +598,14 @@ impl GraphicElementRendered for VectorData {
scene.pop_layer();
}
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
self.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default());
}
fn to_graphic_element(&self) -> GraphicElement {
GraphicElement::VectorData(Box::new(self.clone()))
}
}
impl GraphicElementRendered for Artboard {
@ -948,6 +972,14 @@ impl GraphicElementRendered for GraphicElement {
GraphicElement::Raster(raster) => raster.contains_artboard(),
}
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
match self {
GraphicElement::VectorData(vector_data) => vector_data.new_ids_from_hash(reference),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.new_ids_from_hash(reference),
GraphicElement::Raster(_) => (),
}
}
}
/// Used to stop rust complaining about upstream traits adding display implementations to `Option<Color>`. This would not be an issue as we control that crate.

View File

@ -114,7 +114,6 @@ fn spline<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _primar
}
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
#[node_macro::node(category(""))]
fn path<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> super::VectorData {
let mut vector_data = super::VectorData::from_subpaths(path_data, false);

View File

@ -4,6 +4,7 @@ use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
/// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids).
macro_rules! create_ids {
@ -22,6 +23,14 @@ macro_rules! create_ids {
Self(crate::uuid::generate_uuid())
}
pub fn generate_from_hash(self, node_id: u64) -> Self {
let mut hasher = std::hash::DefaultHasher::new();
node_id.hash(&mut hasher);
self.hash(&mut hasher);
let hash_value = hasher.finish();
Self(hash_value)
}
/// Gets the inner raw value.
pub fn inner(self) -> u64 {
self.0
@ -161,6 +170,10 @@ impl PointDomain {
self.positions.extend(other.positions.iter().map(|&pos| transform.transform_point2(pos)));
}
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) {
for pos in &mut self.positions {
*pos = transform.transform_point2(*pos);
@ -353,6 +366,10 @@ impl SegmentDomain {
self.stroke.extend(&other.stroke);
}
fn map_ids(&mut self, id_map: &IdMap) {
self.ids.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
}
fn transform(&mut self, transform: DAffine2) {
for handles in &mut self.handles {
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
@ -460,6 +477,13 @@ impl RegionDomain {
);
self.fill.extend(&other.fill);
}
fn map_ids(&mut self, id_map: &IdMap) {
self.ids.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
self.segment_range
.iter_mut()
.for_each(|range| *range = *id_map.segment_map.get(range.start()).unwrap_or(range.start())..=*id_map.segment_map.get(range.end()).unwrap_or(range.end()));
}
}
impl super::VectorData {
@ -590,6 +614,23 @@ impl super::VectorData {
self.point_domain.transform(transform);
self.segment_domain.transform(transform);
}
pub fn vector_new_ids_from_hash(&mut self, node_id: u64) {
let point_map = self.point_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::<HashMap<_, _>>();
let segment_map = self.segment_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::<HashMap<_, _>>();
let region_map = self.region_domain.ids().iter().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.map_ids(&id_map);
self.segment_domain.map_ids(&id_map);
self.region_domain.map_ids(&id_map);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]

View File

@ -5,7 +5,7 @@ use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
use crate::vector::style::LineJoin;
use crate::{Color, GraphicGroup};
use crate::{Color, GraphicElement, GraphicGroup};
use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
use glam::{DAffine2, DVec2};
@ -164,95 +164,188 @@ async fn stroke<F: 'n + Send, T: Into<Option<Color>> + 'n + Send>(
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn repeat<F: 'n + Send>(
async fn repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
#[implementations(
(),
(),
Footprint,
Footprint,
)]
footprint: F,
// TODO: Implement other GraphicElementRendered types.
#[implementations(
() -> VectorData,
() -> GraphicGroup,
Footprint -> VectorData,
Footprint -> GraphicGroup,
)]
instance: impl Node<F, Output = VectorData>,
instance: impl Node<F, Output = 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,
) -> VectorData {
) -> GraphicGroup {
let instance = instance.eval(footprint).await;
let first_vector_transform = instance.transform();
let angle = angle.to_radians();
let instances = instances.max(1);
let total = (instances - 1) as f64;
if instances == 1 {
return instance;
}
let mut result = GraphicGroup::EMPTY;
// Repeat the vector data
let mut result = VectorData::empty();
let Some(bounding_box) = instance.bounding_box_with_transform(instance.transform) else {
return instance;
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else {
return result;
};
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);
let modification = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
let transform = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
result.concat(&instance, transform);
let data_transform = new_instance.transform_mut();
*data_transform = modification * first_vector_transform;
result.push((new_instance, None));
}
result.style.set_stroke_transform(DAffine2::IDENTITY);
result
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn circular_repeat<F: 'n + Send>(
async fn circular_repeat<F: 'n + Send + Copy, I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
#[implementations(
(),
(),
Footprint,
Footprint,
)]
footprint: F,
// TODO: Implement other GraphicElementRendered types.
#[implementations(
() -> VectorData,
() -> GraphicGroup,
Footprint -> VectorData,
Footprint -> GraphicGroup,
)]
instance: impl Node<F, Output = I>,
angle_offset: Angle,
#[default(5)] radius: Length,
#[default(5)] instances: IntegerCount,
) -> GraphicGroup {
let instance = instance.eval(footprint).await;
let first_vector_transform = instance.transform();
let instances = instances.max(1);
let mut result = GraphicGroup::EMPTY;
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else {
return result;
};
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);
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));
}
result
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + ConcatElement + TransformMut + Send + 'n>(
#[implementations(
(),
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> VectorData,
() -> VectorData,
Footprint -> VectorData,
)]
instance: impl Node<F, Output = VectorData>,
angle_offset: Angle,
#[default(5)] radius: Length,
#[default(5)] instances: IntegerCount,
) -> VectorData {
points: impl Node<F, Output = VectorData>,
#[expose]
#[implementations(
() -> VectorData,
() -> GraphicGroup,
Footprint -> VectorData,
Footprint -> GraphicGroup,
)]
instance: impl Node<F, Output = 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,
) -> GraphicGroup {
let points = points.eval(footprint).await;
let instance = instance.eval(footprint).await;
let instances = instances.max(1);
let instance_transform = instance.transform();
if instances == 1 {
return instance;
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]);
let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into());
let mut rotation_rng = rand::rngs::StdRng::seed_from_u64(random_rotation_seed.into());
let do_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.abs() > 1e-6;
let mut result = GraphicGroup::default();
for &point in points_list {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points.transform.transform_point2(point);
let rotation = if do_rotation {
let degrees = (rotation_rng.gen::<f64>() - 0.5) * random_rotation;
degrees / 360. * std::f64::consts::TAU
} else {
0.
};
let scale = if do_scale {
if random_scale_bias.abs() < 1e-6 {
// Linear
random_scale_min + scale_rng.gen::<f64>() * random_scale_difference
} else {
// Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias);
let scale_factor = (1. - scale_rng.gen::<f64>() * horizontal_scale_factor).log2() / random_scale_bias;
random_scale_min + scale_factor * random_scale_difference
}
} else {
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 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 mut result = VectorData::empty();
let Some(bounding_box) = instance.bounding_box_with_transform(instance.transform) else {
return instance;
};
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);
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
result.concat(&instance, transform);
}
result.style.set_stroke_transform(DAffine2::IDENTITY);
result
}
@ -381,6 +474,42 @@ async fn solidify_stroke<F: 'n + Send>(
result
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_vector_elements<F: 'n + Send>(
#[implementations(
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> GraphicGroup,
Footprint -> GraphicGroup,
)]
graphic_group_input: impl Node<F, Output = GraphicGroup>,
) -> VectorData {
let graphic_group = graphic_group_input.eval(footprint).await;
fn concat_group(graphic_group: &GraphicGroup, current_transform: DAffine2, result: &mut VectorData) {
for (element, _) in graphic_group.iter() {
match element {
GraphicElement::VectorData(vector_data) => {
result.concat(vector_data, current_transform);
}
GraphicElement::GraphicGroup(graphic_group) => {
concat_group(graphic_group, current_transform * graphic_group.transform, result);
}
_ => {}
}
}
}
let mut result = VectorData::empty();
concat_group(&graphic_group, DAffine2::IDENTITY, &mut result);
// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
result.style.set_stroke_transform(DAffine2::IDENTITY);
result
}
pub trait ConcatElement {
fn concat(&mut self, other: &Self, transform: DAffine2);
}
@ -396,84 +525,6 @@ impl ConcatElement for GraphicGroup {
}
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn copy_to_points<F: 'n + Send + Copy, I: GraphicElementRendered + Default + ConcatElement + TransformMut + Send>(
#[implementations(
(),
(),
Footprint,
)]
footprint: F,
#[implementations(
() -> VectorData,
() -> VectorData,
Footprint -> VectorData,
)]
points: impl Node<F, Output = VectorData>,
#[expose]
#[implementations(
() -> VectorData,
() -> GraphicGroup,
Footprint -> VectorData,
Footprint -> GraphicGroup,
)]
instance: impl Node<F, Output = 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,
) -> I {
let points = points.eval(footprint).await;
let instance = instance.eval(footprint).await;
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]);
let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into());
let mut rotation_rng = rand::rngs::StdRng::seed_from_u64(random_rotation_seed.into());
let do_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.abs() > 1e-6;
let mut result = I::default();
for &point in points_list {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points.transform.transform_point2(point);
let rotation = if do_rotation {
let degrees = (rotation_rng.gen::<f64>() - 0.5) * random_rotation;
degrees / 360. * std::f64::consts::TAU
} else {
0.
};
let scale = if do_scale {
if random_scale_bias.abs() < 1e-6 {
// Linear
random_scale_min + scale_rng.gen::<f64>() * random_scale_difference
} else {
// Weighted (see <https://www.desmos.com/calculator/gmavd3m9bd>)
let horizontal_scale_factor = 1. - 2_f64.powf(random_scale_bias);
let scale_factor = (1. - scale_rng.gen::<f64>() * horizontal_scale_factor).log2() / random_scale_bias;
random_scale_min + scale_factor * random_scale_difference
}
} else {
random_scale_min
};
result.concat(&instance, DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform);
}
result
}
#[node_macro::node(category(""))]
async fn sample_points<F: 'n + Send + Copy>(
#[implementations(
@ -622,7 +673,7 @@ async fn subpath_segment_lengths<F: 'n + Send>(
.collect()
}
#[node_macro::node(name("Splines from Points"), category(""), path(graphene_core::vector))]
#[node_macro::node(name("Splines from Points"), category("Vector"), path(graphene_core::vector))]
fn splines_from_points(_: (), mut vector_data: VectorData) -> VectorData {
let points = &vector_data.point_domain;
@ -838,8 +889,9 @@ mod test {
let direction = DVec2::X * 1.5;
let instances = 3;
let repeated = super::repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
assert_eq!(repeated.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
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);
}
}
@ -848,16 +900,18 @@ mod test {
let direction = DVec2::new(12., 10.);
let instances = 8;
let repeated = super::repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
assert_eq!(repeated.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
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() {
let repeated = super::circular_repeat(Footprint::default(), &vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
assert_eq!(repeated.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
let vector_data = super::flatten_vector_elements(Footprint::default(), &FutureWrapperNode(repeated)).await;
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();
@ -893,9 +947,10 @@ mod test {
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 bounding_box = super::copy_to_points(Footprint::default(), &vector_node(points), &vector_node(instance), 1., 1., 0., 0, 0., 0).await;
assert_eq!(bounding_box.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in bounding_box.region_bezier_paths().enumerate() {
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(), &FutureWrapperNode(copy_to_points)).await;
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],