Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,64 @@ impl Polygon {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
#[cfg(test)]
mod test_polygon {
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::test_utils::test_prelude::*;
use graph_craft::document::value::TaggedValue;
struct ResolvedPolygon {
vertices: u32,
radius: f64,
}
async fn get_polygons(editor: &mut EditorTestUtils) -> Vec<ResolvedPolygon> {
let document = editor.active_document();
let network_interface = &document.network_interface;
document
.metadata()
.all_layers()
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, network_interface)
.find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER))?;
let Some(&TaggedValue::U32(vertices)) = node_inputs[1].as_value() else {
return None;
};
let Some(&TaggedValue::F64(radius)) = node_inputs[2].as_value() else {
return None;
};
Some(ResolvedPolygon { vertices, radius })
})
.collect()
}
#[tokio::test]
async fn polygon_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Shape, 0., 0., 60., 60., ModifierKeys::empty()).await;
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let polys = get_polygons(&mut editor).await;
assert_eq!(polys.len(), 1);
// Default vertices count (6 for Shape tool default)
assert!(polys[0].vertices >= 3, "polygon should have at least 3 vertices");
// For a 60×60 drag both dimensions equal → radius = smaller_dim / 2 = 30
assert!((polys[0].radius - 30.).abs() < 1e-10);
}
#[tokio::test]
async fn polygon_draw_non_square() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
// Drag a non-square region: 40 wide, 60 tall → smaller dimension is 40 → radius = 20
editor.drag_tool(ToolType::Shape, 0., 0., 40., 60., ModifierKeys::empty()).await;
let polys = get_polygons(&mut editor).await;
assert_eq!(polys.len(), 1);
assert!((polys[0].radius - 20.).abs() < 1e-10);
}
#[tokio::test]
async fn polygon_cancel() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Shape).await;
let polys = get_polygons(&mut editor).await;
assert_eq!(polys.len(), 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,64 @@ impl Rectangle {
}
}
}
#[cfg(test)]
mod test_polygon {
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::test_utils::test_prelude::*;
use graph_craft::document::value::TaggedValue;
struct ResolvedPolygon {
vertices: u32,
radius: f64,
}
async fn get_polygons(editor: &mut EditorTestUtils) -> Vec<ResolvedPolygon> {
let document = editor.active_document();
let network_interface = &document.network_interface;
document
.metadata()
.all_layers()
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, network_interface)
.find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::regular_polygon::IDENTIFIER))?;
let Some(&TaggedValue::U32(vertices)) = node_inputs[1].as_value() else {
return None;
};
let Some(&TaggedValue::F64(radius)) = node_inputs[2].as_value() else {
return None;
};
Some(ResolvedPolygon { vertices, radius })
})
.collect()
}
#[tokio::test]
async fn polygon_draw_simple() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool(ToolType::Shape, 0., 0., 60., 60., ModifierKeys::empty()).await;
assert_eq!(editor.active_document().metadata().all_layers().count(), 1);
let polys = get_polygons(&mut editor).await;
assert_eq!(polys.len(), 1);
// Default vertices count (6 for Shape tool default)
assert!(polys[0].vertices >= 3, "polygon should have at least 3 vertices");
// For a 60×60 drag both dimensions equal → radius = smaller_dim / 2 = 30
assert!((polys[0].radius - 30.).abs() < 1e-10);
}
#[tokio::test]
async fn polygon_draw_non_square() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
// Drag a non-square region: 40 wide, 60 tall → smaller dimension is 40 → radius = 20
editor.drag_tool(ToolType::Shape, 0., 0., 40., 60., ModifierKeys::empty()).await;
let polys = get_polygons(&mut editor).await;
assert_eq!(polys.len(), 1);
assert!((polys[0].radius - 20.).abs() < 1e-10);
}
#[tokio::test]
async fn polygon_cancel() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.drag_tool_cancel_rmb(ToolType::Shape).await;
let polys = get_polygons(&mut editor).await;
assert_eq!(polys.len(), 0);
}
}
120 changes: 120 additions & 0 deletions editor/src/messages/tool/common_functionality/snapping/snap_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,123 @@ pub struct SnappedCurve {
pub point: SnappedPoint,
pub document_curve: PathSeg,
}

