611 lines
21 KiB
Rust
611 lines
21 KiB
Rust
//! Hit testing for selection and interaction
|
|
//!
|
|
//! Provides functions for testing if points or rectangles intersect with
|
|
//! shapes and objects, taking into account transform hierarchies.
|
|
|
|
use crate::clip::ClipInstance;
|
|
use crate::layer::VectorLayer;
|
|
use crate::object::ShapeInstance;
|
|
use crate::shape::Shape;
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
use vello::kurbo::{Affine, Point, Rect, Shape as KurboShape};
|
|
|
|
/// Result of a hit test operation
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum HitResult {
|
|
/// Hit a shape instance
|
|
ShapeInstance(Uuid),
|
|
/// Hit a clip instance
|
|
ClipInstance(Uuid),
|
|
}
|
|
|
|
/// Hit test a layer at a specific point
|
|
///
|
|
/// Tests objects in reverse order (front to back) and returns the first hit.
|
|
/// Combines parent_transform with object transforms for hierarchical testing.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `layer` - The vector layer to test
|
|
/// * `point` - The point to test in screen/canvas space
|
|
/// * `tolerance` - Additional tolerance in pixels for stroke hit testing
|
|
/// * `parent_transform` - Transform from parent GraphicsObject(s)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The UUID of the first object hit, or None if no hit
|
|
pub fn hit_test_layer(
|
|
layer: &VectorLayer,
|
|
point: Point,
|
|
tolerance: f64,
|
|
parent_transform: Affine,
|
|
) -> Option<Uuid> {
|
|
// Test objects in reverse order (back to front in Vec = front to back for hit testing)
|
|
for object in layer.shape_instances.iter().rev() {
|
|
// Get the shape for this object
|
|
let shape = layer.get_shape(&object.shape_id)?;
|
|
|
|
// Combine parent transform with object transform
|
|
let combined_transform = parent_transform * object.to_affine();
|
|
|
|
if hit_test_shape(shape, point, tolerance, combined_transform) {
|
|
return Some(object.id);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Hit test a single shape with a given transform
|
|
///
|
|
/// Tests if a point hits the shape, considering both fill and stroke.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `shape` - The shape to test
|
|
/// * `point` - The point to test in screen/canvas space
|
|
/// * `tolerance` - Additional tolerance in pixels for stroke hit testing
|
|
/// * `transform` - The combined transform to apply to the shape
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// true if the point hits the shape, false otherwise
|
|
pub fn hit_test_shape(
|
|
shape: &Shape,
|
|
point: Point,
|
|
tolerance: f64,
|
|
transform: Affine,
|
|
) -> bool {
|
|
// Transform point to shape's local space
|
|
// We need the inverse transform to go from screen space to shape space
|
|
let inverse_transform = transform.inverse();
|
|
let local_point = inverse_transform * point;
|
|
|
|
// Check if point is inside filled path
|
|
if shape.fill_color.is_some() {
|
|
if shape.path().contains(local_point) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check stroke bounds if has stroke
|
|
if let Some(stroke_style) = &shape.stroke_style {
|
|
let stroke_tolerance = stroke_style.width / 2.0 + tolerance;
|
|
|
|
// For stroke hit testing, we need to check if the point is within
|
|
// stroke_tolerance distance of the path
|
|
// kurbo's winding() method can be used, or we can check bounding box first
|
|
|
|
// Quick bounding box check with stroke tolerance
|
|
let bbox = shape.path().bounding_box();
|
|
let expanded_bbox = bbox.inflate(stroke_tolerance, stroke_tolerance);
|
|
|
|
if !expanded_bbox.contains(local_point) {
|
|
return false;
|
|
}
|
|
|
|
// For more accurate stroke hit testing, we would need to:
|
|
// 1. Stroke the path with the stroke width
|
|
// 2. Check if the point is contained in the stroked outline
|
|
// For now, we do a simpler bounding box check
|
|
// TODO: Implement accurate stroke hit testing using kurbo's stroke functionality
|
|
|
|
// Simple approach: if within expanded bbox, consider it a hit for now
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Hit test objects within a rectangle (for marquee selection)
|
|
///
|
|
/// Returns all objects whose bounding boxes intersect with the given rectangle.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `layer` - The vector layer to test
|
|
/// * `rect` - The selection rectangle in screen/canvas space
|
|
/// * `parent_transform` - Transform from parent GraphicsObject(s)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Vector of UUIDs for all objects that intersect the rectangle
|
|
pub fn hit_test_objects_in_rect(
|
|
layer: &VectorLayer,
|
|
rect: Rect,
|
|
parent_transform: Affine,
|
|
) -> Vec<Uuid> {
|
|
let mut hits = Vec::new();
|
|
|
|
for object in &layer.shape_instances {
|
|
// Get the shape for this object
|
|
if let Some(shape) = layer.get_shape(&object.shape_id) {
|
|
// Combine parent transform with object transform
|
|
let combined_transform = parent_transform * object.to_affine();
|
|
|
|
// Get shape bounding box in local space
|
|
let bbox = shape.path().bounding_box();
|
|
|
|
// Transform bounding box to screen space
|
|
let transformed_bbox = combined_transform.transform_rect_bbox(bbox);
|
|
|
|
// Check if rectangles intersect
|
|
if rect.intersect(transformed_bbox).area() > 0.0 {
|
|
hits.push(object.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
hits
|
|
}
|
|
|
|
/// Get the bounding box of an object in screen space
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `object` - The object to get bounds for
|
|
/// * `shape` - The shape definition
|
|
/// * `parent_transform` - Transform from parent GraphicsObject(s)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The bounding box in screen/canvas space
|
|
pub fn get_object_bounds(
|
|
object: &ShapeInstance,
|
|
shape: &Shape,
|
|
parent_transform: Affine,
|
|
) -> Rect {
|
|
let combined_transform = parent_transform * object.to_affine();
|
|
let local_bbox = shape.path().bounding_box();
|
|
combined_transform.transform_rect_bbox(local_bbox)
|
|
}
|
|
|
|
/// Hit test a single clip instance with a given clip bounds
|
|
///
|
|
/// Tests if a point hits the clip instance's bounding box.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `clip_instance` - The clip instance to test
|
|
/// * `clip_width` - The clip's width in pixels
|
|
/// * `clip_height` - The clip's height in pixels
|
|
/// * `point` - The point to test in screen/canvas space
|
|
/// * `parent_transform` - Transform from parent layer/clip
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// true if the point hits the clip instance, false otherwise
|
|
pub fn hit_test_clip_instance(
|
|
clip_instance: &ClipInstance,
|
|
clip_width: f64,
|
|
clip_height: f64,
|
|
point: Point,
|
|
parent_transform: Affine,
|
|
) -> bool {
|
|
// Create bounding rectangle for the clip (top-left origin)
|
|
let clip_rect = Rect::new(0.0, 0.0, clip_width, clip_height);
|
|
|
|
// Combine parent transform with clip instance transform
|
|
let combined_transform = parent_transform * clip_instance.transform.to_affine();
|
|
|
|
// Transform the bounding rectangle to screen space
|
|
let transformed_rect = combined_transform.transform_rect_bbox(clip_rect);
|
|
|
|
// Test if point is inside the transformed rectangle
|
|
transformed_rect.contains(point)
|
|
}
|
|
|
|
/// Get the bounding box of a clip instance in screen space
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `clip_instance` - The clip instance to get bounds for
|
|
/// * `clip_width` - The clip's width in pixels
|
|
/// * `clip_height` - The clip's height in pixels
|
|
/// * `parent_transform` - Transform from parent layer/clip
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The bounding box in screen/canvas space
|
|
pub fn get_clip_instance_bounds(
|
|
clip_instance: &ClipInstance,
|
|
clip_width: f64,
|
|
clip_height: f64,
|
|
parent_transform: Affine,
|
|
) -> Rect {
|
|
let clip_rect = Rect::new(0.0, 0.0, clip_width, clip_height);
|
|
let combined_transform = parent_transform * clip_instance.transform.to_affine();
|
|
combined_transform.transform_rect_bbox(clip_rect)
|
|
}
|
|
|
|
/// Hit test clip instances at a specific point
|
|
///
|
|
/// Tests clip instances in reverse order (front to back) and returns the first hit.
|
|
/// Uses dynamic bounds calculation based on clip content and current time.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `clip_instances` - The clip instances to test
|
|
/// * `document` - Document containing all clip definitions
|
|
/// * `point` - The point to test in screen/canvas space
|
|
/// * `parent_transform` - Transform from parent layer/clip
|
|
/// * `timeline_time` - Current timeline time for evaluating animations
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The UUID of the first clip instance hit, or None if no hit
|
|
pub fn hit_test_clip_instances(
|
|
clip_instances: &[ClipInstance],
|
|
document: &crate::document::Document,
|
|
point: Point,
|
|
parent_transform: Affine,
|
|
timeline_time: f64,
|
|
) -> Option<Uuid> {
|
|
// Test in reverse order (front to back)
|
|
for clip_instance in clip_instances.iter().rev() {
|
|
// Calculate clip-local time from timeline time
|
|
// Apply timeline offset and playback speed, then add trim offset
|
|
let clip_time = ((timeline_time - clip_instance.timeline_start) * clip_instance.playback_speed) + clip_instance.trim_start;
|
|
|
|
// Get dynamic clip bounds from content at this time
|
|
let content_bounds = if let Some(vector_clip) = document.get_vector_clip(&clip_instance.clip_id) {
|
|
vector_clip.calculate_content_bounds(document, clip_time)
|
|
} else if let Some(video_clip) = document.get_video_clip(&clip_instance.clip_id) {
|
|
Rect::new(0.0, 0.0, video_clip.width, video_clip.height)
|
|
} else {
|
|
// Clip not found or is audio (no spatial representation)
|
|
continue;
|
|
};
|
|
|
|
// Transform content bounds to screen space
|
|
let clip_transform = parent_transform * clip_instance.transform.to_affine();
|
|
let clip_bbox = clip_transform.transform_rect_bbox(content_bounds);
|
|
|
|
// Test if point is inside the transformed rectangle
|
|
if clip_bbox.contains(point) {
|
|
return Some(clip_instance.id);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Hit test clip instances within a rectangle (for marquee selection)
|
|
///
|
|
/// Returns all clip instances whose bounding boxes intersect with the given rectangle.
|
|
/// Uses dynamic bounds calculation based on clip content and current time.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `clip_instances` - The clip instances to test
|
|
/// * `document` - Document containing all clip definitions
|
|
/// * `rect` - The selection rectangle in screen/canvas space
|
|
/// * `parent_transform` - Transform from parent layer/clip
|
|
/// * `timeline_time` - Current timeline time for evaluating animations
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Vector of UUIDs for all clip instances that intersect the rectangle
|
|
pub fn hit_test_clip_instances_in_rect(
|
|
clip_instances: &[ClipInstance],
|
|
document: &crate::document::Document,
|
|
rect: Rect,
|
|
parent_transform: Affine,
|
|
timeline_time: f64,
|
|
) -> Vec<Uuid> {
|
|
let mut hits = Vec::new();
|
|
|
|
for clip_instance in clip_instances {
|
|
// Calculate clip-local time from timeline time
|
|
// Apply timeline offset and playback speed, then add trim offset
|
|
let clip_time = ((timeline_time - clip_instance.timeline_start) * clip_instance.playback_speed) + clip_instance.trim_start;
|
|
|
|
// Get dynamic clip bounds from content at this time
|
|
let content_bounds = if let Some(vector_clip) = document.get_vector_clip(&clip_instance.clip_id) {
|
|
vector_clip.calculate_content_bounds(document, clip_time)
|
|
} else if let Some(video_clip) = document.get_video_clip(&clip_instance.clip_id) {
|
|
Rect::new(0.0, 0.0, video_clip.width, video_clip.height)
|
|
} else {
|
|
// Clip not found or is audio (no spatial representation)
|
|
continue;
|
|
};
|
|
|
|
// Transform content bounds to screen space
|
|
let clip_transform = parent_transform * clip_instance.transform.to_affine();
|
|
let clip_bbox = clip_transform.transform_rect_bbox(content_bounds);
|
|
|
|
// Check if rectangles intersect
|
|
if rect.intersect(clip_bbox).area() > 0.0 {
|
|
hits.push(clip_instance.id);
|
|
}
|
|
}
|
|
|
|
hits
|
|
}
|
|
|
|
/// Result of a vector editing hit test
|
|
///
|
|
/// Represents different types of hits in order of priority:
|
|
/// ControlPoint > Vertex > Curve > Fill
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum VectorEditHit {
|
|
/// Hit a control point (BezierEdit tool only)
|
|
ControlPoint {
|
|
shape_instance_id: Uuid,
|
|
curve_index: usize,
|
|
point_index: u8, // 1 or 2 (p1 or p2 of cubic bezier)
|
|
},
|
|
/// Hit a vertex (anchor point)
|
|
Vertex {
|
|
shape_instance_id: Uuid,
|
|
vertex_index: usize,
|
|
},
|
|
/// Hit a curve segment
|
|
Curve {
|
|
shape_instance_id: Uuid,
|
|
curve_index: usize,
|
|
parameter_t: f64, // Where on the curve (0.0 to 1.0)
|
|
},
|
|
/// Hit shape fill
|
|
Fill { shape_instance_id: Uuid },
|
|
}
|
|
|
|
/// Tolerances for vector editing hit testing (in screen pixels)
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct EditingHitTolerance {
|
|
/// Tolerance for hitting control points
|
|
pub control_point: f64,
|
|
/// Tolerance for hitting vertices
|
|
pub vertex: f64,
|
|
/// Tolerance for hitting curves
|
|
pub curve: f64,
|
|
/// Tolerance for hitting fill (usually 0.0 for exact containment)
|
|
pub fill: f64,
|
|
}
|
|
|
|
impl Default for EditingHitTolerance {
|
|
fn default() -> Self {
|
|
Self {
|
|
control_point: 10.0,
|
|
vertex: 15.0,
|
|
curve: 15.0,
|
|
fill: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EditingHitTolerance {
|
|
/// Create tolerances scaled by zoom factor
|
|
///
|
|
/// When zoomed in, hit targets appear larger in screen pixels,
|
|
/// so we divide by zoom to maintain consistent screen-space sizes.
|
|
pub fn scaled_by_zoom(zoom: f64) -> Self {
|
|
Self {
|
|
control_point: 10.0 / zoom,
|
|
vertex: 15.0 / zoom,
|
|
curve: 15.0 / zoom,
|
|
fill: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Hit test for vector editing with priority-based detection
|
|
///
|
|
/// Tests objects in reverse order (front to back) and returns the first hit.
|
|
/// Priority order: Control points > Vertices > Curves > Fill
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `layer` - The vector layer to test
|
|
/// * `point` - The point to test in screen/canvas space
|
|
/// * `tolerance` - Hit tolerances for different element types
|
|
/// * `parent_transform` - Transform from parent GraphicsObject(s)
|
|
/// * `show_control_points` - Whether to test control points (BezierEdit tool)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The first hit in priority order, or None if no hit
|
|
pub fn hit_test_vector_editing(
|
|
layer: &VectorLayer,
|
|
point: Point,
|
|
tolerance: &EditingHitTolerance,
|
|
parent_transform: Affine,
|
|
show_control_points: bool,
|
|
) -> Option<VectorEditHit> {
|
|
use crate::bezpath_editing::extract_editable_curves;
|
|
use vello::kurbo::{ParamCurve, ParamCurveNearest};
|
|
|
|
// Test objects in reverse order (front to back for hit testing)
|
|
for object in layer.shape_instances.iter().rev() {
|
|
// Get the shape for this object
|
|
let shape = match layer.get_shape(&object.shape_id) {
|
|
Some(s) => s,
|
|
None => continue,
|
|
};
|
|
|
|
// Combine parent transform with object transform
|
|
let combined_transform = parent_transform * object.to_affine();
|
|
let inverse_transform = combined_transform.inverse();
|
|
let local_point = inverse_transform * point;
|
|
|
|
// Calculate the scale factor to transform screen-space tolerances to local space
|
|
// We need the inverse scale because we're in local space
|
|
// Affine coefficients are [a, b, c, d, e, f] representing matrix [[a, c, e], [b, d, f]]
|
|
let coeffs = combined_transform.as_coeffs();
|
|
let scale_x = (coeffs[0].powi(2) + coeffs[1].powi(2)).sqrt();
|
|
let scale_y = (coeffs[2].powi(2) + coeffs[3].powi(2)).sqrt();
|
|
let avg_scale = (scale_x + scale_y) / 2.0;
|
|
let local_tolerance_factor = 1.0 / avg_scale.max(0.001); // Avoid division by zero
|
|
|
|
// Extract editable curves and vertices from the shape's path
|
|
let editable = extract_editable_curves(shape.path());
|
|
|
|
// Priority 1: Control points (only in BezierEdit mode)
|
|
if show_control_points {
|
|
let local_cp_tolerance = tolerance.control_point * local_tolerance_factor;
|
|
for (i, curve) in editable.curves.iter().enumerate() {
|
|
// Test p1 (first control point)
|
|
let dist_p1 = (curve.p1 - local_point).hypot();
|
|
if dist_p1 < local_cp_tolerance {
|
|
return Some(VectorEditHit::ControlPoint {
|
|
shape_instance_id: object.id,
|
|
curve_index: i,
|
|
point_index: 1,
|
|
});
|
|
}
|
|
|
|
// Test p2 (second control point)
|
|
let dist_p2 = (curve.p2 - local_point).hypot();
|
|
if dist_p2 < local_cp_tolerance {
|
|
return Some(VectorEditHit::ControlPoint {
|
|
shape_instance_id: object.id,
|
|
curve_index: i,
|
|
point_index: 2,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Priority 2: Vertices (anchor points)
|
|
let local_vertex_tolerance = tolerance.vertex * local_tolerance_factor;
|
|
for (i, vertex) in editable.vertices.iter().enumerate() {
|
|
let dist = (vertex.point - local_point).hypot();
|
|
if dist < local_vertex_tolerance {
|
|
return Some(VectorEditHit::Vertex {
|
|
shape_instance_id: object.id,
|
|
vertex_index: i,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Priority 3: Curves
|
|
let local_curve_tolerance = tolerance.curve * local_tolerance_factor;
|
|
for (i, curve) in editable.curves.iter().enumerate() {
|
|
let nearest = curve.nearest(local_point, 1e-6);
|
|
let nearest_point = curve.eval(nearest.t);
|
|
let dist = (nearest_point - local_point).hypot();
|
|
if dist < local_curve_tolerance {
|
|
return Some(VectorEditHit::Curve {
|
|
shape_instance_id: object.id,
|
|
curve_index: i,
|
|
parameter_t: nearest.t,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Priority 4: Fill
|
|
if shape.fill_color.is_some() && shape.path().contains(local_point) {
|
|
return Some(VectorEditHit::Fill {
|
|
shape_instance_id: object.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::shape::ShapeColor;
|
|
use vello::kurbo::{Circle, Shape as KurboShape};
|
|
|
|
#[test]
|
|
fn test_hit_test_simple_circle() {
|
|
let mut layer = VectorLayer::new("Test Layer");
|
|
|
|
// Create a circle at (100, 100) with radius 50
|
|
let circle = Circle::new((100.0, 100.0), 50.0);
|
|
let path = circle.to_path(0.1);
|
|
let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));
|
|
let shape_instance = ShapeInstance::new(shape.id);
|
|
|
|
layer.add_shape(shape);
|
|
layer.add_object(shape_instance);
|
|
|
|
// Test hit inside circle
|
|
let hit = hit_test_layer(&layer, Point::new(100.0, 100.0), 0.0, Affine::IDENTITY);
|
|
assert!(hit.is_some());
|
|
|
|
// Test miss outside circle
|
|
let miss = hit_test_layer(&layer, Point::new(200.0, 200.0), 0.0, Affine::IDENTITY);
|
|
assert!(miss.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_hit_test_with_transform() {
|
|
let mut layer = VectorLayer::new("Test Layer");
|
|
|
|
// Create a circle at origin
|
|
let circle = Circle::new((0.0, 0.0), 50.0);
|
|
let path = circle.to_path(0.1);
|
|
let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));
|
|
|
|
// Create shape instance with translation
|
|
let shape_instance = ShapeInstance::new(shape.id).with_position(100.0, 100.0);
|
|
|
|
layer.add_shape(shape);
|
|
layer.add_object(shape_instance);
|
|
|
|
// Test hit at translated position
|
|
let hit = hit_test_layer(&layer, Point::new(100.0, 100.0), 0.0, Affine::IDENTITY);
|
|
assert!(hit.is_some());
|
|
|
|
// Test miss at origin (where shape is defined, but object is translated)
|
|
let miss = hit_test_layer(&layer, Point::new(0.0, 0.0), 0.0, Affine::IDENTITY);
|
|
assert!(miss.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_marquee_selection() {
|
|
let mut layer = VectorLayer::new("Test Layer");
|
|
|
|
// Create two circles
|
|
let circle1 = Circle::new((50.0, 50.0), 20.0);
|
|
let path1 = circle1.to_path(0.1);
|
|
let shape1 = Shape::new(path1).with_fill(ShapeColor::rgb(255, 0, 0));
|
|
let shape_instance1 = ShapeInstance::new(shape1.id);
|
|
|
|
let circle2 = Circle::new((150.0, 150.0), 20.0);
|
|
let path2 = circle2.to_path(0.1);
|
|
let shape2 = Shape::new(path2).with_fill(ShapeColor::rgb(0, 255, 0));
|
|
let shape_instance2 = ShapeInstance::new(shape2.id);
|
|
|
|
layer.add_shape(shape1);
|
|
layer.add_object(shape_instance1);
|
|
layer.add_shape(shape2);
|
|
layer.add_object(shape_instance2);
|
|
|
|
// Marquee that contains both circles
|
|
let rect = Rect::new(0.0, 0.0, 200.0, 200.0);
|
|
let hits = hit_test_objects_in_rect(&layer, rect, Affine::IDENTITY);
|
|
assert_eq!(hits.len(), 2);
|
|
|
|
// Marquee that contains only first circle
|
|
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
|
let hits = hit_test_objects_in_rect(&layer, rect, Affine::IDENTITY);
|
|
assert_eq!(hits.len(), 1);
|
|
}
|
|
}
|