Add star/polygon gizmos and refactor the separate shape drawing tools into a unified Shape tool (#2683)

* no trait ,not to fix line

* add hints

* line modification even when other shapes are selected

* added transform and anchor overlays

* removed old code

* fixed transform added hints need to fix modifier keys use

* refactored select-tool

* add point-handle-gizmo

* fix rotate bug

* implement angle snapping gizmo , fix overlay and refactor the code

* implement snapping for point-handle gizmo and implement no of point gizmo need to refactor

* implemented the gizmo for polygon, added tests , brackets to increase sides

* formatting-fix

* small nit-picks

* Make it compile

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0
2025-06-22 00:53:49 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent e520c21b66
commit 8e5abf65cb
34 changed files with 3231 additions and 2137 deletions
@@ -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);
}
}
@@ -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;
@@ -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,15 @@
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;
@@ -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,7 +18,7 @@ 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;
@@ -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
@@ -0,0 +1,966 @@
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::graph_modification_utils::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::shape_gizmos::number_of_points_handle::{NumberOfPointsHandle, NumberOfPointsHandleState};
use crate::messages::tool::common_functionality::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState};
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, polygon_outline, star_outline, 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;
#[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,
}
impl Default for ShapeToolOptions {
fn default() -> Self {
Self {
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
shape_type: ShapeType::Polygon,
vertices: 5,
}
}
}
#[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),
}
#[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())).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())).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;
}
}
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::DraggingStarInnerRadius
| ShapeToolFsmState::DraggingStarNumberPointHandle
| 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),
// Line shape-specific
DraggingLineEndpoints,
// Star shape-specific
DraggingStarInnerRadius,
DraggingStarNumberPointHandle,
// 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,
// Gizmo data
pub point_radius_handle: PointRadiusHandle,
pub number_of_points_handle: NumberOfPointsHandle,
}
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);
}
}
}
fn outlines(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
if let Some(layer) = self.number_of_points_handle.layer.or(self.point_radius_handle.layer) {
star_outline(layer, document, overlay_context);
polygon_outline(layer, document, overlay_context);
return;
}
// Fallback: apply to all selected visible & unlocked star layers
for layer in document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| {
graph_modification_utils::get_star_id(*layer, &document.network_interface).is_some() || graph_modification_utils::get_polygon_id(*layer, &document.network_interface).is_some()
}) {
star_outline(layer, document, overlay_context);
polygon_outline(layer, document, overlay_context);
}
}
}
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);
let dragging_start_gizmos = matches!(self, Self::DraggingStarInnerRadius);
if matches!(self, ShapeToolFsmState::DraggingStarInnerRadius | Self::DraggingStarNumberPointHandle | Self::Ready(_)) && !input.keyboard.key(Key::Control) {
// Manage state handling of the number of point gizmos
tool_data.number_of_points_handle.handle_actions(document, input, mouse_position, &mut overlay_context, responses);
// Manage state handling of point radius handle gizmo
tool_data.point_radius_handle.handle_actions(document, mouse_position);
tool_data.number_of_points_handle.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
tool_data
.point_radius_handle
.overlays(tool_data.number_of_points_handle.layer.is_some(), document, input, mouse_position, &mut overlay_context);
tool_data.outlines(document, &mut overlay_context);
}
let hovered = tool_data.number_of_points_handle.is_hovering() || tool_data.number_of_points_handle.is_dragging() || !tool_data.point_radius_handle.is_inactive();
let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. });
if !is_resizing_or_rotating && !dragging_start_gizmos && !hovered && !modifying_transform_cage {
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
}
if modifying_transform_cage {
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;
}
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) {
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
}
if !is_skewing && dragging_bounds {
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;
// Check if dragging the inner vertices of a star
if tool_data.point_radius_handle.hovered() {
tool_data.last_mouse_position = mouse_pos;
tool_data.point_radius_handle.update_state(PointRadiusHandleState::Dragging);
// Always store it in document space
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::DraggingStarInnerRadius;
}
// Check if dragging the number of points handle of a star or polygon
if tool_data.number_of_points_handle.is_hovering() {
tool_data.last_mouse_position = mouse_pos;
tool_data.number_of_points_handle.update_state(NumberOfPointsHandleState::Dragging);
// Always store it in document space
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::DraggingStarNumberPointHandle;
}
// 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.angle = 0.;
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_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::DraggingStarInnerRadius, ShapeToolMessage::PointerMove(..)) => {
if let Some(layer) = tool_data.point_radius_handle.layer {
tool_data.point_radius_handle.update_inner_radius(document, input, layer, responses, tool_data.data.drag_start);
tool_data.last_mouse_position = input.mouse.position;
}
responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::DraggingStarInnerRadius
}
(ShapeToolFsmState::DraggingStarNumberPointHandle, ShapeToolMessage::PointerMove(..)) => {
tool_data.number_of_points_handle.update_number_of_sides(document, input, responses, tool_data.data.drag_start);
tool_data.last_mouse_position = input.mouse.position;
responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::DraggingStarNumberPointHandle
}
(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::Default, |bounds| bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge)));
if tool_data.cursor != cursor && !input.keyboard.key(Key::Control) && tool_data.point_radius_handle.is_inactive() && !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::DraggingStarInnerRadius
| ShapeToolFsmState::DraggingStarNumberPointHandle,
ShapeToolMessage::DragStop,
) => {
input.mouse.finish_transaction(tool_data.data.drag_start, responses);
tool_data.data.cleanup(responses);
tool_data.number_of_points_handle.cleanup();
tool_data.point_radius_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::DraggingStarInnerRadius
| ShapeToolFsmState::DraggingStarNumberPointHandle,
ShapeToolMessage::Abort,
) => {
responses.add(DocumentMessage::AbortTransaction);
tool_data.data.cleanup(responses);
tool_data.line_data.dragging_endpoint = None;
// Reset gizmo state
tool_data.number_of_points_handle.cleanup();
tool_data.point_radius_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::DraggingStarInnerRadius | ShapeToolFsmState::DraggingStarNumberPointHandle => {
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 });
}
}