Add 'Circle' to the Shape tool and its associated gizmos (#2914)
* merged with circle and impl inner radius gizmo for arc * impl radius-gizmo for arc * fix only one gizmo shown at a time * Code review * make hints update when changing shape,add default behaviour when dragging to make circle earlier fixed to from center * fixed arc-radius hover threshold and show arc-endpoint when hover over arc-radius gizmo --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
97bd0ebac4
commit
523132da17
|
|
@ -294,6 +294,147 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn dashed_ellipse(
|
||||
&mut self,
|
||||
center: DVec2,
|
||||
radius_x: f64,
|
||||
radius_y: f64,
|
||||
rotation: Option<f64>,
|
||||
start_angle: Option<f64>,
|
||||
end_angle: Option<f64>,
|
||||
counterclockwise: Option<bool>,
|
||||
color_fill: Option<&str>,
|
||||
color_stroke: Option<&str>,
|
||||
dash_width: Option<f64>,
|
||||
dash_gap_width: Option<f64>,
|
||||
dash_offset: Option<f64>,
|
||||
) {
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let center = center.round();
|
||||
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
if let Some(dash_width) = dash_width {
|
||||
let dash_gap_width = dash_gap_width.unwrap_or(1.);
|
||||
let array = js_sys::Array::new();
|
||||
array.push(&JsValue::from(dash_width));
|
||||
array.push(&JsValue::from(dash_gap_width));
|
||||
|
||||
if let Some(dash_offset) = dash_offset {
|
||||
if dash_offset != 0. {
|
||||
self.render_context.set_line_dash_offset(dash_offset);
|
||||
}
|
||||
}
|
||||
|
||||
self.render_context
|
||||
.set_line_dash(&JsValue::from(array))
|
||||
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context
|
||||
.ellipse_with_anticlockwise(
|
||||
center.x,
|
||||
center.y,
|
||||
radius_x,
|
||||
radius_y,
|
||||
rotation.unwrap_or_default(),
|
||||
start_angle.unwrap_or_default(),
|
||||
end_angle.unwrap_or(TAU),
|
||||
counterclockwise.unwrap_or_default(),
|
||||
)
|
||||
.expect("Failed to draw ellipse");
|
||||
self.render_context.set_stroke_style_str(color_stroke);
|
||||
|
||||
if let Some(fill_color) = color_fill {
|
||||
self.render_context.set_fill_style_str(fill_color);
|
||||
self.render_context.fill();
|
||||
}
|
||||
self.render_context.stroke();
|
||||
|
||||
// Reset the dash pattern back to solid
|
||||
if dash_width.is_some() {
|
||||
self.render_context
|
||||
.set_line_dash(&JsValue::from(js_sys::Array::new()))
|
||||
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
|
||||
.ok();
|
||||
}
|
||||
if dash_offset.is_some() && dash_offset != Some(0.) {
|
||||
self.render_context.set_line_dash_offset(0.);
|
||||
}
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn dashed_circle(
|
||||
&mut self,
|
||||
position: DVec2,
|
||||
radius: f64,
|
||||
color_fill: Option<&str>,
|
||||
color_stroke: Option<&str>,
|
||||
dash_width: Option<f64>,
|
||||
dash_gap_width: Option<f64>,
|
||||
dash_offset: Option<f64>,
|
||||
transform: Option<DAffine2>,
|
||||
) {
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let position = position.round();
|
||||
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
if let Some(transform) = transform {
|
||||
let [a, b, c, d, e, f] = transform.to_cols_array();
|
||||
self.render_context.transform(a, b, c, d, e, f).expect("Failed to transform circle");
|
||||
}
|
||||
|
||||
if let Some(dash_width) = dash_width {
|
||||
let dash_gap_width = dash_gap_width.unwrap_or(1.);
|
||||
let array = js_sys::Array::new();
|
||||
array.push(&JsValue::from(dash_width));
|
||||
array.push(&JsValue::from(dash_gap_width));
|
||||
|
||||
if let Some(dash_offset) = dash_offset {
|
||||
if dash_offset != 0. {
|
||||
self.render_context.set_line_dash_offset(dash_offset);
|
||||
}
|
||||
}
|
||||
|
||||
self.render_context
|
||||
.set_line_dash(&JsValue::from(array))
|
||||
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
|
||||
self.render_context.set_stroke_style_str(color_stroke);
|
||||
|
||||
if let Some(fill_color) = color_fill {
|
||||
self.render_context.set_fill_style_str(fill_color);
|
||||
self.render_context.fill();
|
||||
}
|
||||
self.render_context.stroke();
|
||||
|
||||
// Reset the dash pattern back to solid
|
||||
if dash_width.is_some() {
|
||||
self.render_context
|
||||
.set_line_dash(&JsValue::from(js_sys::Array::new()))
|
||||
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
|
||||
.ok();
|
||||
}
|
||||
if dash_offset.is_some() && dash_offset != Some(0.) {
|
||||
self.render_context.set_line_dash_offset(0.);
|
||||
}
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
self.dashed_circle(position, radius, color_fill, color_stroke, None, None, None, None);
|
||||
}
|
||||
|
||||
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
|
|
@ -374,23 +515,6 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
|
||||
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
|
||||
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
let position = position.round();
|
||||
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
|
||||
self.render_context.set_fill_style_str(color_fill);
|
||||
self.render_context.set_stroke_style_str(color_stroke);
|
||||
self.render_context.fill();
|
||||
self.render_context.stroke();
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
|
||||
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
|
||||
let step = (end_at - start_from) / segments as f64;
|
||||
|
|
@ -591,7 +715,7 @@ impl OverlayContext {
|
|||
}
|
||||
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.manipulator_handle(end_point_position, true, None);
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -345,6 +345,23 @@ impl OverlayContext {
|
|||
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle);
|
||||
}
|
||||
|
||||
pub fn dashed_ellipse(
|
||||
&mut self,
|
||||
_center: DVec2,
|
||||
_radius_x: f64,
|
||||
_radius_y: f64,
|
||||
_rotation: Option<f64>,
|
||||
_start_angle: Option<f64>,
|
||||
_end_angle: Option<f64>,
|
||||
_counterclockwise: Option<bool>,
|
||||
_color_fill: Option<&str>,
|
||||
_color_stroke: Option<&str>,
|
||||
_dash_width: Option<f64>,
|
||||
_dash_gap_width: Option<f64>,
|
||||
_dash_offset: Option<f64>,
|
||||
) {
|
||||
}
|
||||
|
||||
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
|
||||
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
|
||||
let step = (end_at - start_from) / segments as f64;
|
||||
|
|
@ -541,7 +558,7 @@ impl OverlayContext {
|
|||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.manipulator_handle(end_point_position, true, None);
|
||||
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
|
||||
self.text(text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageH
|
|||
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::arc_shape::ArcGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
|
||||
|
|
@ -26,6 +27,7 @@ pub enum ShapeGizmoHandlers {
|
|||
Star(StarGizmoHandler),
|
||||
Polygon(PolygonGizmoHandler),
|
||||
Arc(ArcGizmoHandler),
|
||||
Circle(CircleGizmoHandler),
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandlers {
|
||||
|
|
@ -36,6 +38,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(_) => "star",
|
||||
Self::Polygon(_) => "polygon",
|
||||
Self::Arc(_) => "arc",
|
||||
Self::Circle(_) => "circle",
|
||||
Self::None => "none",
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +49,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +60,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.is_any_gizmo_hovered(),
|
||||
Self::Polygon(h) => h.is_any_gizmo_hovered(),
|
||||
Self::Arc(h) => h.is_any_gizmo_hovered(),
|
||||
Self::Circle(h) => h.is_any_gizmo_hovered(),
|
||||
Self::None => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +71,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.handle_click(),
|
||||
Self::Polygon(h) => h.handle_click(),
|
||||
Self::Arc(h) => h.handle_click(),
|
||||
Self::Circle(h) => h.handle_click(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +82,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::Arc(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::Circle(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +93,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.cleanup(),
|
||||
Self::Polygon(h) => h.cleanup(),
|
||||
Self::Arc(h) => h.cleanup(),
|
||||
Self::Circle(h) => h.cleanup(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +112,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +130,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +140,7 @@ impl ShapeGizmoHandlers {
|
|||
Self::Star(h) => h.mouse_cursor_icon(),
|
||||
Self::Polygon(h) => h.mouse_cursor_icon(),
|
||||
Self::Arc(h) => h.mouse_cursor_icon(),
|
||||
Self::Circle(h) => h.mouse_cursor_icon(),
|
||||
Self::None => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -169,6 +180,10 @@ impl GizmoManager {
|
|||
if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new()));
|
||||
}
|
||||
// Circle
|
||||
if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
use crate::consts::GIZMO_HIDE_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::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
|
||||
use crate::messages::prelude::{FrontendMessage, Responses};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id, get_stroke_width};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_parameters, extract_circle_radius};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::FRAC_PI_2;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum RadiusHandleState {
|
||||
#[default]
|
||||
Inactive,
|
||||
Hover,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct RadiusHandle {
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
initial_radius: f64,
|
||||
handle_state: RadiusHandleState,
|
||||
angle: f64,
|
||||
previous_mouse_position: DVec2,
|
||||
}
|
||||
|
||||
impl RadiusHandle {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.handle_state = RadiusHandleState::Inactive;
|
||||
self.layer = None;
|
||||
}
|
||||
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.handle_state == RadiusHandleState::Hover
|
||||
}
|
||||
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
self.handle_state == RadiusHandleState::Dragging
|
||||
}
|
||||
|
||||
pub fn update_state(&mut self, state: RadiusHandleState) {
|
||||
self.handle_state = state;
|
||||
}
|
||||
|
||||
pub fn check_if_inside_dash_lines(angle: f64, mouse_position: DVec2, viewport: DAffine2, radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
|
||||
let circle_point = calculate_circle_point_position(angle, radius.abs());
|
||||
let direction = circle_point.normalize();
|
||||
let mouse_distance = mouse_position.distance(center);
|
||||
|
||||
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
|
||||
|
||||
let inner_point = viewport.transform_point2(circle_point - direction * spacing).distance(center);
|
||||
let outer_point = viewport.transform_point2(circle_point + direction * spacing).distance(center);
|
||||
|
||||
mouse_distance >= inner_point && mouse_distance <= outer_point
|
||||
} else {
|
||||
let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs()));
|
||||
mouse_position.distance(center) <= point_position.distance(center)
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_extra_spacing(viewport: DAffine2, radius: f64, viewport_center: DVec2, stroke_width: f64, threshold: f64) -> f64 {
|
||||
let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(viewport_center);
|
||||
let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(viewport_center);
|
||||
let min_radius = start_point.min(end_point);
|
||||
let extra_spacing = if min_radius < threshold { 10. * (min_radius / threshold) } else { 10. };
|
||||
|
||||
stroke_width + extra_spacing
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
|
||||
match &self.handle_state {
|
||||
RadiusHandleState::Inactive => {
|
||||
let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
|
||||
return;
|
||||
};
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X);
|
||||
let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs()));
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if point_position.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if Self::check_if_inside_dash_lines(angle, mouse_position, viewport, radius.abs(), document, layer) {
|
||||
self.layer = Some(layer);
|
||||
self.initial_radius = radius;
|
||||
self.previous_mouse_position = mouse_position;
|
||||
self.angle = angle;
|
||||
|
||||
self.update_state(RadiusHandleState::Hover);
|
||||
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
|
||||
}
|
||||
}
|
||||
RadiusHandleState::Dragging | RadiusHandleState::Hover => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlays(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
match &self.handle_state {
|
||||
RadiusHandleState::Inactive => {}
|
||||
RadiusHandleState::Dragging | RadiusHandleState::Hover => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
|
||||
return;
|
||||
};
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(center);
|
||||
let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(center);
|
||||
|
||||
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
|
||||
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
|
||||
let smaller_radius_x = (start_point - spacing).abs();
|
||||
let smaller_radius_y = (end_point - spacing).abs();
|
||||
|
||||
let larger_radius_x = (start_point + spacing).abs();
|
||||
let larger_radius_y = (end_point + spacing).abs();
|
||||
|
||||
overlay_context.dashed_ellipse(center, smaller_radius_x, smaller_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
|
||||
overlay_context.dashed_ellipse(center, larger_radius_x, larger_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
overlay_context.dashed_ellipse(center, start_point, end_point, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface).or(get_arc_id(layer, &document.network_interface)) else {
|
||||
return;
|
||||
};
|
||||
let Some(current_radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
|
||||
let center = viewport_transform.transform_point2(DVec2::ZERO);
|
||||
|
||||
let delta_vector = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(self.previous_mouse_position);
|
||||
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
|
||||
let sign = radius.dot(delta_vector).signum();
|
||||
|
||||
let net_delta = delta_vector.length() * sign * self.initial_radius.signum();
|
||||
self.previous_mouse_position = input.mouse.position;
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::F64(current_radius + net_delta), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_circle_point_position(theta: f64, radius: f64) -> DVec2 {
|
||||
DVec2::new(radius * theta.cos(), -radius * theta.sin())
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod circle_arc_radius_handle;
|
||||
pub mod number_of_points_dial;
|
||||
pub mod point_radius_handle;
|
||||
pub mod sweep_angle_gizmo;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD};
|
||||
use crate::consts::{ARC_SNAP_THRESHOLD, GIZMO_HIDE_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;
|
||||
|
|
@ -104,17 +104,17 @@ impl SweepAngleGizmo {
|
|||
|
||||
match self.handle_state {
|
||||
SweepAngleGizmoState::Inactive => {
|
||||
// Draw both endpoint handles if an arc is selected
|
||||
let Some((point1, point2)) = arc_end_points(selected_arc_layer, document) else { return };
|
||||
overlay_context.manipulator_handle(point1, false, Some(COLOR_OVERLAY_RED));
|
||||
overlay_context.manipulator_handle(point2, false, Some(COLOR_OVERLAY_RED));
|
||||
overlay_context.manipulator_handle(point1, false, None);
|
||||
overlay_context.manipulator_handle(point2, false, None);
|
||||
}
|
||||
SweepAngleGizmoState::Hover => {
|
||||
// Highlight the currently hovered endpoint only
|
||||
let Some((point1, point2)) = arc_end_points(self.layer, document) else { return };
|
||||
|
||||
let point = if self.endpoint == EndpointType::Start { point1 } else { point2 };
|
||||
overlay_context.manipulator_handle(point, true, Some(COLOR_OVERLAY_RED));
|
||||
let (point, other_point) = if self.endpoint == EndpointType::Start { (point1, point2) } else { (point2, point1) };
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.manipulator_handle(other_point, false, None);
|
||||
}
|
||||
SweepAngleGizmoState::Dragging => {
|
||||
// Show snapping guides and angle arc while dragging
|
||||
|
|
@ -123,11 +123,17 @@ impl SweepAngleGizmo {
|
|||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Depending on which endpoint is being dragged, draw guides relative to the static point
|
||||
let point = if self.endpoint == EndpointType::End { current_end } else { current_start };
|
||||
let (point, other_point) = if self.endpoint == EndpointType::End {
|
||||
(current_end, current_start)
|
||||
} else {
|
||||
(current_start, current_end)
|
||||
};
|
||||
|
||||
// Draw the dashed line from center to drag start position
|
||||
overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5));
|
||||
|
||||
overlay_context.manipulator_handle(other_point, false, None);
|
||||
|
||||
// Draw the angle, text and the bold line
|
||||
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
|
||||
}
|
||||
|
|
@ -143,8 +149,8 @@ impl SweepAngleGizmo {
|
|||
self.dragging_snapping_overlays(a, b, tilt_offset, viewport, overlay_context);
|
||||
|
||||
// Draw lines from endpoints to the arc center
|
||||
overlay_context.line(start, center, Some(COLOR_OVERLAY_RED), Some(2.));
|
||||
overlay_context.line(end, center, Some(COLOR_OVERLAY_RED), Some(2.));
|
||||
overlay_context.line(start, center, None, Some(2.));
|
||||
overlay_context.line(end, center, None, Some(2.));
|
||||
|
||||
// Draw the line from drag start to arc center
|
||||
overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5));
|
||||
|
|
|
|||
|
|
@ -333,6 +333,10 @@ pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
|
|||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
|
||||
}
|
||||
|
||||
pub fn get_circle_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Circle")
|
||||
}
|
||||
|
||||
pub fn get_ellipse_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Ellipse")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,56 +48,11 @@ impl Resize {
|
|||
/// Compute the drag start and end based on the current mouse position. Ignores the state of the layer.
|
||||
/// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`].
|
||||
pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, in_document: bool) -> [DVec2; 2] {
|
||||
let start = self.viewport_drag_start(document);
|
||||
let mouse = input.mouse.position;
|
||||
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
|
||||
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
|
||||
let mut points_viewport = [start, mouse];
|
||||
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
|
||||
let ratio = input.keyboard.get(lock_ratio as usize);
|
||||
let center = input.keyboard.get(center as usize);
|
||||
let snap_data = SnapData::ignore(document, input, &ignore);
|
||||
let config = SnapTypeConfiguration::default();
|
||||
if ratio {
|
||||
let viewport_size = points_viewport[1] - points_viewport[0];
|
||||
let raw_size = if in_document { document_to_viewport.inverse() } else { DAffine2::IDENTITY }.transform_vector2(viewport_size);
|
||||
let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum();
|
||||
let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size };
|
||||
points_viewport[1] = points_viewport[0] + size;
|
||||
|
||||
let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]);
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: self.drag_start,
|
||||
direction: end_document - self.drag_start,
|
||||
};
|
||||
if center {
|
||||
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config);
|
||||
let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document);
|
||||
let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, config);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
|
||||
points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
|
||||
self.snap_manager.update_indicator(best);
|
||||
} else {
|
||||
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config);
|
||||
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
} else if center {
|
||||
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
|
||||
let opposite = 2. * self.drag_start - document_mouse;
|
||||
let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(opposite), config);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
|
||||
points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
|
||||
self.snap_manager.update_indicator(best);
|
||||
} else {
|
||||
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
|
||||
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
|
||||
points_viewport
|
||||
// Use shared snapping logic with optional center and ratio constraints, considering if coordinates are in document space.
|
||||
self.compute_snapped_resize_points(document, input, center, ratio, in_document)
|
||||
}
|
||||
|
||||
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
|
||||
|
|
@ -113,6 +68,81 @@ impl Resize {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn calculate_circle_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key) -> [DVec2; 2] {
|
||||
let center = input.keyboard.get(center as usize);
|
||||
|
||||
// Use shared snapping logic with enforced aspect ratio and optional center snapping.
|
||||
self.compute_snapped_resize_points(document, input, center, true, false)
|
||||
}
|
||||
|
||||
/// Calculates two points in viewport space from a drag, applying snapping, optional center mode, and aspect ratio locking.
|
||||
fn compute_snapped_resize_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: bool, lock_ratio: bool, in_document: bool) -> [DVec2; 2] {
|
||||
let start = self.viewport_drag_start(document);
|
||||
let mouse = input.mouse.position;
|
||||
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
|
||||
let drag_start = self.drag_start;
|
||||
let mut points_viewport = [start, mouse];
|
||||
|
||||
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
|
||||
let snap_data = &SnapData::ignore(document, input, &ignore);
|
||||
|
||||
if lock_ratio {
|
||||
let viewport_size = points_viewport[1] - points_viewport[0];
|
||||
let raw_size = if in_document {
|
||||
document_to_viewport.inverse().transform_vector2(viewport_size)
|
||||
} else {
|
||||
viewport_size
|
||||
};
|
||||
|
||||
let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum();
|
||||
let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size };
|
||||
|
||||
points_viewport[1] = points_viewport[0] + size;
|
||||
let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]);
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: drag_start,
|
||||
direction: end_document - drag_start,
|
||||
};
|
||||
|
||||
if center {
|
||||
let snapped = self
|
||||
.snap_manager
|
||||
.constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default());
|
||||
let far = SnapCandidatePoint::handle(2. * drag_start - end_document);
|
||||
let snapped_far = self.snap_manager.constrained_snap(snap_data, &far, constraint, SnapTypeConfiguration::default());
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
|
||||
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
|
||||
points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document);
|
||||
self.snap_manager.update_indicator(best);
|
||||
} else {
|
||||
let snapped = self
|
||||
.snap_manager
|
||||
.constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default());
|
||||
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
} else {
|
||||
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
|
||||
if center {
|
||||
let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default());
|
||||
let opposite = 2. * drag_start - document_mouse;
|
||||
let snapped_far = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(opposite), SnapTypeConfiguration::default());
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
|
||||
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
|
||||
points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document);
|
||||
self.snap_manager.update_indicator(best);
|
||||
} else {
|
||||
let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default());
|
||||
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
}
|
||||
|
||||
points_viewport
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.snap_manager.cleanup(responses);
|
||||
self.layer = None;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
|
|||
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::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState};
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::sweep_angle_gizmo::{SweepAngleGizmo, SweepAngleGizmoState};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline};
|
||||
|
|
@ -17,6 +18,7 @@ use std::collections::VecDeque;
|
|||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ArcGizmoHandler {
|
||||
sweep_angle_gizmo: SweepAngleGizmo,
|
||||
arc_radius_handle: RadiusHandle,
|
||||
}
|
||||
|
||||
impl ArcGizmoHandler {
|
||||
|
|
@ -26,24 +28,40 @@ impl ArcGizmoHandler {
|
|||
}
|
||||
|
||||
impl ShapeGizmoHandler for ArcGizmoHandler {
|
||||
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, _responses: &mut VecDeque<Message>) {
|
||||
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position);
|
||||
fn handle_state(&mut self, selected_shape_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.sweep_angle_gizmo.handle_actions(selected_shape_layer, document, mouse_position);
|
||||
self.arc_radius_handle.handle_actions(selected_shape_layer, document, mouse_position, responses);
|
||||
}
|
||||
|
||||
fn is_any_gizmo_hovered(&self) -> bool {
|
||||
self.sweep_angle_gizmo.hovered()
|
||||
self.sweep_angle_gizmo.hovered() || self.arc_radius_handle.hovered()
|
||||
}
|
||||
|
||||
fn handle_click(&mut self) {
|
||||
// If hovering over both the gizmos give priority to sweep angle gizmo
|
||||
if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() {
|
||||
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
|
||||
self.arc_radius_handle.update_state(RadiusHandleState::Inactive);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.sweep_angle_gizmo.hovered() {
|
||||
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
|
||||
}
|
||||
|
||||
if self.arc_radius_handle.hovered() {
|
||||
self.arc_radius_handle.update_state(RadiusHandleState::Dragging);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if self.sweep_angle_gizmo.is_dragging_or_snapped() {
|
||||
self.sweep_angle_gizmo.update_arc(document, input, responses);
|
||||
}
|
||||
|
||||
if self.arc_radius_handle.is_dragging() {
|
||||
self.arc_radius_handle.update_inner_radius(document, input, responses, drag_start);
|
||||
}
|
||||
}
|
||||
|
||||
fn dragging_overlays(
|
||||
|
|
@ -58,20 +76,39 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
|
|||
self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context);
|
||||
arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context);
|
||||
}
|
||||
|
||||
if self.arc_radius_handle.is_dragging() {
|
||||
self.sweep_angle_gizmo.overlays(self.arc_radius_handle.layer, document, input, mouse_position, overlay_context);
|
||||
self.arc_radius_handle.overlays(document, overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
selected_shape_layers: Option<LayerNodeIdentifier>,
|
||||
selected_shape_layer: Option<LayerNodeIdentifier>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
_shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext,
|
||||
) {
|
||||
self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context);
|
||||
// If hovering over both the gizmos give priority to sweep angle gizmo
|
||||
if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() {
|
||||
self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context);
|
||||
return;
|
||||
}
|
||||
|
||||
arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context);
|
||||
if self.arc_radius_handle.hovered() {
|
||||
let layer = self.arc_radius_handle.layer;
|
||||
|
||||
self.arc_radius_handle.overlays(document, overlay_context);
|
||||
self.sweep_angle_gizmo.overlays(layer, document, input, mouse_position, overlay_context);
|
||||
}
|
||||
|
||||
self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context);
|
||||
self.arc_radius_handle.overlays(document, overlay_context);
|
||||
|
||||
arc_outline(selected_shape_layer.or(self.sweep_angle_gizmo.layer), document, overlay_context);
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
|
|
@ -79,11 +116,16 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
|
|||
return Some(MouseCursorIcon::Default);
|
||||
}
|
||||
|
||||
if self.arc_radius_handle.hovered() || self.arc_radius_handle.is_dragging() {
|
||||
return Some(MouseCursorIcon::EWResize);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.sweep_angle_gizmo.cleanup();
|
||||
self.arc_radius_handle.cleanup();
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
|
|
@ -122,11 +164,9 @@ impl Arc {
|
|||
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
|
||||
if dimensions.x > dimensions.y {
|
||||
scale.x = dimensions.x / dimensions.y;
|
||||
scale.y = 1.;
|
||||
radius = dimensions.y / 2.;
|
||||
} else {
|
||||
scale.y = dimensions.y / dimensions.x;
|
||||
scale.x = 1.;
|
||||
radius = dimensions.x / 2.;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, ShapeToolModifierKey};
|
||||
use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CircleGizmoHandler {
|
||||
circle_radius_handle: RadiusHandle,
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandler for CircleGizmoHandler {
|
||||
fn is_any_gizmo_hovered(&self) -> bool {
|
||||
self.circle_radius_handle.hovered()
|
||||
}
|
||||
|
||||
fn handle_state(&mut self, selected_circle_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.circle_radius_handle.handle_actions(selected_circle_layer, document, mouse_position, responses);
|
||||
}
|
||||
|
||||
fn handle_click(&mut self) {
|
||||
if self.circle_radius_handle.hovered() {
|
||||
self.circle_radius_handle.update_state(RadiusHandleState::Dragging);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if self.circle_radius_handle.is_dragging() {
|
||||
self.circle_radius_handle.update_inner_radius(document, input, responses, drag_start);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
_selected_circle_layer: Option<LayerNodeIdentifier>,
|
||||
_input: &InputPreprocessorMessageHandler,
|
||||
_shape_editor: &mut &mut ShapeState,
|
||||
_mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
self.circle_radius_handle.overlays(document, overlay_context);
|
||||
}
|
||||
|
||||
fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
_input: &InputPreprocessorMessageHandler,
|
||||
_shape_editor: &mut &mut ShapeState,
|
||||
_mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if self.circle_radius_handle.is_dragging() {
|
||||
self.circle_radius_handle.overlays(document, overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
self.circle_radius_handle.cleanup();
|
||||
}
|
||||
|
||||
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
|
||||
if self.circle_radius_handle.hovered() || self.circle_radius_handle.is_dragging() {
|
||||
return Some(MouseCursorIcon::EWResize);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Circle;
|
||||
|
||||
impl Circle {
|
||||
pub fn create_node() -> NodeTemplate {
|
||||
let node_type = resolve_document_node_type("Circle").expect("Circle can't be found");
|
||||
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.), false))])
|
||||
}
|
||||
|
||||
pub fn update_shape(
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
layer: LayerNodeIdentifier,
|
||||
shape_tool_data: &mut ShapeToolData,
|
||||
modifier: ShapeToolModifierKey,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
let center = modifier[0];
|
||||
let [start, end] = shape_tool_data.data.calculate_circle_points(document, ipp, center);
|
||||
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dimensions = (start - end).abs();
|
||||
let radius: f64;
|
||||
|
||||
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
|
||||
if dimensions.x > dimensions.y {
|
||||
radius = dimensions.y / 2.;
|
||||
} else {
|
||||
radius = dimensions.x / 2.;
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::F64(radius), false),
|
||||
});
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., start.midpoint(end)),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod arc_shape;
|
||||
pub mod circle_shape;
|
||||
pub mod ellipse_shape;
|
||||
pub mod line_shape;
|
||||
pub mod polygon_shape;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ pub enum ShapeType {
|
|||
#[default]
|
||||
Polygon = 0,
|
||||
Star,
|
||||
Circle,
|
||||
Arc,
|
||||
Rectangle,
|
||||
Ellipse,
|
||||
|
|
@ -37,6 +38,7 @@ impl ShapeType {
|
|||
(match self {
|
||||
Self::Polygon => "Polygon",
|
||||
Self::Star => "Star",
|
||||
Self::Circle => "Circle",
|
||||
Self::Arc => "Arc",
|
||||
Self::Rectangle => "Rectangle",
|
||||
Self::Ellipse => "Ellipse",
|
||||
|
|
@ -280,6 +282,19 @@ pub fn arc_end_points_ignore_layer(radius: f64, start_angle: f64, sweep_angle: f
|
|||
}
|
||||
|
||||
/// Calculate the viewport position of a star vertex given its index
|
||||
/// Extract the node input values of Circle.
|
||||
/// Returns an option of (radius).
|
||||
pub fn extract_circle_radius(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<f64> {
|
||||
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Circle")?;
|
||||
|
||||
let Some(&TaggedValue::F64(radius)) = node_inputs.get(1)?.as_value() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(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 };
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
|
|||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::shapes::arc_shape::Arc;
|
||||
use crate::messages::tool::common_functionality::shapes::circle_shape::Circle;
|
||||
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
|
||||
use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays};
|
||||
|
|
@ -110,6 +111,9 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
|
|||
MenuListEntry::new("Star")
|
||||
.label("Star")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()),
|
||||
MenuListEntry::new("Circle")
|
||||
.label("Circle")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Circle)).into()),
|
||||
MenuListEntry::new("Arc")
|
||||
.label("Arc")
|
||||
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()),
|
||||
|
|
@ -229,7 +233,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
|
|||
}
|
||||
}
|
||||
|
||||
self.fsm_state.update_hints(responses);
|
||||
update_dynamic_hints(&self.fsm_state, responses, &self.tool_data);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
|
|
@ -472,6 +476,9 @@ impl Fsm for ShapeToolFsmState {
|
|||
|
||||
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
|
||||
Line::overlays(document, tool_data, &mut overlay_context);
|
||||
if tool_options.shape_type == ShapeType::Circle {
|
||||
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
|
|
@ -650,7 +657,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
};
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle => tool_data.data.start(document, input),
|
||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => tool_data.data.start(document, input),
|
||||
ShapeType::Line => {
|
||||
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
|
||||
|
|
@ -663,6 +670,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
let node = match tool_data.current_shape {
|
||||
ShapeType::Polygon => Polygon::create_node(tool_options.vertices),
|
||||
ShapeType::Star => Star::create_node(tool_options.vertices),
|
||||
ShapeType::Circle => Circle::create_node(),
|
||||
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
|
||||
ShapeType::Rectangle => Rectangle::create_node(),
|
||||
ShapeType::Ellipse => Ellipse::create_node(),
|
||||
|
|
@ -675,7 +683,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
let defered_responses = &mut VecDeque::new();
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => {
|
||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => {
|
||||
defered_responses.add(GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||
|
|
@ -707,12 +715,13 @@ impl Fsm for ShapeToolFsmState {
|
|||
};
|
||||
|
||||
match tool_data.current_shape {
|
||||
ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||
}
|
||||
|
||||
// Auto-panning
|
||||
|
|
@ -892,6 +901,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
tool_data.data.cleanup(responses);
|
||||
tool_data.current_shape = shape;
|
||||
|
||||
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(shape)));
|
||||
ShapeToolFsmState::Ready(shape)
|
||||
}
|
||||
(_, ShapeToolMessage::HideShapeTypeWidget(hide)) => {
|
||||
|
|
@ -903,11 +913,20 @@ impl Fsm for ShapeToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||
let hint_data = match self {
|
||||
ShapeToolFsmState::Ready(shape) => {
|
||||
let hint_groups = match shape {
|
||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => vec![
|
||||
fn update_hints(&self, _responses: &mut VecDeque<Message>) {
|
||||
// Moved logic to update_dynamic_hints
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Message>, tool_data: &ShapeToolData) {
|
||||
let hint_data = match state {
|
||||
ShapeToolFsmState::Ready(_) => {
|
||||
let hint_groups = match tool_data.current_shape {
|
||||
ShapeType::Polygon | ShapeType::Star => vec![
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
|
||||
|
|
@ -931,6 +950,15 @@ impl Fsm for ShapeToolFsmState {
|
|||
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])],
|
||||
ShapeType::Circle => vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])],
|
||||
ShapeType::Arc => vec![HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arc"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Arc").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
|
||||
])],
|
||||
};
|
||||
HintData(hint_groups)
|
||||
}
|
||||
|
|
@ -945,9 +973,12 @@ impl Fsm for ShapeToolFsmState {
|
|||
HintInfo::keys([Key::Alt], "From Center"),
|
||||
HintInfo::keys([Key::Control], "Lock Angle"),
|
||||
]),
|
||||
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
|
||||
};
|
||||
|
||||
if !tool_hint_group.0.is_empty() {
|
||||
common_hint_group.push(tool_hint_group);
|
||||
}
|
||||
|
||||
if matches!(shape, ShapeType::Polygon | ShapeType::Star) {
|
||||
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]));
|
||||
|
|
@ -977,11 +1008,5 @@ impl Fsm for ShapeToolFsmState {
|
|||
]),
|
||||
ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue