mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 06:28:12 +08:00
Merge branch 'master' into merge_point
This commit is contained in:
@@ -151,8 +151,8 @@ pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||
pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf";
|
||||
|
||||
// DOCUMENT
|
||||
pub const FILE_EXTENSION: &str = "graphite";
|
||||
pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document";
|
||||
pub const FILE_SAVE_SUFFIX: &str = ".graphite";
|
||||
pub const MAX_UNDO_HISTORY_LEN: usize = 100; // TODO: Add this to user preferences
|
||||
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
];
|
||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame)),
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::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.
|
||||
@@ -442,7 +442,7 @@ mod test {
|
||||
assert_eq!(layers_before_copy.len(), 3);
|
||||
assert_eq!(layers_after_copy.len(), 6);
|
||||
|
||||
println!("{:?} {:?}", layers_after_copy, layers_before_copy);
|
||||
println!("{layers_after_copy:?} {layers_before_copy:?}");
|
||||
|
||||
assert_eq!(layers_after_copy[5], shape_id);
|
||||
}
|
||||
@@ -497,7 +497,8 @@ mod test {
|
||||
);
|
||||
|
||||
let responses = editor.editor.handle_message(PortfolioMessage::OpenDocumentFile {
|
||||
document_name: document_name.into(),
|
||||
document_name: Some(document_name.to_string()),
|
||||
document_path: None,
|
||||
document_serialized_content,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use super::animation_message_handler::AnimationTimeMode;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, Animation)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use super::app_window_message_handler::AppWindowPlatform;
|
||||
|
||||
#[impl_message(Message, AppWindow)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AppWindowMessage {
|
||||
AppWindowMinimize,
|
||||
AppWindowMaximize,
|
||||
AppWindowUpdatePlatform { platform: AppWindowPlatform },
|
||||
AppWindowClose,
|
||||
}
|
||||
|
||||
@@ -6,34 +6,34 @@ use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||
pub struct AppWindowMessageHandler {
|
||||
platform: AppWindowPlatform,
|
||||
maximized: bool,
|
||||
viewport_hole_punch_active: bool,
|
||||
minimized: 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,
|
||||
responses.add(FrontendMessage::UpdateWindowState {
|
||||
maximized: self.maximized,
|
||||
minimized: self.minimized,
|
||||
});
|
||||
}
|
||||
AppWindowMessage::AppWindowClose => {
|
||||
self.platform = AppWindowPlatform::Web;
|
||||
AppWindowMessage::AppWindowMinimize => {
|
||||
self.minimized = !self.minimized;
|
||||
responses.add(FrontendMessage::UpdateWindowState {
|
||||
maximized: self.maximized,
|
||||
minimized: self.minimized,
|
||||
});
|
||||
}
|
||||
AppWindowMessage::AppWindowUpdatePlatform { platform } => {
|
||||
self.platform = platform;
|
||||
responses.add(FrontendMessage::UpdatePlatform { platform: self.platform });
|
||||
}
|
||||
AppWindowMessage::AppWindowClose => {
|
||||
responses.add(FrontendMessage::CloseWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ use crate::messages::prelude::*;
|
||||
pub enum BroadcastMessage {
|
||||
// Sub-messages
|
||||
#[child]
|
||||
TriggerEvent(BroadcastEvent),
|
||||
TriggerEvent(EventMessage),
|
||||
|
||||
// Messages
|
||||
SubscribeEvent {
|
||||
on: BroadcastEvent,
|
||||
on: EventMessage,
|
||||
send: Box<Message>,
|
||||
},
|
||||
UnsubscribeEvent {
|
||||
on: BroadcastEvent,
|
||||
message: Box<Message>,
|
||||
on: EventMessage,
|
||||
send: Box<Message>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::messages::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct BroadcastMessageHandler {
|
||||
listeners: HashMap<BroadcastEvent, Vec<Message>>,
|
||||
event: EventMessageHandler,
|
||||
listeners: HashMap<EventMessage, Vec<Message>>,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
@@ -10,19 +11,15 @@ impl MessageHandler<BroadcastMessage, ()> for BroadcastMessageHandler {
|
||||
fn process_message(&mut self, message: BroadcastMessage, responses: &mut VecDeque<Message>, _: ()) {
|
||||
match message {
|
||||
// Sub-messages
|
||||
BroadcastMessage::TriggerEvent(event) => {
|
||||
for message in self.listeners.entry(event).or_default() {
|
||||
responses.add_front(message.clone())
|
||||
}
|
||||
}
|
||||
BroadcastMessage::TriggerEvent(message) => self.event.process_message(message, responses, EventMessageContext { listeners: &mut self.listeners }),
|
||||
|
||||
// Messages
|
||||
BroadcastMessage::SubscribeEvent { on, send } => self.listeners.entry(on).or_default().push(*send),
|
||||
BroadcastMessage::UnsubscribeEvent { on, message } => self.listeners.entry(on).or_default().retain(|msg| *msg != *message),
|
||||
BroadcastMessage::UnsubscribeEvent { on, send } => self.listeners.entry(on).or_default().retain(|msg| *msg != *send),
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(BroadcastEventDiscriminant;)
|
||||
actions!(EventMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
|
||||
#[impl_message(Message, BroadcastMessage, TriggerEvent)]
|
||||
pub enum BroadcastEvent {
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, Hash)]
|
||||
pub enum EventMessage {
|
||||
/// Triggered by requestAnimationFrame in JS
|
||||
AnimationFrame,
|
||||
CanvasTransformed,
|
||||
@@ -0,0 +1,22 @@
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct EventMessageContext<'a> {
|
||||
pub listeners: &'a mut HashMap<EventMessage, Vec<Message>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, ExtractField)]
|
||||
pub struct EventMessageHandler {}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<EventMessage, EventMessageContext<'_>> for EventMessageHandler {
|
||||
fn process_message(&mut self, message: EventMessage, responses: &mut VecDeque<Message>, context: EventMessageContext) {
|
||||
for message in context.listeners.entry(message).or_default() {
|
||||
responses.add_front(message.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!(EventMessageDiscriminant;)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod event_message;
|
||||
mod event_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use event_message::{EventMessage, EventMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use event_message_handler::{EventMessageContext, EventMessageHandler};
|
||||
@@ -1,7 +1,7 @@
|
||||
mod broadcast_message;
|
||||
mod broadcast_message_handler;
|
||||
|
||||
pub mod broadcast_event;
|
||||
pub mod event;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use broadcast_message::{BroadcastMessage, BroadcastMessageDiscriminant};
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, Defer)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DeferMessage {
|
||||
SetGraphSubmissionIndex(u64),
|
||||
TriggerGraphRun(u64, DocumentId),
|
||||
SetGraphSubmissionIndex { execution_id: u64 },
|
||||
TriggerGraphRun { execution_id: u64, document_id: DocumentId },
|
||||
AfterGraphRun { messages: Vec<Message> },
|
||||
TriggerNavigationReady,
|
||||
AfterNavigationReady { messages: Vec<Message> },
|
||||
|
||||
@@ -24,10 +24,10 @@ impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandl
|
||||
DeferMessage::AfterNavigationReady { messages } => {
|
||||
self.after_viewport_resize.extend_from_slice(&messages);
|
||||
}
|
||||
DeferMessage::SetGraphSubmissionIndex(execution_id) => {
|
||||
DeferMessage::SetGraphSubmissionIndex { execution_id } => {
|
||||
self.current_graph_submission_id = execution_id + 1;
|
||||
}
|
||||
DeferMessage::TriggerGraphRun(execution_id, document_id) => {
|
||||
DeferMessage::TriggerGraphRun { execution_id, document_id } => {
|
||||
let after_graph_run = self.after_graph_run.entry(document_id).or_default();
|
||||
if after_graph_run.is_empty() {
|
||||
return;
|
||||
|
||||
@@ -33,6 +33,9 @@ pub enum DialogMessage {
|
||||
RequestLicensesDialogWithLocalizedCommitDate {
|
||||
localized_commit_year: String,
|
||||
},
|
||||
RequestLicensesThirdPartyDialogWithLicenseText {
|
||||
license_text: String,
|
||||
},
|
||||
RequestNewDocumentDialog,
|
||||
RequestPreferencesDialog,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::new_document_dialog::NewDocumentDialogMessageContext;
|
||||
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
|
||||
use crate::messages::dialog::simple_dialogs::LicensesThirdPartyDialog;
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
@@ -103,6 +104,10 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
|
||||
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => {
|
||||
let dialog = LicensesThirdPartyDialog { license_text };
|
||||
dialog.send_dialog_to_frontend(responses);
|
||||
}
|
||||
DialogMessage::RequestNewDocumentDialog => {
|
||||
self.new_document_dialog = NewDocumentDialogMessageHandler {
|
||||
name: portfolio.generate_new_document_name(),
|
||||
|
||||
@@ -4,10 +4,10 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, DialogMessage, ExportDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ExportDialogMessage {
|
||||
FileType(FileType),
|
||||
ScaleFactor(f64),
|
||||
TransparentBackground(bool),
|
||||
ExportBounds(ExportBounds),
|
||||
FileType { file_type: FileType },
|
||||
ScaleFactor { factor: f64 },
|
||||
TransparentBackground { transparent: bool },
|
||||
ExportBounds { bounds: ExportBounds },
|
||||
|
||||
Submit,
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
|
||||
let ExportDialogMessageContext { portfolio } = context;
|
||||
|
||||
match message {
|
||||
ExportDialogMessage::FileType(export_type) => self.file_type = export_type,
|
||||
ExportDialogMessage::ScaleFactor(factor) => self.scale_factor = factor,
|
||||
ExportDialogMessage::TransparentBackground(transparent_background) => self.transparent_background = transparent_background,
|
||||
ExportDialogMessage::ExportBounds(export_area) => self.bounds = export_area,
|
||||
ExportDialogMessage::FileType { file_type } => self.file_type = file_type,
|
||||
ExportDialogMessage::ScaleFactor { factor } => self.scale_factor = factor,
|
||||
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
|
||||
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
|
||||
|
||||
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
|
||||
file_name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
|
||||
file_type: self.file_type,
|
||||
scale_factor: self.scale_factor,
|
||||
bounds: self.bounds,
|
||||
@@ -84,24 +84,28 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
|
||||
.into_iter()
|
||||
.map(|(val, name)| RadioEntryData::new(format!("{val:?}")).label(name).on_update(move |_| ExportDialogMessage::FileType(val).into()))
|
||||
.map(|(file_type, name)| {
|
||||
RadioEntryData::new(format!("{file_type:?}"))
|
||||
.label(name)
|
||||
.on_update(move |_| ExportDialogMessage::FileType { file_type }.into())
|
||||
})
|
||||
.collect();
|
||||
|
||||
let export_type = vec![
|
||||
TextLabel::new("File Type").table_align(true).min_width(100).widget_holder(),
|
||||
TextLabel::new("File Type").table_align(true).min_width("100px").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(),
|
||||
];
|
||||
|
||||
let resolution = vec![
|
||||
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_holder(),
|
||||
TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(self.scale_factor))
|
||||
.unit("")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.disabled(self.file_type == FileType::Svg)
|
||||
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor(number_input.value.unwrap()).into())
|
||||
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor { factor: number_input.value.unwrap() }.into())
|
||||
.min_width(200)
|
||||
.widget_holder(),
|
||||
];
|
||||
@@ -125,10 +129,10 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
.map(|choice| {
|
||||
choice
|
||||
.into_iter()
|
||||
.map(|(val, name, disabled)| {
|
||||
MenuListEntry::new(format!("{val:?}"))
|
||||
.map(|(bounds, name, disabled)| {
|
||||
MenuListEntry::new(format!("{bounds:?}"))
|
||||
.label(name)
|
||||
.on_commit(move |_| ExportDialogMessage::ExportBounds(val).into())
|
||||
.on_commit(move |_| ExportDialogMessage::ExportBounds { bounds }.into())
|
||||
.disabled(disabled)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -140,18 +144,18 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
||||
}
|
||||
|
||||
let export_area = vec![
|
||||
TextLabel::new("Bounds").table_align(true).min_width(100).widget_holder(),
|
||||
TextLabel::new("Bounds").table_align(true).min_width("100px").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let transparent_background = vec![
|
||||
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_holder(),
|
||||
TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CheckboxInput::new(self.transparent_background)
|
||||
.disabled(self.file_type == FileType::Jpg)
|
||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground(value.checked).into())
|
||||
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, DialogMessage, NewDocumentDialog)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum NewDocumentDialogMessage {
|
||||
Name(String),
|
||||
Infinite(bool),
|
||||
DimensionsX(f64),
|
||||
DimensionsY(f64),
|
||||
Name { name: String },
|
||||
Infinite { infinite: bool },
|
||||
DimensionsX { width: f64 },
|
||||
DimensionsY { height: f64 },
|
||||
|
||||
Submit,
|
||||
}
|
||||
|
||||
+11
-11
@@ -20,10 +20,10 @@ pub struct NewDocumentDialogMessageHandler {
|
||||
impl<'a> MessageHandler<NewDocumentDialogMessage, NewDocumentDialogMessageContext<'a>> for NewDocumentDialogMessageHandler {
|
||||
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, context: NewDocumentDialogMessageContext<'a>) {
|
||||
match message {
|
||||
NewDocumentDialogMessage::Name(name) => self.name = name,
|
||||
NewDocumentDialogMessage::Infinite(infinite) => self.infinite = infinite,
|
||||
NewDocumentDialogMessage::DimensionsX(x) => self.dimensions.x = x as u32,
|
||||
NewDocumentDialogMessage::DimensionsY(y) => self.dimensions.y = y as u32,
|
||||
NewDocumentDialogMessage::Name { name } => self.name = name,
|
||||
NewDocumentDialogMessage::Infinite { infinite } => self.infinite = infinite,
|
||||
NewDocumentDialogMessage::DimensionsX { width } => self.dimensions.x = width as u32,
|
||||
NewDocumentDialogMessage::DimensionsY { height } => self.dimensions.y = height as u32,
|
||||
NewDocumentDialogMessage::Submit => {
|
||||
responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() });
|
||||
|
||||
@@ -79,26 +79,26 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler {
|
||||
impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
fn layout(&self) -> Layout {
|
||||
let name = vec![
|
||||
TextLabel::new("Name").table_align(true).min_width(90).widget_holder(),
|
||||
TextLabel::new("Name").table_align(true).min_width("90px").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextInput::new(&self.name)
|
||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name(text_input.value.clone()).into())
|
||||
.on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into())
|
||||
.min_width(204) // Matches the 100px of both NumberInputs below + the 4px of the Unrelated-type separator
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let infinite = vec![
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_holder(),
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width("90px").for_checkbox(checkbox_id).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CheckboxInput::new(self.infinite)
|
||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite(checkbox_input.checked).into())
|
||||
.on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into())
|
||||
.for_label(checkbox_id)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let scale = vec![
|
||||
TextLabel::new("Dimensions").table_align(true).min_width(90).widget_holder(),
|
||||
TextLabel::new("Dimensions").table_align(true).min_width("90px").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(self.dimensions.x as f64))
|
||||
.label("W")
|
||||
@@ -108,7 +108,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
.is_integer(true)
|
||||
.disabled(self.infinite)
|
||||
.min_width(100)
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX(number_input.value.unwrap()).into())
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsX { width: number_input.value.unwrap() }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.dimensions.y as f64))
|
||||
@@ -119,7 +119,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
||||
.is_integer(true)
|
||||
.disabled(self.infinite)
|
||||
.min_width(100)
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY(number_input.value.unwrap()).into())
|
||||
.on_update(|number_input: &NumberInput| NewDocumentDialogMessage::DimensionsY { height: number_input.value.unwrap() }.into())
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::messages::broadcast::broadcast_event::BroadcastEvent;
|
||||
use crate::messages::broadcast::event::EventMessage;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
@@ -27,7 +27,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
|
||||
TextButton::new("Discard")
|
||||
.on_update(move |_| {
|
||||
DialogMessage::CloseDialogAndThen {
|
||||
followups: vec![BroadcastEvent::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
followups: vec![EventMessage::ToolAbort.into(), PortfolioMessage::CloseDocument { document_id }.into()],
|
||||
}
|
||||
.into()
|
||||
})
|
||||
|
||||
@@ -16,22 +16,31 @@ impl DialogLayoutHolder for LicensesDialog {
|
||||
}
|
||||
|
||||
fn layout_column_2(&self) -> Layout {
|
||||
let icons_license_link = "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md";
|
||||
let links = [
|
||||
("GraphiteLogo", "Graphite Logo", "https://graphite.rs/logo/"),
|
||||
("IconsGrid", "Graphite Icons", icons_license_link),
|
||||
("License", "Graphite License", "https://graphite.rs/license/"),
|
||||
("License", "Other Licenses", "/third-party-licenses.txt"),
|
||||
#[allow(clippy::type_complexity)]
|
||||
let button_definitions: &[(&str, &str, fn() -> Message)] = &[
|
||||
("GraphiteLogo", "Graphite Logo", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://graphite.rs/logo/".into(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
("IconsGrid", "Graphite Icons", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md".into(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
("License", "Graphite License", || {
|
||||
FrontendMessage::TriggerVisitLink {
|
||||
url: "https://graphite.rs/license/".into(),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
("License", "Other Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()),
|
||||
];
|
||||
let widgets = links
|
||||
.into_iter()
|
||||
.map(|(icon, label, url)| {
|
||||
TextButton::new(label)
|
||||
.icon(Some(icon.into()))
|
||||
.flush(true)
|
||||
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
|
||||
.widget_holder()
|
||||
})
|
||||
let widgets = button_definitions
|
||||
.iter()
|
||||
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_holder())
|
||||
.collect();
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }]))
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
pub struct LicensesThirdPartyDialog {
|
||||
pub license_text: String,
|
||||
}
|
||||
|
||||
impl DialogLayoutHolder for LicensesThirdPartyDialog {
|
||||
const ICON: &'static str = "License12px";
|
||||
const TITLE: &'static str = "Third-Party Software License Notices";
|
||||
|
||||
fn layout_buttons(&self) -> Layout {
|
||||
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutHolder for LicensesThirdPartyDialog {
|
||||
fn layout(&self) -> Layout {
|
||||
// Remove the header and begin with the line containing the first license section (we otherwise keep the title for standalone viewing of the licenses text file)
|
||||
let license_text = if let Some(first_underscore_line) = self.license_text.lines().position(|line| line.contains('_')) {
|
||||
// Find the byte position where the line with underscore starts
|
||||
let char_position = self.license_text.split('\n').take(first_underscore_line).map(|line| line.len() + '\n'.len_utf8()).sum();
|
||||
self.license_text[char_position..].to_string()
|
||||
} else {
|
||||
// This shouldn't be encountered, but if no underscore line is found, we use the full text as a safety fallback
|
||||
self.license_text.clone()
|
||||
};
|
||||
|
||||
// Two characters (one before, one after) the sequence of underscore characters, plus one additional column to provide a space between the text and the scrollbar
|
||||
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count()).max().unwrap_or(0) + 2 + 1;
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new(license_text)
|
||||
.monospace(true)
|
||||
.multiline(true)
|
||||
.min_width(format!("{non_wrapping_column_width}ch"))
|
||||
.widget_holder(),
|
||||
],
|
||||
}]))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod coming_soon_dialog;
|
||||
mod demo_artwork_dialog;
|
||||
mod error_dialog;
|
||||
mod licenses_dialog;
|
||||
mod licenses_third_party_dialog;
|
||||
|
||||
pub use about_graphite_dialog::AboutGraphiteDialog;
|
||||
pub use close_all_documents_dialog::CloseAllDocumentsDialog;
|
||||
@@ -14,3 +15,4 @@ pub use demo_artwork_dialog::ARTWORK;
|
||||
pub use demo_artwork_dialog::DemoArtworkDialog;
|
||||
pub use error_dialog::ErrorDialog;
|
||||
pub use licenses_dialog::LicensesDialog;
|
||||
pub use licenses_third_party_dialog::LicensesThirdPartyDialog;
|
||||
|
||||
@@ -64,6 +64,7 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "commitDate")]
|
||||
commit_date: String,
|
||||
},
|
||||
TriggerDisplayThirdPartyLicensesDialog,
|
||||
TriggerSaveDocument {
|
||||
document_id: DocumentId,
|
||||
name: String,
|
||||
@@ -328,16 +329,18 @@ pub enum FrontendMessage {
|
||||
UpdatePlatform {
|
||||
platform: AppWindowPlatform,
|
||||
},
|
||||
UpdateMaximized {
|
||||
UpdateWindowState {
|
||||
maximized: bool,
|
||||
minimized: bool,
|
||||
},
|
||||
CloseWindow,
|
||||
UpdateViewportHolePunch {
|
||||
active: bool,
|
||||
},
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
RenderOverlays(
|
||||
RenderOverlays {
|
||||
#[serde(skip, default = "OverlayContext::default")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayContext,
|
||||
),
|
||||
context: OverlayContext,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, increments_key: Control }),
|
||||
//
|
||||
// SelectToolMessage
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Shift, center: Alt, duplicate: Alt })),
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove { modifier_keys: SelectToolPointerKeys { axis_align: Shift, snap_angle: Shift, center: Alt, duplicate: Alt } }),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control, skew: Control }),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Alt }),
|
||||
entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter),
|
||||
@@ -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])),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove { modifier: [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 }),
|
||||
@@ -223,9 +223,9 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
|
||||
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt, segment_editing_modifier: Control }),
|
||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAll),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllSelected),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=PathToolMessage::DeselectAllSelected),
|
||||
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift, shrink_selection: Alt }),
|
||||
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift, shrink_selection: Alt }),
|
||||
@@ -297,8 +297,8 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=BrushToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=BrushToolMessage::DragStop),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD) }),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD) }),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=BrushToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=BrushToolMessage::Abort),
|
||||
//
|
||||
@@ -340,6 +340,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyA); modifiers=[Alt], action_dispatch=DocumentMessage::DeselectAllLayers),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
||||
entry!(KeyDown(KeyS); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::SaveDocumentAs),
|
||||
entry!(KeyDown(KeyD); modifiers=[Accel], canonical, action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
|
||||
|
||||
@@ -3,13 +3,16 @@ use crate::messages::prelude::*;
|
||||
#[impl_message(Message, KeyMapping)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum KeyMappingMessage {
|
||||
// Sub-messages
|
||||
#[child]
|
||||
Lookup(InputMapperMessage),
|
||||
#[child]
|
||||
ModifyMapping(MappingVariant),
|
||||
|
||||
// Messages
|
||||
ModifyMapping {
|
||||
mapping: MappingVariant,
|
||||
},
|
||||
}
|
||||
|
||||
#[impl_message(Message, KeyMappingMessage, ModifyMapping)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub enum MappingVariant {
|
||||
#[default]
|
||||
|
||||
@@ -19,8 +19,11 @@ impl MessageHandler<KeyMappingMessage, KeyMappingMessageContext<'_>> for KeyMapp
|
||||
let KeyMappingMessageContext { input, actions } = context;
|
||||
|
||||
match message {
|
||||
// Sub-messages
|
||||
KeyMappingMessage::Lookup(input_message) => self.mapping_handler.process_message(input_message, responses, InputMapperMessageContext { input, actions }),
|
||||
KeyMappingMessage::ModifyMapping(new_layout) => self.mapping_handler.set_mapping(new_layout.into()),
|
||||
|
||||
// Messages
|
||||
KeyMappingMessage::ModifyMapping { mapping } => self.mapping_handler.set_mapping(mapping.into()),
|
||||
}
|
||||
}
|
||||
advertise_actions!();
|
||||
|
||||
@@ -2,6 +2,6 @@ mod key_mapping_message;
|
||||
mod key_mapping_message_handler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant, MappingVariantDiscriminant};
|
||||
pub use key_mapping_message::{KeyMappingMessage, KeyMappingMessageDiscriminant, MappingVariant};
|
||||
#[doc(inline)]
|
||||
pub use key_mapping_message_handler::{KeyMappingMessageContext, KeyMappingMessageHandler};
|
||||
|
||||
@@ -479,7 +479,7 @@ impl<const LENGTH: usize> Iterator for BitVectorIter<'_, LENGTH> {
|
||||
impl<const LENGTH: usize> Display for BitVector<LENGTH> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
for storage in self.0.iter().rev() {
|
||||
write!(f, "{:0width$b}", storage, width = STORAGE_SIZE_BITS)?;
|
||||
write!(f, "{storage:0STORAGE_SIZE_BITS$b}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -44,16 +44,18 @@ pub struct TextLabel {
|
||||
|
||||
pub italic: bool,
|
||||
|
||||
pub monospace: bool,
|
||||
|
||||
pub multiline: bool,
|
||||
|
||||
#[serde(rename = "centerAlign")]
|
||||
pub center_align: bool,
|
||||
|
||||
#[serde(rename = "tableAlign")]
|
||||
pub table_align: bool,
|
||||
|
||||
pub multiline: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
pub min_width: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ pub enum Message {
|
||||
Tool(ToolMessage),
|
||||
|
||||
// Messages
|
||||
NoOp,
|
||||
Batched {
|
||||
messages: Box<[Message]>,
|
||||
},
|
||||
NoOp,
|
||||
}
|
||||
|
||||
/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`.
|
||||
@@ -70,13 +70,11 @@ mod test {
|
||||
fn print_tree_node(tree: &DebugMessageTree, prefix: &str, is_last: bool, file: &mut std::fs::File) {
|
||||
// Print the current node
|
||||
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
|
||||
("├── ", format!("{}│ ", prefix))
|
||||
("├── ", format!("{prefix}│ "))
|
||||
} else if is_last {
|
||||
("└── ", format!("{prefix} "))
|
||||
} else {
|
||||
if is_last {
|
||||
("└── ", format!("{} ", prefix))
|
||||
} else {
|
||||
("├── ", format!("{}│ ", prefix))
|
||||
}
|
||||
("├── ", format!("{prefix}│ "))
|
||||
};
|
||||
|
||||
if tree.path().is_empty() {
|
||||
@@ -94,24 +92,38 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
// Print message field if any
|
||||
if let Some(fields) = tree.fields() {
|
||||
let len = fields.len();
|
||||
for (i, field) in fields.iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{child_prefix}{branch}{field}\n").as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Print handler field if any
|
||||
if let Some(data) = tree.message_handler_fields() {
|
||||
let len = data.fields().len();
|
||||
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() {
|
||||
("├── ", format!("{}│ ", prefix))
|
||||
("├── ", format!("{prefix}│ "))
|
||||
} else {
|
||||
("└── ", format!("{} ", prefix))
|
||||
("└── ", format!("{prefix} "))
|
||||
};
|
||||
if data.path().is_empty() {
|
||||
file.write_all(format!("{}{}{}\n", prefix, branch, data.name()).as_bytes()).unwrap();
|
||||
} else {
|
||||
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
|
||||
}
|
||||
for (i, field) in data.fields().iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
|
||||
const FRONTEND_MESSAGE_STR: &str = "FrontendMessage";
|
||||
if data.name().is_empty() && tree.name() != FRONTEND_MESSAGE_STR {
|
||||
panic!("{}'s MessageHandler is missing #[message_handler_data]", tree.name());
|
||||
} else if tree.name() != FRONTEND_MESSAGE_STR {
|
||||
file.write_all(format!("{}{}{} `{}`\n", prefix, branch, data.name(), data.path()).as_bytes()).unwrap();
|
||||
|
||||
for (i, field) in data.fields().iter().enumerate() {
|
||||
let is_last_field = i == len - 1;
|
||||
let branch = if is_last_field { "└── " } else { "├── " };
|
||||
|
||||
file.write_all(format!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::messages::portfolio::document::data_panel::DataPanelMessage;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::{Affine2, Vec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Context;
|
||||
@@ -594,6 +595,19 @@ impl TableRowLayout for DVec2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Vec2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Vec2"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Vec2".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let widgets = vec![TextLabel::new(format!("({}, {})", self.x, self.y)).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for DAffine2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Transform"
|
||||
@@ -607,6 +621,20 @@ impl TableRowLayout for DAffine2 {
|
||||
}
|
||||
}
|
||||
|
||||
impl TableRowLayout for Affine2 {
|
||||
fn type_name() -> &'static str {
|
||||
"Transform"
|
||||
}
|
||||
fn identifier(&self) -> String {
|
||||
"Transform".to_string()
|
||||
}
|
||||
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
|
||||
let widgets = vec![TextLabel::new(format_transform_matrix(&matrix)).widget_holder()];
|
||||
vec![LayoutGroup::Row { widgets }]
|
||||
}
|
||||
}
|
||||
|
||||
fn format_transform_matrix(transform: &DAffine2) -> String {
|
||||
let (scale, angle, translation) = transform.to_scale_angle_translation();
|
||||
let rotation = if angle == -0. { 0. } else { angle.to_degrees() };
|
||||
|
||||
@@ -53,7 +53,9 @@ pub enum DocumentMessage {
|
||||
DocumentHistoryBackward,
|
||||
DocumentHistoryForward,
|
||||
DocumentStructureChanged,
|
||||
DrawArtboardOverlays(OverlayContext),
|
||||
DrawArtboardOverlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
DuplicateSelectedLayers,
|
||||
EnterNestedNetwork {
|
||||
node_id: NodeId,
|
||||
@@ -72,9 +74,15 @@ pub enum DocumentMessage {
|
||||
open: bool,
|
||||
},
|
||||
GraphViewOverlayToggle,
|
||||
GridOptions(GridSnapping),
|
||||
GridOverlays(OverlayContext),
|
||||
GridVisibility(bool),
|
||||
GridOptions {
|
||||
options: GridSnapping,
|
||||
},
|
||||
GridOverlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
GridVisibility {
|
||||
visible: bool,
|
||||
},
|
||||
GroupSelectedLayers {
|
||||
group_folder_type: GroupFolderType,
|
||||
},
|
||||
@@ -110,6 +118,7 @@ pub enum DocumentMessage {
|
||||
RenderRulers,
|
||||
RenderScrollbars,
|
||||
SaveDocument,
|
||||
SaveDocumentAs,
|
||||
SavedDocument {
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BO
|
||||
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
|
||||
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||
use crate::application::{GRAPHITE_GIT_COMMIT_HASH, generate_uuid};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, COLOR_OVERLAY_GRAY, DEFAULT_DOCUMENT_NAME, FILE_EXTENSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ROTATE_SNAP_INTERVAL};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler};
|
||||
@@ -85,8 +85,6 @@ pub struct DocumentMessageHandler {
|
||||
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
|
||||
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
|
||||
pub collapsed: CollapsedLayers,
|
||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||
pub name: String,
|
||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||
pub commit_hash: String,
|
||||
@@ -113,6 +111,12 @@ pub struct DocumentMessageHandler {
|
||||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
//
|
||||
/// The name of the document, which is displayed in the tab and title bar of the editor.
|
||||
#[serde(skip)]
|
||||
pub name: String,
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
/// Path to network currently viewed in the node graph overlay. This will eventually be stored in each panel, so that multiple panels can refer to different networks
|
||||
#[serde(skip)]
|
||||
breadcrumb_network_path: Vec<NodeId>,
|
||||
@@ -125,9 +129,6 @@ pub struct DocumentMessageHandler {
|
||||
/// Stack of document network snapshots for future history states.
|
||||
#[serde(skip)]
|
||||
document_redo_history: VecDeque<NodeNetworkInterface>,
|
||||
/// The path of the to the document file.
|
||||
#[serde(skip)]
|
||||
path: Option<PathBuf>,
|
||||
/// Hash of the document snapshot that was most recently saved to disk by the user.
|
||||
#[serde(skip)]
|
||||
saved_hash: Option<u64>,
|
||||
@@ -159,7 +160,6 @@ impl Default for DocumentMessageHandler {
|
||||
// ============================================
|
||||
network_interface: default_document_network_interface(),
|
||||
collapsed: CollapsedLayers::default(),
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
commit_hash: GRAPHITE_GIT_COMMIT_HASH.to_string(),
|
||||
document_ptz: PTZ::default(),
|
||||
document_mode: DocumentMode::DesignMode,
|
||||
@@ -172,11 +172,12 @@ impl Default for DocumentMessageHandler {
|
||||
// =============================================
|
||||
// Fields omitted from the saved document format
|
||||
// =============================================
|
||||
name: DEFAULT_DOCUMENT_NAME.to_string(),
|
||||
path: None,
|
||||
breadcrumb_network_path: Vec::new(),
|
||||
selection_network_path: Vec::new(),
|
||||
document_undo_history: VecDeque::new(),
|
||||
document_redo_history: VecDeque::new(),
|
||||
path: None,
|
||||
saved_hash: None,
|
||||
auto_saved_hash: None,
|
||||
layer_range_selection_reference: None,
|
||||
@@ -391,7 +392,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
|
||||
}
|
||||
}
|
||||
DocumentMessage::DrawArtboardOverlays(overlay_context) => {
|
||||
DocumentMessage::DrawArtboardOverlays { context: overlay_context } => {
|
||||
if !overlay_context.visibility_settings.artboard_name() {
|
||||
return;
|
||||
}
|
||||
@@ -588,19 +589,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
DocumentMessage::GraphViewOverlayToggle => {
|
||||
responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open });
|
||||
}
|
||||
DocumentMessage::GridOptions(grid) => {
|
||||
self.snapping_state.grid = grid;
|
||||
DocumentMessage::GridOptions { options } => {
|
||||
self.snapping_state.grid = options;
|
||||
self.snapping_state.grid_snapping = true;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
DocumentMessage::GridOverlays(mut overlay_context) => {
|
||||
DocumentMessage::GridOverlays { context: mut overlay_context } => {
|
||||
if self.snapping_state.grid_snapping {
|
||||
grid_overlay(self, &mut overlay_context)
|
||||
}
|
||||
}
|
||||
DocumentMessage::GridVisibility(enabled) => {
|
||||
self.snapping_state.grid_snapping = enabled;
|
||||
DocumentMessage::GridVisibility { visible } => {
|
||||
self.snapping_state.grid_snapping = visible;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
|
||||
@@ -947,7 +948,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::RenameDocument { new_name } => {
|
||||
self.name = new_name;
|
||||
self.name = new_name.clone();
|
||||
|
||||
self.path = None;
|
||||
self.set_save_state(false);
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
@@ -1020,25 +1025,40 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
multiplier: scrollbar_multiplier.into(),
|
||||
});
|
||||
}
|
||||
DocumentMessage::SaveDocument => {
|
||||
DocumentMessage::SaveDocument | DocumentMessage::SaveDocumentAs => {
|
||||
if let DocumentMessage::SaveDocumentAs = message {
|
||||
self.path = None;
|
||||
}
|
||||
|
||||
self.set_save_state(true);
|
||||
responses.add(PortfolioMessage::AutoSaveActiveDocument);
|
||||
// Update the save status of the just saved document
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
|
||||
let name = match self.name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
true => self.name.clone(),
|
||||
false => self.name.clone() + FILE_SAVE_SUFFIX,
|
||||
};
|
||||
responses.add(FrontendMessage::TriggerSaveDocument {
|
||||
document_id,
|
||||
name,
|
||||
name: format!("{}.{}", self.name.clone(), FILE_EXTENSION),
|
||||
path: self.path.clone(),
|
||||
content: self.serialize_document().into_bytes(),
|
||||
})
|
||||
}
|
||||
DocumentMessage::SavedDocument { path } => {
|
||||
self.path = path;
|
||||
|
||||
// Update the name to match the file stem
|
||||
let document_name_from_path = self.path.as_ref().and_then(|path| {
|
||||
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(name) = document_name_from_path {
|
||||
self.name = name;
|
||||
|
||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||
responses.add(NodeGraphMessage::UpdateNewNodeGraph);
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectParentLayer => {
|
||||
let selected_nodes = self.network_interface.selected_nodes();
|
||||
@@ -1062,7 +1082,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
if !parent_layers.is_empty() {
|
||||
let nodes = parent_layers.into_iter().collect();
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
}
|
||||
DocumentMessage::SelectAllLayers => {
|
||||
@@ -1137,7 +1157,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
} else {
|
||||
responses.add_front(NodeGraphMessage::SelectedNodesAdd { nodes: vec![id] });
|
||||
}
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
} else {
|
||||
nodes.push(id);
|
||||
}
|
||||
@@ -1206,7 +1226,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
Some(overlays_type) => overlays_type,
|
||||
None => {
|
||||
visibility_settings.all = visible;
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
return;
|
||||
}
|
||||
@@ -1229,7 +1249,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
OverlaysType::Handles => visibility_settings.handles = visible,
|
||||
}
|
||||
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::SetRangeSelectionLayer { new_layer } => {
|
||||
@@ -1443,12 +1463,14 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
let transform = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
|
||||
self.network_interface.set_document_to_viewport_transform(transform);
|
||||
// Ensure selection box is kept in sync with the pointer when the PTZ changes
|
||||
responses.add(SelectToolMessage::PointerMove(SelectToolPointerKeys {
|
||||
axis_align: Key::Shift,
|
||||
snap_angle: Key::Shift,
|
||||
center: Key::Alt,
|
||||
duplicate: Key::Alt,
|
||||
}));
|
||||
responses.add(SelectToolMessage::PointerMove {
|
||||
modifier_keys: SelectToolPointerKeys {
|
||||
axis_align: Key::Shift,
|
||||
snap_angle: Key::Shift,
|
||||
center: Key::Alt,
|
||||
duplicate: Key::Alt,
|
||||
},
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
} else {
|
||||
let Some(network_metadata) = self.network_interface.network_metadata(&self.breadcrumb_network_path) else {
|
||||
@@ -1477,11 +1499,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
}
|
||||
DocumentMessage::SelectionStepBack => {
|
||||
self.network_interface.selection_step_back(&self.selection_network_path);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
DocumentMessage::SelectionStepForward => {
|
||||
self.network_interface.selection_step_forward(&self.selection_network_path);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
DocumentMessage::WrapContentInArtboard { place_artboard_at_origin } => {
|
||||
// Get bounding box of all layers
|
||||
@@ -1569,6 +1591,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
ZoomCanvasToFitAll,
|
||||
);
|
||||
|
||||
// Additional actions available on desktop
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
common.extend(actions!(DocumentMessageDiscriminant::SaveDocumentAs));
|
||||
|
||||
// Additional actions if there are any selected layers
|
||||
if self.network_interface.selected_nodes().selected_layers(self.metadata()).next().is_some() {
|
||||
let mut select = actions!(DocumentMessageDiscriminant;
|
||||
@@ -1756,7 +1782,8 @@ impl DocumentMessageHandler {
|
||||
|
||||
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
|
||||
let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content)
|
||||
.or_else(|_| {
|
||||
.or_else(|e| {
|
||||
log::warn!("failed to directly load document with the following error: {e}. Trying old DocumentMessageHandler");
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OldDocumentMessageHandler {
|
||||
@@ -2484,7 +2511,7 @@ impl DocumentMessageHandler {
|
||||
.icon("Grid")
|
||||
.tooltip("Grid")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleGridVisibility))
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility(optional_input.checked).into())
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisibility { visible: optional_input.checked }.into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new()
|
||||
.popover_layout(overlay_options(&self.snapping_state.grid))
|
||||
|
||||
@@ -91,6 +91,32 @@ pub fn get_current_normalized_pivot(inputs: &[NodeInput]) -> DVec2 {
|
||||
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
|
||||
}
|
||||
|
||||
/// Expand a bounds to avoid div zero errors
|
||||
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
/// Returns corners of all subpaths
|
||||
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.bounding_box())
|
||||
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
|
||||
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
|
||||
clamp_bounds(bounds_min, bounds_max)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -138,29 +164,3 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand a bounds to avoid div zero errors
|
||||
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
|
||||
let bounds_size = bounds_max - bounds_min;
|
||||
if bounds_size.x < 1e-10 {
|
||||
bounds_max.x = bounds_min.x + 1.;
|
||||
}
|
||||
if bounds_size.y < 1e-10 {
|
||||
bounds_max.y = bounds_min.y + 1.;
|
||||
}
|
||||
[bounds_min, bounds_max]
|
||||
}
|
||||
/// Returns corners of all subpaths
|
||||
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
subpaths
|
||||
.iter()
|
||||
.filter_map(|subpath| subpath.bounding_box())
|
||||
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
|
||||
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
|
||||
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
|
||||
clamp_bounds(bounds_min, bounds_max)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
||||
let transformed_delta = document_to_viewport.inverse().transform_vector2(delta);
|
||||
|
||||
ptz.pan += transformed_delta;
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(EventMessage::CanvasTransformed);
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
}
|
||||
NavigationMessage::CanvasPanAbortPrepare { x_not_y_axis } => {
|
||||
@@ -286,7 +286,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
||||
ptz.flip = !ptz.flip;
|
||||
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(EventMessage::CanvasTransformed);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
@@ -325,7 +325,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageContext<'_>> for Navigat
|
||||
self.navigation_operation = NavigationOperation::None;
|
||||
|
||||
// Send the final messages to close out the operation
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(EventMessage::CanvasTransformed);
|
||||
responses.add(ToolMessage::UpdateCursor);
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NavigateToolMessage::End);
|
||||
|
||||
@@ -46,7 +46,7 @@ impl NodePropertiesContext<'_> {
|
||||
return None;
|
||||
};
|
||||
widget_override_lambda(*node_id, index, self)
|
||||
.map_err(|error| log::error!("Error in widget override lambda: {}", error))
|
||||
.map_err(|error| log::error!("Error in widget override lambda: {error}"))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
@@ -111,7 +111,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
inputs: vec![NodeInput::value(TaggedValue::None, true)],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -151,19 +151,19 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -233,33 +233,33 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::to_graphic::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// Secondary (left) input type coercion
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 1)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::wrap_graphic::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// Store the ID of the parent node (which encapsulates this sub-network) in each row we are extending the table with.
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// The monitor node is used to display a thumbnail in the UI
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(3), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -349,7 +349,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
nodes: [
|
||||
// Ensure this ID is kept in sync with the ID in set_alias so that the name input is kept in sync with the alias
|
||||
DocumentNode {
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(artboard::create_artboard::IDENTIFIER),
|
||||
inputs: vec![
|
||||
NodeInput::network(concrete!(TaggedValue), 1),
|
||||
@@ -365,7 +365,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
// The monitor node is used to display a thumbnail in the UI.
|
||||
@@ -373,12 +373,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![
|
||||
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
|
||||
NodeInput::node(NodeId(2), 0),
|
||||
@@ -495,13 +495,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::scope("editor-api"), NodeInput::network(concrete!(String), 1)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::load_resource::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::decode_image::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -568,7 +568,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -630,20 +630,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::scope("editor-api")],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::create_surface::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0), NodeInput::network(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::rasterize::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -716,7 +716,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
category: "Raster: Pattern",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::std_nodes::noise_pattern::IDENTIFIER),
|
||||
inputs: vec![
|
||||
NodeInput::value(TaggedValue::None, false),
|
||||
@@ -783,7 +783,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Red), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -792,7 +792,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Green), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -801,7 +801,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Blue), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -810,7 +810,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::value(TaggedValue::RedGreenBlueAlpha(RedGreenBlueAlpha::Alpha), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(raster_nodes::adjustments::extract_channel::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -888,13 +888,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(extract_xy::extract_xy::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -962,7 +962,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(Vec<brush::brush_stroke::BrushStroke>), 1),
|
||||
NodeInput::network(concrete!(BrushCache), 2),
|
||||
],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(brush::brush::brush::IDENTIFIER),
|
||||
..Default::default()
|
||||
}]
|
||||
@@ -1013,7 +1013,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
@@ -1032,7 +1032,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
|
||||
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
@@ -1054,13 +1054,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::scope("editor-api")],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::create_gpu_surface::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -1126,12 +1126,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::node(NodeId(0), 0)],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_upload::upload_texture::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
|
||||
..Default::default()
|
||||
@@ -1247,7 +1247,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1257,7 +1257,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(graphene_std::vector::VectorModification), 1),
|
||||
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
|
||||
],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::path_modify::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1317,7 +1317,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(text::text::IDENTIFIER),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
inputs: vec![
|
||||
NodeInput::scope("editor-api"),
|
||||
NodeInput::value(TaggedValue::String("Lorem ipsum".to_string()), false),
|
||||
@@ -1427,7 +1427,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1439,7 +1439,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(DVec2), 3),
|
||||
NodeInput::network(concrete!(DVec2), 4),
|
||||
],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
call_argument: concrete!(Context),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::transform::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1524,25 +1524,25 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(path_bool::boolean_operation::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1622,7 +1622,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::subpath_segment_lengths::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
@@ -1637,25 +1637,25 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::node(NodeId(0), 0),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::sample_polyline::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(3), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1791,26 +1791,26 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(f64), 1),
|
||||
NodeInput::network(concrete!(u32), 2),
|
||||
],
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(vector::poisson_disk_points::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
|
||||
manual_composition: Some(generic!(T)),
|
||||
call_argument: generic!(T),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1944,10 +1944,10 @@ fn static_input_properties() -> InputProperties {
|
||||
"string".to_string(),
|
||||
Box::new(|node_id, index, context| {
|
||||
let Some(value) = context.network_interface.input_data(&node_id, index, "string_properties", context.selection_network_path) else {
|
||||
return Err(format!("Could not get string properties for node {}", node_id));
|
||||
return Err(format!("Could not get string properties for node {node_id}"));
|
||||
};
|
||||
let Some(string) = value.as_str() else {
|
||||
return Err(format!("Could not downcast string properties for node {}", node_id));
|
||||
return Err(format!("Could not downcast string properties for node {node_id}"));
|
||||
};
|
||||
Ok(node_properties::string_properties(string))
|
||||
}),
|
||||
|
||||
+2
-2
@@ -59,8 +59,8 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
inputs,
|
||||
manual_composition: Some(input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
|
||||
call_argument: (input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
..Default::default()
|
||||
|
||||
@@ -129,7 +129,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
NodeGraphMessage::AddPathNode => {
|
||||
if let Some(layer) = make_path_editable_is_allowed(network_interface, network_interface.document_metadata()) {
|
||||
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
}
|
||||
NodeGraphMessage::AddImport => {
|
||||
@@ -142,7 +142,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
NodeGraphMessage::Init => {
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
on: EventMessage::SelectionChanged,
|
||||
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
|
||||
});
|
||||
network_interface.load_structure();
|
||||
@@ -1472,7 +1472,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.add_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesRemove { nodes } => {
|
||||
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
|
||||
@@ -1480,7 +1480,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.retain_selected_nodes(|node| !nodes.contains(node));
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SelectedNodesSet { nodes } => {
|
||||
let Some(selected_nodes) = network_interface.selected_nodes_mut(selection_network_path) else {
|
||||
@@ -1488,7 +1488,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.set_selected_nodes(nodes);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
}
|
||||
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
|
||||
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
|
||||
@@ -1516,7 +1516,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
let mut nodes = Vec::new();
|
||||
for node_id in &self.frontend_nodes {
|
||||
let Some(node_bbox) = network_interface.node_bounding_box(node_id, breadcrumb_network_path) else {
|
||||
log::error!("Could not get bbox for node: {:?}", node_id);
|
||||
log::error!("Could not get bbox for node: {node_id:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -1721,7 +1721,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
}
|
||||
NodeGraphMessage::ToggleLocked { node_id } => {
|
||||
let Some(node_metadata) = network_interface.document_network_metadata().persistent_metadata.node_metadata.get(&node_id) else {
|
||||
log::error!("Cannot get node {:?} in NodeGraphMessage::ToggleLocked", node_id);
|
||||
log::error!("Cannot get node {node_id:?} in NodeGraphMessage::ToggleLocked");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1888,7 +1888,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||
return;
|
||||
};
|
||||
selected_nodes.clear_selected_nodes();
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
|
||||
responses.add(NodeGraphMessage::SendGraph);
|
||||
}
|
||||
@@ -2486,7 +2486,7 @@ impl NodeGraphMessageHandler {
|
||||
data_type: frontend_data_type,
|
||||
name: "Output 1".to_string(),
|
||||
description: String::new(),
|
||||
resolved_type: format!("{:?}", output_type),
|
||||
resolved_type: format!("{output_type:?}"),
|
||||
connected_to,
|
||||
})
|
||||
} else {
|
||||
@@ -2518,7 +2518,7 @@ impl NodeGraphMessageHandler {
|
||||
data_type,
|
||||
name: output_name,
|
||||
description: String::new(),
|
||||
resolved_type: format!("{:?}", output_type),
|
||||
resolved_type: format!("{output_type:?}"),
|
||||
connected_to,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,9 +147,9 @@ pub(crate) fn property_from_type(
|
||||
Type::Concrete(concrete_type) => {
|
||||
match concrete_type.alias.as_ref().map(|x| x.as_ref()) {
|
||||
// Aliased types (ambiguous values)
|
||||
Some("Percentage") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
|
||||
Some("SignedPercentage") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
|
||||
Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
|
||||
Some("Percentage") | Some("PercentageF32") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
|
||||
Some("SignedPercentage") | Some("SignedPercentageF32") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
|
||||
Some("Angle") | Some("AngleF32") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
|
||||
Some("Multiplier") => number_widget(default_info, number_input.unit(unit.unwrap_or("x"))).into(),
|
||||
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
|
||||
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
|
||||
@@ -166,7 +166,7 @@ pub(crate) fn property_from_type(
|
||||
// ===============
|
||||
// PRIMITIVE TYPES
|
||||
// ===============
|
||||
Some(x) if x == TypeId::of::<f64>() => number_widget(default_info, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY))).into(),
|
||||
Some(x) if x == TypeId::of::<f64>() || x == TypeId::of::<f32>() => number_widget(default_info, number_input.min(min(f64::NEG_INFINITY)).max(max(f64::INFINITY))).into(),
|
||||
Some(x) if x == TypeId::of::<u32>() => number_widget(default_info, number_input.int().min(min(0.)).max(max(f64::from(u32::MAX)))).into(),
|
||||
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(),
|
||||
@@ -805,6 +805,14 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props:
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
Some(&TaggedValue::F32(x)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
.value(Some(x as f64))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F32(x.value.unwrap() as f32), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
Some(&TaggedValue::U32(x)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
@@ -851,6 +859,15 @@ pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props:
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
Some(&TaggedValue::FVec2(vec2)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
// We use an arbitrary `y` instead of an arbitrary `x` here because the "Grid" node's "Spacing" value's height should be used from rectangular mode when transferred to "Y Spacing" in isometric mode
|
||||
.value(Some(vec2.y as f64))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F32(x.value.unwrap() as f32), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ impl FrontendGraphDataType {
|
||||
match TaggedValue::from_type_or_none(input) {
|
||||
TaggedValue::U32(_)
|
||||
| TaggedValue::U64(_)
|
||||
| TaggedValue::F32(_)
|
||||
| TaggedValue::F64(_)
|
||||
| TaggedValue::DVec2(_)
|
||||
| TaggedValue::F64Array4(_)
|
||||
|
||||
@@ -200,7 +200,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||
move |input: &I| {
|
||||
let mut grid = grid.clone();
|
||||
update(&mut grid, input);
|
||||
DocumentMessage::GridOptions(grid).into()
|
||||
DocumentMessage::GridOptions { options: grid }.into()
|
||||
}
|
||||
}
|
||||
let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| {
|
||||
|
||||
@@ -7,14 +7,14 @@ use crate::messages::prelude::*;
|
||||
pub enum OverlaysMessage {
|
||||
Draw,
|
||||
// Serde functionality isn't used but is required by the message system macros
|
||||
AddProvider(
|
||||
AddProvider {
|
||||
#[serde(skip, default = "empty_provider")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayProvider,
|
||||
),
|
||||
RemoveProvider(
|
||||
provider: OverlayProvider,
|
||||
},
|
||||
RemoveProvider {
|
||||
#[serde(skip, default = "empty_provider")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayProvider,
|
||||
),
|
||||
provider: OverlayProvider,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,12 +56,14 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||
let _ = canvas_context.reset_transform();
|
||||
|
||||
if visibility_settings.all() {
|
||||
responses.add(DocumentMessage::GridOverlays(OverlayContext {
|
||||
render_context: canvas_context.clone(),
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings: visibility_settings.clone(),
|
||||
}));
|
||||
responses.add(DocumentMessage::GridOverlays {
|
||||
context: OverlayContext {
|
||||
render_context: canvas_context.clone(),
|
||||
size: size.as_dvec2(),
|
||||
device_pixel_ratio,
|
||||
visibility_settings: visibility_settings.clone(),
|
||||
},
|
||||
});
|
||||
for provider in &self.overlay_providers {
|
||||
responses.add(provider(OverlayContext {
|
||||
render_context: canvas_context.clone(),
|
||||
@@ -81,22 +83,22 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
|
||||
let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
|
||||
|
||||
if visibility_settings.all() {
|
||||
responses.add(DocumentMessage::GridOverlays(overlay_context.clone()));
|
||||
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
|
||||
|
||||
for provider in &self.overlay_providers {
|
||||
responses.add(provider(overlay_context.clone()));
|
||||
}
|
||||
}
|
||||
responses.add(FrontendMessage::RenderOverlays(overlay_context));
|
||||
responses.add(FrontendMessage::RenderOverlays { context: overlay_context });
|
||||
}
|
||||
#[cfg(all(not(target_family = "wasm"), test))]
|
||||
OverlaysMessage::Draw => {
|
||||
let _ = (responses, visibility_settings, ipp, device_pixel_ratio);
|
||||
}
|
||||
OverlaysMessage::AddProvider(message) => {
|
||||
OverlaysMessage::AddProvider { provider: message } => {
|
||||
self.overlay_providers.insert(message);
|
||||
}
|
||||
OverlaysMessage::RemoveProvider(message) => {
|
||||
OverlaysMessage::RemoveProvider { provider: message } => {
|
||||
self.overlay_providers.remove(&message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use super::utility_types::{DrawHandles, OverlayContext};
|
||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::subpath::{Bezier, BezierHandles};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::{PointId, SegmentId};
|
||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
|
||||
@@ -24,33 +26,40 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
|
||||
create_context().expect("Failed to get canvas context")
|
||||
}
|
||||
|
||||
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> Vec<SegmentId> {
|
||||
let selected_points = shape_editor.selected_points();
|
||||
let selected_anchors = selected_points
|
||||
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None })
|
||||
pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) -> HashMap<LayerNodeIdentifier, Vec<SegmentId>> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
for (layer, state) in &shape_editor.selected_shape_state {
|
||||
let Some(vector) = network_interface.compute_modified_vector(*layer) else { continue };
|
||||
let selected_segments = selected_segments_for_layer(&vector, state);
|
||||
|
||||
map.insert(*layer, selected_segments);
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState) -> Vec<SegmentId> {
|
||||
let selected_anchors = state
|
||||
.selected_points()
|
||||
.filter_map(|point| if let ManipulatorPointId::Anchor(p) = point { Some(p) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Collect the segments whose handles are selected
|
||||
let mut selected_segments = shape_editor
|
||||
let mut selected_segments = state
|
||||
.selected_points()
|
||||
.filter_map(|point_id| match point_id {
|
||||
ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(*segment_id),
|
||||
ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(segment_id),
|
||||
ManipulatorPointId::Anchor(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// TODO: Currently if there are two duplicate layers, both of their segments get overlays
|
||||
// Adding segments which are are connected to selected anchors
|
||||
for layer in network_interface.selected_nodes().selected_layers(network_interface.document_metadata()) {
|
||||
let Some(vector) = network_interface.compute_modified_vector(layer) else { continue };
|
||||
|
||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
||||
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
||||
selected_segments.push(segment_id);
|
||||
}
|
||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
||||
if selected_anchors.contains(&start) || selected_anchors.contains(&end) {
|
||||
selected_segments.push(segment_id);
|
||||
}
|
||||
}
|
||||
|
||||
selected_segments
|
||||
}
|
||||
|
||||
@@ -124,22 +133,21 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
overlay_context.outline_vector(&vector, transform);
|
||||
}
|
||||
|
||||
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the selected segments and then add a bold line overlay on them
|
||||
for (segment_id, bezier, _, _) in vector.segment_iter() {
|
||||
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if selected_shape_state.is_segment_selected(segment_id) {
|
||||
overlay_context.outline_select_bezier(bezier, transform);
|
||||
}
|
||||
}
|
||||
|
||||
let selected = shape_editor.selected_shape_state.get(&layer);
|
||||
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
|
||||
let is_selected = |point: ManipulatorPointId| selected_shape_state.is_point_selected(point);
|
||||
|
||||
if display_handles {
|
||||
let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector.adjacent_segment(point_id)).collect();
|
||||
let opposite_handles_data = selected_shape_state.selected_points().filter_map(|point_id| vector.adjacent_segment(&point_id)).collect::<Vec<_>>();
|
||||
|
||||
match draw_handles {
|
||||
DrawHandles::All => {
|
||||
@@ -148,9 +156,11 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
});
|
||||
}
|
||||
DrawHandles::SelectedAnchors(ref selected_segments) => {
|
||||
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
|
||||
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.filter(|(segment_id, ..)| selected_segments.contains(segment_id))
|
||||
.filter(|(segment_id, ..)| focused_segments.contains(segment_id))
|
||||
.for_each(|(segment_id, bezier, _start, _end)| {
|
||||
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
|
||||
});
|
||||
@@ -161,7 +171,9 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
DrawHandles::FrontierHandles(ref segment_endpoints) => {
|
||||
DrawHandles::FrontierHandles(ref segment_endpoints_by_layer) => {
|
||||
let Some(segment_endpoints) = segment_endpoints_by_layer.get(&layer) else { continue };
|
||||
|
||||
vector
|
||||
.segment_bezier_iter()
|
||||
.filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id))
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::consts::{
|
||||
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, SEGMENT_SELECTED_THICKNESS,
|
||||
};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::Message;
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
@@ -1024,7 +1025,7 @@ pub enum Pivot {
|
||||
|
||||
pub enum DrawHandles {
|
||||
All,
|
||||
SelectedAnchors(Vec<SegmentId>),
|
||||
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
|
||||
SelectedAnchors(HashMap<LayerNodeIdentifier, Vec<SegmentId>>),
|
||||
FrontierHandles(HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::consts::{
|
||||
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::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::Message;
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
@@ -400,8 +401,8 @@ pub enum Pivot {
|
||||
|
||||
pub enum DrawHandles {
|
||||
All,
|
||||
SelectedAnchors(Vec<SegmentId>),
|
||||
FrontierHandles(HashMap<SegmentId, Vec<PointId>>),
|
||||
SelectedAnchors(HashMap<LayerNodeIdentifier, Vec<SegmentId>>),
|
||||
FrontierHandles(HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>),
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -1021,6 +1022,8 @@ impl OverlayContextInternal {
|
||||
};
|
||||
|
||||
// Load Source Sans Pro font data
|
||||
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
||||
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||
let font_blob = Some(load_font(FONT_DATA));
|
||||
|
||||
@@ -1046,6 +1049,8 @@ impl OverlayContextInternal {
|
||||
};
|
||||
|
||||
// Load Source Sans Pro font data
|
||||
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
|
||||
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
|
||||
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
|
||||
let font_blob = Some(load_font(FONT_DATA));
|
||||
|
||||
@@ -1152,7 +1157,7 @@ impl OverlayContextInternal {
|
||||
let move_to = last_point != Some(start_id);
|
||||
last_point = Some(end_id);
|
||||
|
||||
self.bezier_to_path(bezier, row.transform.clone(), move_to, &mut path);
|
||||
self.bezier_to_path(bezier, *row.transform, move_to, &mut path);
|
||||
}
|
||||
|
||||
// Render the path
|
||||
|
||||
@@ -161,6 +161,17 @@ impl DocumentMetadata {
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Get the loose bounding box of the click target of the specified layer in the specified transform space
|
||||
pub fn loose_bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.click_targets(layer)?
|
||||
.iter()
|
||||
.filter_map(|click_target| match click_target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform),
|
||||
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
/// Calculate the corners of the bounding box but with a nonzero size.
|
||||
///
|
||||
/// If the layer bounds are `0` in either axis then they are changed to be `1`.
|
||||
|
||||
@@ -25,6 +25,7 @@ use kurbo::BezPath;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
|
||||
/// All network modifications should be done through this API, so the fields cannot be public. However, all fields within this struct can be public since it it not possible to have a public mutable reference.
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
@@ -73,10 +74,8 @@ impl NodeNetworkInterface {
|
||||
fix_network(network);
|
||||
}
|
||||
if let DocumentNodeImplementation::ProtoNode(protonode) = &node.implementation {
|
||||
if protonode.name.contains("PathModifyNode") {
|
||||
if node.inputs.len() < 3 {
|
||||
node.inputs.push(NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath));
|
||||
}
|
||||
if protonode.name.contains("PathModifyNode") && node.inputs.len() < 3 {
|
||||
node.inputs.push(NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,13 +459,9 @@ impl NodeNetworkInterface {
|
||||
/// If the node is not in the hashmap then a default input is found based on the compiled network, using the node_id passed as a parameter
|
||||
pub fn map_ids(&mut self, mut node_template: NodeTemplate, node_id: &NodeId, new_ids: &HashMap<NodeId, NodeId>, network_path: &[NodeId]) -> NodeTemplate {
|
||||
for (input_index, input) in node_template.document_node.inputs.iter_mut().enumerate() {
|
||||
if let &mut NodeInput::Node { node_id: id, output_index, lambda } = input {
|
||||
if let &mut NodeInput::Node { node_id: id, output_index } = input {
|
||||
if let Some(&new_id) = new_ids.get(&id) {
|
||||
*input = NodeInput::Node {
|
||||
node_id: new_id,
|
||||
output_index,
|
||||
lambda,
|
||||
};
|
||||
*input = NodeInput::Node { node_id: new_id, output_index };
|
||||
} else {
|
||||
// Disconnect node input if it is not connected to another node in new_ids
|
||||
let tagged_value = TaggedValue::from_type_or_none(&self.input_type(&InputConnector::node(*node_id, input_index), network_path).0);
|
||||
@@ -547,12 +542,11 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
}
|
||||
DocumentNodeImplementation::ProtoNode(_) => {
|
||||
// If a node has manual composition, then offset the input index by 1 since the proto node also includes the type of the input passed through manual composition.
|
||||
let manual_composition_offset = if node.manual_composition.is_some() { 1 } else { 0 };
|
||||
// Offset the input index by 1 since the proto node also includes the type of the input passed as a call argument.
|
||||
self.resolved_types
|
||||
.types
|
||||
.get(node_id_path.as_slice())
|
||||
.and_then(|node_types| node_types.inputs.get(input_index + manual_composition_offset).cloned())
|
||||
.and_then(|node_types| node_types.inputs.get(input_index + 1).cloned())
|
||||
.map(|node_types| (node_types, TypeSource::Compiled))
|
||||
}
|
||||
DocumentNodeImplementation::Extract => None,
|
||||
@@ -581,7 +575,7 @@ impl NodeNetworkInterface {
|
||||
return (concrete!(()), TypeSource::Error("could not resolve protonode"));
|
||||
};
|
||||
|
||||
let skip_footprint = if node.manual_composition.is_some() { 1 } else { 0 };
|
||||
let skip_footprint = 1;
|
||||
|
||||
let Some(input_type) = std::iter::once(node_types.call_argument.clone()).chain(node_types.inputs.clone()).nth(input_index + skip_footprint) else {
|
||||
log::error!("Could not get type");
|
||||
@@ -821,7 +815,7 @@ impl NodeNetworkInterface {
|
||||
data_type,
|
||||
name,
|
||||
description,
|
||||
resolved_type: format!("{:?}", input_type),
|
||||
resolved_type: format!("{input_type:?}"),
|
||||
connected_to,
|
||||
},
|
||||
click_target,
|
||||
@@ -1069,7 +1063,7 @@ impl NodeNetworkInterface {
|
||||
|
||||
pub fn reference(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<&Option<String>> {
|
||||
let Some(node_metadata) = self.node_metadata(node_id, network_path) else {
|
||||
log::error!("Could not get reference for node: {:?}", node_id);
|
||||
log::error!("Could not get reference for node: {node_id:?}");
|
||||
return None;
|
||||
};
|
||||
Some(&node_metadata.persistent_metadata.reference)
|
||||
@@ -1287,7 +1281,7 @@ impl NodeNetworkInterface {
|
||||
let artboard = self.document_node(&artboard_node_identifier.to_node(), &[]);
|
||||
let clip_input = artboard.unwrap().inputs.get(5).unwrap();
|
||||
if let NodeInput::Value { tagged_value, .. } = clip_input {
|
||||
if tagged_value.clone().into_inner() == TaggedValue::Bool(true) {
|
||||
if tagged_value.clone().deref() == &TaggedValue::Bool(true) {
|
||||
return Some(Quad::clip(
|
||||
self.document_metadata.bounding_box_document(layer).unwrap_or_default(),
|
||||
self.document_metadata.bounding_box_document(artboard_node_identifier).unwrap_or_default(),
|
||||
@@ -1499,7 +1493,7 @@ impl NodeNetworkInterface {
|
||||
let mut node_metadata = DocumentNodeMetadata::default();
|
||||
|
||||
node.inputs = old_node.inputs;
|
||||
node.manual_composition = old_node.manual_composition;
|
||||
node.call_argument = old_node.manual_composition.unwrap();
|
||||
node.visible = old_node.visible;
|
||||
node.skip_deduplication = old_node.skip_deduplication;
|
||||
node.original_location = old_node.original_location;
|
||||
@@ -2522,7 +2516,7 @@ impl NodeNetworkInterface {
|
||||
InputConnector::Node { node_id, input_index } => {
|
||||
let input_metadata = self.transient_input_metadata(node_id, *input_index, network_path)?;
|
||||
let TransientMetadata::Loaded(wire) = &input_metadata.wire else {
|
||||
log::error!("Could not load wire for input: {:?}", input);
|
||||
log::error!("Could not load wire for input: {input:?}");
|
||||
return None;
|
||||
};
|
||||
wire.clone()
|
||||
@@ -2530,7 +2524,7 @@ impl NodeNetworkInterface {
|
||||
InputConnector::Export(export_index) => {
|
||||
let network_metadata = self.network_metadata(network_path)?;
|
||||
let Some(TransientMetadata::Loaded(wire)) = network_metadata.transient_metadata.wires.get(*export_index) else {
|
||||
log::error!("Could not load wire for input: {:?}", input);
|
||||
log::error!("Could not load wire for input: {input:?}");
|
||||
return None;
|
||||
};
|
||||
wire.clone()
|
||||
@@ -2701,12 +2695,12 @@ impl NodeNetworkInterface {
|
||||
return None;
|
||||
}
|
||||
let Some(input_position) = self.get_input_center(&input, network_path) else {
|
||||
log::error!("Could not get dom rect for wire end in root node: {:?}", input);
|
||||
log::error!("Could not get dom rect for wire end in root node: {input:?}");
|
||||
return None;
|
||||
};
|
||||
let upstream_output = OutputConnector::node(root_node.node_id, root_node.output_index);
|
||||
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
|
||||
log::error!("Could not get dom rect for wire start in root node: {:?}", upstream_output);
|
||||
log::error!("Could not get dom rect for wire start in root node: {upstream_output:?}");
|
||||
return None;
|
||||
};
|
||||
let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0);
|
||||
@@ -2733,7 +2727,7 @@ impl NodeNetworkInterface {
|
||||
/// Returns the vector subpath and a boolean of whether the wire should be thick.
|
||||
pub fn vector_wire_from_input(&mut self, input: &InputConnector, wire_style: GraphWireStyle, network_path: &[NodeId]) -> Option<(BezPath, bool)> {
|
||||
let Some(input_position) = self.get_input_center(input, network_path) else {
|
||||
log::error!("Could not get dom rect for wire end: {:?}", input);
|
||||
log::error!("Could not get dom rect for wire end: {input:?}");
|
||||
return None;
|
||||
};
|
||||
// An upstream output could not be found, so the wire does not exist, but it should still be loaded as as empty vector
|
||||
@@ -2741,7 +2735,7 @@ impl NodeNetworkInterface {
|
||||
return Some((BezPath::new(), false));
|
||||
};
|
||||
let Some(output_position) = self.get_output_center(&upstream_output, network_path) else {
|
||||
log::error!("Could not get dom rect for wire start: {:?}", upstream_output);
|
||||
log::error!("Could not get dom rect for wire start: {upstream_output:?}");
|
||||
return None;
|
||||
};
|
||||
let vertical_end = input.node_id().is_some_and(|node_id| self.is_layer(&node_id, network_path) && input.input_index() == 0);
|
||||
@@ -3357,7 +3351,7 @@ impl NodeNetworkInterface {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(&node, &[]))
|
||||
.filter(|node| self.is_layer(node, &[]))
|
||||
.filter_map(|layer| self.document_metadata.bounding_box_viewport(LayerNodeIdentifier::new(*layer, self)))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
@@ -3366,7 +3360,7 @@ impl NodeNetworkInterface {
|
||||
self.selected_nodes()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|node| self.is_layer(&node, &[]) && !self.is_locked(&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)
|
||||
}
|
||||
@@ -4138,7 +4132,7 @@ impl NodeNetworkInterface {
|
||||
if let DocumentNodeImplementation::Network(network) = &node.implementation {
|
||||
let number_of_exports = network.exports.len();
|
||||
let Some(metadata) = self.node_metadata_mut(node_id, network_path) else {
|
||||
log::error!("Could not get metadata for node: {:?}", node_id);
|
||||
log::error!("Could not get metadata for node: {node_id:?}");
|
||||
return;
|
||||
};
|
||||
metadata.persistent_metadata.output_names.resize(number_of_exports, "".to_string());
|
||||
@@ -4155,7 +4149,7 @@ impl NodeNetworkInterface {
|
||||
}
|
||||
|
||||
/// Keep metadata in sync with the new implementation if this is used by anything other than the upgrade scripts
|
||||
pub fn set_manual_compostion(&mut self, node_id: &NodeId, network_path: &[NodeId], manual_composition: Option<Type>) {
|
||||
pub fn set_call_argument(&mut self, node_id: &NodeId, network_path: &[NodeId], call_argument: Type) {
|
||||
let Some(network) = self.network_mut(network_path) else {
|
||||
log::error!("Could not get nested network in set_implementation");
|
||||
return;
|
||||
@@ -4164,7 +4158,7 @@ impl NodeNetworkInterface {
|
||||
log::error!("Could not get node in set_implementation");
|
||||
return;
|
||||
};
|
||||
node.manual_composition = manual_composition;
|
||||
node.call_argument = call_argument;
|
||||
}
|
||||
|
||||
pub fn set_input(&mut self, input_connector: &InputConnector, new_input: NodeInput, network_path: &[NodeId]) {
|
||||
|
||||
@@ -20,6 +20,7 @@ use std::collections::HashMap;
|
||||
const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
|
||||
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
|
||||
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
|
||||
("\"manual_composition\":null", "\"manual_composition\":{\"Generic\":\"T\"}"),
|
||||
];
|
||||
|
||||
pub struct NodeReplacement<'a> {
|
||||
@@ -551,7 +552,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
let mut default_template = NodeTemplate::default();
|
||||
default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.clone());
|
||||
document.network_interface.replace_implementation(node_id, &network_path, &mut default_template);
|
||||
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
|
||||
document.network_interface.set_call_argument(node_id, &network_path, graph_craft::Type::Generic("T".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,11 +577,11 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
|
||||
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
|
||||
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` as their call argument
|
||||
if node.call_argument == graph_craft::concrete!(()) || node.call_argument == graph_craft::concrete!(graphene_std::transform::Footprint) {
|
||||
document
|
||||
.network_interface
|
||||
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
|
||||
.set_call_argument(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
|
||||
}
|
||||
|
||||
// Only nodes that have not been modified and still refer to a definition can be updated
|
||||
@@ -1083,8 +1084,8 @@ mod tests {
|
||||
*hashmap.entry(node.node.clone()).or_default() += 1;
|
||||
});
|
||||
let duplicates = hashmap.iter().filter(|(_, count)| **count > 1).map(|(node, _)| &node.name).collect::<Vec<_>>();
|
||||
if duplicates.len() > 0 {
|
||||
panic!("Duplicate entries in `NODE_REPLACEMENTS`: {:?}", duplicates);
|
||||
if !duplicates.is_empty() {
|
||||
panic!("Duplicate entries in `NODE_REPLACEMENTS`: {duplicates:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,14 +101,25 @@ impl LayoutHolder for MenuBarMessageHandler {
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
],
|
||||
vec![MenuBarEntry {
|
||||
label: "Save".into(),
|
||||
icon: Some("Save".into()),
|
||||
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
|
||||
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
|
||||
disabled: no_active_document,
|
||||
..MenuBarEntry::default()
|
||||
}],
|
||||
vec![
|
||||
MenuBarEntry {
|
||||
label: "Save".into(),
|
||||
icon: Some("Save".into()),
|
||||
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument),
|
||||
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
|
||||
disabled: no_active_document,
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
MenuBarEntry {
|
||||
label: "Save As…".into(),
|
||||
icon: Some("Save".into()),
|
||||
shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs),
|
||||
action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()),
|
||||
disabled: no_active_document,
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
],
|
||||
vec![
|
||||
MenuBarEntry {
|
||||
label: "Import…".into(),
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::messages::prelude::*;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::text::Font;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[impl_message(Message, Portfolio)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
@@ -66,18 +67,20 @@ pub enum PortfolioMessage {
|
||||
NextDocument,
|
||||
OpenDocument,
|
||||
OpenDocumentFile {
|
||||
document_name: String,
|
||||
document_name: Option<String>,
|
||||
document_path: Option<PathBuf>,
|
||||
document_serialized_content: String,
|
||||
},
|
||||
ToggleResetNodesToDefinitionsOnOpen,
|
||||
OpenDocumentFileWithId {
|
||||
document_id: DocumentId,
|
||||
document_name: String,
|
||||
document_name: Option<String>,
|
||||
document_path: Option<PathBuf>,
|
||||
document_is_auto_saved: bool,
|
||||
document_is_saved: bool,
|
||||
document_serialized_content: String,
|
||||
to_front: bool,
|
||||
},
|
||||
ToggleResetNodesToDefinitionsOnOpen,
|
||||
PasteIntoFolder {
|
||||
clipboard: Clipboard,
|
||||
parent: LayerNodeIdentifier,
|
||||
@@ -115,7 +118,7 @@ pub enum PortfolioMessage {
|
||||
document_id: DocumentId,
|
||||
},
|
||||
SubmitDocumentExport {
|
||||
file_name: String,
|
||||
name: String,
|
||||
file_type: FileType,
|
||||
scale_factor: f64,
|
||||
bounds: ExportBounds,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::document::utility_types::network_interface;
|
||||
use super::utility_types::{PanelType, PersistentData};
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH};
|
||||
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
|
||||
use crate::messages::animation::TimingInformation;
|
||||
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
|
||||
use crate::messages::dialog::simple_dialogs;
|
||||
@@ -204,7 +204,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
}
|
||||
PortfolioMessage::CloseAllDocuments => {
|
||||
if self.active_document_id.is_some() {
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(ToolMessage::DeactivateTools);
|
||||
|
||||
// Clear relevant UI layouts if there are no documents
|
||||
@@ -250,7 +250,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
PortfolioMessage::CloseDocumentWithConfirmation { document_id } => {
|
||||
let target_document = self.documents.get(&document_id).unwrap();
|
||||
if target_document.is_saved() {
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(PortfolioMessage::CloseDocument { document_id });
|
||||
} else {
|
||||
let dialog = simple_dialogs::CloseDocumentDialog {
|
||||
@@ -395,7 +395,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
|
||||
let document_id = DocumentId(generate_uuid());
|
||||
if self.active_document().is_some() {
|
||||
new_responses.add(BroadcastEvent::ToolAbort);
|
||||
new_responses.add(EventMessage::ToolAbort);
|
||||
new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
|
||||
}
|
||||
|
||||
@@ -419,12 +419,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
}
|
||||
PortfolioMessage::OpenDocumentFile {
|
||||
document_name,
|
||||
document_path,
|
||||
document_serialized_content,
|
||||
} => {
|
||||
let document_id = DocumentId(generate_uuid());
|
||||
responses.add(PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name,
|
||||
document_path,
|
||||
document_is_auto_saved: false,
|
||||
document_is_saved: true,
|
||||
document_serialized_content,
|
||||
@@ -439,6 +441,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
PortfolioMessage::OpenDocumentFileWithId {
|
||||
document_id,
|
||||
document_name,
|
||||
document_path,
|
||||
document_is_auto_saved,
|
||||
document_is_saved,
|
||||
document_serialized_content,
|
||||
@@ -450,10 +453,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
|
||||
|
||||
// Deserialize the document
|
||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
|
||||
document.name.clone_from(&document_name);
|
||||
document
|
||||
});
|
||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content);
|
||||
|
||||
// Display an error to the user if the document could not be opened
|
||||
let mut document = match document {
|
||||
@@ -514,6 +514,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
document.set_auto_save_state(document_is_auto_saved);
|
||||
document.set_save_state(document_is_saved);
|
||||
|
||||
let document_name_from_path = document_path.as_ref().and_then(|path| {
|
||||
if path.extension().is_some_and(|e| e == FILE_EXTENSION) {
|
||||
path.file_stem().map(|n| n.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
match (document_name, document_path, document_name_from_path) {
|
||||
(Some(name), _, None) => {
|
||||
document.name = name;
|
||||
}
|
||||
(_, Some(path), Some(name)) => {
|
||||
document.name = name;
|
||||
document.path = Some(path);
|
||||
}
|
||||
(_, _, Some(name)) => {
|
||||
document.name = name;
|
||||
}
|
||||
_ => {
|
||||
document.name = DEFAULT_DOCUMENT_NAME.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Load the document into the portfolio so it opens in the editor
|
||||
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
|
||||
}
|
||||
@@ -874,8 +898,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
responses.add(ToolMessage::InitTools);
|
||||
responses.add(NodeGraphMessage::Init);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DocumentMessage::GraphViewOverlay { open: node_graph_open });
|
||||
@@ -899,7 +923,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
}
|
||||
}
|
||||
PortfolioMessage::SubmitDocumentExport {
|
||||
file_name,
|
||||
name,
|
||||
file_type,
|
||||
scale_factor,
|
||||
bounds,
|
||||
@@ -907,7 +931,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
||||
} => {
|
||||
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
|
||||
let export_config = ExportConfig {
|
||||
file_name,
|
||||
name,
|
||||
file_type,
|
||||
scale_factor,
|
||||
bounds,
|
||||
@@ -1132,7 +1156,7 @@ impl PortfolioMessageHandler {
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
if self.active_document().is_some() {
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
responses.add(ToolMessage::DeactivateTools);
|
||||
} else {
|
||||
// Load the default font upon creating the first document
|
||||
|
||||
@@ -62,7 +62,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
||||
}
|
||||
PreferencesMessage::ResetToDefaults => {
|
||||
refresh_dialog(responses);
|
||||
responses.add(KeyMappingMessage::ModifyMapping(MappingVariant::Default));
|
||||
responses.add(KeyMappingMessage::ModifyMapping { mapping: MappingVariant::Default });
|
||||
|
||||
*self = Self::default()
|
||||
}
|
||||
@@ -80,7 +80,7 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
||||
self.zoom_with_scroll = zoom_with_scroll;
|
||||
|
||||
let variant = if zoom_with_scroll { MappingVariant::ZoomWithScroll } else { MappingVariant::Default };
|
||||
responses.add(KeyMappingMessage::ModifyMapping(variant));
|
||||
responses.add(KeyMappingMessage::ModifyMapping { mapping: variant });
|
||||
}
|
||||
PreferencesMessage::SelectionMode { selection_mode } => {
|
||||
self.selection_mode = selection_mode;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Root
|
||||
pub use crate::utility_traits::{ActionList, AsMessage, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild};
|
||||
// Message-related
|
||||
pub use crate::utility_traits::{ActionList, AsMessage, ExtractField, HierarchicalTree, MessageHandler, ToDiscriminant, TransitiveChild};
|
||||
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::event::{EventMessage, EventMessageContext, EventMessageDiscriminant, EventMessageHandler};
|
||||
pub use crate::messages::broadcast::{BroadcastMessage, BroadcastMessageDiscriminant, BroadcastMessageHandler};
|
||||
pub use crate::messages::debug::{DebugMessage, DebugMessageDiscriminant, DebugMessageHandler};
|
||||
pub use crate::messages::defer::{DeferMessage, DeferMessageDiscriminant, DeferMessageHandler};
|
||||
@@ -31,7 +33,6 @@ pub use crate::messages::tool::transform_layer::{TransformLayerMessage, Transfor
|
||||
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
|
||||
|
||||
// Message, MessageDiscriminant
|
||||
pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
|
||||
pub use crate::messages::message::{Message, MessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant};
|
||||
@@ -47,7 +48,7 @@ pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, Sha
|
||||
pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
|
||||
|
||||
// Helper
|
||||
// Helper/miscellaneous
|
||||
pub use crate::messages::globals::global_variables::*;
|
||||
pub use crate::messages::portfolio::document::utility_types::misc::DocumentId;
|
||||
pub use graphite_proc_macros::*;
|
||||
|
||||
@@ -14,7 +14,7 @@ impl AutoPanning {
|
||||
|
||||
for message in messages {
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::AnimationFrame,
|
||||
on: EventMessage::AnimationFrame,
|
||||
send: Box::new(message.clone()),
|
||||
});
|
||||
}
|
||||
@@ -27,8 +27,8 @@ impl AutoPanning {
|
||||
|
||||
for message in messages {
|
||||
responses.add(BroadcastMessage::UnsubscribeEvent {
|
||||
on: BroadcastEvent::AnimationFrame,
|
||||
message: Box::new(message.clone()),
|
||||
on: EventMessage::AnimationFrame,
|
||||
send: Box::new(message.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,14 @@ pub fn pin_pivot_widget(active: bool, enabled: bool, source: PivotToolSource) ->
|
||||
.tooltip(String::from(if active { "Unpin Custom Pivot" } else { "Pin Custom Pivot" }) + "\n\nUnless pinned, the pivot will return to its prior reference point when a new selection is made.")
|
||||
.disabled(!enabled)
|
||||
.on_update(move |_| match source {
|
||||
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotPinned).into(),
|
||||
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotPinned).into(),
|
||||
PivotToolSource::Select => SelectToolMessage::SelectOptions {
|
||||
options: SelectOptionsUpdate::TogglePivotPinned,
|
||||
}
|
||||
.into(),
|
||||
PivotToolSource::Path => PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::TogglePivotPinned,
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
@@ -41,8 +47,14 @@ pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource)
|
||||
MenuListEntry::new(format!("{gizmo_type:?}")).label(gizmo_type.to_string()).on_commit({
|
||||
let value = source.clone();
|
||||
move |_| match value {
|
||||
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::PivotGizmoType(*gizmo_type)).into(),
|
||||
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::PivotGizmoType(*gizmo_type)).into(),
|
||||
PivotToolSource::Select => SelectToolMessage::SelectOptions {
|
||||
options: SelectOptionsUpdate::PivotGizmoType(*gizmo_type),
|
||||
}
|
||||
.into(),
|
||||
PivotToolSource::Path => PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::PivotGizmoType(*gizmo_type),
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -57,8 +69,14 @@ pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource)
|
||||
Disabled: rotation and scaling occurs about the center of the selection bounds.",
|
||||
)
|
||||
.on_update(move |optional_input: &CheckboxInput| match source {
|
||||
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotGizmoType(optional_input.checked)).into(),
|
||||
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotGizmoType(optional_input.checked)).into(),
|
||||
PivotToolSource::Select => SelectToolMessage::SelectOptions {
|
||||
options: SelectOptionsUpdate::TogglePivotGizmoType(optional_input.checked),
|
||||
}
|
||||
.into(),
|
||||
PivotToolSource::Path => PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::TogglePivotGizmoType(optional_input.checked),
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::graph_modification_utils::merge_layers;
|
||||
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
||||
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
|
||||
use crate::consts::HANDLE_LENGTH_FACTOR;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments_for_layer;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
@@ -421,7 +421,7 @@ impl ShapeState {
|
||||
(point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors)
|
||||
}
|
||||
|
||||
pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, vector_meshes: bool) {
|
||||
// First collect all selected anchor points across all layers
|
||||
let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self
|
||||
.selected_shape_state
|
||||
@@ -449,7 +449,8 @@ impl ShapeState {
|
||||
let Some(vector1) = document.network_interface.compute_modified_vector(layer1) else { return };
|
||||
let Some(vector2) = document.network_interface.compute_modified_vector(layer2) else { return };
|
||||
|
||||
if vector1.all_connected(start_point).count() != 1 || vector2.all_connected(end_point).count() != 1 {
|
||||
// If vector meshes is not selected then only for endpoints, otherwise normally applicable
|
||||
if !vector_meshes && (vector1.all_connected(start_point).count() != 1 || vector2.all_connected(end_point).count() != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -559,7 +560,7 @@ impl ShapeState {
|
||||
select_threshold: f64,
|
||||
extend_selection: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: Option<&HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>>,
|
||||
) -> Option<Option<SelectedPointsInfo>> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
@@ -608,7 +609,7 @@ impl ShapeState {
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: Option<&HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>>,
|
||||
point_editing_mode: bool,
|
||||
) -> Option<(bool, Option<SelectedPointsInfo>)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
@@ -622,15 +623,22 @@ impl ShapeState {
|
||||
}
|
||||
let vector = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector)?;
|
||||
|
||||
let selected_shape_state = self.selected_shape_state.get(&layer)?;
|
||||
// Check if point is visible under current overlay mode or not
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
let selected_segments_for_layer = selected_segments_for_layer(&vector, selected_shape_state);
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
if !is_visible_point(manipulator_point_id, &vector, path_overlay_mode, frontier_handles_info, selected_segments, &selected_points) {
|
||||
let frontier_handles_for_layer = frontier_handles_info.and_then(|frontier_handles| frontier_handles.get(&layer));
|
||||
if !is_visible_point(
|
||||
manipulator_point_id,
|
||||
&vector,
|
||||
path_overlay_mode,
|
||||
frontier_handles_for_layer,
|
||||
&selected_segments_for_layer,
|
||||
&selected_points,
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected_shape_state = self.selected_shape_state.get(&layer)?;
|
||||
let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
|
||||
|
||||
// Offset to snap the selected point to the cursor
|
||||
@@ -733,6 +741,13 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects all segments for the selected layers.
|
||||
pub fn select_all_segments_in_selected_layers(&mut self, document: &DocumentMessageHandler) {
|
||||
for (&layer, state) in self.selected_shape_state.iter_mut() {
|
||||
Self::select_all_segments_in_layer_with_state(document, layer, state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper function that selects all anchors, and deselects all handles, for a layer given its [`LayerNodeIdentifier`] and [`SelectedLayerState`].
|
||||
fn select_all_anchors_in_layer_with_state(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, state: &mut SelectedLayerState) {
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
|
||||
@@ -744,6 +759,15 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper function that selects all segments, for a layer given its [`LayerNodeIdentifier`] and [`SelectedLayerState`].
|
||||
fn select_all_segments_in_layer_with_state(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, state: &mut SelectedLayerState) {
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
|
||||
|
||||
for &segment in vector.segment_domain.ids() {
|
||||
state.select_segment(segment);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deselects all points (anchors and handles) across every selected layer.
|
||||
pub fn deselect_all_points(&mut self) {
|
||||
for state in self.selected_shape_state.values_mut() {
|
||||
@@ -1606,7 +1630,7 @@ impl ShapeState {
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: Option<&HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>>,
|
||||
) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
@@ -1621,10 +1645,11 @@ impl ShapeState {
|
||||
if distance_squared < select_threshold_squared {
|
||||
// Check if point is visible in current PathOverlayMode
|
||||
let vector = network_interface.compute_modified_vector(layer)?;
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
let Some(state) = self.selected_shape_state.get(&layer) else { continue };
|
||||
let selected_segments = selected_segments_for_layer(&vector, state);
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
|
||||
if !is_visible_point(manipulator_point_id, &vector, path_overlay_mode, frontier_handles_info, selected_segments, &selected_points) {
|
||||
let frontier_handles_for_layer = frontier_handles_info.and_then(|handles_info| handles_info.get(&layer));
|
||||
if !is_visible_point(manipulator_point_id, &vector, path_overlay_mode, frontier_handles_for_layer, &selected_segments, &selected_points) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1868,53 +1893,64 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_points_by_manipulator_id(&mut self, points: &Vec<ManipulatorPointId>) {
|
||||
let layers_to_modify: Vec<_> = self.selected_shape_state.keys().cloned().collect();
|
||||
pub fn select_anchor_and_connected_handles(&mut self, network_interface: &NodeNetworkInterface) {
|
||||
let mut non_empty_layers = self.selected_shape_state.iter_mut().filter(|(_, state)| !state.is_empty());
|
||||
|
||||
for layer in layers_to_modify {
|
||||
if let Some(state) = self.selected_shape_state.get_mut(&layer) {
|
||||
for point in points {
|
||||
state.select_point(*point);
|
||||
}
|
||||
let Some((layer, state)) = non_empty_layers.next() else { return };
|
||||
if non_empty_layers.next().is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(vector) = network_interface.compute_modified_vector(*layer) else { return };
|
||||
|
||||
// Get the current point and its connected handles
|
||||
let selected_points = state.selected_points.clone();
|
||||
if let Some(point) = selected_points.iter().next() {
|
||||
if let Some(anchor) = point.get_anchor(&vector) {
|
||||
state.select_point(ManipulatorPointId::Anchor(anchor));
|
||||
}
|
||||
if let Some(handles) = point.get_handle_pair(&vector) {
|
||||
state.select_point(handles[0].to_manipulator_point());
|
||||
state.select_point(handles[1].to_manipulator_point());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)).
|
||||
/// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount.
|
||||
/// This can can be activated by double clicking on an anchor with the Path tool.
|
||||
pub fn flip_smooth_sharp(&self, network_interface: &NodeNetworkInterface, target: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) -> bool {
|
||||
let mut process_layer = |layer| {
|
||||
let vector = network_interface.compute_modified_vector(layer)?;
|
||||
let transform_to_screenspace = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface);
|
||||
|
||||
let mut result = None;
|
||||
let mut closest_distance_squared = tolerance * tolerance;
|
||||
|
||||
// Find the closest anchor point on the current layer
|
||||
for (&id, &anchor) in vector.point_domain.ids().iter().zip(vector.point_domain.positions()) {
|
||||
let screenspace = transform_to_screenspace.transform_point2(anchor);
|
||||
let distance_squared = screenspace.distance_squared(target);
|
||||
|
||||
if distance_squared < closest_distance_squared {
|
||||
closest_distance_squared = distance_squared;
|
||||
result = Some((id, anchor));
|
||||
}
|
||||
pub fn select_points_by_layer_and_id(&mut self, points: &HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>) {
|
||||
for (layer, points) in points {
|
||||
if let Some(state) = self.selected_shape_state.get_mut(layer) {
|
||||
points.iter().for_each(|point| state.select_point(*point));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (id, anchor) = result?;
|
||||
let handles = vector.all_connected(id);
|
||||
let positions = handles
|
||||
.filter_map(|handle| handle.to_manipulator_point().get_position(&vector))
|
||||
.filter(|&handle| anchor.abs_diff_eq(handle, 1e-5))
|
||||
.count();
|
||||
pub fn select_point_by_layer_and_id(&mut self, point: ManipulatorPointId, layer: LayerNodeIdentifier) {
|
||||
if let Some(state) = self.selected_shape_state.get_mut(&layer) {
|
||||
state.select_point(point);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the anchor is connected to linear segments.
|
||||
let one_or_more_segment_linear = vector.connected_linear_segments(id) != 0;
|
||||
/// Converts all selected anchor points' handles between sharp (zero-length handles) and smooth (pulled-apart colinear handle(s)).
|
||||
/// If both handles aren't zero-length, they are set to that. If both are zero-length, they are stretched apart by a reasonable amount.
|
||||
/// This can can be activated by double clicking on an anchor with the Path tool.
|
||||
pub fn flip_smooth_sharp(&self, network_interface: &NodeNetworkInterface, responses: &mut VecDeque<Message>) {
|
||||
let mut process_layer = |layer: LayerNodeIdentifier, selected_points: &HashSet<ManipulatorPointId>| {
|
||||
let vector = network_interface.compute_modified_vector(layer)?;
|
||||
|
||||
// Check by comparing the handle positions to the anchor if this manipulator group is a point
|
||||
for point in self.selected_points() {
|
||||
for point in selected_points {
|
||||
let Some(point_id) = point.as_anchor() else { continue };
|
||||
let anchor = point.get_position(&vector)?;
|
||||
let handles = vector.all_connected(point_id);
|
||||
|
||||
// TODO: Check if this method of finding non-colinear is really required
|
||||
let positions = handles
|
||||
.filter_map(|handle| handle.to_manipulator_point().get_position(&vector))
|
||||
.filter(|&handle| anchor.abs_diff_eq(handle, 1e-5))
|
||||
.count();
|
||||
|
||||
// Check if the anchor is connected to linear segments.
|
||||
let one_or_more_segment_linear = vector.connected_linear_segments(point_id) != 0;
|
||||
|
||||
if positions != 0 || one_or_more_segment_linear {
|
||||
self.convert_manipulator_handles_to_colinear(&vector, point_id, responses, layer);
|
||||
} else {
|
||||
@@ -1958,13 +1994,10 @@ impl ShapeState {
|
||||
Some(true)
|
||||
};
|
||||
|
||||
for &layer in self.selected_shape_state.keys() {
|
||||
if let Some(result) = process_layer(layer) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
self.selected_shape_state.iter().for_each(|(layer, state)| {
|
||||
let selected_points = &state.selected_points;
|
||||
process_layer(*layer, selected_points);
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -1974,7 +2007,7 @@ impl ShapeState {
|
||||
selection_shape: SelectionShape,
|
||||
selection_change: SelectionChange,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: Option<&HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>>,
|
||||
select_segments: bool,
|
||||
select_points: bool,
|
||||
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
|
||||
@@ -2046,14 +2079,13 @@ impl ShapeState {
|
||||
network_interface: &NodeNetworkInterface,
|
||||
selection_shape: SelectionShape,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: Option<&HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>>,
|
||||
select_segments: bool,
|
||||
select_points: bool,
|
||||
// Represents if the box/lasso selection touches or encloses the targets (not to be confused with editing modes).
|
||||
selection_mode: SelectionMode,
|
||||
) -> (HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>>, HashMap<LayerNodeIdentifier, HashSet<SegmentId>>) {
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
|
||||
let mut points_inside: HashMap<LayerNodeIdentifier, HashSet<ManipulatorPointId>> = HashMap::new();
|
||||
let mut segments_inside: HashMap<LayerNodeIdentifier, HashSet<SegmentId>> = HashMap::new();
|
||||
@@ -2137,7 +2169,10 @@ impl ShapeState {
|
||||
};
|
||||
|
||||
if select && select_points {
|
||||
let is_visible_handle = is_visible_point(id, &vector, path_overlay_mode, frontier_handles_info, selected_segments.clone(), &selected_points);
|
||||
let frontier_handles_for_layer = frontier_handles_info.and_then(|frontier_handles| frontier_handles.get(&layer));
|
||||
let state = self.selected_shape_state.get(&layer).expect("Cannot find state for layer");
|
||||
let selected_segments_for_layer = selected_segments_for_layer(&vector, state);
|
||||
let is_visible_handle = is_visible_point(id, &vector, path_overlay_mode, frontier_handles_for_layer, &selected_segments_for_layer, &selected_points);
|
||||
|
||||
if is_visible_handle {
|
||||
points_inside.entry(layer).or_default().insert(id);
|
||||
|
||||
@@ -101,14 +101,9 @@ impl Circle {
|
||||
};
|
||||
|
||||
let dimensions = (start - end).abs();
|
||||
let radius: f64;
|
||||
|
||||
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
|
||||
if dimensions.x > dimensions.y {
|
||||
radius = dimensions.y / 2.;
|
||||
} else {
|
||||
radius = dimensions.x / 2.;
|
||||
}
|
||||
let radius: f64 = if dimensions.x > dimensions.y { dimensions.y / 2. } else { dimensions.x / 2. };
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
|
||||
@@ -210,18 +210,18 @@ mod test_line_tool {
|
||||
async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> {
|
||||
let document = editor.active_document();
|
||||
let network_interface = &document.network_interface;
|
||||
let node_id = network_interface
|
||||
|
||||
network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(network_interface)
|
||||
.filter_map(|layer| {
|
||||
let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?;
|
||||
let node_inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Line")?;
|
||||
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
|
||||
return None;
|
||||
};
|
||||
Some((start, end))
|
||||
})
|
||||
.next();
|
||||
node_id
|
||||
.next()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -245,11 +245,7 @@ mod test_line_tool {
|
||||
editor.new_document().await;
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
angle_radians: (30. as f64).to_radians(),
|
||||
})
|
||||
.await;
|
||||
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 30_f64.to_radians() }).await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
let document = editor.active_document();
|
||||
@@ -261,15 +257,11 @@ mod test_line_tool {
|
||||
|
||||
assert!(
|
||||
(start_input - expected_start).length() < 1.,
|
||||
"Start point should match expected document coordinates. Got {:?}, expected {:?}",
|
||||
start_input,
|
||||
expected_start
|
||||
"Start point should match expected document coordinates. Got {start_input:?}, expected {expected_start:?}"
|
||||
);
|
||||
assert!(
|
||||
(end_input - expected_end).length() < 1.,
|
||||
"End point should match expected document coordinates. Got {:?}, expected {:?}",
|
||||
end_input,
|
||||
expected_end
|
||||
"End point should match expected document coordinates. Got {end_input:?}, expected {expected_end:?}"
|
||||
);
|
||||
} else {
|
||||
panic!("Line was not created successfully with transformed viewport");
|
||||
@@ -282,27 +274,19 @@ mod test_line_tool {
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let line_vec = end_input - start_input;
|
||||
let original_angle = line_vec.angle_to(DVec2::X);
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
|
||||
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
|
||||
match (updated_start, updated_end) {
|
||||
(updated_start, updated_end) => {
|
||||
let updated_line_vec = updated_end - updated_start;
|
||||
let updated_angle = updated_line_vec.angle_to(DVec2::X);
|
||||
print!("{:?}", original_angle);
|
||||
print!("{:?}", updated_angle);
|
||||
assert!(
|
||||
line_vec.normalize().dot(updated_line_vec.normalize()).abs() - 1. < 1e-6,
|
||||
"Line angle should be locked when Ctrl is kept pressed"
|
||||
);
|
||||
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let line_vec = end_input - start_input;
|
||||
let original_angle = line_vec.angle_to(DVec2::X);
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
|
||||
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
|
||||
let updated_line_vec = updated_end - updated_start;
|
||||
let updated_angle = updated_line_vec.angle_to(DVec2::X);
|
||||
print!("{original_angle:?}");
|
||||
print!("{updated_angle:?}");
|
||||
assert!(
|
||||
line_vec.normalize().dot(updated_line_vec.normalize()).abs() - 1. < 1e-6,
|
||||
"Line angle should be locked when Ctrl is kept pressed"
|
||||
);
|
||||
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,14 +297,10 @@ mod test_line_tool {
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let expected_start = DVec2::new(0., 100.);
|
||||
let expected_end = DVec2::new(200., 100.);
|
||||
assert!((start_input - expected_start).length() < 1., "Start point should be near (0, 100)");
|
||||
assert!((end_input - expected_end).length() < 1., "End point should be near (200, 100)");
|
||||
}
|
||||
}
|
||||
let expected_start = DVec2::new(0., 100.);
|
||||
let expected_end = DVec2::new(200., 100.);
|
||||
assert!((start_input - expected_start).length() < 1., "Start point should be near (0, 100)");
|
||||
assert!((end_input - expected_end).length() < 1., "End point should be near (200, 100)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -332,7 +332,8 @@ impl SnapManager {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
// We use a loose bounding box here since these are potential candidates which will be filtered later anyway
|
||||
let Some(bounds) = document.metadata().loose_bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
return;
|
||||
};
|
||||
let layer_bounds = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::*;
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DVec2, FloatExt};
|
||||
use graphene_std::math::math_ext::QuadExt;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::subpath::pathseg_points;
|
||||
@@ -13,7 +13,7 @@ use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_po
|
||||
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
|
||||
use graphene_std::vector::misc::dvec2_to_point;
|
||||
use graphene_std::vector::misc::point_to_dvec2;
|
||||
use kurbo::{Affine, DEFAULT_ACCURACY, Nearest, ParamCurve, ParamCurveNearest, PathSeg};
|
||||
use kurbo::{Affine, ParamCurve, PathSeg};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LayerSnapper {
|
||||
@@ -107,9 +107,11 @@ impl LayerSnapper {
|
||||
if path.document_curve.start().distance_squared(path.document_curve.end()) < tolerance * tolerance * 2. {
|
||||
continue;
|
||||
}
|
||||
let Nearest { distance_sq, t } = path.document_curve.nearest(dvec2_to_point(point.document_point), DEFAULT_ACCURACY);
|
||||
let snapped_point_document = point_to_dvec2(path.document_curve.eval(t));
|
||||
let distance = distance_sq.sqrt();
|
||||
let Some((distance_squared, closest)) = path.approx_nearest_point(point.document_point, 10) else {
|
||||
continue;
|
||||
};
|
||||
let snapped_point_document = point_to_dvec2(closest);
|
||||
let distance = distance_squared.sqrt();
|
||||
|
||||
if distance < tolerance {
|
||||
snap_results.curves.push(SnappedCurve {
|
||||
@@ -322,6 +324,99 @@ struct SnapCandidatePath {
|
||||
bounds: Option<Quad>,
|
||||
}
|
||||
|
||||
impl SnapCandidatePath {
|
||||
/// Calculates the point on the curve which lies closest to `point`.
|
||||
///
|
||||
/// ## Algorithm:
|
||||
/// 1. We first perform a coarse scan of the path segment to find the most promising starting point.
|
||||
/// 2. Afterwards we refine this point by performing a binary search to either side assuming that the segment contains at most one extremal point.
|
||||
/// 3. The smaller of the two resulting distances is returned.
|
||||
///
|
||||
/// ## Visualization:
|
||||
/// ```text
|
||||
/// Query Point (×)
|
||||
/// ×
|
||||
/// /|\
|
||||
/// / | \ distance checks
|
||||
/// / | \
|
||||
/// v v v
|
||||
/// ●---●---●---●---● <- Curve with coarse scan points
|
||||
/// 0 0.25 0.5 0.75 1 (parameter t values)
|
||||
/// ^ ^
|
||||
/// | | |
|
||||
/// min mid max
|
||||
/// Find closest scan point
|
||||
///
|
||||
/// Refine left region using binary search:
|
||||
///
|
||||
/// ●------●------●
|
||||
/// 0.25 0.375 0.5
|
||||
///
|
||||
/// Result: | (=0.4)
|
||||
/// And the right region:
|
||||
///
|
||||
/// ●------●------●
|
||||
/// 0.5 0.625 0.75
|
||||
/// Result: | (=0.5)
|
||||
///
|
||||
/// The t value with minimal dist is thus 0.4
|
||||
/// Return: (dist_closest, point_on_curve)
|
||||
/// ```
|
||||
pub fn approx_nearest_point(&self, point: DVec2, lut_steps: usize) -> Option<(f64, kurbo::Point)> {
|
||||
let point = dvec2_to_point(point);
|
||||
|
||||
let time_values = (0..lut_steps).map(|x| x as f64 / lut_steps as f64);
|
||||
let points = time_values.map(|t| (t, self.document_curve.eval(t)));
|
||||
let points_with_distances = points.map(|(t, p)| (t, p.distance_squared(point), p));
|
||||
let (t, _, _) = points_with_distances.min_by(|(_, a, _), (_, b, _)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||
|
||||
let min_t = (t - (lut_steps as f64).recip()).max(0.);
|
||||
let max_t = (t + (lut_steps as f64).recip()).min(1.);
|
||||
let left = self.refine_nearest_point(point, min_t, t);
|
||||
let right = self.refine_nearest_point(point, t, max_t);
|
||||
|
||||
if left.0 < right.0 { Some(left) } else { Some(right) }
|
||||
}
|
||||
|
||||
/// Refines the nearest point search within a given parameter range using binary search.
|
||||
///
|
||||
/// This method performs iterative refinement by:
|
||||
/// 1. Evaluating the midpoint of the current parameter range
|
||||
/// 2. Comparing distances at the endpoints and midpoint
|
||||
/// 3. Narrowing the search range to the side with the shorter distance
|
||||
/// 4. Continuing until convergence (when the range becomes very small)
|
||||
///
|
||||
/// Returns a tuple of (parameter_t, closest_point) where parameter_t is in the range [min_t, max_t].
|
||||
fn refine_nearest_point(&self, point: kurbo::Point, mut min_t: f64, mut max_t: f64) -> (f64, kurbo::Point) {
|
||||
let mut min_dist = self.document_curve.eval(min_t).distance_squared(point);
|
||||
let mut max_dist = self.document_curve.eval(max_t).distance_squared(point);
|
||||
let mut mid_t = max_t.lerp(min_t, 0.5);
|
||||
let mut mid_point = self.document_curve.eval(mid_t);
|
||||
let mut mid_dist = mid_point.distance_squared(point);
|
||||
|
||||
for _ in 0..10 {
|
||||
if (min_dist - max_dist).abs() < 1e-3 {
|
||||
return (mid_dist, mid_point);
|
||||
}
|
||||
if mid_dist > min_dist && mid_dist > max_dist {
|
||||
return (mid_dist, mid_point);
|
||||
}
|
||||
if max_dist > min_dist {
|
||||
max_t = mid_t;
|
||||
max_dist = mid_dist;
|
||||
} else {
|
||||
min_t = mid_t;
|
||||
min_dist = mid_dist;
|
||||
}
|
||||
mid_t = max_t.lerp(min_t, 0.5);
|
||||
mid_point = self.document_curve.eval(mid_t);
|
||||
mid_dist = mid_point.distance_squared(point);
|
||||
}
|
||||
|
||||
(mid_dist, mid_point)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SnapCandidatePoint {
|
||||
pub document_point: DVec2,
|
||||
|
||||
@@ -171,8 +171,8 @@ pub fn is_visible_point(
|
||||
manipulator_point_id: ManipulatorPointId,
|
||||
vector: &Vector,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
selected_segments: Vec<SegmentId>,
|
||||
frontier_handles_for_layer: Option<&HashMap<SegmentId, Vec<PointId>>>,
|
||||
selected_segments: &[SegmentId],
|
||||
selected_points: &HashSet<ManipulatorPointId>,
|
||||
) -> bool {
|
||||
match manipulator_point_id {
|
||||
@@ -197,7 +197,7 @@ pub fn is_visible_point(
|
||||
warn!("No anchor for selected handle");
|
||||
return false;
|
||||
};
|
||||
let Some(frontier_handles) = frontier_handles_info else {
|
||||
let Some(frontier_handles) = frontier_handles_for_layer else {
|
||||
warn!("No frontier handles info provided");
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use graphene_std::raster::color::Color;
|
||||
|
||||
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |overlay_context| DocumentMessage::DrawArtboardOverlays(overlay_context).into();
|
||||
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays { context }.into();
|
||||
|
||||
#[derive(ExtractField)]
|
||||
pub struct ToolMessageContext<'a> {
|
||||
@@ -75,8 +75,8 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
self.tool_state.tool_data.active_tool_type = ToolType::Shape;
|
||||
}
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
|
||||
responses.add(ShapeToolMessage::SetShape(ShapeType::Polygon));
|
||||
responses.add(ShapeToolMessage::HideShapeTypeWidget(false))
|
||||
responses.add(ShapeToolMessage::SetShape { shape: ShapeType::Polygon });
|
||||
responses.add(ShapeToolMessage::HideShapeTypeWidget { hide: false })
|
||||
}
|
||||
ToolMessage::ActivateToolBrush => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }),
|
||||
ToolMessage::ActivateToolShapeLine | ToolMessage::ActivateToolShapeRectangle | ToolMessage::ActivateToolShapeEllipse => {
|
||||
@@ -89,8 +89,8 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
|
||||
self.tool_state.tool_data.active_shape_type = Some(shape.tool_type());
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
|
||||
responses.add(ShapeToolMessage::HideShapeTypeWidget(true));
|
||||
responses.add(ShapeToolMessage::SetShape(shape));
|
||||
responses.add(ShapeToolMessage::HideShapeTypeWidget { hide: true });
|
||||
responses.add(ShapeToolMessage::SetShape { shape });
|
||||
}
|
||||
ToolMessage::ActivateTool { tool_type } => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
@@ -157,13 +157,13 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
tool_data.tools.get(&tool_type).unwrap().activate(responses);
|
||||
|
||||
// Re-add the artboard overlay provider when tools are reactivated
|
||||
responses.add(OverlaysMessage::AddProvider(ARTBOARD_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::AddProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
|
||||
|
||||
// Send the SelectionChanged message to the active tool, this will ensure the selection is updated
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
responses.add(EventMessage::SelectionChanged);
|
||||
|
||||
// Update the working colors for the active tool
|
||||
responses.add(BroadcastEvent::WorkingColorChanged);
|
||||
responses.add(EventMessage::WorkingColorChanged);
|
||||
|
||||
// Send tool options to the frontend
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
@@ -176,11 +176,12 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
tool_data.tools.get(&tool_data.active_tool_type).unwrap().deactivate(responses);
|
||||
|
||||
// Unsubscribe the transform layer to selection change events
|
||||
let message = Box::new(TransformLayerMessage::SelectionChanged.into());
|
||||
let on = BroadcastEvent::SelectionChanged;
|
||||
responses.add(BroadcastMessage::UnsubscribeEvent { message, on });
|
||||
responses.add(BroadcastMessage::UnsubscribeEvent {
|
||||
on: EventMessage::SelectionChanged,
|
||||
send: Box::new(TransformLayerMessage::SelectionChanged.into()),
|
||||
});
|
||||
|
||||
responses.add(OverlaysMessage::RemoveProvider(ARTBOARD_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::RemoveProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data: Default::default() });
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: Default::default() });
|
||||
@@ -190,12 +191,12 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
ToolMessage::InitTools => {
|
||||
// Subscribe the transform layer to selection change events
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
on: EventMessage::SelectionChanged,
|
||||
send: Box::new(TransformLayerMessage::SelectionChanged.into()),
|
||||
});
|
||||
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
on: EventMessage::SelectionChanged,
|
||||
send: Box::new(SelectToolMessage::SyncHistory.into()),
|
||||
});
|
||||
|
||||
@@ -232,12 +233,12 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
||||
tool_data.active_tool_mut().process_message(ToolMessage::UpdateHints, responses, &mut data);
|
||||
tool_data.active_tool_mut().process_message(ToolMessage::UpdateCursor, responses, &mut data);
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(ARTBOARD_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::AddProvider { provider: ARTBOARD_OVERLAY_PROVIDER });
|
||||
}
|
||||
ToolMessage::PreUndo => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
if tool_data.active_tool_type != ToolType::Pen {
|
||||
responses.add(BroadcastEvent::ToolAbort);
|
||||
responses.add(EventMessage::ToolAbort);
|
||||
}
|
||||
}
|
||||
ToolMessage::Redo => {
|
||||
|
||||
@@ -26,7 +26,7 @@ pub struct ArtboardTool {
|
||||
pub enum ArtboardToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
|
||||
// Tool-specific messages
|
||||
UpdateSelectedArtboard,
|
||||
@@ -83,7 +83,7 @@ impl ToolTransition for ArtboardTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(ArtboardToolMessage::Abort.into()),
|
||||
overlay_provider: Some(|overlay_context| ArtboardToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| ArtboardToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ impl ArtboardToolData {
|
||||
let Some(movement) = &bounds.selected_edges else {
|
||||
return;
|
||||
};
|
||||
if self.selected_artboard.unwrap() == LayerNodeIdentifier::ROOT_PARENT {
|
||||
if self.selected_artboard == Some(LayerNodeIdentifier::ROOT_PARENT) {
|
||||
log::error!("Selected artboard cannot be ROOT_PARENT");
|
||||
return;
|
||||
}
|
||||
@@ -227,7 +227,7 @@ impl Fsm for ArtboardToolFsmState {
|
||||
|
||||
let ToolMessage::Artboard(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(state, ArtboardToolMessage::Overlays(mut overlay_context)) => {
|
||||
(state, ArtboardToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
let display_transform_cage = overlay_context.visibility_settings.transform_cage();
|
||||
if display_transform_cage && state != ArtboardToolFsmState::Drawing {
|
||||
if let Some(bounds) = tool_data.selected_artboard.and_then(|layer| document.metadata().bounding_box_document(layer)) {
|
||||
@@ -569,7 +569,7 @@ mod test_artboard {
|
||||
async fn get_artboards(editor: &mut EditorTestUtils) -> Table<graphene_std::Artboard> {
|
||||
let instrumented = match editor.eval_graph().await {
|
||||
Ok(instrumented) => instrumented,
|
||||
Err(e) => panic!("Failed to evaluate graph: {}", e),
|
||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
||||
};
|
||||
instrumented
|
||||
.grab_all_input::<graphene_std::graphic::extend::NewInput<graphene_std::Artboard>>(&editor.runtime)
|
||||
|
||||
@@ -62,7 +62,7 @@ pub enum BrushToolMessage {
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
UpdateOptions(BrushToolMessageOptionsUpdate),
|
||||
UpdateOptions { options: BrushToolMessageOptionsUpdate },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
@@ -106,7 +106,7 @@ impl LayoutHolder for BrushTool {
|
||||
.min(1.)
|
||||
.max(BRUSH_MAX_SIZE) /* Anything bigger would cause the application to be unresponsive and eventually die */
|
||||
.unit(" px")
|
||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap()) }.into())
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.options.hardness))
|
||||
@@ -115,7 +115,12 @@ impl LayoutHolder for BrushTool {
|
||||
.max(100.)
|
||||
.mode_range()
|
||||
.unit("%")
|
||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.options.flow))
|
||||
@@ -124,7 +129,12 @@ impl LayoutHolder for BrushTool {
|
||||
.max(100.)
|
||||
.mode_range()
|
||||
.unit("%")
|
||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.options.spacing))
|
||||
@@ -133,7 +143,12 @@ impl LayoutHolder for BrushTool {
|
||||
.max(100.)
|
||||
.mode_range()
|
||||
.unit("%")
|
||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
@@ -142,9 +157,12 @@ impl LayoutHolder for BrushTool {
|
||||
let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore]
|
||||
.into_iter()
|
||||
.map(|draw_mode| {
|
||||
RadioEntryData::new(format!("{draw_mode:?}"))
|
||||
.label(format!("{draw_mode:?}"))
|
||||
.on_update(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::DrawMode(draw_mode)).into())
|
||||
RadioEntryData::new(format!("{draw_mode:?}")).label(format!("{draw_mode:?}")).on_update(move |_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::DrawMode(draw_mode),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
widgets.push(RadioInput::new(draw_mode_entries).selected_index(Some(self.options.draw_mode as u32)).widget_holder());
|
||||
@@ -154,9 +172,26 @@ impl LayoutHolder for BrushTool {
|
||||
widgets.append(&mut self.options.color.create_widgets(
|
||||
"Color",
|
||||
false,
|
||||
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Color(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::ColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
@@ -167,9 +202,12 @@ impl LayoutHolder for BrushTool {
|
||||
section
|
||||
.iter()
|
||||
.map(|blend_mode| {
|
||||
MenuListEntry::new(format!("{blend_mode:?}"))
|
||||
.label(blend_mode.to_string())
|
||||
.on_commit(|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::BlendMode(*blend_mode)).into())
|
||||
MenuListEntry::new(format!("{blend_mode:?}")).label(blend_mode.to_string()).on_commit(|_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::BlendMode(*blend_mode),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
@@ -189,11 +227,11 @@ impl LayoutHolder for BrushTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for BrushTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Brush(BrushToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
match options {
|
||||
BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode,
|
||||
BrushToolMessageOptionsUpdate::ChangeDiameter(change) => {
|
||||
let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5;
|
||||
@@ -408,10 +446,9 @@ impl Fsm for BrushToolFsmState {
|
||||
BrushToolFsmState::Ready
|
||||
}
|
||||
(_, BrushToolMessage::WorkingColorChanged) => {
|
||||
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
responses.add(BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
});
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
|
||||
@@ -14,7 +14,7 @@ pub enum FillToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
|
||||
// Tool-specific messages
|
||||
PointerMove,
|
||||
@@ -67,7 +67,7 @@ impl ToolTransition for FillTool {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(FillToolMessage::Abort.into()),
|
||||
working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| FillToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ impl Fsm for FillToolFsmState {
|
||||
|
||||
let ToolMessage::Fill(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, FillToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, FillToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
// Choose the working color to preview
|
||||
let use_secondary = input.keyboard.get(Key::Shift as usize);
|
||||
let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color };
|
||||
|
||||
@@ -40,7 +40,7 @@ impl Default for FreehandOptions {
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum FreehandToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
@@ -48,7 +48,7 @@ pub enum FreehandToolMessage {
|
||||
DragStart { append_to_selected: Key },
|
||||
DragStop,
|
||||
PointerMove,
|
||||
UpdateOptions(FreehandOptionsUpdate),
|
||||
UpdateOptions { options: FreehandOptionsUpdate },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
@@ -86,7 +86,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
.label("Weight")
|
||||
.min(1.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
@@ -95,9 +100,26 @@ impl LayoutHolder for FreehandTool {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::FillColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -105,9 +127,26 @@ impl LayoutHolder for FreehandTool {
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
@@ -119,11 +158,11 @@ impl LayoutHolder for FreehandTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for FreehandTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Freehand(FreehandToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
match options {
|
||||
FreehandOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
@@ -164,7 +203,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Free
|
||||
impl ToolTransition for FreehandTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context: OverlayContext| FreehandToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context: OverlayContext| FreehandToolMessage::Overlays { context }.into()),
|
||||
tool_abort: Some(FreehandToolMessage::Abort.into()),
|
||||
working_color_changed: Some(FreehandToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
@@ -203,7 +242,7 @@ impl Fsm for FreehandToolFsmState {
|
||||
|
||||
let ToolMessage::Freehand(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, FreehandToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, FreehandToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
path_endpoint_overlays(document, shape_editor, &mut overlay_context, tool_action_data.preferences);
|
||||
|
||||
self
|
||||
@@ -287,10 +326,9 @@ impl Fsm for FreehandToolFsmState {
|
||||
FreehandToolFsmState::Ready
|
||||
}
|
||||
(_, FreehandToolMessage::WorkingColorChanged) => {
|
||||
responses.add(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
responses.add(FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
});
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
@@ -378,7 +416,10 @@ mod test_freehand {
|
||||
fn verify_path_points(vector_and_transform_list: &[(Vector, DAffine2)], expected_captured_points: &[DVec2], tolerance: f64) -> Result<(), String> {
|
||||
assert_eq!(vector_and_transform_list.len(), 1, "There should be one row of Vector geometry");
|
||||
|
||||
let (vector, transform) = vector_and_transform_list.iter().find(|(data, _)| data.point_domain.ids().len() > 0).ok_or("Could not find path data")?;
|
||||
let (vector, transform) = vector_and_transform_list
|
||||
.iter()
|
||||
.find(|(data, _)| !data.point_domain.ids().is_empty())
|
||||
.ok_or("Could not find path data")?;
|
||||
|
||||
let point_count = vector.point_domain.ids().len();
|
||||
let segment_count = vector.segment_domain.ids().len();
|
||||
@@ -386,7 +427,7 @@ mod test_freehand {
|
||||
let actual_positions: Vec<DVec2> = vector.point_domain.positions().iter().map(|&position| transform.transform_point2(position)).collect();
|
||||
|
||||
if segment_count != point_count - 1 {
|
||||
return Err(format!("Expected segments to be one less than points, got {} segments for {} points", segment_count, point_count));
|
||||
return Err(format!("Expected segments to be one less than points, got {segment_count} segments for {point_count} points"));
|
||||
}
|
||||
|
||||
if point_count != expected_captured_points.len() {
|
||||
@@ -396,7 +437,7 @@ mod test_freehand {
|
||||
for (i, (&expected, &actual)) in expected_captured_points.iter().zip(actual_positions.iter()).enumerate() {
|
||||
let distance = (expected - actual).length();
|
||||
if distance >= tolerance {
|
||||
return Err(format!("Point {} position mismatch: expected {:?}, got {:?} (distance: {})", i, expected, actual, distance));
|
||||
return Err(format!("Point {i} position mismatch: expected {expected:?}, got {actual:?} (distance: {distance})"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +511,7 @@ mod test_freehand {
|
||||
let initial_point_count = initial_vector.point_domain.ids().len();
|
||||
let initial_segment_count = initial_vector.segment_domain.ids().len();
|
||||
|
||||
assert!(initial_point_count >= 2, "Expected at least 2 points in initial path, found {}", initial_point_count);
|
||||
assert!(initial_point_count >= 2, "Expected at least 2 points in initial path, found {initial_point_count}");
|
||||
assert_eq!(
|
||||
initial_segment_count,
|
||||
initial_point_count - 1,
|
||||
@@ -531,17 +572,13 @@ mod test_freehand {
|
||||
|
||||
assert!(
|
||||
extended_point_count > initial_point_count,
|
||||
"Expected more points after extension, initial: {}, after extension: {}",
|
||||
initial_point_count,
|
||||
extended_point_count
|
||||
"Expected more points after extension, initial: {initial_point_count}, after extension: {extended_point_count}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
extended_segment_count,
|
||||
extended_point_count - 1,
|
||||
"Expected segments to be one less than points, points: {}, segments: {}",
|
||||
extended_point_count,
|
||||
extended_segment_count
|
||||
"Expected segments to be one less than points, points: {extended_point_count}, segments: {extended_segment_count}"
|
||||
);
|
||||
|
||||
let layer_count = {
|
||||
@@ -589,8 +626,8 @@ mod test_freehand {
|
||||
|
||||
let existing_layer_id = {
|
||||
let document = editor.active_document();
|
||||
let layer = document.metadata().all_layers().next().unwrap();
|
||||
layer
|
||||
|
||||
document.metadata().all_layers().next().unwrap()
|
||||
};
|
||||
|
||||
editor
|
||||
@@ -647,9 +684,7 @@ mod test_freehand {
|
||||
|
||||
assert!(
|
||||
final_point_count > initial_point_count,
|
||||
"Expected more points after appending to layer, initial: {}, after append: {}",
|
||||
initial_point_count,
|
||||
final_point_count
|
||||
"Expected more points after appending to layer, initial: {initial_point_count}, after append: {final_point_count}"
|
||||
);
|
||||
|
||||
let expected_new_points = second_path_points.len();
|
||||
@@ -679,7 +714,9 @@ mod test_freehand {
|
||||
|
||||
let custom_line_weight = 5.;
|
||||
editor
|
||||
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(custom_line_weight))))
|
||||
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions {
|
||||
options: FreehandOptionsUpdate::LineWeight(custom_line_weight),
|
||||
}))
|
||||
.await;
|
||||
|
||||
let points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct GradientOptions {
|
||||
pub enum GradientToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
|
||||
// Tool-specific messages
|
||||
DeleteStop,
|
||||
@@ -33,7 +33,7 @@ pub enum GradientToolMessage {
|
||||
PointerMove { constrain_axis: Key },
|
||||
PointerOutsideViewport { constrain_axis: Key },
|
||||
PointerUp,
|
||||
UpdateOptions(GradientOptionsUpdate),
|
||||
UpdateOptions { options: GradientOptionsUpdate },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
@@ -56,11 +56,11 @@ impl ToolMetadata for GradientTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for GradientTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Gradient(GradientToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Gradient(GradientToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, false);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
match options {
|
||||
GradientOptionsUpdate::Type(gradient_type) => {
|
||||
self.options.gradient_type = gradient_type;
|
||||
// Update the selected gradient if it exists
|
||||
@@ -91,14 +91,18 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||
impl LayoutHolder for GradientTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let gradient_type = RadioInput::new(vec![
|
||||
RadioEntryData::new("Linear")
|
||||
.label("Linear")
|
||||
.tooltip("Linear gradient")
|
||||
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Linear)).into()),
|
||||
RadioEntryData::new("Radial")
|
||||
.label("Radial")
|
||||
.tooltip("Radial gradient")
|
||||
.on_update(move |_| GradientToolMessage::UpdateOptions(GradientOptionsUpdate::Type(GradientType::Radial)).into()),
|
||||
RadioEntryData::new("Linear").label("Linear").tooltip("Linear gradient").on_update(move |_| {
|
||||
GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::Type(GradientType::Linear),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Radial").label("Radial").tooltip("Radial gradient").on_update(move |_| {
|
||||
GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::Type(GradientType::Radial),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some((self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial) as u32))
|
||||
.widget_holder();
|
||||
@@ -224,7 +228,7 @@ impl ToolTransition for GradientTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(GradientToolMessage::Abort.into()),
|
||||
overlay_provider: Some(|overlay_context| GradientToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| GradientToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -256,7 +260,7 @@ impl Fsm for GradientToolFsmState {
|
||||
|
||||
let ToolMessage::Gradient(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, GradientToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, GradientToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
let selected = tool_data.selected_gradient.as_ref();
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||
@@ -548,7 +552,7 @@ mod test_gradient {
|
||||
async fn get_fills(editor: &mut EditorTestUtils) -> Vec<(Fill, DAffine2)> {
|
||||
let instrumented = match editor.eval_graph().await {
|
||||
Ok(instrumented) => instrumented,
|
||||
Err(e) => panic!("Failed to evaluate graph: {}", e),
|
||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
||||
};
|
||||
|
||||
let document = editor.active_document();
|
||||
@@ -569,7 +573,7 @@ mod test_gradient {
|
||||
let (fill, transform) = fills.first().unwrap();
|
||||
let gradient = fill.as_gradient().expect("Expected gradient fill type");
|
||||
|
||||
(gradient.clone(), transform.clone())
|
||||
(gradient.clone(), *transform)
|
||||
}
|
||||
|
||||
fn assert_stops_at_positions(actual_positions: &[f64], expected_positions: &[f64], tolerance: f64) {
|
||||
@@ -582,7 +586,7 @@ mod test_gradient {
|
||||
);
|
||||
|
||||
for (i, (actual, expected)) in actual_positions.iter().zip(expected_positions.iter()).enumerate() {
|
||||
assert!((actual - expected).abs() < tolerance, "Stop {}: Expected position near {}, got {}", i, expected, actual);
|
||||
assert!((actual - expected).abs() < tolerance, "Stop {i}: Expected position near {expected}, got {actual}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,8 +713,7 @@ mod test_gradient {
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
assert!(
|
||||
positions.iter().any(|pos| (pos - 0.5).abs() < 0.1),
|
||||
"Expected to find a stop near position 0.5, but found: {:?}",
|
||||
positions
|
||||
"Expected to find a stop near position 0.5, but found: {positions:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -778,7 +781,7 @@ mod test_gradient {
|
||||
|
||||
// Verify the end point has been updated to the new position
|
||||
let updated_end = transform.transform_point2(updated_gradient.end);
|
||||
assert!(updated_end.abs_diff_eq(DVec2::new(100., 50.), 1e-10), "Expected end point at (100, 50), got {:?}", updated_end);
|
||||
assert!(updated_end.abs_diff_eq(DVec2::new(100., 50.), 1e-10), "Expected end point at (100, 50), got {updated_end:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -51,12 +51,14 @@ pub struct PathToolOptions {
|
||||
pub enum PathToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
Overlays(OverlayContext),
|
||||
SelectionChanged,
|
||||
Overlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
|
||||
// Tool-specific messages
|
||||
BreakPath,
|
||||
DeselectAllPoints,
|
||||
DeselectAllSelected,
|
||||
Delete,
|
||||
DeleteAndBreakPath,
|
||||
DragStop {
|
||||
@@ -111,7 +113,7 @@ pub enum PathToolMessage {
|
||||
segment_editing_modifier: Key,
|
||||
},
|
||||
RightClick,
|
||||
SelectAllAnchors,
|
||||
SelectAll,
|
||||
SelectedPointUpdated,
|
||||
SelectedPointXChanged {
|
||||
new_x: f64,
|
||||
@@ -123,7 +125,9 @@ pub enum PathToolMessage {
|
||||
position: ReferencePoint,
|
||||
},
|
||||
SwapSelectedHandles,
|
||||
UpdateOptions(PathOptionsUpdate),
|
||||
UpdateOptions {
|
||||
options: PathOptionsUpdate,
|
||||
},
|
||||
UpdateSelectedPointsStatus {
|
||||
overlay_context: OverlayContext,
|
||||
},
|
||||
@@ -276,15 +280,30 @@ impl LayoutHolder for PathTool {
|
||||
RadioEntryData::new("all")
|
||||
.icon("HandleVisibilityAll")
|
||||
.tooltip("Show all handles regardless of selection")
|
||||
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()),
|
||||
.on_update(move |_| {
|
||||
PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("selected")
|
||||
.icon("HandleVisibilitySelected")
|
||||
.tooltip("Show only handles of the segments connected to selected points")
|
||||
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()),
|
||||
.on_update(move |_| {
|
||||
PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("frontier")
|
||||
.icon("HandleVisibilityFrontier")
|
||||
.tooltip("Show only handles at the frontiers of the segments connected to selected points")
|
||||
.on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()),
|
||||
.on_update(move |_| {
|
||||
PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(self.options.path_overlay_mode as u32))
|
||||
.widget_holder();
|
||||
@@ -355,7 +374,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
||||
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
|
||||
|
||||
match message {
|
||||
ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action {
|
||||
ToolMessage::Path(PathToolMessage::UpdateOptions { options }) => match options {
|
||||
PathOptionsUpdate::OverlayModeType(overlay_mode_type) => {
|
||||
self.options.path_overlay_mode = overlay_mode_type;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
@@ -392,12 +411,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
},
|
||||
ToolMessage::Path(PathToolMessage::ClosePath) => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
context.shape_editor.close_selected_path(context.document, responses);
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => {
|
||||
if context.shape_editor.handle_with_pair_selected(&context.document.network_interface) {
|
||||
context.shape_editor.alternate_selected_handles(&context.document.network_interface);
|
||||
@@ -425,8 +438,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
|
||||
Delete,
|
||||
NudgeSelectedPoints,
|
||||
Enter,
|
||||
SelectAllAnchors,
|
||||
DeselectAllPoints,
|
||||
SelectAll,
|
||||
DeselectAllSelected,
|
||||
BreakPath,
|
||||
DeleteAndBreakPath,
|
||||
ClosePath,
|
||||
@@ -489,7 +502,7 @@ impl ToolTransition for PathTool {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(PathToolMessage::Abort.into()),
|
||||
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
|
||||
overlay_provider: Some(|overlay_context| PathToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| PathToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -555,11 +568,11 @@ struct PathToolData {
|
||||
segment_editing_modifier: bool,
|
||||
multiple_toggle_pressed: bool,
|
||||
auto_panning: AutoPanning,
|
||||
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>,
|
||||
saved_points_before_anchor_select_toggle: HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>,
|
||||
select_anchor_toggled: bool,
|
||||
saved_selection_before_handle_drag: HashMap<LayerNodeIdentifier, (HashSet<ManipulatorPointId>, HashSet<SegmentId>)>,
|
||||
handle_drag_toggle: bool,
|
||||
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>,
|
||||
saved_points_before_anchor_convert_smooth_sharp: HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>,
|
||||
last_click_time: u64,
|
||||
dragging_state: DraggingState,
|
||||
angle: f64,
|
||||
@@ -576,7 +589,7 @@ struct PathToolData {
|
||||
molding_info: Option<(DVec2, DVec2)>,
|
||||
molding_segment: bool,
|
||||
temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
frontier_handles_info: Option<HashMap<LayerNodeIdentifier, HashMap<SegmentId, Vec<PointId>>>>,
|
||||
adjacent_anchor_offset: Option<DVec2>,
|
||||
sliding_point_info: Option<SlidingPointInfo>,
|
||||
started_drawing_from_inside: bool,
|
||||
@@ -592,7 +605,7 @@ struct PathToolData {
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
fn save_points_before_anchor_toggle(&mut self, points: Vec<ManipulatorPointId>) -> PathToolFsmState {
|
||||
fn save_points_before_anchor_toggle(&mut self, points: HashMap<LayerNodeIdentifier, Vec<ManipulatorPointId>>) -> PathToolFsmState {
|
||||
self.saved_points_before_anchor_select_toggle = points;
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
}
|
||||
@@ -795,7 +808,7 @@ impl PathToolData {
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
path_overlay_mode,
|
||||
&self.frontier_handles_info,
|
||||
self.frontier_handles_info.as_ref(),
|
||||
point_editing_mode,
|
||||
) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
@@ -813,7 +826,7 @@ impl PathToolData {
|
||||
SELECTION_THRESHOLD,
|
||||
extend_selection,
|
||||
path_overlay_mode,
|
||||
&self.frontier_handles_info,
|
||||
self.frontier_handles_info.as_ref(),
|
||||
) {
|
||||
selection_info = updated_selection_info;
|
||||
}
|
||||
@@ -853,15 +866,15 @@ impl PathToolData {
|
||||
|
||||
let manipulator_point_id = handles[0].to_manipulator_point();
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&vec![manipulator_point_id]);
|
||||
shape_editor.select_point_by_layer_and_id(manipulator_point_id, layer);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((Some(point), Some(vector))) = shape_editor
|
||||
if let Some((Some(point), Some(vector), layer)) = shape_editor
|
||||
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
|
||||
.map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer)))
|
||||
.map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer), layer))
|
||||
{
|
||||
let handles = vector
|
||||
.all_connected(point)
|
||||
@@ -872,7 +885,7 @@ impl PathToolData {
|
||||
|
||||
if drag_zero_handle && (handles.len() == 1 && !endpoint) {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&handles);
|
||||
shape_editor.select_points_by_layer_and_id(&HashMap::from([(layer, handles)]));
|
||||
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
|
||||
}
|
||||
}
|
||||
@@ -1252,7 +1265,7 @@ impl PathToolData {
|
||||
// Check if there is no point nearby
|
||||
// If the point mode is deactivated then don't override closest segment even if there is a closer point
|
||||
if shape_editor
|
||||
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, &self.frontier_handles_info)
|
||||
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.as_ref())
|
||||
.is_some()
|
||||
&& point_editing_mode
|
||||
{
|
||||
@@ -1262,9 +1275,19 @@ impl PathToolData {
|
||||
else if let Some(closest_segment) = &mut self.segment {
|
||||
closest_segment.update_closest_point(document.metadata(), &document.network_interface, position);
|
||||
|
||||
let layer = closest_segment.layer();
|
||||
let segment_id = closest_segment.segment();
|
||||
|
||||
if closest_segment.too_far(position, SEGMENT_INSERTION_DISTANCE) {
|
||||
self.segment = None;
|
||||
}
|
||||
|
||||
// Check if that segment exists or it has been removed
|
||||
if let Some(vector_data) = document.network_interface.compute_modified_vector(layer)
|
||||
&& !(vector_data.segment_domain.ids().iter().any(|segment| *segment == segment_id))
|
||||
{
|
||||
self.segment = None;
|
||||
}
|
||||
}
|
||||
// If not, check that if there is some closest segment or not
|
||||
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, position, SEGMENT_INSERTION_DISTANCE) {
|
||||
@@ -1519,7 +1542,8 @@ impl PathToolData {
|
||||
|
||||
// Now change the selection to this handle
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&vec![handle]);
|
||||
shape_editor.select_point_by_layer_and_id(handle, layer);
|
||||
|
||||
responses.add(PathToolMessage::SelectionChanged);
|
||||
}
|
||||
}
|
||||
@@ -1627,14 +1651,22 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
match (multiple_toggle, point_edit) {
|
||||
(true, true) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false }));
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::PointEditingMode { enabled: false },
|
||||
});
|
||||
}
|
||||
(true, false) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true }));
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::PointEditingMode { enabled: true },
|
||||
});
|
||||
}
|
||||
(_, _) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true }));
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false }));
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::PointEditingMode { enabled: true },
|
||||
});
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::SegmentEditingMode { enabled: false },
|
||||
});
|
||||
|
||||
// Select all of the end points of selected segments
|
||||
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
|
||||
@@ -1672,14 +1704,22 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
match (multiple_toggle, segment_edit) {
|
||||
(true, true) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false }));
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::SegmentEditingMode { enabled: false },
|
||||
});
|
||||
}
|
||||
(true, false) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true }));
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::SegmentEditingMode { enabled: true },
|
||||
});
|
||||
}
|
||||
(_, _) => {
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false }));
|
||||
responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true }));
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::PointEditingMode { enabled: false },
|
||||
});
|
||||
responses.add(PathToolMessage::UpdateOptions {
|
||||
options: PathOptionsUpdate::SegmentEditingMode { enabled: true },
|
||||
});
|
||||
|
||||
// Select all the segments which have both of the ends selected
|
||||
let selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
|
||||
@@ -1702,7 +1742,7 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, PathToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
// Set this to show ghost line only if drag actually happened
|
||||
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
|
||||
for (outline, layer) in &tool_data.ghost_outline {
|
||||
@@ -1726,25 +1766,31 @@ impl Fsm for PathToolFsmState {
|
||||
}
|
||||
PathOverlayMode::FrontierHandles => {
|
||||
let selected_segments = selected_segments(&document.network_interface, shape_editor);
|
||||
let selected_points = shape_editor.selected_points();
|
||||
let selected_anchors = selected_points
|
||||
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Match the behavior of `PathOverlayMode::SelectedPointHandles` when only one point is selected
|
||||
if shape_editor.selected_points().count() == 1 {
|
||||
path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context);
|
||||
} else {
|
||||
let mut segment_endpoints: HashMap<SegmentId, Vec<PointId>> = HashMap::new();
|
||||
let mut segment_endpoints_by_layer = HashMap::new();
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
||||
let mut segment_endpoints: HashMap<SegmentId, Vec<PointId>> = HashMap::new();
|
||||
|
||||
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { continue };
|
||||
|
||||
let selected_points = state.selected_points();
|
||||
let selected_anchors = selected_points
|
||||
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(p) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let Some(focused_segments) = selected_segments.get(&layer) else { continue };
|
||||
|
||||
// The points which are part of only one segment will be rendered
|
||||
let mut selected_segments_by_point: HashMap<PointId, Vec<SegmentId>> = HashMap::new();
|
||||
|
||||
for (segment_id, _bezier, start, end) in vector.segment_bezier_iter() {
|
||||
if selected_segments.contains(&segment_id) {
|
||||
if focused_segments.contains(&segment_id) {
|
||||
selected_segments_by_point.entry(start).or_default().push(segment_id);
|
||||
selected_segments_by_point.entry(end).or_default().push(segment_id);
|
||||
}
|
||||
@@ -1760,13 +1806,15 @@ impl Fsm for PathToolFsmState {
|
||||
segment_endpoints.entry(attached_segments[1]).or_default().push(point);
|
||||
}
|
||||
}
|
||||
|
||||
segment_endpoints_by_layer.insert(layer, segment_endpoints);
|
||||
}
|
||||
|
||||
// Caching segment endpoints for use in point selection logic
|
||||
tool_data.frontier_handles_info = Some(segment_endpoints.clone());
|
||||
tool_data.frontier_handles_info = Some(segment_endpoints_by_layer.clone());
|
||||
|
||||
// Now frontier anchors can be sent for rendering overlays
|
||||
path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints), shape_editor, &mut overlay_context);
|
||||
path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints_by_layer), shape_editor, &mut overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1792,7 +1840,7 @@ impl Fsm for PathToolFsmState {
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
);
|
||||
|
||||
let Some((layer, manipulator_point_id)) = nearest_visible_point_indices else { return };
|
||||
@@ -1899,12 +1947,12 @@ impl Fsm for PathToolFsmState {
|
||||
let (points_inside, segments_inside) = match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position);
|
||||
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y);
|
||||
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
|
||||
shape_editor.get_inside_points_and_segments(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
select_segments,
|
||||
select_points,
|
||||
selection_mode,
|
||||
@@ -1914,7 +1962,7 @@ impl Fsm for PathToolFsmState {
|
||||
&document.network_interface,
|
||||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
select_segments,
|
||||
select_points,
|
||||
selection_mode,
|
||||
@@ -2131,13 +2179,19 @@ impl Fsm for PathToolFsmState {
|
||||
if initial_press {
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
tool_data.select_anchor_toggled = true;
|
||||
tool_data.save_points_before_anchor_toggle(shape_editor.selected_points().cloned().collect());
|
||||
shape_editor.select_handles_and_anchor_connected_to_current_handle(&document.network_interface);
|
||||
|
||||
let mut points_to_save = HashMap::new();
|
||||
for (layer, state) in &shape_editor.selected_shape_state {
|
||||
points_to_save.insert(*layer, state.selected_points().collect::<Vec<_>>());
|
||||
}
|
||||
tool_data.save_points_before_anchor_toggle(points_to_save);
|
||||
|
||||
shape_editor.select_anchor_and_connected_handles(&document.network_interface);
|
||||
} else if released_from_toggle {
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
tool_data.select_anchor_toggled = false;
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
|
||||
shape_editor.select_points_by_layer_and_id(&tool_data.saved_points_before_anchor_select_toggle);
|
||||
tool_data.remove_saved_points();
|
||||
}
|
||||
|
||||
@@ -2316,14 +2370,14 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y);
|
||||
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
|
||||
|
||||
shape_editor.select_all_in_shape(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
@@ -2334,7 +2388,7 @@ impl Fsm for PathToolFsmState {
|
||||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
@@ -2413,14 +2467,14 @@ impl Fsm for PathToolFsmState {
|
||||
} else {
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y);
|
||||
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
|
||||
|
||||
shape_editor.select_all_in_shape(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
@@ -2431,7 +2485,7 @@ impl Fsm for PathToolFsmState {
|
||||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
selection_mode,
|
||||
@@ -2455,7 +2509,7 @@ impl Fsm for PathToolFsmState {
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
);
|
||||
|
||||
let nearest_segment = tool_data.segment.clone();
|
||||
@@ -2508,7 +2562,11 @@ impl Fsm for PathToolFsmState {
|
||||
}
|
||||
if !drag_occurred && !extend_selection && clicked_selected {
|
||||
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>();
|
||||
let mut saved_points = HashMap::new();
|
||||
for (layer, state) in &shape_editor.selected_shape_state {
|
||||
saved_points.insert(*layer, state.selected_points().collect::<Vec<_>>());
|
||||
}
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp = saved_points;
|
||||
}
|
||||
|
||||
shape_editor.deselect_all_points();
|
||||
@@ -2602,7 +2660,7 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
if tool_data.select_anchor_toggled {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
|
||||
shape_editor.select_points_by_layer_and_id(&tool_data.saved_points_before_anchor_select_toggle);
|
||||
tool_data.remove_saved_points();
|
||||
tool_data.select_anchor_toggled = false;
|
||||
}
|
||||
@@ -2647,6 +2705,15 @@ impl Fsm for PathToolFsmState {
|
||||
shape_editor.delete_point_and_break_path(document, responses);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::ClosePath) => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
shape_editor.close_selected_path(document, responses, tool_action_data.preferences.vector_meshes);
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::StartSlidingPoint) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
if tool_data.start_sliding_point(shape_editor, document) {
|
||||
@@ -2963,8 +3030,8 @@ impl Fsm for PathToolFsmState {
|
||||
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp.iter().copied().collect::<Vec<_>>());
|
||||
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
shape_editor.select_points_by_layer_and_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp);
|
||||
shape_editor.flip_smooth_sharp(&document.network_interface, responses);
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
@@ -3061,14 +3128,28 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::SelectAllAnchors) => {
|
||||
(_, PathToolMessage::SelectAll) => {
|
||||
shape_editor.select_all_anchors_in_selected_layers(document);
|
||||
|
||||
let point_editing_mode = tool_options.path_editing_mode.point_editing_mode;
|
||||
let segment_editing_mode = tool_options.path_editing_mode.segment_editing_mode;
|
||||
|
||||
if point_editing_mode {
|
||||
shape_editor.select_all_anchors_in_selected_layers(document);
|
||||
}
|
||||
if segment_editing_mode {
|
||||
shape_editor.select_all_segments_in_selected_layers(document);
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::DeselectAllPoints) => {
|
||||
(_, PathToolMessage::DeselectAllSelected) => {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.deselect_all_segments();
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::SelectedPointXChanged { new_x }) => {
|
||||
@@ -3480,7 +3561,7 @@ fn update_dynamic_hints(
|
||||
position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
&tool_data.frontier_handles_info,
|
||||
tool_data.frontier_handles_info.as_ref(),
|
||||
)
|
||||
.is_some();
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ pub enum PenToolMessage {
|
||||
Abort,
|
||||
SelectionChanged,
|
||||
WorkingColorChanged,
|
||||
Overlays(OverlayContext),
|
||||
Overlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
|
||||
// Tool-specific messages
|
||||
|
||||
@@ -80,7 +82,9 @@ pub enum PenToolMessage {
|
||||
},
|
||||
Redo,
|
||||
Undo,
|
||||
UpdateOptions(PenOptionsUpdate),
|
||||
UpdateOptions {
|
||||
options: PenOptionsUpdate,
|
||||
},
|
||||
RecalculateLatestPointsPosition,
|
||||
RemovePreviousHandle,
|
||||
GRS {
|
||||
@@ -138,7 +142,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
@@ -147,9 +156,26 @@ impl LayoutHolder for PenTool {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::FillColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -157,9 +183,26 @@ impl LayoutHolder for PenTool {
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -173,11 +216,21 @@ impl LayoutHolder for PenTool {
|
||||
RadioEntryData::new("all")
|
||||
.icon("HandleVisibilityAll")
|
||||
.tooltip("Show all handles regardless of selection")
|
||||
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()),
|
||||
.on_update(move |_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("frontier")
|
||||
.icon("HandleVisibilityFrontier")
|
||||
.tooltip("Show only handles at the frontiers of the segments connected to selected points")
|
||||
.on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()),
|
||||
.on_update(move |_| {
|
||||
PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
])
|
||||
.selected_index(Some(self.options.pen_overlay_mode as u32))
|
||||
.widget_holder(),
|
||||
@@ -190,12 +243,12 @@ impl LayoutHolder for PenTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for PenTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Pen(PenToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Pen(PenToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
|
||||
match action {
|
||||
match options {
|
||||
PenOptionsUpdate::OverlayModeType(overlay_mode_type) => {
|
||||
self.options.pen_overlay_mode = overlay_mode_type;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
@@ -253,7 +306,7 @@ impl ToolTransition for PenTool {
|
||||
tool_abort: Some(PenToolMessage::Abort.into()),
|
||||
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|overlay_context| PenToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| PenToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1547,7 +1600,7 @@ impl Fsm for PenToolFsmState {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => {
|
||||
(PenToolFsmState::Ready, PenToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
match tool_options.pen_overlay_mode {
|
||||
PenOverlayMode::AllHandles => {
|
||||
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
|
||||
@@ -1575,7 +1628,7 @@ impl Fsm for PenToolFsmState {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(_, PenToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, PenToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
let display_anchors = overlay_context.visibility_settings.anchors();
|
||||
let display_handles = overlay_context.visibility_settings.handles();
|
||||
|
||||
@@ -1614,17 +1667,21 @@ impl Fsm for PenToolFsmState {
|
||||
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
|
||||
}
|
||||
PenOverlayMode::FrontierHandles => {
|
||||
if let Some(latest_segment) = tool_data.prior_segment {
|
||||
path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context);
|
||||
}
|
||||
// If a vector mesh then there can be more than one prior segments
|
||||
else if let Some(segments) = tool_data.prior_segments.clone() {
|
||||
if preferences.vector_meshes {
|
||||
path_overlays(document, DrawHandles::SelectedAnchors(segments), shape_editor, &mut overlay_context);
|
||||
if let Some(layer) = tool_data.current_layer {
|
||||
if let Some(latest_segment) = tool_data.prior_segment {
|
||||
let selected_anchors_data = HashMap::from([(layer, vec![latest_segment])]);
|
||||
path_overlays(document, DrawHandles::SelectedAnchors(selected_anchors_data), shape_editor, &mut overlay_context);
|
||||
}
|
||||
} else {
|
||||
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
|
||||
};
|
||||
// If a vector mesh then there can be more than one prior segments
|
||||
else if let Some(segments) = tool_data.prior_segments.clone() {
|
||||
if preferences.vector_meshes {
|
||||
let selected_anchors_data = HashMap::from([(layer, segments)]);
|
||||
path_overlays(document, DrawHandles::SelectedAnchors(selected_anchors_data), shape_editor, &mut overlay_context);
|
||||
}
|
||||
} else {
|
||||
path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1753,10 +1810,9 @@ impl Fsm for PenToolFsmState {
|
||||
self
|
||||
}
|
||||
(_, PenToolMessage::WorkingColorChanged) => {
|
||||
responses.add(PenToolMessage::UpdateOptions(PenOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
responses.add(PenToolMessage::UpdateOptions {
|
||||
options: PenOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
});
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
|
||||
|
||||
@@ -78,7 +78,9 @@ pub struct SelectToolPointerKeys {
|
||||
pub enum SelectToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
Overlays(OverlayContext),
|
||||
Overlays {
|
||||
context: OverlayContext,
|
||||
},
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart {
|
||||
@@ -94,9 +96,15 @@ pub enum SelectToolMessage {
|
||||
EditLayer,
|
||||
EditLayerExec,
|
||||
Enter,
|
||||
PointerMove(SelectToolPointerKeys),
|
||||
PointerOutsideViewport(SelectToolPointerKeys),
|
||||
SelectOptions(SelectOptionsUpdate),
|
||||
PointerMove {
|
||||
modifier_keys: SelectToolPointerKeys,
|
||||
},
|
||||
PointerOutsideViewport {
|
||||
modifier_keys: SelectToolPointerKeys,
|
||||
},
|
||||
SelectOptions {
|
||||
options: SelectOptionsUpdate,
|
||||
},
|
||||
SetPivot {
|
||||
position: ReferencePoint,
|
||||
},
|
||||
@@ -127,9 +135,12 @@ impl SelectTool {
|
||||
let layer_selection_behavior_entries = [NestedSelectionBehavior::Shallowest, NestedSelectionBehavior::Deepest]
|
||||
.iter()
|
||||
.map(|mode| {
|
||||
MenuListEntry::new(format!("{mode:?}"))
|
||||
.label(mode.to_string())
|
||||
.on_commit(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into())
|
||||
MenuListEntry::new(format!("{mode:?}")).label(mode.to_string()).on_commit(move |_| {
|
||||
SelectToolMessage::SelectOptions {
|
||||
options: SelectOptionsUpdate::NestedSelectionBehavior(*mode),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -191,11 +202,11 @@ impl SelectTool {
|
||||
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
|
||||
list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| {
|
||||
let mut tooltip = info.label.to_string();
|
||||
if let Some(doc) = info.docstring.as_deref() {
|
||||
if let Some(doc) = info.docstring {
|
||||
tooltip.push_str("\n\n");
|
||||
tooltip.push_str(doc);
|
||||
}
|
||||
IconButton::new(info.icon.as_deref().unwrap(), 24)
|
||||
IconButton::new(info.icon.unwrap(), 24)
|
||||
.tooltip(tooltip)
|
||||
.disabled(selected_count == 0)
|
||||
.on_update(move |_| {
|
||||
@@ -278,7 +289,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let mut redraw_reference_pivot = false;
|
||||
|
||||
if let ToolMessage::Select(SelectToolMessage::SelectOptions(ref option_update)) = message {
|
||||
if let ToolMessage::Select(SelectToolMessage::SelectOptions { options: ref option_update }) = message {
|
||||
match option_update {
|
||||
SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => {
|
||||
self.tool_data.nested_selection_behavior = *nested_selection_behavior;
|
||||
@@ -342,7 +353,7 @@ impl ToolTransition for SelectTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(SelectToolMessage::Abort.into()),
|
||||
overlay_provider: Some(|overlay_context| SelectToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| SelectToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -591,7 +602,7 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
let ToolMessage::Select(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, SelectToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, SelectToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
let selected_layers_count = document.network_interface.selected_nodes().selected_unlocked_layers(&document.network_interface).count();
|
||||
@@ -841,7 +852,13 @@ impl Fsm for SelectToolFsmState {
|
||||
if let Some(pivot) = pivot {
|
||||
let offset = tool_data
|
||||
.pivot_gizmo_start
|
||||
.map(|offset| tool_data.pivot_gizmo.pivot_disconnected().then_some(tool_data.drag_current - offset).unwrap_or_default())
|
||||
.map(|offset| {
|
||||
if tool_data.pivot_gizmo.pivot_disconnected() {
|
||||
tool_data.drag_current - offset
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let shift = tool_data.pivot_gizmo_shift.unwrap_or_default();
|
||||
overlay_context.pivot(pivot + offset + shift, angle);
|
||||
@@ -884,7 +901,7 @@ impl Fsm for SelectToolFsmState {
|
||||
color
|
||||
} else {
|
||||
let color_string = &graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).to_rgba_hex_srgb();
|
||||
&format!("#{}", color_string)
|
||||
&format!("#{color_string}")
|
||||
};
|
||||
let line_center = tool_data.line_center;
|
||||
overlay_context.line(line_center - direction * viewport_diagonal, line_center + direction * viewport_diagonal, Some(color), None);
|
||||
@@ -1142,7 +1159,7 @@ impl Fsm for SelectToolFsmState {
|
||||
deepest,
|
||||
remove,
|
||||
},
|
||||
SelectToolMessage::PointerMove(modifier_keys),
|
||||
SelectToolMessage::PointerMove { modifier_keys },
|
||||
) => {
|
||||
if !has_dragged {
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
@@ -1187,8 +1204,8 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
|
||||
SelectToolMessage::PointerMove { modifier_keys }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
@@ -1200,7 +1217,7 @@ impl Fsm for SelectToolFsmState {
|
||||
remove,
|
||||
}
|
||||
}
|
||||
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { modifier_keys }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
resize_bounds(
|
||||
document,
|
||||
@@ -1215,14 +1232,14 @@ impl Fsm for SelectToolFsmState {
|
||||
ToolType::Select,
|
||||
);
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
|
||||
SelectToolMessage::PointerMove { modifier_keys }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
}
|
||||
SelectToolFsmState::ResizingBounds
|
||||
}
|
||||
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove(_)) => {
|
||||
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove { .. }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
skew_bounds(
|
||||
document,
|
||||
@@ -1236,7 +1253,7 @@ impl Fsm for SelectToolFsmState {
|
||||
}
|
||||
SelectToolFsmState::SkewingBounds { skew }
|
||||
}
|
||||
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(_)) => {
|
||||
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { .. }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
rotate_bounds(
|
||||
document,
|
||||
@@ -1252,7 +1269,7 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
SelectToolFsmState::RotatingBounds
|
||||
}
|
||||
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { modifier_keys }) => {
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = mouse_position;
|
||||
|
||||
@@ -1262,14 +1279,14 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
|
||||
SelectToolMessage::PointerMove { modifier_keys }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
}
|
||||
(SelectToolFsmState::Drawing { selection_shape, has_drawn }, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
(SelectToolFsmState::Drawing { selection_shape, has_drawn }, SelectToolMessage::PointerMove { modifier_keys }) => {
|
||||
if !has_drawn {
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
}
|
||||
@@ -1283,14 +1300,14 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
|
||||
SelectToolMessage::PointerMove { modifier_keys }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
SelectToolFsmState::Drawing { selection_shape, has_drawn: true }
|
||||
}
|
||||
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
|
||||
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove { .. }) => {
|
||||
let dragging_bounds = tool_data
|
||||
.bounding_box_manager
|
||||
.as_mut()
|
||||
@@ -1326,7 +1343,7 @@ impl Fsm for SelectToolFsmState {
|
||||
deepest,
|
||||
remove,
|
||||
},
|
||||
SelectToolMessage::PointerOutsideViewport(_),
|
||||
SelectToolMessage::PointerOutsideViewport { .. },
|
||||
) => {
|
||||
// Auto-panning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
@@ -1342,7 +1359,7 @@ impl Fsm for SelectToolFsmState {
|
||||
remove,
|
||||
}
|
||||
}
|
||||
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. }, SelectToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
@@ -1353,13 +1370,13 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
self
|
||||
}
|
||||
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
(SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
tool_data.drag_start += shift;
|
||||
@@ -1367,11 +1384,11 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
self
|
||||
}
|
||||
(state, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => {
|
||||
(state, SelectToolMessage::PointerOutsideViewport { modifier_keys }) => {
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
SelectToolMessage::PointerOutsideViewport { modifier_keys: modifier_keys.clone() }.into(),
|
||||
SelectToolMessage::PointerMove { modifier_keys }.into(),
|
||||
];
|
||||
tool_data.auto_panning.stop(&messages, responses);
|
||||
|
||||
@@ -1444,7 +1461,11 @@ impl Fsm for SelectToolFsmState {
|
||||
tool_data.select_single_layer = None;
|
||||
|
||||
if let Some(start) = tool_data.pivot_gizmo_start {
|
||||
let offset = tool_data.pivot_gizmo.pivot_disconnected().then_some(tool_data.drag_current - start).unwrap_or_default();
|
||||
let offset = if tool_data.pivot_gizmo.pivot_disconnected() {
|
||||
tool_data.drag_current - start
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
if let Some(v) = tool_data.pivot_gizmo.pivot.pivot.as_mut() {
|
||||
*v += offset;
|
||||
}
|
||||
@@ -1638,7 +1659,9 @@ impl Fsm for SelectToolFsmState {
|
||||
}
|
||||
(_, SelectToolMessage::PivotShift { offset, flush }) => {
|
||||
if flush {
|
||||
tool_data.pivot_gizmo.pivot.pivot.as_mut().map(|v| *v += tool_data.pivot_gizmo_shift.take().unwrap_or_default());
|
||||
if let Some(v) = tool_data.pivot_gizmo.pivot.pivot.as_mut() {
|
||||
*v += tool_data.pivot_gizmo_shift.take().unwrap_or_default();
|
||||
}
|
||||
let pivot_gizmo = tool_data.pivot_gizmo();
|
||||
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
|
||||
return self;
|
||||
|
||||
@@ -27,7 +27,7 @@ use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::misc::ArcType;
|
||||
use std::vec;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct ShapeTool {
|
||||
fsm_state: ShapeToolFsmState,
|
||||
tool_data: ShapeToolData,
|
||||
@@ -73,18 +73,18 @@ pub enum ShapeOptionsUpdate {
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ShapeToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
HideShapeTypeWidget(bool),
|
||||
PointerMove(ShapeToolModifierKey),
|
||||
PointerOutsideViewport(ShapeToolModifierKey),
|
||||
UpdateOptions(ShapeOptionsUpdate),
|
||||
SetShape(ShapeType),
|
||||
HideShapeTypeWidget { hide: bool },
|
||||
PointerMove { modifier: ShapeToolModifierKey },
|
||||
PointerOutsideViewport { modifier: ShapeToolModifierKey },
|
||||
UpdateOptions { options: ShapeOptionsUpdate },
|
||||
SetShape { shape: ShapeType },
|
||||
|
||||
IncreaseSides,
|
||||
DecreaseSides,
|
||||
@@ -99,39 +99,65 @@ fn create_sides_widget(vertices: u32) -> WidgetHolder {
|
||||
.min(3.)
|
||||
.max(1000.)
|
||||
.mode(NumberInputMode::Increment)
|
||||
.on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
|
||||
let entries = vec![vec![
|
||||
MenuListEntry::new("Polygon")
|
||||
.label("Polygon")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Polygon)).into()),
|
||||
MenuListEntry::new("Star")
|
||||
.label("Star")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()),
|
||||
MenuListEntry::new("Circle")
|
||||
.label("Circle")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Circle)).into()),
|
||||
MenuListEntry::new("Arc")
|
||||
.label("Arc")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()),
|
||||
MenuListEntry::new("Polygon").label("Polygon").on_commit(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ShapeType(ShapeType::Polygon),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
MenuListEntry::new("Star").label("Star").on_commit(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ShapeType(ShapeType::Star),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
MenuListEntry::new("Circle").label("Circle").on_commit(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ShapeType(ShapeType::Circle),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
MenuListEntry::new("Arc").label("Arc").on_commit(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ShapeType(ShapeType::Arc),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
]];
|
||||
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder()
|
||||
}
|
||||
|
||||
fn create_arc_type_widget(arc_type: ArcType) -> WidgetHolder {
|
||||
let entries = vec![
|
||||
RadioEntryData::new("Open")
|
||||
.label("Open")
|
||||
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Open)).into()),
|
||||
RadioEntryData::new("Closed")
|
||||
.label("Closed")
|
||||
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Closed)).into()),
|
||||
RadioEntryData::new("Pie")
|
||||
.label("Pie")
|
||||
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::PieSlice)).into()),
|
||||
RadioEntryData::new("Open").label("Open").on_update(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ArcType(ArcType::Open),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Closed").label("Closed").on_update(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ArcType(ArcType::Closed),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
RadioEntryData::new("Pie").label("Pie").on_update(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ArcType(ArcType::PieSlice),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
];
|
||||
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder()
|
||||
}
|
||||
@@ -142,7 +168,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
@@ -169,9 +200,26 @@ impl LayoutHolder for ShapeTool {
|
||||
widgets.append(&mut self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::FillColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -180,9 +228,26 @@ impl LayoutHolder for ShapeTool {
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
@@ -191,13 +256,14 @@ impl LayoutHolder for ShapeTool {
|
||||
}
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
match options {
|
||||
ShapeOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
@@ -285,7 +351,7 @@ impl ToolMetadata for ShapeTool {
|
||||
impl ToolTransition for ShapeTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context| ShapeToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
|
||||
tool_abort: Some(ShapeToolMessage::Abort.into()),
|
||||
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
@@ -402,7 +468,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
let ToolMessage::Shape(event) = event else { return self };
|
||||
|
||||
match (self, event) {
|
||||
(_, ShapeToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, ShapeToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
let mouse_position = tool_data
|
||||
.data
|
||||
.snap_manager
|
||||
@@ -484,11 +550,15 @@ impl Fsm for ShapeToolFsmState {
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1)));
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1),
|
||||
});
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3))));
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)),
|
||||
});
|
||||
self
|
||||
}
|
||||
(
|
||||
@@ -542,7 +612,9 @@ impl Fsm for ShapeToolFsmState {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1)));
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::Vertices(n + 1),
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
@@ -571,7 +643,9 @@ impl Fsm for ShapeToolFsmState {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3))));
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::Vertices((n - 1).max(3)),
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
@@ -603,7 +677,9 @@ impl Fsm for ShapeToolFsmState {
|
||||
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()));
|
||||
responses.add(ShapeToolMessage::PointerMove {
|
||||
modifier: ShapeToolData::shape_tool_modifier_keys(),
|
||||
});
|
||||
|
||||
return ShapeToolFsmState::ModifyingGizmo;
|
||||
}
|
||||
@@ -630,7 +706,9 @@ impl Fsm for ShapeToolFsmState {
|
||||
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()));
|
||||
responses.add(ShapeToolMessage::PointerMove {
|
||||
modifier: ShapeToolData::shape_tool_modifier_keys(),
|
||||
});
|
||||
};
|
||||
|
||||
match (resize, rotate, skew) {
|
||||
@@ -710,7 +788,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
|
||||
ShapeToolFsmState::Drawing(tool_data.current_shape)
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove(modifier)) => {
|
||||
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove { modifier }) => {
|
||||
let Some(layer) = tool_data.data.layer else {
|
||||
return ShapeToolFsmState::Ready(shape);
|
||||
};
|
||||
@@ -726,33 +804,33 @@ impl Fsm for ShapeToolFsmState {
|
||||
}
|
||||
|
||||
// Auto-panning
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()];
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove(modifier)) => {
|
||||
(ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove { modifier }) => {
|
||||
let Some(layer) = tool_data.line_data.editing_layer else {
|
||||
return ShapeToolFsmState::Ready(tool_data.current_shape);
|
||||
};
|
||||
|
||||
Line::update_shape(document, input, layer, tool_data, modifier, responses);
|
||||
// Auto-panning
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()];
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => {
|
||||
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove { .. }) => {
|
||||
tool_data.gizmo_manager.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
ShapeToolFsmState::ModifyingGizmo
|
||||
}
|
||||
(ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove(modifier)) => {
|
||||
(ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove { modifier }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()];
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
|
||||
resize_bounds(
|
||||
document,
|
||||
responses,
|
||||
@@ -771,7 +849,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
ShapeToolFsmState::ResizingBounds
|
||||
}
|
||||
(ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove(modifier)) => {
|
||||
(ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove { modifier }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
rotate_bounds(
|
||||
document,
|
||||
@@ -787,7 +865,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
|
||||
ShapeToolFsmState::RotatingBounds
|
||||
}
|
||||
(ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove(_)) => {
|
||||
(ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove { .. }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
skew_bounds(
|
||||
document,
|
||||
@@ -803,7 +881,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
ShapeToolFsmState::SkewingBounds { skew }
|
||||
}
|
||||
|
||||
(_, ShapeToolMessage::PointerMove(_)) => {
|
||||
(_, ShapeToolMessage::PointerMove { .. }) => {
|
||||
let dragging_bounds = tool_data
|
||||
.bounding_box_manager
|
||||
.as_mut()
|
||||
@@ -825,7 +903,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport(_)) => {
|
||||
(ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
@@ -836,7 +914,7 @@ impl Fsm for ShapeToolFsmState {
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport(..)) => self,
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport { .. }) => self,
|
||||
(_, ShapeToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
@@ -891,21 +969,22 @@ impl Fsm for ShapeToolFsmState {
|
||||
ShapeToolFsmState::Ready(tool_data.current_shape)
|
||||
}
|
||||
(_, ShapeToolMessage::WorkingColorChanged) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
});
|
||||
self
|
||||
}
|
||||
(_, ShapeToolMessage::SetShape(shape)) => {
|
||||
(_, ShapeToolMessage::SetShape { shape }) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.data.cleanup(responses);
|
||||
tool_data.current_shape = shape;
|
||||
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(shape)));
|
||||
responses.add(ShapeToolMessage::UpdateOptions {
|
||||
options: ShapeOptionsUpdate::ShapeType(shape),
|
||||
});
|
||||
ShapeToolFsmState::Ready(shape)
|
||||
}
|
||||
(_, ShapeToolMessage::HideShapeTypeWidget(hide)) => {
|
||||
(_, ShapeToolMessage::HideShapeTypeWidget { hide }) => {
|
||||
tool_data.hide_shape_option_widget = hide;
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
self
|
||||
|
||||
@@ -41,7 +41,7 @@ impl Default for SplineOptions {
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum SplineToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
CanvasTransformed,
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
@@ -54,7 +54,7 @@ pub enum SplineToolMessage {
|
||||
PointerMove,
|
||||
PointerOutsideViewport,
|
||||
Undo,
|
||||
UpdateOptions(SplineOptionsUpdate),
|
||||
UpdateOptions { options: SplineOptionsUpdate },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
@@ -93,7 +93,12 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::LineWeight(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
@@ -102,9 +107,26 @@ impl LayoutHolder for SplineTool {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::FillColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -112,9 +134,26 @@ impl LayoutHolder for SplineTool {
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
@@ -126,11 +165,11 @@ impl LayoutHolder for SplineTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for SplineTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Spline(SplineToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Spline(SplineToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
match options {
|
||||
SplineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
SplineOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
@@ -179,7 +218,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Spli
|
||||
impl ToolTransition for SplineTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context: OverlayContext| SplineToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context: OverlayContext| SplineToolMessage::Overlays { context }.into()),
|
||||
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
|
||||
tool_abort: Some(SplineToolMessage::Abort.into()),
|
||||
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
|
||||
@@ -262,7 +301,7 @@ impl Fsm for SplineToolFsmState {
|
||||
let ToolMessage::Spline(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, SplineToolMessage::CanvasTransformed) => self,
|
||||
(_, SplineToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, SplineToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
path_endpoint_overlays(document, shape_editor, &mut overlay_context, preferences);
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
@@ -440,10 +479,9 @@ impl Fsm for SplineToolFsmState {
|
||||
SplineToolFsmState::Ready
|
||||
}
|
||||
(_, SplineToolMessage::WorkingColorChanged) => {
|
||||
responses.add(SplineToolMessage::UpdateOptions(SplineOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
responses.add(SplineToolMessage::UpdateOptions {
|
||||
options: SplineOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
});
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
@@ -568,11 +606,7 @@ mod test_spline_tool {
|
||||
|
||||
assert!(
|
||||
distance < epsilon,
|
||||
"Point {} position mismatch: expected {:?}, got {:?} (distance: {})",
|
||||
i,
|
||||
expected_point,
|
||||
actual_point,
|
||||
distance
|
||||
"Point {i} position mismatch: expected {expected_point:?}, got {actual_point:?} (distance: {distance})"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -606,8 +640,8 @@ mod test_spline_tool {
|
||||
// Verify initial spline has correct number of points and segments
|
||||
let initial_point_count = first_vector.point_domain.ids().len();
|
||||
let initial_segment_count = first_vector.segment_domain.ids().len();
|
||||
assert_eq!(initial_point_count, 3, "Expected 3 points in initial spline, found {}", initial_point_count);
|
||||
assert_eq!(initial_segment_count, 2, "Expected 2 segments in initial spline, found {}", initial_segment_count);
|
||||
assert_eq!(initial_point_count, 3, "Expected 3 points in initial spline, found {initial_point_count}");
|
||||
assert_eq!(initial_segment_count, 2, "Expected 2 segments in initial spline, found {initial_segment_count}");
|
||||
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(spline_layer);
|
||||
|
||||
@@ -641,8 +675,8 @@ mod test_spline_tool {
|
||||
let extended_point_count = extended_vector.point_domain.ids().len();
|
||||
let extended_segment_count = extended_vector.segment_domain.ids().len();
|
||||
|
||||
assert_eq!(extended_point_count, 5, "Expected 5 points in extended spline, found {}", extended_point_count);
|
||||
assert_eq!(extended_segment_count, 4, "Expected 4 segments in extended spline, found {}", extended_segment_count);
|
||||
assert_eq!(extended_point_count, 5, "Expected 5 points in extended spline, found {extended_point_count}");
|
||||
assert_eq!(extended_segment_count, 4, "Expected 4 segments in extended spline, found {extended_segment_count}");
|
||||
|
||||
// Verify the spline node is still the same
|
||||
let extended_spline_node = find_spline(document, spline_layer).expect("Spline node not found after extension");
|
||||
@@ -677,7 +711,7 @@ mod test_spline_tool {
|
||||
|
||||
// Evaluate the graph to ensure everything is processed
|
||||
if let Err(e) = editor.eval_graph().await {
|
||||
panic!("Graph evaluation failed: {}", e);
|
||||
panic!("Graph evaluation failed: {e}");
|
||||
}
|
||||
|
||||
// Get the layer and vector data
|
||||
@@ -717,7 +751,7 @@ mod test_spline_tool {
|
||||
|
||||
// Evaluating the graph to ensure everything is processed
|
||||
if let Err(e) = editor.eval_graph().await {
|
||||
panic!("Graph evaluation failed: {}", e);
|
||||
panic!("Graph evaluation failed: {e}");
|
||||
}
|
||||
|
||||
// Get the layer and vector data
|
||||
@@ -755,7 +789,7 @@ mod test_spline_tool {
|
||||
|
||||
// Evaluating the graph to ensure everything is processed
|
||||
if let Err(e) = editor.eval_graph().await {
|
||||
panic!("Graph evaluation failed: {}", e);
|
||||
panic!("Graph evaluation failed: {e}");
|
||||
}
|
||||
|
||||
// Get the layer and vector data
|
||||
@@ -794,7 +828,7 @@ mod test_spline_tool {
|
||||
|
||||
editor.handle_message(SplineToolMessage::Confirm).await;
|
||||
if let Err(e) = editor.eval_graph().await {
|
||||
panic!("Graph evaluation failed: {}", e);
|
||||
panic!("Graph evaluation failed: {e}");
|
||||
}
|
||||
|
||||
// Get the layer and vector data
|
||||
@@ -851,8 +885,8 @@ mod test_spline_tool {
|
||||
let point_count = vector.point_domain.ids().len();
|
||||
let segment_count = vector.segment_domain.ids().len();
|
||||
|
||||
assert_eq!(point_count, 3, "Expected 3 points in the spline, found {}", point_count);
|
||||
assert_eq!(segment_count, 2, "Expected 2 segments in the spline, found {}", segment_count);
|
||||
assert_eq!(point_count, 3, "Expected 3 points in the spline, found {point_count}");
|
||||
assert_eq!(segment_count, 2, "Expected 2 segments in the spline, found {segment_count}");
|
||||
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(spline_layer);
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ pub enum TextToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
Overlays(OverlayContext),
|
||||
Overlays { context: OverlayContext },
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
@@ -70,7 +70,7 @@ pub enum TextToolMessage {
|
||||
PointerOutsideViewport { center: Key, lock_ratio: Key },
|
||||
TextChange { new_text: String, is_left_or_right_click: bool },
|
||||
UpdateBounds { new_text: String },
|
||||
UpdateOptions(TextOptionsUpdate),
|
||||
UpdateOptions { options: TextOptionsUpdate },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
@@ -100,20 +100,24 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
||||
let font = FontInput::new(&tool.options.font_name, &tool.options.font_style)
|
||||
.is_style_picker(false)
|
||||
.on_update(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
})
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
},
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let style = FontInput::new(&tool.options.font_name, &tool.options.font_style)
|
||||
.is_style_picker(true)
|
||||
.on_update(|font_input: &FontInput| {
|
||||
TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
})
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::Font {
|
||||
family: font_input.font_family.clone(),
|
||||
style: font_input.font_style.clone(),
|
||||
},
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
@@ -123,7 +127,12 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
||||
.int()
|
||||
.min(1.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FontSize(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let line_height_ratio = NumberInput::new(Some(tool.options.line_height_ratio))
|
||||
.label("Line Height")
|
||||
@@ -131,14 +140,22 @@ fn create_text_widgets(tool: &TextTool) -> Vec<WidgetHolder> {
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.step(0.1)
|
||||
.on_update(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap())).into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::LineHeightRatio(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder();
|
||||
let align_entries: Vec<_> = [TextAlign::Left, TextAlign::Center, TextAlign::Right, TextAlign::JustifyLeft]
|
||||
.into_iter()
|
||||
.map(|align| {
|
||||
RadioEntryData::new(format!("{align:?}"))
|
||||
.label(align.to_string())
|
||||
.on_update(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::Align(align)).into())
|
||||
RadioEntryData::new(format!("{align:?}")).label(align.to_string()).on_update(move |_| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::Align(align),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let align = RadioInput::new(align_entries).selected_index(Some(tool.options.align as u32)).widget_holder();
|
||||
@@ -164,9 +181,26 @@ impl LayoutHolder for TextTool {
|
||||
widgets.append(&mut self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
|_| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FillColor(None),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
|color_type: ToolColorType| {
|
||||
WidgetCallback::new(move |_| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FillColorType(color_type.clone()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
},
|
||||
|color: &ColorInput| {
|
||||
TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
@@ -176,11 +210,11 @@ impl LayoutHolder for TextTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for TextTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
let ToolMessage::Text(TextToolMessage::UpdateOptions(action)) = message else {
|
||||
let ToolMessage::Text(TextToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
match options {
|
||||
TextOptionsUpdate::Font { family, style } => {
|
||||
self.options.font_name = family;
|
||||
self.options.font_style = style;
|
||||
@@ -237,7 +271,7 @@ impl ToolTransition for TextTool {
|
||||
canvas_transformed: None,
|
||||
tool_abort: Some(TextToolMessage::Abort.into()),
|
||||
working_color_changed: Some(TextToolMessage::WorkingColorChanged.into()),
|
||||
overlay_provider: Some(|overlay_context| TextToolMessage::Overlays(overlay_context).into()),
|
||||
overlay_provider: Some(|context| TextToolMessage::Overlays { context }.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -474,7 +508,7 @@ impl Fsm for TextToolFsmState {
|
||||
|
||||
let ToolMessage::Text(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(TextToolFsmState::Editing, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
(TextToolFsmState::Editing, TextToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
responses.add(FrontendMessage::DisplayEditableTextboxTransform {
|
||||
transform: document.metadata().transform_to_viewport(tool_data.layer).to_cols_array(),
|
||||
});
|
||||
@@ -490,7 +524,7 @@ impl Fsm for TextToolFsmState {
|
||||
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(_, TextToolMessage::Overlays(mut overlay_context)) => {
|
||||
(_, TextToolMessage::Overlays { context: mut overlay_context }) => {
|
||||
if matches!(self, Self::Placing) {
|
||||
// Get the updated selection box bounds
|
||||
let quad = Quad::from_box(tool_data.cached_resize_bounds);
|
||||
@@ -852,10 +886,9 @@ impl Fsm for TextToolFsmState {
|
||||
TextToolFsmState::Editing
|
||||
}
|
||||
(_, TextToolMessage::WorkingColorChanged) => {
|
||||
responses.add(TextToolMessage::UpdateOptions(TextOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
responses.add(TextToolMessage::UpdateOptions {
|
||||
options: TextOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
|
||||
});
|
||||
self
|
||||
}
|
||||
(TextToolFsmState::Editing, TextToolMessage::Abort) => {
|
||||
|
||||
@@ -8,10 +8,8 @@ use glam::DVec2;
|
||||
#[impl_message(Message, ToolMessage, TransformLayer)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransformLayerMessage {
|
||||
// Overlays
|
||||
Overlays(OverlayContext),
|
||||
|
||||
// Messages
|
||||
Overlays { context: OverlayContext },
|
||||
ApplyTransformOperation { final_transform: bool },
|
||||
BeginTransformOperation { operation: TransformType },
|
||||
BeginGrab,
|
||||
|
||||
@@ -16,7 +16,7 @@ use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::{Vector, VectorModificationType};
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays(context).into();
|
||||
const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays { context }.into();
|
||||
|
||||
// TODO: Get these from the input mapper
|
||||
const SLOW_KEY: Key = Key::Shift;
|
||||
@@ -69,6 +69,7 @@ pub struct TransformLayerMessageHandler {
|
||||
was_grabbing: bool,
|
||||
}
|
||||
|
||||
#[message_handler_data]
|
||||
impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for TransformLayerMessageHandler {
|
||||
fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque<Message>, context: TransformLayerMessageContext) {
|
||||
let TransformLayerMessageContext {
|
||||
@@ -172,7 +173,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
|
||||
match message {
|
||||
// Overlays
|
||||
TransformLayerMessage::Overlays(mut overlay_context) => {
|
||||
TransformLayerMessage::Overlays { context: mut overlay_context } => {
|
||||
if !overlay_context.visibility_settings.transform_measurement() {
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +189,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
|
||||
let format_rounded = |value: f64, precision: usize| {
|
||||
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
format!("{value:.precision$}").trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
} else {
|
||||
self.typing.string.clone()
|
||||
}
|
||||
@@ -304,7 +305,9 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
|
||||
if final_transform {
|
||||
self.was_grabbing = false;
|
||||
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::RemoveProvider {
|
||||
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
|
||||
});
|
||||
}
|
||||
}
|
||||
TransformLayerMessage::BeginTransformOperation { operation } => {
|
||||
@@ -343,7 +346,9 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
_ => unreachable!(), // Safe because the match arms are exhaustive
|
||||
};
|
||||
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::AddProvider {
|
||||
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
|
||||
});
|
||||
// Find a way better than this hack
|
||||
responses.add(TransformLayerMessage::PointerMove {
|
||||
slow_key: SLOW_KEY,
|
||||
@@ -428,7 +433,9 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
if chain_operation {
|
||||
responses.add(TransformLayerMessage::ApplyTransformOperation { final_transform: false });
|
||||
} else {
|
||||
responses.add(OverlaysMessage::AddProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::AddProvider {
|
||||
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
|
||||
});
|
||||
}
|
||||
responses.add(TransformLayerMessage::BeginTransformOperation { operation: transform_type });
|
||||
responses.add(TransformLayerMessage::PointerMove {
|
||||
@@ -465,7 +472,9 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||
}
|
||||
|
||||
responses.add(SelectToolMessage::PivotShift { offset: None, flush: false });
|
||||
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
|
||||
responses.add(OverlaysMessage::RemoveProvider {
|
||||
provider: TRANSFORM_GRS_OVERLAY_PROVIDER,
|
||||
});
|
||||
}
|
||||
TransformLayerMessage::ConstrainX => {
|
||||
let pivot = document_to_viewport.transform_point2(self.local_pivot);
|
||||
@@ -883,7 +892,7 @@ mod test_transform_layer {
|
||||
let final_transform = get_layer_transform(&mut editor, layer).await.unwrap();
|
||||
|
||||
let translation_diff = (final_transform.translation - original_transform.translation).length();
|
||||
assert!(translation_diff > 10., "Transform should have changed after applying transformation. Diff: {}", translation_diff);
|
||||
assert!(translation_diff > 10., "Transform should have changed after applying transformation. Diff: {translation_diff}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -918,9 +927,7 @@ mod test_transform_layer {
|
||||
// Verify transform is either restored to original OR reset to identity
|
||||
assert!(
|
||||
(final_translation - original_translation).length() < 5. || final_translation.length() < 0.001,
|
||||
"Transform neither restored to original nor reset to identity. Original: {:?}, Final: {:?}",
|
||||
original_translation,
|
||||
final_translation
|
||||
"Transform neither restored to original nor reset to identity. Original: {original_translation:?}, Final: {final_translation:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -949,11 +956,11 @@ mod test_transform_layer {
|
||||
editor.handle_message(TransformLayerMessage::ApplyTransformOperation { final_transform: true }).await;
|
||||
|
||||
let final_transform = get_layer_transform(&mut editor, layer).await.unwrap();
|
||||
println!("Final transform: {:?}", final_transform);
|
||||
println!("Final transform: {final_transform:?}");
|
||||
|
||||
// Check matrix components have changed (rotation affects matrix2)
|
||||
let matrix_diff = (final_transform.matrix2.x_axis - original_transform.matrix2.x_axis).length();
|
||||
assert!(matrix_diff > 0.1, "Rotation should have changed the transform matrix. Diff: {}", matrix_diff);
|
||||
assert!(matrix_diff > 0.1, "Rotation should have changed the transform matrix. Diff: {matrix_diff}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -975,7 +982,7 @@ mod test_transform_layer {
|
||||
assert!(!after_cancel.translation.y.is_nan(), "Transform is NaN after cancel");
|
||||
|
||||
let translation_diff = (after_cancel.translation - original_transform.translation).length();
|
||||
assert!(translation_diff < 1., "Translation component changed too much: {}", translation_diff);
|
||||
assert!(translation_diff < 1., "Translation component changed too much: {translation_diff}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1010,9 +1017,7 @@ mod test_transform_layer {
|
||||
|
||||
assert!(
|
||||
scale_diff_x > 0.1 || scale_diff_y > 0.1,
|
||||
"Scaling should have changed the transform matrix. Diffs: x={}, y={}",
|
||||
scale_diff_x,
|
||||
scale_diff_y
|
||||
"Scaling should have changed the transform matrix. Diffs: x={scale_diff_x}, y={scale_diff_y}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1041,7 +1046,7 @@ mod test_transform_layer {
|
||||
|
||||
// Also check translation component is similar
|
||||
let translation_diff = (after_cancel.translation - original_transform.translation).length();
|
||||
assert!(translation_diff < 1., "Translation component changed too much: {}", translation_diff);
|
||||
assert!(translation_diff < 1., "Translation component changed too much: {translation_diff}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1068,9 +1073,7 @@ mod test_transform_layer {
|
||||
let actual_translation = after_grab_transform.translation - original_transform.translation;
|
||||
assert!(
|
||||
(actual_translation - expected_translation).length() < 1e-5,
|
||||
"Expected translation of {:?}, got {:?}",
|
||||
expected_translation,
|
||||
actual_translation
|
||||
"Expected translation of {expected_translation:?}, got {actual_translation:?}"
|
||||
);
|
||||
|
||||
// 2. Chain to rotation - from current position to create ~45 degree rotation
|
||||
@@ -1106,9 +1109,7 @@ mod test_transform_layer {
|
||||
let after_scale_det = after_scale_transform.matrix2.determinant();
|
||||
assert!(
|
||||
after_scale_det >= 2. * before_scale_det,
|
||||
"Scale should increase the determinant of the matrix (before: {}, after: {})",
|
||||
before_scale_det,
|
||||
after_scale_det
|
||||
"Scale should increase the determinant of the matrix (before: {before_scale_det}, after: {after_scale_det})"
|
||||
);
|
||||
|
||||
editor.handle_message(TransformLayerMessage::ApplyTransformOperation { final_transform: true }).await;
|
||||
@@ -1140,8 +1141,8 @@ mod test_transform_layer {
|
||||
let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length();
|
||||
let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length();
|
||||
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {}", scale_x);
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {}", scale_y);
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {scale_x}");
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {scale_y}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1166,8 +1167,8 @@ mod test_transform_layer {
|
||||
let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length();
|
||||
let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length();
|
||||
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {}", scale_x);
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {}", scale_y);
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {scale_x}");
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {scale_y}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1182,11 +1183,7 @@ mod test_transform_layer {
|
||||
|
||||
// Rotate the document view (45 degrees)
|
||||
editor.handle_message(NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: false }).await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
angle_radians: (45. as f64).to_radians(),
|
||||
})
|
||||
.await;
|
||||
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 45_f64.to_radians() }).await;
|
||||
editor.handle_message(TransformLayerMessage::BeginRotate).await;
|
||||
|
||||
editor.handle_message(TransformLayerMessage::TypeDigit { digit: 9 }).await;
|
||||
@@ -1201,7 +1198,7 @@ mod test_transform_layer {
|
||||
|
||||
// Normalize angle between 0 and 360
|
||||
let angle_change = ((angle_change % 360.) + 360.) % 360.;
|
||||
assert!((angle_change - 90.).abs() < 0.1, "Expected rotation of 90 degrees, got: {}", angle_change);
|
||||
assert!((angle_change - 90.).abs() < 0.1, "Expected rotation of 90 degrees, got: {angle_change}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1256,8 +1253,8 @@ mod test_transform_layer {
|
||||
// Verify scale is near zero.
|
||||
let scale_x = near_zero_transform.matrix2.x_axis.length();
|
||||
let scale_y = near_zero_transform.matrix2.y_axis.length();
|
||||
assert!(scale_x < 0.001, "Scale factor X should be near zero, got: {}", scale_x);
|
||||
assert!(scale_y < 0.001, "Scale factor Y should be near zero, got: {}", scale_y);
|
||||
assert!(scale_x < 0.001, "Scale factor X should be near zero, got: {scale_x}");
|
||||
assert!(scale_y < 0.001, "Scale factor Y should be near zero, got: {scale_y}");
|
||||
assert!(scale_x > 0., "Scale factor X should not be exactly zero");
|
||||
assert!(scale_y > 0., "Scale factor Y should not be exactly zero");
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::common_functionality::shape_editor::ShapeState;
|
||||
use super::tool_messages::*;
|
||||
use crate::messages::broadcast::BroadcastMessage;
|
||||
use crate::messages::broadcast::broadcast_event::BroadcastEvent;
|
||||
use crate::messages::broadcast::event::EventMessage;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, LayoutKeysGroup, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
@@ -141,7 +141,7 @@ impl DocumentToolData {
|
||||
layout_target: LayoutTarget::WorkingColors,
|
||||
});
|
||||
|
||||
responses.add(BroadcastMessage::TriggerEvent(BroadcastEvent::WorkingColorChanged));
|
||||
responses.add(BroadcastMessage::TriggerEvent(EventMessage::WorkingColorChanged));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ pub trait ToolTransition {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap;
|
||||
|
||||
fn activate(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut subscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, event: BroadcastEvent| {
|
||||
let mut subscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, event: EventMessage| {
|
||||
if let Some(mapping) = broadcast_to_tool_mapping {
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: event,
|
||||
@@ -168,32 +168,32 @@ pub trait ToolTransition {
|
||||
};
|
||||
|
||||
let event_to_tool_map = self.event_to_message_map();
|
||||
subscribe_message(event_to_tool_map.canvas_transformed, BroadcastEvent::CanvasTransformed);
|
||||
subscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
|
||||
subscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
|
||||
subscribe_message(event_to_tool_map.working_color_changed, BroadcastEvent::WorkingColorChanged);
|
||||
subscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
||||
subscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
||||
subscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
||||
subscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
||||
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
||||
responses.add(OverlaysMessage::AddProvider(overlay_provider));
|
||||
responses.add(OverlaysMessage::AddProvider { provider: overlay_provider });
|
||||
}
|
||||
}
|
||||
|
||||
fn deactivate(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut unsubscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, event: BroadcastEvent| {
|
||||
let mut unsubscribe_message = |broadcast_to_tool_mapping: Option<ToolMessage>, event: EventMessage| {
|
||||
if let Some(mapping) = broadcast_to_tool_mapping {
|
||||
responses.add(BroadcastMessage::UnsubscribeEvent {
|
||||
on: event,
|
||||
message: Box::new(mapping.into()),
|
||||
send: Box::new(mapping.into()),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let event_to_tool_map = self.event_to_message_map();
|
||||
unsubscribe_message(event_to_tool_map.canvas_transformed, BroadcastEvent::CanvasTransformed);
|
||||
unsubscribe_message(event_to_tool_map.tool_abort, BroadcastEvent::ToolAbort);
|
||||
unsubscribe_message(event_to_tool_map.selection_changed, BroadcastEvent::SelectionChanged);
|
||||
unsubscribe_message(event_to_tool_map.working_color_changed, BroadcastEvent::WorkingColorChanged);
|
||||
unsubscribe_message(event_to_tool_map.canvas_transformed, EventMessage::CanvasTransformed);
|
||||
unsubscribe_message(event_to_tool_map.tool_abort, EventMessage::ToolAbort);
|
||||
unsubscribe_message(event_to_tool_map.selection_changed, EventMessage::SelectionChanged);
|
||||
unsubscribe_message(event_to_tool_map.working_color_changed, EventMessage::WorkingColorChanged);
|
||||
if let Some(overlay_provider) = event_to_tool_map.overlay_provider {
|
||||
responses.add(OverlaysMessage::RemoveProvider(overlay_provider));
|
||||
responses.add(OverlaysMessage::RemoveProvider { provider: overlay_provider });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::consts::FILE_SAVE_SUFFIX;
|
||||
use crate::messages::frontend::utility_types::{ExportBounds, FileType};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
@@ -160,7 +159,7 @@ impl NodeGraphExecutor {
|
||||
|
||||
self.futures.insert(execution_id, ExecutionContext { export_config: None, document_id });
|
||||
|
||||
Ok(DeferMessage::SetGraphSubmissionIndex(execution_id).into())
|
||||
Ok(DeferMessage::SetGraphSubmissionIndex { execution_id }.into())
|
||||
}
|
||||
|
||||
/// Evaluates a node graph, computing the entire graph
|
||||
@@ -229,18 +228,11 @@ impl NodeGraphExecutor {
|
||||
};
|
||||
|
||||
let ExportConfig {
|
||||
file_type,
|
||||
file_name,
|
||||
size,
|
||||
scale_factor,
|
||||
..
|
||||
file_type, name, size, scale_factor, ..
|
||||
} = export_config;
|
||||
|
||||
let file_suffix = &format!(".{file_type:?}").to_lowercase();
|
||||
let name = match file_name.ends_with(FILE_SAVE_SUFFIX) {
|
||||
true => file_name.replace(FILE_SAVE_SUFFIX, file_suffix),
|
||||
false => file_name + file_suffix,
|
||||
};
|
||||
let name = name + file_suffix;
|
||||
|
||||
if file_type == FileType::Svg {
|
||||
responses.add(FrontendMessage::TriggerSaveFile { name, content: svg.into_bytes() });
|
||||
@@ -288,7 +280,10 @@ impl NodeGraphExecutor {
|
||||
} else {
|
||||
self.process_node_graph_output(node_graph_output, responses)?;
|
||||
}
|
||||
responses.add(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id));
|
||||
responses.add(DeferMessage::TriggerGraphRun {
|
||||
execution_id,
|
||||
document_id: execution_context.document_id,
|
||||
});
|
||||
|
||||
// Update the Data panel on the frontend using the value of the inspect result.
|
||||
if let Some(inspect_result) = (self.previous_node_to_inspect.is_some()).then_some(inspect_result).flatten() {
|
||||
@@ -436,7 +431,7 @@ mod test {
|
||||
let monitor_node = DocumentNode {
|
||||
inputs: vec![input],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(graph_craft::generic!(T)),
|
||||
call_argument: graph_craft::generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ pub struct GraphUpdate {
|
||||
|
||||
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ExportConfig {
|
||||
pub file_name: String,
|
||||
pub name: String,
|
||||
pub file_type: FileType,
|
||||
pub scale_factor: f64,
|
||||
pub bounds: ExportBounds,
|
||||
@@ -432,7 +432,7 @@ pub struct InspectResult {
|
||||
|
||||
impl InspectResult {
|
||||
pub fn take_data(&mut self) -> Option<Arc<dyn std::any::Any + Send + Sync + 'static>> {
|
||||
return self.introspected_data.clone();
|
||||
self.introspected_data.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ impl InspectState {
|
||||
let monitor_node = DocumentNode {
|
||||
inputs: vec![NodeInput::node(inspect_node, 0)], // Connect to the primary output of the inspect node
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::memo::monitor::IDENTIFIER),
|
||||
manual_composition: Some(graph_craft::generic!(T)),
|
||||
call_argument: graph_craft::generic!(T),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -60,3 +60,9 @@ pub trait HierarchicalTree {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtractField {
|
||||
fn field_types() -> Vec<(String, usize)>;
|
||||
fn path() -> &'static str;
|
||||
fn print_field_types();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ impl MessageData {
|
||||
#[derive(Debug)]
|
||||
pub struct DebugMessageTree {
|
||||
name: String,
|
||||
fields: Option<Vec<String>>,
|
||||
variants: Option<Vec<DebugMessageTree>>,
|
||||
message_handler: Option<MessageData>,
|
||||
message_handler_data: Option<MessageData>,
|
||||
@@ -36,6 +37,7 @@ impl DebugMessageTree {
|
||||
pub fn new(name: &str) -> DebugMessageTree {
|
||||
DebugMessageTree {
|
||||
name: name.to_string(),
|
||||
fields: None,
|
||||
variants: None,
|
||||
message_handler: None,
|
||||
message_handler_data: None,
|
||||
@@ -43,6 +45,10 @@ impl DebugMessageTree {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_fields(&mut self, fields: Vec<String>) {
|
||||
self.fields = Some(fields);
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, path: &'static str) {
|
||||
self.path = path;
|
||||
}
|
||||
@@ -67,6 +73,10 @@ impl DebugMessageTree {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn fields(&self) -> Option<&Vec<String>> {
|
||||
self.fields.as_ref()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &'static str {
|
||||
self.path
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user