mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 03:58:12 +08:00
Merge branch 'master' into merge_point
This commit is contained in:
+1
-2
@@ -14,8 +14,6 @@ license = "Apache-2.0"
|
||||
default = ["wasm"]
|
||||
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
|
||||
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
|
||||
tauri = ["ron", "decouple-execution"]
|
||||
decouple-execution = []
|
||||
resvg = ["graphene-std/resvg"]
|
||||
vello = ["graphene-std/vello", "resvg"]
|
||||
ron = ["dep:ron"]
|
||||
@@ -47,6 +45,7 @@ usvg = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
spin = "0.9.8"
|
||||
|
||||
+17
-43
@@ -5,7 +5,6 @@ use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Dispatcher {
|
||||
buffered_queue: Option<Vec<VecDeque<Message>>>,
|
||||
message_queues: Vec<VecDeque<Message>>,
|
||||
pub responses: Vec<FrontendMessage>,
|
||||
pub message_handlers: DispatcherMessageHandlers,
|
||||
@@ -14,8 +13,10 @@ pub struct Dispatcher {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DispatcherMessageHandlers {
|
||||
animation_message_handler: AnimationMessageHandler,
|
||||
app_window_message_handler: AppWindowMessageHandler,
|
||||
broadcast_message_handler: BroadcastMessageHandler,
|
||||
debug_message_handler: DebugMessageHandler,
|
||||
defer_message_handler: DeferMessageHandler,
|
||||
dialog_message_handler: DialogMessageHandler,
|
||||
globals_message_handler: GlobalsMessageHandler,
|
||||
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
|
||||
@@ -50,7 +51,10 @@ 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))];
|
||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)),
|
||||
MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter),
|
||||
];
|
||||
// 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", "CurrentTime", "Time"];
|
||||
|
||||
@@ -90,14 +94,6 @@ impl Dispatcher {
|
||||
|
||||
pub fn handle_message<T: Into<Message>>(&mut self, message: T, process_after_all_current: bool) {
|
||||
let message = message.into();
|
||||
// Add all additional messages to the buffer if it exists (except from the end buffer message)
|
||||
if !matches!(message, Message::EndBuffer { .. }) {
|
||||
if let Some(buffered_queue) = &mut self.buffered_queue {
|
||||
Self::schedule_execution(buffered_queue, true, [message]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we are not maintaining the buffer, simply add to the current queue
|
||||
Self::schedule_execution(&mut self.message_queues, process_after_all_current, [message]);
|
||||
@@ -129,10 +125,16 @@ impl Dispatcher {
|
||||
Message::Animation(message) => {
|
||||
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::AppWindow(message) => {
|
||||
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()),
|
||||
Message::Debug(message) => {
|
||||
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Defer(message) => {
|
||||
self.message_handlers.defer_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Dialog(message) => {
|
||||
let context = DialogMessageContext {
|
||||
portfolio: &self.message_handlers.portfolio_message_handler,
|
||||
@@ -204,11 +206,14 @@ impl Dispatcher {
|
||||
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
|
||||
}
|
||||
Message::Tool(message) => {
|
||||
let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap();
|
||||
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
|
||||
let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else {
|
||||
warn!("Called ToolMessage without an active document.\nGot {message:?}");
|
||||
return;
|
||||
};
|
||||
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
|
||||
warn!("Called ToolMessage with an invalid active document.\nGot {message:?}");
|
||||
return;
|
||||
};
|
||||
|
||||
let context = ToolMessageContext {
|
||||
document_id,
|
||||
@@ -228,37 +233,6 @@ impl Dispatcher {
|
||||
Message::Batched { messages } => {
|
||||
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
|
||||
}
|
||||
Message::StartBuffer => {
|
||||
self.buffered_queue = Some(std::mem::take(&mut self.message_queues));
|
||||
}
|
||||
Message::EndBuffer { render_metadata } => {
|
||||
// Assign the message queue to the currently buffered queue
|
||||
if let Some(buffered_queue) = self.buffered_queue.take() {
|
||||
self.cleanup_queues(false);
|
||||
assert!(self.message_queues.is_empty(), "message queues are always empty when ending a buffer");
|
||||
self.message_queues = buffered_queue;
|
||||
};
|
||||
|
||||
let graphene_std::renderer::RenderMetadata {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
click_targets,
|
||||
clip_targets,
|
||||
} = render_metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
let messages = [
|
||||
DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
},
|
||||
DocumentMessage::UpdateClickTargets { click_targets },
|
||||
DocumentMessage::UpdateClipTargets { clip_targets },
|
||||
];
|
||||
Self::schedule_execution(&mut self.message_queues, false, messages.map(Message::from));
|
||||
}
|
||||
}
|
||||
|
||||
// If there are child messages, append the queue to the list of queues
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, AppWindow)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AppWindowMessage {
|
||||
AppWindowMinimize,
|
||||
AppWindowMaximize,
|
||||
AppWindowClose,
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::messages::app_window::AppWindowMessage;
|
||||
use crate::messages::prelude::*;
|
||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct AppWindowMessageHandler {
|
||||
platform: AppWindowPlatform,
|
||||
maximized: bool,
|
||||
viewport_hole_punch_active: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<AppWindowMessage, ()> for AppWindowMessageHandler {
|
||||
fn process_message(&mut self, message: AppWindowMessage, responses: &mut std::collections::VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
AppWindowMessage::AppWindowMinimize => {
|
||||
self.platform = if self.platform == AppWindowPlatform::Mac {
|
||||
AppWindowPlatform::Windows
|
||||
} else {
|
||||
AppWindowPlatform::Mac
|
||||
};
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
AppWindowMessage::AppWindowMaximize => {
|
||||
self.maximized = !self.maximized;
|
||||
responses.add(FrontendMessage::UpdateMaximized { maximized: self.maximized });
|
||||
|
||||
self.viewport_hole_punch_active = !self.viewport_hole_punch_active;
|
||||
responses.add(FrontendMessage::UpdateViewportHolePunch {
|
||||
active: self.viewport_hole_punch_active,
|
||||
});
|
||||
}
|
||||
AppWindowMessage::AppWindowClose => {
|
||||
self.platform = AppWindowPlatform::Web;
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(AppWindowMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum AppWindowPlatform {
|
||||
#[default]
|
||||
Web,
|
||||
Windows,
|
||||
Mac,
|
||||
Linux,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod app_window_message;
|
||||
pub mod app_window_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use app_window_message::{AppWindowMessage, AppWindowMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use app_window_message_handler::AppWindowMessageHandler;
|
||||
@@ -0,0 +1,11 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Defer)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DeferMessage {
|
||||
SetGraphSubmissionIndex(u64),
|
||||
TriggerGraphRun(u64),
|
||||
AfterGraphRun { messages: Vec<Message> },
|
||||
TriggerNavigationReady,
|
||||
AfterNavigationReady { messages: Vec<Message> },
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Default, ExtractField)]
|
||||
pub struct DeferMessageHandler {
|
||||
after_graph_run: Vec<(u64, Message)>,
|
||||
after_viewport_resize: Vec<Message>,
|
||||
current_graph_submission_id: u64,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<DeferMessage, ()> for DeferMessageHandler {
|
||||
fn process_message(&mut self, message: DeferMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
DeferMessage::AfterGraphRun { mut messages } => {
|
||||
self.after_graph_run.extend(messages.drain(..).map(|m| (self.current_graph_submission_id, m)));
|
||||
}
|
||||
DeferMessage::AfterNavigationReady { messages } => {
|
||||
self.after_viewport_resize.extend_from_slice(&messages);
|
||||
}
|
||||
DeferMessage::SetGraphSubmissionIndex(execution_id) => {
|
||||
self.current_graph_submission_id = execution_id + 1;
|
||||
}
|
||||
DeferMessage::TriggerGraphRun(execution_id) => {
|
||||
if self.after_graph_run.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Find the index of the last message we can process
|
||||
let num_elements_to_remove = self.after_graph_run.binary_search_by_key(&(execution_id + 1), |x| x.0).unwrap_or_else(|pos| pos - 1);
|
||||
let elements = self.after_graph_run.drain(0..=num_elements_to_remove);
|
||||
for (_, message) in elements.rev() {
|
||||
responses.add_front(message);
|
||||
}
|
||||
}
|
||||
DeferMessage::TriggerNavigationReady => {
|
||||
for message in self.after_viewport_resize.drain(..).rev() {
|
||||
responses.add_front(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advertise_actions!(DeferMessageDiscriminant;
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod defer_message;
|
||||
mod defer_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use defer_message::{DeferMessage, DeferMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use defer_message_handler::DeferMessageHandler;
|
||||
+12
-10
@@ -24,18 +24,20 @@ impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHa
|
||||
|
||||
let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0;
|
||||
if create_artboard {
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::NewArtboard {
|
||||
id: NodeId::new(),
|
||||
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![
|
||||
GraphOperationMessage::NewArtboard {
|
||||
id: NodeId::new(),
|
||||
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
});
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()],
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
|
||||
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
|
||||
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{
|
||||
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform,
|
||||
@@ -58,7 +59,6 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "commitDate")]
|
||||
commit_date: String,
|
||||
},
|
||||
TriggerDelayedZoomCanvasToFitAll,
|
||||
TriggerDownloadImage {
|
||||
svg: String,
|
||||
name: String,
|
||||
@@ -309,4 +309,13 @@ pub enum FrontendMessage {
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdatePlatform {
|
||||
platform: AppWindowPlatform,
|
||||
},
|
||||
UpdateMaximized {
|
||||
maximized: bool,
|
||||
},
|
||||
UpdateViewportHolePunch {
|
||||
active: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control, Shift])),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control])),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
|
||||
@@ -78,7 +78,9 @@ impl<'a> serde::Deserialize<'a> for CheckboxId {
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
let id = u64::deserialize(deserializer)?;
|
||||
let optional_id: Option<u64> = Option::deserialize(deserializer)?;
|
||||
// TODO: This is potentially weird because after deserialization the two labels will be decoupled if the value not existent
|
||||
let id = optional_id.unwrap_or(0);
|
||||
let checkbox_id = CheckboxId(OnceCell::new().into());
|
||||
checkbox_id.0.set(id).map_err(serde::de::Error::custom)?;
|
||||
Ok(checkbox_id)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::renderer::RenderMetadata;
|
||||
use graphite_proc_macros::*;
|
||||
|
||||
#[impl_message]
|
||||
@@ -9,10 +8,14 @@ pub enum Message {
|
||||
#[child]
|
||||
Animation(AnimationMessage),
|
||||
#[child]
|
||||
AppWindow(AppWindowMessage),
|
||||
#[child]
|
||||
Broadcast(BroadcastMessage),
|
||||
#[child]
|
||||
Debug(DebugMessage),
|
||||
#[child]
|
||||
Defer(DeferMessage),
|
||||
#[child]
|
||||
Dialog(DialogMessage),
|
||||
#[child]
|
||||
Frontend(FrontendMessage),
|
||||
@@ -38,10 +41,6 @@ pub enum Message {
|
||||
Batched {
|
||||
messages: Box<[Message]>,
|
||||
},
|
||||
StartBuffer,
|
||||
EndBuffer {
|
||||
render_metadata: RenderMetadata,
|
||||
},
|
||||
}
|
||||
|
||||
/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! The root-level messages forming the first layer of the message system architecture.
|
||||
|
||||
pub mod animation;
|
||||
pub mod app_window;
|
||||
pub mod broadcast;
|
||||
pub mod debug;
|
||||
pub mod defer;
|
||||
pub mod dialog;
|
||||
pub mod frontend;
|
||||
pub mod globals;
|
||||
|
||||
@@ -1435,6 +1435,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
},
|
||||
})
|
||||
}
|
||||
// Some parts of the editior (e.g. navigation messages) depend on these bounds to be present
|
||||
let bounds = if self.graph_view_overlay_open {
|
||||
self.network_interface.all_nodes_bounding_box(&self.breadcrumb_network_path).cloned()
|
||||
} else {
|
||||
self.network_interface.document_bounds_document_space(true)
|
||||
};
|
||||
if bounds.is_some() {
|
||||
responses.add(DeferMessage::TriggerNavigationReady);
|
||||
} else {
|
||||
// If we don't have bounds yet, we need wait until the node graph has run once more
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![DocumentMessage::PTZUpdate.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectionStepBack => {
|
||||
self.network_interface.selection_step_back(&self.selection_network_path);
|
||||
@@ -1870,10 +1884,11 @@ impl DocumentMessageHandler {
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
|
||||
|
||||
// TODO: Remove once the footprint is used to load the imports/export distances from the edge
|
||||
responses.add(NodeGraphMessage::UnloadWires);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
responses.add(Message::StartBuffer);
|
||||
|
||||
Some(previous_network)
|
||||
}
|
||||
pub fn redo_with_history(&mut self, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
|
||||
@@ -630,8 +630,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::VectorData(VectorDataTable::default()), true),
|
||||
NodeInput::value(
|
||||
TaggedValue::Footprint(Footprint {
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(100., 100.), 0., DVec2::new(0., 0.)),
|
||||
resolution: UVec2::new(100, 100),
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1000., 1000.), 0., DVec2::new(0., 0.)),
|
||||
resolution: UVec2::new(1000, 1000),
|
||||
..Default::default()
|
||||
}),
|
||||
false,
|
||||
@@ -851,7 +851,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Coordinate",
|
||||
identifier: "Split Vec2",
|
||||
category: "Math: Vector",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
@@ -882,7 +882,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
input_metadata: vec![("Coordinate", "TODO").into()],
|
||||
input_metadata: vec![("Vec2", "TODO").into()],
|
||||
output_names: vec!["X".to_string(), "Y".to_string()],
|
||||
has_primary_output: false,
|
||||
network_metadata: Some(NodeNetworkMetadata {
|
||||
@@ -917,7 +917,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed(
|
||||
"Decomposes the X and Y components of a 2D coordinate.\n\nThe inverse of this node is \"Coordinate Value\", which can have either or both its X and Y exposed as graph inputs.",
|
||||
"Decomposes the X and Y components of a vec2.\n\nThe inverse of this node is \"Vec2 Value\", which can have either or both its X and Y parameters exposed as graph inputs.",
|
||||
),
|
||||
properties: None,
|
||||
},
|
||||
@@ -2042,7 +2042,7 @@ fn static_input_properties() -> InputProperties {
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(vec![node_properties::coordinate_widget(
|
||||
Ok(vec![node_properties::vec2_widget(
|
||||
ParameterWidgetsInfo::new(node_id, index, true, context),
|
||||
&x,
|
||||
&y,
|
||||
@@ -2298,7 +2298,7 @@ fn static_input_properties() -> InputProperties {
|
||||
"spline_input".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
Ok(vec![LayoutGroup::Row {
|
||||
widgets: node_properties::array_of_coordinates_widget(ParameterWidgetsInfo::new(node_id, index, true, context), TextInput::default().centered(true)),
|
||||
widgets: node_properties::array_of_vec2_widget(ParameterWidgetsInfo::new(node_id, index, true, context), TextInput::default().centered(true)),
|
||||
}])
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -160,7 +160,7 @@ pub(crate) fn property_from_type(
|
||||
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
|
||||
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
|
||||
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
|
||||
Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
|
||||
Some("TextArea") => text_area_widget(default_info).into(),
|
||||
|
||||
// For all other types, use TypeId-based matching
|
||||
@@ -175,13 +175,13 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if x == TypeId::of::<u64>() => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||
Some(x) if x == TypeId::of::<bool>() => bool_widget(default_info, CheckboxInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None, false),
|
||||
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false),
|
||||
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
|
||||
// ==========================
|
||||
// PRIMITIVE COLLECTION TYPES
|
||||
// ==========================
|
||||
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_coordinates_widget(default_info, TextInput::default()).into(),
|
||||
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vec2_widget(default_info, TextInput::default()).into(),
|
||||
// ====================
|
||||
// GRAPHICAL DATA TYPES
|
||||
// ====================
|
||||
@@ -480,6 +480,10 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
|
||||
resolution_widgets.push(
|
||||
NumberInput::new(Some((footprint.resolution.as_dvec2() / bounds).x * 100.))
|
||||
.label("Resolution")
|
||||
.mode_range()
|
||||
.min(0.)
|
||||
.range_min(Some(1.))
|
||||
.range_max(Some(100.))
|
||||
.unit("%")
|
||||
.on_update(update_value(
|
||||
move |x: &NumberInput| {
|
||||
@@ -626,7 +630,7 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
|
||||
pub fn vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(parameter_widgets_info);
|
||||
@@ -723,7 +727,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
|
||||
widgets
|
||||
}
|
||||
|
||||
pub fn array_of_coordinates_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
|
||||
pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(parameter_widgets_info);
|
||||
@@ -1249,7 +1253,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
let spacing = coordinate_widget(ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::INDEX, true, context), "W", "H", " px", Some(0.), false);
|
||||
let spacing = vec2_widget(ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::INDEX, true, context), "W", "H", " px", Some(0.), false);
|
||||
widgets.push(spacing);
|
||||
}
|
||||
GridType::Isometric => {
|
||||
@@ -1259,7 +1263,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
NumberInput::default().label("H").min(0.).unit(" px"),
|
||||
),
|
||||
};
|
||||
let angles = coordinate_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false);
|
||||
let angles = vec2_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false);
|
||||
widgets.extend([spacing, angles]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ pub mod grid_overlays;
|
||||
mod overlays_message;
|
||||
mod overlays_message_handler;
|
||||
pub mod utility_functions;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod utility_types;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod utility_types_vello;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use utility_types_vello as utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};
|
||||
|
||||
@@ -21,7 +21,6 @@ pub struct OverlaysMessageHandler {
|
||||
impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMessageHandler {
|
||||
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, context: OverlaysMessageContext) {
|
||||
let OverlaysMessageContext { visibility_settings, ipp, .. } = context;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let device_pixel_ratio = context.device_pixel_ratio;
|
||||
|
||||
match message {
|
||||
@@ -69,9 +68,39 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(test)]
|
||||
OverlaysMessage::Draw => {}
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(test)))]
|
||||
OverlaysMessage::Draw => {
|
||||
warn!("Cannot render overlays on non-Wasm targets.\n{responses:?} {visibility_settings:?} {ipp:?}",);
|
||||
use super::utility_types::OverlayContext;
|
||||
use vello::Scene;
|
||||
|
||||
let size = ipp.viewport_bounds.size().as_uvec2();
|
||||
|
||||
let scene = Scene::new();
|
||||
|
||||
if visibility_settings.all() {
|
||||
let overlay_context = OverlayContext {
|
||||
scene,
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings,
|
||||
};
|
||||
|
||||
responses.add(DocumentMessage::GridOverlays(overlay_context.clone()));
|
||||
|
||||
for provider in &self.overlay_providers {
|
||||
let overlay_context = OverlayContext {
|
||||
scene: Scene::new(),
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings,
|
||||
};
|
||||
responses.add(provider(overlay_context));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Render the Vello scene to a texture and display it
|
||||
}
|
||||
OverlaysMessage::AddProvider(message) => {
|
||||
self.overlay_providers.insert(message);
|
||||
|
||||
@@ -423,11 +423,9 @@ impl OverlayContext {
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, dash_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
|
||||
let end_point2 = pivot + dash_radius * DVec2::from_angle(offset_angle);
|
||||
self.line(pivot, end_point1, None, None);
|
||||
self.dashed_line(pivot, end_point2, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||
}
|
||||
|
||||
@@ -592,9 +590,9 @@ impl OverlayContext {
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, dash_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, dash_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,758 @@
|
||||
use crate::consts::{
|
||||
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
|
||||
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
|
||||
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorData};
|
||||
use std::collections::HashMap;
|
||||
use vello::Scene;
|
||||
use vello::kurbo::{self, BezPath};
|
||||
use vello::peniko;
|
||||
|
||||
pub type OverlayProvider = fn(OverlayContext) -> Message;
|
||||
|
||||
pub fn empty_provider() -> OverlayProvider {
|
||||
|_| Message::NoOp
|
||||
}
|
||||
|
||||
// Types of overlays used by DocumentMessage to enable/disable select group of overlays in the frontend
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum OverlaysType {
|
||||
ArtboardName,
|
||||
CompassRose,
|
||||
QuickMeasurement,
|
||||
TransformMeasurement,
|
||||
TransformCage,
|
||||
HoverOutline,
|
||||
SelectionOutline,
|
||||
Pivot,
|
||||
Origin,
|
||||
Path,
|
||||
Anchors,
|
||||
Handles,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct OverlaysVisibilitySettings {
|
||||
pub all: bool,
|
||||
pub artboard_name: bool,
|
||||
pub compass_rose: bool,
|
||||
pub quick_measurement: bool,
|
||||
pub transform_measurement: bool,
|
||||
pub transform_cage: bool,
|
||||
pub hover_outline: bool,
|
||||
pub selection_outline: bool,
|
||||
pub pivot: bool,
|
||||
pub origin: bool,
|
||||
pub path: bool,
|
||||
pub anchors: bool,
|
||||
pub handles: bool,
|
||||
}
|
||||
|
||||
impl Default for OverlaysVisibilitySettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
all: true,
|
||||
artboard_name: true,
|
||||
compass_rose: true,
|
||||
quick_measurement: true,
|
||||
transform_measurement: true,
|
||||
transform_cage: true,
|
||||
hover_outline: true,
|
||||
selection_outline: true,
|
||||
pivot: true,
|
||||
origin: true,
|
||||
path: true,
|
||||
anchors: true,
|
||||
handles: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OverlaysVisibilitySettings {
|
||||
pub fn all(&self) -> bool {
|
||||
self.all
|
||||
}
|
||||
|
||||
pub fn artboard_name(&self) -> bool {
|
||||
self.all && self.artboard_name
|
||||
}
|
||||
|
||||
pub fn compass_rose(&self) -> bool {
|
||||
self.all && self.compass_rose
|
||||
}
|
||||
|
||||
pub fn quick_measurement(&self) -> bool {
|
||||
self.all && self.quick_measurement
|
||||
}
|
||||
|
||||
pub fn transform_measurement(&self) -> bool {
|
||||
self.all && self.transform_measurement
|
||||
}
|
||||
|
||||
pub fn transform_cage(&self) -> bool {
|
||||
self.all && self.transform_cage
|
||||
}
|
||||
|
||||
pub fn hover_outline(&self) -> bool {
|
||||
self.all && self.hover_outline
|
||||
}
|
||||
|
||||
pub fn selection_outline(&self) -> bool {
|
||||
self.all && self.selection_outline
|
||||
}
|
||||
|
||||
pub fn pivot(&self) -> bool {
|
||||
self.all && self.pivot
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> bool {
|
||||
self.all && self.origin
|
||||
}
|
||||
|
||||
pub fn path(&self) -> bool {
|
||||
self.all && self.path
|
||||
}
|
||||
|
||||
pub fn anchors(&self) -> bool {
|
||||
self.all && self.anchors
|
||||
}
|
||||
|
||||
pub fn handles(&self) -> bool {
|
||||
self.all && self.anchors && self.handles
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct OverlayContext {
|
||||
// Serde functionality isn't used but is required by the message system macros
|
||||
#[serde(skip)]
|
||||
#[specta(skip)]
|
||||
pub scene: Scene,
|
||||
pub size: DVec2,
|
||||
// The device pixel ratio is a property provided by the browser window and is the CSS pixel size divided by the physical monitor's pixel size.
|
||||
// It allows better pixel density of visualizations on high-DPI displays where the OS display scaling is not 100%, or where the browser is zoomed.
|
||||
pub device_pixel_ratio: f64,
|
||||
pub visibility_settings: OverlaysVisibilitySettings,
|
||||
}
|
||||
|
||||
// Manual implementations since Scene doesn't implement PartialEq or Debug
|
||||
impl PartialEq for OverlayContext {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.size == other.size && self.device_pixel_ratio == other.device_pixel_ratio && self.visibility_settings == other.visibility_settings
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OverlayContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("OverlayContext")
|
||||
.field("scene", &"Scene { ... }")
|
||||
.field("size", &self.size)
|
||||
.field("device_pixel_ratio", &self.device_pixel_ratio)
|
||||
.field("visibility_settings", &self.visibility_settings)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Default implementation for Scene
|
||||
impl Default for OverlayContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scene: Scene::new(),
|
||||
size: DVec2::ZERO,
|
||||
device_pixel_ratio: 1.0,
|
||||
visibility_settings: OverlaysVisibilitySettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Message hashing isn't used but is required by the message system macros
|
||||
impl core::hash::Hash for OverlayContext {
|
||||
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
|
||||
}
|
||||
|
||||
impl OverlayContext {
|
||||
fn parse_color(color: &str) -> peniko::Color {
|
||||
let hex = color.trim_start_matches('#');
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
|
||||
let a = if hex.len() >= 8 { u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) } else { 255 };
|
||||
peniko::Color::from_rgba8(r, g, b, a)
|
||||
}
|
||||
|
||||
pub fn quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>) {
|
||||
self.dashed_polygon(&quad.0, stroke_color, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn draw_triangle(&mut self, base: DVec2, direction: DVec2, size: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let normal = direction.perp();
|
||||
let top = base + direction * size;
|
||||
let edge1 = base + normal * size / 2.;
|
||||
let edge2 = base - normal * size / 2.;
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(top.x, top.y));
|
||||
path.line_to(kurbo::Point::new(edge1.x, edge1.y));
|
||||
path.line_to(kurbo::Point::new(edge2.x, edge2.y));
|
||||
path.close_path();
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &path);
|
||||
}
|
||||
|
||||
pub fn dashed_quad(&mut self, quad: Quad, stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
self.dashed_polygon(&quad.0, stroke_color, color_fill, dash_width, dash_gap_width, dash_offset);
|
||||
}
|
||||
|
||||
pub fn polygon(&mut self, polygon: &[DVec2], stroke_color: Option<&str>, color_fill: Option<&str>) {
|
||||
self.dashed_polygon(polygon, stroke_color, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn dashed_polygon(&mut self, polygon: &[DVec2], stroke_color: Option<&str>, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
if polygon.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
let mut path = BezPath::new();
|
||||
if let Some(first) = polygon.last() {
|
||||
path.move_to(kurbo::Point::new(first.x.round() - 0.5, first.y.round() - 0.5));
|
||||
}
|
||||
|
||||
for point in polygon {
|
||||
path.line_to(kurbo::Point::new(point.x.round() - 0.5, point.y.round() - 0.5));
|
||||
}
|
||||
path.close_path();
|
||||
|
||||
if let Some(color_fill) = color_fill {
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &path);
|
||||
}
|
||||
|
||||
let stroke_color = stroke_color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let mut stroke = kurbo::Stroke::new(1.0);
|
||||
|
||||
if let Some(dash_width) = dash_width {
|
||||
let dash_gap = dash_gap_width.unwrap_or(1.);
|
||||
stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width, dash_gap]);
|
||||
}
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(stroke_color), None, &path);
|
||||
}
|
||||
|
||||
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>) {
|
||||
self.dashed_line(start, end, color, thickness, None, None, None)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, thickness: Option<f64>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
let transform = self.get_transform();
|
||||
|
||||
let start = start.round() - DVec2::splat(0.5);
|
||||
let end = end.round() - DVec2::splat(0.5);
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
path.line_to(kurbo::Point::new(end.x, end.y));
|
||||
|
||||
let mut stroke = kurbo::Stroke::new(thickness.unwrap_or(1.));
|
||||
|
||||
if let Some(dash_width) = dash_width {
|
||||
let dash_gap = dash_gap_width.unwrap_or(1.);
|
||||
stroke = stroke.with_dashes(dash_offset.unwrap_or(0.), [dash_width, dash_gap]);
|
||||
}
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &path);
|
||||
}
|
||||
|
||||
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
|
||||
let transform = self.get_transform();
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
|
||||
let circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.);
|
||||
|
||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle);
|
||||
|
||||
self.scene
|
||||
.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle);
|
||||
}
|
||||
|
||||
pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
|
||||
let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE };
|
||||
self.square(position, None, Some(color_fill), Some(color_stroke));
|
||||
}
|
||||
|
||||
fn get_transform(&self) -> kurbo::Affine {
|
||||
kurbo::Affine::scale(self.device_pixel_ratio)
|
||||
}
|
||||
|
||||
pub fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
let corner = position - DVec2::splat(size) / 2.;
|
||||
|
||||
let transform = self.get_transform();
|
||||
let rect = kurbo::Rect::new(corner.x, corner.y, corner.x + size, corner.y + size);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &rect);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &rect);
|
||||
}
|
||||
|
||||
pub fn pixel(&mut self, position: DVec2, color: Option<&str>) {
|
||||
let size = 1.;
|
||||
let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
|
||||
let position = position.round() - DVec2::splat(0.5);
|
||||
let corner = position - DVec2::splat(size) / 2.;
|
||||
|
||||
let transform = self.get_transform();
|
||||
let rect = kurbo::Rect::new(corner.x, corner.y, corner.x + size, corner.y + size);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &rect);
|
||||
}
|
||||
|
||||
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let position = position.round();
|
||||
|
||||
let transform = self.get_transform();
|
||||
let circle = kurbo::Circle::new((position.x, position.y), radius);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle);
|
||||
}
|
||||
|
||||
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
|
||||
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
|
||||
let step = (end_at - start_from) / segments as f64;
|
||||
let half_step = step / 2.;
|
||||
let factor = 4. / 3. * half_step.sin() / (1. + half_step.cos());
|
||||
|
||||
let mut path = BezPath::new();
|
||||
|
||||
for i in 0..segments {
|
||||
let start_angle = start_from + step * i as f64;
|
||||
let end_angle = start_angle + step;
|
||||
let start_vec = DVec2::from_angle(start_angle);
|
||||
let end_vec = DVec2::from_angle(end_angle);
|
||||
|
||||
let start = center + radius * start_vec;
|
||||
let end = center + radius * end_vec;
|
||||
|
||||
let handle_start = start + start_vec.perp() * radius * factor;
|
||||
let handle_end = end - end_vec.perp() * radius * factor;
|
||||
|
||||
if i == 0 {
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
}
|
||||
|
||||
path.curve_to(
|
||||
kurbo::Point::new(handle_start.x, handle_start.y),
|
||||
kurbo::Point::new(handle_end.x, handle_end.y),
|
||||
kurbo::Point::new(end.x, end.y),
|
||||
);
|
||||
}
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
|
||||
self.line(pivot, end_point1, None, None);
|
||||
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||
}
|
||||
|
||||
pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
|
||||
let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
|
||||
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
|
||||
self.line(pivot, end_point1, None, None);
|
||||
self.dashed_line(pivot, end_point2, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
|
||||
}
|
||||
|
||||
pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
|
||||
let sign = scale.signum();
|
||||
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
let fill_color = Some(fill_color.as_str());
|
||||
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None, None);
|
||||
self.circle(start, radius, fill_color, None);
|
||||
self.circle(start, radius * scale.abs(), fill_color, None);
|
||||
self.text(
|
||||
text,
|
||||
COLOR_OVERLAY_BLUE,
|
||||
None,
|
||||
DAffine2::from_translation(start + sign * DVec2::X * radius * (1. + scale.abs()) / 2.),
|
||||
2.,
|
||||
[Pivot::Middle, Pivot::End],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn compass_rose(&mut self, compass_center: DVec2, angle: f64, show_compass_with_hover_ring: Option<bool>) {
|
||||
const HOVER_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_HOVER_RING_DIAMETER / 2.;
|
||||
const MAIN_RING_OUTER_RADIUS: f64 = COMPASS_ROSE_MAIN_RING_DIAMETER / 2.;
|
||||
const MAIN_RING_INNER_RADIUS: f64 = COMPASS_ROSE_RING_INNER_DIAMETER / 2.;
|
||||
const ARROW_RADIUS: f64 = COMPASS_ROSE_ARROW_SIZE / 2.;
|
||||
const HOVER_RING_STROKE_WIDTH: f64 = HOVER_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
|
||||
const HOVER_RING_CENTERLINE_RADIUS: f64 = (HOVER_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
|
||||
const MAIN_RING_STROKE_WIDTH: f64 = MAIN_RING_OUTER_RADIUS - MAIN_RING_INNER_RADIUS;
|
||||
const MAIN_RING_CENTERLINE_RADIUS: f64 = (MAIN_RING_OUTER_RADIUS + MAIN_RING_INNER_RADIUS) / 2.;
|
||||
|
||||
let Some(show_hover_ring) = show_compass_with_hover_ring else { return };
|
||||
|
||||
let transform = self.get_transform();
|
||||
let center = compass_center.round() - DVec2::splat(0.5);
|
||||
|
||||
// Hover ring
|
||||
if show_hover_ring {
|
||||
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
|
||||
let circle = kurbo::Circle::new((center.x, center.y), HOVER_RING_CENTERLINE_RADIUS);
|
||||
self.scene
|
||||
.stroke(&kurbo::Stroke::new(HOVER_RING_STROKE_WIDTH), transform, Self::parse_color(&fill_color), None, &circle);
|
||||
}
|
||||
|
||||
// Arrows
|
||||
for i in 0..4 {
|
||||
let direction = DVec2::from_angle(i as f64 * FRAC_PI_2 + angle);
|
||||
let color = if i % 2 == 0 { COLOR_OVERLAY_RED } else { COLOR_OVERLAY_GREEN };
|
||||
|
||||
let tip = center + direction * HOVER_RING_OUTER_RADIUS;
|
||||
let base = center + direction * (MAIN_RING_INNER_RADIUS + MAIN_RING_OUTER_RADIUS) / 2.;
|
||||
|
||||
let r = (ARROW_RADIUS.powi(2) + MAIN_RING_INNER_RADIUS.powi(2)).sqrt();
|
||||
let (cos, sin) = (MAIN_RING_INNER_RADIUS / r, ARROW_RADIUS / r);
|
||||
let side1 = center + r * DVec2::new(cos * direction.x - sin * direction.y, sin * direction.x + direction.y * cos);
|
||||
let side2 = center + r * DVec2::new(cos * direction.x + sin * direction.y, -sin * direction.x + direction.y * cos);
|
||||
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(tip.x, tip.y));
|
||||
path.line_to(kurbo::Point::new(side1.x, side1.y));
|
||||
path.line_to(kurbo::Point::new(base.x, base.y));
|
||||
path.line_to(kurbo::Point::new(side2.x, side2.y));
|
||||
path.close_path();
|
||||
|
||||
let color_parsed = Self::parse_color(color);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, color_parsed, None, &path);
|
||||
self.scene.stroke(&kurbo::Stroke::new(0.01), transform, color_parsed, None, &path);
|
||||
}
|
||||
|
||||
// Main ring
|
||||
let circle = kurbo::Circle::new((center.x, center.y), MAIN_RING_CENTERLINE_RADIUS);
|
||||
self.scene
|
||||
.stroke(&kurbo::Stroke::new(MAIN_RING_STROKE_WIDTH), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &circle);
|
||||
}
|
||||
|
||||
pub fn pivot(&mut self, position: DVec2, angle: f64) {
|
||||
let uv = DVec2::from_angle(angle);
|
||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
// Circle
|
||||
let circle = kurbo::Circle::new((x, y), PIVOT_DIAMETER / 2.);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &circle);
|
||||
|
||||
// Crosshair
|
||||
const CROSSHAIR_RADIUS: f64 = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
|
||||
|
||||
let mut stroke = kurbo::Stroke::new(PIVOT_CROSSHAIR_THICKNESS);
|
||||
stroke = stroke.with_caps(kurbo::Cap::Round);
|
||||
|
||||
// Horizontal line
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.x, y + CROSSHAIR_RADIUS * uv.y));
|
||||
path.line_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.x, y - CROSSHAIR_RADIUS * uv.y));
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path);
|
||||
|
||||
// Vertical line
|
||||
let mut path = BezPath::new();
|
||||
path.move_to(kurbo::Point::new(x - CROSSHAIR_RADIUS * uv.y, y + CROSSHAIR_RADIUS * uv.x));
|
||||
path.line_to(kurbo::Point::new(x + CROSSHAIR_RADIUS * uv.y, y - CROSSHAIR_RADIUS * uv.x));
|
||||
|
||||
self.scene.stroke(&stroke, transform, Self::parse_color(COLOR_OVERLAY_YELLOW), None, &path);
|
||||
}
|
||||
|
||||
pub fn dowel_pin(&mut self, position: DVec2, angle: f64, color: Option<&str>) {
|
||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_YELLOW_DULL);
|
||||
|
||||
let transform = self.get_transform();
|
||||
|
||||
// Draw the background circle with a white fill and colored outline
|
||||
let circle = kurbo::Circle::new((x, y), DOWEL_PIN_RADIUS);
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(COLOR_OVERLAY_WHITE), None, &circle);
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color), None, &circle);
|
||||
|
||||
// Draw the two filled sectors using paths
|
||||
let mut path = BezPath::new();
|
||||
|
||||
// Top-left sector
|
||||
path.move_to(kurbo::Point::new(x, y));
|
||||
let end_x = x + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).cos();
|
||||
let end_y = y + DOWEL_PIN_RADIUS * (FRAC_PI_2 + angle).sin();
|
||||
path.line_to(kurbo::Point::new(end_x, end_y));
|
||||
// Draw arc manually
|
||||
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
|
||||
arc.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.curve_to(p1, p2, p);
|
||||
});
|
||||
path.close_path();
|
||||
|
||||
// Bottom-right sector
|
||||
path.move_to(kurbo::Point::new(x, y));
|
||||
let end_x = x + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).cos();
|
||||
let end_y = y + DOWEL_PIN_RADIUS * (PI + FRAC_PI_2 + angle).sin();
|
||||
path.line_to(kurbo::Point::new(end_x, end_y));
|
||||
// Draw arc manually
|
||||
let arc = kurbo::Arc::new((x, y), (DOWEL_PIN_RADIUS, DOWEL_PIN_RADIUS), PI + FRAC_PI_2 + angle, FRAC_PI_2, 0.0);
|
||||
arc.to_cubic_beziers(0.1, |p1, p2, p| {
|
||||
path.curve_to(p1, p2, p);
|
||||
});
|
||||
path.close_path();
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color), None, &path);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.text(text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
|
||||
/// Used by the Pen and Path tools to outline the path of the shape.
|
||||
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
|
||||
let mut last_point = None;
|
||||
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
|
||||
let move_to = last_point != Some(start_id);
|
||||
last_point = Some(end_id);
|
||||
|
||||
self.bezier_to_path(bezier, transform, move_to, &mut path);
|
||||
}
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
/// Used by the Pen tool in order to show how the bezier curve would look like.
|
||||
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
self.bezier_to_path(bezier, transform, true, &mut path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
/// Used by the path tool segment mode in order to show the selected segments.
|
||||
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
self.bezier_to_path(bezier, transform, true, &mut path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path);
|
||||
}
|
||||
|
||||
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
let vello_transform = self.get_transform();
|
||||
let mut path = BezPath::new();
|
||||
self.bezier_to_path(bezier, transform, true, &mut path);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path);
|
||||
}
|
||||
|
||||
fn bezier_to_path(&self, bezier: Bezier, transform: DAffine2, move_to: bool, path: &mut BezPath) {
|
||||
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
|
||||
if move_to {
|
||||
path.move_to(kurbo::Point::new(start.x, start.y));
|
||||
}
|
||||
|
||||
match handles {
|
||||
bezier_rs::BezierHandles::Linear => path.line_to(kurbo::Point::new(end.x, end.y)),
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => path.quad_to(kurbo::Point::new(handle.x, handle.y), kurbo::Point::new(end.x, end.y)),
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(
|
||||
kurbo::Point::new(handle_start.x, handle_start.y),
|
||||
kurbo::Point::new(handle_end.x, handle_end.y),
|
||||
kurbo::Point::new(end.x, end.y),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath {
|
||||
let mut path = BezPath::new();
|
||||
|
||||
for subpath in subpaths {
|
||||
let subpath = subpath.borrow();
|
||||
let mut curves = subpath.iter().peekable();
|
||||
|
||||
let Some(first) = curves.peek() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let start_point = transform.transform_point2(first.start());
|
||||
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
|
||||
|
||||
for curve in curves {
|
||||
match curve.handles {
|
||||
bezier_rs::BezierHandles::Linear => {
|
||||
let a = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
path.line_to(kurbo::Point::new(a.x, a.y));
|
||||
}
|
||||
bezier_rs::BezierHandles::Quadratic { handle } => {
|
||||
let a = transform.transform_point2(handle);
|
||||
let b = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let b = b.round() - DVec2::splat(0.5);
|
||||
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
|
||||
}
|
||||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let a = transform.transform_point2(handle_start);
|
||||
let b = transform.transform_point2(handle_end);
|
||||
let c = transform.transform_point2(curve.end());
|
||||
let a = a.round() - DVec2::splat(0.5);
|
||||
let b = b.round() - DVec2::splat(0.5);
|
||||
let c = c.round() - DVec2::splat(0.5);
|
||||
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if subpath.closed() {
|
||||
path.close_path();
|
||||
}
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Used by the Select tool to outline a path or a free point when selected or hovered.
|
||||
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
|
||||
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
|
||||
|
||||
for target_type in target_types {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
|
||||
}
|
||||
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
if !subpaths.is_empty() {
|
||||
let path = self.push_path(subpaths.iter(), transform);
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
|
||||
self.scene.stroke(&kurbo::Stroke::new(1.0), self.get_transform(), Self::parse_color(color), None, &path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
/// Used by the Pen tool to show the path being closed.
|
||||
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
|
||||
let path = self.push_path(subpaths, transform);
|
||||
|
||||
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
|
||||
}
|
||||
|
||||
/// Fills the area inside the path with a pattern. Assumes `color` is in gamma space.
|
||||
/// Used by the fill tool to show the area to be filled.
|
||||
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &Color) {
|
||||
// TODO: Implement pattern fill in Vello
|
||||
// For now, just fill with a semi-transparent version of the color
|
||||
let path = self.push_path(subpaths, transform);
|
||||
let semi_transparent_color = color.with_alpha(0.5);
|
||||
|
||||
self.scene.fill(
|
||||
peniko::Fill::NonZero,
|
||||
self.get_transform(),
|
||||
peniko::Color::from_rgba8(
|
||||
(semi_transparent_color.r() * 255.) as u8,
|
||||
(semi_transparent_color.g() * 255.) as u8,
|
||||
(semi_transparent_color.b() * 255.) as u8,
|
||||
(semi_transparent_color.a() * 255.) as u8,
|
||||
),
|
||||
None,
|
||||
&path,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_width(&self, _text: &str) -> f64 {
|
||||
// TODO: Implement proper text measurement in Vello
|
||||
0.
|
||||
}
|
||||
|
||||
pub fn text(&self, _text: &str, _font_color: &str, _background_color: Option<&str>, _transform: DAffine2, _padding: f64, _pivot: [Pivot; 2]) {
|
||||
// TODO: Implement text rendering in Vello
|
||||
}
|
||||
|
||||
pub fn translation_box(&mut self, translation: DVec2, quad: Quad, typed_string: Option<String>) {
|
||||
if translation.x.abs() > 1e-3 {
|
||||
self.dashed_line(quad.top_left(), quad.top_right(), None, None, Some(2.), Some(2.), Some(0.5));
|
||||
|
||||
let width = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.x).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
};
|
||||
let x_transform = DAffine2::from_translation((quad.top_left() + quad.top_right()) / 2.);
|
||||
self.text(width, COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
}
|
||||
|
||||
if translation.y.abs() > 1e-3 {
|
||||
self.dashed_line(quad.top_left(), quad.bottom_left(), None, None, Some(2.), Some(2.), Some(0.5));
|
||||
|
||||
let height = match typed_string {
|
||||
Some(ref typed_string) => typed_string,
|
||||
None => &format!("{:.2}", translation.y).trim_end_matches('0').trim_end_matches('.').to_string(),
|
||||
};
|
||||
let y_transform = DAffine2::from_translation((quad.top_left() + quad.bottom_left()) / 2.);
|
||||
let height_pivot = if translation.x > -1e-3 { Pivot::Start } else { Pivot::End };
|
||||
self.text(height, COLOR_OVERLAY_BLUE, None, y_transform, 3., [height_pivot, Pivot::Middle]);
|
||||
}
|
||||
|
||||
if translation.x.abs() > 1e-3 && translation.y.abs() > 1e-3 {
|
||||
self.line(quad.top_right(), quad.bottom_right(), None, None);
|
||||
self.line(quad.bottom_left(), quad.bottom_right(), None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Pivot {
|
||||
Start,
|
||||
Middle,
|
||||
End,
|
||||
}
|
||||
|
||||
pub enum DrawHandles {
|
||||
All,
|
||||
SelectedAnchors(Vec<SegmentId>),
|
||||
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
|
||||
None,
|
||||
}
|
||||
@@ -3375,7 +3375,7 @@ impl NodeNetworkInterface {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(&node, &[]) && !self.is_layer(&node, &[]))
|
||||
.filter(|node| self.is_layer(&node, &[]) && !self.is_locked(&node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
@@ -186,13 +186,18 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
aliases: &["graphene_core::ops::PercentageValueNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::math_nodes::coordinate_value::IDENTIFIER,
|
||||
node: graphene_std::math_nodes::vec_2_value::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_core::ops::CoordinateValueNode",
|
||||
"graphene_core::ops::ConstructVector2",
|
||||
"graphene_core::ops::Vector2ValueNode",
|
||||
"graphene_core::ops::CoordinateValueNode",
|
||||
"graphene_math_nodes::CoordinateValueNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::vector::vec_2_to_point::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::PositionToPointNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::math_nodes::color_value::IDENTIFIER,
|
||||
aliases: &["graphene_core::ops::ColorValueNode"],
|
||||
|
||||
@@ -107,15 +107,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
let compatible_type = first_layer.and_then(|layer| {
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
|
||||
graph_layer.horizontal_layer_flow().nth(1).map(|node_id| {
|
||||
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
|
||||
Some(format!("type:{}", output_type.nested_type()))
|
||||
format!("type:{}", output_type.nested_type())
|
||||
})
|
||||
});
|
||||
|
||||
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
|
||||
|
||||
let is_modifiable = first_layer.map_or(false, |layer| {
|
||||
let is_modifiable = first_layer.is_some_and(|layer| {
|
||||
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
|
||||
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
|
||||
});
|
||||
@@ -363,13 +363,15 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
|
||||
for document_id in self.document_ids.iter() {
|
||||
let inspect_node = self.inspect_node_id();
|
||||
let _ = self.executor.submit_node_graph_evaluation(
|
||||
if let Ok(message) = 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,
|
||||
);
|
||||
) {
|
||||
responses.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
if self.active_document_mut().is_some() {
|
||||
@@ -568,8 +570,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(PortfolioMessage::CenterPastedLayers { layers });
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,13 +704,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
if create_document {
|
||||
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
|
||||
|
||||
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
|
||||
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
||||
});
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PasteSvg {
|
||||
@@ -733,13 +735,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
if create_document {
|
||||
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
|
||||
});
|
||||
|
||||
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
|
||||
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
|
||||
responses.add(DeferMessage::AfterNavigationReady {
|
||||
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
|
||||
});
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PrevDocument => {
|
||||
@@ -846,11 +848,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
ignore_hash,
|
||||
);
|
||||
|
||||
if let Err(description) = result {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "Unable to update node graph".to_string(),
|
||||
description,
|
||||
});
|
||||
match result {
|
||||
Err(description) => {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "Unable to update node graph".to_string(),
|
||||
description,
|
||||
});
|
||||
}
|
||||
Ok(message) => responses.add(message),
|
||||
}
|
||||
}
|
||||
PortfolioMessage::ToggleRulers => {
|
||||
@@ -883,6 +888,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents });
|
||||
}
|
||||
PortfolioMessage::UpdateVelloPreference => {
|
||||
let active = if cfg!(target_arch = "wasm32") { false } else { preferences.use_vello };
|
||||
responses.add(FrontendMessage::UpdateViewportHolePunch { active });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.persistent_data.use_vello = preferences.use_vello;
|
||||
}
|
||||
@@ -1019,9 +1026,6 @@ impl PortfolioMessageHandler {
|
||||
/text>"#
|
||||
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
|
||||
.to_string();
|
||||
responses.add(Message::EndBuffer {
|
||||
render_metadata: graphene_std::renderer::RenderMetadata::default(),
|
||||
});
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
|
||||
}
|
||||
result
|
||||
|
||||
@@ -53,7 +53,7 @@ impl From<String> for PanelType {
|
||||
"Layers" => PanelType::Layers,
|
||||
"Properties" => PanelType::Properties,
|
||||
"Spreadsheet" => PanelType::Spreadsheet,
|
||||
_ => panic!("Unknown panel type: {}", value),
|
||||
_ => panic!("Unknown panel type: {value}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, Message
|
||||
pub use crate::utility_types::{DebugMessageTree, MessageData};
|
||||
// Message, MessageData, MessageDiscriminant, MessageHandler
|
||||
pub use crate::messages::animation::{AnimationMessage, AnimationMessageDiscriminant, AnimationMessageHandler};
|
||||
pub use crate::messages::app_window::{AppWindowMessage, AppWindowMessageDiscriminant, AppWindowMessageHandler};
|
||||
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
|
||||
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
|
||||
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
|
||||
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};
|
||||
pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler};
|
||||
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
|
||||
@@ -65,10 +67,12 @@ pub trait Responses {
|
||||
}
|
||||
|
||||
impl Responses for VecDeque<Message> {
|
||||
#[inline(always)]
|
||||
fn add(&mut self, message: impl Into<Message>) {
|
||||
self.push_back(message.into());
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn add_front(&mut self, message: impl Into<Message>) {
|
||||
self.push_front(message.into());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -123,6 +124,15 @@ impl ShapeGizmoHandlers {
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gizmo_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
match self {
|
||||
Self::Star(h) => h.mouse_cursor_icon(),
|
||||
Self::Polygon(h) => h.mouse_cursor_icon(),
|
||||
Self::Arc(h) => h.mouse_cursor_icon(),
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
|
||||
@@ -256,4 +266,12 @@ impl GizmoManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the cursor icon to display when hovering or dragging a gizmo.
|
||||
///
|
||||
/// If a gizmo is active (hovered or being manipulated), it returns the cursor icon associated with that gizmo;
|
||||
/// otherwise, returns `None` to indicate the default crosshair cursor should be used.
|
||||
pub fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
self.active_shape_handler.as_ref().and_then(|h| h.gizmo_cursor_icon())
|
||||
}
|
||||
}
|
||||
|
||||
+17
-13
@@ -1,10 +1,9 @@
|
||||
use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage};
|
||||
use crate::messages::prelude::DocumentMessageHandler;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{arc_end_points, calculate_arc_text_transform, extract_arc_parameters, format_rounded};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
@@ -57,7 +56,7 @@ impl SweepAngleGizmo {
|
||||
self.handle_state == SweepAngleGizmoState::Dragging || self.handle_state == SweepAngleGizmoState::Snapped
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
||||
if self.handle_state == SweepAngleGizmoState::Inactive {
|
||||
let Some((start, end)) = arc_end_points(Some(layer), document) else { return };
|
||||
let Some((_, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
|
||||
@@ -89,8 +88,6 @@ impl SweepAngleGizmo {
|
||||
self.snap_angles = Self::calculate_snap_angles();
|
||||
|
||||
self.update_state(SweepAngleGizmoState::Hover);
|
||||
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +124,11 @@ impl SweepAngleGizmo {
|
||||
|
||||
// Depending on which endpoint is being dragged, draw guides relative to the static point
|
||||
let point = if self.endpoint == EndpointType::End { current_end } else { current_start };
|
||||
|
||||
// Draw the dashed line from center to drag start position
|
||||
overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5));
|
||||
|
||||
// Draw the angle, text and the bold line
|
||||
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
|
||||
}
|
||||
SweepAngleGizmoState::Snapped => {
|
||||
@@ -143,6 +145,9 @@ impl SweepAngleGizmo {
|
||||
// Draw lines from endpoints to the arc center
|
||||
overlay_context.line(start, center, Some(COLOR_OVERLAY_RED), Some(2.));
|
||||
overlay_context.line(end, center, Some(COLOR_OVERLAY_RED), Some(2.));
|
||||
|
||||
// Draw the line from drag start to arc center
|
||||
overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +160,6 @@ impl SweepAngleGizmo {
|
||||
let final_vector = final_point - center;
|
||||
let offset_angle = initial_vector.to_angle() + tilt_offset;
|
||||
|
||||
let dash_radius = initial_point.distance(center);
|
||||
let bold_radius = final_point.distance(center);
|
||||
|
||||
let angle = initial_vector.angle_to(final_vector).to_degrees();
|
||||
@@ -170,7 +174,7 @@ impl SweepAngleGizmo {
|
||||
|
||||
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
|
||||
|
||||
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, dash_radius, center, &text, transform);
|
||||
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, center, &text, transform);
|
||||
}
|
||||
|
||||
pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
@@ -209,12 +213,11 @@ impl SweepAngleGizmo {
|
||||
let wrapped = new_sweep_angle % 360.;
|
||||
self.total_angle_delta = -wrapped;
|
||||
|
||||
// Remaining drag gets passed to the ending endpoint
|
||||
let rest_angle = angle_delta + wrapped;
|
||||
self.endpoint = EndpointType::End;
|
||||
|
||||
self.initial_sweep_angle = 360.;
|
||||
self.initial_start_angle = current_start_angle + rest_angle;
|
||||
self.initial_start_angle = current_start_angle;
|
||||
self.update_state(SweepAngleGizmoState::Snapped);
|
||||
|
||||
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle - wrapped, input, responses);
|
||||
}
|
||||
@@ -288,12 +291,13 @@ impl SweepAngleGizmo {
|
||||
}
|
||||
// Clamp sweep angle above 360°, switch to start
|
||||
() if new_sweep_angle > 360. => {
|
||||
let delta = angle_delta - (360. - current_sweep_angle);
|
||||
let delta = angle_delta - (360. - new_sweep_angle);
|
||||
let sign = -delta.signum();
|
||||
|
||||
self.total_angle_delta = angle_delta;
|
||||
self.total_angle_delta = angle_delta - (360. - new_sweep_angle);
|
||||
self.initial_sweep_angle = 360.;
|
||||
self.endpoint = EndpointType::Start;
|
||||
self.update_state(SweepAngleGizmoState::Snapped);
|
||||
|
||||
self.apply_arc_update(node_id, self.initial_start_angle + angle_delta, self.initial_sweep_angle + angle_delta.abs() * sign, input, responses);
|
||||
}
|
||||
@@ -339,7 +343,7 @@ impl SweepAngleGizmo {
|
||||
pub fn calculate_snap_angles() -> Vec<f64> {
|
||||
let mut snap_points = Vec::new();
|
||||
|
||||
for i in 0..8 {
|
||||
for i in 0..=8 {
|
||||
let snap_point = i as f64 * FRAC_PI_4;
|
||||
snap_points.push(snap_point.to_degrees());
|
||||
}
|
||||
|
||||
@@ -153,8 +153,9 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(PenToolMessage::RecalculateLatestPointsPosition);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PenToolMessage::RecalculateLatestPointsPosition.into()],
|
||||
});
|
||||
}
|
||||
|
||||
/// Merge the `first_endpoint` with `second_endpoint`.
|
||||
|
||||
@@ -26,8 +26,8 @@ impl ArcGizmoHandler {
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandler for ArcGizmoHandler {
|
||||
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position, responses);
|
||||
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, _responses: &mut VecDeque<Message>) {
|
||||
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position);
|
||||
}
|
||||
|
||||
fn is_any_gizmo_hovered(&self) -> bool {
|
||||
@@ -74,6 +74,14 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
|
||||
arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context);
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.sweep_angle_gizmo.hovered() || self.sweep_angle_gizmo.is_dragging_or_snapped() {
|
||||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.sweep_angle_gizmo.cleanup();
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Ellipse {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Line {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, _, lock_angle, snap_angle] = modifier;
|
||||
let [center, snap_angle, lock_angle] = modifier;
|
||||
|
||||
shape_tool_data.line_data.drag_current = ipp.mouse.position;
|
||||
|
||||
|
||||
@@ -89,6 +89,18 @@ impl ShapeGizmoHandler for PolygonGizmoHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() {
|
||||
return Some(MouseCursorIcon::EWResize);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() {
|
||||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.number_of_points_dial.cleanup();
|
||||
self.point_radius_handle.cleanup();
|
||||
@@ -112,7 +124,7 @@ impl Polygon {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Rectangle {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::ShapeToolData;
|
||||
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
@@ -74,7 +75,7 @@ impl ShapeType {
|
||||
}
|
||||
}
|
||||
|
||||
pub type ShapeToolModifierKey = [Key; 4];
|
||||
pub type ShapeToolModifierKey = [Key; 3];
|
||||
|
||||
/// The `ShapeGizmoHandler` trait defines the interactive behavior and overlay logic for shape-specific tools in the editor.
|
||||
/// A gizmo is a visual handle or control point used to manipulate a shape's properties (e.g., number of sides, radius, angle).
|
||||
@@ -127,6 +128,8 @@ pub trait ShapeGizmoHandler {
|
||||
///
|
||||
/// For example, dragging states or hover flags should be cleared to avoid visual glitches when switching tools or shapes.
|
||||
fn cleanup(&mut self);
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon>;
|
||||
}
|
||||
|
||||
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
|
||||
|
||||
@@ -90,6 +90,18 @@ impl ShapeGizmoHandler for StarGizmoHandler {
|
||||
self.number_of_points_dial.cleanup();
|
||||
self.point_radius_handle.cleanup();
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() {
|
||||
return Some(MouseCursorIcon::EWResize);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() {
|
||||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -114,7 +126,7 @@ impl Star {
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
let [center, lock_ratio, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct BrushTool {
|
||||
}
|
||||
|
||||
pub struct BrushOptions {
|
||||
legacy_warning_was_shown: bool,
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
@@ -41,7 +40,6 @@ pub struct BrushOptions {
|
||||
impl Default for BrushOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
legacy_warning_was_shown: false,
|
||||
diameter: DEFAULT_BRUSH_SIZE,
|
||||
hardness: 0.,
|
||||
flow: 100.,
|
||||
@@ -79,7 +77,6 @@ pub enum BrushToolMessageOptionsUpdate {
|
||||
Hardness(f64),
|
||||
Spacing(f64),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
NoDisplayLegacyWarning,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -224,7 +221,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
|
||||
self.options.color.primary_working_color = primary;
|
||||
self.options.color.secondary_working_color = secondary;
|
||||
}
|
||||
BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning => self.options.legacy_warning_was_shown = true,
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
@@ -322,20 +318,6 @@ impl Fsm for BrushToolFsmState {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
if !tool_options.legacy_warning_was_shown {
|
||||
responses.add(DialogMessage::DisplayDialogError {
|
||||
title: "Unsupported tool".into(),
|
||||
description: "
|
||||
The current Brush tool is a legacy feature with\n\
|
||||
significant quality and performance limitations.\n\
|
||||
It will be replaced soon by a new implementation.\n\
|
||||
"
|
||||
.trim()
|
||||
.into(),
|
||||
});
|
||||
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::NoDisplayLegacyWarning));
|
||||
}
|
||||
|
||||
let ToolMessage::Brush(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
|
||||
@@ -383,8 +365,9 @@ impl Fsm for BrushToolFsmState {
|
||||
else {
|
||||
new_brush_layer(document, responses);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(BrushToolMessage::DragStart);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![BrushToolMessage::DragStart.into()],
|
||||
});
|
||||
BrushToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,9 +251,12 @@ impl Fsm for FreehandToolFsmState {
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||
responses.add(Message::StartBuffer);
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
let defered_responses = &mut VecDeque::new();
|
||||
tool_options.fill.apply_fill(layer, defered_responses);
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, defered_responses);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: defered_responses.drain(..).collect(),
|
||||
});
|
||||
tool_data.layer = Some(layer);
|
||||
|
||||
FreehandToolFsmState::Drawing
|
||||
|
||||
@@ -2239,7 +2239,9 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.snapping_axis = None;
|
||||
tool_data.sliding_point_info = None;
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
if drag_occurred || extend_selection {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.opposite_handle_position = None;
|
||||
@@ -2292,7 +2294,9 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PathToolMessage::SelectedPointUpdated.into()],
|
||||
});
|
||||
}
|
||||
|
||||
return PathToolFsmState::Ready;
|
||||
|
||||
@@ -1257,10 +1257,10 @@ impl PenToolData {
|
||||
self.prior_segments = None;
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||
|
||||
// This causes the following message to be run only after the next graph evaluation runs and the transforms are updated
|
||||
responses.add(Message::StartBuffer);
|
||||
// It is necessary to defer this until the transform of the layer can be accurately computed (quite hacky)
|
||||
responses.add(PenToolMessage::AddPointLayerPosition { layer, viewport });
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![PenToolMessage::AddPointLayerPosition { layer, viewport }.into()],
|
||||
});
|
||||
}
|
||||
|
||||
/// Perform extension of an existing path
|
||||
@@ -1721,9 +1721,9 @@ impl Fsm for PenToolFsmState {
|
||||
let next_point = tool_data.next_point;
|
||||
let start = latest_point.id;
|
||||
|
||||
if let Some(layer) = layer {
|
||||
let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
|
||||
|
||||
if let Some(layer) = layer
|
||||
&& let Some(mut vector_data) = document.network_interface.compute_modified_vector(layer)
|
||||
{
|
||||
let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| {
|
||||
vector_data.point_domain.position_from_id(id).map_or(false, |pos| {
|
||||
let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
|
||||
|
||||
@@ -335,7 +335,7 @@ pub struct ShapeToolData {
|
||||
current_shape: ShapeType,
|
||||
|
||||
// Gizmos
|
||||
gizmo_manger: GizmoManager,
|
||||
gizmo_manager: GizmoManager,
|
||||
}
|
||||
|
||||
impl ShapeToolData {
|
||||
@@ -351,6 +351,23 @@ impl ShapeToolData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn transform_cage_mouse_icon(&mut self, input: &InputPreprocessorMessageHandler) -> MouseCursorIcon {
|
||||
let dragging_bounds = self
|
||||
.bounding_box_manager
|
||||
.as_mut()
|
||||
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
|
||||
.is_some();
|
||||
|
||||
self.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Crosshair, |bounds| {
|
||||
let cursor_icon = bounds.get_cursor(input, true, dragging_bounds, Some(self.skew_edge));
|
||||
if cursor_icon == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor_icon }
|
||||
})
|
||||
}
|
||||
|
||||
fn shape_tool_modifier_keys() -> [Key; 3] {
|
||||
[Key::Alt, Key::Shift, Key::Control]
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for ShapeToolFsmState {
|
||||
@@ -388,30 +405,34 @@ impl Fsm for ShapeToolFsmState {
|
||||
.indicator_pos()
|
||||
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
|
||||
.unwrap_or(input.mouse.position);
|
||||
let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds);
|
||||
|
||||
if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) {
|
||||
tool_data.gizmo_manger.handle_actions(mouse_position, document, responses);
|
||||
tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
tool_data.gizmo_manager.handle_actions(mouse_position, document, responses);
|
||||
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
}
|
||||
|
||||
if matches!(self, ShapeToolFsmState::ModifyingGizmo) && !input.keyboard.key(Key::Control) {
|
||||
tool_data.gizmo_manger.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
tool_data.gizmo_manager.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or(MouseCursorIcon::Crosshair);
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
|
||||
let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. });
|
||||
let hovering_over_gizmo = tool_data.gizmo_manger.hovering_over_gizmo();
|
||||
let hovering_over_gizmo = tool_data.gizmo_manager.hovering_over_gizmo();
|
||||
|
||||
if !is_resizing_or_rotating && !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
|
||||
if !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
|
||||
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
}
|
||||
|
||||
if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) {
|
||||
transform_cage_overlays(document, tool_data, &mut overlay_context);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: tool_data.cursor });
|
||||
}
|
||||
|
||||
if input.keyboard.key(Key::Control) && matches!(self, ShapeToolFsmState::Ready(_)) {
|
||||
anchor_overlays(document, &mut overlay_context);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
} else if matches!(self, ShapeToolFsmState::Ready(_)) {
|
||||
Line::overlays(document, tool_data, &mut overlay_context);
|
||||
|
||||
@@ -433,7 +454,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
let edges = bounds.check_selected_edges(input.mouse.position);
|
||||
let is_skewing = matches!(self, ShapeToolFsmState::SkewingBounds { .. });
|
||||
let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge));
|
||||
if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating && !hovering_over_gizmo) {
|
||||
if is_skewing || (dragging_bounds && is_near_square && !hovering_over_gizmo) {
|
||||
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
|
||||
}
|
||||
if !is_skewing && dragging_bounds && !hovering_over_gizmo {
|
||||
@@ -442,6 +463,11 @@ impl Fsm for ShapeToolFsmState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or_else(|| tool_data.transform_cage_mouse_icon(input));
|
||||
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
|
||||
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
|
||||
@@ -562,8 +588,16 @@ impl Fsm for ShapeToolFsmState {
|
||||
|
||||
tool_data.line_data.drag_current = mouse_pos;
|
||||
|
||||
if tool_data.gizmo_manger.handle_click() {
|
||||
if tool_data.gizmo_manager.handle_click() && !input.keyboard.key(Key::Accel) {
|
||||
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or(MouseCursorIcon::Crosshair);
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
// Send a PointerMove message to refresh the cursor icon
|
||||
responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys()));
|
||||
|
||||
return ShapeToolFsmState::ModifyingGizmo;
|
||||
}
|
||||
|
||||
@@ -584,17 +618,31 @@ impl Fsm for ShapeToolFsmState {
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, None);
|
||||
|
||||
if !input.keyboard.key(Key::Control) {
|
||||
// Helper function to update cursor and send pointer move message
|
||||
let update_cursor_and_pointer = |tool_data: &mut ShapeToolData, responses: &mut VecDeque<Message>| {
|
||||
let cursor = tool_data.transform_cage_mouse_icon(input);
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
responses.add(ShapeToolMessage::PointerMove(ShapeToolData::shape_tool_modifier_keys()));
|
||||
};
|
||||
|
||||
match (resize, rotate, skew) {
|
||||
(true, false, false) => {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
update_cursor_and_pointer(tool_data, responses);
|
||||
|
||||
return ShapeToolFsmState::ResizingBounds;
|
||||
}
|
||||
(false, true, false) => {
|
||||
tool_data.data.drag_start = mouse_pos;
|
||||
update_cursor_and_pointer(tool_data, responses);
|
||||
|
||||
return ShapeToolFsmState::RotatingBounds;
|
||||
}
|
||||
(false, false, true) => {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
update_cursor_and_pointer(tool_data, responses);
|
||||
|
||||
return ShapeToolFsmState::SkewingBounds { skew: Key::Control };
|
||||
}
|
||||
_ => {}
|
||||
@@ -624,29 +672,33 @@ impl Fsm for ShapeToolFsmState {
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
|
||||
responses.add(Message::StartBuffer);
|
||||
let defered_responses = &mut VecDeque::new();
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => {
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
defered_responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.fill.apply_fill(layer, defered_responses);
|
||||
}
|
||||
ShapeType::Line => {
|
||||
tool_data.line_data.weight = tool_options.line_weight;
|
||||
tool_data.line_data.editing_layer = Some(layer);
|
||||
}
|
||||
}
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
|
||||
tool_data.data.layer = Some(layer);
|
||||
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: defered_responses.drain(..).collect(),
|
||||
});
|
||||
|
||||
ShapeToolFsmState::Drawing(tool_data.current_shape)
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove(modifier)) => {
|
||||
@@ -682,8 +734,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses);
|
||||
tool_data.gizmo_manager.handle_update(tool_data.data.drag_start, document, input, responses);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
@@ -754,7 +805,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
if cursor == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor }
|
||||
});
|
||||
|
||||
if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && !all_selected_layers_line {
|
||||
if tool_data.cursor != cursor {
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
@@ -793,7 +844,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
input.mouse.finish_transaction(tool_data.data.drag_start, responses);
|
||||
tool_data.data.cleanup(responses);
|
||||
|
||||
tool_data.gizmo_manger.handle_cleanup();
|
||||
tool_data.gizmo_manager.handle_cleanup();
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_transforms.clear();
|
||||
@@ -818,12 +869,13 @@ impl Fsm for ShapeToolFsmState {
|
||||
tool_data.data.cleanup(responses);
|
||||
tool_data.line_data.dragging_endpoint = None;
|
||||
|
||||
tool_data.gizmo_manger.handle_cleanup();
|
||||
tool_data.gizmo_manager.handle_cleanup();
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
tool_data.cursor = MouseCursorIcon::Crosshair;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
|
||||
ShapeToolFsmState::Ready(tool_data.current_shape)
|
||||
|
||||
@@ -360,8 +360,6 @@ impl Fsm for SplineToolFsmState {
|
||||
tool_options.stroke.apply_stroke(tool_data.weight, layer, responses);
|
||||
tool_data.current_layer = Some(layer);
|
||||
|
||||
responses.add(Message::StartBuffer);
|
||||
|
||||
SplineToolFsmState::Drawing
|
||||
}
|
||||
(SplineToolFsmState::Drawing, SplineToolMessage::DragStop) => {
|
||||
|
||||
@@ -385,20 +385,25 @@ impl TextToolData {
|
||||
parent: document.new_layer_parent(true),
|
||||
insert_index: 0,
|
||||
});
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() {
|
||||
Fill::Solid(editing_text.color.unwrap().to_gamma_srgb())
|
||||
} else {
|
||||
Fill::None
|
||||
},
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
transform: editing_text.transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: true,
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![
|
||||
GraphOperationMessage::FillSet {
|
||||
layer: self.layer,
|
||||
fill: if editing_text.color.is_some() {
|
||||
Fill::Solid(editing_text.color.unwrap().to_gamma_srgb())
|
||||
} else {
|
||||
Fill::None
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
GraphOperationMessage::TransformSet {
|
||||
layer: self.layer,
|
||||
transform: editing_text.transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: true,
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
});
|
||||
self.editing_text = Some(editing_text);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
use graph_craft::document::value::{RenderOutput, TaggedValue};
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, generate_uuid};
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graphene_std::application_io::TimingInformation;
|
||||
@@ -29,7 +29,6 @@ pub struct ExecutionRequest {
|
||||
render_config: RenderConfig,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ExecutionResponse {
|
||||
execution_id: u64,
|
||||
result: Result<TaggedValue, String>,
|
||||
@@ -46,7 +45,6 @@ pub struct CompilationResponse {
|
||||
node_graph_errors: GraphErrors,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum NodeGraphUpdate {
|
||||
ExecutionResponse(ExecutionResponse),
|
||||
CompilationResponse(CompilationResponse),
|
||||
@@ -56,6 +54,7 @@ pub enum NodeGraphUpdate {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NodeGraphExecutor {
|
||||
runtime_io: NodeRuntimeIO,
|
||||
current_execution_id: u64,
|
||||
futures: HashMap<u64, ExecutionContext>,
|
||||
node_graph_hash: u64,
|
||||
old_inspect_node: Option<NodeId>,
|
||||
@@ -78,13 +77,15 @@ impl NodeGraphExecutor {
|
||||
futures: Default::default(),
|
||||
runtime_io: NodeRuntimeIO::with_channels(request_sender, response_receiver),
|
||||
node_graph_hash: 0,
|
||||
current_execution_id: 0,
|
||||
old_inspect_node: None,
|
||||
};
|
||||
(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();
|
||||
fn queue_execution(&mut self, render_config: RenderConfig) -> u64 {
|
||||
let execution_id = self.current_execution_id;
|
||||
self.current_execution_id += 1;
|
||||
let request = ExecutionRequest { execution_id, render_config };
|
||||
self.runtime_io.send(GraphRuntimeRequest::ExecutionRequest(request)).expect("Failed to send generation request");
|
||||
|
||||
@@ -105,7 +106,7 @@ impl NodeGraphExecutor {
|
||||
#[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();
|
||||
self.node_graph_hash = crate::application::generate_uuid();
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
let instrumented = Instrumented::new(&mut network);
|
||||
|
||||
@@ -132,7 +133,7 @@ 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, time: TimingInformation) -> Result<(), String> {
|
||||
pub(crate) fn submit_current_node_graph_evaluation(&mut self, document: &mut DocumentMessageHandler, viewport_resolution: UVec2, time: TimingInformation) -> Result<Message, String> {
|
||||
let render_config = RenderConfig {
|
||||
viewport: Footprint {
|
||||
transform: document.metadata().document_to_viewport,
|
||||
@@ -154,7 +155,7 @@ impl NodeGraphExecutor {
|
||||
|
||||
self.futures.insert(execution_id, ExecutionContext { export_config: None });
|
||||
|
||||
Ok(())
|
||||
Ok(DeferMessage::SetGraphSubmissionIndex(execution_id).into())
|
||||
}
|
||||
|
||||
/// Evaluates a node graph, computing the entire graph
|
||||
@@ -165,11 +166,9 @@ impl NodeGraphExecutor {
|
||||
time: TimingInformation,
|
||||
inspect_node: Option<NodeId>,
|
||||
ignore_hash: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<Message, String> {
|
||||
self.update_node_graph(document, inspect_node, ignore_hash)?;
|
||||
self.submit_current_node_graph_evaluation(document, viewport_resolution, time)?;
|
||||
|
||||
Ok(())
|
||||
self.submit_current_node_graph_evaluation(document, viewport_resolution, time)
|
||||
}
|
||||
|
||||
/// Evaluates a node graph for export
|
||||
@@ -280,6 +279,7 @@ impl NodeGraphExecutor {
|
||||
} else {
|
||||
self.process_node_graph_output(node_graph_output, transform, responses)?
|
||||
}
|
||||
responses.add(DeferMessage::TriggerGraphRun(execution_id));
|
||||
|
||||
// Update the spreadsheet on the frontend using the value of the inspect result.
|
||||
if self.old_inspect_node.is_some() {
|
||||
@@ -364,6 +364,7 @@ impl NodeGraphExecutor {
|
||||
);
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
}
|
||||
graphene_std::wasm_application_io::RenderOutputType::Texture { .. } => {}
|
||||
_ => {
|
||||
return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
|
||||
}
|
||||
@@ -384,9 +385,22 @@ impl NodeGraphExecutor {
|
||||
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
|
||||
}
|
||||
};
|
||||
responses.add(Message::EndBuffer {
|
||||
render_metadata: render_output_metadata,
|
||||
let graphene_std::renderer::RenderMetadata {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
click_targets,
|
||||
clip_targets,
|
||||
} = render_output_metadata;
|
||||
|
||||
// Run these update state messages immediately
|
||||
responses.add(DocumentMessage::UpdateUpstreamTransforms {
|
||||
upstream_footprints: footprints,
|
||||
local_transforms,
|
||||
first_instance_source_id,
|
||||
});
|
||||
responses.add(DocumentMessage::UpdateClickTargets { click_targets });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
@@ -8,7 +8,7 @@ use graph_craft::proto::GraphErrors;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::Context;
|
||||
use graphene_std::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
|
||||
use graphene_std::instances::Instance;
|
||||
use graphene_std::memo::IORecord;
|
||||
use graphene_std::renderer::{GraphicElementRendered, RenderParams, SvgRender};
|
||||
@@ -16,7 +16,7 @@ use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
|
||||
use graphene_std::text::FontCache;
|
||||
use graphene_std::vector::style::ViewMode;
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
|
||||
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
|
||||
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
|
||||
use interpreted_executor::util::wrap_network_in_scope;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -131,12 +131,12 @@ impl NodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
pub async fn run(&mut self) -> Option<ImageTexture> {
|
||||
if self.editor_api.application_io.is_none() {
|
||||
self.editor_api = WasmEditorApi {
|
||||
#[cfg(not(test))]
|
||||
#[cfg(all(not(test), target_arch = "wasm32"))]
|
||||
application_io: Some(WasmApplicationIo::new().await.into()),
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, not(target_arch = "wasm32")))]
|
||||
application_io: Some(WasmApplicationIo::new_offscreen().await.into()),
|
||||
font_cache: self.editor_api.font_cache.clone(),
|
||||
node_graph_message_sender: Box::new(self.sender.clone()),
|
||||
@@ -213,6 +213,16 @@ impl NodeRuntime {
|
||||
// Resolve the result from the inspection by accessing the monitor node
|
||||
let inspect_result = self.inspect_state.and_then(|state| state.access(&self.executor));
|
||||
|
||||
let texture = if let Ok(TaggedValue::RenderOutput(RenderOutput {
|
||||
data: RenderOutputType::Texture(texture),
|
||||
..
|
||||
})) = &result
|
||||
{
|
||||
// We can early return becaus we know that there is at most one execution request and it will always be handled last
|
||||
Some(texture.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.sender.send_execution_response(ExecutionResponse {
|
||||
execution_id,
|
||||
result,
|
||||
@@ -221,9 +231,11 @@ impl NodeRuntime {
|
||||
vector_modify: self.vector_modify.clone(),
|
||||
inspect_result,
|
||||
});
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
|
||||
@@ -382,18 +394,30 @@ pub async fn introspect_node(path: &[NodeId]) -> Result<Arc<dyn std::any::Any +
|
||||
Err(IntrospectError::RuntimeNotReady)
|
||||
}
|
||||
|
||||
pub async fn run_node_graph() -> bool {
|
||||
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return false };
|
||||
pub async fn run_node_graph() -> (bool, Option<ImageTexture>) {
|
||||
let Some(mut runtime) = NODE_RUNTIME.try_lock() else { return (false, None) };
|
||||
if let Some(ref mut runtime) = runtime.as_mut() {
|
||||
runtime.run().await;
|
||||
return (true, runtime.run().await);
|
||||
}
|
||||
true
|
||||
(false, None)
|
||||
}
|
||||
|
||||
pub async fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> {
|
||||
let mut node_runtime = NODE_RUNTIME.lock();
|
||||
node_runtime.replace(runtime)
|
||||
}
|
||||
pub async fn replace_application_io(application_io: WasmApplicationIo) {
|
||||
let mut node_runtime = NODE_RUNTIME.lock();
|
||||
if let Some(node_runtime) = &mut *node_runtime {
|
||||
node_runtime.editor_api = WasmEditorApi {
|
||||
font_cache: node_runtime.editor_api.font_cache.clone(),
|
||||
application_io: Some(application_io.into()),
|
||||
node_graph_message_sender: Box::new(node_runtime.sender.clone()),
|
||||
editor_preferences: Box::new(node_runtime.editor_preferences.clone()),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
/// Which node is inspected and which monitor node is used (if any) for the current execution
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -403,22 +427,14 @@ struct InspectState {
|
||||
}
|
||||
/// The resulting value from the temporary inspected during execution
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct InspectResult {
|
||||
#[cfg(not(feature = "decouple-execution"))]
|
||||
introspected_data: Option<Arc<dyn std::any::Any + Send + Sync + 'static>>,
|
||||
#[cfg(feature = "decouple-execution")]
|
||||
introspected_data: Option<TaggedValue>,
|
||||
pub inspect_node: NodeId,
|
||||
}
|
||||
|
||||
impl InspectResult {
|
||||
pub fn take_data(&mut self) -> Option<Arc<dyn std::any::Any + Send + Sync + 'static>> {
|
||||
#[cfg(not(feature = "decouple-execution"))]
|
||||
return self.introspected_data.clone();
|
||||
|
||||
#[cfg(feature = "decouple-execution")]
|
||||
return self.introspected_data.take().map(|value| value.to_any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,8 +479,6 @@ impl InspectState {
|
||||
fn access(&self, executor: &DynamicExecutor) -> Option<InspectResult> {
|
||||
let introspected_data = executor.introspect(&[self.monitor_node]).inspect_err(|e| warn!("Failed to introspect monitor node {e}")).ok();
|
||||
// TODO: Consider displaying the error instead of ignoring it
|
||||
#[cfg(feature = "decouple-execution")]
|
||||
let introspected_data = introspected_data.as_ref().and_then(|data| TaggedValue::try_from_std_any_ref(data).ok());
|
||||
|
||||
Some(InspectResult {
|
||||
inspect_node: self.inspect_node,
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
use super::*;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
// Invoke with arguments (default)
|
||||
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
|
||||
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
|
||||
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name="invoke")]
|
||||
async fn invoke_without_arg(cmd: &str) -> JsValue;
|
||||
}
|
||||
|
||||
/// Handles communication with the NodeRuntime, either locally or via Tauri
|
||||
/// Handles communication with the NodeRuntime
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRuntimeIO {
|
||||
// Send to
|
||||
#[cfg(any(not(feature = "tauri"), test))]
|
||||
sender: Sender<GraphRuntimeRequest>,
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
sender: Sender<NodeGraphUpdate>,
|
||||
receiver: Receiver<NodeGraphUpdate>,
|
||||
}
|
||||
|
||||
@@ -31,25 +18,13 @@ impl Default for NodeRuntimeIO {
|
||||
impl NodeRuntimeIO {
|
||||
/// Creates a new NodeRuntimeIO instance
|
||||
pub fn new() -> Self {
|
||||
#[cfg(any(not(feature = "tauri"), test))]
|
||||
{
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
futures::executor::block_on(replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)));
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
let (request_sender, request_receiver) = std::sync::mpsc::channel();
|
||||
futures::executor::block_on(replace_node_runtime(NodeRuntime::new(request_receiver, response_sender)));
|
||||
|
||||
Self {
|
||||
sender: request_sender,
|
||||
receiver: response_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
{
|
||||
let (response_sender, response_receiver) = std::sync::mpsc::channel();
|
||||
Self {
|
||||
sender: response_sender,
|
||||
receiver: response_receiver,
|
||||
}
|
||||
Self {
|
||||
sender: request_sender,
|
||||
receiver: response_receiver,
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
@@ -59,44 +34,11 @@ impl NodeRuntimeIO {
|
||||
|
||||
/// Sends a message to the NodeRuntime
|
||||
pub fn send(&self, message: GraphRuntimeRequest) -> Result<(), String> {
|
||||
#[cfg(any(not(feature = "tauri"), test))]
|
||||
{
|
||||
self.sender.send(message).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
{
|
||||
let serialized = ron::to_string(&message).map_err(|e| e.to_string()).unwrap();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let js_message = create_message_object(&serialized);
|
||||
invoke("runtime_message", js_message).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
self.sender.send(message).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Receives any pending updates from the NodeRuntime
|
||||
pub fn receive(&self) -> impl Iterator<Item = NodeGraphUpdate> + use<'_> {
|
||||
// TODO: This introduces extra latency
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
{
|
||||
let sender = self.sender.clone();
|
||||
// In the Tauri case, responses are handled separately via poll_node_runtime_updates
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let messages = invoke_without_arg("poll_node_graph").await;
|
||||
let vec: Vec<_> = ron::from_str(&messages.as_string().unwrap()).unwrap();
|
||||
for message in vec {
|
||||
sender.send(message).unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
self.receiver.try_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "tauri", not(test)))]
|
||||
pub fn create_message_object(message: &str) -> JsValue {
|
||||
let obj = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&obj, &JsValue::from_str("message"), &JsValue::from_str(message)).unwrap();
|
||||
obj.into()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user