mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 22:38:13 +08:00
merge
This commit is contained in:
+11
-25
@@ -13,10 +13,7 @@ license = "Apache-2.0"
|
||||
[features]
|
||||
default = ["wasm"]
|
||||
wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"]
|
||||
gpu = [
|
||||
"interpreted-executor/gpu",
|
||||
"wgpu-executor",
|
||||
]
|
||||
gpu = ["interpreted-executor/gpu", "wgpu-executor"]
|
||||
tauri = ["ron", "decouple-execution"]
|
||||
decouple-execution = []
|
||||
resvg = ["graphene-std/resvg"]
|
||||
@@ -25,12 +22,11 @@ ron = ["dep:ron"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
graphite-proc-macros = { path = "../proc-macros" }
|
||||
graph-craft = { path = "../node-graph/graph-craft" }
|
||||
interpreted-executor = { path = "../node-graph/interpreted-executor", features = [
|
||||
"serde",
|
||||
] }
|
||||
graphene-std = { path = "../node-graph/gstd", features = ["serde"] }
|
||||
graphite-proc-macros = { workspace = true }
|
||||
graph-craft = { workspace = true }
|
||||
interpreted-executor = { workspace = true }
|
||||
graphene-std = { workspace = true }
|
||||
preprocessor = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
js-sys = { workspace = true }
|
||||
@@ -40,32 +36,22 @@ thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
bezier-rs = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
glam = { workspace = true, features = ["serde", "debug-glam-assert"] }
|
||||
glam = { workspace = true }
|
||||
derivative = { workspace = true }
|
||||
specta = { workspace = true }
|
||||
image = { workspace = true, features = ["bmp", "png"] }
|
||||
dyn-any = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
usvg = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
web-sys = { workspace = true, features = [
|
||||
"Document",
|
||||
"DomRect",
|
||||
"Element",
|
||||
"HtmlCanvasElement",
|
||||
"CanvasRenderingContext2d",
|
||||
"CanvasPattern",
|
||||
"OffscreenCanvas",
|
||||
"OffscreenCanvasRenderingContext2d",
|
||||
"TextMetrics",
|
||||
] }
|
||||
web-sys = { workspace = true }
|
||||
|
||||
# Required dependencies
|
||||
spin = "0.9.8"
|
||||
|
||||
# Optional local dependencies
|
||||
wgpu-executor = { path = "../node-graph/wgpu-executor", optional = true }
|
||||
wgpu-executor = { workspace = true, optional = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
@@ -76,7 +62,7 @@ ron = { workspace = true, optional = true }
|
||||
# Workspace dependencies
|
||||
env_logger = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros"] }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
# TODO: figure out why we check these features when they do not exist
|
||||
|
||||
@@ -55,53 +55,3 @@ pub fn commit_info_localized(localized_commit_date: &str) -> String {
|
||||
localized_commit_date
|
||||
)
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds;
|
||||
// use crate::messages::prelude::*;
|
||||
|
||||
// // TODO: Fix and reenable
|
||||
// #[ignore]
|
||||
// #[test]
|
||||
// fn debug_ub() {
|
||||
// use super::Message;
|
||||
|
||||
// let mut editor = super::Editor::new();
|
||||
// let mut responses = Vec::new();
|
||||
|
||||
// let messages: Vec<Message> = vec![
|
||||
// Message::Init,
|
||||
// Message::Preferences(PreferencesMessage::Load {
|
||||
// preferences: r#"{ "imaginate_server_hostname": "http://localhost:7860/", "imaginate_refresh_frequency": 1, "zoom_with_scroll": false }"#.to_string(),
|
||||
// }),
|
||||
// PortfolioMessage::OpenDocumentFileWithId {
|
||||
// document_id: DocumentId(0),
|
||||
// document_name: "".into(),
|
||||
// document_is_auto_saved: true,
|
||||
// document_is_saved: true,
|
||||
// document_serialized_content: r#" [removed until test is reenabled] "#.into(),
|
||||
// to_front: false,
|
||||
// }
|
||||
// .into(),
|
||||
// InputPreprocessorMessage::BoundsOfViewports {
|
||||
// bounds_of_viewports: vec![ViewportBounds::from_slice(&[0., 0., 1920., 1080.])],
|
||||
// }
|
||||
// .into(),
|
||||
// ];
|
||||
|
||||
// use futures::executor::block_on;
|
||||
// for message in messages {
|
||||
// block_on(crate::node_graph_executor::run_node_graph());
|
||||
// let mut res = VecDeque::new();
|
||||
// editor.poll_node_graph_evaluation(&mut res).expect("poll_node_graph_evaluation failed");
|
||||
|
||||
// let res = editor.handle_message(message);
|
||||
// responses.push(res);
|
||||
// }
|
||||
// let responses = responses.pop().unwrap();
|
||||
// // let trigger_message = responses[responses.len() - 2].clone();
|
||||
|
||||
// println!("responses: {responses:#?}");
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -102,7 +102,7 @@ pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.;
|
||||
pub const SELECTION_THRESHOLD: f64 = 10.;
|
||||
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
|
||||
pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const SEGMENT_INSERTION_DISTANCE: f64 = 8.;
|
||||
pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
|
||||
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
|
||||
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
|
||||
|
||||
@@ -119,6 +119,13 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
||||
|
||||
// GIZMOS
|
||||
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
|
||||
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
|
||||
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
|
||||
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
|
||||
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
|
||||
|
||||
// SCROLLBARS
|
||||
pub const SCROLLBAR_SPACING: f64 = 0.1;
|
||||
pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
|
||||
@@ -126,6 +133,7 @@ pub const SCALE_EFFECT: f64 = 0.5;
|
||||
|
||||
// COLORS
|
||||
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
||||
pub const COLOR_OVERLAY_BLUE_50: &str = "rgba(0, 168, 255, 0.5)";
|
||||
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
|
||||
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
|
||||
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
|
||||
|
||||
@@ -204,27 +204,6 @@ impl PreferencesDialogMessageHandler {
|
||||
.widget_holder(),
|
||||
];
|
||||
|
||||
// TODO: Reenable when Imaginate is restored
|
||||
// let imaginate_server_hostname = vec![
|
||||
// TextLabel::new("Imaginate").min_width(60).italic(true).widget_holder(),
|
||||
// TextLabel::new("Server Hostname").table_align(true).widget_holder(),
|
||||
// TextInput::new(&preferences.imaginate_server_hostname)
|
||||
// .min_width(200)
|
||||
// .on_update(|text_input: &TextInput| PreferencesMessage::ImaginateServerHostname { hostname: text_input.value.clone() }.into())
|
||||
// .widget_holder(),
|
||||
// ];
|
||||
// let imaginate_refresh_frequency = vec![
|
||||
// TextLabel::new("").min_width(60).widget_holder(),
|
||||
// TextLabel::new("Refresh Frequency").table_align(true).widget_holder(),
|
||||
// NumberInput::new(Some(preferences.imaginate_refresh_frequency))
|
||||
// .unit(" seconds")
|
||||
// .min(0.)
|
||||
// .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
// .min_width(200)
|
||||
// .on_update(|number_input: &NumberInput| PreferencesMessage::ImaginateRefreshFrequency { seconds: number_input.value.unwrap() }.into())
|
||||
// .widget_holder(),
|
||||
// ];
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![
|
||||
LayoutGroup::Row { widgets: navigation_header },
|
||||
LayoutGroup::Row { widgets: zoom_rate_label },
|
||||
@@ -238,8 +217,6 @@ impl PreferencesDialogMessageHandler {
|
||||
LayoutGroup::Row { widgets: graph_wire_style },
|
||||
LayoutGroup::Row { widgets: use_vello },
|
||||
LayoutGroup::Row { widgets: vector_meshes },
|
||||
// LayoutGroup::Row { widgets: imaginate_server_hostname },
|
||||
// LayoutGroup::Row { widgets: imaginate_refresh_frequency },
|
||||
]))
|
||||
}
|
||||
|
||||
|
||||
@@ -99,19 +99,6 @@ pub enum FrontendMessage {
|
||||
#[serde(rename = "copyText")]
|
||||
copy_text: String,
|
||||
},
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
TriggerUpgradeDocumentToVectorManipulationFormat {
|
||||
#[serde(rename = "documentId")]
|
||||
document_id: DocumentId,
|
||||
#[serde(rename = "documentName")]
|
||||
document_name: String,
|
||||
#[serde(rename = "documentIsAutoSaved")]
|
||||
document_is_auto_saved: bool,
|
||||
#[serde(rename = "documentIsSaved")]
|
||||
document_is_saved: bool,
|
||||
#[serde(rename = "documentSerializedContent")]
|
||||
document_serialized_content: String,
|
||||
},
|
||||
TriggerVisitLink {
|
||||
url: String,
|
||||
},
|
||||
|
||||
@@ -171,40 +171,40 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(MouseRight); action_dispatch=GradientToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=GradientToolMessage::Abort),
|
||||
//
|
||||
// RectangleToolMessage
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=RectangleToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=RectangleToolMessage::DragStop),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=RectangleToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=RectangleToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// ImaginateToolMessage
|
||||
// entry!(KeyDown(MouseLeft); action_dispatch=ImaginateToolMessage::DragStart),
|
||||
// entry!(KeyUp(MouseLeft); action_dispatch=ImaginateToolMessage::DragStop),
|
||||
// entry!(KeyDown(MouseRight); action_dispatch=ImaginateToolMessage::Abort),
|
||||
// entry!(KeyDown(Escape); action_dispatch=ImaginateToolMessage::Abort),
|
||||
// entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=ImaginateToolMessage::Resize { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// EllipseToolMessage
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=EllipseToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=EllipseToolMessage::DragStop),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=EllipseToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=EllipseToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=EllipseToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// PolygonToolMessage
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PolygonToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=PolygonToolMessage::DragStop),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=PolygonToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=PolygonToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PolygonToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// LineToolMessage
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=LineToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=LineToolMessage::DragStop),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=LineToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=LineToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=LineToolMessage::PointerMove { center: Alt, lock_angle: Control, snap_angle: Shift }),
|
||||
// ShapeToolMessage
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=ShapeToolMessage::DragStart),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=ShapeToolMessage::DragStop),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=ShapeToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=ShapeToolMessage::Abort),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=ShapeToolMessage::DecreaseSides),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=ShapeToolMessage::IncreaseSides),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=ShapeToolMessage::PointerMove([Alt, Shift, Control, Shift])),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowDown); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowLeft); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); modifiers=[ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowDown); modifiers=[ArrowLeft], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowLeft); modifiers=[ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowLeft); modifiers=[ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowLeft); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[ArrowDown], action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowRight); action_dispatch=ShapeToolMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }),
|
||||
entry!(KeyDown(ArrowUp); action_dispatch=ShapeToolMessage::IncreaseSides),
|
||||
entry!(KeyDown(ArrowDown); action_dispatch=ShapeToolMessage::DecreaseSides),
|
||||
//
|
||||
// PathToolMessage
|
||||
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
|
||||
@@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control }),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
|
||||
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
|
||||
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
||||
@@ -308,10 +308,10 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath),
|
||||
entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen),
|
||||
entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand),
|
||||
entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine),
|
||||
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle),
|
||||
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse),
|
||||
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolPolygon),
|
||||
entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolShapeLine),
|
||||
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolShapeRectangle),
|
||||
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolShapeEllipse),
|
||||
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape),
|
||||
entry!(KeyDown(KeyB); action_dispatch=ToolMessage::ActivateToolBrush),
|
||||
entry!(KeyDown(KeyX); modifiers=[Accel, Shift], action_dispatch=ToolMessage::ResetColors),
|
||||
entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors),
|
||||
|
||||
@@ -121,7 +121,10 @@ impl LayoutMessageHandler {
|
||||
};
|
||||
|
||||
(|| {
|
||||
let update_value = value.as_object().expect("ColorInput update was not of type: object");
|
||||
let Some(update_value) = value.as_object() else {
|
||||
warn!("ColorInput update was not of type: object");
|
||||
return Message::NoOp;
|
||||
};
|
||||
|
||||
// None
|
||||
let is_none = update_value.get("none").and_then(|x| x.as_bool());
|
||||
@@ -154,7 +157,8 @@ impl LayoutMessageHandler {
|
||||
return (color_button.on_update.callback)(color_button);
|
||||
}
|
||||
|
||||
panic!("ColorInput update was not able to be parsed with color data: {color_button:?}");
|
||||
warn!("ColorInput update was not able to be parsed with color data: {color_button:?}");
|
||||
Message::NoOp
|
||||
})()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,12 +11,13 @@ use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::renderer::ClickTarget;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::click_target::ClickTarget;
|
||||
use graphene_std::vector::style::ViewMode;
|
||||
|
||||
#[impl_message(Message, PortfolioMessage, Document)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub enum DocumentMessage {
|
||||
Noop,
|
||||
// Sub-messages
|
||||
@@ -72,13 +73,6 @@ pub enum DocumentMessage {
|
||||
GroupSelectedLayers {
|
||||
group_folder_type: GroupFolderType,
|
||||
},
|
||||
// ImaginateGenerate {
|
||||
// imaginate_node: Vec<NodeId>,
|
||||
// },
|
||||
// ImaginateRandom {
|
||||
// imaginate_node: Vec<NodeId>,
|
||||
// then_generate: bool,
|
||||
// },
|
||||
MoveSelectedLayersTo {
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
@@ -157,6 +151,7 @@ pub enum DocumentMessage {
|
||||
},
|
||||
SetSnapping {
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
closure: Option<for<'a> fn(&'a mut SnappingState) -> &'a mut bool>,
|
||||
snapping_state: bool,
|
||||
},
|
||||
|
||||
@@ -29,11 +29,13 @@ use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::path_bool::{boolean_intersect, path_bool_lib};
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::{Raster, RasterDataTable};
|
||||
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
|
||||
use graphene_std::vector::PointId;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::style::ViewMode;
|
||||
use graphene_std::vector::{PointId, path_bool_lib};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct DocumentMessageData<'a> {
|
||||
@@ -613,37 +615,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders });
|
||||
}
|
||||
}
|
||||
// DocumentMessage::ImaginateGenerate { imaginate_node } => {
|
||||
// let random_value = generate_uuid();
|
||||
// responses.add(NodeGraphMessage::SetInputValue {
|
||||
// node_id: *imaginate_node.last().unwrap(),
|
||||
// // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeDefinition` in `document_node_type.rs`
|
||||
// input_index: 17,
|
||||
// value: graph_craft::document::value::TaggedValue::U64(random_value),
|
||||
// });
|
||||
|
||||
// responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
|
||||
// }
|
||||
// DocumentMessage::ImaginateRandom { imaginate_node, then_generate } => {
|
||||
// // Generate a random seed. We only want values between -2^53 and 2^53, because integer values
|
||||
// // outside of this range can get rounded in f64
|
||||
// let random_bits = generate_uuid();
|
||||
// let random_value = ((random_bits >> 11) as f64).copysign(f64::from_bits(random_bits & (1 << 63)));
|
||||
|
||||
// responses.add(DocumentMessage::AddTransaction);
|
||||
// // Set a random seed input
|
||||
// responses.add(NodeGraphMessage::SetInputValue {
|
||||
// node_id: *imaginate_node.last().unwrap(),
|
||||
// // Needs to match the index of the seed parameter in `pub const IMAGINATE_NODE: DocumentNodeDefinition` in `document_node_type.rs`
|
||||
// input_index: 3,
|
||||
// value: graph_craft::document::value::TaggedValue::F64(random_value),
|
||||
// });
|
||||
|
||||
// // Generate the image
|
||||
// if then_generate {
|
||||
// responses.add(DocumentMessage::ImaginateGenerate { imaginate_node });
|
||||
// }
|
||||
// }
|
||||
DocumentMessage::MoveSelectedLayersTo { parent, insert_index } => {
|
||||
if !self.selection_network_path.is_empty() {
|
||||
log::error!("Moving selected layers is only supported for the Document Network");
|
||||
@@ -1655,7 +1626,7 @@ impl DocumentMessageHandler {
|
||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => {
|
||||
let mut point = point.clone();
|
||||
let mut point = *point;
|
||||
point.apply_transform(layer_transform);
|
||||
viewport_polygon.contains_point(point.position)
|
||||
}
|
||||
@@ -1705,6 +1676,21 @@ impl DocumentMessageHandler {
|
||||
self.click_list(ipp).last()
|
||||
}
|
||||
|
||||
pub fn click_based_on_position(&self, mouse_snapped_positon: DVec2) -> Option<LayerNodeIdentifier> {
|
||||
ClickXRayIter::new(&self.network_interface, XRayTarget::Point(mouse_snapped_positon))
|
||||
.filter(move |&layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.skip_while(|&layer| layer == LayerNodeIdentifier::ROOT_PARENT)
|
||||
.scan(true, |last_had_children, layer| {
|
||||
if *last_had_children {
|
||||
*last_had_children = layer.has_children(self.network_interface.document_metadata());
|
||||
Some(layer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.last()
|
||||
}
|
||||
|
||||
/// Get the combined bounding box of the click targets of the selected visible layers in viewport space
|
||||
pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
|
||||
self.network_interface
|
||||
@@ -2977,7 +2963,7 @@ impl<'a> ClickXRayIter<'a> {
|
||||
// We do this on this using the target area to reduce computation (as the target area is usually very simple).
|
||||
if clip && intersects {
|
||||
let clip_path = click_targets_to_path_lib_segments(click_targets.iter().flat_map(|x| x.iter()), transform);
|
||||
let subtracted = graphene_std::vector::boolean_intersect(path, clip_path).into_iter().flatten().collect::<Vec<_>>();
|
||||
let subtracted = boolean_intersect(path, clip_path).into_iter().flatten().collect::<Vec<_>>();
|
||||
if subtracted.is_empty() {
|
||||
use_children = false;
|
||||
} else {
|
||||
@@ -3362,9 +3348,9 @@ mod document_message_handler_tests {
|
||||
let rect_bbox_after = document.metadata().bounding_box_viewport(rect_layer).unwrap();
|
||||
|
||||
// Verifing the rectangle maintains approximately the same position in viewport space
|
||||
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0.0, -25.0), regression (#2688) causes it to be: DVec2(100.0, 25.0)
|
||||
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0.0, -25.0), regression (#2688) causes it to be: DVec2(200.0, 75.0)
|
||||
let distance = before_center.distance(after_center); // TODO: Should be: 0.0, regression (#2688) causes it to be: 111.80339887498948
|
||||
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(100., 25.)
|
||||
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(200., 75.)
|
||||
let distance = before_center.distance(after_center); // TODO: Should be: 0., regression (#2688) causes it to be: 111.80339887498948
|
||||
|
||||
assert!(
|
||||
distance < 1.,
|
||||
|
||||
@@ -79,7 +79,7 @@ pub enum GraphOperationMessage {
|
||||
},
|
||||
NewBooleanOperationLayer {
|
||||
id: NodeId,
|
||||
operation: graphene_std::vector::misc::BooleanOperation,
|
||||
operation: graphene_std::path_bool::BooleanOperation,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
|
||||
+1
-1
@@ -10,8 +10,8 @@ use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::convert_usvg_path;
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -134,7 +134,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
LayerNodeIdentifier::new(new_id, self.network_interface, &[])
|
||||
}
|
||||
|
||||
pub fn insert_boolean_data(&mut self, operation: graphene_std::vector::misc::BooleanOperation, layer: LayerNodeIdentifier) {
|
||||
pub fn insert_boolean_data(&mut self, operation: graphene_std::path_bool::BooleanOperation, layer: LayerNodeIdentifier) {
|
||||
let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
|
||||
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroupTable::default()), true)),
|
||||
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
mod document_node_derive;
|
||||
|
||||
use super::node_properties::choice::enum_choice;
|
||||
use super::node_properties::{self, ParameterWidgetsInfo};
|
||||
use super::utility_types::FrontendNodeType;
|
||||
@@ -14,7 +16,7 @@ use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::concrete;
|
||||
use graph_craft::document::value::*;
|
||||
use graph_craft::document::*;
|
||||
use graphene_std::ops::XY;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::raster::brush_cache::BrushCache;
|
||||
use graphene_std::raster::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType, RedGreenBlueAlpha};
|
||||
use graphene_std::raster_types::{CPU, RasterDataTable};
|
||||
@@ -91,7 +93,7 @@ static DOCUMENT_NODE_TYPES: once_cell::sync::Lazy<Vec<DocumentNodeDefinition>> =
|
||||
/// Defines the "signature" or "header file"-like metadata for the document nodes, but not the implementation (which is defined in the node registry).
|
||||
/// The [`DocumentNode`] is the instance while these [`DocumentNodeDefinition`]s are the "classes" or "blueprints" from which the instances are built.
|
||||
fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
let mut custom = vec![
|
||||
let custom = vec![
|
||||
// TODO: Auto-generate this from its proto node macro
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Identity",
|
||||
@@ -164,13 +166,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -241,21 +243,21 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 1)],
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::graphic_element::ToElementNode"),
|
||||
manual_composition: Some(generic!(T)),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
..Default::default()
|
||||
},
|
||||
// Primary (bottom) input type coercion
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(T), 0)],
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::graphic_element::ToGroupNode"),
|
||||
manual_composition: Some(generic!(T)),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
..Default::default()
|
||||
},
|
||||
// The monitor node is used to display a thumbnail in the UI
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0)],
|
||||
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
|
||||
manual_composition: Some(generic!(T)),
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
skip_deduplication: true,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -316,7 +318,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Layer".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(1, -3)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -3)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -333,7 +335,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed("The Merge node combines graphical data through composition."),
|
||||
description: Cow::Borrowed("Merge attaches a layer to the stack's group."),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
@@ -429,7 +431,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "To Artboard".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-10, -3)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-14, -3)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -437,7 +439,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Monitor".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-2, -3)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-7, -3)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -445,7 +447,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Append Artboards".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(6, -4)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, -4)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -467,7 +469,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Load Image",
|
||||
category: "Network",
|
||||
category: "Web Request",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
@@ -784,7 +786,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Noise Pattern",
|
||||
category: "Raster",
|
||||
category: "Raster: Pattern",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
@@ -837,7 +839,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Channels",
|
||||
category: "Raster",
|
||||
category: "Raster: Channels",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
@@ -958,13 +960,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::extract_xy::ExtractXyNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::extract_xy::ExtractXyNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1464,7 +1466,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
// description: Cow::Borrowed("TODO"),
|
||||
// properties: None,
|
||||
// },
|
||||
// (*IMAGINATE_NODE).clone(),
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Path",
|
||||
category: "Vector",
|
||||
@@ -1649,7 +1650,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
NodeInput::network(concrete!(DVec2), 5),
|
||||
],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::TransformNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::TransformNode")),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -1733,7 +1734,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
nodes: vec![
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(VectorDataTable), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::vector::BooleanOperationNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_path_bool::BooleanOperationNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1745,13 +1746,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1764,7 +1765,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
}),
|
||||
inputs: vec![
|
||||
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroupTable::default()), true),
|
||||
NodeInput::value(TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false),
|
||||
NodeInput::value(TaggedValue::BooleanOperation(path_bool::BooleanOperation::Union), false),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1822,12 +1823,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Sample Points",
|
||||
category: "Vector",
|
||||
identifier: "Sample Polyline",
|
||||
category: "Vector: Modifier",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![NodeInput::node(NodeId(4), 0)], // Taken from output 0 of Sample Points
|
||||
exports: vec![NodeInput::node(NodeId(4), 0)],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0)],
|
||||
@@ -1838,13 +1839,15 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNode {
|
||||
inputs: vec![
|
||||
NodeInput::network(concrete!(graphene_std::vector::VectorDataTable), 0),
|
||||
NodeInput::network(concrete!(f64), 1), // From the document node's parameters
|
||||
NodeInput::network(concrete!(f64), 2), // From the document node's parameters
|
||||
NodeInput::network(concrete!(f64), 3), // From the document node's parameters
|
||||
NodeInput::network(concrete!(bool), 4), // From the document node's parameters
|
||||
NodeInput::node(NodeId(0), 0), // From output 0 of SubpathSegmentLengthsNode
|
||||
NodeInput::network(concrete!(vector::misc::PointSpacingType), 1),
|
||||
NodeInput::network(concrete!(f64), 2),
|
||||
NodeInput::network(concrete!(f64), 3),
|
||||
NodeInput::network(concrete!(f64), 4),
|
||||
NodeInput::network(concrete!(f64), 5),
|
||||
NodeInput::network(concrete!(bool), 6),
|
||||
NodeInput::node(NodeId(0), 0),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SamplePointsNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::SamplePolylineNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1856,13 +1859,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(3), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1875,6 +1878,8 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
}),
|
||||
inputs: vec![
|
||||
NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorDataTable::default()), true),
|
||||
NodeInput::value(TaggedValue::PointSpacingType(Default::default()), false),
|
||||
NodeInput::value(TaggedValue::F64(100.), false),
|
||||
NodeInput::value(TaggedValue::F64(100.), false),
|
||||
NodeInput::value(TaggedValue::F64(0.), false),
|
||||
NodeInput::value(TaggedValue::F64(0.), false),
|
||||
@@ -1889,14 +1894,14 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Subpath Segment Lengths".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 5)),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 7)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Sample Points".to_string(),
|
||||
display_name: "Sample Polyline".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1937,18 +1942,28 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
}),
|
||||
input_properties: vec![
|
||||
("Vector Data", "The shape to be resampled and converted into a polyline.").into(),
|
||||
Into::<PropertiesRow>::into(("Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_SPACING)),
|
||||
PropertiesRow::with_override(
|
||||
"Spacing",
|
||||
"Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).",
|
||||
"Separation",
|
||||
node_properties::SAMPLE_POLYLINE_TOOLTIP_SEPARATION,
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
min: Some(1.),
|
||||
min: Some(0.),
|
||||
unit: Some(" px".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
PropertiesRow::with_override(
|
||||
"Quantity",
|
||||
node_properties::SAMPLE_POLYLINE_TOOLTIP_QUANTITY,
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
min: Some(2.),
|
||||
is_integer: true,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
PropertiesRow::with_override(
|
||||
"Start Offset",
|
||||
"Exclude some distance from the start of the path before the first instance.",
|
||||
node_properties::SAMPLE_POLYLINE_TOOLTIP_START_OFFSET,
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
min: Some(0.),
|
||||
unit: Some(" px".to_string()),
|
||||
@@ -1957,25 +1972,25 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
),
|
||||
PropertiesRow::with_override(
|
||||
"Stop Offset",
|
||||
"Exclude some distance from the end of the path after the last instance.",
|
||||
node_properties::SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET,
|
||||
WidgetOverride::Number(NumberInputSettings {
|
||||
min: Some(0.),
|
||||
unit: Some(" px".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
Into::<PropertiesRow>::into(("Adaptive Spacing", "Round 'Spacing' to a nearby value that divides into the path length evenly.")),
|
||||
Into::<PropertiesRow>::into(("Adaptive Spacing", node_properties::SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING)),
|
||||
],
|
||||
output_names: vec!["Vector".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed("Convert vector geometry into a polyline composed of evenly spaced points."),
|
||||
properties: None,
|
||||
properties: Some("sample_polyline_properties"),
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Scatter Points",
|
||||
category: "Vector",
|
||||
category: "Vector: Modifier",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
@@ -1999,13 +2014,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(1), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(2), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -2100,233 +2115,9 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
||||
},
|
||||
];
|
||||
|
||||
// Remove struct generics
|
||||
for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() {
|
||||
let NodeTemplate {
|
||||
document_node: DocumentNode { implementation, .. },
|
||||
..
|
||||
} = node_template;
|
||||
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = implementation {
|
||||
if let Some((new_name, _suffix)) = name.rsplit_once("<") {
|
||||
*name = Cow::Owned(new_name.to_string())
|
||||
}
|
||||
};
|
||||
}
|
||||
let node_registry = graphene_std::registry::NODE_REGISTRY.lock().unwrap();
|
||||
'outer: for (id, metadata) in graphene_std::registry::NODE_METADATA.lock().unwrap().iter() {
|
||||
use graphene_std::registry::*;
|
||||
let id = id.clone();
|
||||
|
||||
for node in custom.iter() {
|
||||
let DocumentNodeDefinition {
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode { implementation, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
} = node;
|
||||
match implementation {
|
||||
DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) if name == &id => continue 'outer,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let NodeMetadata {
|
||||
display_name,
|
||||
category,
|
||||
fields,
|
||||
description,
|
||||
properties,
|
||||
} = metadata;
|
||||
let Some(implementations) = &node_registry.get(&id) else { continue };
|
||||
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
|
||||
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
|
||||
let mut input_type = &first_node_io.call_argument;
|
||||
if valid_inputs.len() > 1 {
|
||||
input_type = &const { generic!(D) };
|
||||
}
|
||||
let output_type = &first_node_io.return_value;
|
||||
|
||||
let inputs = fields
|
||||
.iter()
|
||||
.zip(first_node_io.inputs.iter())
|
||||
.enumerate()
|
||||
.map(|(index, (field, node_io_ty))| {
|
||||
let ty = field.default_type.as_ref().unwrap_or(node_io_ty);
|
||||
let exposed = if index == 0 { *ty != fn_type_fut!(Context, ()) } else { field.exposed };
|
||||
|
||||
match field.value_source {
|
||||
RegistryValueSource::None => {}
|
||||
RegistryValueSource::Default(data) => return NodeInput::value(TaggedValue::from_primitive_string(data, ty).unwrap_or(TaggedValue::None), exposed),
|
||||
RegistryValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)),
|
||||
};
|
||||
|
||||
if let Some(type_default) = TaggedValue::from_type(ty) {
|
||||
return NodeInput::value(type_default, exposed);
|
||||
}
|
||||
NodeInput::value(TaggedValue::None, true)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let node = DocumentNodeDefinition {
|
||||
identifier: display_name,
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
inputs,
|
||||
manual_composition: Some(input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
// TODO: Store information for input overrides in the node macro
|
||||
input_properties: fields
|
||||
.iter()
|
||||
.map(|f| match f.widget_override {
|
||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||
RegistryWidgetOverride::Hidden => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Hidden),
|
||||
RegistryWidgetOverride::String(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||
RegistryWidgetOverride::Custom(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
output_names: vec![output_type.to_string()],
|
||||
has_primary_output: true,
|
||||
locked: false,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
category: category.unwrap_or("UNCATEGORIZED"),
|
||||
description: Cow::Borrowed(description),
|
||||
properties: *properties,
|
||||
};
|
||||
custom.push(node);
|
||||
}
|
||||
custom
|
||||
document_node_derive::post_process_nodes(custom)
|
||||
}
|
||||
|
||||
// pub static IMAGINATE_NODE: Lazy<DocumentNodeDefinition> = Lazy::new(|| DocumentNodeDefinition {
|
||||
// identifier: "Imaginate",
|
||||
// category: "Raster",
|
||||
// node_template: NodeTemplate {
|
||||
// document_node: DocumentNode {
|
||||
// implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
// exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||
// nodes: [
|
||||
// DocumentNode {
|
||||
// inputs: vec![NodeInput::network(concrete!(RasterDataTable<CPU>), 0)],
|
||||
// implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode"),
|
||||
// manual_composition: Some(concrete!(Context)),
|
||||
// skip_deduplication: true,
|
||||
// ..Default::default()
|
||||
// },
|
||||
// DocumentNode {
|
||||
// inputs: vec![
|
||||
// NodeInput::node(NodeId(0), 0),
|
||||
// NodeInput::network(concrete!(&WasmEditorApi), 1),
|
||||
// NodeInput::network(concrete!(ImaginateController), 2),
|
||||
// NodeInput::network(concrete!(f64), 3),
|
||||
// NodeInput::network(concrete!(Option<DVec2>), 4),
|
||||
// NodeInput::network(concrete!(u32), 5),
|
||||
// NodeInput::network(concrete!(ImaginateSamplingMethod), 6),
|
||||
// NodeInput::network(concrete!(f64), 7),
|
||||
// NodeInput::network(concrete!(String), 8),
|
||||
// NodeInput::network(concrete!(String), 9),
|
||||
// NodeInput::network(concrete!(bool), 10),
|
||||
// NodeInput::network(concrete!(f64), 11),
|
||||
// NodeInput::network(concrete!(bool), 12),
|
||||
// NodeInput::network(concrete!(f64), 13),
|
||||
// NodeInput::network(concrete!(ImaginateMaskStartingFill), 14),
|
||||
// NodeInput::network(concrete!(bool), 15),
|
||||
// NodeInput::network(concrete!(bool), 16),
|
||||
// NodeInput::network(concrete!(u64), 17),
|
||||
// ],
|
||||
// implementation: DocumentNodeImplementation::proto("graphene_std::raster::ImaginateNode"),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ]
|
||||
// .into_iter()
|
||||
// .enumerate()
|
||||
// .map(|(id, node)| (NodeId(id as u64), node))
|
||||
// .collect(),
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// inputs: vec![
|
||||
// NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true),
|
||||
// NodeInput::scope("editor-api"),
|
||||
// NodeInput::value(TaggedValue::ImaginateController(Default::default()), false),
|
||||
// NodeInput::value(TaggedValue::F64(0.), false), // Remember to keep index used in `ImaginateRandom` updated with this entry's index
|
||||
// NodeInput::value(TaggedValue::OptionalDVec2(None), false),
|
||||
// NodeInput::value(TaggedValue::U32(30), false),
|
||||
// NodeInput::value(TaggedValue::ImaginateSamplingMethod(ImaginateSamplingMethod::EulerA), false),
|
||||
// NodeInput::value(TaggedValue::F64(7.5), false),
|
||||
// NodeInput::value(TaggedValue::String(String::new()), false),
|
||||
// NodeInput::value(TaggedValue::String(String::new()), false),
|
||||
// NodeInput::value(TaggedValue::Bool(false), false),
|
||||
// NodeInput::value(TaggedValue::F64(66.), false),
|
||||
// NodeInput::value(TaggedValue::Bool(true), false),
|
||||
// NodeInput::value(TaggedValue::F64(4.), false),
|
||||
// NodeInput::value(TaggedValue::ImaginateMaskStartingFill(ImaginateMaskStartingFill::Fill), false),
|
||||
// NodeInput::value(TaggedValue::Bool(false), false),
|
||||
// NodeInput::value(TaggedValue::Bool(false), false),
|
||||
// NodeInput::value(TaggedValue::U64(0), false),
|
||||
// ],
|
||||
// ..Default::default()
|
||||
// },
|
||||
// persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
// network_metadata: Some(NodeNetworkMetadata {
|
||||
// persistent_metadata: NodeNetworkPersistentMetadata {
|
||||
// node_metadata: [
|
||||
// DocumentNodeMetadata {
|
||||
// persistent_metadata: DocumentNodePersistentMetadata {
|
||||
// display_name: "Monitor".to_string(),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ..Default::default()
|
||||
// },
|
||||
// DocumentNodeMetadata {
|
||||
// persistent_metadata: DocumentNodePersistentMetadata {
|
||||
// display_name: "Imaginate".to_string(),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ]
|
||||
// .into_iter()
|
||||
// .enumerate()
|
||||
// .map(|(id, node)| (NodeId(id as u64), node))
|
||||
// .collect(),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// input_properties: vec![
|
||||
// "Input Image".into(),
|
||||
// "Editor Api".into(),
|
||||
// "Controller".into(),
|
||||
// "Seed".into(),
|
||||
// "Resolution".into(),
|
||||
// "Samples".into(),
|
||||
// "Sampling Method".into(),
|
||||
// "Prompt Guidance".into(),
|
||||
// "Prompt".into(),
|
||||
// "Negative Prompt".into(),
|
||||
// "Adapt Input Image".into(),
|
||||
// "Image Creativity".into(),
|
||||
// "Inpaint".into(),
|
||||
// "Mask Blur".into(),
|
||||
// "Mask Starting Fill".into(),
|
||||
// "Improve Faces".into(),
|
||||
// "Tiling".into(),
|
||||
// ],
|
||||
// output_names: vec!["Image".to_string()],
|
||||
// ..Default::default()
|
||||
// },
|
||||
// },
|
||||
// description: Cow::Borrowed("TODO"),
|
||||
// properties: None, // Some(&node_properties::imaginate_properties),
|
||||
// });
|
||||
|
||||
type NodeProperties = HashMap<String, Box<dyn Fn(NodeId, &mut NodePropertiesContext) -> Vec<LayoutGroup> + Send + Sync>>;
|
||||
|
||||
pub static NODE_OVERRIDES: once_cell::sync::Lazy<NodeProperties> = once_cell::sync::Lazy::new(static_node_properties);
|
||||
@@ -2344,6 +2135,7 @@ fn static_node_properties() -> NodeProperties {
|
||||
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
|
||||
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
|
||||
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
||||
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
|
||||
map.insert(
|
||||
"identity_properties".to_string(),
|
||||
Box::new(|_node_id, _context| node_properties::string_properties("The identity node simply passes its data through.")),
|
||||
@@ -3060,19 +2852,3 @@ impl DocumentNodeDefinition {
|
||||
self.node_template_input_override(self.node_template.document_node.inputs.clone().into_iter().map(Some))
|
||||
}
|
||||
}
|
||||
|
||||
// Previously used by the Imaginate node, but usage was commented out since it did nothing.
|
||||
// pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetwork {
|
||||
// let mut network = NodeNetwork { ..Default::default() };
|
||||
// network.push_node_to_document_network(
|
||||
// resolve_document_node_type("Input Frame")
|
||||
// .expect("Input Frame node does not exist")
|
||||
// .to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))),
|
||||
// );
|
||||
// network.push_node_to_document_network(
|
||||
// resolve_document_node_type("Output")
|
||||
// .expect("Output node does not exist")
|
||||
// .to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))),
|
||||
// );
|
||||
// network
|
||||
// }
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
use super::DocumentNodeDefinition;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{DocumentNodePersistentMetadata, NodeTemplate, PropertiesRow, WidgetOverride};
|
||||
use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::document::*;
|
||||
use graphene_std::registry::*;
|
||||
use graphene_std::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec<DocumentNodeDefinition> {
|
||||
// Remove struct generics
|
||||
for DocumentNodeDefinition { node_template, .. } in custom.iter_mut() {
|
||||
let NodeTemplate {
|
||||
document_node: DocumentNode { implementation, .. },
|
||||
..
|
||||
} = node_template;
|
||||
|
||||
if let DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) = implementation {
|
||||
if let Some((new_name, _suffix)) = name.rsplit_once("<") {
|
||||
*name = Cow::Owned(new_name.to_string())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();
|
||||
'outer: for (id, metadata) in NODE_METADATA.lock().unwrap().iter() {
|
||||
for node in custom.iter() {
|
||||
let DocumentNodeDefinition {
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode { implementation, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
} = node;
|
||||
match implementation {
|
||||
DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier { name }) if name == id => continue 'outer,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let NodeMetadata {
|
||||
display_name,
|
||||
category,
|
||||
fields,
|
||||
description,
|
||||
properties,
|
||||
} = metadata;
|
||||
|
||||
let Some(implementations) = &node_registry.get(id) else { continue };
|
||||
|
||||
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
|
||||
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
|
||||
|
||||
let input_type = if valid_inputs.len() > 1 { &const { generic!(D) } } else { &first_node_io.call_argument };
|
||||
let output_type = &first_node_io.return_value;
|
||||
|
||||
let inputs = preprocessor::node_inputs(fields, first_node_io);
|
||||
let node = DocumentNodeDefinition {
|
||||
identifier: display_name,
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
inputs,
|
||||
manual_composition: Some(input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
// TODO: Store information for input overrides in the node macro
|
||||
input_properties: fields
|
||||
.iter()
|
||||
.map(|f| match f.widget_override {
|
||||
RegistryWidgetOverride::None => (f.name, f.description).into(),
|
||||
RegistryWidgetOverride::Hidden => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Hidden),
|
||||
RegistryWidgetOverride::String(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::String(str.to_string())),
|
||||
RegistryWidgetOverride::Custom(str) => PropertiesRow::with_override(f.name, f.description, WidgetOverride::Custom(str.to_string())),
|
||||
})
|
||||
.collect(),
|
||||
output_names: vec![output_type.to_string()],
|
||||
has_primary_output: true,
|
||||
locked: false,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
category: category.unwrap_or("UNCATEGORIZED"),
|
||||
description: Cow::Borrowed(description),
|
||||
properties: *properties,
|
||||
};
|
||||
|
||||
custom.push(node);
|
||||
}
|
||||
|
||||
custom
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graph_craft::proto::GraphErrors;
|
||||
use graphene_std::math::math_ext::QuadExt;
|
||||
use graphene_std::*;
|
||||
use renderer::Quad;
|
||||
use std::cmp::Ordering;
|
||||
@@ -567,7 +568,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
let new_ids: HashMap<_, _> = data.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||
let nodes: Vec<_> = new_ids.iter().map(|(_, id)| *id).collect();
|
||||
let nodes: Vec<_> = new_ids.values().copied().collect();
|
||||
responses.add(NodeGraphMessage::AddNodes {
|
||||
nodes: data,
|
||||
new_ids: new_ids.clone(),
|
||||
@@ -1396,12 +1397,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
input,
|
||||
});
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
if (network_interface
|
||||
.reference(&node_id, selection_network_path)
|
||||
.is_none_or(|reference| *reference != Some("Imaginate".to_string())) // TODO: Potentially remove the reference to Imaginate
|
||||
|| input_index == 0)
|
||||
&& network_interface.connected_to_output(&node_id, selection_network_path)
|
||||
{
|
||||
if !(network_interface.reference(&node_id, selection_network_path).is_none() || input_index == 0) && network_interface.connected_to_output(&node_id, selection_network_path) {
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ use graph_craft::Type;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::ops::XY;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::path_bool::BooleanOperation;
|
||||
use graphene_std::raster::curve::Curve;
|
||||
use graphene_std::raster::{
|
||||
BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute,
|
||||
@@ -22,10 +23,9 @@ use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use graphene_std::text::Font;
|
||||
use graphene_std::transform::{Footprint, ReferencePoint};
|
||||
use graphene_std::vector::VectorDataTable;
|
||||
use graphene_std::vector::generator_nodes::grid;
|
||||
use graphene_std::vector::misc::ArcType;
|
||||
use graphene_std::vector::misc::CentroidType;
|
||||
use graphene_std::vector::misc::{BooleanOperation, GridType};
|
||||
use graphene_std::vector::misc::GridType;
|
||||
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
|
||||
use graphene_std::vector::misc::{CentroidType, PointSpacingType};
|
||||
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
|
||||
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
|
||||
@@ -238,6 +238,8 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<MergeByDistanceAlgorithm>() => enum_choice::<MergeByDistanceAlgorithm>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PointSpacingType>() => enum_choice::<PointSpacingType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<LuminanceCalculation>() => enum_choice::<LuminanceCalculation>().for_socket(default_info).property_row(),
|
||||
@@ -938,8 +940,7 @@ pub fn get_document_node<'a>(node_id: NodeId, context: &'a NodePropertiesContext
|
||||
}
|
||||
|
||||
pub fn query_node_and_input_info<'a>(node_id: NodeId, input_index: usize, context: &'a NodePropertiesContext<'a>) -> Result<(&'a DocumentNode, &'a str, &'a str), String> {
|
||||
let node_id2 = node_id.clone();
|
||||
let document_node = get_document_node(node_id2, context)?;
|
||||
let document_node = get_document_node(node_id, context)?;
|
||||
let input_name = context.network_interface.input_name(node_id, input_index, context.selection_network_path).unwrap_or_else(|| {
|
||||
log::warn!("input name not found in query_node_and_input_info");
|
||||
""
|
||||
@@ -981,16 +982,19 @@ pub fn query_noise_pattern_state(node_id: NodeId, context: &NodePropertiesContex
|
||||
}
|
||||
|
||||
pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesContext) -> Result<bool, String> {
|
||||
use graphene_std::vector::assign_colors::*;
|
||||
|
||||
let document_node = get_document_node(node_id, context)?;
|
||||
// This is safe since the node is a proto node and the implementation cannot be changed.
|
||||
let randomize_index = 5;
|
||||
Ok(match document_node.inputs.get(randomize_index).and_then(|input| input.as_value()) {
|
||||
Ok(match document_node.inputs.get(RandomizeInput::INDEX).and_then(|input| input.as_value()) {
|
||||
Some(TaggedValue::Bool(randomize_enabled)) => *randomize_enabled,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::brightness_contrast::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1000,17 +1004,18 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
|
||||
};
|
||||
|
||||
// Use Classic
|
||||
let use_classic_index = 3;
|
||||
let use_classic = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, use_classic_index, true, context), CheckboxInput::default());
|
||||
let use_classic_value = match document_node.inputs[use_classic_index].as_value() {
|
||||
let use_classic = bool_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, UseClassicInput::INDEX, true, context),
|
||||
CheckboxInput::default(),
|
||||
);
|
||||
let use_classic_value = match document_node.inputs[UseClassicInput::INDEX].as_value() {
|
||||
Some(TaggedValue::Bool(use_classic_choice)) => *use_classic_choice,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// Brightness
|
||||
let brightness_index = 1;
|
||||
let brightness = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, brightness_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, BrightnessInput::INDEX, true, context),
|
||||
NumberInput::default()
|
||||
.unit("%")
|
||||
.mode_range()
|
||||
@@ -1020,9 +1025,8 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
|
||||
);
|
||||
|
||||
// Contrast
|
||||
let contrast_index = 2;
|
||||
let contrast = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, contrast_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, ContrastInput::INDEX, true, context),
|
||||
NumberInput::default()
|
||||
.unit("%")
|
||||
.mode_range()
|
||||
@@ -1041,6 +1045,8 @@ pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut Node
|
||||
}
|
||||
|
||||
pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::channel_mixer::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1050,20 +1056,21 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
|
||||
};
|
||||
|
||||
// Monochrome
|
||||
let monochrome_index = 1;
|
||||
let is_monochrome = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, monochrome_index, true, context), CheckboxInput::default());
|
||||
let is_monochrome_value = match document_node.inputs[monochrome_index].as_value() {
|
||||
let is_monochrome = bool_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, MonochromeInput::INDEX, true, context),
|
||||
CheckboxInput::default(),
|
||||
);
|
||||
let is_monochrome_value = match document_node.inputs[MonochromeInput::INDEX].as_value() {
|
||||
Some(TaggedValue::Bool(monochrome_choice)) => *monochrome_choice,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// Output channel choice
|
||||
let output_channel_index = 18;
|
||||
let output_channel = enum_choice::<RedGreenBlue>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, output_channel_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, OutputChannelInput::INDEX, true, context))
|
||||
.exposable(false)
|
||||
.property_row();
|
||||
let output_channel_value = match &document_node.inputs[output_channel_index].as_value() {
|
||||
let output_channel_value = match &document_node.inputs[OutputChannelInput::INDEX].as_value() {
|
||||
Some(TaggedValue::RedGreenBlue(choice)) => choice,
|
||||
_ => {
|
||||
warn!("Channel Mixer node properties panel could not be displayed.");
|
||||
@@ -1073,10 +1080,10 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
|
||||
|
||||
// Output Channel modes
|
||||
let (red_output_index, green_output_index, blue_output_index, constant_output_index) = match (is_monochrome_value, output_channel_value) {
|
||||
(true, _) => (2, 3, 4, 5),
|
||||
(false, RedGreenBlue::Red) => (6, 7, 8, 9),
|
||||
(false, RedGreenBlue::Green) => (10, 11, 12, 13),
|
||||
(false, RedGreenBlue::Blue) => (14, 15, 16, 17),
|
||||
(true, _) => (MonochromeRInput::INDEX, MonochromeGInput::INDEX, MonochromeBInput::INDEX, MonochromeCInput::INDEX),
|
||||
(false, RedGreenBlue::Red) => (RedRInput::INDEX, RedGInput::INDEX, RedBInput::INDEX, RedCInput::INDEX),
|
||||
(false, RedGreenBlue::Green) => (GreenRInput::INDEX, GreenGInput::INDEX, GreenBInput::INDEX, GreenCInput::INDEX),
|
||||
(false, RedGreenBlue::Blue) => (BlueRInput::INDEX, BlueGInput::INDEX, BlueBInput::INDEX, BlueCInput::INDEX),
|
||||
};
|
||||
let number_input = NumberInput::default().mode_range().min(-200.).max(200.).unit("%");
|
||||
let red = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, red_output_index, true, context), number_input.clone());
|
||||
@@ -1101,6 +1108,8 @@ pub(crate) fn channel_mixer_properties(node_id: NodeId, context: &mut NodeProper
|
||||
}
|
||||
|
||||
pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::selective_color::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1108,15 +1117,14 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
// Colors choice
|
||||
let colors_index = 38;
|
||||
|
||||
// Colors choice
|
||||
let colors = enum_choice::<SelectiveColorChoice>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, colors_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, ColorsInput::INDEX, true, context))
|
||||
.exposable(false)
|
||||
.property_row();
|
||||
|
||||
let colors_choice_index = match &document_node.inputs[colors_index].as_value() {
|
||||
let colors_choice = match &document_node.inputs[ColorsInput::INDEX].as_value() {
|
||||
Some(TaggedValue::SelectiveColorChoice(choice)) => choice,
|
||||
_ => {
|
||||
warn!("Selective Color node properties panel could not be displayed.");
|
||||
@@ -1125,16 +1133,16 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
|
||||
};
|
||||
|
||||
// CMYK
|
||||
let (c_index, m_index, y_index, k_index) = match colors_choice_index {
|
||||
SelectiveColorChoice::Reds => (2, 3, 4, 5),
|
||||
SelectiveColorChoice::Yellows => (6, 7, 8, 9),
|
||||
SelectiveColorChoice::Greens => (10, 11, 12, 13),
|
||||
SelectiveColorChoice::Cyans => (14, 15, 16, 17),
|
||||
SelectiveColorChoice::Blues => (18, 19, 20, 21),
|
||||
SelectiveColorChoice::Magentas => (22, 23, 24, 25),
|
||||
SelectiveColorChoice::Whites => (26, 27, 28, 29),
|
||||
SelectiveColorChoice::Neutrals => (30, 31, 32, 33),
|
||||
SelectiveColorChoice::Blacks => (34, 35, 36, 37),
|
||||
let (c_index, m_index, y_index, k_index) = match colors_choice {
|
||||
SelectiveColorChoice::Reds => (RCInput::INDEX, RMInput::INDEX, RYInput::INDEX, RKInput::INDEX),
|
||||
SelectiveColorChoice::Yellows => (YCInput::INDEX, YMInput::INDEX, YYInput::INDEX, YKInput::INDEX),
|
||||
SelectiveColorChoice::Greens => (GCInput::INDEX, GMInput::INDEX, GYInput::INDEX, GKInput::INDEX),
|
||||
SelectiveColorChoice::Cyans => (CCInput::INDEX, CMInput::INDEX, CYInput::INDEX, CKInput::INDEX),
|
||||
SelectiveColorChoice::Blues => (BCInput::INDEX, BMInput::INDEX, BYInput::INDEX, BKInput::INDEX),
|
||||
SelectiveColorChoice::Magentas => (MCInput::INDEX, MMInput::INDEX, MYInput::INDEX, MKInput::INDEX),
|
||||
SelectiveColorChoice::Whites => (WCInput::INDEX, WMInput::INDEX, WYInput::INDEX, WKInput::INDEX),
|
||||
SelectiveColorChoice::Neutrals => (NCInput::INDEX, NMInput::INDEX, NYInput::INDEX, NKInput::INDEX),
|
||||
SelectiveColorChoice::Blacks => (KCInput::INDEX, KMInput::INDEX, KYInput::INDEX, KKInput::INDEX),
|
||||
};
|
||||
let number_input = NumberInput::default().mode_range().min(-100.).max(100.).unit("%");
|
||||
let cyan = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, c_index, true, context), number_input.clone());
|
||||
@@ -1143,9 +1151,8 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
|
||||
let black = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, k_index, true, context), number_input);
|
||||
|
||||
// Mode
|
||||
let mode_index = 1;
|
||||
let mode = enum_choice::<RelativeAbsolute>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, mode_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, ModeInput::INDEX, true, context))
|
||||
.property_row();
|
||||
|
||||
vec![
|
||||
@@ -1162,11 +1169,7 @@ pub(crate) fn selective_color_properties(node_id: NodeId, context: &mut NodeProp
|
||||
}
|
||||
|
||||
pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let grid_type_index = grid::GridTypeInput::INDEX;
|
||||
let spacing_index = grid::SpacingInput::<f64>::INDEX;
|
||||
let angles_index = grid::AnglesInput::INDEX;
|
||||
let rows_index = grid::RowsInput::INDEX;
|
||||
let columns_index = grid::ColumnsInput::INDEX;
|
||||
use graphene_std::vector::generator_nodes::grid::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
@@ -1176,43 +1179,115 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
}
|
||||
};
|
||||
let grid_type = enum_choice::<GridType>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, grid_type_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, GridTypeInput::INDEX, true, context))
|
||||
.property_row();
|
||||
|
||||
let mut widgets = vec![grid_type];
|
||||
|
||||
let Some(grid_type_input) = document_node.inputs.get(grid_type_index) else {
|
||||
let Some(grid_type_input) = document_node.inputs.get(GridTypeInput::INDEX) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return vec![];
|
||||
};
|
||||
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
let spacing = coordinate_widget(ParameterWidgetsInfo::from_index(document_node, node_id, spacing_index, true, context), "W", "H", " px", Some(0.));
|
||||
let spacing = coordinate_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::<f64>::INDEX, true, context),
|
||||
"W",
|
||||
"H",
|
||||
" px",
|
||||
Some(0.),
|
||||
);
|
||||
widgets.push(spacing);
|
||||
}
|
||||
GridType::Isometric => {
|
||||
let spacing = LayoutGroup::Row {
|
||||
widgets: number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, spacing_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::<f64>::INDEX, true, context),
|
||||
NumberInput::default().label("H").min(0.).unit(" px"),
|
||||
),
|
||||
};
|
||||
let angles = coordinate_widget(ParameterWidgetsInfo::from_index(document_node, node_id, angles_index, true, context), "", "", "°", None);
|
||||
let angles = coordinate_widget(ParameterWidgetsInfo::from_index(document_node, node_id, AnglesInput::INDEX, true, context), "", "", "°", None);
|
||||
widgets.extend([spacing, angles]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rows = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, rows_index, true, context), NumberInput::default().min(1.));
|
||||
let columns = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, columns_index, true, context), NumberInput::default().min(1.));
|
||||
let columns = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, ColumnsInput::INDEX, true, context),
|
||||
NumberInput::default().min(1.),
|
||||
);
|
||||
let rows = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, RowsInput::INDEX, true, context),
|
||||
NumberInput::default().min(1.),
|
||||
);
|
||||
|
||||
widgets.extend([LayoutGroup::Row { widgets: rows }, LayoutGroup::Row { widgets: columns }]);
|
||||
widgets.extend([LayoutGroup::Row { widgets: columns }, LayoutGroup::Row { widgets: rows }]);
|
||||
|
||||
widgets
|
||||
}
|
||||
|
||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
|
||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
|
||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
|
||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_START_OFFSET: &str = "Exclude some distance from the start of the path before the first instance.";
|
||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET: &str = "Exclude some distance from the end of the path after the last instance.";
|
||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING: &str = "Round 'Separation' to a nearby value that divides into the path length evenly.";
|
||||
|
||||
pub(crate) fn sample_polyline_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::sample_polyline::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
log::error!("Could not get document node in sample_polyline_properties: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let current_spacing = document_node.inputs.get(SpacingInput::INDEX).and_then(|input| input.as_value()).cloned();
|
||||
let is_quantity = matches!(current_spacing, Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)));
|
||||
|
||||
let spacing = enum_choice::<PointSpacingType>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, SpacingInput::INDEX, true, context))
|
||||
.property_row();
|
||||
let separation = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, SeparationInput::INDEX, true, context),
|
||||
NumberInput::default().min(0.).unit(" px"),
|
||||
);
|
||||
let quantity = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, QuantityInput::INDEX, true, context),
|
||||
NumberInput::default().min(2.).int(),
|
||||
);
|
||||
let start_offset = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, StartOffsetInput::INDEX, true, context),
|
||||
NumberInput::default().min(0.).unit(" px"),
|
||||
);
|
||||
let stop_offset = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, StopOffsetInput::INDEX, true, context),
|
||||
NumberInput::default().min(0.).unit(" px"),
|
||||
);
|
||||
let adaptive_spacing = bool_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, AdaptiveSpacingInput::INDEX, true, context),
|
||||
CheckboxInput::default().disabled(is_quantity),
|
||||
);
|
||||
|
||||
vec![
|
||||
spacing.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SPACING),
|
||||
match current_spacing {
|
||||
Some(TaggedValue::PointSpacingType(PointSpacingType::Separation)) => LayoutGroup::Row { widgets: separation }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_SEPARATION),
|
||||
Some(TaggedValue::PointSpacingType(PointSpacingType::Quantity)) => LayoutGroup::Row { widgets: quantity }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_QUANTITY),
|
||||
_ => LayoutGroup::Row { widgets: vec![] },
|
||||
},
|
||||
LayoutGroup::Row { widgets: start_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_START_OFFSET),
|
||||
LayoutGroup::Row { widgets: stop_offset }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_STOP_OFFSET),
|
||||
LayoutGroup::Row { widgets: adaptive_spacing }.with_tooltip(SAMPLE_POLYLINE_TOOLTIP_ADAPTIVE_SPACING),
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::raster::exposure::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1220,10 +1295,16 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let exposure = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, 1, true, context), NumberInput::default().min(-20.).max(20.));
|
||||
let offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, 2, true, context), NumberInput::default().min(-0.5).max(0.5));
|
||||
let exposure = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, ExposureInput::INDEX, true, context),
|
||||
NumberInput::default().min(-20.).max(20.),
|
||||
);
|
||||
let offset = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, OffsetInput::INDEX, true, context),
|
||||
NumberInput::default().min(-0.5).max(0.5),
|
||||
);
|
||||
let gamma_correction = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, 3, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, GammaCorrectionInput::INDEX, true, context),
|
||||
NumberInput::default().min(0.01).max(9.99).increment_step(0.1),
|
||||
);
|
||||
|
||||
@@ -1235,6 +1316,8 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
|
||||
}
|
||||
|
||||
pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::generator_nodes::rectangle::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1242,21 +1325,15 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let size_x_index = 1;
|
||||
let size_y_index = 2;
|
||||
let corner_rounding_type_index = 3;
|
||||
let corner_radius_index = 4;
|
||||
let clamped_index = 5;
|
||||
|
||||
// Size X
|
||||
let size_x = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, size_x_index, true, context), NumberInput::default());
|
||||
let size_x = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, WidthInput::INDEX, true, context), NumberInput::default());
|
||||
|
||||
// Size Y
|
||||
let size_y = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, size_y_index, true, context), NumberInput::default());
|
||||
let size_y = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, HeightInput::INDEX, true, context), NumberInput::default());
|
||||
|
||||
// Corner Radius
|
||||
let mut corner_radius_row_1 = start_widgets(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, corner_radius_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, CornerRadiusInput::<f64>::INDEX, true, context),
|
||||
FrontendGraphDataType::Number,
|
||||
);
|
||||
corner_radius_row_1.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -1265,13 +1342,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
corner_radius_row_2.push(TextLabel::new("").widget_holder());
|
||||
add_blank_assist(&mut corner_radius_row_2);
|
||||
|
||||
let Some(input) = document_node.inputs.get(corner_rounding_type_index) else {
|
||||
let Some(input) = document_node.inputs.get(IndividualCornerRadiiInput::INDEX) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return vec![];
|
||||
};
|
||||
if let Some(&TaggedValue::Bool(is_individual)) = input.as_non_exposed_value() {
|
||||
// Values
|
||||
let Some(input) = document_node.inputs.get(corner_radius_index) else {
|
||||
let Some(input) = document_node.inputs.get(CornerRadiusInput::<f64>::INDEX) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return vec![];
|
||||
};
|
||||
@@ -1293,13 +1370,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
Message::Batched(Box::new([
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: corner_rounding_type_index,
|
||||
input_index: IndividualCornerRadiiInput::INDEX,
|
||||
value: TaggedValue::Bool(false),
|
||||
}
|
||||
.into(),
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: corner_radius_index,
|
||||
input_index: CornerRadiusInput::<f64>::INDEX,
|
||||
value: TaggedValue::F64(uniform_val),
|
||||
}
|
||||
.into(),
|
||||
@@ -1312,13 +1389,13 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
Message::Batched(Box::new([
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: corner_rounding_type_index,
|
||||
input_index: IndividualCornerRadiiInput::INDEX,
|
||||
value: TaggedValue::Bool(true),
|
||||
}
|
||||
.into(),
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: corner_radius_index,
|
||||
input_index: CornerRadiusInput::<f64>::INDEX,
|
||||
value: TaggedValue::F64Array4(individual_val),
|
||||
}
|
||||
.into(),
|
||||
@@ -1345,12 +1422,12 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
};
|
||||
TextInput::default()
|
||||
.value(individual_val.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
|
||||
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, corner_radius_index))
|
||||
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, CornerRadiusInput::<f64>::INDEX))
|
||||
.widget_holder()
|
||||
} else {
|
||||
NumberInput::default()
|
||||
.value(Some(uniform_val))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, corner_radius_index))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, CornerRadiusInput::<f64>::INDEX))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder()
|
||||
};
|
||||
@@ -1358,7 +1435,7 @@ pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodeProperties
|
||||
}
|
||||
|
||||
// Clamped
|
||||
let clamped = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, clamped_index, true, context), CheckboxInput::default());
|
||||
let clamped = bool_widget(ParameterWidgetsInfo::from_index(document_node, node_id, ClampedInput::INDEX, true, context), CheckboxInput::default());
|
||||
|
||||
vec![
|
||||
LayoutGroup::Row { widgets: size_x },
|
||||
@@ -1485,6 +1562,8 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
||||
|
||||
/// Fill Node Widgets LayoutGroup
|
||||
pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::fill::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1492,16 +1571,16 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let fill_index = 1;
|
||||
let backup_color_index = 2;
|
||||
let backup_gradient_index = 3;
|
||||
|
||||
let mut widgets_first_row = start_widgets(ParameterWidgetsInfo::from_index(document_node, node_id, fill_index, true, context), FrontendGraphDataType::General);
|
||||
let mut widgets_first_row = start_widgets(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, FillInput::<Color>::INDEX, true, context),
|
||||
FrontendGraphDataType::General,
|
||||
);
|
||||
|
||||
let (fill, backup_color, backup_gradient) = if let (Some(TaggedValue::Fill(fill)), &Some(&TaggedValue::OptionalColor(backup_color)), Some(TaggedValue::Gradient(backup_gradient))) = (
|
||||
&document_node.inputs[fill_index].as_value(),
|
||||
&document_node.inputs[backup_color_index].as_value(),
|
||||
&document_node.inputs[backup_gradient_index].as_value(),
|
||||
&document_node.inputs[FillInput::<Color>::INDEX].as_value(),
|
||||
&document_node.inputs[BackupColorInput::INDEX].as_value(),
|
||||
&document_node.inputs[BackupGradientInput::INDEX].as_value(),
|
||||
) {
|
||||
(fill, backup_color, backup_gradient)
|
||||
} else {
|
||||
@@ -1520,26 +1599,26 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
match &fill2 {
|
||||
Fill::None => NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: backup_color_index,
|
||||
input_index: BackupColorInput::INDEX,
|
||||
value: TaggedValue::OptionalColor(None),
|
||||
}
|
||||
.into(),
|
||||
Fill::Solid(color) => NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: backup_color_index,
|
||||
input_index: BackupColorInput::INDEX,
|
||||
value: TaggedValue::OptionalColor(Some(*color)),
|
||||
}
|
||||
.into(),
|
||||
Fill::Gradient(gradient) => NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: backup_gradient_index,
|
||||
input_index: BackupGradientInput::INDEX,
|
||||
value: TaggedValue::Gradient(gradient.clone()),
|
||||
}
|
||||
.into(),
|
||||
},
|
||||
NodeGraphMessage::SetInputValue {
|
||||
node_id,
|
||||
input_index: fill_index,
|
||||
input_index: FillInput::<Color>::INDEX,
|
||||
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
|
||||
}
|
||||
.into(),
|
||||
@@ -1567,7 +1646,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
fill_index,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_holder();
|
||||
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -1578,11 +1657,11 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
let entries = vec![
|
||||
RadioEntryData::new("solid")
|
||||
.label("Solid")
|
||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, fill_index))
|
||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_color_fill.clone()), node_id, FillInput::<Color>::INDEX))
|
||||
.on_commit(commit_value),
|
||||
RadioEntryData::new("gradient")
|
||||
.label("Gradient")
|
||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, fill_index))
|
||||
.on_update(update_value(move |_| TaggedValue::Fill(backup_gradient_fill.clone()), node_id, FillInput::<Color>::INDEX))
|
||||
.on_commit(commit_value),
|
||||
];
|
||||
|
||||
@@ -1617,7 +1696,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
}
|
||||
},
|
||||
node_id,
|
||||
fill_index,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.widget_holder();
|
||||
row.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
@@ -1638,7 +1717,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
||||
},
|
||||
node_id,
|
||||
fill_index,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.on_commit(commit_value),
|
||||
RadioEntryData::new("Radial")
|
||||
@@ -1650,7 +1729,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
TaggedValue::Fill(Fill::Gradient(new_gradient))
|
||||
},
|
||||
node_id,
|
||||
fill_index,
|
||||
FillInput::<Color>::INDEX,
|
||||
))
|
||||
.on_commit(commit_value),
|
||||
];
|
||||
@@ -1667,6 +1746,8 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
}
|
||||
|
||||
pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::stroke::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1674,34 +1755,28 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let color_index = graphene_std::vector::stroke::ColorInput::<Option<Color>>::INDEX;
|
||||
let weight_index = graphene_std::vector::stroke::WeightInput::INDEX;
|
||||
let align_index = graphene_std::vector::stroke::AlignInput::INDEX;
|
||||
let cap_index = graphene_std::vector::stroke::CapInput::INDEX;
|
||||
let join_index = graphene_std::vector::stroke::JoinInput::INDEX;
|
||||
let miter_limit_index = graphene_std::vector::stroke::MiterLimitInput::INDEX;
|
||||
let paint_order_index = graphene_std::vector::stroke::PaintOrderInput::INDEX;
|
||||
let dash_lengths_index = graphene_std::vector::stroke::DashLengthsInput::INDEX;
|
||||
let dash_offset_index = graphene_std::vector::stroke::DashOffsetInput::INDEX;
|
||||
|
||||
let color = color_widget(ParameterWidgetsInfo::from_index(document_node, node_id, color_index, true, context), ColorInput::default());
|
||||
let color = color_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, ColorInput::<Option<Color>>::INDEX, true, context),
|
||||
crate::messages::layout::utility_types::widgets::button_widgets::ColorInput::default(),
|
||||
);
|
||||
let weight = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, weight_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, WeightInput::INDEX, true, context),
|
||||
NumberInput::default().unit(" px").min(0.),
|
||||
);
|
||||
let align = enum_choice::<StrokeAlign>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, align_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, AlignInput::INDEX, true, context))
|
||||
.property_row();
|
||||
let cap = enum_choice::<StrokeCap>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, cap_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, CapInput::INDEX, true, context))
|
||||
.property_row();
|
||||
let join = enum_choice::<StrokeJoin>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, JoinInput::INDEX, true, context))
|
||||
.property_row();
|
||||
let miter_limit = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, MiterLimitInput::INDEX, true, context),
|
||||
NumberInput::default().min(0.).disabled({
|
||||
let join_value = match &document_node.inputs[join_index].as_value() {
|
||||
let join_value = match &document_node.inputs[JoinInput::INDEX].as_value() {
|
||||
Some(TaggedValue::StrokeJoin(x)) => x,
|
||||
_ => &StrokeJoin::Miter,
|
||||
};
|
||||
@@ -1709,18 +1784,18 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
}),
|
||||
);
|
||||
let paint_order = enum_choice::<PaintOrder>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, paint_order_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, PaintOrderInput::INDEX, true, context))
|
||||
.property_row();
|
||||
let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() {
|
||||
let dash_lengths_val = match &document_node.inputs[DashLengthsInput::INDEX].as_value() {
|
||||
Some(TaggedValue::VecF64(x)) => x,
|
||||
_ => &vec![],
|
||||
};
|
||||
let dash_lengths = array_of_number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, dash_lengths_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, DashLengthsInput::INDEX, true, context),
|
||||
TextInput::default().centered(true),
|
||||
);
|
||||
let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty());
|
||||
let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input);
|
||||
let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, DashOffsetInput::INDEX, true, context), number_input);
|
||||
|
||||
vec![
|
||||
color,
|
||||
@@ -1736,6 +1811,8 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
||||
}
|
||||
|
||||
pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::vector::offset_path::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1743,30 +1820,28 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let distance_index = graphene_std::vector::offset_path::DistanceInput::INDEX;
|
||||
let join_index = graphene_std::vector::offset_path::JoinInput::INDEX;
|
||||
let miter_limit_index = graphene_std::vector::offset_path::MiterLimitInput::INDEX;
|
||||
|
||||
let number_input = NumberInput::default().unit(" px");
|
||||
let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input);
|
||||
let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, DistanceInput::INDEX, true, context), number_input);
|
||||
|
||||
let join = enum_choice::<StrokeJoin>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context))
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, JoinInput::INDEX, true, context))
|
||||
.property_row();
|
||||
|
||||
let number_input = NumberInput::default().min(0.).disabled({
|
||||
let join_val = match &document_node.inputs[join_index].as_value() {
|
||||
let join_val = match &document_node.inputs[JoinInput::INDEX].as_value() {
|
||||
Some(TaggedValue::StrokeJoin(x)) => x,
|
||||
_ => &StrokeJoin::Miter,
|
||||
};
|
||||
join_val != &StrokeJoin::Miter
|
||||
});
|
||||
let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), number_input);
|
||||
let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, MiterLimitInput::INDEX, true, context), number_input);
|
||||
|
||||
vec![LayoutGroup::Row { widgets: distance }, join, LayoutGroup::Row { widgets: miter_limit }]
|
||||
}
|
||||
|
||||
pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
use graphene_std::math_nodes::math::*;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
@@ -1775,16 +1850,13 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
|
||||
}
|
||||
};
|
||||
|
||||
let expression_index = 1;
|
||||
let operation_b_index = 2;
|
||||
|
||||
let expression = (|| {
|
||||
let mut widgets = start_widgets(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, expression_index, true, context),
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, ExpressionInput::INDEX, true, context),
|
||||
FrontendGraphDataType::General,
|
||||
);
|
||||
|
||||
let Some(input) = document_node.inputs.get(expression_index) else {
|
||||
let Some(input) = document_node.inputs.get(ExpressionInput::INDEX) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return vec![];
|
||||
};
|
||||
@@ -1808,7 +1880,7 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
|
||||
})
|
||||
},
|
||||
node_id,
|
||||
expression_index,
|
||||
ExpressionInput::INDEX,
|
||||
))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
@@ -1816,7 +1888,10 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
|
||||
}
|
||||
widgets
|
||||
})();
|
||||
let operand_b = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, operation_b_index, true, context), NumberInput::default());
|
||||
let operand_b = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, OperandBInput::<f64>::INDEX, true, context),
|
||||
NumberInput::default(),
|
||||
);
|
||||
let operand_a_hint = vec![TextLabel::new("(Operand A is the primary input)").widget_holder()];
|
||||
|
||||
vec![
|
||||
@@ -1924,17 +1999,16 @@ pub mod choice {
|
||||
C: Fn(&()) -> Message + 'static + Send + Sync,
|
||||
{
|
||||
let items = E::list()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|group| {
|
||||
group
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|(item, metadata)| {
|
||||
let item = item.clone();
|
||||
let updater = updater_factory();
|
||||
let committer = committer_factory();
|
||||
MenuListEntry::new(metadata.name.as_ref())
|
||||
.label(metadata.label.as_ref())
|
||||
.on_update(move |_| updater(&item))
|
||||
.on_update(move |_| updater(item))
|
||||
.on_commit(committer)
|
||||
})
|
||||
.collect()
|
||||
@@ -1949,14 +2023,12 @@ pub mod choice {
|
||||
C: Fn(&()) -> Message + 'static + Send + Sync,
|
||||
{
|
||||
let items = E::list()
|
||||
.into_iter()
|
||||
.map(|group| group.into_iter())
|
||||
.flatten()
|
||||
.iter()
|
||||
.flat_map(|group| group.iter())
|
||||
.map(|(item, var_meta)| {
|
||||
let item = item.clone();
|
||||
let updater = updater_factory();
|
||||
let committer = committer_factory();
|
||||
let entry = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(&item)).on_commit(committer);
|
||||
let entry = RadioEntryData::new(var_meta.name.as_ref()).on_update(move |_| updater(item)).on_commit(committer);
|
||||
match (var_meta.icon.as_deref(), var_meta.docstring.as_deref()) {
|
||||
(None, None) => entry.label(var_meta.label.as_ref()),
|
||||
(None, Some(doc)) => entry.label(var_meta.label.as_ref()).tooltip(doc),
|
||||
@@ -2026,7 +2098,7 @@ pub mod choice {
|
||||
return LayoutGroup::Row { widgets: vec![] };
|
||||
};
|
||||
|
||||
let input: Option<W::Value> = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).map(Clone::clone);
|
||||
let input: Option<W::Value> = input.as_non_exposed_value().and_then(|v| <&W::Value as TryFrom<&TaggedValue>>::try_from(v).ok()).cloned();
|
||||
|
||||
if let Some(current) = input {
|
||||
let committer = || super::commit_value;
|
||||
|
||||
@@ -1,547 +0,0 @@
|
||||
//! This has all been copied out of node_properties.rs to avoid leaving hundreds of lines of commented out code in that file. It's left here instead for future reference.
|
||||
|
||||
// pub fn imaginate_sampling_method(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
|
||||
// let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
// vec![
|
||||
// DropdownInput::new(
|
||||
// ImaginateSamplingMethod::list()
|
||||
// .into_iter()
|
||||
// .map(|method| {
|
||||
// vec![
|
||||
// MenuListEntry::new(format!("{:?}", method))
|
||||
// .label(method.to_string())
|
||||
// .on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, index)),
|
||||
// ]
|
||||
// })
|
||||
// .collect(),
|
||||
// )
|
||||
// .widget_holder(),
|
||||
// ]
|
||||
// .into()
|
||||
// }
|
||||
|
||||
// pub fn imaginate_mask_starting_fill(parameter_widgets_info: ParameterWidgetsInfo) -> LayoutGroup {
|
||||
// let ParameterWidgetsInfo { node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
// vec![
|
||||
// DropdownInput::new(
|
||||
// ImaginateMaskStartingFill::list()
|
||||
// .into_iter()
|
||||
// .map(|fill| {
|
||||
// vec![
|
||||
// MenuListEntry::new(format!("{:?}", fill))
|
||||
// .label(fill.to_string())
|
||||
// .on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(fill), node_id, index)),
|
||||
// ]
|
||||
// })
|
||||
// .collect(),
|
||||
// )
|
||||
// .widget_holder(),
|
||||
// ]
|
||||
// .into()
|
||||
// }
|
||||
|
||||
// pub(crate) fn imaginate_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
// let imaginate_node = [context.selection_network_path, &[node_id]].concat();
|
||||
|
||||
// let resolve_input = |name: &str| {
|
||||
// IMAGINATE_NODE
|
||||
// .default_node_template()
|
||||
// .persistent_node_metadata
|
||||
// .input_properties
|
||||
// .iter()
|
||||
// .position(|row| row.input_name.as_str() == name)
|
||||
// .unwrap_or_else(|| panic!("Input {name} not found"))
|
||||
// };
|
||||
// let seed_index = resolve_input("Seed");
|
||||
// let resolution_index = resolve_input("Resolution");
|
||||
// let samples_index = resolve_input("Samples");
|
||||
// let sampling_method_index = resolve_input("Sampling Method");
|
||||
// let text_guidance_index = resolve_input("Prompt Guidance");
|
||||
// let text_index = resolve_input("Prompt");
|
||||
// let neg_index = resolve_input("Negative Prompt");
|
||||
// let base_img_index = resolve_input("Adapt Input Image");
|
||||
// let img_creativity_index = resolve_input("Image Creativity");
|
||||
// // let mask_index = resolve_input("Masking Layer");
|
||||
// // let inpaint_index = resolve_input("Inpaint");
|
||||
// // let mask_blur_index = resolve_input("Mask Blur");
|
||||
// // let mask_fill_index = resolve_input("Mask Starting Fill");
|
||||
// let faces_index = resolve_input("Improve Faces");
|
||||
// let tiling_index = resolve_input("Tiling");
|
||||
|
||||
// let document_node = match get_document_node(node_id, context) {
|
||||
// Ok(document_node) => document_node,
|
||||
// Err(err) => {
|
||||
// log::error!("Could not get document node in imaginate_properties: {err}");
|
||||
// return Vec::new();
|
||||
// }
|
||||
// };
|
||||
// let controller = &document_node.inputs[resolve_input("Controller")];
|
||||
|
||||
// let server_status = {
|
||||
// let server_status = context.persistent_data.imaginate.server_status();
|
||||
// let status_text = server_status.to_text();
|
||||
// let mut widgets = vec![
|
||||
// TextLabel::new("Server").widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// IconButton::new("Settings", 24)
|
||||
// .tooltip("Preferences: Imaginate")
|
||||
// .on_update(|_| DialogMessage::RequestPreferencesDialog.into())
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// TextLabel::new(status_text).bold(true).widget_holder(),
|
||||
// Separator::new(SeparatorType::Related).widget_holder(),
|
||||
// IconButton::new("Reload", 24)
|
||||
// .tooltip("Refresh connection status")
|
||||
// .on_update(|_| PortfolioMessage::ImaginateCheckServerStatus.into())
|
||||
// .widget_holder(),
|
||||
// ];
|
||||
// if let ImaginateServerStatus::Unavailable | ImaginateServerStatus::Failed(_) = server_status {
|
||||
// widgets.extend([
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// TextButton::new("Server Help")
|
||||
// .tooltip("Learn how to connect Imaginate to an image generation server")
|
||||
// .on_update(|_| {
|
||||
// FrontendMessage::TriggerVisitLink {
|
||||
// url: "https://github.com/GraphiteEditor/Graphite/discussions/1089".to_string(),
|
||||
// }
|
||||
// .into()
|
||||
// })
|
||||
// .widget_holder(),
|
||||
// ]);
|
||||
// }
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Connection status to the server that computes generated images")
|
||||
// };
|
||||
|
||||
// let Some(TaggedValue::ImaginateController(controller)) = controller.as_value() else {
|
||||
// panic!("Invalid output status input")
|
||||
// };
|
||||
// let imaginate_status = controller.get_status();
|
||||
|
||||
// let use_base_image = if let Some(&TaggedValue::Bool(use_base_image)) = &document_node.inputs[base_img_index].as_value() {
|
||||
// use_base_image
|
||||
// } else {
|
||||
// true
|
||||
// };
|
||||
|
||||
// let transform_not_connected = false;
|
||||
|
||||
// let progress = {
|
||||
// let mut widgets = vec![TextLabel::new("Progress").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
|
||||
// add_blank_assist(&mut widgets);
|
||||
// let status = imaginate_status.to_text();
|
||||
// widgets.push(TextLabel::new(status.as_ref()).bold(true).widget_holder());
|
||||
// LayoutGroup::Row { widgets }.with_tooltip(match imaginate_status {
|
||||
// ImaginateStatus::Failed(_) => status.as_ref(),
|
||||
// _ => "When generating, the percentage represents how many sampling steps have so far been processed out of the target number",
|
||||
// })
|
||||
// };
|
||||
|
||||
// let image_controls = {
|
||||
// let mut widgets = vec![TextLabel::new("Image").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder()];
|
||||
|
||||
// match &imaginate_status {
|
||||
// ImaginateStatus::Beginning | ImaginateStatus::Uploading => {
|
||||
// add_blank_assist(&mut widgets);
|
||||
// widgets.push(TextButton::new("Beginning...").tooltip("Sending image generation request to the server").disabled(true).widget_holder());
|
||||
// }
|
||||
// ImaginateStatus::Generating(_) => {
|
||||
// add_blank_assist(&mut widgets);
|
||||
// widgets.push(
|
||||
// TextButton::new("Terminate")
|
||||
// .tooltip("Cancel the in-progress image generation and keep the latest progress")
|
||||
// .on_update({
|
||||
// let controller = controller.clone();
|
||||
// move |_| {
|
||||
// controller.request_termination();
|
||||
// Message::NoOp
|
||||
// }
|
||||
// })
|
||||
// .widget_holder(),
|
||||
// );
|
||||
// }
|
||||
// ImaginateStatus::Terminating => {
|
||||
// add_blank_assist(&mut widgets);
|
||||
// widgets.push(
|
||||
// TextButton::new("Terminating...")
|
||||
// .tooltip("Waiting on the final image generated after termination")
|
||||
// .disabled(true)
|
||||
// .widget_holder(),
|
||||
// );
|
||||
// }
|
||||
// ImaginateStatus::Ready | ImaginateStatus::ReadyDone | ImaginateStatus::Terminated | ImaginateStatus::Failed(_) => widgets.extend_from_slice(&[
|
||||
// IconButton::new("Random", 24)
|
||||
// .tooltip("Generate with a new random seed")
|
||||
// .on_update({
|
||||
// let imaginate_node = imaginate_node.clone();
|
||||
// let controller = controller.clone();
|
||||
// move |_| {
|
||||
// controller.trigger_regenerate();
|
||||
// DocumentMessage::ImaginateRandom {
|
||||
// imaginate_node: imaginate_node.clone(),
|
||||
// then_generate: true,
|
||||
// }
|
||||
// .into()
|
||||
// }
|
||||
// })
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// TextButton::new("Generate")
|
||||
// .tooltip("Fill layer frame by generating a new image")
|
||||
// .on_update({
|
||||
// let controller = controller.clone();
|
||||
// let imaginate_node = imaginate_node.clone();
|
||||
// move |_| {
|
||||
// controller.trigger_regenerate();
|
||||
// DocumentMessage::ImaginateGenerate {
|
||||
// imaginate_node: imaginate_node.clone(),
|
||||
// }
|
||||
// .into()
|
||||
// }
|
||||
// })
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Related).widget_holder(),
|
||||
// TextButton::new("Clear")
|
||||
// .tooltip("Remove generated image from the layer frame")
|
||||
// .disabled(!matches!(imaginate_status, ImaginateStatus::ReadyDone))
|
||||
// .on_update({
|
||||
// let controller = controller.clone();
|
||||
// let imaginate_node = imaginate_node.clone();
|
||||
// move |_| {
|
||||
// controller.set_status(ImaginateStatus::Ready);
|
||||
// DocumentMessage::ImaginateGenerate {
|
||||
// imaginate_node: imaginate_node.clone(),
|
||||
// }
|
||||
// .into()
|
||||
// }
|
||||
// })
|
||||
// .widget_holder(),
|
||||
// ]),
|
||||
// }
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Buttons that control the image generation process")
|
||||
// };
|
||||
|
||||
// // Requires custom layout for the regenerate button
|
||||
// let seed = {
|
||||
// let mut widgets = start_widgets(document_node, node_id, seed_index, "Seed", FrontendGraphDataType::Number, false);
|
||||
|
||||
// let Some(input) = document_node.inputs.get(seed_index) else {
|
||||
// log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
// return vec![];
|
||||
// };
|
||||
// if let Some(&TaggedValue::F64(seed)) = &input.as_non_exposed_value() {
|
||||
// widgets.extend_from_slice(&[
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// IconButton::new("Resync", 24)
|
||||
// .tooltip("Set a new random seed")
|
||||
// .on_update({
|
||||
// let imaginate_node = imaginate_node.clone();
|
||||
// move |_| {
|
||||
// DocumentMessage::ImaginateRandom {
|
||||
// imaginate_node: imaginate_node.clone(),
|
||||
// then_generate: false,
|
||||
// }
|
||||
// .into()
|
||||
// }
|
||||
// })
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// NumberInput::new(Some(seed))
|
||||
// .int()
|
||||
// .min(-((1_u64 << f64::MANTISSA_DIGITS) as f64))
|
||||
// .max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
// .on_update(update_value(move |input: &NumberInput| TaggedValue::F64(input.value.unwrap()), node_id, seed_index))
|
||||
// .on_commit(commit_value)
|
||||
// .mode(NumberInputMode::Increment)
|
||||
// .widget_holder(),
|
||||
// ])
|
||||
// }
|
||||
// // Note: Limited by f64. You cannot even have all the possible u64 values :)
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Seed determines the random outcome, enabling limitless unique variations")
|
||||
// };
|
||||
|
||||
// // let transform = context
|
||||
// // .executor
|
||||
// // .introspect_node_in_network(context.network, &imaginate_node, |network| network.inputs.first().copied(), |frame: &RasterData<Color>| frame.transform)
|
||||
// // .unwrap_or_default();
|
||||
// let image_size = context
|
||||
// .executor
|
||||
// .introspect_node_in_network(
|
||||
// context.network_interface.document_network().unwrap(),
|
||||
// &imaginate_node,
|
||||
// |network| {
|
||||
// network
|
||||
// .nodes
|
||||
// .iter()
|
||||
// .find(|node| {
|
||||
// node.1
|
||||
// .inputs
|
||||
// .iter()
|
||||
// .any(|node_input| if let NodeInput::Network { import_index, .. } = node_input { *import_index == 0 } else { false })
|
||||
// })
|
||||
// .map(|(node_id, _)| node_id)
|
||||
// .copied()
|
||||
// },
|
||||
// |frame: &IORecord<(), RasterData<Color>>| (frame.output.image.width, frame.output.image.height),
|
||||
// )
|
||||
// .unwrap_or_default();
|
||||
|
||||
// let document_node = match get_document_node(node_id, context) {
|
||||
// Ok(document_node) => document_node,
|
||||
// Err(err) => {
|
||||
// log::error!("Could not get document node in imaginate_properties: {err}");
|
||||
// return Vec::new();
|
||||
// }
|
||||
// };
|
||||
|
||||
// let resolution = {
|
||||
// let mut widgets = start_widgets(document_node, node_id, resolution_index, "Resolution", FrontendGraphDataType::Number, false);
|
||||
|
||||
// let round = |size: DVec2| {
|
||||
// let (x, y) = graphene_std::imaginate::pick_safe_imaginate_resolution(size.into());
|
||||
// DVec2::new(x as f64, y as f64)
|
||||
// };
|
||||
|
||||
// let Some(input) = document_node.inputs.get(resolution_index) else {
|
||||
// log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
// return vec![];
|
||||
// };
|
||||
// if let Some(&TaggedValue::OptionalDVec2(vec2)) = &input.as_non_exposed_value() {
|
||||
// let dimensions_is_auto = vec2.is_none();
|
||||
// let vec2 = vec2.unwrap_or_else(|| round((image_size.0 as f64, image_size.1 as f64).into()));
|
||||
|
||||
// widgets.extend_from_slice(&[
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// IconButton::new("FrameAll", 24)
|
||||
// .tooltip("Set the layer dimensions to this resolution")
|
||||
// .on_update(move |_| DialogMessage::RequestComingSoonDialog { issue: None }.into())
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// CheckboxInput::new(!dimensions_is_auto || transform_not_connected)
|
||||
// .icon("Edit12px")
|
||||
// .tooltip({
|
||||
// let message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64)";
|
||||
// let manual_message = "Set a custom resolution instead of using the input's dimensions (rounded to the nearest 64).\n\
|
||||
// \n\
|
||||
// (Resolution must be set manually while the 'Transform' input is disconnected.)";
|
||||
|
||||
// if transform_not_connected {
|
||||
// manual_message
|
||||
// } else {
|
||||
// message
|
||||
// }
|
||||
// })
|
||||
// .disabled(transform_not_connected)
|
||||
// .on_update(update_value(
|
||||
// move |checkbox_input: &CheckboxInput| TaggedValue::OptionalDVec2(if checkbox_input.checked { Some(vec2) } else { None }),
|
||||
// node_id,
|
||||
// resolution_index,
|
||||
// ))
|
||||
// .on_commit(commit_value)
|
||||
// .for_label(checkbox_id.clone())
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Related).widget_holder(),
|
||||
// NumberInput::new(Some(vec2.x))
|
||||
// .label("W")
|
||||
// .min(64.)
|
||||
// .step(64.)
|
||||
// .unit(" px")
|
||||
// .disabled(dimensions_is_auto && !transform_not_connected)
|
||||
// .on_update(update_value(
|
||||
// move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(number_input.value.unwrap(), vec2.y)))),
|
||||
// node_id,
|
||||
// resolution_index,
|
||||
// ))
|
||||
// .on_commit(commit_value)
|
||||
// .widget_holder(),
|
||||
// Separator::new(SeparatorType::Related).widget_holder(),
|
||||
// NumberInput::new(Some(vec2.y))
|
||||
// .label("H")
|
||||
// .min(64.)
|
||||
// .step(64.)
|
||||
// .unit(" px")
|
||||
// .disabled(dimensions_is_auto && !transform_not_connected)
|
||||
// .on_update(update_value(
|
||||
// move |number_input: &NumberInput| TaggedValue::OptionalDVec2(Some(round(DVec2::new(vec2.x, number_input.value.unwrap())))),
|
||||
// node_id,
|
||||
// resolution_index,
|
||||
// ))
|
||||
// .on_commit(commit_value)
|
||||
// .widget_holder(),
|
||||
// ])
|
||||
// }
|
||||
// LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// "Width and height of the image that will be generated. Larger resolutions take longer to compute.\n\
|
||||
// \n\
|
||||
// 512x512 yields optimal results because the AI is trained to understand that scale best. Larger sizes may tend to integrate the prompt's subject more than once. Small sizes are often incoherent.\n\
|
||||
// \n\
|
||||
// Dimensions must be a multiple of 64, so these are set by rounding the layer dimensions. A resolution exceeding 1 megapixel is reduced below that limit because larger sizes may exceed available GPU memory on the server.")
|
||||
// };
|
||||
|
||||
// let sampling_steps = {
|
||||
// let widgets = number_widget(document_node, node_id, samples_index, "Sampling Steps", NumberInput::default().min(0.).max(150.).int(), true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Number of iterations to improve the image generation quality, with diminishing returns around 40 when using the Euler A sampling method")
|
||||
// };
|
||||
|
||||
// let sampling_method = {
|
||||
// let mut widgets = start_widgets(document_node, node_id, sampling_method_index, "Sampling Method", FrontendGraphDataType::General, true);
|
||||
|
||||
// let Some(input) = document_node.inputs.get(sampling_method_index) else {
|
||||
// log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
// return vec![];
|
||||
// };
|
||||
// if let Some(&TaggedValue::ImaginateSamplingMethod(sampling_method)) = &input.as_non_exposed_value() {
|
||||
// let sampling_methods = ImaginateSamplingMethod::list();
|
||||
// let mut entries = Vec::with_capacity(sampling_methods.len());
|
||||
// for method in sampling_methods {
|
||||
// entries.push(
|
||||
// MenuListEntry::new(format!("{method:?}"))
|
||||
// .label(method.to_string())
|
||||
// .on_update(update_value(move |_| TaggedValue::ImaginateSamplingMethod(method), node_id, sampling_method_index))
|
||||
// .on_commit(commit_value),
|
||||
// );
|
||||
// }
|
||||
// let entries = vec![entries];
|
||||
|
||||
// widgets.extend_from_slice(&[
|
||||
// Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// DropdownInput::new(entries).selected_index(Some(sampling_method as u32)).widget_holder(),
|
||||
// ]);
|
||||
// }
|
||||
// LayoutGroup::Row { widgets }.with_tooltip("Algorithm used to generate the image during each sampling step")
|
||||
// };
|
||||
|
||||
// let text_guidance = {
|
||||
// let widgets = number_widget(document_node, node_id, text_guidance_index, "Prompt Guidance", NumberInput::default().min(0.).max(30.), true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// "Amplification of the text prompt's influence over the outcome. At 0, the prompt is entirely ignored.\n\
|
||||
// \n\
|
||||
// Lower values are more creative and exploratory. Higher values are more literal and uninspired.\n\
|
||||
// \n\
|
||||
// This parameter is otherwise known as CFG (classifier-free guidance).",
|
||||
// )
|
||||
// };
|
||||
|
||||
// let text_prompt = {
|
||||
// let widgets = text_area_widget(document_node, node_id, text_index, "Prompt", true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// "Description of the desired image subject and style.\n\
|
||||
// \n\
|
||||
// Include an artist name like \"Rembrandt\" or art medium like \"watercolor\" or \"photography\" to influence the look. List multiple to meld styles.\n\
|
||||
// \n\
|
||||
// To boost (or lessen) the importance of a word or phrase, wrap it in parentheses ending with a colon and a multiplier, for example:\n\
|
||||
// \"Colorless green ideas (sleep:1.3) furiously\"",
|
||||
// )
|
||||
// };
|
||||
// let negative_prompt = {
|
||||
// let widgets = text_area_widget(document_node, node_id, neg_index, "Negative Prompt", true);
|
||||
// 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().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 = {
|
||||
// let props = NumberInput::default().percentage().disabled(!use_base_image);
|
||||
// let widgets = number_widget(document_node, node_id, img_creativity_index, "Image Creativity", props, true);
|
||||
// LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// "Strength of the artistic liberties allowing changes from the input image. The image is unchanged at 0% and completely different at 100%.\n\
|
||||
// \n\
|
||||
// This parameter is otherwise known as denoising strength.",
|
||||
// )
|
||||
// };
|
||||
|
||||
// let mut layout = vec![
|
||||
// server_status,
|
||||
// progress,
|
||||
// image_controls,
|
||||
// seed,
|
||||
// resolution,
|
||||
// sampling_steps,
|
||||
// sampling_method,
|
||||
// text_guidance,
|
||||
// text_prompt,
|
||||
// negative_prompt,
|
||||
// base_image,
|
||||
// image_creativity,
|
||||
// // layer_mask,
|
||||
// ];
|
||||
|
||||
// // if use_base_image && layer_reference_input_layer_is_some {
|
||||
// // let in_paint = {
|
||||
// // let mut widgets = start_widgets(document_node, node_id, inpaint_index, "Inpaint", FrontendGraphDataType::Boolean, true);
|
||||
|
||||
// // if let Some(& TaggedValue::Bool(in_paint)
|
||||
// //)/ } = &document_node.inputs[inpaint_index].as_non_exposed_value()
|
||||
// // {
|
||||
// // widgets.extend_from_slice(&[
|
||||
// // Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// // RadioInput::new(
|
||||
// // [(true, "Inpaint"), (false, "Outpaint")]
|
||||
// // .into_iter()
|
||||
// // .map(|(paint, name)| RadioEntryData::new(name).label(name).on_update(update_value(move |_| TaggedValue::Bool(paint), node_id, inpaint_index)))
|
||||
// // .collect(),
|
||||
// // )
|
||||
// // .selected_index(Some(1 - in_paint as u32))
|
||||
// // .widget_holder(),
|
||||
// // ]);
|
||||
// // }
|
||||
// // LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// // "Constrain image generation to the interior (inpaint) or exterior (outpaint) of the mask, while referencing the other unchanged parts as context imagery.\n\
|
||||
// // \n\
|
||||
// // An unwanted part of an image can be replaced by drawing around it with a black shape and inpainting with that mask layer.\n\
|
||||
// // \n\
|
||||
// // An image can be uncropped by resizing the Imaginate layer to the target bounds and outpainting with a black rectangle mask matching the original image bounds.",
|
||||
// // )
|
||||
// // };
|
||||
|
||||
// // let blur_radius = {
|
||||
// // let number_props = NumberInput::default().unit(" px").min(0.).max(25.).int();
|
||||
// // let widgets = number_widget(document_node, node_id, mask_blur_index, "Mask Blur", number_props, true);
|
||||
// // LayoutGroup::Row { widgets }.with_tooltip("Blur radius for the mask. Useful for softening sharp edges to blend the masked area with the rest of the image.")
|
||||
// // };
|
||||
|
||||
// // let mask_starting_fill = {
|
||||
// // let mut widgets = start_widgets(document_node, node_id, mask_fill_index, "Mask Starting Fill", FrontendGraphDataType::General, true);
|
||||
|
||||
// // if let Some(& TaggedValue::ImaginateMaskStartingFill(starting_fill)
|
||||
// //)/ } = &document_node.inputs[mask_fill_index].as_non_exposed_value()
|
||||
// // {
|
||||
// // let mask_fill_content_modes = ImaginateMaskStartingFill::list();
|
||||
// // let mut entries = Vec::with_capacity(mask_fill_content_modes.len());
|
||||
// // for mode in mask_fill_content_modes {
|
||||
// // entries.push(MenuListEntry::new(format!("{mode:?}")).label(mode.to_string()).on_update(update_value(move |_| TaggedValue::ImaginateMaskStartingFill(mode), node_id, mask_fill_index)));
|
||||
// // }
|
||||
// // let entries = vec![entries];
|
||||
|
||||
// // widgets.extend_from_slice(&[
|
||||
// // Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
// // DropdownInput::new(entries).selected_index(Some(starting_fill as u32)).widget_holder(),
|
||||
// // ]);
|
||||
// // }
|
||||
// // LayoutGroup::Row { widgets }.with_tooltip(
|
||||
// // "Begin in/outpainting the masked areas using this fill content as the starting input image.\n\
|
||||
// // \n\
|
||||
// // Each option can be visualized by generating with 'Sampling Steps' set to 0.",
|
||||
// // )
|
||||
// // };
|
||||
// // layout.extend_from_slice(&[in_paint, blur_radius, mask_starting_fill]);
|
||||
// // }
|
||||
|
||||
// let improve_faces = {
|
||||
// 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\
|
||||
// This filter can be used on its own by enabling 'Adapt Input Image' and setting 'Sampling Steps' to 0.",
|
||||
// )
|
||||
// };
|
||||
// let tiling = {
|
||||
// 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]);
|
||||
|
||||
// layout
|
||||
// }
|
||||
@@ -2,10 +2,19 @@ use super::utility_types::{OverlayProvider, empty_provider};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
#[impl_message(Message, DocumentMessage, Overlays)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub enum OverlaysMessage {
|
||||
Draw,
|
||||
// Serde functionality isn't used but is required by the message system macros
|
||||
AddProvider(#[serde(skip, default = "empty_provider")] OverlayProvider),
|
||||
RemoveProvider(#[serde(skip, default = "empty_provider")] OverlayProvider),
|
||||
AddProvider(
|
||||
#[serde(skip, default = "empty_provider")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayProvider,
|
||||
),
|
||||
RemoveProvider(
|
||||
#[serde(skip, default = "empty_provider")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
OverlayProvider,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -124,8 +124,19 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle
|
||||
overlay_context.outline_vector(&vector_data, transform);
|
||||
}
|
||||
|
||||
// Get the selected segments and then add a bold line overlay on them
|
||||
for (segment_id, bezier, _, _) in vector_data.segment_bezier_iter() {
|
||||
let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if selected_shape_state.is_segment_selected(segment_id) {
|
||||
overlay_context.outline_select_bezier(bezier, transform);
|
||||
}
|
||||
}
|
||||
|
||||
let selected = shape_editor.selected_shape_state.get(&layer);
|
||||
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
|
||||
let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
|
||||
|
||||
if display_handles {
|
||||
let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector_data.adjacent_segment(point_id)).collect();
|
||||
@@ -187,7 +198,7 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
|
||||
//let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let selected = shape_editor.selected_shape_state.get(&layer);
|
||||
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
|
||||
let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_point_selected(point));
|
||||
|
||||
for point in vector_data.extendable_points(preferences.vector_meshes) {
|
||||
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::utility_functions::overlay_canvas_context;
|
||||
use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER,
|
||||
COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER,
|
||||
COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
@@ -9,8 +9,8 @@ use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::renderer::ClickTargetType;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorData};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
@@ -581,6 +581,35 @@ impl OverlayContext {
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
/// Used by the path tool segment mode in order to show the selected segments.
|
||||
pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.bezier_command(bezier, transform, true);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.set_line_width(4.);
|
||||
self.render_context.stroke();
|
||||
|
||||
self.render_context.set_line_width(1.);
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.bezier_command(bezier, transform, true);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
|
||||
self.render_context.set_line_width(4.);
|
||||
self.render_context.stroke();
|
||||
|
||||
self.render_context.set_line_width(1.);
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::{PointId, VectorData};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::num::NonZeroU64;
|
||||
@@ -294,7 +295,7 @@ impl LayerNodeIdentifier {
|
||||
}
|
||||
|
||||
/// Iterator over all direct children (excluding self and recursive children)
|
||||
pub fn children(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
pub fn children(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
|
||||
AxisIter {
|
||||
layer_node: self.first_child(metadata),
|
||||
next_node: Self::next_sibling,
|
||||
@@ -302,7 +303,7 @@ impl LayerNodeIdentifier {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn downstream_siblings(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
pub fn downstream_siblings(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
|
||||
AxisIter {
|
||||
layer_node: Some(self),
|
||||
next_node: Self::previous_sibling,
|
||||
@@ -311,7 +312,7 @@ impl LayerNodeIdentifier {
|
||||
}
|
||||
|
||||
/// All ancestors of this layer, including self, going to the document root
|
||||
pub fn ancestors(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
pub fn ancestors(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
|
||||
AxisIter {
|
||||
layer_node: Some(self),
|
||||
next_node: Self::parent,
|
||||
@@ -320,7 +321,7 @@ impl LayerNodeIdentifier {
|
||||
}
|
||||
|
||||
/// Iterator through all the last children, starting from self
|
||||
pub fn last_children(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
pub fn last_children(self, metadata: &DocumentMetadata) -> AxisIter<'_> {
|
||||
AxisIter {
|
||||
layer_node: Some(self),
|
||||
next_node: Self::last_child,
|
||||
@@ -329,7 +330,7 @@ impl LayerNodeIdentifier {
|
||||
}
|
||||
|
||||
/// Iterator through all descendants, including recursive children (not including self)
|
||||
pub fn descendants(self, metadata: &DocumentMetadata) -> DescendantsIter {
|
||||
pub fn descendants(self, metadata: &DocumentMetadata) -> DescendantsIter<'_> {
|
||||
DescendantsIter {
|
||||
front: self.first_child(metadata),
|
||||
back: self.last_child(metadata).and_then(|child| child.last_children(metadata).last()),
|
||||
|
||||
@@ -693,5 +693,5 @@ impl PTZ {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GroupFolderType {
|
||||
Layer,
|
||||
BooleanOperation(graphene_std::vector::misc::BooleanOperation),
|
||||
BooleanOperation(graphene_std::path_bool::BooleanOperation),
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
|
||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
|
||||
use interpreted_executor::node_registry::NODE_REGISTRY;
|
||||
@@ -6515,6 +6516,12 @@ pub struct NodePersistentMetadata {
|
||||
position: NodePosition,
|
||||
}
|
||||
|
||||
impl NodePersistentMetadata {
|
||||
pub fn new(position: NodePosition) -> Self {
|
||||
Self { position }
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer can either be position as Absolute or in a Stack
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum LayerPosition {
|
||||
|
||||
@@ -9,9 +9,7 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::ManipulatorPointId;
|
||||
use graphene_std::vector::VectorModificationType;
|
||||
use graphene_std::vector::{HandleId, PointId};
|
||||
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, VectorModificationType};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
@@ -88,6 +86,18 @@ impl OriginalTransforms {
|
||||
let Some(selected_points) = shape_editor.selected_points_in_layer(layer) else {
|
||||
continue;
|
||||
};
|
||||
let Some(selected_segments) = shape_editor.selected_segments_in_layer(layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut selected_points = selected_points.clone();
|
||||
|
||||
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
if selected_segments.contains(&segment_id) {
|
||||
selected_points.insert(ManipulatorPointId::Anchor(start));
|
||||
selected_points.insert(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
}
|
||||
|
||||
// Anchors also move their handles
|
||||
let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor());
|
||||
@@ -604,7 +614,7 @@ impl<'a> Selected<'a> {
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
|
||||
if transform_operation.is_some_and(|transform_operation| matches!(transform_operation, TransformOperation::Scaling(_))) && initial_points.anchors.len() > 1 {
|
||||
if transform_operation.is_some_and(|transform_operation| matches!(transform_operation, TransformOperation::Scaling(_))) && (initial_points.anchors.len() == 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// This file contains lots of hacky code for upgrading old documents to the new format
|
||||
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||
use crate::messages::prelude::DocumentMessageHandler;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::IVec2;
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue};
|
||||
use graphene_std::text::TypesettingConfig;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
|
||||
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
|
||||
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
|
||||
];
|
||||
|
||||
const REPLACEMENTS: &[(&str, &str)] = &[
|
||||
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
|
||||
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
|
||||
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
|
||||
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
|
||||
("graphene_core::ops::MathNode", "graphene_math_nodes::MathNode"),
|
||||
("graphene_core::ops::AddNode", "graphene_math_nodes::AddNode"),
|
||||
("graphene_core::ops::SubtractNode", "graphene_math_nodes::SubtractNode"),
|
||||
("graphene_core::ops::MultiplyNode", "graphene_math_nodes::MultiplyNode"),
|
||||
("graphene_core::ops::DivideNode", "graphene_math_nodes::DivideNode"),
|
||||
("graphene_core::ops::ModuloNode", "graphene_math_nodes::ModuloNode"),
|
||||
("graphene_core::ops::ExponentNode", "graphene_math_nodes::ExponentNode"),
|
||||
("graphene_core::ops::RootNode", "graphene_math_nodes::RootNode"),
|
||||
("graphene_core::ops::LogarithmNode", "graphene_math_nodes::LogarithmNode"),
|
||||
("graphene_core::ops::SineNode", "graphene_math_nodes::SineNode"),
|
||||
("graphene_core::ops::CosineNode", "graphene_math_nodes::CosineNode"),
|
||||
("graphene_core::ops::TangentNode", "graphene_math_nodes::TangentNode"),
|
||||
("graphene_core::ops::SineInverseNode", "graphene_math_nodes::SineInverseNode"),
|
||||
("graphene_core::ops::CosineInverseNode", "graphene_math_nodes::CosineInverseNode"),
|
||||
("graphene_core::ops::TangentInverseNode", "graphene_math_nodes::TangentInverseNode"),
|
||||
("graphene_core::ops::RandomNode", "graphene_math_nodes::RandomNode"),
|
||||
("graphene_core::ops::ToU32Node", "graphene_math_nodes::ToU32Node"),
|
||||
("graphene_core::ops::ToU64Node", "graphene_math_nodes::ToU64Node"),
|
||||
("graphene_core::ops::ToF64Node", "graphene_math_nodes::ToF64Node"),
|
||||
("graphene_core::ops::RoundNode", "graphene_math_nodes::RoundNode"),
|
||||
("graphene_core::ops::FloorNode", "graphene_math_nodes::FloorNode"),
|
||||
("graphene_core::ops::CeilingNode", "graphene_math_nodes::CeilingNode"),
|
||||
("graphene_core::ops::MinNode", "graphene_math_nodes::MinNode"),
|
||||
("graphene_core::ops::MaxNode", "graphene_math_nodes::MaxNode"),
|
||||
("graphene_core::ops::ClampNode", "graphene_math_nodes::ClampNode"),
|
||||
("graphene_core::ops::EqualsNode", "graphene_math_nodes::EqualsNode"),
|
||||
("graphene_core::ops::NotEqualsNode", "graphene_math_nodes::NotEqualsNode"),
|
||||
("graphene_core::ops::LessThanNode", "graphene_math_nodes::LessThanNode"),
|
||||
("graphene_core::ops::GreaterThanNode", "graphene_math_nodes::GreaterThanNode"),
|
||||
("graphene_core::ops::LogicalOrNode", "graphene_math_nodes::LogicalOrNode"),
|
||||
("graphene_core::ops::LogicalAndNode", "graphene_math_nodes::LogicalAndNode"),
|
||||
("graphene_core::ops::LogicalNotNode", "graphene_math_nodes::LogicalNotNode"),
|
||||
("graphene_core::ops::BoolValueNode", "graphene_math_nodes::BoolValueNode"),
|
||||
("graphene_core::ops::NumberValueNode", "graphene_math_nodes::NumberValueNode"),
|
||||
("graphene_core::ops::PercentageValueNode", "graphene_math_nodes::PercentageValueNode"),
|
||||
("graphene_core::ops::CoordinateValueNode", "graphene_math_nodes::CoordinateValueNode"),
|
||||
("graphene_core::ops::ColorValueNode", "graphene_math_nodes::ColorValueNode"),
|
||||
("graphene_core::ops::GradientValueNode", "graphene_math_nodes::GradientValueNode"),
|
||||
("graphene_core::ops::StringValueNode", "graphene_math_nodes::StringValueNode"),
|
||||
("graphene_core::ops::DotProductNode", "graphene_math_nodes::DotProductNode"),
|
||||
("graphene_core::ops::SizeOfNode", "graphene_core::debug::SizeOfNode"),
|
||||
("graphene_core::ops::SomeNode", "graphene_core::debug::SomeNode"),
|
||||
("graphene_core::ops::UnwrapNode", "graphene_core::debug::UnwrapNode"),
|
||||
("graphene_core::ops::CloneNode", "graphene_core::debug::CloneNode"),
|
||||
("graphene_core::ops::ExtractXyNode", "graphene_core::extract_xy::ExtractXyNode"),
|
||||
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
|
||||
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
|
||||
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
|
||||
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
|
||||
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
|
||||
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
|
||||
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
|
||||
("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"),
|
||||
("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"),
|
||||
("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"),
|
||||
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
|
||||
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
|
||||
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
|
||||
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
|
||||
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
|
||||
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
|
||||
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
|
||||
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
|
||||
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
|
||||
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
|
||||
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
|
||||
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"),
|
||||
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"),
|
||||
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"),
|
||||
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
|
||||
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
|
||||
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
|
||||
("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
|
||||
("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
|
||||
("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"),
|
||||
("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"),
|
||||
("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"),
|
||||
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
|
||||
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
|
||||
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
|
||||
("graphene_core::vector::generator_nodes::RectangleGenerator", "graphene_core::vector::generator_nodes::RectangleNode"),
|
||||
(
|
||||
"graphene_core::vector::generator_nodes::RegularPolygonGenerator",
|
||||
"graphene_core::vector::generator_nodes::RegularPolygonNode",
|
||||
),
|
||||
("graphene_core::vector::generator_nodes::StarGenerator", "graphene_core::vector::generator_nodes::StarNode"),
|
||||
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
|
||||
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
|
||||
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
|
||||
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
|
||||
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
|
||||
("graphene_std::vector::BooleanOperationNode", "graphene_path_bool::BooleanOperationNode"),
|
||||
];
|
||||
|
||||
pub fn document_migration_string_preprocessing(document_serialized_content: String) -> String {
|
||||
TEXT_REPLACEMENTS
|
||||
.iter()
|
||||
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new))
|
||||
}
|
||||
|
||||
pub fn document_migration_reset_node_definition(document_serialized_content: &str) -> bool {
|
||||
// Upgrade a document being opened to use fresh copies of all nodes
|
||||
if document_serialized_content.contains("node_output_index") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI)
|
||||
if document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode") {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) {
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
network.generate_node_paths(&[]);
|
||||
|
||||
// Apply string replacements to each node
|
||||
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
|
||||
for (node_id, path) in &node_ids {
|
||||
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
|
||||
|
||||
if let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = document
|
||||
.network_interface
|
||||
.nested_network(&network_path)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get(node_id)
|
||||
.map(|node| node.implementation.clone())
|
||||
{
|
||||
for (old, new) in REPLACEMENTS {
|
||||
let node_path_without_type_args = protonode_id.name.split('<').next();
|
||||
if node_path_without_type_args == Some(old) {
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation(node_id, &network_path, DocumentNodeImplementation::ProtoNode(new.to_string().into()));
|
||||
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reset_node_definitions_on_open {
|
||||
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
|
||||
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
|
||||
for node_id in &document
|
||||
.network_interface
|
||||
.document_network_metadata()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<NodeId>>()
|
||||
{
|
||||
if let Some(reference) = document
|
||||
.network_interface
|
||||
.document_network_metadata()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.get(node_id)
|
||||
.and_then(|node| node.persistent_metadata.reference.as_ref())
|
||||
{
|
||||
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
|
||||
let default_definition_node = node_definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
|
||||
document.network_interface.set_manual_compostion(node_id, &[], default_definition_node.document_node.manual_composition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if document
|
||||
.network_interface
|
||||
.document_network_metadata()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.iter()
|
||||
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
|
||||
{
|
||||
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
|
||||
}
|
||||
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
network.generate_node_paths(&[]);
|
||||
|
||||
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
|
||||
|
||||
// Apply upgrades to each node
|
||||
for (node_id, path) in &node_ids {
|
||||
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
|
||||
let network_path = &network_path;
|
||||
|
||||
let Some(node) = document.network_interface.nested_network(network_path).unwrap().nodes.get(node_id).cloned() else {
|
||||
log::error!("could not get node in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
|
||||
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
|
||||
document
|
||||
.network_interface
|
||||
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
|
||||
}
|
||||
|
||||
let Some(node_metadata) = document.network_interface.network_metadata(network_path).unwrap().persistent_metadata.node_metadata.get(node_id) else {
|
||||
log::error!("could not get node metadata for node {node_id} in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
|
||||
// TODO: Investigate if this should be an expected case, because currently it runs hundreds of times normally.
|
||||
// TODO: Either delete the commented out error below if this is normal, or fix the underlying issue if this is not expected.
|
||||
// log::error!("could not get reference in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
let inputs_count = node.inputs.len();
|
||||
|
||||
// Upgrade Fill nodes to the format change in #1778
|
||||
if reference == "Fill" && inputs_count == 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
|
||||
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
|
||||
let TaggedValue::FillType(fill_type) = fill_type else { continue };
|
||||
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
|
||||
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
|
||||
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
|
||||
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
|
||||
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
|
||||
let TaggedValue::DVec2(start) = start else { continue };
|
||||
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
|
||||
let TaggedValue::DVec2(end) = end else { continue };
|
||||
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
|
||||
let TaggedValue::DAffine2(transform) = transform else { continue };
|
||||
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
|
||||
let TaggedValue::GradientStops(positions) = positions else { continue };
|
||||
|
||||
let fill = match (fill_type, solid_color) {
|
||||
(FillType::Solid, None) => Fill::None,
|
||||
(FillType::Solid, Some(color)) => Fill::Solid(color),
|
||||
(FillType::Gradient, _) => Fill::Gradient(Gradient {
|
||||
stops: positions,
|
||||
gradient_type,
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
}),
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Fill(fill.clone()), false), network_path);
|
||||
match fill {
|
||||
Fill::None => {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(None), false), network_path);
|
||||
}
|
||||
Fill::Solid(color) => {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(Some(color)), false), network_path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(gradient), false), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||
if reference == "Stroke" && inputs_count == 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document.network_interface.insert_input_properties_row(node_id, 8, network_path);
|
||||
document.network_interface.insert_input_properties_row(node_id, 9, network_path);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
|
||||
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||
}
|
||||
|
||||
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
|
||||
if reference == "Splines from Points" {
|
||||
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
|
||||
}
|
||||
|
||||
// Upgrade the old "Spline" node to the new "Spline" node
|
||||
if reference == "Spline" {
|
||||
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
|
||||
let identifier = document
|
||||
.network_interface
|
||||
.implementation(node_id, network_path)
|
||||
.and_then(|implementation| implementation.get_proto_node());
|
||||
if identifier.map(|identifier| &identifier.name) != Some(&"graphene_core::vector::generator_nodes::SplineNode".into()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
|
||||
let node = document.network_interface.document_node(node_id, network_path).unwrap();
|
||||
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
|
||||
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
|
||||
continue;
|
||||
};
|
||||
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
|
||||
|
||||
// Retrieve the output connectors linked to the "Spline" node's output port
|
||||
let spline_outputs = document
|
||||
.network_interface
|
||||
.outward_wires(network_path)
|
||||
.unwrap()
|
||||
.get(&OutputConnector::node(*node_id, 0))
|
||||
.expect("Vec of InputConnector Spline node is connected to its output port 0.")
|
||||
.clone();
|
||||
|
||||
// Get the node's current position in the graph
|
||||
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
|
||||
log::error!("Could not get position of spline node.");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the "Path" node definition and fill it in with the vector data and default vector modification
|
||||
let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist.");
|
||||
let path_node = path_node_type.node_template_input_override([
|
||||
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
|
||||
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
|
||||
]);
|
||||
|
||||
// Get the "Spline" node definition and wire it up with the "Path" node as input
|
||||
let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist.");
|
||||
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
|
||||
|
||||
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
|
||||
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
||||
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
|
||||
let new_spline_id = *new_ids.get(&NodeId(0)).unwrap();
|
||||
let new_path_id = *new_ids.get(&NodeId(1)).unwrap();
|
||||
|
||||
// Remove the old "Spline" node from the document
|
||||
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
|
||||
|
||||
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
|
||||
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
|
||||
|
||||
// Reposition the new "Spline" node to match the original "Spline" node's position
|
||||
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
|
||||
|
||||
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
|
||||
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
|
||||
|
||||
// Redirect each output connection from the old node to the new "Spline" node's output port
|
||||
for input_connector in spline_outputs {
|
||||
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
|
||||
if reference == "Text" && inputs_count != 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 4),
|
||||
if inputs_count == 6 {
|
||||
old_inputs[4].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 5),
|
||||
if inputs_count == 6 {
|
||||
old_inputs[5].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 6),
|
||||
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 7),
|
||||
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
|
||||
network_path,
|
||||
);
|
||||
}
|
||||
|
||||
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
|
||||
if (reference == "Sine" || reference == "Cosine" || reference == "Tangent") && inputs_count == 1 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
|
||||
if reference == "Modulo" && inputs_count == 2 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Mirror node to add the `keep_original` boolean input
|
||||
if reference == "Mirror" && inputs_count == 3 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
|
||||
if reference == "Mirror" && inputs_count == 4 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return };
|
||||
let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y };
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 1),
|
||||
NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false),
|
||||
network_path,
|
||||
);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
|
||||
}
|
||||
|
||||
// Upgrade artboard name being passed as hidden value input to "To Artboard"
|
||||
if reference == "Artboard" && reset_node_definitions_on_open {
|
||||
let label = document.network_interface.display_name(node_id, network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
|
||||
}
|
||||
|
||||
if reference == "Image" && inputs_count == 1 {
|
||||
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
|
||||
let new_image_node = node_definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, new_image_node.document_node.implementation);
|
||||
|
||||
// Insert a new empty input for the image
|
||||
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]);
|
||||
document.network_interface.set_reference(node_id, network_path, Some("Image".to_string()));
|
||||
}
|
||||
|
||||
if reference == "Noise Pattern" && inputs_count == 15 {
|
||||
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
|
||||
let new_noise_pattern_node = node_definition.default_node_template();
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation(node_id, network_path, new_noise_pattern_node.document_node.implementation);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), network_path);
|
||||
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
|
||||
for (i, input) in old_inputs.iter().enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
if reference == "Instance on Points" && inputs_count == 2 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
}
|
||||
|
||||
if reference == "Morph" && inputs_count == 4 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
// We have removed the last input, so we don't add index 3
|
||||
}
|
||||
|
||||
if reference == "Brush" && inputs_count == 4 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
// We have removed the second input ("bounds"), so we don't add index 1 and we shift the rest of the inputs down by one
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path);
|
||||
}
|
||||
|
||||
if reference == "Flatten Vector Elements" {
|
||||
let node_definition = resolve_document_node_type("Flatten Path").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
|
||||
}
|
||||
|
||||
if reference == "Remove Handles" {
|
||||
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
|
||||
}
|
||||
|
||||
if reference == "Generate Handles" {
|
||||
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
|
||||
}
|
||||
|
||||
if reference == "Merge by Distance" && inputs_count == 2 {
|
||||
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 2),
|
||||
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Topological), false),
|
||||
network_path,
|
||||
);
|
||||
}
|
||||
|
||||
if reference == "Spatial Merge by Distance" {
|
||||
let node_definition = resolve_document_node_type("Merge by Distance").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 2),
|
||||
NodeInput::value(TaggedValue::MergeByDistanceAlgorithm(graphene_std::vector::misc::MergeByDistanceAlgorithm::Spatial), false),
|
||||
network_path,
|
||||
);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Merge by Distance".to_string());
|
||||
}
|
||||
|
||||
if reference == "Sample Points" && inputs_count == 5 {
|
||||
let node_definition = resolve_document_node_type("Sample Polyline").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
let new_spacing_value = NodeInput::value(TaggedValue::PointSpacingType(graphene_std::vector::misc::PointSpacingType::Separation), false);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), new_spacing_value, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[4].clone(), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Sample Polyline".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure layers are positioned as stacks if they are upstream siblings of another layer
|
||||
document.network_interface.load_structure();
|
||||
let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::<Vec<_>>();
|
||||
for layer in all_layers {
|
||||
let Some((downstream_node, input_index)) = document
|
||||
.network_interface
|
||||
.outward_wires(&[])
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
|
||||
.and_then(|outward_wires| outward_wires.first())
|
||||
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
|
||||
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
|
||||
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
|
||||
let (Some(layer_position), Some(downstream_position)) = (document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[])) else {
|
||||
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
|
||||
continue;
|
||||
};
|
||||
if layer_position.x == downstream_position.x {
|
||||
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
|
||||
use crate::messages::prelude::*;
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
use graphene_std::path_bool::BooleanOperation;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MenuBarMessageHandler {
|
||||
|
||||
@@ -2,6 +2,7 @@ mod portfolio_message;
|
||||
mod portfolio_message_handler;
|
||||
|
||||
pub mod document;
|
||||
pub mod document_migration;
|
||||
pub mod menu_bar;
|
||||
pub mod spreadsheet;
|
||||
pub mod utility_types;
|
||||
|
||||
@@ -54,9 +54,6 @@ pub enum PortfolioMessage {
|
||||
preview_url: String,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// ImaginateCheckServerStatus,
|
||||
// ImaginatePollServerStatus,
|
||||
// ImaginateServerHostname,
|
||||
Import,
|
||||
LoadDocumentResources {
|
||||
document_id: DocumentId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use super::document::utility_types::network_interface::{self, InputConnector, OutputConnector};
|
||||
use super::document::utility_types::network_interface;
|
||||
use super::spreadsheet::SpreadsheetMessageHandler;
|
||||
use super::utility_types::{PanelType, PersistentData};
|
||||
use crate::application::generate_uuid;
|
||||
@@ -11,21 +11,17 @@ 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;
|
||||
use crate::messages::portfolio::document_migration::*;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
|
||||
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use graphene_std::text::Font;
|
||||
use std::vec;
|
||||
|
||||
pub struct PortfolioMessageData<'a> {
|
||||
@@ -332,35 +328,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
// PortfolioMessage::ImaginateCheckServerStatus => {
|
||||
// let server_status = self.persistent_data.imaginate.server_status().clone();
|
||||
// self.persistent_data.imaginate.poll_server_check();
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// if let Some(fut) = self.persistent_data.imaginate.initiate_server_check() {
|
||||
// wasm_bindgen_futures::spawn_local(async move {
|
||||
// let () = fut.await;
|
||||
// use wasm_bindgen::prelude::*;
|
||||
|
||||
// #[wasm_bindgen(module = "/../frontend/src/editor.ts")]
|
||||
// extern "C" {
|
||||
// #[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||
// fn inject();
|
||||
// }
|
||||
// inject();
|
||||
// })
|
||||
// }
|
||||
// if &server_status != self.persistent_data.imaginate.server_status() {
|
||||
// responses.add(PropertiesPanelMessage::Refresh);
|
||||
// }
|
||||
// }
|
||||
// PortfolioMessage::ImaginatePollServerStatus => {
|
||||
// self.persistent_data.imaginate.poll_server_check();
|
||||
// responses.add(PropertiesPanelMessage::Refresh);
|
||||
// }
|
||||
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
|
||||
// PortfolioMessage::ImaginateServerHostname => {
|
||||
// self.persistent_data.imaginate.set_host_name(&preferences.imaginate_server_hostname);
|
||||
// }
|
||||
PortfolioMessage::Import => {
|
||||
// This portfolio message wraps the frontend message so it can be listed as an action, which isn't possible for frontend messages
|
||||
responses.add(FrontendMessage::TriggerImport);
|
||||
@@ -429,30 +397,18 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
document_serialized_content,
|
||||
to_front,
|
||||
} => {
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// This big code block contains lots of hacky code for upgrading old documents to the new format
|
||||
|
||||
// Upgrade a document being opened to use fresh copies of all nodes
|
||||
let replace_implementations_from_definition = reset_node_definitions_on_open || document_serialized_content.contains("node_output_index");
|
||||
// Upgrade layer implementation from https://github.com/GraphiteEditor/Graphite/pull/1946 (see also `fn fix_nodes()` in `main.rs` of Graphene CLI)
|
||||
let upgrade_from_before_returning_nested_click_targets =
|
||||
document_serialized_content.contains("graphene_core::ConstructLayerNode") || document_serialized_content.contains("graphene_core::AddArtboardNode");
|
||||
let upgrade_vector_manipulation_format = document_serialized_content.contains("ManipulatorGroupIds") && !document_name.contains("__DO_NOT_UPGRADE__");
|
||||
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
|
||||
|
||||
const TEXT_REPLACEMENTS: [(&str, &str); 2] = [
|
||||
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePointsNode"),
|
||||
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
|
||||
];
|
||||
let document_serialized_content = TEXT_REPLACEMENTS
|
||||
.iter()
|
||||
.fold(document_serialized_content, |document_serialized_content, (old, new)| document_serialized_content.replace(old, new));
|
||||
// Upgrade the document being opened to use fresh copies of all nodes
|
||||
let reset_node_definitions_on_open = reset_node_definitions_on_open || document_migration_reset_node_definition(&document_serialized_content);
|
||||
// Upgrade the document being opened with string replacements on the original JSON
|
||||
let document_serialized_content = document_migration_string_preprocessing(document_serialized_content);
|
||||
|
||||
// Deserialize the document
|
||||
let document = DocumentMessageHandler::deserialize_document(&document_serialized_content).map(|mut document| {
|
||||
document.name.clone_from(&document_name);
|
||||
document
|
||||
});
|
||||
|
||||
// Display an error to the user if the document could not be opened
|
||||
let mut document = match document {
|
||||
Ok(document) => document,
|
||||
Err(e) => {
|
||||
@@ -467,618 +423,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
||||
}
|
||||
};
|
||||
|
||||
const REPLACEMENTS: [(&str, &str); 40] = [
|
||||
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
|
||||
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
|
||||
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
|
||||
("graphene_core::ToGraphicGroupNode", "graphene_core::graphic_element::ToGroupNode"),
|
||||
("graphene_core::logic::LogicAndNode", "graphene_core::ops::LogicAndNode"),
|
||||
("graphene_core::logic::LogicNotNode", "graphene_core::ops::LogicNotNode"),
|
||||
("graphene_core::logic::LogicOrNode", "graphene_core::ops::LogicOrNode"),
|
||||
("graphene_core::ops::ConstructVector2", "graphene_core::ops::CoordinateValueNode"),
|
||||
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
|
||||
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
|
||||
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
|
||||
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
|
||||
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
|
||||
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
|
||||
("graphene_core::raster::ExtractChannelNode", "graphene_core::raster::adjustments::ExtractChannelNode"),
|
||||
("graphene_core::raster::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode"),
|
||||
("graphene_core::raster::HueSaturationNode", "graphene_core::raster::adjustments::HueSaturationNode"),
|
||||
("graphene_core::vector::GenerateHandlesNode", "graphene_core::vector::AutoTangentsNode"),
|
||||
("graphene_core::vector::RemoveHandlesNode", "graphene_core::vector::AutoTangentsNode"),
|
||||
("graphene_core::raster::InvertNode", "graphene_core::raster::adjustments::InvertNode"),
|
||||
("graphene_core::raster::InvertRGBNode", "graphene_core::raster::adjustments::InvertNode"),
|
||||
("graphene_core::raster::LevelsNode", "graphene_core::raster::adjustments::LevelsNode"),
|
||||
("graphene_core::raster::LuminanceNode", "graphene_core::raster::adjustments::LuminanceNode"),
|
||||
("graphene_core::raster::ExtractOpaqueNode", "graphene_core::raster::adjustments::MakeOpaqueNode"),
|
||||
("graphene_core::raster::PosterizeNode", "graphene_core::raster::adjustments::PosterizeNode"),
|
||||
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
|
||||
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
|
||||
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
|
||||
("graphene_core::transform::SetTransformNode", "graphene_core::transform::ReplaceTransformNode"),
|
||||
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
|
||||
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
|
||||
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),
|
||||
("graphene_core::vector::generator_nodes::RectangleGenerator", "graphene_core::vector::generator_nodes::RectangleNode"),
|
||||
(
|
||||
"graphene_core::vector::generator_nodes::RegularPolygonGenerator",
|
||||
"graphene_core::vector::generator_nodes::RegularPolygonNode",
|
||||
),
|
||||
("graphene_core::vector::generator_nodes::StarGenerator", "graphene_core::vector::generator_nodes::StarNode"),
|
||||
("graphene_std::executor::BlendGpuImageNode", "graphene_std::gpu_nodes::BlendGpuImageNode"),
|
||||
("graphene_std::raster::SampleNode", "graphene_std::raster::SampleImageNode"),
|
||||
("graphene_core::transform::CullNode", "graphene_core::ops::IdentityNode"),
|
||||
("graphene_std::raster::MaskImageNode", "graphene_std::raster::MaskNode"),
|
||||
("graphene_core::vector::FlattenVectorElementsNode", "graphene_core::vector::FlattenPathNode"),
|
||||
];
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
network.generate_node_paths(&[]);
|
||||
|
||||
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
|
||||
|
||||
// Apply upgrades to each node
|
||||
for (node_id, path) in &node_ids {
|
||||
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
|
||||
|
||||
if let Some(DocumentNodeImplementation::ProtoNode(protonode_id)) = document
|
||||
.network_interface
|
||||
.nested_network(&network_path)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get(node_id)
|
||||
.map(|node| node.implementation.clone())
|
||||
{
|
||||
for (old, new) in REPLACEMENTS {
|
||||
let node_path_without_type_args = protonode_id.name.split('<').next();
|
||||
if node_path_without_type_args == Some(old) {
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation(node_id, &network_path, DocumentNodeImplementation::ProtoNode(new.to_string().into()));
|
||||
document.network_interface.set_manual_compostion(node_id, &network_path, Some(graph_craft::Type::Generic("T".into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade all old nodes to support editable subgraphs introduced in #1750
|
||||
if replace_implementations_from_definition || upgrade_from_before_returning_nested_click_targets {
|
||||
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
|
||||
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
|
||||
for node_id in &document
|
||||
.network_interface
|
||||
.document_network_metadata()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<NodeId>>()
|
||||
{
|
||||
if let Some(reference) = document
|
||||
.network_interface
|
||||
.document_network_metadata()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.get(node_id)
|
||||
.and_then(|node| node.persistent_metadata.reference.as_ref())
|
||||
{
|
||||
let Some(node_definition) = resolve_document_node_type(reference) else { continue };
|
||||
let default_definition_node = node_definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, &[], default_definition_node.document_node.implementation);
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, &[], default_definition_node.persistent_node_metadata);
|
||||
document.network_interface.set_manual_compostion(node_id, &[], default_definition_node.document_node.manual_composition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if document
|
||||
.network_interface
|
||||
.document_network_metadata()
|
||||
.persistent_metadata
|
||||
.node_metadata
|
||||
.iter()
|
||||
.any(|(node_id, node)| node.persistent_metadata.reference.as_ref().is_some_and(|reference| reference == "Output") && *node_id == NodeId(0))
|
||||
{
|
||||
document.network_interface.delete_nodes(vec![NodeId(0)], true, &[]);
|
||||
}
|
||||
|
||||
let mut network = document.network_interface.document_network().clone();
|
||||
network.generate_node_paths(&[]);
|
||||
|
||||
let node_ids: Vec<_> = network.recursive_nodes().map(|(&id, node)| (id, node.original_location.path.clone().unwrap())).collect();
|
||||
|
||||
// Apply upgrades to each node
|
||||
for (node_id, path) in &node_ids {
|
||||
let network_path: Vec<_> = path.iter().copied().take(path.len() - 1).collect();
|
||||
let network_path = &network_path;
|
||||
|
||||
let Some(node) = document.network_interface.nested_network(network_path).unwrap().nodes.get(node_id).cloned() else {
|
||||
log::error!("could not get node in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` for manual composition
|
||||
if node.manual_composition == Some(graph_craft::concrete!(())) || node.manual_composition == Some(graph_craft::concrete!(graphene_std::transform::Footprint)) {
|
||||
document
|
||||
.network_interface
|
||||
.set_manual_compostion(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
|
||||
}
|
||||
|
||||
let Some(node_metadata) = document.network_interface.network_metadata(network_path).unwrap().persistent_metadata.node_metadata.get(node_id) else {
|
||||
log::error!("could not get node metadata for node {node_id} in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(ref reference) = node_metadata.persistent_metadata.reference.clone() else {
|
||||
// TODO: Investigate if this should be an expected case, because currently it runs hundreds of times normally.
|
||||
// TODO: Either delete the commented out error below if this is normal, or fix the underlying issue if this is not expected.
|
||||
// log::error!("could not get reference in deserialize_document");
|
||||
continue;
|
||||
};
|
||||
|
||||
let inputs_count = node.inputs.len();
|
||||
|
||||
// Upgrade Fill nodes to the format change in #1778
|
||||
if reference == "Fill" && inputs_count == 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
|
||||
let Some(fill_type) = old_inputs[1].as_value().cloned() else { continue };
|
||||
let TaggedValue::FillType(fill_type) = fill_type else { continue };
|
||||
let Some(solid_color) = old_inputs[2].as_value().cloned() else { continue };
|
||||
let TaggedValue::OptionalColor(solid_color) = solid_color else { continue };
|
||||
let Some(gradient_type) = old_inputs[3].as_value().cloned() else { continue };
|
||||
let TaggedValue::GradientType(gradient_type) = gradient_type else { continue };
|
||||
let Some(start) = old_inputs[4].as_value().cloned() else { continue };
|
||||
let TaggedValue::DVec2(start) = start else { continue };
|
||||
let Some(end) = old_inputs[5].as_value().cloned() else { continue };
|
||||
let TaggedValue::DVec2(end) = end else { continue };
|
||||
let Some(transform) = old_inputs[6].as_value().cloned() else { continue };
|
||||
let TaggedValue::DAffine2(transform) = transform else { continue };
|
||||
let Some(positions) = old_inputs[7].as_value().cloned() else { continue };
|
||||
let TaggedValue::GradientStops(positions) = positions else { continue };
|
||||
|
||||
let fill = match (fill_type, solid_color) {
|
||||
(FillType::Solid, None) => Fill::None,
|
||||
(FillType::Solid, Some(color)) => Fill::Solid(color),
|
||||
(FillType::Gradient, _) => Fill::Gradient(Gradient {
|
||||
stops: positions,
|
||||
gradient_type,
|
||||
start,
|
||||
end,
|
||||
transform,
|
||||
}),
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Fill(fill.clone()), false), network_path);
|
||||
match fill {
|
||||
Fill::None => {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(None), false), network_path);
|
||||
}
|
||||
Fill::Solid(color) => {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::OptionalColor(Some(color)), false), network_path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Gradient(gradient), false), network_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||
if reference == "Stroke" && inputs_count == 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document.network_interface.insert_input_properties_row(node_id, 8, network_path);
|
||||
document.network_interface.insert_input_properties_row(node_id, 9, network_path);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
|
||||
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||
}
|
||||
|
||||
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
|
||||
if reference == "Splines from Points" {
|
||||
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
|
||||
}
|
||||
|
||||
// Upgrade the old "Spline" node to the new "Spline" node
|
||||
if reference == "Spline" {
|
||||
// Retrieve the proto node identifier and verify it is the old "Spline" node, otherwise skip it if this is the new "Spline" node
|
||||
let identifier = document
|
||||
.network_interface
|
||||
.implementation(node_id, network_path)
|
||||
.and_then(|implementation| implementation.get_proto_node());
|
||||
if identifier.map(|identifier| &identifier.name) != Some(&"graphene_core::vector::generator_nodes::SplineNode".into()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Obtain the document node for the given node ID, extract the vector points, and create vector data from the list of points
|
||||
let node = document.network_interface.document_node(node_id, network_path).unwrap();
|
||||
let Some(TaggedValue::VecDVec2(points)) = node.inputs.get(1).and_then(|tagged_value| tagged_value.as_value()) else {
|
||||
log::error!("The old Spline node's input at index 1 is not a TaggedValue::VecDVec2");
|
||||
continue;
|
||||
};
|
||||
let vector_data = VectorData::from_subpath(Subpath::from_anchors_linear(points.to_vec(), false));
|
||||
|
||||
// Retrieve the output connectors linked to the "Spline" node's output port
|
||||
let spline_outputs = document
|
||||
.network_interface
|
||||
.outward_wires(network_path)
|
||||
.unwrap()
|
||||
.get(&OutputConnector::node(*node_id, 0))
|
||||
.expect("Vec of InputConnector Spline node is connected to its output port 0.")
|
||||
.clone();
|
||||
|
||||
// Get the node's current position in the graph
|
||||
let Some(node_position) = document.network_interface.position(node_id, network_path) else {
|
||||
log::error!("Could not get position of spline node.");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Get the "Path" node definition and fill it in with the vector data and default vector modification
|
||||
let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist.");
|
||||
let path_node = path_node_type.node_template_input_override([
|
||||
Some(NodeInput::value(TaggedValue::VectorData(VectorDataTable::new(vector_data)), true)),
|
||||
Some(NodeInput::value(TaggedValue::VectorModification(Default::default()), false)),
|
||||
]);
|
||||
|
||||
// Get the "Spline" node definition and wire it up with the "Path" node as input
|
||||
let spline_node_type = resolve_document_node_type("Spline").expect("Spline node does not exist.");
|
||||
let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]);
|
||||
|
||||
// Create a new node group with the "Path" and "Spline" nodes and generate new node IDs for them
|
||||
let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)];
|
||||
let new_ids = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect::<HashMap<_, _>>();
|
||||
let new_spline_id = *new_ids.get(&NodeId(0)).unwrap();
|
||||
let new_path_id = *new_ids.get(&NodeId(1)).unwrap();
|
||||
|
||||
// Remove the old "Spline" node from the document
|
||||
document.network_interface.delete_nodes(vec![*node_id], false, network_path);
|
||||
|
||||
// Insert the new "Path" and "Spline" nodes into the network interface with generated IDs
|
||||
document.network_interface.insert_node_group(nodes.clone(), new_ids, network_path);
|
||||
|
||||
// Reposition the new "Spline" node to match the original "Spline" node's position
|
||||
document.network_interface.shift_node(&new_spline_id, node_position, network_path);
|
||||
|
||||
// Reposition the new "Path" node with an offset relative to the original "Spline" node's position
|
||||
document.network_interface.shift_node(&new_path_id, node_position + IVec2::new(-7, 0), network_path);
|
||||
|
||||
// Redirect each output connection from the old node to the new "Spline" node's output port
|
||||
for input_connector in spline_outputs {
|
||||
document.network_interface.set_input(&input_connector, NodeInput::node(new_spline_id, 0), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade Text node to include line height and character spacing, which were previously hardcoded to 1, from https://github.com/GraphiteEditor/Graphite/pull/2016
|
||||
if reference == "Text" && inputs_count != 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 4),
|
||||
if inputs_count == 6 {
|
||||
old_inputs[4].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().line_height_ratio), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 5),
|
||||
if inputs_count == 6 {
|
||||
old_inputs[5].clone()
|
||||
} else {
|
||||
NodeInput::value(TaggedValue::F64(TypesettingConfig::default().character_spacing), false)
|
||||
},
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 6),
|
||||
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_width), false),
|
||||
network_path,
|
||||
);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 7),
|
||||
NodeInput::value(TaggedValue::OptionalF64(TypesettingConfig::default().max_height), false),
|
||||
network_path,
|
||||
);
|
||||
}
|
||||
|
||||
// Upgrade Sine, Cosine, and Tangent nodes to include a boolean input for whether the output should be in radians, which was previously the only option but is now not the default
|
||||
if (reference == "Sine" || reference == "Cosine" || reference == "Tangent") && inputs_count == 1 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Modulo node to include a boolean input for whether the output should be always positive, which was previously not an option
|
||||
if reference == "Modulo" && inputs_count == 2 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Mirror node to add the `keep_original` boolean input
|
||||
if reference == "Mirror" && inputs_count == 3 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
}
|
||||
|
||||
// Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64`
|
||||
if reference == "Mirror" && inputs_count == 4 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return };
|
||||
let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y };
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node(*node_id, 1),
|
||||
NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false),
|
||||
network_path,
|
||||
);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
|
||||
}
|
||||
|
||||
// Upgrade artboard name being passed as hidden value input to "To Artboard"
|
||||
if reference == "Artboard" && upgrade_from_before_returning_nested_click_targets {
|
||||
let label = document.network_interface.display_name(node_id, network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
|
||||
}
|
||||
|
||||
if reference == "Image" && inputs_count == 1 {
|
||||
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
|
||||
let new_image_node = node_definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, new_image_node.document_node.implementation);
|
||||
|
||||
// Insert a new empty input for the image
|
||||
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]);
|
||||
document.network_interface.set_reference(node_id, network_path, Some("Image".to_string()));
|
||||
}
|
||||
|
||||
if reference == "Noise Pattern" && inputs_count == 15 {
|
||||
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
|
||||
let new_noise_pattern_node = node_definition.default_node_template();
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation(node_id, network_path, new_noise_pattern_node.document_node.implementation);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), network_path);
|
||||
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), network_path);
|
||||
for (i, input) in old_inputs.iter().enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
if reference == "Instance on Points" && inputs_count == 2 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
}
|
||||
|
||||
if reference == "Morph" && inputs_count == 4 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
// We have removed the last input, so we don't add index 3
|
||||
}
|
||||
|
||||
if reference == "Brush" && inputs_count == 4 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
// We have removed the second input ("bounds"), so we don't add index 1 and we shift the rest of the inputs down by one
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[3].clone(), network_path);
|
||||
}
|
||||
|
||||
if reference == "Flatten Vector Elements" {
|
||||
let node_definition = resolve_document_node_type("Flatten Path").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Flatten Path".to_string());
|
||||
}
|
||||
|
||||
if reference == "Remove Handles" {
|
||||
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 1), NodeInput::value(TaggedValue::F64(0.), false), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(false), false), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
|
||||
}
|
||||
|
||||
if reference == "Generate Handles" {
|
||||
let node_definition = resolve_document_node_type("Auto-Tangents").unwrap();
|
||||
let new_node_template = node_definition.default_node_template();
|
||||
let document_node = new_node_template.document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document
|
||||
.network_interface
|
||||
.replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
document
|
||||
.network_interface
|
||||
.set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::Bool(true), false), network_path);
|
||||
|
||||
document.network_interface.replace_reference_name(node_id, network_path, "Auto-Tangents".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this document upgrade code
|
||||
// Upgrade document to the new vector manipulation format introduced in #1676
|
||||
let document_serialized_content = document.serialize_document();
|
||||
if upgrade_vector_manipulation_format && !document_serialized_content.is_empty() {
|
||||
responses.add(FrontendMessage::TriggerUpgradeDocumentToVectorManipulationFormat {
|
||||
document_id,
|
||||
document_name,
|
||||
document_is_auto_saved,
|
||||
document_is_saved,
|
||||
document_serialized_content,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure layers are positioned as stacks if they upstream siblings of another layer
|
||||
document.network_interface.load_structure();
|
||||
let all_layers = LayerNodeIdentifier::ROOT_PARENT.descendants(document.network_interface.document_metadata()).collect::<Vec<_>>();
|
||||
for layer in all_layers {
|
||||
let Some((downstream_node, input_index)) = document
|
||||
.network_interface
|
||||
.outward_wires(&[])
|
||||
.and_then(|outward_wires| outward_wires.get(&OutputConnector::node(layer.to_node(), 0)))
|
||||
.and_then(|outward_wires| outward_wires.first())
|
||||
.and_then(|input_connector| input_connector.node_id().map(|node_id| (node_id, input_connector.input_index())))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// If the downstream node is a layer and the input is the first input and the current layer is not in a stack
|
||||
if input_index == 0 && document.network_interface.is_layer(&downstream_node, &[]) && !document.network_interface.is_stack(&layer.to_node(), &[]) {
|
||||
// Ensure the layer is horizontally aligned with the downstream layer to prevent changing the layout of old files
|
||||
let (Some(layer_position), Some(downstream_position)) =
|
||||
(document.network_interface.position(&layer.to_node(), &[]), document.network_interface.position(&downstream_node, &[]))
|
||||
else {
|
||||
log::error!("Could not get position for layer {:?} or downstream node {} when opening file", layer.to_node(), downstream_node);
|
||||
continue;
|
||||
};
|
||||
if layer_position.x == downstream_position.x {
|
||||
document.network_interface.set_stack_position_calculated_offset(&layer.to_node(), &downstream_node, &[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Upgrade the document's nodes to be compatible with the latest version
|
||||
document_migration_upgrades(&mut document, reset_node_definitions_on_open);
|
||||
|
||||
// Set the save state of the document based on what's given to us by the caller to this message
|
||||
document.set_auto_save_state(document_is_auto_saved);
|
||||
document.set_save_state(document_is_saved);
|
||||
|
||||
// Load the document into the portfolio so it opens in the editor
|
||||
self.load_document(document, document_id, responses, to_front);
|
||||
}
|
||||
PortfolioMessage::PasteIntoFolder { clipboard, parent, insert_index } => {
|
||||
|
||||
@@ -4,7 +4,6 @@ use graphene_std::text::FontCache;
|
||||
pub struct PersistentData {
|
||||
pub font_cache: FontCache,
|
||||
pub use_vello: bool,
|
||||
// pub imaginate: ImaginatePersistentData,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
|
||||
@@ -16,6 +16,4 @@ pub enum PreferencesMessage {
|
||||
ModifyLayout { zoom_with_scroll: bool },
|
||||
GraphWireStyle { style: GraphWireStyle },
|
||||
ViewportZoomWheelRate { rate: f64 },
|
||||
// ImaginateRefreshFrequency { seconds: f64 },
|
||||
// ImaginateServerHostname { hostname: String },
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ use graph_craft::wasm_application_io::EditorPreferences;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct PreferencesMessageHandler {
|
||||
// pub imaginate_server_hostname: String,
|
||||
// pub imaginate_refresh_frequency: f64,
|
||||
pub selection_mode: SelectionMode,
|
||||
pub zoom_with_scroll: bool,
|
||||
pub use_vello: bool,
|
||||
@@ -24,7 +22,6 @@ impl PreferencesMessageHandler {
|
||||
|
||||
pub fn editor_preferences(&self) -> EditorPreferences {
|
||||
EditorPreferences {
|
||||
// imaginate_hostname: self.imaginate_server_hostname.clone(),
|
||||
use_vello: self.use_vello && self.supports_wgpu(),
|
||||
}
|
||||
}
|
||||
@@ -37,8 +34,6 @@ impl PreferencesMessageHandler {
|
||||
impl Default for PreferencesMessageHandler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// imaginate_server_hostname: EditorPreferences::default().imaginate_hostname,
|
||||
// imaginate_refresh_frequency: 1.,
|
||||
selection_mode: SelectionMode::Touched,
|
||||
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
|
||||
use_vello: EditorPreferences::default().use_vello,
|
||||
@@ -57,10 +52,6 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
||||
if let Ok(deserialized_preferences) = serde_json::from_str::<PreferencesMessageHandler>(&preferences) {
|
||||
*self = deserialized_preferences;
|
||||
|
||||
// TODO: Reenable when Imaginate is restored
|
||||
// responses.add(PortfolioMessage::ImaginateServerHostname);
|
||||
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
|
||||
responses.add(PortfolioMessage::EditorPreferences);
|
||||
responses.add(PortfolioMessage::UpdateVelloPreference);
|
||||
responses.add(PreferencesMessage::ModifyLayout {
|
||||
@@ -101,27 +92,6 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
|
||||
self.viewport_zoom_wheel_rate = rate;
|
||||
}
|
||||
}
|
||||
// TODO: Reenable when Imaginate is restored (and move back up one line since the auto-formatter doesn't like it in that block)
|
||||
// PreferencesMessage::ImaginateRefreshFrequency { seconds } => {
|
||||
// self.imaginate_refresh_frequency = seconds;
|
||||
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
// responses.add(PortfolioMessage::EditorPreferences);
|
||||
// }
|
||||
// PreferencesMessage::ImaginateServerHostname { hostname } => {
|
||||
// let initial = hostname.clone();
|
||||
// let has_protocol = hostname.starts_with("http://") || hostname.starts_with("https://");
|
||||
// let hostname = if has_protocol { hostname } else { "http://".to_string() + &hostname };
|
||||
// let hostname = if hostname.ends_with('/') { hostname } else { hostname + "/" };
|
||||
|
||||
// if hostname != initial {
|
||||
// refresh_dialog(responses);
|
||||
// }
|
||||
|
||||
// self.imaginate_server_hostname = hostname;
|
||||
// responses.add(PortfolioMessage::ImaginateServerHostname);
|
||||
// responses.add(PortfolioMessage::ImaginateCheckServerStatus);
|
||||
// responses.add(PortfolioMessage::EditorPreferences);
|
||||
//}
|
||||
|
||||
responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() });
|
||||
}
|
||||
|
||||
@@ -34,19 +34,15 @@ pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastE
|
||||
pub use crate::messages::message::{Message, MessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::ellipse_tool::{EllipseToolMessage, EllipseToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant};
|
||||
// pub use crate::messages::tool::tool_messages::imaginate_tool::{ImaginateToolMessage, ImaginateToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::line_tool::{LineToolMessage, LineToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::path_tool::{PathToolMessage, PathToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::pen_tool::{PenToolMessage, PenToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::polygon_tool::{PolygonToolMessage, PolygonToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::rectangle_tool::{RectangleToolMessage, RectangleToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
|
||||
use glam::DVec2;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// A unified enum wrapper around all available shape-specific gizmo handlers.
|
||||
///
|
||||
/// This abstraction allows `GizmoManager` to interact with different shape gizmos (like Star or Polygon)
|
||||
/// using a common interface without needing to know the specific shape type at compile time.
|
||||
///
|
||||
/// Each variant stores a concrete handler (e.g., `StarGizmoHandler`, `PolygonGizmoHandler`) that implements
|
||||
/// the shape-specific logic for rendering overlays, responding to input, and modifying shape parameters.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ShapeGizmoHandlers {
|
||||
#[default]
|
||||
None,
|
||||
Star(StarGizmoHandler),
|
||||
Polygon(PolygonGizmoHandler),
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandlers {
|
||||
/// Returns the kind of shape the handler is managing, such as `"star"` or `"polygon"`.
|
||||
/// Used for grouping logic and distinguishing between handler types at runtime.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Star(_) => "star",
|
||||
Self::Polygon(_) => "polygon",
|
||||
Self::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches interaction state updates to the corresponding shape-specific handler.
|
||||
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
match self {
|
||||
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if any interactive part of the gizmo is currently hovered.
|
||||
pub fn is_any_gizmo_hovered(&self) -> bool {
|
||||
match self {
|
||||
Self::Star(h) => h.is_any_gizmo_hovered(),
|
||||
Self::Polygon(h) => h.is_any_gizmo_hovered(),
|
||||
Self::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Passes the click interaction to the appropriate gizmo handler if one is hovered.
|
||||
pub fn handle_click(&mut self) {
|
||||
match self {
|
||||
Self::Star(h) => h.handle_click(),
|
||||
Self::Polygon(h) => h.handle_click(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the gizmo state while the user is dragging a handle (e.g., adjusting radius).
|
||||
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
match self {
|
||||
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up any state used by the gizmo handler.
|
||||
pub fn cleanup(&mut self) {
|
||||
match self {
|
||||
Self::Star(h) => h.cleanup(),
|
||||
Self::Polygon(h) => h.cleanup(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws overlays like control points or outlines for the shape handled by this gizmo.
|
||||
pub fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
layer: Option<LayerNodeIdentifier>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
match self {
|
||||
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws live-updating overlays during drag interactions for the shape handled by this gizmo.
|
||||
pub fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
match self {
|
||||
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
|
||||
///
|
||||
/// The `GizmoManager` is responsible for detecting which shapes are selected, activating the appropriate
|
||||
/// shape-specific gizmo, and routing user interactions (hover, click, drag) to the correct handler.
|
||||
/// It allows editing multiple shapes of the same type or focusing on a single active shape when a gizmo is hovered.
|
||||
///
|
||||
/// ## Responsibilities:
|
||||
/// - Detect which selected layers support shape gizmos (e.g., stars, polygons)
|
||||
/// - Activate the correct handler and manage state between frames
|
||||
/// - Route click, hover, and drag events to the proper shape gizmo
|
||||
/// - Render overlays and dragging visuals
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GizmoManager {
|
||||
active_shape_handler: Option<ShapeGizmoHandlers>,
|
||||
layers_handlers: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)>,
|
||||
}
|
||||
|
||||
impl GizmoManager {
|
||||
/// Detects and returns a shape gizmo handler based on the layer type (e.g., star, polygon).
|
||||
///
|
||||
/// Returns `None` if the given layer does not represent a shape with a registered gizmo.
|
||||
pub fn detect_shape_handler(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<ShapeGizmoHandlers> {
|
||||
// Star
|
||||
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` if a gizmo is currently active (hovered or being interacted with).
|
||||
pub fn hovering_over_gizmo(&self) -> bool {
|
||||
self.active_shape_handler.is_some()
|
||||
}
|
||||
|
||||
/// Called every frame to check selected layers and update the active shape gizmo, if hovered.
|
||||
///
|
||||
/// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing.
|
||||
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = Vec::new();
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
|
||||
if let Some(mut handler) = Self::detect_shape_handler(layer, document) {
|
||||
handler.handle_state(layer, mouse_position, document, responses);
|
||||
let is_hovered = handler.is_any_gizmo_hovered();
|
||||
|
||||
if is_hovered {
|
||||
self.layers_handlers.clear();
|
||||
self.active_shape_handler = Some(handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to group this handler with others of the same type
|
||||
if let Some((_, layers)) = handlers_layer.iter_mut().find(|(existing_handler, _)| existing_handler.kind() == handler.kind()) {
|
||||
layers.push(layer);
|
||||
} else {
|
||||
handlers_layer.push((handler, vec![layer]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.layers_handlers = handlers_layer;
|
||||
self.active_shape_handler = None;
|
||||
}
|
||||
|
||||
/// Handles click interactions if a gizmo is active. Returns `true` if a gizmo handled the click.
|
||||
pub fn handle_click(&mut self) -> bool {
|
||||
if let Some(handle) = &mut self.active_shape_handler {
|
||||
handle.handle_click();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn handle_cleanup(&mut self) {
|
||||
if let Some(handle) = &mut self.active_shape_handler {
|
||||
handle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// Passes drag update data to the active gizmo to update shape parameters live.
|
||||
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if let Some(handle) = &mut self.active_shape_handler {
|
||||
handle.handle_update(drag_start, document, input, responses);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws overlays for the currently active shape gizmo during a drag interaction.
|
||||
pub fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if let Some(handle) = &self.active_shape_handler {
|
||||
handle.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws overlays for either the active gizmo (if hovered) or all grouped selected gizmos.
|
||||
///
|
||||
/// If no single gizmo is active, it renders overlays for all grouped layers with associated handlers.
|
||||
pub fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if let Some(handler) = &self.active_shape_handler {
|
||||
handler.overlays(document, None, input, shape_editor, mouse_position, overlay_context);
|
||||
return;
|
||||
}
|
||||
|
||||
for (handler, selected_layers) in &self.layers_handlers {
|
||||
for layer in selected_layers {
|
||||
handler.overlays(document, Some(*layer), input, shape_editor, mouse_position, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod gizmo_manager;
|
||||
pub mod shape_gizmos;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod number_of_points_dial;
|
||||
pub mod point_radius_handle;
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION, NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::prelude::Responses;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_polygon_parameters, inside_polygon, inside_star, polygon_outline, polygon_vertex_position, star_outline};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum NumberOfPointsDialState {
|
||||
#[default]
|
||||
Inactive,
|
||||
Hover,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NumberOfPointsDial {
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
pub initial_points: u32,
|
||||
pub handle_state: NumberOfPointsDialState,
|
||||
}
|
||||
|
||||
impl NumberOfPointsDial {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.handle_state = NumberOfPointsDialState::Inactive;
|
||||
self.layer = None;
|
||||
}
|
||||
|
||||
pub fn update_state(&mut self, state: NumberOfPointsDialState) {
|
||||
self.handle_state = state;
|
||||
}
|
||||
|
||||
pub fn is_hovering(&self) -> bool {
|
||||
self.handle_state == NumberOfPointsDialState::Hover
|
||||
}
|
||||
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
self.handle_state == NumberOfPointsDialState::Dragging
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
match &self.handle_state {
|
||||
NumberOfPointsDialState::Inactive => {
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
|
||||
|
||||
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.layer = Some(layer);
|
||||
self.initial_points = sides;
|
||||
self.update_state(NumberOfPointsDialState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
|
||||
|
||||
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.layer = Some(layer);
|
||||
self.initial_points = sides;
|
||||
self.update_state(NumberOfPointsDialState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
|
||||
}
|
||||
}
|
||||
}
|
||||
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if mouse_position.distance(center) > NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsDialState::Hover) {
|
||||
self.update_state(NumberOfPointsDialState::Inactive);
|
||||
self.layer = None;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option<LayerNodeIdentifier>, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
|
||||
match &self.handle_state {
|
||||
NumberOfPointsDialState::Inactive => {
|
||||
let Some(layer) = layer else { return };
|
||||
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let radius = radius1.max(radius2);
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
|
||||
if closest_segment.layer() == layer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
|
||||
|
||||
if inside_star(viewport, sides, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.draw_spokes(center, viewport, sides, radius, overlay_context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
|
||||
if closest_segment.layer() == layer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
|
||||
|
||||
if inside_polygon(viewport, sides, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.draw_spokes(center, viewport, sides, radius, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
|
||||
let Some(layer) = self.layer else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the star's greater radius or polygon's radius, as well as the number of sides
|
||||
let Some((sides, radius)) = extract_star_parameters(Some(layer), document)
|
||||
.map(|(sides, r1, r2)| (sides, r1.max(r2)))
|
||||
.or_else(|| extract_polygon_parameters(Some(layer), document))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
// Draw either the star or polygon outline
|
||||
star_outline(Some(layer), document, overlay_context);
|
||||
polygon_outline(Some(layer), document, overlay_context);
|
||||
|
||||
self.draw_spokes(center, viewport, sides, radius, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_spokes(&self, center: DVec2, viewport: DAffine2, sides: u32, radius: f64, overlay_context: &mut OverlayContext) {
|
||||
for i in 0..sides {
|
||||
let angle = ((i as f64) * TAU) / (sides as f64);
|
||||
|
||||
let point = viewport.transform_point2(DVec2 {
|
||||
x: radius * angle.sin(),
|
||||
y: -radius * angle.cos(),
|
||||
});
|
||||
|
||||
let Some(direction) = (point - center).try_normalize() else { continue };
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
let end_point = direction * NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH;
|
||||
if matches!(self.handle_state, NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging) {
|
||||
overlay_context.line(center, end_point * NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION + center, None, None);
|
||||
} else {
|
||||
overlay_context.line(center, end_point + center, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
|
||||
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
|
||||
let net_delta = (delta.length() / 25.).round() * sign;
|
||||
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
use crate::consts::GIZMO_HIDE_THRESHOLD;
|
||||
use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector};
|
||||
use crate::messages::prelude::FrontendMessage;
|
||||
use crate::messages::prelude::Responses;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, polygon_outline, polygon_vertex_position, star_outline};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_4, PI, SQRT_2};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum PointRadiusHandleState {
|
||||
#[default]
|
||||
Inactive,
|
||||
Hover,
|
||||
Dragging,
|
||||
Snapped(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct PointRadiusHandle {
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
point: u32,
|
||||
radius_index: usize,
|
||||
snap_radii: Vec<f64>,
|
||||
initial_radius: f64,
|
||||
handle_state: PointRadiusHandleState,
|
||||
}
|
||||
|
||||
impl PointRadiusHandle {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.handle_state = PointRadiusHandleState::Inactive;
|
||||
self.snap_radii.clear();
|
||||
self.layer = None;
|
||||
}
|
||||
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.handle_state == PointRadiusHandleState::Hover
|
||||
}
|
||||
|
||||
pub fn is_dragging_or_snapped(&self) -> bool {
|
||||
self.handle_state == PointRadiusHandleState::Dragging || matches!(self.handle_state, PointRadiusHandleState::Snapped(_))
|
||||
}
|
||||
|
||||
pub fn update_state(&mut self, state: PointRadiusHandleState) {
|
||||
self.handle_state = state;
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
|
||||
match &self.handle_state {
|
||||
PointRadiusHandleState::Inactive => {
|
||||
// Draw the point handle gizmo for the star shape
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..2 * sides {
|
||||
let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) };
|
||||
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
self.radius_index = radius_index;
|
||||
self.layer = Some(layer);
|
||||
self.point = i;
|
||||
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
|
||||
self.initial_radius = radius;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
self.update_state(PointRadiusHandleState::Hover);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the point handle gizmo for the polygon shape
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..sides {
|
||||
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
self.radius_index = 2;
|
||||
self.layer = Some(layer);
|
||||
self.point = i;
|
||||
self.snap_radii.clear();
|
||||
self.initial_radius = radius;
|
||||
self.update_state(PointRadiusHandleState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
|
||||
self.update_state(PointRadiusHandleState::Inactive);
|
||||
self.layer = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
|
||||
|
||||
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
|
||||
self.update_state(PointRadiusHandleState::Inactive);
|
||||
self.layer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
PointRadiusHandleState::Snapped(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlays(
|
||||
&self,
|
||||
selected_star_layer: Option<LayerNodeIdentifier>,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
match &self.handle_state {
|
||||
PointRadiusHandleState::Inactive => {
|
||||
let Some(layer) = selected_star_layer else { return };
|
||||
|
||||
// Draw the point handle gizmo for the star shape
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..(2 * sides) {
|
||||
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
let viewport_diagonal = input.viewport_bounds.size().length();
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
let Some(direction) = (point - center).try_normalize() else { continue };
|
||||
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
let angle = ((i as f64) * PI) / (sides as f64);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
|
||||
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
overlay_context.manipulator_handle(point, false, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the point handle gizmo for the Polygon shape
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..sides {
|
||||
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
let viewport_diagonal = input.viewport_bounds.size().length();
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
let Some(direction) = (point - center).try_normalize() else { continue };
|
||||
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
overlay_context.manipulator_handle(point, false, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
let viewport_diagonal = input.viewport_bounds.size().length();
|
||||
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let angle = ((self.point as f64) * PI) / (sides as f64);
|
||||
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
let Some(direction) = (point - center).try_normalize() else { return };
|
||||
|
||||
// Draws the ray from the center to the dragging point extending till the viewport
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
star_outline(Some(layer), document, overlay_context);
|
||||
|
||||
// Make the ticks for snapping
|
||||
|
||||
// If dragging to make radius negative don't show the
|
||||
if (mouse_position - center).dot(direction) < 0. {
|
||||
return;
|
||||
}
|
||||
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
|
||||
|
||||
let Some(direction) = (point - center).try_normalize() else { return };
|
||||
|
||||
// Draws the ray from the center to the dragging point extending till the viewport
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
|
||||
polygon_outline(Some(layer), document, overlay_context);
|
||||
}
|
||||
}
|
||||
PointRadiusHandleState::Snapped(snapping_index) => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
match snapping_index {
|
||||
// Make a triangle with previous two points
|
||||
0 => {
|
||||
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, sides, radius1, radius2);
|
||||
let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
|
||||
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
|
||||
let l1 = (before_outer_position - outer_position).length() * 0.2;
|
||||
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
|
||||
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
|
||||
let Some(direction) = (center - outer_position).try_normalize() else { return };
|
||||
|
||||
let new_point = SQRT_2 * l1 * direction + outer_position;
|
||||
|
||||
let before_outer_position = l1 * l1_direction + outer_position;
|
||||
let point_position = l1 * l2_direction + outer_position;
|
||||
|
||||
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
}
|
||||
1 => {
|
||||
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
|
||||
|
||||
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2);
|
||||
|
||||
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
|
||||
let l1 = (before_outer_position - point_position).length() * 0.2;
|
||||
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
|
||||
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
|
||||
let Some(direction) = (center - point_position).try_normalize() else { return };
|
||||
|
||||
let new_point = SQRT_2 * l1 * direction + point_position;
|
||||
|
||||
let before_outer_position = l1 * l1_direction + point_position;
|
||||
let after_point_position = l1 * l2_direction + point_position;
|
||||
|
||||
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
}
|
||||
i => {
|
||||
// Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0
|
||||
if i % 2 != 0 {
|
||||
// Flipped case
|
||||
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
let target_index = (1 - (*i as i32)).abs() + (self.point as i32);
|
||||
let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
|
||||
|
||||
let mirrored_index = 2 * (self.point as i32) - target_index;
|
||||
let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
} else {
|
||||
let outer_index = (self.point as i32) - 1;
|
||||
let outer_position = star_vertex_position(viewport, outer_index, sides, radius1, radius2);
|
||||
|
||||
// The vertex which is colinear with the point we are dragging and its previous outer vertex
|
||||
let target_index = (self.point as i32) + (*i as i32) - 1;
|
||||
let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
|
||||
|
||||
let mirrored_index = 2 * outer_index - target_index;
|
||||
|
||||
let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
star_outline(Some(layer), document, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) -> Vec<f64> {
|
||||
let mut snap_radii = Vec::new();
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star") else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
let other_index = if radius_index == 3 { 2 } else { 3 };
|
||||
|
||||
let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else {
|
||||
return snap_radii;
|
||||
};
|
||||
let Some(&TaggedValue::U32(sides)) = node_inputs[1].as_value() else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
// Inner radius for 90°
|
||||
let b = FRAC_PI_4 * 3. - PI / (sides as f64);
|
||||
let angle = b.sin();
|
||||
let required_radius = (other_radius / angle) * FRAC_1_SQRT_2;
|
||||
|
||||
snap_radii.push(required_radius);
|
||||
|
||||
// Also push the case when the when it length increases more than the other
|
||||
|
||||
let flipped = other_radius * angle * SQRT_2;
|
||||
|
||||
snap_radii.push(flipped);
|
||||
|
||||
for i in 1..sides {
|
||||
let sides = sides as f64;
|
||||
let i = i as f64;
|
||||
let denominator = 2. * ((PI * (i - 1.)) / sides).cos() * ((PI * i) / sides).sin();
|
||||
let numerator = ((2. * PI * i) / sides).sin();
|
||||
let factor = numerator / denominator;
|
||||
|
||||
if factor < 0. {
|
||||
break;
|
||||
}
|
||||
|
||||
if other_radius * factor > 1e-6 {
|
||||
snap_radii.push(other_radius * factor);
|
||||
}
|
||||
|
||||
snap_radii.push((other_radius * 1.) / factor);
|
||||
}
|
||||
|
||||
snap_radii
|
||||
}
|
||||
|
||||
fn check_snapping(&self, new_radius: f64, original_radius: f64) -> Option<(usize, f64)> {
|
||||
self.snap_radii
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, rad)| (**rad - new_radius).abs() < POINT_RADIUS_HANDLE_SNAP_THRESHOLD)
|
||||
.min_by(|(i_a, a), (i_b, b)| {
|
||||
let dist_a = (**a - new_radius).abs();
|
||||
let dist_b = (**b - new_radius).abs();
|
||||
|
||||
// Check if either index is 0 or 1 and prioritize them
|
||||
match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) {
|
||||
// `a` is priority index, `b` is not
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
// `b` is priority index, `a` is not
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
// Normal comparison
|
||||
_ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal),
|
||||
}
|
||||
})
|
||||
.map(|(i, rad)| (i, *rad - original_radius))
|
||||
}
|
||||
|
||||
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
|
||||
let document_transform = document.network_interface.document_metadata().transform_to_document(layer);
|
||||
let center = viewport_transform.transform_point2(DVec2::ZERO);
|
||||
let radius_index = self.radius_index;
|
||||
|
||||
let original_radius = self.initial_radius;
|
||||
|
||||
let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start);
|
||||
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
|
||||
let projection = delta.project_onto(radius);
|
||||
let sign = radius.dot(delta).signum();
|
||||
|
||||
let mut net_delta = projection.length() * sign;
|
||||
let new_radius = original_radius + net_delta;
|
||||
|
||||
self.update_state(PointRadiusHandleState::Dragging);
|
||||
if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) {
|
||||
net_delta = snapped_delta;
|
||||
self.update_state(PointRadiusHandleState::Snapped(index));
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, radius_index),
|
||||
input: NodeInput::value(TaggedValue::F64(original_radius + net_delta), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
pub mod auto_panning;
|
||||
pub mod color_selector;
|
||||
pub mod compass_rose;
|
||||
pub mod gizmos;
|
||||
pub mod graph_modification_utils;
|
||||
pub mod measure;
|
||||
pub mod pivot;
|
||||
pub mod resize;
|
||||
pub mod shape_editor;
|
||||
pub mod shapes;
|
||||
pub mod snapping;
|
||||
pub mod transformation_cage;
|
||||
pub mod utility_functions;
|
||||
|
||||
@@ -8,7 +8,7 @@ use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resize {
|
||||
/// Stored as a document position so the start doesn't move if the canvas is panned.
|
||||
drag_start: DVec2,
|
||||
pub drag_start: DVec2,
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
pub snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use super::graph_modification_utils::merge_layers;
|
||||
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
||||
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
|
||||
use super::utility_functions::{adjust_handle_colinearity, calculate_bezier_bbox, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
|
||||
use crate::consts::HANDLE_LENGTH_FACTOR;
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
|
||||
use crate::messages::portfolio::document::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::preferences::SelectionMode;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
|
||||
use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
|
||||
use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, 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_std::vector::{HandleId, SegmentId};
|
||||
use graphene_std::vector::{HandleExt, HandleId, SegmentId};
|
||||
use graphene_std::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -45,6 +46,7 @@ pub enum ManipulatorAngle {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SelectedLayerState {
|
||||
selected_points: HashSet<ManipulatorPointId>,
|
||||
selected_segments: HashSet<SegmentId>,
|
||||
/// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`].
|
||||
ignore_handles: bool,
|
||||
ignore_anchors: bool,
|
||||
@@ -54,11 +56,27 @@ pub struct SelectedLayerState {
|
||||
}
|
||||
|
||||
impl SelectedLayerState {
|
||||
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
|
||||
pub fn selected_points(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
|
||||
self.selected_points.iter().copied()
|
||||
}
|
||||
|
||||
pub fn is_selected(&self, point: ManipulatorPointId) -> bool {
|
||||
pub fn selected_segments(&self) -> impl Iterator<Item = SegmentId> + '_ {
|
||||
self.selected_segments.iter().copied()
|
||||
}
|
||||
|
||||
pub fn selected_points_count(&self) -> usize {
|
||||
self.selected_points.len()
|
||||
}
|
||||
|
||||
pub fn selected_segments_count(&self) -> usize {
|
||||
self.selected_segments.len()
|
||||
}
|
||||
|
||||
pub fn is_segment_selected(&self, segment: SegmentId) -> bool {
|
||||
self.selected_segments.contains(&segment)
|
||||
}
|
||||
|
||||
pub fn is_point_selected(&self, point: ManipulatorPointId) -> bool {
|
||||
self.selected_points.contains(&point)
|
||||
}
|
||||
|
||||
@@ -66,10 +84,26 @@ impl SelectedLayerState {
|
||||
self.selected_points.insert(point);
|
||||
}
|
||||
|
||||
pub fn select_segment(&mut self, segment: SegmentId) {
|
||||
self.selected_segments.insert(segment);
|
||||
}
|
||||
|
||||
pub fn deselect_point(&mut self, point: ManipulatorPointId) {
|
||||
self.selected_points.remove(&point);
|
||||
}
|
||||
|
||||
pub fn deselect_segment(&mut self, segment: SegmentId) {
|
||||
self.selected_segments.remove(&segment);
|
||||
}
|
||||
|
||||
pub fn clear_points(&mut self) {
|
||||
self.selected_points.clear();
|
||||
}
|
||||
|
||||
pub fn clear_segments(&mut self) {
|
||||
self.selected_segments.clear();
|
||||
}
|
||||
|
||||
pub fn ignore_handles(&mut self, status: bool) {
|
||||
if self.ignore_handles != status {
|
||||
return;
|
||||
@@ -101,14 +135,6 @@ impl SelectedLayerState {
|
||||
self.ignored_anchor_points.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_points(&mut self) {
|
||||
self.selected_points.clear();
|
||||
}
|
||||
|
||||
pub fn selected_points_count(&self) -> usize {
|
||||
self.selected_points.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>;
|
||||
@@ -128,6 +154,12 @@ pub struct SelectedPointsInfo {
|
||||
pub vector_data: VectorData,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SelectedSegmentsInfo {
|
||||
pub segments: Vec<SegmentId>,
|
||||
pub vector_data: VectorData,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ManipulatorPointInfo {
|
||||
pub layer: LayerNodeIdentifier,
|
||||
@@ -136,6 +168,7 @@ pub struct ManipulatorPointInfo {
|
||||
|
||||
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<HandleId, f64>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClosestSegment {
|
||||
layer: LayerNodeIdentifier,
|
||||
segment: SegmentId,
|
||||
@@ -159,6 +192,10 @@ impl ClosestSegment {
|
||||
self.points
|
||||
}
|
||||
|
||||
pub fn bezier(&self) -> Bezier {
|
||||
self.bezier
|
||||
}
|
||||
|
||||
pub fn closest_point_document(&self) -> DVec2 {
|
||||
self.bezier.evaluate(TValue::Parametric(self.t))
|
||||
}
|
||||
@@ -473,7 +510,7 @@ impl ShapeState {
|
||||
|
||||
if let Some(id) = selected.as_anchor() {
|
||||
for neighbor in vector_data.connected_points(id) {
|
||||
if state.is_selected(ManipulatorPointId::Anchor(neighbor)) {
|
||||
if state.is_point_selected(ManipulatorPointId::Anchor(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
let Some(position) = vector_data.point_domain.position_from_id(neighbor) else { continue };
|
||||
@@ -512,38 +549,30 @@ impl ShapeState {
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
|
||||
let selected_shape_state = self.selected_shape_state.get(&layer)?;
|
||||
let already_selected = selected_shape_state.is_selected(manipulator_point_id);
|
||||
|
||||
// Should we select or deselect the point?
|
||||
let new_selected = if already_selected { !extend_selection } else { true };
|
||||
let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
|
||||
|
||||
// Offset to snap the selected point to the cursor
|
||||
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
|
||||
|
||||
// This is selecting the manipulator only for now, next to generalize to points
|
||||
if new_selected {
|
||||
let retain_existing_selection = extend_selection || already_selected;
|
||||
if !retain_existing_selection {
|
||||
self.deselect_all_points();
|
||||
}
|
||||
|
||||
// Add to the selected points
|
||||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||
selected_shape_state.select_point(manipulator_point_id);
|
||||
|
||||
let points = self
|
||||
.selected_shape_state
|
||||
.iter()
|
||||
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
|
||||
.collect();
|
||||
|
||||
return Some(Some(SelectedPointsInfo { points, offset, vector_data }));
|
||||
} else {
|
||||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||
selected_shape_state.deselect_point(manipulator_point_id);
|
||||
|
||||
return Some(None);
|
||||
let retain_existing_selection = extend_selection || already_selected;
|
||||
if !retain_existing_selection {
|
||||
self.deselect_all_points();
|
||||
self.deselect_all_segments();
|
||||
}
|
||||
|
||||
// Add to the selected points (deselect is managed in DraggingState, DragStop)
|
||||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||
selected_shape_state.select_point(manipulator_point_id);
|
||||
|
||||
let points = self
|
||||
.selected_shape_state
|
||||
.iter()
|
||||
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
|
||||
.collect();
|
||||
|
||||
return Some(Some(SelectedPointsInfo { points, offset, vector_data }));
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -555,11 +584,16 @@ impl ShapeState {
|
||||
select_threshold: f64,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
point_editing_mode: bool,
|
||||
) -> Option<(bool, Option<SelectedPointsInfo>)> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !point_editing_mode {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) {
|
||||
let vector_data = network_interface.compute_modified_vector(layer)?;
|
||||
let point_position = manipulator_point_id.get_position(&vector_data)?;
|
||||
@@ -572,7 +606,7 @@ impl ShapeState {
|
||||
}
|
||||
|
||||
let selected_shape_state = self.selected_shape_state.get(&layer)?;
|
||||
let already_selected = selected_shape_state.is_selected(manipulator_point_id);
|
||||
let already_selected = selected_shape_state.is_point_selected(manipulator_point_id);
|
||||
|
||||
// Offset to snap the selected point to the cursor
|
||||
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
|
||||
@@ -630,7 +664,7 @@ impl ShapeState {
|
||||
// Select all connected points
|
||||
while let Some(point) = selected_stack.pop() {
|
||||
let anchor_point = ManipulatorPointId::Anchor(point);
|
||||
if !state.is_selected(anchor_point) {
|
||||
if !state.is_point_selected(anchor_point) {
|
||||
state.select_point(anchor_point);
|
||||
selected_stack.extend(vector_data.connected_points(point));
|
||||
}
|
||||
@@ -671,6 +705,13 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deselects all segments across every selected layer
|
||||
pub fn deselect_all_segments(&mut self) {
|
||||
for state in self.selected_shape_state.values_mut() {
|
||||
state.selected_segments.clear()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_selected_anchors_status(&mut self, status: bool) {
|
||||
for state in self.selected_shape_state.values_mut() {
|
||||
self.ignore_anchors = !status;
|
||||
@@ -736,10 +777,18 @@ impl ShapeState {
|
||||
self.selected_shape_state.values().flat_map(|state| &state.selected_points)
|
||||
}
|
||||
|
||||
pub fn selected_segments(&self) -> impl Iterator<Item = &'_ SegmentId> {
|
||||
self.selected_shape_state.values().flat_map(|state| &state.selected_segments)
|
||||
}
|
||||
|
||||
pub fn selected_points_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<ManipulatorPointId>> {
|
||||
self.selected_shape_state.get(&layer).map(|state| &state.selected_points)
|
||||
}
|
||||
|
||||
pub fn selected_segments_in_layer(&self, layer: LayerNodeIdentifier) -> Option<&HashSet<SegmentId>> {
|
||||
self.selected_shape_state.get(&layer).map(|state| &state.selected_segments)
|
||||
}
|
||||
|
||||
pub fn move_primary(&self, segment: SegmentId, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||
responses.add(GraphOperationMessage::Vector {
|
||||
layer,
|
||||
@@ -766,7 +815,7 @@ impl ShapeState {
|
||||
let Some((start, _end, bezier)) = vector_data.segment_points_from_id(segment) else { continue };
|
||||
|
||||
if let BezierHandles::Quadratic { handle } = bezier.handles {
|
||||
if selected.is_some_and(|selected| selected.is_selected(ManipulatorPointId::Anchor(start))) {
|
||||
if selected.is_some_and(|selected| selected.is_point_selected(ManipulatorPointId::Anchor(start))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -850,6 +899,9 @@ impl ShapeState {
|
||||
let non_zero_handles = handles.iter().filter(|handle| handle.length(vector_data) > 1e-6).count();
|
||||
let handle_segments = handles.iter().map(|handles| handles.segment).collect::<Vec<_>>();
|
||||
|
||||
// Check if the anchor is connected to linear segments and has no handles
|
||||
let linear_segments = vector_data.connected_linear_segments(point_id) != 0;
|
||||
|
||||
// Grab the next and previous manipulator groups by simply looking at the next / previous index
|
||||
let points = handles.iter().map(|handle| vector_data.other_point(handle.segment, point_id));
|
||||
let anchor_positions = points
|
||||
@@ -891,7 +943,7 @@ impl ShapeState {
|
||||
handle_direction *= -1.;
|
||||
}
|
||||
|
||||
if non_zero_handles != 0 {
|
||||
if non_zero_handles != 0 && !linear_segments {
|
||||
let [a, b] = handles.as_slice() else { return };
|
||||
let (non_zero_handle, zero_handle) = if a.length(vector_data) > 1e-6 { (a, b) } else { (b, a) };
|
||||
let Some(direction) = non_zero_handle
|
||||
@@ -1012,9 +1064,9 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the selected points by dragging the mouse.
|
||||
/// Move the selected points and segments by dragging the mouse.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn move_selected_points(
|
||||
pub fn move_selected_points_and_segments(
|
||||
&self,
|
||||
handle_lengths: Option<OpposingHandleLengths>,
|
||||
document: &DocumentMessageHandler,
|
||||
@@ -1040,7 +1092,17 @@ impl ShapeState {
|
||||
};
|
||||
let delta = delta_transform.inverse().transform_vector2(delta);
|
||||
|
||||
for &point in state.selected_points.iter() {
|
||||
// Make a new collection of anchor points which needs to be moved
|
||||
let mut affected_points = state.selected_points.clone();
|
||||
|
||||
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
if state.is_segment_selected(segment_id) {
|
||||
affected_points.insert(ManipulatorPointId::Anchor(start));
|
||||
affected_points.insert(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
}
|
||||
|
||||
for &point in affected_points.iter() {
|
||||
if self.is_point_ignored(&point) {
|
||||
continue;
|
||||
}
|
||||
@@ -1055,7 +1117,7 @@ impl ShapeState {
|
||||
};
|
||||
|
||||
let Some(anchor_id) = point.get_anchor(&vector_data) else { continue };
|
||||
if state.is_selected(ManipulatorPointId::Anchor(anchor_id)) {
|
||||
if state.is_point_selected(ManipulatorPointId::Anchor(anchor_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1074,7 +1136,7 @@ impl ShapeState {
|
||||
continue;
|
||||
}
|
||||
|
||||
if state.is_selected(other.to_manipulator_point()) {
|
||||
if state.is_point_selected(other.to_manipulator_point()) {
|
||||
// If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state.
|
||||
let handles = [handle, other];
|
||||
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||
@@ -1125,12 +1187,12 @@ impl ShapeState {
|
||||
// ii) The anchor is not selected.
|
||||
|
||||
let anchor = handles[0].to_manipulator_point().get_anchor(&vector_data)?;
|
||||
let anchor_selected = state.is_selected(ManipulatorPointId::Anchor(anchor));
|
||||
let anchor_selected = state.is_point_selected(ManipulatorPointId::Anchor(anchor));
|
||||
if anchor_selected {
|
||||
return None;
|
||||
}
|
||||
|
||||
let handles_selected = handles.map(|handle| state.is_selected(handle.to_manipulator_point()));
|
||||
let handles_selected = handles.map(|handle| state.is_point_selected(handle.to_manipulator_point()));
|
||||
|
||||
let other = match handles_selected {
|
||||
[true, false] => handles[1],
|
||||
@@ -1208,11 +1270,15 @@ impl ShapeState {
|
||||
continue;
|
||||
};
|
||||
|
||||
let selected_segments = &state.selected_segments;
|
||||
|
||||
for point in std::mem::take(&mut state.selected_points) {
|
||||
match point {
|
||||
ManipulatorPointId::Anchor(anchor) => {
|
||||
if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector_data) {
|
||||
missing_anchors.insert(anchor, handles);
|
||||
if !vector_data.all_connected(anchor).any(|a| selected_segments.contains(&a.segment)) {
|
||||
missing_anchors.insert(anchor, handles);
|
||||
}
|
||||
}
|
||||
deleted_anchors.insert(anchor);
|
||||
}
|
||||
@@ -1257,6 +1323,8 @@ impl ShapeState {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Avoid reconnecting to points which have adjacent segments selected
|
||||
|
||||
// Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor
|
||||
let [handle_start, handle_end] = [start, end].map(|(handle, _)| {
|
||||
let handle = handle.opposite();
|
||||
@@ -1304,6 +1372,20 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_selected_segments(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for (segment, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
if state.selected_segments.contains(&segment) {
|
||||
self.dissolve_segment(responses, layer, &vector_data, segment, [start, end]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
@@ -1693,10 +1775,13 @@ impl ShapeState {
|
||||
.filter(|&handle| anchor.abs_diff_eq(handle, 1e-5))
|
||||
.count();
|
||||
|
||||
// Check if the anchor is connected to linear segments.
|
||||
let one_or_more_segment_linear = vector_data.connected_linear_segments(id) != 0;
|
||||
|
||||
// Check by comparing the handle positions to the anchor if this manipulator group is a point
|
||||
for point in self.selected_points() {
|
||||
let Some(point_id) = point.as_anchor() else { continue };
|
||||
if positions != 0 {
|
||||
if positions != 0 || one_or_more_segment_linear {
|
||||
self.convert_manipulator_handles_to_colinear(&vector_data, point_id, responses, layer);
|
||||
} else {
|
||||
for handle in vector_data.all_connected(point_id) {
|
||||
@@ -1748,6 +1833,7 @@ impl ShapeState {
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn select_all_in_shape(
|
||||
&mut self,
|
||||
network_interface: &NodeNetworkInterface,
|
||||
@@ -1755,13 +1841,17 @@ impl ShapeState {
|
||||
selection_change: SelectionChange,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
select_segments: bool,
|
||||
// Here, "selection mode" represents touched or enclosed, not to be confused with editing modes
|
||||
selection_mode: SelectionMode,
|
||||
) {
|
||||
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()
|
||||
state.clear_points();
|
||||
state.clear_segments();
|
||||
}
|
||||
|
||||
let vector_data = network_interface.compute_modified_vector(layer);
|
||||
@@ -1787,7 +1877,46 @@ impl ShapeState {
|
||||
None
|
||||
};
|
||||
|
||||
// Selection segments
|
||||
for (id, bezier, _, _) in vector_data.segment_bezier_iter() {
|
||||
if select_segments {
|
||||
// Select segments if they lie inside the bounding box or lasso polygon
|
||||
let segment_bbox = calculate_bezier_bbox(bezier);
|
||||
let bottom_left = transform.transform_point2(segment_bbox[0]);
|
||||
let top_right = transform.transform_point2(segment_bbox[1]);
|
||||
|
||||
let select = match selection_shape {
|
||||
SelectionShape::Box(quad) => {
|
||||
let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all();
|
||||
match selection_mode {
|
||||
SelectionMode::Enclosed => enclosed,
|
||||
_ => {
|
||||
// Check for intersection with the segment
|
||||
enclosed || is_intersecting(bezier, quad, transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
SelectionShape::Lasso(_) => {
|
||||
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand.");
|
||||
|
||||
// Sample 10 points on the bezier and check if all or some lie inside the polygon
|
||||
let points = bezier.compute_lookup_table(Some(10), None);
|
||||
match selection_mode {
|
||||
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains_point(p)),
|
||||
_ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains_point(p)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if select {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_segment(id),
|
||||
_ => state.select_segment(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Selecting handles
|
||||
for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] {
|
||||
let Some(position) = position else { continue };
|
||||
let transformed_position = transform.transform_point2(position);
|
||||
@@ -1820,6 +1949,7 @@ impl ShapeState {
|
||||
}
|
||||
}
|
||||
|
||||
// Checking for selection of anchor points
|
||||
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
|
||||
let transformed_position = transform.transform_point2(position);
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
use super::shape_utility::ShapeToolModifierKey;
|
||||
use super::*;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Ellipse;
|
||||
|
||||
impl Ellipse {
|
||||
pub fn create_node() -> NodeTemplate {
|
||||
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node can't be found");
|
||||
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
|
||||
}
|
||||
|
||||
pub fn update_shape(
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
layer: LayerNodeIdentifier,
|
||||
shape_tool_data: &mut ShapeToolData,
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false),
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_translation(start.midpoint(end)),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ellipse {
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graphene_std::vector::generator_nodes::ellipse;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ResolvedEllipse {
|
||||
radius_x: f64,
|
||||
radius_y: f64,
|
||||
transform: DAffine2,
|
||||
}
|
||||
|
||||
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
|
||||
let instrumented = match editor.eval_graph().await {
|
||||
Ok(instrumented) => instrumented,
|
||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
||||
};
|
||||
|
||||
let document = editor.active_document();
|
||||
let layers = document.metadata().all_layers();
|
||||
layers
|
||||
.filter_map(|layer| {
|
||||
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?;
|
||||
Some(ResolvedEllipse {
|
||||
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_simple() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
|
||||
|
||||
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 4.5,
|
||||
radius_y: 5.,
|
||||
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_circle() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 10.,
|
||||
radius_y: 10.,
|
||||
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
println!("{ellipse:?}");
|
||||
assert_eq!(ellipse[0].radius_x, 5.);
|
||||
assert_eq!(ellipse[0].radius_y, 5.);
|
||||
|
||||
assert!(
|
||||
ellipse[0]
|
||||
.transform
|
||||
.abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_center_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(ellipse[0].radius_x, 10.);
|
||||
assert_eq!(ellipse[0].radius_y, 10.);
|
||||
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_cancel() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool_cancel_rmb(ToolType::Ellipse).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
use super::shape_utility::ShapeToolModifierKey;
|
||||
use crate::consts::{BOUNDS_SELECT_THRESHOLD, LINE_ROTATE_SNAP_ANGLE};
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
pub use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration};
|
||||
use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug, Default)]
|
||||
pub enum LineEnd {
|
||||
#[default]
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LineToolData {
|
||||
pub drag_start: DVec2,
|
||||
pub drag_current: DVec2,
|
||||
pub angle: f64,
|
||||
pub weight: f64,
|
||||
pub selected_layers_with_position: HashMap<LayerNodeIdentifier, [DVec2; 2]>,
|
||||
pub editing_layer: Option<LayerNodeIdentifier>,
|
||||
pub dragging_endpoint: Option<LineEnd>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Line;
|
||||
|
||||
impl Line {
|
||||
pub fn create_node(document: &DocumentMessageHandler, drag_start: DVec2) -> NodeTemplate {
|
||||
let node_type = resolve_document_node_type("Line").expect("Line node can't be found");
|
||||
node_type.node_template_input_override([
|
||||
None,
|
||||
Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)),
|
||||
Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn update_shape(
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
layer: LayerNodeIdentifier,
|
||||
shape_tool_data: &mut ShapeToolData,
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, _, lock_angle, snap_angle] = modifier;
|
||||
|
||||
shape_tool_data.line_data.drag_current = ipp.mouse.position;
|
||||
|
||||
let keyboard = &ipp.keyboard;
|
||||
let ignore = [layer];
|
||||
let snap_data = SnapData::ignore(document, ipp, &ignore);
|
||||
let mut document_points = generate_line(shape_tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
|
||||
|
||||
if shape_tool_data.line_data.dragging_endpoint == Some(LineEnd::Start) {
|
||||
document_points.swap(0, 1);
|
||||
}
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
|
||||
shape_tool_data.line_data.selected_layers_with_position = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.filter_map(|layer| {
|
||||
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?;
|
||||
|
||||
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point));
|
||||
if !start.abs_diff_eq(end, f64::EPSILON * 1000.) {
|
||||
overlay_context.line(viewport_start, viewport_end, None, None);
|
||||
overlay_context.square(viewport_start, Some(6.), None, None);
|
||||
overlay_context.square(viewport_end, Some(6.), None, None);
|
||||
}
|
||||
|
||||
Some((layer, [start, end]))
|
||||
})
|
||||
.collect::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
|
||||
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
|
||||
let mut document_points = [tool_data.data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current)];
|
||||
|
||||
let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X);
|
||||
let mut line_length = (document_points[1] - document_points[0]).length();
|
||||
|
||||
if lock_angle {
|
||||
angle = tool_data.line_data.angle;
|
||||
} else if snap_angle {
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
}
|
||||
|
||||
tool_data.line_data.angle = angle;
|
||||
|
||||
if lock_angle {
|
||||
let angle_vec = DVec2::new(angle.cos(), angle.sin());
|
||||
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
|
||||
}
|
||||
|
||||
document_points[1] = document_points[0] + line_length * DVec2::new(angle.cos(), angle.sin());
|
||||
|
||||
let constrained = snap_angle || lock_angle;
|
||||
let snap = &mut tool_data.data.snap_manager;
|
||||
|
||||
let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.data.drag_start]);
|
||||
let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.data.drag_start]);
|
||||
let config = SnapTypeConfiguration::default();
|
||||
|
||||
if constrained {
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: document_points[0],
|
||||
direction: document_points[1] - document_points[0],
|
||||
};
|
||||
if center {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
|
||||
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
|
||||
document_points[0] = best.snapped_point_document;
|
||||
snap.update_indicator(best);
|
||||
} else {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
|
||||
document_points[1] = snapped.snapped_point_document;
|
||||
snap.update_indicator(snapped);
|
||||
}
|
||||
} else if center {
|
||||
let snapped = snap.free_snap(&snap_data, &near_point, config);
|
||||
let snapped_far = snap.free_snap(&snap_data, &far_point, config);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
|
||||
document_points[0] = best.snapped_point_document;
|
||||
snap.update_indicator(best);
|
||||
} else {
|
||||
let snapped = snap.free_snap(&snap_data, &near_point, config);
|
||||
document_points[1] = snapped.snapped_point_document;
|
||||
snap.update_indicator(snapped);
|
||||
}
|
||||
|
||||
document_points
|
||||
}
|
||||
|
||||
pub fn clicked_on_line_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let (Some(&TaggedValue::DVec2(document_start)), Some(&TaggedValue::DVec2(document_end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
|
||||
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
|
||||
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
|
||||
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
|
||||
|
||||
let drag_start = input.mouse.position;
|
||||
let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(point));
|
||||
|
||||
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
|
||||
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
|
||||
|
||||
if start_click || end_click {
|
||||
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
|
||||
shape_tool_data.data.drag_start = if end_click { document_start } else { document_end };
|
||||
shape_tool_data.line_data.editing_layer = Some(layer);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_line_tool {
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
|
||||
async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> {
|
||||
let document = editor.active_document();
|
||||
let network_interface = &document.network_interface;
|
||||
let node_id = network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(network_interface)
|
||||
.filter_map(|layer| {
|
||||
let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?;
|
||||
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
|
||||
return None;
|
||||
};
|
||||
Some((start, end))
|
||||
})
|
||||
.next();
|
||||
node_id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_basicdraw() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)");
|
||||
assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_with_transformed_viewport() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
angle_radians: (30. as f64).to_radians(),
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
let document = editor.active_document();
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let viewport_to_document = document_to_viewport.inverse();
|
||||
|
||||
let expected_start = viewport_to_document.transform_point2(DVec2::ZERO);
|
||||
let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.));
|
||||
|
||||
assert!(
|
||||
(start_input - expected_start).length() < 1.,
|
||||
"Start point should match expected document coordinates. Got {:?}, expected {:?}",
|
||||
start_input,
|
||||
expected_start
|
||||
);
|
||||
assert!(
|
||||
(end_input - expected_end).length() < 1.,
|
||||
"End point should match expected document coordinates. Got {:?}, expected {:?}",
|
||||
end_input,
|
||||
expected_end
|
||||
);
|
||||
} else {
|
||||
panic!("Line was not created successfully with transformed viewport");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_ctrl_anglelock() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let line_vec = end_input - start_input;
|
||||
let original_angle = line_vec.angle_to(DVec2::X);
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
|
||||
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
|
||||
match (updated_start, updated_end) {
|
||||
(updated_start, updated_end) => {
|
||||
let updated_line_vec = updated_end - updated_start;
|
||||
let updated_angle = updated_line_vec.angle_to(DVec2::X);
|
||||
print!("{:?}", original_angle);
|
||||
print!("{:?}", updated_angle);
|
||||
assert!(
|
||||
line_vec.normalize().dot(updated_line_vec.normalize()).abs() - 1. < 1e-6,
|
||||
"Line angle should be locked when Ctrl is kept pressed"
|
||||
);
|
||||
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_alt() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let expected_start = DVec2::new(0., 100.);
|
||||
let expected_end = DVec2::new(200., 100.);
|
||||
assert!((start_input - expected_start).length() < 1., "Start point should be near (0, 100)");
|
||||
assert!((end_input - expected_end).length() < 1., "End point should be near (200, 100)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_alt_shift_drag() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let line_vec = end_input - start_input;
|
||||
let angle_radians = line_vec.angle_to(DVec2::X);
|
||||
let angle_degrees = angle_radians.to_degrees();
|
||||
let nearest_angle = (angle_degrees / 15.).round() * 15.;
|
||||
|
||||
assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_with_transformed_artboard() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await;
|
||||
|
||||
let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard");
|
||||
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::TransformChange {
|
||||
layer: artboard_id,
|
||||
transform: DAffine2::from_angle(45_f64.to_radians()),
|
||||
transform_in: TransformIn::Local,
|
||||
skip_rerender: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await;
|
||||
|
||||
let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard");
|
||||
// The line should still be diagonal with equal change in x and y
|
||||
let line_vector = end_input - start_input;
|
||||
// Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square)
|
||||
let line_length = line_vector.length();
|
||||
assert!(
|
||||
(line_length - 141.42).abs() < 1., // 100 * sqrt(2) ~= 141.42
|
||||
"Line length should be approximately 141.42 units. Got: {line_length}"
|
||||
);
|
||||
assert!((line_vector.x - 100.).abs() < 1., "X-component of line vector should be approximately 100. Got: {}", line_vector.x);
|
||||
assert!(
|
||||
(line_vector.y.abs() - 100.).abs() < 1.,
|
||||
"Absolute Y-component of line vector should be approximately 100. Got: {}",
|
||||
line_vector.y.abs()
|
||||
);
|
||||
let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees();
|
||||
assert!((angle_degrees - (-45.)).abs() < 1., "Line angle should be close to -45 degrees. Got: {angle_degrees}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod ellipse_shape;
|
||||
pub mod line_shape;
|
||||
pub mod polygon_shape;
|
||||
pub mod rectangle_shape;
|
||||
pub mod shape_utility;
|
||||
pub mod star_shape;
|
||||
|
||||
pub use super::shapes::ellipse_shape::Ellipse;
|
||||
pub use super::shapes::line_shape::{Line, LineEnd};
|
||||
pub use super::shapes::rectangle_shape::Rectangle;
|
||||
pub use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
|
||||
@@ -0,0 +1,151 @@
|
||||
use super::shape_utility::ShapeToolModifierKey;
|
||||
use super::shape_utility::update_radius_sign;
|
||||
use super::*;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDial;
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::NumberOfPointsDialState;
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandle;
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandleState;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::polygon_outline;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PolygonGizmoHandler {
|
||||
number_of_points_dial: NumberOfPointsDial,
|
||||
point_radius_handle: PointRadiusHandle,
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandler for PolygonGizmoHandler {
|
||||
fn is_any_gizmo_hovered(&self) -> bool {
|
||||
self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered()
|
||||
}
|
||||
|
||||
fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses);
|
||||
self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses);
|
||||
}
|
||||
|
||||
fn handle_click(&mut self) {
|
||||
if self.number_of_points_dial.is_hovering() {
|
||||
self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.point_radius_handle.hovered() {
|
||||
self.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if self.number_of_points_dial.is_dragging() {
|
||||
self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() {
|
||||
self.point_radius_handle.update_inner_radius(document, input, responses, drag_start);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
selected_polygon_layer: Option<LayerNodeIdentifier>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
self.number_of_points_dial.overlays(document, selected_polygon_layer, shape_editor, mouse_position, overlay_context);
|
||||
self.point_radius_handle.overlays(selected_polygon_layer, document, input, mouse_position, overlay_context);
|
||||
|
||||
polygon_outline(selected_polygon_layer, document, overlay_context);
|
||||
}
|
||||
|
||||
fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if self.number_of_points_dial.is_dragging() {
|
||||
self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() {
|
||||
self.point_radius_handle.overlays(None, document, input, mouse_position, overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.number_of_points_dial.cleanup();
|
||||
self.point_radius_handle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Polygon;
|
||||
|
||||
impl Polygon {
|
||||
pub fn create_node(vertices: u32) -> NodeTemplate {
|
||||
let node_type = resolve_document_node_type("Regular Polygon").expect("Regular Polygon can't be found");
|
||||
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::U32(vertices), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
|
||||
}
|
||||
|
||||
pub fn update_shape(
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
layer: LayerNodeIdentifier,
|
||||
shape_tool_data: &mut ShapeToolData,
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
update_radius_sign(end, start, layer, document, responses);
|
||||
|
||||
let dimensions = (start - end).abs();
|
||||
|
||||
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
|
||||
let mut scale = DVec2::ONE;
|
||||
let radius;
|
||||
if dimensions.x > dimensions.y {
|
||||
scale.x = dimensions.x / dimensions.y;
|
||||
radius = dimensions.y / 2.;
|
||||
} else {
|
||||
scale.y = dimensions.y / dimensions.x;
|
||||
radius = dimensions.x / 2.;
|
||||
}
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use super::shape_utility::ShapeToolModifierKey;
|
||||
use super::*;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Rectangle;
|
||||
|
||||
impl Rectangle {
|
||||
pub fn create_node() -> NodeTemplate {
|
||||
let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node can't be found");
|
||||
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))])
|
||||
}
|
||||
|
||||
pub fn update_shape(
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
layer: LayerNodeIdentifier,
|
||||
shape_tool_data: &mut ShapeToolData,
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false),
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_translation(start.midpoint(end)),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
use super::ShapeToolData;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage, Responses};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::Key;
|
||||
use crate::messages::tool::utility_types::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::vector::click_target::ClickTargetType;
|
||||
use graphene_std::vector::misc::dvec2_to_point;
|
||||
use kurbo::{BezPath, PathEl, Shape};
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ShapeType {
|
||||
#[default]
|
||||
Polygon = 0,
|
||||
Star = 1,
|
||||
Rectangle = 2,
|
||||
Ellipse = 3,
|
||||
Line = 4,
|
||||
}
|
||||
|
||||
impl ShapeType {
|
||||
pub fn name(&self) -> String {
|
||||
(match self {
|
||||
Self::Polygon => "Polygon",
|
||||
Self::Star => "Star",
|
||||
Self::Rectangle => "Rectangle",
|
||||
Self::Ellipse => "Ellipse",
|
||||
Self::Line => "Line",
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn tooltip(&self) -> String {
|
||||
(match self {
|
||||
Self::Line => "Line Tool",
|
||||
Self::Rectangle => "Rectangle Tool",
|
||||
Self::Ellipse => "Ellipse Tool",
|
||||
_ => "",
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> String {
|
||||
(match self {
|
||||
Self::Line => "VectorLineTool",
|
||||
Self::Rectangle => "VectorRectangleTool",
|
||||
Self::Ellipse => "VectorEllipseTool",
|
||||
_ => "",
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn tool_type(&self) -> ToolType {
|
||||
match self {
|
||||
Self::Line => ToolType::Line,
|
||||
Self::Rectangle => ToolType::Rectangle,
|
||||
Self::Ellipse => ToolType::Ellipse,
|
||||
_ => ToolType::Shape,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ShapeToolModifierKey = [Key; 4];
|
||||
|
||||
/// The `ShapeGizmoHandler` trait defines the interactive behavior and overlay logic for shape-specific tools in the editor.
|
||||
/// A gizmo is a visual handle or control point used to manipulate a shape's properties (e.g., number of sides, radius, angle).
|
||||
pub trait ShapeGizmoHandler {
|
||||
/// Called every frame to update the gizmo's interaction state based on the mouse position and selection.
|
||||
///
|
||||
/// This includes detecting hover states and preparing interaction flags or visual feedback (e.g., highlighting a hovered handle).
|
||||
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>);
|
||||
|
||||
/// Called when a mouse click occurs over the canvas and a gizmo handle is hovered.
|
||||
///
|
||||
/// Used to initiate drag interactions or toggle states on the handle, depending on the tool.
|
||||
/// For example, a hovered "number of points" handle might enter a "Dragging" state.
|
||||
fn handle_click(&mut self);
|
||||
|
||||
/// Called during a drag interaction to update the shape's parameters in real time.
|
||||
///
|
||||
/// For example, a handle might calculate the distance from the drag start to determine a new radius or update the number of points.
|
||||
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>);
|
||||
|
||||
/// Draws the static or hover-dependent overlays associated with the gizmo.
|
||||
///
|
||||
/// These overlays include visual indicators like shape outlines, control points, and hover highlights.
|
||||
fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
selected_shape_layers: Option<LayerNodeIdentifier>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
);
|
||||
|
||||
/// Draws overlays specifically during a drag operation.
|
||||
///
|
||||
/// Used to give real-time visual feedback based on drag progress, such as showing the updated shape preview or snapping guides.
|
||||
fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
);
|
||||
|
||||
/// Returns `true` if any handle or control point in the gizmo is currently being hovered.
|
||||
fn is_any_gizmo_hovered(&self) -> bool;
|
||||
|
||||
/// Resets or clears any internal state maintained by the gizmo when it is no longer active.
|
||||
///
|
||||
/// For example, dragging states or hover flags should be cleared to avoid visual glitches when switching tools or shapes.
|
||||
fn cleanup(&mut self);
|
||||
}
|
||||
|
||||
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
|
||||
pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let sign_num = if end[1] > start[1] { 1. } else { -1. };
|
||||
let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
|
||||
if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
|
||||
let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return };
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(polygon_node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
|
||||
let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return };
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(star_node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(star_node_id, 3),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_cage_overlays(document: &DocumentMessageHandler, tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
|
||||
let mut transform = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Check if the matrix is not invertible
|
||||
let mut transform_tampered = false;
|
||||
if transform.matrix2.determinant() == 0. {
|
||||
transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
|
||||
transform_tampered = true;
|
||||
}
|
||||
|
||||
let bounds = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.filter_map(|layer| {
|
||||
document
|
||||
.metadata()
|
||||
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
|
||||
})
|
||||
.reduce(graphene_std::renderer::Quad::combine_bounds);
|
||||
|
||||
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.render_overlays(overlay_context, true);
|
||||
} else {
|
||||
tool_data.bounding_box_manager.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anchor_overlays(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
overlay_context.outline_vector(&vector_data, transform);
|
||||
|
||||
for (_, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
|
||||
overlay_context.manipulator_anchor(transform.transform_point2(position), false, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the node input values of Star.
|
||||
/// Returns an option of (sides, radius1, radius2).
|
||||
pub fn extract_star_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64, f64)> {
|
||||
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Star")?;
|
||||
|
||||
let (Some(&TaggedValue::U32(sides)), Some(&TaggedValue::F64(radius_1)), Some(&TaggedValue::F64(radius_2))) =
|
||||
(node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value())
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((sides, radius_1, radius_2))
|
||||
}
|
||||
|
||||
/// Extract the node input values of Polygon.
|
||||
/// Returns an option of (sides, radius).
|
||||
pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64)> {
|
||||
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Regular Polygon")?;
|
||||
|
||||
let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(radius))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((n, radius))
|
||||
}
|
||||
|
||||
/// Calculate the viewport position of as a star vertex given its index
|
||||
pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 {
|
||||
let angle = ((vertex_index as f64) * PI) / (n as f64);
|
||||
let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 };
|
||||
|
||||
viewport.transform_point2(DVec2 {
|
||||
x: radius * angle.sin(),
|
||||
y: -radius * angle.cos(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate the viewport position of a polygon vertex given its index
|
||||
pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius: f64) -> DVec2 {
|
||||
let angle = ((vertex_index as f64) * TAU) / (n as f64);
|
||||
|
||||
viewport.transform_point2(DVec2 {
|
||||
x: radius * angle.sin(),
|
||||
y: -radius * angle.cos(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Outlines the geometric shape made by star-node
|
||||
pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
let Some(layer) = layer else { return };
|
||||
let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
let points = sides as u64;
|
||||
let diameter: f64 = radius1 * 2.;
|
||||
let inner_diameter = radius2 * 2.;
|
||||
|
||||
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))];
|
||||
|
||||
overlay_context.outline(subpath.iter(), viewport, None);
|
||||
}
|
||||
|
||||
/// Outlines the geometric shape made by polygon-node
|
||||
pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
let Some(layer) = layer else { return };
|
||||
let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
let points = sides as u64;
|
||||
let radius: f64 = radius * 2.;
|
||||
|
||||
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))];
|
||||
|
||||
overlay_context.outline(subpath.iter(), viewport, None);
|
||||
}
|
||||
|
||||
/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications
|
||||
pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
for i in 0..2 * n {
|
||||
let new_point = dvec2_to_point(star_vertex_position(viewport, i as i32, n, radius1, radius2));
|
||||
|
||||
if i == 0 {
|
||||
paths.push(PathEl::MoveTo(new_point));
|
||||
} else {
|
||||
paths.push(PathEl::LineTo(new_point));
|
||||
}
|
||||
}
|
||||
|
||||
paths.push(PathEl::ClosePath);
|
||||
|
||||
let bez_path = BezPath::from_vec(paths);
|
||||
let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box());
|
||||
|
||||
if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y {
|
||||
return false;
|
||||
}
|
||||
|
||||
let winding = shape.winding(dvec2_to_point(mouse_position));
|
||||
|
||||
// Non-zero fill rule
|
||||
winding != 0
|
||||
}
|
||||
|
||||
/// Check if the the cursor is inside the geometric polygon shape made by the Polygon node without any upstream node modifications
|
||||
pub fn inside_polygon(viewport: DAffine2, n: u32, radius: f64, mouse_position: DVec2) -> bool {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
for i in 0..n {
|
||||
let new_point = dvec2_to_point(polygon_vertex_position(viewport, i as i32, n, radius));
|
||||
|
||||
if i == 0 {
|
||||
paths.push(PathEl::MoveTo(new_point));
|
||||
} else {
|
||||
paths.push(PathEl::LineTo(new_point));
|
||||
}
|
||||
}
|
||||
|
||||
paths.push(PathEl::ClosePath);
|
||||
|
||||
let bez_path = BezPath::from_vec(paths);
|
||||
let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box());
|
||||
|
||||
if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y {
|
||||
return false;
|
||||
}
|
||||
|
||||
let winding = shape.winding(dvec2_to_point(mouse_position));
|
||||
|
||||
// Non-zero fill rule
|
||||
winding != 0
|
||||
}
|
||||
|
||||
pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffine2, angle: f64, overlay_context: &mut OverlayContext) {
|
||||
for &snapped_radius in snap_radii {
|
||||
let Some(tick_direction) = direction.perp().try_normalize() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let tick_position = viewport.transform_point2(DVec2 {
|
||||
x: snapped_radius * angle.sin(),
|
||||
y: -snapped_radius * angle.cos(),
|
||||
});
|
||||
|
||||
overlay_context.line(tick_position, tick_position + tick_direction * 5., None, Some(2.));
|
||||
overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use super::shape_utility::{ShapeToolModifierKey, update_radius_sign};
|
||||
use super::*;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::{NumberOfPointsDial, NumberOfPointsDialState};
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, star_outline};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use core::f64;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct StarGizmoHandler {
|
||||
number_of_points_dial: NumberOfPointsDial,
|
||||
point_radius_handle: PointRadiusHandle,
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandler for StarGizmoHandler {
|
||||
fn is_any_gizmo_hovered(&self) -> bool {
|
||||
self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered()
|
||||
}
|
||||
|
||||
fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses);
|
||||
self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses);
|
||||
}
|
||||
|
||||
fn handle_click(&mut self) {
|
||||
if self.number_of_points_dial.is_hovering() {
|
||||
self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.point_radius_handle.hovered() {
|
||||
self.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if self.number_of_points_dial.is_dragging() {
|
||||
self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() {
|
||||
self.point_radius_handle.update_inner_radius(document, input, responses, drag_start);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
selected_star_layer: Option<LayerNodeIdentifier>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
self.number_of_points_dial.overlays(document, selected_star_layer, shape_editor, mouse_position, overlay_context);
|
||||
self.point_radius_handle.overlays(selected_star_layer, document, input, mouse_position, overlay_context);
|
||||
|
||||
star_outline(selected_star_layer, document, overlay_context);
|
||||
}
|
||||
|
||||
fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if self.number_of_points_dial.is_dragging() {
|
||||
self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context);
|
||||
}
|
||||
|
||||
if self.point_radius_handle.is_dragging_or_snapped() {
|
||||
self.point_radius_handle.overlays(None, document, input, mouse_position, overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.number_of_points_dial.cleanup();
|
||||
self.point_radius_handle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Star;
|
||||
|
||||
impl Star {
|
||||
pub fn create_node(vertices: u32) -> NodeTemplate {
|
||||
let node_type = resolve_document_node_type("Star").expect("Star node can't be found");
|
||||
node_type.node_template_input_override([
|
||||
None,
|
||||
Some(NodeInput::value(TaggedValue::U32(vertices), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn update_shape(
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
layer: LayerNodeIdentifier,
|
||||
shape_tool_data: &mut ShapeToolData,
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let [center, lock_ratio, _, _] = modifier;
|
||||
|
||||
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
update_radius_sign(end, start, layer, document, responses);
|
||||
|
||||
let dimensions = (start - end).abs();
|
||||
|
||||
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
|
||||
let mut scale = DVec2::ONE;
|
||||
let radius: f64;
|
||||
if dimensions.x > dimensions.y {
|
||||
scale.x = dimensions.x / dimensions.y;
|
||||
radius = dimensions.y / 2.;
|
||||
} else {
|
||||
scale.y = dimensions.y / dimensions.x;
|
||||
radius = dimensions.x / 2.;
|
||||
}
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 3),
|
||||
input: NodeInput::value(TaggedValue::F64(radius / 2.), false),
|
||||
});
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,6 +250,10 @@ impl SnapManager {
|
||||
self.update_indicator(snapped);
|
||||
}
|
||||
|
||||
pub fn indicator_pos(&self) -> Option<DVec2> {
|
||||
self.indicator.as_ref().map(|point| point.snapped_point_document)
|
||||
}
|
||||
|
||||
fn find_best_snap(snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: SnapResults, constrained: bool, off_screen: bool, to_path: bool) -> SnappedPoint {
|
||||
let mut snapped_points = Vec::new();
|
||||
let document = snap_data.document;
|
||||
|
||||
@@ -70,7 +70,7 @@ impl AlignmentSnapper {
|
||||
if let Some(quad) = target_point.quad.map(|q| q.0) {
|
||||
if quad[0] == quad[3] && quad[1] == quad[2] && quad[0] == target_point.document_point {
|
||||
let [p1, p2, ..] = quad;
|
||||
let direction = (p2 - p1).normalize();
|
||||
let Some(direction) = (p2 - p1).try_normalize() else { return };
|
||||
let normal = DVec2::new(-direction.y, direction.x);
|
||||
|
||||
for endpoint in [p1, p2] {
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::misc::*;
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::math::math_ext::QuadExt;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::PointId;
|
||||
|
||||
|
||||
@@ -578,7 +578,9 @@ impl BoundingBoxManager {
|
||||
category,
|
||||
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedLandscape
|
||||
) {
|
||||
horizontal_edges.map(|point| draw_handle(point, horizontal_angle));
|
||||
for point in horizontal_edges {
|
||||
draw_handle(point, horizontal_angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the vertical midpoint drag handles
|
||||
@@ -586,7 +588,9 @@ impl BoundingBoxManager {
|
||||
category,
|
||||
TransformCageSizeCategory::Full | TransformCageSizeCategory::Narrow | TransformCageSizeCategory::ReducedPortrait
|
||||
) {
|
||||
vertical_edges.map(|point| draw_handle(point, vertical_angle));
|
||||
for point in vertical_edges {
|
||||
draw_handle(point, vertical_angle);
|
||||
}
|
||||
}
|
||||
|
||||
let angle = quad
|
||||
@@ -601,7 +605,9 @@ impl BoundingBoxManager {
|
||||
category,
|
||||
TransformCageSizeCategory::Full | TransformCageSizeCategory::ReducedBoth | TransformCageSizeCategory::ReducedLandscape | TransformCageSizeCategory::ReducedPortrait
|
||||
) {
|
||||
quad.0.map(|point| draw_handle(point, angle));
|
||||
for point in quad.0 {
|
||||
draw_handle(point, angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the flat line endpoint drag handles
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
|
||||
use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
|
||||
use crate::consts::ROTATE_INCREMENT;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
|
||||
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
|
||||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use bezier_rs::{Bezier, BezierHandles};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::text::{FontCache, load_face};
|
||||
use graphene_std::vector::{HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
|
||||
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
|
||||
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
|
||||
|
||||
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
|
||||
pub fn should_extend(
|
||||
@@ -198,6 +205,294 @@ pub fn is_visible_point(
|
||||
}
|
||||
}
|
||||
|
||||
/// Function to find the bounding box of bezier (uses method from kurbo)
|
||||
pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] {
|
||||
let start = Point::new(bezier.start.x, bezier.start.y);
|
||||
let end = Point::new(bezier.end.x, bezier.end.y);
|
||||
let bbox = match bezier.handles {
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let p1 = Point::new(handle_start.x, handle_start.y);
|
||||
let p2 = Point::new(handle_end.x, handle_end.y);
|
||||
CubicBez::new(start, p1, p2, end).bounding_box()
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let p1 = Point::new(handle.x, handle.y);
|
||||
QuadBez::new(start, p1, end).bounding_box()
|
||||
}
|
||||
BezierHandles::Linear => Line::new(start, end).bounding_box(),
|
||||
};
|
||||
[DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)]
|
||||
}
|
||||
|
||||
pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool {
|
||||
let to_layerspace = transform.inverse();
|
||||
let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])];
|
||||
let start = Point::new(bezier.start.x, bezier.start.y);
|
||||
let end = Point::new(bezier.end.x, bezier.end.y);
|
||||
let segment = match bezier.handles {
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let p1 = Point::new(handle_start.x, handle_start.y);
|
||||
let p2 = Point::new(handle_end.x, handle_end.y);
|
||||
PathSeg::Cubic(CubicBez::new(start, p1, p2, end))
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let p1 = Point::new(handle.x, handle.y);
|
||||
PathSeg::Quad(QuadBez::new(start, p1, end))
|
||||
}
|
||||
BezierHandles::Linear => PathSeg::Line(Line::new(start, end)),
|
||||
};
|
||||
|
||||
// Create a list of all the sides
|
||||
let sides = [
|
||||
Line::new((quad[0].x, quad[0].y), (quad[1].x, quad[0].y)),
|
||||
Line::new((quad[0].x, quad[0].y), (quad[0].x, quad[1].y)),
|
||||
Line::new((quad[1].x, quad[1].y), (quad[1].x, quad[0].y)),
|
||||
Line::new((quad[1].x, quad[1].y), (quad[0].x, quad[1].y)),
|
||||
];
|
||||
|
||||
let mut is_intersecting = false;
|
||||
for line in sides {
|
||||
let intersections = segment.intersect_line(line);
|
||||
let mut intersects = false;
|
||||
for intersection in intersections {
|
||||
if intersection.line_t <= 1. && intersection.line_t >= 0. && intersection.segment_t <= 1. && intersection.segment_t >= 0. {
|
||||
// There is a valid intersection point
|
||||
intersects = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if intersects {
|
||||
is_intersecting = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
is_intersecting
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn resize_bounds(
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
bounds: &mut BoundingBoxManager,
|
||||
dragging_layers: &mut Vec<LayerNodeIdentifier>,
|
||||
snap_manager: &mut SnapManager,
|
||||
snap_candidates: &mut Vec<SnapCandidatePoint>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
center: bool,
|
||||
constrain: bool,
|
||||
tool: ToolType,
|
||||
) {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
let center = center.then_some(bounds.center_of_transformation);
|
||||
let snap = Some(SizeSnapData {
|
||||
manager: snap_manager,
|
||||
points: snap_candidates,
|
||||
snap_data: SnapData::ignore(document, input, dragging_layers),
|
||||
});
|
||||
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
|
||||
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
|
||||
|
||||
let pivot_transform = DAffine2::from_translation(pivot);
|
||||
let transformation = pivot_transform * delta * pivot_transform.inverse();
|
||||
|
||||
dragging_layers.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, dragging_layers, responses, &document.network_interface, None, &tool, None);
|
||||
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn rotate_bounds(
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
bounds: &mut BoundingBoxManager,
|
||||
dragging_layers: &mut Vec<LayerNodeIdentifier>,
|
||||
drag_start: DVec2,
|
||||
mouse_position: DVec2,
|
||||
snap_angle: bool,
|
||||
tool: ToolType,
|
||||
) {
|
||||
let angle = {
|
||||
let start_offset = drag_start - bounds.center_of_transformation;
|
||||
let end_offset = mouse_position - bounds.center_of_transformation;
|
||||
start_offset.angle_to(end_offset)
|
||||
};
|
||||
|
||||
let snapped_angle = if snap_angle {
|
||||
let snap_resolution = ROTATE_INCREMENT.to_radians();
|
||||
(angle / snap_resolution).round() * snap_resolution
|
||||
} else {
|
||||
angle
|
||||
};
|
||||
|
||||
let delta = DAffine2::from_angle(snapped_angle);
|
||||
|
||||
dragging_layers.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of replacement_selected_layers");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
dragging_layers,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&tool,
|
||||
None,
|
||||
);
|
||||
selected.update_transforms(delta, None, None);
|
||||
}
|
||||
|
||||
pub fn skew_bounds(
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
bounds: &mut BoundingBoxManager,
|
||||
free_movement: bool,
|
||||
layers: &mut Vec<LayerNodeIdentifier>,
|
||||
mouse_position: DVec2,
|
||||
tool: ToolType,
|
||||
) {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
let mut pivot = DVec2::ZERO;
|
||||
|
||||
let transformation = movement.skew_transform(mouse_position, bounds.original_bound_transform, free_movement);
|
||||
|
||||
layers.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, layers, responses, &document.network_interface, None, &tool, None);
|
||||
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace returned tuple (where at most 1 element is true at a time) with an enum.
|
||||
/// Returns the tuple (resize, rotate, skew).
|
||||
pub fn transforming_transform_cage(
|
||||
document: &DocumentMessageHandler,
|
||||
mut bounding_box_manager: &mut Option<BoundingBoxManager>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
layers_dragging: &mut Vec<LayerNodeIdentifier>,
|
||||
) -> (bool, bool, bool) {
|
||||
let dragging_bounds = bounding_box_manager.as_mut().and_then(|bounding_box| {
|
||||
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||
|
||||
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
|
||||
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
|
||||
bounding_box.opposite_pivot = selected_edges.calculate_pivot();
|
||||
selected_edges
|
||||
});
|
||||
|
||||
edges
|
||||
});
|
||||
|
||||
let rotating_bounds = bounding_box_manager.as_ref().map(|bounding_box| bounding_box.check_rotate(input.mouse.position)).unwrap_or_default();
|
||||
|
||||
let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect();
|
||||
|
||||
let is_flat_layer = bounding_box_manager.as_ref().map(|bounding_box_manager| bounding_box_manager.transform_tampered).unwrap_or(true);
|
||||
|
||||
if dragging_bounds.is_some() && !is_flat_layer {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
*layers_dragging = selected;
|
||||
|
||||
if let Some(bounds) = &mut bounding_box_manager {
|
||||
bounds.original_bound_transform = bounds.transform;
|
||||
|
||||
layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
layers_dragging,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
);
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
|
||||
// Check if we're hovering over a skew triangle
|
||||
let edges = bounds.check_selected_edges(input.mouse.position);
|
||||
if let Some(edges) = edges {
|
||||
let closest_edge = bounds.get_closest_edge(edges, input.mouse.position);
|
||||
if bounds.check_skew_handle(input.mouse.position, closest_edge) {
|
||||
// No resize or rotate, just skew
|
||||
return (false, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Just resize, no rotate or skew
|
||||
return (true, false, false);
|
||||
}
|
||||
|
||||
if rotating_bounds {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
if let Some(bounds) = &mut bounding_box_manager {
|
||||
layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&selected,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
);
|
||||
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
}
|
||||
|
||||
*layers_dragging = selected;
|
||||
|
||||
// No resize or skew, just rotate
|
||||
return (false, true, false);
|
||||
}
|
||||
|
||||
// No resize, rotate, or skew
|
||||
(false, false, false)
|
||||
}
|
||||
|
||||
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {
|
||||
|
||||
@@ -32,13 +32,7 @@ pub enum ToolMessage {
|
||||
#[child]
|
||||
Spline(SplineToolMessage),
|
||||
#[child]
|
||||
Line(LineToolMessage),
|
||||
#[child]
|
||||
Rectangle(RectangleToolMessage),
|
||||
#[child]
|
||||
Ellipse(EllipseToolMessage),
|
||||
#[child]
|
||||
Polygon(PolygonToolMessage),
|
||||
Shape(ShapeToolMessage),
|
||||
#[child]
|
||||
Text(TextToolMessage),
|
||||
|
||||
@@ -54,30 +48,29 @@ pub enum ToolMessage {
|
||||
// Relight(RelightToolMessage),
|
||||
// // #[child]
|
||||
// Detail(DetailToolMessage),
|
||||
// #[child]
|
||||
// Imaginate(ImaginateToolMessage),
|
||||
|
||||
// Messages
|
||||
|
||||
// General tools
|
||||
ActivateToolSelect,
|
||||
ActivateToolArtboard,
|
||||
ActivateToolNavigate,
|
||||
ActivateToolEyedropper,
|
||||
ActivateToolText,
|
||||
ActivateToolFill,
|
||||
ActivateToolGradient,
|
||||
|
||||
// Vector tools
|
||||
ActivateToolPath,
|
||||
ActivateToolPen,
|
||||
ActivateToolFreehand,
|
||||
ActivateToolSpline,
|
||||
ActivateToolLine,
|
||||
ActivateToolRectangle,
|
||||
ActivateToolEllipse,
|
||||
ActivateToolPolygon,
|
||||
|
||||
ActivateToolShapeLine,
|
||||
ActivateToolShapeRectangle,
|
||||
ActivateToolShapeEllipse,
|
||||
ActivateToolShape,
|
||||
ActivateToolText,
|
||||
// Raster tools
|
||||
ActivateToolBrush,
|
||||
// ActivateToolImaginate,
|
||||
//
|
||||
|
||||
ActivateTool {
|
||||
tool_type: ToolType,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::common_functionality::shape_editor::ShapeState;
|
||||
use super::common_functionality::shapes::shape_utility::ShapeType::{self, Ellipse, Line, Rectangle};
|
||||
use super::utility_types::{ToolActionHandlerData, ToolFsmState, tool_message_to_tool_type};
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
@@ -58,21 +59,46 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
||||
ToolMessage::ActivateToolPen => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }),
|
||||
ToolMessage::ActivateToolFreehand => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }),
|
||||
ToolMessage::ActivateToolSpline => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }),
|
||||
ToolMessage::ActivateToolLine => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Line }),
|
||||
ToolMessage::ActivateToolRectangle => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }),
|
||||
ToolMessage::ActivateToolEllipse => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }),
|
||||
ToolMessage::ActivateToolPolygon => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Polygon }),
|
||||
|
||||
ToolMessage::ActivateToolShape => {
|
||||
if self.tool_state.tool_data.active_shape_type.is_some() {
|
||||
self.tool_state.tool_data.active_shape_type = None;
|
||||
self.tool_state.tool_data.active_tool_type = ToolType::Shape;
|
||||
}
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
|
||||
responses.add(ShapeToolMessage::SetShape(ShapeType::Polygon));
|
||||
responses.add(ShapeToolMessage::HideShapeTypeWidget(false))
|
||||
}
|
||||
ToolMessage::ActivateToolBrush => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }),
|
||||
// ToolMessage::ActivateToolImaginate => responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }),
|
||||
ToolMessage::ActivateToolShapeLine | ToolMessage::ActivateToolShapeRectangle | ToolMessage::ActivateToolShapeEllipse => {
|
||||
let shape = match message {
|
||||
ToolMessage::ActivateToolShapeLine => Line,
|
||||
ToolMessage::ActivateToolShapeRectangle => Rectangle,
|
||||
ToolMessage::ActivateToolShapeEllipse => Ellipse,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.tool_state.tool_data.active_shape_type = Some(shape.tool_type());
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape });
|
||||
responses.add(ShapeToolMessage::HideShapeTypeWidget(true));
|
||||
responses.add(ShapeToolMessage::SetShape(shape));
|
||||
}
|
||||
ToolMessage::ActivateTool { tool_type } => {
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
let old_tool = tool_data.active_tool_type;
|
||||
let old_tool = tool_data.active_tool_type.get_tool();
|
||||
let tool_type = tool_type.get_tool();
|
||||
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
tool_data.send_layout(responses, LayoutTarget::ToolShelf);
|
||||
|
||||
// Do nothing if switching to the same tool
|
||||
if self.tool_is_active && tool_type == old_tool {
|
||||
return;
|
||||
}
|
||||
|
||||
if tool_type != ToolType::Shape {
|
||||
tool_data.active_shape_type = None;
|
||||
}
|
||||
|
||||
self.tool_is_active = true;
|
||||
|
||||
// Send the old and new tools a transition to their FSM Abort states
|
||||
@@ -299,7 +325,6 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
||||
ActivateToolArtboard,
|
||||
ActivateToolNavigate,
|
||||
ActivateToolEyedropper,
|
||||
ActivateToolText,
|
||||
ActivateToolFill,
|
||||
ActivateToolGradient,
|
||||
|
||||
@@ -307,13 +332,13 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
||||
ActivateToolPen,
|
||||
ActivateToolFreehand,
|
||||
ActivateToolSpline,
|
||||
ActivateToolLine,
|
||||
ActivateToolRectangle,
|
||||
ActivateToolEllipse,
|
||||
ActivateToolPolygon,
|
||||
ActivateToolShapeLine,
|
||||
ActivateToolShapeRectangle,
|
||||
ActivateToolShapeEllipse,
|
||||
ActivateToolShape,
|
||||
ActivateToolText,
|
||||
|
||||
ActivateToolBrush,
|
||||
// ActivateToolImaginate,
|
||||
|
||||
SelectRandomPrimaryColor,
|
||||
ResetColors,
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::DEFAULT_STROKE_WIDTH;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EllipseTool {
|
||||
fsm_state: EllipseToolFsmState,
|
||||
data: EllipseToolData,
|
||||
options: EllipseToolOptions,
|
||||
}
|
||||
|
||||
pub struct EllipseToolOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
}
|
||||
|
||||
impl Default for EllipseToolOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_secondary(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum EllipseOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Ellipse)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum EllipseToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove { center: Key, lock_ratio: Key },
|
||||
PointerOutsideViewport { center: Key, lock_ratio: Key },
|
||||
UpdateOptions(EllipseOptionsUpdate),
|
||||
}
|
||||
|
||||
impl ToolMetadata for EllipseTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorEllipseTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Ellipse Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Ellipse
|
||||
}
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
NumberInput::new(Some(line_weight))
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
impl LayoutHolder for EllipseTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for EllipseTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
let ToolMessage::Ellipse(EllipseToolMessage::UpdateOptions(action)) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.data, tool_data, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
EllipseOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
}
|
||||
EllipseOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
EllipseOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
EllipseOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
}
|
||||
EllipseOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
EllipseOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
EllipseToolFsmState::Ready => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
EllipseToolFsmState::Drawing => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for EllipseTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context| EllipseToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(EllipseToolMessage::Abort.into()),
|
||||
working_color_changed: Some(EllipseToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum EllipseToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct EllipseToolData {
|
||||
data: Resize,
|
||||
auto_panning: AutoPanning,
|
||||
}
|
||||
|
||||
impl Fsm for EllipseToolFsmState {
|
||||
type ToolData = EllipseToolData;
|
||||
type ToolOptions = EllipseToolOptions;
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Ellipse(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, EllipseToolMessage::Overlays(mut overlay_context)) => {
|
||||
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(EllipseToolFsmState::Ready, EllipseToolMessage::DragStart) => {
|
||||
shape_data.start(document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Create a new ellipse vector shape
|
||||
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node does not exist");
|
||||
let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))]);
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
shape_data.layer = Some(layer);
|
||||
|
||||
EllipseToolFsmState::Drawing
|
||||
}
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
|
||||
if let Some(layer) = shape_data.layer {
|
||||
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false),
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_translation((start + end) / 2.),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
EllipseToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(_, EllipseToolMessage::PointerMove { .. }) => {
|
||||
shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
EllipseToolFsmState::Drawing
|
||||
}
|
||||
(state, EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
EllipseToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
EllipseToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.stop(&messages, responses);
|
||||
|
||||
state
|
||||
}
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
EllipseToolFsmState::Ready
|
||||
}
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::Abort) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
EllipseToolFsmState::Ready
|
||||
}
|
||||
(_, EllipseToolMessage::WorkingColorChanged) => {
|
||||
responses.add(EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])]),
|
||||
EllipseToolFsmState::Drawing => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ellipse {
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graphene_std::vector::generator_nodes::ellipse;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ResolvedEllipse {
|
||||
radius_x: f64,
|
||||
radius_y: f64,
|
||||
transform: DAffine2,
|
||||
}
|
||||
|
||||
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
|
||||
let instrumented = match editor.eval_graph().await {
|
||||
Ok(instrumented) => instrumented,
|
||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
||||
};
|
||||
|
||||
let document = editor.active_document();
|
||||
let layers = document.metadata().all_layers();
|
||||
layers
|
||||
.filter_map(|layer| {
|
||||
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?;
|
||||
Some(ResolvedEllipse {
|
||||
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
|
||||
transform: document.metadata().transform_to_document(layer),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_simple() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
|
||||
|
||||
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 4.5,
|
||||
radius_y: 5.,
|
||||
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_circle() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(
|
||||
ellipse[0],
|
||||
ResolvedEllipse {
|
||||
radius_x: 10.,
|
||||
radius_y: 10.,
|
||||
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
println!("{ellipse:?}");
|
||||
assert_eq!(ellipse[0].radius_x, 5.);
|
||||
assert_eq!(ellipse[0].radius_y, 5.);
|
||||
|
||||
assert!(
|
||||
ellipse[0]
|
||||
.transform
|
||||
.abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_draw_center_square_rotated() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
// 45 degree rotation of content clockwise
|
||||
angle_radians: f64::consts::FRAC_PI_4,
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 1);
|
||||
assert_eq!(ellipse[0].radius_x, 10.);
|
||||
assert_eq!(ellipse[0].radius_y, 10.);
|
||||
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ellipse_cancel() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool_cancel_rmb(ToolType::Ellipse).await;
|
||||
|
||||
let ellipse = get_ellipse(&mut editor).await;
|
||||
assert_eq!(ellipse.len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -416,7 +416,7 @@ mod test_freehand {
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::TransformSet {
|
||||
layer: artboard,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10.0, -5.0)),
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10., -5.)),
|
||||
transform_in: TransformIn::Local,
|
||||
skip_rerender: false,
|
||||
})
|
||||
@@ -424,14 +424,14 @@ mod test_freehand {
|
||||
|
||||
editor.select_tool(ToolType::Freehand).await;
|
||||
|
||||
let mouse_points = [DVec2::new(150.0, 100.0), DVec2::new(200.0, 150.0), DVec2::new(250.0, 130.0), DVec2::new(300.0, 170.0)];
|
||||
let mouse_points = [DVec2::new(150., 100.), DVec2::new(200., 150.), DVec2::new(250., 130.), DVec2::new(300., 170.)];
|
||||
|
||||
// Expected points that will actually be captured by the tool
|
||||
let expected_captured_points = &mouse_points[1..];
|
||||
editor.drag_path(&mouse_points, ModifierKeys::empty()).await;
|
||||
|
||||
let vector_data_list = get_vector_data(&mut editor).await;
|
||||
verify_path_points(&vector_data_list, expected_captured_points, 1.0).expect("Path points verification failed");
|
||||
verify_path_points(&vector_data_list, expected_captured_points, 1.).expect("Path points verification failed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -439,7 +439,7 @@ mod test_freehand {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let initial_points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)];
|
||||
let initial_points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
|
||||
|
||||
editor.select_tool(ToolType::Freehand).await;
|
||||
|
||||
@@ -491,7 +491,7 @@ mod test_freehand {
|
||||
|
||||
assert!(endpoint_viewport_pos.is_finite(), "Endpoint position is not finite");
|
||||
|
||||
let extension_points = [DVec2::new(400.0, 200.0), DVec2::new(500.0, 100.0)];
|
||||
let extension_points = [DVec2::new(400., 200.), DVec2::new(500., 100.)];
|
||||
|
||||
let layer_node_id = {
|
||||
let document = editor.active_document();
|
||||
@@ -558,7 +558,7 @@ mod test_freehand {
|
||||
|
||||
editor.select_tool(ToolType::Freehand).await;
|
||||
|
||||
let initial_points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)];
|
||||
let initial_points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
|
||||
|
||||
let first_point = initial_points[0];
|
||||
editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
@@ -599,7 +599,7 @@ mod test_freehand {
|
||||
})
|
||||
.await;
|
||||
|
||||
let second_path_points = [DVec2::new(400.0, 100.0), DVec2::new(500.0, 200.0), DVec2::new(600.0, 100.0)];
|
||||
let second_path_points = [DVec2::new(400., 100.), DVec2::new(500., 200.), DVec2::new(600., 100.)];
|
||||
|
||||
let first_second_point = second_path_points[0];
|
||||
editor.move_mouse(first_second_point.x, first_second_point.y, ModifierKeys::SHIFT, MouseKeys::empty()).await;
|
||||
@@ -677,12 +677,12 @@ mod test_freehand {
|
||||
|
||||
editor.select_tool(ToolType::Freehand).await;
|
||||
|
||||
let custom_line_weight = 5.0;
|
||||
let custom_line_weight = 5.;
|
||||
editor
|
||||
.handle_message(ToolMessage::Freehand(FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::LineWeight(custom_line_weight))))
|
||||
.await;
|
||||
|
||||
let points = [DVec2::new(100.0, 100.0), DVec2::new(200.0, 200.0), DVec2::new(300.0, 100.0)];
|
||||
let points = [DVec2::new(100., 100.), DVec2::new(200., 200.), DVec2::new(300., 100.)];
|
||||
|
||||
let first_point = points[0];
|
||||
editor.move_mouse(first_point.x, first_point.y, ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
|
||||
@@ -535,7 +535,8 @@ impl Fsm for GradientToolFsmState {
|
||||
mod test_gradient {
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::EditorMouseState;
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
|
||||
use crate::messages::portfolio::document::{graph_operation::utility_types::TransformIn, utility_types::misc::GroupFolderType};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graphene_std::vector::fill;
|
||||
@@ -717,7 +718,7 @@ mod test_gradient {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2.0 }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
|
||||
|
||||
editor.drag_tool(ToolType::Rectangle, -5., -3., 100., 100., ModifierKeys::empty()).await;
|
||||
|
||||
@@ -726,7 +727,7 @@ mod test_gradient {
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::TransformSet {
|
||||
layer: selected_layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10.0, -5.0)),
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(1.5, 0.8), 0.3, DVec2::new(10., -5.)),
|
||||
transform_in: TransformIn::Local,
|
||||
skip_rerender: false,
|
||||
})
|
||||
@@ -802,7 +803,7 @@ mod test_gradient {
|
||||
stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
|
||||
let positions: Vec<f64> = stops.iter().map(|(pos, _)| *pos).collect();
|
||||
assert_stops_at_positions(&positions, &[0.0, 0.5, 1.0], 0.1);
|
||||
assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1);
|
||||
|
||||
let middle_color = stops[1].1.to_rgba8_srgb();
|
||||
|
||||
@@ -842,7 +843,7 @@ mod test_gradient {
|
||||
|
||||
// Check positions are now correctly ordered
|
||||
let updated_positions: Vec<f64> = updated_stops.iter().map(|(pos, _)| *pos).collect();
|
||||
assert_stops_at_positions(&updated_positions, &[0.0, 0.8, 1.0], 0.1);
|
||||
assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
|
||||
|
||||
// Colors should maintain their associations with the stop points
|
||||
assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
|
||||
@@ -876,7 +877,7 @@ mod test_gradient {
|
||||
let positions: Vec<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
|
||||
// Use helper function to verify positions
|
||||
assert_stops_at_positions(&positions, &[0.0, 0.25, 0.75, 1.0], 0.05);
|
||||
assert_stops_at_positions(&positions, &[0., 0.25, 0.75, 1.], 0.05);
|
||||
|
||||
// Select the stop at position 0.75 and delete it
|
||||
let position2 = DVec2::new(75., 0.);
|
||||
@@ -902,7 +903,7 @@ mod test_gradient {
|
||||
let final_positions: Vec<f64> = final_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
|
||||
// Verify final positions with helper function
|
||||
assert_stops_at_positions(&final_positions, &[0.0, 0.25, 1.0], 0.05);
|
||||
assert_stops_at_positions(&final_positions, &[0., 0.25, 1.], 0.05);
|
||||
|
||||
// Additional verification that 0.75 stop is gone
|
||||
assert!(!final_positions.iter().any(|pos| (pos - 0.75).abs() < 0.05), "Stop at position 0.75 should have been deleted");
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ImaginateTool {
|
||||
fsm_state: ImaginateToolFsmState,
|
||||
tool_data: ImaginateToolData,
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Imaginate)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ImaginateToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize { center: Key, lock_ratio: Key },
|
||||
}
|
||||
|
||||
impl LayoutHolder for ImaginateTool {
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ImaginateTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
ImaginateToolFsmState::Ready => actions!(ImaginateToolMessageDiscriminant;
|
||||
DragStart,
|
||||
),
|
||||
ImaginateToolFsmState::Drawing => actions!(ImaginateToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolMetadata for ImaginateTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"RasterImaginateTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Imaginate Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Imaginate
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for ImaginateTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(ImaginateToolMessage::Abort.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum ImaginateToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ImaginateToolData {
|
||||
data: Resize,
|
||||
}
|
||||
|
||||
impl Fsm for ImaginateToolFsmState {
|
||||
type ToolData = ImaginateToolData;
|
||||
type ToolOptions = ();
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
ToolActionHandlerData { document, input, .. }: &mut ToolActionHandlerData,
|
||||
_tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Imaginate(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
|
||||
shape_data.start(document, input);
|
||||
// responses.add(DocumentMessage::AddTransaction);
|
||||
//shape_data.layer = Some(LayerNodeIdentifier::new(NodeId::new(), &document.network_interface));
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
|
||||
// // Utility function to offset the position of each consecutive node
|
||||
// let mut pos = 8;
|
||||
// let mut next_pos = || {
|
||||
// pos += 8;
|
||||
// DocumentNodeMetadata::position((pos, 4))
|
||||
// };
|
||||
|
||||
// // Get the node type for the Transform and Imaginate nodes
|
||||
// let Some(transform_node_type) = resolve_document_node_type("Transform") else {
|
||||
// warn!("Transform node should be in registry");
|
||||
// return ImaginateToolFsmState::Drawing;
|
||||
// };
|
||||
// let imaginate_node_type = &*IMAGINATE_NODE;
|
||||
|
||||
// // Give them a unique ID
|
||||
// let transform_node_id = NodeId(100);
|
||||
//let imaginate_node_id = NodeId(101);
|
||||
|
||||
// Create the network based on the Input -> Output passthrough default network
|
||||
// let mut network = new_image_network(16, imaginate_node_id);
|
||||
|
||||
// // Insert the nodes into the default network
|
||||
// network.insert_node(
|
||||
// transform_node_id,
|
||||
// transform_node_type.to_document_node_default_inputs([Some(NodeInput::node(NodeId(0), 0))], next_pos()),
|
||||
// );
|
||||
// network.insert_node(
|
||||
// imaginate_node_id,
|
||||
// imaginate_node_type.to_document_node_default_inputs([Some(NodeInput::node(transform_node_id, 0))], next_pos()),
|
||||
// );
|
||||
// responses.add(NodeGraphMessage::ShiftNode { node_id: imaginate_node_id });
|
||||
|
||||
// // Add a layer with a frame to the document
|
||||
// responses.add(Operation::AddFrame {
|
||||
// path: shape_data.layer.unwrap().to_path(),
|
||||
// insert_index: -1,
|
||||
// transform: DAffine2::ZERO.to_cols_array(),
|
||||
// network,
|
||||
// });
|
||||
|
||||
ImaginateToolFsmState::Drawing
|
||||
}
|
||||
(state, ImaginateToolMessage::Resize { center, lock_ratio }) => {
|
||||
let message = shape_data.calculate_transform(document, input, center, lock_ratio, true);
|
||||
responses.try_add(message);
|
||||
|
||||
state
|
||||
}
|
||||
(ImaginateToolFsmState::Drawing, ImaginateToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
ImaginateToolFsmState::Ready
|
||||
}
|
||||
(ImaginateToolFsmState::Drawing, ImaginateToolMessage::Abort) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
ImaginateToolFsmState::Ready
|
||||
}
|
||||
(_, ImaginateToolMessage::Abort) => ImaginateToolFsmState::Ready,
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
ImaginateToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Repaint Frame"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])]),
|
||||
ImaginateToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")])]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
@@ -1,624 +0,0 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, LINE_ROTATE_SNAP_ANGLE};
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LineTool {
|
||||
fsm_state: LineToolFsmState,
|
||||
tool_data: LineToolData,
|
||||
options: LineOptions,
|
||||
}
|
||||
|
||||
pub struct LineOptions {
|
||||
line_weight: f64,
|
||||
stroke: ToolColorOptions,
|
||||
}
|
||||
|
||||
impl Default for LineOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Line)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum LineToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove { center: Key, lock_angle: Key, snap_angle: Key },
|
||||
PointerOutsideViewport { center: Key, lock_angle: Key, snap_angle: Key },
|
||||
UpdateOptions(LineOptionsUpdate),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum LineOptionsUpdate {
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
impl ToolMetadata for LineTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorLineTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Line Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Line
|
||||
}
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
NumberInput::new(Some(line_weight))
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
impl LayoutHolder for LineTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
);
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for LineTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
let ToolMessage::Line(LineToolMessage::UpdateOptions(action)) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
LineOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
}
|
||||
LineOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
LineOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
LineToolFsmState::Ready => actions!(LineToolMessageDiscriminant; DragStart, PointerMove),
|
||||
LineToolFsmState::Drawing => actions!(LineToolMessageDiscriminant; DragStop, PointerMove, Abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for LineTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context| LineToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(LineToolMessage::Abort.into()),
|
||||
working_color_changed: Some(LineToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
enum LineToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum LineEnd {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct LineToolData {
|
||||
drag_begin: DVec2,
|
||||
drag_start_shifted: DVec2,
|
||||
drag_current_shifted: DVec2,
|
||||
drag_start: DVec2,
|
||||
drag_current: DVec2,
|
||||
angle: f64,
|
||||
weight: f64,
|
||||
selected_layers_with_position: HashMap<LayerNodeIdentifier, [DVec2; 2]>,
|
||||
editing_layer: Option<LayerNodeIdentifier>,
|
||||
snap_manager: SnapManager,
|
||||
auto_panning: AutoPanning,
|
||||
dragging_endpoint: Option<LineEnd>,
|
||||
}
|
||||
|
||||
impl Fsm for LineToolFsmState {
|
||||
type ToolData = LineToolData;
|
||||
type ToolOptions = LineOptions;
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let ToolMessage::Line(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, LineToolMessage::Overlays(mut overlay_context)) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
tool_data.selected_layers_with_position = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.filter_map(|layer| {
|
||||
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?;
|
||||
|
||||
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point));
|
||||
if !start.abs_diff_eq(end, f64::EPSILON * 1000.) {
|
||||
overlay_context.line(viewport_start, viewport_end, None, None);
|
||||
overlay_context.square(viewport_start, Some(6.), None, None);
|
||||
overlay_context.square(viewport_end, Some(6.), None, None);
|
||||
}
|
||||
|
||||
Some((layer, [start, end]))
|
||||
})
|
||||
.collect::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
|
||||
|
||||
self
|
||||
}
|
||||
(LineToolFsmState::Ready, LineToolMessage::DragStart) => {
|
||||
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
|
||||
tool_data.drag_start = snapped.snapped_point_document;
|
||||
tool_data.drag_begin = document.metadata().document_to_viewport.transform_point2(tool_data.drag_start);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
for (layer, [document_start, document_end]) in tool_data.selected_layers_with_position.iter() {
|
||||
let transform = document.metadata().transform_to_viewport(*layer);
|
||||
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
|
||||
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
|
||||
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
|
||||
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
|
||||
|
||||
let drag_start = input.mouse.position;
|
||||
let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(*point));
|
||||
|
||||
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
|
||||
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
|
||||
|
||||
if start_click || end_click {
|
||||
tool_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
|
||||
tool_data.drag_start = if end_click { *document_start } else { *document_end };
|
||||
tool_data.editing_layer = Some(*layer);
|
||||
return LineToolFsmState::Drawing;
|
||||
}
|
||||
}
|
||||
|
||||
let node_type = resolve_document_node_type("Line").expect("Line node does not exist");
|
||||
let node = node_type.node_template_input_override([
|
||||
None,
|
||||
Some(NodeInput::value(
|
||||
TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(tool_data.drag_start)),
|
||||
false,
|
||||
)),
|
||||
Some(NodeInput::value(
|
||||
TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(tool_data.drag_start)),
|
||||
false,
|
||||
)),
|
||||
]);
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
responses.add(Message::StartBuffer);
|
||||
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
|
||||
tool_data.editing_layer = Some(layer);
|
||||
tool_data.angle = 0.;
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
||||
LineToolFsmState::Drawing
|
||||
}
|
||||
(LineToolFsmState::Drawing, LineToolMessage::PointerMove { center, snap_angle, lock_angle }) => {
|
||||
let Some(layer) = tool_data.editing_layer else { return LineToolFsmState::Ready };
|
||||
|
||||
tool_data.drag_current_shifted = document.metadata().transform_to_viewport(layer).inverse().transform_point2(input.mouse.position);
|
||||
tool_data.drag_current = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
tool_data.drag_start_shifted = document.metadata().transform_to_viewport(layer).inverse().transform_point2(tool_data.drag_begin);
|
||||
|
||||
let keyboard = &input.keyboard;
|
||||
let ignore = vec![layer];
|
||||
let snap_data = SnapData::ignore(document, input, &ignore);
|
||||
let mut document_points = generate_line(tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
|
||||
|
||||
if tool_data.dragging_endpoint == Some(LineEnd::Start) {
|
||||
document_points.swap(0, 1);
|
||||
}
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else {
|
||||
return LineToolFsmState::Ready;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
LineToolMessage::PointerOutsideViewport { center, snap_angle, lock_angle }.into(),
|
||||
LineToolMessage::PointerMove { center, snap_angle, lock_angle }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
LineToolFsmState::Drawing
|
||||
}
|
||||
(_, LineToolMessage::PointerMove { .. }) => {
|
||||
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(LineToolFsmState::Drawing, LineToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
LineToolFsmState::Drawing
|
||||
}
|
||||
(state, LineToolMessage::PointerOutsideViewport { center, lock_angle, snap_angle }) => {
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
LineToolMessage::PointerOutsideViewport { center, lock_angle, snap_angle }.into(),
|
||||
LineToolMessage::PointerMove { center, lock_angle, snap_angle }.into(),
|
||||
];
|
||||
tool_data.auto_panning.stop(&messages, responses);
|
||||
|
||||
state
|
||||
}
|
||||
(LineToolFsmState::Drawing, LineToolMessage::DragStop) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.editing_layer.take();
|
||||
input.mouse.finish_transaction(tool_data.drag_start, responses);
|
||||
LineToolFsmState::Ready
|
||||
}
|
||||
(LineToolFsmState::Drawing, LineToolMessage::Abort) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.editing_layer.take();
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
LineToolFsmState::Ready
|
||||
}
|
||||
(_, LineToolMessage::WorkingColorChanged) => {
|
||||
responses.add(LineToolMessage::UpdateOptions(LineOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
LineToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"),
|
||||
HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(),
|
||||
])]),
|
||||
LineToolFsmState::Drawing => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![
|
||||
HintInfo::keys([Key::Shift], "15° Increments"),
|
||||
HintInfo::keys([Key::Alt], "From Center"),
|
||||
HintInfo::keys([Key::Control], "Lock Angle"),
|
||||
]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_line(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
|
||||
let shift = tool_data.drag_current_shifted - tool_data.drag_current;
|
||||
let mut document_points = [tool_data.drag_start, tool_data.drag_current];
|
||||
|
||||
let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X);
|
||||
let mut line_length = (document_points[1] - document_points[0]).length();
|
||||
|
||||
if lock_angle {
|
||||
angle = tool_data.angle;
|
||||
} else if snap_angle {
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
}
|
||||
|
||||
tool_data.angle = angle;
|
||||
|
||||
let angle_vec = DVec2::from_angle(angle);
|
||||
if lock_angle {
|
||||
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
|
||||
}
|
||||
|
||||
document_points[1] = document_points[0] + line_length * angle_vec;
|
||||
|
||||
let constrained = snap_angle || lock_angle;
|
||||
let snap = &mut tool_data.snap_manager;
|
||||
|
||||
let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.drag_start]);
|
||||
let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.drag_start]);
|
||||
let mid_point = SnapCandidatePoint::handle_neighbors((tool_data.drag_start + document_points[1]) / 2., [tool_data.drag_start]);
|
||||
let config = SnapTypeConfiguration::default();
|
||||
|
||||
if constrained {
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: document_points[0],
|
||||
direction: document_points[1] - document_points[0],
|
||||
};
|
||||
if center {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
|
||||
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
|
||||
document_points[0] = best.snapped_point_document;
|
||||
snap.update_indicator(best);
|
||||
} else {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
|
||||
let snapped_mid = snap.constrained_snap(&snap_data, &mid_point, constraint, config);
|
||||
let best = if snap_data.document.snapping_state.path.line_midpoint && snapped_mid.other_snap_better(&snapped_mid) {
|
||||
document_points[1] += (snapped_mid.snapped_point_document - mid_point.document_point) * 2.;
|
||||
snapped_mid
|
||||
} else {
|
||||
document_points[1] = snapped.snapped_point_document;
|
||||
snapped.clone()
|
||||
};
|
||||
snap.update_indicator(best);
|
||||
}
|
||||
} else if center {
|
||||
let snapped = snap.free_snap(&snap_data, &near_point, config);
|
||||
let snapped_far = snap.free_snap(&snap_data, &far_point, config);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
|
||||
document_points[0] = best.snapped_point_document;
|
||||
snap.update_indicator(best);
|
||||
} else {
|
||||
let snapped = snap.free_snap(&snap_data, &near_point, config);
|
||||
let snapped_mid = snap.free_snap(&snap_data, &mid_point, config);
|
||||
let best = if snap_data.document.snapping_state.path.line_midpoint && snapped_mid.other_snap_better(&snapped_mid) {
|
||||
document_points[1] += (snapped_mid.snapped_point_document - mid_point.document_point) * 2.;
|
||||
snapped_mid
|
||||
} else {
|
||||
document_points[1] = snapped.snapped_point_document;
|
||||
snapped.clone()
|
||||
};
|
||||
snap.update_indicator(best);
|
||||
}
|
||||
|
||||
// Snapping happens in other space, while document graph renders in another.
|
||||
document_points.map(|vector| vector + shift)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_line_tool {
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
|
||||
async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> {
|
||||
let document = editor.active_document();
|
||||
let network_interface = &document.network_interface;
|
||||
let node_id = network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(network_interface)
|
||||
.filter_map(|layer| {
|
||||
let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?;
|
||||
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
|
||||
return None;
|
||||
};
|
||||
Some((start, end))
|
||||
})
|
||||
.next();
|
||||
node_id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_basicdraw() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)");
|
||||
assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_with_transformed_viewport() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await;
|
||||
editor
|
||||
.handle_message(NavigationMessage::CanvasTiltSet {
|
||||
angle_radians: (30. as f64).to_radians(),
|
||||
})
|
||||
.await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
let document = editor.active_document();
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let viewport_to_document = document_to_viewport.inverse();
|
||||
|
||||
let expected_start = viewport_to_document.transform_point2(DVec2::ZERO);
|
||||
let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.));
|
||||
|
||||
assert!(
|
||||
(start_input - expected_start).length() < 1.,
|
||||
"Start point should match expected document coordinates. Got {:?}, expected {:?}",
|
||||
start_input,
|
||||
expected_start
|
||||
);
|
||||
assert!(
|
||||
(end_input - expected_end).length() < 1.,
|
||||
"End point should match expected document coordinates. Got {:?}, expected {:?}",
|
||||
end_input,
|
||||
expected_end
|
||||
);
|
||||
} else {
|
||||
panic!("Line was not created successfully with transformed viewport");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_ctrl_anglelock() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let line_vec = end_input - start_input;
|
||||
let original_angle = line_vec.angle_to(DVec2::X);
|
||||
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
|
||||
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
|
||||
match (updated_start, updated_end) {
|
||||
(updated_start, updated_end) => {
|
||||
let updated_line_vec = updated_end - updated_start;
|
||||
let updated_angle = updated_line_vec.angle_to(DVec2::X);
|
||||
assert!((original_angle - updated_angle).abs() < 0.1, "Line angle should be locked when Ctrl is kept pressed");
|
||||
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_alt() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let expected_start = DVec2::new(0., 100.);
|
||||
let expected_end = DVec2::new(200., 100.);
|
||||
assert!((start_input - expected_start).length() < 1., "start point should be near (0,100)");
|
||||
assert!((end_input - expected_end).length() < 1., "end point should be near (200,100)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_alt_shift_drag() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await;
|
||||
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
|
||||
match (start_input, end_input) {
|
||||
(start_input, end_input) => {
|
||||
let line_vec = end_input - start_input;
|
||||
let angle_radians = line_vec.angle_to(DVec2::X);
|
||||
let angle_degrees = angle_radians.to_degrees();
|
||||
let nearest_angle = (angle_degrees / 15.).round() * 15.;
|
||||
|
||||
assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_line_tool_with_transformed_artboard() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await;
|
||||
|
||||
let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard");
|
||||
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::TransformChange {
|
||||
layer: artboard_id,
|
||||
transform: DAffine2::from_angle(45.0_f64.to_radians()),
|
||||
transform_in: TransformIn::Local,
|
||||
skip_rerender: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await;
|
||||
|
||||
let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard");
|
||||
// The line should still be diagonal with equal change in x and y
|
||||
let line_vector = end_input - start_input;
|
||||
// Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square)
|
||||
let line_length = line_vector.length();
|
||||
assert!(
|
||||
(line_length - 141.42).abs() < 1.0, // 100 * sqrt(2) ~= 141.42
|
||||
"Line length should be approximately 141.42 units. Got: {line_length}"
|
||||
);
|
||||
assert!((line_vector.x - 100.0).abs() < 1.0, "X-component of line vector should be approximately 100. Got: {}", line_vector.x);
|
||||
assert!(
|
||||
(line_vector.y.abs() - 100.0).abs() < 1.0,
|
||||
"Absolute Y-component of line vector should be approximately 100. Got: {}",
|
||||
line_vector.y.abs()
|
||||
);
|
||||
let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees();
|
||||
assert!((angle_degrees - (-45.0)).abs() < 1.0, "Line angle should be close to -45 degrees. Got: {angle_degrees}");
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
pub mod artboard_tool;
|
||||
pub mod brush_tool;
|
||||
pub mod ellipse_tool;
|
||||
pub mod eyedropper_tool;
|
||||
pub mod fill_tool;
|
||||
pub mod freehand_tool;
|
||||
pub mod gradient_tool;
|
||||
// pub mod imaginate_tool;
|
||||
pub mod line_tool;
|
||||
pub mod navigate_tool;
|
||||
pub mod path_tool;
|
||||
pub mod pen_tool;
|
||||
pub mod polygon_tool;
|
||||
pub mod rectangle_tool;
|
||||
pub mod select_tool;
|
||||
pub mod shape_tool;
|
||||
pub mod spline_tool;
|
||||
pub mod text_tool;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi
|
||||
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
|
||||
use bezier_rs::{Bezier, TValue};
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
|
||||
use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData};
|
||||
use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType};
|
||||
use std::vec;
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct PathTool {
|
||||
#[derive(Default)]
|
||||
pub struct PathToolOptions {
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
path_editing_mode: PathEditingMode,
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Path)]
|
||||
@@ -69,6 +70,7 @@ pub enum PathToolMessage {
|
||||
lasso_select: Key,
|
||||
handle_drag_from_anchor: Key,
|
||||
drag_restore_handle: Key,
|
||||
molding_in_segment_edit: Key,
|
||||
},
|
||||
NudgeSelectedPoints {
|
||||
delta_x: f64,
|
||||
@@ -116,9 +118,26 @@ pub enum PathOverlayMode {
|
||||
FrontierHandles = 2,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
pub struct PathEditingMode {
|
||||
point_editing_mode: bool,
|
||||
segment_editing_mode: bool,
|
||||
}
|
||||
|
||||
impl Default for PathEditingMode {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
point_editing_mode: true,
|
||||
segment_editing_mode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum PathOptionsUpdate {
|
||||
OverlayModeType(PathOverlayMode),
|
||||
PointEditingMode { enabled: bool },
|
||||
SegmentEditingMode { enabled: bool },
|
||||
}
|
||||
|
||||
impl ToolMetadata for PathTool {
|
||||
@@ -203,6 +222,19 @@ impl LayoutHolder for PathTool {
|
||||
.for_checkbox(&mut checkbox_id)
|
||||
.widget_holder();
|
||||
|
||||
let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode)
|
||||
// TODO(Keavon): Replace with a real icon
|
||||
.icon("Dot")
|
||||
.tooltip("Point Editing Mode")
|
||||
.on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into())
|
||||
.widget_holder();
|
||||
let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode)
|
||||
// TODO(Keavon): Replace with a real icon
|
||||
.icon("Remove")
|
||||
.tooltip("Segment Editing Mode")
|
||||
.on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into())
|
||||
.widget_holder();
|
||||
|
||||
let path_overlay_mode_widget = RadioInput::new(vec![
|
||||
RadioEntryData::new("all")
|
||||
.icon("HandleVisibilityAll")
|
||||
@@ -227,8 +259,12 @@ impl LayoutHolder for PathTool {
|
||||
y_location,
|
||||
unrelated_seperator.clone(),
|
||||
colinear_handle_checkbox,
|
||||
related_seperator,
|
||||
related_seperator.clone(),
|
||||
colinear_handles_label,
|
||||
unrelated_seperator.clone(),
|
||||
point_editing_mode,
|
||||
related_seperator.clone(),
|
||||
segment_editing_mode,
|
||||
unrelated_seperator,
|
||||
path_overlay_mode_widget,
|
||||
],
|
||||
@@ -246,6 +282,14 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
||||
self.options.path_overlay_mode = overlay_mode_type;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
PathOptionsUpdate::PointEditingMode { enabled } => {
|
||||
self.options.path_editing_mode.point_editing_mode = enabled;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
PathOptionsUpdate::SegmentEditingMode { enabled } => {
|
||||
self.options.path_editing_mode.segment_editing_mode = enabled;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
},
|
||||
ToolMessage::Path(PathToolMessage::ClosePath) => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
@@ -405,6 +449,7 @@ struct PathToolData {
|
||||
angle: f64,
|
||||
opposite_handle_position: Option<DVec2>,
|
||||
last_clicked_point_was_selected: bool,
|
||||
last_clicked_segment_was_selected: bool,
|
||||
snapping_axis: Option<Axis>,
|
||||
alt_clicked_on_anchor: bool,
|
||||
alt_dragging_from_anchor: bool,
|
||||
@@ -416,6 +461,7 @@ struct PathToolData {
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
adjacent_anchor_offset: Option<DVec2>,
|
||||
sliding_point_info: Option<SlidingPointInfo>,
|
||||
started_drawing_from_inside: bool,
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
@@ -476,6 +522,7 @@ impl PathToolData {
|
||||
self.selection_status = selection_status;
|
||||
}
|
||||
|
||||
// TODO: This function is for basic point select mode. We definitely need to make a new one for the segment select mode.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
@@ -487,7 +534,10 @@ impl PathToolData {
|
||||
lasso_select: bool,
|
||||
handle_drag_from_anchor: bool,
|
||||
drag_zero_handle: bool,
|
||||
molding_in_segment_edit: bool,
|
||||
path_overlay_mode: PathOverlayMode,
|
||||
segment_editing_mode: bool,
|
||||
point_editing_mode: bool,
|
||||
) -> PathToolFsmState {
|
||||
self.double_click_handled = false;
|
||||
self.opposing_handle_lengths = None;
|
||||
@@ -510,6 +560,7 @@ impl PathToolData {
|
||||
SELECTION_THRESHOLD,
|
||||
path_overlay_mode,
|
||||
self.frontier_handles_info.clone(),
|
||||
point_editing_mode,
|
||||
) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
@@ -593,25 +644,51 @@ impl PathToolData {
|
||||
}
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
}
|
||||
// We didn't find a point nearby, so we will see if there is a segment to insert a point on
|
||||
else if let Some(closed_segment) = &mut self.segment {
|
||||
// We didn't find a point nearby, so we will see if there is a segment to select or insert a point on
|
||||
else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Calculating and storing handle positions
|
||||
let handle1 = ManipulatorPointId::PrimaryHandle(closed_segment.segment());
|
||||
let handle2 = ManipulatorPointId::EndHandle(closed_segment.segment());
|
||||
if segment_editing_mode && !molding_in_segment_edit {
|
||||
let layer = segment.layer();
|
||||
let segment_id = segment.segment();
|
||||
let already_selected = shape_editor.selected_shape_state.get(&layer).is_some_and(|state| state.is_segment_selected(segment_id));
|
||||
self.last_clicked_segment_was_selected = already_selected;
|
||||
|
||||
if let Some(vector_data) = document.network_interface.compute_modified_vector(closed_segment.layer()) {
|
||||
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) {
|
||||
self.molding_info = Some((pos1, pos2))
|
||||
if !(already_selected && extend_selection) {
|
||||
let retain_existing_selection = extend_selection || already_selected;
|
||||
if !retain_existing_selection {
|
||||
shape_editor.deselect_all_segments();
|
||||
shape_editor.deselect_all_points();
|
||||
}
|
||||
|
||||
// Add to selected segments
|
||||
if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) {
|
||||
selected_shape_state.select_segment(segment_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PathToolFsmState::MoldingSegment
|
||||
self.drag_start_pos = input.mouse.position;
|
||||
|
||||
let viewport_to_document = document.metadata().document_to_viewport.inverse();
|
||||
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
} else {
|
||||
let handle1 = ManipulatorPointId::PrimaryHandle(segment.segment());
|
||||
let handle2 = ManipulatorPointId::EndHandle(segment.segment());
|
||||
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
|
||||
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) {
|
||||
self.molding_info = Some((pos1, pos2))
|
||||
}
|
||||
}
|
||||
PathToolFsmState::MoldingSegment
|
||||
}
|
||||
}
|
||||
// We didn't find a segment, so consider selecting the nearest shape instead
|
||||
// We didn't find a segment, so consider selecting the nearest shape instead and start drawing
|
||||
else if let Some(layer) = document.click(input) {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.deselect_all_segments();
|
||||
if extend_selection {
|
||||
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
|
||||
} else {
|
||||
@@ -620,9 +697,10 @@ impl PathToolData {
|
||||
self.drag_start_pos = input.mouse.position;
|
||||
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
self.started_drawing_from_inside = true;
|
||||
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
|
||||
PathToolFsmState::Drawing { selection_shape }
|
||||
}
|
||||
// Start drawing
|
||||
else {
|
||||
@@ -644,7 +722,7 @@ impl PathToolData {
|
||||
let transform = document.metadata().transform_to_document(layer);
|
||||
|
||||
let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder);
|
||||
for point in state.selected() {
|
||||
for point in state.selected_points() {
|
||||
let Some(anchor) = point.get_anchor(&vector_data) else { continue };
|
||||
layer_manipulators.insert(anchor);
|
||||
let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) else { continue };
|
||||
@@ -748,7 +826,7 @@ impl PathToolData {
|
||||
}
|
||||
|
||||
// Only count selected handles
|
||||
let selected_handle = selection.selected().next()?.as_handle()?;
|
||||
let selected_handle = selection.selected_points().next()?.as_handle()?;
|
||||
let handle_id = selected_handle.to_manipulator_point();
|
||||
|
||||
let layer_to_document = document.metadata().transform_to_document(*layer);
|
||||
@@ -875,7 +953,7 @@ impl PathToolData {
|
||||
let drag_start = self.drag_start_pos;
|
||||
let opposite_delta = drag_start - current_mouse;
|
||||
|
||||
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses);
|
||||
shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses);
|
||||
|
||||
// Calculate the projected delta and shift the points along that delta
|
||||
let delta = current_mouse - drag_start;
|
||||
@@ -887,7 +965,7 @@ impl PathToolData {
|
||||
_ => DVec2::new(delta.x, 0.),
|
||||
};
|
||||
|
||||
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses);
|
||||
shape_editor.move_selected_points_and_segments(None, document, projected_delta, false, true, false, None, false, responses);
|
||||
}
|
||||
|
||||
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
@@ -903,12 +981,12 @@ impl PathToolData {
|
||||
_ => DVec2::new(opposite_delta.x, 0.),
|
||||
};
|
||||
|
||||
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, responses);
|
||||
shape_editor.move_selected_points_and_segments(None, document, opposite_projected_delta, false, true, false, None, false, responses);
|
||||
|
||||
// Calculate what actually would have been the original delta for the point, and apply that
|
||||
let delta = current_mouse - drag_start;
|
||||
|
||||
shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses);
|
||||
shape_editor.move_selected_points_and_segments(None, document, delta, false, true, false, None, false, responses);
|
||||
|
||||
self.snapping_axis = None;
|
||||
}
|
||||
@@ -930,6 +1008,28 @@ impl PathToolData {
|
||||
tangent_vector.try_normalize()
|
||||
}
|
||||
|
||||
fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) {
|
||||
// Check if there is no point nearby
|
||||
if shape_editor
|
||||
.find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone())
|
||||
.is_some()
|
||||
{
|
||||
self.segment = None;
|
||||
}
|
||||
// If already hovering on a segment, then recalculate its closest point
|
||||
else if let Some(closest_segment) = &mut self.segment {
|
||||
closest_segment.update_closest_point(document.metadata(), position);
|
||||
|
||||
if closest_segment.too_far(position, SEGMENT_INSERTION_DISTANCE) {
|
||||
self.segment = None;
|
||||
}
|
||||
}
|
||||
// If not, check that if there is some closest segment or not
|
||||
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, position, SEGMENT_INSERTION_DISTANCE) {
|
||||
self.segment = Some(closest_segment);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool {
|
||||
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
|
||||
|
||||
@@ -1192,7 +1292,7 @@ impl PathToolData {
|
||||
self.temporary_colinear_handles = false;
|
||||
skip_opposite = true;
|
||||
}
|
||||
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
|
||||
shape_editor.move_selected_points_and_segments(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
|
||||
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
|
||||
} else {
|
||||
let Some(axis) = self.snapping_axis else { return };
|
||||
@@ -1201,7 +1301,7 @@ impl PathToolData {
|
||||
Axis::Y => DVec2::new(0., unsnapped_delta.y),
|
||||
_ => DVec2::new(unsnapped_delta.x, 0.),
|
||||
};
|
||||
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
|
||||
shape_editor.move_selected_points_and_segments(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
|
||||
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
|
||||
}
|
||||
|
||||
@@ -1225,7 +1325,7 @@ impl Fsm for PathToolFsmState {
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
|
||||
|
||||
update_dynamic_hints(self, responses, shape_editor, document, tool_data);
|
||||
update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options);
|
||||
|
||||
let ToolMessage::Path(event) = event else { return self };
|
||||
match (self, event) {
|
||||
@@ -1309,48 +1409,43 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
match self {
|
||||
Self::Ready => {
|
||||
// Check if there is no point nearby
|
||||
if 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(),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
tool_data.segment = None;
|
||||
}
|
||||
// If already hovering on a segment, then recalculate its closest point
|
||||
else if let Some(closest_segment) = &mut tool_data.segment {
|
||||
closest_segment.update_closest_point(document.metadata(), input.mouse.position);
|
||||
|
||||
if closest_segment.too_far(input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
|
||||
tool_data.segment = None;
|
||||
}
|
||||
}
|
||||
// If not, check that if there is some closest segment or not
|
||||
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
|
||||
tool_data.segment = Some(closest_segment);
|
||||
}
|
||||
tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode);
|
||||
|
||||
if let Some(closest_segment) = &tool_data.segment {
|
||||
let perp = closest_segment.calculate_perp(document);
|
||||
let point = closest_segment.closest_point(document.metadata());
|
||||
if tool_options.path_editing_mode.segment_editing_mode {
|
||||
let transform = document.metadata().transform_to_viewport(closest_segment.layer());
|
||||
|
||||
// Draw an X on the segment
|
||||
if tool_data.delete_segment_pressed {
|
||||
let angle = 45_f64.to_radians();
|
||||
let tilted_line = DVec2::from_angle(angle).rotate(perp);
|
||||
let tilted_perp = tilted_line.perp();
|
||||
overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform);
|
||||
|
||||
overlay_context.line(point - tilted_line * SEGMENT_OVERLAY_SIZE, point + tilted_line * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
|
||||
overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
|
||||
}
|
||||
// Draw a line on the segment
|
||||
else {
|
||||
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
|
||||
// Draw the anchors again
|
||||
let display_anchors = overlay_context.visibility_settings.anchors();
|
||||
if display_anchors {
|
||||
let start_pos = transform.transform_point2(closest_segment.bezier().start);
|
||||
let end_pos = transform.transform_point2(closest_segment.bezier().end);
|
||||
let start_id = closest_segment.points()[0];
|
||||
let end_id = closest_segment.points()[1];
|
||||
if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) {
|
||||
overlay_context.manipulator_anchor(start_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(start_id)), None);
|
||||
overlay_context.manipulator_anchor(end_pos, shape_state.is_point_selected(ManipulatorPointId::Anchor(end_id)), None);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let perp = closest_segment.calculate_perp(document);
|
||||
let point = closest_segment.closest_point(document.metadata());
|
||||
|
||||
// Draw an X on the segment
|
||||
if tool_data.delete_segment_pressed {
|
||||
let angle = 45_f64.to_radians();
|
||||
let tilted_line = DVec2::from_angle(angle).rotate(perp);
|
||||
let tilted_perp = tilted_line.perp();
|
||||
|
||||
overlay_context.line(point - tilted_line * SEGMENT_OVERLAY_SIZE, point + tilted_line * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
|
||||
overlay_context.line(point - tilted_perp * SEGMENT_OVERLAY_SIZE, point + tilted_perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
|
||||
}
|
||||
// Draw a line on the segment
|
||||
else {
|
||||
overlay_context.line(point - perp * SEGMENT_OVERLAY_SIZE, point + perp * SEGMENT_OVERLAY_SIZE, Some(COLOR_OVERLAY_BLUE), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1370,11 +1465,13 @@ impl Fsm for PathToolFsmState {
|
||||
let quad = tool_data.selection_quad(document.metadata());
|
||||
let polygon = &tool_data.lasso_polygon;
|
||||
|
||||
match (selection_shape, selection_mode) {
|
||||
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Box, _) => overlay_context.quad(quad, None, fill_color),
|
||||
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color),
|
||||
match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) {
|
||||
// Don't draw box if it is from inside a shape and selection just began
|
||||
(SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color),
|
||||
(SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color),
|
||||
(SelectionShapeType::Box, _, _) => {}
|
||||
}
|
||||
}
|
||||
Self::Dragging(_) => {
|
||||
@@ -1420,12 +1517,14 @@ impl Fsm for PathToolFsmState {
|
||||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_restore_handle,
|
||||
molding_in_segment_edit,
|
||||
},
|
||||
) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
let lasso_select = input.keyboard.get(lasso_select as usize);
|
||||
let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize);
|
||||
let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize);
|
||||
let molding_in_segment_edit = input.keyboard.get(molding_in_segment_edit as usize);
|
||||
|
||||
tool_data.selection_mode = None;
|
||||
tool_data.lasso_polygon.clear();
|
||||
@@ -1439,7 +1538,10 @@ impl Fsm for PathToolFsmState {
|
||||
lasso_select,
|
||||
handle_drag_from_anchor,
|
||||
drag_zero_handle,
|
||||
molding_in_segment_edit,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
tool_options.path_editing_mode.point_editing_mode,
|
||||
)
|
||||
}
|
||||
(
|
||||
@@ -1455,6 +1557,7 @@ impl Fsm for PathToolFsmState {
|
||||
},
|
||||
) => {
|
||||
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
tool_data.started_drawing_from_inside = false;
|
||||
|
||||
if selection_shape == SelectionShapeType::Lasso {
|
||||
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
|
||||
@@ -1516,12 +1619,6 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.handle_drag_toggle = true;
|
||||
}
|
||||
|
||||
if tool_data.selection_status.is_none() {
|
||||
if let Some(layer) = document.click(input) {
|
||||
shape_editor.select_all_anchors_in_layer(document, layer);
|
||||
}
|
||||
}
|
||||
|
||||
let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize);
|
||||
let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled;
|
||||
let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled;
|
||||
@@ -1707,6 +1804,11 @@ impl Fsm for PathToolFsmState {
|
||||
if tool_data.drag_start_pos == previous_mouse {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
let selection_mode = match tool_action_data.preferences.get_selection_mode() {
|
||||
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
|
||||
selection_mode => selection_mode,
|
||||
};
|
||||
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = [tool_data.drag_start_pos, previous_mouse];
|
||||
@@ -1716,6 +1818,8 @@ impl Fsm for PathToolFsmState {
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
|
||||
@@ -1724,6 +1828,8 @@ impl Fsm for PathToolFsmState {
|
||||
selection_change,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1735,6 +1841,7 @@ impl Fsm for PathToolFsmState {
|
||||
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.deselect_all_segments();
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
|
||||
|
||||
tool_data.saved_points_before_handle_drag.clear();
|
||||
@@ -1783,8 +1890,17 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
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![] });
|
||||
|
||||
let selection_mode = match tool_action_data.preferences.get_selection_mode() {
|
||||
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
|
||||
selection_mode => selection_mode,
|
||||
};
|
||||
|
||||
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
|
||||
// If click happens inside of a shape then don't set selected nodes to empty
|
||||
if document.click(input).is_none() {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
}
|
||||
} else {
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
@@ -1795,6 +1911,8 @@ impl Fsm for PathToolFsmState {
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(
|
||||
@@ -1803,6 +1921,8 @@ impl Fsm for PathToolFsmState {
|
||||
select_kind,
|
||||
tool_options.path_overlay_mode,
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
tool_options.path_editing_mode.segment_editing_mode,
|
||||
selection_mode,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1823,32 +1943,31 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.frontier_handles_info.clone(),
|
||||
);
|
||||
|
||||
let nearest_segment = tool_data.segment.clone();
|
||||
|
||||
if let Some(segment) = &mut tool_data.segment {
|
||||
if !drag_occurred && !tool_data.molding_segment {
|
||||
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
|
||||
if !drag_occurred && !tool_data.molding_segment && !segment_mode {
|
||||
if tool_data.delete_segment_pressed {
|
||||
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
|
||||
shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points());
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
} else {
|
||||
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection);
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
} else {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
}
|
||||
|
||||
tool_data.segment = None;
|
||||
tool_data.molding_info = None;
|
||||
tool_data.molding_segment = false;
|
||||
tool_data.temporary_adjacent_handles_while_molding = None;
|
||||
|
||||
return PathToolFsmState::Ready;
|
||||
}
|
||||
|
||||
let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
|
||||
|
||||
if let Some((layer, nearest_point)) = nearest_point {
|
||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
||||
if !drag_occurred && extend_selection {
|
||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
||||
if clicked_selected && tool_data.last_clicked_point_was_selected {
|
||||
shape_editor.selected_shape_state.entry(layer).or_default().deselect_point(nearest_point);
|
||||
} else {
|
||||
@@ -1856,6 +1975,49 @@ impl Fsm for PathToolFsmState {
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
if !drag_occurred && !extend_selection && clicked_selected {
|
||||
if tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp = shape_editor.selected_points().copied().collect::<HashSet<_>>();
|
||||
}
|
||||
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.deselect_all_segments();
|
||||
|
||||
shape_editor.selected_shape_state.entry(layer).or_default().select_point(nearest_point);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
// Segment editing mode
|
||||
else if let Some(nearest_segment) = nearest_segment {
|
||||
if segment_mode {
|
||||
let clicked_selected = shape_editor.selected_segments().any(|&segment| segment == nearest_segment.segment());
|
||||
if !drag_occurred && extend_selection {
|
||||
if clicked_selected && tool_data.last_clicked_segment_was_selected {
|
||||
shape_editor
|
||||
.selected_shape_state
|
||||
.entry(nearest_segment.layer())
|
||||
.or_default()
|
||||
.deselect_segment(nearest_segment.segment());
|
||||
} else {
|
||||
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment());
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
if !drag_occurred && !extend_selection && clicked_selected {
|
||||
shape_editor.deselect_all_segments();
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment());
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Deselect all points if the user clicks the filled region of the shape
|
||||
else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.deselect_all_segments();
|
||||
}
|
||||
|
||||
if tool_data.temporary_colinear_handles {
|
||||
@@ -1881,25 +2043,6 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.select_anchor_toggled = false;
|
||||
}
|
||||
|
||||
if let Some((layer, nearest_point)) = nearest_point {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Deselect all points if the user clicks the filled region of the shape
|
||||
else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
|
||||
shape_editor.deselect_all_points();
|
||||
}
|
||||
|
||||
tool_data.snapping_axis = None;
|
||||
tool_data.sliding_point_info = None;
|
||||
|
||||
@@ -1915,6 +2058,7 @@ impl Fsm for PathToolFsmState {
|
||||
(_, PathToolMessage::Delete) => {
|
||||
// Delete the selected points and clean up overlays
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
shape_editor.delete_selected_segments(document, responses);
|
||||
shape_editor.delete_selected_points(document, responses);
|
||||
responses.add(PathToolMessage::SelectionChanged);
|
||||
|
||||
@@ -1951,6 +2095,7 @@ impl Fsm for PathToolFsmState {
|
||||
if let Some(layer) = document.click(input) {
|
||||
// Select all points in the layer
|
||||
shape_editor.select_connected_anchors(document, layer, input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
|
||||
PathToolFsmState::Ready
|
||||
@@ -1960,7 +2105,7 @@ impl Fsm for PathToolFsmState {
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::NudgeSelectedPoints { delta_x, delta_y }) => {
|
||||
shape_editor.move_selected_points(
|
||||
shape_editor.move_selected_points_and_segments(
|
||||
tool_data.opposing_handle_lengths.take(),
|
||||
document,
|
||||
(delta_x, delta_y).into(),
|
||||
@@ -2040,10 +2185,6 @@ enum SelectionStatus {
|
||||
}
|
||||
|
||||
impl SelectionStatus {
|
||||
fn is_none(&self) -> bool {
|
||||
self == &SelectionStatus::None
|
||||
}
|
||||
|
||||
fn as_one(&self) -> Option<&SingleSelectedPoint> {
|
||||
match self {
|
||||
SelectionStatus::One(one) => Some(one),
|
||||
@@ -2173,9 +2314,7 @@ 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 (anchor, handle_position) = handle_id.get_anchor(vector_data).zip(handle_id.get_position(vector_data))?;
|
||||
|
||||
let check_if_close = |point_id: &PointId| {
|
||||
let Some(anchor_position) = vector_data.point_domain.position_from_id(*point_id) else {
|
||||
@@ -2184,7 +2323,7 @@ fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector_data:
|
||||
(anchor_position - handle_position).length() < 10.
|
||||
};
|
||||
|
||||
vector_data.connected_points(anchor).find(|point| check_if_close(point))
|
||||
vector_data.connected_points(anchor).find(check_if_close)
|
||||
}
|
||||
fn calculate_adjacent_anchor_tangent(
|
||||
currently_dragged_handle: ManipulatorPointId,
|
||||
@@ -2240,7 +2379,7 @@ fn calculate_adjacent_anchor_tangent(
|
||||
};
|
||||
|
||||
let angle = shared_segment_handle
|
||||
.get_position(&vector_data)
|
||||
.get_position(vector_data)
|
||||
.zip(adjacent_anchor_position)
|
||||
.map(|(handle, anchor)| -(handle - anchor).angle_to(DVec2::X));
|
||||
|
||||
@@ -2251,7 +2390,14 @@ fn calculate_adjacent_anchor_tangent(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Message>, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) {
|
||||
fn update_dynamic_hints(
|
||||
state: PathToolFsmState,
|
||||
responses: &mut VecDeque<Message>,
|
||||
shape_editor: &mut ShapeState,
|
||||
document: &DocumentMessageHandler,
|
||||
tool_data: &PathToolData,
|
||||
tool_options: &PathToolOptions,
|
||||
) {
|
||||
// Condinting based on currently selected segment if it has any one g1 continuous handle
|
||||
|
||||
let hint_data = match state {
|
||||
@@ -2285,12 +2431,27 @@ fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Messag
|
||||
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus());
|
||||
}
|
||||
|
||||
let mut hint_data = vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
||||
];
|
||||
let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) {
|
||||
(true, true) => {
|
||||
vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]),
|
||||
]
|
||||
}
|
||||
(true, false) => {
|
||||
vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
||||
]
|
||||
}
|
||||
(false, _) => {
|
||||
vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if at_least_one_anchor_selected {
|
||||
// TODO: Dynamically show either "Smooth" or "Sharp" based on the current state
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::DEFAULT_STROKE_WIDTH;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PolygonTool {
|
||||
fsm_state: PolygonToolFsmState,
|
||||
tool_data: PolygonToolData,
|
||||
options: PolygonOptions,
|
||||
}
|
||||
|
||||
pub struct PolygonOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
vertices: u32,
|
||||
polygon_type: PolygonType,
|
||||
}
|
||||
|
||||
impl Default for PolygonOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
vertices: 5,
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_secondary(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
polygon_type: PolygonType::Convex,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Polygon)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum PolygonToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove { center: Key, lock_ratio: Key },
|
||||
PointerOutsideViewport { center: Key, lock_ratio: Key },
|
||||
UpdateOptions(PolygonOptionsUpdate),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum PolygonType {
|
||||
Convex = 0,
|
||||
Star = 1,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum PolygonOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
PolygonType(PolygonType),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
Vertices(u32),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
impl ToolMetadata for PolygonTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorPolygonTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Polygon Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Polygon
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sides_widget(vertices: u32) -> WidgetHolder {
|
||||
NumberInput::new(Some(vertices as f64))
|
||||
.label("Sides")
|
||||
.int()
|
||||
.min(3.)
|
||||
.max(1000.)
|
||||
.mode(NumberInputMode::Increment)
|
||||
.on_update(|number_input: &NumberInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
fn create_star_option_widget(polygon_type: PolygonType) -> WidgetHolder {
|
||||
let entries = vec![
|
||||
RadioEntryData::new("convex")
|
||||
.label("Convex")
|
||||
.on_update(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::PolygonType(PolygonType::Convex)).into()),
|
||||
RadioEntryData::new("star")
|
||||
.label("Star")
|
||||
.on_update(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::PolygonType(PolygonType::Star)).into()),
|
||||
];
|
||||
RadioInput::new(entries).selected_index(Some(polygon_type as u32)).widget_holder()
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
NumberInput::new(Some(line_weight))
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
impl LayoutHolder for PolygonTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = vec![
|
||||
create_star_option_widget(self.options.polygon_type),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
create_sides_widget(self.options.vertices),
|
||||
];
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
||||
widgets.append(&mut self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PolygonTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
let ToolMessage::Polygon(PolygonToolMessage::UpdateOptions(action)) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
PolygonOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices,
|
||||
PolygonOptionsUpdate::PolygonType(polygon_type) => self.options.polygon_type = polygon_type,
|
||||
PolygonOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
}
|
||||
PolygonOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
PolygonOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
PolygonOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
}
|
||||
PolygonOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
PolygonOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
PolygonToolFsmState::Ready => actions!(PolygonToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
PolygonToolFsmState::Drawing => actions!(PolygonToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for PolygonTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context| PolygonToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(PolygonToolMessage::Abort.into()),
|
||||
working_color_changed: Some(PolygonToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum PolygonToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct PolygonToolData {
|
||||
data: Resize,
|
||||
auto_panning: AutoPanning,
|
||||
}
|
||||
|
||||
impl Fsm for PolygonToolFsmState {
|
||||
type ToolData = PolygonToolData;
|
||||
type ToolOptions = PolygonOptions;
|
||||
|
||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||
let ToolActionHandlerData {
|
||||
document, global_tool_data, input, ..
|
||||
} = tool_action_data;
|
||||
|
||||
let polygon_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Polygon(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, PolygonToolMessage::Overlays(mut overlay_context)) => {
|
||||
polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(PolygonToolFsmState::Ready, PolygonToolMessage::DragStart) => {
|
||||
polygon_data.start(document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let node = match tool_options.polygon_type {
|
||||
PolygonType::Convex => resolve_document_node_type("Regular Polygon")
|
||||
.expect("Regular Polygon node does not exist")
|
||||
.node_template_input_override([
|
||||
None,
|
||||
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
|
||||
]),
|
||||
PolygonType::Star => resolve_document_node_type("Star").expect("Star node does not exist").node_template_input_override([
|
||||
None,
|
||||
Some(NodeInput::value(TaggedValue::U32(tool_options.vertices), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
|
||||
]),
|
||||
};
|
||||
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
polygon_data.layer = Some(layer);
|
||||
|
||||
PolygonToolFsmState::Drawing
|
||||
}
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
if let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) {
|
||||
if let Some(layer) = tool_data.data.layer {
|
||||
// TODO: We need to determine how to allow the polygon node to make irregular shapes
|
||||
update_radius_sign(end, start, layer, document, responses);
|
||||
|
||||
let dimensions = (start - end).abs();
|
||||
let mut scale = DVec2::ONE;
|
||||
let radius: f64;
|
||||
|
||||
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
|
||||
if dimensions.x > dimensions.y {
|
||||
scale.x = dimensions.x / dimensions.y;
|
||||
radius = dimensions.y / 2.;
|
||||
} else {
|
||||
scale.y = dimensions.y / dimensions.x;
|
||||
radius = dimensions.x / 2.;
|
||||
}
|
||||
|
||||
match tool_options.polygon_type {
|
||||
PolygonType::Convex => {
|
||||
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
}
|
||||
PolygonType::Star => {
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 3),
|
||||
input: NodeInput::value(TaggedValue::F64(radius / 2.), false),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
PolygonToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(_, PolygonToolMessage::PointerMove { .. }) => {
|
||||
polygon_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
PolygonToolFsmState::Drawing
|
||||
}
|
||||
(state, PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
PolygonToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
PolygonToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.stop(&messages, responses);
|
||||
|
||||
state
|
||||
}
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(polygon_data.viewport_drag_start(document), responses);
|
||||
polygon_data.cleanup(responses);
|
||||
|
||||
PolygonToolFsmState::Ready
|
||||
}
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::Abort) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
|
||||
polygon_data.cleanup(responses);
|
||||
|
||||
PolygonToolFsmState::Ready
|
||||
}
|
||||
(_, PolygonToolMessage::WorkingColorChanged) => {
|
||||
responses.add(PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
PolygonToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])]),
|
||||
PolygonToolFsmState::Drawing => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
|
||||
/// In the case where the polygon/star is upside down and the number of sides is odd, we negate the radius instead of using a negative scale.
|
||||
fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &mut DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let sign_num = if end[1] > start[1] { 1. } else { -1. };
|
||||
let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
|
||||
|
||||
if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
|
||||
let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return };
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(polygon_node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
|
||||
let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return };
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(star_node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(star_node_id, 3),
|
||||
input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::DEFAULT_STROKE_WIDTH;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RectangleTool {
|
||||
fsm_state: RectangleToolFsmState,
|
||||
tool_data: RectangleToolData,
|
||||
options: RectangleToolOptions,
|
||||
}
|
||||
|
||||
pub struct RectangleToolOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
}
|
||||
|
||||
impl Default for RectangleToolOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_secondary(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum RectangleOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Rectangle)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum RectangleToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove { center: Key, lock_ratio: Key },
|
||||
PointerOutsideViewport { center: Key, lock_ratio: Key },
|
||||
UpdateOptions(RectangleOptionsUpdate),
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
NumberInput::new(Some(line_weight))
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
impl LayoutHolder for RectangleTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
);
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for RectangleTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
let ToolMessage::Rectangle(RectangleToolMessage::UpdateOptions(action)) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
RectangleOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
}
|
||||
RectangleOptionsUpdate::FillColorType(color_type) => self.options.fill.color_type = color_type,
|
||||
RectangleOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight,
|
||||
RectangleOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
}
|
||||
RectangleOptionsUpdate::StrokeColorType(color_type) => self.options.stroke.color_type = color_type,
|
||||
RectangleOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
}
|
||||
}
|
||||
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
RectangleToolFsmState::Ready => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
RectangleToolFsmState::Drawing => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolMetadata for RectangleTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorRectangleTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Rectangle Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
||||
ToolType::Rectangle
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for RectangleTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context| RectangleToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(RectangleToolMessage::Abort.into()),
|
||||
working_color_changed: Some(RectangleToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum RectangleToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
Drawing,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RectangleToolData {
|
||||
data: Resize,
|
||||
auto_panning: AutoPanning,
|
||||
}
|
||||
|
||||
impl Fsm for RectangleToolFsmState {
|
||||
type ToolData = RectangleToolData;
|
||||
type ToolOptions = RectangleToolOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
ToolActionHandlerData {
|
||||
document, global_tool_data, input, ..
|
||||
}: &mut ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Rectangle(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(_, RectangleToolMessage::Overlays(mut overlay_context)) => {
|
||||
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => {
|
||||
shape_data.start(document, input);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node does not exist");
|
||||
let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))]);
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
responses.add(Message::StartBuffer);
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
shape_data.layer = Some(layer);
|
||||
|
||||
RectangleToolFsmState::Drawing
|
||||
}
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
if let Some([start, end]) = shape_data.calculate_points(document, input, center, lock_ratio) {
|
||||
if let Some(layer) = shape_data.layer {
|
||||
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 2),
|
||||
input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false),
|
||||
});
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_translation((start + end) / 2.),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
RectangleToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
RectangleToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(_, RectangleToolMessage::PointerMove { .. }) => {
|
||||
shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
RectangleToolFsmState::Drawing
|
||||
}
|
||||
(state, RectangleToolMessage::PointerOutsideViewport { center, lock_ratio }) => {
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
RectangleToolMessage::PointerOutsideViewport { center, lock_ratio }.into(),
|
||||
RectangleToolMessage::PointerMove { center, lock_ratio }.into(),
|
||||
];
|
||||
tool_data.auto_panning.stop(&messages, responses);
|
||||
|
||||
state
|
||||
}
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
RectangleToolFsmState::Ready
|
||||
}
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::Abort) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
RectangleToolFsmState::Ready
|
||||
}
|
||||
(_, RectangleToolMessage::WorkingColorChanged) => {
|
||||
responses.add(RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])]),
|
||||
RectangleToolFsmState::Drawing => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT,
|
||||
SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE,
|
||||
};
|
||||
use crate::consts::*;
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
@@ -12,7 +9,6 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
|
||||
@@ -22,14 +18,14 @@ use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::common_functionality::utility_functions::text_bounding_box;
|
||||
use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage};
|
||||
use bezier_rs::Subpath;
|
||||
use glam::DMat2;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::path_bool::BooleanOperation;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::renderer::Rect;
|
||||
use graphene_std::transform::ReferencePoint;
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -861,35 +857,19 @@ impl Fsm for SelectToolFsmState {
|
||||
remove_from_selection,
|
||||
select_deepest,
|
||||
lasso_select,
|
||||
skew,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
tool_data.selection_mode = None;
|
||||
|
||||
let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| {
|
||||
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||
|
||||
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
|
||||
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
|
||||
bounding_box.opposite_pivot = selected_edges.calculate_pivot();
|
||||
selected_edges
|
||||
});
|
||||
|
||||
edges
|
||||
});
|
||||
|
||||
let rotating_bounds = tool_data
|
||||
.bounding_box_manager
|
||||
.as_ref()
|
||||
.map(|bounding_box| bounding_box.check_rotate(input.mouse.position))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect();
|
||||
let intersection_list = document.click_list(input).collect::<Vec<_>>();
|
||||
let intersection = document.find_deepest(&intersection_list);
|
||||
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging);
|
||||
|
||||
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
|
||||
// If the user is dragging the rotate trigger, go into RotatingBounds mode.
|
||||
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
||||
@@ -907,15 +887,10 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
let show_compass = bounds.is_some_and(|quad| quad.all_sides_at_least_width(COMPASS_ROSE_HOVER_RING_DIAMETER) && quad.contains(mouse_position));
|
||||
let can_grab_compass_rose = compass_rose_state.can_grab() && (show_compass || bounds.is_none());
|
||||
let is_flat_layer = tool_data
|
||||
.bounding_box_manager
|
||||
.as_ref()
|
||||
.map(|bounding_box_manager| bounding_box_manager.transform_tampered)
|
||||
.unwrap_or(true);
|
||||
|
||||
let state =
|
||||
let state = if is_over_pivot
|
||||
// Dragging the pivot
|
||||
if is_over_pivot {
|
||||
{
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
@@ -924,47 +899,12 @@ impl Fsm for SelectToolFsmState {
|
||||
SelectToolFsmState::DraggingPivot
|
||||
}
|
||||
// Dragging one (or two, forming a corner) of the transform cage bounding box edges
|
||||
else if dragging_bounds.is_some() && !is_flat_layer {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_bound_transform = bounds.transform;
|
||||
|
||||
tool_data.layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&tool_data.layers_dragging,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None
|
||||
);
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
|
||||
// Check if we're hovering over a skew triangle
|
||||
let edges = bounds.check_selected_edges(input.mouse.position);
|
||||
if let Some(edges) = edges {
|
||||
let closest_edge = bounds.get_closest_edge(edges, input.mouse.position);
|
||||
if bounds.check_skew_handle(input.mouse.position, closest_edge) {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
return SelectToolFsmState::SkewingBounds { skew };
|
||||
}
|
||||
}
|
||||
}
|
||||
else if resize {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
SelectToolFsmState::ResizingBounds
|
||||
} else if skew {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
SelectToolFsmState::SkewingBounds { skew: Key::Control }
|
||||
}
|
||||
// Dragging the selected layers around to transform them
|
||||
else if can_grab_compass_rose || intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) {
|
||||
@@ -983,37 +923,16 @@ impl Fsm for SelectToolFsmState {
|
||||
let axis_state = compass_rose_state.axis_type().filter(|_| can_grab_compass_rose);
|
||||
(axis_state.unwrap_or_default(), axis_state.is_some())
|
||||
};
|
||||
SelectToolFsmState::Dragging { axis, using_compass, has_dragged: false, deepest: input.keyboard.key(select_deepest), remove: input.keyboard.key(extend_selection) }
|
||||
SelectToolFsmState::Dragging {
|
||||
axis,
|
||||
using_compass,
|
||||
has_dragged: false,
|
||||
deepest: input.keyboard.key(select_deepest),
|
||||
remove: input.keyboard.key(extend_selection),
|
||||
}
|
||||
}
|
||||
// Dragging near the transform cage bounding box to rotate it
|
||||
else if rotating_bounds {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
tool_data.layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&selected,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None
|
||||
);
|
||||
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
}
|
||||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
else if rotate {
|
||||
SelectToolFsmState::RotatingBounds
|
||||
}
|
||||
// Dragging a selection box
|
||||
@@ -1036,7 +955,13 @@ impl Fsm for SelectToolFsmState {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
SelectToolFsmState::Dragging { axis: Axis::None, using_compass: false, has_dragged: false, deepest: input.keyboard.key(select_deepest), remove: input.keyboard.key(extend_selection) }
|
||||
SelectToolFsmState::Dragging {
|
||||
axis: Axis::None,
|
||||
using_compass: false,
|
||||
has_dragged: false,
|
||||
deepest: input.keyboard.key(select_deepest),
|
||||
remove: input.keyboard.key(extend_selection),
|
||||
}
|
||||
} else {
|
||||
let selection_shape = if input.keyboard.key(lasso_select) { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
|
||||
SelectToolFsmState::Drawing { selection_shape, has_drawn: false }
|
||||
@@ -1120,123 +1045,52 @@ impl Fsm for SelectToolFsmState {
|
||||
}
|
||||
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
let (center, constrain) = (input.keyboard.key(modifier_keys.center), input.keyboard.key(modifier_keys.axis_align));
|
||||
|
||||
let center = center.then_some(bounds.center_of_transformation);
|
||||
let snap = Some(SizeSnapData {
|
||||
manager: &mut tool_data.snap_manager,
|
||||
points: &mut tool_data.snap_candidates,
|
||||
snap_data: SnapData::ignore(document, input, &tool_data.layers_dragging),
|
||||
});
|
||||
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
|
||||
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
|
||||
|
||||
let pivot_transform = DAffine2::from_translation(pivot);
|
||||
let transformation = pivot_transform * delta * pivot_transform.inverse();
|
||||
|
||||
tool_data.layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
let selected = &tool_data.layers_dragging;
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut pivot,
|
||||
selected,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
);
|
||||
|
||||
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
}
|
||||
resize_bounds(
|
||||
document,
|
||||
responses,
|
||||
bounds,
|
||||
&mut tool_data.layers_dragging,
|
||||
&mut tool_data.snap_manager,
|
||||
&mut tool_data.snap_candidates,
|
||||
input,
|
||||
input.keyboard.key(modifier_keys.center),
|
||||
input.keyboard.key(modifier_keys.axis_align),
|
||||
ToolType::Select,
|
||||
);
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
SelectToolMessage::PointerMove(modifier_keys).into(),
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
}
|
||||
SelectToolFsmState::ResizingBounds
|
||||
}
|
||||
(SelectToolFsmState::SkewingBounds { skew }, SelectToolMessage::PointerMove(_)) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
let free_movement = input.keyboard.key(skew);
|
||||
let transformation = movement.skew_transform(input.mouse.position, bounds.original_bound_transform, free_movement);
|
||||
|
||||
tool_data.layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of layers_dragging");
|
||||
false
|
||||
}
|
||||
});
|
||||
let selected = &tool_data.layers_dragging;
|
||||
let mut pivot = DVec2::ZERO;
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut pivot,
|
||||
selected,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
);
|
||||
|
||||
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
|
||||
}
|
||||
skew_bounds(
|
||||
document,
|
||||
responses,
|
||||
bounds,
|
||||
input.keyboard.key(skew),
|
||||
&mut tool_data.layers_dragging,
|
||||
input.mouse.position,
|
||||
ToolType::Select,
|
||||
);
|
||||
}
|
||||
SelectToolFsmState::SkewingBounds { skew }
|
||||
}
|
||||
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(_)) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
let angle = {
|
||||
let start_offset = tool_data.drag_start - bounds.center_of_transformation;
|
||||
let end_offset = input.mouse.position - bounds.center_of_transformation;
|
||||
|
||||
start_offset.angle_to(end_offset)
|
||||
};
|
||||
|
||||
let snapped_angle = if input.keyboard.key(modifier_keys.snap_angle) {
|
||||
let snap_resolution = ROTATE_INCREMENT.to_radians();
|
||||
(angle / snap_resolution).round() * snap_resolution
|
||||
} else {
|
||||
angle
|
||||
};
|
||||
|
||||
let delta = DAffine2::from_angle(snapped_angle);
|
||||
|
||||
tool_data.layers_dragging.retain(|layer| {
|
||||
if *layer != LayerNodeIdentifier::ROOT_PARENT {
|
||||
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
|
||||
} else {
|
||||
log::error!("ROOT_PARENT should not be part of replacement_selected_layers");
|
||||
false
|
||||
}
|
||||
});
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&tool_data.layers_dragging,
|
||||
rotate_bounds(
|
||||
document,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
bounds,
|
||||
&mut tool_data.layers_dragging,
|
||||
tool_data.drag_start,
|
||||
input.mouse.position,
|
||||
input.keyboard.key(Key::Shift),
|
||||
ToolType::Select,
|
||||
);
|
||||
|
||||
selected.update_transforms(delta, None, None);
|
||||
}
|
||||
|
||||
SelectToolFsmState::RotatingBounds
|
||||
@@ -1548,6 +1402,7 @@ impl Fsm for SelectToolFsmState {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.axis_align = false;
|
||||
tool_data.lasso_polygon.clear();
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
@@ -1568,6 +1423,7 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.lasso_polygon.clear();
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
|
||||
@@ -0,0 +1,909 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoManager;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
|
||||
use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays};
|
||||
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
|
||||
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::vector::misc::ArcType;
|
||||
use std::vec;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ShapeTool {
|
||||
fsm_state: ShapeToolFsmState,
|
||||
tool_data: ShapeToolData,
|
||||
options: ShapeToolOptions,
|
||||
}
|
||||
|
||||
pub struct ShapeToolOptions {
|
||||
line_weight: f64,
|
||||
fill: ToolColorOptions,
|
||||
stroke: ToolColorOptions,
|
||||
vertices: u32,
|
||||
shape_type: ShapeType,
|
||||
arc_type: ArcType,
|
||||
}
|
||||
|
||||
impl Default for ShapeToolOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
line_weight: DEFAULT_STROKE_WIDTH,
|
||||
fill: ToolColorOptions::new_secondary(),
|
||||
stroke: ToolColorOptions::new_primary(),
|
||||
vertices: 5,
|
||||
shape_type: ShapeType::Polygon,
|
||||
arc_type: ArcType::Open,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ShapeOptionsUpdate {
|
||||
FillColor(Option<Color>),
|
||||
FillColorType(ToolColorType),
|
||||
LineWeight(f64),
|
||||
StrokeColor(Option<Color>),
|
||||
StrokeColorType(ToolColorType),
|
||||
WorkingColors(Option<Color>, Option<Color>),
|
||||
Vertices(u32),
|
||||
ShapeType(ShapeType),
|
||||
ArcType(ArcType),
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Shape)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum ShapeToolMessage {
|
||||
// Standard messages
|
||||
Overlays(OverlayContext),
|
||||
Abort,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
HideShapeTypeWidget(bool),
|
||||
PointerMove(ShapeToolModifierKey),
|
||||
PointerOutsideViewport(ShapeToolModifierKey),
|
||||
UpdateOptions(ShapeOptionsUpdate),
|
||||
SetShape(ShapeType),
|
||||
|
||||
IncreaseSides,
|
||||
DecreaseSides,
|
||||
|
||||
NudgeSelectedLayers { delta_x: f64, delta_y: f64, resize: Key, resize_opposite_corner: Key },
|
||||
}
|
||||
|
||||
fn create_sides_widget(vertices: u32) -> WidgetHolder {
|
||||
NumberInput::new(Some(vertices as f64))
|
||||
.label("Sides")
|
||||
.int()
|
||||
.min(3.)
|
||||
.max(1000.)
|
||||
.mode(NumberInputMode::Increment)
|
||||
.on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
|
||||
let entries = vec![vec![
|
||||
MenuListEntry::new("Polygon")
|
||||
.label("Polygon")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Polygon)).into()),
|
||||
MenuListEntry::new("Star")
|
||||
.label("Star")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()),
|
||||
]];
|
||||
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder()
|
||||
}
|
||||
|
||||
fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||
NumberInput::new(Some(line_weight))
|
||||
.unit(" px")
|
||||
.label("Weight")
|
||||
.min(0.)
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::LineWeight(number_input.value.unwrap())).into())
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
impl LayoutHolder for ShapeTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = vec![];
|
||||
|
||||
if !self.tool_data.hide_shape_option_widget {
|
||||
widgets.push(create_shape_option_widget(self.options.shape_type));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
|
||||
if self.options.shape_type == ShapeType::Polygon || self.options.shape_type == ShapeType::Star {
|
||||
widgets.push(create_sides_widget(self.options.vertices));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
}
|
||||
}
|
||||
|
||||
if self.options.shape_type != ShapeType::Line {
|
||||
widgets.append(&mut self.options.fill.create_widgets(
|
||||
"Fill",
|
||||
true,
|
||||
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
));
|
||||
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
}
|
||||
|
||||
widgets.append(&mut self.options.stroke.create_widgets(
|
||||
"Stroke",
|
||||
true,
|
||||
|_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(),
|
||||
|color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|
||||
|color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
|
||||
));
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(create_weight_widget(self.options.line_weight));
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for ShapeTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match action {
|
||||
ShapeOptionsUpdate::FillColor(color) => {
|
||||
self.options.fill.custom_color = color;
|
||||
self.options.fill.color_type = ToolColorType::Custom;
|
||||
}
|
||||
ShapeOptionsUpdate::FillColorType(color_type) => {
|
||||
self.options.fill.color_type = color_type;
|
||||
}
|
||||
ShapeOptionsUpdate::LineWeight(line_weight) => {
|
||||
self.options.line_weight = line_weight;
|
||||
}
|
||||
ShapeOptionsUpdate::StrokeColor(color) => {
|
||||
self.options.stroke.custom_color = color;
|
||||
self.options.stroke.color_type = ToolColorType::Custom;
|
||||
}
|
||||
ShapeOptionsUpdate::StrokeColorType(color_type) => {
|
||||
self.options.stroke.color_type = color_type;
|
||||
}
|
||||
ShapeOptionsUpdate::WorkingColors(primary, secondary) => {
|
||||
self.options.stroke.primary_working_color = primary;
|
||||
self.options.stroke.secondary_working_color = secondary;
|
||||
self.options.fill.primary_working_color = primary;
|
||||
self.options.fill.secondary_working_color = secondary;
|
||||
}
|
||||
ShapeOptionsUpdate::ShapeType(shape) => {
|
||||
self.options.shape_type = shape;
|
||||
self.tool_data.current_shape = shape;
|
||||
}
|
||||
ShapeOptionsUpdate::Vertices(vertices) => {
|
||||
self.options.vertices = vertices;
|
||||
}
|
||||
ShapeOptionsUpdate::ArcType(arc_type) => {
|
||||
self.options.arc_type = arc_type;
|
||||
}
|
||||
}
|
||||
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
ShapeToolFsmState::Ready(_) => actions!(ShapeToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
SetShape,
|
||||
Abort,
|
||||
HideShapeTypeWidget,
|
||||
IncreaseSides,
|
||||
DecreaseSides,
|
||||
NudgeSelectedLayers,
|
||||
),
|
||||
ShapeToolFsmState::Drawing(_)
|
||||
| ShapeToolFsmState::ResizingBounds
|
||||
| ShapeToolFsmState::DraggingLineEndpoints
|
||||
| ShapeToolFsmState::RotatingBounds
|
||||
| ShapeToolFsmState::ModifyingGizmo
|
||||
| ShapeToolFsmState::SkewingBounds { .. } => {
|
||||
actions!(ShapeToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
PointerMove,
|
||||
SetShape,
|
||||
HideShapeTypeWidget,
|
||||
IncreaseSides,
|
||||
DecreaseSides,
|
||||
NudgeSelectedLayers,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolMetadata for ShapeTool {
|
||||
fn icon_name(&self) -> String {
|
||||
"VectorPolygonTool".into()
|
||||
}
|
||||
fn tooltip(&self) -> String {
|
||||
"Shape Tool".into()
|
||||
}
|
||||
fn tool_type(&self) -> ToolType {
|
||||
ToolType::Shape
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolTransition for ShapeTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
overlay_provider: Some(|overlay_context| ShapeToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(ShapeToolMessage::Abort.into()),
|
||||
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ShapeToolFsmState {
|
||||
Ready(ShapeType),
|
||||
Drawing(ShapeType),
|
||||
|
||||
// Gizmos
|
||||
DraggingLineEndpoints,
|
||||
ModifyingGizmo,
|
||||
|
||||
// Transform cage
|
||||
ResizingBounds,
|
||||
RotatingBounds,
|
||||
SkewingBounds { skew: Key },
|
||||
}
|
||||
|
||||
impl Default for ShapeToolFsmState {
|
||||
fn default() -> Self {
|
||||
ShapeToolFsmState::Ready(ShapeType::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ShapeToolData {
|
||||
pub data: Resize,
|
||||
auto_panning: AutoPanning,
|
||||
|
||||
// In viewport space
|
||||
pub last_mouse_position: DVec2,
|
||||
|
||||
// Hide the dropdown menu when using Line, Rectangle, or Ellipse aliases
|
||||
pub hide_shape_option_widget: bool,
|
||||
|
||||
// Shape-specific data
|
||||
pub line_data: LineToolData,
|
||||
|
||||
// Used for by transform cage
|
||||
pub bounding_box_manager: Option<BoundingBoxManager>,
|
||||
layers_dragging: Vec<LayerNodeIdentifier>,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
skew_edge: EdgeBool,
|
||||
cursor: MouseCursorIcon,
|
||||
|
||||
// Current shape which is being drawn
|
||||
current_shape: ShapeType,
|
||||
|
||||
// Gizmos
|
||||
gizmo_manger: GizmoManager,
|
||||
}
|
||||
|
||||
impl ShapeToolData {
|
||||
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
self.snap_candidates.clear();
|
||||
for &layer in &self.layers_dragging {
|
||||
if (self.snap_candidates.len() as f64) < document.snapping_state.tolerance {
|
||||
snapping::get_layer_snap_points(layer, &SnapData::new(document, input), &mut self.snap_candidates);
|
||||
}
|
||||
if let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) {
|
||||
let quad = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
|
||||
snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for ShapeToolFsmState {
|
||||
type ToolData = ShapeToolData;
|
||||
type ToolOptions = ShapeToolOptions;
|
||||
|
||||
fn transition(
|
||||
self,
|
||||
event: ToolMessage,
|
||||
tool_data: &mut Self::ToolData,
|
||||
ToolActionHandlerData {
|
||||
document,
|
||||
global_tool_data,
|
||||
input,
|
||||
preferences,
|
||||
shape_editor,
|
||||
..
|
||||
}: &mut ToolActionHandlerData,
|
||||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
let all_selected_layers_line = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.all(|layer| graph_modification_utils::get_line_id(layer, &document.network_interface).is_some());
|
||||
|
||||
let ToolMessage::Shape(event) = event else { return self };
|
||||
|
||||
match (self, event) {
|
||||
(_, ShapeToolMessage::Overlays(mut overlay_context)) => {
|
||||
let mouse_position = tool_data
|
||||
.data
|
||||
.snap_manager
|
||||
.indicator_pos()
|
||||
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
|
||||
.unwrap_or(input.mouse.position);
|
||||
let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds);
|
||||
|
||||
if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) {
|
||||
tool_data.gizmo_manger.handle_actions(mouse_position, document, responses);
|
||||
tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
}
|
||||
|
||||
if matches!(self, ShapeToolFsmState::ModifyingGizmo) && !input.keyboard.key(Key::Control) {
|
||||
tool_data.gizmo_manger.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
}
|
||||
|
||||
let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. });
|
||||
let hovering_over_gizmo = tool_data.gizmo_manger.hovering_over_gizmo();
|
||||
|
||||
if !is_resizing_or_rotating && !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
|
||||
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
}
|
||||
|
||||
if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) {
|
||||
transform_cage_overlays(document, tool_data, &mut overlay_context);
|
||||
}
|
||||
|
||||
if input.keyboard.key(Key::Control) && matches!(self, ShapeToolFsmState::Ready(_)) {
|
||||
anchor_overlays(document, &mut overlay_context);
|
||||
} else if matches!(self, ShapeToolFsmState::Ready(_)) {
|
||||
Line::overlays(document, tool_data, &mut overlay_context);
|
||||
|
||||
if all_selected_layers_line {
|
||||
return self;
|
||||
}
|
||||
|
||||
if !hovering_over_gizmo {
|
||||
transform_cage_overlays(document, tool_data, &mut overlay_context);
|
||||
}
|
||||
|
||||
let dragging_bounds = tool_data
|
||||
.bounding_box_manager
|
||||
.as_mut()
|
||||
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
|
||||
.is_some();
|
||||
|
||||
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, ShapeToolFsmState::SkewingBounds { .. });
|
||||
let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge));
|
||||
if is_skewing || (dragging_bounds && is_near_square && !is_resizing_or_rotating && !hovering_over_gizmo) {
|
||||
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
|
||||
}
|
||||
if !is_skewing && dragging_bounds && !hovering_over_gizmo {
|
||||
if let Some(edges) = edges {
|
||||
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
|
||||
Line::overlays(document, tool_data, &mut overlay_context);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1)));
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3))));
|
||||
self
|
||||
}
|
||||
(
|
||||
ShapeToolFsmState::Ready(_),
|
||||
ShapeToolMessage::NudgeSelectedLayers {
|
||||
delta_x,
|
||||
delta_y,
|
||||
resize,
|
||||
resize_opposite_corner,
|
||||
},
|
||||
) => {
|
||||
responses.add(DocumentMessage::NudgeSelectedLayers {
|
||||
delta_x,
|
||||
delta_y,
|
||||
resize,
|
||||
resize_opposite_corner,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::NudgeSelectedLayers { .. }) => {
|
||||
let increase = input.keyboard.key(Key::ArrowUp);
|
||||
let decrease = input.keyboard.key(Key::ArrowDown);
|
||||
|
||||
if increase {
|
||||
responses.add(ShapeToolMessage::IncreaseSides);
|
||||
return self;
|
||||
}
|
||||
|
||||
if decrease {
|
||||
responses.add(ShapeToolMessage::DecreaseSides);
|
||||
return self;
|
||||
}
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => {
|
||||
if let Some(layer) = tool_data.data.layer {
|
||||
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface))
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface)
|
||||
.find_node_inputs("Regular Polygon")
|
||||
.or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star"))
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1)));
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::U32(n + 1), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => {
|
||||
if let Some(layer) = tool_data.data.layer {
|
||||
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface))
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface)
|
||||
.find_node_inputs("Regular Polygon")
|
||||
.or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star"))
|
||||
else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else {
|
||||
return self;
|
||||
};
|
||||
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3))));
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::U32((n - 1).max(3)), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => {
|
||||
tool_data.line_data.drag_start = input.mouse.position;
|
||||
|
||||
// Snapped position in viewport space
|
||||
let mouse_pos = tool_data
|
||||
.data
|
||||
.snap_manager
|
||||
.indicator_pos()
|
||||
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
|
||||
.unwrap_or(input.mouse.position);
|
||||
|
||||
tool_data.line_data.drag_current = mouse_pos;
|
||||
|
||||
if tool_data.gizmo_manger.handle_click() {
|
||||
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
|
||||
return ShapeToolFsmState::ModifyingGizmo;
|
||||
}
|
||||
|
||||
// If clicked on endpoints of a selected line, drag its endpoints
|
||||
if let Some((layer, _, _)) = closest_point(
|
||||
document,
|
||||
mouse_pos,
|
||||
SNAP_POINT_TOLERANCE,
|
||||
document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface),
|
||||
|_| false,
|
||||
preferences,
|
||||
) {
|
||||
if clicked_on_line_endpoints(layer, document, input, tool_data) && !input.keyboard.key(Key::Control) {
|
||||
return ShapeToolFsmState::DraggingLineEndpoints;
|
||||
}
|
||||
}
|
||||
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging);
|
||||
|
||||
if !input.keyboard.key(Key::Control) {
|
||||
match (resize, rotate, skew) {
|
||||
(true, false, false) => {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
return ShapeToolFsmState::ResizingBounds;
|
||||
}
|
||||
(false, true, false) => {
|
||||
tool_data.data.drag_start = mouse_pos;
|
||||
return ShapeToolFsmState::RotatingBounds;
|
||||
}
|
||||
(false, false, true) => {
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
return ShapeToolFsmState::SkewingBounds { skew: Key::Control };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input),
|
||||
ShapeType::Line => {
|
||||
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
|
||||
tool_data.data.drag_start = snapped.snapped_point_document;
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let node = match tool_data.current_shape {
|
||||
ShapeType::Polygon => Polygon::create_node(tool_options.vertices),
|
||||
ShapeType::Star => Star::create_node(tool_options.vertices),
|
||||
ShapeType::Rectangle => Rectangle::create_node(),
|
||||
ShapeType::Ellipse => Ellipse::create_node(),
|
||||
ShapeType::Line => Line::create_node(document, tool_data.data.drag_start),
|
||||
};
|
||||
|
||||
let nodes = vec![(NodeId(0), node)];
|
||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input), responses);
|
||||
|
||||
responses.add(Message::StartBuffer);
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => {
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
|
||||
tool_options.fill.apply_fill(layer, responses);
|
||||
}
|
||||
ShapeType::Line => {
|
||||
tool_data.line_data.weight = tool_options.line_weight;
|
||||
tool_data.line_data.editing_layer = Some(layer);
|
||||
}
|
||||
}
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
|
||||
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses);
|
||||
tool_data.data.layer = Some(layer);
|
||||
|
||||
ShapeToolFsmState::Drawing(tool_data.current_shape)
|
||||
}
|
||||
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove(modifier)) => {
|
||||
let Some(layer) = tool_data.data.layer else {
|
||||
return ShapeToolFsmState::Ready(shape);
|
||||
};
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
}
|
||||
|
||||
// Auto-panning
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove(modifier)) => {
|
||||
let Some(layer) = tool_data.line_data.editing_layer else {
|
||||
return ShapeToolFsmState::Ready(tool_data.current_shape);
|
||||
};
|
||||
|
||||
Line::update_shape(document, input, layer, tool_data, modifier, responses);
|
||||
// Auto-panning
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
ShapeToolFsmState::ModifyingGizmo
|
||||
}
|
||||
(ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove(modifier)) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
let messages = [ShapeToolMessage::PointerOutsideViewport(modifier).into(), ShapeToolMessage::PointerMove(modifier).into()];
|
||||
resize_bounds(
|
||||
document,
|
||||
responses,
|
||||
bounds,
|
||||
&mut tool_data.layers_dragging,
|
||||
&mut tool_data.data.snap_manager,
|
||||
&mut tool_data.snap_candidates,
|
||||
input,
|
||||
input.keyboard.key(modifier[0]),
|
||||
input.keyboard.key(modifier[1]),
|
||||
ToolType::Shape,
|
||||
);
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
ShapeToolFsmState::ResizingBounds
|
||||
}
|
||||
(ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove(modifier)) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
rotate_bounds(
|
||||
document,
|
||||
responses,
|
||||
bounds,
|
||||
&mut tool_data.layers_dragging,
|
||||
tool_data.data.drag_start,
|
||||
input.mouse.position,
|
||||
input.keyboard.key(modifier[1]),
|
||||
ToolType::Shape,
|
||||
);
|
||||
}
|
||||
|
||||
ShapeToolFsmState::RotatingBounds
|
||||
}
|
||||
(ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove(_)) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
skew_bounds(
|
||||
document,
|
||||
responses,
|
||||
bounds,
|
||||
input.keyboard.key(skew),
|
||||
&mut tool_data.layers_dragging,
|
||||
input.mouse.position,
|
||||
ToolType::Shape,
|
||||
);
|
||||
}
|
||||
|
||||
ShapeToolFsmState::SkewingBounds { skew }
|
||||
}
|
||||
|
||||
(_, ShapeToolMessage::PointerMove(_)) => {
|
||||
let dragging_bounds = tool_data
|
||||
.bounding_box_manager
|
||||
.as_mut()
|
||||
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
|
||||
.is_some();
|
||||
|
||||
let cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Crosshair, |bounds| {
|
||||
let cursor = bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge));
|
||||
if cursor == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor }
|
||||
});
|
||||
|
||||
if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && !all_selected_layers_line {
|
||||
tool_data.cursor = cursor;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
||||
}
|
||||
|
||||
tool_data.data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport(_)) => {
|
||||
// Auto-panning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.center_of_transformation += shift;
|
||||
bounds.original_bound_transform.translation += shift;
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport(..)) => self,
|
||||
(_, ShapeToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
self
|
||||
}
|
||||
(
|
||||
ShapeToolFsmState::Drawing(_)
|
||||
| ShapeToolFsmState::DraggingLineEndpoints
|
||||
| ShapeToolFsmState::ResizingBounds
|
||||
| ShapeToolFsmState::RotatingBounds
|
||||
| ShapeToolFsmState::SkewingBounds { .. }
|
||||
| ShapeToolFsmState::ModifyingGizmo,
|
||||
ShapeToolMessage::DragStop,
|
||||
) => {
|
||||
input.mouse.finish_transaction(tool_data.data.drag_start, responses);
|
||||
tool_data.data.cleanup(responses);
|
||||
|
||||
tool_data.gizmo_manger.handle_cleanup();
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
tool_data.line_data.dragging_endpoint = None;
|
||||
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
|
||||
ShapeToolFsmState::Ready(tool_data.current_shape)
|
||||
}
|
||||
(
|
||||
ShapeToolFsmState::Drawing(_)
|
||||
| ShapeToolFsmState::DraggingLineEndpoints
|
||||
| ShapeToolFsmState::ResizingBounds
|
||||
| ShapeToolFsmState::RotatingBounds
|
||||
| ShapeToolFsmState::SkewingBounds { .. }
|
||||
| ShapeToolFsmState::ModifyingGizmo,
|
||||
ShapeToolMessage::Abort,
|
||||
) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.data.cleanup(responses);
|
||||
tool_data.line_data.dragging_endpoint = None;
|
||||
|
||||
tool_data.gizmo_manger.handle_cleanup();
|
||||
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
|
||||
ShapeToolFsmState::Ready(tool_data.current_shape)
|
||||
}
|
||||
(_, ShapeToolMessage::WorkingColorChanged) => {
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
)));
|
||||
self
|
||||
}
|
||||
(_, ShapeToolMessage::SetShape(shape)) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.data.cleanup(responses);
|
||||
tool_data.current_shape = shape;
|
||||
|
||||
ShapeToolFsmState::Ready(shape)
|
||||
}
|
||||
(_, ShapeToolMessage::HideShapeTypeWidget(hide)) => {
|
||||
tool_data.hide_shape_option_widget = hide;
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
ShapeToolFsmState::Ready(shape) => {
|
||||
let hint_groups = match shape {
|
||||
ShapeType::Polygon | ShapeType::Star => vec![
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]),
|
||||
],
|
||||
ShapeType::Ellipse => vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])],
|
||||
ShapeType::Line => vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"),
|
||||
HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(),
|
||||
])],
|
||||
ShapeType::Rectangle => vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])],
|
||||
};
|
||||
HintData(hint_groups)
|
||||
}
|
||||
ShapeToolFsmState::Drawing(shape) => {
|
||||
let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])];
|
||||
let tool_hint_group = match shape {
|
||||
ShapeType::Polygon | ShapeType::Star => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
ShapeType::Line => HintGroup(vec![
|
||||
HintInfo::keys([Key::Shift], "15° Increments"),
|
||||
HintInfo::keys([Key::Alt], "From Center"),
|
||||
HintInfo::keys([Key::Control], "Lock Angle"),
|
||||
]),
|
||||
};
|
||||
|
||||
common_hint_group.push(tool_hint_group);
|
||||
|
||||
if matches!(shape, ShapeType::Polygon | ShapeType::Star) {
|
||||
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]));
|
||||
}
|
||||
|
||||
HintData(common_hint_group)
|
||||
}
|
||||
ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![
|
||||
HintInfo::keys([Key::Shift], "15° Increments"),
|
||||
HintInfo::keys([Key::Alt], "From Center"),
|
||||
HintInfo::keys([Key::Control], "Lock Angle"),
|
||||
]),
|
||||
]),
|
||||
ShapeToolFsmState::ResizingBounds => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]),
|
||||
]),
|
||||
ShapeToolFsmState::RotatingBounds => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]),
|
||||
]),
|
||||
ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]),
|
||||
]),
|
||||
ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
@@ -656,15 +656,15 @@ mod test_spline_tool {
|
||||
editor.new_document().await;
|
||||
|
||||
// Zooming the viewport
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2.0 }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
|
||||
|
||||
// Selecting the spline tool
|
||||
editor.select_tool(ToolType::Spline).await;
|
||||
|
||||
// Adding points by clicking at different positions
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
|
||||
|
||||
// Finish the spline
|
||||
editor.handle_message(SplineToolMessage::Confirm).await;
|
||||
@@ -686,7 +686,7 @@ mod test_spline_tool {
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Expected points in viewport coordinates
|
||||
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)];
|
||||
let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
|
||||
|
||||
// Assert all points are correctly positioned
|
||||
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
|
||||
@@ -697,15 +697,15 @@ mod test_spline_tool {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
|
||||
let pan_amount = DVec2::new(200.0, 150.0);
|
||||
let pan_amount = DVec2::new(200., 150.);
|
||||
editor.handle_message(NavigationMessage::CanvasPan { delta: pan_amount }).await;
|
||||
|
||||
editor.select_tool(ToolType::Spline).await;
|
||||
|
||||
// Add points by clicking at different positions
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
|
||||
|
||||
editor.handle_message(SplineToolMessage::Confirm).await;
|
||||
|
||||
@@ -726,7 +726,7 @@ mod test_spline_tool {
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Expected points in viewport coordinates
|
||||
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)];
|
||||
let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
|
||||
|
||||
// Assert all points are correctly positioned
|
||||
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
|
||||
@@ -738,12 +738,12 @@ mod test_spline_tool {
|
||||
editor.new_document().await;
|
||||
|
||||
// Tilt/rotate the viewport (45 degrees)
|
||||
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 45.0_f64.to_radians() }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 45_f64.to_radians() }).await;
|
||||
editor.select_tool(ToolType::Spline).await;
|
||||
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
|
||||
|
||||
editor.handle_message(SplineToolMessage::Confirm).await;
|
||||
|
||||
@@ -764,7 +764,7 @@ mod test_spline_tool {
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Expected points in viewport coordinates
|
||||
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)];
|
||||
let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
|
||||
|
||||
// Assert all points are correctly positioned
|
||||
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
|
||||
@@ -777,14 +777,14 @@ mod test_spline_tool {
|
||||
|
||||
// Applying multiple transformations
|
||||
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 1.5 }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100.0, 75.0) }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 30.0_f64.to_radians() }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 75.) }).await;
|
||||
editor.handle_message(NavigationMessage::CanvasTiltSet { angle_radians: 30_f64.to_radians() }).await;
|
||||
|
||||
editor.select_tool(ToolType::Spline).await;
|
||||
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100.0, 50.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150.0, 100.0), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(50., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(100., 50.), ModifierKeys::empty()).await;
|
||||
editor.click_tool(ToolType::Spline, MouseKeys::LEFT, DVec2::new(150., 100.), ModifierKeys::empty()).await;
|
||||
|
||||
editor.handle_message(SplineToolMessage::Confirm).await;
|
||||
if let Err(e) = editor.eval_graph().await {
|
||||
@@ -803,7 +803,7 @@ mod test_spline_tool {
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Expected points in viewport coordinates
|
||||
let expected_points = vec![DVec2::new(50.0, 50.0), DVec2::new(100.0, 50.0), DVec2::new(150.0, 100.0)];
|
||||
let expected_points = vec![DVec2::new(50., 50.), DVec2::new(100., 50.), DVec2::new(150., 100.)];
|
||||
|
||||
// Assert all points are correctly positioned
|
||||
assert_point_positions(&vector_data, layer_to_viewport, &expected_points, 1e-10);
|
||||
|
||||
@@ -139,6 +139,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
let using_path_tool = tool_data.active_tool_type == ToolType::Path;
|
||||
let using_select_tool = tool_data.active_tool_type == ToolType::Select;
|
||||
let using_pen_tool = tool_data.active_tool_type == ToolType::Pen;
|
||||
let using_shape_tool = tool_data.active_tool_type == ToolType::Shape;
|
||||
|
||||
// TODO: Add support for transforming layer not in the document network
|
||||
let selected_layers = document
|
||||
@@ -179,14 +180,28 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
*selected.pivot = selected.mean_average_of_pivots();
|
||||
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
|
||||
self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots());
|
||||
} else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
|
||||
}
|
||||
// Here vector data from all layers is not considered which can be a problem in pivot calculation
|
||||
else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
|
||||
*selected.original_transforms = OriginalTransforms::default();
|
||||
|
||||
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
|
||||
let selected_points = shape_editor.selected_points().collect::<Vec<_>>();
|
||||
|
||||
let selected_segments = shape_editor.selected_segments().collect::<HashSet<_>>();
|
||||
|
||||
let mut affected_points = shape_editor.selected_points().copied().collect::<Vec<_>>();
|
||||
|
||||
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
if selected_segments.contains(&segment_id) {
|
||||
affected_points.push(ManipulatorPointId::Anchor(start));
|
||||
affected_points.push(ManipulatorPointId::Anchor(end));
|
||||
}
|
||||
}
|
||||
|
||||
let affected_point_refs = affected_points.iter().collect();
|
||||
|
||||
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
|
||||
if let Some((new_pivot, grab_target)) = calculate_pivot(&selected_points, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
|
||||
if let Some((new_pivot, grab_target)) = calculate_pivot(&affected_point_refs, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
|
||||
*selected.pivot = new_pivot;
|
||||
|
||||
self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
|
||||
@@ -389,8 +404,9 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
}
|
||||
TransformLayerMessage::BeginGRS { transform_type } => {
|
||||
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
|
||||
if (using_path_tool && selected_points.is_empty())
|
||||
|| (!using_path_tool && !using_select_tool && !using_pen_tool)
|
||||
let selected_segments = shape_editor.selected_segments().collect::<Vec<_>>();
|
||||
if (using_path_tool && selected_points.is_empty() && selected_segments.is_empty())
|
||||
|| (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool)
|
||||
|| selected_layers.is_empty()
|
||||
|| transform_type.equivalent_to(self.transform_operation)
|
||||
{
|
||||
@@ -715,7 +731,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_transform_layer {
|
||||
use crate::messages::portfolio::document::graph_operation::{transform_utils, utility_types::ModifyInputsContext};
|
||||
use crate::messages::portfolio::document::graph_operation::transform_utils;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::prelude::Message;
|
||||
use crate::messages::tool::transform_layer::transform_layer_message_handler::VectorModificationType;
|
||||
@@ -1019,8 +1036,8 @@ mod test_transform_layer {
|
||||
let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length();
|
||||
let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length();
|
||||
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2.0, got: {}", scale_x);
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2.0, got: {}", scale_y);
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {}", scale_x);
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {}", scale_y);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1045,8 +1062,8 @@ mod test_transform_layer {
|
||||
let scale_x = final_transform.matrix2.x_axis.length() / original_transform.matrix2.x_axis.length();
|
||||
let scale_y = final_transform.matrix2.y_axis.length() / original_transform.matrix2.y_axis.length();
|
||||
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2.0, got: {}", scale_x);
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2.0, got: {}", scale_y);
|
||||
assert!((scale_x - 2.).abs() < 0.1, "Expected scale factor X of 2, got: {}", scale_x);
|
||||
assert!((scale_y - 2.).abs() < 0.1, "Expected scale factor Y of 2, got: {}", scale_y);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayProvider;
|
||||
use crate::messages::preferences::PreferencesMessageHandler;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::text::FontCache;
|
||||
@@ -162,7 +163,7 @@ pub trait ToolTransition {
|
||||
on: event,
|
||||
send: Box::new(mapping.into()),
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let event_to_tool_map = self.event_to_message_map();
|
||||
@@ -182,7 +183,7 @@ pub trait ToolTransition {
|
||||
on: event,
|
||||
message: Box::new(mapping.into()),
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let event_to_tool_map = self.event_to_message_map();
|
||||
@@ -204,6 +205,7 @@ pub trait ToolMetadata {
|
||||
|
||||
pub struct ToolData {
|
||||
pub active_tool_type: ToolType,
|
||||
pub active_shape_type: Option<ToolType>,
|
||||
pub tools: HashMap<ToolType, Box<Tool>>,
|
||||
}
|
||||
|
||||
@@ -225,32 +227,51 @@ impl ToolData {
|
||||
|
||||
impl LayoutHolder for ToolData {
|
||||
fn layout(&self) -> Layout {
|
||||
let active_tool = self.active_shape_type.unwrap_or(self.active_tool_type);
|
||||
|
||||
let tool_groups_layout = list_tools_in_groups()
|
||||
.iter()
|
||||
.map(|tool_group| tool_group.iter().map(|tool_availability| {
|
||||
match tool_availability {
|
||||
ToolAvailability::Available(tool) => ToolEntry::new(tool.tool_type(), tool.icon_name())
|
||||
.tooltip(tool.tooltip())
|
||||
.tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(tool.tool_type()))),
|
||||
ToolAvailability::ComingSoon(tool) => tool.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
.map(|tool_group|
|
||||
tool_group
|
||||
.iter()
|
||||
.map(|tool_availability| {
|
||||
match tool_availability {
|
||||
ToolAvailability::Available(tool) =>
|
||||
ToolEntry::new(tool.tool_type(), tool.icon_name())
|
||||
.tooltip(tool.tooltip())
|
||||
.tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(tool.tool_type()))),
|
||||
ToolAvailability::AvailableAsShape(shape) =>
|
||||
ToolEntry::new(shape.tool_type(), shape.icon_name())
|
||||
.tooltip(shape.tooltip())
|
||||
.tooltip_shortcut(action_keys!(tool_type_to_activate_tool_message(shape.tool_type()))),
|
||||
ToolAvailability::ComingSoon(tool) => tool.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.flat_map(|group| {
|
||||
let separator = std::iter::once(Separator::new(SeparatorType::Section).direction(SeparatorDirection::Vertical).widget_holder());
|
||||
let buttons = group.into_iter().map(|ToolEntry { tooltip, tooltip_shortcut, tool_type, icon_name }| {
|
||||
IconButton::new(icon_name, 32)
|
||||
.disabled(false)
|
||||
.active(self.active_tool_type == tool_type)
|
||||
.active(match tool_type {
|
||||
ToolType::Line | ToolType::Ellipse | ToolType::Rectangle => { self.active_shape_type.is_some() && active_tool == tool_type }
|
||||
_ => active_tool == tool_type,
|
||||
})
|
||||
.tooltip(tooltip.clone())
|
||||
.tooltip_shortcut(tooltip_shortcut)
|
||||
.on_update(move |_| {
|
||||
if !tooltip.contains("Coming Soon") {
|
||||
ToolMessage::ActivateTool { tool_type }.into()
|
||||
} else {
|
||||
DialogMessage::RequestComingSoonDialog { issue: None }.into()
|
||||
match tool_type {
|
||||
ToolType::Line => ToolMessage::ActivateToolShapeLine.into(),
|
||||
ToolType::Rectangle => ToolMessage::ActivateToolShapeRectangle.into(),
|
||||
ToolType::Ellipse => ToolMessage::ActivateToolShapeEllipse.into(),
|
||||
ToolType::Shape => ToolMessage::ActivateToolShape.into(),
|
||||
_ => {
|
||||
if !tooltip.contains("Coming Soon") { (ToolMessage::ActivateTool { tool_type }).into() } else { (DialogMessage::RequestComingSoonDialog { issue: None }).into() }
|
||||
}
|
||||
}
|
||||
}).widget_holder()
|
||||
})
|
||||
.widget_holder()
|
||||
});
|
||||
|
||||
separator.chain(buttons)
|
||||
@@ -287,11 +308,13 @@ impl Default for ToolFsmState {
|
||||
Self {
|
||||
tool_data: ToolData {
|
||||
active_tool_type: ToolType::Select,
|
||||
active_shape_type: None,
|
||||
tools: list_tools_in_groups()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|tool| match tool {
|
||||
ToolAvailability::Available(tool) => Some((tool.tool_type(), tool)),
|
||||
ToolAvailability::AvailableAsShape(_) => None,
|
||||
ToolAvailability::ComingSoon(_) => None,
|
||||
})
|
||||
.collect(),
|
||||
@@ -327,10 +350,10 @@ pub enum ToolType {
|
||||
Pen,
|
||||
Freehand,
|
||||
Spline,
|
||||
Line,
|
||||
Rectangle,
|
||||
Ellipse,
|
||||
Polygon,
|
||||
Shape,
|
||||
Line, // Shape tool alias
|
||||
Rectangle, // Shape tool alias
|
||||
Ellipse, // Shape tool alias
|
||||
Text,
|
||||
|
||||
// Raster tool group
|
||||
@@ -340,12 +363,25 @@ pub enum ToolType {
|
||||
Patch,
|
||||
Detail,
|
||||
Relight,
|
||||
Imaginate,
|
||||
Frame,
|
||||
}
|
||||
|
||||
impl ToolType {
|
||||
pub fn get_shape(&self) -> Option<Self> {
|
||||
match self {
|
||||
Self::Rectangle | Self::Line | Self::Ellipse => Some(*self),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tool(self) -> Self {
|
||||
if self.get_shape().is_some() { ToolType::Shape } else { self }
|
||||
}
|
||||
}
|
||||
|
||||
enum ToolAvailability {
|
||||
Available(Box<Tool>),
|
||||
AvailableAsShape(ShapeType),
|
||||
ComingSoon(ToolEntry),
|
||||
}
|
||||
|
||||
@@ -367,10 +403,10 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
|
||||
ToolAvailability::Available(Box::<pen_tool::PenTool>::default()),
|
||||
ToolAvailability::Available(Box::<freehand_tool::FreehandTool>::default()),
|
||||
ToolAvailability::Available(Box::<spline_tool::SplineTool>::default()),
|
||||
ToolAvailability::Available(Box::<line_tool::LineTool>::default()),
|
||||
ToolAvailability::Available(Box::<rectangle_tool::RectangleTool>::default()),
|
||||
ToolAvailability::Available(Box::<ellipse_tool::EllipseTool>::default()),
|
||||
ToolAvailability::Available(Box::<polygon_tool::PolygonTool>::default()),
|
||||
ToolAvailability::AvailableAsShape(ShapeType::Line),
|
||||
ToolAvailability::AvailableAsShape(ShapeType::Rectangle),
|
||||
ToolAvailability::AvailableAsShape(ShapeType::Ellipse),
|
||||
ToolAvailability::Available(Box::<shape_tool::ShapeTool>::default()),
|
||||
ToolAvailability::Available(Box::<text_tool::TextTool>::default()),
|
||||
],
|
||||
vec![
|
||||
@@ -381,9 +417,6 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
|
||||
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Patch, "RasterPatchTool").tooltip("Coming Soon: Patch Tool")),
|
||||
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Detail, "RasterDetailTool").tooltip("Coming Soon: Detail Tool (D)")),
|
||||
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Relight, "RasterRelightTool").tooltip("Coming Soon: Relight Tool (O)")),
|
||||
// TODO: Fix and reenable Imaginate tool
|
||||
// ToolAvailability::Available(Box::<imaginate_tool::ImaginateTool>::default()),
|
||||
ToolAvailability::ComingSoon(ToolEntry::new(ToolType::Heal, "RasterImaginateTool").tooltip("Coming Soon: Imaginate Tool")),
|
||||
],
|
||||
]
|
||||
}
|
||||
@@ -403,10 +436,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
|
||||
ToolMessage::Pen(_) => ToolType::Pen,
|
||||
ToolMessage::Freehand(_) => ToolType::Freehand,
|
||||
ToolMessage::Spline(_) => ToolType::Spline,
|
||||
ToolMessage::Line(_) => ToolType::Line,
|
||||
ToolMessage::Rectangle(_) => ToolType::Rectangle,
|
||||
ToolMessage::Ellipse(_) => ToolType::Ellipse,
|
||||
ToolMessage::Polygon(_) => ToolType::Polygon,
|
||||
ToolMessage::Shape(_) => ToolType::Shape, // Includes the Line, Rectangle, and Ellipse aliases
|
||||
ToolMessage::Text(_) => ToolType::Text,
|
||||
|
||||
// Raster tool group
|
||||
@@ -416,7 +446,6 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
|
||||
// ToolMessage::Patch(_) => ToolType::Patch,
|
||||
// ToolMessage::Detail(_) => ToolType::Detail,
|
||||
// ToolMessage::Relight(_) => ToolType::Relight,
|
||||
// ToolMessage::Imaginate(_) => ToolType::Imaginate,
|
||||
_ => panic!("Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {tool_message:?}"),
|
||||
}
|
||||
}
|
||||
@@ -436,10 +465,10 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
|
||||
ToolType::Pen => ToolMessageDiscriminant::ActivateToolPen,
|
||||
ToolType::Freehand => ToolMessageDiscriminant::ActivateToolFreehand,
|
||||
ToolType::Spline => ToolMessageDiscriminant::ActivateToolSpline,
|
||||
ToolType::Line => ToolMessageDiscriminant::ActivateToolLine,
|
||||
ToolType::Rectangle => ToolMessageDiscriminant::ActivateToolRectangle,
|
||||
ToolType::Ellipse => ToolMessageDiscriminant::ActivateToolEllipse,
|
||||
ToolType::Polygon => ToolMessageDiscriminant::ActivateToolPolygon,
|
||||
ToolType::Line => ToolMessageDiscriminant::ActivateToolShapeLine, // Shape tool alias
|
||||
ToolType::Rectangle => ToolMessageDiscriminant::ActivateToolShapeRectangle, // Shape tool alias
|
||||
ToolType::Ellipse => ToolMessageDiscriminant::ActivateToolShapeEllipse, // Shape tool alias
|
||||
ToolType::Shape => ToolMessageDiscriminant::ActivateToolShape,
|
||||
ToolType::Text => ToolMessageDiscriminant::ActivateToolText,
|
||||
|
||||
// Raster tool group
|
||||
@@ -449,7 +478,6 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
|
||||
// ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch,
|
||||
// ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail,
|
||||
// ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight,
|
||||
// ToolType::Imaginate => ToolMessageDiscriminant::ActivateToolImaginate,
|
||||
_ => panic!("Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {tool_type:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,9 +299,6 @@ impl NodeGraphExecutor {
|
||||
}
|
||||
}
|
||||
}
|
||||
// NodeGraphUpdate::NodeGraphUpdateMessage(NodeGraphUpdateMessage::ImaginateStatusUpdate) => {
|
||||
// responses.add(DocumentMessage::PropertiesPanel(PropertiesPanelMessage::Refresh));
|
||||
// }
|
||||
NodeGraphUpdate::CompilationResponse(execution_response) => {
|
||||
let CompilationResponse { node_graph_errors, result } = execution_response;
|
||||
let type_delta = match result {
|
||||
|
||||
@@ -45,6 +45,9 @@ pub struct NodeRuntime {
|
||||
/// Which node is inspected and which monitor node is used (if any) for the current execution
|
||||
inspect_state: Option<InspectState>,
|
||||
|
||||
/// Mapping of the fully-qualified node paths to their preprocessor substitutions.
|
||||
substitutions: HashMap<String, DocumentNode>,
|
||||
|
||||
// TODO: Remove, it doesn't need to be persisted anymore
|
||||
/// The current renders of the thumbnails for layer nodes.
|
||||
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
|
||||
@@ -120,6 +123,8 @@ impl NodeRuntime {
|
||||
node_graph_errors: Vec::new(),
|
||||
monitor_nodes: Vec::new(),
|
||||
|
||||
substitutions: preprocessor::generate_node_substitutions(),
|
||||
|
||||
thumbnail_renders: Default::default(),
|
||||
vector_modify: Default::default(),
|
||||
inspect_state: None,
|
||||
@@ -221,11 +226,14 @@ impl NodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_network(&mut self, graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
|
||||
async fn update_network(&mut self, mut graph: NodeNetwork) -> Result<ResolvedDocumentNodeTypesDelta, String> {
|
||||
preprocessor::expand_network(&mut graph, &self.substitutions);
|
||||
|
||||
let scoped_network = wrap_network_in_scope(graph, self.editor_api.clone());
|
||||
|
||||
// We assume only one output
|
||||
assert_eq!(scoped_network.exports.len(), 1, "Graph with multiple outputs not yet handled");
|
||||
|
||||
let c = Compiler {};
|
||||
let proto_network = match c.compile_single(scoped_network) {
|
||||
Ok(network) => network,
|
||||
|
||||
@@ -89,7 +89,7 @@ impl EditorTestUtils {
|
||||
}
|
||||
|
||||
pub async fn draw_polygon(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
self.drag_tool(ToolType::Polygon, x1, y1, x2, y2, ModifierKeys::default()).await;
|
||||
self.drag_tool(ToolType::Shape, x1, y1, x2, y2, ModifierKeys::default()).await;
|
||||
}
|
||||
|
||||
pub async fn draw_ellipse(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) {
|
||||
@@ -218,7 +218,12 @@ impl EditorTestUtils {
|
||||
}
|
||||
|
||||
pub async fn select_tool(&mut self, tool_type: ToolType) {
|
||||
self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await;
|
||||
match tool_type {
|
||||
ToolType::Line => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeLine)).await,
|
||||
ToolType::Rectangle => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeRectangle)).await,
|
||||
ToolType::Ellipse => self.handle_message(Message::Tool(ToolMessage::ActivateToolShapeEllipse)).await,
|
||||
_ => self.handle_message(Message::Tool(ToolMessage::ActivateTool { tool_type })).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select_primary_color(&mut self, color: Color) {
|
||||
|
||||
Reference in New Issue
Block a user