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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 856 additions and 930 deletions

File diff suppressed because one or more lines are too long

View file

@ -47,6 +47,7 @@ ignore = [
"RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative` "RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative`
"RUSTSEC-2025-0007", # Unmaintained but still fully functional crate `ring` "RUSTSEC-2025-0007", # Unmaintained but still fully functional crate `ring`
"RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste` "RUSTSEC-2024-0436", # Unmaintained but still fully functional crate `paste`
"RUSTSEC-2025-0014", # Unmaintained but still fully functional crate `humantime`
] ]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score # Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories # lower than the range specified will be ignored. Note that ignored advisories

View file

@ -141,14 +141,18 @@ impl Dispatcher {
}; };
let graphene_std::renderer::RenderMetadata { let graphene_std::renderer::RenderMetadata {
footprints, upstream_footprints: footprints,
local_transforms,
click_targets, click_targets,
clip_targets, clip_targets,
} = render_metadata; } = render_metadata;
// Run these update state messages immediately // Run these update state messages immediately
let messages = [ let messages = [
DocumentMessage::UpdateUpstreamTransforms { upstream_transforms: footprints }, DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints: footprints,
local_transforms,
},
DocumentMessage::UpdateClickTargets { click_targets }, DocumentMessage::UpdateClickTargets { click_targets },
DocumentMessage::UpdateClipTargets { clip_targets }, DocumentMessage::UpdateClipTargets { clip_targets },
]; ];

View file

@ -179,7 +179,8 @@ pub enum DocumentMessage {
ToggleOverlaysVisibility, ToggleOverlaysVisibility,
ToggleSnapping, ToggleSnapping,
UpdateUpstreamTransforms { UpdateUpstreamTransforms {
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>, upstream_footprints: HashMap<NodeId, Footprint>,
local_transforms: HashMap<NodeId, DAffine2>,
}, },
UpdateClickTargets { UpdateClickTargets {
click_targets: HashMap<NodeId, Vec<ClickTarget>>, click_targets: HashMap<NodeId, Vec<ClickTarget>>,

View file

@ -1233,8 +1233,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.snapping_state.snapping_enabled = !self.snapping_state.snapping_enabled; self.snapping_state.snapping_enabled = !self.snapping_state.snapping_enabled;
responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(PortfolioMessage::UpdateDocumentWidgets);
} }
DocumentMessage::UpdateUpstreamTransforms { upstream_transforms } => { DocumentMessage::UpdateUpstreamTransforms {
self.network_interface.update_transforms(upstream_transforms); upstream_footprints,
local_transforms,
} => {
self.network_interface.update_transforms(upstream_footprints, local_transforms);
} }
DocumentMessage::UpdateClickTargets { click_targets } => { DocumentMessage::UpdateClickTargets { click_targets } => {
// TODO: Allow non layer nodes to have click targets // TODO: Allow non layer nodes to have click targets
@ -1634,6 +1637,44 @@ impl DocumentMessageHandler {
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> { pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content) let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content)
.or_else(|_| { .or_else(|_| {
// TODO: Eventually remove this document upgrade code
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct OldDocumentMessageHandler {
// ============================================
// Fields that are saved in the document format
// ============================================
//
/// The node graph that generates this document's artwork.
/// It recursively stores its sub-graphs, so this root graph is the whole snapshot of the document content.
pub network: OldNodeNetwork,
/// List of the [`NodeId`]s that are currently selected by the user.
pub selected_nodes: SelectedNodes,
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
pub collapsed: CollapsedLayers,
/// The name of the document, which is displayed in the tab and title bar of the editor.
pub name: String,
/// The full Git commit hash of the Graphite repository that was used to build the editor.
/// We save this to provide a hint about which version of the editor was used to create the document.
pub commit_hash: String,
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visible: bool,
/// Sets whether or not the rulers should be drawn along the top and left edges of the viewport area.
pub rulers_visible: bool,
/// Sets whether or not the node graph is drawn (as an overlay) on top of the viewport area, or otherwise if it's hidden.
pub graph_view_overlay_open: bool,
/// The current user choices for snapping behavior, including whether snapping is enabled at all.
pub snapping_state: SnappingState,
}
serde_json::from_str::<OldDocumentMessageHandler>(serialized_content).map(|old_message_handler| DocumentMessageHandler { serde_json::from_str::<OldDocumentMessageHandler>(serialized_content).map(|old_message_handler| DocumentMessageHandler {
network_interface: NodeNetworkInterface::from_old_network(old_message_handler.network), network_interface: NodeNetworkInterface::from_old_network(old_message_handler.network),
collapsed: old_message_handler.collapsed, collapsed: old_message_handler.collapsed,
@ -2639,41 +2680,3 @@ impl Iterator for ClickXRayIter<'_> {
None None
} }
} }
// TODO: Eventually remove this document upgrade code
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct OldDocumentMessageHandler {
// ============================================
// Fields that are saved in the document format
// ============================================
//
/// The node graph that generates this document's artwork.
/// It recursively stores its sub-graphs, so this root graph is the whole snapshot of the document content.
pub network: OldNodeNetwork,
/// List of the [`NodeId`]s that are currently selected by the user.
pub selected_nodes: SelectedNodes,
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
pub collapsed: CollapsedLayers,
/// The name of the document, which is displayed in the tab and title bar of the editor.
pub name: String,
/// The full Git commit hash of the Graphite repository that was used to build the editor.
/// We save this to provide a hint about which version of the editor was used to create the document.
pub commit_hash: String,
/// The current pan, tilt, and zoom state of the viewport's view of the document canvas.
pub document_ptz: PTZ,
/// The current mode that the document is in, which starts out as Design Mode. This choice affects the editing behavior of the tools.
pub document_mode: DocumentMode,
/// The current view mode that the user has set for rendering the document within the viewport.
/// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently.
pub view_mode: ViewMode,
/// Sets whether or not all the viewport overlays should be drawn on top of the artwork.
/// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more.
pub overlays_visible: bool,
/// Sets whether or not the rulers should be drawn along the top and left edges of the viewport area.
pub rulers_visible: bool,
/// Sets whether or not the node graph is drawn (as an overlay) on top of the viewport area, or otherwise if it's hidden.
pub graph_view_overlay_open: bool,
/// The current user choices for snapping behavior, including whether snapping is enabled at all.
pub snapping_state: SnappingState,
}

View file

@ -237,47 +237,54 @@ impl<'a> ModifyInputsContext<'a> {
} }
}) })
} }
/// Gets the node id of a node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist. /// Gets the node id of a node with a specific reference that is upstream from the layer node, and optionally creates it if it does not exist.
/// The returned node is based on the selection dots in the layer. The right most dot will always insert/access the path that flows directly into the layer. /// The returned node is based on the selection dots in the layer. The right most dot will always insert/access the path that flows directly into the layer.
/// Each dot after that represents an existing path node. If there is an existing upstream node, then it will always be returned first. /// Each dot after that represents an existing path node. If there is an existing upstream node, then it will always be returned first.
pub fn existing_node_id(&mut self, reference: &'static str, create_if_nonexistent: bool) -> Option<NodeId> { pub fn existing_node_id(&mut self, reference_name: &'static str, create_if_nonexistent: bool) -> Option<NodeId> {
// Start from the layer node or export // Start from the layer node or export
let output_layer = self.get_output_layer()?; let output_layer = self.get_output_layer()?;
let upstream = self let existing_node_id = Self::locate_node_in_layer_chain(reference_name, output_layer, self.network_interface);
.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 mut existing_node_id = None;
for upstream_node in upstream.collect::<Vec<_>>() {
// Check if this is the node we have been searching for.
if self
.network_interface
.reference(&upstream_node, &[])
.is_some_and(|node_reference| *node_reference == Some(reference.to_string()))
{
existing_node_id = Some(upstream_node);
break;
}
let is_traversal_start = |node_id: NodeId| {
self.layer_node.map(|layer| layer.to_node()) == Some(node_id) || self.network_interface.document_network().exports.iter().any(|export| export.as_node() == Some(node_id))
};
if !is_traversal_start(upstream_node) && (self.network_interface.is_layer(&upstream_node, &[])) {
break;
}
}
// Create a new node if the node does not exist and update its inputs // Create a new node if the node does not exist and update its inputs
if create_if_nonexistent { if create_if_nonexistent {
return existing_node_id.or_else(|| self.create_node(reference)); return existing_node_id.or_else(|| self.create_node(reference_name));
} }
existing_node_id existing_node_id
} }
/// Gets the node id of a node with a specific reference (name) that is upstream (leftward) from the layer node, but before reaching another upstream layer stack.
/// For example, if given a group layer, this would find a requested "Transform" or "Boolean Operation" node in its chain, between the group layer and its layer stack child contents.
/// It would also travel up an entire layer that's not fed by a stack until reaching the generator node, such as a "Rectangle" or "Path" layer.
pub fn locate_node_in_layer_chain(reference_name: &str, left_of_layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
let upstream = network_interface.upstream_flow_back_from_nodes(vec![left_of_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow);
// Look at all of the upstream nodes
for upstream_node in upstream {
// Check if this is the node we have been searching for.
if network_interface
.reference(&upstream_node, &[])
.is_some_and(|node_reference| *node_reference == Some(reference_name.to_string()))
{
if !network_interface.is_visible(&upstream_node, &[]) {
continue;
}
return Some(upstream_node);
}
// Take until another layer node is found (but not the first layer node)
let is_traversal_start = |node_id: NodeId| left_of_layer.to_node() == node_id || network_interface.document_network().exports.iter().any(|export| export.as_node() == Some(node_id));
if !is_traversal_start(upstream_node) && (network_interface.is_layer(&upstream_node, &[])) {
return None;
}
}
None
}
/// Create a new node inside the layer /// Create a new node inside the layer
pub fn create_node(&mut self, reference: &str) -> Option<NodeId> { pub fn create_node(&mut self, reference: &str) -> Option<NodeId> {
let output_layer = self.get_output_layer()?; let output_layer = self.get_output_layer()?;

View file

@ -2255,7 +2255,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default() ..Default::default()
}), }),
), ),
PropertiesRow::with_override("Skew", WidgetOverride::Hidden), PropertiesRow::with_override(
"Skew",
WidgetOverride::Vec2(Vec2InputSettings {
x: "X".to_string(),
y: "Y".to_string(),
unit: "°".to_string(),
..Default::default()
}),
),
PropertiesRow::with_override("Pivot", WidgetOverride::Hidden), PropertiesRow::with_override("Pivot", WidgetOverride::Hidden),
], ],
output_names: vec!["Data".to_string()], output_names: vec!["Data".to_string()],

View file

