Add tests to the Ellipse, Artboard, and Fill tools (#2181)
* Add ellipse tests * Add tests for fill tool and re-enable some other tests * Code review * Fix Rust crate advisory --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
1190e82322
commit
b171eeba84
|
|
@ -3619,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5565,15 +5565,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
version = "0.17.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ impl Editor {
|
|||
Self { dispatcher: Dispatcher::new() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_local_executor() -> (Self, crate::node_graph_executor::NodeRuntime) {
|
||||
let (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime();
|
||||
let dispatcher = Dispatcher::with_executor(executor);
|
||||
(Self { dispatcher }, runtime)
|
||||
}
|
||||
|
||||
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Vec<FrontendMessage> {
|
||||
self.dispatcher.handle_message(message, true);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ pub struct DispatcherMessageHandlers {
|
|||
workspace_message_handler: WorkspaceMessageHandler,
|
||||
}
|
||||
|
||||
impl DispatcherMessageHandlers {
|
||||
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
|
||||
Self {
|
||||
portfolio_message_handler: PortfolioMessageHandler::with_executor(executor),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For optimization, these are messages guaranteed to be redundant when repeated.
|
||||
/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior.
|
||||
/// In addition, these messages do not change any state in the backend (aside from caches).
|
||||
|
|
@ -53,6 +62,13 @@ impl Dispatcher {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
|
||||
Self {
|
||||
message_handlers: DispatcherMessageHandlers::with_executor(executor),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// If the deepest queues (higher index in queues list) are now empty (after being popped from) then remove them
|
||||
fn cleanup_queues(&mut self, leave_last: bool) {
|
||||
while self.message_queues.last().filter(|queue| queue.is_empty()).is_some() {
|
||||
|
|
@ -328,60 +344,48 @@ impl Dispatcher {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::application::Editor;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::test_utils::EditorTestUtils;
|
||||
use graphene_core::raster::color::Color;
|
||||
|
||||
fn init_logger() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
}
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
|
||||
/// Create an editor with three layers
|
||||
/// 1. A red rectangle
|
||||
/// 2. A blue shape
|
||||
/// 3. A green ellipse
|
||||
fn create_editor_with_three_layers() -> Editor {
|
||||
init_logger();
|
||||
let mut editor = Editor::create();
|
||||
async fn create_editor_with_three_layers() -> EditorTestUtils {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
editor.new_document();
|
||||
editor.new_document().await;
|
||||
|
||||
editor.select_primary_color(Color::RED);
|
||||
editor.draw_rect(100., 200., 300., 400.);
|
||||
editor.select_primary_color(Color::RED).await;
|
||||
editor.draw_rect(100., 200., 300., 400.).await;
|
||||
|
||||
editor.select_primary_color(Color::BLUE);
|
||||
editor.draw_polygon(10., 1200., 1300., 400.);
|
||||
editor.select_primary_color(Color::BLUE).await;
|
||||
editor.draw_polygon(10., 1200., 1300., 400.).await;
|
||||
|
||||
editor.select_primary_color(Color::GREEN);
|
||||
editor.draw_ellipse(104., 1200., 1300., 400.);
|
||||
editor.select_primary_color(Color::GREEN).await;
|
||||
editor.draw_ellipse(104., 1200., 1300., 400.).await;
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
// TODO: Fix text
|
||||
#[ignore]
|
||||
#[test]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - copy
|
||||
/// - paste
|
||||
/// - assert that ellipse was copied
|
||||
fn copy_paste_single_layer() {
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
#[tokio::test]
|
||||
async fn copy_paste_single_layer() {
|
||||
let mut editor = create_editor_with_three_layers().await;
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
let layers_before_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await;
|
||||
editor
|
||||
.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||
insert_index: 0,
|
||||
});
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
})
|
||||
.await;
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata().all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata().all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 4);
|
||||
|
|
@ -392,33 +396,30 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix text
|
||||
#[ignore]
|
||||
#[test]
|
||||
#[cfg_attr(miri, ignore)]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - select shape
|
||||
/// - copy
|
||||
/// - paste
|
||||
/// - assert that shape was copied
|
||||
fn copy_paste_single_layer_from_middle() {
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
#[tokio::test]
|
||||
async fn copy_paste_single_layer_from_middle() {
|
||||
let mut editor = create_editor_with_three_layers().await;
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
let shape_id = document_before_copy.metadata().all_layers().nth(1).unwrap();
|
||||
let layers_before_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
|
||||
let shape_id = editor.active_document().metadata().all_layers().nth(1).unwrap();
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] });
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![shape_id.to_node()] }).await;
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await;
|
||||
editor
|
||||
.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||
insert_index: 0,
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata().all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata().all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 4);
|
||||
|
|
@ -429,9 +430,6 @@ mod test {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Fix text
|
||||
#[ignore]
|
||||
#[test]
|
||||
#[cfg_attr(miri, ignore)]
|
||||
/// - create rect, shape and ellipse
|
||||
/// - select ellipse and rect
|
||||
|
|
@ -440,36 +438,40 @@ mod test {
|
|||
/// - create another rect
|
||||
/// - paste
|
||||
/// - paste
|
||||
fn copy_paste_deleted_layers() {
|
||||
let mut editor = create_editor_with_three_layers();
|
||||
#[tokio::test]
|
||||
async fn copy_paste_deleted_layers() {
|
||||
let mut editor = create_editor_with_three_layers().await;
|
||||
assert_eq!(editor.active_document().metadata().all_layers().count(), 3);
|
||||
|
||||
let document_before_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
let mut layers = document_before_copy.metadata().all_layers();
|
||||
let rect_id = layers.next().expect("rectangle");
|
||||
let shape_id = layers.next().expect("shape");
|
||||
let ellipse_id = layers.next().expect("ellipse");
|
||||
let layers_before_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
|
||||
let rect_id = layers_before_copy[0];
|
||||
let shape_id = layers_before_copy[1];
|
||||
let ellipse_id = layers_before_copy[2];
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet {
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::SelectedNodesSet {
|
||||
nodes: vec![rect_id.to_node(), ellipse_id.to_node()],
|
||||
});
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
|
||||
editor.draw_rect(0., 800., 12., 200.);
|
||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
})
|
||||
.await;
|
||||
editor.handle_message(PortfolioMessage::Copy { clipboard: Clipboard::Internal }).await;
|
||||
editor.handle_message(NodeGraphMessage::DeleteSelectedNodes { delete_children: true }).await;
|
||||
editor.draw_rect(0., 800., 12., 200.).await;
|
||||
editor
|
||||
.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||
insert_index: 0,
|
||||
});
|
||||
editor.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
})
|
||||
.await;
|
||||
editor
|
||||
.handle_message(PortfolioMessage::PasteIntoFolder {
|
||||
clipboard: Clipboard::Internal,
|
||||
parent: LayerNodeIdentifier::ROOT_PARENT,
|
||||
insert_index: 0,
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
let document_after_copy = editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap().clone();
|
||||
|
||||
let layers_before_copy = document_before_copy.metadata().all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = document_after_copy.metadata().all_layers().collect::<Vec<_>>();
|
||||
let layers_after_copy = editor.active_document().metadata().all_layers().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 6);
|
||||
|
|
@ -498,8 +500,7 @@ mod test {
|
|||
panic!()
|
||||
};
|
||||
|
||||
init_logger();
|
||||
let mut editor = Editor::create();
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
// UNCOMMENT THIS FOR RUNNING UNDER MIRI
|
||||
//
|
||||
|
|
@ -523,20 +524,13 @@ mod test {
|
|||
"Demo artwork '{document_name}' has more than 1 line (remember to open and re-save it in Graphite)",
|
||||
);
|
||||
|
||||
let responses = editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: document_name.into(),
|
||||
document_serialized_content,
|
||||
});
|
||||
|
||||
// Check if the graph renders
|
||||
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
|
||||
portfolio
|
||||
.executor
|
||||
.submit_node_graph_evaluation(portfolio.documents.get_mut(&portfolio.active_document_id.unwrap()).unwrap(), glam::UVec2::ONE, true)
|
||||
.expect("submit_node_graph_evaluation failed");
|
||||
crate::node_graph_executor::run_node_graph().await;
|
||||
let mut messages = VecDeque::new();
|
||||
editor.poll_node_graph_evaluation(&mut messages).expect("Graph should render");
|
||||
editor.eval_graph().await;
|
||||
|
||||
for response in responses {
|
||||
// Check for the existence of the file format incompatibility warning dialog after opening the test file
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@ pub mod consts;
|
|||
pub mod dispatcher;
|
||||
pub mod messages;
|
||||
pub mod node_graph_executor;
|
||||
#[cfg(test)]
|
||||
pub mod test_utils;
|
||||
pub mod utility_traits;
|
||||
|
|
|
|||
|
|
@ -5622,6 +5622,34 @@ impl NodeNetworkInterface {
|
|||
self.force_set_upstream_to_chain(node_id, network_path);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_recursive(&self) -> NodesRecursiveIter<'_> {
|
||||
NodesRecursiveIter {
|
||||
stack: vec![&self.network],
|
||||
current_slice: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodesRecursiveIter<'a> {
|
||||
stack: Vec<&'a NodeNetwork>,
|
||||
current_slice: Option<std::collections::hash_map::Iter<'a, NodeId, DocumentNode>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NodesRecursiveIter<'a> {
|
||||
type Item = (NodeId, &'a DocumentNode);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if let Some((id, node)) = self.current_slice.as_mut().and_then(|iter| iter.next()) {
|
||||
if let DocumentNodeImplementation::Network(network) = &node.implementation {
|
||||
self.stack.push(network);
|
||||
}
|
||||
return Some((*id, node));
|
||||
}
|
||||
let network = self.stack.pop()?;
|
||||
self.current_slice = Some(network.nodes.iter());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
|
|||
|
|
@ -1160,7 +1160,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
}
|
||||
|
||||
impl PortfolioMessageHandler {
|
||||
pub async fn introspect_node(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any>, IntrospectError> {
|
||||
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
|
||||
Self { executor, ..Default::default() }
|
||||
}
|
||||
|
||||
pub async fn introspect_node(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync>, IntrospectError> {
|
||||
self.executor.introspect_node(node_path).await
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -394,6 +394,15 @@ impl<'a> NodeGraphLayer<'a> {
|
|||
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
|
||||
}
|
||||
|
||||
/// Node id of a protonode if it exists in the layer's primary flow
|
||||
pub fn upstream_node_id_from_protonode(&self, protonode_identifier: &'static str) -> Option<NodeId> {
|
||||
self.horizontal_layer_flow().find(move |node_id| {
|
||||
self.network_interface
|
||||
.implementation(node_id, &[])
|
||||
.is_some_and(move |implementation| *implementation == graph_craft::document::DocumentNodeImplementation::proto(protonode_identifier))
|
||||
})
|
||||
}
|
||||
|
||||
/// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached.
|
||||
pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec<NodeInput>> {
|
||||
self.horizontal_layer_flow()
|
||||
|
|
|
|||
|
|
@ -3,18 +3,16 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
|
|||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::renderer::Quad;
|
||||
|
||||
use glam::{IVec2, Vec2Swizzles};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ArtboardTool {
|
||||
fsm_state: ArtboardToolFsmState,
|
||||
|
|
@ -112,7 +110,8 @@ struct ArtboardToolData {
|
|||
drag_current: DVec2,
|
||||
auto_panning: AutoPanning,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
dragging_current_artboard_location: IVec2,
|
||||
dragging_current_artboard_location: glam::IVec2,
|
||||
draw: Resize,
|
||||
}
|
||||
|
||||
impl ArtboardToolData {
|
||||
|
|
@ -256,14 +255,7 @@ impl Fsm for ArtboardToolFsmState {
|
|||
tool_data.get_snap_candidates(document, input);
|
||||
ArtboardToolFsmState::Dragging
|
||||
} else {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
|
||||
let point = SnapCandidatePoint::handle(to_document.transform_point2(input.mouse.position));
|
||||
|
||||
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
|
||||
|
||||
tool_data.drag_start = snapped.snapped_point_document;
|
||||
tool_data.drag_current = snapped.snapped_point_document;
|
||||
tool_data.draw.start(document, input);
|
||||
|
||||
ArtboardToolFsmState::Drawing
|
||||
};
|
||||
|
|
@ -324,46 +316,15 @@ impl Fsm for ArtboardToolFsmState {
|
|||
ArtboardToolFsmState::Dragging
|
||||
}
|
||||
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||
let to_viewport = document.metadata().document_to_viewport;
|
||||
let ignore = if let Some(layer) = tool_data.selected_artboard { vec![layer] } else { vec![] };
|
||||
let snap_data = SnapData::ignore(document, input, &ignore);
|
||||
|
||||
let document_mouse = to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
let config = SnapTypeConfiguration::default();
|
||||
let snapped = tool_data.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
|
||||
let snapped_mouse_position = to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
|
||||
tool_data.snap_manager.update_indicator(snapped);
|
||||
|
||||
let mut start = to_viewport.transform_point2(tool_data.drag_start);
|
||||
let mut size = snapped_mouse_position - start;
|
||||
|
||||
// Constrain axis
|
||||
if input.keyboard.get(constrain_axis_or_aspect as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
|
||||
// From center
|
||||
if input.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
}
|
||||
|
||||
let start = to_viewport.inverse().transform_point2(start);
|
||||
let size = to_viewport.inverse().transform_vector2(size);
|
||||
let end = start + size;
|
||||
|
||||
let [start, end] = tool_data.draw.calculate_points_ignore_layer(document, input, center, constrain_axis_or_aspect);
|
||||
if let Some(artboard) = tool_data.selected_artboard {
|
||||
if artboard == LayerNodeIdentifier::ROOT_PARENT {
|
||||
log::error!("Selected artboard cannot be ROOT_PARENT");
|
||||
} else {
|
||||
assert_ne!(artboard, LayerNodeIdentifier::ROOT_PARENT, "Selected artboard cannot be ROOT_PARENT");
|
||||
|
||||
responses.add(GraphOperationMessage::ResizeArtboard {
|
||||
layer: artboard,
|
||||
location: start.min(end).round().as_ivec2(),
|
||||
dimensions: (start.round() - end.round()).abs().as_ivec2(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let id = NodeId::new();
|
||||
|
||||
|
|
@ -374,8 +335,8 @@ impl Fsm for ArtboardToolFsmState {
|
|||
artboard: graphene_core::Artboard {
|
||||
graphic_group: graphene_core::GraphicGroupTable::default(),
|
||||
label: String::from("Artboard"),
|
||||
location: start.round().as_ivec2(),
|
||||
dimensions: IVec2::splat(1),
|
||||
location: start.min(end).round().as_ivec2(),
|
||||
dimensions: (start.round() - end.round()).abs().as_ivec2(),
|
||||
background: graphene_core::Color::WHITE,
|
||||
clip: false,
|
||||
},
|
||||
|
|
@ -594,3 +555,102 @@ impl Fsm for ArtboardToolFsmState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_artboard {
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
|
||||
async fn get_artboards(editor: &mut EditorTestUtils) -> Vec<graphene_core::Artboard> {
|
||||
let instrumented = editor.eval_graph().await;
|
||||
instrumented.grab_all_input::<graphene_core::append_artboard::ArtboardInput>(&editor.runtime).collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_draw_simple() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::empty()).await;
|
||||
|
||||
let artboards = get_artboards(&mut editor).await;
|
||||
|
||||
assert_eq!(artboards.len(), 1);
|
||||
assert_eq!(artboards[0].location, IVec2::new(10, 0));
|
||||
assert_eq!(artboards[0].dimensions, IVec2::new(10, 11));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_draw_square() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Artboard, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
|
||||
|
||||
let artboards = get_artboards(&mut editor).await;
|
||||
assert_eq!(artboards.len(), 1);
|
||||
assert_eq!(artboards[0].location, IVec2::new(-10, 10));
|
||||
assert_eq!(artboards[0].dimensions, IVec2::new(20, 20));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_draw_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
// Viewport coordinates
|
||||
editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT).await;
|
||||
|
||||
let artboards = get_artboards(&mut editor).await;
|
||||
assert_eq!(artboards.len(), 1);
|
||||
assert_eq!(artboards[0].location, IVec2::new(0, 0));
|
||||
let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 10.);
|
||||
assert_eq!(artboards[0].dimensions, desired_size.round().as_ivec2());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_draw_center_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
// Viewport coordinates
|
||||
editor.drag_tool(ToolType::Artboard, 0., 0., 0., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await;
|
||||
|
||||
let artboards = get_artboards(&mut editor).await;
|
||||
assert_eq!(artboards.len(), 1);
|
||||
assert_eq!(artboards[0].location, DVec2::splat(f64::consts::FRAC_1_SQRT_2 * -10.).as_ivec2());
|
||||
let desired_size = DVec2::splat(f64::consts::FRAC_1_SQRT_2 * 20.);
|
||||
assert_eq!(artboards[0].dimensions, desired_size.round().as_ivec2());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_delete() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Artboard, 10.1, 10.8, 19.9, 0.2, ModifierKeys::default()).await;
|
||||
editor.press(Key::Delete, ModifierKeys::default()).await;
|
||||
|
||||
let artboards = get_artboards(&mut editor).await;
|
||||
assert_eq!(artboards.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn artboard_cancel() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
|
||||
editor.new_document().await;
|
||||
|
||||
editor.drag_tool_cancel_rmb(ToolType::Artboard).await;
|
||||
let artboards = get_artboards(&mut editor).await;
|
||||
assert_eq!(artboards.len(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,3 +315,134 @@ impl Fsm for EllipseToolFsmState {
|
|||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ellipse {
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graphene_core::vector::generator_nodes::ellipse;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ResolvedEllipse {
|
||||
radius_x: f64,
|
||||
radius_y: f64,
|
||||
transform: DAffine2,
|
||||
}
|
||||
|
||||
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
|
||||
let instrumented = editor.eval_graph().await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let layers = document.metadata().all_layers();
|
||||
layers
|
||||
.filter_map(|layer| {
|
||||
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?;
|
||||
Some(ResolvedEllipse {
|
||||
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_simple() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
|
||||
|
||||
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 4.5,
|
||||
radius_y: 5.,
|
||||
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_circle() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 10.,
|
||||
radius_y: 10.,
|
||||
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
println!("{ellipse:?}");
|
||||
// TODO: re-enable after https://github.com/GraphiteEditor/Graphite/issues/2370
|
||||
// assert_eq!(ellipse[0].radius_x, 5.);
|
||||
// assert_eq!(ellipse[0].radius_y, 5.);
|
||||
|
||||
// assert!(ellipse[0]
|
||||
// .transform
|
||||
// .abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001));
|
||||
|
||||
float_eq!(ellipse[0].radius_x, 11. / core::f64::consts::SQRT_2 / 2.);
|
||||
float_eq!(ellipse[0].radius_y, 11. / core::f64::consts::SQRT_2 / 2.);
|
||||
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_translation(DVec2::splat(11. / core::f64::consts::SQRT_2 / 2.)), 0.001));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_center_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
// TODO: re-enable after https://github.com/GraphiteEditor/Graphite/issues/2370
|
||||
// assert_eq!(ellipse[0].radius_x, 10.);
|
||||
// assert_eq!(ellipse[0].radius_y, 10.);
|
||||
// assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001));
|
||||
float_eq!(ellipse[0].radius_x, 11. / core::f64::consts::SQRT_2);
|
||||
float_eq!(ellipse[0].radius_y, 11. / core::f64::consts::SQRT_2);
|
||||
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::IDENTITY, 0.001));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_cancel() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool_cancel_rmb(ToolType::Ellipse).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,3 +124,61 @@ impl Fsm for FillToolFsmState {
|
|||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_fill {
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
use graphene_core::vector::fill;
|
||||
use graphene_std::vector::style::Fill;
|
||||
|
||||
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Fill> {
|
||||
let instrumented = editor.eval_graph().await;
|
||||
|
||||
instrumented.grab_all_input::<fill::FillInput<Fill>>(&editor.runtime).collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_artboard() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Artboard, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||
assert!(get_fills(&mut editor,).await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// TODO: fix https://github.com/GraphiteEditor/Graphite/issues/2270
|
||||
#[should_panic]
|
||||
async fn ignore_raster() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await;
|
||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||
assert!(get_fills(&mut editor,).await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn primary() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
editor.select_primary_color(Color::GREEN).await;
|
||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||
let fills = get_fills(&mut editor).await;
|
||||
assert_eq!(fills.len(), 1);
|
||||
assert_eq!(fills[0], Fill::Solid(Color::GREEN));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn secondary() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
let color = Color::YELLOW;
|
||||
editor.handle_message(ToolMessage::SelectSecondaryColor { color }).await;
|
||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
|
||||
let fills = get_fills(&mut editor).await;
|
||||
assert_eq!(fills.len(), 1);
|
||||
assert_eq!(fills[0], Fill::Solid(color));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::messages::prelude::*;
|
|||
|
||||
use graph_craft::concrete;
|
||||
use graph_craft::document::value::{RenderOutput, TaggedValue};
|
||||
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork};
|
||||
use graph_craft::graphene_compiler::Compiler;
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
|
|
@ -346,7 +346,7 @@ impl NodeRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any>, IntrospectError> {
|
||||
pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
||||
let runtime = NODE_RUNTIME.lock();
|
||||
if let Some(ref mut runtime) = runtime.as_ref() {
|
||||
return runtime.executor.introspect(path);
|
||||
|
|
@ -396,6 +396,22 @@ impl Default for NodeGraphExecutor {
|
|||
}
|
||||
|
||||
impl NodeGraphExecutor {
|
||||
/// A local runtime is useful on threads since having global state causes flakes
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_with_local_runtime() -> (NodeRuntime, Self) {
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
let node_runtime = NodeRuntime::new(request_receiver, response_sender);
|
||||
|
||||
let node_executor = Self {
|
||||
futures: Default::default(),
|
||||
sender: request_sender,
|
||||
receiver: response_receiver,
|
||||
node_graph_hash: 0,
|
||||
};
|
||||
(node_runtime, node_executor)
|
||||
}
|
||||
|
||||
/// Execute the network by flattening it and creating a borrow stack.
|
||||
fn queue_execution(&self, render_config: RenderConfig) -> u64 {
|
||||
let execution_id = generate_uuid();
|
||||
|
|
@ -405,7 +421,7 @@ impl NodeGraphExecutor {
|
|||
execution_id
|
||||
}
|
||||
|
||||
pub async fn introspect_node(&self, path: &[NodeId]) -> Result<Arc<dyn std::any::Any>, IntrospectError> {
|
||||
pub async fn introspect_node(&self, path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
||||
introspect_node(path).await
|
||||
}
|
||||
|
||||
|
|
@ -439,9 +455,20 @@ impl NodeGraphExecutor {
|
|||
Some(extract_data(downcasted))
|
||||
}
|
||||
|
||||
/// Evaluates a node graph, computing the entire graph
|
||||
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, ignore_hash: bool) -> Result<(), String> {
|
||||
// Get the node graph layer
|
||||
/// Updates the network to monitor all inputs. Useful for the testing.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn update_node_graph_instrumented(&mut self, document: &mut DocumentMessageHandler) -> Result<Instrumented, String> {
|
||||
// We should always invalidate the cache.
|
||||
self.node_graph_hash = generate_uuid();
|
||||
let mut network = document.network_interface.network(&[]).unwrap().clone();
|
||||
let instrumented = Instrumented::new(&mut network);
|
||||
|
||||
self.sender.send(NodeRuntimeMessage::GraphUpdate(network)).map_err(|e| e.to_string())?;
|
||||
Ok(instrumented)
|
||||
}
|
||||
|
||||
/// Update the cached network if necessary.
|
||||
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, ignore_hash: bool) -> Result<(), String> {
|
||||
let network_hash = document.network_interface.network(&[]).unwrap().current_hash();
|
||||
if network_hash != self.node_graph_hash || ignore_hash {
|
||||
self.node_graph_hash = network_hash;
|
||||
|
|
@ -449,7 +476,11 @@ impl NodeGraphExecutor {
|
|||
.send(NodeRuntimeMessage::GraphUpdate(document.network_interface.network(&[]).unwrap().clone()))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds an evaluate request for whatever current network is cached.
|
||||
pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2) -> Result<(), String> {
|
||||
let render_config = RenderConfig {
|
||||
viewport: Footprint {
|
||||
transform: document.metadata().document_to_viewport,
|
||||
|
|
@ -469,6 +500,13 @@ impl NodeGraphExecutor {
|
|||
let execution_id = self.queue_execution(render_config);
|
||||
|
||||
self.futures.insert(execution_id, ExecutionContext { export_config: None });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Evaluates a node graph, computing the entire graph
|
||||
pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, ignore_hash: bool) -> Result<(), String> {
|
||||
self.update_node_graph(document, ignore_hash)?;
|
||||
self.submit_current_node_graph_evaluation(document, viewport_resolution)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -675,3 +713,99 @@ impl NodeGraphExecutor {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores all of the monitor nodes that have been attached to a graph
|
||||
#[derive(Default)]
|
||||
pub struct Instrumented {
|
||||
protonodes_by_name: HashMap<String, Vec<Vec<Vec<NodeId>>>>,
|
||||
protonodes_by_path: HashMap<Vec<NodeId>, Vec<Vec<NodeId>>>,
|
||||
}
|
||||
|
||||
impl Instrumented {
|
||||
/// Adds montior nodes to the network
|
||||
fn add(&mut self, network: &mut NodeNetwork, path: &mut Vec<NodeId>) {
|
||||
// Required to do seperately to satiate the borrow checker.
|
||||
let mut monitor_nodes = Vec::new();
|
||||
for (id, node) in network.nodes.iter_mut() {
|
||||
// Recursively instrument
|
||||
if let DocumentNodeImplementation::Network(nested) = &mut node.implementation {
|
||||
path.push(*id);
|
||||
self.add(nested, path);
|
||||
path.pop();
|
||||
}
|
||||
let mut monitor_node_ids = Vec::with_capacity(node.inputs.len());
|
||||
for input in &mut node.inputs {
|
||||
let node_id = NodeId::new();
|
||||
let old_input = std::mem::replace(input, NodeInput::node(node_id, 0));
|
||||
monitor_nodes.push((old_input, node_id));
|
||||
path.push(node_id);
|
||||
monitor_node_ids.push(path.clone());
|
||||
path.pop();
|
||||
}
|
||||
if let DocumentNodeImplementation::ProtoNode(identifier) = &mut node.implementation {
|
||||
path.push(*id);
|
||||
self.protonodes_by_name.entry(identifier.name.to_string()).or_default().push(monitor_node_ids.clone());
|
||||
self.protonodes_by_path.insert(path.clone(), monitor_node_ids);
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
for (input, monitor_id) in monitor_nodes {
|
||||
let monitor_node = DocumentNode {
|
||||
inputs: vec![input],
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
|
||||
manual_composition: Some(graph_craft::generic!(T)),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
};
|
||||
network.nodes.insert(monitor_id, monitor_node);
|
||||
}
|
||||
}
|
||||
|
||||
/// Instrument a graph and return a new [Instrumented] state.
|
||||
pub fn new(network: &mut NodeNetwork) -> Self {
|
||||
let mut instrumented = Self::default();
|
||||
instrumented.add(network, &mut Vec::new());
|
||||
instrumented
|
||||
}
|
||||
|
||||
fn downcast<Input: graphene_std::NodeInputDecleration>(dynamic: Arc<dyn std::any::Any + Send + Sync>) -> Option<Input::Result>
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
{
|
||||
// This is quite inflexible since it only allows the footprint as inputs.
|
||||
if let Some(x) = dynamic.downcast_ref::<IORecord<(), Input::Result>>() {
|
||||
Some(x.output.clone())
|
||||
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Footprint, Input::Result>>() {
|
||||
Some(x.output.clone())
|
||||
} else if let Some(x) = dynamic.downcast_ref::<IORecord<Context, Input::Result>>() {
|
||||
Some(x.output.clone())
|
||||
} else {
|
||||
panic!("cannot downcast type for introspection");
|
||||
}
|
||||
}
|
||||
|
||||
/// Grab all of the values of the input every time it occurs in the graph.
|
||||
pub fn grab_all_input<'a, Input: graphene_std::NodeInputDecleration + 'a>(&'a self, runtime: &'a NodeRuntime) -> impl Iterator<Item = Input::Result> + 'a
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
{
|
||||
self.protonodes_by_name
|
||||
.get(Input::identifier())
|
||||
.map_or([].as_slice(), |x| x.as_slice())
|
||||
.iter()
|
||||
.filter_map(|inputs| inputs.get(Input::INDEX))
|
||||
.filter_map(|input_monitor_node| runtime.executor.introspect(input_monitor_node).ok())
|
||||
.filter_map(Instrumented::downcast::<Input>)
|
||||
}
|
||||
|
||||
pub fn grab_protonode_input<Input: graphene_std::NodeInputDecleration>(&self, path: &Vec<NodeId>, runtime: &NodeRuntime) -> Option<Input::Result>
|
||||
where
|
||||
Input::Result: Send + Sync + Clone + 'static,
|
||||
{
|
||||
let input_monitor_node = self.protonodes_by_path.get(path)?.get(Input::INDEX)?;
|
||||
|
||||
let dynamic = runtime.executor.introspect(input_monitor_node).ok()?;
|
||||
|
||||
Self::downcast::<Input>(dynamic)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,36 +4,29 @@ use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
|||
use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, MouseKeys, ScrollDelta, ViewportPosition};
|
||||
use crate::messages::portfolio::utility_types::Platform;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::Key;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::node_graph_executor::Instrumented;
|
||||
use crate::node_graph_executor::NodeRuntime;
|
||||
|
||||
use graph_craft::document::DocumentNode;
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::InputAccessor;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
/// A set of utility functions to make the writing of editor test more declarative
|
||||
pub trait EditorTestUtils {
|
||||
fn create() -> Editor;
|
||||
|
||||
fn new_document(&mut self);
|
||||
|
||||
fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
|
||||
/// Select given tool and drag it from (x1, y1) to (x2, y2)
|
||||
fn drag_tool(&mut self, typ: ToolType, x1: f64, y1: f64, x2: f64, y2: f64);
|
||||
fn move_mouse(&mut self, x: f64, y: f64);
|
||||
fn mousedown(&mut self, state: EditorMouseState);
|
||||
fn mouseup(&mut self, state: EditorMouseState);
|
||||
fn left_mousedown(&mut self, x: f64, y: f64);
|
||||
fn input(&mut self, message: InputPreprocessorMessage);
|
||||
fn select_tool(&mut self, typ: ToolType);
|
||||
fn select_primary_color(&mut self, color: Color);
|
||||
pub struct EditorTestUtils {
|
||||
pub editor: Editor,
|
||||
pub runtime: NodeRuntime,
|
||||
}
|
||||
|
||||
impl EditorTestUtils for Editor {
|
||||
fn create() -> Editor {
|
||||
impl EditorTestUtils {
|
||||
pub fn create() -> Self {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
set_uuid_seed(0);
|
||||
|
||||
let mut editor = Editor::new();
|
||||
let (mut editor, runtime) = Editor::new_local_executor();
|
||||
|
||||
// We have to set this directly instead of using `GlobalsMessage::SetPlatform` because race conditions with multiple tests can cause that message handler to set it more than once, which is a failure.
|
||||
// It isn't sufficient to guard the message dispatch here with a check if the once_cell is empty, because that isn't atomic and the time between checking and handling the dispatch can let multiple through.
|
||||
|
|
@ -41,73 +34,250 @@ impl EditorTestUtils for Editor {
|
|||
|
||||
editor.handle_message(Message::Init);
|
||||
|
||||
editor
|
||||
Self { editor, runtime }
|
||||
}
|
||||
|
||||
fn new_document(&mut self) {
|
||||
self.handle_message(Message::Portfolio(PortfolioMessage::NewDocumentWithName { name: String::from("Test document") }));
|
||||
pub fn eval_graph<'a>(&'a mut self) -> impl std::future::Future<Output = Instrumented> + 'a {
|
||||
// An inner function is required since async functions in traits are a bit weird
|
||||
async fn run<'a>(editor: &'a mut Editor, runtime: &'a mut NodeRuntime) -> Instrumented {
|
||||
let portfolio = &mut editor.dispatcher.message_handlers.portfolio_message_handler;
|
||||
let exector = &mut portfolio.executor;
|
||||
let document = portfolio.documents.get_mut(&portfolio.active_document_id.unwrap()).unwrap();
|
||||
|
||||
let instrumented = exector.update_node_graph_instrumented(document).expect("update_node_graph_instrumented failed");
|
||||
|
||||
let viewport_resolution = glam::UVec2::ONE;
|
||||
exector
|
||||
.submit_current_node_graph_evaluation(document, viewport_resolution)
|
||||
.expect("submit_current_node_graph_evaluation failed");
|
||||
runtime.run().await;
|
||||
|
||||
let mut messages = VecDeque::new();
|
||||
editor.poll_node_graph_evaluation(&mut messages).expect("Graph should render");
|
||||
let frontend_messages = messages.into_iter().flat_map(|message| editor.handle_message(message));
|
||||
|
||||
for message in frontend_messages {
|
||||
message.check_node_graph_error();
|
||||
}
|
||||
|
||||
fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2);
|
||||
instrumented
|
||||
}
|
||||
|
||||
fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Polygon, x1, y1, x2, y2);
|
||||
run(&mut self.editor, &mut self.runtime)
|
||||
}
|
||||
|
||||
fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Ellipse, x1, y1, x2, y2);
|
||||
pub async fn handle_message(&mut self, message: impl Into<Message>) {
|
||||
self.editor.handle_message(message);
|
||||
|
||||
// Required to process any buffered messages
|
||||
self.eval_graph().await;
|
||||
}
|
||||
|
||||
fn drag_tool(&mut self, typ: ToolType, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.select_tool(typ);
|
||||
self.move_mouse(x1, y1);
|
||||
self.left_mousedown(x1, y1);
|
||||
self.move_mouse(x2, y2);
|
||||
self.mouseup(EditorMouseState {
|
||||
pub async fn new_document(&mut self) {
|
||||
self.handle_message(Message::Portfolio(PortfolioMessage::NewDocumentWithName { name: String::from("Test document") }))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn draw_rect(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2, ModifierKeys::default()).await;
|
||||
}
|
||||
|
||||
pub async fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Polygon, x1, y1, x2, y2, ModifierKeys::default()).await;
|
||||
}
|
||||
|
||||
pub async fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Ellipse, x1, y1, x2, y2, ModifierKeys::default()).await;
|
||||
}
|
||||
|
||||
pub async fn click_tool(&mut self, typ: ToolType, button: MouseKeys, position: DVec2, modifier_keys: ModifierKeys) {
|
||||
self.select_tool(typ).await;
|
||||
|
||||
self.move_mouse(position.x, position.y, modifier_keys, MouseKeys::empty()).await;
|
||||
|
||||
self.mousedown(
|
||||
EditorMouseState {
|
||||
editor_position: position,
|
||||
mouse_keys: button,
|
||||
..Default::default()
|
||||
},
|
||||
modifier_keys,
|
||||
)
|
||||
.await;
|
||||
|
||||
self.mouseup(
|
||||
EditorMouseState {
|
||||
editor_position: position,
|
||||
..Default::default()
|
||||
},
|
||||
modifier_keys,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn drag_tool(&mut self, typ: ToolType, x1: f64, y1: f64, x2: f64, y2: f64, modifier_keys: ModifierKeys) {
|
||||
self.select_tool(typ).await;
|
||||
|
||||
self.move_mouse(x1, y1, modifier_keys, MouseKeys::empty()).await;
|
||||
|
||||
self.left_mousedown(x1, y1, modifier_keys).await;
|
||||
|
||||
self.move_mouse(x2, y2, modifier_keys, MouseKeys::LEFT).await;
|
||||
|
||||
self.mouseup(
|
||||
EditorMouseState {
|
||||
editor_position: (x2, y2).into(),
|
||||
mouse_keys: MouseKeys::empty(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
});
|
||||
},
|
||||
modifier_keys,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn move_mouse(&mut self, x: f64, y: f64) {
|
||||
pub async fn drag_tool_cancel_rmb(&mut self, typ: ToolType) {
|
||||
self.select_tool(typ).await;
|
||||
|
||||
self.move_mouse(50., 50., ModifierKeys::default(), MouseKeys::empty()).await;
|
||||
|
||||
self.left_mousedown(50., 50., ModifierKeys::default()).await;
|
||||
|
||||
self.move_mouse(100., 100., ModifierKeys::default(), MouseKeys::LEFT).await;
|
||||
|
||||
self.mousedown(
|
||||
EditorMouseState {
|
||||
editor_position: (100., 100.).into(),
|
||||
mouse_keys: MouseKeys::LEFT | MouseKeys::RIGHT,
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
},
|
||||
ModifierKeys::default(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub fn active_document(&self) -> &DocumentMessageHandler {
|
||||
self.editor.dispatcher.message_handlers.portfolio_message_handler.active_document().unwrap()
|
||||
}
|
||||
|
||||
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
|
||||
self.editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_node<'a, T: InputAccessor<'a, DocumentNode>>(&'a self) -> impl Iterator<Item = T> + 'a {
|
||||
self.active_document()
|
||||
.network_interface
|
||||
.iter_recursive()
|
||||
.inspect(|node| println!("{:#?}", node.1.implementation))
|
||||
.filter_map(move |(_, document)| T::new_with_source(document))
|
||||
}
|
||||
|
||||
pub async fn move_mouse(&mut self, x: f64, y: f64, modifier_keys: ModifierKeys, mouse_keys: MouseKeys) {
|
||||
let editor_mouse_state = EditorMouseState {
|
||||
editor_position: ViewportPosition::new(x, y),
|
||||
mouse_keys,
|
||||
..Default::default()
|
||||
};
|
||||
let modifier_keys = ModifierKeys::default();
|
||||
self.input(InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys });
|
||||
self.input(InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys }).await;
|
||||
}
|
||||
|
||||
fn mousedown(&mut self, editor_mouse_state: EditorMouseState) {
|
||||
let modifier_keys = ModifierKeys::default();
|
||||
self.input(InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys });
|
||||
pub async fn mousedown(&mut self, editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys) {
|
||||
self.input(InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys }).await;
|
||||
}
|
||||
|
||||
fn mouseup(&mut self, editor_mouse_state: EditorMouseState) {
|
||||
let modifier_keys = ModifierKeys::default();
|
||||
self.handle_message(InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys });
|
||||
pub async fn mouseup(&mut self, editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys) {
|
||||
self.handle_message(InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys }).await;
|
||||
}
|
||||
|
||||
fn left_mousedown(&mut self, x: f64, y: f64) {
|
||||
self.mousedown(EditorMouseState {
|
||||
pub async fn press(&mut self, key: Key, modifier_keys: ModifierKeys) {
|
||||
let key_repeat = false;
|
||||
|
||||
self.handle_message(InputPreprocessorMessage::KeyDown { key, modifier_keys, key_repeat }).await;
|
||||
self.handle_message(InputPreprocessorMessage::KeyUp { key, modifier_keys, key_repeat }).await;
|
||||
}
|
||||
|
||||
pub async fn left_mousedown(&mut self, x: f64, y: f64, modifier_keys: ModifierKeys) {
|
||||
self.mousedown(
|
||||
EditorMouseState {
|
||||
editor_position: (x, y).into(),
|
||||
mouse_keys: MouseKeys::LEFT,
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
});
|
||||
},
|
||||
modifier_keys,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn input(&mut self, message: InputPreprocessorMessage) {
|
||||
self.handle_message(Message::InputPreprocessor(message));
|
||||
pub async fn input(&mut self, message: InputPreprocessorMessage) {
|
||||
self.handle_message(Message::InputPreprocessor(message)).await;
|
||||
}
|
||||
|
||||
fn select_tool(&mut self, tool_type: ToolType) {
|
||||
self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type }));
|
||||
pub async fn select_tool(&mut self, tool_type: ToolType) {
|
||||
self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await;
|
||||
}
|
||||
|
||||
fn select_primary_color(&mut self, color: Color) {
|
||||
self.handle_message(Message::Tool(ToolMessage::SelectPrimaryColor { color }));
|
||||
pub async fn select_primary_color(&mut self, color: Color) {
|
||||
self.handle_message(Message::Tool(ToolMessage::SelectPrimaryColor { color })).await;
|
||||
}
|
||||
|
||||
pub async fn create_raster_image(&mut self, image: graphene_core::raster::Image<Color>, mouse: Option<(f64, f64)>) {
|
||||
self.handle_message(PortfolioMessage::PasteImage {
|
||||
name: None,
|
||||
image,
|
||||
mouse,
|
||||
parent_and_insert_index: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FrontendMessageTestUtils {
|
||||
fn check_node_graph_error(&self);
|
||||
}
|
||||
|
||||
impl FrontendMessageTestUtils for FrontendMessage {
|
||||
fn check_node_graph_error(&self) {
|
||||
let FrontendMessage::UpdateNodeGraph { nodes, .. } = self else { return };
|
||||
|
||||
for node in nodes {
|
||||
if let Some(error) = &node.errors {
|
||||
panic!("error on {}: {}", node.display_name, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_prelude {
|
||||
pub use super::FrontendMessageTestUtils;
|
||||
pub use crate::application::Editor;
|
||||
pub use crate::float_eq;
|
||||
pub use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys};
|
||||
pub use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
|
||||
pub use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
pub use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
pub use crate::messages::prelude::*;
|
||||
pub use crate::messages::tool::common_functionality::graph_modification_utils::{is_layer_fed_by_node_of_name, NodeGraphLayer};
|
||||
pub use crate::messages::tool::utility_types::ToolType;
|
||||
pub use crate::node_graph_executor::NodeRuntime;
|
||||
pub use crate::test_utils::EditorTestUtils;
|
||||
pub use core::f64;
|
||||
pub use glam::DVec2;
|
||||
pub use glam::IVec2;
|
||||
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};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! float_eq {
|
||||
($left:expr, $right:expr $(,)?) => {
|
||||
match (&$left, &$right) {
|
||||
(left_val, right_val) => {
|
||||
if (*left_val - *right_val).abs() > 1e-10 {
|
||||
panic!("assertion `left == right` failed\n left: {}\n right: {}", *left_val, *right_val)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ pub trait Node<'i, Input> {
|
|||
}
|
||||
/// 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>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
log::warn!("Node::serialize not implemented for {}", core::any::type_name::<Self>());
|
||||
None
|
||||
}
|
||||
|
|
@ -170,3 +170,25 @@ pub use crate::application_io::{SurfaceFrame, SurfaceId};
|
|||
pub type WasmSurfaceHandle = application_io::SurfaceHandle<web_sys::HtmlCanvasElement>;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandleFrame = application_io::SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
|
||||
|
||||
pub trait InputAccessorSource<'a, T>: InputAccessorSourceIdentifier + core::fmt::Debug {
|
||||
fn get_input(&'a self, index: usize) -> Option<&'a T>;
|
||||
fn set_input(&'a mut self, index: usize, value: T);
|
||||
}
|
||||
|
||||
pub trait InputAccessorSourceIdentifier {
|
||||
fn has_identifier(&self, identifier: &str) -> bool;
|
||||
}
|
||||
|
||||
pub trait InputAccessor<'n, Source: 'n>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn new_with_source(source: &'n Source) -> Option<Self>;
|
||||
}
|
||||
|
||||
pub trait NodeInputDecleration {
|
||||
const INDEX: usize;
|
||||
fn identifier() -> &'static str;
|
||||
type Result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,9 +134,9 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<Arc<dyn core::any::Any>> {
|
||||
fn serialize(&self) -> Option<Arc<dyn core::any::Any + Send + Sync>> {
|
||||
let io = self.io.lock().unwrap();
|
||||
(io).as_ref().map(|output| output.clone() as Arc<dyn core::any::Any>)
|
||||
(io).as_ref().map(|output| output.clone() as Arc<dyn core::any::Any + Send + Sync>)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -506,7 +506,7 @@ where
|
|||
self.0.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
self.0.serialize()
|
||||
}
|
||||
}
|
||||
|
|
@ -581,4 +581,29 @@ mod test {
|
|||
let fnn = FnNode::new(|(a, b)| (b, a));
|
||||
assert_eq!(fnn.eval((1u32, 2u32)), (2, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn add_vectors() {
|
||||
assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn subtract_f64() {
|
||||
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn divide_vectors() {
|
||||
assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn modulo_positive() {
|
||||
assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn modulo_negative() {
|
||||
assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1461,7 +1461,7 @@ const WINDOW_SIZE: usize = 1024;
|
|||
|
||||
#[cfg(feature = "alloc")]
|
||||
#[node_macro::node(category(""))]
|
||||
fn generate_curves<C: Channel + super::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
|
||||
fn generate_curves<C: Channel + crate::raster::Linear>(_: impl Ctx, curve: Curve, #[implementations(f32, f64)] _target_format: C) -> ValueMapperNode<C> {
|
||||
use bezier_rs::{Bezier, TValue};
|
||||
|
||||
let [mut pos, mut param]: [[f32; 2]; 2] = [[0.; 2], curve.first_handle];
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ where
|
|||
self.node.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ where
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
|
|
@ -273,7 +273,7 @@ where
|
|||
self.node.reset();
|
||||
}
|
||||
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any>> {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn core::any::Any + Send + Sync>> {
|
||||
self.node.serialize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ use std::marker::PhantomData;
|
|||
use std::str::FromStr;
|
||||
pub use std::sync::Arc;
|
||||
|
||||
pub struct TaggedValueTypeError;
|
||||
|
||||
/// Macro to generate the tagged value enum.
|
||||
macro_rules! tagged_value {
|
||||
($ ($( #[$meta:meta] )* $identifier:ident ($ty:ty) ),* $(,)?) => {
|
||||
|
|
@ -111,6 +113,26 @@ macro_rules! tagged_value {
|
|||
Self::from_type(input).unwrap_or(TaggedValue::None)
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
impl From<$ty> for TaggedValue {
|
||||
fn from(value: $ty) -> Self {
|
||||
Self::$identifier(value)
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
$(
|
||||
impl<'a> TryFrom<&'a TaggedValue> for &'a $ty {
|
||||
type Error = TaggedValueTypeError;
|
||||
fn try_from(value: &'a TaggedValue) -> Result<Self, Self::Error> {
|
||||
match value{
|
||||
TaggedValue::$identifier(value) => Ok(value),
|
||||
_ => Err(TaggedValueTypeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +150,7 @@ tagged_value! {
|
|||
#[cfg_attr(feature = "serde", serde(deserialize_with = "graphene_core::migrate_artboard_group"))]
|
||||
ArtboardGroup(graphene_core::ArtboardGroupTable),
|
||||
GraphicElement(graphene_core::GraphicElement),
|
||||
Artboard(graphene_core::Artboard),
|
||||
String(String),
|
||||
U32(u32),
|
||||
U64(u64),
|
||||
|
|
|
|||
|
|
@ -923,12 +923,12 @@ mod test {
|
|||
assert_eq!(
|
||||
ids,
|
||||
vec![
|
||||
NodeId(907133870432995942),
|
||||
NodeId(13049623730817360317),
|
||||
NodeId(2177355904460308500),
|
||||
NodeId(17479234042764485524),
|
||||
NodeId(10988236038173832469),
|
||||
NodeId(11097818235165626738),
|
||||
NodeId(10795919842314709924),
|
||||
NodeId(5986931472261716476),
|
||||
NodeId(1689970140162147057),
|
||||
NodeId(17084072420335757359),
|
||||
NodeId(17163508657634907814),
|
||||
NodeId(3540151743833532788)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ impl DynamicExecutor {
|
|||
}
|
||||
|
||||
/// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path.
|
||||
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any>, IntrospectError> {
|
||||
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
||||
self.tree.introspect(node_path)
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ impl BorrowTree {
|
|||
}
|
||||
|
||||
/// Calls the `Node::serialize` for that specific node, returning for example the cached value for a monitor node. The node path must match the document node path.
|
||||
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any>, IntrospectError> {
|
||||
pub fn introspect(&self, node_path: &[NodeId]) -> Result<Arc<dyn std::any::Any + Send + Sync + 'static>, IntrospectError> {
|
||||
let (id, _) = self.source_map.get(node_path).ok_or_else(|| IntrospectError::PathNotFound(node_path.to_vec()))?;
|
||||
let (node, _path) = self.nodes.get(id).ok_or(IntrospectError::ProtoNodeNotFound(*id))?;
|
||||
node.serialize().ok_or(IntrospectError::NoData)
|
||||
|
|
|
|||
|
|
@ -103,6 +103,28 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
node_io
|
||||
},
|
||||
),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::RasterFrame]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::instances::Instances<Artboard>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => String]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => glam::IVec2]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => glam::DVec2]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => bool]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => f64]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u32]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<f64>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::BooleanOperation]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option<graphene_core::Color>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Fill]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineCap]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineJoin]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Stroke]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Gradient]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::GradientStops]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<graphene_core::uuid::NodeId>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::Color]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_core::vector::VectorModification>]),
|
||||
#[cfg(feature = "gpu")]
|
||||
(
|
||||
ProtoNodeIdentifier::new("graphene_std::executor::MapGpuSingleImageNode"),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ use convert_case::{Case, Casing};
|
|||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use proc_macro_crate::FoundCrate;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Error, Ident, Token, WhereClause, WherePredicate};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse_quote, Error, Ident, PatIdent, Token, WhereClause, WherePredicate};
|
||||
static NODE_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
|
||||
|
|
@ -250,6 +253,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
|
||||
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
|
||||
|
||||
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier);
|
||||
Ok(quote! {
|
||||
/// Underlying implementation for [#struct_name]
|
||||
#[inline]
|
||||
|
|
@ -265,6 +269,9 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
#[doc(inline)]
|
||||
pub use #mod_name::#struct_name;
|
||||
|
||||
#[doc(hidden)]
|
||||
#node_input_accessor
|
||||
|
||||
#[doc(hidden)]
|
||||
mod #mod_name {
|
||||
use super::*;
|
||||
|
|
@ -326,6 +333,93 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||
})
|
||||
}
|
||||
|
||||
/// Generates strongly typed utilites to access inputs
|
||||
fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::GenericParam], field_idents: &[&PatIdent], graphene_core: &TokenStream2, identifier: &TokenStream2) -> TokenStream2 {
|
||||
if parsed.attributes.skip_impl {
|
||||
return quote! {};
|
||||
}
|
||||
let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake));
|
||||
|
||||
let (mut modified, mut generic_collector) = FilterUsedGenerics::new(fn_generics);
|
||||
|
||||
let mut generated_input_accessor = Vec::new();
|
||||
for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() {
|
||||
let mut ty = match parsed_input {
|
||||
ParsedField::Regular { ty, .. } => ty,
|
||||
ParsedField::Node { output_type, .. } => output_type,
|
||||
}
|
||||
.clone();
|
||||
|
||||
// We only want the necessary generics.
|
||||
let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty);
|
||||
// TODO: figure out a better name that doesn't conflict with so many types
|
||||
let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal));
|
||||
let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter());
|
||||
|
||||
// Only create structs with phantom data where necessary.
|
||||
generated_input_accessor.push(if phantom_data_declerations.is_empty() {
|
||||
quote! {
|
||||
pub struct #struct_name;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
pub struct #struct_name <#(#used),*>{
|
||||
#(#phantom_data_declerations,)*
|
||||
}
|
||||
}
|
||||
});
|
||||
generated_input_accessor.push(quote! {
|
||||
impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> {
|
||||
const INDEX: usize = #input_index;
|
||||
fn identifier() -> &'static str {
|
||||
protonode_identifier()
|
||||
}
|
||||
type Result = #ty;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
quote! {
|
||||
pub mod #inputs_module_name {
|
||||
use super::*;
|
||||
|
||||
pub fn protonode_identifier() -> &'static str {
|
||||
// Storing the string in a once lock should reduce allocations (since we call this in a loop)?
|
||||
static NODE_NAME: std::sync::OnceLock<String> = std::sync::OnceLock::new();
|
||||
NODE_NAME.get_or_init(|| #identifier )
|
||||
}
|
||||
#(#generated_input_accessor)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors.
|
||||
fn generate_phantom_data<'a>(fn_generics: impl Iterator<Item = &'a crate::GenericParam>) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
|
||||
let mut phantom_data_declerations = Vec::new();
|
||||
let mut fn_generic_params = Vec::new();
|
||||
|
||||
for fn_generic_param in fn_generics {
|
||||
let field_name = format_ident!("phantom_{}", phantom_data_declerations.len());
|
||||
|
||||
match fn_generic_param {
|
||||
crate::GenericParam::Lifetime(lifetime_param) => {
|
||||
let lifetime = &lifetime_param.lifetime;
|
||||
|
||||
fn_generic_params.push(quote! {#lifetime});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>})
|
||||
}
|
||||
crate::GenericParam::Type(type_param) => {
|
||||
let generic_name = &type_param.ident;
|
||||
|
||||
fn_generic_params.push(quote! {#generic_name});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(fn_generic_params, phantom_data_declerations)
|
||||
}
|
||||
|
||||
fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], struct_name: &Ident, identifier: &TokenStream2) -> Result<TokenStream2, syn::Error> {
|
||||
if parsed.attributes.skip_impl {
|
||||
return Ok(quote!());
|
||||
|
|
@ -469,3 +563,83 @@ fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type {
|
|||
LifetimeReplacer(lifetime).visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
/// Get only the necessary generics.
|
||||
struct FilterUsedGenerics {
|
||||
all: Vec<crate::GenericParam>,
|
||||
used: Vec<bool>,
|
||||
}
|
||||
|
||||
impl VisitMut for FilterUsedGenerics {
|
||||
fn visit_lifetime_mut(&mut self, used_lifetime: &mut syn::Lifetime) {
|
||||
for (generic, used) in self.all.iter().zip(self.used.iter_mut()) {
|
||||
let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue };
|
||||
if used_lifetime == &lifetime_param.lifetime {
|
||||
*used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_path_mut(&mut self, path: &mut syn::Path) {
|
||||
for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() {
|
||||
let crate::GenericParam::Type(type_param) = generic else { continue };
|
||||
if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident {
|
||||
*used = true;
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
path.segments[0].ident = format_ident!("G{index}");
|
||||
}
|
||||
}
|
||||
for mut el in Punctuated::pairs_mut(&mut path.segments) {
|
||||
self.visit_path_segment_mut(el.value_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterUsedGenerics {
|
||||
fn new(fn_generics: &[crate::GenericParam]) -> (Vec<crate::GenericParam>, Self) {
|
||||
let mut all_possible_generics = fn_generics.to_vec();
|
||||
// The 'n lifetime may also be needed; we must add it in
|
||||
all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site()))));
|
||||
|
||||
let modified = all_possible_generics
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(index, mut generic)| {
|
||||
let crate::GenericParam::Type(type_param) = &mut generic else { return generic };
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
type_param.ident = format_ident!("G{index}");
|
||||
generic
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let generic_collector = Self {
|
||||
used: vec![false; all_possible_generics.len()],
|
||||
all: all_possible_generics,
|
||||
};
|
||||
|
||||
(modified, generic_collector)
|
||||
}
|
||||
|
||||
fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator<Item = &'a crate::GenericParam> {
|
||||
modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value)
|
||||
}
|
||||
|
||||
fn filter_unnecessary_generics(&mut self, modified: &mut Vec<syn::GenericParam>, ty: &mut Type) -> Vec<syn::GenericParam> {
|
||||
self.used.fill(false);
|
||||
|
||||
// Find out which generics are necessary to support the node input
|
||||
self.visit_type_mut(ty);
|
||||
|
||||
// Sometimes generics may reference other generics. This is a non-optimal way of dealing with that.
|
||||
for _ in 0..=self.all.len() {
|
||||
for (index, item) in modified.iter_mut().enumerate() {
|
||||
if self.used[index] {
|
||||
self.visit_generic_param_mut(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.used(&*modified).cloned().collect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue