mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 21:58:12 +08:00
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:
co-authored by
Keavon Chambers
parent
e520c21b66
commit
8e5abf65cb
@@ -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;
|
||||
+241
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user