@ -1,3 +1,6 @@
use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use super::network_interface::NodeNetworkInterface; use super::network_interface::NodeNetworkInterface;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_core::renderer::ClickTarget; use graphene_core::renderer::ClickTarget;
@ -17,7 +20,8 @@ use std::num::NonZeroU64;
// TODO: it might be better to have a system that can query the state of the node network on demand. // TODO: it might be better to have a system that can query the state of the node network on demand.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DocumentMetadata { pub struct DocumentMetadata {
pub upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>, pub upstream_footprints: HashMap<NodeId, Footprint>,
pub local_transforms: HashMap<NodeId, DAffine2>,
pub structure: HashMap<LayerNodeIdentifier, NodeRelations>, pub structure: HashMap<LayerNodeIdentifier, NodeRelations>,
pub click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, pub click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
pub clip_targets: HashSet<NodeId>, pub clip_targets: HashSet<NodeId>,
@ -29,7 +33,8 @@ pub struct DocumentMetadata {
impl Default for DocumentMetadata { impl Default for DocumentMetadata {
fn default() -> Self { fn default() -> Self {
Self { Self {
upstream_transforms: HashMap::new(), upstream_footprints: HashMap::new(),
local_transforms: HashMap::new(),
structure: HashMap::new(), structure: HashMap::new(),
vector_modify: HashMap::new(), vector_modify: HashMap::new(),
click_targets: HashMap::new(), click_targets: HashMap::new(),
@ -77,14 +82,27 @@ impl DocumentMetadata {
} }
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 { pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.upstream_transforms let footprint = self.upstream_footprints.get(&layer.to_node()).map(|footprint| footprint.transform).unwrap_or(self.document_to_viewport);
.get(&layer.to_node()) let local_transform = self.local_transforms.get(&layer.to_node()).copied().unwrap_or_default();
.map(|(footprint, transform)| footprint.transform * *transform)
.unwrap_or(self.document_to_viewport) footprint * local_transform
}
pub fn transform_to_viewport_with_first_transform_node_if_group(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> DAffine2 {
let footprint = self.upstream_footprints.get(&layer.to_node()).map(|footprint| footprint.transform).unwrap_or(self.document_to_viewport);
let local_transform = self.local_transforms.get(&layer.to_node()).copied();
let transform = local_transform.unwrap_or_else(|| {
let transform_node_id = ModifyInputsContext::locate_node_in_layer_chain("Transform", layer, network_interface);
let transform_node = transform_node_id.and_then(|id| network_interface.document_node(&id, &[]));
transform_node.map(|node| transform_utils::get_current_transform(node.inputs.as_slice())).unwrap_or_default()
});
footprint * transform
} }
pub fn upstream_transform(&self, node_id: NodeId) -> DAffine2 { pub fn upstream_transform(&self, node_id: NodeId) -> DAffine2 {
self.upstream_transforms.get(&node_id).copied().map(|(_, transform)| transform).unwrap_or(DAffine2::IDENTITY) self.local_transforms.get(&node_id).copied().unwrap_or(DAffine2::IDENTITY)
} }
pub fn downstream_transform_to_document(&self, layer: LayerNodeIdentifier) -> DAffine2 { pub fn downstream_transform_to_document(&self, layer: LayerNodeIdentifier) -> DAffine2 {
@ -96,10 +114,10 @@ impl DocumentMetadata {
return self.transform_to_viewport(layer); return self.transform_to_viewport(layer);
} }
self.upstream_transforms self.upstream_footprints
.get(&layer.to_node()) .get(&layer.to_node())
.copied() .copied()
.map(|(footprint, _)| footprint.transform) .map(|footprint| footprint.transform)
.unwrap_or_else(|| self.transform_to_viewport(layer)) .unwrap_or_else(|| self.transform_to_viewport(layer))
} }
} }

View file

@ -3244,14 +3244,16 @@ impl NodeNetworkInterface {
let nodes: HashSet<NodeId> = self.document_network().nodes.keys().cloned().collect::<HashSet<_>>(); let nodes: HashSet<NodeId> = self.document_network().nodes.keys().cloned().collect::<HashSet<_>>();
self.document_metadata.upstream_transforms.retain(|node, _| nodes.contains(node)); self.document_metadata.upstream_footprints.retain(|node, _| nodes.contains(node));
self.document_metadata.local_transforms.retain(|node, _| nodes.contains(node));
self.document_metadata.vector_modify.retain(|node, _| nodes.contains(node)); self.document_metadata.vector_modify.retain(|node, _| nodes.contains(node));
self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer)); self.document_metadata.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
} }
/// Update the cached transforms of the layers /// Update the cached transforms of the layers
pub fn update_transforms(&mut self, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>) { pub fn update_transforms(&mut self, upstream_footprints: HashMap<NodeId, Footprint>, local_transforms: HashMap<NodeId, DAffine2>) {
self.document_metadata.upstream_transforms = new_upstream_transforms; self.document_metadata.upstream_footprints = upstream_footprints;
self.document_metadata.local_transforms = local_transforms;
} }
/// Update the cached click targets of the layers /// Update the cached click targets of the layers

View file

@ -1,6 +1,7 @@
use super::network_interface::NodeNetworkInterface; use super::network_interface::NodeNetworkInterface;
use crate::consts::{ROTATE_INCREMENT, SCALE_INCREMENT}; use crate::consts::{ROTATE_INCREMENT, SCALE_INCREMENT};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::{ModifyInputsContext, TransformIn};
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
@ -54,9 +55,15 @@ impl OriginalTransforms {
} }
} }
pub fn update<'a>(&mut self, selected: &'a [LayerNodeIdentifier], network_interface: &NodeNetworkInterface, shape_editor: Option<&'a ShapeState>) { /// Gets the transform from the most downstream transform node
let document_metadata = network_interface.document_metadata(); fn get_layer_transform(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<DAffine2> {
let transform_node_id = ModifyInputsContext::locate_node_in_layer_chain("Transform", layer, network_interface)?;
let document_node = network_interface.document_network().nodes.get(&transform_node_id)?;
Some(transform_utils::get_current_transform(&document_node.inputs))
}
pub fn update<'a>(&mut self, selected: &'a [LayerNodeIdentifier], network_interface: &NodeNetworkInterface, shape_editor: Option<&'a ShapeState>) {
match self { match self {
OriginalTransforms::Layer(layer_map) => { OriginalTransforms::Layer(layer_map) => {
layer_map.retain(|layer, _| selected.contains(layer)); layer_map.retain(|layer, _| selected.contains(layer));
@ -64,7 +71,8 @@ impl OriginalTransforms {
if layer == LayerNodeIdentifier::ROOT_PARENT { if layer == LayerNodeIdentifier::ROOT_PARENT {
continue; continue;
} }
layer_map.entry(layer).or_insert_with(|| document_metadata.upstream_transform(layer.to_node()));
layer_map.entry(layer).or_insert_with(|| Self::get_layer_transform(layer, network_interface).unwrap_or_default());
} }
} }
OriginalTransforms::Path(path_map) => { OriginalTransforms::Path(path_map) => {
@ -550,7 +558,7 @@ impl<'a> Selected<'a> {
.unwrap_or(DAffine2::IDENTITY); .unwrap_or(DAffine2::IDENTITY);
if transform.matrix2.determinant().abs() <= f64::EPSILON { if transform.matrix2.determinant().abs() <= f64::EPSILON {
transform.matrix2 += DMat2::IDENTITY * 1e-4; transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
} }
let bounds = self let bounds = self

View file

@ -523,16 +523,16 @@ impl Fsm for SelectToolFsmState {
} }
// Update bounds // Update bounds
let transform = document let mut transform = document
.network_interface .network_interface
.selected_nodes() .selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface) .selected_visible_and_unlocked_layers(&document.network_interface)
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])) .find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport(layer)); .map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
.unwrap_or_default();
let mut transform = transform.unwrap_or(DAffine2::IDENTITY);
let mut transform_tampered = false;
// Check if the matrix is not invertible // Check if the matrix is not invertible
let mut transform_tampered = false;
if transform.matrix2.determinant() == 0. { if transform.matrix2.determinant() == 0. {
transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this? transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
transform_tampered = true; transform_tampered = true;
@ -549,6 +549,7 @@ impl Fsm for SelectToolFsmState {
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer)) .bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
}) })
.reduce(graphene_core::renderer::Quad::combine_bounds); .reduce(graphene_core::renderer::Quad::combine_bounds);
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default()); let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());

View file

@ -525,7 +525,6 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
responses.add(PenToolMessage::Abort); responses.add(PenToolMessage::Abort);
responses.add(ToolMessage::UpdateHints); responses.add(ToolMessage::UpdateHints);
} else { } else {
selected.revert_operation();
selected.original_transforms.clear(); selected.original_transforms.clear();
self.typing.clear(); self.typing.clear();
self.transform_operation = TransformOperation::None; self.transform_operation = TransformOperation::None;

View file

@ -10,7 +10,7 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::wasm_application_io::EditorPreferences;
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::memo::IORecord; use graphene_core::memo::IORecord;
use graphene_core::renderer::{GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender}; use graphene_core::renderer::{GraphicElementRendered, RenderParams, SvgRender};
use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment}; use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_core::text::FontCache; use graphene_core::text::FontCache;
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
@ -323,7 +323,7 @@ impl NodeRuntime {
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY); let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
// Render the thumbnail from a `GraphicElement` into an SVG string // Render the thumbnail from a `GraphicElement` into an SVG string
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, bounds, true, false, false); let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false);
let mut render = SvgRender::new(); let mut render = SvgRender::new();
graphic_element.render_svg(&mut render, &render_params); graphic_element.render_svg(&mut render, &render_params);
@ -655,7 +655,7 @@ impl NodeGraphExecutor {
fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) { fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
// Setup rendering // Setup rendering
let mut render = SvgRender::new(); let mut render = SvgRender::new();
let render_params = RenderParams::new(ViewMode::Normal, ImageRenderMode::Base64, None, false, false, false); let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false);
// Render SVG // Render SVG
render_object.render_svg(&mut render, &render_params); render_object.render_svg(&mut render, &render_params);

View file

@ -266,7 +266,7 @@ pub mod test_prelude {
pub use graph_craft::document::DocumentNode; pub use graph_craft::document::DocumentNode;
pub use graphene_core::raster::{Color, Image}; pub use graphene_core::raster::{Color, Image};
pub use graphene_core::{InputAccessor, InputAccessorSource}; pub use graphene_core::{InputAccessor, InputAccessorSource};
pub use graphene_std::{transform::Footprint, GraphicGroup}; pub use graphene_std::transform::Footprint;
#[macro_export] #[macro_export]
macro_rules! float_eq { macro_rules! float_eq {

View file

@ -74,7 +74,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
if (currentDocumentId) { if (currentDocumentId && currentDocumentId in previouslySavedDocuments) {
const doc = previouslySavedDocuments[currentDocumentId]; const doc = previouslySavedDocuments[currentDocumentId];
editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false); editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false);
editor.handle.selectDocument(BigInt(currentDocumentId)); editor.handle.selectDocument(BigInt(currentDocumentId));

View file

@ -49,6 +49,7 @@ rand = { workspace = true, default-features = false, features = ["std_rng"] }
glam = { workspace = true, default-features = false, features = [ glam = { workspace = true, default-features = false, features = [
"scalar-math", "scalar-math",
] } ] }
serde_json = { workspace = true }
# Required dependencies # Required dependencies
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] } half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }

View file

