Add an editor preference for touched/enclosed/directional based selection (#2156)

* implemented left selection logic

* added logic for right ward selection

* removed the logs code

* corrected capitalization error

* corrected capitalization error

* added radio buttons for selection_mode

* fixed multiple selection of checkboxes

* adapted to the RadioEntryData

* State management bug

* integrated message system to selection_mode

* updated

* updated

* added selection mode to transition arms

* removed from portfolio message and added preference in ToolMessageData

* removed dead code of selection_mode from frontend logic

* removed dead code for zoomWithScroll

* Cleanup

* Rename, simplify, use dashed box, and highlight only outlines of layers that'll get selected

* More code review

---------

Co-authored-by: Pratik Agrawal <patrik@Pratiks-MacBook-Air.local>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Pratik Agrawal 2025-01-26 12:04:37 +05:30 committed by GitHub
parent 93880abc4c
commit 96c57605b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 266 additions and 113 deletions

View File

@ -221,20 +221,21 @@ impl Dispatcher {
} }
Message::Tool(message) => { Message::Tool(message) => {
let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap(); let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap();
if let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) { let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
let data = ToolMessageData {
document_id,
document,
input: &self.message_handlers.input_preprocessor_message_handler,
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
node_graph: &self.message_handlers.portfolio_message_handler.executor,
preferences: &self.message_handlers.preferences_message_handler,
};
self.message_handlers.tool_message_handler.process_message(message, &mut queue, data);
} else {
warn!("Called ToolMessage without an active document.\nGot {message:?}"); warn!("Called ToolMessage without an active document.\nGot {message:?}");
} return;
};
let data = ToolMessageData {
document_id,
document,
input: &self.message_handlers.input_preprocessor_message_handler,
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
node_graph: &self.message_handlers.portfolio_message_handler.executor,
preferences: &self.message_handlers.preferences_message_handler,
};
self.message_handlers.tool_message_handler.process_message(message, &mut queue, data);
} }
Message::Workspace(message) => { Message::Workspace(message) => {
self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ()); self.message_handlers.workspace_message_handler.process_message(message, &mut queue, ());

View File

@ -1,4 +1,5 @@
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
pub struct PreferencesDialogMessageData<'a> { pub struct PreferencesDialogMessageData<'a> {
@ -31,6 +32,39 @@ impl PreferencesDialogMessageHandler {
const TITLE: &'static str = "Editor Preferences"; const TITLE: &'static str = "Editor Preferences";
fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout { fn layout(&self, preferences: &PreferencesMessageHandler) -> Layout {
let selection_section = vec![TextLabel::new("Selection").italic(true).widget_holder()];
let selection_mode = RadioInput::new(vec![
RadioEntryData::new(SelectionMode::Touched.to_string())
.label(SelectionMode::Touched.to_string())
.tooltip(SelectionMode::Touched.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Touched,
}
.into()
}),
RadioEntryData::new(SelectionMode::Enclosed.to_string())
.label(SelectionMode::Enclosed.to_string())
.tooltip(SelectionMode::Enclosed.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Enclosed,
}
.into()
}),
RadioEntryData::new(SelectionMode::Directional.to_string())
.label(SelectionMode::Directional.to_string())
.tooltip(SelectionMode::Directional.tooltip_description())
.on_update(move |_| {
PreferencesMessage::SelectionMode {
selection_mode: SelectionMode::Directional,
}
.into()
}),
])
.selected_index(Some(preferences.selection_mode as u32))
.widget_holder();
let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)"; let zoom_with_scroll_tooltip = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)";
let input_section = vec![TextLabel::new("Input").italic(true).widget_holder()]; let input_section = vec![TextLabel::new("Input").italic(true).widget_holder()];
let zoom_with_scroll = vec![ let zoom_with_scroll = vec![
@ -43,9 +77,9 @@ impl PreferencesDialogMessageHandler {
.into() .into()
}) })
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(), TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(),
]; ];
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)"; let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
let renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()]; let renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()];
let use_vello = vec![ let use_vello = vec![
@ -54,7 +88,6 @@ impl PreferencesDialogMessageHandler {
.disabled(!preferences.supports_wgpu()) .disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into()) .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Vello Renderer") TextLabel::new("Vello Renderer")
.table_align(true) .table_align(true)
.tooltip(vello_tooltip) .tooltip(vello_tooltip)
@ -62,11 +95,19 @@ impl PreferencesDialogMessageHandler {
.widget_holder(), .widget_holder(),
]; ];
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 line joins and fills.";
let vector_meshes = vec![
CheckboxInput::new(preferences.vector_meshes)
.tooltip(vector_mesh_tooltip)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.widget_holder(),
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(),
];
// TODO: Reenable when Imaginate is restored // TODO: Reenable when Imaginate is restored
// let imaginate_server_hostname = vec![ // let imaginate_server_hostname = vec![
// TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(), // TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(),
// TextLabel::new("Server Hostname").table_align(true).widget_holder(), // TextLabel::new("Server Hostname").table_align(true).widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextInput::new(&preferences.imaginate_server_hostname) // TextInput::new(&preferences.imaginate_server_hostname)
// .min_width(200) // .min_width(200)
// .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into()) // .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into())
@ -75,7 +116,6 @@ impl PreferencesDialogMessageHandler {
// let imaginate_refresh_frequency = vec![ // let imaginate_refresh_frequency = vec![
// TextLabel::new("").min_width(60).widget_holder(), // TextLabel::new("").min_width(60).widget_holder(),
// TextLabel::new("Refresh Frequency").table_align(true).widget_holder(), // TextLabel::new("Refresh Frequency").table_align(true).widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// NumberInput::new(Some(preferences.imaginate_refresh_frequency)) // NumberInput::new(Some(preferences.imaginate_refresh_frequency))
// .unit(" seconds") // .unit(" seconds")
// .min(0.) // .min(0.)
@ -85,17 +125,9 @@ impl PreferencesDialogMessageHandler {
// .widget_holder(), // .widget_holder(),
// ]; // ];
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 line joins and fills.";
let vector_meshes = vec![
CheckboxInput::new(preferences.vector_meshes)
.tooltip(vector_mesh_tooltip)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![ Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: selection_section },
LayoutGroup::Row { widgets: vec![selection_mode] },
LayoutGroup::Row { widgets: input_section }, LayoutGroup::Row { widgets: input_section },
LayoutGroup::Row { widgets: zoom_with_scroll }, LayoutGroup::Row { widgets: zoom_with_scroll },
LayoutGroup::Row { widgets: renderer_section }, LayoutGroup::Row { widgets: renderer_section },

View File

@ -1407,6 +1407,29 @@ impl DocumentMessageHandler {
self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
} }
pub fn is_layer_fully_inside(&self, layer: &LayerNodeIdentifier, quad: graphene_core::renderer::Quad) -> bool {
// Get the bounding box of the layer in document space
let Some(bounding_box) = self.metadata().bounding_box_viewport(*layer) else { return false };
// Check if the bounding box is fully within the selection quad
let [top_left, bottom_right] = bounding_box;
let quad_bbox = quad.bounding_box();
let quad_left = quad_bbox[0].x;
let quad_right = quad_bbox[1].x;
let quad_top = quad_bbox[0].y.max(quad_bbox[1].y); // Correct top
let quad_bottom = quad_bbox[0].y.min(quad_bbox[1].y); // Correct bottom
// Extract layer's bounding box coordinates
let layer_left = top_left.x;
let layer_right = bottom_right.x;
let layer_top = bottom_right.y;
let layer_bottom = top_left.y;
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
}
/// Find all of the layers that were clicked on from a viewport space location /// Find all of the layers that were clicked on from a viewport space location
pub fn click_xray(&self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + '_ { pub fn click_xray(&self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);

View File

@ -460,7 +460,7 @@ impl DoubleEndedIterator for DescendantsIter<'_> {
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct NodeRelations { pub struct NodeRelations {
parent: Option<LayerNodeIdentifier>, pub parent: Option<LayerNodeIdentifier>,
previous_sibling: Option<LayerNodeIdentifier>, previous_sibling: Option<LayerNodeIdentifier>,
next_sibling: Option<LayerNodeIdentifier>, next_sibling: Option<LayerNodeIdentifier>,
first_child: Option<LayerNodeIdentifier>, first_child: Option<LayerNodeIdentifier>,

View File

@ -46,16 +46,16 @@ pub enum PortfolioMessage {
document_id: DocumentId, document_id: DocumentId,
}, },
DestroyAllDocuments, DestroyAllDocuments,
EditorPreferences,
FontLoaded { FontLoaded {
font_family: String, font_family: String,
font_style: String, font_style: String,
preview_url: String, preview_url: String,
data: Vec<u8>, data: Vec<u8>,
}, },
ImaginateCheckServerStatus, // ImaginateCheckServerStatus,
ImaginatePollServerStatus, // ImaginatePollServerStatus,
EditorPreferences, // ImaginateServerHostname,
ImaginateServerHostname,
Import, Import,
LoadDocumentResources { LoadDocumentResources {
document_id: DocumentId, document_id: DocumentId,

View File

@ -9,6 +9,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; 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::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::DocumentMessageData; use crate::messages::portfolio::document::DocumentMessageData;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
@ -38,6 +39,7 @@ pub struct PortfolioMessageHandler {
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize], copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub persistent_data: PersistentData, pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor, pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
} }
impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler { impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler {
@ -295,35 +297,35 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
} }
PortfolioMessage::ImaginateCheckServerStatus => { // PortfolioMessage::ImaginateCheckServerStatus => {
let server_status = self.persistent_data.imaginate.server_status().clone(); // let server_status = self.persistent_data.imaginate.server_status().clone();
self.persistent_data.imaginate.poll_server_check(); // self.persistent_data.imaginate.poll_server_check();
#[cfg(target_arch = "wasm32")] // #[cfg(target_arch = "wasm32")]
if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() { // if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
wasm_bindgen_futures::spawn_local(async move { // wasm_bindgen_futures::spawn_local(async move {
let () = fut.await; // let () = fut.await;
use wasm_bindgen::prelude::*; // use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/../frontend/src/editor.ts")] // #[wasm_bindgen(module = "/../frontend/src/editor.ts")]
extern "C" { // extern "C" {
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)] // #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
fn inject(); // fn inject();
} // }
inject(); // inject();
}) // })
} // }
if &server_status != self.persistent_data.imaginate.server_status() { // if &server_status != self.persistent_data.imaginate.server_status() {
responses.add(PropertiesPanelMessage::Refresh); // responses.add(PropertiesPanelMessage::Refresh);
} // }
} // }
PortfolioMessage::ImaginatePollServerStatus => { // PortfolioMessage::ImaginatePollServerStatus => {
self.persistent_data.imaginate.poll_server_check(); // self.persistent_data.imaginate.poll_server_check();
responses.add(PropertiesPanelMessage::Refresh); // responses.add(PropertiesPanelMessage::Refresh);
} // }
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
PortfolioMessage::ImaginateServerHostname => { // PortfolioMessage::ImaginateServerHostname => {
self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname); // self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
} // }
PortfolioMessage::Import => { PortfolioMessage::Import => {
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages // This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
responses.add(FrontendMessage::TriggerImport); responses.add(FrontendMessage::TriggerImport);

View File

@ -1,4 +1,5 @@
use graphene_std::{imaginate::ImaginatePersistentData, text::FontCache}; use graphene_std::imaginate::ImaginatePersistentData;
use graphene_std::text::FontCache;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct PersistentData { pub struct PersistentData {

View File

@ -1,7 +1,10 @@
mod preferences_message; mod preferences_message;
mod preferences_message_handler; mod preferences_message_handler;
pub mod utility_types;
#[doc(inline)] #[doc(inline)]
pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant}; pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant};
#[doc(inline)] #[doc(inline)]
pub use preferences_message_handler::PreferencesMessageHandler; pub use preferences_message_handler::PreferencesMessageHandler;
#[doc(inline)]
pub use utility_types::SelectionMode;

View File

@ -1,14 +1,18 @@
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
#[impl_message(Message, Preferences)] #[impl_message(Message, Preferences)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum PreferencesMessage { pub enum PreferencesMessage {
// Management messages
Load { preferences: String }, Load { preferences: String },
ResetToDefaults, ResetToDefaults,
ImaginateRefreshFrequency { seconds: f64 }, // Per-preference messages
UseVello { use_vello: bool }, UseVello { use_vello: bool },
ImaginateServerHostname { hostname: String }, SelectionMode { selection_mode: SelectionMode },
VectorMeshes { enabled: bool }, VectorMeshes { enabled: bool },
ModifyLayout { zoom_with_scroll: bool }, ModifyLayout { zoom_with_scroll: bool },
// ImaginateRefreshFrequency { seconds: f64 },
// ImaginateServerHostname { hostname: String },
} }

View File

@ -1,17 +1,24 @@
use crate::messages::input_mapper::key_mapping::MappingVariant; use crate::messages::input_mapper::key_mapping::MappingVariant;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::wasm_application_io::EditorPreferences;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct PreferencesMessageHandler { pub struct PreferencesMessageHandler {
pub imaginate_server_hostname: String, pub imaginate_server_hostname: String,
pub imaginate_refresh_frequency: f64, pub imaginate_refresh_frequency: f64,
pub selection_mode: SelectionMode,
pub zoom_with_scroll: bool, pub zoom_with_scroll: bool,
pub use_vello: bool, pub use_vello: bool,
pub vector_meshes: bool, pub vector_meshes: bool,
} }
impl PreferencesMessageHandler { impl PreferencesMessageHandler {
pub fn get_selection_mode(&self) -> SelectionMode {
self.selection_mode
}
pub fn editor_preferences(&self) -> EditorPreferences { pub fn editor_preferences(&self) -> EditorPreferences {
EditorPreferences { EditorPreferences {
imaginate_hostname: self.imaginate_server_hostname.clone(), imaginate_hostname: self.imaginate_server_hostname.clone(),
@ -33,6 +40,7 @@ impl Default for PreferencesMessageHandler {
Self { Self {
imaginate_server_hostname: host_name, imaginate_server_hostname: host_name,
imaginate_refresh_frequency: 1., imaginate_refresh_frequency: 1.,
selection_mode: SelectionMode::Touched,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
use_vello, use_vello,
vector_meshes: false, vector_meshes: false,
@ -43,6 +51,7 @@ impl Default for PreferencesMessageHandler {
impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler { impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _data: ()) { fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _data: ()) {
match message { match message {
// Management messages
PreferencesMessage::Load { preferences } => { PreferencesMessage::Load { preferences } => {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) { if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences; *self = deserialized_preferences;
@ -65,31 +74,12 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
*self = Self::default() *self = Self::default()
} }
PreferencesMessage::ImaginateRefreshFrequency { seconds } => { // Per-preference messages
self.imaginate_refresh_frequency = seconds;
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::EditorPreferences);
}
PreferencesMessage::UseVello { use_vello } => { PreferencesMessage::UseVello { use_vello } => {
self.use_vello = use_vello; self.use_vello = use_vello;
responses.add(PortfolioMessage::UpdateVelloPreference); responses.add(PortfolioMessage::UpdateVelloPreference);
responses.add(PortfolioMessage::EditorPreferences); responses.add(PortfolioMessage::EditorPreferences);
} }
PreferencesMessage::ImaginateServerHostname { hostname } => {
let initial = hostname.clone();
let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
if hostname != initial {
refresh_dialog(responses);
}
self.imaginate_server_hostname = hostname;
responses.add(PortfolioMessage::ImaginateServerHostname);
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::EditorPreferences);
}
PreferencesMessage::VectorMeshes { enabled } => { PreferencesMessage::VectorMeshes { enabled } => {
self.vector_meshes = enabled; self.vector_meshes = enabled;
} }
@ -102,7 +92,31 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
}; };
responses.add(KeyMappingMessage::ModifyMapping(variant)); responses.add(KeyMappingMessage::ModifyMapping(variant));
} }
PreferencesMessage::SelectionMode { selection_mode } => {
self.selection_mode = selection_mode;
}
} }
// TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block)
// PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
// self.imaginate_refresh_frequency = seconds;
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
// responses.add(PortfolioMessage::EditorPreferences);
// }
// PreferencesMessage::ImaginateServerHostname { hostname } => {
// let initial = hostname.clone();
// let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
// let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
// let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
// if hostname != initial {
// refresh_dialog(responses);
// }
// self.imaginate_server_hostname = hostname;
// responses.add(PortfolioMessage::ImaginateServerHostname);
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
// responses.add(PortfolioMessage::EditorPreferences);
//}
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }); responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
} }

View File

@ -0,0 +1,27 @@
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type, Hash)]
pub enum SelectionMode {
#[default]
Touched = 0,
Enclosed = 1,
Directional = 2,
}
impl std::fmt::Display for SelectionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SelectionMode::Touched => write!(f, "Touched"),
SelectionMode::Enclosed => write!(f, "Enclosed"),
SelectionMode::Directional => write!(f, "Directional"),
}
}
}
impl SelectionMode {
pub fn tooltip_description(&self) -> &'static str {
match self {
SelectionMode::Touched => "Select all layers at least partially covered by the dragged selection area",
SelectionMode::Enclosed => "Select only layers fully enclosed by the dragged selection area",
SelectionMode::Directional => r#""Touched" for leftward drags, "Enclosed" for rightward drags"#,
}
}
}

