This commit is contained in:
mtvare6
2025-09-01 16:21:28 +05:30
488 changed files with 27610 additions and 32159 deletions
+5 -11
View File
@@ -12,20 +12,17 @@ license = "Apache-2.0"
[features]
default = ["wasm"]
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
wasm = ["wasm-bindgen", "graphene-std/wasm"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
tauri = ["ron", "decouple-execution"]
decouple-execution = []
resvg = ["graphene-std/resvg"]
vello = ["graphene-std/vello", "resvg"]
ron = ["dep:ron"]
[dependencies]
# Local dependencies
graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true }
graphene-std = { workspace = true } # NOTE: `graphene-core` should not be added here because `graphene-std` re-exports its contents
preprocessor = { workspace = true }
# Workspace dependencies
@@ -35,7 +32,6 @@ bitflags = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
bezier-rs = { workspace = true }
kurbo = { workspace = true }
futures = { workspace = true }
glam = { workspace = true }
@@ -46,17 +42,15 @@ num_enum = { workspace = true }
usvg = { workspace = true }
once_cell = { workspace = true }
web-sys = { workspace = true }
# Required dependencies
spin = "0.9.8"
vello = { workspace = true }
base64 = { workspace = true }
spin = { workspace = true }
# Optional local dependencies
wgpu-executor = { workspace = true, optional = true }
# Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
[dev-dependencies]
# Workspace dependencies
+6 -2
View File
@@ -106,6 +106,7 @@ pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
// PEN TOOL
@@ -126,6 +127,9 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
pub const ARC_SNAP_THRESHOLD: f64 = 5.;
pub const ARC_SWEEP_GIZMO_RADIUS: f64 = 14.;
pub const ARC_SWEEP_GIZMO_TEXT_HEIGHT: f64 = 12.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
// SCROLLBARS
@@ -146,10 +150,10 @@ 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 = 15;
pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1;
// INPUT
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
+25 -49
View File
@@ -1,11 +1,11 @@
use crate::messages::debug::utility_types::MessageLoggingVerbosity;
use crate::messages::defer::DeferMessageContext;
use crate::messages::dialog::DialogMessageContext;
use crate::messages::layout::layout_message_handler::LayoutMessageContext;
use crate::messages::prelude::*;
#[derive(Debug, Default)]
pub struct Dispatcher {
buffered_queue: Option<Vec<VecDeque<Message>>>,
message_queues: Vec<VecDeque<Message>>,
pub responses: Vec<FrontendMessage>,
pub message_handlers: DispatcherMessageHandlers,
@@ -14,8 +14,10 @@ pub struct Dispatcher {
#[derive(Debug, Default)]
pub struct DispatcherMessageHandlers {
animation_message_handler: AnimationMessageHandler,
app_window_message_handler: AppWindowMessageHandler,
broadcast_message_handler: BroadcastMessageHandler,
debug_message_handler: DebugMessageHandler,
defer_message_handler: DeferMessageHandler,
dialog_message_handler: DialogMessageHandler,
globals_message_handler: GlobalsMessageHandler,
input_preprocessor_message_handler: InputPreprocessorMessageHandler,
@@ -24,7 +26,6 @@ pub struct DispatcherMessageHandlers {
pub portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler,
tool_message_handler: ToolMessageHandler,
workspace_message_handler: WorkspaceMessageHandler,
}
impl DispatcherMessageHandlers {
@@ -50,7 +51,10 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(BroadcastEventDiscriminant::AnimationFrame))];
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(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.
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
@@ -90,14 +94,6 @@ impl Dispatcher {
pub fn handle_message<T: Into<Message>>(&mut self, message: T, process_after_all_current: bool) {
let message = message.into();
// Add all additional messages to the buffer if it exists (except from the end buffer message)
if !matches!(message, Message::EndBuffer { .. }) {
if let Some(buffered_queue) = &mut self.buffered_queue {
Self::schedule_execution(buffered_queue, true, [message]);
return;
}
}
// If we are not maintaining the buffer, simply add to the current queue
Self::schedule_execution(&mut self.message_queues, process_after_all_current, [message]);
@@ -129,14 +125,24 @@ impl Dispatcher {
Message::Animation(message) => {
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
}
Message::AppWindow(message) => {
self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ());
}
Message::Broadcast(message) => self.message_handlers.broadcast_message_handler.process_message(message, &mut queue, ()),
Message::Debug(message) => {
self.message_handlers.debug_message_handler.process_message(message, &mut queue, ());
}
Message::Defer(message) => {
let context = DeferMessageContext {
portfolio: &self.message_handlers.portfolio_message_handler,
};
self.message_handlers.defer_message_handler.process_message(message, &mut queue, context);
}
Message::Dialog(message) => {
let context = DialogMessageContext {
portfolio: &self.message_handlers.portfolio_message_handler,
preferences: &self.message_handlers.preferences_message_handler,
viewport_bounds: &self.message_handlers.input_preprocessor_message_handler.viewport_bounds,
};
self.message_handlers.dialog_message_handler.process_message(message, &mut queue, context);
}
@@ -204,11 +210,14 @@ impl Dispatcher {
self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ());
}
Message::Tool(message) => {
let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap();
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else {
warn!("Called ToolMessage without an active document.\nGot {message:?}");
return;
};
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
warn!("Called ToolMessage with an invalid active document.\nGot {message:?}");
return;
};
let context = ToolMessageContext {
document_id,
@@ -221,44 +230,10 @@ impl Dispatcher {
self.message_handlers.tool_message_handler.process_message(message, &mut queue, context);
}
Message::Workspace(message) => {
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());
}
Message::NoOp => {}
Message::Batched { messages } => {
messages.iter().for_each(|message| self.handle_message(message.to_owned(), false));
}
Message::StartBuffer => {
self.buffered_queue = Some(std::mem::take(&mut self.message_queues));
}
Message::EndBuffer { render_metadata } => {
// Assign the message queue to the currently buffered queue
if let Some(buffered_queue) = self.buffered_queue.take() {
self.cleanup_queues(false);
assert!(self.message_queues.is_empty(), "message queues are always empty when ending a buffer");
self.message_queues = buffered_queue;
};
let graphene_std::renderer::RenderMetadata {
upstream_footprints: footprints,
local_transforms,
first_instance_source_id,
click_targets,
clip_targets,
} = render_metadata;
// Run these update state messages immediately
let messages = [
DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints: footprints,
local_transforms,
first_instance_source_id,
},
DocumentMessage::UpdateClickTargets { click_targets },
DocumentMessage::UpdateClipTargets { clip_targets },
];
Self::schedule_execution(&mut self.message_queues, false, messages.map(Message::from));
}
}
// If there are child messages, append the queue to the list of queues
@@ -467,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);
}
@@ -522,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)]
@@ -0,0 +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,
}
@@ -0,0 +1,52 @@
use crate::messages::app_window::AppWindowMessage;
use crate::messages::prelude::*;
use graphite_proc_macros::{ExtractField, message_handler_data};
#[derive(Debug, Clone, Default, ExtractField)]
pub struct AppWindowMessageHandler {
platform: AppWindowPlatform,
maximized: bool,
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::AppWindowMaximize => {
self.maximized = !self.maximized;
responses.add(FrontendMessage::UpdateWindowState {
maximized: self.maximized,
minimized: self.minimized,
});
}
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);
}
}
}
fn actions(&self) -> ActionList {
actions!(AppWindowMessageDiscriminant;)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum AppWindowPlatform {
#[default]
Web,
Windows,
Mac,
Linux,
}
+7
View File
@@ -0,0 +1,7 @@
mod app_window_message;
pub mod app_window_message_handler;
#[doc(inline)]
pub use app_window_message::{AppWindowMessage, AppWindowMessageDiscriminant};
#[doc(inline)]
pub use app_window_message_handler::AppWindowMessageHandler;
@@ -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;)
}
}
@@ -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 -1
View File
@@ -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};
@@ -0,0 +1,11 @@
use crate::messages::prelude::*;
#[impl_message(Message, Defer)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum DeferMessage {
SetGraphSubmissionIndex { execution_id: u64 },
TriggerGraphRun { execution_id: u64, document_id: DocumentId },
AfterGraphRun { messages: Vec<Message> },
TriggerNavigationReady,
AfterNavigationReady { messages: Vec<Message> },
}
@@ -0,0 +1,57 @@
use crate::messages::prelude::*;
#[derive(ExtractField)]
pub struct DeferMessageContext<'a> {
pub portfolio: &'a PortfolioMessageHandler,
}
#[derive(Debug, Default, ExtractField)]
pub struct DeferMessageHandler {
after_graph_run: HashMap<DocumentId, Vec<(u64, Message)>>,
after_viewport_resize: Vec<Message>,
current_graph_submission_id: u64,
}
#[message_handler_data]
impl MessageHandler<DeferMessage, DeferMessageContext<'_>> for DeferMessageHandler {
fn process_message(&mut self, message: DeferMessage, responses: &mut VecDeque<Message>, context: DeferMessageContext) {
match message {
DeferMessage::AfterGraphRun { mut messages } => {
let after_graph_run = self.after_graph_run.entry(context.portfolio.active_document_id.unwrap_or(DocumentId(0))).or_default();
after_graph_run.extend(messages.drain(..).map(|m| (self.current_graph_submission_id, m)));
responses.add(NodeGraphMessage::RunDocumentGraph);
}
DeferMessage::AfterNavigationReady { messages } => {
self.after_viewport_resize.extend_from_slice(&messages);
}
DeferMessage::SetGraphSubmissionIndex { execution_id } => {
self.current_graph_submission_id = execution_id + 1;
}
DeferMessage::TriggerGraphRun { execution_id, document_id } => {
let after_graph_run = self.after_graph_run.entry(document_id).or_default();
if after_graph_run.is_empty() {
return;
}
// Find the index of the last message we can process
let split = after_graph_run.partition_point(|&(id, _)| id <= execution_id);
let elements = after_graph_run.drain(..split);
for (_, message) in elements.rev() {
responses.add_front(message);
}
for (&document_id, messages) in self.after_graph_run.iter() {
if !messages.is_empty() {
responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
}
}
}
DeferMessage::TriggerNavigationReady => {
for message in self.after_viewport_resize.drain(..).rev() {
responses.add_front(message);
}
}
}
}
advertise_actions!(DeferMessageDiscriminant;
);
}
+7
View File
@@ -0,0 +1,7 @@
mod defer_message;
mod defer_message_handler;
#[doc(inline)]
pub use defer_message::{DeferMessage, DeferMessageDiscriminant};
#[doc(inline)]
pub use defer_message_handler::{DeferMessageContext, DeferMessageHandler};
@@ -33,6 +33,9 @@ pub enum DialogMessage {
RequestLicensesDialogWithLocalizedCommitDate {
localized_commit_year: String,
},
RequestLicensesThirdPartyDialogWithLicenseText {
license_text: String,
},
RequestNewDocumentDialog,
RequestPreferencesDialog,
}
@@ -1,10 +1,14 @@
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::*;
#[derive(ExtractField)]
pub struct DialogMessageContext<'a> {
pub portfolio: &'a PortfolioMessageHandler,
pub viewport_bounds: &'a ViewportBounds,
pub preferences: &'a PreferencesMessageHandler,
}
@@ -19,11 +23,15 @@ pub struct DialogMessageHandler {
#[message_handler_data]
impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHandler {
fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque<Message>, context: DialogMessageContext) {
let DialogMessageContext { portfolio, preferences } = context;
let DialogMessageContext {
portfolio,
preferences,
viewport_bounds,
} = context;
match message {
DialogMessage::ExportDialog(message) => self.export_dialog.process_message(message, responses, ExportDialogMessageContext { portfolio }),
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, ()),
DialogMessage::NewDocumentDialog(message) => self.new_document_dialog.process_message(message, responses, NewDocumentDialogMessageContext { viewport_bounds }),
DialogMessage::PreferencesDialog(message) => self.preferences_dialog.process_message(message, responses, PreferencesDialogMessageContext { preferences }),
DialogMessage::CloseAllDocumentsWithConfirmation => {
@@ -96,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(),
];
@@ -111,24 +115,24 @@ impl LayoutHolder for ExportDialogMessageHandler {
(ExportBounds::Selection, "Selection".to_string(), !self.has_selection),
];
let artboards = self.artboards.iter().map(|(&layer, name)| (ExportBounds::Artboard(layer), name.to_string(), false)).collect();
let groups = [standard_bounds, artboards];
let choices = [standard_bounds, artboards];
let current_bounds = if !self.has_selection && self.bounds == ExportBounds::Selection {
ExportBounds::AllArtwork
} else {
self.bounds
};
let index = groups.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap();
let index = choices.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap();
let mut entries = groups
let mut entries = choices
.into_iter()
.map(|group| {
group
.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,19 +144,19 @@ 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 mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
let transparent_background = vec![
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(&mut 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())
.for_label(checkbox_id.clone())
.on_update(move |value: &CheckboxInput| ExportDialogMessage::TransparentBackground { transparent: value.checked }.into())
.for_label(checkbox_id)
.widget_holder(),
];
@@ -4,4 +4,4 @@ mod new_document_dialog_message_handler;
#[doc(inline)]
pub use new_document_dialog_message::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant};
#[doc(inline)]
pub use new_document_dialog_message_handler::NewDocumentDialogMessageHandler;
pub use new_document_dialog_message_handler::{NewDocumentDialogMessageContext, NewDocumentDialogMessageHandler};
@@ -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,
}
@@ -1,8 +1,13 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
use crate::messages::{input_mapper::utility_types::input_mouse::ViewportBounds, layout::utility_types::widget_prelude::*};
use glam::{IVec2, UVec2};
use graph_craft::document::NodeId;
#[derive(ExtractField)]
pub struct NewDocumentDialogMessageContext<'a> {
pub viewport_bounds: &'a ViewportBounds,
}
/// A dialog to allow users to set some initial options about a new document.
#[derive(Debug, Clone, Default, ExtractField)]
pub struct NewDocumentDialogMessageHandler {
@@ -12,30 +17,34 @@ pub struct NewDocumentDialogMessageHandler {
}
#[message_handler_data]
impl MessageHandler<NewDocumentDialogMessage, ()> for NewDocumentDialogMessageHandler {
fn process_message(&mut self, message: NewDocumentDialogMessage, responses: &mut VecDeque<Message>, _: ()) {
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() });
let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0;
if create_artboard {
responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::NewArtboard {
id: NodeId::new(),
artboard: graphene_std::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()),
});
responses.add(NavigationMessage::CanvasPan { delta: self.dimensions.as_dvec2() });
responses.add(NodeGraphMessage::RunDocumentGraph);
// If we already have bounds, we won't receive a viewport bounds update so we just fabricate one ourselves
if *context.viewport_bounds != ViewportBounds::default() {
responses.add(InputPreprocessorMessage::BoundsOfViewports {
bounds_of_viewports: vec![context.viewport_bounds.clone()],
});
}
responses.add(DeferMessage::AfterNavigationReady {
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()],
});
}
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
responses.add(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
responses.add(DocumentMessage::DeselectAllLayers);
}
}
@@ -70,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 mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
let infinite = vec![
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(&mut 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())
.for_label(checkbox_id.clone())
.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")
@@ -99,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))
@@ -110,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(),
];
@@ -68,7 +68,7 @@ impl PreferencesDialogMessageHandler {
.widget_holder(),
];
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)";
let zoom_with_scroll = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
@@ -81,12 +81,12 @@ impl PreferencesDialogMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Zoom with Scroll")
.table_align(true)
.tooltip(zoom_with_scroll_tooltip)
.for_checkbox(&mut checkbox_id)
.for_checkbox(checkbox_id)
.widget_holder(),
];
@@ -169,7 +169,7 @@ impl PreferencesDialogMessageHandler {
graph_wire_style,
];
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
let use_vello = vec![
Separator::new(SeparatorType::Unrelated).widget_holder(),
@@ -178,17 +178,17 @@ impl PreferencesDialogMessageHandler {
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Vello Renderer")
.table_align(true)
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
.for_checkbox(&mut checkbox_id)
.for_checkbox(checkbox_id)
.widget_holder(),
];
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
let vector_mesh_tooltip =
"Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle stroke joins and fills.";
let vector_meshes = vec![
@@ -197,13 +197,9 @@ impl PreferencesDialogMessageHandler {
CheckboxInput::new(preferences.vector_meshes)
.tooltip(vector_mesh_tooltip)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.for_label(checkbox_id.clone())
.widget_holder(),
TextLabel::new("Vector Meshes")
.table_align(true)
.tooltip(vector_mesh_tooltip)
.for_checkbox(&mut checkbox_id)
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).for_checkbox(checkbox_id).widget_holder(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![
@@ -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;
@@ -1,4 +1,5 @@
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{
BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, Transform,
@@ -7,12 +8,19 @@ use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, La
use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate};
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::HintData;
use glam::IVec2;
use graph_craft::document::NodeId;
use graphene_std::raster::Image;
use graphene_std::raster::color::Color;
use graphene_std::text::Font;
use graphene_std::text::{Font, TextAlign};
use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))]
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
#[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
#[derivative(Debug, PartialEq)]
pub enum FrontendMessage {
// Display prefix: make the frontend show something, like a dialog
DisplayDialog {
@@ -37,6 +45,7 @@ pub enum FrontendMessage {
max_width: Option<f64>,
#[serde(rename = "maxHeight")]
max_height: Option<f64>,
align: TextAlign,
},
DisplayEditableTextboxTransform {
transform: [f64; 6],
@@ -56,17 +65,23 @@ pub enum FrontendMessage {
#[serde(rename = "commitDate")]
commit_date: String,
},
TriggerDelayedZoomCanvasToFitAll,
TriggerDownloadImage {
TriggerDisplayThirdPartyLicensesDialog,
TriggerSaveDocument {
document_id: DocumentId,
name: String,
path: Option<PathBuf>,
content: Vec<u8>,
},
TriggerSaveFile {
name: String,
content: Vec<u8>,
},
TriggerExportImage {
svg: String,
name: String,
mime: String,
size: (f64, f64),
},
TriggerDownloadTextFile {
document: String,
name: String,
},
TriggerFetchAndOpenDocument {
name: String,
filename: String,
@@ -110,12 +125,19 @@ pub enum FrontendMessage {
document_id: DocumentId,
},
UpdateImportsExports {
imports: Vec<(FrontendGraphOutput, i32, i32)>,
exports: Vec<(FrontendGraphInput, i32, i32)>,
#[serde(rename = "addImport")]
add_import: Option<(i32, i32)>,
#[serde(rename = "addExport")]
add_export: Option<(i32, i32)>,
/// If the primary import is not visible, then it is None.
imports: Vec<Option<FrontendGraphOutput>>,
/// If the primary export is not visible, then it is None.
exports: Vec<Option<FrontendGraphInput>>,
/// The primary import location.
#[serde(rename = "importPosition")]
import_position: IVec2,
/// The primary export location.
#[serde(rename = "exportPosition")]
export_position: IVec2,
/// The document network does not have an add import or export button.
#[serde(rename = "addImportExport")]
add_import_export: bool,
},
UpdateInSelectedNetwork {
#[serde(rename = "inSelectedNetwork")]
@@ -136,11 +158,16 @@ pub enum FrontendMessage {
UpdateGraphViewOverlay {
open: bool,
},
UpdateSpreadsheetState {
UpdateDataPanelState {
open: bool,
node: Option<NodeId>,
},
UpdateSpreadsheetLayout {
UpdatePropertiesPanelState {
open: bool,
},
UpdateLayersPanelState {
open: bool,
},
UpdateDataPanelLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
@@ -179,6 +206,9 @@ pub enum FrontendMessage {
UpdateDocumentArtwork {
svg: String,
},
UpdateImageData {
image_data: Vec<(u64, Image<Color>)>,
},
UpdateDocumentBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
@@ -280,7 +310,7 @@ pub enum FrontendMessage {
#[serde(rename = "openDocuments")]
open_documents: Vec<FrontendDocumentDetails>,
},
UpdatePropertyPanelSectionsLayout {
UpdatePropertiesPanelLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
@@ -304,4 +334,21 @@ pub enum FrontendMessage {
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,
},
UpdatePlatform {
platform: AppWindowPlatform,
},
UpdateWindowState {
maximized: bool,
minimized: bool,
},
CloseWindow,
UpdateViewportHolePunch {
active: bool,
},
#[cfg(not(target_family = "wasm"))]
RenderOverlays {
#[serde(skip, default = "OverlayContext::default")]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
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, Shift])),
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 }),
@@ -211,18 +211,21 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PathToolMessage::Cut { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PathToolMessage::Copy { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=PathToolMessage::Duplicate),
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, segment_editing_modifier: Control }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
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 }),
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 }),
@@ -294,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),
//
@@ -337,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 }),
@@ -424,6 +428,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyX); modifiers=[Accel], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
entry!(KeyDown(KeyR); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleRulers),
entry!(KeyDown(KeyD); modifiers=[Alt], action_dispatch=PortfolioMessage::ToggleDataPanelOpen),
//
// FrontendMessage
entry!(KeyDown(KeyV); modifiers=[Accel], action_dispatch=FrontendMessage::TriggerPaste),
@@ -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(())
@@ -36,6 +36,14 @@ impl MessageHandler<InputPreprocessorMessage, InputPreprocessorMessageContext> f
responses.add(NavigationMessage::CanvasPan { delta: DVec2::ZERO });
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
responses.add(DeferMessage::AfterGraphRun {
messages: vec![
DeferMessage::AfterGraphRun {
messages: vec![DeferMessage::TriggerNavigationReady.into()],
}
.into(),
],
});
}
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => {
self.update_states_of_modifier_keys(modifier_keys, keyboard_platform, responses);
@@ -59,8 +59,8 @@ impl LayoutMessageHandler {
/// Get the widget path for the widget with the specified id
fn get_widget_path(widget_layout: &WidgetLayout, widget_id: WidgetId) -> Option<(&WidgetHolder, Vec<usize>)> {
let mut stack = widget_layout.layout.iter().enumerate().map(|(index, val)| (vec![index], val)).collect::<Vec<_>>();
while let Some((mut widget_path, group)) = stack.pop() {
match group {
while let Some((mut widget_path, layout_group)) = stack.pop() {
match layout_group {
// Check if any of the widgets in the current column or row have the correct id
LayoutGroup::Column { widgets } | LayoutGroup::Row { widgets } => {
for (index, widget) in widgets.iter().enumerate() {
@@ -308,6 +308,7 @@ impl LayoutMessageHandler {
responses.add(callback_message);
}
Widget::ImageLabel(_) => {}
Widget::IconLabel(_) => {}
Widget::InvisibleStandinInput(invisible) => {
let callback_message = match action {
@@ -481,18 +482,18 @@ impl LayoutMessageHandler {
diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping));
let message = match layout_target {
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff },
LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff },
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff },
LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff },
LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff },
LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff },
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff },
LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
LayoutTarget::Spreadsheet => FrontendMessage::UpdateSpreadsheetLayout { layout_target, diff },
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff },
@@ -42,9 +42,9 @@ pub enum LayoutTarget {
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
NodeGraphControlBar,
/// The body of the Properties panel containing many collapsable sections.
PropertiesSections,
PropertiesPanel,
/// The spredsheet panel allows for the visualisation of data in the graph.
Spreadsheet,
DataPanel,
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
ToolOptions,
/// The vertical buttons for all of the tools on the left of the canvas.
@@ -369,6 +369,7 @@ impl LayoutGroup {
Widget::IconButton(x) => &mut x.tooltip,
Widget::IconLabel(x) => &mut x.tooltip,
Widget::ImageButton(x) => &mut x.tooltip,
Widget::ImageLabel(x) => &mut x.tooltip,
Widget::NumberInput(x) => &mut x.tooltip,
Widget::ParameterExposeButton(x) => &mut x.tooltip,
Widget::PopoverButton(x) => &mut x.tooltip,
@@ -546,6 +547,7 @@ pub enum Widget {
IconButton(IconButton),
IconLabel(IconLabel),
ImageButton(ImageButton),
ImageLabel(ImageLabel),
InvisibleStandinInput(InvisibleStandinInput),
NodeCatalog(NodeCatalog),
NumberInput(NumberInput),
@@ -622,6 +624,7 @@ impl DiffUpdate {
Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ImageButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconLabel(_)
| Widget::ImageLabel(_)
| Widget::CurveInput(_)
| Widget::InvisibleStandinInput(_)
| Widget::NodeCatalog(_)
@@ -653,7 +656,7 @@ impl DiffUpdate {
};
match self {
Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|group| group.iter_mut()).for_each(convert_tooltip),
Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(convert_tooltip),
Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(convert_tooltip),
Self::Widget(widget_holder) => convert_tooltip(widget_holder),
}
@@ -168,8 +168,6 @@ pub struct ColorInput {
#[widget_builder(constructor)]
pub value: FillChoice,
pub disabled: bool,
// TODO: Implement
// #[serde(rename = "allowTransparency")]
// #[derivative(Default(value = "false"))]
@@ -179,9 +177,11 @@ pub struct ColorInput {
#[derivative(Default(value = "true"))]
pub allow_none: bool,
// TODO: Implement
// pub disabled: bool,
//
pub disabled: bool,
#[serde(rename = "menuDirection")]
pub menu_direction: Option<MenuDirection>,
pub tooltip: String,
#[serde(skip)]
@@ -5,8 +5,6 @@ use graphene_std::Color;
use graphene_std::raster::curve::Curve;
use graphene_std::transform::ReferencePoint;
use graphite_proc_macros::WidgetBuilder;
use once_cell::sync::OnceCell;
use std::sync::Arc;
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
@@ -20,7 +18,7 @@ pub struct CheckboxInput {
pub tooltip: String,
#[serde(rename = "forLabel", skip_serializing_if = "checkbox_id_is_empty")]
#[serde(rename = "forLabel")]
pub for_label: CheckboxId,
#[serde(skip)]
@@ -44,19 +42,24 @@ impl Default for CheckboxInput {
icon: "Checkmark".into(),
tooltip: Default::default(),
tooltip_shortcut: Default::default(),
for_label: CheckboxId::default(),
for_label: CheckboxId::new(),
on_update: Default::default(),
on_commit: Default::default(),
}
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct CheckboxId(Arc<OnceCell<u64>>);
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CheckboxId(u64);
impl CheckboxId {
pub fn fill(&mut self) {
let _ = self.0.set(graphene_std::uuid::generate_uuid());
pub fn new() -> Self {
Self(graphene_std::uuid::generate_uuid())
}
}
impl Default for CheckboxId {
fn default() -> Self {
Self::new()
}
}
impl specta::Type for CheckboxId {
@@ -65,29 +68,6 @@ impl specta::Type for CheckboxId {
specta::datatype::DataType::Primitive(specta::datatype::PrimitiveType::u64)
}
}
impl serde::Serialize for CheckboxId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.get().copied().serialize(serializer)
}
}
impl<'a> serde::Deserialize<'a> for CheckboxId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
let id = u64::deserialize(deserializer)?;
let checkbox_id = CheckboxId(OnceCell::new().into());
checkbox_id.0.set(id).map_err(serde::de::Error::custom)?;
Ok(checkbox_id)
}
}
fn checkbox_id_is_empty(id: &CheckboxId) -> bool {
id.0.get().is_none()
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)]
@@ -436,6 +416,9 @@ pub struct TextInput {
#[serde(rename = "minWidth")]
pub min_width: u32,
#[serde(rename = "maxWidth")]
pub max_width: u32,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@@ -44,34 +44,40 @@ 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,
#[serde(rename = "checkboxId")]
#[widget_builder(skip)]
pub checkbox_id: CheckboxId,
#[serde(rename = "forCheckbox")]
pub for_checkbox: CheckboxId,
// Body
#[widget_builder(constructor)]
pub value: String,
}
impl TextLabel {
pub fn for_checkbox(mut self, id: &mut CheckboxId) -> Self {
id.fill();
self.checkbox_id = id.clone();
self
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq)]
pub struct ImageLabel {
#[widget_builder(constructor)]
pub url: String,
pub width: Option<String>,
pub height: Option<String>,
pub tooltip: String,
}
// TODO: Add UserInputLabel
+39 -30
View File
@@ -1,5 +1,4 @@
use crate::messages::prelude::*;
use graphene_std::renderer::RenderMetadata;
use graphite_proc_macros::*;
#[impl_message]
@@ -9,10 +8,14 @@ pub enum Message {
#[child]
Animation(AnimationMessage),
#[child]
AppWindow(AppWindowMessage),
#[child]
Broadcast(BroadcastMessage),
#[child]
Debug(DebugMessage),
#[child]
Defer(DeferMessage),
#[child]
Dialog(DialogMessage),
#[child]
Frontend(FrontendMessage),
@@ -30,18 +33,12 @@ pub enum Message {
Preferences(PreferencesMessage),
#[child]
Tool(ToolMessage),
#[child]
Workspace(WorkspaceMessage),
// Messages
NoOp,
Batched {
messages: Box<[Message]>,
},
StartBuffer,
EndBuffer {
render_metadata: RenderMetadata,
},
NoOp,
}
/// Provides an impl of `specta::Type` for `MessageDiscriminant`, the struct created by `impl_message`.
@@ -72,14 +69,12 @@ 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.has_message_handler_data_fields() || tree.has_message_handler_fields() {
("├── ", format!("{}│ ", prefix))
let (branch, child_prefix) = if tree.message_handler_data_fields().is_some() || tree.message_handler_fields().is_some() {
("├── ", format!("{prefix}│ "))
} else if is_last {
("└── ", format!("{prefix} "))
} else {
if is_last {
("└── ", format!("{} ", prefix))
} else {
("├── ", format!("{}│ ", prefix))
}
("├── ", format!("{prefix}│ "))
};
if tree.path().is_empty() {
@@ -97,24 +92,38 @@ mod test {
}
}
// Print handler field if any
if let Some(data) = tree.message_handler_fields() {
let len = data.fields().len();
let (branch, child_prefix) = if tree.has_message_handler_data_fields() {
("├── ", format!("{}│ ", prefix))
} else {
("└── ", 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() {
// 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!("{}{}{}\n", child_prefix, branch, field.0).as_bytes()).unwrap();
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}│ "))
} else {
("└── ", format!("{prefix} "))
};
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();
}
}
}
+2 -1
View File
@@ -1,8 +1,10 @@
//! The root-level messages forming the first layer of the message system architecture.
pub mod animation;
pub mod app_window;
pub mod broadcast;
pub mod debug;
pub mod defer;
pub mod dialog;
pub mod frontend;
pub mod globals;
@@ -14,4 +16,3 @@ pub mod portfolio;
pub mod preferences;
pub mod prelude;
pub mod tool;
pub mod workspace;
@@ -1,32 +1,32 @@
use crate::messages::prelude::*;
use crate::node_graph_executor::InspectResult;
/// The spreadsheet UI allows for instance data to be previewed.
#[impl_message(Message, PortfolioMessage, Spreadsheet)]
/// The Data panel UI allows the user to visualize the output data of the selected node.
#[impl_message(Message, DocumentMessage, DataPanel)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum SpreadsheetMessage {
ToggleOpen,
pub enum DataPanelMessage {
UpdateLayout {
#[serde(skip)]
inspect_result: InspectResult,
},
ClearLayout,
PushToInstancePath {
PushToElementPath {
index: usize,
},
TruncateInstancePath {
TruncateElementPath {
len: usize,
},
ViewVectorDataDomain {
domain: VectorDataDomain,
ViewVectorTableTab {
tab: VectorTableTab,
},
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum VectorDataDomain {
pub enum VectorTableTab {
#[default]
Properties,
Points,
Segments,
Regions,
@@ -0,0 +1,655 @@
use super::VectorTableTab;
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget, WidgetLayout};
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;
use graphene_std::gradient::GradientStops;
use graphene_std::memo::IORecord;
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::table::Table;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, FillChoice};
use graphene_std::{Artboard, Graphic};
use std::any::Any;
use std::sync::Arc;
#[derive(ExtractField)]
pub struct DataPanelMessageContext<'a> {
pub network_interface: &'a mut NodeNetworkInterface,
pub data_panel_open: bool,
}
/// The data panel allows for graph data to be previewed.
#[derive(Default, Debug, Clone, ExtractField)]
pub struct DataPanelMessageHandler {
introspected_node: Option<NodeId>,
introspected_data: Option<Arc<dyn Any + Send + Sync>>,
element_path: Vec<usize>,
active_vector_table_tab: VectorTableTab,
}
#[message_handler_data]
impl MessageHandler<DataPanelMessage, DataPanelMessageContext<'_>> for DataPanelMessageHandler {
fn process_message(&mut self, message: DataPanelMessage, responses: &mut VecDeque<Message>, context: DataPanelMessageContext) {
match message {
DataPanelMessage::UpdateLayout { mut inspect_result } => {
self.introspected_node = Some(inspect_result.inspect_node);
self.introspected_data = inspect_result.take_data();
self.update_layout(responses, context);
}
DataPanelMessage::ClearLayout => {
self.introspected_node = None;
self.introspected_data = None;
self.element_path.clear();
self.active_vector_table_tab = VectorTableTab::default();
self.update_layout(responses, context);
}
DataPanelMessage::PushToElementPath { index } => {
self.element_path.push(index);
self.update_layout(responses, context);
}
DataPanelMessage::TruncateElementPath { len } => {
self.element_path.truncate(len);
self.update_layout(responses, context);
}
DataPanelMessage::ViewVectorTableTab { tab } => {
self.active_vector_table_tab = tab;
self.update_layout(responses, context);
}
}
}
fn actions(&self) -> ActionList {
actions!(DataPanelMessage;)
}
}
impl DataPanelMessageHandler {
fn update_layout(&mut self, responses: &mut VecDeque<Message>, context: DataPanelMessageContext<'_>) {
let DataPanelMessageContext { network_interface, .. } = context;
let mut layout_data = LayoutData {
current_depth: 0,
desired_path: &mut self.element_path,
breadcrumbs: Vec::new(),
vector_table_tab: self.active_vector_table_tab,
};
// Main data visualization
let mut layout = self
.introspected_data
.as_ref()
.map(|instrospected_data| generate_layout(instrospected_data, &mut layout_data).unwrap_or_else(|| label("Visualization of this data type is not yet supported")))
.unwrap_or_default();
let mut widgets = Vec::new();
// Selected layer/node name
if let Some(node_id) = self.introspected_node {
let is_layer = network_interface.is_layer(&node_id, &[]);
widgets.extend([
if is_layer {
IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder()
} else {
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder()
},
Separator::new(SeparatorType::Related).widget_holder(),
TextInput::new(network_interface.display_name(&node_id, &[]))
.tooltip(if is_layer { "Name of the selected layer" } else { "Name of the selected node" })
.on_update(move |text_input| {
NodeGraphMessage::SetDisplayName {
node_id,
alias: text_input.value.clone(),
skip_adding_history_step: false,
}
.into()
})
.max_width(200)
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
]);
}
// Element path breadcrumbs
if !layout_data.breadcrumbs.is_empty() {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
.on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into())
.widget_holder();
widgets.push(breadcrumb);
}
if !widgets.is_empty() {
layout.insert(0, LayoutGroup::Row { widgets });
}
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout { layout }),
layout_target: LayoutTarget::DataPanel,
});
}
}
struct LayoutData<'a> {
current_depth: usize,
desired_path: &'a mut Vec<usize>,
breadcrumbs: Vec<String>,
vector_table_tab: VectorTableTab,
}
macro_rules! generate_layout_downcast {
($introspected_data:expr, $data:expr, [ $($ty:ty),* $(,)? ]) => {
if false { None }
$(
else if let Some(io) = $introspected_data.downcast_ref::<IORecord<Context, $ty>>() {
Some(io.output.layout_with_breadcrumb($data))
}
)*
else { None }
}
}
// TODO: We simply try all these types sequentially. Find a better strategy.
fn generate_layout(introspected_data: &Arc<dyn std::any::Any + Send + Sync + 'static>, data: &mut LayoutData) -> Option<Vec<LayoutGroup>> {
generate_layout_downcast!(introspected_data, data, [
Table<Artboard>,
Table<Graphic>,
Table<Vector>,
Table<Raster<CPU>>,
Table<Raster<GPU>>,
Table<Color>,
Table<GradientStops>,
f64,
u32,
u64,
bool,
String,
Option<f64>,
DVec2,
DAffine2,
])
}
fn column_headings(value: &[&str]) -> Vec<WidgetHolder> {
value.iter().map(|text| TextLabel::new(*text).widget_holder()).collect()
}
fn label(x: impl Into<String>) -> Vec<LayoutGroup> {
let error = vec![TextLabel::new(x).widget_holder()];
vec![LayoutGroup::Row { widgets: error }]
}
trait TableRowLayout {
fn type_name() -> &'static str;
fn identifier(&self) -> String;
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(self.identifier());
self.element_page(data)
}
fn element_widget(&self, index: usize) -> WidgetHolder {
TextButton::new(self.identifier())
.on_update(move |_| DataPanelMessage::PushToElementPath { index }.into())
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![]
}
}
impl<T: TableRowLayout> TableRowLayout for Table<T> {
fn type_name() -> &'static str {
"Table"
}
fn identifier(&self) -> String {
format!("Table<{}> ({} row{})", T::type_name(), self.len(), if self.len() == 1 { "" } else { "s" })
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
if let Some(index) = data.desired_path.get(data.current_depth).copied() {
if let Some(row) = self.get(index) {
data.current_depth += 1;
let result = row.element.layout_with_breadcrumb(data);
data.current_depth -= 1;
return result;
} else {
warn!("Desired path truncated");
data.desired_path.truncate(data.current_depth);
}
}
let mut rows = self
.iter()
.enumerate()
.map(|(index, row)| {
vec![
TextLabel::new(format!("{index}")).widget_holder(),
row.element.element_widget(index),
TextLabel::new(format_transform_matrix(row.transform)).widget_holder(),
TextLabel::new(format!("{}", row.alpha_blending)).widget_holder(),
TextLabel::new(row.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(),
]
})
.collect::<Vec<_>>();
rows.insert(0, column_headings(&["", "element", "transform", "alpha_blending", "source_node_id"]));
vec![LayoutGroup::Table { rows }]
}
}
impl TableRowLayout for Artboard {
fn type_name() -> &'static str {
"Artboard"
}
fn identifier(&self) -> String {
self.label.clone()
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.content.element_page(data)
}
}
impl TableRowLayout for Graphic {
fn type_name() -> &'static str {
"Graphic"
}
fn identifier(&self) -> String {
match self {
Self::Graphic(table) => table.identifier(),
Self::Vector(table) => table.identifier(),
Self::RasterCPU(table) => table.identifier(),
Self::RasterGPU(table) => table.identifier(),
Self::Color(table) => table.identifier(),
Self::Gradient(table) => table.identifier(),
}
}
// Don't put a breadcrumb for Graphic
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
self.element_page(data)
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
match self {
Self::Graphic(table) => table.layout_with_breadcrumb(data),
Self::Vector(table) => table.layout_with_breadcrumb(data),
Self::RasterCPU(table) => table.layout_with_breadcrumb(data),
Self::RasterGPU(table) => table.layout_with_breadcrumb(data),
Self::Color(table) => table.layout_with_breadcrumb(data),
Self::Gradient(table) => table.layout_with_breadcrumb(data),
}
}
}
impl TableRowLayout for Vector {
fn type_name() -> &'static str {
"Vector"
}
fn identifier(&self) -> String {
format!(
"Vector ({} point{}, {} segment{})",
self.point_domain.ids().len(),
if self.point_domain.ids().len() == 1 { "" } else { "s" },
self.segment_domain.ids().len(),
if self.segment_domain.ids().len() == 1 { "" } else { "s" }
)
}
fn element_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions]
.into_iter()
.map(|tab| {
RadioEntryData::new(format!("{tab:?}"))
.label(format!("{tab:?}"))
.on_update(move |_| DataPanelMessage::ViewVectorTableTab { tab }.into())
})
.collect();
let table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_holder()];
let mut table_rows = Vec::new();
match data.vector_table_tab {
VectorTableTab::Properties => {
table_rows.push(column_headings(&["property", "value"]));
match self.style.fill.clone() {
Fill::None => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]),
Fill::Solid(color) => table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Solid(color)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]),
Fill::Gradient(gradient) => {
table_rows.push(vec![
TextLabel::new("Fill").widget_holder(),
ColorInput::new(FillChoice::Gradient(gradient.stops))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Type").widget_holder(),
TextLabel::new(gradient.gradient_type.to_string()).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Fill Gradient Start").widget_holder(),
TextLabel::new(format_dvec2(gradient.start)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Fill Gradient End").widget_holder(), TextLabel::new(format_dvec2(gradient.end)).widget_holder()]);
}
}
if let Some(stroke) = self.style.stroke.clone() {
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
table_rows.push(vec![
TextLabel::new("Stroke").widget_holder(),
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Weight").widget_holder(), TextLabel::new(format!("{} px", stroke.weight)).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Lengths").widget_holder(),
TextLabel::new(if stroke.dash_lengths.is_empty() {
"-".to_string()
} else {
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
})
.widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Dash Offset").widget_holder(),
TextLabel::new(format!("{}", stroke.dash_offset)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Cap").widget_holder(), TextLabel::new(stroke.cap.to_string()).widget_holder()]);
table_rows.push(vec![TextLabel::new("Stroke Join").widget_holder(), TextLabel::new(stroke.join.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Join Miter Limit").widget_holder(),
TextLabel::new(format!("{}", stroke.join_miter_limit)).widget_holder(),
]);
table_rows.push(vec![TextLabel::new("Stroke Align").widget_holder(), TextLabel::new(stroke.align.to_string()).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Stroke Transform").widget_holder(),
TextLabel::new(format_transform_matrix(&stroke.transform)).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Non-Scaling").widget_holder(),
TextLabel::new((if stroke.non_scaling { "Yes" } else { "No" }).to_string()).widget_holder(),
]);
table_rows.push(vec![
TextLabel::new("Stroke Paint Order").widget_holder(),
TextLabel::new(stroke.paint_order.to_string()).widget_holder(),
]);
}
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
table_rows.push(vec![TextLabel::new("Colinear Handle IDs").widget_holder(), TextLabel::new(colinear).widget_holder()]);
table_rows.push(vec![
TextLabel::new("Upstream Nested Layers").widget_holder(),
TextLabel::new(if self.upstream_nested_layers.is_some() {
"Yes (this preserves references to its upstream nested layers for editing by tools)"
} else {
"No (this doesn't preserve references to its upstream nested layers for editing by tools)"
})
.widget_holder(),
]);
}
VectorTableTab::Points => {
table_rows.push(column_headings(&["", "position"]));
table_rows.extend(
self.point_domain
.iter()
.map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{position}")).widget_holder()]),
);
}
VectorTableTab::Segments => {
table_rows.push(column_headings(&["", "start_index", "end_index", "handles"]));
table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{start}")).widget_holder(),
TextLabel::new(format!("{end}")).widget_holder(),
TextLabel::new(format!("{handles:?}")).widget_holder(),
]
}));
}
VectorTableTab::Regions => {
table_rows.push(column_headings(&["", "segment_range", "fill"]));
table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| {
vec![
TextLabel::new(format!("{}", id.inner())).widget_holder(),
TextLabel::new(format!("{segment_range:?}")).widget_holder(),
TextLabel::new(format!("{}", fill.inner())).widget_holder(),
]
}));
}
}
vec![LayoutGroup::Row { widgets: table_tabs }, LayoutGroup::Table { rows: table_rows }]
}
}
impl TableRowLayout for Raster<CPU> {
fn type_name() -> &'static str {
"Raster"
}
fn identifier(&self) -> String {
format!("Raster ({}x{})", self.width, self.height)
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let base64_string = self.data().base64_string.clone().unwrap_or_else(|| {
use base64::Engine;
let output = self.data().to_png();
let preamble = "data:image/png;base64,";
let mut base64_string = String::with_capacity(preamble.len() + output.len() * 4);
base64_string.push_str(preamble);
base64::engine::general_purpose::STANDARD.encode_string(output, &mut base64_string);
base64_string
});
let widgets = vec![ImageLabel::new(base64_string).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Raster<GPU> {
fn type_name() -> &'static str {
"Raster"
}
fn identifier(&self) -> String {
format!("Raster ({}x{})", self.data().width(), self.data().height())
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new("Raster is a texture on the GPU and cannot currently be displayed here").widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Color {
fn type_name() -> &'static str {
"Color"
}
fn identifier(&self) -> String {
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Solid(*self)).disabled(true).menu_direction(Some(MenuDirection::Top)).widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for GradientStops {
fn type_name() -> &'static str {
"Gradient"
}
fn identifier(&self) -> String {
format!("Gradient ({} stops)", self.0.len())
}
fn element_widget(&self, _index: usize) -> WidgetHolder {
ColorInput::new(FillChoice::Gradient(self.clone()))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.widget_holder()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![self.element_widget(0)];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for f64 {
fn type_name() -> &'static str {
"Number (f64)"
}
fn identifier(&self) -> String {
"Number (f64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for u32 {
fn type_name() -> &'static str {
"Number (u32)"
}
fn identifier(&self) -> String {
"Number (u32)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for u64 {
fn type_name() -> &'static str {
"Number (u64)"
}
fn identifier(&self) -> String {
"Number (u64)".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for bool {
fn type_name() -> &'static str {
"Bool"
}
fn identifier(&self) -> String {
"Bool".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(self.to_string()).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for String {
fn type_name() -> &'static str {
"String"
}
fn identifier(&self) -> String {
"String".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextAreaInput::new(self.to_string()).disabled(true).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for Option<f64> {
fn type_name() -> &'static str {
"Option<f64>"
}
fn identifier(&self) -> String {
"Option<f64>".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format!("{self:?}")).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
impl TableRowLayout for DVec2 {
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 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"
}
fn identifier(&self) -> String {
"Transform".to_string()
}
fn element_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
let widgets = vec![TextLabel::new(format_transform_matrix(self)).widget_holder()];
vec![LayoutGroup::Row { widgets }]
}
}
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() };
let round = |x: f64| (x * 1e3).round() / 1e3;
format!(
"Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)",
round(translation.x),
round(translation.y),
round(scale.x),
round(scale.y)
)
}
fn format_dvec2(value: DVec2) -> String {
let round = |x: f64| (x * 1e3).round() / 1e3;
format!("({} px, {} px)", round(value.x), round(value.y))
}
@@ -0,0 +1,7 @@
mod data_panel_message;
mod data_panel_message_handler;
#[doc(inline)]
pub use data_panel_message::*;
#[doc(inline)]
pub use data_panel_message_handler::*;
@@ -1,5 +1,8 @@
use std::path::PathBuf;
use super::utility_types::misc::{GroupFolderType, SnappingState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::overlays::utility_types::OverlaysType;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -31,6 +34,8 @@ pub enum DocumentMessage {
Overlays(OverlaysMessage),
#[child]
PropertiesPanel(PropertiesPanelMessage),
#[child]
DataPanel(DataPanelMessage),
// Messages
AlignSelectedLayers {
@@ -48,7 +53,9 @@ pub enum DocumentMessage {
DocumentHistoryBackward,
DocumentHistoryForward,
DocumentStructureChanged,
DrawArtboardOverlays(OverlayContext),
DrawArtboardOverlays {
context: OverlayContext,
},
DuplicateSelectedLayers,
EnterNestedNetwork {
node_id: NodeId,
@@ -67,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,
},
@@ -105,6 +118,10 @@ pub enum DocumentMessage {
RenderRulers,
RenderScrollbars,
SaveDocument,
SaveDocumentAs,
SavedDocument {
path: Option<PathBuf>,
},
SelectParentLayer,
SelectAllLayers,
SelectedLayersLower,
@@ -182,7 +199,7 @@ pub enum DocumentMessage {
UpdateUpstreamTransforms {
upstream_footprints: HashMap<NodeId, Footprint>,
local_transforms: HashMap<NodeId, DAffine2>,
first_instance_source_id: HashMap<NodeId, Option<NodeId>>,
first_element_source_id: HashMap<NodeId, Option<NodeId>>,
},
UpdateClickTargets {
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
@@ -6,9 +6,10 @@ 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};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext;
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
@@ -16,8 +17,9 @@ use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::portfolio::utility_types::PersistentData;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
@@ -25,17 +27,21 @@ use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor;
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::math::quad::Quad;
use graphene_std::path_bool::{boolean_intersect, path_bool_lib};
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{Raster, RasterDataTable};
use graphene_std::raster_types::Raster;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::style::ViewMode;
use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez};
use std::path::PathBuf;
use std::time::Duration;
#[derive(ExtractField)]
@@ -47,6 +53,9 @@ pub struct DocumentMessageContext<'a> {
pub current_tool: &'a ToolType,
pub preferences: &'a PreferencesMessageHandler,
pub device_pixel_ratio: f64,
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
@@ -61,9 +70,11 @@ pub struct DocumentMessageHandler {
#[serde(skip)]
pub node_graph_handler: NodeGraphMessageHandler,
#[serde(skip)]
overlays_message_handler: OverlaysMessageHandler,
pub overlays_message_handler: OverlaysMessageHandler,
#[serde(skip)]
properties_panel_message_handler: PropertiesPanelMessageHandler,
pub properties_panel_message_handler: PropertiesPanelMessageHandler,
#[serde(skip)]
pub data_panel_message_handler: DataPanelMessageHandler,
// ============================================
// Fields that are saved in the document format
@@ -74,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,
@@ -102,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>,
@@ -139,12 +154,12 @@ impl Default for DocumentMessageHandler {
node_graph_handler: NodeGraphMessageHandler::default(),
overlays_message_handler: OverlaysMessageHandler::default(),
properties_panel_message_handler: PropertiesPanelMessageHandler::default(),
data_panel_message_handler: DataPanelMessageHandler::default(),
// ============================================
// Fields that are saved in the document format
// ============================================
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,
@@ -157,6 +172,8 @@ 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(),
@@ -180,6 +197,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
current_tool,
preferences,
device_pixel_ratio,
data_panel_open,
layers_panel_open,
properties_panel_open,
} = context;
match message {
@@ -217,9 +237,20 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
document_name: self.name.as_str(),
executor,
persistent_data,
properties_panel_open,
};
self.properties_panel_message_handler.process_message(message, responses, context);
}
DocumentMessage::DataPanel(message) => {
self.data_panel_message_handler.process_message(
message,
responses,
DataPanelMessageContext {
network_interface: &mut self.network_interface,
data_panel_open,
},
);
}
DocumentMessage::NodeGraph(message) => {
self.node_graph_handler.process_message(
message,
@@ -235,6 +266,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
graph_fade_artwork_percentage: self.graph_fade_artwork_percentage,
navigation_handler: &self.navigation_handler,
preferences,
layers_panel_open,
},
);
}
@@ -350,14 +382,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
DocumentMessage::DocumentHistoryBackward => self.undo_with_history(ipp, responses),
DocumentMessage::DocumentHistoryForward => self.redo_with_history(ipp, responses),
DocumentMessage::DocumentStructureChanged => {
self.update_layers_panel_control_bar_widgets(responses);
self.update_layers_panel_bottom_bar_widgets(responses);
if layers_panel_open {
self.network_interface.load_structure();
let data_buffer: RawBuffer = self.serialize_root();
self.network_interface.load_structure();
let data_buffer: RawBuffer = self.serialize_root();
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer });
}
}
DocumentMessage::DrawArtboardOverlays(overlay_context) => {
DocumentMessage::DrawArtboardOverlays { context: overlay_context } => {
if !overlay_context.visibility_settings.artboard_name() {
return;
}
@@ -548,24 +583,25 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(NodeGraphMessage::UpdateHints);
} else {
responses.add(ToolMessage::ActivateTool { tool_type: *current_tool });
responses.add(OverlaysMessage::Draw); // Redraw overlays when graph is closed
}
}
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 } => {
@@ -691,7 +727,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
});
if layer_to_move.parent(self.metadata()) != Some(parent) {
// TODO: Fix this so it works when dragging a layer into a group parent which has a Transform node, which used to work before #2689 caused this regression by removing the empty VectorData table row.
// TODO: Fix this so it works when dragging a layer into a group parent which has a Transform node, which used to work before #2689 caused this regression by removing the empty vector table row.
// TODO: See #2688 for this issue.
let layer_local_transform = self.network_interface.document_metadata().transform_to_viewport(layer_to_move);
let undo_transform = self.network_interface.document_metadata().transform_to_viewport(parent).inverse();
@@ -837,7 +873,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
responses.add(DocumentMessage::AddTransaction);
let layer = graph_modification_utils::new_image_layer(RasterDataTable::new(Raster::new_cpu(image)), layer_node_id, self.new_layer_parent(true), responses);
let layer = graph_modification_utils::new_image_layer(Table::new_from_element(Raster::new_cpu(image)), layer_node_id, self.new_layer_parent(true), responses);
if let Some(name) = name {
responses.add(NodeGraphMessage::SetDisplayName {
@@ -912,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);
}
@@ -985,21 +1025,41 @@ 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::TriggerDownloadTextFile {
document: self.serialize_document(),
name,
responses.add(FrontendMessage::TriggerSaveDocument {
document_id,
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();
let selected_layers = selected_nodes.selected_layers(self.metadata());
@@ -1022,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 => {
@@ -1097,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);
}
@@ -1117,7 +1177,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
}
}
DocumentMessage::SetActivePanel { active_panel: panel } => {
use crate::messages::portfolio::utility_types::PanelType;
match panel {
PanelType::Document => {
if self.graph_view_overlay_open {
@@ -1167,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;
}
@@ -1190,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 } => {
@@ -1302,10 +1361,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints,
local_transforms,
first_instance_source_id,
first_element_source_id,
} => {
self.network_interface.update_transforms(upstream_footprints, local_transforms);
self.network_interface.update_first_instance_source_id(first_instance_source_id);
self.network_interface.update_first_element_source_id(first_element_source_id);
}
DocumentMessage::UpdateClickTargets { click_targets } => {
// TODO: Allow non layer nodes to have click targets
@@ -1404,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 {
@@ -1438,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
@@ -1530,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;
@@ -1717,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 {
@@ -1870,10 +1936,11 @@ impl DocumentMessageHandler {
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
// TODO: Remove once the footprint is used to load the imports/export distances from the edge
responses.add(NodeGraphMessage::UnloadWires);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
responses.add(Message::StartBuffer);
Some(previous_network)
}
pub fn redo_with_history(&mut self, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@@ -2145,7 +2212,7 @@ impl DocumentMessageHandler {
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.artboard_name)
.on_update(|optional_input: &CheckboxInput| {
@@ -2155,15 +2222,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Artboard Name".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Artboard Name".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.transform_measurement)
.on_update(|optional_input: &CheckboxInput| {
@@ -2173,9 +2240,9 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
@@ -2184,7 +2251,7 @@ impl DocumentMessageHandler {
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.quick_measurement)
.on_update(|optional_input: &CheckboxInput| {
@@ -2194,15 +2261,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Quick Measurement".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Quick Measurement".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.transform_cage)
.on_update(|optional_input: &CheckboxInput| {
@@ -2212,15 +2279,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Transform Cage".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Transform Cage".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.compass_rose)
.on_update(|optional_input: &CheckboxInput| {
@@ -2230,15 +2297,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Transform Dial".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Transform Dial".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.pivot)
.on_update(|optional_input: &CheckboxInput| {
@@ -2248,15 +2315,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Transform Pivot".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Transform Pivot".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.pivot)
.on_update(|optional_input: &CheckboxInput| {
@@ -2266,15 +2333,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Transform Origin".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Transform Origin".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.hover_outline)
.on_update(|optional_input: &CheckboxInput| {
@@ -2284,15 +2351,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Hover Outline".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Hover Outline".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.selection_outline)
.on_update(|optional_input: &CheckboxInput| {
@@ -2302,9 +2369,9 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Selection Outline".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Selection Outline".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
@@ -2313,7 +2380,7 @@ impl DocumentMessageHandler {
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.path)
.on_update(|optional_input: &CheckboxInput| {
@@ -2323,15 +2390,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Path".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Path".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.anchors)
.on_update(|optional_input: &CheckboxInput| {
@@ -2341,15 +2408,15 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Anchors".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new("Anchors".to_string()).for_checkbox(checkbox_id).widget_holder(),
]
},
},
LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(self.overlays_visibility_settings.handles)
.disabled(!self.overlays_visibility_settings.anchors)
@@ -2360,11 +2427,11 @@ impl DocumentMessageHandler {
}
.into()
})
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new("Handles".to_string())
.disabled(!self.overlays_visibility_settings.anchors)
.for_checkbox(&mut checkbox_id)
.for_checkbox(checkbox_id)
.widget_holder(),
]
},
@@ -2397,7 +2464,7 @@ impl DocumentMessageHandler {
.into_iter()
.chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(*closure(&mut snapping_state))
.on_update(move |input: &CheckboxInput| {
@@ -2408,9 +2475,9 @@ impl DocumentMessageHandler {
.into()
})
.tooltip(tooltip)
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new(name).tooltip(tooltip).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new(name).tooltip(tooltip).for_checkbox(checkbox_id).widget_holder(),
]
},
}))
@@ -2419,7 +2486,7 @@ impl DocumentMessageHandler {
}])
.chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
widgets: {
let mut checkbox_id = CheckboxId::default();
let checkbox_id = CheckboxId::new();
vec![
CheckboxInput::new(*closure(&mut snapping_state2))
.on_update(move |input: &CheckboxInput| {
@@ -2430,9 +2497,9 @@ impl DocumentMessageHandler {
.into()
})
.tooltip(tooltip)
.for_label(checkbox_id.clone())
.for_label(checkbox_id)
.widget_holder(),
TextLabel::new(name).tooltip(tooltip).for_checkbox(&mut checkbox_id).widget_holder(),
TextLabel::new(name).tooltip(tooltip).for_checkbox(checkbox_id).widget_holder(),
]
},
}))
@@ -2444,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))
@@ -2537,7 +2604,11 @@ impl DocumentMessageHandler {
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
}
pub fn update_layers_panel_control_bar_widgets(&self, responses: &mut VecDeque<Message>) {
pub fn update_layers_panel_control_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
// Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode).
let selected_nodes = self.network_interface.selected_nodes();
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
@@ -2695,12 +2766,17 @@ impl DocumentMessageHandler {
});
}
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) {
pub fn update_layers_panel_bottom_bar_widgets(&mut self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_nodes = self.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(self.metadata());
let selected_layer = selected_layers.next();
let has_selection = selected_layer.is_some();
let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let widgets = vec![
PopoverButton::new()
@@ -2714,7 +2790,7 @@ impl DocumentMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &self.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type {
let (output_type, _) = self.network_interface.output_type(&node_id, 0, &self.selection_network_path);
let (output_type, _) = self.network_interface.output_type(&OutputConnector::node(node_id, 0), &self.selection_network_path);
Some(format!("type:{}", output_type.nested_type()))
} else {
None
@@ -2905,7 +2981,7 @@ impl DocumentMessageHandler {
/// Create a network interface with a single export
fn default_document_network_interface() -> NodeNetworkInterface {
let mut network_interface = NodeNetworkInterface::default();
network_interface.add_export(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroupTable::default()), -1, "", &[]);
network_interface.add_export(TaggedValue::Artboard(Default::default()), -1, "", &[]);
network_interface
}
@@ -2937,10 +3013,10 @@ fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
}
fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
let segment = |bezier: bezier_rs::Bezier| match bezier.handles {
bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bezier.start, bezier.end),
bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bezier.start, handle, bezier.end),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
let segment = |bezier: PathSeg| match bezier {
PathSeg::Line(line) => path_bool_lib::PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)),
PathSeg::Quad(quad_bez) => path_bool_lib::PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)),
PathSeg::Cubic(cubic_bez) => path_bool_lib::PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)),
};
click_targets
.filter_map(|target| {
@@ -2951,7 +3027,7 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
}
})
.flatten()
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
.map(|bezier| segment(Affine::new(transform.to_cols_array()) * bezier))
.collect()
}
@@ -2974,11 +3050,11 @@ impl<'a> ClickXRayIter<'a> {
/// Handles the checking of the layer where the target is a rect or path
fn check_layer_area_target(&mut self, click_targets: Option<&Vec<ClickTarget>>, clip: bool, layer: LayerNodeIdentifier, path: Vec<path_bool_lib::PathSegment>, transform: DAffine2) -> XRayResult {
// Convert back to Bezier-rs types for intersections
// Convert back to Kurbo types for intersections
let segment = |bezier: &path_bool_lib::PathSegment| match *bezier {
path_bool_lib::PathSegment::Line(start, end) => bezier_rs::Bezier::from_linear_dvec2(start, end),
path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => bezier_rs::Bezier::from_cubic_dvec2(start, h1, h2, end),
path_bool_lib::PathSegment::Quadratic(start, h1, end) => bezier_rs::Bezier::from_quadratic_dvec2(start, h1, end),
path_bool_lib::PathSegment::Line(start, end) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))),
path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(h2), dvec2_to_point(end))),
path_bool_lib::PathSegment::Quadratic(start, h1, end) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(end))),
path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(),
};
let get_clip = || path.iter().map(segment);
@@ -3027,7 +3103,10 @@ impl<'a> ClickXRayIter<'a> {
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform),
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
XRayTarget::Polygon(polygon) => {
let polygon = polygon.iter_closed().map(|line| path_bool_lib::PathSegment::Line(line.start, line.end)).collect();
let polygon = polygon
.iter_closed()
.map(|line| path_bool_lib::PathSegment::Line(point_to_dvec2(line.start()), point_to_dvec2(line.end())))
.collect();
self.check_layer_area_target(click_targets, clip, layer, polygon, transform)
}
}
@@ -2,13 +2,14 @@ use super::utility_types::TransformIn;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate;
use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, IVec2};
use graph_craft::document::NodeId;
use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::PointId;
use graphene_std::vector::VectorModificationType;
@@ -69,7 +70,7 @@ pub enum GraphOperationMessage {
},
NewBitmapLayer {
id: NodeId,
image_frame: RasterDataTable<CPU>,
image_frame: Table<Raster<CPU>>,
parent: LayerNodeIdentifier,
insert_index: usize,
},
@@ -174,7 +174,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
GraphOperationMessage::NewVectorLayer { id, subpaths, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id);
modify_inputs.insert_vector_data(subpaths, layer, true, true, true);
modify_inputs.insert_vector(subpaths, layer, true, true, true);
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
@@ -349,7 +349,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
let subpaths = convert_usvg_path(path);
let bounds = subpaths.iter().filter_map(|subpath| subpath.bounding_box()).reduce(Quad::combine_bounds).unwrap_or_default();
modify_inputs.insert_vector_data(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
modify_inputs.insert_vector(subpaths, layer, true, path.fill().is_some(), path.stroke().is_some());
if let Some(transform_node_id) = modify_inputs.existing_node_id("Transform", true) {
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, transform * usvg_transform(node.abs_transform()));
@@ -426,7 +426,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Linear,
stops,
})
@@ -453,7 +452,6 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
Fill::Gradient(Gradient {
start,
end,
transform: DAffine2::IDENTITY,
gradient_type: GradientType::Radial,
stops,
})
@@ -1,8 +1,8 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface};
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
/// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`.
@@ -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)
}
@@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector};
use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::{DAffine2, IVec2};
use graph_craft::concrete;
use graph_craft::document::value::TaggedValue;
@@ -11,12 +10,14 @@ use graph_craft::document::{NodeId, NodeInput};
use graphene_std::Artboard;
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::Vector;
use graphene_std::vector::style::{Fill, Stroke};
use graphene_std::vector::{PointId, VectorModificationType};
use graphene_std::vector::{VectorData, VectorDataTable};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
use graphene_std::{Graphic, NodeInputDecleration};
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum TransformIn {
@@ -130,11 +131,11 @@ impl<'a> ModifyInputsContext<'a> {
/// Creates an artboard as the primary export for the document network
pub fn create_artboard(&mut self, new_id: NodeId, artboard: Artboard) -> LayerNodeIdentifier {
let artboard_node_template = resolve_document_node_type("Artboard").expect("Node").node_template_input_override([
Some(NodeInput::value(TaggedValue::ArtboardGroup(graphene_std::ArtboardGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::Artboard(Default::default()), true)),
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.location.into()), false)),
Some(NodeInput::value(TaggedValue::DVec2(artboard.dimensions.into()), false)),
Some(NodeInput::value(TaggedValue::Color(artboard.background), false)),
Some(NodeInput::value(TaggedValue::Color(Table::new_from_element(artboard.background)), false)),
Some(NodeInput::value(TaggedValue::Bool(artboard.clip), false)),
]);
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
@@ -143,7 +144,7 @@ impl<'a> ModifyInputsContext<'a> {
pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)),
Some(NodeInput::value(TaggedValue::Graphic(Default::default()), true)),
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
]);
@@ -152,12 +153,12 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]);
}
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
let vector_data = VectorDataTable::new(VectorData::from_subpaths(subpaths, true));
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
let vector = Table::new_from_element(Vector::from_subpaths(subpaths, true));
let shape = resolve_document_node_type("Path")
.expect("Path node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::VectorData(vector_data), false))]);
.node_template_input_override([Some(NodeInput::value(TaggedValue::Vector(vector), false))]);
let shape_id = NodeId::new();
self.network_interface.insert_node(shape_id, shape, &[]);
self.network_interface.move_node_to_chain_start(&shape_id, layer, &[]);
@@ -198,6 +199,7 @@ impl<'a> ModifyInputsContext<'a> {
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_width), false)),
Some(NodeInput::value(TaggedValue::OptionalF64(typesetting.max_height), false)),
Some(NodeInput::value(TaggedValue::F64(typesetting.tilt), false)),
Some(NodeInput::value(TaggedValue::TextAlign(typesetting.align), false)),
]);
let text_id = NodeId::new();
@@ -217,11 +219,11 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.move_node_to_chain_start(&stroke_id, layer, &[]);
}
pub fn insert_image_data(&mut self, image_frame: RasterDataTable<CPU>, layer: LayerNodeIdentifier) {
pub fn insert_image_data(&mut self, image_frame: Table<Raster<CPU>>, layer: LayerNodeIdentifier) {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let image = resolve_document_node_type("Image Value")
.expect("ImageValue node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::RasterData(image_frame), false))]);
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::Raster(image_frame), false))]);
let image_id = NodeId::new();
self.network_interface.insert_node(image_id, image, &[]);
@@ -261,7 +263,7 @@ impl<'a> ModifyInputsContext<'a> {
}
/// Gets the node id of a node with a specific reference (name) that is upstream (leftward) from the layer node, but before reaching another upstream layer stack.
/// For example, if given a group layer, this would find a requested "Transform" or "Boolean Operation" node in its chain, between the group layer and its layer stack child contents.
/// For example, if given a parent layer, this would find a requested "Transform" or "Boolean Operation" node in its chain, between the parent layer and its layer stack child contents.
/// It would also travel up an entire layer that's not fed by a stack until reaching the generator node, such as a "Rectangle" or "Path" layer.
pub fn locate_node_in_layer_chain(reference_name: &str, left_of_layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
let upstream = network_interface.upstream_flow_back_from_nodes(vec![left_of_layer.to_node()], &[], network_interface::FlowType::HorizontalFlow);
@@ -294,14 +296,15 @@ impl<'a> ModifyInputsContext<'a> {
pub fn create_node(&mut self, reference: &str) -> Option<NodeId> {
let output_layer = self.get_output_layer()?;
let Some(node_definition) = resolve_document_node_type(reference) else {
log::error!("Node type {} does not exist in ModifyInputsContext::existing_node_id", reference);
log::error!("Node type {reference} does not exist in ModifyInputsContext::existing_node_id");
return None;
};
// If inserting a path node, insert a Flatten Path if the type is a graphic group.
// TODO: Allow the path node to operate on Graphic Group data by utilizing the reference for each vector data in a group.
// If inserting a 'Path' node, insert a 'Flatten Path' node if the type is `Graphic`.
// TODO: Allow the 'Path' node to operate on table data by utilizing the reference (index or ID?) for each row.
if node_definition.identifier == "Path" {
let layer_input_type = self.network_interface.input_type(&InputConnector::node(output_layer.to_node(), 1), &[]).0.nested_type().clone();
if layer_input_type == concrete!(GraphicGroupTable) {
if layer_input_type == concrete!(Table<Graphic>) {
let Some(flatten_path_definition) = resolve_document_node_type("Flatten Path") else {
log::error!("Flatten Path does not exist in ModifyInputsContext::existing_node_id");
return None;
@@ -326,11 +329,11 @@ impl<'a> ModifyInputsContext<'a> {
match &fill {
Fill::None => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(None), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new()), false), true);
}
Fill::Solid(color) => {
let input_connector = InputConnector::node(fill_node_id, backup_color_index);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(Some(*color)), false), true);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(Table::new_from_element(*color)), false), true);
}
Fill::Gradient(gradient) => {
let input_connector = InputConnector::node(fill_node_id, backup_gradient_index);
@@ -369,8 +372,10 @@ impl<'a> ModifyInputsContext<'a> {
pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Option<graphene_std::Color>>::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
let stroke_color = if let Some(color) = stroke.color { Table::new_from_element(color) } else { Table::new() };
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_color), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
@@ -1,6 +1,7 @@
mod document_message;
mod document_message_handler;
pub mod data_panel;
pub mod graph_operation;
pub mod navigation;
pub mod node_graph;
@@ -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);
@@ -19,11 +19,12 @@ use graph_craft::document::*;
use graphene_std::brush::brush_cache::BrushCache;
use graphene_std::extract_xy::XY;
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
use graphene_std::raster_types::{CPU, RasterDataTable};
use graphene_std::raster_types::{CPU, Raster};
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
#[allow(unused_imports)]
use graphene_std::transform::Footprint;
use graphene_std::vector::VectorDataTable;
use graphene_std::vector::Vector;
use graphene_std::*;
use std::collections::{HashMap, HashSet, VecDeque};
@@ -45,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
@@ -85,7 +86,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
let custom = vec![
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
identifier: "Identity",
identifier: "Passthrough",
category: "General",
node_template: NodeTemplate {
document_node: DocumentNode {
@@ -94,13 +95,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("In", "TODO").into()],
input_metadata: vec![("Content", "TODO").into()],
output_names: vec!["Out".to_string()],
..Default::default()
},
},
description: Cow::Borrowed("Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes."),
properties: Some("identity_properties"),
description: Cow::Borrowed("Returns the input value without changing it. This is useful for rerouting wires for organization purposes."),
properties: None,
},
// TODO: Auto-generate this from its proto node macro
DocumentNodeDefinition {
@@ -110,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()
},
@@ -150,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()
},
]
@@ -226,38 +227,41 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(3), 0)],
exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [
// Secondary (left) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 1)],
implementation: DocumentNodeImplementation::ProtoNode(graphic_element::to_element::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
..Default::default()
},
// Primary (bottom) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphic_element::to_group::IDENTIFIER),
manual_composition: Some(concrete!(Context)),
implementation: DocumentNodeImplementation::ProtoNode(graphic::to_graphic::IDENTIFIER),
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),
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),
call_argument: concrete!(Context),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
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)),
inputs: vec![
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic_element::layer::IDENTIFIER),
call_argument: generic!(T),
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
},
]
@@ -268,13 +272,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true),
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true),
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Graphical Data", "TODO").into(), ("Over", "TODO").into()],
input_metadata: vec![("Base", "TODO").into(), ("Content", "TODO").into()],
output_names: vec!["Out".to_string()],
node_type_metadata: NodeTypePersistentMetadata::layer(IVec2::new(0, 0)),
network_metadata: Some(NodeNetworkMetadata {
@@ -282,16 +286,24 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Element".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)),
display_name: "To Graphic".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Group".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
display_name: "Wrap Graphic".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -1)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Source Node ID".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -1)),
..Default::default()
},
..Default::default()
@@ -306,7 +318,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Layer".to_string(),
display_name: "Extend".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
..Default::default()
},
@@ -324,7 +336,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Merge attaches a layer to the stack's group."),
description: Cow::Borrowed("Merges new content as an entry into the graphic table that represents a layer compositing stack."),
properties: None,
},
DocumentNodeDefinition {
@@ -333,12 +345,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
exports: vec![NodeInput::node(NodeId(3), 0)],
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)),
implementation: DocumentNodeImplementation::ProtoNode(graphic_element::to_artboard::IDENTIFIER),
call_argument: generic!(T),
implementation: DocumentNodeImplementation::ProtoNode(artboard::create_artboard::IDENTIFIER),
inputs: vec![
NodeInput::network(concrete!(TaggedValue), 1),
NodeInput::value(TaggedValue::String(String::from("Artboard")), false),
@@ -349,23 +361,29 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
],
..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(0), 0), NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath)],
implementation: DocumentNodeImplementation::ProtoNode(graphic::source_node_id::IDENTIFIER),
call_argument: concrete!(Context),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI.
// TODO: Check if thumbnail is reversed
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
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!(ArtboardGroupTable))), 0),
NodeInput::node(NodeId(1), 0),
NodeInput::Reflection(graph_craft::document::DocumentNodeMetadata::DocumentNodePath),
NodeInput::network(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(Table<Artboard>))), 0),
NodeInput::node(NodeId(2), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(graphic_element::append_artboard::IDENTIFIER),
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
..Default::default()
},
]
@@ -376,19 +394,19 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::ArtboardGroup(ArtboardGroupTable::default()), true),
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true),
NodeInput::value(TaggedValue::Artboard(Default::default()), true),
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
NodeInput::value(TaggedValue::DVec2(DVec2::new(1920., 1080.)), false),
NodeInput::value(TaggedValue::Color(Color::WHITE), false),
NodeInput::value(TaggedValue::Color(Table::new_from_element(Color::WHITE)), false),
NodeInput::value(TaggedValue::Bool(false), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![
("Artboards", "TODO").into(),
InputMetadata::with_name_description_override("Contents", "TODO", WidgetOverride::Hidden),
("Base", "TODO").into(),
InputMetadata::with_name_description_override("Content", "TODO", WidgetOverride::Hidden),
InputMetadata::with_name_description_override(
"Location",
"TODO",
@@ -421,7 +439,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "To Artboard".to_string(),
display_name: "Create Artboard".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-21, -3)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Source Node ID".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
..Default::default()
},
@@ -437,7 +463,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Append Artboards".to_string(),
display_name: "Extend".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)),
..Default::default()
},
@@ -468,13 +494,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()
},
@@ -541,7 +567,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()
@@ -591,7 +617,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("Creates a new canvas object."),
properties: None,
},
#[cfg(all(feature = "gpu", target_arch = "wasm32"))]
#[cfg(all(feature = "gpu", target_family = "wasm"))]
DocumentNodeDefinition {
identifier: "Rasterize",
category: "Raster",
@@ -603,20 +629,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()
},
]
@@ -627,11 +653,11 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(VectorDataTable::default()), true),
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::value(
TaggedValue::Footprint(Footprint {
transform: DAffine2::from_scale_angle_translation(DVec2::new(100., 100.), 0., DVec2::new(0., 0.)),
resolution: UVec2::new(100, 100),
transform: DAffine2::from_scale_angle_translation(DVec2::new(1000., 1000.), 0., DVec2::new(0., 0.)),
resolution: UVec2::new(1000, 1000),
..Default::default()
}),
false,
@@ -681,7 +707,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Rasterizes the given vector data"),
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
@@ -689,7 +715,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),
@@ -744,6 +770,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![
NodeInput::value(TaggedValue::None, false),
NodeInput::node(NodeId(0), 0),
NodeInput::node(NodeId(1), 0),
NodeInput::node(NodeId(2), 0),
@@ -752,38 +779,38 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
nodes: [
DocumentNode {
inputs: vec![
NodeInput::network(concrete!(RasterDataTable<CPU>), 0),
NodeInput::network(concrete!(Table<Raster<CPU>>), 0),
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 {
inputs: vec![
NodeInput::network(concrete!(RasterDataTable<CPU>), 0),
NodeInput::network(concrete!(Table<Raster<CPU>>), 0),
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 {
inputs: vec![
NodeInput::network(concrete!(RasterDataTable<CPU>), 0),
NodeInput::network(concrete!(Table<Raster<CPU>>), 0),
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 {
inputs: vec![
NodeInput::network(concrete!(RasterDataTable<CPU>), 0),
NodeInput::network(concrete!(Table<Raster<CPU>>), 0),
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()
},
]
@@ -793,13 +820,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true)],
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Image", "TODO").into()],
output_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
has_primary_output: false,
output_names: vec!["".to_string(), "Red".to_string(), "Green".to_string(), "Blue".to_string(), "Alpha".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
@@ -851,23 +877,23 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
properties: None,
},
DocumentNodeDefinition {
identifier: "Split Coordinate",
identifier: "Split Vec2",
category: "Math: Vector",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
exports: vec![NodeInput::value(TaggedValue::None, false), NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
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!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
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()
},
]
@@ -878,13 +904,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true)],
inputs: vec![NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Coordinate", "TODO").into()],
output_names: vec!["X".to_string(), "Y".to_string()],
has_primary_output: false,
input_metadata: vec![("Vec2", "TODO").into()],
output_names: vec!["".to_string(), "X".to_string(), "Y".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
@@ -917,7 +942,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
},
description: Cow::Borrowed(
"Decomposes the X and Y components of a 2D coordinate.\n\nThe inverse of this node is \"Coordinate Value\", which can have either or both its X and Y exposed as graph inputs.",
"Decomposes the X and Y components of a vec2.\n\nThe inverse of this node is \"Vec2 Value\", which can have either or both its X and Y parameters exposed as graph inputs.",
),
properties: None,
},
@@ -931,11 +956,11 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
exports: vec![NodeInput::node(NodeId(0), 0)],
nodes: vec![DocumentNode {
inputs: vec![
NodeInput::network(concrete!(RasterDataTable<CPU>), 0),
NodeInput::network(concrete!(Table<Raster<CPU>>), 0),
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()
}]
@@ -946,7 +971,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true),
NodeInput::value(TaggedValue::Raster(Default::default()), true),
NodeInput::value(TaggedValue::BrushStrokes(Vec::new()), false),
NodeInput::value(TaggedValue::BrushCache(BrushCache::default()), false),
],
@@ -985,8 +1010,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
inputs: vec![NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true)],
manual_composition: Some(concrete!(Context)),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
call_argument: concrete!(Context),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@@ -1004,8 +1029,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
inputs: vec![NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true)],
manual_composition: Some(concrete!(Context)),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
call_argument: concrete!(Context),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
@@ -1027,13 +1052,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()
@@ -1083,6 +1108,86 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
description: Cow::Borrowed("TODO"),
properties: None,
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
identifier: "Upload Texture",
category: "Debug: GPU",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::scope("editor-api")],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::IntoNode<&WgpuExecutor>")),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::network(concrete!(Table<Raster<CPU>>), 0), NodeInput::node(NodeId(0), 0)],
call_argument: generic!(T),
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_upload::upload_texture::IDENTIFIER),
..Default::default()
},
DocumentNode {
call_argument: generic!(T),
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::Raster(Default::default()), true)],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
output_names: vec!["Texture".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Extract Executor".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Upload Texture".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Cache".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
DocumentNodeDefinition {
identifier: "Extract",
category: "Debug",
@@ -1115,7 +1220,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
// document_node: DocumentNode {
// implementation: DocumentNodeImplementation::proto("graphene_core::raster::CurvesNode"),
// inputs: vec![
// NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true),
// NodeInput::value(TaggedValue::Raster(Default::default()), true),
// NodeInput::value(TaggedValue::Curve(Default::default()), false),
// ],
// ..Default::default()
@@ -1138,9 +1243,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: vec![
DocumentNode {
inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0)],
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()
},
@@ -1150,7 +1255,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()
},
@@ -1162,14 +1267,14 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(VectorDataTable::default()), true),
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::value(TaggedValue::VectorModification(Default::default()), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
input_metadata: vec![("Vector Data", "TODO").into(), ("Modification", "TODO").into()],
output_names: vec!["Vector Data".to_string()],
input_metadata: vec![("Content", "TODO").into(), ("Modification", "TODO").into()],
output_names: vec!["Modified".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
@@ -1210,7 +1315,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),
@@ -1224,6 +1329,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().tilt), false),
NodeInput::value(TaggedValue::TextAlign(text::TextAlign::default()), false),
NodeInput::value(TaggedValue::Bool(false), false),
],
..Default::default()
@@ -1257,7 +1363,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
"TODO",
WidgetOverride::Number(NumberInputSettings {
unit: Some(" px".to_string()),
min: Some(0.),
step: Some(0.1),
..Default::default()
}),
@@ -1292,7 +1397,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
),
("Per-Glyph Instances", "Splits each text glyph into its own instance, i.e. row in the table of vector data.").into(),
InputMetadata::with_name_description_override("Align", "TODO", WidgetOverride::Custom("text_align".to_string())),
("Per-Glyph Instances", "Splits each text glyph into its own row in the table of vector geometry.").into(),
],
output_names: vec!["Vector".to_string()],
..Default::default()
@@ -1319,7 +1425,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()
},
@@ -1331,7 +1437,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()
},
@@ -1414,27 +1520,27 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
exports: vec![NodeInput::node(NodeId(3), 0)],
nodes: vec![
DocumentNode {
inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
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()
},
]
@@ -1445,7 +1551,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true),
NodeInput::value(TaggedValue::Graphic(Default::default()), true),
NodeInput::value(TaggedValue::BooleanOperation(path_bool::BooleanOperation::Union), false),
],
..Default::default()
@@ -1495,7 +1601,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
..Default::default()
}),
input_metadata: vec![("Group of Paths", "TODO").into(), ("Operation", "TODO").into()],
input_metadata: vec![("Content", "TODO").into(), ("Operation", "TODO").into()],
output_names: vec!["Vector".to_string()],
..Default::default()
},
@@ -1512,14 +1618,14 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0)],
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 {
inputs: vec![
NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0),
NodeInput::network(concrete!(Table<Vector>), 0),
NodeInput::network(concrete!(vector::misc::PointSpacingType), 1),
NodeInput::network(concrete!(f64), 2),
NodeInput::network(concrete!(u32), 3),
@@ -1529,25 +1635,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()
},
]
@@ -1558,7 +1664,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false),
NodeInput::value(TaggedValue::F64(100.), false),
NodeInput::value(TaggedValue::U32(100), false),
@@ -1622,7 +1728,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
input_metadata: vec![
("Vector Data", "The shape to be resampled and converted into a polyline.").into(),
("Content", "The shape to be resampled and converted into a polyline.").into(),
("Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_SPACING).into(),
InputMetadata::with_name_description_override(
"Separation",
@@ -1679,30 +1785,30 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
nodes: [
DocumentNode {
inputs: vec![
NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0),
NodeInput::network(concrete!(Table<Vector>), 0),
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()
},
]
@@ -1713,7 +1819,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
NodeInput::value(TaggedValue::Vector(Default::default()), true),
NodeInput::value(TaggedValue::F64(10.), false),
NodeInput::value(TaggedValue::U32(0), false),
],
@@ -1765,7 +1871,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
}),
input_metadata: vec![
("Vector Data", "TODO").into(),
("Content", "TODO").into(),
InputMetadata::with_name_description_override(
"Separation Disk Diameter",
"TODO",
@@ -1817,13 +1923,9 @@ fn static_node_properties() -> NodeProperties {
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
map.insert(
"identity_properties".to_string(),
Box::new(|_node_id, _context| node_properties::string_properties("The identity node passes its data through.")),
);
map.insert(
"monitor_properties".to_string(),
Box::new(|_node_id, _context| node_properties::string_properties("The Monitor node is used by the editor to access the data flowing through it.")),
Box::new(|_node_id, _context| node_properties::string_properties("Used internally by the editor to obtain a layer thumbnail.")),
);
map
}
@@ -1840,10 +1942,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))
}),
@@ -1961,7 +2063,7 @@ fn static_input_properties() -> InputProperties {
.and_then(|value| value.as_bool())
.unwrap_or_default();
Ok(vec![node_properties::coordinate_widget(
Ok(vec![node_properties::vec2_widget(
ParameterWidgetsInfo::new(node_id, index, true, context),
&x,
&y,
@@ -2217,7 +2319,7 @@ fn static_input_properties() -> InputProperties {
"spline_input".to_string(),
Box::new(|node_id, index, context| {
Ok(vec![LayoutGroup::Row {
widgets: node_properties::array_of_coordinates_widget(ParameterWidgetsInfo::new(node_id, index, true, context), TextInput::default().centered(true)),
widgets: node_properties::array_of_vec2_widget(ParameterWidgetsInfo::new(node_id, index, true, context), TextInput::default().centered(true)),
}])
}),
);
@@ -2324,6 +2426,13 @@ fn static_input_properties() -> InputProperties {
)])
}),
);
map.insert(
"text_align".to_string(),
Box::new(|node_id, index, context| {
let choices = enum_choice::<text::TextAlign>().for_socket(ParameterWidgetsInfo::new(node_id, index, true, context)).property_row();
Ok(vec![choices])
}),
);
map
}
@@ -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()
@@ -77,7 +77,6 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
})
.collect(),
output_names: vec![output_type.to_string()],
has_primary_output: true,
locked: false,
..Default::default()
},
@@ -18,7 +18,11 @@ pub enum NodeGraphMessage {
},
AddPathNode,
AddImport,
AddPrimaryImport,
AddSecondaryImport,
AddExport,
AddPrimaryExport,
AddSecondaryExport,
Init,
SelectedNodesUpdated,
Copy,
@@ -63,6 +67,12 @@ pub enum NodeGraphMessage {
set_to_exposed: bool,
start_transaction: bool,
},
ExposeEncapsulatingPrimaryInput {
exposed: bool,
},
ExposePrimaryExport {
exposed: bool,
},
InsertNode {
node_id: NodeId,
node_template: NodeTemplate,
@@ -1,4 +1,4 @@
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendGraphInput, FrontendGraphOutput, FrontendNode};
use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, FrontendNode};
use super::{document_node_definitions, node_properties};
use crate::consts::GRID_SIZE;
use crate::messages::input_mapper::utility_types::macros::action_keys;
@@ -17,17 +17,16 @@ use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle,
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_clip_mode};
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors;
use graphene_std::math::math_ext::QuadExt;
use graphene_std::vector::misc::subpath_to_kurbo_bezpath;
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
use graphene_std::*;
use kurbo::{Line, Point};
use kurbo::{DEFAULT_ACCURACY, Shape};
use renderer::Quad;
use std::cmp::Ordering;
@@ -43,6 +42,7 @@ pub struct NodeGraphMessageContext<'a> {
pub graph_fade_artwork_percentage: f64,
pub navigation_handler: &'a NavigationMessageHandler,
pub preferences: &'a PreferencesMessageHandler,
pub layers_panel_open: bool,
}
#[derive(Debug, Clone, ExtractField)]
@@ -89,7 +89,7 @@ pub struct NodeGraphMessageHandler {
reordering_import: Option<usize>,
/// The index of the export that is being moved
reordering_export: Option<usize>,
/// The end index of the moved port
/// The end index of the moved connector
end_index: Option<usize>,
/// Used to keep track of what nodes are sent to the front end so that only visible ones are sent to the frontend
frontend_nodes: Vec<NodeId>,
@@ -112,6 +112,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
graph_fade_artwork_percentage,
navigation_handler,
preferences,
layers_panel_open,
} = context;
match message {
@@ -126,48 +127,56 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] });
}
NodeGraphMessage::AddPathNode => {
let selected_nodes = network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let first_layer = selected_layers.next();
let second_layer = selected_layers.next();
let has_single_selection = first_layer.is_some() && second_layer.is_none();
let compatible_type = first_layer.and_then(|layer| {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &network_interface);
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]);
Some(format!("type:{}", output_type.nested_type()))
})
});
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
if first_layer.is_some() && has_single_selection && is_compatible {
if let Some(layer) = first_layer {
let node_type = "Path".to_string();
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &network_interface);
let is_modifiable = matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)));
if !is_modifiable {
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction {
node_type: node_type.clone(),
layer: LayerNodeIdentifier::new_unchecked(layer.to_node()),
});
responses.add(BroadcastEvent::SelectionChanged);
}
}
if let Some(layer) = make_path_editable_is_allowed(network_interface) {
responses.add(NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type: "Path".to_string(), layer });
responses.add(EventMessage::SelectionChanged);
}
}
NodeGraphMessage::AddImport => {
network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::AddPrimaryImport => {
if network_interface.number_of_imports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddImport);
} else {
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: true });
}
}
NodeGraphMessage::AddSecondaryImport => {
// If necessary, add a hidden primary import before the secondary import
if network_interface.number_of_imports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddImport);
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false });
}
// Add the secondary import
responses.add(NodeGraphMessage::AddImport);
}
NodeGraphMessage::AddExport => {
network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::AddPrimaryExport => {
if network_interface.number_of_exports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddExport);
} else {
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: true });
}
}
NodeGraphMessage::AddSecondaryExport => {
// If necessary, add a hidden primary import before the secondary import
if network_interface.number_of_exports(breadcrumb_network_path) == 0 {
responses.add(NodeGraphMessage::AddExport);
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: false });
}
// Add the secondary export
responses.add(NodeGraphMessage::AddExport);
}
NodeGraphMessage::Init => {
responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged,
on: EventMessage::SelectionChanged,
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
});
network_interface.load_structure();
@@ -182,11 +191,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
responses.add(MenuBarMessage::SendLayout);
responses.add(NodeGraphMessage::UpdateLayerPanel);
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::SendSelectedNodes);
responses.add(ArtboardToolMessage::UpdateSelectedArtboard);
responses.add(DocumentMessage::DocumentStructureChanged);
responses.add(OverlaysMessage::Draw);
responses.add(NodeGraphMessage::SendGraph);
responses.add(PortfolioMessage::SubmitActiveGraphRender);
}
NodeGraphMessage::CreateWire { output_connector, input_connector } => {
// TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762
@@ -424,6 +435,77 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::ZoomCanvasTo100Percent);
}
}
NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed } => {
let Some((node_id, network_path)) = breadcrumb_network_path.split_last() else {
return;
};
let encapsulating_connector = InputConnector::node(*node_id, 0);
if !exposed {
network_interface.disconnect_input(&encapsulating_connector, network_path);
}
let Some(mut input) = network_interface.input_from_connector(&encapsulating_connector, network_path).cloned() else {
return;
};
if let NodeInput::Value { exposed: old_exposed, .. } = &mut input {
*old_exposed = exposed;
}
network_interface.set_input(&encapsulating_connector, input, network_path);
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::Import(0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
// Disconnect all connections in the encapsulating network
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, breadcrumb_network_path);
}
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendWires);
}
NodeGraphMessage::ExposePrimaryExport { exposed } => {
let export_connector: InputConnector = InputConnector::Export(0);
if !exposed {
network_interface.disconnect_input(&export_connector, breadcrumb_network_path);
}
let Some(mut input) = network_interface.input_from_connector(&export_connector, breadcrumb_network_path).cloned() else {
return;
};
if let NodeInput::Value { exposed: old_exposed, .. } = &mut input {
*old_exposed = exposed;
}
network_interface.set_input(&export_connector, input, breadcrumb_network_path);
// Disconnect all connections in the encapsulating network
if let Some((encapsulating_node, encapsulating_path)) = breadcrumb_network_path.split_last() {
let Some(outward_wires) = network_interface.outward_wires(encapsulating_path) else {
log::error!("Could not get outward wires in remove_import");
return;
};
let Some(downstream_connections) = outward_wires.get(&OutputConnector::node(*encapsulating_node, 0)).cloned() else {
log::error!("Could not get outward wires for import in remove_import");
return;
};
for downstream_connection in &downstream_connections {
network_interface.disconnect_input(downstream_connection, encapsulating_path);
}
}
responses.add(NodeGraphMessage::UpdateImportsExports);
}
NodeGraphMessage::InsertNode { node_id, node_template } => {
network_interface.insert_node(node_id, node_template, selection_network_path);
}
@@ -729,21 +811,21 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
};
if modify_import_export.add_import_export.clicked_input_port_from_point(node_graph_point).is_some() {
if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddExport);
return;
} else if modify_import_export.add_import_export.clicked_output_port_from_point(node_graph_point).is_some() {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddImport);
return;
} else if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
if remove_import_index == 0 {
responses.add(NodeGraphMessage::ExposeEncapsulatingPrimaryInput { exposed: false })
} else {
responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
}
return;
} else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index });
if remove_export_index == 0 {
responses.add(NodeGraphMessage::ExposePrimaryExport { exposed: false })
} else {
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index });
}
return;
} else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::StartTransaction);
@@ -779,10 +861,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
// Alt-click sets the clicked node as previewed
if alt_click {
if let Some(clicked_node) = clicked_id {
self.preview_on_mouse_up = Some(clicked_node);
}
if alt_click && let Some(clicked_node) = clicked_id {
self.preview_on_mouse_up = Some(clicked_node);
}
// Begin moving an existing wire
@@ -808,14 +888,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
self.initial_disconnecting = false;
self.wire_in_progress_from_connector = network_interface.output_position(&clicked_output, selection_network_path);
if let Some((output_type, source)) = clicked_output
.node_id()
.map(|node_id| network_interface.output_type(&node_id, clicked_output.index(), breadcrumb_network_path))
{
self.wire_in_progress_type = FrontendGraphDataType::displayed_type(&output_type, &source);
} else {
self.wire_in_progress_type = FrontendGraphDataType::General;
}
let (output_type, source) = &network_interface.output_type(&clicked_output, breadcrumb_network_path);
self.wire_in_progress_type = FrontendGraphDataType::displayed_type(output_type, source);
self.update_node_graph_hints(responses);
return;
@@ -984,8 +1058,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
to_connector_is_layer,
GraphWireStyle::Direct,
);
let mut path_string = String::new();
let _ = vector_wire.subpath_to_svg(&mut path_string, DAffine2::IDENTITY);
let path_string = vector_wire.to_svg();
let wire_path = WirePath {
path_string,
data_type: self.wire_in_progress_type,
@@ -1047,7 +1120,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};
(position > point.y).then_some(*index)
})
.unwrap_or(modify_import_export.reorder_imports_exports.output_ports().count()),
.filter(|end_index| *end_index > 0) // An import cannot be reordered to be the primary
.unwrap_or_else(|| modify_import_export.reorder_imports_exports.output_ports().count() + 1),
);
responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index });
} else if self.reordering_export.is_some() {
@@ -1067,7 +1141,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
};
(position > point.y).then_some(*index)
})
.unwrap_or(modify_import_export.reorder_imports_exports.input_ports().count()),
.filter(|end_index| *end_index > 0) // An export cannot be reordered to be the primary
.unwrap_or_else(|| modify_import_export.reorder_imports_exports.input_ports().count() + 1),
);
responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index });
}
@@ -1122,27 +1197,27 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SendGraph);
} else if output_connector.is_some() && input_connector.is_none() && !self.initial_disconnecting {
} else if !self.initial_disconnecting
&& input_connector.is_none()
&& let Some(output_connector) = output_connector
{
// If the add node menu is already open, we don't want to open it again
if self.context_menu.is_some() {
return;
}
// Get the output types from the network interface
let (output_type, type_source) = network_interface.output_type(&output_connector, selection_network_path);
let Some(network_metadata) = network_interface.network_metadata(selection_network_path) else {
warn!("No network_metadata");
return;
};
// Get the compatible type from the output connector
let compatible_type = output_connector.and_then(|output_connector| {
output_connector.node_id().and_then(|node_id| {
// Get the output types from the network interface
let (output_type, type_source) = network_interface.output_type(&node_id, output_connector.index(), selection_network_path);
match type_source {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => Some(format!("type:{}", output_type.nested_type())),
}
})
});
let compatible_type = match type_source {
TypeSource::RandomProtonodeImplementation | TypeSource::Error(_) => None,
_ => Some(format!("type:{}", output_type.nested_type())),
};
let appear_right_of_mouse = if ipp.mouse.position.x > ipp.viewport_bounds.size().x - 173. { -173. } else { 0. };
let appear_above_mouse = if ipp.mouse.position.y > ipp.viewport_bounds.size().y - 34. { -34. } else { 0. };
let node_graph_shift = DVec2::new(appear_right_of_mouse, appear_above_mouse) / network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.matrix2.x_axis.x;
@@ -1222,7 +1297,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
.filter(|input| input.1.as_value().is_some())
.map(|input| input.0);
if let Some(selected_node_input_connect_index) = selected_node_input_connect_index {
let Some(bounding_box) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
let Some(node_bbox) = network_interface.node_bounding_box(&selected_node_id, selection_network_path) else {
log::error!("Could not get bounding box for node: {selected_node_id}");
return;
};
@@ -1243,34 +1318,15 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
{
return None;
}
log::debug!("preferences.graph_wire_style: {:?}", preferences.graph_wire_style);
let (wire, is_stack) = network_interface.vector_wire_from_input(&input, preferences.graph_wire_style, selection_network_path)?;
let bbox_rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
let node_bbox = kurbo::Rect::new(node_bbox[0].x, node_bbox[0].y, node_bbox[1].x, node_bbox[1].y).to_path(DEFAULT_ACCURACY);
let inside = bezpath_is_inside_bezpath(&wire, &node_bbox, None, None);
let p1 = DVec2::new(bbox_rect.x0, bbox_rect.y0);
let p2 = DVec2::new(bbox_rect.x1, bbox_rect.y0);
let p3 = DVec2::new(bbox_rect.x1, bbox_rect.y1);
let p4 = DVec2::new(bbox_rect.x0, bbox_rect.y1);
let ps = [p1, p2, p3, p4];
let inside = wire.is_inside_subpath(&Subpath::from_anchors_linear(ps, true), None, None);
let wire = subpath_to_kurbo_bezpath(wire);
let intersect = wire.segments().any(|segment| {
let rect = kurbo::Rect::new(bounding_box[0].x, bounding_box[0].y, bounding_box[1].x, bounding_box[1].y);
let top_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x1, rect.y0));
let bottom_line = Line::new(Point::new(rect.x0, rect.y1), Point::new(rect.x1, rect.y1));
let left_line = Line::new(Point::new(rect.x0, rect.y0), Point::new(rect.x0, rect.y1));
let right_line = Line::new(Point::new(rect.x1, rect.y0), Point::new(rect.x1, rect.y1));
!segment.intersect_line(top_line).is_empty()
|| !segment.intersect_line(bottom_line).is_empty()
|| !segment.intersect_line(left_line).is_empty()
|| !segment.intersect_line(right_line).is_empty()
});
let intersect = wire
.segments()
.any(|segment| node_bbox.segments().filter_map(|segment| segment.as_line()).any(|line| !segment.intersect_line(line).is_empty()));
(intersect || inside).then_some((input, is_stack))
})
@@ -1485,11 +1541,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
NodeGraphMessage::RemoveImport { import_index: usize } => {
network_interface.remove_import(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::RemoveExport { export_index: usize } => {
network_interface.remove_export(usize, selection_network_path);
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
@@ -1515,7 +1573,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 {
@@ -1523,7 +1581,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 {
@@ -1531,8 +1589,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
return;
};
selected_nodes.set_selected_nodes(nodes);
responses.add(BroadcastEvent::SelectionChanged);
responses.add(PropertiesPanelMessage::Refresh);
responses.add(EventMessage::SelectionChanged);
}
NodeGraphMessage::SendClickTargets => responses.add(FrontendMessage::UpdateClickTargets {
click_targets: Some(network_interface.collect_frontend_click_targets(breadcrumb_network_path)),
@@ -1560,7 +1617,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;
};
@@ -1724,6 +1781,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::SendGraph);
responses.add(OverlaysMessage::Draw); // Redraw overlays to update artboard names
}
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => {
network_interface.set_display_name(&node_id, alias, selection_network_path);
@@ -1764,7 +1822,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;
};
@@ -1864,7 +1922,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let shift = ipp.keyboard.get(Key::Shift as usize);
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(selection_network_path) else {
log::error!("Could not get selected nodes in PointerMove");
log::error!("Could not get selected nodes in UpdateBoxSelection");
return;
};
let previous_selection = selected_nodes.selected_nodes_ref().iter().cloned().collect::<HashSet<_>>();
@@ -1880,7 +1938,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
continue;
};
let quad = Quad::from_box([box_selection_start, box_selection_end_graph]);
if click_targets.node_click_target.intersect_path(|| quad.bezier_lines(), DAffine2::IDENTITY) {
if click_targets.node_click_target.intersect_path(|| quad.to_lines(), DAffine2::IDENTITY) {
nodes.insert(node_id);
}
}
@@ -1893,34 +1951,30 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
}
}
NodeGraphMessage::UpdateImportsExports => {
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default();
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default();
let add_import = network_interface
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.output_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
let add_export = network_interface
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.input_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
let imports = network_interface.frontend_imports(breadcrumb_network_path);
let exports = network_interface.frontend_exports(breadcrumb_network_path);
let Some((import_position, export_position)) = network_interface.import_export_position(breadcrumb_network_path) else {
log::error!("Could not get import export positions");
return;
};
// Do not show the add import or add export button in the document network;
let add_import_export = !breadcrumb_network_path.is_empty();
responses.add(NodeGraphMessage::UpdateVisibleNodes);
responses.add(NodeGraphMessage::SendWires);
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
add_import,
add_export,
import_position,
export_position,
add_import_export,
});
}
NodeGraphMessage::UpdateLayerPanel => {
Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses);
Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses);
}
NodeGraphMessage::UpdateEdges => {
// Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes
@@ -1931,7 +1985,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);
}
@@ -2033,11 +2087,6 @@ impl NodeGraphMessageHandler {
return;
};
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
warn!("No network in update_selection_action_buttons");
return;
};
let Some(selected_nodes) = network_interface.selected_nodes_in_nested_network(breadcrumb_network_path) else {
warn!("No selected nodes in update_selection_action_buttons");
return;
@@ -2051,6 +2100,7 @@ impl NodeGraphMessageHandler {
let mut selected_layers = selected_nodes.selected_layers(network_interface.document_metadata());
let selected_layer = selected_layers.next();
let has_multiple_selection = selected_layers.next().is_some();
for _ in selected_layers {}
let mut widgets = vec![
PopoverButton::new()
@@ -2063,7 +2113,7 @@ impl NodeGraphMessageHandler {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type {
let (output_type, _) = network_interface.output_type(&node_id, 0, &[]);
let (output_type, _) = network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
Some(format!("type:{}", output_type.nested_type()))
} else {
None
@@ -2149,6 +2199,11 @@ impl NodeGraphMessageHandler {
let mut selection = selected_nodes.selected_nodes();
let (selection, no_other_selections) = (selection.next(), selection.count() == 0);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
warn!("No network in update_selection_action_buttons");
return;
};
let previewing = if matches!(network_interface.previewing(breadcrumb_network_path), Previewing::Yes { .. }) {
network.exports.iter().find_map(|export| {
let NodeInput::Node { node_id, .. } = export else { return None };
@@ -2265,6 +2320,12 @@ impl NodeGraphMessageHandler {
}
}
// The same layer/node may appear several times. Sort and dedup them for a stable ordering.
layers.sort();
layers.dedup();
nodes.sort();
nodes.dedup();
// Next, we decide what to display based on the number of layers and nodes selected
match *layers.as_slice() {
// If no layers are selected, show properties for all selected nodes
@@ -2370,12 +2431,12 @@ impl NodeGraphMessageHandler {
.icon(Some("Node".to_string()))
.tooltip("Add an operation to the end of this layer's chain of nodes")
.popover_layout({
let layer_identifier = LayerNodeIdentifier::new(layer, &context.network_interface);
let layer_identifier = LayerNodeIdentifier::new(layer, context.network_interface);
let compatible_type = {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, &context.network_interface);
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer_identifier, context.network_interface);
let node_type = graph_layer.horizontal_layer_flow().nth(1);
if let Some(node_id) = node_type {
let (output_type, _) = context.network_interface.output_type(&node_id, 0, &[]);
let (output_type, _) = context.network_interface.output_type(&OutputConnector::node(node_id, 0), &[]);
Some(format!("type:{}", output_type.nested_type()))
} else {
None
@@ -2454,7 +2515,7 @@ impl NodeGraphMessageHandler {
} else {
added_wires.push(WirePathUpdate {
id: NodeId(u64::MAX),
input_index: usize::MAX,
input_index: u32::MAX as usize,
wire_path_update: None,
})
}
@@ -2463,110 +2524,48 @@ impl NodeGraphMessageHandler {
}
fn collect_nodes(&self, network_interface: &mut NodeNetworkInterface, breadcrumb_network_path: &[NodeId]) -> Vec<FrontendNode> {
let Some(outward_wires) = network_interface.outward_wires(breadcrumb_network_path).cloned() else {
return Vec::new();
};
let mut can_be_layer_lookup = HashSet::new();
let mut position_lookup = HashMap::new();
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
};
for node_id in network.nodes.keys().cloned().collect::<Vec<_>>() {
if network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path) {
can_be_layer_lookup.insert(node_id);
}
if let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) {
position_lookup.insert(node_id, position);
} else {
log::error!("Could not get position for node {node_id}");
}
}
let mut frontend_inputs_lookup = frontend_inputs_lookup(breadcrumb_network_path, network_interface);
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
};
let Some(network_metadata) = network_interface.network_metadata(breadcrumb_network_path) else {
log::error!("Could not get network_metadata when collecting nodes");
return Vec::new();
};
let mut nodes = Vec::new();
for (&node_id, node) in &network.nodes {
let node_id_path = [breadcrumb_network_path, (&[node_id])].concat();
for (node_id, visible) in network.nodes.iter().map(|(node_id, node)| (*node_id, node.visible)).collect::<Vec<_>>() {
let node_id_path = [breadcrumb_network_path, &[node_id]].concat();
let inputs = frontend_inputs_lookup.remove(&node_id).unwrap_or_default();
let primary_input_connector = InputConnector::node(node_id, 0);
let mut inputs = inputs.into_iter().map(|input| {
input.map(|input| FrontendGraphInput {
data_type: FrontendGraphDataType::displayed_type(&input.ty, &input.type_source),
resolved_type: format!("{:?}", &input.ty),
valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(),
name: input.input_name,
description: input.input_description,
connected_to: input.output_connector,
})
});
let primary_input = inputs.next().flatten();
let exposed_inputs = inputs.flatten().collect();
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let frontend_data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let connected_to = outward_wires.get(&OutputConnector::node(node_id, 0)).cloned().unwrap_or_default();
let primary_output = if network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
Some(FrontendGraphOutput {
data_type: frontend_data_type,
name: "Output 1".to_string(),
description: String::new(),
resolved_type: format!("{:?}", output_type),
connected_to,
})
let primary_input = if network_interface
.input_from_connector(&primary_input_connector, breadcrumb_network_path)
.is_some_and(|input| input.is_exposed())
{
network_interface.frontend_input_from_connector(&primary_input_connector, breadcrumb_network_path)
} else {
None
};
let exposed_inputs = (1..network_interface.number_of_inputs(&node_id, breadcrumb_network_path))
.filter_map(|input_index| network_interface.frontend_input_from_connector(&InputConnector::node(node_id, input_index), breadcrumb_network_path))
.collect();
let mut exposed_outputs = Vec::new();
for output_index in 0..network_interface.number_of_outputs(&node_id, breadcrumb_network_path) {
if output_index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
continue;
}
let (output_type, type_source) = network_interface.output_type(&node_id, 0, breadcrumb_network_path);
let data_type = FrontendGraphDataType::displayed_type(&output_type, &type_source);
let primary_output = network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, 0), breadcrumb_network_path);
let Some(node_metadata) = network_metadata.persistent_metadata.node_metadata.get(&node_id) else {
log::error!("Could not get node_metadata when getting output for {node_id}");
continue;
};
let output_name = node_metadata
.persistent_metadata
.output_names
.get(output_index)
.cloned()
.filter(|output_name| !output_name.is_empty())
.unwrap_or_else(|| output_type.nested_type().to_string());
let connected_to = outward_wires.get(&OutputConnector::node(node_id, output_index)).cloned().unwrap_or_default();
exposed_outputs.push(FrontendGraphOutput {
data_type,
name: output_name,
description: String::new(),
resolved_type: format!("{:?}", output_type),
connected_to,
});
}
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
log::error!("Could not get nested network when collecting nodes");
return Vec::new();
let exposed_outputs = (1..network_interface.number_of_outputs(&node_id, breadcrumb_network_path))
.filter_map(|output_index| network_interface.frontend_output_from_connector(&OutputConnector::node(node_id, output_index), breadcrumb_network_path))
.collect();
let (primary_output_connected_to_layer, primary_input_connected_to_layer) = if network_interface.is_layer(&node_id, breadcrumb_network_path) {
(
network_interface.primary_output_connected_to_layer(&node_id, breadcrumb_network_path),
network_interface.primary_input_connected_to_layer(&node_id, breadcrumb_network_path),
)
} else {
(false, false)
};
let is_export = network.exports.first().is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id));
let is_export = network_interface
.input_from_connector(&InputConnector::Export(0), breadcrumb_network_path)
.is_some_and(|export| export.as_node().is_some_and(|export_node_id| node_id == export_node_id));
let is_root_node = network_interface.root_node(breadcrumb_network_path).is_some_and(|root_node| root_node.node_id == node_id);
let Some(position) = position_lookup.get(&node_id).map(|pos| (pos.x, pos.y)) else {
let Some(position) = network_interface.position(&node_id, breadcrumb_network_path) else {
log::error!("Could not get position for node: {node_id}");
continue;
};
@@ -2592,19 +2591,20 @@ impl NodeGraphMessageHandler {
is_layer: network_interface
.node_metadata(&node_id, breadcrumb_network_path)
.is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()),
can_be_layer: can_be_layer_lookup.contains(&node_id),
can_be_layer: network_interface.is_eligible_to_be_layer(&node_id, breadcrumb_network_path),
reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(),
display_name: network_interface.display_name(&node_id, breadcrumb_network_path),
primary_input,
exposed_inputs,
primary_output,
exposed_outputs,
primary_output_connected_to_layer,
primary_input_connected_to_layer,
position,
previewed,
visible: node.visible,
visible,
locked,
errors,
ui_only: false,
});
}
@@ -2626,7 +2626,11 @@ impl NodeGraphMessageHandler {
Some(subgraph_names)
}
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, responses: &mut VecDeque<Message>) {
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
if !layers_panel_open {
return;
}
let selected_layers = network_interface
.selected_nodes()
.selected_layers(network_interface.document_metadata())
@@ -2709,7 +2713,7 @@ impl NodeGraphMessageHandler {
}
}
pub fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
fn update_node_graph_hints(&self, responses: &mut VecDeque<Message>) {
// A wire is in progress and its start and end connectors are set
let wiring = self.wire_in_progress_from_connector.is_some();
@@ -2750,73 +2754,6 @@ impl NodeGraphMessageHandler {
}
}
#[derive(Default)]
struct InputLookup {
input_name: String,
input_description: String,
ty: Type,
type_source: TypeSource,
valid_types: Vec<Type>,
output_connector: Option<OutputConnector>,
}
type FrontendInputsLookup = HashMap<NodeId, Vec<Option<InputLookup>>>;
/// Create a lookup hashmap that can be used to create the frontend inputs. This is needed because `input_type` requires a mutable `network_interface`.
fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface: &mut NodeNetworkInterface) -> FrontendInputsLookup {
let Some(network) = network_interface.nested_network(breadcrumb_network_path) else {
return Default::default();
};
let mut frontend_inputs_lookup = HashMap::new();
for (node_id, index, output_connector, is_exposed) in network
.nodes
.iter()
.flat_map(|(node_id, node)| {
node.inputs
.iter()
.enumerate()
.map(|(index, input)| (*node_id, index, OutputConnector::from_input(input), input.is_exposed()))
})
.collect::<Vec<_>>()
{
// Skip not exposed inputs (they still get an entry to help with finding the primary input)
let lookup = if !is_exposed {
None
} else {
// Get the name from the metadata here (since it also requires a reference to the `network_interface`)
let (input_name, input_description) = network_interface.displayed_input_name_and_description(&node_id, index, breadcrumb_network_path);
Some(InputLookup {
input_name,
input_description,
output_connector,
..Default::default()
})
};
frontend_inputs_lookup.entry(node_id).or_insert_with(Vec::new).push(lookup);
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
let (ty, type_source) = network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path);
value.ty = ty;
value.type_source = type_source;
}
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
value.valid_types = network_interface.valid_input_types(&InputConnector::node(node_id, index), breadcrumb_network_path);
}
}
frontend_inputs_lookup
}
impl Default for NodeGraphMessageHandler {
fn default() -> Self {
Self {
@@ -11,6 +11,7 @@ use glam::{DAffine2, DVec2};
use graph_craft::Type;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graphene_std::NodeInputDecleration;
use graphene_std::animation::RealTimeMode;
use graphene_std::extract_xy::XY;
use graphene_std::path_bool::BooleanOperation;
@@ -19,16 +20,11 @@ use graphene_std::raster::{
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
SelectiveColorChoice,
};
use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font;
use graphene_std::table::{Table, TableRow};
use graphene_std::text::{Font, TextAlign};
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::GridType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{CentroidType, PointSpacingType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
let widget = TextLabel::new(text).widget_holder();
@@ -151,16 +147,16 @@ 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(),
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
Some("PixelSize") => vec2_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None, false),
Some("TextArea") => text_area_widget(default_info).into(),
// For all other types, use TypeId-based matching
@@ -170,29 +166,25 @@ 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(),
Some(x) if x == TypeId::of::<String>() => text_widget(default_info).into(),
Some(x) if x == TypeId::of::<DVec2>() => coordinate_widget(default_info, "X", "Y", "", None, false),
Some(x) if x == TypeId::of::<DVec2>() => vec2_widget(default_info, "X", "Y", "", None, false),
Some(x) if x == TypeId::of::<DAffine2>() => transform_widget(default_info, &mut extra_widgets),
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default()),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default()),
// ==========================
// PRIMITIVE COLLECTION TYPES
// ==========================
Some(x) if x == TypeId::of::<Vec<f64>>() => array_of_number_widget(default_info, TextInput::default()).into(),
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_coordinates_widget(default_info, TextInput::default()).into(),
// ====================
// GRAPHICAL DATA TYPES
// ====================
Some(x) if x == TypeId::of::<VectorDataTable>() => vector_data_widget(default_info).into(),
Some(x) if x == TypeId::of::<RasterDataTable<CPU>>() || x == TypeId::of::<RasterDataTable<GPU>>() => raster_widget(default_info).into(),
Some(x) if x == TypeId::of::<GraphicGroupTable>() => group_widget(default_info).into(),
Some(x) if x == TypeId::of::<Vec<DVec2>>() => array_of_vec2_widget(default_info, TextInput::default()).into(),
// ============
// STRUCT TYPES
// ============
Some(x) if x == TypeId::of::<Color>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Option<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Table<Color>>() => color_widget(default_info, ColorInput::default().allow_none(true)),
Some(x) if x == TypeId::of::<Table<GradientStops>>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<GradientStops>() => color_widget(default_info, ColorInput::default().allow_none(false)),
Some(x) if x == TypeId::of::<Font>() => font_widget(default_info),
Some(x) if x == TypeId::of::<Curve>() => curve_widget(default_info),
@@ -223,6 +215,7 @@ pub(crate) fn property_from_type(
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<TextAlign>() => enum_choice::<TextAlign>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
@@ -479,6 +472,10 @@ pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
resolution_widgets.push(
NumberInput::new(Some((footprint.resolution.as_dvec2() / bounds).x * 100.))
.label("Resolution")
.mode_range()
.min(0.)
.range_min(Some(1.))
.range_max(Some(100.))
.unit("%")
.on_update(update_value(
move |x: &NumberInput| {
@@ -625,7 +622,7 @@ pub fn transform_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widg
}
}
pub fn coordinate_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
pub fn vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, x: &str, y: &str, unit: &str, min: Option<f64>, is_integer: bool) -> LayoutGroup {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info);
@@ -722,7 +719,7 @@ pub fn array_of_number_widget(parameter_widgets_info: ParameterWidgetsInfo, text
widgets
}
pub fn array_of_coordinates_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
pub fn array_of_vec2_widget(parameter_widgets_info: ParameterWidgetsInfo, text_props: TextInput) -> Vec<WidgetHolder> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
let mut widgets = start_widgets(parameter_widgets_info);
@@ -791,33 +788,6 @@ pub fn font_inputs(parameter_widgets_info: ParameterWidgetsInfo) -> (Vec<WidgetH
(first_widgets, second_widgets)
}
pub fn vector_data_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(parameter_widgets_info);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(TextLabel::new("Vector data is supplied through the node graph").widget_holder());
widgets
}
pub fn raster_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(parameter_widgets_info);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(TextLabel::new("Raster data is supplied through the node graph").widget_holder());
widgets
}
pub fn group_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(parameter_widgets_info);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(TextLabel::new("Group data is supplied through the node graph").widget_holder());
widgets
}
pub fn number_widget(parameter_widgets_info: ParameterWidgetsInfo, number_props: NumberInput) -> Vec<WidgetHolder> {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
@@ -837,6 +807,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
@@ -883,6 +861,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(),
]),
_ => {}
}
@@ -941,35 +928,62 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input
match &**tagged_value {
TaggedValue::Color(color) => widgets.push(
TaggedValue::ColorNotInTable(color) => widgets.push(
color_button
.value(FillChoice::Solid(*color))
.on_update(update_value(|x: &ColorInput| TaggedValue::Color(x.value.as_solid().unwrap_or_default()), node_id, index))
.allow_none(false)
.on_update(update_value(|input: &ColorInput| TaggedValue::ColorNotInTable(input.value.as_solid().unwrap()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::OptionalColor(color) => widgets.push(
TaggedValue::OptionalColorNotInTable(color) => widgets.push(
color_button
.value(match color {
Some(color) => FillChoice::Solid(*color),
.value(color.map_or(FillChoice::None, FillChoice::Solid))
.allow_none(true)
.on_update(update_value(|input: &ColorInput| TaggedValue::OptionalColorNotInTable(input.value.as_solid()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::Color(color_table) => widgets.push(
color_button
.value(match color_table.iter().next() {
Some(color) => FillChoice::Solid(*color.element),
None => FillChoice::None,
})
.on_update(update_value(|x: &ColorInput| TaggedValue::OptionalColor(x.value.as_solid()), node_id, index))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::GradientStops(x) => widgets.push(
color_button
.value(FillChoice::Gradient(x.clone()))
.on_update(update_value(
|x: &ColorInput| TaggedValue::GradientStops(x.value.as_gradient().cloned().unwrap_or_default()),
|input: &ColorInput| TaggedValue::Color(input.value.as_solid().iter().map(|&color| TableRow::new_from_element(color)).collect()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
_ => {}
TaggedValue::GradientTable(gradient_table) => widgets.push(
color_button
.value(match gradient_table.iter().next() {
Some(row) => FillChoice::Gradient(row.element.clone()),
None => FillChoice::None,
})
.on_update(update_value(
|input: &ColorInput| TaggedValue::GradientTable(input.value.as_gradient().iter().map(|&gradient| TableRow::new_from_element(gradient.clone())).collect()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
TaggedValue::GradientStops(gradient_stops) => widgets.push(
color_button
.value(FillChoice::Gradient(gradient_stops.clone()))
.on_update(update_value(
|input: &ColorInput| TaggedValue::GradientStops(input.value.as_gradient().cloned().unwrap_or_default()),
node_id,
index,
))
.on_commit(commit_value)
.widget_holder(),
),
x => warn!("Colour {x:?}"),
}
LayoutGroup::Row { widgets }
@@ -1248,7 +1262,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
match grid_type {
GridType::Rectangular => {
let spacing = coordinate_widget(ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::INDEX, true, context), "W", "H", " px", Some(0.), false);
let spacing = vec2_widget(ParameterWidgetsInfo::new(node_id, SpacingInput::<f64>::INDEX, true, context), "W", "H", " px", Some(0.), false);
widgets.push(spacing);
}
GridType::Isometric => {
@@ -1258,7 +1272,7 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
NumberInput::default().label("H").min(0.).unit(" px"),
),
};
let angles = coordinate_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false);
let angles = vec2_widget(ParameterWidgetsInfo::new(node_id, AnglesInput::INDEX, true, context), "", "", "°", None, false);
widgets.extend([spacing, angles]);
}
}
@@ -1592,7 +1606,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}
};
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), &Some(&TaggedValue::OptionalColor(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), Some(TaggedValue::Color(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
&document_node.inputs[BackupColorInput::INDEX].as_value(),
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
@@ -1602,7 +1616,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
return vec![LayoutGroup::Row { widgets: widgets_first_row }];
};
let fill2 = fill.clone();
let backup_color_fill: Fill = backup_color.into();
let backup_color_fill: Fill = backup_color.clone().into();
let backup_gradient_fill: Fill = backup_gradient.clone().into();
widgets_first_row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@@ -1615,13 +1629,13 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Fill::None => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(None),
value: TaggedValue::Color(Table::new()),
}
.into(),
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupColorInput::INDEX,
value: TaggedValue::OptionalColor(Some(*color)),
value: TaggedValue::Color(Table::new_from_element(*color)),
}
.into(),
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
@@ -1783,7 +1797,7 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
let miter_limit_disabled = join_value != &StrokeJoin::Miter;
let color = color_widget(
ParameterWidgetsInfo::new(node_id, ColorInput::<Option<Color>>::INDEX, true, context),
ParameterWidgetsInfo::new(node_id, ColorInput::INDEX, true, context),
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
);
let weight = number_widget(ParameterWidgetsInfo::new(node_id, WeightInput::INDEX, true, context), NumberInput::default().unit(" px").min(0.));
@@ -1877,7 +1891,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
let mut expression = x.value.trim().to_string();
if ["+", "-", "*", "/", "^", "%"].iter().any(|&infix| infix == expression) {
expression = format!("A {} B", expression);
expression = format!("A {expression} B");
} else if expression == "^" {
expression = String::from("A^B");
}
@@ -1938,7 +1952,7 @@ pub mod choice {
use super::ParameterWidgetsInfo;
use crate::messages::tool::tool_messages::tool_prelude::*;
use graph_craft::document::value::TaggedValue;
use graphene_std::registry::{ChoiceTypeStatic, ChoiceWidgetHint};
use graphene_std::choice_type::{ChoiceTypeStatic, ChoiceWidgetHint};
use std::marker::PhantomData;
pub trait WidgetFactory {
@@ -1992,16 +2006,13 @@ pub mod choice {
{
let items = E::list()
.iter()
.map(|group| {
group
.map(|section| {
section
.iter()
.map(|(item, metadata)| {
let updater = updater_factory();
let committer = committer_factory();
MenuListEntry::new(metadata.name.as_ref())
.label(metadata.label.as_ref())
.on_update(move |_| updater(item))
.on_commit(committer)
MenuListEntry::new(metadata.name).label(metadata.label).on_update(move |_| updater(item)).on_commit(committer)
})
.collect()
})
@@ -2016,15 +2027,15 @@ pub mod choice {
{
let items = E::list()
.iter()
.flat_map(|group| group.iter())
.flat_map(|section| section.iter())
.map(|(item, var_meta)| {
let updater = updater_factory();
let committer = committer_factory();
let entry = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(item)).on_commit(committer);
match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) {
(None, None) => entry.label(var_meta.label.as_ref()),
(None, Some(doc)) => entry.label(var_meta.label.as_ref()).tooltip(doc),
(Some(icon), None) => entry.icon(icon).tooltip(var_meta.label.as_ref()),
let entry = RadioEntryData::new(var_meta.name).on_update(move |_| updater(item)).on_commit(committer);
match (var_meta.icon, var_meta.docstring) {
(None, None) => entry.label(var_meta.label),
(None, Some(doc)) => entry.label(var_meta.label).tooltip(doc),
(Some(icon), None) => entry.icon(icon).tooltip(var_meta.label),
(Some(icon), Some(doc)) => entry.icon(icon).tooltip(format!("{}\n\n{}", var_meta.label, doc)),
}
})
@@ -2078,7 +2089,7 @@ pub mod choice {
pub fn property_row(self) -> LayoutGroup {
let ParameterWidgetsInfo { document_node, node_id, index, .. } = self.parameter_info;
let Some(document_node) = document_node else {
log::error!("Could not get document node when building property row for node {:?}", node_id);
log::error!("Could not get document node when building property row for node {node_id:?}");
return LayoutGroup::Row { widgets: Vec::new() };
};
@@ -1,4 +1,5 @@
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector, TypeSource};
use crate::messages::portfolio::document::utility_types::network_interface::TypeSource;
use glam::IVec2;
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_std::Type;
@@ -8,29 +9,35 @@ use std::borrow::Cow;
pub enum FrontendGraphDataType {
#[default]
General,
Raster,
VectorData,
Number,
Group,
Artboard,
Graphic,
Raster,
Vector,
Color,
Gradient,
Typography,
}
impl FrontendGraphDataType {
pub fn from_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::Image(_) | TaggedValue::RasterData(_) => Self::Raster,
TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::VectorData,
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F32(_)
| TaggedValue::F64(_)
| TaggedValue::DVec2(_)
| TaggedValue::OptionalDVec2(_)
| TaggedValue::F64Array4(_)
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::GraphicGroup(_) | TaggedValue::GraphicElement(_) => Self::Group, // TODO: Is GraphicElement supposed to be included here?
TaggedValue::ArtboardGroup(_) => Self::Artboard,
TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::Color(_) => Self::Color,
TaggedValue::Gradient(_) | TaggedValue::GradientStops(_) | TaggedValue::GradientTable(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography,
_ => Self::General,
}
}
@@ -54,7 +61,8 @@ pub struct FrontendGraphInput {
#[serde(rename = "validTypes")]
pub valid_types: Vec<String>,
#[serde(rename = "connectedTo")]
pub connected_to: Option<OutputConnector>,
/// Either "nothing", "import index {index}", or "{node name} output {output_index}".
pub connected_to: String,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -62,11 +70,13 @@ pub struct FrontendGraphOutput {
#[serde(rename = "dataType")]
pub data_type: FrontendGraphDataType,
pub name: String,
pub description: String,
#[serde(rename = "resolvedType")]
pub resolved_type: String,
pub description: String,
/// If connected to an export, it is "export index {index}".
/// If connected to a node, it is "{node name} input {input_index}".
#[serde(rename = "connectedTo")]
pub connected_to: Vec<InputConnector>,
pub connected_to: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -87,13 +97,15 @@ pub struct FrontendNode {
pub primary_output: Option<FrontendGraphOutput>,
#[serde(rename = "exposedOutputs")]
pub exposed_outputs: Vec<FrontendGraphOutput>,
pub position: (i32, i32),
#[serde(rename = "primaryOutputConnectedToLayer")]
pub primary_output_connected_to_layer: bool,
#[serde(rename = "primaryInputConnectedToLayer")]
pub primary_input_connected_to_layer: bool,
pub position: IVec2,
pub visible: bool,
pub locked: bool,
pub previewed: bool,
pub errors: Option<String>,
#[serde(rename = "uiOnly")]
pub ui_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -178,8 +190,8 @@ pub struct FrontendClickTargets {
pub node_click_targets: Vec<String>,
#[serde(rename = "layerClickTargets")]
pub layer_click_targets: Vec<String>,
#[serde(rename = "portClickTargets")]
pub port_click_targets: Vec<String>,
#[serde(rename = "connectorClickTargets")]
pub connector_click_targets: Vec<String>,
#[serde(rename = "iconClickTargets")]
pub icon_click_targets: Vec<String>,
#[serde(rename = "allNodesBoundingBox")]
@@ -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>| {
@@ -2,6 +2,7 @@ pub mod grid_overlays;
mod overlays_message;
mod overlays_message_handler;
pub mod utility_functions;
#[cfg_attr(not(target_family = "wasm"), path = "utility_types_vello.rs")]
pub mod utility_types;
#[doc(inline)]
@@ -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,
},
}
@@ -11,21 +11,24 @@ pub struct OverlaysMessageContext<'a> {
#[derive(Debug, Clone, Default, ExtractField)]
pub struct OverlaysMessageHandler {
pub overlay_providers: HashSet<OverlayProvider>,
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
canvas: Option<web_sys::HtmlCanvasElement>,
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
context: Option<web_sys::CanvasRenderingContext2d>,
}
#[message_handler_data]
impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMessageHandler {
fn process_message(&mut self, message: OverlaysMessage, responses: &mut VecDeque<Message>, context: OverlaysMessageContext) {
let OverlaysMessageContext { visibility_settings, ipp, .. } = context;
#[cfg(target_arch = "wasm32")]
let device_pixel_ratio = context.device_pixel_ratio;
let OverlaysMessageContext {
visibility_settings,
ipp,
device_pixel_ratio,
..
} = context;
match message {
#[cfg(target_arch = "wasm32")]
#[cfg(target_family = "wasm")]
OverlaysMessage::Draw => {
use super::utility_functions::overlay_canvas_element;
use super::utility_types::OverlayContext;
@@ -53,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(),
@@ -69,14 +74,31 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageContext<'_>> for OverlaysMes
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_family = "wasm"), not(test)))]
OverlaysMessage::Draw => {
warn!("Cannot render overlays on non-Wasm targets.\n{responses:?} {visibility_settings:?} {ipp:?}",);
use super::utility_types::OverlayContext;
let size = ipp.viewport_bounds.size();
let overlay_context = OverlayContext::new(size, device_pixel_ratio, visibility_settings);
if visibility_settings.all() {
responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() });
for provider in &self.overlay_providers {
responses.add(provider(overlay_context.clone()));
}
}
responses.add(FrontendMessage::RenderOverlays { context: overlay_context });
}
OverlaysMessage::AddProvider(message) => {
#[cfg(all(not(target_family = "wasm"), test))]
OverlaysMessage::Draw => {
let _ = (responses, visibility_settings, ipp, device_pixel_ratio);
}
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 bezier_rs::{Bezier, BezierHandles};
use glam::{DAffine2, DVec2};
use graphene_std::vector::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::vector::misc::ManipulatorPointId;
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_data) = network_interface.compute_modified_vector(layer) else { continue };
for (segment_id, _bezier, start, end) in vector_data.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
}
@@ -118,51 +127,54 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
let display_anchors = overlay_context.visibility_settings.anchors();
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface);
if display_path {
overlay_context.outline_vector(&vector_data, transform);
overlay_context.outline_vector(&vector, transform);
}
// Get the selected segments and then add a bold line overlay on them
for (segment_id, bezier, _, _) in vector_data.segment_bezier_iter() {
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
continue;
};
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() {
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_data.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 => {
vector_data.segment_bezier_iter().for_each(|(segment_id, bezier, _start, _end)| {
vector.segment_bezier_iter().for_each(|(segment_id, bezier, _start, _end)| {
overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context);
});
}
DrawHandles::SelectedAnchors(ref selected_segments) => {
vector_data
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);
});
for (segment_id, bezier, start, end) in vector_data.segment_bezier_iter() {
for (segment_id, bezier, start, end) in vector.segment_bezier_iter() {
if let Some((corresponding_anchor, _)) = opposite_handles_data.iter().find(|(_, adj_segment_id)| adj_segment_id == &segment_id) {
overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), *corresponding_anchor, transform, is_selected, overlay_context);
}
}
}
DrawHandles::FrontierHandles(ref segment_endpoints) => {
vector_data
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))
.for_each(|(segment_id, bezier, start, end)| {
@@ -179,7 +191,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
}
if display_anchors {
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
for (&id, &position) in vector.point_domain.ids().iter().zip(vector.point_domain.positions()) {
overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(ManipulatorPointId::Anchor(id)), None);
}
}
@@ -192,7 +204,7 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
}
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else {
continue;
};
//let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
@@ -200,8 +212,8 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
let selected = shape_editor.selected_shape_state.get(&layer);
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
for point in vector_data.extendable_points(preferences.vector_meshes) {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
for point in vector.extendable_points(preferences.vector_meshes) {
let Some(position) = vector.point_domain.position_from_id(point) else { continue };
let position = transform.transform_point2(position);
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);
}
@@ -1,18 +1,21 @@
use super::utility_functions::overlay_canvas_context;
use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE,
COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH,
PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS,
};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath};
use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::{PointId, SegmentId, VectorData};
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg};
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
@@ -23,7 +26,7 @@ pub fn empty_provider() -> OverlayProvider {
|_| Message::NoOp
}
// Types of overlays used by DocumentMessage to enable/disable select group of overlays in the frontend
/// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays.
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum OverlaysType {
ArtboardName,
@@ -294,6 +297,147 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
#[allow(clippy::too_many_arguments)]
pub fn dashed_ellipse(
&mut self,
center: DVec2,
radius_x: f64,
radius_y: f64,
rotation: Option<f64>,
start_angle: Option<f64>,
end_angle: Option<f64>,
counterclockwise: Option<bool>,
color_fill: Option<&str>,
color_stroke: Option<&str>,
dash_width: Option<f64>,
dash_gap_width: Option<f64>,
dash_offset: Option<f64>,
) {
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let center = center.round();
self.start_dpi_aware_transform();
if let Some(dash_width) = dash_width {
let dash_gap_width = dash_gap_width.unwrap_or(1.);
let array = js_sys::Array::new();
array.push(&JsValue::from(dash_width));
array.push(&JsValue::from(dash_gap_width));
if let Some(dash_offset) = dash_offset {
if dash_offset != 0. {
self.render_context.set_line_dash_offset(dash_offset);
}
}
self.render_context
.set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
self.render_context.begin_path();
self.render_context
.ellipse_with_anticlockwise(
center.x,
center.y,
radius_x,
radius_y,
rotation.unwrap_or_default(),
start_angle.unwrap_or_default(),
end_angle.unwrap_or(TAU),
counterclockwise.unwrap_or_default(),
)
.expect("Failed to draw ellipse");
self.render_context.set_stroke_style_str(color_stroke);
if let Some(fill_color) = color_fill {
self.render_context.set_fill_style_str(fill_color);
self.render_context.fill();
}
self.render_context.stroke();
// Reset the dash pattern back to solid
if dash_width.is_some() {
self.render_context
.set_line_dash(&JsValue::from(js_sys::Array::new()))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
if dash_offset.is_some() && dash_offset != Some(0.) {
self.render_context.set_line_dash_offset(0.);
}
self.end_dpi_aware_transform();
}
pub fn dashed_circle(
&mut self,
position: DVec2,
radius: f64,
color_fill: Option<&str>,
color_stroke: Option<&str>,
dash_width: Option<f64>,
dash_gap_width: Option<f64>,
dash_offset: Option<f64>,
transform: Option<DAffine2>,
) {
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round();
self.start_dpi_aware_transform();
if let Some(transform) = transform {
let [a, b, c, d, e, f] = transform.to_cols_array();
self.render_context.transform(a, b, c, d, e, f).expect("Failed to transform circle");
}
if let Some(dash_width) = dash_width {
let dash_gap_width = dash_gap_width.unwrap_or(1.);
let array = js_sys::Array::new();
array.push(&JsValue::from(dash_width));
array.push(&JsValue::from(dash_gap_width));
if let Some(dash_offset) = dash_offset {
if dash_offset != 0. {
self.render_context.set_line_dash_offset(dash_offset);
}
}
self.render_context
.set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
self.render_context.begin_path();
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
self.render_context.set_stroke_style_str(color_stroke);
if let Some(fill_color) = color_fill {
self.render_context.set_fill_style_str(fill_color);
self.render_context.fill();
}
self.render_context.stroke();
// Reset the dash pattern back to solid
if dash_width.is_some() {
self.render_context
.set_line_dash(&JsValue::from(js_sys::Array::new()))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
if dash_offset.is_some() && dash_offset != Some(0.) {
self.render_context.set_line_dash_offset(0.);
}
self.end_dpi_aware_transform();
}
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
self.dashed_circle(position, radius, color_fill, color_stroke, None, None, None, None);
}
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
self.start_dpi_aware_transform();
@@ -319,6 +463,42 @@ impl OverlayContext {
self.square(position, None, Some(color_fill), Some(color_stroke));
}
pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) {
self.start_dpi_aware_transform();
let position = position.round() - DVec2::splat(0.5);
self.render_context.begin_path();
self.render_context
.arc(position.x, position.y, (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2., 0., TAU)
.expect("Failed to draw the circle");
self.render_context.set_fill_style_str(COLOR_OVERLAY_BLUE_50);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
self.render_context.fill();
self.render_context.stroke();
self.render_context.begin_path();
self.render_context
.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU)
.expect("Failed to draw the circle");
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.render_context.set_fill_style_str(color_fill);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.fill();
self.render_context.stroke();
self.end_dpi_aware_transform();
}
pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) {
self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50));
let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE));
}
/// Transforms the canvas context to adjust for DPI scaling
///
/// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`].
@@ -374,23 +554,6 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round();
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
self.render_context.set_fill_style_str(color_fill);
self.render_context.set_stroke_style_str(color_stroke);
self.render_context.fill();
self.render_context.stroke();
self.end_dpi_aware_transform();
}
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
let step = (end_at - start_from) / segments as f64;
@@ -411,11 +574,7 @@ impl OverlayContext {
let handle_start = start + start_vec.perp() * radius * factor;
let handle_end = end - end_vec.perp() * radius * factor;
let bezier = Bezier {
start,
end,
handles: bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
};
let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end)));
self.bezier_command(bezier, DAffine2::IDENTITY, i == 0);
}
@@ -423,6 +582,12 @@ impl OverlayContext {
self.render_context.stroke();
}
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
self.line(pivot, end_point1, None, None);
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
}
pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
@@ -584,13 +749,19 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
self.manipulator_handle(end_point_position, true, None);
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
}
/// Used by the Pen and Path tools to outline the path of the shape.
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
pub fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
let mut last_point = None;
for (_, bezier, start_id, end_id) in vector_data.segment_bezier_iter() {
for (_, bezier, start_id, end_id) in vector.segment_iter() {
let move_to = last_point != Some(start_id);
last_point = Some(end_id);
@@ -604,7 +775,7 @@ impl OverlayContext {
}
/// Used by the Pen tool in order to show how the bezier curve would look like.
pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
@@ -616,13 +787,13 @@ impl OverlayContext {
}
/// Used by the path tool segment mode in order to show the selected segments.
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.bezier_command(bezier, transform, true);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
self.render_context.set_line_width(4.);
self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS);
self.render_context.stroke();
self.render_context.set_line_width(1.);
@@ -630,13 +801,13 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) {
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.bezier_command(bezier, transform, true);
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
self.render_context.set_line_width(4.);
self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS);
self.render_context.stroke();
self.render_context.set_line_width(1.);
@@ -644,18 +815,18 @@ impl OverlayContext {
self.end_dpi_aware_transform();
}
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
fn bezier_command(&self, bezier: PathSeg, transform: DAffine2, move_to: bool) {
self.start_dpi_aware_transform();
let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point));
let bezier = Affine::new(transform.to_cols_array()) * bezier;
if move_to {
self.render_context.move_to(start.x, start.y);
self.render_context.move_to(bezier.start().x, bezier.start().y);
}
match handles {
bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y),
bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y),
match bezier.as_path_el() {
kurbo::PathEl::LineTo(point) => self.render_context.line_to(point.x, point.y),
kurbo::PathEl::QuadTo(point, point1) => self.render_context.quadratic_curve_to(point.x, point.y, point1.x, point1.y),
kurbo::PathEl::CurveTo(point, point1, point2) => self.render_context.bezier_curve_to(point.x, point.y, point1.x, point1.y, point2.x, point2.y),
_ => unreachable!(),
}
self.end_dpi_aware_transform();
@@ -669,36 +840,35 @@ impl OverlayContext {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let Some(first) = curves.peek() else {
let Some(&first) = curves.peek() else {
continue;
};
self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
for curve in curves {
match curve.handles {
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
self.render_context.move_to(start_point.x, start_point.y);
self.render_context.line_to(a.x, a.y)
for curve in curves {
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = a.round() - DVec2::splat(0.5);
self.render_context.line_to(a.x, a.y);
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let a = transform.transform_point2(handle);
let b = transform.transform_point2(curve.end());
PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let a = transform.transform_point2(handle_start);
let b = transform.transform_point2(handle_end);
let c = transform.transform_point2(curve.end());
PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
}
}
}
@@ -713,7 +883,7 @@ impl OverlayContext {
/// Used by the Select tool to outline a path or a free point when selected or hovered.
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<bezier_rs::Subpath<PointId>> = vec![];
let mut subpaths: Vec<Subpath<PointId>> = vec![];
target_types.for_each(|target_type| match target_type.borrow() {
ClickTargetType::FreePoint(point) => {
@@ -855,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,
}
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,7 @@ pub struct PropertiesPanelMessageContext<'a> {
pub document_name: &'a str,
pub executor: &'a mut NodeGraphExecutor,
pub persistent_data: &'a PersistentData,
pub properties_panel_open: bool,
}
#[derive(Debug, Clone, Default, ExtractField)]
@@ -28,16 +29,22 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
document_name,
executor,
persistent_data,
properties_panel_open,
} = context;
match message {
PropertiesPanelMessage::Clear => {
responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
layout_target: LayoutTarget::PropertiesSections,
layout_target: LayoutTarget::PropertiesPanel,
});
}
PropertiesPanelMessage::Refresh => {
if !properties_panel_open {
responses.add(PropertiesPanelMessage::Clear);
return;
}
let mut node_properties_context = NodePropertiesContext {
persistent_data,
responses,
@@ -50,7 +57,7 @@ impl MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageContext<'_>> f
node_properties_context.responses.add(LayoutMessage::SendLayout {
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
layout_target: LayoutTarget::PropertiesSections,
layout_target: LayoutTarget::PropertiesPanel,
});
}
}
@@ -6,9 +6,10 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData};
use graphene_std::vector::{PointId, Vector};
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
@@ -22,11 +23,11 @@ use std::num::NonZeroU64;
pub struct DocumentMetadata {
pub upstream_footprints: HashMap<NodeId, Footprint>,
pub local_transforms: HashMap<NodeId, DAffine2>,
pub first_instance_source_ids: HashMap<NodeId, Option<NodeId>>,
pub first_element_source_ids: HashMap<NodeId, Option<NodeId>>,
pub structure: HashMap<LayerNodeIdentifier, NodeRelations>,
pub click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
pub clip_targets: HashSet<NodeId>,
pub vector_modify: HashMap<NodeId, VectorData>,
pub vector_modify: HashMap<NodeId, Vector>,
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
}
@@ -90,8 +91,8 @@ impl DocumentMetadata {
let mut use_local = true;
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, network_interface);
if let Some(path_node) = graph_layer.upstream_node_id_from_name("Path") {
if let Some(&source) = self.first_instance_source_ids.get(&layer.to_node()) {
if let Some(path_node) = graph_layer.upstream_visible_node_id_from_name_in_layer("Path") {
if let Some(&source) = self.first_element_source_ids.get(&layer.to_node()) {
if !network_interface
.upstream_flow_back_from_nodes(vec![path_node], &[], FlowType::HorizontalFlow)
.any(|upstream| Some(upstream) == source)
@@ -160,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`.
@@ -196,7 +208,7 @@ impl DocumentMetadata {
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
}
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().filter_map(|target| match target.target_type() {
@@ -303,10 +315,10 @@ impl LayerNodeIdentifier {
child.ancestors(metadata).any(|ancestor| ancestor == self)
}
/// Is the layer last child of parent group? Used for clipping
/// Is the layer the last child of its stack? Used for clipping
pub fn can_be_clipped(self, metadata: &DocumentMetadata) -> bool {
self.parent(metadata)
.map_or(false, |layer| layer.last_child(metadata).expect("Parent accessed via child should have children") != self)
.is_some_and(|layer| layer.last_child(metadata).expect("Parent accessed via child should have children") != self)
}
/// Iterator over all direct children (excluding self and recursive children)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,136 @@
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, InputMetadata, InputPersistentMetadata, NodeNetworkMetadata, NodeTypePersistentMetadata};
use serde_json::Value;
use std::collections::HashMap;
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataInputNames {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_names: Vec<String>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataInputNames> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataInputNames) -> Self {
DocumentNodePersistentMetadata {
input_metadata: Vec::new(),
..old.into()
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataPropertiesRow {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_properties: Vec<PropertiesRow>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PropertiesRow {
pub input_data: HashMap<String, Value>,
pub widget_override: Option<String>,
#[serde(skip)]
pub input_name: String,
#[serde(skip)]
pub input_description: String,
}
impl From<DocumentNodePersistentMetadataPropertiesRow> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataPropertiesRow) -> Self {
let mut input_metadata = Vec::new();
for properties_row in old.input_properties {
input_metadata.push(InputMetadata {
persistent_metadata: InputPersistentMetadata {
input_data: properties_row.input_data,
widget_override: properties_row.widget_override,
input_name: properties_row.input_name,
input_description: properties_row.input_description,
},
..Default::default()
})
}
DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_metadata: Vec::new(),
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
/// Persistent metadata for each node in the network, which must be included when creating, serializing, and deserializing saving a node.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DocumentNodePersistentMetadataHasPrimaryOutput {
pub reference: Option<String>,
#[serde(default)]
pub display_name: String,
pub input_metadata: Vec<InputMetadata>,
pub output_names: Vec<String>,
pub has_primary_output: bool,
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub pinned: bool,
pub node_type_metadata: NodeTypePersistentMetadata,
pub network_metadata: Option<NodeNetworkMetadata>,
}
impl From<DocumentNodePersistentMetadataHasPrimaryOutput> for DocumentNodePersistentMetadata {
fn from(old: DocumentNodePersistentMetadataHasPrimaryOutput) -> Self {
DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_metadata: old.input_metadata,
output_names: old.output_names,
locked: old.locked,
pinned: old.pinned,
node_type_metadata: old.node_type_metadata,
network_metadata: old.network_metadata,
}
}
}
pub fn deserialize_node_persistent_metadata<'de, D>(deserializer: D) -> Result<DocumentNodePersistentMetadata, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let value = Value::deserialize(deserializer)?;
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadataHasPrimaryOutput>(value.clone()) {
return Ok(document.into());
};
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadata>(value.clone()) {
return Ok(document);
};
if let Ok(document) = serde_json::from_value::<DocumentNodePersistentMetadataPropertiesRow>(value.clone()) {
return Ok(document.into());
};
match serde_json::from_value::<DocumentNodePersistentMetadataInputNames>(value.clone()) {
Ok(document) => Ok(document.into()),
Err(e) => Err(serde::de::Error::custom(e)),
}
}
@@ -61,6 +61,7 @@ pub struct LayerPanelEntry {
pub clippable: bool,
}
/// IMPORTANT: the same node may appear multiple times.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
pub struct SelectedNodes(pub Vec<NodeId>);
@@ -129,6 +130,7 @@ impl SelectedNodes {
self.selected_layers(metadata).any(|selected| selected == layer)
}
/// IMPORTANT: the same node may appear multiple times.
pub fn selected_nodes(&self) -> impl Iterator<Item = &NodeId> + '_ {
self.0.iter()
}
@@ -8,7 +8,8 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::utility_types::ToolType;
use glam::{DAffine2, DMat2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, VectorModificationType};
use graphene_std::vector::misc::{HandleId, ManipulatorPointId};
use graphene_std::vector::{HandleExt, PointId, VectorModificationType};
use std::collections::{HashMap, VecDeque};
use std::f64::consts::PI;
@@ -79,7 +80,7 @@ impl OriginalTransforms {
if path_map.contains_key(&layer) {
continue;
}
let Some(vector_data) = network_interface.compute_modified_vector(layer) else {
let Some(vector) = network_interface.compute_modified_vector(layer) else {
continue;
};
let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
@@ -91,7 +92,7 @@ impl OriginalTransforms {
let mut selected_points = selected_points.clone();
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
for (segment_id, _, start, end) in vector.segment_bezier_iter() {
if selected_segments.contains(&segment_id) {
selected_points.insert(ManipulatorPointId::Anchor(start));
selected_points.insert(ManipulatorPointId::Anchor(end));
@@ -100,23 +101,23 @@ impl OriginalTransforms {
// Anchors also move their handles
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());
let anchors = anchor_ids.filter_map(|id| vector_data.point_domain.position_from_id(id).map(|pos| (id, AnchorPoint { initial: pos, current: pos })));
let anchors = anchor_ids.filter_map(|id| vector.point_domain.position_from_id(id).map(|pos| (id, AnchorPoint { initial: pos, current: pos })));
let anchors = anchors.collect();
let selected_handles = selected_points.iter().filter_map(|point| point.as_handle());
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());
let connected_handles = anchor_ids.flat_map(|point| vector_data.all_connected(point));
let connected_handles = anchor_ids.flat_map(|point| vector.all_connected(point));
let all_handles = selected_handles.chain(connected_handles);
let handles = all_handles
.filter_map(|id| {
let anchor = id.to_manipulator_point().get_anchor(&vector_data)?;
let initial = id.to_manipulator_point().get_position(&vector_data)?;
let relative = vector_data.point_domain.position_from_id(anchor)?;
let other_handle = vector_data
let anchor = id.to_manipulator_point().get_anchor(&vector)?;
let initial = id.to_manipulator_point().get_position(&vector)?;
let relative = vector.point_domain.position_from_id(anchor)?;
let other_handle = vector
.other_colinear_handle(id)
.filter(|other| !selected_points.contains(&other.to_manipulator_point()) && !selected_points.contains(&ManipulatorPointId::Anchor(anchor)));
let mirror = other_handle.and_then(|id| Some((id, id.to_manipulator_point().get_position(&vector_data)?)));
let mirror = other_handle.and_then(|id| Some((id, id.to_manipulator_point().get_position(&vector)?)));
Some((id, HandlePoint { initial, relative, anchor, mirror }))
})
@@ -517,8 +518,8 @@ impl<'a> Selected<'a> {
tool_type: &'a ToolType,
pen_handle: Option<&'a mut DVec2>,
) -> Self {
// If user is using the Select tool then use the original layer transforms
if (*tool_type == ToolType::Select) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) {
// If user is using the Select tool or Shape tool then use the original layer transforms
if (*tool_type == ToolType::Select || *tool_type == ToolType::Shape) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) {
*original_transforms = OriginalTransforms::Layer(HashMap::new());
}
@@ -1,8 +1,7 @@
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use bezier_rs::{ManipulatorGroup, Subpath};
use glam::{DVec2, IVec2};
use graphene_std::uuid::NodeId;
use graphene_std::vector::PointId;
use graphene_std::{uuid::NodeId, vector::misc::dvec2_to_point};
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, Point, Shape};
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct WirePath {
@@ -53,7 +52,7 @@ impl GraphWireStyle {
}
}
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> Subpath<PointId> {
pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool, graph_wire_style: GraphWireStyle) -> BezPath {
let grid_spacing = 24.;
match graph_wire_style {
GraphWireStyle::Direct => {
@@ -85,44 +84,21 @@ pub fn build_vector_wire(output_position: DVec2, input_position: DVec2, vertical
let delta01 = DVec2::new((locations[1].x - locations[0].x) * smoothing, (locations[1].y - locations[0].y) * smoothing);
let delta23 = DVec2::new((locations[3].x - locations[2].x) * smoothing, (locations[3].y - locations[2].y) * smoothing);
Subpath::new(
vec![
ManipulatorGroup {
anchor: locations[0],
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[1],
in_handle: None,
out_handle: Some(locations[1] + delta01),
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[2],
in_handle: Some(locations[2] - delta23),
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[3],
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
],
false,
)
let mut wire = BezPath::new();
wire.move_to(dvec2_to_point(locations[0]));
wire.line_to(dvec2_to_point(locations[1]));
wire.curve_to(dvec2_to_point(locations[1] + delta01), dvec2_to_point(locations[2] - delta23), dvec2_to_point(locations[2]));
wire.line_to(dvec2_to_point(locations[3]));
wire
}
GraphWireStyle::GridAligned => {
let locations = straight_wire_paths(output_position, input_position, vertical_out, vertical_in);
straight_wire_subpath(locations)
let locations = straight_wire_path(output_position, input_position, vertical_out, vertical_in);
straight_wire_to_bezpath(locations)
}
}
}
fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
fn straight_wire_path(output_position: DVec2, input_position: DVec2, vertical_out: bool, vertical_in: bool) -> Vec<IVec2> {
let grid_spacing = 24;
let line_width = 2;
@@ -446,40 +422,24 @@ fn straight_wire_paths(output_position: DVec2, input_position: DVec2, vertical_o
vec![IVec2::new(x1, y1), IVec2::new(x20, y1), IVec2::new(x20, y3), IVec2::new(x4, y3)]
}
fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
fn straight_wire_to_bezpath(locations: Vec<IVec2>) -> BezPath {
if locations.is_empty() {
return Subpath::new(Vec::new(), false);
return BezPath::new();
}
let to_point = |location: IVec2| Point::new(location.x as f64, location.y as f64);
if locations.len() == 2 {
return Subpath::new(
vec![
ManipulatorGroup {
anchor: locations[0].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
ManipulatorGroup {
anchor: locations[1].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
},
],
false,
);
let p1 = to_point(locations[0]);
let p2 = to_point(locations[1]);
Line::new(p1, p2).to_path(DEFAULT_ACCURACY);
}
let corner_radius = 10;
// Create path with rounded corners
let mut path = vec![ManipulatorGroup {
anchor: locations[0].into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
}];
let mut path = BezPath::new();
path.move_to(to_point(locations[0]));
for i in 1..(locations.len() - 1) {
let prev = locations[i - 1];
@@ -563,27 +523,9 @@ fn straight_wire_subpath(locations: Vec<IVec2>) -> Subpath<PointId> {
},
);
path.extend(vec![
ManipulatorGroup {
anchor: corner_start.into(),
in_handle: None,
out_handle: Some(corner_start_mid.into()),
id: PointId::generate(),
},
ManipulatorGroup {
anchor: corner_end.into(),
in_handle: Some(corner_end_mid.into()),
out_handle: None,
id: PointId::generate(),
},
])
path.line_to(to_point(corner_start));
path.curve_to(to_point(corner_start_mid), to_point(corner_end_mid), to_point(corner_end));
}
path.push(ManipulatorGroup {
anchor: (*locations.last().unwrap()).into(),
in_handle: None,
out_handle: None,
id: PointId::generate(),
});
Subpath::new(path, false)
path.line_to(to_point(*locations.last().unwrap()));
path
}
@@ -5,20 +5,22 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions:
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::DocumentMessageHandler;
use bezier_rs::Subpath;
use glam::IVec2;
use graph_craft::document::DocumentNode;
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
use graphene_std::ProtoNodeIdentifier;
use graphene_std::text::TypesettingConfig;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector;
use graphene_std::vector::style::{PaintOrder, StrokeAlign};
use graphene_std::vector::{VectorData, VectorDataTable};
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> {
@@ -27,22 +29,56 @@ pub struct NodeReplacement<'a> {
}
const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
// graphic element
// artboard
NodeReplacement {
node: graphene_std::graphic_element::append_artboard::IDENTIFIER,
aliases: &["graphene_core::AddArtboardNode"],
node: graphene_std::artboard::create_artboard::IDENTIFIER,
aliases: &[
"graphene_core::ConstructArtboardNode",
"graphene_core::graphic_element::ToArtboardNode",
"graphene_core::artboard::ToArtboardNode",
],
},
// graphic
NodeReplacement {
node: graphene_std::graphic::to_graphic::IDENTIFIER,
aliases: &[
"graphene_core::ToGraphicGroupNode",
"graphene_core::graphic_element::ToGroupNode",
"graphene_core::graphic::ToGroupNode",
],
},
NodeReplacement {
node: graphene_std::graphic_element::to_artboard::IDENTIFIER,
aliases: &["graphene_core::ConstructArtboardNode"],
node: graphene_std::graphic::wrap_graphic::IDENTIFIER,
aliases: &[
// Converted from "To Element"
"graphene_core::ToGraphicElementNode",
"graphene_core::graphic_element::ToElementNode",
"graphene_core::graphic::ToElementNode",
],
},
NodeReplacement {
node: graphene_std::graphic_element::to_element::IDENTIFIER,
aliases: &["graphene_core::ToGraphicElementNode"],
node: graphene_std::graphic::legacy_layer_extend::IDENTIFIER,
aliases: &[
"graphene_core::graphic_element::LayerNode",
"graphene_core::graphic::LayerNode",
// Converted from "Append Artboard"
"graphene_core::AddArtboardNode",
"graphene_core::graphic_element::AppendArtboardNode",
"graphene_core::graphic::AppendArtboardNode",
"graphene_core::artboard::AppendArtboardNode",
],
},
NodeReplacement {
node: graphene_std::graphic_element::to_group::IDENTIFIER,
aliases: &["graphene_core::ToGraphicGroupNode"],
node: graphene_std::graphic::flatten_graphic::IDENTIFIER,
aliases: &["graphene_core::graphic_element::FlattenGroupNode", "graphene_core::graphic::FlattenGroupNode"],
},
NodeReplacement {
node: graphene_std::graphic::flatten_vector::IDENTIFIER,
aliases: &["graphene_core::graphic_element::FlattenVectorNode"],
},
NodeReplacement {
node: graphene_std::graphic::index::IDENTIFIER,
aliases: &["graphene_core::graphic_element::IndexNode"],
},
// math_nodes
NodeReplacement {
@@ -186,13 +222,26 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::ops::PercentageValueNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::coordinate_value::IDENTIFIER,
node: graphene_std::math_nodes::vec_2_value::IDENTIFIER,
aliases: &[
"graphene_core::ops::CoordinateValueNode",
"graphene_core::ops::ConstructVector2",
"graphene_core::ops::Vector2ValueNode",
"graphene_core::ops::CoordinateValueNode",
"graphene_math_nodes::CoordinateValueNode",
],
},
NodeReplacement {
node: graphene_std::vector::cut_segments::IDENTIFIER,
aliases: &["graphene_core::vector::SplitSegmentsNode"],
},
NodeReplacement {
node: graphene_std::vector::cut_path::IDENTIFIER,
aliases: &["graphene_core::vector::SplitPathNode"],
},
NodeReplacement {
node: graphene_std::vector::vec_2_to_point::IDENTIFIER,
aliases: &["graphene_core::vector::PositionToPointNode"],
},
NodeReplacement {
node: graphene_std::math_nodes::color_value::IDENTIFIER,
aliases: &["graphene_core::ops::ColorValueNode"],
@@ -223,8 +272,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
aliases: &["graphene_core::ops::SomeNode"],
},
NodeReplacement {
node: graphene_std::debug::unwrap::IDENTIFIER,
aliases: &["graphene_core::ops::UnwrapNode"],
node: graphene_std::debug::unwrap_option::IDENTIFIER,
aliases: &["graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"],
},
NodeReplacement {
node: graphene_std::debug::clone::IDENTIFIER,
@@ -251,7 +300,24 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::vector::auto_tangents::IDENTIFIER,
aliases: &["graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::RemoveHandlesNode"],
},
// raster::adjustments
// graphene_raster_nodes::blending_nodes
NodeReplacement {
node: graphene_std::raster_nodes::blending_nodes::blend::IDENTIFIER,
aliases: &[
"graphene_raster_nodes::adjustments::BlendNode",
"graphene_core::raster::adjustments::BlendNode",
"graphene_core::raster::BlendNode",
],
},
NodeReplacement {
node: graphene_std::raster_nodes::blending_nodes::color_overlay::IDENTIFIER,
aliases: &[
"graphene_raster_nodes::adjustments::ColorOverlayNode",
"graphene_core::raster::adjustments::ColorOverlayNode",
"graphene_raster_nodes::generate_curves::ColorOverlayNode",
],
},
// graphene_raster_nodes::adjustments
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::luminance::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::LuminanceNode", "graphene_core::raster::LuminanceNode"],
@@ -292,20 +358,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::raster_nodes::adjustments::threshold::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::ThresholdNode", "graphene_core::raster::ThresholdNode"],
},
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::blend::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::BlendNode", "graphene_core::raster::BlendNode"],
},
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::blend_color_pair::IDENTIFIER,
aliases: &["graphene_core::raster::BlendColorPairNode"],
},
// this node doesn't seem to exist?
// (graphene_std::raster_nodes::adjustments::blend_color::IDENTIFIER, &["graphene_core::raster::adjustments::BlendColorsNode","graphene_core::raster::BlendColorsNode"]),
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::gradient_map::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::GradientMapNode", "graphene_core::raster::GradientMapNode"],
},
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::vibrance::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::VibranceNode", "graphene_core::raster::VibranceNode"],
@@ -326,11 +378,16 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::raster_nodes::adjustments::exposure::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::ExposureNode", "graphene_core::raster::ExposureNode"],
},
// graphene_raster_nodes::*
NodeReplacement {
node: graphene_std::raster_nodes::adjustments::color_overlay::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::ColorOverlayNode", "graphene_raster_nodes::generate_curves::ColorOverlayNode"],
node: graphene_std::raster_nodes::gradient_map::gradient_map::IDENTIFIER,
aliases: &[
"graphene_raster_nodes::gradient_map::GradientMapNode",
"graphene_raster_nodes::adjustments::GradientMapNode",
"graphene_core::raster::adjustments::GradientMapNode",
"graphene_core::raster::GradientMapNode",
],
},
// raster
NodeReplacement {
node: graphene_std::raster_nodes::generate_curves::generate_curves::IDENTIFIER,
aliases: &["graphene_core::raster::adjustments::GenerateCurvesNode"],
@@ -369,7 +426,7 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
},
NodeReplacement {
node: graphene_std::raster_nodes::std_nodes::image_value::IDENTIFIER,
aliases: &["graphene_std::raster::ImageValueNode"],
aliases: &["graphene_std::raster::ImageValueNode", "graphene_std::raster::ImageNode"],
},
NodeReplacement {
node: graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER,
@@ -438,6 +495,10 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::path_bool::boolean_operation::IDENTIFIER,
aliases: &["graphene_std::vector::BooleanOperationNode"],
},
NodeReplacement {
node: graphene_std::vector::path_modify::IDENTIFIER,
aliases: &["graphene_core::vector::vector_data::modification::PathModifyNode"],
},
// brush
NodeReplacement {
node: graphene_std::brush::brush::brush_stamp_generator::IDENTIFIER,
@@ -499,7 +560,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()));
}
}
}
@@ -524,11 +585,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
@@ -573,17 +634,17 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
return None;
}
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
// Obtain the document node for the given node ID, extract the vector points, and create a Vector path from the list of points
let node = document.network_interface.document_node(node_id, network_path)?;
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
return None;
};
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
let vector = Vector::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
// Retrieve the output connectors linked to the "Spline" node's output port
// Retrieve the output connectors linked to the "Spline" node's output connector
let Some(spline_outputs) = document.network_interface.outward_wires(network_path)?.get(&OutputConnector::node(*node_id, 0)).cloned() else {
log::error!("Vec of InputConnector Spline node is connected to its output port 0.");
log::error!("Vec of InputConnector Spline node is connected to its output connector 0.");
return None;
};
@@ -593,13 +654,13 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
return None;
};
// Get the "Path" node definition and fill it in with the vector data and default vector modification
// Get the "Path" node definition and fill it in with the Vector path and default vector modification
let Some(path_node_type) = resolve_document_node_type("Path") else {
log::error!("Path node does not exist.");
return None;
};
let path_node = path_node_type.node_template_input_override([
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
Some(NodeInput::value(TaggedValue::Vector(Table::new_from_element(vector)), true)),
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
]);
@@ -628,14 +689,14 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
// Redirect each output connection from the old node to the new "Spline" node's output port
// Redirect each output connection from the old node to the new "Spline" node's output connector
for input_connector in spline_outputs {
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
}
}
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
if reference == "Text" && inputs_count != 10 {
if reference == "Text" && inputs_count != 11 {
let mut template = resolve_document_node_type(reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut template);
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut template)?;
@@ -691,8 +752,17 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 9),
if inputs_count >= 10 {
if inputs_count >= 11 {
old_inputs[9].clone()
} else {
NodeInput::value(TaggedValue::TextAlign(TextAlign::default()), false)
},
network_path,
);
document.network_interface.set_input(
&InputConnector::node(*node_id, 10),
if inputs_count >= 11 {
old_inputs[10].clone()
} else {
NodeInput::value(TaggedValue::Bool(false), false)
},
@@ -765,7 +835,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
}
// Upgrade artboard name being passed as hidden value input to "To Artboard"
// Upgrade artboard name being passed as hidden value input to "Create Artboard"
if reference == "Artboard" && reset_node_definitions_on_open {
let label = document.network_interface.display_name(node_id, network_path);
document
@@ -1022,8 +1092,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:?}");
}
}
}
@@ -16,10 +16,12 @@ pub struct MenuBarMessageHandler {
pub has_selected_nodes: bool,
pub has_selected_layers: bool,
pub has_selection_history: (bool, bool),
pub spreadsheet_view_open: bool,
pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool,
pub single_path_node_compatible_layer_selected: bool,
pub make_path_editable_is_allowed: bool,
pub data_panel_open: bool,
pub layers_panel_open: bool,
pub properties_panel_open: bool,
}
#[message_handler_data]
@@ -46,7 +48,7 @@ impl LayoutHolder for MenuBarMessageHandler {
let message_logging_verbosity_names = self.message_logging_verbosity == MessageLoggingVerbosity::Names;
let message_logging_verbosity_contents = self.message_logging_verbosity == MessageLoggingVerbosity::Contents;
let reset_node_definitions_on_open = self.reset_node_definitions_on_open;
let single_path_node_compatible_layer_selected = self.single_path_node_compatible_layer_selected;
let make_path_editable_is_allowed = self.make_path_editable_is_allowed;
let menu_bar_entries = vec![
MenuBarEntry {
@@ -99,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(),
@@ -359,8 +372,8 @@ impl LayoutHolder for MenuBarMessageHandler {
choices
.into_iter()
.map(|group| {
group
.map(|section| {
section
.into_iter()
.map(|(axis, aggregate, icon, name)| MenuBarEntry {
label: name.into(),
@@ -419,7 +432,7 @@ impl LayoutHolder for MenuBarMessageHandler {
action: MenuBarEntry::no_action(),
disabled: no_active_document || !has_selected_layers,
children: MenuBarEntryChildren(vec![{
let list = <BooleanOperation as graphene_std::registry::ChoiceTypeStatic>::list();
let list = <BooleanOperation as graphene_std::choice_type::ChoiceTypeStatic>::list();
list.iter()
.flat_map(|i| i.iter())
.map(move |(operation, info)| MenuBarEntry {
@@ -442,7 +455,7 @@ impl LayoutHolder for MenuBarMessageHandler {
icon: Some("NodeShape".into()),
shortcut: None,
action: MenuBarEntry::create_action(|_| NodeGraphMessage::AddPathNode.into()),
disabled: !single_path_node_compatible_layer_selected,
disabled: !make_path_editable_is_allowed,
..MenuBarEntry::default()
}],
]),
@@ -585,18 +598,40 @@ impl LayoutHolder for MenuBarMessageHandler {
disabled: no_active_document,
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Window".into(),
false,
MenuBarEntryChildren(vec![
vec![
MenuBarEntry {
label: "Properties".into(),
icon: Some(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()),
..MenuBarEntry::default()
},
MenuBarEntry {
label: "Layers".into(),
icon: Some(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleLayersPanelOpen.into()),
..MenuBarEntry::default()
},
],
vec![MenuBarEntry {
label: "Window: Spreadsheet".into(),
icon: Some(if self.spreadsheet_view_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
action: MenuBarEntry::create_action(|_| SpreadsheetMessage::ToggleOpen.into()),
disabled: no_active_document,
label: "Data".into(),
icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen),
action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()),
..MenuBarEntry::default()
}],
]),
),
MenuBarEntry::new_root(
"Help".into(),
true,
false,
MenuBarEntryChildren(vec![
vec![MenuBarEntry {
label: "About Graphite…".into(),
-1
View File
@@ -4,7 +4,6 @@ mod portfolio_message_handler;
pub mod document;
pub mod document_migration;
pub mod menu_bar;
pub mod spreadsheet;
pub mod utility_types;
#[doc(inline)]
@@ -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)]
@@ -15,8 +16,6 @@ pub enum PortfolioMessage {
MenuBar(MenuBarMessage),
#[child]
Document(DocumentMessage),
#[child]
Spreadsheet(SpreadsheetMessage),
// Messages
Init,
@@ -68,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,
@@ -88,6 +89,9 @@ pub enum PortfolioMessage {
PasteSerializedData {
data: String,
},
PasteSerializedVector {
data: String,
},
CenterPastedLayers {
layers: Vec<LayerNodeIdentifier>,
},
@@ -114,7 +118,7 @@ pub enum PortfolioMessage {
document_id: DocumentId,
},
SubmitDocumentExport {
file_name: String,
name: String,
file_type: FileType,
scale_factor: f64,
bounds: ExportBounds,
@@ -125,6 +129,9 @@ pub enum PortfolioMessage {
document_id: DocumentId,
ignore_hash: bool,
},
ToggleDataPanelOpen,
TogglePropertiesPanelOpen,
ToggleLayersPanelOpen,
ToggleRulers,
UpdateDocumentWidgets,
UpdateOpenDocumentsList,
@@ -1,9 +1,8 @@
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
use super::document::utility_types::network_interface;
use super::spreadsheet::SpreadsheetMessageHandler;
use super::utility_types::{PanelType, PersistentData};
use crate::application::generate_uuid;
use crate::consts::DEFAULT_DOCUMENT_NAME;
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;
@@ -12,6 +11,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::DocumentMessageContext;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::utility_types::network_interface::OutputConnector;
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
@@ -19,27 +19,33 @@ use crate::messages::portfolio::document_migration::*;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use derivative::*;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graph_craft::document::value::TaggedValue;
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::subpath::BezierHandles;
use graphene_std::text::Font;
use graphene_std::vector::misc::HandleId;
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
use std::vec;
#[derive(ExtractField)]
pub struct PortfolioMessageContext<'a> {
pub ipp: &'a InputPreprocessorMessageHandler,
pub preferences: &'a PreferencesMessageHandler,
pub animation: &'a AnimationMessageHandler,
pub current_tool: &'a ToolType,
pub message_logging_verbosity: MessageLoggingVerbosity,
pub reset_node_definitions_on_open: bool,
pub timing_information: TimingInformation,
pub animation: &'a AnimationMessageHandler,
}
#[derive(Debug, Default, ExtractField)]
#[derive(Debug, Derivative, ExtractField)]
#[derivative(Default)]
pub struct PortfolioMessageHandler {
menu_bar_message_handler: MenuBarMessageHandler,
pub documents: HashMap<DocumentId, DocumentMessageHandler>,
@@ -50,10 +56,13 @@ pub struct PortfolioMessageHandler {
pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
/// The spreadsheet UI allows for instance data to be previewed.
pub spreadsheet: SpreadsheetMessageHandler,
device_pixel_ratio: Option<f64>,
pub reset_node_definitions_on_open: bool,
pub data_panel_open: bool,
#[derivative(Default(value = "true"))]
pub layers_panel_open: bool,
#[derivative(Default(value = "true"))]
pub properties_panel_open: bool,
}
#[message_handler_data]
@@ -62,11 +71,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let PortfolioMessageContext {
ipp,
preferences,
animation,
current_tool,
message_logging_verbosity,
reset_node_definitions_on_open,
timing_information,
animation,
} = context;
match message {
@@ -80,8 +89,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.menu_bar_message_handler.has_selected_nodes = false;
self.menu_bar_message_handler.has_selected_layers = false;
self.menu_bar_message_handler.has_selection_history = (false, false);
self.menu_bar_message_handler.single_path_node_compatible_layer_selected = false;
self.menu_bar_message_handler.spreadsheet_view_open = self.spreadsheet.spreadsheet_view_open;
self.menu_bar_message_handler.make_path_editable_is_allowed = false;
self.menu_bar_message_handler.data_panel_open = self.data_panel_open;
self.menu_bar_message_handler.layers_panel_open = self.layers_panel_open;
self.menu_bar_message_handler.properties_panel_open = self.properties_panel_open;
self.menu_bar_message_handler.message_logging_verbosity = message_logging_verbosity;
self.menu_bar_message_handler.reset_node_definitions_on_open = reset_node_definitions_on_open;
@@ -98,37 +109,11 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let metadata = &document.network_interface.document_network_metadata().persistent_metadata;
(!metadata.selection_undo_history.is_empty(), !metadata.selection_redo_history.is_empty())
};
self.menu_bar_message_handler.single_path_node_compatible_layer_selected = {
let selected_nodes = document.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
let first_layer = selected_layers.next();
let second_layer = selected_layers.next();
let has_single_selection = first_layer.is_some() && second_layer.is_none();
let compatible_type = first_layer.and_then(|layer| {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
graph_layer.horizontal_layer_flow().nth(1).and_then(|node_id| {
let (output_type, _) = document.network_interface.output_type(&node_id, 0, &[]);
Some(format!("type:{}", output_type.nested_type()))
})
});
let is_compatible = compatible_type.as_deref() == Some("type:Instances<VectorData>");
let is_modifiable = first_layer.map_or(false, |layer| {
let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface);
matches!(graph_layer.find_input("Path", 1), Some(TaggedValue::VectorModification(_)))
});
first_layer.is_some() && has_single_selection && is_compatible && !is_modifiable
}
self.menu_bar_message_handler.make_path_editable_is_allowed = make_path_editable_is_allowed(&mut document.network_interface).is_some();
}
self.menu_bar_message_handler.process_message(message, responses, ());
}
PortfolioMessage::Spreadsheet(message) => {
self.spreadsheet.process_message(message, responses, ());
}
PortfolioMessage::Document(message) => {
if let Some(document_id) = self.active_document_id {
if let Some(document) = self.documents.get_mut(&document_id) {
@@ -140,6 +125,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool,
preferences,
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
};
document.process_message(message, responses, document_inputs)
}
@@ -174,6 +162,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool,
preferences,
device_pixel_ratio: self.device_pixel_ratio.unwrap_or(1.),
data_panel_open: self.data_panel_open,
layers_panel_open: self.layers_panel_open,
properties_panel_open: self.properties_panel_open,
};
document.process_message(message, responses, document_inputs)
}
@@ -213,12 +204,13 @@ 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
responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel);
responses.add(DataPanelMessage::ClearLayout);
let hint_data = HintData(vec![HintGroup(vec![])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
@@ -243,6 +235,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Clear UI layouts that assume the existence of a document
responses.add(PropertiesPanelMessage::Clear);
responses.add(DocumentMessage::ClearLayersPanel);
responses.add(DataPanelMessage::ClearLayout);
let hint_data = HintData(vec![HintGroup(vec![])]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
@@ -257,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 {
@@ -362,14 +355,17 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
self.persistent_data.font_cache.insert(font, preview_url, data);
self.executor.update_font_cache(self.persistent_data.font_cache.clone());
for document_id in self.document_ids.iter() {
let inspect_node = self.inspect_node_id();
let _ = self.executor.submit_node_graph_evaluation(
let node_to_inspect = self.node_to_inspect();
if let Ok(message) = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
*document_id,
ipp.viewport_bounds.size().as_uvec2(),
timing_information,
inspect_node,
node_to_inspect,
true,
);
) {
responses.add_front(message);
}
}
if self.active_document_mut().is_some() {
@@ -394,16 +390,19 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PortfolioMessage::NewDocumentWithName { name } => {
let mut new_document = DocumentMessageHandler::default();
new_document.name = name;
responses.add(DocumentMessage::PTZUpdate);
let mut new_responses = VecDeque::new();
new_responses.add(DocumentMessage::PTZUpdate);
let document_id = DocumentId(generate_uuid());
if self.active_document().is_some() {
responses.add(BroadcastEvent::ToolAbort);
responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
new_responses.add(EventMessage::ToolAbort);
new_responses.add(NavigationMessage::CanvasPan { delta: (0., 0.).into() });
}
self.load_document(new_document, document_id, responses, false);
responses.add(PortfolioMessage::SelectDocument { document_id });
self.load_document(new_document, document_id, self.layers_panel_open, &mut new_responses, false);
new_responses.add(PortfolioMessage::SelectDocument { document_id });
new_responses.extend(responses.drain(..));
*responses = new_responses;
}
PortfolioMessage::NextDocument => {
if let Some(active_document_id) = self.active_document_id {
@@ -420,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,
@@ -440,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,
@@ -451,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 {
@@ -515,8 +514,32 @@ 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, responses, to_front);
self.load_document(document, document_id, self.layers_panel_open, responses, to_front);
}
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
let mut all_new_ids = Vec::new();
@@ -568,11 +591,105 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
responses.add(Message::StartBuffer);
responses.add(PortfolioMessage::CenterPastedLayers { layers });
responses.add(DeferMessage::AfterGraphRun {
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
});
}
}
}
// Custom paste implementation for Path tool
PortfolioMessage::PasteSerializedVector { data } => {
// If using Path tool then send the operation to Path tool
if *current_tool == ToolType::Path {
responses.add(PathToolMessage::Paste { data });
return;
}
// If not using Path tool, create new layers and add paths into those
if let Some(document) = self.active_document() {
let Ok(data) = serde_json::from_str::<Vec<(LayerNodeIdentifier, Vector, DAffine2)>>(&data) else {
return;
};
let mut layers = Vec::new();
for (_, new_vector, transform) in data {
let Some(node_type) = resolve_document_node_type("Path") else {
error!("Path node does not exist");
continue;
};
let nodes = vec![(NodeId(0), node_type.default_node_template())];
let parent = document.new_layer_parent(false);
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
layers.push(layer);
// Adding the transform back into the layer
responses.add(GraphOperationMessage::TransformSet {
layer,
transform,
transform_in: TransformIn::Local,
skip_rerender: false,
});
// Add default fill and stroke to the layer
let fill_color = Color::WHITE;
let stroke_color = Color::BLACK;
let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb());
responses.add(GraphOperationMessage::FillSet { layer, fill });
let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
// Create new point ids and add those into the existing Vector path
let mut points_map = HashMap::new();
for (point, position) in new_vector.point_domain.iter() {
let new_point_id = PointId::generate();
points_map.insert(point, new_point_id);
let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
// Create new segment ids and add the segments into the existing Vector path
let mut segments_map = HashMap::new();
for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() {
let new_segment_id = SegmentId::generate();
segments_map.insert(segment_id, new_segment_id);
let handles = match bezier.handles {
BezierHandles::Linear => [None, None],
BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None],
BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)],
};
let points = [points_map[&start], points_map[&end]];
let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
// Set G1 continuity
for handles in new_vector.colinear_manipulators {
let to_new_handle = |handle: HandleId| -> HandleId {
HandleId {
ty: handle.ty,
segment: segments_map[&handle.segment],
}
};
let new_handles = [to_new_handle(handles[0]), to_new_handle(handles[1])];
let modification_type = VectorModificationType::SetG1Continuous { handles: new_handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::Defer(DeferMessage::AfterGraphRun {
messages: vec![PortfolioMessage::CenterPastedLayers { layers }.into()],
}));
}
}
PortfolioMessage::CenterPastedLayers { layers } => {
if let Some(document) = self.active_document_mut() {
let viewport_bounds_quad_pixels = Quad::from_box([DVec2::ZERO, ipp.viewport_bounds.size()]);
@@ -688,7 +805,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document {
responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()),
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
});
}
@@ -701,13 +818,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document {
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
responses.add(Message::StartBuffer);
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
responses.add(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
responses.add(DeferMessage::AfterGraphRun {
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
});
responses.add(DeferMessage::AfterNavigationReady {
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
});
}
}
PortfolioMessage::PasteSvg {
@@ -720,7 +836,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document {
responses.add(PortfolioMessage::NewDocumentWithName {
name: name.clone().unwrap_or("Untitled Document".into()),
name: name.clone().unwrap_or(DEFAULT_DOCUMENT_NAME.into()),
});
}
@@ -733,13 +849,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
if create_document {
// Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image
responses.add(Message::StartBuffer);
responses.add(DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true });
responses.add(DeferMessage::AfterGraphRun {
messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()],
});
// TODO: Figure out how to get StartBuffer to work here so we can delete this and use `DocumentMessage::ZoomCanvasToFitAll` instead
// Currently, it is necessary to use `FrontendMessage::TriggerDelayedZoomCanvasToFitAll` rather than `DocumentMessage::ZoomCanvasToFitAll` because the size of the viewport is not yet populated
responses.add(Message::StartBuffer);
responses.add(FrontendMessage::TriggerDelayedZoomCanvasToFitAll);
responses.add(DeferMessage::AfterNavigationReady {
messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()],
});
}
}
PortfolioMessage::PrevDocument => {
@@ -782,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 });
@@ -807,7 +923,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
}
PortfolioMessage::SubmitDocumentExport {
file_name,
name,
file_type,
scale_factor,
bounds,
@@ -815,14 +931,14 @@ 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,
transparent_background,
..Default::default()
};
let result = self.executor.submit_document_export(document, export_config);
let result = self.executor.submit_document_export(document, self.active_document_id.unwrap(), export_config);
if let Err(description) = result {
responses.add(DialogMessage::DisplayDialogError {
@@ -837,20 +953,76 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
}
}
PortfolioMessage::SubmitGraphRender { document_id, ignore_hash } => {
let inspect_node = self.inspect_node_id();
let node_to_inspect = self.node_to_inspect();
let result = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(&document_id).expect("Tried to render non-existent document"),
document_id,
ipp.viewport_bounds.size().as_uvec2(),
timing_information,
inspect_node,
node_to_inspect,
ignore_hash,
);
if let Err(description) = result {
responses.add(DialogMessage::DisplayDialogError {
title: "Unable to update node graph".to_string(),
description,
match result {
Err(description) => {
responses.add(DialogMessage::DisplayDialogError {
title: "Unable to update node graph".to_string(),
description,
});
}
Ok(message) => responses.add_front(message),
}
}
PortfolioMessage::ToggleDataPanelOpen => {
self.data_panel_open = !self.data_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.data_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the data panel next time it is opened
responses.add(DataPanelMessage::ClearLayout);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateDataPanelState { open: self.data_panel_open });
}
}
PortfolioMessage::TogglePropertiesPanelOpen => {
self.properties_panel_open = !self.properties_panel_open;
responses.add(MenuBarMessage::SendLayout);
responses.add(FrontendMessage::UpdatePropertiesPanelState { open: self.properties_panel_open });
// Run the graph to grab the data
if self.properties_panel_open {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
responses.add(PropertiesPanelMessage::Refresh);
}
PortfolioMessage::ToggleLayersPanelOpen => {
self.layers_panel_open = !self.layers_panel_open;
responses.add(MenuBarMessage::SendLayout);
// Run the graph to grab the data
if self.layers_panel_open {
// When opening, we make the frontend show the panel first so it can start receiving its message subscriptions for the data it will display
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(DeferMessage::AfterGraphRun {
messages: vec![NodeGraphMessage::UpdateLayerPanel.into(), DocumentMessage::DocumentStructureChanged.into()],
});
} else {
// If we don't clear the panel, the layout diffing system will assume widgets still exist when it attempts to update the layers panel next time it is opened
responses.add(DocumentMessage::ClearLayersPanel);
// When closing, we make the frontend hide the panel last so it can finish receiving its message subscriptions before it is destroyed
responses.add(FrontendMessage::UpdateLayersPanelState { open: self.layers_panel_open });
}
}
PortfolioMessage::ToggleRulers => {
@@ -883,6 +1055,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents });
}
PortfolioMessage::UpdateVelloPreference => {
let active = if cfg!(target_family = "wasm") { false } else { preferences.use_vello };
responses.add(FrontendMessage::UpdateViewportHolePunch { active });
responses.add(NodeGraphMessage::RunDocumentGraph);
self.persistent_data.use_vello = preferences.use_vello;
}
@@ -900,6 +1074,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
PasteIntoFolder,
PrevDocument,
ToggleRulers,
ToggleDataPanelOpen,
);
// Extend with actions that require an active document
@@ -969,19 +1144,19 @@ impl PortfolioMessageHandler {
}
}
fn load_document(&mut self, new_document: DocumentMessageHandler, document_id: DocumentId, responses: &mut VecDeque<Message>, to_front: bool) {
fn load_document(&mut self, mut new_document: DocumentMessageHandler, document_id: DocumentId, layers_panel_open: bool, responses: &mut VecDeque<Message>, to_front: bool) {
if to_front {
self.document_ids.push_front(document_id);
} else {
self.document_ids.push_back(document_id);
}
new_document.update_layers_panel_control_bar_widgets(responses);
new_document.update_layers_panel_bottom_bar_widgets(responses);
new_document.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
new_document.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
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
@@ -1019,25 +1194,22 @@ impl PortfolioMessageHandler {
/text>"#
// It's a mystery why the `/text>` tag above needs to be missing its `<`, but when it exists it prints the `<` character in the text. However this works with it removed.
.to_string();
responses.add(Message::EndBuffer {
render_metadata: graphene_std::renderer::RenderMetadata::default(),
});
responses.add(FrontendMessage::UpdateDocumentArtwork { svg: error });
}
result
}
/// Get the id of the node that should be used as the target for the spreadsheet
pub fn inspect_node_id(&self) -> Option<NodeId> {
// Spreadsheet not open, skipping
if !self.spreadsheet.spreadsheet_view_open {
/// Get the ID of the selected node that should be used as the current source for the Data panel.
pub fn node_to_inspect(&self) -> Option<NodeId> {
// Skip if the Data panel is not open
if !self.data_panel_open {
return None;
}
let document = self.documents.get(&self.active_document_id?)?;
let selected_nodes = document.network_interface.selected_nodes().0;
// Selected nodes != 1, skipping
// Skip if there is not exactly one selected node
if selected_nodes.len() != 1 {
return None;
}
@@ -1,7 +0,0 @@
mod spreadsheet_message;
mod spreadsheet_message_handler;
#[doc(inline)]
pub use spreadsheet_message::*;
#[doc(inline)]
pub use spreadsheet_message_handler::*;
@@ -43,7 +43,7 @@ pub enum PanelType {
Document,
Layers,
Properties,
Spreadsheet,
DataPanel,
}
impl From<String> for PanelType {
@@ -52,8 +52,8 @@ impl From<String> for PanelType {
"Document" => PanelType::Document,
"Layers" => PanelType::Layers,
"Properties" => PanelType::Properties,
"Spreadsheet" => PanelType::Spreadsheet,
_ => panic!("Unknown panel type: {}", value),
"Data" => PanelType::DataPanel,
_ => panic!("Unknown panel type: {value}"),
}
}
}
@@ -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;
+10 -6
View File
@@ -1,10 +1,14 @@
// 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};
pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDialogMessageContext, ExportDialogMessageDiscriminant, ExportDialogMessageHandler};
pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler};
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
@@ -15,6 +19,7 @@ pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappi
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
pub use crate::messages::layout::{LayoutMessage, LayoutMessageDiscriminant, LayoutMessageHandler};
pub use crate::messages::portfolio::document::data_panel::{DataPanelMessage, DataPanelMessageDiscriminant};
pub use crate::messages::portfolio::document::graph_operation::{GraphOperationMessage, GraphOperationMessageContext, GraphOperationMessageDiscriminant, GraphOperationMessageHandler};
pub use crate::messages::portfolio::document::navigation::{NavigationMessage, NavigationMessageContext, NavigationMessageDiscriminant, NavigationMessageHandler};
pub use crate::messages::portfolio::document::node_graph::{NodeGraphMessage, NodeGraphMessageDiscriminant, NodeGraphMessageHandler};
@@ -22,15 +27,12 @@ pub use crate::messages::portfolio::document::overlays::{OverlaysMessage, Overla
pub use crate::messages::portfolio::document::properties_panel::{PropertiesPanelMessage, PropertiesPanelMessageDiscriminant, PropertiesPanelMessageHandler};
pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageContext, DocumentMessageDiscriminant, DocumentMessageHandler};
pub use crate::messages::portfolio::menu_bar::{MenuBarMessage, MenuBarMessageDiscriminant, MenuBarMessageHandler};
pub use crate::messages::portfolio::spreadsheet::{SpreadsheetMessage, SpreadsheetMessageDiscriminant};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant, WorkspaceMessageHandler};
// 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};
@@ -46,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::*;
@@ -65,10 +67,12 @@ pub trait Responses {
}
impl Responses for VecDeque<Message> {
#[inline(always)]
fn add(&mut self, message: impl Into<Message>) {
self.push_back(message.into());
}
#[inline(always)]
fn add_front(&mut self, message: impl Into<Message>) {
self.push_front(message.into());
}
@@ -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()),
});
}
}
@@ -1,9 +1,12 @@
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler;
use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler;
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
@@ -23,6 +26,8 @@ pub enum ShapeGizmoHandlers {
None,
Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler),
Arc(ArcGizmoHandler),
Circle(CircleGizmoHandler),
}
impl ShapeGizmoHandlers {
@@ -32,6 +37,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(_) => "star",
Self::Polygon(_) => "polygon",
Self::Arc(_) => "arc",
Self::Circle(_) => "circle",
Self::None => "none",
}
}
@@ -41,6 +48,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {}
}
}
@@ -50,6 +59,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.is_any_gizmo_hovered(),
Self::Polygon(h) => h.is_any_gizmo_hovered(),
Self::Arc(h) => h.is_any_gizmo_hovered(),
Self::Circle(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
@@ -59,6 +70,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(),
Self::Arc(h) => h.handle_click(),
Self::Circle(h) => h.handle_click(),
Self::None => {}
}
}
@@ -68,6 +81,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
Self::Arc(h) => h.handle_update(drag_start, document, input, responses),
Self::Circle(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {}
}
}
@@ -77,6 +92,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(),
Self::Arc(h) => h.cleanup(),
Self::Circle(h) => h.cleanup(),
Self::None => {}
}
}
@@ -94,6 +111,8 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
@@ -110,9 +129,21 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
pub fn gizmo_cursor_icon(&self) -> Option<MouseCursorIcon> {
match self {
Self::Star(h) => h.mouse_cursor_icon(),
Self::Polygon(h) => h.mouse_cursor_icon(),
Self::Arc(h) => h.mouse_cursor_icon(),
Self::Circle(h) => h.mouse_cursor_icon(),
Self::None => None,
}
}
}
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
@@ -141,11 +172,18 @@ impl GizmoManager {
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
}
// Polygon
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
}
// Arc
if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new()));
}
// Circle
if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default()));
}
None
}
@@ -243,4 +281,12 @@ impl GizmoManager {
}
}
}
/// Returns the cursor icon to display when hovering or dragging a gizmo.
///
/// If a gizmo is active (hovered or being manipulated), it returns the cursor icon associated with that gizmo;
/// otherwise, returns `None` to indicate the default crosshair cursor should be used.
pub fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
self.active_shape_handler.as_ref().and_then(|h| h.gizmo_cursor_icon())
}
}
@@ -0,0 +1,177 @@
use crate::consts::GIZMO_HIDE_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::prelude::{FrontendMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id, get_stroke_width};
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_parameters, extract_circle_radius};
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::FRAC_PI_2;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum RadiusHandleState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RadiusHandle {
pub layer: Option<LayerNodeIdentifier>,
initial_radius: f64,
handle_state: RadiusHandleState,
angle: f64,
previous_mouse_position: DVec2,
}
impl RadiusHandle {
pub fn cleanup(&mut self) {
self.handle_state = RadiusHandleState::Inactive;
self.layer = None;
}
pub fn hovered(&self) -> bool {
self.handle_state == RadiusHandleState::Hover
}
pub fn is_dragging(&self) -> bool {
self.handle_state == RadiusHandleState::Dragging
}
pub fn update_state(&mut self, state: RadiusHandleState) {
self.handle_state = state;
}
pub fn check_if_inside_dash_lines(angle: f64, mouse_position: DVec2, viewport: DAffine2, radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
let circle_point = calculate_circle_point_position(angle, radius.abs());
let direction = circle_point.normalize();
let mouse_distance = mouse_position.distance(center);
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
let inner_point = viewport.transform_point2(circle_point - direction * spacing).distance(center);
let outer_point = viewport.transform_point2(circle_point + direction * spacing).distance(center);
mouse_distance >= inner_point && mouse_distance <= outer_point
} else {
let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs()));
mouse_position.distance(center) <= point_position.distance(center)
}
}
fn calculate_extra_spacing(viewport: DAffine2, radius: f64, viewport_center: DVec2, stroke_width: f64, threshold: f64) -> f64 {
let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(viewport_center);
let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(viewport_center);
let min_radius = start_point.min(end_point);
let extra_spacing = if min_radius < threshold { 10. * (min_radius / threshold) } else { 10. };
stroke_width + extra_spacing
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
match &self.handle_state {
RadiusHandleState::Inactive => {
let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X);
let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs()));
let center = viewport.transform_point2(DVec2::ZERO);
if point_position.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if Self::check_if_inside_dash_lines(angle, mouse_position, viewport, radius.abs(), document, layer) {
self.layer = Some(layer);
self.initial_radius = radius;
self.previous_mouse_position = mouse_position;
self.angle = angle;
self.update_state(RadiusHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
RadiusHandleState::Dragging | RadiusHandleState::Hover => {}
}
}
pub fn overlays(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
match &self.handle_state {
RadiusHandleState::Inactive => {}
RadiusHandleState::Dragging | RadiusHandleState::Hover => {
let Some(layer) = self.layer else { return };
let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let x_point = viewport.transform_point2(calculate_circle_point_position(0., radius));
let y_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius));
let direction_x = viewport.transform_vector2(DVec2::X);
let direction_y = viewport.transform_vector2(-DVec2::Y);
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
let smaller_radius_x = (x_point - direction_x * spacing).distance(center);
let smaller_radius_y = (y_point - direction_y * spacing).distance(center);
let larger_radius_x = (x_point + direction_x * spacing).distance(center);
let larger_radius_y = (y_point + direction_y * spacing).distance(center);
overlay_context.dashed_ellipse(center, smaller_radius_x, smaller_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
overlay_context.dashed_ellipse(center, larger_radius_x, larger_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
return;
}
let radius_x = x_point.distance(center);
let radius_y = y_point.distance(center);
overlay_context.dashed_ellipse(center, radius_x, radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
}
}
}
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface).or(get_arc_id(layer, &document.network_interface)) else {
return;
};
let Some(current_radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
return;
};
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
let center = viewport_transform.transform_point2(DVec2::ZERO);
let delta_vector = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(self.previous_mouse_position);
let radius = drag_start - center;
let sign = radius.dot(delta_vector).signum();
let net_delta = delta_vector.length() * sign * self.initial_radius.signum();
self.previous_mouse_position = input.mouse.position;
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(current_radius + net_delta), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
fn calculate_circle_point_position(theta: f64, radius: f64) -> DVec2 {
DVec2::new(radius * theta.cos(), -radius * theta.sin())
}
@@ -1,2 +1,4 @@
pub mod circle_arc_radius_handle;
pub mod number_of_points_dial;
pub mod point_radius_handle;
pub mod sweep_angle_gizmo;
@@ -189,8 +189,8 @@ impl NumberOfPointsDial {
}
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
let delta = input.mouse.position - drag_start;
let sign = (input.mouse.position.x - drag_start.x).signum();
let net_delta = (delta.length() / 25.).round() * sign;
let Some(layer) = self.layer else { return };
@@ -142,14 +142,7 @@ impl PointRadiusHandle {
}
}
pub fn overlays(
&self,
selected_star_layer: Option<LayerNodeIdentifier>,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
pub fn overlays(&self, selected_star_layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, overlay_context: &mut OverlayContext) {
match &self.handle_state {
PointRadiusHandleState::Inactive => {
let Some(layer) = selected_star_layer else { return };
@@ -161,25 +154,12 @@ impl PointRadiusHandle {
for i in 0..(2 * sides) {
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
let angle = ((i as f64) * PI) / (sides as f64);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
overlay_context.manipulator_handle(point, false, None);
}
}
@@ -191,22 +171,12 @@ impl PointRadiusHandle {
for i in 0..sides {
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
return;
}
overlay_context.manipulator_handle(point, false, None);
}
}
@@ -232,12 +202,9 @@ impl PointRadiusHandle {
star_outline(Some(layer), document, overlay_context);
// Make the ticks for snapping
// If dragging to make radius negative don't show the
if (mouse_position - center).dot(direction) < 0. {
return;
if (radius1.signum() * radius2.signum()).is_sign_positive() {
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
}
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
@@ -262,7 +229,6 @@ impl PointRadiusHandle {
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
match snapping_index {
// Make a triangle with previous two points
@@ -274,41 +240,57 @@ impl PointRadiusHandle {
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let before_outer_position = viewport.inverse().transform_point2(before_outer_position);
let outer_position = viewport.inverse().transform_point2(outer_position);
let point_position = viewport.inverse().transform_point2(point_position);
let l1 = (before_outer_position - outer_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
let Some(direction) = (center - outer_position).try_normalize() else { return };
let Some(direction) = (-outer_position).try_normalize() else { return };
let new_point = SQRT_2 * l1 * direction + outer_position;
let before_outer_position = l1 * l1_direction + outer_position;
let point_position = l1 * l2_direction + outer_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(
viewport.transform_point2(before_outer_position),
viewport.transform_point2(new_point),
Some(COLOR_OVERLAY_RED),
Some(3.),
);
overlay_context.line(viewport.transform_point2(new_point), viewport.transform_point2(point_position), Some(COLOR_OVERLAY_RED), Some(3.));
}
1 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let before_outer_position = viewport.inverse().transform_point2(before_outer_position);
let after_point_position = viewport.inverse().transform_point2(after_point_position);
let point_position = viewport.inverse().transform_point2(point_position);
let l1 = (before_outer_position - point_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
let Some(direction) = (center - point_position).try_normalize() else { return };
let Some(direction) = (-point_position).try_normalize() else { return };
let new_point = SQRT_2 * l1 * direction + point_position;
let before_outer_position = l1 * l1_direction + point_position;
let after_point_position = l1 * l2_direction + point_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(
viewport.transform_point2(before_outer_position),
viewport.transform_point2(new_point),
Some(COLOR_OVERLAY_RED),
Some(3.),
);
overlay_context.line(viewport.transform_point2(new_point), viewport.transform_point2(after_point_position), Some(COLOR_OVERLAY_RED), Some(3.));
}
i => {
// Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0
@@ -353,25 +335,36 @@ impl PointRadiusHandle {
return snap_radii;
};
let other_index = if radius_index == 3 { 2 } else { 3 };
let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else {
let (Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) = (node_inputs[2].as_value(), node_inputs[3].as_value()) else {
return snap_radii;
};
let other_radius = if radius_index == 3 { radius_1 } else { radius_2 };
let Some(&TaggedValue::U32(sides)) = node_inputs[1].as_value() else {
return snap_radii;
};
let both_radii_negative = radius_1.is_sign_negative() && radius_2.is_sign_negative();
let both_radii_same_sign = (radius_1.signum() * radius_2.signum()).is_sign_positive();
// When only one of the radii is negative, no need for snapping
if !both_radii_same_sign {
return snap_radii;
}
let sign = if both_radii_negative { -1. } else { 1. };
// Inner radius for 90°
let b = FRAC_PI_4 * 3. - PI / (sides as f64);
let angle = b.sin();
let required_radius = (other_radius / angle) * FRAC_1_SQRT_2;
let required_radius = (other_radius.abs() * sign / angle) * FRAC_1_SQRT_2;
snap_radii.push(required_radius);
// Also push the case when the when it length increases more than the other
let flipped = other_radius * angle * SQRT_2;
let flipped = other_radius.abs() * sign * angle * SQRT_2;
snap_radii.push(flipped);
@@ -386,11 +379,11 @@ impl PointRadiusHandle {
break;
}
if other_radius * factor > 1e-6 {
snap_radii.push(other_radius * factor);
if other_radius.abs() * factor > 1e-6 {
snap_radii.push(other_radius.abs() * sign * factor);
}
snap_radii.push((other_radius * 1.) / factor);
snap_radii.push((other_radius.abs() * sign) / factor);
}
snap_radii
@@ -426,21 +419,23 @@ impl PointRadiusHandle {
};
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
let document_transform = document.network_interface.document_metadata().transform_to_document(layer);
let center = viewport_transform.transform_point2(DVec2::ZERO);
let radius_index = self.radius_index;
let original_radius = self.initial_radius;
let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start);
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(drag_start);
let radius = drag_start - center;
let projection = delta.project_onto(radius);
let sign = radius.dot(delta).signum();
let mut net_delta = projection.length() * sign;
let mut net_delta = projection.length() * sign * original_radius.signum();
let new_radius = original_radius + net_delta;
self.update_state(PointRadiusHandleState::Dragging);
self.check_if_radius_flipped(original_radius, new_radius, document, layer, radius_index);
if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) {
net_delta = snapped_delta;
self.update_state(PointRadiusHandleState::Snapped(index));
@@ -452,4 +447,23 @@ impl PointRadiusHandle {
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
fn check_if_radius_flipped(&mut self, original_radius: f64, new_radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) {
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star") else {
return;
};
let (Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) = (node_inputs[2].as_value(), node_inputs[3].as_value()) else {
return;
};
let other_radius = if radius_index == 3 { radius_1 } else { radius_2 };
let flipped = (other_radius.is_sign_positive() && original_radius.is_sign_negative() && new_radius.is_sign_positive())
|| (other_radius.is_sign_negative() && original_radius.is_sign_positive() && new_radius.is_sign_negative());
if flipped {
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
}
}
}
@@ -0,0 +1,365 @@
use crate::consts::{ARC_SNAP_THRESHOLD, GIZMO_HIDE_THRESHOLD};
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::DocumentMessageHandler;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shapes::shape_utility::{arc_end_points, calculate_arc_text_transform, extract_arc_parameters, format_rounded};
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use std::collections::VecDeque;
use std::f64::consts::FRAC_PI_4;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum SweepAngleGizmoState {
#[default]
Inactive,
Hover,
Dragging,
Snapped,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum EndpointType {
#[default]
None,
Start,
End,
}
#[derive(Clone, Debug, Default)]
pub struct SweepAngleGizmo {
pub layer: Option<LayerNodeIdentifier>,
endpoint: EndpointType,
initial_start_angle: f64,
initial_sweep_angle: f64,
position_before_rotation: DVec2,
previous_mouse_position: DVec2,
total_angle_delta: f64,
snap_angles: Vec<f64>,
handle_state: SweepAngleGizmoState,
}
impl SweepAngleGizmo {
pub fn hovered(&self) -> bool {
self.handle_state == SweepAngleGizmoState::Hover
}
pub fn update_state(&mut self, state: SweepAngleGizmoState) {
self.handle_state = state;
}
pub fn is_dragging_or_snapped(&self) -> bool {
self.handle_state == SweepAngleGizmoState::Dragging || self.handle_state == SweepAngleGizmoState::Snapped
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2) {
if self.handle_state == SweepAngleGizmoState::Inactive {
let Some((start, end)) = arc_end_points(Some(layer), document) else { return };
let Some((_, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
return;
};
let center = document.metadata().transform_to_viewport(layer).transform_point2(DVec2::ZERO);
if center.distance(start) < GIZMO_HIDE_THRESHOLD {
return;
}
let (close_to_gizmo, endpoint_type) = if mouse_position.distance(start) < 5. {
(true, EndpointType::Start)
} else if mouse_position.distance(end) < 5. {
(true, EndpointType::End)
} else {
(false, EndpointType::None)
};
if close_to_gizmo {
self.layer = Some(layer);
self.initial_start_angle = start_angle;
self.initial_sweep_angle = sweep_angle;
self.previous_mouse_position = mouse_position;
self.total_angle_delta = 0.;
self.position_before_rotation = if endpoint_type == EndpointType::End { end } else { start };
self.endpoint = endpoint_type;
self.snap_angles = Self::calculate_snap_angles();
self.update_state(SweepAngleGizmoState::Hover);
}
}
}
pub fn overlays(
&self,
selected_arc_layer: Option<LayerNodeIdentifier>,
document: &DocumentMessageHandler,
_input: &InputPreprocessorMessageHandler,
_mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
let tilt_offset = document.document_ptz.unmodified_tilt();
match self.handle_state {
SweepAngleGizmoState::Inactive => {
let Some((point1, point2)) = arc_end_points(selected_arc_layer, document) else { return };
overlay_context.manipulator_handle(point1, false, None);
overlay_context.manipulator_handle(point2, false, None);
}
SweepAngleGizmoState::Hover => {
// Highlight the currently hovered endpoint only
let Some((point1, point2)) = arc_end_points(self.layer, document) else { return };
let (point, other_point) = if self.endpoint == EndpointType::Start { (point1, point2) } else { (point2, point1) };
overlay_context.manipulator_handle(point, true, None);
overlay_context.manipulator_handle(other_point, false, None);
}
SweepAngleGizmoState::Dragging => {
// Show snapping guides and angle arc while dragging
let Some(layer) = self.layer else { return };
let Some((current_start, current_end)) = arc_end_points(self.layer, document) else { return };
let viewport = document.metadata().transform_to_viewport(layer);
// Depending on which endpoint is being dragged, draw guides relative to the static point
let (point, other_point) = if self.endpoint == EndpointType::End {
(current_end, current_start)
} else {
(current_start, current_end)
};
// Draw the dashed line from center to drag start position
overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5));
overlay_context.manipulator_handle(other_point, false, None);
// Draw the angle, text and the bold line
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
}
SweepAngleGizmoState::Snapped => {
// When snapping is active, draw snapping arcs and angular guidelines
let Some((start, end)) = arc_end_points(self.layer, document) else { return };
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
// Draw snapping arc and angle overlays between the two points
let (a, b) = if self.endpoint == EndpointType::Start { (end, start) } else { (start, end) };
self.dragging_snapping_overlays(a, b, tilt_offset, viewport, overlay_context);
// Draw lines from endpoints to the arc center
overlay_context.line(start, center, None, Some(2.));
overlay_context.line(end, center, None, Some(2.));
// Draw the line from drag start to arc center
overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5));
}
}
}
/// Draws the visual overlay during arc handle dragging or snapping interactions.
/// This includes the dynamic arc sweep, angle label, and visual guides centered around the arc's origin.
pub fn dragging_snapping_overlays(&self, initial_point: DVec2, final_point: DVec2, tilt_offset: f64, viewport: DAffine2, overlay_context: &mut OverlayContext) {
let center = viewport.transform_point2(DVec2::ZERO);
let initial_vector = initial_point - center;
let final_vector = final_point - center;
let offset_angle = initial_vector.to_angle() + tilt_offset;
let bold_radius = final_point.distance(center);
let angle = initial_vector.angle_to(final_vector).to_degrees();
let display_angle = viewport
.inverse()
.transform_point2(final_point)
.angle_to(viewport.inverse().transform_point2(initial_point))
.to_degrees();
let text = format!("{}°", format_rounded(display_angle, 2));
let text_texture_width = overlay_context.get_width(&text) / 2.;
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, center, &text, transform);
}
pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
let Some(layer) = self.layer else { return };
let Some((_, current_start_angle, current_sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let angle_delta = viewport
.inverse()
.transform_point2(self.previous_mouse_position)
.angle_to(viewport.inverse().transform_point2(input.mouse.position))
.to_degrees();
let angle = self.total_angle_delta + angle_delta;
let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else {
return;
};
self.update_state(SweepAngleGizmoState::Dragging);
match self.endpoint {
EndpointType::Start => {
// Dragging start changes both start and sweep
let sign = -angle.signum();
let mut total = angle;
let new_start_angle = self.initial_start_angle + total;
let new_sweep_angle = self.initial_sweep_angle + total.abs() * sign;
match () {
// Clamp sweep angle to 360°
() if new_sweep_angle > 360. => {
let wrapped = new_sweep_angle % 360.;
self.total_angle_delta = -wrapped;
self.endpoint = EndpointType::End;
self.initial_sweep_angle = 360.;
self.initial_start_angle = current_start_angle;
self.update_state(SweepAngleGizmoState::Snapped);
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle - wrapped, input, responses);
}
() if new_sweep_angle < 0. => {
let rest_angle = angle_delta + new_sweep_angle;
self.total_angle_delta = new_sweep_angle.abs();
self.endpoint = EndpointType::End;
self.initial_sweep_angle = 0.;
self.initial_start_angle = current_start_angle + rest_angle;
self.apply_arc_update(node_id, self.initial_start_angle, new_sweep_angle.abs(), input, responses);
}
// Wrap start angle > 180° back into [-180°, 180°] and adjust sweep
() if new_start_angle > 180. => {
let overflow = new_start_angle % 180.;
let rest_angle = angle_delta - overflow;
// We wrap the angle back into [-180°, 180°] range by jumping from +180° to -180°
// Example: dragging past 190° becomes -170°, and we subtract the overshoot from sweep
// Sweep angle must shrink to maintain consistent arc
self.total_angle_delta = rest_angle;
self.initial_start_angle = -180.;
self.initial_sweep_angle = current_sweep_angle - rest_angle;
self.apply_arc_update(node_id, self.initial_start_angle + overflow, self.initial_sweep_angle - overflow, input, responses);
}
// Wrap start angle < -180° back into [-180°, 180°] and adjust sweep
() if new_start_angle < -180. => {
let underflow = new_start_angle % 180.;
let rest_angle = angle_delta - underflow;
// We wrap the angle back into [-180°, 180°] by jumping from -190° to +170°
// Sweep must grow to reflect continued clockwise drag past -180°
// Start angle flips from -190° to +170°, and sweep increases accordingly
self.total_angle_delta = underflow;
self.initial_start_angle = 180.;
self.initial_sweep_angle = current_sweep_angle + rest_angle.abs();
self.apply_arc_update(node_id, self.initial_start_angle + underflow, self.initial_sweep_angle + underflow.abs(), input, responses);
}
_ => {
if let Some(snapped_delta) = self.check_snapping(self.initial_sweep_angle + total.abs() * sign) {
total += snapped_delta;
self.update_state(SweepAngleGizmoState::Snapped);
}
self.total_angle_delta = angle;
self.apply_arc_update(node_id, self.initial_start_angle + total, self.initial_sweep_angle + total.abs() * sign, input, responses);
}
}
}
EndpointType::End => {
// Dragging the end only changes sweep angle
let mut total = angle;
let new_sweep_angle = self.initial_sweep_angle + angle;
match () {
// Clamp sweep angle below 0°, switch to start
() if new_sweep_angle < 0. => {
let delta = angle_delta - current_sweep_angle;
let sign = -delta.signum();
self.initial_sweep_angle = 0.;
self.total_angle_delta = delta;
self.endpoint = EndpointType::Start;
self.apply_arc_update(node_id, self.initial_start_angle + delta, self.initial_sweep_angle + delta.abs() * sign, input, responses);
}
// Clamp sweep angle above 360°, switch to start
() if new_sweep_angle > 360. => {
let delta = angle_delta - (360. - new_sweep_angle);
let sign = -delta.signum();
self.total_angle_delta = angle_delta - (360. - new_sweep_angle);
self.initial_sweep_angle = 360.;
self.endpoint = EndpointType::Start;
self.update_state(SweepAngleGizmoState::Snapped);
self.apply_arc_update(node_id, self.initial_start_angle + angle_delta, self.initial_sweep_angle + angle_delta.abs() * sign, input, responses);
}
_ => {
if let Some(snapped_delta) = self.check_snapping(self.initial_sweep_angle + angle) {
total += snapped_delta;
self.update_state(SweepAngleGizmoState::Snapped);
}
self.total_angle_delta = angle;
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle + total, input, responses);
}
}
}
EndpointType::None => {}
}
}
/// Applies the updated start and sweep angles to the arc.
fn apply_arc_update(&mut self, node_id: NodeId, start_angle: f64, sweep_angle: f64, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
self.snap_angles = Self::calculate_snap_angles();
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(start_angle), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 3),
input: NodeInput::value(TaggedValue::F64(sweep_angle), false),
});
self.previous_mouse_position = input.mouse.position;
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn check_snapping(&self, new_sweep_angle: f64) -> Option<f64> {
self.snap_angles.iter().find(|angle| (**angle - new_sweep_angle).abs() <= ARC_SNAP_THRESHOLD).map(|angle| {
let delta = angle - new_sweep_angle;
if self.endpoint == EndpointType::End { delta } else { -delta }
})
}
pub fn calculate_snap_angles() -> Vec<f64> {
let mut snap_points = Vec::new();
for i in 0..=8 {
let snap_point = i as f64 * FRAC_PI_4;
snap_points.push(snap_point.to_degrees());
}
snap_points
}
pub fn cleanup(&mut self) {
self.layer = None;
self.endpoint = EndpointType::None;
self.handle_state = SweepAngleGizmoState::Inactive;
}
}
@@ -3,7 +3,6 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate};
use crate::messages::prelude::*;
use bezier_rs::Subpath;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
@@ -11,10 +10,13 @@ use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::Color;
use graphene_std::NodeInputDecleration;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::Gradient;
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::style::{Fill, Gradient};
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
use std::collections::VecDeque;
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
@@ -33,7 +35,7 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
if first_layer == second_layer {
return;
}
// Calculate the downstream transforms in order to bring the other vector data into the same layer space
// Calculate the downstream transforms in order to bring the other vector geometry into the same layer space
let first_layer_transform = document.metadata().downstream_transform_to_document(first_layer);
let second_layer_transform = document.metadata().downstream_transform_to_document(second_layer);
@@ -153,31 +155,32 @@ pub fn merge_layers(document: &DocumentMessageHandler, first_layer: LayerNodeIde
});
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(Message::StartBuffer);
responses.add(PenToolMessage::RecalculateLatestPointsPosition);
responses.add(DeferMessage::AfterGraphRun {
messages: vec![PenToolMessage::RecalculateLatestPointsPosition.into()],
});
}
/// Merge the `first_endpoint` with `second_endpoint`.
pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, first_endpoint: PointId, second_endpont: PointId, responses: &mut VecDeque<Message>) {
let transform = document.metadata().transform_to_document(layer);
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return };
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
let segment = vector_data.segment_bezier_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont);
let segment = vector.segment_bezier_iter().find(|(_, _, start, end)| *end == second_endpont || *start == second_endpont);
let Some((segment, _, mut segment_start_point, mut segment_end_point)) = segment else {
log::error!("Could not get the segment for second_endpoint.");
return;
};
let mut handles = [None; 2];
if let Some(handle_position) = ManipulatorPointId::PrimaryHandle(segment).get_position(&vector_data) {
let anchor_position = ManipulatorPointId::Anchor(segment_start_point).get_position(&vector_data).unwrap();
if let Some(handle_position) = ManipulatorPointId::PrimaryHandle(segment).get_position(&vector) {
let anchor_position = ManipulatorPointId::Anchor(segment_start_point).get_position(&vector).unwrap();
let handle_position = transform.transform_point2(handle_position);
let anchor_position = transform.transform_point2(anchor_position);
let anchor_to_handle = handle_position - anchor_position;
handles[0] = Some(anchor_to_handle);
}
if let Some(handle_position) = ManipulatorPointId::EndHandle(segment).get_position(&vector_data) {
let anchor_position = ManipulatorPointId::Anchor(segment_end_point).get_position(&vector_data).unwrap();
if let Some(handle_position) = ManipulatorPointId::EndHandle(segment).get_position(&vector) {
let anchor_position = ManipulatorPointId::Anchor(segment_end_point).get_position(&vector).unwrap();
let handle_position = transform.transform_point2(handle_position);
let anchor_position = transform.transform_point2(anchor_position);
let anchor_to_handle = handle_position - anchor_position;
@@ -210,7 +213,7 @@ pub fn new_vector_layer(subpaths: Vec<Subpath<PointId>>, id: NodeId, parent: Lay
}
/// Create a new bitmap layer.
pub fn new_image_layer(image_frame: RasterDataTable<CPU>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
pub fn new_image_layer(image_frame: Table<Raster<CPU>>, id: NodeId, parent: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
let insert_index = 0;
responses.add(GraphOperationMessage::NewBitmapLayer {
id,
@@ -270,7 +273,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?;
let TaggedValue::Fill(graphene_std::vector::style::Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
let TaggedValue::Fill(Fill::Gradient(gradient)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(gradient.clone())
@@ -281,7 +284,7 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
let fill_index = 1;
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Fill")?;
let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
let TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
return None;
};
Some(color.to_linear_srgb())
@@ -332,6 +335,10 @@ pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
}
pub fn get_circle_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Circle")
}
pub fn get_ellipse_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Ellipse")
}
@@ -352,6 +359,10 @@ pub fn get_star_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Star")
}
pub fn get_arc_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc")
}
pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Text")
}
@@ -368,9 +379,8 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
let Some(&TaggedValue::OptionalF64(max_width)) = inputs[6].as_value() else { return None };
let Some(&TaggedValue::OptionalF64(max_height)) = inputs[7].as_value() else { return None };
let Some(&TaggedValue::F64(tilt)) = inputs[8].as_value() else { return None };
let Some(TaggedValue::Bool(per_glyph_instances)) = &inputs[9].as_value() else {
return None;
};
let Some(&TaggedValue::TextAlign(align)) = inputs[9].as_value() else { return None };
let Some(&TaggedValue::Bool(per_glyph_instances)) = inputs[10].as_value() else { return None };
let typesetting = TypesettingConfig {
font_size,
@@ -379,8 +389,9 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
character_spacing,
max_height,
tilt,
align,
};
Some((text, font, typesetting, *per_glyph_instances))
Some((text, font, typesetting, per_glyph_instances))
}
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
@@ -424,6 +435,16 @@ impl<'a> NodeGraphLayer<'a> {
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
}
/// Node id of a visible node if it exists in the layer's primary flow until another layer
pub fn upstream_visible_node_id_from_name_in_layer(&self, node_name: &str) -> Option<NodeId> {
// `.skip(1)` is used to skip self
self.horizontal_layer_flow()
.skip(1)
.take_while(|node_id| !self.network_interface.is_layer(node_id, &[]))
.filter(|node_id| self.network_interface.is_visible(node_id, &[]))
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
}
/// Node id of a protonode if it exists in the layer's primary flow
pub fn upstream_node_id_from_protonode(&self, protonode_identifier: ProtoNodeIdentifier) -> Option<NodeId> {
self.horizontal_layer_flow()
@@ -438,10 +459,11 @@ impl<'a> NodeGraphLayer<'a> {
/// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached.
pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec<NodeInput>> {
// `.skip(1)` is used to skip self
self.horizontal_layer_flow()
.skip(1)// Skip self
.take_while(|node_id| !self.network_interface.is_layer(node_id,&[]))
.find(|node_id| self.network_interface.reference(node_id,&[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
.skip(1)
.take_while(|node_id| !self.network_interface.is_layer(node_id, &[]))
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
.and_then(|node_id| self.network_interface.document_network().nodes.get(&node_id).map(|node| &node.inputs))
}
@@ -455,6 +477,6 @@ impl<'a> NodeGraphLayer<'a> {
pub fn is_raster_layer(layer: LayerNodeIdentifier, network_interface: &mut NodeNetworkInterface) -> bool {
let layer_input_type = network_interface.input_type(&InputConnector::node(layer.to_node(), 1), &[]).0.nested_type().clone();
layer_input_type == concrete!(RasterDataTable<CPU>) || layer_input_type == concrete!(RasterDataTable<GPU>)
layer_input_type == concrete!(Table<Raster<CPU>>) || layer_input_type == concrete!(Table<Raster<GPU>>)
}
}
@@ -43,18 +43,19 @@ fn draw_line_with_length(line_start: DVec2, line_end: DVec2, transform: DAffine2
}
}
/// Draws a dashed outline around a rectangle to visualize the AABB
/// Draws a dashed outline around the given rectangle (assumed to be in document space).
/// The provided transform is applied to convert coordinates (e.g., to viewport space) during rendering.
fn draw_dashed_rect_outline(rect: Rect, transform: DAffine2, overlay_context: &mut OverlayContext) {
let min = rect.min();
let max = rect.max();
// Create the four corners of the rectangle
let top_left = transform.transform_point2(DVec2::new(min.x, min.y));
let top_right = transform.transform_point2(DVec2::new(max.x, min.y));
let bottom_right = transform.transform_point2(DVec2::new(max.x, max.y));
let bottom_left = transform.transform_point2(DVec2::new(min.x, max.y));
// Define corners in document space
let top_left = DVec2::new(min.x, min.y);
let top_right = DVec2::new(max.x, min.y);
let bottom_right = DVec2::new(max.x, max.y);
let bottom_left = DVec2::new(min.x, max.y);
// Draw the four sides as dashed lines
// Draw each edge using document-space coordinates; transform is applied inside draw_dashed_line
draw_dashed_line(top_left, top_right, transform, overlay_context);
draw_dashed_line(top_right, bottom_right, transform, overlay_context);
draw_dashed_line(bottom_right, bottom_left, transform, overlay_context);
@@ -8,7 +8,8 @@ use crate::messages::tool::tool_messages::path_tool::PathOptionsUpdate;
use crate::messages::tool::tool_messages::select_tool::SelectOptionsUpdate;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::{DAffine2, DVec2};
use graphene_std::{transform::ReferencePoint, vector::ManipulatorPointId};
use graphene_std::transform::ReferencePoint;
use graphene_std::vector::misc::ManipulatorPointId;
use std::fmt;
pub fn pin_pivot_widget(active: bool, enabled: bool, source: PivotToolSource) -> WidgetHolder {
@@ -16,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()
}
@@ -40,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(),
}
})
})
@@ -56,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(),
@@ -48,56 +48,11 @@ impl Resize {
/// Compute the drag start and end based on the current mouse position. Ignores the state of the layer.
/// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`].
pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, in_document: bool) -> [DVec2; 2] {
let start = self.viewport_drag_start(document);
let mouse = input.mouse.position;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
let mut points_viewport = [start, mouse];
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
let ratio = input.keyboard.get(lock_ratio as usize);
let center = input.keyboard.get(center as usize);
let snap_data = SnapData::ignore(document, input, &ignore);
let config = SnapTypeConfiguration::default();
if ratio {
let viewport_size = points_viewport[1] - points_viewport[0];
let raw_size = if in_document { document_to_viewport.inverse() } else { DAffine2::IDENTITY }.transform_vector2(viewport_size);
let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum();
let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size };
points_viewport[1] = points_viewport[0] + size;
let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]);
let constraint = SnapConstraint::Line {
origin: self.drag_start,
direction: end_document - self.drag_start,
};
if center {
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config);
let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document);
let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config);
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
} else if center {
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
let opposite = 2. * self.drag_start - document_mouse;
let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(opposite), config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
points_viewport
// Use shared snapping logic with optional center and ratio constraints, considering if coordinates are in document space.
self.compute_snapped_resize_points(document, input, center, ratio, in_document)
}
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
@@ -113,6 +68,81 @@ impl Resize {
)
}
pub fn calculate_circle_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key) -> [DVec2; 2] {
let center = input.keyboard.get(center as usize);
// Use shared snapping logic with enforced aspect ratio and optional center snapping.
self.compute_snapped_resize_points(document, input, center, true, false)
}
/// Calculates two points in viewport space from a drag, applying snapping, optional center mode, and aspect ratio locking.
fn compute_snapped_resize_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: bool, lock_ratio: bool, in_document: bool) -> [DVec2; 2] {
let start = self.viewport_drag_start(document);
let mouse = input.mouse.position;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
let drag_start = self.drag_start;
let mut points_viewport = [start, mouse];
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
let snap_data = &SnapData::ignore(document, input, &ignore);
if lock_ratio {
let viewport_size = points_viewport[1] - points_viewport[0];
let raw_size = if in_document {
document_to_viewport.inverse().transform_vector2(viewport_size)
} else {
viewport_size
};
let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum();
let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size };
points_viewport[1] = points_viewport[0] + size;
let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]);
let constraint = SnapConstraint::Line {
origin: drag_start,
direction: end_document - drag_start,
};
if center {
let snapped = self
.snap_manager
.constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default());
let far = SnapCandidatePoint::handle(2. * drag_start - end_document);
let snapped_far = self.snap_manager.constrained_snap(snap_data, &far, constraint, SnapTypeConfiguration::default());
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self
.snap_manager
.constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default());
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
} else {
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
if center {
let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default());
let opposite = 2. * drag_start - document_mouse;
let snapped_far = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(opposite), SnapTypeConfiguration::default());
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default());
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
}
points_viewport
}
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.snap_manager.cleanup(responses);
self.layer = None;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,186 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::sweep_angle_gizmo::{SweepAngleGizmo, SweepAngleGizmoState};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline};
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::vector::misc::ArcType;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct ArcGizmoHandler {
sweep_angle_gizmo: SweepAngleGizmo,
arc_radius_handle: RadiusHandle,
}
impl ArcGizmoHandler {
pub fn new() -> Self {
Self { ..Default::default() }
}
}
impl ShapeGizmoHandler for ArcGizmoHandler {
fn handle_state(&mut self, selected_shape_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.sweep_angle_gizmo.handle_actions(selected_shape_layer, document, mouse_position);
self.arc_radius_handle.handle_actions(selected_shape_layer, document, mouse_position, responses);
}
fn is_any_gizmo_hovered(&self) -> bool {
self.sweep_angle_gizmo.hovered() || self.arc_radius_handle.hovered()
}
fn handle_click(&mut self) {
// If hovering over both the gizmos give priority to sweep angle gizmo
if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() {
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
self.arc_radius_handle.update_state(RadiusHandleState::Inactive);
return;
}
if self.sweep_angle_gizmo.hovered() {
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
}
if self.arc_radius_handle.hovered() {
self.arc_radius_handle.update_state(RadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.sweep_angle_gizmo.is_dragging_or_snapped() {
self.sweep_angle_gizmo.update_arc(document, input, responses);
}
if self.arc_radius_handle.is_dragging() {
self.arc_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState,
mouse_position: DVec2,
overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext,
) {
if self.sweep_angle_gizmo.is_dragging_or_snapped() {
self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context);
arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context);
}
if self.arc_radius_handle.is_dragging() {
self.sweep_angle_gizmo.overlays(self.arc_radius_handle.layer, document, input, mouse_position, overlay_context);
self.arc_radius_handle.overlays(document, overlay_context);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_shape_layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState,
mouse_position: DVec2,
overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext,
) {
// If hovering over both the gizmos give priority to sweep angle gizmo
if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() {
self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context);
return;
}
if self.arc_radius_handle.hovered() {
let layer = self.arc_radius_handle.layer;
self.arc_radius_handle.overlays(document, overlay_context);
self.sweep_angle_gizmo.overlays(layer, document, input, mouse_position, overlay_context);
}
self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context);
self.arc_radius_handle.overlays(document, overlay_context);
arc_outline(selected_shape_layer.or(self.sweep_angle_gizmo.layer), document, overlay_context);
}
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.sweep_angle_gizmo.hovered() || self.sweep_angle_gizmo.is_dragging_or_snapped() {
return Some(MouseCursorIcon::Default);
}
if self.arc_radius_handle.hovered() || self.arc_radius_handle.is_dragging() {
return Some(MouseCursorIcon::EWResize);
}
None
}
fn cleanup(&mut self) {
self.sweep_angle_gizmo.cleanup();
self.arc_radius_handle.cleanup();
}
}
#[derive(Default)]
pub struct Arc;
impl Arc {
pub fn create_node(arc_type: ArcType) -> NodeTemplate {
let node_type = resolve_document_node_type("Arc").expect("Ellipse node does not exist");
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.), false)),
Some(NodeInput::value(TaggedValue::F64(270.), false)),
Some(NodeInput::value(TaggedValue::ArcType(arc_type), false)),
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let (center, lock_ratio) = (modifier[0], modifier[1]);
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else {
return;
};
let dimensions = (start - end).abs();
let mut scale = DVec2::ONE;
let radius: f64;
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
radius = dimensions.x / 2.;
}
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -0,0 +1,120 @@
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, ShapeToolModifierKey};
use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
#[derive(Clone, Debug, Default)]
pub struct CircleGizmoHandler {
circle_radius_handle: RadiusHandle,
}
impl ShapeGizmoHandler for CircleGizmoHandler {
fn is_any_gizmo_hovered(&self) -> bool {
self.circle_radius_handle.hovered()
}
fn handle_state(&mut self, selected_circle_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.circle_radius_handle.handle_actions(selected_circle_layer, document, mouse_position, responses);
}
fn handle_click(&mut self) {
if self.circle_radius_handle.hovered() {
self.circle_radius_handle.update_state(RadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.circle_radius_handle.is_dragging() {
self.circle_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
_selected_circle_layer: Option<LayerNodeIdentifier>,
_input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut ShapeState,
_mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
self.circle_radius_handle.overlays(document, overlay_context);
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
_input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut ShapeState,
_mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if self.circle_radius_handle.is_dragging() {
self.circle_radius_handle.overlays(document, overlay_context);
}
}
fn cleanup(&mut self) {
self.circle_radius_handle.cleanup();
}
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.circle_radius_handle.hovered() || self.circle_radius_handle.is_dragging() {
return Some(MouseCursorIcon::EWResize);
}
None
}
}
#[derive(Default)]
pub struct Circle;
impl Circle {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_document_node_type("Circle").expect("Circle can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let center = modifier[0];
let [start, end] = shape_tool_data.data.calculate_circle_points(document, ipp, center);
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) else {
return;
};
let dimensions = (start - end).abs();
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
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),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
@@ -28,7 +28,7 @@ impl Ellipse {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
let [center, lock_ratio, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
@@ -53,7 +53,7 @@ impl Line {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, _, lock_angle, snap_angle] = modifier;
let [center, snap_angle, lock_angle] = modifier;
shape_tool_data.line_data.drag_current = ipp.mouse.position;
@@ -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)");
}
}
@@ -1,3 +1,5 @@
pub mod arc_shape;
pub mod circle_shape;
pub mod ellipse_shape;
pub mod line_shape;
pub mod polygon_shape;

Some files were not shown because too many files have changed in this diff Show More