mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Merge branch 'GraphiteEditor:master' into hierarchical-tree
This commit is contained in:
commit
cff1d81208
58 changed files with 1742 additions and 751 deletions
18
.nix/flake.lock
generated
18
.nix/flake.lock
generated
|
@ -20,11 +20,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1743583204,
|
||||
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
|
||||
"lastModified": 1748190013,
|
||||
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5135c59491985879812717f4c9fea69604e7f26f",
|
||||
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -36,11 +36,11 @@
|
|||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1739214665,
|
||||
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
|
||||
"lastModified": 1748190013,
|
||||
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
|
||||
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -65,11 +65,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1743682350,
|
||||
"narHash": "sha256-S/MyKOFajCiBm5H5laoE59wB6w0NJ4wJG53iAPfYW3k=",
|
||||
"lastModified": 1748399823,
|
||||
"narHash": "sha256-kahD8D5hOXOsGbNdoLLnqCL887cjHkx98Izc37nDjlA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "c4a8327b0f25d1d81edecbb6105f74d7cf9d7382",
|
||||
"rev": "d68a69dc71bc19beb3479800392112c2f6218159",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
pkgs.git
|
||||
pkgs.gobject-introspection
|
||||
pkgs-unstable.cargo-tauri
|
||||
pkgs-unstable.cargo-about
|
||||
|
||||
# Linker
|
||||
pkgs.mold
|
||||
|
|
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
|
@ -136,3 +136,6 @@ 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;
|
||||
|
||||
// INPUT
|
||||
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;
|
||||
|
|
|
@ -145,12 +145,14 @@ impl LayoutHolder for ExportDialogMessageHandler {
|
|||
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(),
|
||||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
let transparent_background = vec![
|
||||
TextLabel::new("Transparency").table_align(true).min_width(100).widget_holder(),
|
||||
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(&mut 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())
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
|
|
|
@ -78,11 +78,13 @@ impl LayoutHolder for NewDocumentDialogMessageHandler {
|
|||
.widget_holder(),
|
||||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
let infinite = vec![
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).widget_holder(),
|
||||
TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(&mut 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())
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ impl PreferencesDialogMessageHandler {
|
|||
.widget_holder(),
|
||||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
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(),
|
||||
|
@ -80,8 +81,13 @@ impl PreferencesDialogMessageHandler {
|
|||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Zoom with Scroll")
|
||||
.table_align(true)
|
||||
.tooltip(zoom_with_scroll_tooltip)
|
||||
.for_checkbox(&mut checkbox_id)
|
||||
.widget_holder(),
|
||||
TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(),
|
||||
];
|
||||
|
||||
// =======
|
||||
|
@ -163,6 +169,7 @@ impl PreferencesDialogMessageHandler {
|
|||
graph_wire_style,
|
||||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
|
||||
let use_vello = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
|
@ -171,14 +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())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Vello Renderer")
|
||||
.table_align(true)
|
||||
.tooltip(vello_tooltip)
|
||||
.disabled(!preferences.supports_wgpu())
|
||||
.for_checkbox(&mut checkbox_id)
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
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![
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
|
@ -186,8 +196,13 @@ 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)
|
||||
.widget_holder(),
|
||||
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(),
|
||||
];
|
||||
|
||||
// TODO: Reenable when Imaginate is restored
|
||||
|
|
|
@ -240,7 +240,17 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "hintData")]
|
||||
hint_data: HintData,
|
||||
},
|
||||
UpdateLayersPanelControlBarLayout {
|
||||
UpdateLayersPanelControlBarLeftLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateLayersPanelControlBarRightLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
},
|
||||
UpdateLayersPanelBottomBarLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
|
|
|
@ -429,7 +429,9 @@ impl LayoutMessageHandler {
|
|||
LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff },
|
||||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
|
||||
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
|
||||
LayoutTarget::LayersPanelControlBar => FrontendMessage::UpdateLayersPanelControlBarLayout { 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::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff },
|
||||
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
|
||||
|
|
|
@ -31,8 +31,12 @@ pub enum LayoutTarget {
|
|||
DocumentBar,
|
||||
/// Contains the dropdown for design / select / guide mode found on the top left of the canvas.
|
||||
DocumentMode,
|
||||
/// Options for opacity seen at the top of the Layers panel.
|
||||
LayersPanelControlBar,
|
||||
/// Blending options at the top of the Layers panel.
|
||||
LayersPanelControlLeftBar,
|
||||
/// Selected layer status (locked/hidden) at the top of the Layers panel.
|
||||
LayersPanelControlRightBar,
|
||||
/// Controls for adding, grouping, and deleting layers at the bottom of the Layers panel.
|
||||
LayersPanelBottomBar,
|
||||
/// The dropdown menu at the very top of the application: File, Edit, etc.
|
||||
MenuBar,
|
||||
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
|
||||
|
|
|
@ -42,6 +42,9 @@ pub struct IconButton {
|
|||
pub struct PopoverButton {
|
||||
pub style: Option<String>,
|
||||
|
||||
#[serde(rename = "menuDirection")]
|
||||
pub menu_direction: Option<MenuDirection>,
|
||||
|
||||
pub icon: Option<String>,
|
||||
|
||||
pub disabled: bool,
|
||||
|
@ -58,6 +61,20 @@ pub struct PopoverButton {
|
|||
pub popover_min_width: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum MenuDirection {
|
||||
Top,
|
||||
#[default]
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
Center,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct ParameterExposeButton {
|
||||
|
|
|
@ -5,6 +5,8 @@ use graphene_core::Color;
|
|||
use graphene_core::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)]
|
||||
|
@ -18,6 +20,9 @@ pub struct CheckboxInput {
|
|||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(rename = "forLabel", skip_serializing_if = "checkbox_id_is_empty")]
|
||||
pub for_label: CheckboxId,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
|
@ -39,12 +44,51 @@ impl Default for CheckboxInput {
|
|||
icon: "Checkmark".into(),
|
||||
tooltip: Default::default(),
|
||||
tooltip_shortcut: Default::default(),
|
||||
for_label: CheckboxId::default(),
|
||||
on_update: Default::default(),
|
||||
on_commit: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub struct CheckboxId(Arc<OnceCell<u64>>);
|
||||
|
||||
impl CheckboxId {
|
||||
pub fn fill(&mut self) {
|
||||
let _ = self.0.set(graphene_core::uuid::generate_uuid());
|
||||
}
|
||||
}
|
||||
impl specta::Type for CheckboxId {
|
||||
fn inline(_type_map: &mut specta::TypeCollection, _generics: specta::Generics) -> specta::datatype::DataType {
|
||||
// TODO: This might not be right, but it works for now. We just need the type `bigint | undefined`.
|
||||
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)]
|
||||
pub struct DropdownInput {
|
||||
|
@ -67,6 +111,13 @@ pub struct DropdownInput {
|
|||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Styling
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
#[serde(rename = "maxWidth")]
|
||||
pub max_width: u32,
|
||||
//
|
||||
// Callbacks
|
||||
// `on_update` exists on the `MenuListEntry`, not this parent `DropdownInput`
|
||||
|
@ -208,6 +259,9 @@ pub struct NumberInput {
|
|||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
#[serde(rename = "maxWidth")]
|
||||
pub max_width: u32,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use super::input_widgets::CheckboxId;
|
||||
use derivative::*;
|
||||
use graphite_proc_macros::WidgetBuilder;
|
||||
|
||||
|
@ -56,9 +57,21 @@ pub struct TextLabel {
|
|||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(rename = "checkboxId")]
|
||||
#[widget_builder(skip)]
|
||||
pub checkbox_id: 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
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add UserInputLabel
|
||||
|
|
|
@ -301,7 +301,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
// Clear the control bar
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(Default::default()),
|
||||
layout_target: LayoutTarget::LayersPanelControlBar,
|
||||
layout_target: LayoutTarget::LayersPanelControlLeftBar,
|
||||
});
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(Default::default()),
|
||||
layout_target: LayoutTarget::LayersPanelControlRightBar,
|
||||
});
|
||||
|
||||
// Clear the bottom bar
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(Default::default()),
|
||||
layout_target: LayoutTarget::LayersPanelBottomBar,
|
||||
});
|
||||
}
|
||||
DocumentMessage::CreateEmptyFolder => {
|
||||
|
@ -346,6 +356,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
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);
|
||||
|
||||
self.network_interface.load_structure();
|
||||
let data_buffer: RawBuffer = self.serialize_root();
|
||||
|
@ -2132,165 +2143,212 @@ impl DocumentMessageHandler {
|
|||
widgets: vec![TextLabel::new("General").widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.artboard_name)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::ArtboardName),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Artboard Name".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.artboard_name)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::ArtboardName),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Artboard Name".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.transform_measurement)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::TransformMeasurement),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("G/R/S Measurement".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.transform_measurement)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::TransformMeasurement),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("G/R/S Measurement".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Select Tool").widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.quick_measurement)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::QuickMeasurement),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Quick Measurement".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.quick_measurement)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::QuickMeasurement),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Quick Measurement".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.transform_cage)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::TransformCage),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Cage".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.transform_cage)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::TransformCage),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Cage".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.compass_rose)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::CompassRose),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Dial".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.compass_rose)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::CompassRose),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Dial".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.pivot)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Pivot),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Pivot".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.pivot)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Pivot),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Pivot".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.hover_outline)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::HoverOutline),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Hover Outline".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.hover_outline)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::HoverOutline),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Hover Outline".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.selection_outline)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::SelectionOutline),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Selection Outline".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.selection_outline)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::SelectionOutline),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Selection Outline".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Pen & Path Tools").widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.path)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Path),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Path".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.path)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Path),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Path".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.anchors)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Anchors),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Anchors".to_string()).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.anchors)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Anchors),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Anchors".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.handles)
|
||||
.disabled(!self.overlays_visibility_settings.anchors)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Handles),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new("Handles".to_string()).disabled(!self.overlays_visibility_settings.anchors).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.handles)
|
||||
.disabled(!self.overlays_visibility_settings.anchors)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Handles),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Handles".to_string())
|
||||
.disabled(!self.overlays_visibility_settings.anchors)
|
||||
.for_checkbox(&mut checkbox_id)
|
||||
.widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
.widget_holder(),
|
||||
|
@ -2319,25 +2377,45 @@ impl DocumentMessageHandler {
|
|||
]
|
||||
.into_iter()
|
||||
.chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(*closure(&mut snapping_state))
|
||||
.on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into())
|
||||
.tooltip(tooltip)
|
||||
.widget_holder(),
|
||||
TextLabel::new(name).tooltip(tooltip).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(*closure(&mut snapping_state))
|
||||
.on_update(move |input: &CheckboxInput| {
|
||||
DocumentMessage::SetSnapping {
|
||||
closure: Some(closure),
|
||||
snapping_state: input.checked,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.tooltip(tooltip)
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new(name).tooltip(tooltip).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
}))
|
||||
.chain([LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(SnappingOptions::Paths.to_string()).widget_holder()],
|
||||
}])
|
||||
.chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(*closure(&mut snapping_state2))
|
||||
.on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into())
|
||||
.tooltip(tooltip)
|
||||
.widget_holder(),
|
||||
TextLabel::new(name).tooltip(tooltip).widget_holder(),
|
||||
],
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(*closure(&mut snapping_state2))
|
||||
.on_update(move |input: &CheckboxInput| {
|
||||
DocumentMessage::SetSnapping {
|
||||
closure: Some(closure),
|
||||
snapping_state: input.checked,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.tooltip(tooltip)
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new(name).tooltip(tooltip).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
}))
|
||||
.collect(),
|
||||
)
|
||||
|
@ -2511,12 +2589,14 @@ impl DocumentMessageHandler {
|
|||
.selected_index(blend_mode.and_then(|blend_mode| blend_mode.index_in_list_svg_subset()).map(|index| index as u32))
|
||||
.disabled(disabled)
|
||||
.draw_icon(false)
|
||||
.max_width(100)
|
||||
.tooltip("Blend Mode")
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(opacity)
|
||||
.label("Opacity")
|
||||
.unit("%")
|
||||
.display_decimal_places(2)
|
||||
.display_decimal_places(0)
|
||||
.disabled(disabled)
|
||||
.min(0.)
|
||||
.max(100.)
|
||||
|
@ -2531,33 +2611,13 @@ impl DocumentMessageHandler {
|
|||
}
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::AddTransaction.into())
|
||||
.max_width(100)
|
||||
.tooltip("Opacity")
|
||||
.widget_holder(),
|
||||
//
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
//
|
||||
IconButton::new("NewLayer", 24)
|
||||
.tooltip("New Layer")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("Folder", 24)
|
||||
.tooltip("Group Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
|
||||
.on_update(|_| {
|
||||
let group_folder_type = GroupFolderType::Layer;
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
IconButton::new("Trash", 24)
|
||||
.tooltip("Delete Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
|
||||
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
//
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
//
|
||||
];
|
||||
let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
|
||||
|
||||
let widgets = vec![
|
||||
IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24)
|
||||
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
|
||||
.tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
|
||||
|
@ -2573,11 +2633,75 @@ impl DocumentMessageHandler {
|
|||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
];
|
||||
let layers_panel_control_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
|
||||
let layers_panel_control_bar_right = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
|
||||
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(layers_panel_control_bar),
|
||||
layout_target: LayoutTarget::LayersPanelControlBar,
|
||||
layout: Layout::WidgetLayout(layers_panel_control_bar_left),
|
||||
layout_target: LayoutTarget::LayersPanelControlLeftBar,
|
||||
});
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(layers_panel_control_bar_right),
|
||||
layout_target: LayoutTarget::LayersPanelControlRightBar,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_layers_panel_bottom_bar_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||
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();
|
||||
|
||||
let widgets = vec![
|
||||
PopoverButton::new()
|
||||
.icon(Some("Node".to_string()))
|
||||
.menu_direction(Some(MenuDirection::Top))
|
||||
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
||||
.disabled(!has_selection || has_multiple_selection)
|
||||
.popover_layout({
|
||||
let node_chooser = NodeCatalog::new()
|
||||
.on_update(move |node_type| {
|
||||
if let Some(layer) = selected_layer {
|
||||
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
||||
node_type: node_type.clone(),
|
||||
layer: LayerNodeIdentifier::new_unchecked(layer.to_node()),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
})
|
||||
.widget_holder();
|
||||
vec![LayoutGroup::Row { widgets: vec![node_chooser] }]
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
IconButton::new("Folder", 24)
|
||||
.tooltip("Group Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
|
||||
.on_update(|_| {
|
||||
let group_folder_type = GroupFolderType::Layer;
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
IconButton::new("NewLayer", 24)
|
||||
.tooltip("New Layer")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("Trash", 24)
|
||||
.tooltip("Delete Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
|
||||
.on_update(|_| DocumentMessage::DeleteSelectedLayers.into())
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
];
|
||||
let layers_panel_bottom_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
|
||||
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(layers_panel_bottom_bar),
|
||||
layout_target: LayoutTarget::LayersPanelBottomBar,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1215,11 +1215,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||
if is_stack_wire(&wire) { stack_wires.push(wire) } else { node_wires.push(wire) }
|
||||
}
|
||||
|
||||
// Auto convert node to layer when inserting on a single stack wire
|
||||
if stack_wires.len() == 1 && node_wires.is_empty() {
|
||||
network_interface.set_to_node_or_layer(&selected_node_id, selection_network_path, true)
|
||||
}
|
||||
|
||||
let overlapping_wire = if network_interface.is_layer(&selected_node_id, selection_network_path) {
|
||||
if stack_wires.len() == 1 {
|
||||
stack_wires.first()
|
||||
|
@ -1852,11 +1847,6 @@ impl NodeGraphMessageHandler {
|
|||
//
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
//
|
||||
IconButton::new("NewLayer", 24)
|
||||
.tooltip("New Layer")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("Folder", 24)
|
||||
.tooltip("Group Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
|
||||
|
@ -1866,6 +1856,11 @@ impl NodeGraphMessageHandler {
|
|||
})
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
IconButton::new("NewLayer", 24)
|
||||
.tooltip("New Layer")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder))
|
||||
.on_update(|_| DocumentMessage::CreateEmptyFolder.into())
|
||||
.widget_holder(),
|
||||
IconButton::new("Trash", 24)
|
||||
.tooltip("Delete Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers))
|
||||
|
@ -2010,12 +2005,38 @@ impl NodeGraphMessageHandler {
|
|||
}
|
||||
|
||||
// Next, we decide what to display based on the number of layers and nodes selected
|
||||
match layers.len() {
|
||||
match *layers.as_slice() {
|
||||
// If no layers are selected, show properties for all selected nodes
|
||||
0 => {
|
||||
[] => {
|
||||
let selected_nodes = nodes.iter().map(|node_id| node_properties::generate_node_properties(*node_id, context)).collect::<Vec<_>>();
|
||||
if !selected_nodes.is_empty() {
|
||||
return selected_nodes;
|
||||
let mut properties = Vec::new();
|
||||
|
||||
if let [node_id] = *nodes.as_slice() {
|
||||
properties.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
IconLabel::new("Node").tooltip("Name of the selected node").widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
TextInput::new(context.network_interface.display_name(&node_id, context.selection_network_path))
|
||||
.tooltip("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()
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
properties.extend(selected_nodes);
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
// TODO: Display properties for encapsulating node when no nodes are selected in a nested network
|
||||
|
@ -2057,8 +2078,7 @@ impl NodeGraphMessageHandler {
|
|||
properties
|
||||
}
|
||||
// If one layer is selected, filter out all selected nodes that are not upstream of it. If there are no nodes left, show properties for the layer. Otherwise, show nothing.
|
||||
1 => {
|
||||
let layer = layers[0];
|
||||
[layer] => {
|
||||
let nodes_not_upstream_of_layer = nodes.into_iter().filter(|&selected_node_id| {
|
||||
!context
|
||||
.network_interface
|
||||
|
|
|
@ -339,6 +339,7 @@
|
|||
// resolution_index,
|
||||
// ))
|
||||
// .on_commit(commit_value)
|
||||
// .for_label(checkbox_id.clone())
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Related).widget_holder(),
|
||||
// NumberInput::new(Some(vec2.x))
|
||||
|
@ -438,7 +439,7 @@
|
|||
// LayoutGroup::Row { widgets }.with_tooltip("A negative text prompt can be used to list things like objects or colors to avoid")
|
||||
// };
|
||||
// let base_image = {
|
||||
// let widgets = bool_widget(document_node, node_id, base_img_index, "Adapt Input Image", CheckboxInput::default(), true);
|
||||
// let widgets = bool_widget(document_node, node_id, base_img_index, "Adapt Input Image", CheckboxInput::default().for_label(checkbox_id.clone()), true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Generate an image based upon the bitmap data plugged into this node")
|
||||
// };
|
||||
// let image_creativity = {
|
||||
|
@ -529,7 +530,7 @@
|
|||
// // }
|
||||
|
||||
// let improve_faces = {
|
||||
// let widgets = bool_widget(document_node, node_id, faces_index, "Improve Faces", CheckboxInput::default(), true);
|
||||
// let widgets = bool_widget(document_node, node_id, faces_index, "Improve Faces", CheckboxInput::default().for_label(checkbox_id.clone()), true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// "Postprocess human (or human-like) faces to look subtly less distorted.\n\
|
||||
// \n\
|
||||
|
@ -537,7 +538,7 @@
|
|||
// )
|
||||
// };
|
||||
// let tiling = {
|
||||
// let widgets = bool_widget(document_node, node_id, tiling_index, "Tiling", CheckboxInput::default(), true);
|
||||
// let widgets = bool_widget(document_node, node_id, tiling_index, "Tiling", CheckboxInput::default().for_label(checkbox_id.clone()), true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Generate the image so its edges loop seamlessly to make repeatable patterns or textures")
|
||||
// };
|
||||
// layout.extend_from_slice(&[improve_faces, tiling]);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::utility_types::{DrawHandles, OverlayContext};
|
||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||
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};
|
||||
|
@ -23,7 +24,7 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
|
|||
create_context().expect("Failed to get canvas context")
|
||||
}
|
||||
|
||||
pub fn selected_segments(document: &DocumentMessageHandler, shape_editor: &mut ShapeState) -> Vec<SegmentId> {
|
||||
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 })
|
||||
|
@ -40,8 +41,8 @@ pub fn selected_segments(document: &DocumentMessageHandler, shape_editor: &mut S
|
|||
|
||||
// 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 document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
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) {
|
||||
|
|
|
@ -404,8 +404,7 @@ pub const SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 5] = [
|
|||
(
|
||||
"Distribute Evenly",
|
||||
(|snapping_state| &mut snapping_state.bounding_box.distribute_evenly) as GetSnapState,
|
||||
// TODO: Fix the bug/limitation that requires 'Center Points' and 'Corner Points' to be enabled
|
||||
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers\n(due to a bug, 'Center Points' and 'Corner Points' must be enabled)",
|
||||
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers",
|
||||
),
|
||||
];
|
||||
pub const SNAP_FUNCTIONS_FOR_PATHS: [(&str, GetSnapState, &str); 7] = [
|
||||
|
|
|
@ -90,6 +90,9 @@ pub enum PortfolioMessage {
|
|||
PasteSerializedData {
|
||||
data: String,
|
||||
},
|
||||
CenterPastedLayers {
|
||||
layers: Vec<LayerNodeIdentifier>,
|
||||
},
|
||||
PasteImage {
|
||||
name: Option<String>,
|
||||
image: Image<Color>,
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::messages::dialog::simple_dialogs;
|
|||
use crate::messages::frontend::utility_types::FrontendDocumentDetails;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::DocumentMessageData;
|
||||
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::clipboards::{Clipboard, CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
||||
|
@ -18,9 +19,10 @@ use crate::messages::prelude::*;
|
|||
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||
use bezier_rs::Subpath;
|
||||
use glam::IVec2;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient};
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
|
@ -1003,8 +1005,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
}
|
||||
PortfolioMessage::PasteSerializedData { data } => {
|
||||
if let Some(document) = self.active_document() {
|
||||
let mut all_new_ids = Vec::new();
|
||||
if let Ok(data) = serde_json::from_str::<Vec<CopyBufferEntry>>(&data) {
|
||||
let parent = document.new_layer_parent(false);
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let mut added_nodes = false;
|
||||
|
||||
|
@ -1014,16 +1018,129 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
responses.add(DocumentMessage::AddTransaction);
|
||||
added_nodes = true;
|
||||
}
|
||||
|
||||
document.load_layer_resources(responses);
|
||||
let new_ids: HashMap<_, _> = entry.nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||
let layer = LayerNodeIdentifier::new_unchecked(new_ids[&NodeId(0)]);
|
||||
all_new_ids.extend(new_ids.values().cloned());
|
||||
|
||||
responses.add(NodeGraphMessage::AddNodes { nodes: entry.nodes, new_ids });
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index: 0 });
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all_new_ids });
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(PortfolioMessage::CenterPastedLayers { layers });
|
||||
}
|
||||
}
|
||||
}
|
||||
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()]);
|
||||
let viewport_center_pixels = viewport_bounds_quad_pixels.center(); // In viewport pixel coordinates
|
||||
|
||||
let doc_to_viewport_transform = document.metadata().document_to_viewport;
|
||||
let viewport_to_doc_transform = doc_to_viewport_transform.inverse();
|
||||
|
||||
let viewport_quad_doc_space = viewport_to_doc_transform * viewport_bounds_quad_pixels;
|
||||
|
||||
let mut top_level_items_to_center: Vec<LayerNodeIdentifier> = Vec::new();
|
||||
let mut artboards_in_selection: Vec<LayerNodeIdentifier> = Vec::new();
|
||||
|
||||
for &layer_id in &layers {
|
||||
if document.network_interface.is_artboard(&layer_id.to_node(), &document.node_graph_handler.network) {
|
||||
artboards_in_selection.push(layer_id);
|
||||
}
|
||||
}
|
||||
|
||||
for &layer_id in &layers {
|
||||
let is_child_of_selected_artboard = artboards_in_selection.iter().any(|&artboard_id| {
|
||||
if layer_id == artboard_id {
|
||||
return false;
|
||||
}
|
||||
layer_id.ancestors(document.metadata()).any(|ancestor| ancestor == artboard_id)
|
||||
});
|
||||
|
||||
if !is_child_of_selected_artboard {
|
||||
top_level_items_to_center.push(layer_id);
|
||||
}
|
||||
}
|
||||
|
||||
if top_level_items_to_center.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut combined_min_doc = DVec2::MAX;
|
||||
let mut combined_max_doc = DVec2::MIN;
|
||||
let mut has_any_bounds = false;
|
||||
|
||||
for &item_id in &top_level_items_to_center {
|
||||
if let Some(bounds_doc) = document.metadata().bounding_box_document(item_id) {
|
||||
combined_min_doc = combined_min_doc.min(bounds_doc[0]);
|
||||
combined_max_doc = combined_max_doc.max(bounds_doc[1]);
|
||||
has_any_bounds = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !has_any_bounds {
|
||||
return;
|
||||
}
|
||||
|
||||
let combined_bounds_doc_quad = Quad::from_box([combined_min_doc, combined_max_doc]);
|
||||
|
||||
if combined_bounds_doc_quad.intersects(viewport_quad_doc_space) {
|
||||
return;
|
||||
}
|
||||
|
||||
let combined_center_doc = combined_bounds_doc_quad.center();
|
||||
let combined_center_viewport_pixels = doc_to_viewport_transform.transform_point2(combined_center_doc);
|
||||
let translation_viewport_pixels_rounded = (viewport_center_pixels - combined_center_viewport_pixels).round();
|
||||
|
||||
let final_translation_offset_doc = viewport_to_doc_transform.transform_vector2(translation_viewport_pixels_rounded);
|
||||
|
||||
if final_translation_offset_doc.abs_diff_eq(glam::DVec2::ZERO, 1e-9) {
|
||||
return;
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
for &item_id in &top_level_items_to_center {
|
||||
if document.network_interface.is_artboard(&item_id.to_node(), &document.node_graph_handler.network) {
|
||||
if let Some(bounds_doc) = document.metadata().bounding_box_document(item_id) {
|
||||
let current_artboard_origin_doc = bounds_doc[0];
|
||||
let dimensions_doc = bounds_doc[1] - bounds_doc[0];
|
||||
let new_artboard_origin_doc = current_artboard_origin_doc + final_translation_offset_doc;
|
||||
|
||||
responses.add(GraphOperationMessage::ResizeArtboard {
|
||||
layer: item_id,
|
||||
location: new_artboard_origin_doc.round().as_ivec2(),
|
||||
dimensions: dimensions_doc.round().as_ivec2(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let current_abs_doc_transform = document.metadata().transform_to_document(item_id);
|
||||
|
||||
let new_abs_doc_transform = DAffine2 {
|
||||
matrix2: current_abs_doc_transform.matrix2,
|
||||
translation: current_abs_doc_transform.translation + final_translation_offset_doc,
|
||||
};
|
||||
|
||||
let transform = doc_to_viewport_transform * new_abs_doc_transform;
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: item_id,
|
||||
transform,
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
PortfolioMessage::PasteImage {
|
||||
name,
|
||||
image,
|
||||
|
@ -1320,6 +1437,7 @@ impl PortfolioMessageHandler {
|
|||
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);
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
|
|
|
@ -2,12 +2,14 @@ use super::graph_modification_utils::{self, merge_layers};
|
|||
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
||||
use super::utility_functions::calculate_segment_angle;
|
||||
use crate::consts::HANDLE_LENGTH_FACTOR;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, SnapSource};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
|
||||
use crate::messages::tool::tool_messages::path_tool::PointSelectState;
|
||||
use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
|
||||
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
|
||||
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::transform::Transform;
|
||||
|
@ -70,7 +72,7 @@ impl SelectedLayerState {
|
|||
}
|
||||
|
||||
pub fn ignore_handles(&mut self, status: bool) {
|
||||
if self.ignore_handles == !status {
|
||||
if self.ignore_handles != status {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -86,7 +88,7 @@ impl SelectedLayerState {
|
|||
}
|
||||
|
||||
pub fn ignore_anchors(&mut self, status: bool) {
|
||||
if self.ignore_anchors == !status {
|
||||
if self.ignore_anchors != status {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -421,12 +423,20 @@ impl ShapeState {
|
|||
|
||||
/// Select/deselect the first point within the selection threshold.
|
||||
/// Returns a tuple of the points if found and the offset, or `None` otherwise.
|
||||
pub fn change_point_selection(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, extend_selection: bool) -> Option<Option<SelectedPointsInfo>> {
|
||||
pub fn change_point_selection(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
extend_selection: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
) -> Option<Option<SelectedPointsInfo>> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
|
||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_visible_point_indices(network_interface, mouse_position, select_threshold, path_overlay_mode, frontier_handles_info) {
|
||||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
||||
|
@ -467,7 +477,14 @@ impl ShapeState {
|
|||
None
|
||||
}
|
||||
|
||||
pub fn get_point_selection_state(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64) -> Option<(bool, Option<SelectedPointsInfo>)> {
|
||||
pub fn get_point_selection_state(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
) -> Option<(bool, Option<SelectedPointsInfo>)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
@ -476,6 +493,13 @@ impl ShapeState {
|
|||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
||||
// Check if point is visible under current overlay mode or not
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
if !is_visible_point(manipulator_point_id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments, &selected_points) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected_shape_state = self.selected_shape_state.get(&layer)?;
|
||||
let already_selected = selected_shape_state.is_selected(manipulator_point_id);
|
||||
|
||||
|
@ -774,7 +798,7 @@ impl ShapeState {
|
|||
|
||||
// For a non-endpoint anchor, handles are perpendicular to the average tangent of adjacent segments.(Refer:https://github.com/GraphiteEditor/Graphite/pull/2620#issuecomment-2881501494)
|
||||
let mut handle_direction = if segment_count > 1. {
|
||||
segment_angle = segment_angle / segment_count;
|
||||
segment_angle /= segment_count;
|
||||
segment_angle += std::f64::consts::FRAC_PI_2;
|
||||
DVec2::new(segment_angle.cos(), segment_angle.sin())
|
||||
} else {
|
||||
|
@ -801,7 +825,7 @@ impl ShapeState {
|
|||
let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) };
|
||||
let Some(direction) = non_zero_handle
|
||||
.to_manipulator_point()
|
||||
.get_position(&vector_data)
|
||||
.get_position(vector_data)
|
||||
.and_then(|position| (position - anchor_position).try_normalize())
|
||||
else {
|
||||
return;
|
||||
|
@ -1320,6 +1344,42 @@ impl ShapeState {
|
|||
None
|
||||
}
|
||||
|
||||
pub fn find_nearest_visible_point_indices(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let select_threshold_squared = select_threshold.powi(2);
|
||||
|
||||
// Find the closest control point among all elements of shapes_to_modify
|
||||
for &layer in self.selected_shape_state.keys() {
|
||||
if let Some((manipulator_point_id, distance_squared)) = Self::closest_point_in_layer(network_interface, layer, mouse_position) {
|
||||
// Choose the first point under the threshold
|
||||
if distance_squared < select_threshold_squared {
|
||||
// Check if point is visible in current PathOverlayMode
|
||||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
|
||||
if !is_visible_point(manipulator_point_id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments, &selected_points) {
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some((layer, manipulator_point_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// TODO Use quadtree or some equivalent spatial acceleration structure to improve this to O(log(n))
|
||||
/// Find the closest manipulator, manipulator point, and distance so we can select path elements.
|
||||
/// Brute force comparison to determine which manipulator (handle or anchor) we want to select taking O(n) time.
|
||||
|
@ -1538,6 +1598,7 @@ impl ShapeState {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a nearby clicked anchor point's handles between sharp (zero-length handles) and smooth (pulled-apart handle(s)).
|
||||
/// If both handles aren't zero-length, they are set that. If both are zero-length, they are stretched apart by a reasonable amount.
|
||||
/// This can can be activated by double clicking on an anchor with the Path tool.
|
||||
|
@ -1568,44 +1629,47 @@ impl ShapeState {
|
|||
.count();
|
||||
|
||||
// Check by comparing the handle positions to the anchor if this manipulator group is a point
|
||||
if positions != 0 {
|
||||
self.convert_manipulator_handles_to_colinear(&vector_data, id, responses, layer);
|
||||
} else {
|
||||
for handle in vector_data.all_connected(id) {
|
||||
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
|
||||
for point in self.selected_points() {
|
||||
let Some(point_id) = point.as_anchor() else { continue };
|
||||
if positions != 0 {
|
||||
self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer);
|
||||
} else {
|
||||
for handle in vector_data.all_connected(point_id) {
|
||||
let Some(bezier) = vector_data.segment_from_id(handle.segment) else { continue };
|
||||
|
||||
match bezier.handles {
|
||||
BezierHandles::Linear => {}
|
||||
BezierHandles::Quadratic { .. } => {
|
||||
let segment = handle.segment;
|
||||
// Convert to linear
|
||||
let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
match bezier.handles {
|
||||
BezierHandles::Linear => {}
|
||||
BezierHandles::Quadratic { .. } => {
|
||||
let segment = handle.segment;
|
||||
// Convert to linear
|
||||
let modification_type = VectorModificationType::SetHandles { segment, handles: [None; 2] };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
// Set the manipulator to have non-colinear handles
|
||||
for &handles in &vector_data.colinear_manipulators {
|
||||
if handles.contains(&HandleId::primary(segment)) {
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
// Set the manipulator to have non-colinear handles
|
||||
for &handles in &vector_data.colinear_manipulators {
|
||||
if handles.contains(&HandleId::primary(segment)) {
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BezierHandles::Cubic { .. } => {
|
||||
// Set handle position to anchor position
|
||||
let modification_type = handle.set_relative_position(DVec2::ZERO);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
BezierHandles::Cubic { .. } => {
|
||||
// Set handle position to anchor position
|
||||
let modification_type = handle.set_relative_position(DVec2::ZERO);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
// Set the manipulator to have non-colinear handles
|
||||
for &handles in &vector_data.colinear_manipulators {
|
||||
if handles.contains(&handle) {
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
// Set the manipulator to have non-colinear handles
|
||||
for &handles in &vector_data.colinear_manipulators {
|
||||
if handles.contains(&handle) {
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Some(true)
|
||||
};
|
||||
|
@ -1619,7 +1683,17 @@ impl ShapeState {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn select_all_in_shape(&mut self, network_interface: &NodeNetworkInterface, selection_shape: SelectionShape, selection_change: SelectionChange) {
|
||||
pub fn select_all_in_shape(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
selection_shape: SelectionShape,
|
||||
selection_change: SelectionChange,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
) {
|
||||
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
|
||||
let selected_segments = selected_segments(network_interface, self);
|
||||
|
||||
for (&layer, state) in &mut self.selected_shape_state {
|
||||
if selection_change == SelectionChange::Clear {
|
||||
state.clear_points()
|
||||
|
@ -1662,13 +1736,17 @@ impl ShapeState {
|
|||
};
|
||||
|
||||
if select {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_point(id),
|
||||
_ => {
|
||||
// Select only the handles which are of nonzero length
|
||||
if let Some(handle) = id.as_handle() {
|
||||
if handle.length(&vector_data) > 0. {
|
||||
state.select_point(id)
|
||||
let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info.clone(), selected_segments.clone(), &selected_points);
|
||||
|
||||
if is_visible_handle {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_point(id),
|
||||
_ => {
|
||||
// Select only the handles which are of nonzero length
|
||||
if let Some(handle) = id.as_handle() {
|
||||
if handle.length(&vector_data) > 0. {
|
||||
state.select_point(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,13 +114,13 @@ fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
|
|||
(Some(result), None) | (None, Some(result)) => Some(result),
|
||||
(Some(mut result), Some(align)) => {
|
||||
let SnapTarget::DistributeEvenly(distribution) = result.target else { return Some(result) };
|
||||
if distribution.is_x() && align.alignment_target_x.is_some() {
|
||||
if distribution.is_x() && align.alignment_target_horizontal.is_some() {
|
||||
result.snapped_point_document.y = align.snapped_point_document.y;
|
||||
result.alignment_target_x = align.alignment_target_x;
|
||||
result.alignment_target_horizontal = align.alignment_target_horizontal;
|
||||
}
|
||||
if distribution.is_y() && align.alignment_target_y.is_some() {
|
||||
if distribution.is_y() && align.alignment_target_vertical.is_some() {
|
||||
result.snapped_point_document.x = align.snapped_point_document.x;
|
||||
result.alignment_target_y = align.alignment_target_y;
|
||||
result.alignment_target_vertical = align.alignment_target_vertical;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
|
@ -456,10 +456,10 @@ impl SnapManager {
|
|||
}
|
||||
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
|
||||
|
||||
Self::alignment_x_overlay(&ind.distribution_boxes_x, to_viewport, overlay_context);
|
||||
Self::alignment_y_overlay(&ind.distribution_boxes_y, to_viewport, overlay_context);
|
||||
Self::alignment_x_overlay(&ind.distribution_boxes_horizontal, to_viewport, overlay_context);
|
||||
Self::alignment_y_overlay(&ind.distribution_boxes_vertical, to_viewport, overlay_context);
|
||||
|
||||
let align = [ind.alignment_target_x, ind.alignment_target_y].map(|target| target.map(|target| to_viewport.transform_point2(target)));
|
||||
let align = [ind.alignment_target_horizontal, ind.alignment_target_vertical].map(|target| target.map(|target| to_viewport.transform_point2(target)));
|
||||
let any_align = align.iter().flatten().next().is_some();
|
||||
for &target in align.iter().flatten() {
|
||||
overlay_context.line(viewport, target, None, None);
|
||||
|
@ -471,7 +471,7 @@ impl SnapManager {
|
|||
overlay_context.manipulator_handle(viewport, false, None);
|
||||
}
|
||||
|
||||
if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() {
|
||||
if !any_align && ind.distribution_equal_distance_horizontal.is_none() && ind.distribution_equal_distance_vertical.is_none() {
|
||||
let text = format!("[{}] from [{}]", ind.target, ind.source);
|
||||
let transform = DAffine2::from_translation(viewport - DVec2::new(0., 4.));
|
||||
overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_LABEL_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
|
||||
|
|
|
@ -90,7 +90,7 @@ impl AlignmentSnapper {
|
|||
distance_to_align_target,
|
||||
fully_constrained: false,
|
||||
at_intersection: true,
|
||||
alignment_target_x: Some(endpoint),
|
||||
alignment_target_horizontal: Some(endpoint),
|
||||
..Default::default()
|
||||
};
|
||||
snap_results.points.push(snap_point);
|
||||
|
@ -129,7 +129,7 @@ impl AlignmentSnapper {
|
|||
distance: distance_to_snapped,
|
||||
tolerance,
|
||||
distance_to_align_target,
|
||||
alignment_target_x: Some(target_position),
|
||||
alignment_target_horizontal: Some(target_position),
|
||||
fully_constrained: true,
|
||||
at_intersection: matches!(constraint, SnapConstraint::Line { .. }),
|
||||
..Default::default()
|
||||
|
@ -148,7 +148,7 @@ impl AlignmentSnapper {
|
|||
distance: distance_to_snapped,
|
||||
tolerance,
|
||||
distance_to_align_target,
|
||||
alignment_target_y: Some(target_position),
|
||||
alignment_target_vertical: Some(target_position),
|
||||
fully_constrained: true,
|
||||
at_intersection: matches!(constraint, SnapConstraint::Line { .. }),
|
||||
..Default::default()
|
||||
|
@ -174,8 +174,8 @@ impl AlignmentSnapper {
|
|||
target_bounds: snap_x.target_bounds,
|
||||
distance,
|
||||
tolerance,
|
||||
alignment_target_x: snap_x.alignment_target_x,
|
||||
alignment_target_y: snap_y.alignment_target_y,
|
||||
alignment_target_horizontal: snap_x.alignment_target_horizontal,
|
||||
alignment_target_vertical: snap_y.alignment_target_vertical,
|
||||
constrained: true,
|
||||
at_intersection: true,
|
||||
..Default::default()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use super::*;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::*;
|
||||
use crate::messages::prelude::*;
|
||||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DistributionSnapper {
|
||||
|
@ -79,18 +79,21 @@ impl DistributionSnapper {
|
|||
let screen_bounds = (document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()])).bounding_box();
|
||||
let max_extent = (screen_bounds[1] - screen_bounds[0]).abs().max_element();
|
||||
|
||||
// Collect artboard bounds
|
||||
for layer in document.metadata().all_layers() {
|
||||
if document.network_interface.is_artboard(&layer.to_node(), &[]) && !snap_data.ignore.contains(&layer) {
|
||||
self.add_bounds(layer, snap_data, bbox_to_snap, max_extent);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect alignment candidate bounds
|
||||
for &layer in snap_data.alignment_candidates.map_or([].as_slice(), |candidates| candidates.as_slice()) {
|
||||
if !snap_data.ignore_bounds(layer) {
|
||||
self.add_bounds(layer, snap_data, bbox_to_snap, max_extent);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort and merge intersecting rectangles
|
||||
self.right.sort_unstable_by(|a, b| a.center().x.total_cmp(&b.center().x));
|
||||
self.left.sort_unstable_by(|a, b| b.center().x.total_cmp(&a.center().x));
|
||||
self.down.sort_unstable_by(|a, b| a.center().y.total_cmp(&b.center().y));
|
||||
|
@ -184,20 +187,18 @@ impl DistributionSnapper {
|
|||
fn snap_bbox_points(&self, tolerance: f64, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, bounds: Rect) {
|
||||
let mut consider_x = true;
|
||||
let mut consider_y = true;
|
||||
|
||||
if let SnapConstraint::Line { direction, .. } = constraint {
|
||||
let direction = direction.normalize_or_zero();
|
||||
if direction.x == 0. {
|
||||
consider_x = false;
|
||||
} else if direction.y == 0. {
|
||||
consider_y = false;
|
||||
}
|
||||
consider_x = direction.x != 0.;
|
||||
consider_y = direction.y != 0.;
|
||||
}
|
||||
|
||||
let mut snap_x: Option<SnappedPoint> = None;
|
||||
let mut snap_y: Option<SnappedPoint> = None;
|
||||
|
||||
self.x(consider_x, bounds, tolerance, &mut snap_x, point);
|
||||
self.y(consider_y, bounds, tolerance, &mut snap_y, point);
|
||||
self.horizontal_snap(consider_x, bounds, tolerance, &mut snap_x, point);
|
||||
self.vertical_snap(consider_y, bounds, tolerance, &mut snap_y, point);
|
||||
|
||||
match (snap_x, snap_y) {
|
||||
(Some(x), Some(y)) => {
|
||||
|
@ -209,8 +210,8 @@ impl DistributionSnapper {
|
|||
final_point.snapped_point_document += y.snapped_point_document - point.document_point;
|
||||
final_point.source_bounds = Some(final_bounds.into());
|
||||
final_point.target = SnapTarget::DistributeEvenly(DistributionSnapTarget::XY);
|
||||
final_point.distribution_boxes_y = y.distribution_boxes_y;
|
||||
final_point.distribution_equal_distance_y = y.distribution_equal_distance_y;
|
||||
final_point.distribution_boxes_vertical = y.distribution_boxes_vertical;
|
||||
final_point.distribution_equal_distance_vertical = y.distribution_equal_distance_vertical;
|
||||
final_point.distance = (final_point.distance * final_point.distance + y.distance * y.distance).sqrt();
|
||||
snap_results.points.push(final_point);
|
||||
}
|
||||
|
@ -220,42 +221,55 @@ impl DistributionSnapper {
|
|||
}
|
||||
}
|
||||
|
||||
fn x(&self, consider_x: bool, bounds: Rect, tolerance: f64, snap_x: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
|
||||
// Right
|
||||
if consider_x && !self.right.is_empty() {
|
||||
fn horizontal_snap(&self, consider_x: bool, bounds: Rect, tolerance: f64, snap_x: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
|
||||
if !consider_x {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try right distribution first
|
||||
if !self.right.is_empty() {
|
||||
let (equal_dist, mut vec_right) = Self::top_level_matches(bounds, &self.right, tolerance, dist_right);
|
||||
if let Some(distances) = equal_dist {
|
||||
let translation = DVec2::X * (distances.first - distances.equal);
|
||||
vec_right.push_front(bounds.translate(translation));
|
||||
|
||||
// Find matching left distribution
|
||||
for &left in Self::exact_further_matches(bounds.translate(translation), &self.left, dist_left, distances.equal, 2).iter().skip(1) {
|
||||
vec_right.push_front(left);
|
||||
}
|
||||
|
||||
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Right, vec_right, distances, bounds, translation, tolerance))
|
||||
// Adjust bounds to maintain alignment
|
||||
if vec_right.len() > 1 {
|
||||
vec_right[0][0].y = vec_right[0][0].y.min(vec_right[1][1].y);
|
||||
vec_right[0][1].y = vec_right[0][1].y.min(vec_right[1][1].y);
|
||||
}
|
||||
|
||||
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Right, vec_right, distances, bounds, translation, tolerance));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Left
|
||||
if consider_x && !self.left.is_empty() && snap_x.is_none() {
|
||||
// Try left distribution if right didn't work
|
||||
if !self.left.is_empty() {
|
||||
let (equal_dist, mut vec_left) = Self::top_level_matches(bounds, &self.left, tolerance, dist_left);
|
||||
if let Some(distances) = equal_dist {
|
||||
let translation = -DVec2::X * (distances.first - distances.equal);
|
||||
vec_left.make_contiguous().reverse();
|
||||
vec_left.push_back(bounds.translate(translation));
|
||||
|
||||
// Find matching right distribution
|
||||
for &right in Self::exact_further_matches(bounds.translate(translation), &self.right, dist_right, distances.equal, 2).iter().skip(1) {
|
||||
vec_left.push_back(right);
|
||||
}
|
||||
|
||||
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Left, vec_left, distances, bounds, translation, tolerance))
|
||||
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Left, vec_left, distances, bounds, translation, tolerance));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Center X
|
||||
if consider_x && !self.left.is_empty() && !self.right.is_empty() && snap_x.is_none() {
|
||||
// Try center distribution if both sides exist
|
||||
if !self.left.is_empty() && !self.right.is_empty() {
|
||||
let target_x = (self.right[0].min() + self.left[0].max()).x / 2.;
|
||||
|
||||
let offset = target_x - bounds.center().x;
|
||||
|
||||
if offset.abs() < tolerance {
|
||||
|
@ -263,67 +277,93 @@ impl DistributionSnapper {
|
|||
let equal = bounds.translate(translation).min().x - self.left[0].max().x;
|
||||
let first = equal + offset;
|
||||
let distances = DistributionMatch { first, equal };
|
||||
let boxes = VecDeque::from([self.left[0], bounds.translate(translation), self.right[0]]);
|
||||
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::X, boxes, distances, bounds, translation, tolerance))
|
||||
|
||||
let mut boxes = VecDeque::from([self.left[0], bounds.translate(translation), self.right[0]]);
|
||||
|
||||
// Adjust bounds to maintain alignment
|
||||
if boxes.len() > 1 {
|
||||
boxes[1][0].y = boxes[1][0].y.min(boxes[0][1].y);
|
||||
boxes[1][1].y = boxes[1][1].y.min(boxes[0][1].y);
|
||||
}
|
||||
|
||||
*snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::X, boxes, distances, bounds, translation, tolerance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn y(&self, consider_y: bool, bounds: Rect, tolerance: f64, snap_y: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
|
||||
// Down
|
||||
if consider_y && !self.down.is_empty() {
|
||||
fn vertical_snap(&self, consider_y: bool, bounds: Rect, tolerance: f64, snap_y: &mut Option<SnappedPoint>, point: &SnapCandidatePoint) {
|
||||
if !consider_y {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try down distribution first
|
||||
if !self.down.is_empty() {
|
||||
let (equal_dist, mut vec_down) = Self::top_level_matches(bounds, &self.down, tolerance, dist_down);
|
||||
if let Some(distances) = equal_dist {
|
||||
let translation = DVec2::Y * (distances.first - distances.equal);
|
||||
vec_down.push_front(bounds.translate(translation));
|
||||
|
||||
// Find matching up distribution
|
||||
for &up in Self::exact_further_matches(bounds.translate(translation), &self.up, dist_up, distances.equal, 2).iter().skip(1) {
|
||||
vec_down.push_front(up);
|
||||
}
|
||||
|
||||
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Down, vec_down, distances, bounds, translation, tolerance))
|
||||
// Adjust bounds to maintain alignment
|
||||
if vec_down.len() > 1 {
|
||||
vec_down[0][0].x = vec_down[0][0].x.min(vec_down[1][1].x);
|
||||
vec_down[0][1].x = vec_down[0][1].x.min(vec_down[1][1].x);
|
||||
}
|
||||
|
||||
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Down, vec_down, distances, bounds, translation, tolerance));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Up
|
||||
if consider_y && !self.up.is_empty() && snap_y.is_none() {
|
||||
// Try up distribution if down didn't work
|
||||
if !self.up.is_empty() {
|
||||
let (equal_dist, mut vec_up) = Self::top_level_matches(bounds, &self.up, tolerance, dist_up);
|
||||
if let Some(distances) = equal_dist {
|
||||
let translation = -DVec2::Y * (distances.first - distances.equal);
|
||||
vec_up.make_contiguous().reverse();
|
||||
vec_up.push_back(bounds.translate(translation));
|
||||
|
||||
// Find matching down distribution
|
||||
for &down in Self::exact_further_matches(bounds.translate(translation), &self.down, dist_down, distances.equal, 2).iter().skip(1) {
|
||||
vec_up.push_back(down);
|
||||
}
|
||||
|
||||
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Up, vec_up, distances, bounds, translation, tolerance))
|
||||
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Up, vec_up, distances, bounds, translation, tolerance));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Center Y
|
||||
if consider_y && !self.up.is_empty() && !self.down.is_empty() && snap_y.is_none() {
|
||||
// Try center distribution if both sides exist
|
||||
if !self.up.is_empty() && !self.down.is_empty() {
|
||||
let target_y = (self.down[0].min() + self.up[0].max()).y / 2.;
|
||||
|
||||
let offset = target_y - bounds.center().y;
|
||||
|
||||
if offset.abs() < tolerance {
|
||||
let translation = DVec2::Y * offset;
|
||||
|
||||
let equal = bounds.translate(translation).min().y - self.up[0].max().y;
|
||||
let first = equal + offset;
|
||||
let distances = DistributionMatch { first, equal };
|
||||
|
||||
let boxes = VecDeque::from([self.up[0], bounds.translate(translation), self.down[0]]);
|
||||
let mut boxes = VecDeque::from([self.up[0], bounds.translate(translation), self.down[0]]);
|
||||
|
||||
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Y, boxes, distances, bounds, translation, tolerance))
|
||||
// Adjust bounds to maintain alignment
|
||||
if boxes.len() > 1 {
|
||||
boxes[1][0].x = boxes[1][0].x.min(boxes[0][1].x);
|
||||
boxes[1][1].x = boxes[1][1].x.min(boxes[0][1].x);
|
||||
}
|
||||
|
||||
*snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Y, boxes, distances, bounds, translation, tolerance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, config: SnapTypeConfiguration) {
|
||||
let Some(bounds) = config.bbox else { return };
|
||||
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint) || !snap_data.document.snapping_state.bounding_box.distribute_evenly {
|
||||
if !snap_data.document.snapping_state.bounding_box.distribute_evenly {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -333,7 +373,7 @@ impl DistributionSnapper {
|
|||
|
||||
pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, config: SnapTypeConfiguration) {
|
||||
let Some(bounds) = config.bbox else { return };
|
||||
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint) || !snap_data.document.snapping_state.bounding_box.distribute_evenly {
|
||||
if !snap_data.document.snapping_state.bounding_box.distribute_evenly {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -415,10 +455,14 @@ fn dist_snap_point_right() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x[0], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
|
||||
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
|
||||
expected_box[0].y = expected_box[0].y.min(dist_snapper.left[0][1].y);
|
||||
expected_box[1].y = expected_box[1].y.min(dist_snapper.left[0][1].y);
|
||||
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[0], expected_box);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -428,16 +472,28 @@ fn dist_snap_point_right_left() {
|
|||
left: [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let source = Rect::from_square(DVec2::new(0.5, 0.), 2.);
|
||||
let snap_results = &mut SnapResults::default();
|
||||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 5);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x[1], Rect::from_square(DVec2::new(-10., 0.), 2.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 5);
|
||||
|
||||
let mut expected_left1 = dist_snapper.left[1];
|
||||
let mut expected_center = Rect::from_square(DVec2::new(0., 0.), 2.);
|
||||
|
||||
expected_center[0].y = expected_center[0].y.min(dist_snapper.left[1][1].y).min(dist_snapper.right[0][1].y);
|
||||
expected_center[1].y = expected_center[1].y.min(dist_snapper.left[1][1].y).min(dist_snapper.right[0][1].y);
|
||||
|
||||
expected_left1[0].y = expected_left1[0].y.min(dist_snapper.left[0][1].y).min(expected_center[1].y);
|
||||
expected_left1[1].y = expected_left1[1].y.min(dist_snapper.left[0][1].y).min(expected_center[1].y);
|
||||
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[1], expected_left1);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], expected_center);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -451,10 +507,10 @@ fn dist_snap_point_left() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -469,10 +525,10 @@ fn dist_snap_point_left_right() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 4);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 4);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -487,10 +543,15 @@ fn dist_snap_point_center_x() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x[1], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(6.));
|
||||
|
||||
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
|
||||
expected_box[0].y = expected_box[0].y.min(dist_snapper.left[0][1].y);
|
||||
expected_box[1].y = expected_box[1].y.min(dist_snapper.left[0][1].y);
|
||||
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal[1], expected_box);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
@ -507,10 +568,15 @@ fn dist_snap_point_down() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y[0], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
|
||||
|
||||
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
|
||||
expected_box[0].x = expected_box[0].x.min(dist_snapper.down[0][1].x);
|
||||
expected_box[1].x = expected_box[1].x.min(dist_snapper.down[0][1].x);
|
||||
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical[0], expected_box);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -523,13 +589,23 @@ fn dist_snap_point_down_up() {
|
|||
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
|
||||
let snap_results = &mut SnapResults::default();
|
||||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 5);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y[1], Rect::from_square(DVec2::new(0., -10.), 2.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 5);
|
||||
|
||||
let mut expected_center = Rect::from_square(DVec2::new(0., 0.), 2.);
|
||||
expected_center[0].x = expected_center[0].x.min(dist_snapper.up[1][1].x).min(dist_snapper.down[0][1].x);
|
||||
expected_center[1].x = expected_center[1].x.min(dist_snapper.up[1][1].x).min(dist_snapper.down[0][1].x);
|
||||
|
||||
let mut expected_up = Rect::from_square(DVec2::new(0., -10.), 2.);
|
||||
expected_up[0].x = expected_up[0].x.min(dist_snapper.up[0][1].x).min(expected_center[0].x);
|
||||
expected_up[1].x = expected_up[1].x.min(dist_snapper.up[0][1].x).min(expected_center[1].x);
|
||||
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical[1], expected_up);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], expected_center);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -543,10 +619,10 @@ fn dist_snap_point_up() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -561,10 +637,10 @@ fn dist_snap_point_up_down() {
|
|||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 4);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 4);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical[2], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -577,12 +653,18 @@ fn dist_snap_point_center_y() {
|
|||
let source = Rect::from_square(DVec2::new(0., 0.5), 2.);
|
||||
let snap_results = &mut SnapResults::default();
|
||||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y[1], Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
|
||||
|
||||
let mut expected_box = Rect::from_square(DVec2::new(0., 0.), 2.);
|
||||
expected_box[0].x = expected_box[0].x.min(dist_snapper.up[0][1].x).min(dist_snapper.down[0][1].x);
|
||||
expected_box[1].x = expected_box[1].x.min(dist_snapper.up[0][1].x).min(dist_snapper.down[0][1].x);
|
||||
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical[1], expected_box);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -592,17 +674,25 @@ fn dist_snap_point_center_xy() {
|
|||
down: [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(),
|
||||
left: [-12., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
|
||||
right: [12., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(),
|
||||
..Default::default()
|
||||
};
|
||||
let source = Rect::from_square(DVec2::new(0.3, 0.4), 2.);
|
||||
let snap_results = &mut SnapResults::default();
|
||||
dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source);
|
||||
|
||||
assert_eq!(snap_results.points.len(), 1);
|
||||
assert_eq!(snap_results.points[0].distance, 0.5000000000000001);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(8.));
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_horizontal, Some(8.));
|
||||
assert_eq!(snap_results.points[0].distribution_equal_distance_vertical, Some(6.));
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_horizontal.len(), 3);
|
||||
assert_eq!(snap_results.points[0].distribution_boxes_vertical.len(), 3);
|
||||
|
||||
assert!(snap_results.points[0].distribution_boxes_horizontal[1][0].y <= dist_snapper.left[0][1].y);
|
||||
assert!(snap_results.points[0].distribution_boxes_horizontal[1][1].y <= dist_snapper.left[0][1].y);
|
||||
assert!(snap_results.points[0].distribution_boxes_vertical[1][0].x <= dist_snapper.up[0][1].x);
|
||||
assert!(snap_results.points[0].distribution_boxes_vertical[1][1].x <= dist_snapper.up[0][1].x);
|
||||
|
||||
assert_eq!(Rect::from_box(snap_results.points[0].source_bounds.unwrap().bounding_box()), Rect::from_square(DVec2::new(0., 0.), 2.));
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_horizontal, 0);
|
||||
assert_boxes_in_order(&snap_results.points[0].distribution_boxes_vertical, 1);
|
||||
}
|
||||
|
|
|
@ -29,17 +29,17 @@ pub struct SnappedPoint {
|
|||
pub outline_layers: [Option<LayerNodeIdentifier>; 2],
|
||||
pub distance: f64,
|
||||
pub tolerance: f64,
|
||||
pub distribution_boxes_x: VecDeque<Rect>,
|
||||
pub distribution_equal_distance_x: Option<f64>,
|
||||
pub distribution_boxes_y: VecDeque<Rect>,
|
||||
pub distribution_equal_distance_y: Option<f64>,
|
||||
pub distribution_boxes_horizontal: VecDeque<Rect>,
|
||||
pub distribution_equal_distance_horizontal: Option<f64>,
|
||||
pub distribution_boxes_vertical: VecDeque<Rect>,
|
||||
pub distribution_equal_distance_vertical: Option<f64>,
|
||||
pub distance_to_align_target: f64, // If aligning so that the top is aligned but the X pos is 200 from the target, this is 200.
|
||||
pub alignment_target_x: Option<DVec2>,
|
||||
pub alignment_target_y: Option<DVec2>,
|
||||
pub alignment_target_horizontal: Option<DVec2>,
|
||||
pub alignment_target_vertical: Option<DVec2>,
|
||||
}
|
||||
impl SnappedPoint {
|
||||
pub fn align(&self) -> bool {
|
||||
self.alignment_target_x.is_some() || self.alignment_target_y.is_some()
|
||||
self.alignment_target_horizontal.is_some() || self.alignment_target_vertical.is_some()
|
||||
}
|
||||
pub fn infinite_snap(snapped_point_document: DVec2) -> Self {
|
||||
Self {
|
||||
|
@ -58,15 +58,15 @@ impl SnappedPoint {
|
|||
pub fn distribute(point: &SnapCandidatePoint, target: DistributionSnapTarget, boxes: VecDeque<Rect>, distances: DistributionMatch, bounds: Rect, translation: DVec2, tolerance: f64) -> Self {
|
||||
let is_x = target.is_x();
|
||||
|
||||
let [distribution_boxes_x, distribution_boxes_y] = if is_x { [boxes, Default::default()] } else { [Default::default(), boxes] };
|
||||
let [distribution_boxes_horizontal, distribution_boxes_vertical] = if is_x { [boxes, Default::default()] } else { [Default::default(), boxes] };
|
||||
Self {
|
||||
snapped_point_document: point.document_point + translation,
|
||||
source: point.source,
|
||||
target: SnapTarget::DistributeEvenly(target),
|
||||
distribution_boxes_x,
|
||||
distribution_equal_distance_x: is_x.then_some(distances.equal),
|
||||
distribution_boxes_y,
|
||||
distribution_equal_distance_y: (!is_x).then_some(distances.equal),
|
||||
distribution_boxes_horizontal,
|
||||
distribution_equal_distance_horizontal: is_x.then_some(distances.equal),
|
||||
distribution_boxes_vertical,
|
||||
distribution_equal_distance_vertical: (!is_x).then_some(distances.equal),
|
||||
distance: (distances.first - distances.equal).abs(),
|
||||
constrained: true,
|
||||
source_bounds: Some(bounds.translate(translation).into()),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
|
||||
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
|
||||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{FontCache, load_face};
|
||||
|
@ -67,7 +68,7 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
|
|||
Quad::from_box([DVec2::ZERO, far])
|
||||
}
|
||||
|
||||
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option<f64> {
|
||||
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, prefer_handle_direction: bool) -> Option<f64> {
|
||||
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
|
||||
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
|
||||
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
|
||||
|
@ -81,15 +82,58 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data:
|
|||
|
||||
let required_handle = if is_start(anchor, segment) {
|
||||
start_handle
|
||||
.filter(|&handle| pen_tool && handle != anchor_position)
|
||||
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
|
||||
.or(end_handle.filter(|&handle| Some(handle) != start_point))
|
||||
.or(start_point)
|
||||
} else {
|
||||
end_handle
|
||||
.filter(|&handle| pen_tool && handle != anchor_position)
|
||||
.filter(|&handle| prefer_handle_direction && handle != anchor_position)
|
||||
.or(start_handle.filter(|&handle| Some(handle) != start_point))
|
||||
.or(start_point)
|
||||
};
|
||||
|
||||
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
|
||||
}
|
||||
|
||||
/// Check whether a point is visible in the current overlay mode.
|
||||
pub fn is_visible_point(
|
||||
manipulator_point_id: ManipulatorPointId,
|
||||
vector_data: &VectorData,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
selected_segments: Vec<SegmentId>,
|
||||
selected_points: &HashSet<ManipulatorPointId>,
|
||||
) -> bool {
|
||||
match manipulator_point_id {
|
||||
ManipulatorPointId::Anchor(_) => true,
|
||||
ManipulatorPointId::EndHandle(segment_id) | ManipulatorPointId::PrimaryHandle(segment_id) => {
|
||||
match (path_overlay_mode, selected_points.len() == 1) {
|
||||
(PathOverlayMode::AllHandles, _) => true,
|
||||
(PathOverlayMode::SelectedPointHandles, _) | (PathOverlayMode::FrontierHandles, true) => {
|
||||
if selected_segments.contains(&segment_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Either the segment is a part of selected segments or the opposite handle is a part of existing selection
|
||||
let Some(handle_pair) = manipulator_point_id.get_handle_pair(vector_data) else { return false };
|
||||
let other_handle = handle_pair[1].to_manipulator_point();
|
||||
|
||||
// Return whether the list of selected points contain the other handle
|
||||
selected_points.contains(&other_handle)
|
||||
}
|
||||
(PathOverlayMode::FrontierHandles, false) => {
|
||||
let Some(anchor) = manipulator_point_id.get_anchor(vector_data) else {
|
||||
warn!("No anchor for selected handle");
|
||||
return false;
|
||||
};
|
||||
let Some(frontier_handles) = &frontier_handles_info else {
|
||||
warn!("No frontier handles info provided");
|
||||
return false;
|
||||
};
|
||||
|
||||
frontier_handles.get(&segment_id).map(|anchors| anchors.contains(&anchor)).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::consts::{
|
|||
};
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments};
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Axis;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
|
@ -181,6 +181,7 @@ impl LayoutHolder for PathTool {
|
|||
})
|
||||
// TODO: Remove `unwrap_or_default` once checkboxes are capable of displaying a mixed state
|
||||
.unwrap_or_default();
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
let colinear_handle_checkbox = CheckboxInput::new(colinear_handles_state)
|
||||
.disabled(!self.tool_data.can_toggle_colinearity)
|
||||
.on_update(|&CheckboxInput { checked, .. }| {
|
||||
|
@ -191,10 +192,12 @@ impl LayoutHolder for PathTool {
|
|||
}
|
||||
})
|
||||
.tooltip(colinear_handles_tooltip)
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder();
|
||||
let colinear_handles_label = TextLabel::new("Colinear Handles")
|
||||
.disabled(!self.tool_data.can_toggle_colinearity)
|
||||
.tooltip(colinear_handles_tooltip)
|
||||
.for_checkbox(&mut checkbox_id)
|
||||
.widget_holder();
|
||||
|
||||
let path_overlay_mode_widget = RadioInput::new(vec![
|
||||
|
@ -366,6 +369,8 @@ struct PathToolData {
|
|||
select_anchor_toggled: bool,
|
||||
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
|
||||
handle_drag_toggle: bool,
|
||||
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>,
|
||||
last_click_time: u64,
|
||||
dragging_state: DraggingState,
|
||||
angle: f64,
|
||||
opposite_handle_position: Option<DVec2>,
|
||||
|
@ -375,6 +380,8 @@ struct PathToolData {
|
|||
alt_dragging_from_anchor: bool,
|
||||
angle_locked: bool,
|
||||
temporary_colinear_handles: bool,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
adjacent_anchor_offset: Option<DVec2>,
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
|
@ -387,13 +394,13 @@ impl PathToolData {
|
|||
self.saved_points_before_anchor_select_toggle.clear();
|
||||
}
|
||||
|
||||
pub fn selection_quad(&self) -> Quad {
|
||||
let bbox = self.selection_box();
|
||||
pub fn selection_quad(&self, metadata: &DocumentMetadata) -> Quad {
|
||||
let bbox = self.selection_box(metadata);
|
||||
Quad::from_box(bbox)
|
||||
}
|
||||
|
||||
pub fn calculate_selection_mode_from_direction(&mut self) -> SelectionMode {
|
||||
let bbox = self.selection_box();
|
||||
pub fn calculate_selection_mode_from_direction(&mut self, metadata: &DocumentMetadata) -> SelectionMode {
|
||||
let bbox = self.selection_box(metadata);
|
||||
let above_threshold = bbox[1].distance_squared(bbox[0]) > DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2);
|
||||
|
||||
if self.selection_mode.is_none() && above_threshold {
|
||||
|
@ -409,12 +416,15 @@ impl PathToolData {
|
|||
self.selection_mode.unwrap_or(SelectionMode::Touched)
|
||||
}
|
||||
|
||||
pub fn selection_box(&self) -> [DVec2; 2] {
|
||||
if self.previous_mouse_position == self.drag_start_pos {
|
||||
pub fn selection_box(&self, metadata: &DocumentMetadata) -> [DVec2; 2] {
|
||||
// Convert previous mouse position to viewport space first
|
||||
let document_to_viewport = metadata.document_to_viewport;
|
||||
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position);
|
||||
if previous_mouse == self.drag_start_pos {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
[self.drag_start_pos - tolerance, self.drag_start_pos + tolerance]
|
||||
} else {
|
||||
[self.drag_start_pos, self.previous_mouse_position]
|
||||
[self.drag_start_pos, previous_mouse]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,16 +453,30 @@ impl PathToolData {
|
|||
lasso_select: bool,
|
||||
handle_drag_from_anchor: bool,
|
||||
drag_zero_handle: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
) -> PathToolFsmState {
|
||||
self.double_click_handled = false;
|
||||
self.opposing_handle_lengths = None;
|
||||
|
||||
self.drag_start_pos = input.mouse.position;
|
||||
|
||||
if !self.saved_points_before_anchor_convert_smooth_sharp.is_empty() && (input.time - self.last_click_time > 500) {
|
||||
self.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
}
|
||||
|
||||
self.last_click_time = input.time;
|
||||
|
||||
let old_selection = shape_editor.selected_points().cloned().collect::<Vec<_>>();
|
||||
|
||||
// Check if the point is already selected; if not, select the first point within the threshold (in pixels)
|
||||
if let Some((already_selected, mut selection_info)) = shape_editor.get_point_selection_state(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
|
||||
// Don't select the points which are not shown currently in PathOverlayMode
|
||||
if let Some((already_selected, mut selection_info)) = shape_editor.get_point_selection_state(
|
||||
&document.network_interface,
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
path_overlay_mode,
|
||||
self.frontier_handles_info.clone(),
|
||||
) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
self.last_clicked_point_was_selected = already_selected;
|
||||
|
@ -460,7 +484,14 @@ impl PathToolData {
|
|||
// If the point is already selected and shift (`extend_selection`) is used, keep the selection unchanged.
|
||||
// Otherwise, select the first point within the threshold.
|
||||
if !(already_selected && extend_selection) {
|
||||
if let Some(updated_selection_info) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, extend_selection) {
|
||||
if let Some(updated_selection_info) = shape_editor.change_point_selection(
|
||||
&document.network_interface,
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
extend_selection,
|
||||
path_overlay_mode,
|
||||
self.frontier_handles_info.clone(),
|
||||
) {
|
||||
selection_info = updated_selection_info;
|
||||
}
|
||||
}
|
||||
|
@ -490,7 +521,7 @@ impl PathToolData {
|
|||
let modification_type = handle.set_relative_position(DVec2::ZERO);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
for &handles in &vector_data.colinear_manipulators {
|
||||
if handles.contains(&handle) {
|
||||
if handles.contains(handle) {
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
|
@ -507,7 +538,7 @@ impl PathToolData {
|
|||
|
||||
if let Some((Some(point), Some(vector_data))) = shape_editor
|
||||
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
|
||||
.and_then(|(layer, point)| Some((point.as_anchor(), document.network_interface.compute_modified_vector(layer))))
|
||||
.map(|(layer, point)| (point.as_anchor(), document.network_interface.compute_modified_vector(layer)))
|
||||
{
|
||||
let handles = vector_data
|
||||
.all_connected(point)
|
||||
|
@ -716,18 +747,39 @@ impl PathToolData {
|
|||
) -> f64 {
|
||||
let current_angle = -handle_vector.angle_to(DVec2::X);
|
||||
|
||||
if let Some(vector_data) = shape_editor
|
||||
if let Some((vector_data, layer)) = shape_editor
|
||||
.selected_shape_state
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer))
|
||||
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer).map(|vector_data| (vector_data, layer)))
|
||||
{
|
||||
let adjacent_anchor = check_handle_over_adjacent_anchor(handle_id, &vector_data);
|
||||
let mut required_angle = None;
|
||||
|
||||
// If the handle is dragged over one of its adjacent anchors while holding down the Ctrl key, compute the angle based on the tangent formed with the neighboring anchor points.
|
||||
if adjacent_anchor.is_some() && lock_angle && !self.angle_locked {
|
||||
let anchor = handle_id.get_anchor(&vector_data);
|
||||
let (angle, anchor_position) = calculate_adjacent_anchor_tangent(handle_id, anchor, adjacent_anchor, &vector_data);
|
||||
|
||||
let layer_to_document = document.metadata().transform_to_document(*layer);
|
||||
|
||||
self.adjacent_anchor_offset = handle_id
|
||||
.get_anchor_position(&vector_data)
|
||||
.and_then(|handle_anchor| anchor_position.map(|adjacent_anchor| layer_to_document.transform_point2(adjacent_anchor) - layer_to_document.transform_point2(handle_anchor)));
|
||||
|
||||
required_angle = angle;
|
||||
}
|
||||
|
||||
// If the handle is dragged near its adjacent anchors while holding down the Ctrl key, compute the angle using the tangent direction of neighboring segments.
|
||||
if relative_vector.length() < 25. && lock_angle && !self.angle_locked {
|
||||
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents) {
|
||||
self.angle = angle;
|
||||
self.angle_locked = true;
|
||||
return angle;
|
||||
}
|
||||
required_angle = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id, tangent_to_neighboring_tangents);
|
||||
}
|
||||
|
||||
// Finalize and apply angle locking if a valid target angle was determined.
|
||||
if let Some(angle) = required_angle {
|
||||
self.angle = angle;
|
||||
self.angle_locked = true;
|
||||
return angle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -875,27 +927,36 @@ impl PathToolData {
|
|||
let current_mouse = input.mouse.position;
|
||||
let raw_delta = document_to_viewport.inverse().transform_vector2(current_mouse - previous_mouse);
|
||||
|
||||
let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
|
||||
let cursor_pos = handle_pos + raw_delta;
|
||||
let snapped_delta = if let Some((handle_position, anchor_position, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
|
||||
let cursor_position = handle_position + raw_delta;
|
||||
|
||||
let handle_angle = self.calculate_handle_angle(
|
||||
shape_editor,
|
||||
document,
|
||||
responses,
|
||||
handle_pos - anchor_pos,
|
||||
cursor_pos - anchor_pos,
|
||||
handle_position - anchor_position,
|
||||
cursor_position - anchor_position,
|
||||
handle_id,
|
||||
lock_angle,
|
||||
snap_angle,
|
||||
equidistant,
|
||||
);
|
||||
|
||||
let adjacent_anchor_offset = self.adjacent_anchor_offset.unwrap_or(DVec2::ZERO);
|
||||
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
|
||||
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
|
||||
let constrained_target = anchor_pos + constrained_direction * projected_length;
|
||||
let constrained_delta = constrained_target - handle_pos;
|
||||
let projected_length = (cursor_position - anchor_position - adjacent_anchor_offset).dot(constrained_direction);
|
||||
let constrained_target = anchor_position + adjacent_anchor_offset + constrained_direction * projected_length;
|
||||
let constrained_delta = constrained_target - handle_position;
|
||||
|
||||
self.apply_snapping(constrained_direction, handle_pos + constrained_delta, anchor_pos, lock_angle || snap_angle, handle_pos, document, input)
|
||||
self.apply_snapping(
|
||||
constrained_direction,
|
||||
handle_position + constrained_delta,
|
||||
anchor_position + adjacent_anchor_offset,
|
||||
lock_angle || snap_angle,
|
||||
handle_position,
|
||||
document,
|
||||
input,
|
||||
)
|
||||
} else {
|
||||
shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse)
|
||||
};
|
||||
|
@ -1010,14 +1071,16 @@ impl Fsm for PathToolFsmState {
|
|||
match tool_options.path_overlay_mode {
|
||||
PathOverlayMode::AllHandles => {
|
||||
path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context);
|
||||
tool_data.frontier_handles_info = None;
|
||||
}
|
||||
PathOverlayMode::SelectedPointHandles => {
|
||||
let selected_segments = selected_segments(document, shape_editor);
|
||||
let selected_segments = selected_segments(&document.network_interface, shape_editor);
|
||||
|
||||
path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context);
|
||||
tool_data.frontier_handles_info = None;
|
||||
}
|
||||
PathOverlayMode::FrontierHandles => {
|
||||
let selected_segments = selected_segments(document, shape_editor);
|
||||
let selected_segments = selected_segments(&document.network_interface, shape_editor);
|
||||
let selected_points = shape_editor.selected_points();
|
||||
let selected_anchors = selected_points
|
||||
.filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None })
|
||||
|
@ -1045,13 +1108,18 @@ impl Fsm for PathToolFsmState {
|
|||
for (point, attached_segments) in selected_segments_by_point {
|
||||
if attached_segments.len() == 1 {
|
||||
segment_endpoints.entry(attached_segments[0]).or_default().push(point);
|
||||
} else if !selected_anchors.contains(&point) {
|
||||
}
|
||||
// Handle the edge case where a point, although not explicitly selected, is shared by two segments.
|
||||
else if !selected_anchors.contains(&point) {
|
||||
segment_endpoints.entry(attached_segments[0]).or_default().push(point);
|
||||
segment_endpoints.entry(attached_segments[1]).or_default().push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Caching segment endpoints for use in point selection logic
|
||||
tool_data.frontier_handles_info = Some(segment_endpoints.clone());
|
||||
|
||||
// Now frontier anchors can be sent for rendering overlays
|
||||
path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints), shape_editor, &mut overlay_context);
|
||||
}
|
||||
|
@ -1088,11 +1156,11 @@ impl Fsm for PathToolFsmState {
|
|||
let fill_color = Some(fill_color.as_str());
|
||||
|
||||
let selection_mode = match tool_action_data.preferences.get_selection_mode() {
|
||||
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(),
|
||||
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
|
||||
selection_mode => selection_mode,
|
||||
};
|
||||
|
||||
let quad = tool_data.selection_quad();
|
||||
let quad = tool_data.selection_quad(document.metadata());
|
||||
let polygon = &tool_data.lasso_polygon;
|
||||
|
||||
match (selection_shape, selection_mode) {
|
||||
|
@ -1153,7 +1221,17 @@ impl Fsm for PathToolFsmState {
|
|||
tool_data.selection_mode = None;
|
||||
tool_data.lasso_polygon.clear();
|
||||
|
||||
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, lasso_select, handle_drag_from_anchor, drag_zero_handle)
|
||||
tool_data.mouse_down(
|
||||
shape_editor,
|
||||
document,
|
||||
input,
|
||||
responses,
|
||||
extend_selection,
|
||||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_zero_handle,
|
||||
tool_options.path_overlay_mode,
|
||||
)
|
||||
}
|
||||
(
|
||||
PathToolFsmState::Drawing { selection_shape },
|
||||
|
@ -1166,7 +1244,7 @@ impl Fsm for PathToolFsmState {
|
|||
delete_segment,
|
||||
},
|
||||
) => {
|
||||
tool_data.previous_mouse_position = input.mouse.position;
|
||||
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
if selection_shape == SelectionShapeType::Lasso {
|
||||
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
|
||||
|
@ -1255,6 +1333,7 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
if !lock_angle_state {
|
||||
tool_data.angle_locked = false;
|
||||
tool_data.adjacent_anchor_offset = None;
|
||||
}
|
||||
|
||||
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
|
||||
|
@ -1297,9 +1376,23 @@ impl Fsm for PathToolFsmState {
|
|||
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
||||
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
||||
|
||||
if !tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
}
|
||||
|
||||
if tool_data.adjacent_anchor_offset.is_some() {
|
||||
tool_data.adjacent_anchor_offset = None;
|
||||
}
|
||||
|
||||
// If there is a point nearby, then remove the overlay
|
||||
if shape_editor
|
||||
.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD)
|
||||
.find_nearest_visible_point_indices(
|
||||
&document.network_interface,
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
tool_data.segment = None;
|
||||
|
@ -1385,15 +1478,29 @@ impl Fsm for PathToolFsmState {
|
|||
SelectionChange::Clear
|
||||
};
|
||||
|
||||
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position);
|
||||
if tool_data.drag_start_pos == previous_mouse {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = [tool_data.drag_start_pos, tool_data.previous_mouse_position];
|
||||
shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Box(bbox), selection_change);
|
||||
let bbox = [tool_data.drag_start_pos, previous_mouse];
|
||||
shape_editor.select_all_in_shape(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Lasso(&tool_data.lasso_polygon), selection_change),
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
|
||||
&document.network_interface,
|
||||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1431,15 +1538,29 @@ impl Fsm for PathToolFsmState {
|
|||
SelectionChange::Clear
|
||||
};
|
||||
|
||||
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position);
|
||||
if tool_data.drag_start_pos == previous_mouse {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = [tool_data.drag_start_pos, tool_data.previous_mouse_position];
|
||||
shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Box(bbox), select_kind);
|
||||
let bbox = [tool_data.drag_start_pos, previous_mouse];
|
||||
shape_editor.select_all_in_shape(
|
||||
&document.network_interface,
|
||||
SelectionShape::Box(bbox),
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Lasso(&tool_data.lasso_polygon), select_kind),
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
|
||||
&document.network_interface,
|
||||
SelectionShape::Lasso(&tool_data.lasso_polygon),
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
@ -1450,7 +1571,14 @@ impl Fsm for PathToolFsmState {
|
|||
(_, PathToolMessage::DragStop { extend_selection, .. }) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD;
|
||||
let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD);
|
||||
// TODO: Here we want only visible points to be considered
|
||||
let nearest_point = shape_editor.find_nearest_visible_point_indices(
|
||||
&document.network_interface,
|
||||
input.mouse.position,
|
||||
SELECTION_THRESHOLD,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
);
|
||||
|
||||
if let Some((layer, nearest_point)) = nearest_point {
|
||||
if !drag_occurred && extend_selection {
|
||||
|
@ -1491,6 +1619,10 @@ impl Fsm for PathToolFsmState {
|
|||
if !drag_occurred && !extend_selection {
|
||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
||||
if clicked_selected {
|
||||
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>();
|
||||
}
|
||||
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
@ -1538,7 +1670,11 @@ impl Fsm for PathToolFsmState {
|
|||
// Flip the selected point between smooth and sharp
|
||||
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp.iter().copied().collect::<Vec<_>>());
|
||||
shape_editor.flip_smooth_sharp(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
}
|
||||
|
@ -1860,3 +1996,82 @@ fn calculate_lock_angle(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data: &VectorData) -> Option<PointId> {
|
||||
let Some((anchor, handle_position)) = handle_id.get_anchor(&vector_data).zip(handle_id.get_position(vector_data)) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let check_if_close = |point_id: &PointId| {
|
||||
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else {
|
||||
return false;
|
||||
};
|
||||
(anchor_position - handle_position).length() < 10.
|
||||
};
|
||||
|
||||
vector_data.connected_points(anchor).find(|point| check_if_close(point))
|
||||
}
|
||||
fn calculate_adjacent_anchor_tangent(
|
||||
currently_dragged_handle: ManipulatorPointId,
|
||||
anchor: Option<PointId>,
|
||||
adjacent_anchor: Option<PointId>,
|
||||
vector_data: &VectorData,
|
||||
) -> (Option<f64>, Option<DVec2>) {
|
||||
// Early return if no anchor or no adjacent anchors
|
||||
|
||||
let Some((dragged_handle_anchor, adjacent_anchor)) = anchor.zip(adjacent_anchor) else {
|
||||
return (None, None);
|
||||
};
|
||||
let adjacent_anchor_position = vector_data.point_domain.position_from_id(adjacent_anchor);
|
||||
|
||||
let handles: Vec<_> = vector_data.all_connected(adjacent_anchor).filter(|handle| handle.length(vector_data) > 1e-6).collect();
|
||||
|
||||
match handles.len() {
|
||||
0 => {
|
||||
// Find non-shared segments
|
||||
let non_shared_segment: Vec<_> = vector_data
|
||||
.segment_bezier_iter()
|
||||
.filter_map(|(segment_id, _, start, end)| {
|
||||
let touches_adjacent = start == adjacent_anchor || end == adjacent_anchor;
|
||||
let shares_with_dragged = start == dragged_handle_anchor || end == dragged_handle_anchor;
|
||||
|
||||
if touches_adjacent && !shares_with_dragged { Some(segment_id) } else { None }
|
||||
})
|
||||
.collect();
|
||||
|
||||
match non_shared_segment.first() {
|
||||
Some(&segment) => {
|
||||
let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true);
|
||||
(angle, adjacent_anchor_position)
|
||||
}
|
||||
None => (None, None),
|
||||
}
|
||||
}
|
||||
|
||||
1 => {
|
||||
let segment = handles[0].segment;
|
||||
let angle = calculate_segment_angle(adjacent_anchor, segment, vector_data, true);
|
||||
(angle, adjacent_anchor_position)
|
||||
}
|
||||
|
||||
2 => {
|
||||
// Use the angle formed by the handle of the shared segment relative to its associated anchor point.
|
||||
let Some(shared_segment_handle) = handles
|
||||
.iter()
|
||||
.find(|handle| handle.opposite().to_manipulator_point() == currently_dragged_handle)
|
||||
.map(|handle| handle.to_manipulator_point())
|
||||
else {
|
||||
return (None, None);
|
||||
};
|
||||
|
||||
let angle = shared_segment_handle
|
||||
.get_position(&vector_data)
|
||||
.zip(adjacent_anchor_position)
|
||||
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X));
|
||||
|
||||
(angle, adjacent_anchor_position)
|
||||
}
|
||||
|
||||
_ => (None, None),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -642,13 +642,13 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
if overlay_context.visibility_settings.transform_cage() {
|
||||
if let Some(bounds) = bounds {
|
||||
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
|
||||
if let Some(bounds) = bounds {
|
||||
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
|
||||
|
||||
bounding_box_manager.bounds = bounds;
|
||||
bounding_box_manager.transform = transform;
|
||||
bounding_box_manager.transform_tampered = transform_tampered;
|
||||
bounding_box_manager.bounds = bounds;
|
||||
bounding_box_manager.transform = transform;
|
||||
bounding_box_manager.transform_tampered = transform_tampered;
|
||||
if overlay_context.visibility_settings.transform_cage() {
|
||||
bounding_box_manager.render_overlays(&mut overlay_context, true);
|
||||
}
|
||||
} else {
|
||||
|
@ -682,7 +682,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
let is_resizing_or_rotating = matches!(self, SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. } | SelectToolFsmState::RotatingBounds);
|
||||
|
||||
if overlay_context.visibility_settings.transform_cage() && bounds.is_some() {
|
||||
if overlay_context.visibility_settings.transform_cage() {
|
||||
if let Some(bounds) = tool_data.bounding_box_manager.as_mut() {
|
||||
let edges = bounds.check_selected_edges(input.mouse.position);
|
||||
let is_skewing = matches!(self, SelectToolFsmState::SkewingBounds { .. });
|
||||
|
|
3
frontend/assets/icon-12px-solid/clipped.svg
Normal file
3
frontend/assets/icon-12px-solid/clipped.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="M4.5,4H10V3H4.5C3.673,3,3,3.673,3,4.5V7H1l2.5,3L6,7H4V4.5C4,4.224,4.224,4,4.5,4z" />
|
||||
</svg>
|
After Width: | Height: | Size: 163 B |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 17 KiB |
|
@ -3,10 +3,11 @@
|
|||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { HSV, RGB, FillChoice } from "@graphite/messages";
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, tick, onDestroy, onMount } from "svelte";
|
||||
|
||||
import type { MenuListEntry } from "@graphite/messages";
|
||||
import type { MenuListEntry, MenuDirection } from "@graphite/messages";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import FloatingMenu, { type MenuDirection } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts" context="module">
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
|
||||
|
||||
/// Prevents the escape key from closing the parent floating menu of the given element.
|
||||
|
@ -22,6 +21,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
import { browserVersion } from "@graphite/utility-functions/platform";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
@ -184,6 +184,7 @@
|
|||
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
||||
if (!workspace || !self || !floatingMenuContainer || !floatingMenuContent || !floatingMenuContentDiv) return;
|
||||
|
||||
const viewportBounds = document.documentElement.getBoundingClientRect();
|
||||
workspaceBounds = workspace.getBoundingClientRect();
|
||||
floatingMenuBounds = self.getBoundingClientRect();
|
||||
const floatingMenuContainerBounds = floatingMenuContainer.getBoundingClientRect();
|
||||
|
@ -195,17 +196,17 @@
|
|||
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
|
||||
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
|
||||
const tailOffset = type === "Popover" ? 10 : 0;
|
||||
if (direction === "Bottom") floatingMenuContentDiv.style.top = `${tailOffset + floatingMenuBounds.top}px`;
|
||||
if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + floatingMenuBounds.bottom}px`;
|
||||
if (direction === "Right") floatingMenuContentDiv.style.left = `${tailOffset + floatingMenuBounds.left}px`;
|
||||
if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + floatingMenuBounds.right}px`;
|
||||
if (direction === "Bottom") floatingMenuContentDiv.style.top = `${tailOffset + floatingMenuBounds.y}px`;
|
||||
if (direction === "Top") floatingMenuContentDiv.style.bottom = `${tailOffset + (viewportBounds.height - floatingMenuBounds.y)}px`;
|
||||
if (direction === "Right") floatingMenuContentDiv.style.left = `${tailOffset + floatingMenuBounds.x}px`;
|
||||
if (direction === "Left") floatingMenuContentDiv.style.right = `${tailOffset + (viewportBounds.width - floatingMenuBounds.x)}px`;
|
||||
|
||||
// Required to correctly position tail when scrolled (it has a `position: fixed` to prevent clipping)
|
||||
// We use `.style` on a div (instead of a style DOM attribute binding) because the binding causes the `afterUpdate()` hook to call the function we're in recursively forever
|
||||
if (tail && direction === "Bottom") tail.style.top = `${floatingMenuBounds.top}px`;
|
||||
if (tail && direction === "Top") tail.style.bottom = `${floatingMenuBounds.bottom}px`;
|
||||
if (tail && direction === "Right") tail.style.left = `${floatingMenuBounds.left}px`;
|
||||
if (tail && direction === "Left") tail.style.right = `${floatingMenuBounds.right}px`;
|
||||
if (tail && direction === "Bottom") tail.style.top = `${floatingMenuBounds.y}px`;
|
||||
if (tail && direction === "Top") tail.style.bottom = `${viewportBounds.height - floatingMenuBounds.y}px`;
|
||||
if (tail && direction === "Right") tail.style.left = `${floatingMenuBounds.x}px`;
|
||||
if (tail && direction === "Left") tail.style.right = `${viewportBounds.width - floatingMenuBounds.x}px`;
|
||||
}
|
||||
|
||||
type Edge = "Top" | "Bottom" | "Left" | "Right";
|
||||
|
|
|
@ -3,7 +3,15 @@
|
|||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { beginDraggingElement } from "@graphite/io-managers/drag";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerStructureJs, UpdateLayersPanelControlBarLayout } from "@graphite/messages";
|
||||
import {
|
||||
defaultWidgetLayout,
|
||||
patchWidgetLayout,
|
||||
UpdateDocumentLayerDetails,
|
||||
UpdateDocumentLayerStructureJs,
|
||||
UpdateLayersPanelControlBarLeftLayout,
|
||||
UpdateLayersPanelControlBarRightLayout,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
} from "@graphite/messages";
|
||||
import type { DataBuffer, LayerPanelEntry } from "@graphite/messages";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
@ -13,6 +21,7 @@
|
|||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
type LayerListingInfo = {
|
||||
|
@ -47,12 +56,24 @@
|
|||
let dragInPanel = false;
|
||||
|
||||
// Layouts
|
||||
let layersPanelControlBarLayout = defaultWidgetLayout();
|
||||
let layersPanelControlBarLeftLayout = defaultWidgetLayout();
|
||||
let layersPanelControlBarRightLayout = defaultWidgetLayout();
|
||||
let layersPanelBottomBarLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLayout, (updateLayersPanelControlBarLayout) => {
|
||||
patchWidgetLayout(layersPanelControlBarLayout, updateLayersPanelControlBarLayout);
|
||||
layersPanelControlBarLayout = layersPanelControlBarLayout;
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
|
||||
patchWidgetLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout);
|
||||
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarRightLayout, (updateLayersPanelControlBarRightLayout) => {
|
||||
patchWidgetLayout(layersPanelControlBarRightLayout, updateLayersPanelControlBarRightLayout);
|
||||
layersPanelControlBarRightLayout = layersPanelControlBarRightLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelBottomBarLayout, (updateLayersPanelBottomBarLayout) => {
|
||||
patchWidgetLayout(layersPanelBottomBarLayout, updateLayersPanelBottomBarLayout);
|
||||
layersPanelBottomBarLayout = layersPanelBottomBarLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructureJs, (updateDocumentLayerStructure) => {
|
||||
|
@ -407,7 +428,9 @@
|
|||
|
||||
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
|
||||
<LayoutRow class="control-bar" scrollableX={true}>
|
||||
<WidgetLayout layout={layersPanelControlBarLayout} />
|
||||
<WidgetLayout layout={layersPanelControlBarLeftLayout} />
|
||||
<Separator />
|
||||
<WidgetLayout layout={layersPanelControlBarRightLayout} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={updateInsertLine} on:dragend={drop} on:drop={drop}>
|
||||
|
@ -490,6 +513,9 @@
|
|||
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`} />
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<LayoutRow class="bottom-bar" scrollableX={true}>
|
||||
<WidgetLayout layout={layersPanelBottomBarLayout} />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
@ -499,40 +525,34 @@
|
|||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
margin: 0 4px;
|
||||
border-bottom: 1px solid var(--color-2-mildblack);
|
||||
justify-content: space-between;
|
||||
|
||||
.widget-span {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
// Blend mode selector and opacity slider
|
||||
.dropdown-input,
|
||||
.number-input {
|
||||
.widget-span:first-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Blend mode selector
|
||||
.dropdown-input {
|
||||
max-width: 120px;
|
||||
flex-basis: 120px;
|
||||
}
|
||||
// Bottom bar
|
||||
.bottom-bar {
|
||||
height: 24px;
|
||||
padding-top: 4px;
|
||||
flex: 0 0 auto;
|
||||
margin: 0 4px;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid var(--color-2-mildblack);
|
||||
|
||||
// Opacity slider
|
||||
.number-input {
|
||||
max-width: 180px;
|
||||
flex-basis: 180px;
|
||||
|
||||
+ .separator ~ .separator {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.widget-span > * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Layer hierarchy
|
||||
.list-area {
|
||||
margin: 4px 0;
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
// Combine with the bottom bar to avoid a double border
|
||||
margin-bottom: -1px;
|
||||
|
||||
.layer {
|
||||
flex: 0 0 auto;
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
width: calc(100% + 2 * 4px);
|
||||
margin-top: 8px;
|
||||
|
||||
span {
|
||||
.text-label {
|
||||
white-space: wrap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -994,7 +994,7 @@
|
|||
{/if}
|
||||
<div class="details">
|
||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
||||
<span>{node.displayName}</span>
|
||||
<TextLabel>{node.displayName}</TextLabel>
|
||||
</div>
|
||||
<div class="solo-drag-grip" title="Drag only this layer without pushing others outside the stack"></div>
|
||||
<IconButton
|
||||
|
@ -1585,7 +1585,7 @@
|
|||
.details {
|
||||
margin: 0 8px;
|
||||
|
||||
span {
|
||||
.text-label {
|
||||
white-space: nowrap;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type WidgetLayout, isWidgetTable } from "@graphite/messages";
|
||||
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
import WidgetTable from "@graphite/components/widgets/WidgetTable.svelte";
|
||||
|
@ -19,7 +20,7 @@
|
|||
{:else if isWidgetTable(layoutGroup)}
|
||||
<WidgetTable widgetData={layoutGroup} layoutTarget={layout.layoutTarget} />
|
||||
{:else}
|
||||
<span style="color: #d6536e">Error: The widget layout that belongs here has an invalid layout group type</span>
|
||||
<TextLabel styles={{ color: "#d6536e" }}>Error: The widget layout that belongs here has an invalid layout group type</TextLabel>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
|
|
|
@ -65,11 +65,11 @@
|
|||
{#if isWidgetSpanRow(layoutGroup)}
|
||||
<WidgetSpan widgetData={layoutGroup} {layoutTarget} />
|
||||
{:else if isWidgetSpanColumn(layoutGroup)}
|
||||
<span style="color: #d6536e">Error: The WidgetSpan used here should be a row not a column</span>
|
||||
<TextLabel styles={{ color: "#d6536e" }}>Error: The WidgetSpan used here should be a row not a column</TextLabel>
|
||||
{:else if isWidgetSection(layoutGroup)}
|
||||
<svelte:self widgetData={layoutGroup} {layoutTarget} />
|
||||
{:else}
|
||||
<span style="color: #d6536e">Error: The widget that belongs here has an invalid layout group type</span>
|
||||
<TextLabel styles={{ color: "#d6536e" }}>Error: The widget that belongs here has an invalid layout group type</TextLabel>
|
||||
{/if}
|
||||
{/each}
|
||||
</LayoutCol>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
import { type IconName, type PopoverButtonStyle } from "@graphite/utility-functions/icons";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
@ -7,6 +8,7 @@
|
|||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
export let style: PopoverButtonStyle = "DropdownArrow";
|
||||
export let menuDirection: MenuDirection = "Bottom";
|
||||
export let icon: IconName | undefined = undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
|
@ -23,13 +25,13 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LayoutRow class="popover-button" classes={{ "has-icon": icon !== undefined }}>
|
||||
<LayoutRow class="popover-button" classes={{ "has-icon": icon !== undefined, "direction-top": menuDirection === "Top" }}>
|
||||
<IconButton class="dropdown-icon" classes={{ open }} {disabled} action={() => onClick()} icon={style || "DropdownArrow"} size={16} {tooltip} data-floating-menu-spawner />
|
||||
{#if icon !== undefined}
|
||||
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} />
|
||||
{/if}
|
||||
|
||||
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction="Bottom">
|
||||
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}>
|
||||
<slot />
|
||||
</FloatingMenu>
|
||||
</LayoutRow>
|
||||
|
@ -48,6 +50,10 @@
|
|||
padding-left: calc(36px - 16px);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
&.direction-top .dropdown-icon .icon-label {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
|
@ -86,5 +92,9 @@
|
|||
margin-bottom: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.direction-top .floating-menu {
|
||||
bottom: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
class:emphasized
|
||||
class:disabled
|
||||
class:flush
|
||||
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
|
||||
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
|
||||
title={tooltip}
|
||||
data-emphasized={emphasized || undefined}
|
||||
data-disabled={disabled || undefined}
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
export let disabled = false;
|
||||
export let icon: IconName = "Checkmark";
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let forLabel: bigint | undefined = undefined;
|
||||
|
||||
let inputElement: HTMLInputElement | undefined;
|
||||
|
||||
let id = String(Math.random()).substring(2);
|
||||
const backupId = String(Math.random()).substring(2);
|
||||
|
||||
$: id = forLabel !== undefined ? String(forLabel) : backupId;
|
||||
$: displayIcon = (!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName;
|
||||
|
||||
export function isChecked() {
|
||||
|
|
|
@ -21,12 +21,13 @@
|
|||
export let interactive = true;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
|
||||
let activeEntry = makeActiveEntry();
|
||||
let activeEntrySkipWatcher = false;
|
||||
let initialSelectedIndex: number | undefined = undefined;
|
||||
let open = false;
|
||||
let minWidth = 0;
|
||||
|
||||
$: watchSelectedIndex(selectedIndex);
|
||||
$: watchActiveEntry(activeEntry);
|
||||
|
@ -76,11 +77,15 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LayoutRow class="dropdown-input" bind:this={self} data-dropdown-input>
|
||||
<LayoutRow
|
||||
class="dropdown-input"
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}), ...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}) }}
|
||||
bind:this={self}
|
||||
data-dropdown-input
|
||||
>
|
||||
<LayoutRow
|
||||
class="dropdown-box"
|
||||
classes={{ disabled, open }}
|
||||
styles={{ "min-width": `${minWidth}px` }}
|
||||
{tooltip}
|
||||
on:click={() => !disabled && (open = true)}
|
||||
on:blur={unFocusDropdownBox}
|
||||
|
@ -150,7 +155,7 @@
|
|||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
span {
|
||||
.text-label {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
|
||||
|
|
|
@ -104,7 +104,15 @@
|
|||
|
||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow class="dropdown-box" classes={{ disabled }} styles={{ "min-width": `${minWidth}px` }} {tooltip} tabindex={disabled ? -1 : 0} on:click={toggleOpen} data-floating-menu-spawner>
|
||||
<LayoutRow
|
||||
class="dropdown-box"
|
||||
classes={{ disabled }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
{tooltip}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
on:click={toggleOpen}
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
<TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel>
|
||||
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
|
||||
</LayoutRow>
|
||||
|
@ -148,7 +156,7 @@
|
|||
&.open {
|
||||
background: var(--color-6-lowergray);
|
||||
|
||||
span {
|
||||
.text-label {
|
||||
color: var(--color-f-white);
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +168,7 @@
|
|||
&.disabled {
|
||||
background: var(--color-2-mildblack);
|
||||
|
||||
span {
|
||||
.text-label {
|
||||
color: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
// Styling
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
|
||||
// Callbacks
|
||||
export let incrementCallbackIncrease: (() => void) | undefined = undefined;
|
||||
|
@ -88,6 +89,7 @@
|
|||
$: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any";
|
||||
$: styles = {
|
||||
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
||||
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
||||
...(mode === "Range" ? { "--progress-factor": Math.min(Math.max((rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin), 0), 1) } : {}),
|
||||
};
|
||||
|
||||
|
|
|
@ -24,14 +24,14 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LayoutRow class="radio-input" classes={{ disabled }} styles={{ "min-width": minWidth > 0 ? `${minWidth}px` : "" }}>
|
||||
<LayoutRow class="radio-input" classes={{ disabled, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}>
|
||||
{#each entries as entry, index}
|
||||
<button class:active={index === selectedIndex} class:mixed class:disabled on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
|
||||
<button class:active={!mixed ? index === selectedIndex : undefined} on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
|
||||
{#if entry.icon}
|
||||
<IconLabel icon={entry.icon} />
|
||||
{/if}
|
||||
{#if entry.label}
|
||||
<TextLabel italic={mixed}>{entry.label}</TextLabel>
|
||||
<TextLabel>{entry.label}</TextLabel>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
|
@ -39,12 +39,17 @@
|
|||
|
||||
<style lang="scss" global>
|
||||
.radio-input {
|
||||
background: var(--color-5-dullgray);
|
||||
border-radius: 2px;
|
||||
height: 24px;
|
||||
|
||||
button {
|
||||
background: var(--color-5-dullgray);
|
||||
fill: var(--color-e-nearwhite);
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
border-radius: 2px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin: 2px 1px;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -54,8 +59,12 @@
|
|||
min-width: fit-content;
|
||||
flex: 1 1 0;
|
||||
|
||||
&.mixed {
|
||||
background: var(--color-4-dimgray);
|
||||
&:first-of-type {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -76,7 +85,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.icon-label {
|
||||
margin: 2px;
|
||||
|
||||
+ .text-label {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text-label {
|
||||
margin: 0 8px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.mixed {
|
||||
background: var(--color-4-dimgray);
|
||||
|
||||
button:not(:hover),
|
||||
&.disabled button:hover {
|
||||
background: var(--color-5-dullgray);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-4-dimgray);
|
||||
|
||||
button {
|
||||
background: var(--color-4-dimgray);
|
||||
color: var(--color-8-uppergray);
|
||||
|
||||
|
@ -93,37 +129,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
& + button {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
margin: 0 4px;
|
||||
|
||||
+ .text-label {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text-label {
|
||||
margin: 0 8px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.combined-before button:first-of-type,
|
||||
&.combined-after button:last-of-type {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<FieldInput
|
||||
class={`text-input ${className}`.trim()}
|
||||
classes={{ centered, ...classes }}
|
||||
styles={{ "min-width": minWidth > 0 ? `${minWidth}px` : undefined }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
{value}
|
||||
on:value
|
||||
on:textFocused={onTextFocused}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
export let minWidth = 0;
|
||||
export let multiline = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let checkboxId: bigint | undefined = undefined;
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
|
@ -22,7 +23,7 @@
|
|||
.join(" ");
|
||||
</script>
|
||||
|
||||
<span
|
||||
<label
|
||||
class={`text-label ${className} ${extraClasses}`.trim()}
|
||||
class:disabled
|
||||
class:bold
|
||||
|
@ -30,12 +31,13 @@
|
|||
class:multiline
|
||||
class:center-align={centerAlign}
|
||||
class:table-align={tableAlign}
|
||||
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
|
||||
style:min-width={minWidth > 0 ? `${minWidth}px` : undefined}
|
||||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||
title={tooltip}
|
||||
for={checkboxId !== undefined ? `checkbox-input-${checkboxId}` : undefined}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<style lang="scss" global>
|
||||
.text-label {
|
||||
|
|
|
@ -248,7 +248,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
span {
|
||||
.text-label {
|
||||
flex: 1 1 100%;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -212,7 +212,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
if (textToolInteractiveInputElement) return;
|
||||
|
||||
// Allow only double-clicks
|
||||
if (e.detail !== 2) return;
|
||||
if (e.detail % 2 == 1) return;
|
||||
|
||||
// `e.buttons` is always 0 in the `mouseup` event, so we have to convert from `e.button` instead
|
||||
let buttons = 1;
|
||||
|
|
|
@ -960,6 +960,8 @@ export class CheckboxInput extends WidgetProps {
|
|||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
forLabel!: bigint | undefined;
|
||||
}
|
||||
|
||||
export class ColorInput extends WidgetProps {
|
||||
|
@ -1085,6 +1087,12 @@ export class DropdownInput extends WidgetProps {
|
|||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
// Styling
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
maxWidth!: number;
|
||||
}
|
||||
|
||||
export class FontInput extends WidgetProps {
|
||||
|
@ -1185,6 +1193,8 @@ export class NumberInput extends WidgetProps {
|
|||
// Styling
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
maxWidth!: number;
|
||||
}
|
||||
|
||||
export class NodeCatalog extends WidgetProps {
|
||||
|
@ -1194,6 +1204,8 @@ export class NodeCatalog extends WidgetProps {
|
|||
export class PopoverButton extends WidgetProps {
|
||||
style!: PopoverButtonStyle | undefined;
|
||||
|
||||
menuDirection!: MenuDirection | undefined;
|
||||
|
||||
icon!: IconName | undefined;
|
||||
|
||||
disabled!: boolean;
|
||||
|
@ -1207,6 +1219,8 @@ export class PopoverButton extends WidgetProps {
|
|||
popoverMinWidth: number | undefined;
|
||||
}
|
||||
|
||||
export type MenuDirection = "Top" | "Bottom" | "Left" | "Right" | "TopLeft" | "TopRight" | "BottomLeft" | "BottomRight" | "Center";
|
||||
|
||||
export type RadioEntryData = {
|
||||
value?: string;
|
||||
label?: string;
|
||||
|
@ -1348,6 +1362,8 @@ export class TextLabel extends WidgetProps {
|
|||
|
||||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
checkboxId!: bigint | undefined;
|
||||
}
|
||||
|
||||
export type ReferencePoint = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight";
|
||||
|
@ -1581,7 +1597,11 @@ export class UpdateDocumentBarLayout extends WidgetDiffUpdate {}
|
|||
|
||||
export class UpdateDocumentModeLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdateLayersPanelControlBarLayout extends WidgetDiffUpdate {}
|
||||
export class UpdateLayersPanelControlBarLeftLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdateLayersPanelControlBarRightLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdateLayersPanelBottomBarLayout extends WidgetDiffUpdate {}
|
||||
|
||||
// Extends JsMessage instead of WidgetDiffUpdate because the menu bar isn't diffed
|
||||
export class UpdateMenuBarLayout extends JsMessage {
|
||||
|
@ -1680,7 +1700,9 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateImportsExports,
|
||||
UpdateInputHints,
|
||||
UpdateInSelectedNetwork,
|
||||
UpdateLayersPanelControlBarLayout,
|
||||
UpdateLayersPanelControlBarLeftLayout,
|
||||
UpdateLayersPanelControlBarRightLayout,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
UpdateLayerWidths,
|
||||
UpdateMenuBarLayout,
|
||||
UpdateMouseCursor,
|
||||
|
|
|
@ -10,6 +10,7 @@ const GRAPHICS = {
|
|||
// 12px Solid
|
||||
import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
|
||||
import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
|
||||
import Clipped from "@graphite-frontend/assets/icon-12px-solid/clipped.svg";
|
||||
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
|
||||
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
|
||||
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
|
||||
|
@ -51,6 +52,7 @@ import WorkingColors from "@graphite-frontend/assets/icon-12px-solid/working-col
|
|||
const SOLID_12PX = {
|
||||
Add: { svg: Add, size: 12 },
|
||||
Checkmark: { svg: Checkmark, size: 12 },
|
||||
Clipped: { svg: Clipped, size: 12 },
|
||||
CloseX: { svg: CloseX, size: 12 },
|
||||
Delay: { svg: Delay, size: 12 },
|
||||
DropdownArrow: { svg: DropdownArrow, size: 12 },
|
||||
|
|
|
@ -67,7 +67,7 @@ impl Rect {
|
|||
Self::from_box([self[0] - delta, self[1] + delta])
|
||||
}
|
||||
|
||||
/// Expand a rect by a certain amount on top/bottom and on left/right
|
||||
/// Checks if two rects intersect
|
||||
#[must_use]
|
||||
pub fn intersects(&self, other: Self) -> bool {
|
||||
let [mina, maxa] = [self[0].min(self[1]), self[0].max(self[1])];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::poisson_disk::poisson_disk_sample;
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use glam::DVec2;
|
||||
use kurbo::{Affine, BezPath, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, Rect, Shape};
|
||||
use kurbo::{BezPath, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, Rect, Shape};
|
||||
|
||||
/// Accuracy to find the position on [kurbo::Bezpath].
|
||||
const POSITION_ACCURACY: f64 = 1e-5;
|
||||
|
@ -198,77 +198,46 @@ fn bezpath_t_value_to_parametric(bezpath: &kurbo::BezPath, t: BezPathTValue, pre
|
|||
///
|
||||
/// While the conceptual process described above asymptotically slows down and is never guaranteed to produce a maximal set in finite time,
|
||||
/// this is implemented with an algorithm that produces a maximal set in O(n) time. The slowest part is actually checking if points are inside the subpath shape.
|
||||
pub fn poisson_disk_points(bezpath: &BezPath, separation_disk_diameter: f64, rng: impl FnMut() -> f64, subpaths: &[(BezPath, Rect)], subpath_index: usize) -> Vec<DVec2> {
|
||||
if bezpath.elements().is_empty() {
|
||||
pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], separation_disk_diameter: f64, rng: impl FnMut() -> f64) -> Vec<DVec2> {
|
||||
let (this_bezpath, this_bbox) = bezpaths[bezpath_index].clone();
|
||||
|
||||
if this_bezpath.elements().is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let bbox = bezpath.bounding_box();
|
||||
let (offset_x, offset_y) = (bbox.x0, bbox.y0);
|
||||
let (width, height) = (bbox.x1 - bbox.x0, bbox.y1 - bbox.y0);
|
||||
|
||||
// TODO: Optimize the following code and make it more robust
|
||||
|
||||
let mut shape = bezpath.clone();
|
||||
shape.close_path();
|
||||
shape.apply_affine(Affine::translate((-offset_x, -offset_y)));
|
||||
|
||||
let point_in_shape_checker = |point: DVec2| {
|
||||
// Check against all paths the point is contained in to compute the correct winding number
|
||||
let mut number = 0;
|
||||
for (i, (shape, bbox)) in subpaths.iter().enumerate() {
|
||||
let point = point + DVec2::new(bbox.x0, bbox.y0);
|
||||
|
||||
for (i, (shape, bbox)) in bezpaths.iter().enumerate() {
|
||||
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
|
||||
continue;
|
||||
}
|
||||
let winding = shape.winding(dvec2_to_point(point));
|
||||
|
||||
if i == subpath_index && winding == 0 {
|
||||
let winding = shape.winding(dvec2_to_point(point));
|
||||
if winding == 0 && i == bezpath_index {
|
||||
return false;
|
||||
}
|
||||
number += winding;
|
||||
}
|
||||
|
||||
// Non-zero fill rule
|
||||
number != 0
|
||||
};
|
||||
|
||||
let square_edges_intersect_shape_checker = |position: DVec2, size: f64| {
|
||||
let rect = Rect::new(position.x, position.y, position.x + size, position.y + size);
|
||||
bezpath_rectangle_intersections_exist(bezpath, rect)
|
||||
};
|
||||
|
||||
let mut points = poisson_disk_sample(width, height, separation_disk_diameter, point_in_shape_checker, square_edges_intersect_shape_checker, rng);
|
||||
for point in &mut points {
|
||||
point.x += offset_x;
|
||||
point.y += offset_y;
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
fn bezpath_rectangle_intersections_exist(bezpath: &BezPath, rect: Rect) -> bool {
|
||||
if !bezpath.bounding_box().overlaps(rect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Top left
|
||||
let p1 = Point::new(rect.x0, rect.y0);
|
||||
// Top right
|
||||
let p2 = Point::new(rect.x1, rect.y0);
|
||||
// Bottom right
|
||||
let p3 = Point::new(rect.x1, rect.y1);
|
||||
// Bottom left
|
||||
let p4 = Point::new(rect.x0, rect.y1);
|
||||
|
||||
let top_line = Line::new((p1.x, p1.y), (p2.x, p2.y));
|
||||
let right_line = Line::new((p2.x, p2.y), (p3.x, p3.y));
|
||||
let bottom_line = Line::new((p3.x, p3.y), (p4.x, p4.y));
|
||||
let left_line = Line::new((p4.x, p4.y), (p1.x, p1.y));
|
||||
|
||||
for segment in bezpath.segments() {
|
||||
for line in [top_line, right_line, bottom_line, left_line] {
|
||||
if !segment.intersect_line(line).is_empty() {
|
||||
let line_intersect_shape_checker = |p0: (f64, f64), p1: (f64, f64)| {
|
||||
for segment in this_bezpath.segments() {
|
||||
if !segment.intersect_line(Line::new(p0, p1)).is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
false
|
||||
};
|
||||
|
||||
let offset = DVec2::new(this_bbox.x0, this_bbox.y0);
|
||||
let width = this_bbox.width();
|
||||
let height = this_bbox.height();
|
||||
|
||||
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use core::f64;
|
||||
use glam::DVec2;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8;
|
||||
|
||||
|
@ -8,11 +9,12 @@ const DEEPEST_SUBDIVISION_LEVEL_BEFORE_DISCARDING: usize = 8;
|
|||
/// "Poisson Disk Point Sets by Hierarchical Dart Throwing"
|
||||
/// <https://scholarsarchive.byu.edu/facpub/237/>
|
||||
pub fn poisson_disk_sample(
|
||||
offset: DVec2,
|
||||
width: f64,
|
||||
height: f64,
|
||||
diameter: f64,
|
||||
point_in_shape_checker: impl Fn(DVec2) -> bool,
|
||||
square_edges_intersect_shape_checker: impl Fn(DVec2, f64) -> bool,
|
||||
line_intersect_shape_checker: impl Fn((f64, f64), (f64, f64)) -> bool,
|
||||
rng: impl FnMut() -> f64,
|
||||
) -> Vec<DVec2> {
|
||||
let mut rng = rng;
|
||||
|
@ -28,7 +30,7 @@ pub fn poisson_disk_sample(
|
|||
let base_level_grid_size = greater_dimension / (greater_dimension * std::f64::consts::SQRT_2 / (diameter / 2.)).ceil();
|
||||
|
||||
// Initialize the problem by including all base-level squares in the active list since they're all part of the yet-to-be-targetted dartboard domain
|
||||
let base_level = ActiveListLevel::new_filled(base_level_grid_size, width, height, &point_in_shape_checker, &square_edges_intersect_shape_checker);
|
||||
let base_level = ActiveListLevel::new_filled(base_level_grid_size, offset, width, height, &point_in_shape_checker, &line_intersect_shape_checker);
|
||||
// In the future, if necessary, this could be turned into a fixed-length array with worst-case length `f64::MANTISSA_DIGITS`
|
||||
let mut active_list_levels = vec![base_level];
|
||||
|
||||
|
@ -60,7 +62,7 @@ pub fn poisson_disk_sample(
|
|||
// If the dart hit a valid spot, save that point (we're now permanently done with this target square's region)
|
||||
if point_not_covered_by_poisson_points(point, diameter_squared, &points_grid) {
|
||||
// Silently reject the point if it lies outside the shape
|
||||
if active_square.fully_in_shape() || point_in_shape_checker(point) {
|
||||
if active_square.fully_in_shape() || point_in_shape_checker(point + offset) {
|
||||
points_grid.insert(point);
|
||||
}
|
||||
}
|
||||
|
@ -105,10 +107,21 @@ pub fn poisson_disk_sample(
|
|||
// Intersecting the shape's border
|
||||
else {
|
||||
// The sub-square is fully inside the shape if its top-left corner is inside and its edges don't intersect the shape border
|
||||
let sub_square_fully_inside_shape =
|
||||
!square_edges_intersect_shape_checker(sub_square, subdivided_size) && point_in_shape_checker(sub_square) && point_in_shape_checker(sub_square + subdivided_size);
|
||||
// if !square_edges_intersect_shape_checker(sub_square, subdivided_size) { assert_eq!(point_in_shape_checker(sub_square), point_in_shape_checker(sub_square + subdivided_size)); }
|
||||
// Sometimes this fails so it is necessary to also check the bottom right corner.
|
||||
let point_with_offset = sub_square + offset;
|
||||
let square_edges_intersect_shape = {
|
||||
let min = point_with_offset;
|
||||
let max = min + DVec2::splat(subdivided_size);
|
||||
|
||||
// Top edge line
|
||||
line_intersect_shape_checker((min.x, min.y), (max.x, min.y)) ||
|
||||
// Right edge line
|
||||
line_intersect_shape_checker((max.x, min.y), (max.x, max.y)) ||
|
||||
// Bottom edge line
|
||||
line_intersect_shape_checker((max.x, max.y), (min.x, max.y)) ||
|
||||
// Left edge line
|
||||
line_intersect_shape_checker((min.x, max.y), (min.x, min.y))
|
||||
};
|
||||
let sub_square_fully_inside_shape = !square_edges_intersect_shape && point_in_shape_checker(point_with_offset) && point_in_shape_checker(point_with_offset + subdivided_size);
|
||||
|
||||
Some(ActiveSquare::new(sub_square, sub_square_fully_inside_shape))
|
||||
}
|
||||
|
@ -117,7 +130,7 @@ pub fn poisson_disk_sample(
|
|||
}
|
||||
}
|
||||
|
||||
points_grid.final_points()
|
||||
points_grid.final_points(offset)
|
||||
}
|
||||
|
||||
/// Randomly pick a square in the dartboard domain, with probability proportional to its area.
|
||||
|
@ -209,24 +222,64 @@ impl ActiveListLevel {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn new_filled(square_size: f64, width: f64, height: f64, point_in_shape_checker: impl Fn(DVec2) -> bool, square_edges_intersect_shape_checker: impl Fn(DVec2, f64) -> bool) -> Self {
|
||||
pub fn new_filled(
|
||||
square_size: f64,
|
||||
offset: DVec2,
|
||||
width: f64,
|
||||
height: f64,
|
||||
point_in_shape_checker: impl Fn(DVec2) -> bool,
|
||||
line_intersect_shape_checker: impl Fn((f64, f64), (f64, f64)) -> bool,
|
||||
) -> Self {
|
||||
// These should divide evenly but rounding is to protect against small numerical imprecision errors
|
||||
let x_squares = (width / square_size).round() as usize;
|
||||
let y_squares = (height / square_size).round() as usize;
|
||||
|
||||
// Hashes based on the grid cell coordinates and direction of the line: (x, y, is_vertical)
|
||||
let mut line_intersection_cache: HashMap<(usize, usize, bool), bool> = HashMap::new();
|
||||
|
||||
// Populate each square with its top-left corner coordinate
|
||||
let active_squares: Vec<_> = cartesian_product(0..x_squares, 0..y_squares)
|
||||
.filter_map(|(x, y)| {
|
||||
let corner = (x as f64 * square_size, y as f64 * square_size).into();
|
||||
let corner = DVec2::new(x as f64 * square_size, y as f64 * square_size);
|
||||
let corner_with_offset = corner + offset;
|
||||
|
||||
let point_in_shape = point_in_shape_checker(corner);
|
||||
let square_edges_intersect_shape = square_edges_intersect_shape_checker(corner, square_size);
|
||||
let square_not_outside_shape = point_in_shape || square_edges_intersect_shape;
|
||||
let square_in_shape = point_in_shape_checker(corner + square_size) && !square_edges_intersect_shape;
|
||||
// if !square_edges_intersect_shape { assert_eq!(point_in_shape_checker(corner), point_in_shape_checker(corner + square_size)); }
|
||||
// Sometimes this fails so it is necessary to also check the bottom right corner.
|
||||
square_not_outside_shape.then_some(ActiveSquare::new(corner, square_in_shape))
|
||||
// Lazily check (and cache) if the square's edges intersect the shape, which is an expensive operation
|
||||
let mut square_edges_intersect_shape_value = None;
|
||||
let mut square_edges_intersect_shape = || {
|
||||
square_edges_intersect_shape_value.unwrap_or_else(|| {
|
||||
let square_edges_intersect_shape = {
|
||||
let min = corner_with_offset;
|
||||
let max = min + DVec2::splat(square_size);
|
||||
|
||||
// Top edge line
|
||||
*line_intersection_cache.entry((x, y, false)).or_insert_with(|| line_intersect_shape_checker((min.x, min.y), (max.x, min.y))) ||
|
||||
// Right edge line
|
||||
*line_intersection_cache.entry((x + 1, y, true)).or_insert_with(|| line_intersect_shape_checker((max.x, min.y), (max.x, max.y))) ||
|
||||
// Bottom edge line
|
||||
*line_intersection_cache.entry((x, y + 1, false)).or_insert_with(|| line_intersect_shape_checker((max.x, max.y), (min.x, max.y))) ||
|
||||
// Left edge line
|
||||
*line_intersection_cache.entry((x, y, true)).or_insert_with(|| line_intersect_shape_checker((min.x, max.y), (min.x, min.y)))
|
||||
};
|
||||
square_edges_intersect_shape_value = Some(square_edges_intersect_shape);
|
||||
square_edges_intersect_shape
|
||||
})
|
||||
};
|
||||
|
||||
// Check if this cell's top-left corner is inside the shape
|
||||
let point_in_shape = point_in_shape_checker(corner_with_offset);
|
||||
|
||||
// Determine if the square is inside the shape
|
||||
let square_not_outside_shape = point_in_shape || square_edges_intersect_shape();
|
||||
if square_not_outside_shape {
|
||||
// Check if this cell's bottom-right corner is inside the shape
|
||||
let opposite_corner_with_offset = DVec2::new((x + 1) as f64 * square_size, (y + 1) as f64 * square_size) + offset;
|
||||
let opposite_corner_in_shape = point_in_shape_checker(opposite_corner_with_offset);
|
||||
|
||||
let square_in_shape = opposite_corner_in_shape && !square_edges_intersect_shape();
|
||||
Some(ActiveSquare::new(corner, square_in_shape))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -363,7 +416,7 @@ impl AccelerationGrid {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn final_points(&self) -> Vec<DVec2> {
|
||||
self.cells.iter().flat_map(|cell| cell.list_cell()).collect()
|
||||
pub fn final_points(&self, offset: DVec2) -> Vec<DVec2> {
|
||||
self.cells.iter().flat_map(|cell| cell.list_cell()).map(|point| point + offset).collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ mod attributes;
|
|||
mod indexed;
|
||||
mod modification;
|
||||
|
||||
use super::misc::point_to_dvec2;
|
||||
use super::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use super::style::{PathStyle, Stroke};
|
||||
use crate::instances::Instances;
|
||||
use crate::{AlphaBlending, Color, GraphicGroupTable};
|
||||
|
@ -12,6 +12,7 @@ use core::borrow::Borrow;
|
|||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
pub use indexed::VectorDataIndex;
|
||||
use kurbo::{Affine, Shape};
|
||||
pub use modification::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -193,6 +194,23 @@ impl VectorData {
|
|||
vector_data
|
||||
}
|
||||
|
||||
pub fn close_subpaths(&mut self) {
|
||||
let segments_to_add: Vec<_> = self
|
||||
.stroke_bezier_paths()
|
||||
.filter(|subpath| !subpath.closed)
|
||||
.filter_map(|subpath| {
|
||||
let (first, last) = subpath.manipulator_groups().first().zip(subpath.manipulator_groups().last())?;
|
||||
let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?;
|
||||
Some((start, end))
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (start, end) in segments_to_add {
|
||||
let segment_id = self.segment_domain.next_id().next_id();
|
||||
self.segment_domain.push(segment_id, start, end, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the subpaths without any transform
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box_with_transform(DAffine2::IDENTITY)
|
||||
|
@ -316,6 +334,34 @@ impl VectorData {
|
|||
self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point))
|
||||
}
|
||||
|
||||
pub fn check_point_inside_shape(&self, vector_data_transform: DAffine2, point: DVec2) -> bool {
|
||||
let bez_paths: Vec<_> = self
|
||||
.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
// TODO: apply transform to points instead of modifying the paths
|
||||
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
|
||||
bezpath.close_path();
|
||||
let bbox = bezpath.bounding_box();
|
||||
(bezpath, bbox)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Check against all paths the point is contained in to compute the correct winding number
|
||||
let mut number = 0;
|
||||
|
||||
for (shape, bbox) in bez_paths {
|
||||
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
|
||||
continue;
|
||||
}
|
||||
|
||||
let winding = shape.winding(dvec2_to_point(point));
|
||||
number += winding;
|
||||
}
|
||||
|
||||
// Non-zero fill rule
|
||||
number != 0
|
||||
}
|
||||
|
||||
/// Points that can be extended from.
|
||||
///
|
||||
/// This is usually only points with exactly one connection unless vector meshes are enabled.
|
||||
|
|
|
@ -560,9 +560,9 @@ async fn spatial_merge_by_distance(
|
|||
) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for vector_data in vector_data.instance_ref_iter() {
|
||||
let vector_data_transform = *vector_data.transform;
|
||||
let vector_data = vector_data.instance;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let vector_data_transform = vector_data_instance.transform;
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let point_count = vector_data.point_domain.positions().len();
|
||||
|
||||
|
@ -676,12 +676,9 @@ async fn spatial_merge_by_distance(
|
|||
result.segment_domain = new_segment_domain;
|
||||
|
||||
// Create and return the result
|
||||
result_table.push(Instance {
|
||||
instance: result,
|
||||
transform: vector_data_transform,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
});
|
||||
vector_data_instance.instance = result;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -695,9 +692,9 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
|||
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for vector_data in vector_data.instance_ref_iter() {
|
||||
let vector_data_transform = *vector_data.transform;
|
||||
let vector_data = vector_data.instance;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let vector_data_transform = vector_data_instance.transform;
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
// Get the bounding box of the source vector data
|
||||
let source_bbox = vector_data.bounding_box_with_transform(vector_data_transform).unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||
|
@ -755,12 +752,10 @@ async fn box_warp(_: impl Ctx, vector_data: VectorDataTable, #[expose] rectangle
|
|||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Add this to the table and reset the transform since we've applied it directly to the points
|
||||
result_table.push(Instance {
|
||||
instance: result,
|
||||
transform: DAffine2::IDENTITY,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
});
|
||||
vector_data_instance.instance = result;
|
||||
vector_data_instance.transform = DAffine2::IDENTITY;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -787,9 +782,8 @@ async fn remove_handles(
|
|||
) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for vector_data in vector_data.instance_iter() {
|
||||
let vector_data_transform = vector_data.transform;
|
||||
let mut vector_data = vector_data.instance;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let mut vector_data = vector_data_instance.instance;
|
||||
|
||||
for (_, handles, start, end) in vector_data.segment_domain.handles_mut() {
|
||||
// Only convert to linear if handles are within the threshold distance
|
||||
|
@ -821,12 +815,9 @@ async fn remove_handles(
|
|||
}
|
||||
}
|
||||
|
||||
result_table.push(Instance {
|
||||
instance: vector_data,
|
||||
transform: vector_data_transform,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
});
|
||||
vector_data_instance.instance = vector_data;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -1039,9 +1030,8 @@ async fn generate_handles(
|
|||
async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for vector_data in vector_data.instance_ref_iter() {
|
||||
let vector_data_transform = *vector_data.transform;
|
||||
let vector_data = vector_data.instance;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let mut result = vector_data
|
||||
.bounding_box()
|
||||
|
@ -1050,12 +1040,9 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
|
|||
result.style = vector_data.style.clone();
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
result_table.push(Instance {
|
||||
instance: result,
|
||||
transform: vector_data_transform,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
});
|
||||
vector_data_instance.instance = result;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -1075,9 +1062,9 @@ async fn dimensions(_: impl Ctx, vector_data: VectorDataTable) -> DVec2 {
|
|||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for vector_data in vector_data.instance_ref_iter() {
|
||||
let vector_data_transform = *vector_data.transform;
|
||||
let vector_data = vector_data.instance;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let vector_data_transform = vector_data_instance.transform;
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let subpaths = vector_data.stroke_bezier_paths();
|
||||
let mut result = VectorData::empty();
|
||||
|
@ -1105,12 +1092,9 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
|
|||
result.append_subpath(subpath_out, false);
|
||||
}
|
||||
|
||||
result_table.push(Instance {
|
||||
instance: result,
|
||||
transform: vector_data_transform,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
});
|
||||
vector_data_instance.instance = result;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -1120,9 +1104,8 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
|
|||
async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for vector_data in vector_data.instance_ref_iter() {
|
||||
let vector_data_transform = *vector_data.transform;
|
||||
let vector_data = vector_data.instance;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let stroke = vector_data.style.stroke().clone().unwrap_or_default();
|
||||
let bezpaths = vector_data.stroke_bezpath_iter();
|
||||
|
@ -1165,12 +1148,9 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
|
|||
result.style.set_stroke(Stroke::default());
|
||||
}
|
||||
|
||||
result_table.push(Instance {
|
||||
instance: result,
|
||||
transform: vector_data_transform,
|
||||
alpha_blending: Default::default(),
|
||||
source_node_id: None,
|
||||
});
|
||||
vector_data_instance.instance = result;
|
||||
vector_data_instance.source_node_id = None;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
result_table
|
||||
|
@ -1227,48 +1207,58 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
|
|||
// Limit the smallest spacing to something sensible to avoid freezing the application.
|
||||
let spacing = spacing.max(0.01);
|
||||
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
// Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath.
|
||||
// So we can index into `subpath_segment_lengths` to get the length of the segments.
|
||||
// NOTE: `subpath_segment_lengths` has precalulated lengths with transformation applied.
|
||||
let bezpaths = vector_data.one_instance_ref().instance.stroke_bezpath_iter();
|
||||
|
||||
// Initialize the result VectorData with the same transformation as the input.
|
||||
let mut result = VectorDataTable::default();
|
||||
*result.transform_mut() = vector_data_transform;
|
||||
|
||||
// Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments.
|
||||
let mut next_segment_index = 0;
|
||||
|
||||
for mut bezpath in bezpaths {
|
||||
// Apply the tranformation to the current bezpath to calculate points after transformation.
|
||||
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
|
||||
|
||||
let segment_count = bezpath.segments().count();
|
||||
|
||||
// For the current bezpath we get its segment's length by calculating the start index and end index.
|
||||
let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count];
|
||||
|
||||
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
|
||||
next_segment_index += segment_count;
|
||||
|
||||
let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
|
||||
continue;
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let mut result = VectorData {
|
||||
point_domain: Default::default(),
|
||||
segment_domain: Default::default(),
|
||||
region_domain: Default::default(),
|
||||
colinear_manipulators: Default::default(),
|
||||
style: std::mem::take(&mut vector_data_instance.instance.style),
|
||||
upstream_graphic_group: std::mem::take(&mut vector_data_instance.instance.upstream_graphic_group),
|
||||
};
|
||||
|
||||
// Reverse the transformation applied to the bezpath as the `result` already has the transformation set.
|
||||
sample_bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()).inverse());
|
||||
// Using `stroke_bezpath_iter` so that the `subpath_segment_lengths` is aligned to the segments of each bezpath.
|
||||
// So we can index into `subpath_segment_lengths` to get the length of the segments.
|
||||
// NOTE: `subpath_segment_lengths` has precalulated lengths with transformation applied.
|
||||
let bezpaths = vector_data_instance.instance.stroke_bezpath_iter();
|
||||
|
||||
// Append the bezpath (subpath) that connects generated points by lines.
|
||||
result.one_instance_mut().instance.append_bezpath(sample_bezpath);
|
||||
// Keeps track of the index of the first segment of the next bezpath in order to get lengths of all segments.
|
||||
let mut next_segment_index = 0;
|
||||
|
||||
for mut bezpath in bezpaths {
|
||||
// Apply the tranformation to the current bezpath to calculate points after transformation.
|
||||
bezpath.apply_affine(Affine::new(vector_data_instance.transform.to_cols_array()));
|
||||
|
||||
let segment_count = bezpath.segments().count();
|
||||
|
||||
// For the current bezpath we get its segment's length by calculating the start index and end index.
|
||||
let current_bezpath_segments_length = &subpath_segment_lengths[next_segment_index..next_segment_index + segment_count];
|
||||
|
||||
// Increment the segment index by the number of segments in the current bezpath to calculate the next bezpath segment's length.
|
||||
next_segment_index += segment_count;
|
||||
|
||||
let Some(mut sample_bezpath) = sample_points_on_bezpath(bezpath, spacing, start_offset, stop_offset, adaptive_spacing, current_bezpath_segments_length) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Reverse the transformation applied to the bezpath as the `result` already has the transformation set.
|
||||
sample_bezpath.apply_affine(Affine::new(vector_data_instance.transform.to_cols_array()).inverse());
|
||||
|
||||
// Append the bezpath (subpath) that connects generated points by lines.
|
||||
result.append_bezpath(sample_bezpath);
|
||||
}
|
||||
|
||||
// Transfer the style from the input vector data to the result.
|
||||
result.style = vector_data_instance.instance.style;
|
||||
result.style.set_stroke_transform(vector_data_instance.transform);
|
||||
|
||||
vector_data_instance.instance = result;
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
// Transfer the style from the input vector data to the result.
|
||||
result.one_instance_mut().instance.style = vector_data.one_instance_ref().instance.style.clone();
|
||||
result.one_instance_mut().instance.style.set_stroke_transform(vector_data_transform);
|
||||
|
||||
// Return the resulting vector data with newly generated points and segments.
|
||||
result
|
||||
result_table
|
||||
}
|
||||
|
||||
/// Determines the position of a point on the path, given by its progress from 0 to 1 along the path.
|
||||
|
@ -1287,18 +1277,21 @@ async fn position_on_path(
|
|||
) -> DVec2 {
|
||||
let euclidian = !parameterized_distance;
|
||||
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
let mut bezpaths = vector_data.stroke_bezpath_iter().collect::<Vec<kurbo::BezPath>>();
|
||||
let mut bezpaths = vector_data
|
||||
.instance_iter()
|
||||
.flat_map(|vector_data| {
|
||||
let transform = vector_data.transform;
|
||||
vector_data.instance.stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let bezpath_count = bezpaths.len() as f64;
|
||||
let progress = progress.clamp(0., bezpath_count);
|
||||
let progress = if reverse { bezpath_count - progress } else { progress };
|
||||
let index = if progress >= bezpath_count { (bezpath_count - 1.) as usize } else { progress as usize };
|
||||
|
||||
bezpaths.get_mut(index).map_or(DVec2::ZERO, |bezpath| {
|
||||
bezpaths.get_mut(index).map_or(DVec2::ZERO, |(bezpath, transform)| {
|
||||
let t = if progress == bezpath_count { 1. } else { progress.fract() };
|
||||
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
point_to_dvec2(position_on_bezpath(bezpath, t, euclidian, None))
|
||||
})
|
||||
|
@ -1320,18 +1313,21 @@ async fn tangent_on_path(
|
|||
) -> f64 {
|
||||
let euclidian = !parameterized_distance;
|
||||
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
let mut bezpaths = vector_data.stroke_bezpath_iter().collect::<Vec<kurbo::BezPath>>();
|
||||
let mut bezpaths = vector_data
|
||||
.instance_iter()
|
||||
.flat_map(|vector_data| {
|
||||
let transform = vector_data.transform;
|
||||
vector_data.instance.stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let bezpath_count = bezpaths.len() as f64;
|
||||
let progress = progress.clamp(0., bezpath_count);
|
||||
let progress = if reverse { bezpath_count - progress } else { progress };
|
||||
let index = if progress >= bezpath_count { (bezpath_count - 1.) as usize } else { progress as usize };
|
||||
|
||||
bezpaths.get_mut(index).map_or(0., |bezpath| {
|
||||
bezpaths.get_mut(index).map_or(0., |(bezpath, transform)| {
|
||||
let t = if progress == bezpath_count { 1. } else { progress.fract() };
|
||||
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
let mut tangent = point_to_dvec2(tangent_on_bezpath(bezpath, t, euclidian, None));
|
||||
if tangent == DVec2::ZERO {
|
||||
|
@ -1355,59 +1351,68 @@ async fn poisson_disk_points(
|
|||
separation_disk_diameter: f64,
|
||||
seed: SeedValue,
|
||||
) -> VectorDataTable {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
||||
let mut result = VectorData::empty();
|
||||
|
||||
if separation_disk_diameter <= 0.01 {
|
||||
return VectorDataTable::new(result);
|
||||
}
|
||||
let path_with_bounding_boxes: Vec<_> = vector_data
|
||||
.stroke_bezpath_iter()
|
||||
.map(|mut subpath| {
|
||||
// TODO: apply transform to points instead of modifying the paths
|
||||
subpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
|
||||
let bbox = subpath.bounding_box();
|
||||
(subpath, bbox)
|
||||
})
|
||||
.collect();
|
||||
let mut result_table = VectorDataTable::empty();
|
||||
|
||||
for (i, (subpath, _)) in path_with_bounding_boxes.iter().enumerate() {
|
||||
if subpath.segments().count() < 2 {
|
||||
continue;
|
||||
}
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
let mut result = VectorData::empty();
|
||||
|
||||
let mut poisson_disk_bezpath = BezPath::new();
|
||||
let path_with_bounding_boxes: Vec<_> = vector_data_instance
|
||||
.instance
|
||||
.stroke_bezpath_iter()
|
||||
.map(|mut bezpath| {
|
||||
// TODO: apply transform to points instead of modifying the paths
|
||||
bezpath.apply_affine(Affine::new(vector_data_instance.transform.to_cols_array()));
|
||||
bezpath.close_path();
|
||||
let bbox = bezpath.bounding_box();
|
||||
(bezpath, bbox)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for point in bezpath_algorithms::poisson_disk_points(subpath, separation_disk_diameter, || rng.random::<f64>(), &path_with_bounding_boxes, i) {
|
||||
if poisson_disk_bezpath.elements().is_empty() {
|
||||
poisson_disk_bezpath.move_to(dvec2_to_point(point));
|
||||
} else {
|
||||
poisson_disk_bezpath.line_to(dvec2_to_point(point));
|
||||
for (i, (subpath, _)) in path_with_bounding_boxes.iter().enumerate() {
|
||||
if subpath.segments().count() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut poisson_disk_bezpath = BezPath::new();
|
||||
|
||||
for point in bezpath_algorithms::poisson_disk_points(i, &path_with_bounding_boxes, separation_disk_diameter, || rng.random::<f64>()) {
|
||||
if poisson_disk_bezpath.elements().is_empty() {
|
||||
poisson_disk_bezpath.move_to(dvec2_to_point(point));
|
||||
} else {
|
||||
poisson_disk_bezpath.line_to(dvec2_to_point(point));
|
||||
}
|
||||
}
|
||||
result.append_bezpath(poisson_disk_bezpath);
|
||||
}
|
||||
result.append_bezpath(poisson_disk_bezpath);
|
||||
|
||||
// Transfer the style from the input vector data to the result.
|
||||
result.style = vector_data_instance.instance.style.clone();
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
vector_data_instance.instance = result;
|
||||
|
||||
result_table.push(vector_data_instance);
|
||||
}
|
||||
|
||||
// Transfer the style from the input vector data to the result.
|
||||
result.style = vector_data.style.clone();
|
||||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
VectorDataTable::new(result)
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""), path(graphene_core::vector))]
|
||||
async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> Vec<f64> {
|
||||
let vector_data_transform = vector_data.transform();
|
||||
let vector_data = vector_data.one_instance_ref().instance;
|
||||
|
||||
vector_data
|
||||
.stroke_bezpath_iter()
|
||||
.flat_map(|mut bezpath| {
|
||||
bezpath.apply_affine(Affine::new(vector_data_transform.to_cols_array()));
|
||||
bezpath.segments().map(|segment| segment.perimeter(PERIMETER_ACCURACY)).collect::<Vec<f64>>()
|
||||
.instance_iter()
|
||||
.flat_map(|vector_data| {
|
||||
let transform = vector_data.transform;
|
||||
vector_data
|
||||
.instance
|
||||
.stroke_bezpath_iter()
|
||||
.flat_map(|mut bezpath| {
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
bezpath.segments().map(|segment| segment.perimeter(PERIMETER_ACCURACY)).collect::<Vec<f64>>()
|
||||
})
|
||||
.collect::<Vec<f64>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -1710,6 +1715,23 @@ fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length)
|
|||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
fn close_path(_: impl Ctx, source: VectorDataTable) -> VectorDataTable {
|
||||
let mut new_table = VectorDataTable::empty();
|
||||
|
||||
for mut source_instance in source.instance_iter() {
|
||||
source_instance.instance.close_subpaths();
|
||||
new_table.push(source_instance);
|
||||
}
|
||||
|
||||
new_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
fn point_inside(_: impl Ctx, source: VectorDataTable, point: DVec2) -> bool {
|
||||
source.instance_iter().any(|instance| instance.instance.check_point_inside_shape(instance.transform, point))
|
||||
}
|
||||
|
||||
#[node_macro::node(name("Merge by Distance"), category("Vector"), path(graphene_core::vector))]
|
||||
fn merge_by_distance(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
|
||||
let source_transform = source.transform();
|
||||
|
|
|
@ -43,7 +43,7 @@ async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<W
|
|||
// image: ImageFrameTable<graphene_core::raster::SRGBA8>,
|
||||
// surface_handle: Arc<WasmSurfaceHandle>,
|
||||
// ) -> graphene_core::application_io::SurfaceHandleFrame<HtmlCanvasElement> {
|
||||
// let image = image.one_instance().instance;
|
||||
// let image = image.one_instance_ref().instance;
|
||||
// let image_data = image.image.data;
|
||||
// let array: Clamped<&[u8]> = Clamped(bytemuck::cast_slice(image_data.as_slice()));
|
||||
// if image.image.width > 0 && image.image.height > 0 {
|
||||
|
|
|
@ -44,6 +44,7 @@ in
|
|||
cargo-watch
|
||||
cargo-nextest
|
||||
cargo-expand
|
||||
cargo-about
|
||||
wasm-pack
|
||||
binaryen
|
||||
wasm-bindgen-cli
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue