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
@@ -6,6 +6,8 @@ pub mod measure;
pub mod pivot;
pub mod resize;
pub mod shape_editor;
pub mod shape_gizmos;
pub mod shapes;
pub mod snapping;
pub mod transformation_cage;
pub mod utility_functions;
@@ -8,7 +8,7 @@ use glam::{DAffine2, DVec2, Vec2Swizzles};
#[derive(Clone, Debug, Default)]
pub struct Resize {
/// Stored as a document position so the start doesn't move if the canvas is panned.
drag_start: DVec2,
pub drag_start: DVec2,
pub layer: Option<LayerNodeIdentifier>,
pub snap_manager: SnapManager,
}
@@ -0,0 +1,2 @@
pub mod number_of_points_handle;
pub mod point_radius_handle;
@@ -0,0 +1,241 @@
use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION, NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{
extract_polygon_parameters, extract_star_parameters, inside_polygon, inside_star, polygon_vertex_position, star_vertex_position,
};
use crate::messages::tool::tool_messages::tool_prelude::Key;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::TAU;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum NumberOfPointsHandleState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default)]
pub struct NumberOfPointsHandle {
pub layer: Option<LayerNodeIdentifier>,
pub initial_points: u32,
pub handle_state: NumberOfPointsHandleState,
}
impl NumberOfPointsHandle {
pub fn cleanup(&mut self) {
self.handle_state = NumberOfPointsHandleState::Inactive;
self.layer = None;
}
pub fn update_state(&mut self, state: NumberOfPointsHandleState) {
self.handle_state = state;
}
pub fn is_hovering(&self) -> bool {
self.handle_state == NumberOfPointsHandleState::Hover
}
pub fn is_dragging(&self) -> bool {
self.handle_state == NumberOfPointsHandleState::Dragging
}
pub fn handle_actions(
&mut self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
responses: &mut VecDeque<Message>,
) {
if input.keyboard.key(Key::Control) {
return;
}
match &self.handle_state {
NumberOfPointsHandleState::Inactive => {
let selected_nodes = document.network_interface.selected_nodes();
let layers = 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()
});
for layer in layers {
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = star_vertex_position(viewport, 0, n, radius1, radius2);
if mouse_position.distance(center) < NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = n;
self.update_state(NumberOfPointsHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let point_on_max_radius = polygon_vertex_position(viewport, 0, n, radius);
if mouse_position.distance(center) < NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.layer = Some(layer);
self.initial_points = n;
self.update_state(NumberOfPointsHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
}
}
NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging => {
let Some(layer) = self.layer else { return };
let Some((n, radius)) = extract_star_parameters(Some(layer), document)
.map(|(n, r1, r2)| (n, r1.max(r2)))
.or_else(|| extract_polygon_parameters(Some(layer), document))
else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if mouse_position.distance(center) > NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsHandleState::Hover) {
self.update_state(NumberOfPointsHandleState::Inactive);
self.layer = None;
self.draw_spokes(center, viewport, n, radius, overlay_context);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
}
}
pub fn overlays(
&mut self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
shape_editor: &mut &mut ShapeState,
mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if input.keyboard.key(Key::Control) {
return;
}
match &self.handle_state {
NumberOfPointsHandleState::Inactive => {
let selected_nodes = document.network_interface.selected_nodes();
let layers = 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()
});
for layer in layers {
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let radius = radius1.max(radius2);
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = star_vertex_position(viewport, 0, n, radius1, radius2);
if inside_star(viewport, n, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, n, radius, overlay_context);
return;
}
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
if closest_segment.layer() == layer {
return;
}
}
let point_on_max_radius = polygon_vertex_position(viewport, 0, n, radius);
if inside_polygon(viewport, n, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
self.draw_spokes(center, viewport, n, radius, overlay_context);
return;
}
}
}
}
NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging => {
let Some(layer) = self.layer else { return };
let Some((n, radius)) = extract_star_parameters(Some(layer), document)
.map(|(n, r1, r2)| (n, r1.max(r2)))
.or_else(|| extract_polygon_parameters(Some(layer), document))
else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
self.draw_spokes(center, viewport, n, radius, overlay_context);
}
}
}
fn draw_spokes(&self, center: DVec2, viewport: DAffine2, n: u32, radius: f64, overlay_context: &mut OverlayContext) {
for i in 0..n {
let angle = ((i as f64) * TAU) / (n as f64);
let point = viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
});
let Some(direction) = (point - center).try_normalize() else { continue };
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
let end_point = direction * NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH;
if matches!(self.handle_state, NumberOfPointsHandleState::Hover | NumberOfPointsHandleState::Dragging) {
overlay_context.line(center, end_point * NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION + center, None, None);
} else {
overlay_context.line(center, end_point + center, None, None);
}
}
}
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
let net_delta = (delta.length() / 25.).round() * sign;
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return;
};
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -0,0 +1,441 @@
use crate::consts::{COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD, POINT_RADIUS_HANDLE_SNAP_THRESHOLD};
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::Responses;
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, extract_star_parameters, polygon_vertex_position, star_vertex_position};
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_4, PI, SQRT_2};
#[derive(Clone, Debug, Default, PartialEq)]
pub enum PointRadiusHandleState {
#[default]
Inactive,
Hover,
Dragging,
Snapped(usize),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PointRadiusHandle {
pub layer: Option<LayerNodeIdentifier>,
point: u32,
radius_index: usize,
snap_radii: Vec<f64>,
initial_radius: f64,
handle_state: PointRadiusHandleState,
}
impl PointRadiusHandle {
pub fn cleanup(&mut self) {
self.handle_state = PointRadiusHandleState::Inactive;
self.snap_radii.clear();
self.layer = None;
}
pub fn is_inactive(&self) -> bool {
self.handle_state == PointRadiusHandleState::Inactive
}
pub fn hovered(&self) -> bool {
self.handle_state == PointRadiusHandleState::Hover
}
pub fn update_state(&mut self, state: PointRadiusHandleState) {
self.handle_state = state;
}
pub fn handle_actions(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) {
match &self.handle_state {
PointRadiusHandleState::Inactive => {
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()
}) {
// Draw the point handle gizmo for the star shape
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * n {
let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) };
let point = star_vertex_position(viewport, i as i32, n, radius1, radius2);
let center = viewport.transform_point2(DVec2::ZERO);
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
self.radius_index = radius_index;
self.layer = Some(layer);
self.point = i;
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
self.initial_radius = radius;
self.update_state(PointRadiusHandleState::Hover);
return;
}
}
}
// Draw the point handle gizmo for the polygon shape
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..n {
let point = polygon_vertex_position(viewport, i as i32, n, radius);
let center = viewport.transform_point2(DVec2::ZERO);
// If the user zooms out so the shape is very small, hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
self.radius_index = 2;
self.layer = Some(layer);
self.point = i;
self.snap_radii.clear();
self.initial_radius = radius;
self.update_state(PointRadiusHandleState::Hover);
return;
}
}
}
}
}
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let point = star_vertex_position(viewport, self.point as i32, n, radius1, radius2);
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
self.update_state(PointRadiusHandleState::Inactive);
self.layer = None;
return;
}
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, n, radius);
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
self.update_state(PointRadiusHandleState::Inactive);
self.layer = None;
}
}
}
PointRadiusHandleState::Snapped(_) => {}
}
}
pub fn overlays(&mut self, other_gizmo_active: bool, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
match &self.handle_state {
PointRadiusHandleState::Inactive => {
let selected_nodes = document.network_interface.selected_nodes();
let layers = 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()
});
for layer in layers {
if other_gizmo_active {
return;
}
// Draw the point handle gizmo for the star shape
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..(2 * n) {
let point = star_vertex_position(viewport, i as i32, n, radius1, radius2);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
let angle = ((i as f64) * PI) / (n as f64);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
return;
}
overlay_context.manipulator_handle(point, false, None);
}
}
// Draw the point handle gizmo for the Polygon shape
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..n {
let point = polygon_vertex_position(viewport, i as i32, n, radius);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
// If the user zooms out such that shape is very small hide the gizmo
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if point.distance(mouse_position) < 5. {
let Some(direction) = (point - center).try_normalize() else { continue };
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
return;
}
overlay_context.manipulator_handle(point, false, None);
}
}
}
}
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let viewport_diagonal = input.viewport_bounds.size().length();
if let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
let angle = ((self.point as f64) * PI) / (n as f64);
let point = star_vertex_position(viewport, self.point as i32, n, radius1, radius2);
let Some(direction) = (point - center).try_normalize() else { return };
// Draws the ray from the center to the dragging point extending till the viewport
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
// Makes the tick marks for snapping
// Only show the snapping ticks if the radius is positive
if (mouse_position - center).dot(direction) >= 0. {
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
}
return;
}
if let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) {
let point = polygon_vertex_position(viewport, self.point as i32, n, radius);
let Some(direction) = (point - center).try_normalize() else { return };
// Draws the ray from the center to the dragging point and extending until the viewport edge is reached
overlay_context.manipulator_handle(point, true, None);
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
}
}
PointRadiusHandleState::Snapped(snapping_index) => {
let Some(layer) = self.layer else { return };
let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
match snapping_index {
// Make a triangle with the previous two points
0 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, n, radius1, radius2);
let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, n, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2);
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
let Some(direction) = (center - outer_position).try_normalize() else { return };
let l1 = 0.2 * (before_outer_position - outer_position).length();
let new_point = SQRT_2 * l1 * direction + outer_position;
let before_outer_position = l1 * l1_direction + outer_position;
let point_position = l1 * l2_direction + outer_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
}
1 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, n, radius1, radius2);
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, n, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2);
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
let Some(direction) = (center - point_position).try_normalize() else { return };
let l1 = 0.2 * (before_outer_position - point_position).length();
let new_point = SQRT_2 * l1 * direction + point_position;
let before_outer_position = l1 * l1_direction + point_position;
let after_point_position = l1 * l2_direction + point_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
}
i => {
// Use `self.point` as an absolute reference, as it matches the index of the star's vertices starting from 0
if i % 2 != 0 {
// Flipped case
let point_position = star_vertex_position(viewport, self.point as i32, n, radius1, radius2);
let target_index = (1 - (*i as i32)).abs() + (self.point as i32);
let target_point_position = star_vertex_position(viewport, target_index, n, radius1, radius2);
let mirrored_index = 2 * (self.point as i32) - target_index;
let mirrored = star_vertex_position(viewport, mirrored_index, n, radius1, radius2);
overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
} else {
let outer_index = (self.point as i32) - 1;
let outer_position = star_vertex_position(viewport, outer_index, n, radius1, radius2);
// The vertex which is colinear with the point we are dragging and its previous outer vertex
let target_index = (self.point as i32) + (*i as i32) - 1;
let target_point_position = star_vertex_position(viewport, target_index, n, radius1, radius2);
let mirrored_index = 2 * outer_index - target_index;
let mirrored = star_vertex_position(viewport, mirrored_index, n, radius1, radius2);
overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
}
}
}
}
}
}
fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) -> Vec<f64> {
let mut snap_radii = Vec::new();
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star") else {
return snap_radii;
};
let other_index = if radius_index == 3 { 2 } else { 3 };
let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else {
return snap_radii;
};
let Some(&TaggedValue::U32(n)) = node_inputs[1].as_value() else {
return snap_radii;
};
// Inner radius for 90°
let b = FRAC_PI_4 * 3. - PI / (n as f64);
let angle = b.sin();
let required_radius = (other_radius / angle) * FRAC_1_SQRT_2;
snap_radii.push(required_radius);
// Also add the case where the radius exceeds the other radius (the "flipped" case)
let flipped = other_radius * angle * SQRT_2;
snap_radii.push(flipped);
for i in 1..n {
let n = n as f64;
let i = i as f64;
let denominator = 2. * ((PI * (i - 1.)) / n).cos() * ((PI * i) / n).sin();
let numerator = ((2. * PI * i) / n).sin();
let factor = numerator / denominator;
if factor < 0. {
break;
}
if other_radius * factor > 1e-6 {
snap_radii.push(other_radius * factor);
}
snap_radii.push((other_radius * 1.) / factor);
}
snap_radii
}
fn check_snapping(&self, new_radius: f64, original_radius: f64) -> Option<(usize, f64)> {
self.snap_radii
.iter()
.enumerate()
.filter(|(_, rad)| (**rad - new_radius).abs() < POINT_RADIUS_HANDLE_SNAP_THRESHOLD)
.min_by(|(i_a, a), (i_b, b)| {
let dist_a = (**a - new_radius).abs();
let dist_b = (**b - new_radius).abs();
// Check if either index is 0 or 1 and prioritize them
match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) {
(true, false) => std::cmp::Ordering::Less, // a is priority index, b is not
(false, true) => std::cmp::Ordering::Greater, // b is priority index, a is not
_ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal), // normal comparison
}
})
.map(|(i, rad)| (i, *rad - original_radius))
}
pub fn update_inner_radius(
&mut self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
drag_start: DVec2,
) {
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
return;
};
let transform = document.network_interface.document_metadata().transform_to_viewport(layer);
let center = transform.transform_point2(DVec2::ZERO);
let radius_index = self.radius_index;
let original_radius = self.initial_radius;
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
let projection = delta.project_onto(radius);
let sign = radius.dot(delta).signum();
let mut net_delta = projection.length() * sign;
let new_radius = original_radius + net_delta;
self.update_state(PointRadiusHandleState::Dragging);
if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) {
net_delta = snapped_delta;
self.update_state(PointRadiusHandleState::Snapped(index));
}
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, radius_index),
input: NodeInput::value(TaggedValue::F64(original_radius + net_delta), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
@@ -0,0 +1,181 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Ellipse;
impl Ellipse {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_document_node_type("Ellipse").expect("Ellipse node can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(((start.x - end.x) / 2.).abs()), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(((start.y - end.y) / 2.).abs()), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_translation(start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
#[cfg(test)]
mod test_ellipse {
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graphene_std::vector::generator_nodes::ellipse;
#[derive(Debug, PartialEq)]
struct ResolvedEllipse {
radius_x: f64,
radius_y: f64,
transform: DAffine2,
}
async fn get_ellipse(editor: &mut EditorTestUtils) -> Vec<ResolvedEllipse> {
let instrumented = match editor.eval_graph().await {
Ok(instrumented) => instrumented,
Err(e) => panic!("Failed to evaluate graph: {e}"),
};
let document = editor.active_document();
let layers = document.metadata().all_layers();
layers
.filter_map(|layer| {
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
let ellipse_node = node_graph_layer.upstream_node_id_from_protonode(ellipse::protonode_identifier())?;
Some(ResolvedEllipse {
radius_x: instrumented.grab_protonode_input::<ellipse::RadiusXInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
radius_y: instrumented.grab_protonode_input::<ellipse::RadiusYInput>(&vec![ellipse_node], &editor.runtime).unwrap(),
transform: document.metadata().transform_to_document(layer),
})
})
.collect()
}
#[tokio::test]
async fn ellipse_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Ellipse, 10., 10., 19., 0., ModifierKeys::empty()).await;
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(
ellipse[0],
ResolvedEllipse {
radius_x: 4.5,
radius_y: 5.,
transform: DAffine2::from_translation(DVec2::new(14.5, 5.)) // Uses center
}
);
}
#[tokio::test]
async fn ellipse_draw_circle() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Ellipse, 10., 10., -10., 11., ModifierKeys::SHIFT).await;
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(
ellipse[0],
ResolvedEllipse {
radius_x: 10.,
radius_y: 10.,
transform: DAffine2::from_translation(DVec2::new(0., 20.)) // Uses center
}
);
}
#[tokio::test]
async fn ellipse_draw_square_rotated() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
// 45 degree rotation of content clockwise
angle_radians: f64::consts::FRAC_PI_4,
})
.await;
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT).await; // Viewport coordinates
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
println!("{ellipse:?}");
assert_eq!(ellipse[0].radius_x, 5.);
assert_eq!(ellipse[0].radius_y, 5.);
assert!(
ellipse[0]
.transform
.abs_diff_eq(DAffine2::from_angle_translation(-f64::consts::FRAC_PI_4, DVec2::X * f64::consts::FRAC_1_SQRT_2 * 10.), 0.001)
);
}
#[tokio::test]
async fn ellipse_draw_center_square_rotated() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
// 45 degree rotation of content clockwise
angle_radians: f64::consts::FRAC_PI_4,
})
.await;
editor.drag_tool(ToolType::Ellipse, 0., 0., 1., 10., ModifierKeys::SHIFT | ModifierKeys::ALT).await; // Viewport coordinates
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 1);
assert_eq!(ellipse[0].radius_x, 10.);
assert_eq!(ellipse[0].radius_y, 10.);
assert!(ellipse[0].transform.abs_diff_eq(DAffine2::from_angle(-f64::consts::FRAC_PI_4), 0.001));
}
#[tokio::test]
async fn ellipse_cancel() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Ellipse).await;
let ellipse = get_ellipse(&mut editor).await;
assert_eq!(ellipse.len(), 0);
}
}
@@ -0,0 +1,383 @@
use super::shape_utility::ShapeToolModifierKey;
use crate::consts::{BOUNDS_SELECT_THRESHOLD, LINE_ROTATE_SNAP_ANGLE};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
pub use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration};
use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Clone, PartialEq, Debug, Default)]
pub enum LineEnd {
#[default]
Start,
End,
}
#[derive(Clone, Debug, Default)]
pub struct LineToolData {
pub drag_start: DVec2,
pub drag_current: DVec2,
pub angle: f64,
pub weight: f64,
pub selected_layers_with_position: HashMap<LayerNodeIdentifier, [DVec2; 2]>,
pub editing_layer: Option<LayerNodeIdentifier>,
pub dragging_endpoint: Option<LineEnd>,
}
#[derive(Default)]
pub struct Line;
impl Line {
pub fn create_node(document: &DocumentMessageHandler, drag_start: DVec2) -> NodeTemplate {
let node_type = resolve_document_node_type("Line").expect("Line node can't be found");
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)),
Some(NodeInput::value(TaggedValue::DVec2(document.metadata().document_to_viewport.transform_point2(drag_start)), false)),
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, _, lock_angle, snap_angle] = modifier;
shape_tool_data.line_data.drag_current = ipp.mouse.position;
let keyboard = &ipp.keyboard;
let ignore = [layer];
let snap_data = SnapData::ignore(document, ipp, &ignore);
let mut document_points = generate_line(shape_tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
if shape_tool_data.line_data.dragging_endpoint == Some(LineEnd::Start) {
document_points.swap(0, 1);
}
let Some(node_id) = graph_modification_utils::get_line_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::DVec2(document_points[0]), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::DVec2(document_points[1]), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
shape_tool_data.line_data.selected_layers_with_position = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line")?;
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return None;
};
let [viewport_start, viewport_end] = [start, end].map(|point| document.metadata().transform_to_viewport(layer).transform_point2(point));
if !start.abs_diff_eq(end, f64::EPSILON * 1000.) {
overlay_context.line(viewport_start, viewport_end, None, None);
overlay_context.square(viewport_start, Some(6.), None, None);
overlay_context.square(viewport_end, Some(6.), None, None);
}
Some((layer, [start, end]))
})
.collect::<HashMap<LayerNodeIdentifier, [DVec2; 2]>>();
}
}
fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
let mut document_points = [tool_data.data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current)];
let mut angle = -(document_points[1] - document_points[0]).angle_to(DVec2::X);
let mut line_length = (document_points[1] - document_points[0]).length();
if lock_angle {
angle = tool_data.line_data.angle;
} else if snap_angle {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
tool_data.line_data.angle = angle;
if lock_angle {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
}
document_points[1] = document_points[0] + line_length * DVec2::new(angle.cos(), angle.sin());
let constrained = snap_angle || lock_angle;
let snap = &mut tool_data.data.snap_manager;
let near_point = SnapCandidatePoint::handle_neighbors(document_points[1], [tool_data.data.drag_start]);
let far_point = SnapCandidatePoint::handle_neighbors(2. * document_points[0] - document_points[1], [tool_data.data.drag_start]);
let config = SnapTypeConfiguration::default();
if constrained {
let constraint = SnapConstraint::Line {
origin: document_points[0],
direction: document_points[1] - document_points[0],
};
if center {
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
document_points[0] = best.snapped_point_document;
snap.update_indicator(best);
} else {
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, config);
document_points[1] = snapped.snapped_point_document;
snap.update_indicator(snapped);
}
} else if center {
let snapped = snap.free_snap(&snap_data, &near_point, config);
let snapped_far = snap.free_snap(&snap_data, &far_point, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
document_points[0] = best.snapped_point_document;
snap.update_indicator(best);
} else {
let snapped = snap.free_snap(&snap_data, &near_point, config);
document_points[1] = snapped.snapped_point_document;
snap.update_indicator(snapped);
}
document_points
}
pub fn clicked_on_line_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Line") else {
return false;
};
let (Some(&TaggedValue::DVec2(document_start)), Some(&TaggedValue::DVec2(document_end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return false;
};
let transform = document.metadata().transform_to_viewport(layer);
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
let drag_start = input.mouse.position;
let [start, end] = [document_start, document_end].map(|point| transform.transform_point2(point));
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
if start_click || end_click {
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
shape_tool_data.data.drag_start = if end_click { document_start } else { document_end };
shape_tool_data.line_data.editing_layer = Some(layer);
return true;
}
false
}
#[cfg(test)]
mod test_line_tool {
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graph_craft::document::value::TaggedValue;
async fn get_line_node_inputs(editor: &mut EditorTestUtils) -> Option<(DVec2, DVec2)> {
let document = editor.active_document();
let network_interface = &document.network_interface;
let node_id = network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &network_interface).find_node_inputs("Line")?;
let (Some(&TaggedValue::DVec2(start)), Some(&TaggedValue::DVec2(end))) = (node_inputs[1].as_value(), node_inputs[2].as_value()) else {
return None;
};
Some((start, end))
})
.next();
node_id
}
#[tokio::test]
async fn test_line_tool_basicdraw() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
assert!((start_input - DVec2::ZERO).length() < 1., "Start point should be near (0,0)");
assert!((end_input - DVec2::new(100., 100.)).length() < 1., "End point should be near (100,100)");
}
}
}
}
#[tokio::test]
async fn test_line_tool_with_transformed_viewport() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.handle_message(NavigationMessage::CanvasZoomSet { zoom_factor: 2. }).await;
editor.handle_message(NavigationMessage::CanvasPan { delta: DVec2::new(100., 50.) }).await;
editor
.handle_message(NavigationMessage::CanvasTiltSet {
angle_radians: (30. as f64).to_radians(),
})
.await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::empty()).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
let document = editor.active_document();
let document_to_viewport = document.metadata().document_to_viewport;
let viewport_to_document = document_to_viewport.inverse();
let expected_start = viewport_to_document.transform_point2(DVec2::ZERO);
let expected_end = viewport_to_document.transform_point2(DVec2::new(100., 100.));
assert!(
(start_input - expected_start).length() < 1.,
"Start point should match expected document coordinates. Got {:?}, expected {:?}",
start_input,
expected_start
);
assert!(
(end_input - expected_end).length() < 1.,
"End point should match expected document coordinates. Got {:?}, expected {:?}",
end_input,
expected_end
);
} else {
panic!("Line was not created successfully with transformed viewport");
}
}
#[tokio::test]
async fn test_line_tool_ctrl_anglelock() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 0., 0., 100., 100., ModifierKeys::CONTROL).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let line_vec = end_input - start_input;
let original_angle = line_vec.angle_to(DVec2::X);
editor.drag_tool(ToolType::Line, 0., 0., 200., 50., ModifierKeys::CONTROL).await;
if let Some((updated_start, updated_end)) = get_line_node_inputs(&mut editor).await {
match (updated_start, updated_end) {
(updated_start, updated_end) => {
let updated_line_vec = updated_end - updated_start;
let updated_angle = updated_line_vec.angle_to(DVec2::X);
print!("{:?}", original_angle);
print!("{:?}", updated_angle);
assert!(
line_vec.normalize().dot(updated_line_vec.normalize()).abs() - 1. < 1e-6,
"Line angle should be locked when Ctrl is kept pressed"
);
assert!((updated_start - updated_end).length() > 1., "Line should be able to change length when Ctrl is kept pressed");
}
}
}
}
}
}
}
#[tokio::test]
async fn test_line_tool_alt() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 100., 100., 200., 100., ModifierKeys::ALT).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let expected_start = DVec2::new(0., 100.);
let expected_end = DVec2::new(200., 100.);
assert!((start_input - expected_start).length() < 1., "Start point should be near (0, 100)");
assert!((end_input - expected_end).length() < 1., "End point should be near (200, 100)");
}
}
}
}
#[tokio::test]
async fn test_line_tool_alt_shift_drag() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Line, 100., 100., 150., 120., ModifierKeys::ALT | ModifierKeys::SHIFT).await;
if let Some((start_input, end_input)) = get_line_node_inputs(&mut editor).await {
match (start_input, end_input) {
(start_input, end_input) => {
let line_vec = end_input - start_input;
let angle_radians = line_vec.angle_to(DVec2::X);
let angle_degrees = angle_radians.to_degrees();
let nearest_angle = (angle_degrees / 15.).round() * 15.;
assert!((angle_degrees - nearest_angle).abs() < 1., "Angle should snap to the nearest 15 degrees");
}
}
}
}
#[tokio::test]
async fn test_line_tool_with_transformed_artboard() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Artboard, 0., 0., 200., 200., ModifierKeys::empty()).await;
let artboard_id = editor.get_selected_layer().await.expect("Should have selected the artboard");
editor
.handle_message(GraphOperationMessage::TransformChange {
layer: artboard_id,
transform: DAffine2::from_angle(45_f64.to_radians()),
transform_in: TransformIn::Local,
skip_rerender: false,
})
.await;
editor.drag_tool(ToolType::Line, 50., 50., 150., 150., ModifierKeys::empty()).await;
let (start_input, end_input) = get_line_node_inputs(&mut editor).await.expect("Line was not created successfully within transformed artboard");
// The line should still be diagonal with equal change in x and y
let line_vector = end_input - start_input;
// Verifying the line is approximately 100*sqrt(2) units in length (diagonal of 100x100 square)
let line_length = line_vector.length();
assert!(
(line_length - 141.42).abs() < 1., // 100 * sqrt(2) ~= 141.42
"Line length should be approximately 141.42 units. Got: {line_length}"
);
assert!((line_vector.x - 100.).abs() < 1., "X-component of line vector should be approximately 100. Got: {}", line_vector.x);
assert!(
(line_vector.y.abs() - 100.).abs() < 1.,
"Absolute Y-component of line vector should be approximately 100. Got: {}",
line_vector.y.abs()
);
let angle_degrees = line_vector.angle_to(DVec2::X).to_degrees();
assert!((angle_degrees - (-45.)).abs() < 1., "Line angle should be close to -45 degrees. Got: {angle_degrees}");
}
}
@@ -0,0 +1,11 @@
pub mod ellipse_shape;
pub mod line_shape;
pub mod polygon_shape;
pub mod rectangle_shape;
pub mod shape_utility;
pub mod star_shape;
pub use super::shapes::ellipse_shape::Ellipse;
pub use super::shapes::line_shape::{Line, LineEnd};
pub use super::shapes::rectangle_shape::Rectangle;
pub use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
@@ -0,0 +1,68 @@
use super::shape_utility::ShapeToolModifierKey;
use super::shape_utility::update_radius_sign;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Polygon;
impl Polygon {
pub fn create_node(vertices: u32) -> NodeTemplate {
let node_type = resolve_document_node_type("Regular Polygon").expect("Regular Polygon can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::U32(vertices), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
// TODO: We need to determine how to allow the polygon node to make irregular shapes
update_radius_sign(end, start, layer, document, responses);
let dimensions = (start - end).abs();
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
let mut scale = DVec2::ONE;
let radius;
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
radius = dimensions.x / 2.;
}
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -0,0 +1,54 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Rectangle;
impl Rectangle {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_document_node_type("Rectangle").expect("Rectangle node can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(1.), false)), Some(NodeInput::value(TaggedValue::F64(1.), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64((start.x - end.x).abs()), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64((start.y - end.y).abs()), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_translation(start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -0,0 +1,320 @@
use super::ShapeToolData;
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
use crate::messages::prelude::{DocumentMessageHandler, NodeGraphMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::*;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::renderer::ClickTargetType;
use graphene_std::vector::misc::dvec2_to_point;
use kurbo::{BezPath, PathEl, Shape};
use std::collections::VecDeque;
use std::f64::consts::{PI, TAU};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeType {
#[default]
Polygon = 0,
Star = 1,
Rectangle = 2,
Ellipse = 3,
Line = 4,
}
impl ShapeType {
pub fn name(&self) -> String {
(match self {
Self::Polygon => "Polygon",
Self::Star => "Star",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Line => "Line",
})
.into()
}
pub fn tooltip(&self) -> String {
(match self {
Self::Line => "Line Tool",
Self::Rectangle => "Rectangle Tool",
Self::Ellipse => "Ellipse Tool",
_ => "",
})
.into()
}
pub fn icon_name(&self) -> String {
(match self {
Self::Line => "VectorLineTool",
Self::Rectangle => "VectorRectangleTool",
Self::Ellipse => "VectorEllipseTool",
_ => "",
})
.into()
}
pub fn tool_type(&self) -> ToolType {
match self {
Self::Line => ToolType::Line,
Self::Rectangle => ToolType::Rectangle,
Self::Ellipse => ToolType::Ellipse,
_ => ToolType::Shape,
}
}
}
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
pub type ShapeToolModifierKey = [Key; 4];
pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let sign_num = if end[1] > start[1] { 1. } else { -1. };
let new_layer = NodeGraphLayer::new(layer, &document.network_interface);
if new_layer.find_input("Regular Polygon", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
let Some(polygon_node_id) = new_layer.upstream_node_id_from_name("Regular Polygon") else { return };
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(polygon_node_id, 2),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
});
return;
}
if new_layer.find_input("Star", 1).unwrap_or(&TaggedValue::U32(0)).to_u32() % 2 == 1 {
let Some(star_node_id) = new_layer.upstream_node_id_from_name("Star") else { return };
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(star_node_id, 2),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.5), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(star_node_id, 3),
input: NodeInput::value(TaggedValue::F64(sign_num * 0.25), false),
});
}
}
pub fn transform_cage_overlays(document: &DocumentMessageHandler, tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
let mut transform = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
.unwrap_or_default();
// Check if the matrix is not invertible
let mut transform_tampered = false;
if transform.matrix2.determinant() == 0. {
transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
transform_tampered = true;
}
let bounds = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.filter_map(|layer| {
document
.metadata()
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
})
.reduce(graphene_std::renderer::Quad::combine_bounds);
if let Some(bounds) = bounds {
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
bounding_box_manager.bounds = bounds;
bounding_box_manager.transform = transform;
bounding_box_manager.transform_tampered = transform_tampered;
bounding_box_manager.render_overlays(overlay_context, true);
} else {
tool_data.bounding_box_manager.take();
}
}
pub fn anchor_overlays(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
for layer in document.network_interface.selected_nodes().selected_layers(document.metadata()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue };
let transform = document.metadata().transform_to_viewport(layer);
overlay_context.outline_vector(&vector_data, transform);
for (_, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
overlay_context.manipulator_anchor(transform.transform_point2(position), false, None);
}
}
}
/// Extract the node input values of Star
pub fn extract_star_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64, f64)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Star")?;
let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(outer)), Some(&TaggedValue::F64(inner))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value(), node_inputs.get(3)?.as_value())
else {
return None;
};
Some((n, outer, inner))
}
/// Extract the node input values of Polygon
pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Regular Polygon")?;
let (Some(&TaggedValue::U32(n)), Some(&TaggedValue::F64(radius))) = (node_inputs.get(1)?.as_value(), node_inputs.get(2)?.as_value()) else {
return None;
};
Some((n, radius))
}
/// Calculate the viewport position of as a star vertex given its index
pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 {
let angle = ((vertex_index as f64) * PI) / (n as f64);
let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 };
viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
})
}
/// Calculate the viewport position of as a polygon vertex given its index
pub fn polygon_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius: f64) -> DVec2 {
let angle = ((vertex_index as f64) * TAU) / (n as f64);
viewport.transform_point2(DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
})
}
/// Outlines the geometric shape made by the Star node
pub fn star_outline(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let mut anchors = Vec::new();
let Some((n, radius1, radius2)) = extract_star_parameters(Some(layer), document) else { return };
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * n {
let angle = ((i as f64) * PI) / (n as f64);
let radius = if i % 2 == 0 { radius1 } else { radius2 };
let point = DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
};
anchors.push(point);
}
let subpath = [ClickTargetType::Subpath(Subpath::from_anchors_linear(anchors, true))];
overlay_context.outline(subpath.iter(), viewport, None);
}
/// Outlines the geometric shape made by the Polygon node
pub fn polygon_outline(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let mut anchors = Vec::new();
let Some((n, radius)) = extract_polygon_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
for i in 0..2 * n {
let angle = ((i as f64) * TAU) / (n as f64);
let point = DVec2 {
x: radius * angle.sin(),
y: -radius * angle.cos(),
};
anchors.push(point);
}
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::from_anchors_linear(anchors, true))];
overlay_context.outline(subpath.iter(), viewport, None);
}
/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications
pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool {
let mut paths = Vec::new();
for i in 0..2 * n {
let new_point = dvec2_to_point(star_vertex_position(viewport, i as i32, n, radius1, radius2));
if i == 0 {
paths.push(PathEl::MoveTo(new_point));
} else {
paths.push(PathEl::LineTo(new_point));
}
}
paths.push(PathEl::ClosePath);
let bez_path = BezPath::from_vec(paths);
let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box());
if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y {
return false;
}
let winding = shape.winding(dvec2_to_point(mouse_position));
// Non-zero fill rule
winding != 0
}
/// Check if the the cursor is inside the geometric polygon shape made by the Polygon node without any upstream node modifications
pub fn inside_polygon(viewport: DAffine2, n: u32, radius: f64, mouse_position: DVec2) -> bool {
let mut paths = Vec::new();
for i in 0..n {
let new_point = dvec2_to_point(polygon_vertex_position(viewport, i as i32, n, radius));
if i == 0 {
paths.push(PathEl::MoveTo(new_point));
} else {
paths.push(PathEl::LineTo(new_point));
}
}
paths.push(PathEl::ClosePath);
let bez_path = BezPath::from_vec(paths);
let (shape, bbox) = (bez_path.clone(), bez_path.bounding_box());
if bbox.x0 > mouse_position.x || bbox.y0 > mouse_position.y || bbox.x1 < mouse_position.x || bbox.y1 < mouse_position.y {
return false;
}
let winding = shape.winding(dvec2_to_point(mouse_position));
// Non-zero fill rule
winding != 0
}
pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffine2, angle: f64, overlay_context: &mut OverlayContext) {
for &snapped_radius in snap_radii {
let Some(tick_direction) = direction.perp().try_normalize() else {
return;
};
let tick_position = viewport.transform_point2(DVec2 {
x: snapped_radius * angle.sin(),
y: -snapped_radius * angle.cos(),
});
overlay_context.line(tick_position, tick_position + tick_direction * 5., None, Some(2.));
overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.));
}
}
@@ -0,0 +1,78 @@
use super::shape_utility::{ShapeToolModifierKey, update_radius_sign};
use super::*;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::tool_messages::tool_prelude::*;
use core::f64;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Star;
impl Star {
pub fn create_node(vertices: u32) -> NodeTemplate {
let node_type = resolve_document_node_type("Star").expect("Star node can't be found");
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::U32(vertices), false)),
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.25), false)),
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
// TODO: We need to determine how to allow the polygon node to make irregular shapes
update_radius_sign(end, start, layer, document, responses);
let dimensions = (start - end).abs();
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
let mut scale = DVec2::ONE;
let radius: f64;
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
radius = dimensions.x / 2.;
}
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface) else {
return;
};
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 3),
input: NodeInput::value(TaggedValue::F64(radius / 2.), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}
@@ -250,6 +250,10 @@ impl SnapManager {
self.update_indicator(snapped);
}
pub fn indicator_pos(&self) -> Option<DVec2> {
self.indicator.as_ref().map(|point| point.snapped_point_document)
}
fn find_best_snap(snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: SnapResults, constrained: bool, off_screen: bool, to_path: bool) -> SnappedPoint {
let mut snapped_points = Vec::new();
let document = snap_data.document;
@@ -70,7 +70,7 @@ impl AlignmentSnapper {
if let Some(quad) = target_point.quad.map(|q| q.0) {
if quad[0] == quad[3] && quad[1] == quad[2] && quad[0] == target_point.document_point {
let [p1, p2, ..] = quad;
let direction = (p2 - p1).normalize();
let Some(direction) = (p2 - p1).try_normalize() else { return };
let normal = DVec2::new(-direction.y, direction.x);
for endpoint in [p1, p2] {
@@ -1,9 +1,15 @@
use super::snapping::{SnapCandidatePoint, SnapData, SnapManager};
use super::transformation_cage::{BoundingBoxManager, SizeSnapData};
use crate::consts::ROTATE_INCREMENT;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::transformation::Selected;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges;
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use crate::messages::tool::utility_types::ToolType;
use bezier_rs::Bezier;
use glam::DVec2;
use glam::{DAffine2, DVec2};
use graphene_std::renderer::Quad;
use graphene_std::text::{FontCache, load_face};
use graphene_std::vector::{HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
@@ -198,6 +204,228 @@ pub fn is_visible_point(
}
}
pub fn resize_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
dragging_layers: &mut Vec<LayerNodeIdentifier>,
snap_manager: &mut SnapManager,
snap_candidates: &mut Vec<SnapCandidatePoint>,
input: &InputPreprocessorMessageHandler,
center: bool,
constrain: bool,
tool: ToolType,
) {
if let Some(movement) = &mut bounds.selected_edges {
let center = center.then_some(bounds.center_of_transformation);
let snap = Some(SizeSnapData {
manager: snap_manager,
points: snap_candidates,
snap_data: SnapData::ignore(document, input, &dragging_layers),
});
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
let pivot_transform = DAffine2::from_translation(pivot);
let transformation = pivot_transform * delta * pivot_transform.inverse();
dragging_layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &dragging_layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
}
}
pub fn rotate_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
dragging_layers: &mut Vec<LayerNodeIdentifier>,
drag_start: DVec2,
mouse_position: DVec2,
snap_angle: bool,
tool: ToolType,
) {
let angle = {
let start_offset = drag_start - bounds.center_of_transformation;
let end_offset = mouse_position - bounds.center_of_transformation;
start_offset.angle_to(end_offset)
};
let snapped_angle = if snap_angle {
let snap_resolution = ROTATE_INCREMENT.to_radians();
(angle / snap_resolution).round() * snap_resolution
} else {
angle
};
let delta = DAffine2::from_angle(snapped_angle);
dragging_layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of replacement_selected_layers");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
&dragging_layers,
responses,
&document.network_interface,
None,
&tool,
None,
);
selected.update_transforms(delta, None, None);
}
pub fn skew_bounds(
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
bounds: &mut BoundingBoxManager,
free_movement: bool,
layers: &mut Vec<LayerNodeIdentifier>,
mouse_position: DVec2,
tool: ToolType,
) {
if let Some(movement) = &mut bounds.selected_edges {
let mut pivot = DVec2::ZERO;
let transformation = movement.skew_transform(mouse_position, bounds.original_bound_transform, free_movement);
layers.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, &layers, responses, &document.network_interface, None, &tool, None);
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse(), None);
}
}
// TODO: Replace returned tuple (where at most 1 element is true at a time) with an enum.
/// Returns the tuple (resize, rotate, skew).
pub fn transforming_transform_cage(
document: &DocumentMessageHandler,
mut bounding_box_manager: &mut Option<BoundingBoxManager>,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
layers_dragging: &mut Vec<LayerNodeIdentifier>,
) -> (bool, bool, bool) {
let dragging_bounds = bounding_box_manager.as_mut().and_then(|bounding_box| {
let edges = bounding_box.check_selected_edges(input.mouse.position);
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
bounding_box.opposite_pivot = selected_edges.calculate_pivot();
selected_edges
});
edges
});
let rotating_bounds = bounding_box_manager.as_ref().map(|bounding_box| bounding_box.check_rotate(input.mouse.position)).unwrap_or_default();
let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect();
let is_flat_layer = bounding_box_manager.as_ref().map(|bounding_box_manager| bounding_box_manager.transform_tampered).unwrap_or(true);
if dragging_bounds.is_some() && !is_flat_layer {
responses.add(DocumentMessage::StartTransaction);
*layers_dragging = selected;
if let Some(bounds) = &mut bounding_box_manager {
bounds.original_bound_transform = bounds.transform;
layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
&layers_dragging,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
// Check if we're hovering over a skew triangle
let edges = bounds.check_selected_edges(input.mouse.position);
if let Some(edges) = edges {
let closest_edge = bounds.get_closest_edge(edges, input.mouse.position);
if bounds.check_skew_handle(input.mouse.position, closest_edge) {
// No resize or rotate, just skew
return (false, false, true);
}
}
}
// Just resize, no rotate or skew
return (true, false, false);
}
if rotating_bounds {
responses.add(DocumentMessage::StartTransaction);
if let Some(bounds) = &mut bounding_box_manager {
layers_dragging.retain(|layer| {
if *layer != LayerNodeIdentifier::ROOT_PARENT {
document.network_interface.document_network().nodes.contains_key(&layer.to_node())
} else {
log::error!("ROOT_PARENT should not be part of layers_dragging");
false
}
});
let mut selected = Selected::new(
&mut bounds.original_transforms,
&mut bounds.center_of_transformation,
&selected,
responses,
&document.network_interface,
None,
&ToolType::Select,
None,
);
bounds.center_of_transformation = selected.mean_average_of_pivots();
}
*layers_dragging = selected;
// No resize or skew, just rotate
return (false, true, false);
}
// No resize, rotate, or skew
return (false, false, false);
}
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
#[allow(clippy::too_many_arguments)]
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {