Fix how transforms work with footprints and remove a redundant transforms field (#1484)

* Prune unused thumbnails in node graph executor

* Fix transform downcasting failure for GraphicElementData

* Remove more warnings

* Revert upstream transform calculation change

* Use footprint to calculate layer transforms

* Fix artboards

* Move artwork with artboard

* Remove Keavon's warnings

* Prevent misordered FrontendMessages failing to reach JS handlers

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert 2023-12-03 23:17:28 +01:00 committed by GitHub
parent 1093aabc88
commit b7fe38cf31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 116 additions and 112 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
use glam::{DAffine2, DVec2};
use graphene_core::renderer::ClickTarget;
use graphene_core::transform::Footprint;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
@ -9,8 +10,7 @@ use graphene_core::renderer::Quad;
#[derive(Debug, Clone)]
pub struct DocumentMetadata {
transforms: HashMap<LayerNodeIdentifier, DAffine2>,
upstream_transforms: HashMap<NodeId, DAffine2>,
upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
structure: HashMap<LayerNodeIdentifier, NodeRelations>,
artboards: HashSet<LayerNodeIdentifier>,
folders: HashSet<LayerNodeIdentifier>,
@ -23,7 +23,6 @@ pub struct DocumentMetadata {
impl Default for DocumentMetadata {
fn default() -> Self {
Self {
transforms: HashMap::new(),
upstream_transforms: HashMap::new(),
click_targets: HashMap::new(),
structure: HashMap::from_iter([(LayerNodeIdentifier::ROOT, NodeRelations::default())]),
@ -207,7 +206,6 @@ impl DocumentMetadata {
self.selected_nodes.retain(|node| graph.nodes.contains_key(node));
self.upstream_transforms.retain(|node, _| graph.nodes.contains_key(node));
self.transforms.retain(|layer, _| self.structure.contains_key(layer));
self.click_targets.retain(|layer, _| self.structure.contains_key(layer));
}
}
@ -222,38 +220,29 @@ fn sibling_below<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a
// transforms
impl DocumentMetadata {
/// Update the cached transforms of the layers
pub fn update_transforms(&mut self, mut new_transforms: HashMap<LayerNodeIdentifier, DAffine2>, new_upstream_transforms: HashMap<NodeId, DAffine2>) {
let mut stack = vec![(LayerNodeIdentifier::ROOT, DAffine2::IDENTITY)];
while let Some((layer, transform)) = stack.pop() {
for child in layer.children(self) {
let Some(new_transform) = new_transforms.get_mut(&child) else { continue };
*new_transform = transform * *new_transform;
stack.push((child, *new_transform));
}
}
self.transforms = new_transforms;
pub fn update_transforms(&mut self, new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>) {
self.upstream_transforms = new_upstream_transforms;
}
/// Access the cached transformation to document space from layer space
pub fn transform_to_document(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.transforms.get(&layer).copied().unwrap_or_else(|| {
warn!("Tried to access transform of bad layer {layer:?}");
DAffine2::IDENTITY
})
}
pub fn local_transform(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.transform_to_document(layer.parent(self).unwrap_or_default()).inverse() * self.transform_to_document(layer)
self.document_to_viewport.inverse() * self.transform_to_viewport(layer)
}
pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 {
self.document_to_viewport * self.transform_to_document(layer)
self.upstream_transforms
.get(&layer.to_node())
.copied()
.map(|(footprint, transform)| footprint.transform * transform)
.unwrap_or(DAffine2::IDENTITY)
}
pub fn upstream_transform(&self, node_id: NodeId) -> DAffine2 {
self.upstream_transforms.get(&node_id).copied().unwrap_or(DAffine2::IDENTITY)
self.upstream_transforms.get(&node_id).copied().map(|(_, transform)| transform).unwrap_or(DAffine2::IDENTITY)
}
pub fn downstream_transform_to_viewport(&self, node_id: NodeId) -> DAffine2 {
self.upstream_transforms.get(&node_id).copied().map(|(footprint, _)| footprint.transform).unwrap_or(DAffine2::IDENTITY)
}
}

View File

@ -576,23 +576,11 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
skip_rerender,
} => {
let layer_identifier = LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network);
let parent_transform = document
.metadata
.transform_to_viewport(layer_identifier.parent(&document.metadata).unwrap_or(LayerNodeIdentifier::ROOT));
let parent_transform = document.metadata.downstream_transform_to_viewport(layer_identifier.to_node());
let bounds = LayerBounds::new(document, &layer);
if let Some(mut modify_inputs) = ModifyInputsContext::new_layer(&layer, document, node_graph, responses) {
modify_inputs.transform_change(transform, transform_in, parent_transform, bounds, skip_rerender);
}
let transform = transform.to_cols_array();
responses.add(match transform_in {
TransformIn::Local => Operation::TransformLayer { path: layer, transform },
TransformIn::Scope { scope } => {
let scope = scope.to_cols_array();
Operation::TransformLayerInScope { path: layer, transform, scope }
}
TransformIn::Viewport => Operation::TransformLayerInViewport { path: layer, transform },
});
}
GraphOperationMessage::TransformSet {
layer,
@ -601,9 +589,7 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
skip_rerender,
} => {
let layer_identifier = LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network);
let parent_transform = document
.metadata
.transform_to_viewport(layer_identifier.parent(&document.metadata).unwrap_or(LayerNodeIdentifier::ROOT));
let parent_transform = document.metadata.downstream_transform_to_viewport(layer_identifier.to_node());
let current_transform = Some(document.metadata.transform_to_viewport(layer_identifier));
let bounds = LayerBounds::new(document, &layer);

View File

@ -258,7 +258,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
DocumentNodeBlueprint {
name: "Artboard",
category: "General",
identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _>"),
identifier: NodeImplementation::proto("graphene_core::ConstructArtboardNode<_, _, _, _, _>"),
inputs: vec![
DocumentInputType::value("Graphic Group", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
DocumentInputType::value("Location", TaggedValue::IVec2(glam::IVec2::ZERO), false),
@ -268,6 +268,7 @@ fn static_nodes() -> Vec<DocumentNodeBlueprint> {
],
outputs: vec![DocumentOutputType::new("Out", FrontendGraphDataType::Artboard)],
properties: node_properties::artboard_properties,
manual_composition: Some(concrete!(Footprint)),
..Default::default()
},
DocumentNodeBlueprint {

View File

@ -34,7 +34,7 @@ impl OriginalTransforms {
match self {
OriginalTransforms::Layer(layer_map) => {
for &layer in selected {
layer_map.entry(layer).or_insert_with(|| document.metadata.local_transform(layer));
layer_map.entry(layer).or_insert_with(|| document.metadata.upstream_transform(layer.to_node()));
}
}
OriginalTransforms::Path(path_map) => {
@ -368,8 +368,7 @@ impl<'a> Selected<'a> {
fn transform_layer(document: &Document, layer: LayerNodeIdentifier, original_transform: Option<&DAffine2>, transformation: DAffine2, responses: &mut VecDeque<Message>) {
let Some(&original_transform) = original_transform else { return };
let parent = layer.parent(&document.metadata);
let to = parent.map(|parent| document.metadata.transform_to_viewport(parent)).unwrap_or(document.metadata.document_to_viewport);
let to = document.metadata.downstream_transform_to_viewport(layer.to_node());
let new = to.inverse() * transformation * to * original_transform;
responses.add(GraphOperationMessage::TransformSet {
layer: layer.to_path(),

View File

@ -691,7 +691,6 @@ impl PortfolioMessageHandler {
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) {
let Some(active_document) = self.active_document_id.and_then(|id| self.documents.get_mut(&id)) else {
warn!("Polling node graph with no document");
return;
};

View File

@ -14,6 +14,7 @@ use graph_craft::graphene_compiler::Compiler;
use graph_craft::imaginate_input::ImaginatePreferences;
use graph_craft::{concrete, Type};
use graphene_core::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_core::memo::IORecord;
use graphene_core::raster::{Image, ImageFrame};
use graphene_core::renderer::{ClickTarget, GraphicElementRendered, SvgSegment, SvgSegmentList};
use graphene_core::text::FontCache;
@ -21,7 +22,7 @@ use graphene_core::transform::{Footprint, Transform};
use graphene_core::vector::style::ViewMode;
use graphene_core::vector::VectorData;
use graphene_core::{Color, SurfaceFrame, SurfaceId};
use graphene_core::{Color, GraphicElementData, SurfaceFrame, SurfaceId};
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::DynamicExecutor;
@ -56,10 +57,10 @@ pub struct NodeRuntime {
imaginate_preferences: ImaginatePreferences,
pub(crate) thumbnails: HashMap<NodeId, SvgSegmentList>,
pub(crate) click_targets: HashMap<NodeId, Vec<ClickTarget>>,
pub(crate) transforms: HashMap<NodeId, DAffine2>,
pub(crate) upstream_transforms: HashMap<NodeId, DAffine2>,
pub(crate) upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
graph_hash: Option<u64>,
canvas_cache: HashMap<Vec<LayerId>, SurfaceId>,
monitor_nodes: Vec<Vec<NodeId>>,
}
enum NodeRuntimeMessage {
@ -81,8 +82,7 @@ pub(crate) struct GenerationResponse {
updates: VecDeque<Message>,
new_thumbnails: HashMap<NodeId, SvgSegmentList>,
new_click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
new_transforms: HashMap<LayerNodeIdentifier, DAffine2>,
new_upstream_transforms: HashMap<NodeId, DAffine2>,
new_upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
transform: DAffine2,
}
@ -109,8 +109,6 @@ thread_local! {
pub(crate) static NODE_RUNTIME: Rc<RefCell<Option<NodeRuntime>>> = Rc::new(RefCell::new(None));
}
type MonitorNodes = Vec<Vec<NodeId>>;
impl NodeRuntime {
fn new(receiver: Receiver<NodeRuntimeMessage>, sender: Sender<NodeGraphUpdate>) -> Self {
let executor = DynamicExecutor::default();
@ -124,9 +122,9 @@ impl NodeRuntime {
wasm_io: None,
canvas_cache: HashMap::new(),
click_targets: HashMap::new(),
transforms: HashMap::new(),
graph_hash: None,
upstream_transforms: HashMap::new(),
monitor_nodes: Vec::new(),
}
}
pub async fn run(&mut self) {
@ -151,19 +149,18 @@ impl NodeRuntime {
..
}) => {
let transform = render_config.viewport.transform;
let (result, monitor_nodes) = self.execute_network(&path, graph, render_config).await;
let result = self.execute_network(&path, graph, render_config).await;
let mut responses = VecDeque::new();
if let Some(ref monitor_nodes) = monitor_nodes {
self.update_thumbnails(&path, monitor_nodes, &mut responses);
self.update_upstream_transforms(monitor_nodes);
}
self.update_thumbnails(&path, &mut responses);
self.update_upstream_transforms();
let response = GenerationResponse {
generation_id,
result,
updates: responses,
new_thumbnails: self.thumbnails.clone(),
new_click_targets: self.click_targets.clone().into_iter().map(|(id, targets)| (LayerNodeIdentifier::new_unchecked(id), targets)).collect(),
new_transforms: self.transforms.clone().into_iter().map(|(id, transform)| (LayerNodeIdentifier::new_unchecked(id), transform)).collect(),
new_upstream_transforms: self.upstream_transforms.clone(),
transform,
};
@ -173,7 +170,7 @@ impl NodeRuntime {
}
}
async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, render_config: RenderConfig) -> (Result<TaggedValue, String>, Option<MonitorNodes>) {
async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, render_config: RenderConfig) -> Result<TaggedValue, String> {
if self.wasm_io.is_none() {
self.wasm_io = Some(WasmApplicationIo::new().await);
}
@ -200,12 +197,10 @@ impl NodeRuntime {
self.graph_hash = None;
}
let mut cached_monitor_nodes = None;
if self.graph_hash.is_none() {
let scoped_network = wrap_network_in_scope(graph, font_hash_code);
let monitor_nodes = scoped_network
self.monitor_nodes = scoped_network
.recursive_nodes()
.filter(|(_, node)| node.implementation == DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_, _, _>"))
.map(|(_, node)| node.path.clone().unwrap_or_default())
@ -216,16 +211,15 @@ impl NodeRuntime {
let c = Compiler {};
let proto_network = match c.compile_single(scoped_network) {
Ok(network) => network,
Err(e) => return (Err(e), Some(monitor_nodes)),
Err(e) => return Err(e),
};
assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?");
if let Err(e) = self.executor.update(proto_network).await {
error!("Failed to update executor:\n{e}");
return (Err(e), Some(monitor_nodes));
return Err(e);
}
cached_monitor_nodes = Some(monitor_nodes);
self.graph_hash = Some(hash_code);
}
@ -239,7 +233,7 @@ impl NodeRuntime {
};
let result = match result {
Ok(value) => value,
Err(e) => return (Err(e), cached_monitor_nodes),
Err(e) => return Err(e),
};
if let TaggedValue::SurfaceFrame(SurfaceFrame { surface_id, transform: _ }) = result {
@ -252,13 +246,14 @@ impl NodeRuntime {
}
}
}
(Ok(result), cached_monitor_nodes)
Ok(result)
}
/// Recomputes the thumbnails for the layers in the graph, modifying the state and updating the UI.
pub fn update_thumbnails(&mut self, layer_path: &[LayerId], monitor_nodes: &[Vec<u64>], responses: &mut VecDeque<Message>) {
pub fn update_thumbnails(&mut self, layer_path: &[LayerId], responses: &mut VecDeque<Message>) {
let mut image_data: Vec<_> = Vec::new();
for node_path in monitor_nodes {
self.thumbnails.retain(|id, _| self.monitor_nodes.iter().any(|node_path| node_path.contains(id)));
for node_path in &self.monitor_nodes {
let Some(node_id) = node_path.get(node_path.len() - 2).copied() else {
warn!("Monitor node has invalid node id");
continue;
@ -268,8 +263,7 @@ impl NodeRuntime {
continue;
};
let Some(io_data) = value.downcast_ref::<graphene_core::memo::IORecord<Footprint, graphene_core::GraphicElementData>>() else {
warn!("Failed to downcast thumbnail to graphic element data");
let Some(io_data) = value.downcast_ref::<IORecord<Footprint, graphene_core::GraphicElementData>>() else {
continue;
};
let graphic_element_data = &io_data.output;
@ -283,10 +277,9 @@ impl NodeRuntime {
let click_targets = self.click_targets.entry(node_id).or_default();
click_targets.clear();
// Add the graphic element data's click targets to the click targets vector
graphic_element_data.add_click_targets(click_targets);
self.transforms.insert(node_id, graphic_element_data.transform());
let old_thumbnail = self.thumbnails.entry(node_id).or_default();
if *old_thumbnail != render.svg {
responses.add(FrontendMessage::UpdateNodeThumbnail {
@ -305,8 +298,8 @@ impl NodeRuntime {
}
}
pub fn update_upstream_transforms(&mut self, monitor_nodes: &[Vec<u64>]) {
for node_path in monitor_nodes {
pub fn update_upstream_transforms(&mut self) {
for node_path in &self.monitor_nodes {
let Some(node_id) = node_path.get(node_path.len() - 2).copied() else {
warn!("Monitor node has invalid node id");
continue;
@ -315,14 +308,16 @@ impl NodeRuntime {
warn!("Failed to introspect monitor node for upstream transforms");
continue;
};
let Some(transform) = value
.downcast_ref::<graphene_core::memo::IORecord<Footprint, VectorData>>()
.map(|vector_data| vector_data.output.transform())
.or_else(|| {
value
.downcast_ref::<graphene_core::memo::IORecord<Footprint, ImageFrame<Color>>>()
.map(|image| image.output.transform())
})
fn try_downcast<T: Transform + 'static>(value: &dyn std::any::Any) -> Option<(Footprint, DAffine2)> {
let io_data = value.downcast_ref::<IORecord<Footprint, T>>()?;
let transform = io_data.output.transform();
Some((io_data.input, transform))
}
let Some(transform) = try_downcast::<VectorData>(value.as_ref())
.or_else(|| try_downcast::<ImageFrame<Color>>(value.as_ref()))
.or_else(|| try_downcast::<GraphicElementData>(value.as_ref()))
else {
warn!("Failed to downcast transform input");
continue;
@ -527,7 +522,6 @@ impl NodeGraphExecutor {
updates,
new_thumbnails,
new_click_targets,
new_transforms,
new_upstream_transforms,
transform,
}) => {
@ -566,7 +560,7 @@ impl NodeGraphExecutor {
});
}
self.thumbnails = new_thumbnails;
document.metadata.update_transforms(new_transforms, new_upstream_transforms);
document.metadata.update_transforms(new_upstream_transforms);
document.metadata.update_click_targets(new_click_targets);
let node_graph_output = result.map_err(|e| format!("Node graph evaluation failed: {e:?}"))?;
let execution_context = self.futures.remove(&generation_id).ok_or_else(|| "Invalid generation ID".to_string())?;

View File

@ -44,16 +44,26 @@ export function createSubscriptionRouter() {
// The resulting `message` is an instance of a class that extends `JsMessage`.
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, instance);
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
const callback = subscriptions[message.constructor.name];
// If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
// The frontend should always have a callback for all messages, and so we display an error if one was not found.
if (message) {
if (callback) callback(message);
// eslint-disable-next-line no-console
else console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
}
// The frontend should always have a callback for all messages, but due to message ordering, we might have to delay a few stack frames until we do.
let retries = 0;
const callCallback = () => {
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
const callback = subscriptions[message.constructor.name];
// Attempt to call the callback, but try again several times on the next stack frame if it is not yet registered due to message ordering.
if (callback) {
callback(message);
} else if (retries <= 3) {
retries += 1;
setTimeout(callCallback, 0);
} else {
// eslint-disable-next-line no-console
console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
}
};
callCallback();
};
return {

View File

@ -1,4 +1,5 @@
use crate::raster::{BlendMode, ImageFrame};
use crate::transform::Footprint;
use crate::vector::VectorData;
use crate::{Color, Node};
@ -136,7 +137,8 @@ fn to_graphic_element_data<Data: Into<GraphicElementData>>(graphic_element_data:
graphic_element_data.into()
}
pub struct ConstructArtboardNode<Location, Dimensions, Background, Clip> {
pub struct ConstructArtboardNode<Contents, Location, Dimensions, Background, Clip> {
contents: Contents,
location: Location,
dimensions: Dimensions,
background: Background,
@ -144,7 +146,16 @@ pub struct ConstructArtboardNode<Location, Dimensions, Background, Clip> {
}
#[node_fn(ConstructArtboardNode)]
fn construct_artboard(graphic_group: GraphicGroup, location: IVec2, dimensions: IVec2, background: Color, clip: bool) -> Artboard {
async fn construct_artboard<Fut: Future<Output = GraphicGroup>>(
mut footprint: Footprint,
contents: impl Node<Footprint, Output = Fut>,
location: IVec2,
dimensions: IVec2,
background: Color,
clip: bool,
) -> Artboard {
footprint.transform = footprint.transform * DAffine2::from_translation(location.as_dvec2());
let graphic_group = self.contents.eval(footprint).await;
Artboard {
graphic_group,
location: location.min(location + dimensions),

View File

@ -284,15 +284,20 @@ impl GraphicElementRendered for Artboard {
"g",
|attributes| {
attributes.push("class", "artboard");
attributes.push("transform", format_transform_matrix(self.graphic_group.transform));
attributes.push(
"transform",
format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform),
);
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
use std::fmt::Write;
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="{}" y="{}" width="{}" height="{}"/></clipPath>"##,
self.location.x, self.location.y, self.dimensions.x, self.dimensions.y
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" transform="{}"/></clipPath>"##,
self.dimensions.x,
self.dimensions.y,
format_transform_matrix(self.graphic_group.transform.inverse())
)
.unwrap();
attributes.push("clip-path", selector);

View File

@ -54,6 +54,10 @@ pub trait Node<'i, Input: 'i>: 'i {
fn eval(&'i self, input: Input) -> Self::Output;
/// Resets the node, e.g. the LetNode's cache is set to None.
fn reset(&self) {}
/// Returns the name of the node for diagnostic purposes.
fn node_name(&self) -> &'static str {
core::any::type_name::<Self>()
}
/// Serialize the node which is used for the `introspect` function which can retrieve values from monitor nodes.
#[cfg(feature = "std")]
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
@ -98,10 +102,6 @@ where
Self::Output: 'i + StaticTypeSized,
Input: 'i + StaticTypeSized,
{
fn node_name(&self) -> &'static str {
core::any::type_name::<Self>()
}
fn input_type(&self) -> TypeId {
TypeId::of::<Input::Static>()
}

View File

@ -9,6 +9,7 @@ use crate::raster::bbox::AxisAlignedBbox;
use crate::raster::ImageFrame;
use crate::raster::Pixel;
use crate::vector::VectorData;
use crate::Artboard;
use crate::GraphicElementData;
use crate::GraphicGroup;
use crate::Node;
@ -76,7 +77,7 @@ impl Transform for GraphicElementData {
GraphicElementData::ImageFrame(image_frame) => image_frame.transform(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.transform(),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.transform(),
GraphicElementData::Artboard(artboard) => artboard.transform(),
}
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
@ -85,7 +86,7 @@ impl Transform for GraphicElementData {
GraphicElementData::ImageFrame(image_frame) => image_frame.local_pivot(pivot),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.local_pivot(pivot),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.local_pivot(pivot),
GraphicElementData::Artboard(artboard) => artboard.local_pivot(pivot),
}
}
fn decompose_scale(&self) -> DVec2 {
@ -94,7 +95,7 @@ impl Transform for GraphicElementData {
GraphicElementData::ImageFrame(image_frame) => image_frame.decompose_scale(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.decompose_scale(),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.decompose_scale(),
GraphicElementData::Artboard(artboard) => artboard.decompose_scale(),
}
}
}
@ -105,7 +106,7 @@ impl TransformMut for GraphicElementData {
GraphicElementData::ImageFrame(image_frame) => image_frame.transform_mut(),
GraphicElementData::Text(_) => todo!("Transform of text"),
GraphicElementData::GraphicGroup(graphic_group) => graphic_group.transform_mut(),
GraphicElementData::Artboard(artboard) => artboard.graphic_group.transform_mut(),
GraphicElementData::Artboard(_) => todo!("Transform of artboard"),
}
}
}
@ -124,6 +125,15 @@ impl TransformMut for VectorData {
}
}
impl Transform for Artboard {
fn transform(&self) -> DAffine2 {
DAffine2::IDENTITY
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.location.as_dvec2() + self.dimensions.as_dvec2() * pivot
}
}
impl Transform for DAffine2 {
fn transform(&self) -> DAffine2 {
*self

View File

@ -844,7 +844,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_core::ToGraphicElementData, input: ImageFrame<Color>, params: []),
register_node!(graphene_core::ToGraphicElementData, input: GraphicGroup, params: []),
register_node!(graphene_core::ToGraphicElementData, input: Artboard, params: []),
register_node!(graphene_core::ConstructArtboardNode<_, _, _, _>, input: GraphicGroup, params: [glam::IVec2, glam::IVec2, Color, bool]),
async_node!(graphene_core::ConstructArtboardNode<_, _, _, _, _>, input: Footprint, output: Artboard, fn_params: [Footprint => GraphicGroup, () => glam::IVec2, () => glam::IVec2, () => Color, () => bool]),
];
let mut map: HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
for (id, c, types) in node_types.into_iter().flatten() {