@ -2,14 +2,13 @@ use crate::application_io::{ImageTexture, TextureFrameTable};
use crate::instances::Instances; use crate::instances::Instances;
use crate::raster::image::{Image, ImageFrameTable}; use crate::raster::image::{Image, ImageFrameTable};
use crate::raster::BlendMode; use crate::raster::BlendMode;
use crate::transform::{Transform, TransformMut}; use crate::transform::TransformMut;
use crate::uuid::NodeId; use crate::uuid::NodeId;
use crate::vector::{VectorData, VectorDataTable}; use crate::vector::{VectorData, VectorDataTable};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
use dyn_any::DynAny; use dyn_any::DynAny;
use core::ops::{Deref, DerefMut};
use glam::{DAffine2, IVec2}; use glam::{DAffine2, IVec2};
use std::hash::Hash; use std::hash::Hash;
@ -52,83 +51,84 @@ pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
transform: DAffine2, transform: DAffine2,
alpha_blending: AlphaBlending, alpha_blending: AlphaBlending,
} }
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<(GraphicElement, Option<NodeId>)>,
}
pub type OldGraphicGroupTable = Instances<GraphicGroup>;
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
enum EitherFormat { enum EitherFormat {
GraphicGroup(GraphicGroup),
OldGraphicGroup(OldGraphicGroup), OldGraphicGroup(OldGraphicGroup),
GraphicGroupTable(GraphicGroupTable), InstanceTable(serde_json::Value),
} }
Ok(match EitherFormat::deserialize(deserializer)? { Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::GraphicGroup(graphic_group) => GraphicGroupTable::new(graphic_group),
EitherFormat::OldGraphicGroup(old) => { EitherFormat::OldGraphicGroup(old) => {
let mut graphic_group_table = GraphicGroupTable::new(GraphicGroup { elements: old.elements }); let mut graphic_group_table = GraphicGroupTable::empty();
*graphic_group_table.one_instance_mut().transform = old.transform; for (graphic_element, source_node_id) in old.elements {
*graphic_group_table.one_instance_mut().alpha_blending = old.alpha_blending; let last = graphic_group_table.push(graphic_element);
*last.source_node_id = source_node_id;
*last.transform = old.transform;
*last.alpha_blending = old.alpha_blending;
}
graphic_group_table graphic_group_table
} }
EitherFormat::GraphicGroupTable(graphic_group_table) => graphic_group_table, EitherFormat::InstanceTable(value) => {
// Try to deserialize as either table format
if let Ok(old_table) = serde_json::from_value::<OldGraphicGroupTable>(value.clone()) {
let mut graphic_group_table = GraphicGroupTable::empty();
for instance in old_table.instances() {
for (graphic_element, source_node_id) in &instance.instance.elements {
let new_row = graphic_group_table.push(graphic_element.clone());
*new_row.source_node_id = *source_node_id;
*new_row.transform = *instance.transform;
*new_row.alpha_blending = *instance.alpha_blending;
}
}
graphic_group_table
} else if let Ok(new_table) = serde_json::from_value::<GraphicGroupTable>(value) {
new_table
} else {
return Err(serde::de::Error::custom("Failed to deserialize GraphicGroupTable"));
}
}
}) })
} }
pub type GraphicGroupTable = Instances<GraphicGroup>; // TODO: Rename to GraphicElementTable
pub type GraphicGroupTable = Instances<GraphicElement>;
/// A list of [`GraphicElement`]s
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<(GraphicElement, Option<NodeId>)>,
}
impl core::hash::Hash for GraphicGroup {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.elements.hash(state);
}
}
impl GraphicGroup {
pub fn new(elements: Vec<GraphicElement>) -> Self {
Self {
elements: elements.into_iter().map(|element| (element, None)).collect(),
}
}
}
impl From<GraphicGroup> for GraphicGroupTable {
fn from(graphic_group: GraphicGroup) -> Self {
Self::new(graphic_group)
}
}
impl From<VectorData> for GraphicGroupTable { impl From<VectorData> for GraphicGroupTable {
fn from(vector_data: VectorData) -> Self { fn from(vector_data: VectorData) -> Self {
Self::new(GraphicGroup::new(vec![GraphicElement::VectorData(VectorDataTable::new(vector_data))])) Self::new(GraphicElement::VectorData(VectorDataTable::new(vector_data)))
} }
} }
impl From<VectorDataTable> for GraphicGroupTable { impl From<VectorDataTable> for GraphicGroupTable {
fn from(vector_data: VectorDataTable) -> Self { fn from(vector_data: VectorDataTable) -> Self {
Self::new(GraphicGroup::new(vec![GraphicElement::VectorData(vector_data)])) Self::new(GraphicElement::VectorData(vector_data))
} }
} }
impl From<Image<Color>> for GraphicGroupTable { impl From<Image<Color>> for GraphicGroupTable {
fn from(image: Image<Color>) -> Self { fn from(image: Image<Color>) -> Self {
Self::new(GraphicGroup::new(vec![GraphicElement::RasterFrame(RasterFrame::ImageFrame(ImageFrameTable::new(image)))])) Self::new(GraphicElement::RasterFrame(RasterFrame::ImageFrame(ImageFrameTable::new(image))))
} }
} }
impl From<ImageFrameTable<Color>> for GraphicGroupTable { impl From<ImageFrameTable<Color>> for GraphicGroupTable {
fn from(image_frame: ImageFrameTable<Color>) -> Self { fn from(image_frame: ImageFrameTable<Color>) -> Self {
Self::new(GraphicGroup::new(vec![GraphicElement::RasterFrame(RasterFrame::ImageFrame(image_frame))])) Self::new(GraphicElement::RasterFrame(RasterFrame::ImageFrame(image_frame)))
} }
} }
impl From<ImageTexture> for GraphicGroupTable { impl From<ImageTexture> for GraphicGroupTable {
fn from(image_texture: ImageTexture) -> Self { fn from(image_texture: ImageTexture) -> Self {
Self::new(GraphicGroup::new(vec![GraphicElement::RasterFrame(RasterFrame::TextureFrame(TextureFrameTable::new(image_texture)))])) Self::new(GraphicElement::RasterFrame(RasterFrame::TextureFrame(TextureFrameTable::new(image_texture))))
} }
} }
impl From<TextureFrameTable> for GraphicGroupTable { impl From<TextureFrameTable> for GraphicGroupTable {
fn from(texture_frame: TextureFrameTable) -> Self { fn from(texture_frame: TextureFrameTable) -> Self {
Self::new(GraphicGroup::new(vec![GraphicElement::RasterFrame(RasterFrame::TextureFrame(texture_frame))])) Self::new(GraphicElement::RasterFrame(RasterFrame::TextureFrame(texture_frame)))
} }
} }
@ -278,8 +278,8 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
EitherFormat::ArtboardGroup(artboard_group) => { EitherFormat::ArtboardGroup(artboard_group) => {
let mut table = ArtboardGroupTable::empty(); let mut table = ArtboardGroupTable::empty();
for (artboard, source_node_id) in artboard_group.artboards { for (artboard, source_node_id) in artboard_group.artboards {
table.push(artboard); let pushed = table.push(artboard);
*table.instances_mut().last().unwrap().source_node_id = source_node_id; *pushed.source_node_id = source_node_id;
} }
table table
} }
@ -290,23 +290,40 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
pub type ArtboardGroupTable = Instances<Artboard>; pub type ArtboardGroupTable = Instances<Artboard>;
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn layer(_: impl Ctx, stack: GraphicGroupTable, mut element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable { async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable {
let mut stack = stack;
if stack.transform().matrix2.determinant() != 0. {
*element.transform_mut() = stack.transform().inverse() * element.transform();
} else {
stack.one_instance_mut().instance.clear();
*stack.transform_mut() = DAffine2::IDENTITY;
}
// Get the penultimate element of the node path, or None if the path is too short // Get the penultimate element of the node path, or None if the path is too short
let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied(); let pushed = stack.push(element);
stack.one_instance_mut().instance.push((element, encapsulating_node_id)); *pushed.source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
stack stack
} }
// TODO: Once we have nicely working spreadsheet tables, test this and make it nicely user-facing and move it from "Debug" to "General"
#[node_macro::node(category("Debug"))]
async fn concatenate<T: Clone>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
ImageFrameTable<Color>,
TextureFrameTable,
)]
from: Instances<T>,
#[expose]
#[implementations(
GraphicGroupTable,
VectorDataTable,
ImageFrameTable<Color>,
TextureFrameTable,
)]
mut to: Instances<T>,
) -> Instances<T> {
for instance in from.instances() {
to.push_instance(instance);
}
to
}
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn to_element<Data: Into<GraphicElement> + 'n>( async fn to_element<Data: Into<GraphicElement> + 'n>(
_: impl Ctx, _: impl Ctx,
@ -337,38 +354,39 @@ async fn to_group<Data: Into<GraphicGroupTable> + 'n>(
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
async fn flatten_group(_: impl Ctx, group: GraphicGroupTable, fully_flatten: bool) -> GraphicGroupTable { async fn flatten_group(_: impl Ctx, group: GraphicGroupTable, fully_flatten: bool) -> GraphicGroupTable {
fn flatten_group(result_group: &mut GraphicGroupTable, current_group_table: GraphicGroupTable, fully_flatten: bool) { // TODO: Avoid mutable reference, instead return a new GraphicGroupTable?
let mut collection_group = GraphicGroup::default(); fn flatten_group(output_group_table: &mut GraphicGroupTable, current_group_table: GraphicGroupTable, fully_flatten: bool, recursion_depth: usize) {
let current_group_elements = current_group_table.one_instance().instance.elements.clone(); for current_instance in current_group_table.instances() {
let current_element = current_instance.instance.clone();
let reference = *current_instance.source_node_id;
for (element, reference) in current_group_elements { let recurse = fully_flatten || recursion_depth == 0;
if let GraphicElement::GraphicGroup(mut nested_group_table) = element {
*nested_group_table.transform_mut() = nested_group_table.transform() * current_group_table.transform();
let mut sub_group_table = GraphicGroupTable::default(); match current_element {
if fully_flatten { // If we're allowed to recurse, flatten any GraphicGroups we encounter
flatten_group(&mut sub_group_table, nested_group_table, fully_flatten); GraphicElement::GraphicGroup(mut current_element) if recurse => {
} else { // Apply the parent group's transform to all child elements
let nested_group_table_transform = nested_group_table.transform(); for graphic_element in current_element.instances_mut() {
for (collection_element, _) in &mut nested_group_table.one_instance_mut().instance.elements { *graphic_element.transform = *current_instance.transform * *graphic_element.transform;
*collection_element.transform_mut() = nested_group_table_transform * collection_element.transform();
} }
sub_group_table = nested_group_table;
}
collection_group.append(&mut sub_group_table.one_instance_mut().instance.elements); flatten_group(output_group_table, current_element, fully_flatten, recursion_depth + 1);
} else { }
collection_group.push((element, reference)); // Handle any leaf elements we encounter, which can be either non-GraphicGroup elements or GraphicGroups that we don't want to flatten
_ => {
let pushed = output_group_table.push(current_element);
*pushed.source_node_id = reference;
// Apply the parent group's transform to the leaf element
*pushed.transform = *current_instance.transform * *pushed.transform;
}
} }
} }
result_group.one_instance_mut().instance.append(&mut collection_group.elements);
} }
let mut flat_group = GraphicGroupTable::default(); let mut output = GraphicGroupTable::default();
flatten_group(&mut flat_group, group, fully_flatten); flatten_group(&mut output, group, fully_flatten, 0);
flat_group output
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
@ -411,8 +429,8 @@ async fn append_artboard(_ctx: impl Ctx, mut artboards: ArtboardGroupTable, artb
// This is used to get the ID of the user-facing "Artboard" node (which encapsulates this internal "Append Artboard" node). // This is used to get the ID of the user-facing "Artboard" node (which encapsulates this internal "Append Artboard" node).
let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied(); let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
artboards.push(artboard); let pushed = artboards.push(artboard);
*artboards.instances_mut().last().unwrap().source_node_id = encapsulating_node_id; *pushed.source_node_id = encapsulating_node_id;
artboards artboards
} }
@ -450,46 +468,8 @@ impl From<VectorDataTable> for GraphicElement {
GraphicElement::VectorData(vector_data) GraphicElement::VectorData(vector_data)
} }
} }
// TODO: Remove this one
impl From<GraphicGroup> for GraphicElement {
fn from(graphic_group: GraphicGroup) -> Self {
GraphicElement::GraphicGroup(GraphicGroupTable::new(graphic_group))
}
}
impl From<GraphicGroupTable> for GraphicElement { impl From<GraphicGroupTable> for GraphicElement {
fn from(graphic_group: GraphicGroupTable) -> Self { fn from(graphic_group: GraphicGroupTable) -> Self {
GraphicElement::GraphicGroup(graphic_group) GraphicElement::GraphicGroup(graphic_group)
} }
} }
impl Deref for GraphicGroup {
type Target = Vec<(GraphicElement, Option<NodeId>)>;
fn deref(&self) -> &Self::Target {
&self.elements
}
}
impl DerefMut for GraphicGroup {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.elements
}
}
/// This is a helper trait used for the Into Implementation.
/// We can't just implement this for all for which from is implemented
/// as that would conflict with the implementation for `Self`
trait ToGraphicElement: Into<GraphicElement> {}
impl ToGraphicElement for VectorDataTable {}
impl ToGraphicElement for ImageFrameTable<Color> {}
impl ToGraphicElement for ImageTexture {}
impl<T> From<T> for GraphicGroup
where
T: ToGraphicElement,
{
fn from(value: T) -> Self {
Self {
elements: (vec![(value.into(), None)]),
}
}
}

View file

@ -168,8 +168,10 @@ impl SvgRender {
pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) { pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) {
self.indent(); self.indent();
self.svg.push("<".into()); self.svg.push("<".into());
self.svg.push(name.into()); self.svg.push(name.into());
attributes(&mut SvgRenderAttrs(self)); attributes(&mut SvgRenderAttrs(self));
self.svg.push("/>".into()); self.svg.push("/>".into());
@ -210,12 +212,6 @@ impl Default for SvgRender {
} }
} }
#[derive(Default)]
pub enum ImageRenderMode {
#[default]
Base64,
}
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct RenderContext { pub struct RenderContext {
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
@ -226,7 +222,6 @@ pub struct RenderContext {
#[derive(Default)] #[derive(Default)]
pub struct RenderParams { pub struct RenderParams {
pub view_mode: ViewMode, pub view_mode: ViewMode,
pub image_render_mode: ImageRenderMode,
pub culling_bounds: Option<[DVec2; 2]>, pub culling_bounds: Option<[DVec2; 2]>,
pub thumbnail: bool, pub thumbnail: bool,
/// Don't render the rectangle for an artboard to allow exporting with a transparent background. /// Don't render the rectangle for an artboard to allow exporting with a transparent background.
@ -236,10 +231,9 @@ pub struct RenderParams {
} }
impl RenderParams { impl RenderParams {
pub fn new(view_mode: ViewMode, image_render_mode: ImageRenderMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self { pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
Self { Self {
view_mode, view_mode,
image_render_mode,
culling_bounds, culling_bounds,
thumbnail, thumbnail,
hide_artboards, hide_artboards,
@ -271,7 +265,8 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
#[derive(Debug, Default, Clone, PartialEq, DynAny)] #[derive(Debug, Default, Clone, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RenderMetadata { pub struct RenderMetadata {
pub footprints: HashMap<NodeId, (Footprint, DAffine2)>, pub upstream_footprints: HashMap<NodeId, Footprint>,
pub local_transforms: HashMap<NodeId, DAffine2>,
pub click_targets: HashMap<NodeId, Vec<ClickTarget>>, pub click_targets: HashMap<NodeId, Vec<ClickTarget>>,
pub clip_targets: HashSet<NodeId>, pub clip_targets: HashSet<NodeId>,
} }
@ -280,6 +275,9 @@ pub struct RenderMetadata {
pub trait GraphicElementRendered { pub trait GraphicElementRendered {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams); fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams);
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _render_context: &mut RenderContext) {}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>; fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>;
// The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection // The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection
@ -291,9 +289,6 @@ pub trait GraphicElementRendered {
// Recursively iterate over data in the render (including groups upstream from vector data in the case of a boolean operation) to collect the footprints, click targets, and vector modify // Recursively iterate over data in the render (including groups upstream from vector data in the case of a boolean operation) to collect the footprints, click targets, and vector modify
fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option<NodeId>) {} fn collect_metadata(&self, _metadata: &mut RenderMetadata, _footprint: Footprint, _element_id: Option<NodeId>) {}
#[cfg(feature = "vello")]
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _render_context: &mut RenderContext) {}
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
false false
} }
@ -311,7 +306,7 @@ impl GraphicElementRendered for GraphicGroupTable {
render.parent_tag( render.parent_tag(
"g", "g",
|attributes| { |attributes| {
let matrix = format_transform_matrix(instance.transform()); let matrix = format_transform_matrix(*instance.transform);
if !matrix.is_empty() { if !matrix.is_empty() {
attributes.push("transform", matrix); attributes.push("transform", matrix);
} }
@ -325,49 +320,66 @@ impl GraphicElementRendered for GraphicGroupTable {
} }
}, },
|render| { |render| {
for (element, _) in instance.instance.iter() { instance.instance.render_svg(render, render_params);
element.render_svg(render, render_params);
}
}, },
); );
} }
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
for instance in self.instances() {
let transform = transform * *instance.transform;
let alpha_blending = *instance.alpha_blending;
let blending = vello::peniko::BlendMode::new(alpha_blending.blend_mode.into(), vello::peniko::Compose::SrcOver);
let mut layer = false;
if alpha_blending.opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
if let Some(bounds) = self.instances().filter_map(|element| element.instance.bounding_box(transform)).reduce(Quad::combine_bounds) {
scene.push_layer(
blending,
alpha_blending.opacity,
kurbo::Affine::IDENTITY,
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
);
layer = true;
}
}
instance.instance.render_to_vello(scene, transform, context);
if layer {
scene.pop_layer();
}
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.instances() self.instances()
.flat_map(|instance| { .filter_map(|element| element.instance.bounding_box(transform * *element.transform))
instance
.instance
.iter()
.filter_map(|(element, _)| element.bounding_box(transform * instance.transform()))
.reduce(Quad::combine_bounds)
})
.reduce(Quad::combine_bounds) .reduce(Quad::combine_bounds)
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let instance_transform = self.transform(); for instance in self.instances() {
let instance = self.one_instance().instance; if let Some(element_id) = instance.source_node_id {
let mut footprint = footprint;
footprint.transform *= *instance.transform;
let mut footprint = footprint; instance.instance.collect_metadata(metadata, footprint, Some(*element_id));
footprint.transform *= instance_transform;
for (element, element_id) in instance.elements.iter() {
if let Some(element_id) = element_id {
element.collect_metadata(metadata, footprint, Some(*element_id));
} }
} }
if let Some(graphic_group_id) = element_id { if let Some(graphic_group_id) = element_id {
let mut all_upstream_click_targets = Vec::new(); let mut all_upstream_click_targets = Vec::new();
for (element, _) in instance.elements.iter() { for instance in self.instances() {
let mut new_click_targets = Vec::new(); let mut new_click_targets = Vec::new();
instance.instance.add_upstream_click_targets(&mut new_click_targets);
element.add_upstream_click_targets(&mut new_click_targets);
for click_target in new_click_targets.iter_mut() { for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(element.transform()) click_target.apply_transform(*instance.transform)
} }
all_upstream_click_targets.extend(new_click_targets); all_upstream_click_targets.extend(new_click_targets);
@ -379,60 +391,25 @@ impl GraphicElementRendered for GraphicGroupTable {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for instance in self.instances() { for instance in self.instances() {
for (element, _) in instance.instance.elements.iter() { let mut new_click_targets = Vec::new();
let mut new_click_targets = Vec::new();
element.add_upstream_click_targets(&mut new_click_targets); instance.instance.add_upstream_click_targets(&mut new_click_targets);
for click_target in new_click_targets.iter_mut() { for click_target in new_click_targets.iter_mut() {
click_target.apply_transform(element.transform()) click_target.apply_transform(*instance.transform)
}
click_targets.extend(new_click_targets);
}
}
}
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
for instance in self.instances() {
let transform = transform * instance.transform();
let alpha_blending = *instance.alpha_blending;
let blending = vello::peniko::BlendMode::new(alpha_blending.blend_mode.into(), vello::peniko::Compose::SrcOver);
let mut layer = false;
if alpha_blending.opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
if let Some(bounds) = instance.instance.iter().filter_map(|(element, _)| element.bounding_box(transform)).reduce(Quad::combine_bounds) {
scene.push_layer(
blending,
alpha_blending.opacity,
kurbo::Affine::IDENTITY,
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
);
layer = true;
}
} }
for (element, _) in instance.instance.iter() { click_targets.extend(new_click_targets);
element.render_to_vello(scene, transform, context);
}
if layer {
scene.pop_layer();
}
} }
} }
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
self.instances().any(|instance| instance.instance.iter().any(|(element, _)| element.contains_artboard())) self.instances().any(|instance| instance.instance.contains_artboard())
} }
fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, _reference: Option<NodeId>) {
for instance in self.instances_mut() { for instance in self.instances_mut() {
for (element, node_id) in instance.instance.elements.iter_mut() { instance.instance.new_ids_from_hash(*instance.source_node_id);
element.new_ids_from_hash(*node_id);
}
} }
} }
@ -444,11 +421,11 @@ impl GraphicElementRendered for GraphicGroupTable {
impl GraphicElementRendered for VectorDataTable { impl GraphicElementRendered for VectorDataTable {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for instance in self.instances() { for instance in self.instances() {
let multiplied_transform = render.transform * instance.transform(); let multiplied_transform = render.transform * *instance.transform;
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
let applied_stroke_transform = set_stroke_transform.unwrap_or(instance.transform()); let applied_stroke_transform = set_stroke_transform.unwrap_or(*instance.transform);
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse()); let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
let layer_bounds = instance.instance.bounding_box().unwrap_or_default(); let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
@ -462,7 +439,9 @@ impl GraphicElementRendered for VectorDataTable {
render.leaf_tag("path", |attributes| { render.leaf_tag("path", |attributes| {
attributes.push("d", path); attributes.push("d", path);
let matrix = format_transform_matrix(element_transform); let matrix = format_transform_matrix(element_transform);
attributes.push("transform", matrix); if !matrix.is_empty() {
attributes.push("transform", matrix);
}
let defs = &mut attributes.0.svg_defs; let defs = &mut attributes.0.svg_defs;
@ -483,67 +462,6 @@ impl GraphicElementRendered for VectorDataTable {
} }
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.instances()
.flat_map(|instance| {
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
let miter_limit = instance.instance.style.stroke().map(|s| s.line_join_miter_limit).unwrap_or(1.);
let scale = transform.decompose_scale();
// We use the full line width here to account for different styles of line caps
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
instance.instance.bounding_box_with_transform(transform * instance.transform()).map(|[a, b]| [a - offset, b + offset])
})
.reduce(Quad::combine_bounds)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
let instance_transform = self.transform();
let instance = self.one_instance().instance;
if let Some(element_id) = element_id {
let stroke_width = instance.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = instance.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
let click_targets = instance
.stroke_bezier_paths()
.map(fill)
.map(|subpath| ClickTarget::new(subpath, stroke_width))
.collect::<Vec<ClickTarget>>();
metadata.click_targets.insert(element_id, click_targets);
}
if let Some(upstream_graphic_group) = &instance.upstream_graphic_group {
footprint.transform *= instance_transform;
upstream_graphic_group.collect_metadata(metadata, footprint, None);
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for instance in self.instances() {
let stroke_width = instance.instance.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = instance.instance.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
click_targets.extend(instance.instance.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget::new(subpath, stroke_width)));
}
}
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext) {
use crate::vector::style::GradientType; use crate::vector::style::GradientType;
@ -552,7 +470,7 @@ impl GraphicElementRendered for VectorDataTable {
for instance in self.instances() { for instance in self.instances() {
let mut layer = false; let mut layer = false;
let multiplied_transform = parent_transform * instance.transform(); let multiplied_transform = parent_transform * *instance.transform;
let set_stroke_transform = instance let set_stroke_transform = instance
.instance .instance
.style .style
@ -667,6 +585,71 @@ impl GraphicElementRendered for VectorDataTable {
} }
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.instances()
.flat_map(|instance| {
let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default();
let miter_limit = instance.instance.style.stroke().map(|s| s.line_join_miter_limit).unwrap_or(1.);
let scale = transform.decompose_scale();
// We use the full line width here to account for different styles of line caps
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
})
.reduce(Quad::combine_bounds)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
let instance_transform = self.transform();
for instance in self.instances().map(|instance| instance.instance) {
if let Some(element_id) = element_id {
let stroke_width = instance.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = instance.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
let click_targets = instance
.stroke_bezier_paths()
.map(fill)
.map(|subpath| ClickTarget::new(subpath, stroke_width))
.collect::<Vec<ClickTarget>>();
metadata.click_targets.insert(element_id, click_targets);
}
if let Some(upstream_graphic_group) = &instance.upstream_graphic_group {
footprint.transform *= instance_transform;
upstream_graphic_group.collect_metadata(metadata, footprint, None);
}
}
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
for instance in self.instances() {
let stroke_width = instance.instance.style.stroke().as_ref().map_or(0., Stroke::weight);
let filled = instance.instance.style.fill() != &Fill::None;
let fill = |mut subpath: bezier_rs::Subpath<_>| {
if filled {
subpath.set_closed(true);
}
subpath
};
click_targets.extend(instance.instance.stroke_bezier_paths().map(fill).map(|subpath| {
let mut click_target = ClickTarget::new(subpath, stroke_width);
click_target.apply_transform(*instance.transform);
click_target
}));
}
}
fn new_ids_from_hash(&mut self, reference: Option<NodeId>) { fn new_ids_from_hash(&mut self, reference: Option<NodeId>) {
for instance in self.instances_mut() { for instance in self.instances_mut() {
instance.instance.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()); instance.instance.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default());
@ -674,9 +657,7 @@ impl GraphicElementRendered for VectorDataTable {
} }
fn to_graphic_element(&self) -> GraphicElement { fn to_graphic_element(&self) -> GraphicElement {
let instance = self.one_instance().instance; GraphicElement::VectorData(self.clone())
GraphicElement::VectorData(VectorDataTable::new(instance.clone()))
} }
} }
@ -727,37 +708,6 @@ impl GraphicElementRendered for Artboard {
); );
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip {
Some(artboard_bounds)
} else {
[self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds)
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, DAffine2::from_translation(self.location.as_dvec2())));
if self.clip {
metadata.clip_targets.insert(element_id);
}
}
footprint.transform *= self.transform();
self.graphic_group.collect_metadata(metadata, footprint, None);
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
if self.graphic_group.transform().matrix2.determinant() != 0. {
subpath.apply_transform(self.graphic_group.transform().inverse());
click_targets.push(ClickTarget::new(subpath, 0.));
}
}
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
use vello::peniko; use vello::peniko;
@ -783,6 +733,34 @@ impl GraphicElementRendered for Artboard {
} }
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box();
if self.clip {
Some(artboard_bounds)
} else {
[self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds)
}
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, DAffine2::from_translation(self.location.as_dvec2()));
if self.clip {
metadata.clip_targets.insert(element_id);
}
}
footprint.transform *= self.transform();
self.graphic_group.collect_metadata(metadata, footprint, None);
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath_rectangle = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
click_targets.push(ClickTarget::new(subpath_rectangle, 0.));
}
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
true true
} }
@ -795,6 +773,13 @@ impl GraphicElementRendered for ArtboardGroupTable {
} }
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
for instance in self.instances() {
instance.instance.render_to_vello(scene, transform, context)
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.instances().filter_map(|instance| instance.instance.bounding_box(transform)).reduce(Quad::combine_bounds) self.instances().filter_map(|instance| instance.instance.bounding_box(transform)).reduce(Quad::combine_bounds)
} }
@ -811,83 +796,48 @@ impl GraphicElementRendered for ArtboardGroupTable {
} }
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
for instance in self.instances() {
instance.instance.render_to_vello(scene, transform, context)
}
}
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
self.instances().count() > 0 self.instances().count() > 0
} }
} }
impl GraphicElementRendered for ImageFrameTable<Color> { impl GraphicElementRendered for ImageFrameTable<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
for instance in self.instances() { for instance in self.instances() {
let transform = instance.transform() * render.transform; let transform = *instance.transform * render.transform;
match render_params.image_render_mode { let image = &instance.instance;
ImageRenderMode::Base64 => { if image.data.is_empty() {
let image = &instance.instance; return;
if image.data.is_empty() {
return;
}
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
let output = image.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("href", base64_string);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if instance.alpha_blending.opacity < 1. {
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
}
if instance.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", instance.alpha_blending.blend_mode.render());
}
});
}
} }
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
let output = image.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("href", base64_string);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if instance.alpha_blending.opacity < 1. {
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
}
if instance.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", instance.alpha_blending.blend_mode.render());
}
});
} }
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.instances()
.flat_map(|instance| {
let transform = transform * instance.transform();
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let instance_transform = self.transform();
let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, instance_transform));
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new(subpath, 0.));
}
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) {
use vello::peniko; use vello::peniko;
@ -898,76 +848,45 @@ impl GraphicElementRendered for ImageFrameTable<Color> {
return; return;
} }
let image = vello::peniko::Image::new(image.to_flat_u8().0.into(), peniko::Format::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat); let image = vello::peniko::Image::new(image.to_flat_u8().0.into(), peniko::Format::Rgba8, image.width, image.height).with_extend(peniko::Extend::Repeat);
let transform = transform * instance.transform() * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); let transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64));
scene.draw_image(&image, vello::kurbo::Affine::new(transform.to_cols_array())); scene.draw_image(&image, vello::kurbo::Affine::new(transform.to_cols_array()));
} }
} }
}
impl GraphicElementRendered for RasterFrame {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let transform = self.transform() * render.transform;
match render_params.image_render_mode {
ImageRenderMode::Base64 => {
let image = match self {
RasterFrame::ImageFrame(ref image) => image,
RasterFrame::TextureFrame(_) => return,
};
for instance in image.instances() {
let (image, blending) = (&instance.instance, instance.alpha_blending);
if image.data.is_empty() {
return;
}
let base64_string = image.base64_string.clone().unwrap_or_else(|| {
let output = image.to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
render.leaf_tag("image", |attributes| {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("href", base64_string);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if blending.opacity < 1. {
attributes.push("opacity", blending.opacity.to_string());
}
if blending.blend_mode != BlendMode::default() {
attributes.push("style", blending.blend_mode.render());
}
});
}
}
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let transform = transform * self.transform(); self.instances()
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) .flat_map(|instance| {
let transform = transform * *instance.transform;
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
})
.reduce(Quad::combine_bounds)
} }
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return }; let instance_transform = self.transform();
let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]); metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, self.transform())); metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, instance_transform);
} }
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new(subpath, 0.)); click_targets.push(ClickTarget::new(subpath, 0.));
} }
}
impl GraphicElementRendered for RasterFrame {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
RasterFrame::ImageFrame(ref image) => image.render_svg(render, render_params),
RasterFrame::TextureFrame(_) => unimplemented!(),
}
}
#[cfg(feature = "vello")] #[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
@ -1016,6 +935,25 @@ impl GraphicElementRendered for RasterFrame {
} }
} }
} }
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
let transform = transform * self.transform();
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
}
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, self.transform());
}
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
click_targets.push(ClickTarget::new(subpath, 0.));
}
} }
impl GraphicElementRendered for GraphicElement { impl GraphicElementRendered for GraphicElement {
@ -1027,6 +965,15 @@ impl GraphicElementRendered for GraphicElement {
} }
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
match self {
GraphicElement::VectorData(vector_data) => vector_data.render_to_vello(scene, transform, context),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_to_vello(scene, transform, context),
GraphicElement::RasterFrame(raster) => raster.render_to_vello(scene, transform, context),
}
}
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
match self { match self {
GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform), GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform),
@ -1037,7 +984,19 @@ impl GraphicElementRendered for GraphicElement {
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
if let Some(element_id) = element_id { if let Some(element_id) = element_id {
metadata.footprints.insert(element_id, (footprint, self.transform())); match self {
GraphicElement::GraphicGroup(_) => {
metadata.upstream_footprints.insert(element_id, footprint);
}
GraphicElement::VectorData(vector_data) => {
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, vector_data.transform());
}
GraphicElement::RasterFrame(raster_frame) => {
metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, raster_frame.transform());
}
}
} }
match self { match self {
@ -1055,15 +1014,6 @@ impl GraphicElementRendered for GraphicElement {
} }
} }
#[cfg(feature = "vello")]
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
match self {
GraphicElement::VectorData(vector_data) => vector_data.render_to_vello(scene, transform, context),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_to_vello(scene, transform, context),
GraphicElement::RasterFrame(raster) => raster.render_to_vello(scene, transform, context),
}
}
fn contains_artboard(&self) -> bool { fn contains_artboard(&self) -> bool {
match self { match self {
GraphicElement::VectorData(vector_data) => vector_data.contains_artboard(), GraphicElement::VectorData(vector_data) => vector_data.contains_artboard(),
@ -1095,7 +1045,7 @@ fn text_attributes(attributes: &mut SvgRenderAttrs) {
attributes.push("font-size", "30"); attributes.push("font-size", "30");
} }
impl<T: Primitive> GraphicElementRendered for T { impl<P: Primitive> GraphicElementRendered for P {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
render.parent_tag("text", text_attributes, |render| render.leaf_node(format!("{self}"))); render.parent_tag("text", text_attributes, |render| render.leaf_node(format!("{self}")));
} }

View file

@ -1,14 +1,14 @@
use crate::application_io::{ImageTexture, TextureFrameTable}; use crate::application_io::TextureFrameTable;
use crate::raster::image::{Image, ImageFrameTable}; use crate::raster::image::{Image, ImageFrameTable};
use crate::raster::Pixel; use crate::raster::Pixel;
use crate::transform::{Transform, TransformMut}; use crate::transform::{Transform, TransformMut};
use crate::uuid::NodeId; use crate::uuid::NodeId;
use crate::vector::{InstanceId, VectorData, VectorDataTable}; use crate::vector::{InstanceId, VectorDataTable};
use crate::{AlphaBlending, GraphicElement, GraphicGroup, GraphicGroupTable, RasterFrame}; use crate::{AlphaBlending, GraphicElement, RasterFrame};
use dyn_any::StaticType; use dyn_any::StaticType;
use glam::{DAffine2, DVec2}; use glam::DAffine2;
use std::hash::Hash; use std::hash::Hash;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -61,6 +61,25 @@ impl<T> Instances<T> {
} }
} }
pub fn push_instance(&mut self, instance: Instance<T>) -> InstanceMut<T>
where
T: Clone,
{
self.id.push(*instance.id);
self.instance.push(instance.instance.clone());
self.transform.push(*instance.transform);
self.alpha_blending.push(*instance.alpha_blending);
self.source_node_id.push(*instance.source_node_id);
InstanceMut {
id: self.id.last_mut().expect("Shouldn't be empty"),
instance: self.instance.last_mut().expect("Shouldn't be empty"),
transform: self.transform.last_mut().expect("Shouldn't be empty"),
alpha_blending: self.alpha_blending.last_mut().expect("Shouldn't be empty"),
source_node_id: self.source_node_id.last_mut().expect("Shouldn't be empty"),
}
}
pub fn one_instance(&self) -> Instance<T> { pub fn one_instance(&self) -> Instance<T> {
Instance { Instance {
id: self.id.first().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", self.instance.len())), id: self.id.first().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED, FOUND {}", self.instance.len())),
@ -83,8 +102,7 @@ impl<T> Instances<T> {
} }
} }
pub fn instances(&self) -> impl Iterator<Item = Instance<T>> { pub fn instances(&self) -> impl DoubleEndedIterator<Item = Instance<T>> {
// assert!(self.instance.len() == 1, "ONE INSTANCE EXPECTED, FOUND {} (instances)", self.instance.len());
self.id self.id
.iter() .iter()
.zip(self.instance.iter()) .zip(self.instance.iter())
@ -100,8 +118,7 @@ impl<T> Instances<T> {
}) })
} }
pub fn instances_mut(&mut self) -> impl Iterator<Item = InstanceMut<T>> { pub fn instances_mut(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<T>> {
// assert!(self.instance.len() == 1, "ONE INSTANCE EXPECTED, FOUND {} (instances_mut)", self.instance.len());
self.id self.id
.iter_mut() .iter_mut()
.zip(self.instance.iter_mut()) .zip(self.instance.iter_mut())
@ -120,8 +137,11 @@ impl<T> Instances<T> {
impl<T: Default + Hash + 'static> Default for Instances<T> { impl<T: Default + Hash + 'static> Default for Instances<T> {
fn default() -> Self { fn default() -> Self {
// TODO: Remove once all types have been converted to tables
let converted_to_tables = [TypeId::of::<crate::Artboard>(), TypeId::of::<crate::GraphicElement>()];
use core::any::TypeId; use core::any::TypeId;
if TypeId::of::<T>() == TypeId::of::<crate::Artboard>() { if converted_to_tables.contains(&TypeId::of::<T>()) {
// TODO: Remove the 'static trait bound when this special casing is removed by making all types return empty // TODO: Remove the 'static trait bound when this special casing is removed by making all types return empty
Self::empty() Self::empty()
} else { } else {
@ -177,104 +197,27 @@ pub struct InstanceMut<'a, T> {
pub source_node_id: &'a mut Option<NodeId>, pub source_node_id: &'a mut Option<NodeId>,
} }
// GRAPHIC ELEMENT // VECTOR DATA TABLE
impl Transform for GraphicElement { impl Transform for VectorDataTable {
fn transform(&self) -> DAffine2 { fn transform(&self) -> DAffine2 {
match self { *self.one_instance().transform
GraphicElement::GraphicGroup(group) => group.transform(),
GraphicElement::VectorData(vector_data) => vector_data.transform(),
GraphicElement::RasterFrame(frame) => frame.transform(),
}
} }
} }
impl TransformMut for GraphicElement { impl TransformMut for VectorDataTable {
fn transform_mut(&mut self) -> &mut DAffine2 { fn transform_mut(&mut self) -> &mut DAffine2 {
match self { self.one_instance_mut().transform
GraphicElement::GraphicGroup(group) => group.transform_mut(),
GraphicElement::VectorData(vector_data) => vector_data.transform_mut(),
GraphicElement::RasterFrame(frame) => frame.transform_mut(),
}
}
}
// GRAPHIC GROUP
impl Transform for Instance<'_, GraphicGroup> {
fn transform(&self) -> DAffine2 {
*self.transform
}
}
impl Transform for InstanceMut<'_, GraphicGroup> {
fn transform(&self) -> DAffine2 {
*self.transform
}
}
impl TransformMut for InstanceMut<'_, GraphicGroup> {
fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform
}
}
// GRAPHIC GROUP TABLE
impl Transform for GraphicGroupTable {
fn transform(&self) -> DAffine2 {
self.one_instance().transform()
}
}
impl TransformMut for GraphicGroupTable {
fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED"))
}
}
// IMAGE TEXTURE
impl Transform for Instance<'_, ImageTexture> {
fn transform(&self) -> DAffine2 {
*self.transform
}
}
impl Transform for InstanceMut<'_, ImageTexture> {
fn transform(&self) -> DAffine2 {
*self.transform
}
}
impl TransformMut for InstanceMut<'_, ImageTexture> {
fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform
} }
} }
// TEXTURE FRAME TABLE // TEXTURE FRAME TABLE
impl Transform for TextureFrameTable { impl Transform for TextureFrameTable {
fn transform(&self) -> DAffine2 { fn transform(&self) -> DAffine2 {
self.one_instance().transform() *self.one_instance().transform
} }
} }
impl TransformMut for TextureFrameTable { impl TransformMut for TextureFrameTable {
fn transform_mut(&mut self) -> &mut DAffine2 { fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED")) self.one_instance_mut().transform
}
}
// IMAGE
impl<P: Pixel> Transform for Instance<'_, Image<P>> {
fn transform(&self) -> DAffine2 {
*self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.transform.transform_point2(pivot)
}
}
impl<P: Pixel> Transform for InstanceMut<'_, Image<P>> {
fn transform(&self) -> DAffine2 {
*self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.transform.transform_point2(pivot)
}
}
impl<P: Pixel> TransformMut for InstanceMut<'_, Image<P>> {
fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform
} }
} }
@ -284,7 +227,7 @@ where
GraphicElement: From<Image<P>>, GraphicElement: From<Image<P>>,
{ {
fn transform(&self) -> DAffine2 { fn transform(&self) -> DAffine2 {
self.one_instance().transform() *self.one_instance().transform
} }
} }
impl<P: Pixel> TransformMut for ImageFrameTable<P> impl<P: Pixel> TransformMut for ImageFrameTable<P>
@ -292,42 +235,7 @@ where
GraphicElement: From<Image<P>>, GraphicElement: From<Image<P>>,
{ {
fn transform_mut(&mut self) -> &mut DAffine2 { fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED")) self.one_instance_mut().transform
}
}
// VECTOR DATA
impl Transform for Instance<'_, VectorData> {
fn transform(&self) -> DAffine2 {
*self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.transform.transform_point2(self.instance.layerspace_pivot(pivot))
}
}
impl Transform for InstanceMut<'_, VectorData> {
fn transform(&self) -> DAffine2 {
*self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.transform.transform_point2(self.instance.layerspace_pivot(pivot))
}
}
impl TransformMut for InstanceMut<'_, VectorData> {
fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform
}
}
// VECTOR DATA TABLE
impl Transform for VectorDataTable {
fn transform(&self) -> DAffine2 {
self.one_instance().transform()
}
}
impl TransformMut for VectorDataTable {
fn transform_mut(&mut self) -> &mut DAffine2 {
self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED"))
} }
} }
@ -343,8 +251,8 @@ impl Transform for RasterFrame {
impl TransformMut for RasterFrame { impl TransformMut for RasterFrame {
fn transform_mut(&mut self) -> &mut DAffine2 { fn transform_mut(&mut self) -> &mut DAffine2 {
match self { match self {
RasterFrame::ImageFrame(image_frame) => image_frame.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED")), RasterFrame::ImageFrame(image_frame) => image_frame.transform_mut(),
RasterFrame::TextureFrame(texture_frame) => texture_frame.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED")), RasterFrame::TextureFrame(texture_frame) => texture_frame.transform_mut(),
} }
} }
} }

View file

@ -335,6 +335,7 @@ fn blend_mode<T: SetBlendMode>(
mut value: T, mut value: T,
blend_mode: BlendMode, blend_mode: BlendMode,
) -> T { ) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode); value.set_blend_mode(blend_mode);
value value
} }
@ -350,7 +351,7 @@ fn opacity<T: MultiplyAlpha>(
mut value: T, mut value: T,
#[default(100.)] factor: Percentage, #[default(100.)] factor: Percentage,
) -> T { ) -> T {
let opacity_multiplier = factor / 100.; // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity_multiplier); value.multiply_alpha(factor / 100.);
value value
} }

View file

@ -1583,7 +1583,7 @@ mod test {
let opacity = 100_f64; let opacity = 100_f64;
let result = super::color_overlay((), ImageFrameTable::new(image.clone()), overlay_color, BlendMode::Multiply, opacity); let result = super::color_overlay((), ImageFrameTable::new(image.clone()), overlay_color, BlendMode::Multiply, opacity);
let result = result.one_instance().instance; let result = result.instances().next().unwrap().instance;
// The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0) // The output should just be the original green and alpha channels (as we multiply them by 1 and other channels by 0)
assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a())); assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a()));

View file

@ -1,4 +1,5 @@
use crate::application_io::TextureFrameTable; use crate::application_io::TextureFrameTable;
use crate::instances::Instances;
use crate::raster::bbox::AxisAlignedBbox; use crate::raster::bbox::AxisAlignedBbox;
use crate::raster::image::ImageFrameTable; use crate::raster::image::ImageFrameTable;
use crate::vector::VectorDataTable; use crate::vector::VectorDataTable;
@ -90,8 +91,6 @@ pub struct Footprint {
pub resolution: glam::UVec2, pub resolution: glam::UVec2,
/// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient /// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient
pub quality: RenderQuality, pub quality: RenderQuality,
/// When the transform is set downstream, all upstream modifications have to be ignored
pub ignore_modifications: bool,
} }
impl Default for Footprint { impl Default for Footprint {
@ -106,7 +105,6 @@ impl Footprint {
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
resolution: glam::UVec2::new(1920, 1080), resolution: glam::UVec2::new(1920, 1080),
quality: RenderQuality::Full, quality: RenderQuality::Full,
ignore_modifications: false,
} }
} }
pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox { pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
@ -156,7 +154,7 @@ impl ApplyTransform for () {
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn transform<T: 'n + TransformMut + 'static>( async fn transform<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations( #[implementations(
Context -> VectorDataTable, Context -> VectorDataTable,
@ -164,39 +162,40 @@ async fn transform<T: 'n + TransformMut + 'static>(
Context -> ImageFrameTable<Color>, Context -> ImageFrameTable<Color>,
Context -> TextureFrameTable, Context -> TextureFrameTable,
)] )]
transform_target: impl Node<Context<'static>, Output = T>, transform_target: impl Node<Context<'static>, Output = Instances<T>>,
translate: DVec2, translate: DVec2,
rotate: f64, rotate: f64,
scale: DVec2, scale: DVec2,
shear: DVec2, shear: DVec2,
_pivot: DVec2, _pivot: DVec2,
) -> T { ) -> Instances<T> {
let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]); let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied(); let footprint = ctx.try_footprint().copied();
let mut ctx = OwnedContextImpl::from(ctx); let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint { if let Some(mut footprint) = footprint {
if !footprint.ignore_modifications { footprint.apply_transform(&matrix);
footprint.apply_transform(&modification);
}
ctx = ctx.with_footprint(footprint); ctx = ctx.with_footprint(footprint);
} }
let mut transform_target = transform_target.eval(ctx.into_context()).await; let mut transform_target = transform_target.eval(ctx.into_context()).await;
let data_transform = transform_target.transform_mut(); for data_transform in transform_target.instances_mut() {
*data_transform = modification * (*data_transform); *data_transform.transform = matrix * *data_transform.transform;
}
transform_target transform_target
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
fn replace_transform<Data: TransformMut, TransformInput: Transform>( fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx, _: impl Ctx,
#[implementations(VectorDataTable, ImageFrameTable<Color>, GraphicGroupTable)] mut data: Data, #[implementations(VectorDataTable, ImageFrameTable<Color>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(DAffine2)] transform: TransformInput, #[implementations(DAffine2)] transform: TransformInput,
) -> Data { ) -> Instances<Data> {
let data_transform = data.transform_mut(); for data_transform in data.instances_mut() {
*data_transform = transform.transform(); *data_transform.transform = transform.transform();
}
data data
} }

