Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions editor/src/messages/tool/common_functionality/shapes/grid_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,58 @@ fn calculate_isometric_x_position(y_spacing: f64, rad_a: f64, rad_b: f64) -> f64
let spacing_x = y_spacing / (rad_a.tan() + rad_b.tan());
spacing_x * 9.
}
#[cfg(test)]
mod tests {
use super::calculate_grid_params;
use glam::DVec2;

#[test]
fn grid_params_basic_rectangle() {
// Simple downward-right drag: translation = start, dimensions = raw/9, no angle
let (translation, dimensions, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), false, false, false);
assert_eq!(translation, DVec2::ZERO);
assert_eq!(dimensions, DVec2::splat(10.)); // 90/9 = 10
assert!(angle.is_none());
}

#[test]
fn grid_params_lock_ratio_forces_square_spacing() {
// Non-square drag (90x45) with lock_ratio: uses larger dim (90), dimensions = 90/9 = 10
let (_, dimensions, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 45.), false, false, true);
assert_eq!(dimensions, DVec2::splat(10.));
assert!(angle.is_none());
}

#[test]
fn grid_params_center_doubles_dimensions_and_shifts_translation() {
// Center draw: dimensions doubled, translation shifted back by raw_dimensions
let (translation, dimensions, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), false, true, false);
assert_eq!(translation, DVec2::splat(-90.));
assert_eq!(dimensions, DVec2::splat(20.)); // 2 * 90/9 = 20
assert!(angle.is_none());
}

#[test]
fn grid_params_negative_drag_adjusts_translation() {
// Drag up-left from (100,100) to (10,10): translation must shift to (10,10)
let (translation, dimensions, angle) = calculate_grid_params(DVec2::splat(100.), DVec2::splat(10.), false, false, false);
assert!((translation.x - 10.).abs() < 1e-10, "Expected translation.x=10, got {}", translation.x);
assert!((translation.y - 10.).abs() < 1e-10, "Expected translation.y=10, got {}", translation.y);
assert_eq!(dimensions, DVec2::splat(10.)); // (90,90)/9
assert!(angle.is_none());
}

#[test]
fn grid_params_isometric_produces_angle() {
// Isometric grid (no lock_ratio): angle is dynamically computed from drag
let (_, _, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), true, false, false);
assert!(angle.is_some(), "Isometric grid should return an angle");
}

#[test]
fn grid_params_isometric_lock_ratio_fixes_angle_at_30() {
// Isometric + lock_ratio: angle is standardized at 30 degrees
let (_, _, angle) = calculate_grid_params(DVec2::ZERO, DVec2::new(90., 90.), true, false, true);
assert_eq!(angle, Some(30.), "Isometric lock_ratio should fix angle at 30°");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,83 @@ impl Polygon {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}

#[cfg(test)]
mod test_polygon {
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::test_utils::test_prelude::*;
use graph_craft::document::value::TaggedValue;

/// Reads sides and radius from the first polygon node found in the document.
fn get_polygon_inputs(editor: &EditorTestUtils) -> Option<(u32, f64)> {
let document = editor.active_document();
document.metadata().all_layers().find_map(|layer| {
let inputs = NodeGraphLayer::new(layer, &document.network_interface)
.find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::regular_polygon::IDENTIFIER))?;
let Some(&TaggedValue::U32(sides)) = inputs.get(1).and_then(|i| i.as_value()) else {
return None;
};
let Some(&TaggedValue::F64(radius)) = inputs.get(2).and_then(|i| i.as_value()) else {
return None;
};
Some((sides, radius))
})
}

#[tokio::test]
async fn polygon_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Shape, 0., 0., 100., 100., ModifierKeys::empty()).await;

assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let (sides, radius) = get_polygon_inputs(&editor).expect("Polygon node should exist after draw");
assert!(sides >= 3, "Polygon should have at least 3 sides, got {sides}");
assert!((radius - 50.).abs() < 1., "Expected radius ≈ 50 for 100×100 drag, got {radius}");
}

#[tokio::test]
async fn polygon_draw_non_square_uses_shorter_dimension() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
// Drag wider than tall: dimensions = (100, 60), radius = shorter/2 = 30
editor.drag_tool(ToolType::Shape, 0., 0., 100., 60., ModifierKeys::empty()).await;

let (_, radius) = get_polygon_inputs(&editor).expect("Polygon node should exist");
assert!((radius - 30.).abs() < 1., "Expected radius ≈ 30 for 100×60 drag, got {radius}");
}

