Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e1a1b11
made spiral node
0SlowPoke0 Jun 29, 2025
2b819c1
number of turns in decimal and arc-angle implementation
0SlowPoke0 Jun 30, 2025
b8edd8a
logarithmic spiral
0SlowPoke0 Jun 30, 2025
c2a9011
Merge branch 'master' into spiral-node
0SlowPoke0 Jun 30, 2025
0dbf315
Merge branch 'master' into spiral-node
0SlowPoke0 Jul 1, 2025
917e79e
unified log and arc spiral into spiral node
0SlowPoke0 Jul 1, 2025
1cd98d6
Merge branch 'master' into spiral-node
0SlowPoke0 Jul 1, 2025
a9327ee
Merge branch 'master' into spiral-node
0SlowPoke0 Jul 3, 2025
91a5c88
add spiral shape in shape tool
0SlowPoke0 Jul 3, 2025
1773c92
fix min value and degree unit
0SlowPoke0 Jul 3, 2025
0594b49
Merge branch 'master' into spiral-node
0SlowPoke0 Jul 4, 2025
937f518
Merge remote-tracking branch 'origin/master' into spiral-node
0SlowPoke0 Jul 10, 2025
2dd6b31
Merge remote-tracking branch 'origin/master' into spiral-node
0SlowPoke0 Jul 10, 2025
57437a9
Merge remote-tracking branch 'origin/master' into spiral-node
0SlowPoke0 Jul 10, 2025
8870409
make it compile
0SlowPoke0 Jul 10, 2025
f3b6ab3
impl turns handle gizmo
0SlowPoke0 Jul 10, 2025
0ec46db
impl inner outer gizmos
0SlowPoke0 Jul 10, 2025
8e43744
impl radius 1 and radius 2 for inner-radius and tightness,need to fix…
0SlowPoke0 Jul 13, 2025
892c29b
make outer and inner same gizmo and fix turns and tightness handle
0SlowPoke0 Jul 18, 2025
6cf7d4d
need to fix the updated dash lines
0SlowPoke0 Jul 20, 2025
bcbf30b
fixed all bugs and divide futher when angle greater than 180
0SlowPoke0 Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
pub const SPIRAL_INNER_RADIUS_INDEX_GIZMO_THRESHOLD: f64 = 10.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;

// SCROLLBARS
Expand All @@ -150,3 +151,10 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15;

// INPUT
pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500;

/// SPIRAL NODE INPUT INDICES
pub const SPIRAL_TYPE_INDEX: usize = 1;
pub const SPIRAL_INNER_RADIUS_INDEX: usize = 2;
pub const SPIRAL_OUTER_RADIUS_INDEX: usize = 3;
pub const SPIRAL_TURNS_INDEX: usize = 4;
pub const SPIRAL_START_ANGLE: usize = 6;
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,7 @@ fn static_node_properties() -> NodeProperties {
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
map.insert(
"identity_properties".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint};
use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::GridType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{CentroidType, PointSpacingType};
use graphene_std::vector::misc::{GridType, SpiralType};
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
Expand Down Expand Up @@ -1202,6 +1202,70 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
widgets
}

pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::vector::generator_nodes::spiral::*;

let spiral_type = enum_choice::<SpiralType>()
.for_socket(ParameterWidgetsInfo::new(node_id, SpiralTypeInput::INDEX, true, context))
.property_row();

let mut widgets = vec![spiral_type];

let document_node = match get_document_node(node_id, context) {
Ok(document_node) => document_node,
Err(err) => {
log::error!("Could not get document node in exposure_properties: {err}");
return Vec::new();
}
};