View file

@ -147,6 +147,8 @@ impl Gradient {
/// Adds the gradient def through mutating the first argument, returning the gradient ID. /// 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 { 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 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]); 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 core::borrow::Borrow;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use std::collections::HashMap;
// TODO: Eventually remove this migration document upgrade code // TODO: Eventually remove this migration document upgrade code
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> { 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, 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 { impl Default for VectorData {

View file

@ -1,6 +1,4 @@
use crate::transform::Transform; use crate::vector::vector_data::{HandleId, VectorData};
use crate::vector::vector_data::{HandleId, VectorData, VectorDataTable};
use crate::vector::ConcatElement;
use dyn_any::DynAny; use dyn_any::DynAny;
@ -166,16 +164,16 @@ impl PointDomain {
self.id.iter().position(|&check_id| check_id == id) 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.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))); 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)); 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 { for pos in &mut self.position {
*pos = transform.transform_point2(*pos); *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.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.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)); 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); 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)); 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 { for handles in &mut self.handles {
*handles = handles.apply_transformation(|p| transform.transform_point2(p)); *handles = handles.apply_transformation(|p| transform.transform_point2(p));
} }
@ -474,7 +472,7 @@ impl RegionDomain {
&self.fill &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.id.extend(other.id.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
self.segment_range.extend( self.segment_range.extend(
other other
@ -485,7 +483,7 @@ impl RegionDomain {
self.fill.extend(&other.fill); 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.id.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
self.segment_range self.segment_range
.iter_mut() .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. /// Represents the conversion of ids used when concatenating vector data with conflicting ids.
struct IdMap { pub struct IdMap {
point_offset: usize, pub point_offset: usize,
point_map: HashMap<PointId, PointId>, pub point_map: HashMap<PointId, PointId>,
segment_map: HashMap<SegmentId, SegmentId>, pub segment_map: HashMap<SegmentId, SegmentId>,
region_map: HashMap<RegionId, RegionId>, pub region_map: HashMap<RegionId, RegionId>,
} }

View file

@ -1,7 +1,7 @@
use super::misc::CentroidType; use super::misc::CentroidType;
use super::style::{Fill, Gradient, GradientStops, Stroke}; use super::style::{Fill, Gradient, GradientStops, Stroke};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable}; 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::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
use crate::renderer::GraphicElementRendered; use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut}; 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. /// 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 /// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
trait VectorIterMut { trait VectorDataTableIterMut {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)>; fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>>;
} }
impl VectorIterMut for GraphicGroupTable { impl VectorDataTableIterMut for GraphicGroupTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> { fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
let parent_transform = self.transform();
let instance = self.one_instance_mut().instance;
// Grab only the direct children // Grab only the direct children
instance.iter_mut().filter_map(|(element, _)| element.as_vector_data_mut()).map(move |vector_data| { self.instances_mut()
let transform = parent_transform * vector_data.transform(); .filter_map(|element| element.instance.as_vector_data_mut())
.flat_map(move |vector_data| vector_data.instances_mut())
let vector_data_instance = vector_data.one_instance_mut().instance;
(vector_data_instance, transform)
})
} }
} }
impl VectorIterMut for VectorDataTable { impl VectorDataTableIterMut for VectorDataTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> { fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
self.instances_mut().map(|instance| { self.instances_mut()
let transform = instance.transform();
(instance.instance, transform)
})
} }
} }
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T: VectorIterMut>( async fn assign_colors<T>(
_: impl Ctx, _: impl Ctx,
#[implementations(GraphicGroupTable, VectorDataTable)] #[implementations(GraphicGroupTable, VectorDataTable)]
#[widget(ParsedWidgetOverride::Hidden)] #[widget(ParsedWidgetOverride::Hidden)]
/// The vector elements, or group of vector elements, to apply the fill and/or stroke style to.
mut vector_group: T, mut vector_group: T,
#[default(true)] fill: bool, #[default(true)]
/// Whether to style the fill.
fill: bool,
/// Whether to style the stroke.
stroke: bool, 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, reverse: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")] randomize: bool, #[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")]
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] seed: SeedValue, /// Whether to randomize the color selection for each element from throughout the gradient.
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32, randomize: bool,
) -> T { #[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 length = vector_group.vector_iter_mut().count();
let gradient = if reverse { gradient.reversed() } else { gradient }; let gradient = if reverse { gradient.reversed() } else { gradient };
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); 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 { let factor = match randomize {
true => rng.random::<f64>(), true => rng.random::<f64>(),
false => match repeat_every { false => match repeat_every {
@ -76,11 +82,11 @@ async fn assign_colors<T: VectorIterMut>(
let color = gradient.evalute(factor); let color = gradient.evalute(factor);
if fill { if fill {
vector_data.style.set_fill(Fill::Solid(color)); vector_data.instance.style.set_fill(Fill::Solid(color));
} }
if stroke { if stroke {
if let Some(stroke) = vector_data.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) { if let Some(stroke) = vector_data.instance.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
vector_data.style.set_stroke(stroke); 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"))] #[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, _: impl Ctx,
#[implementations( #[implementations(
VectorDataTable, VectorDataTable,
@ -101,7 +107,8 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
GraphicGroupTable, GraphicGroupTable,
GraphicGroupTable GraphicGroupTable
)] )]
mut vector_data: TargetTy, /// The vector elements, or group of vector elements, to apply the fill to.
mut vector_data: V,
#[implementations( #[implementations(
Fill, Fill,
Option<Color>, Option<Color>,
@ -113,22 +120,33 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
Gradient, Gradient,
)] )]
#[default(Color::BLACK)] #[default(Color::BLACK)]
fill: FillTy, /// The fill to paint the path with.
fill: F,
_backup_color: Option<Color>, _backup_color: Option<Color>,
_backup_gradient: Gradient, _backup_gradient: Gradient,
) -> TargetTy { ) -> V
where
V: VectorDataTableIterMut + 'n + Send,
{
let fill: Fill = fill.into(); let fill: Fill = fill.into();
for (target, _transform) in vector_data.vector_iter_mut() { for vector in vector_data.vector_iter_mut() {
target.style.set_fill(fill.clone()); let mut fill = fill.clone();
if let Fill::Gradient(gradient) = &mut fill {
gradient.transform *= *vector.transform;
}
vector.instance.style.set_fill(fill);
} }
vector_data 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"))] #[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, _: 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( #[implementations(
Option<Color>, Option<Color>,
Color, Color,
@ -136,14 +154,26 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
Color, Color,
)] )]
#[default(Color::BLACK)] #[default(Color::BLACK)]
color: ColorTy, /// The stroke color.
#[default(2.)] weight: f64, 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>, dash_lengths: Vec<f64>,
/// The offset distance from the starting point of the dash pattern.
dash_offset: f64, dash_offset: f64,
/// The shape of the stroke at open endpoints.
line_cap: crate::vector::style::LineCap, line_cap: crate::vector::style::LineCap,
/// The curvature of the bent stroke at sharp corners.
line_join: LineJoin, line_join: LineJoin,
#[default(4.)] miter_limit: f64, #[default(4.)]
) -> TargetTy { /// 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 { let stroke = Stroke {
color: color.into(), color: color.into(),
weight, weight,
@ -154,110 +184,111 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
line_join_miter_limit: miter_limit, line_join_miter_limit: miter_limit,
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
}; };
for (target, transform) in vector_data.vector_iter_mut() { for vector in vector_data.vector_iter_mut() {
target.style.set_stroke(Stroke { transform, ..stroke.clone() }); let mut stroke = stroke.clone();
stroke.transform *= *vector.transform;
vector.instance.style.set_stroke(stroke);
} }
vector_data vector_data
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[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, _: impl Ctx,
// TODO: Implement other GraphicElementRendered types. // TODO: Implement other GraphicElementRendered types.
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I, #[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
#[default(100., 100.)] #[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. // 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, direction: DVec2,
angle: Angle, angle: Angle,
#[default(4)] instances: IntegerCount, #[default(4)] instances: IntegerCount,
) -> GraphicGroupTable { ) -> GraphicGroupTable
let first_vector_transform = instance.transform(); where
Instances<I>: GraphicElementRendered,
{
let angle = angle.to_radians(); let angle = angle.to_radians();
let instances = instances.max(1); let instances = instances.max(1);
let total = (instances - 1) as f64; let total = (instances - 1) as f64;
let mut result_table = GraphicGroupTable::default(); 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 Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
let center = (bounding_box[0] + bounding_box[1]) / 2.; let center = (bounding_box[0] + bounding_box[1]) / 2.;
for i in 0..instances { for index in 0..instances {
let translation = i as f64 * direction / total; let angle = index as f64 * angle / total;
let angle = i as f64 * angle / total; let translation = index as f64 * direction / 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 modification = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
let data_transform = new_instance.transform_mut(); let mut new_graphic_element = instance.to_graphic_element().clone();
*data_transform = modification * first_vector_transform; new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
result.push((new_instance, None));
let new_instance = result_table.push(new_graphic_element);
*new_instance.transform = modification;
} }
result_table result_table
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[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, _: impl Ctx,
// TODO: Implement other GraphicElementRendered types. // TODO: Implement other GraphicElementRendered types.
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I, #[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
angle_offset: Angle, angle_offset: Angle,
#[default(5)] radius: f64, #[default(5)] radius: f64,
#[default(5)] instances: IntegerCount, #[default(5)] instances: IntegerCount,
) -> GraphicGroupTable { ) -> GraphicGroupTable
let first_vector_transform = instance.transform(); where
Instances<I>: GraphicElementRendered,
{
let instances = instances.max(1); let instances = instances.max(1);
let mut result_table = GraphicGroupTable::default(); 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 Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
let center = (bounding_box[0] + bounding_box[1]) / 2.; let center = (bounding_box[0] + bounding_box[1]) / 2.;
let base_transform = DVec2::new(0., radius) - center; let base_transform = DVec2::new(0., radius) - center;
for i in 0..instances { for index in 0..instances {
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians(); let rotation = DAffine2::from_angle((std::f64::consts::TAU / instances as f64) * index 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 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(); let mut new_graphic_element = instance.to_graphic_element().clone();
*data_transform = modification * first_vector_transform; new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
result.push((new_instance, None));
let new_instance = result_table.push(new_graphic_element);
*new_instance.transform = modification;
} }
result_table result_table
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[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, _: impl Ctx,
points: VectorDataTable, points: VectorDataTable,
#[expose] #[expose]
#[implementations(VectorDataTable, GraphicGroupTable)] #[implementations(VectorDataTable, GraphicGroupTable)]
instance: I, instance: Instances<I>,
#[default(1)] random_scale_min: f64, #[default(1)] random_scale_min: f64,
#[default(1)] random_scale_max: f64, #[default(1)] random_scale_max: f64,
random_scale_bias: f64, random_scale_bias: f64,
random_scale_seed: SeedValue, random_scale_seed: SeedValue,
random_rotation: Angle, random_rotation: Angle,
random_rotation_seed: SeedValue, random_rotation_seed: SeedValue,
) -> GraphicGroupTable { ) -> GraphicGroupTable
where
Instances<I>: GraphicElementRendered,
{
let points_transform = points.transform(); let points_transform = points.transform();
let points = points.one_instance().instance; let points_list = points.instances().flat_map(|element| element.instance.point_domain.positions());
let instance_transform = instance.transform();
let random_scale_difference = random_scale_max - random_scale_min; 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_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]); 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 do_rotation = random_rotation.abs() > 1e-6;
let mut result_table = GraphicGroupTable::default(); 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 center_transform = DAffine2::from_translation(instance_center);
let translation = points_transform.transform_point2(point); let translation = points_transform.transform_point2(point);
@ -296,12 +326,11 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
random_scale_min random_scale_min
}; };
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element()); let mut new_graphic_element = instance.to_graphic_element().clone();
new_instance.new_ids_from_hash(None); new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
let data_transform = new_instance.transform_mut(); let new_instance = result_table.push(new_graphic_element);
*data_transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform * instance_transform; *new_instance.transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform;
result.push((new_instance, None));
} }
result_table 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 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 // 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. // 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>) { fn flatten_group(graphic_group_table: &GraphicGroupTable, output: &mut InstanceMut<VectorData>) {
for (element, reference) in graphic_group_table.one_instance().instance.iter() { for current_element in graphic_group_table.instances() {
match element { match current_element.instance {
GraphicElement::VectorData(vector_data) => { GraphicElement::VectorData(vector_data_table) => {
for instance in vector_data.instances() { // Loop through every row of the VectorDataTable and concatenate each instance's subpath into the output VectorData instance.
*result.alpha_blending = *instance.alpha_blending; for vector_data_instance in vector_data_table.instances() {
result let other = vector_data_instance.instance;
.instance let transform = *current_element.transform * *vector_data_instance.transform;
.concat(instance.instance, current_transform * instance.transform(), reference.map(|node_id| node_id.0).unwrap_or_default()); 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) => { 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 output_table = VectorDataTable::default();
let mut result_instance = result_table.one_instance_mut(); let Some(mut output) = output_table.instances_mut().next() else { return output_table };
// 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);
result_table flatten_group(&graphic_group_input, &mut output);
}
pub trait ConcatElement { output_table
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;
}
} }
#[node_macro::node(category(""), path(graphene_core::vector))] #[node_macro::node(category(""), path(graphene_core::vector))]
@ -1077,7 +1093,7 @@ mod test {
let instances = 3; let instances = 3;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await; 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 = 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); assert_eq!(vector_data.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() { 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); 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 instances = 8;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await; 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 = 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); assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() { 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); assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
} }
} }
#[tokio::test] #[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 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 = 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); assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() { for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.; let expected_angle = (index as f64 + 1.) * 45.;
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.; let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees(); 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] #[tokio::test]
async fn bounding_box() { async fn bounding_box() {
let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await; 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); assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_box.region_bezier_paths().next().unwrap().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.),]); 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 // Test a VectorData with non-zero rotation
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)); let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
let mut square = VectorDataTable::new(square); 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 { let bounding_box = BoundingBoxNode {
vector_data: FutureWrapperNode(square), vector_data: FutureWrapperNode(square),
} }
.eval(Footprint::default()) .eval(Footprint::default())
.await; .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); assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_box.region_bezier_paths().next().unwrap().1; let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
let sqrt2 = core::f64::consts::SQRT_2; let sqrt2 = core::f64::consts::SQRT_2;
@ -1136,15 +1155,19 @@ mod test {
async fn copy_to_points() { async fn copy_to_points() {
let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.); let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE); let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec(); 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 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 flatten_vector_elements = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flattened_copy_to_points.one_instance().instance; 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()); 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() { for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
let offset = expected_points[index]; let offset = expected_points[index];
assert_eq!( assert_eq!(
&subpath.anchors()[..4], &subpath.anchors(),
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),] &[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() { async fn sample_points() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); 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 = 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); 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.]) { 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}"); assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
@ -1163,7 +1186,7 @@ mod test {
async fn adaptive_spacing() { async fn adaptive_spacing() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)); 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 = 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); 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.]) { 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}"); assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
@ -1178,7 +1201,7 @@ mod test {
0, 0,
) )
.await; .await;
let sample_points = sample_points.one_instance().instance; let sample_points = sample_points.instances().next().unwrap().instance;
assert!( assert!(
(20..=40).contains(&sample_points.point_domain.positions().len()), (20..=40).contains(&sample_points.point_domain.positions().len()),
"actual len {}", "actual len {}",
@ -1197,7 +1220,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn spline() { async fn spline() {
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await; 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.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.)]); 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 source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO); 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 = 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!( assert_eq!(
&sample_points.point_domain.positions()[..4], &sample_points.point_domain.positions()[..4],
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)] 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() { async fn bevel_rect() {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.); let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.); 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.point_domain.positions().len(), 8);
assert_eq!(beveled.segment_domain.ids().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 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 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 = 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.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3); assert_eq!(beveled.segment_domain.ids().len(), 3);
@ -1273,11 +1296,10 @@ mod test {
let vector_data = VectorData::from_subpath(source); let vector_data = VectorData::from_subpath(source);
let mut vector_data_table = VectorDataTable::new(vector_data.clone()); 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 = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
*vector_data_table.one_instance_mut().transform_mut() = transform;
let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.); 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.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3); assert_eq!(beveled.segment_domain.ids().len(), 3);
@ -1295,7 +1317,7 @@ mod test {
async fn bevel_too_high() { 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 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 = 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.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5); 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 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 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 = 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.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5); assert_eq!(beveled.segment_domain.ids().len(), 5);