View File

@ -21,11 +21,11 @@ pub struct SizeSnapData<'a> {
/// Contains the edges that are being dragged along with the original bounds. /// Contains the edges that are being dragged along with the original bounds.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SelectedEdges { pub struct SelectedEdges {
bounds: [DVec2; 2], pub bounds: [DVec2; 2],
top: bool, pub top: bool,
bottom: bool, pub bottom: bool,
left: bool, pub left: bool,
right: bool, pub right: bool,
// Aspect ratio in the form of width/height, so x:1 = width:height // Aspect ratio in the form of width/height, so x:1 = width:height
aspect_ratio: f64, aspect_ratio: f64,
} }

View File

@ -1,4 +1,5 @@
use super::utility_types::ToolType; use super::utility_types::ToolType;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_core::raster::color::Color; use graphene_core::raster::color::Color;
@ -98,4 +99,7 @@ pub enum ToolMessage {
Undo, Undo,
UpdateCursor, UpdateCursor,
UpdateHints, UpdateHints,
UpdateSelectionMode {
selection_mode: SelectionMode,
},
} }

View File

@ -9,6 +9,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::preferences::SelectionMode;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name}; use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name};
use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::pivot::Pivot;
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
@ -250,12 +251,13 @@ impl ToolTransition for SelectTool {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum SelectToolFsmState { enum SelectToolFsmState {
Ready { selection: NestedSelectionBehavior }, Ready { selection: NestedSelectionBehavior },
DrawingBox { selection: NestedSelectionBehavior }, DrawingBox,
Dragging, Dragging,
ResizingBounds, ResizingBounds,
RotatingBounds, RotatingBounds,
DraggingPivot, DraggingPivot,
} }
impl Default for SelectToolFsmState { impl Default for SelectToolFsmState {
fn default() -> Self { fn default() -> Self {
let selection = NestedSelectionBehavior::Deepest; let selection = NestedSelectionBehavior::Deepest;
@ -297,12 +299,22 @@ impl SelectToolData {
} }
} }
fn selection_quad(&self) -> Quad { pub fn selection_quad(&self) -> Quad {
let bbox = self.selection_box(); let bbox = self.selection_box();
Quad::from_box(bbox) Quad::from_box(bbox)
} }
fn selection_box(&self) -> [DVec2; 2] { pub fn calculate_direction(&self) -> SelectionMode {
let bbox: [DVec2; 2] = self.selection_box();
if bbox[1].x < bbox[0].x {
SelectionMode::Touched
} else {
// This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area
SelectionMode::Enclosed
}
}
pub fn selection_box(&self) -> [DVec2; 2] {
if self.drag_current == self.drag_start { if self.drag_current == self.drag_start {
let tolerance = DVec2::splat(SELECTION_TOLERANCE); let tolerance = DVec2::splat(SELECTION_TOLERANCE);
[self.drag_start - tolerance, self.drag_start + tolerance] [self.drag_start - tolerance, self.drag_start + tolerance]
@ -475,21 +487,41 @@ impl Fsm for SelectToolFsmState {
tool_data.pivot.update_pivot(document, &mut overlay_context); tool_data.pivot.update_pivot(document, &mut overlay_context);
// Check if the tool is in box selection mode // Check if the tool is in box selection mode
if matches!(self, Self::DrawingBox { .. }) { if matches!(self, Self::DrawingBox) {
// Get the updated selection box bounds // Get the updated selection box bounds
let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]); let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]);
let mut selection_direction = tool_action_data.preferences.get_selection_mode();
if selection_direction == SelectionMode::Directional {
selection_direction = tool_data.calculate_direction();
}
// Draw outline visualizations on the layers to be selected // Draw outline visualizations on the layers to be selected
for layer in document.intersect_quad_no_artboards(quad, input) { let mut draw_layer_outline = |layer| overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); let intersection = document.intersect_quad_no_artboards(quad, input);
if selection_direction == SelectionMode::Enclosed {
for layer in intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)) {
draw_layer_outline(layer);
}
} else {
for layer in intersection {
draw_layer_outline(layer);
}
} }
// Update the selection box // Update the selection box
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap() .unwrap()
.with_alpha(0.05) .with_alpha(0.05)
.rgba_hex(); .rgba_hex();
overlay_context.quad(quad, Some(&("#".to_string() + &fill_color))); fill_color.insert(0, '#');
let fill_color = Some(fill_color.as_str());
if selection_direction == SelectionMode::Enclosed {
overlay_context.dashed_quad(quad, fill_color, Some(4.), Some(4.), Some(0.5));
} else {
overlay_context.quad(quad, fill_color);
}
} }
// Only highlight layers if the viewport is not being panned (middle mouse button is pressed) // Only highlight layers if the viewport is not being panned (middle mouse button is pressed)
// TODO: Don't use `Key::Mmb` directly, instead take it as a variable from the input mappings list like in all other places // TODO: Don't use `Key::Mmb` directly, instead take it as a variable from the input mappings list like in all other places
@ -675,9 +707,7 @@ impl Fsm for SelectToolFsmState {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
SelectToolFsmState::Dragging SelectToolFsmState::Dragging
} else { } else {
// Make a box selection, preserving previously selected layers SelectToolFsmState::DrawingBox
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::DrawingBox { selection }
} }
}; };
tool_data.non_duplicated_layers = None; tool_data.non_duplicated_layers = None;
@ -824,7 +854,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::DraggingPivot SelectToolFsmState::DraggingPivot
} }
(SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerMove(modifier_keys)) => { (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => {
tool_data.drag_current = input.mouse.position; tool_data.drag_current = input.mouse.position;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
@ -835,8 +865,7 @@ impl Fsm for SelectToolFsmState {
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
let selection = tool_data.nested_selection_behavior; SelectToolFsmState::DrawingBox
SelectToolFsmState::DrawingBox { selection }
} }
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => { (SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true)); let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
@ -883,7 +912,7 @@ impl Fsm for SelectToolFsmState {
self self
} }
(SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerOutsideViewport(_)) => { (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(_)) => {
// AutoPanning // AutoPanning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) { if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
tool_data.drag_start += shift; tool_data.drag_start += shift;
@ -1012,14 +1041,26 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Ready { selection } SelectToolFsmState::Ready { selection }
} }
( (
SelectToolFsmState::DrawingBox { .. }, SelectToolFsmState::DrawingBox,
SelectToolMessage::DragStop { SelectToolMessage::DragStop {
remove_from_selection, remove_from_selection,
negative_box_selection, negative_box_selection,
}, },
) => { ) => {
let quad = tool_data.selection_quad(); let quad = tool_data.selection_quad();
let new_selected: HashSet<_> = document.intersect_quad_no_artboards(quad, input).collect();
let mut selection_direction = tool_action_data.preferences.get_selection_mode();
if selection_direction == SelectionMode::Directional {
selection_direction = tool_data.calculate_direction();
}
let intersection = document.intersect_quad_no_artboards(quad, input);
let new_selected: HashSet<_> = if selection_direction == SelectionMode::Enclosed {
intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)).collect()
} else {
intersection.collect()
};
let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect(); let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
if new_selected != current_selected { if new_selected != current_selected {
// Negative selection when both Shift and Ctrl are pressed // Negative selection when both Shift and Ctrl are pressed
@ -1166,7 +1207,7 @@ impl Fsm for SelectToolFsmState {
]); ]);
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });
} }
SelectToolFsmState::DrawingBox { .. } => { SelectToolFsmState::DrawingBox => {
let hint_data = HintData(vec![ let hint_data = HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Control, Key::Shift], "Remove from Selection").add_mac_keys([Key::Command, Key::Shift])]), HintGroup(vec![HintInfo::keys([Key::Control, Key::Shift], "Remove from Selection").add_mac_keys([Key::Command, Key::Shift])]),