let Some(spiral_type_input) = document_node.inputs.get(SpiralTypeInput::INDEX) else {
log::warn!("A widget failed to be built because its node's input index is invalid.");
return vec![];
};
if let Some(&TaggedValue::SpiralType(spiral_type)) = spiral_type_input.as_non_exposed_value() {
match spiral_type {
SpiralType::Archimedean => {
let inner_radius = LayoutGroup::Row {
widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")),
};

let outer_radius = LayoutGroup::Row {
widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().unit(" px")),
};

widgets.extend([inner_radius, outer_radius]);
}
SpiralType::Logarithmic => {
let inner_radius = LayoutGroup::Row {
widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")),
};

let outer_radius = LayoutGroup::Row {
widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().min(0.1).unit(" px")),
};

widgets.extend([inner_radius, outer_radius]);
}
}
}

let turns = number_widget(ParameterWidgetsInfo::new(node_id, TurnsInput::INDEX, true, context), NumberInput::default().min(0.1));
let angle_offset = number_widget(
ParameterWidgetsInfo::new(node_id, AngleOffsetInput::INDEX, true, context),
NumberInput::default().min(1.).max(180.).unit("°"),
);
let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, StartAngleInput::INDEX, true, context), NumberInput::default().unit("°"));

widgets.extend([
LayoutGroup::Row { widgets: turns },
LayoutGroup::Row { widgets: angle_offset },
LayoutGroup::Row { widgets: start_angle },
]);

widgets
}

pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
Expand Down
132 changes: 128 additions & 4 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,23 +372,147 @@ 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);
#[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_fill_style_str(color_fill);
self.render_context.set_stroke_style_str(color_stroke);
self.render_context.fill();

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 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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::polygon_shape::PolygonGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::spiral_shape::SpiralGizmoHandler;
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
use glam::DVec2;
use std::collections::VecDeque;
Expand All @@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers {
None,
Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler),
Spiral(SpiralGizmoHandler),
}

impl ShapeGizmoHandlers {
Expand All @@ -32,15 +34,17 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(_) => "star",
Self::Polygon(_) => "polygon",
Self::Spiral(_) => "spiral",
Self::None => "none",
}
}

/// Dispatches interaction state updates to the corresponding shape-specific handler.
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
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::Star(h) => h.handle_state(layer, mouse_position, document, input, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, input, responses),
Self::Spiral(h) => h.handle_state(layer, mouse_position, document, input, responses),
Self::None => {}
}
}
Expand All @@ -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::Spiral(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
Expand All @@ -59,6 +64,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(),
Self::Spiral(h) => h.handle_click(),
Self::None => {}
}
}
Expand All @@ -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::Spiral(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {}
}
}
Expand All @@ -77,6 +84,7 @@ impl ShapeGizmoHandlers {
match self {
Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(),
Self::Spiral(h) => h.cleanup(),
Self::None => {}
}
}
Expand All @@ -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::Spiral(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
Expand All @@ -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::Spiral(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {}
}
}
Expand Down Expand Up @@ -147,6 +157,11 @@ impl GizmoManager {
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
}

// Spiral
if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default()));
}

None
}

Expand All @@ -158,12 +173,12 @@ impl GizmoManager {
/// Called every frame to check selected layers and update the active shape gizmo, if hovered.
///
/// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing.
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = Vec::new();

for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
if let Some(mut handler) = Self::detect_shape_handler(layer, document) {
handler.handle_state(layer, mouse_position, document, responses);
handler.handle_state(layer, mouse_position, document, input, responses);
let is_hovered = handler.is_any_gizmo_hovered();

if is_hovered {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
pub mod number_of_points_dial;
pub mod point_radius_handle;
pub mod spiral_inner_radius_handle;
pub mod spiral_tightness_gizmo;
pub mod spiral_turns_handle;
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl NumberOfPointsDial {
self.handle_state = state;
}

pub fn is_hovering(&self) -> bool {
pub fn hovered(&self) -> bool {
self.handle_state == NumberOfPointsDialState::Hover
}

Expand Down Expand Up @@ -189,8 +189,8 @@ impl NumberOfPointsDial {
}

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 delta = input.mouse.position - drag_start;
let sign = (input.mouse.position.x - drag_start.x).signum();
let net_delta = (delta.length() / 25.).round() * sign;

let Some(layer) = self.layer else { return };
Expand Down
Loading