#[cfg(test)]
mod tests {
use super::*;
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapSource, SnapSource};
use glam::DVec2;
#[test]
fn is_snapped_with_finite_distance() {
let point = SnappedPoint { distance: 3.0, ..Default::default() };
assert!(point.is_snapped());
}
#[test]
fn is_not_snapped_with_infinite_distance() {
let point = SnappedPoint::infinite_snap(DVec2::ZERO);
assert!(!point.is_snapped());
}
#[test]
fn infinite_snap_sets_position_and_infinite_distance() {
let pos = DVec2::new(10., 20.);
let point = SnappedPoint::infinite_snap(pos);
assert_eq!(point.snapped_point_document, pos);
assert!(!point.is_snapped());
assert!(point.distance.is_infinite());
}
#[test]
fn from_source_point_sets_position_and_source() {
let pos = DVec2::new(5., 15.);
let source = SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint);
let point = SnappedPoint::from_source_point(pos, source);
assert_eq!(point.snapped_point_document, pos);
assert_eq!(point.source, source);
}
#[test]
fn align_returns_true_with_horizontal_target() {
let point = SnappedPoint {
alignment_target_horizontal: Some(DVec2::ZERO),
..Default::default()
};
assert!(point.align());
}
#[test]
fn align_returns_true_with_vertical_target() {
let point = SnappedPoint {
alignment_target_vertical: Some(DVec2::ZERO),
..Default::default()
};
assert!(point.align());
}
#[test]
fn align_returns_false_with_no_targets() {
assert!(!SnappedPoint::default().align());
}
#[test]
fn other_snap_better_self_infinite_other_finite() {
let self_snap = SnappedPoint::infinite_snap(DVec2::ZERO);
let other_snap = SnappedPoint { distance: 1.0, ..Default::default() };
assert!(self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_self_finite_other_infinite() {
let self_snap = SnappedPoint { distance: 1.0, ..Default::default() };
let other_snap = SnappedPoint::infinite_snap(DVec2::ZERO);
assert!(!self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_both_infinite() {
let self_snap = SnappedPoint::infinite_snap(DVec2::ZERO);
let other_snap = SnappedPoint::infinite_snap(DVec2::ONE);
// Neither finite, so other_closer check: INF < INF + bias = false. Result: false.
assert!(!self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_when_other_significantly_closer() {
let self_snap = SnappedPoint { distance: 5.0, ..Default::default() };
let other_snap = SnappedPoint { distance: 1.0, ..Default::default() };
// 1.0 < 5.0 + 0.01 → other_closer = true
assert!(self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_when_self_significantly_closer() {
let self_snap = SnappedPoint { distance: 1.0, ..Default::default() };
let other_snap = SnappedPoint { distance: 5.0, ..Default::default() };
// 5.0 < 1.0 + 0.01 → false
assert!(!self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_constrained_beats_unconstrained_even_if_further() {
let self_snap = SnappedPoint { distance: 1.0, constrained: false, ..Default::default() };
let other_snap = SnappedPoint { distance: 2.0, constrained: true, ..Default::default() };
// other_more_constrained = true; self_more_constrained = false → other wins
assert!(self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_self_constrained_blocks_other() {
let self_snap = SnappedPoint { distance: 2.0, constrained: true, ..Default::default() };
let other_snap = SnappedPoint { distance: 1.0, constrained: false, ..Default::default() };
// self_more_constrained = true → result is false regardless of distance
assert!(!self_snap.other_snap_better(&other_snap));
}
#[test]
fn other_snap_better_prefers_non_intersection_at_same_position() {
let pos = DVec2::new(3., 4.);
let self_snap = SnappedPoint {
snapped_point_document: pos,
distance: 1.0,
constrained: true,
at_intersection: true,
..Default::default()
};
let other_snap = SnappedPoint {
snapped_point_document: pos,
distance: 1.0,
constrained: true,
at_intersection: false,
..Default::default()
};
// Both constrained at same position; self is intersection, other is not → other wins
assert!(self_snap.other_snap_better(&other_snap));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,21 @@ use kurbo::{CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, PathSeg, Point, QuadBe

/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
closest_point(document, goal, tolerance, layers, |_| false)
let mut best = None;
let mut best_distance_squared = tolerance * tolerance;
for layer in layers {
let viewspace = document.metadata().transform_to_viewport(layer);
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
for id in vector.anchor_endpoints() {
let Some(point) = vector.point_domain.position_from_id(id) else { continue };
let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
if distance_squared < best_distance_squared {
best = Some((layer, id, point));
best_distance_squared = distance_squared;
}
}
}
best
}

/// Determine the closest point to the goal point under max_distance.
Expand Down
Loading