mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
d2fc919ba6
commit
a696aae044
28 changed files with 856 additions and 930 deletions
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
|
@ -47,6 +47,7 @@ ignore = [
|
|||
"RUSTSEC-2024-0388", # Unmaintained but still fully functional crate `derivative`
|
||||
"RUSTSEC-2025-0007", # Unmaintained but still fully functional crate `ring`
|
||||
"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
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
|
|
|
@ -141,14 +141,18 @@ impl Dispatcher {
|
|||
};
|
||||
|
||||
let graphene_std::renderer::RenderMetadata {
|
||||
footprints,
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
click_targets,
|
||||
clip_targets,
|
||||
} = render_metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
let messages = [
|
||||
DocumentMessage::UpdateUpstreamTransforms { upstream_transforms: footprints },
|
||||
DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
},
|
||||
DocumentMessage::UpdateClickTargets { click_targets },
|
||||
DocumentMessage::UpdateClipTargets { clip_targets },
|
||||
];
|
||||
|
|
|
@ -179,7 +179,8 @@ pub enum DocumentMessage {
|
|||
ToggleOverlaysVisibility,
|
||||
ToggleSnapping,
|
||||
UpdateUpstreamTransforms {
|
||||
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
|
||||
upstream_footprints: HashMap<NodeId, Footprint>,
|
||||
local_transforms: HashMap<NodeId, DAffine2>,
|
||||
},
|
||||
UpdateClickTargets {
|
||||
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
|
||||
|
|
|
@ -1233,8 +1233,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
self.snapping_state.snapping_enabled = !self.snapping_state.snapping_enabled;
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
DocumentMessage::UpdateUpstreamTransforms { upstream_transforms } => {
|
||||
self.network_interface.update_transforms(upstream_transforms);
|
||||
DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints,
|
||||
local_transforms,
|
||||
} => {
|
||||
self.network_interface.update_transforms(upstream_footprints, local_transforms);
|
||||
}
|
||||
DocumentMessage::UpdateClickTargets { 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> {
|
||||
let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content)
|
||||
.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 {
|
||||
network_interface: NodeNetworkInterface::from_old_network(old_message_handler.network),
|
||||
collapsed: old_message_handler.collapsed,
|
||||
|
@ -2639,41 +2680,3 @@ impl Iterator for ClickXRayIter<'_> {
|
|||
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,
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
/// 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.
|
||||
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
|
||||
let output_layer = self.get_output_layer()?;
|
||||
|
||||
let upstream = self
|
||||
.network_interface
|
||||
.upstream_flow_back_from_nodes(vec![output_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow);
|
||||
|
||||
// Take until another layer node is found (but not the first layer node)
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
let existing_node_id = Self::locate_node_in_layer_chain(reference_name, output_layer, self.network_interface);
|
||||
|
||||
// Create a new node if the node does not exist and update its inputs
|
||||
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
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn create_node(&mut self, reference: &str) -> Option<NodeId> {
|
||||
let output_layer = self.get_output_layer()?;
|
||||
|
|
|
@ -2255,7 +2255,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
..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),
|
||||
],
|
||||
output_names: vec!["Data".to_string()],
|
||||
|
|
|
@ -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 graph_craft::document::NodeId;
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
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 click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
|
||||
pub clip_targets: HashSet<NodeId>,
|
||||
|
@ -29,7 +33,8 @@ pub struct DocumentMetadata {
|
|||
impl Default for DocumentMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
upstream_transforms: HashMap::new(),
|
||||
upstream_footprints: HashMap::new(),
|
||||
local_transforms: HashMap::new(),
|
||||
structure: HashMap::new(),
|
||||
vector_modify: HashMap::new(),
|
||||
click_targets: HashMap::new(),
|
||||
|
@ -77,14 +82,27 @@ impl DocumentMetadata {
|
|||
}
|
||||
|
||||
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
|
||||
self.upstream_transforms
|
||||
.get(&layer.to_node())
|
||||
.map(|(footprint, transform)| footprint.transform * *transform)
|
||||
.unwrap_or(self.document_to_viewport)
|
||||
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().unwrap_or_default();
|
||||
|
||||
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 {
|
||||
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 {
|
||||
|
@ -96,10 +114,10 @@ impl DocumentMetadata {
|
|||
return self.transform_to_viewport(layer);
|
||||
}
|
||||
|
||||
self.upstream_transforms
|
||||
self.upstream_footprints
|
||||
.get(&layer.to_node())
|
||||
.copied()
|
||||
.map(|(footprint, _)| footprint.transform)
|
||||
.map(|footprint| footprint.transform)
|
||||
.unwrap_or_else(|| self.transform_to_viewport(layer))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3244,14 +3244,16 @@ impl NodeNetworkInterface {
|
|||
|
||||
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.click_targets.retain(|layer, _| self.document_metadata.structure.contains_key(layer));
|
||||
}
|
||||
|
||||
/// Update the cached transforms of the layers
|
||||
pub fn update_transforms(&mut self, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>) {
|
||||
self.document_metadata.upstream_transforms = new_upstream_transforms;
|
||||
pub fn update_transforms(&mut self, upstream_footprints: HashMap<NodeId, Footprint>, local_transforms: HashMap<NodeId, DAffine2>) {
|
||||
self.document_metadata.upstream_footprints = upstream_footprints;
|
||||
self.document_metadata.local_transforms = local_transforms;
|
||||
}
|
||||
|
||||
/// Update the cached click targets of the layers
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::network_interface::NodeNetworkInterface;
|
||||
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::prelude::*;
|
||||
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>) {
|
||||
let document_metadata = network_interface.document_metadata();
|
||||
/// Gets the transform from the most downstream transform node
|
||||
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 {
|
||||
OriginalTransforms::Layer(layer_map) => {
|
||||
layer_map.retain(|layer, _| selected.contains(layer));
|
||||
|
@ -64,7 +71,8 @@ impl OriginalTransforms {
|
|||
if layer == LayerNodeIdentifier::ROOT_PARENT {
|
||||
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) => {
|
||||
|
@ -550,7 +558,7 @@ impl<'a> Selected<'a> {
|
|||
.unwrap_or(DAffine2::IDENTITY);
|
||||
|
||||
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
|
||||
|
|
|
@ -523,16 +523,16 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
|
||||
// Update bounds
|
||||
let transform = document
|
||||
let mut transform = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.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
|
||||
let mut transform_tampered = false;
|
||||
if transform.matrix2.determinant() == 0. {
|
||||
transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
|
||||
transform_tampered = true;
|
||||
|
@ -549,6 +549,7 @@ impl Fsm for SelectToolFsmState {
|
|||
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
|
||||
})
|
||||
.reduce(graphene_core::renderer::Quad::combine_bounds);
|
||||
|
||||
if let Some(bounds) = bounds {
|
||||
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
|
||||
|
||||
|
|
|
@ -525,7 +525,6 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
responses.add(PenToolMessage::Abort);
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
} else {
|
||||
selected.revert_operation();
|
||||
selected.original_transforms.clear();
|
||||
self.typing.clear();
|
||||
self.transform_operation = TransformOperation::None;
|
||||
|
|
|
@ -10,7 +10,7 @@ use graph_craft::proto::GraphErrors;
|
|||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
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::text::FontCache;
|
||||
use graphene_core::transform::Footprint;
|
||||
|
@ -323,7 +323,7 @@ impl NodeRuntime {
|
|||
let bounds = graphic_element.bounding_box(DAffine2::IDENTITY);
|
||||
|
||||
// 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();
|
||||
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>) {
|
||||
// Setup rendering
|
||||
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_object.render_svg(&mut render, &render_params);
|
||||
|
|
|
@ -266,7 +266,7 @@ pub mod test_prelude {
|
|||
pub use graph_craft::document::DocumentNode;
|
||||
pub use graphene_core::raster::{Color, Image};
|
||||
pub use graphene_core::{InputAccessor, InputAccessorSource};
|
||||
pub use graphene_std::{transform::Footprint, GraphicGroup};
|
||||
pub use graphene_std::transform::Footprint;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! float_eq {
|
||||
|
|
|
@ -74,7 +74,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
|
|||
|
||||
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
|
||||
|
||||
if (currentDocumentId) {
|
||||
if (currentDocumentId && currentDocumentId in previouslySavedDocuments) {
|
||||
const doc = previouslySavedDocuments[currentDocumentId];
|
||||
editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document, false);
|
||||
editor.handle.selectDocument(BigInt(currentDocumentId));
|
||||
|
|
|
@ -49,6 +49,7 @@ rand = { workspace = true, default-features = false, features = ["std_rng"] }
|
|||
glam = { workspace = true, default-features = false, features = [
|
||||
"scalar-math",
|
||||
] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
|
||||
|
|
|
@ -2,14 +2,13 @@ use crate::application_io::{ImageTexture, TextureFrameTable};
|
|||
use crate::instances::Instances;
|
||||
use crate::raster::image::{Image, ImageFrameTable};
|
||||
use crate::raster::BlendMode;
|
||||
use crate::transform::{Transform, TransformMut};
|
||||
use crate::transform::TransformMut;
|
||||
use crate::uuid::NodeId;
|
||||
use crate::vector::{VectorData, VectorDataTable};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl};
|
||||
|
||||
use dyn_any::DynAny;
|
||||
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use glam::{DAffine2, IVec2};
|
||||
use std::hash::Hash;
|
||||
|
||||
|
@ -52,83 +51,84 @@ pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
|
|||
transform: DAffine2,
|
||||
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)]
|
||||
#[serde(untagged)]
|
||||
enum EitherFormat {
|
||||
GraphicGroup(GraphicGroup),
|
||||
OldGraphicGroup(OldGraphicGroup),
|
||||
GraphicGroupTable(GraphicGroupTable),
|
||||
InstanceTable(serde_json::Value),
|
||||
}
|
||||
|
||||
Ok(match EitherFormat::deserialize(deserializer)? {
|
||||
EitherFormat::GraphicGroup(graphic_group) => GraphicGroupTable::new(graphic_group),
|
||||
EitherFormat::OldGraphicGroup(old) => {
|
||||
let mut graphic_group_table = GraphicGroupTable::new(GraphicGroup { elements: old.elements });
|
||||
*graphic_group_table.one_instance_mut().transform = old.transform;
|
||||
*graphic_group_table.one_instance_mut().alpha_blending = old.alpha_blending;
|
||||
let mut graphic_group_table = GraphicGroupTable::empty();
|
||||
for (graphic_element, source_node_id) in old.elements {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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) => {
|
||||
let mut table = ArtboardGroupTable::empty();
|
||||
for (artboard, source_node_id) in artboard_group.artboards {
|
||||
table.push(artboard);
|
||||
*table.instances_mut().last().unwrap().source_node_id = source_node_id;
|
||||
let pushed = table.push(artboard);
|
||||
*pushed.source_node_id = source_node_id;
|
||||
}
|
||||
table
|
||||
}
|
||||
|
@ -290,23 +290,40 @@ pub fn migrate_artboard_group<'de, D: serde::Deserializer<'de>>(deserializer: D)
|
|||
pub type ArtboardGroupTable = Instances<Artboard>;
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn layer(_: impl Ctx, stack: GraphicGroupTable, mut 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;
|
||||
}
|
||||
|
||||
async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElement, node_path: Vec<NodeId>) -> GraphicGroupTable {
|
||||
// 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();
|
||||
stack.one_instance_mut().instance.push((element, encapsulating_node_id));
|
||||
let pushed = stack.push(element);
|
||||
*pushed.source_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
|
||||
|
||||
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"))]
|
||||
async fn to_element<Data: Into<GraphicElement> + 'n>(
|
||||
_: impl Ctx,
|
||||
|
@ -337,38 +354,39 @@ async fn to_group<Data: Into<GraphicGroupTable> + 'n>(
|
|||
|
||||
#[node_macro::node(category("General"))]
|
||||
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) {
|
||||
let mut collection_group = GraphicGroup::default();
|
||||
let current_group_elements = current_group_table.one_instance().instance.elements.clone();
|
||||
// TODO: Avoid mutable reference, instead return a new GraphicGroupTable?
|
||||
fn flatten_group(output_group_table: &mut GraphicGroupTable, current_group_table: GraphicGroupTable, fully_flatten: bool, recursion_depth: usize) {
|
||||
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 {
|
||||
if let GraphicElement::GraphicGroup(mut nested_group_table) = element {
|
||||
*nested_group_table.transform_mut() = nested_group_table.transform() * current_group_table.transform();
|
||||
let recurse = fully_flatten || recursion_depth == 0;
|
||||
|
||||
let mut sub_group_table = GraphicGroupTable::default();
|
||||
if fully_flatten {
|
||||
flatten_group(&mut sub_group_table, nested_group_table, fully_flatten);
|
||||
} else {
|
||||
let nested_group_table_transform = nested_group_table.transform();
|
||||
for (collection_element, _) in &mut nested_group_table.one_instance_mut().instance.elements {
|
||||
*collection_element.transform_mut() = nested_group_table_transform * collection_element.transform();
|
||||
match current_element {
|
||||
// If we're allowed to recurse, flatten any GraphicGroups we encounter
|
||||
GraphicElement::GraphicGroup(mut current_element) if recurse => {
|
||||
// Apply the parent group's transform to all child elements
|
||||
for graphic_element in current_element.instances_mut() {
|
||||
*graphic_element.transform = *current_instance.transform * *graphic_element.transform;
|
||||
}
|
||||
sub_group_table = nested_group_table;
|
||||
}
|
||||
|
||||
collection_group.append(&mut sub_group_table.one_instance_mut().instance.elements);
|
||||
} else {
|
||||
collection_group.push((element, reference));
|
||||
flatten_group(output_group_table, current_element, fully_flatten, recursion_depth + 1);
|
||||
}
|
||||
// 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();
|
||||
flatten_group(&mut flat_group, group, fully_flatten);
|
||||
let mut output = GraphicGroupTable::default();
|
||||
flatten_group(&mut output, group, fully_flatten, 0);
|
||||
|
||||
flat_group
|
||||
output
|
||||
}
|
||||
|
||||
#[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).
|
||||
let encapsulating_node_id = node_path.get(node_path.len().wrapping_sub(2)).copied();
|
||||
|
||||
artboards.push(artboard);
|
||||
*artboards.instances_mut().last().unwrap().source_node_id = encapsulating_node_id;
|
||||
let pushed = artboards.push(artboard);
|
||||
*pushed.source_node_id = encapsulating_node_id;
|
||||
|
||||
artboards
|
||||
}
|
||||
|
@ -450,46 +468,8 @@ impl From<VectorDataTable> for GraphicElement {
|
|||
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 {
|
||||
fn from(graphic_group: GraphicGroupTable) -> Self {
|
||||
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)]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,8 +168,10 @@ impl SvgRender {
|
|||
|
||||
pub fn leaf_tag(&mut self, name: impl Into<SvgSegment>, attributes: impl FnOnce(&mut SvgRenderAttrs)) {
|
||||
self.indent();
|
||||
|
||||
self.svg.push("<".into());
|
||||
self.svg.push(name.into());
|
||||
|
||||
attributes(&mut SvgRenderAttrs(self));
|
||||
|
||||
self.svg.push("/>".into());
|
||||
|
@ -210,12 +212,6 @@ impl Default for SvgRender {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum ImageRenderMode {
|
||||
#[default]
|
||||
Base64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RenderContext {
|
||||
#[cfg(feature = "wgpu")]
|
||||
|
@ -226,7 +222,6 @@ pub struct RenderContext {
|
|||
#[derive(Default)]
|
||||
pub struct RenderParams {
|
||||
pub view_mode: ViewMode,
|
||||
pub image_render_mode: ImageRenderMode,
|
||||
pub culling_bounds: Option<[DVec2; 2]>,
|
||||
pub thumbnail: bool,
|
||||
/// Don't render the rectangle for an artboard to allow exporting with a transparent background.
|
||||
|
@ -236,10 +231,9 @@ pub struct 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 {
|
||||
view_mode,
|
||||
image_render_mode,
|
||||
culling_bounds,
|
||||
thumbnail,
|
||||
hide_artboards,
|
||||
|
@ -271,7 +265,8 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
|||
#[derive(Debug, Default, Clone, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
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 clip_targets: HashSet<NodeId>,
|
||||
}
|
||||
|
@ -280,6 +275,9 @@ pub struct RenderMetadata {
|
|||
pub trait GraphicElementRendered {
|
||||
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]>;
|
||||
|
||||
// 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
|
||||
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 {
|
||||
false
|
||||
}
|
||||
|
@ -311,7 +306,7 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
render.parent_tag(
|
||||
"g",
|
||||
|attributes| {
|
||||
let matrix = format_transform_matrix(instance.transform());
|
||||
let matrix = format_transform_matrix(*instance.transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
|
@ -325,49 +320,66 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
}
|
||||
},
|
||||
|render| {
|
||||
for (element, _) in instance.instance.iter() {
|
||||
element.render_svg(render, render_params);
|
||||
}
|
||||
instance.instance.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]> {
|
||||
self.instances()
|
||||
.flat_map(|instance| {
|
||||
instance
|
||||
.instance
|
||||
.iter()
|
||||
.filter_map(|(element, _)| element.bounding_box(transform * instance.transform()))
|
||||
.reduce(Quad::combine_bounds)
|
||||
})
|
||||
.filter_map(|element| element.instance.bounding_box(transform * *element.transform))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
|
||||
let instance_transform = self.transform();
|
||||
let instance = self.one_instance().instance;
|
||||
for instance in self.instances() {
|
||||
if let Some(element_id) = instance.source_node_id {
|
||||
let mut footprint = footprint;
|
||||
footprint.transform *= *instance.transform;
|
||||
|
||||
let mut footprint = footprint;
|
||||
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));
|
||||
instance.instance.collect_metadata(metadata, footprint, Some(*element_id));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(graphic_group_id) = element_id {
|
||||
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();
|
||||
|
||||
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() {
|
||||
click_target.apply_transform(element.transform())
|
||||
click_target.apply_transform(*instance.transform)
|
||||
}
|
||||
|
||||
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>) {
|
||||
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() {
|
||||
click_target.apply_transform(element.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 click_target in new_click_targets.iter_mut() {
|
||||
click_target.apply_transform(*instance.transform)
|
||||
}
|
||||
|
||||
for (element, _) in instance.instance.iter() {
|
||||
element.render_to_vello(scene, transform, context);
|
||||
}
|
||||
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
}
|
||||
click_targets.extend(new_click_targets);
|
||||
}
|
||||
}
|
||||
|
||||
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>) {
|
||||
for instance in self.instances_mut() {
|
||||
for (element, node_id) in instance.instance.elements.iter_mut() {
|
||||
element.new_ids_from_hash(*node_id);
|
||||
}
|
||||
instance.instance.new_ids_from_hash(*instance.source_node_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -444,11 +421,11 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
impl GraphicElementRendered for VectorDataTable {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
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
|
||||
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 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 = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
|
||||
|
@ -462,7 +439,9 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
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;
|
||||
|
||||
|
@ -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")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext) {
|
||||
use crate::vector::style::GradientType;
|
||||
|
@ -552,7 +470,7 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
for instance in self.instances() {
|
||||
let mut layer = false;
|
||||
|
||||
let multiplied_transform = parent_transform * instance.transform();
|
||||
let multiplied_transform = parent_transform * *instance.transform;
|
||||
let set_stroke_transform = instance
|
||||
.instance
|
||||
.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>) {
|
||||
for instance in self.instances_mut() {
|
||||
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 {
|
||||
let instance = self.one_instance().instance;
|
||||
|
||||
GraphicElement::VectorData(VectorDataTable::new(instance.clone()))
|
||||
GraphicElement::VectorData(self.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")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext) {
|
||||
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 {
|
||||
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]> {
|
||||
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 {
|
||||
self.instances().count() > 0
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
let transform = instance.transform() * render.transform;
|
||||
let transform = *instance.transform * render.transform;
|
||||
|
||||
match render_params.image_render_mode {
|
||||
ImageRenderMode::Base64 => {
|
||||
let image = &instance.instance;
|
||||
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 image = &instance.instance;
|
||||
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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext) {
|
||||
use vello::peniko;
|
||||
|
@ -898,76 +848,45 @@ impl GraphicElementRendered for ImageFrameTable<Color> {
|
|||
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 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]> {
|
||||
let transform = transform * self.transform();
|
||||
(transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box())
|
||||
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 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);
|
||||
|
||||
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>) {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
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")]
|
||||
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 {
|
||||
|
@ -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]> {
|
||||
match self {
|
||||
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>) {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.contains_artboard(),
|
||||
|
@ -1095,7 +1045,7 @@ fn text_attributes(attributes: &mut SvgRenderAttrs) {
|
|||
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) {
|
||||
render.parent_tag("text", text_attributes, |render| render.leaf_node(format!("{self}")));
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use crate::application_io::{ImageTexture, TextureFrameTable};
|
||||
use crate::application_io::TextureFrameTable;
|
||||
use crate::raster::image::{Image, ImageFrameTable};
|
||||
use crate::raster::Pixel;
|
||||
use crate::transform::{Transform, TransformMut};
|
||||
use crate::uuid::NodeId;
|
||||
use crate::vector::{InstanceId, VectorData, VectorDataTable};
|
||||
use crate::{AlphaBlending, GraphicElement, GraphicGroup, GraphicGroupTable, RasterFrame};
|
||||
use crate::vector::{InstanceId, VectorDataTable};
|
||||
use crate::{AlphaBlending, GraphicElement, RasterFrame};
|
||||
|
||||
use dyn_any::StaticType;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DAffine2;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[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> {
|
||||
Instance {
|
||||
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>> {
|
||||
// assert!(self.instance.len() == 1, "ONE INSTANCE EXPECTED, FOUND {} (instances)", self.instance.len());
|
||||
pub fn instances(&self) -> impl DoubleEndedIterator<Item = Instance<T>> {
|
||||
self.id
|
||||
.iter()
|
||||
.zip(self.instance.iter())
|
||||
|
@ -100,8 +118,7 @@ impl<T> Instances<T> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn instances_mut(&mut self) -> impl Iterator<Item = InstanceMut<T>> {
|
||||
// assert!(self.instance.len() == 1, "ONE INSTANCE EXPECTED, FOUND {} (instances_mut)", self.instance.len());
|
||||
pub fn instances_mut(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<T>> {
|
||||
self.id
|
||||
.iter_mut()
|
||||
.zip(self.instance.iter_mut())
|
||||
|
@ -120,8 +137,11 @@ impl<T> Instances<T> {
|
|||
|
||||
impl<T: Default + Hash + 'static> Default for Instances<T> {
|
||||
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;
|
||||
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
|
||||
Self::empty()
|
||||
} else {
|
||||
|
@ -177,104 +197,27 @@ pub struct InstanceMut<'a, T> {
|
|||
pub source_node_id: &'a mut Option<NodeId>,
|
||||
}
|
||||
|
||||
// GRAPHIC ELEMENT
|
||||
impl Transform for GraphicElement {
|
||||
// VECTOR DATA TABLE
|
||||
impl Transform for VectorDataTable {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
match self {
|
||||
GraphicElement::GraphicGroup(group) => group.transform(),
|
||||
GraphicElement::VectorData(vector_data) => vector_data.transform(),
|
||||
GraphicElement::RasterFrame(frame) => frame.transform(),
|
||||
}
|
||||
*self.one_instance().transform
|
||||
}
|
||||
}
|
||||
impl TransformMut for GraphicElement {
|
||||
impl TransformMut for VectorDataTable {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
match self {
|
||||
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
|
||||
self.one_instance_mut().transform
|
||||
}
|
||||
}
|
||||
|
||||
// TEXTURE FRAME TABLE
|
||||
impl Transform for TextureFrameTable {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
self.one_instance().transform()
|
||||
*self.one_instance().transform
|
||||
}
|
||||
}
|
||||
impl TransformMut for TextureFrameTable {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED"))
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
self.one_instance_mut().transform
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,7 +227,7 @@ where
|
|||
GraphicElement: From<Image<P>>,
|
||||
{
|
||||
fn transform(&self) -> DAffine2 {
|
||||
self.one_instance().transform()
|
||||
*self.one_instance().transform
|
||||
}
|
||||
}
|
||||
impl<P: Pixel> TransformMut for ImageFrameTable<P>
|
||||
|
@ -292,42 +235,7 @@ where
|
|||
GraphicElement: From<Image<P>>,
|
||||
{
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
self.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED"))
|
||||
}
|
||||
}
|
||||
|
||||
// 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"))
|
||||
self.one_instance_mut().transform
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -343,8 +251,8 @@ impl Transform for RasterFrame {
|
|||
impl TransformMut for RasterFrame {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
match self {
|
||||
RasterFrame::ImageFrame(image_frame) => image_frame.transform.first_mut().unwrap_or_else(|| panic!("ONE INSTANCE EXPECTED")),
|
||||
RasterFrame::TextureFrame(texture_frame) => texture_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_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -335,6 +335,7 @@ fn blend_mode<T: SetBlendMode>(
|
|||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
) -> 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
|
||||
}
|
||||
|
@ -350,7 +351,7 @@ fn opacity<T: MultiplyAlpha>(
|
|||
mut value: T,
|
||||
#[default(100.)] factor: Percentage,
|
||||
) -> T {
|
||||
let opacity_multiplier = factor / 100.;
|
||||
value.multiply_alpha(opacity_multiplier);
|
||||
// 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(factor / 100.);
|
||||
value
|
||||
}
|
||||
|
|
|
@ -1583,7 +1583,7 @@ mod test {
|
|||
let opacity = 100_f64;
|
||||
|
||||
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)
|
||||
assert_eq!(result.data[0], Color::from_rgbaf32_unchecked(0., image_color.g(), 0., image_color.a()));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::application_io::TextureFrameTable;
|
||||
use crate::instances::Instances;
|
||||
use crate::raster::bbox::AxisAlignedBbox;
|
||||
use crate::raster::image::ImageFrameTable;
|
||||
use crate::vector::VectorDataTable;
|
||||
|
@ -90,8 +91,6 @@ pub struct Footprint {
|
|||
pub resolution: glam::UVec2,
|
||||
/// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient
|
||||
pub quality: RenderQuality,
|
||||
/// When the transform is set downstream, all upstream modifications have to be ignored
|
||||
pub ignore_modifications: bool,
|
||||
}
|
||||
|
||||
impl Default for Footprint {
|
||||
|
@ -106,7 +105,6 @@ impl Footprint {
|
|||
transform: DAffine2::IDENTITY,
|
||||
resolution: glam::UVec2::new(1920, 1080),
|
||||
quality: RenderQuality::Full,
|
||||
ignore_modifications: false,
|
||||
}
|
||||
}
|
||||
pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox {
|
||||
|
@ -156,7 +154,7 @@ impl ApplyTransform for () {
|
|||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn transform<T: 'n + TransformMut + 'static>(
|
||||
async fn transform<T: 'n + 'static>(
|
||||
ctx: impl Ctx + CloneVarArgs + ExtractAll,
|
||||
#[implementations(
|
||||
Context -> VectorDataTable,
|
||||
|
@ -164,39 +162,40 @@ async fn transform<T: 'n + TransformMut + 'static>(
|
|||
Context -> ImageFrameTable<Color>,
|
||||
Context -> TextureFrameTable,
|
||||
)]
|
||||
transform_target: impl Node<Context<'static>, Output = T>,
|
||||
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
|
||||
translate: DVec2,
|
||||
rotate: f64,
|
||||
scale: DVec2,
|
||||
shear: DVec2,
|
||||
_pivot: DVec2,
|
||||
) -> T {
|
||||
let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
|
||||
) -> Instances<T> {
|
||||
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 mut ctx = OwnedContextImpl::from(ctx);
|
||||
if let Some(mut footprint) = footprint {
|
||||
if !footprint.ignore_modifications {
|
||||
footprint.apply_transform(&modification);
|
||||
}
|
||||
footprint.apply_transform(&matrix);
|
||||
ctx = ctx.with_footprint(footprint);
|
||||
}
|
||||
|
||||
let mut transform_target = transform_target.eval(ctx.into_context()).await;
|
||||
|
||||
let data_transform = transform_target.transform_mut();
|
||||
*data_transform = modification * (*data_transform);
|
||||
for data_transform in transform_target.instances_mut() {
|
||||
*data_transform.transform = matrix * *data_transform.transform;
|
||||
}
|
||||
|
||||
transform_target
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
fn replace_transform<Data: TransformMut, TransformInput: Transform>(
|
||||
fn replace_transform<Data, TransformInput: Transform>(
|
||||
_: impl Ctx,
|
||||
#[implementations(VectorDataTable, ImageFrameTable<Color>, GraphicGroupTable)] mut data: Data,
|
||||
#[implementations(VectorDataTable, ImageFrameTable<Color>, GraphicGroupTable)] mut data: Instances<Data>,
|
||||
#[implementations(DAffine2)] transform: TransformInput,
|
||||
) -> Data {
|
||||
let data_transform = data.transform_mut();
|
||||
*data_transform = transform.transform();
|
||||
) -> Instances<Data> {
|
||||
for data_transform in data.instances_mut() {
|
||||
*data_transform.transform = transform.transform();
|
||||
}
|
||||
data
|
||||
}
|
||||
|
|
|
@ -147,6 +147,8 @@ impl Gradient {
|
|||
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
|
||||
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use dyn_any::DynAny;
|
|||
|
||||
use core::borrow::Borrow;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
|
||||
|
@ -338,6 +339,48 @@ impl VectorData {
|
|||
ManipulatorPointId::Anchor(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn concat(&mut self, other: &Self, transform: DAffine2, node_id: u64) {
|
||||
let point_map = other
|
||||
.point_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.point_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let segment_map = other
|
||||
.segment_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.segment_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let region_map = other
|
||||
.region_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.region_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
|
||||
self.point_domain.concat(&other.point_domain, transform, &id_map);
|
||||
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
|
||||
self.region_domain.concat(&other.region_domain, transform, &id_map);
|
||||
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = other.style.clone();
|
||||
|
||||
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VectorData {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use crate::transform::Transform;
|
||||
use crate::vector::vector_data::{HandleId, VectorData, VectorDataTable};
|
||||
use crate::vector::ConcatElement;
|
||||
use crate::vector::vector_data::{HandleId, VectorData};
|
||||
|
||||
use dyn_any::DynAny;
|
||||
|
||||
|
@ -166,16 +164,16 @@ impl PointDomain {
|
|||
self.id.iter().position(|&check_id| check_id == id)
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
pub fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
self.id.extend(other.id.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
|
||||
self.position.extend(other.position.iter().map(|&pos| transform.transform_point2(pos)));
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
pub fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.point_map.get(id).unwrap_or(id));
|
||||
}
|
||||
|
||||
fn transform(&mut self, transform: DAffine2) {
|
||||
pub fn transform(&mut self, transform: DAffine2) {
|
||||
for pos in &mut self.position {
|
||||
*pos = transform.transform_point2(*pos);
|
||||
}
|
||||
|
@ -364,7 +362,7 @@ impl SegmentDomain {
|
|||
}
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
pub fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
self.id.extend(other.id.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id)));
|
||||
self.start_point.extend(other.start_point.iter().map(|&index| id_map.point_offset + index));
|
||||
self.end_point.extend(other.end_point.iter().map(|&index| id_map.point_offset + index));
|
||||
|
@ -372,11 +370,11 @@ impl SegmentDomain {
|
|||
self.stroke.extend(&other.stroke);
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
pub fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
|
||||
}
|
||||
|
||||
fn transform(&mut self, transform: DAffine2) {
|
||||
pub fn transform(&mut self, transform: DAffine2) {
|
||||
for handles in &mut self.handles {
|
||||
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
|
||||
}
|
||||
|
@ -474,7 +472,7 @@ impl RegionDomain {
|
|||
&self.fill
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
|
||||
pub fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
|
||||
self.id.extend(other.id.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
|
||||
self.segment_range.extend(
|
||||
other
|
||||
|
@ -485,7 +483,7 @@ impl RegionDomain {
|
|||
self.fill.extend(&other.fill);
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
pub fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
|
||||
self.segment_range
|
||||
.iter_mut()
|
||||
|
@ -764,57 +762,10 @@ impl bezier_rs::Identifier for PointId {
|
|||
}
|
||||
}
|
||||
|
||||
impl ConcatElement for VectorData {
|
||||
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
|
||||
let new_ids = other
|
||||
.point_domain
|
||||
.id
|
||||
.iter()
|
||||
.filter(|id| self.point_domain.id.contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)));
|
||||
let point_map = new_ids.collect::<HashMap<_, _>>();
|
||||
let new_ids = other
|
||||
.segment_domain
|
||||
.id
|
||||
.iter()
|
||||
.filter(|id| self.segment_domain.id.contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)));
|
||||
let segment_map = new_ids.collect::<HashMap<_, _>>();
|
||||
let new_ids = other
|
||||
.region_domain
|
||||
.id
|
||||
.iter()
|
||||
.filter(|id| self.region_domain.id.contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)));
|
||||
let region_map = new_ids.collect::<HashMap<_, _>>();
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
self.point_domain.concat(&other.point_domain, transform, &id_map);
|
||||
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
|
||||
self.region_domain.concat(&other.region_domain, transform, &id_map);
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = other.style.clone();
|
||||
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
impl ConcatElement for VectorDataTable {
|
||||
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
|
||||
for (instance, other_instance) in self.instances_mut().zip(other.instances()) {
|
||||
*instance.alpha_blending = *other_instance.alpha_blending;
|
||||
instance.instance.concat(other_instance.instance, transform * other_instance.transform(), node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the conversion of ids used when concatenating vector data with conflicting ids.
|
||||
struct IdMap {
|
||||
point_offset: usize,
|
||||
point_map: HashMap<PointId, PointId>,
|
||||
segment_map: HashMap<SegmentId, SegmentId>,
|
||||
region_map: HashMap<RegionId, RegionId>,
|
||||
pub struct IdMap {
|
||||
pub point_offset: usize,
|
||||
pub point_map: HashMap<PointId, PointId>,
|
||||
pub segment_map: HashMap<SegmentId, SegmentId>,
|
||||
pub region_map: HashMap<RegionId, RegionId>,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::misc::CentroidType;
|
||||
use super::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
|
||||
use crate::instances::InstanceMut;
|
||||
use crate::instances::{InstanceMut, Instances};
|
||||
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
|
||||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, Transform, TransformMut};
|
||||
|
@ -15,55 +15,61 @@ use rand::{Rng, SeedableRng};
|
|||
|
||||
/// Implemented for types that can be converted to an iterator of vector data.
|
||||
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
|
||||
trait VectorIterMut {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)>;
|
||||
trait VectorDataTableIterMut {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>>;
|
||||
}
|
||||
|
||||
impl VectorIterMut for GraphicGroupTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
|
||||
let parent_transform = self.transform();
|
||||
let instance = self.one_instance_mut().instance;
|
||||
|
||||
impl VectorDataTableIterMut for GraphicGroupTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
|
||||
// Grab only the direct children
|
||||
instance.iter_mut().filter_map(|(element, _)| element.as_vector_data_mut()).map(move |vector_data| {
|
||||
let transform = parent_transform * vector_data.transform();
|
||||
|
||||
let vector_data_instance = vector_data.one_instance_mut().instance;
|
||||
|
||||
(vector_data_instance, transform)
|
||||
})
|
||||
self.instances_mut()
|
||||
.filter_map(|element| element.instance.as_vector_data_mut())
|
||||
.flat_map(move |vector_data| vector_data.instances_mut())
|
||||
}
|
||||
}
|
||||
|
||||
impl VectorIterMut for VectorDataTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
|
||||
self.instances_mut().map(|instance| {
|
||||
let transform = instance.transform();
|
||||
(instance.instance, transform)
|
||||
})
|
||||
impl VectorDataTableIterMut for VectorDataTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
|
||||
self.instances_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
|
||||
async fn assign_colors<T: VectorIterMut>(
|
||||
async fn assign_colors<T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(GraphicGroupTable, VectorDataTable)]
|
||||
#[widget(ParsedWidgetOverride::Hidden)]
|
||||
/// The vector elements, or group of vector elements, to apply the fill and/or stroke style to.
|
||||
mut vector_group: T,
|
||||
#[default(true)] fill: bool,
|
||||
#[default(true)]
|
||||
/// Whether to style the fill.
|
||||
fill: bool,
|
||||
/// Whether to style the stroke.
|
||||
stroke: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")] gradient: GradientStops,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
|
||||
/// The range of colors to select from.
|
||||
gradient: GradientStops,
|
||||
/// Whether to reverse the gradient.
|
||||
reverse: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")] randomize: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] seed: SeedValue,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32,
|
||||
) -> T {
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")]
|
||||
/// Whether to randomize the color selection for each element from throughout the gradient.
|
||||
randomize: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")]
|
||||
/// The seed used for randomization.
|
||||
seed: SeedValue,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
|
||||
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
|
||||
repeat_every: u32,
|
||||
) -> T
|
||||
where
|
||||
T: VectorDataTableIterMut + 'n + Send,
|
||||
{
|
||||
let length = vector_group.vector_iter_mut().count();
|
||||
let gradient = if reverse { gradient.reversed() } else { gradient };
|
||||
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
||||
|
||||
for (i, (vector_data, _)) in vector_group.vector_iter_mut().enumerate() {
|
||||
for (i, vector_data) in vector_group.vector_iter_mut().enumerate() {
|
||||
let factor = match randomize {
|
||||
true => rng.random::<f64>(),
|
||||
false => match repeat_every {
|
||||
|
@ -76,11 +82,11 @@ async fn assign_colors<T: VectorIterMut>(
|
|||
let color = gradient.evalute(factor);
|
||||
|
||||
if fill {
|
||||
vector_data.style.set_fill(Fill::Solid(color));
|
||||
vector_data.instance.style.set_fill(Fill::Solid(color));
|
||||
}
|
||||
if stroke {
|
||||
if let Some(stroke) = vector_data.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
|
||||
vector_data.style.set_stroke(stroke);
|
||||
if let Some(stroke) = vector_data.instance.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
|
||||
vector_data.instance.style.set_stroke(stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +95,7 @@ async fn assign_colors<T: VectorIterMut>(
|
|||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
|
||||
async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
|
||||
async fn fill<F: Into<Fill> + 'n + Send, V>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
VectorDataTable,
|
||||
|
@ -101,7 +107,8 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
|
|||
GraphicGroupTable,
|
||||
GraphicGroupTable
|
||||
)]
|
||||
mut vector_data: TargetTy,
|
||||
/// The vector elements, or group of vector elements, to apply the fill to.
|
||||
mut vector_data: V,
|
||||
#[implementations(
|
||||
Fill,
|
||||
Option<Color>,
|
||||
|
@ -113,22 +120,33 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
|
|||
Gradient,
|
||||
)]
|
||||
#[default(Color::BLACK)]
|
||||
fill: FillTy,
|
||||
/// The fill to paint the path with.
|
||||
fill: F,
|
||||
_backup_color: Option<Color>,
|
||||
_backup_gradient: Gradient,
|
||||
) -> TargetTy {
|
||||
) -> V
|
||||
where
|
||||
V: VectorDataTableIterMut + 'n + Send,
|
||||
{
|
||||
let fill: Fill = fill.into();
|
||||
for (target, _transform) in vector_data.vector_iter_mut() {
|
||||
target.style.set_fill(fill.clone());
|
||||
for vector in vector_data.vector_iter_mut() {
|
||||
let mut fill = fill.clone();
|
||||
if let Fill::Gradient(gradient) = &mut fill {
|
||||
gradient.transform *= *vector.transform;
|
||||
}
|
||||
vector.instance.style.set_fill(fill);
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Applies a stroke style to the vector data contained in the input.
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
|
||||
async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
|
||||
async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
|
||||
_: impl Ctx,
|
||||
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)] mut vector_data: TargetTy,
|
||||
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)]
|
||||
/// The vector elements, or group of vector elements, to apply the stroke to.
|
||||
mut vector_data: Instances<V>,
|
||||
#[implementations(
|
||||
Option<Color>,
|
||||
Color,
|
||||
|
@ -136,14 +154,26 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
|
|||
Color,
|
||||
)]
|
||||
#[default(Color::BLACK)]
|
||||
color: ColorTy,
|
||||
#[default(2.)] weight: f64,
|
||||
/// The stroke color.
|
||||
color: C,
|
||||
#[default(2.)]
|
||||
/// The stroke weight.
|
||||
weight: f64,
|
||||
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||
dash_lengths: Vec<f64>,
|
||||
/// The offset distance from the starting point of the dash pattern.
|
||||
dash_offset: f64,
|
||||
/// The shape of the stroke at open endpoints.
|
||||
line_cap: crate::vector::style::LineCap,
|
||||
/// The curvature of the bent stroke at sharp corners.
|
||||
line_join: LineJoin,
|
||||
#[default(4.)] miter_limit: f64,
|
||||
) -> TargetTy {
|
||||
#[default(4.)]
|
||||
/// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio.
|
||||
miter_limit: f64,
|
||||
) -> Instances<V>
|
||||
where
|
||||
Instances<V>: VectorDataTableIterMut + 'n + Send,
|
||||
{
|
||||
let stroke = Stroke {
|
||||
color: color.into(),
|
||||
weight,
|
||||
|
@ -154,110 +184,111 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
|
|||
line_join_miter_limit: miter_limit,
|
||||
transform: DAffine2::IDENTITY,
|
||||
};
|
||||
for (target, transform) in vector_data.vector_iter_mut() {
|
||||
target.style.set_stroke(Stroke { transform, ..stroke.clone() });
|
||||
for vector in vector_data.vector_iter_mut() {
|
||||
let mut stroke = stroke.clone();
|
||||
stroke.transform *= *vector.transform;
|
||||
vector.instance.style.set_stroke(stroke);
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
|
||||
async fn repeat<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
|
||||
#[default(100., 100.)]
|
||||
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
|
||||
direction: DVec2,
|
||||
angle: Angle,
|
||||
#[default(4)] instances: IntegerCount,
|
||||
) -> GraphicGroupTable {
|
||||
let first_vector_transform = instance.transform();
|
||||
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let angle = angle.to_radians();
|
||||
let instances = instances.max(1);
|
||||
let total = (instances - 1) as f64;
|
||||
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let result = result_table.one_instance_mut().instance;
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
|
||||
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
|
||||
for i in 0..instances {
|
||||
let translation = i as f64 * direction / total;
|
||||
let angle = i as f64 * angle / total;
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
for index in 0..instances {
|
||||
let angle = index as f64 * angle / total;
|
||||
let translation = index as f64 * direction / total;
|
||||
let modification = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = modification * first_vector_transform;
|
||||
result.push((new_instance, None));
|
||||
let mut new_graphic_element = instance.to_graphic_element().clone();
|
||||
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
|
||||
|
||||
let new_instance = result_table.push(new_graphic_element);
|
||||
*new_instance.transform = modification;
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn circular_repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
|
||||
async fn circular_repeat<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
|
||||
angle_offset: Angle,
|
||||
#[default(5)] radius: f64,
|
||||
#[default(5)] instances: IntegerCount,
|
||||
) -> GraphicGroupTable {
|
||||
let first_vector_transform = instance.transform();
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let instances = instances.max(1);
|
||||
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let result = result_table.one_instance_mut().instance;
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
|
||||
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
let base_transform = DVec2::new(0., radius) - center;
|
||||
|
||||
for i in 0..instances {
|
||||
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians();
|
||||
let rotation = DAffine2::from_angle(angle);
|
||||
for index in 0..instances {
|
||||
let rotation = DAffine2::from_angle((std::f64::consts::TAU / instances as f64) * index as f64 + angle_offset.to_radians());
|
||||
let modification = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = modification * first_vector_transform;
|
||||
result.push((new_instance, None));
|
||||
let mut new_graphic_element = instance.to_graphic_element().clone();
|
||||
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
|
||||
|
||||
let new_instance = result_table.push(new_graphic_element);
|
||||
*new_instance.transform = modification;
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
|
||||
async fn copy_to_points<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
points: VectorDataTable,
|
||||
#[expose]
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)]
|
||||
instance: I,
|
||||
instance: Instances<I>,
|
||||
#[default(1)] random_scale_min: f64,
|
||||
#[default(1)] random_scale_max: f64,
|
||||
random_scale_bias: f64,
|
||||
random_scale_seed: SeedValue,
|
||||
random_rotation: Angle,
|
||||
random_rotation_seed: SeedValue,
|
||||
) -> GraphicGroupTable {
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let points_transform = points.transform();
|
||||
let points = points.one_instance().instance;
|
||||
|
||||
let instance_transform = instance.transform();
|
||||
let points_list = points.instances().flat_map(|element| element.instance.point_domain.positions());
|
||||
|
||||
let random_scale_difference = random_scale_max - random_scale_min;
|
||||
|
||||
let points_list = points.point_domain.positions();
|
||||
|
||||
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
|
||||
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
|
||||
|
||||
|
@ -268,9 +299,8 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
|
|||
let do_rotation = random_rotation.abs() > 1e-6;
|
||||
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let result = result_table.one_instance_mut().instance;
|
||||
|
||||
for &point in points_list {
|
||||
for (index, &point) in points_list.into_iter().enumerate() {
|
||||
let center_transform = DAffine2::from_translation(instance_center);
|
||||
|
||||
let translation = points_transform.transform_point2(point);
|
||||
|
@ -296,12 +326,11 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
|
|||
random_scale_min
|
||||
};
|
||||
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
let mut new_graphic_element = instance.to_graphic_element().clone();
|
||||
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform * instance_transform;
|
||||
result.push((new_instance, None));
|
||||
let new_instance = result_table.push(new_graphic_element);
|
||||
*new_instance.transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform;
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -406,53 +435,40 @@ async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupT
|
|||
// A node based solution to support passing through vector data could be a network node with a cache node connected to
|
||||
// a flatten vector elements connected to an if else node, another connection from the cache directly
|
||||
// To the if else node, and another connection from the cache to a matches type node connected to the if else node.
|
||||
fn concat_group(graphic_group_table: &GraphicGroupTable, current_transform: DAffine2, result: &mut InstanceMut<VectorData>) {
|
||||
for (element, reference) in graphic_group_table.one_instance().instance.iter() {
|
||||
match element {
|
||||
GraphicElement::VectorData(vector_data) => {
|
||||
for instance in vector_data.instances() {
|
||||
*result.alpha_blending = *instance.alpha_blending;
|
||||
result
|
||||
.instance
|
||||
.concat(instance.instance, current_transform * instance.transform(), reference.map(|node_id| node_id.0).unwrap_or_default());
|
||||
fn flatten_group(graphic_group_table: &GraphicGroupTable, output: &mut InstanceMut<VectorData>) {
|
||||
for current_element in graphic_group_table.instances() {
|
||||
match current_element.instance {
|
||||
GraphicElement::VectorData(vector_data_table) => {
|
||||
// Loop through every row of the VectorDataTable and concatenate each instance's subpath into the output VectorData instance.
|
||||
for vector_data_instance in vector_data_table.instances() {
|
||||
let other = vector_data_instance.instance;
|
||||
let transform = *current_element.transform * *vector_data_instance.transform;
|
||||
let node_id = current_element.source_node_id.map(|node_id| node_id.0).unwrap_or_default();
|
||||
output.instance.concat(other, transform, node_id);
|
||||
|
||||
// Use the last encountered style as the output style
|
||||
output.instance.style = vector_data_instance.instance.style.clone();
|
||||
}
|
||||
}
|
||||
GraphicElement::GraphicGroup(graphic_group) => {
|
||||
concat_group(graphic_group, current_transform * graphic_group.transform(), result);
|
||||
let mut graphic_group = graphic_group.clone();
|
||||
for instance in graphic_group.instances_mut() {
|
||||
*instance.transform = *current_element.transform * *instance.transform;
|
||||
}
|
||||
|
||||
flatten_group(&graphic_group, output);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_table = VectorDataTable::default();
|
||||
let mut result_instance = result_table.one_instance_mut();
|
||||
// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
|
||||
result_instance.instance.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
concat_group(&graphic_group_input, DAffine2::IDENTITY, &mut result_instance);
|
||||
let mut output_table = VectorDataTable::default();
|
||||
let Some(mut output) = output_table.instances_mut().next() else { return output_table };
|
||||
|
||||
result_table
|
||||
}
|
||||
flatten_group(&graphic_group_input, &mut output);
|
||||
|
||||
pub trait ConcatElement {
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, node_id: u64);
|
||||
}
|
||||
|
||||
impl ConcatElement for GraphicGroupTable {
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, _node_id: u64) {
|
||||
let other_transform = other.transform();
|
||||
|
||||
let self_group = self.one_instance_mut().instance;
|
||||
let other_group = other.one_instance().instance;
|
||||
|
||||
// TODO: Decide if we want to keep this behavior whereby the layers are flattened
|
||||
for (mut element, footprint_mapping) in other_group.iter().cloned() {
|
||||
*element.transform_mut() = transform * element.transform() * other_transform;
|
||||
self_group.push((element, footprint_mapping));
|
||||
}
|
||||
|
||||
*self.one_instance_mut().alpha_blending = *other.one_instance().alpha_blending;
|
||||
}
|
||||
output_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""), path(graphene_core::vector))]
|
||||
|
@ -1077,7 +1093,7 @@ mod test {
|
|||
let instances = 3;
|
||||
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let vector_data = vector_data.instances().next().unwrap().instance;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 3);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
|
||||
|
@ -1089,29 +1105,32 @@ mod test {
|
|||
let instances = 8;
|
||||
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let vector_data = vector_data.instances().next().unwrap().instance;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 8);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn circle_repeat() {
|
||||
async fn circular_repeat() {
|
||||
let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let vector_data = vector_data.instances().next().unwrap().instance;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 8);
|
||||
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
let expected_angle = (index as f64 + 1.) * 45.;
|
||||
|
||||
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
|
||||
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
|
||||
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5);
|
||||
|
||||
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn bounding_box() {
|
||||
let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await;
|
||||
let bounding_box = bounding_box.one_instance().instance;
|
||||
let bounding_box = bounding_box.instances().next().unwrap().instance;
|
||||
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
|
||||
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
|
||||
assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
|
||||
|
@ -1119,13 +1138,13 @@ mod test {
|
|||
// Test a VectorData with non-zero rotation
|
||||
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
|
||||
let mut square = VectorDataTable::new(square);
|
||||
*square.one_instance_mut().transform_mut() *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
|
||||
*square.one_instance_mut().transform *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
|
||||
let bounding_box = BoundingBoxNode {
|
||||
vector_data: FutureWrapperNode(square),
|
||||
}
|
||||
.eval(Footprint::default())
|
||||
.await;
|
||||
let bounding_box = bounding_box.one_instance().instance;
|
||||
let bounding_box = bounding_box.instances().next().unwrap().instance;
|
||||
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
|
||||
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
|
||||
let sqrt2 = core::f64::consts::SQRT_2;
|
||||
|
@ -1136,15 +1155,19 @@ mod test {
|
|||
async fn copy_to_points() {
|
||||
let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
|
||||
let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
|
||||
|
||||
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
|
||||
|
||||
let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
|
||||
let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
|
||||
let flattened_copy_to_points = flattened_copy_to_points.one_instance().instance;
|
||||
let flatten_vector_elements = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
|
||||
let flattened_copy_to_points = flatten_vector_elements.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
|
||||
|
||||
for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
|
||||
let offset = expected_points[index];
|
||||
assert_eq!(
|
||||
&subpath.anchors()[..4],
|
||||
&subpath.anchors(),
|
||||
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]
|
||||
);
|
||||
}
|
||||
|
@ -1153,7 +1176,7 @@ mod test {
|
|||
async fn sample_points() {
|
||||
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert_eq!(sample_points.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
|
||||
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
|
||||
|
@ -1163,7 +1186,7 @@ mod test {
|
|||
async fn adaptive_spacing() {
|
||||
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert_eq!(sample_points.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
|
||||
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
|
||||
|
@ -1178,7 +1201,7 @@ mod test {
|
|||
0,
|
||||
)
|
||||
.await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert!(
|
||||
(20..=40).contains(&sample_points.point_domain.positions().len()),
|
||||
"actual len {}",
|
||||
|
@ -1197,7 +1220,7 @@ mod test {
|
|||
#[tokio::test]
|
||||
async fn spline() {
|
||||
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
|
||||
let spline = spline.one_instance().instance;
|
||||
let spline = spline.instances().next().unwrap().instance;
|
||||
assert_eq!(spline.stroke_bezier_paths().count(), 1);
|
||||
assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]);
|
||||
}
|
||||
|
@ -1206,7 +1229,7 @@ mod test {
|
|||
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
|
||||
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
|
||||
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5, 0).await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert_eq!(
|
||||
&sample_points.point_domain.positions()[..4],
|
||||
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
|
||||
|
@ -1229,7 +1252,7 @@ mod test {
|
|||
async fn bevel_rect() {
|
||||
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
|
||||
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 8);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 8);
|
||||
|
@ -1252,7 +1275,7 @@ mod test {
|
|||
let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.);
|
||||
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), curve], false);
|
||||
let beveled = super::bevel((), vector_node(source), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 4);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 3);
|
||||
|
@ -1273,11 +1296,10 @@ mod test {
|
|||
let vector_data = VectorData::from_subpath(source);
|
||||
let mut vector_data_table = VectorDataTable::new(vector_data.clone());
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
|
||||
*vector_data_table.one_instance_mut().transform_mut() = transform;
|
||||
*vector_data_table.one_instance_mut().transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
|
||||
|
||||
let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 4);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 3);
|
||||
|
@ -1295,7 +1317,7 @@ mod test {
|
|||
async fn bevel_too_high() {
|
||||
let source = Subpath::from_anchors([DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)], false);
|
||||
let beveled = super::bevel(Footprint::default(), vector_node(source), 999.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 6);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||
|
@ -1316,7 +1338,7 @@ mod test {
|
|||
let point = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO, DVec2::ZERO);
|
||||
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), point, curve], false);
|
||||
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 6);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||
|
|
|
@ -13,44 +13,53 @@ use std::ops::Mul;
|
|||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn boolean_operation(_: impl Ctx, group_of_paths: GraphicGroupTable, operation: BooleanOperation) -> VectorDataTable {
|
||||
fn vector_from_image<T: Transform>(image_frame: T) -> VectorDataTable {
|
||||
let corner1 = DVec2::ZERO;
|
||||
let corner2 = DVec2::new(1., 1.);
|
||||
fn flatten_vector_data(graphic_group_table: &GraphicGroupTable) -> Vec<VectorDataTable> {
|
||||
graphic_group_table
|
||||
.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);
|
||||
subpath.apply_transform(image_frame.transform());
|
||||
vector_data
|
||||
}
|
||||
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);
|
||||
vector_data.style.set_fill(Fill::Solid(Color::from_rgb_str("777777").unwrap().to_gamma_srgb()));
|
||||
// Convert the image frame into a rectangular subpath with the image's transform
|
||||
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 {
|
||||
match graphic_element {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.clone(),
|
||||
// Union all vector data in the graphic group into a single vector
|
||||
GraphicElement::GraphicGroup(graphic_group) => {
|
||||
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<_>>()
|
||||
// Recursively flatten the inner group into vector data
|
||||
boolean_operation_on_vector_data(&flatten_vector_data(&graphic_group), BooleanOperation::Union)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
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)]
|
||||
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));
|
||||
*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().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 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
|
||||
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
|
||||
let result_vector_data_table_transform = result_vector_data_table.transform();
|
||||
|
|
|
@ -5,10 +5,12 @@ pub use graph_craft::wasm_application_io::*;
|
|||
use graphene_core::application_io::SurfaceHandle;
|
||||
use graphene_core::application_io::{ApplicationIo, ExportFormat, RenderConfig};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use graphene_core::instances::Instances;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use graphene_core::raster::bbox::Bbox;
|
||||
use graphene_core::raster::image::{Image, ImageFrameTable};
|
||||
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;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use graphene_core::transform::TransformMut;
|
||||
|
@ -149,19 +151,22 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
|
|||
RenderOutputType::CanvasFrame(frame)
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
#[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,
|
||||
#[implementations(
|
||||
VectorDataTable,
|
||||
ImageFrameTable<Color>,
|
||||
GraphicGroupTable,
|
||||
)]
|
||||
mut data: T,
|
||||
mut data: Instances<T>,
|
||||
footprint: Footprint,
|
||||
surface_handle: Arc<SurfaceHandle<HtmlCanvasElement>>,
|
||||
) -> ImageFrameTable<Color> {
|
||||
) -> ImageFrameTable<Color>
|
||||
where
|
||||
Instances<T>: GraphicElementRendered,
|
||||
{
|
||||
if footprint.transform.matrix2.determinant() == 0. {
|
||||
log::trace!("Invalid footprint received for rasterization");
|
||||
return ImageFrameTable::empty();
|
||||
|
@ -176,7 +181,9 @@ async fn rasterize<T: GraphicElementRendered + graphene_core::transform::Transfo
|
|||
..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);
|
||||
render.format_svg(glam::DVec2::ZERO, size);
|
||||
let svg_string = render.svg.to_svg_string();
|
||||
|
@ -232,7 +239,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
|
|||
ctx.footprint();
|
||||
|
||||
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 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 mut metadata = RenderMetadata {
|
||||
footprints: HashMap::new(),
|
||||
upstream_footprints: HashMap::new(),
|
||||
local_transforms: HashMap::new(),
|
||||
click_targets: HashMap::new(),
|
||||
clip_targets: HashSet::new(),
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue