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) => {
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 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 {
let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else {
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) => {
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::preferences::SelectionMode;
use crate::messages::prelude::*;
pub struct PreferencesDialogMessageData<'a> {
@ -31,6 +32,39 @@ impl PreferencesDialogMessageHandler {
const TITLE: &'static str = "Editor Preferences";
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 input_section = vec![TextLabel::new("Input").italic(true).widget_holder()];
let zoom_with_scroll = vec![
@ -43,9 +77,9 @@ impl PreferencesDialogMessageHandler {
.into()
})
.widget_holder(),
Separator::new(SeparatorType::Unrelated).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 renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()];
let use_vello = vec![
@ -54,7 +88,6 @@ impl PreferencesDialogMessageHandler {
.disabled(!preferences.supports_wgpu())
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Vello Renderer")
.table_align(true)
.tooltip(vello_tooltip)
@ -62,11 +95,19 @@ impl PreferencesDialogMessageHandler {
.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
// let imaginate_server_hostname = vec![
// TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(),
// TextLabel::new("Server Hostname").table_align(true).widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// TextInput::new(&preferences.imaginate_server_hostname)
// .min_width(200)
// .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into())
@ -75,7 +116,6 @@ impl PreferencesDialogMessageHandler {
// let imaginate_refresh_frequency = vec![
// TextLabel::new("").min_width(60).widget_holder(),
// TextLabel::new("Refresh Frequency").table_align(true).widget_holder(),
// Separator::new(SeparatorType::Unrelated).widget_holder(),
// NumberInput::new(Some(preferences.imaginate_refresh_frequency))
// .unit(" seconds")
// .min(0.)
@ -85,17 +125,9 @@ impl PreferencesDialogMessageHandler {
// .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![
LayoutGroup::Row { widgets: selection_section },
LayoutGroup::Row { widgets: vec![selection_mode] },
LayoutGroup::Row { widgets: input_section },
LayoutGroup::Row { widgets: zoom_with_scroll },
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(), &[]))
}
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
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);

View file

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

View file

@ -46,16 +46,16 @@ pub enum PortfolioMessage {
document_id: DocumentId,
},
DestroyAllDocuments,
EditorPreferences,
FontLoaded {
font_family: String,
font_style: String,
preview_url: String,
data: Vec<u8>,
},
ImaginateCheckServerStatus,
ImaginatePollServerStatus,
EditorPreferences,
ImaginateServerHostname,
// ImaginateCheckServerStatus,
// ImaginatePollServerStatus,
// ImaginateServerHostname,
Import,
LoadDocumentResources {
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::utility_types::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use crate::messages::portfolio::document::DocumentMessageData;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
@ -38,6 +39,7 @@ pub struct PortfolioMessageHandler {
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
pub persistent_data: PersistentData,
pub executor: NodeGraphExecutor,
pub selection_mode: SelectionMode,
}
impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMessageHandler {
@ -295,35 +297,35 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
PortfolioMessage::ImaginateCheckServerStatus => {
let server_status = self.persistent_data.imaginate.server_status().clone();
self.persistent_data.imaginate.poll_server_check();
#[cfg(target_arch = "wasm32")]
if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
wasm_bindgen_futures::spawn_local(async move {
let () = fut.await;
use wasm_bindgen::prelude::*;
// PortfolioMessage::ImaginateCheckServerStatus => {
// let server_status = self.persistent_data.imaginate.server_status().clone();
// self.persistent_data.imaginate.poll_server_check();
// #[cfg(target_arch = "wasm32")]
// if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
// wasm_bindgen_futures::spawn_local(async move {
// let () = fut.await;
// use wasm_bindgen::prelude::*;
#[wasm_bindgen(module = "/../frontend/src/editor.ts")]
extern "C" {
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
fn inject();
}
inject();
})
}
if &server_status != self.persistent_data.imaginate.server_status() {
responses.add(PropertiesPanelMessage::Refresh);
}
}
PortfolioMessage::ImaginatePollServerStatus => {
self.persistent_data.imaginate.poll_server_check();
responses.add(PropertiesPanelMessage::Refresh);
}
// #[wasm_bindgen(module = "/../frontend/src/editor.ts")]
// extern "C" {
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
// fn inject();
// }
// inject();
// })
// }
// if &server_status != self.persistent_data.imaginate.server_status() {
// responses.add(PropertiesPanelMessage::Refresh);
// }
// }
// PortfolioMessage::ImaginatePollServerStatus => {
// self.persistent_data.imaginate.poll_server_check();
// responses.add(PropertiesPanelMessage::Refresh);
// }
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
PortfolioMessage::ImaginateServerHostname => {
self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
}
// PortfolioMessage::ImaginateServerHostname => {
// self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
// }
PortfolioMessage::Import => {
// 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);

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)]
pub struct PersistentData {

View file

@ -1,7 +1,10 @@
mod preferences_message;
mod preferences_message_handler;
pub mod utility_types;
#[doc(inline)]
pub use preferences_message::{PreferencesMessage, PreferencesMessageDiscriminant};
#[doc(inline)]
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::*;
#[impl_message(Message, Preferences)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum PreferencesMessage {
// Management messages
Load { preferences: String },
ResetToDefaults,
ImaginateRefreshFrequency { seconds: f64 },
// Per-preference messages
UseVello { use_vello: bool },
ImaginateServerHostname { hostname: String },
SelectionMode { selection_mode: SelectionMode },
VectorMeshes { enabled: 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::preferences::SelectionMode;
use crate::messages::prelude::*;
use graph_craft::wasm_application_io::EditorPreferences;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct PreferencesMessageHandler {
pub imaginate_server_hostname: String,
pub imaginate_refresh_frequency: f64,
pub selection_mode: SelectionMode,
pub zoom_with_scroll: bool,
pub use_vello: bool,
pub vector_meshes: bool,
}
impl PreferencesMessageHandler {
pub fn get_selection_mode(&self) -> SelectionMode {
self.selection_mode
}
pub fn editor_preferences(&self) -> EditorPreferences {
EditorPreferences {
imaginate_hostname: self.imaginate_server_hostname.clone(),
@ -33,6 +40,7 @@ impl Default for PreferencesMessageHandler {
Self {
imaginate_server_hostname: host_name,
imaginate_refresh_frequency: 1.,
selection_mode: SelectionMode::Touched,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
use_vello,
vector_meshes: false,
@ -43,6 +51,7 @@ impl Default for PreferencesMessageHandler {
impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
fn process_message(&mut self, message: PreferencesMessage, responses: &mut VecDeque<Message>, _data: ()) {
match message {
// Management messages
PreferencesMessage::Load { preferences } => {
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
*self = deserialized_preferences;
@ -65,31 +74,12 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
*self = Self::default()
}
PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
self.imaginate_refresh_frequency = seconds;
responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::EditorPreferences);
}
// Per-preference messages
PreferencesMessage::UseVello { use_vello } => {
self.use_vello = use_vello;
responses.add(PortfolioMessage::UpdateVelloPreference);
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 } => {
self.vector_meshes = enabled;
}
@ -102,7 +92,31 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
};
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() });
}

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.
#[derive(Clone, Debug, Default)]
pub struct SelectedEdges {
bounds: [DVec2; 2],
top: bool,
bottom: bool,
left: bool,
right: bool,
pub bounds: [DVec2; 2],
pub top: bool,
pub bottom: bool,
pub left: bool,
pub right: bool,
// Aspect ratio in the form of width/height, so x:1 = width:height
aspect_ratio: f64,
}

View file

@ -1,4 +1,5 @@
use super::utility_types::ToolType;
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use graphene_core::raster::color::Color;
@ -98,4 +99,7 @@ pub enum ToolMessage {
Undo,
UpdateCursor,
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::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
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::pivot::Pivot;
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)]
enum SelectToolFsmState {
Ready { selection: NestedSelectionBehavior },
DrawingBox { selection: NestedSelectionBehavior },
DrawingBox,
Dragging,
ResizingBounds,
RotatingBounds,
DraggingPivot,
}
impl Default for SelectToolFsmState {
fn default() -> Self {
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();
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 {
let tolerance = DVec2::splat(SELECTION_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);
// 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
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
for layer in document.intersect_quad_no_artboards(quad, input) {
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
let mut draw_layer_outline = |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
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()
.with_alpha(0.05)
.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)
// 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);
SelectToolFsmState::Dragging
} else {
// Make a box selection, preserving previously selected layers
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::DrawingBox { selection }
SelectToolFsmState::DrawingBox
}
};
tool_data.non_duplicated_layers = None;
@ -824,7 +854,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::DraggingPivot
}
(SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerMove(modifier_keys)) => {
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => {
tool_data.drag_current = input.mouse.position;
responses.add(OverlaysMessage::Draw);
@ -835,8 +865,7 @@ impl Fsm for SelectToolFsmState {
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
let selection = tool_data.nested_selection_behavior;
SelectToolFsmState::DrawingBox { selection }
SelectToolFsmState::DrawingBox
}
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
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
}
(SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(_)) => {
// AutoPanning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
tool_data.drag_start += shift;
@ -1012,14 +1041,26 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Ready { selection }
}
(
SelectToolFsmState::DrawingBox { .. },
SelectToolFsmState::DrawingBox,
SelectToolMessage::DragStop {
remove_from_selection,
negative_box_selection,
},
) => {
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();
if new_selected != current_selected {
// Negative selection when both Shift and Ctrl are pressed
@ -1166,7 +1207,7 @@ impl Fsm for SelectToolFsmState {
]);
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}
SelectToolFsmState::DrawingBox { .. } => {
SelectToolFsmState::DrawingBox => {
let hint_data = HintData(vec![
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])]),

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::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
use crate::messages::preferences::PreferencesMessageHandler;
use crate::messages::prelude::*;
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.
// 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.
export function injectImaginatePollServerStatus() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorHandle?.injectImaginatePollServerStatus();
}
// export function injectImaginatePollServerStatus() {
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// (window as any).editorHandle?.injectImaginatePollServerStatus();
// }

View file

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