mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
@@ -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(|| {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user