#[tokio::test]
async fn polygon_draw_shift_lock_ratio() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
// SHIFT forces equal dimensions — a 100×60 drag becomes 100×100
editor.drag_tool(ToolType::Shape, 0., 0., 100., 60., ModifierKeys::SHIFT).await;

let (_, radius) = get_polygon_inputs(&editor).expect("Polygon node should exist");
assert!((radius - 50.).abs() < 1., "Expected radius ≈ 50 with SHIFT lock ratio on 100×60 drag, got {radius}");
}

#[tokio::test]
async fn polygon_default_six_sides() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Shape, 0., 0., 100., 100., ModifierKeys::empty()).await;

let (sides, _) = get_polygon_inputs(&editor).expect("Polygon node should exist");
assert_eq!(sides, 5, "Default polygon should have 5 sides");
}

#[tokio::test]
async fn polygon_cancel_rmb_no_layer() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Shape).await;

assert_eq!(
editor.active_document().metadata().all_layers().count(),
0,
"RMB-cancelled polygon should not create a layer"
);
}
}
98 changes: 98 additions & 0 deletions editor/src/messages/tool/common_functionality/shapes/star_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,101 @@ impl Star {
}
}
}

#[cfg(test)]
mod test_star {
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
use crate::messages::tool::tool_messages::shape_tool::ShapeOptionsUpdate;
use crate::test_utils::test_prelude::*;
use graph_craft::document::value::TaggedValue;

/// Switch to Star shape type, then manually drag to avoid drag_tool re-selecting and resetting options.
async fn draw_star(editor: &mut EditorTestUtils, x1: f64, y1: f64, x2: f64, y2: f64, modifier_keys: ModifierKeys) {
editor.select_tool(ToolType::Shape).await;
editor
.handle_message(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Star),
})
.await;
editor.move_mouse(x1, y1, modifier_keys, MouseKeys::empty()).await;
editor.left_mousedown(x1, y1, modifier_keys).await;
editor.move_mouse(x2, y2, modifier_keys, MouseKeys::LEFT).await;
editor.left_mouseup(x2, y2, modifier_keys).await;
}

/// Returns (sides, outer_radius, inner_radius) from the first star node in the document.
fn get_star_inputs(editor: &EditorTestUtils) -> Option<(u32, f64, f64)> {
let document = editor.active_document();
document.metadata().all_layers().find_map(|layer| {
let inputs = NodeGraphLayer::new(layer, &document.network_interface)
.find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::star::IDENTIFIER))?;
let Some(&TaggedValue::U32(sides)) = inputs.get(1).and_then(|i| i.as_value()) else {
return None;
};
let Some(&TaggedValue::F64(outer_radius)) = inputs.get(2).and_then(|i| i.as_value()) else {
return None;
};
let Some(&TaggedValue::F64(inner_radius)) = inputs.get(3).and_then(|i| i.as_value()) else {
return None;
};
Some((sides, outer_radius, inner_radius))
})
}

#[tokio::test]
async fn star_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
draw_star(&mut editor, 0., 0., 100., 100., ModifierKeys::empty()).await;

assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let (sides, outer_radius, _) = get_star_inputs(&editor).expect("Star node should exist after draw");
assert!(sides >= 2, "Star should have at least 2 points, got {sides}");
assert!(outer_radius > 0., "Outer radius should be positive, got {outer_radius}");
}

#[tokio::test]
async fn star_inner_radius_is_half_outer() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
draw_star(&mut editor, 0., 0., 100., 100., ModifierKeys::empty()).await;

let (_, outer_radius, inner_radius) = get_star_inputs(&editor).expect("Star node should exist");
assert!(
(inner_radius - outer_radius / 2.).abs() < 1e-10,
"Inner radius {inner_radius} should equal outer_radius/2 = {}",
outer_radius / 2.
);
}

#[tokio::test]
async fn star_draw_correct_outer_radius() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
// 100x100 drag: dimensions=(100,100), x==y, radius = x/2 = 50
draw_star(&mut editor, 0., 0., 100., 100., ModifierKeys::empty()).await;

let (_, outer_radius, _) = get_star_inputs(&editor).expect("Star node should exist");
assert!((outer_radius - 50.).abs() < 1., "Expected outer radius ~50 for 100x100 drag, got {outer_radius}");
}

#[tokio::test]
async fn star_cancel_rmb_no_layer() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.select_tool(ToolType::Shape).await;
editor
.handle_message(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Star),
})
.await;
editor.drag_tool_cancel_rmb(ToolType::Shape).await;

assert_eq!(
editor.active_document().metadata().all_layers().count(),
0,
"RMB-cancelled star should not create a layer"
);
}
}