View File

@ -9,6 +9,7 @@ use crate::messages::input_mapper::utility_types::macros::action_keys;
use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::input_mapper::utility_types::misc::ActionKeys;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider; use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
use crate::messages::preferences::PreferencesMessageHandler;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::node_graph_executor::NodeGraphExecutor; use crate::node_graph_executor::NodeGraphExecutor;

View File

@ -81,7 +81,7 @@ export function createEditor(): Editor {
// TODO: Then, delete the `(window as any).editorHandle = handle;` line above. // TODO: Then, delete the `(window as any).editorHandle = handle;` line above.
// This function is called by an FFI binding within the Rust code directly, rather than using the FrontendMessage system. // This function is called by an FFI binding within the Rust code directly, rather than using the FrontendMessage system.
// Then, this directly calls the `injectImaginatePollServerStatus` function on the `EditorHandle` object which is a JS binding generated by wasm-bindgen, going straight back into the Rust code. // Then, this directly calls the `injectImaginatePollServerStatus` function on the `EditorHandle` object which is a JS binding generated by wasm-bindgen, going straight back into the Rust code.
export function injectImaginatePollServerStatus() { // export function injectImaginatePollServerStatus() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorHandle?.injectImaginatePollServerStatus(); // (window as any).editorHandle?.injectImaginatePollServerStatus();
} // }

View File

@ -725,10 +725,10 @@ impl EditorHandle {
self.dispatch(message); self.dispatch(message);
} }
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)] // #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
pub fn inject_imaginate_poll_server_status(&self) { // pub fn inject_imaginate_poll_server_status(&self) {
self.dispatch(PortfolioMessage::ImaginatePollServerStatus); // self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
} // }
// TODO: Eventually remove this document upgrade code // TODO: Eventually remove this document upgrade code
#[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)] #[wasm_bindgen(js_name = triggerUpgradeDocumentToVectorManipulationFormat)]