impl circle shape and circle gizmos

This commit is contained in:
0SlowPoke0
2025-07-14 16:46:21 +05:30
parent c5800aa96e
commit 9835b583da
9 changed files with 347 additions and 22 deletions

View File

@@ -294,6 +294,73 @@ impl OverlayContext {
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 +441,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;

View File

@@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::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;
@@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers {
None,
Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler),
Circle(CircleGizmoHandler),
}
impl ShapeGizmoHandlers {
@@ -32,6 +34,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(_) => "star",
Self::Polygon(_) => "polygon",
Self::Circle(_) => "circle",
Self::None => "none",
}
}
@@ -41,6 +44,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {}
}
}
@@ -50,6 +54,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.is_any_gizmo_hovered(),
Self::Polygon(h) => h.is_any_gizmo_hovered(),
Self::Circle(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
@@ -59,6 +64,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(),
Self::Circle(h) => h.handle_click(),
Self::None => {}
}
}
@@ -68,6 +74,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
Self::Circle(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {}
}
}
@@ -77,6 +84,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(),
Self::Circle(h) => h.cleanup(),
Self::None => {}
}
}
@@ -94,6 +102,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
@@ -110,6 +119,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
@@ -147,6 +157,11 @@ impl GizmoManager {
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
}
// Polygon
if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default()));
}
None
}

View File

@@ -0,0 +1,124 @@
use crate::consts::GIZMO_HIDE_THRESHOLD;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector};
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::prelude::{FrontendMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils::{self};
use crate::messages::tool::common_functionality::shapes::shape_utility::extract_circle_radius;
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[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_or_snapped(&self) -> bool {
self.handle_state == RadiusHandleState::Dragging
}
pub fn update_state(&mut self, state: RadiusHandleState) {
self.handle_state = state;
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
match &self.handle_state {
RadiusHandleState::Inactive => {
let Some(radius) = extract_circle_radius(layer, document) 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(get_circle_point_position(angle, radius.abs()));
let center = viewport.transform_point2(DVec2::ZERO);
log::info!("reaching here");
if point_position.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if mouse_position.distance(center) <= point_position.distance(center) {
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) else { return };
let viewport = document.metadata().transform_to_viewport(layer);
overlay_context.dashed_circle(DVec2::ZERO, radius.abs(), None, None, Some(20.), Some(4.), Some(0.5), Some(viewport));
// overlay_context.dashed_line(center, point_position, 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) else {
return;
};
let Some(current_radius) = extract_circle_radius(layer, document) 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 get_circle_point_position(theta: f64, radius: f64) -> DVec2 {
DVec2::new(radius * theta.cos(), -radius * theta.sin())
}

View File

@@ -1,2 +1,3 @@
pub mod circle_radius_handle;
pub mod number_of_points_dial;
pub mod point_radius_handle;

View File

@@ -332,6 +332,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")
}

View File

@@ -0,0 +1,104 @@
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_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;
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_or_snapped() {
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_or_snapped() {
self.circle_radius_handle.overlays(document, overlay_context);
}
}
fn cleanup(&mut self) {
self.circle_radius_handle.cleanup();
}
}
#[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, responses: &mut VecDeque<Message>) {
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let radius = ipp.mouse.position.distance(center);
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_translation(center),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}

View File

@@ -1,3 +1,4 @@
pub mod circle_shape;
pub mod ellipse_shape;
pub mod line_shape;
pub mod polygon_shape;

View File

@@ -24,9 +24,10 @@ pub enum ShapeType {
#[default]
Polygon = 0,
Star = 1,
Rectangle = 2,
Ellipse = 3,
Line = 4,
Circle = 2,
Rectangle = 3,
Ellipse = 4,
Line = 5,
}
impl ShapeType {
@@ -37,6 +38,7 @@ impl ShapeType {
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Line => "Line",
Self::Circle => "Circle",
})
.into()
}
@@ -234,6 +236,18 @@ pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document:
Some((n, radius))
}
/// 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);

View File

@@ -10,6 +10,7 @@ use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoMan
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::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};
@@ -109,6 +110,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()),
]];
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder()
}
@@ -422,6 +426,9 @@ impl Fsm for ShapeToolFsmState {
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
Line::overlays(document, tool_data, &mut overlay_context);
if matches!(tool_options.shape_type, ShapeType::Circle) {
tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
}
}
self
@@ -578,7 +585,7 @@ impl Fsm for ShapeToolFsmState {
};
match tool_data.current_shape {
ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input),
ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Circle => 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());
@@ -594,6 +601,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(),
ShapeType::Line => Line::create_node(document, tool_data.data.drag_start),
ShapeType::Circle => Circle::create_node(),
};
let nodes = vec![(NodeId(0), node)];
@@ -602,7 +610,7 @@ impl Fsm for ShapeToolFsmState {
responses.add(Message::StartBuffer);
match tool_data.current_shape {
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => {
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star | ShapeType::Circle => {
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
@@ -635,6 +643,7 @@ impl Fsm for ShapeToolFsmState {
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::Circle => Circle::update_shape(document, input, layer, responses),
}
// Auto-panning
@@ -814,6 +823,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)) => {
@@ -853,6 +863,7 @@ 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")])],
};
HintData(hint_groups)
}
@@ -867,6 +878,7 @@ impl Fsm for ShapeToolFsmState {
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
ShapeType::Circle => HintGroup(vec![]),
};
common_hint_group.push(tool_hint_group);