Merge branch 'master' into merge_point

This commit is contained in:
Adesh Gupta
2025-08-01 19:24:34 +05:30
committed by GitHub
115 changed files with 3104 additions and 788 deletions
+17 -43
View File
@@ -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,
}
+7
View File
@@ -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;
);
}
+7
View File
@@ -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;
@@ -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)
+4 -5
View File
@@ -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`.
+2
View File
@@ -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}"),
}
}
}
+4
View File
@@ -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())
}
}
@@ -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);
+28 -14
View File
@@ -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);
+33 -19
View File
@@ -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,
+8 -66
View File
@@ -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()
}