View file

@ -13,44 +13,53 @@ use std::ops::Mul;
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable { async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable {
fn vector_from_image<T: Transform>(image_frame: T) -> VectorDataTable { fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> Vec<VectorDataTable> {
let corner1 = DVec2::ZERO; graphic_group_table
let corner2 = DVec2::new(1., 1.); .instances()
.map(|element| match element.instance.clone() {
GraphicElement::VectorData(mut vector_data) => {
// Apply the parent group's transform to each element of vector data
for sub_vector_data in vector_data.instances_mut() {
*sub_vector_data.transform = *element.transform * *sub_vector_data.transform;
}
let mut subpath = Subpath::new_rect(corner1, corner2); vector_data
subpath.apply_transform(image_frame.transform()); }
GraphicElement::RasterFrame(mut image) => {
// Apply the parent group's transform to each element of raster data
match &mut image {
graphene_core::RasterFrame::ImageFrame(image) => {
for instance in image.instances_mut() {
*instance.transform = *element.transform * *instance.transform;
}
}
graphene_core::RasterFrame::TextureFrame(image) => {
for instance in image.instances_mut() {
*instance.transform = *element.transform * *instance.transform;
}
}
}
let mut vector_data = VectorData::from_subpath(subpath); // Convert the image frame into a rectangular subpath with the image's transform
vector_data.style.set_fill(Fill::Solid(Color::from_rgb_str("777777").unwrap().to_gamma_srgb())); let mut subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(image.transform());
VectorDataTable::new(vector_data) // Create a vector data table from the rectangular subpath, with a default black fill
} let mut vector_data = VectorData::from_subpath(subpath);
vector_data.style.set_fill(Fill::Solid(Color::BLACK));
VectorDataTable::new(vector_data)
}
GraphicElement::GraphicGroup(mut graphic_group) => {
// Apply the parent group's transform to each element of inner group
for sub_element in graphic_group.instances_mut() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
fn union_vector_data(graphic_element: &GraphicElement) -> VectorDataTable { // Recursively flatten the inner group into vector data
match graphic_element { boolean_operation_on_vector_data(&flatten_vector_data(&graphic_group), BooleanOperation::Union)
GraphicElement::VectorData(vector_data) => vector_data.clone(), }
// Union all vector data in the graphic group into a single vector })
GraphicElement::GraphicGroup(graphic_group) => { .collect()
let vector_data = collect_vector_data(graphic_group);
boolean_operation_on_vector_data(&vector_data, BooleanOperation::Union)
}
GraphicElement::RasterFrame(image) => vector_from_image(image),
}
}
fn collect_vector_data(graphic_group_table: &GraphicGroupTable) -> Vec<VectorDataTable> {
let graphic_group = graphic_group_table.one_instance();
// Ensure all non vector data in the graphic group is converted to vector data
let vector_data_tables = graphic_group.instance.iter().map(|(element, _)| union_vector_data(element));
// Apply the transform from the parent graphic group
let transformed_vector_data = vector_data_tables.map(|mut vector_data_table| {
*vector_data_table.transform_mut() = graphic_group.transform() * vector_data_table.transform();
vector_data_table
});
transformed_vector_data.collect::<Vec<_>>()
} }
fn subtract<'a>(vector_data: impl Iterator<Item = &'a VectorDataTable>) -> VectorDataTable { fn subtract<'a>(vector_data: impl Iterator<Item = &'a VectorDataTable>) -> VectorDataTable {
@ -162,12 +171,12 @@ async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, opera
#[allow(unused_unsafe)] #[allow(unused_unsafe)]
let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) }; let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
let mut boolean_intersection_result = VectorDataTable::new(from_path(&boolean_intersection_string)); let mut boolean_intersection_result = VectorDataTable::new(from_path(&boolean_intersection_string));
*boolean_intersection_result.transform_mut() = all_other_vector_data_instance.transform(); *boolean_intersection_result.transform_mut() = *all_other_vector_data_instance.transform;
boolean_intersection_result.one_instance_mut().instance.style = all_other_vector_data_instance.instance.style.clone(); boolean_intersection_result.one_instance_mut().instance.style = all_other_vector_data_instance.instance.style.clone();
*boolean_intersection_result.one_instance_mut().alpha_blending = *all_other_vector_data_instance.alpha_blending; *boolean_intersection_result.one_instance_mut().alpha_blending = *all_other_vector_data_instance.alpha_blending;
let transform_of_lower_into_space_of_upper = boolean_intersection_result.one_instance_mut().transform().inverse() * any_intersection.transform(); let transform_of_lower_into_space_of_upper = boolean_intersection_result.one_instance_mut().transform.inverse() * any_intersection.transform();
let upper_path_string = to_path(boolean_intersection_result.one_instance_mut().instance, DAffine2::IDENTITY); let upper_path_string = to_path(boolean_intersection_result.one_instance_mut().instance, DAffine2::IDENTITY);
let lower_path_string = to_path(any_intersection.one_instance_mut().instance, transform_of_lower_into_space_of_upper); let lower_path_string = to_path(any_intersection.one_instance_mut().instance, transform_of_lower_into_space_of_upper);
@ -190,7 +199,7 @@ async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, opera
} }
// The first index is the bottom of the stack // The first index is the bottom of the stack
let mut result_vector_data_table = boolean_operation_on_vector_data(&collect_vector_data(&group_of_paths), operation); let mut result_vector_data_table = boolean_operation_on_vector_data(&flatten_vector_data(&group_of_paths), operation);
// Replace the transformation matrix with a mutation of the vector points themselves // Replace the transformation matrix with a mutation of the vector points themselves
let result_vector_data_table_transform = result_vector_data_table.transform(); let result_vector_data_table_transform = result_vector_data_table.transform();

