diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 5392b50c6f..8a3b168e32 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -13,6 +13,7 @@ pub struct Dispatcher { #[derive(Debug, Default)] pub struct DispatcherMessageHandlers { + animation_message_handler: AnimationMessageHandler, broadcast_message_handler: BroadcastMessageHandler, debug_message_handler: DebugMessageHandler, dialog_message_handler: DialogMessageHandler, @@ -50,12 +51,9 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), ]; -const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ - MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)), - MessageDiscriminant::InputPreprocessor(InputPreprocessorMessageDiscriminant::FrameTimeAdvance), -]; +const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame))]; // TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best. -const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw"]; +const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; impl Dispatcher { pub fn new() -> Self { @@ -177,6 +175,9 @@ impl Dispatcher { // Finish loading persistent data from the browser database queue.add(FrontendMessage::TriggerLoadRestAutoSaveDocuments); } + Message::Animation(message) => { + self.message_handlers.animation_message_handler.process_message(message, &mut queue, ()); + } Message::Batched(messages) => { messages.iter().for_each(|message| self.handle_message(message.to_owned(), false)); } @@ -232,6 +233,7 @@ impl Dispatcher { let preferences = &self.message_handlers.preferences_message_handler; let current_tool = &self.message_handlers.tool_message_handler.tool_state.tool_data.active_tool_type; let message_logging_verbosity = self.message_handlers.debug_message_handler.message_logging_verbosity; + let timing_information = self.message_handlers.animation_message_handler.timing_information(); self.message_handlers.portfolio_message_handler.process_message( message, @@ -241,6 +243,7 @@ impl Dispatcher { preferences, current_tool, message_logging_verbosity, + timing_information, }, ); } @@ -283,6 +286,7 @@ impl Dispatcher { // TODO: Reduce the number of heap allocations let mut list = Vec::new(); list.extend(self.message_handlers.dialog_message_handler.actions()); + list.extend(self.message_handlers.animation_message_handler.actions()); list.extend(self.message_handlers.input_preprocessor_message_handler.actions()); list.extend(self.message_handlers.key_mapping_message_handler.actions()); list.extend(self.message_handlers.debug_message_handler.actions()); diff --git a/editor/src/messages/animation/animation_message.rs b/editor/src/messages/animation/animation_message.rs new file mode 100644 index 0000000000..19dabcf96a --- /dev/null +++ b/editor/src/messages/animation/animation_message.rs @@ -0,0 +1,17 @@ +use crate::messages::prelude::*; + +use super::animation_message_handler::AnimationTimeMode; + +#[impl_message(Message, Animation)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum AnimationMessage { + ToggleLivePreview, + EnableLivePreview, + DisableLivePreview, + ResetAnimation, + SetFrameIndex(f64), + SetTime(f64), + UpdateTime, + IncrementFrameCounter, + SetAnimationTimeMode(AnimationTimeMode), +} diff --git a/editor/src/messages/animation/animation_message_handler.rs b/editor/src/messages/animation/animation_message_handler.rs new file mode 100644 index 0000000000..ff1e286dd7 --- /dev/null +++ b/editor/src/messages/animation/animation_message_handler.rs @@ -0,0 +1,84 @@ +use std::time::Duration; + +use crate::messages::prelude::*; + +use super::TimingInformation; + +#[derive(PartialEq, Clone, Default, Debug, serde::Serialize, serde::Deserialize)] +pub enum AnimationTimeMode { + #[default] + TimeBased, + FrameBased, +} + +#[derive(Debug, Default)] +pub struct AnimationMessageHandler { + live_preview: bool, + timestamp: f64, + frame_index: f64, + animation_start: Option, + fps: f64, + animation_time_mode: AnimationTimeMode, +} +impl AnimationMessageHandler { + pub(crate) fn timing_information(&self) -> TimingInformation { + let animation_time = self.timestamp - self.animation_start.unwrap_or(self.timestamp); + let animation_time = match self.animation_time_mode { + AnimationTimeMode::TimeBased => Duration::from_millis(animation_time as u64), + AnimationTimeMode::FrameBased => Duration::from_secs((self.frame_index / self.fps) as u64), + }; + TimingInformation { time: self.timestamp, animation_time } + } +} + +impl MessageHandler for AnimationMessageHandler { + fn process_message(&mut self, message: AnimationMessage, responses: &mut VecDeque, _data: ()) { + match message { + AnimationMessage::ToggleLivePreview => { + if self.animation_start.is_none() { + self.animation_start = Some(self.timestamp); + } + self.live_preview = !self.live_preview + } + AnimationMessage::EnableLivePreview => { + if self.animation_start.is_none() { + self.animation_start = Some(self.timestamp); + } + self.live_preview = true + } + AnimationMessage::DisableLivePreview => self.live_preview = false, + AnimationMessage::SetFrameIndex(frame) => { + self.frame_index = frame; + log::debug!("set frame index to {}", frame); + responses.add(PortfolioMessage::SubmitActiveGraphRender) + } + AnimationMessage::SetTime(time) => { + self.timestamp = time; + responses.add(AnimationMessage::UpdateTime); + } + AnimationMessage::IncrementFrameCounter => { + if self.live_preview { + self.frame_index += 1.; + responses.add(AnimationMessage::UpdateTime); + } + } + AnimationMessage::UpdateTime => { + if self.live_preview { + responses.add(PortfolioMessage::SubmitActiveGraphRender) + } + } + AnimationMessage::ResetAnimation => { + self.frame_index = 0.; + self.animation_start = None; + responses.add(PortfolioMessage::SubmitActiveGraphRender) + } + AnimationMessage::SetAnimationTimeMode(animation_time_mode) => self.animation_time_mode = animation_time_mode, + } + } + + advertise_actions!(AnimationMessageDiscriminant; + ToggleLivePreview, + SetFrameIndex, + ResetAnimation, + ); +} diff --git a/editor/src/messages/animation/mod.rs b/editor/src/messages/animation/mod.rs new file mode 100644 index 0000000000..57e0d72dad --- /dev/null +++ b/editor/src/messages/animation/mod.rs @@ -0,0 +1,9 @@ +mod animation_message; +mod animation_message_handler; + +#[doc(inline)] +pub use animation_message::{AnimationMessage, AnimationMessageDiscriminant}; +#[doc(inline)] +pub use animation_message_handler::AnimationMessageHandler; + +pub use graphene_core::application_io::TimingInformation; diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 9879bd459b..b66276a27a 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -430,6 +430,9 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Digit0); modifiers=[Alt], action_dispatch=DebugMessage::MessageOff), entry!(KeyDown(Digit1); modifiers=[Alt], action_dispatch=DebugMessage::MessageNames), entry!(KeyDown(Digit2); modifiers=[Alt], action_dispatch=DebugMessage::MessageContents), + // AnimationMessage + entry!(KeyDown(Space); modifiers=[Shift], action_dispatch=AnimationMessage::ToggleLivePreview), + entry!(KeyDown(ArrowLeft); modifiers=[Control], action_dispatch=AnimationMessage::ResetAnimation), ]; let (mut key_up, mut key_down, mut key_up_no_repeat, mut key_down_no_repeat, mut double_click, mut wheel_scroll, mut pointer_move) = mappings; diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message.rs index ec5d7a4bfa..53347542fc 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message.rs @@ -1,7 +1,6 @@ use crate::messages::input_mapper::utility_types::input_keyboard::{Key, ModifierKeys}; use crate::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ViewportBounds}; use crate::messages::prelude::*; -use core::time::Duration; #[impl_message(Message, InputPreprocessor)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -13,6 +12,6 @@ pub enum InputPreprocessorMessage { PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, - FrameTimeAdvance { timestamp: Duration }, + CurrentTime { timestamp: u64 }, WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, } diff --git a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs index cd36f3f657..b78022575d 100644 --- a/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs +++ b/editor/src/messages/input_preprocessor/input_preprocessor_message_handler.rs @@ -12,6 +12,7 @@ pub struct InputPreprocessorMessageData { #[derive(Debug, Default)] pub struct InputPreprocessorMessageHandler { pub frame_time: FrameTimeInfo, + pub time: u64, pub keyboard: KeyStates, pub mouse: MouseState, pub viewport_bounds: ViewportBounds, @@ -93,8 +94,9 @@ impl MessageHandler for self.translate_mouse_event(mouse_state, false, responses); } - InputPreprocessorMessage::FrameTimeAdvance { timestamp } => { - self.frame_time.advance_timestamp(timestamp); + InputPreprocessorMessage::CurrentTime { timestamp } => { + responses.add(AnimationMessage::SetTime(timestamp as f64)); + self.time = timestamp; } InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => { self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses); diff --git a/editor/src/messages/message.rs b/editor/src/messages/message.rs index 446d04bc05..80323d9309 100644 --- a/editor/src/messages/message.rs +++ b/editor/src/messages/message.rs @@ -10,6 +10,8 @@ pub enum Message { StartBuffer, EndBuffer(graphene_std::renderer::RenderMetadata), + #[child] + Animation(AnimationMessage), #[child] Broadcast(BroadcastMessage), #[child] diff --git a/editor/src/messages/mod.rs b/editor/src/messages/mod.rs index c6f3c23d24..7b43a40108 100644 --- a/editor/src/messages/mod.rs +++ b/editor/src/messages/mod.rs @@ -1,5 +1,6 @@ //! The root-level messages forming the first layer of the message system architecture. +pub mod animation; pub mod broadcast; pub mod debug; pub mod dialog; diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 9fc1432d7b..d56ac6f12b 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -2086,7 +2086,7 @@ fn static_nodes() -> Vec { }, DocumentNodeDefinition { identifier: "Text", - category: "Vector", + category: "Text", node_template: NodeTemplate { document_node: DocumentNode { implementation: DocumentNodeImplementation::proto("graphene_std::text::TextNode"), diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 908751bf76..6cb3627fc4 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -20,6 +20,7 @@ use graphene_core::raster::{ use graphene_core::text::Font; use graphene_core::vector::misc::CentroidType; use graphene_core::vector::style::{GradientType, LineCap, LineJoin}; +use graphene_std::animation::RealTimeMode; use graphene_std::application_io::TextureFrameTable; use graphene_std::transform::Footprint; use graphene_std::vector::VectorDataTable; @@ -165,6 +166,7 @@ pub(crate) fn property_from_type( last.clone() } Some(x) if x == TypeId::of::() => blend_mode(document_node, node_id, index, name, true), + Some(x) if x == TypeId::of::() => real_time_mode(document_node, node_id, index, name, true), Some(x) if x == TypeId::of::() => color_channel(document_node, node_id, index, name, true), Some(x) if x == TypeId::of::() => rgba_channel(document_node, node_id, index, name, true), Some(x) if x == TypeId::of::() => noise_type(document_node, node_id, index, name, true), @@ -778,6 +780,40 @@ pub fn color_channel(document_node: &DocumentNode, node_id: NodeId, index: usize LayoutGroup::Row { widgets }.with_tooltip("Color Channel") } +pub fn real_time_mode(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return LayoutGroup::Row { widgets: vec![] }; + }; + if let Some(&TaggedValue::RealTimeMode(mode)) = input.as_non_exposed_value() { + let calculation_modes = [ + RealTimeMode::Utc, + RealTimeMode::Year, + RealTimeMode::Hour, + RealTimeMode::Minute, + RealTimeMode::Second, + RealTimeMode::Millisecond, + ]; + let mut entries = Vec::with_capacity(calculation_modes.len()); + for method in calculation_modes { + entries.push( + MenuListEntry::new(format!("{method:?}")) + .label(method.to_string()) + .on_update(update_value(move |_| TaggedValue::RealTimeMode(method), node_id, index)) + .on_commit(commit_value), + ); + } + let entries = vec![entries]; + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(), + ]); + } + LayoutGroup::Row { widgets }.with_tooltip("Real Time Mode") +} + pub fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); let Some(input) = document_node.inputs.get(index) else { diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 5a2ca18c85..7c1fc33053 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -115,6 +115,7 @@ pub enum PortfolioMessage { bounds: ExportBounds, transparent_background: bool, }, + SubmitActiveGraphRender, SubmitGraphRender { document_id: DocumentId, ignore_hash: bool, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 9b46bb9753..a44b5ba969 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -4,6 +4,7 @@ use super::spreadsheet::SpreadsheetMessageHandler; use super::utility_types::{PanelType, PersistentData}; use crate::application::generate_uuid; use crate::consts::DEFAULT_DOCUMENT_NAME; +use crate::messages::animation::TimingInformation; use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::dialog::simple_dialogs; use crate::messages::frontend::utility_types::FrontendDocumentDetails; @@ -32,6 +33,7 @@ pub struct PortfolioMessageData<'a> { pub preferences: &'a PreferencesMessageHandler, pub current_tool: &'a ToolType, pub message_logging_verbosity: MessageLoggingVerbosity, + pub timing_information: TimingInformation, } #[derive(Debug, Default)] @@ -56,6 +58,7 @@ impl MessageHandler> for PortfolioMes preferences, current_tool, message_logging_verbosity, + timing_information, } = data; match message { @@ -306,6 +309,7 @@ impl MessageHandler> for PortfolioMes let _ = self.executor.submit_node_graph_evaluation( self.documents.get_mut(document_id).expect("Tried to render non-existent document"), ipp.viewport_bounds.size().as_uvec2(), + timing_information, inspect_node, true, ); @@ -1072,11 +1076,17 @@ impl MessageHandler> for PortfolioMes }); } } + PortfolioMessage::SubmitActiveGraphRender => { + if let Some(document_id) = self.active_document_id { + responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false }); + } + } PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => { let inspect_node = self.inspect_node_id(); let result = self.executor.submit_node_graph_evaluation( self.documents.get_mut(&document_id).expect("Tried to render non-existent document"), ipp.viewport_bounds.size().as_uvec2(), + timing_information, inspect_node, ignore_hash, ); diff --git a/editor/src/messages/prelude.rs b/editor/src/messages/prelude.rs index 0fa4ca600a..9e5a4a2c99 100644 --- a/editor/src/messages/prelude.rs +++ b/editor/src/messages/prelude.rs @@ -2,6 +2,7 @@ pub use crate::utility_traits::{ActionList, AsMessage, MessageHandler, ToDiscriminant, TransitiveChild}; // Message, MessageData, MessageDiscriminant, MessageHandler +pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler}; pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler}; pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler}; pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageData, ExportDialogMessageDiscriminant, ExportDialogMessageHandler}; diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index f391fe34ab..9e0b2700b4 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -57,7 +57,9 @@ where /// Calculates the bounding box of the layer's text, based on the settings for max width and height specified in the typesetting config. pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, font_cache: &FontCache) -> Quad { - let (text, font, typesetting) = get_text(layer, &document.network_interface).expect("Text layer should have text when interacting with the Text tool"); + let Some((text, font, typesetting)) = get_text(layer, &document.network_interface) else { + return Quad::from_box([DVec2::ZERO, DVec2::ZERO]); + }; let buzz_face = font_cache.get(font).map(|data| load_face(data)); let far = graphene_core::text::bounding_box(text, buzz_face.as_ref(), typesetting, false); diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index f97ed3c87f..7785a28534 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,4 +1,5 @@ use crate::consts::FILE_SAVE_SUFFIX; +use crate::messages::animation::TimingInformation; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::prelude::*; use glam::{DAffine2, DVec2, UVec2}; @@ -572,13 +573,14 @@ impl NodeGraphExecutor { } /// 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> { + pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, time: TimingInformation) -> Result<(), String> { let render_config = RenderConfig { viewport: Footprint { transform: document.metadata().document_to_viewport, resolution: viewport_resolution, ..Default::default() }, + time, #[cfg(any(feature = "resvg", feature = "vello"))] export_format: graphene_core::application_io::ExportFormat::Canvas, #[cfg(not(any(feature = "resvg", feature = "vello")))] @@ -596,9 +598,16 @@ impl NodeGraphExecutor { } /// Evaluates a node graph, computing the entire graph - pub fn submit_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, inspect_node: Option, ignore_hash: bool) -> Result<(), String> { + pub fn submit_node_graph_evaluation( + &mut self, + document: &mut DocumentMessageHandler, + viewport_resolution: UVec2, + time: TimingInformation, + inspect_node: Option, + ignore_hash: bool, + ) -> Result<(), String> { self.update_node_graph(document, inspect_node, ignore_hash)?; - self.submit_current_node_graph_evaluation(document, viewport_resolution)?; + self.submit_current_node_graph_evaluation(document, viewport_resolution, time)?; Ok(()) } @@ -623,6 +632,7 @@ impl NodeGraphExecutor { resolution: (size * export_config.scale_factor).as_uvec2(), ..Default::default() }, + time: Default::default(), export_format: graphene_core::application_io::ExportFormat::Svg, view_mode: document.view_mode, hide_artboards: export_config.transparent_background, diff --git a/editor/src/test_utils.rs b/editor/src/test_utils.rs index be17b4d235..9827bf7d73 100644 --- a/editor/src/test_utils.rs +++ b/editor/src/test_utils.rs @@ -46,7 +46,7 @@ impl EditorTestUtils { let viewport_resolution = glam::UVec2::ONE; exector - .submit_current_node_graph_evaluation(document, viewport_resolution) + .submit_current_node_graph_evaluation(document, viewport_resolution, Default::default()) .expect("submit_current_node_graph_evaluation failed"); runtime.run().await; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index e23822fc5a..64e863eb1f 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -138,19 +138,18 @@ impl EditorHandle { let f = std::rc::Rc::new(RefCell::new(None)); let g = f.clone(); - *g.borrow_mut() = Some(Closure::new(move |timestamp| { + *g.borrow_mut() = Some(Closure::new(move |_timestamp| { wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { editor_and_handle(|editor, handle| { - let micros: f64 = timestamp * 1000.; - let timestamp = Duration::from_micros(micros.round() as u64); - - for message in editor.handle_message(InputPreprocessorMessage::FrameTimeAdvance { timestamp }) { + for message in editor.handle_message(InputPreprocessorMessage::CurrentTime { + timestamp: js_sys::Date::now() as u64, + }) { handle.send_frontend_message_to_js(message); } - for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) { + for message in editor.handle_message(AnimationMessage::IncrementFrameCounter) { handle.send_frontend_message_to_js(message); } }); @@ -826,7 +825,13 @@ impl EditorHandle { 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, None, true) + .submit_node_graph_evaluation( + portfolio.documents.get_mut(&portfolio.active_document_id().unwrap()).unwrap(), + glam::UVec2::ONE, + Default::default(), + None, + true, + ) .unwrap(); editor::node_graph_executor::run_node_graph().await; diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 2c217d705c..e67fc256d0 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -295,6 +295,7 @@ impl Subpath { /// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex. pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self { + let sides = sides.max(3); let angle_increment = std::f64::consts::TAU / (sides as f64); let anchor_positions = (0..sides).map(|i| { let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2; @@ -306,6 +307,7 @@ impl Subpath { /// Constructs a star polygon (n-star). See [new_regular_polygon], but with interspersed vertices at an `inner_radius`. pub fn new_star_polygon(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> Self { + let sides = sides.max(2); let angle_increment = 0.5 * std::f64::consts::TAU / (sides as f64); let anchor_positions = (0..sides * 2).map(|i| { let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2; diff --git a/node-graph/gcore/src/animation.rs b/node-graph/gcore/src/animation.rs new file mode 100644 index 0000000000..adb63c6c0f --- /dev/null +++ b/node-graph/gcore/src/animation.rs @@ -0,0 +1,64 @@ +use crate::{Ctx, ExtractAnimationTime, ExtractTime}; + +const DAY: f64 = 1000. * 3600. * 24.; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, dyn_any::DynAny, Default, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum RealTimeMode { + Utc, + Year, + Hour, + Minute, + #[default] + Second, + Millisecond, +} +impl core::fmt::Display for RealTimeMode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + RealTimeMode::Utc => write!(f, "UTC"), + RealTimeMode::Year => write!(f, "Year"), + RealTimeMode::Hour => write!(f, "Hour"), + RealTimeMode::Minute => write!(f, "Minute"), + RealTimeMode::Second => write!(f, "Second"), + RealTimeMode::Millisecond => write!(f, "Millisecond"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnimationTimeMode { + AnimationTime, + FrameNumber, +} + +#[node_macro::node(category("Animation"))] +fn real_time(ctx: impl Ctx + ExtractTime, _primary: (), mode: RealTimeMode) -> f64 { + let time = ctx.try_time().unwrap_or_default(); + // TODO: Implement proper conversion using and existing time implementation + match mode { + RealTimeMode::Utc => time, + RealTimeMode::Year => (time / DAY / 365.25).floor() + 1970., + RealTimeMode::Hour => (time / 1000. / 3600.).floor() % 24., + RealTimeMode::Minute => (time / 1000. / 60.).floor() % 60., + + RealTimeMode::Second => (time / 1000.).floor() % 60., + RealTimeMode::Millisecond => time % 1000., + } +} + +#[node_macro::node(category("Animation"))] +fn animation_time(ctx: impl Ctx + ExtractAnimationTime) -> f64 { + ctx.try_animation_time().unwrap_or_default() +} + +// These nodes require more sophistcated algorithms for giving the correct result + +// #[node_macro::node(category("Animation"))] +// fn month(ctx: impl Ctx + ExtractTime) -> f64 { +// ((ctx.try_time().unwrap_or_default() / DAY / 365.25 % 1.) * 12.).floor() +// } +// #[node_macro::node(category("Animation"))] +// fn day(ctx: impl Ctx + ExtractTime) -> f64 { +// (ctx.try_time().unwrap_or_default() / DAY +// } diff --git a/node-graph/gcore/src/application_io.rs b/node-graph/gcore/src/application_io.rs index db07fc4492..202597eb1e 100644 --- a/node-graph/gcore/src/application_io.rs +++ b/node-graph/gcore/src/application_io.rs @@ -8,6 +8,7 @@ use core::future::Future; use core::hash::{Hash, Hasher}; use core::pin::Pin; use core::ptr::addr_of; +use core::time::Duration; use dyn_any::{DynAny, StaticType, StaticTypeSized}; use glam::{DAffine2, UVec2}; @@ -250,10 +251,17 @@ pub enum ExportFormat { Canvas, } +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub struct TimingInformation { + pub time: f64, + pub animation_time: Duration, +} + #[derive(Debug, Default, Clone, Copy, PartialEq, DynAny)] pub struct RenderConfig { pub viewport: Footprint, pub export_format: ExportFormat, + pub time: TimingInformation, pub view_mode: ViewMode, pub hide_artboards: bool, pub for_export: bool, diff --git a/node-graph/gcore/src/context.rs b/node-graph/gcore/src/context.rs index 7448cd901c..ae9fc66049 100644 --- a/node-graph/gcore/src/context.rs +++ b/node-graph/gcore/src/context.rs @@ -22,6 +22,10 @@ pub trait ExtractTime { fn try_time(&self) -> Option; } +pub trait ExtractAnimationTime { + fn try_animation_time(&self) -> Option; +} + pub trait ExtractIndex { fn try_index(&self) -> Option; } @@ -38,9 +42,9 @@ pub trait CloneVarArgs: ExtractVarArgs { fn arc_clone(&self) -> Option>; } -pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractTime + ExtractVarArgs {} +pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractTime + ExtractAnimationTime + ExtractVarArgs {} -impl ExtractAll for T {} +impl ExtractAll for T {} #[derive(Debug, Clone, PartialEq, Eq)] pub enum VarArgsResult { @@ -81,6 +85,11 @@ impl ExtractTime for Option { self.as_ref().and_then(|x| x.try_time()) } } +impl ExtractAnimationTime for Option { + fn try_animation_time(&self) -> Option { + self.as_ref().and_then(|x| x.try_animation_time()) + } +} impl ExtractIndex for Option { fn try_index(&self) -> Option { self.as_ref().and_then(|x| x.try_index()) @@ -107,6 +116,11 @@ impl ExtractTime for Arc { (**self).try_time() } } +impl ExtractAnimationTime for Arc { + fn try_animation_time(&self) -> Option { + (**self).try_animation_time() + } +} impl ExtractIndex for Arc { fn try_index(&self) -> Option { (**self).try_index() @@ -182,6 +196,11 @@ impl ExtractTime for OwnedContextImpl { self.time } } +impl ExtractAnimationTime for OwnedContextImpl { + fn try_animation_time(&self) -> Option { + self.animation_time + } +} impl ExtractIndex for OwnedContextImpl { fn try_index(&self) -> Option { self.index @@ -227,6 +246,7 @@ pub struct OwnedContextImpl { // This could be converted into a single enum to save extra bytes index: Option, time: Option, + animation_time: Option, } impl Default for OwnedContextImpl { @@ -252,6 +272,7 @@ impl OwnedContextImpl { let footprint = value.try_footprint().copied(); let index = value.try_index(); let time = value.try_time(); + let frame_time = value.try_animation_time(); let parent = value.arc_clone(); OwnedContextImpl { footprint, @@ -259,6 +280,7 @@ impl OwnedContextImpl { parent, index, time, + animation_time: frame_time, } } pub const fn empty() -> Self { @@ -268,6 +290,7 @@ impl OwnedContextImpl { parent: None, index: None, time: None, + animation_time: None, } } } @@ -280,6 +303,14 @@ impl OwnedContextImpl { self.footprint = Some(footprint); self } + pub fn with_time(mut self, time: f64) -> Self { + self.time = Some(time); + self + } + pub fn with_animation_time(mut self, animation_time: f64) -> Self { + self.animation_time = Some(animation_time); + self + } pub fn into_context(self) -> Option> { Some(Arc::new(self)) } diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 030ccc8d6d..7abd260100 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -296,31 +296,31 @@ async fn layer(_: impl Ctx, mut stack: GraphicGroupTable, element: GraphicElemen stack } -// TODO: Once we have nicely working spreadsheet tables, test this and make it nicely user-facing and move it from "Debug" to "General" -#[node_macro::node(category("Debug"))] -async fn concatenate( - _: impl Ctx, - #[implementations( - GraphicGroupTable, - VectorDataTable, - ImageFrameTable, - TextureFrameTable, - )] - from: Instances, - #[expose] - #[implementations( - GraphicGroupTable, - VectorDataTable, - ImageFrameTable, - TextureFrameTable, - )] - mut to: Instances, -) -> Instances { - for instance in from.instances() { - to.push_instance(instance); - } - to -} +// // TODO: Once we have nicely working spreadsheet tables, test this and make it nicely user-facing and move it from "Debug" to "General" +// #[node_macro::node(category("Debug"))] +// async fn concatenate( +// _: impl Ctx, +// #[implementations( +// GraphicGroupTable, +// VectorDataTable, +// ImageFrameTable, +// TextureFrameTable, +// )] +// from: Instances, +// #[expose] +// #[implementations( +// GraphicGroupTable, +// VectorDataTable, +// ImageFrameTable, +// TextureFrameTable, +// )] +// mut to: Instances, +// ) -> Instances { +// for instance in from.instances() { +// to.push_instance(instance); +// } +// to +// } #[node_macro::node(category("Debug"))] async fn to_element + 'n>( diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index ff8c75d601..f4a8649e44 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -13,6 +13,7 @@ pub use crate as graphene_core; #[cfg(feature = "reflections")] pub use ctor; +pub mod animation; pub mod consts; pub mod context; pub mod generic; diff --git a/node-graph/gcore/src/logic.rs b/node-graph/gcore/src/logic.rs index c1e7d21f05..cefb4617e6 100644 --- a/node-graph/gcore/src/logic.rs +++ b/node-graph/gcore/src/logic.rs @@ -10,12 +10,35 @@ fn log_to_console(_: impl Ctx, #[implementations(String, bo value } -#[node_macro::node(category("Debug"), skip_impl)] +#[node_macro::node(category("Text"))] fn to_string(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> String { format!("{:?}", value) } -#[node_macro::node(category("Debug"))] +#[node_macro::node(category("Text"))] +fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, #[implementations(String)] second: String) -> String { + first.clone() + &second +} + +#[node_macro::node(category("Text"))] +fn string_replace(_: impl Ctx, #[implementations(String)] string: String, from: String, to: String) -> String { + string.replace(&from, &to) +} + +#[node_macro::node(category("Text"))] +fn string_slice(_: impl Ctx, #[implementations(String)] string: String, start: f64, end: f64) -> String { + let start = if start < 0. { string.len() - start.abs() as usize } else { start as usize }; + let end = if end <= 0. { string.len() - end.abs() as usize } else { end as usize }; + let n = end.saturating_sub(start); + string.char_indices().skip(start).take(n).map(|(_, c)| c).collect() +} + +#[node_macro::node(category("Text"))] +fn string_length(_: impl Ctx, #[implementations(String)] string: String) -> usize { + string.len() +} + +#[node_macro::node(category("Text"))] async fn switch( #[implementations(Context)] ctx: C, condition: bool, diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 785c5b7ba0..6f05264697 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -412,6 +412,12 @@ fn blend_mode_value(_: impl Ctx, _primary: (), blend_mode: BlendMode) -> BlendMo blend_mode } +/// Constructs a string value which may be set to any plain text. +#[node_macro::node(category("Value"))] +fn string_value(_: impl Ctx, _primary: (), string: String) -> String { + string +} + /// Meant for debugging purposes, not general use. Returns the size of the input type in bytes. #[cfg(feature = "std")] #[node_macro::node(category("Debug"))] diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index c1ff933787..ad09e31d03 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -3,6 +3,8 @@ use crate::vector::{HandleId, VectorData, VectorDataTable}; use bezier_rs::Subpath; use glam::DVec2; +use super::misc::AsU64; + trait CornerRadius { fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable; } @@ -70,30 +72,32 @@ fn rectangle( } #[node_macro::node(category("Vector: Shape"))] -fn regular_polygon( +fn regular_polygon( _: impl Ctx, _primary: (), #[default(6)] #[min(3.)] - sides: u32, + #[implementations(u32, u64, f64)] + sides: T, #[default(50)] radius: f64, ) -> VectorDataTable { - let points = sides.into(); + let points = sides.as_u64(); let radius: f64 = radius * 2.; VectorDataTable::new(VectorData::from_subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))) } #[node_macro::node(category("Vector: Shape"))] -fn star( +fn star( _: impl Ctx, _primary: (), #[default(5)] #[min(2.)] - sides: u32, + #[implementations(u32, u64, f64)] + sides: T, #[default(50)] radius: f64, #[default(25)] inner_radius: f64, ) -> VectorDataTable { - let points = sides.into(); + let points = sides.as_u64(); let diameter: f64 = radius * 2.; let inner_diameter = inner_radius * 2.; diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index f1c1fd0689..476963b4bc 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -47,3 +47,41 @@ impl core::fmt::Display for BooleanOperation { } } } + +pub trait AsU64 { + fn as_u64(&self) -> u64; +} +impl AsU64 for u32 { + fn as_u64(&self) -> u64 { + *self as u64 + } +} +impl AsU64 for u64 { + fn as_u64(&self) -> u64 { + *self + } +} +impl AsU64 for f64 { + fn as_u64(&self) -> u64 { + *self as u64 + } +} + +pub trait AsI64 { + fn as_i64(&self) -> i64; +} +impl AsI64 for u32 { + fn as_i64(&self) -> i64 { + *self as i64 + } +} +impl AsI64 for u64 { + fn as_i64(&self) -> i64 { + *self as i64 + } +} +impl AsI64 for f64 { + fn as_i64(&self) -> i64 { + *self as i64 + } +} diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 2834469710..42fcdf5e70 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -182,6 +182,7 @@ tagged_value! { NodePath(Vec), VecDVec2(Vec), RedGreenBlue(graphene_core::raster::RedGreenBlue), + RealTimeMode(graphene_core::animation::RealTimeMode), RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha), NoiseType(graphene_core::raster::NoiseType), FractalType(graphene_core::raster::FractalType), diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index e8087515e0..463a10bf64 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -559,12 +559,10 @@ impl core::fmt::Debug for GraphErrorType { let inputs = inputs.replace("Option>", "Context"); write!( f, - "This node isn't compatible with the com-\n\ - bination of types for the data it is given:\n\ + "This node isn't compatible with the combination of types for the data it is given:\n\ {inputs}\n\ \n\ - Each invalid input should be replaced by\n\ - data with one of these supported types:\n\ + Each invalid input should be replaced by data with one of these supported types:\n\ {}", errors.join("\n") ) diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 686ea72df1..964b5db4f4 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -235,7 +235,11 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( _surface_handle: impl Node, Output = Option>, ) -> RenderOutput { let footprint = render_config.viewport; - let ctx = OwnedContextImpl::default().with_footprint(footprint).into_context(); + let ctx = OwnedContextImpl::default() + .with_footprint(footprint) + .with_time(render_config.time.time) + .with_animation_time(render_config.time.animation_time.as_secs_f64()) + .into_context(); ctx.footprint(); let RenderConfig { hide_artboards, for_export, .. } = render_config;