View file

@ -5,10 +5,12 @@ pub use graph_craft::wasm_application_io::*;
use graphene_core::application_io::SurfaceHandle; use graphene_core::application_io::SurfaceHandle;
use graphene_core::application_io::{ApplicationIo, ExportFormat, RenderConfig}; use graphene_core::application_io::{ApplicationIo, ExportFormat, RenderConfig};
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use graphene_core::instances::Instances;
#[cfg(target_arch = "wasm32")]
use graphene_core::raster::bbox::Bbox; use graphene_core::raster::bbox::Bbox;
use graphene_core::raster::image::{Image, ImageFrameTable}; use graphene_core::raster::image::{Image, ImageFrameTable};
use graphene_core::renderer::RenderMetadata; use graphene_core::renderer::RenderMetadata;
use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, ImageRenderMode, RenderParams, RenderSvgSegmentList, SvgRender}; use graphene_core::renderer::{format_transform_matrix, GraphicElementRendered, RenderParams, RenderSvgSegmentList, SvgRender};
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use graphene_core::transform::TransformMut; use graphene_core::transform::TransformMut;
@ -149,19 +151,22 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
RenderOutputType::CanvasFrame(frame) RenderOutputType::CanvasFrame(frame)
} }
#[node_macro::node(category(""))]
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
async fn rasterize<T: GraphicElementRendered + graphene_core::transform::TransformMut + WasmNotSend + 'n>( #[node_macro::node(category(""))]
async fn rasterize<T: WasmNotSend + 'n>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
VectorDataTable, VectorDataTable,
ImageFrameTable<Color>, ImageFrameTable<Color>,
GraphicGroupTable, GraphicGroupTable,
)] )]
mut data: T, mut data: Instances<T>,
footprint: Footprint, footprint: Footprint,
surface_handle: Arc<SurfaceHandle<HtmlCanvasElement>>, surface_handle: Arc<SurfaceHandle<HtmlCanvasElement>>,
) -> ImageFrameTable<Color> { ) -> ImageFrameTable<Color>
where
Instances<T>: GraphicElementRendered,
{
if footprint.transform.matrix2.determinant() == 0. { if footprint.transform.matrix2.determinant() == 0. {
log::trace!("Invalid footprint received for rasterization"); log::trace!("Invalid footprint received for rasterization");
return ImageFrameTable::empty(); return ImageFrameTable::empty();
@ -176,7 +181,9 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
..Default::default() ..Default::default()
}; };
*data.transform_mut() = DAffine2::from_translation(-aabb.start) * data.transform(); for instance in data.instances_mut() {
*instance.transform = DAffine2::from_translation(-aabb.start) * *instance.transform;
}
data.render_svg(&mut render, &render_params); data.render_svg(&mut render, &render_params);
render.format_svg(glam::DVec2::ZERO, size); render.format_svg(glam::DVec2::ZERO, size);
let svg_string = render.svg.to_svg_string(); let svg_string = render.svg.to_svg_string();
@ -232,7 +239,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
ctx.footprint(); ctx.footprint();
let RenderConfig { hide_artboards, for_export, .. } = render_config; let RenderConfig { hide_artboards, for_export, .. } = render_config;
let render_params = RenderParams::new(render_config.view_mode, ImageRenderMode::Base64, None, false, hide_artboards, for_export); let render_params = RenderParams::new(render_config.view_mode, None, false, hide_artboards, for_export);
let data = data.eval(ctx.clone()).await; let data = data.eval(ctx.clone()).await;
let editor_api = editor_api.eval(ctx.clone()).await; let editor_api = editor_api.eval(ctx.clone()).await;
@ -245,7 +252,8 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
let use_vello = use_vello && surface_handle.is_some(); let use_vello = use_vello && surface_handle.is_some();
let mut metadata = RenderMetadata { let mut metadata = RenderMetadata {
footprints: HashMap::new(), upstream_footprints: HashMap::new(),
local_transforms: HashMap::new(),
click_targets: HashMap::new(), click_targets: HashMap::new(),
clip_targets: HashSet::new(), clip_targets: HashSet::new(),
}; };