Skip to main content

math_geometry_2d/
surface.rs

1//! Library-owned runtime surface for `math-geometry-2d`.
2
3use runtime_core::{
4    describe_surface_response, parse_surface_input, structured_operation_response,
5    surface_operation, validate_max_items, OperationId, PackageSurface, RuntimeCapabilities,
6    SurfaceError, SurfaceOperation, SurfaceRequest, SurfaceResponse,
7};
8use serde::Deserialize;
9
10use crate::{Affine2, Bounds2f, LineSegment2, Point2f, Polygon2f, RectF32};
11
12const MAX_VALUES: usize = 100_000;
13
14/// Returns the package surface exposed by every transport wrapper.
15pub fn package_surface() -> PackageSurface {
16    PackageSurface {
17        library: env!("CARGO_PKG_NAME").to_string(),
18        version: env!("CARGO_PKG_VERSION").to_string(),
19        capabilities: RuntimeCapabilities::pure_rust(),
20        operations: vec![
21            operation(
22                "describe",
23                "Describe package",
24                "Shared 2D geometry contracts for multimodal image, video, and layout processing.",
25                serde_json::json!({"includeOperations": true}),
26            ),
27            operation(
28                "geometry.bounds",
29                "Point bounds",
30                "Computes 2D bounds, dimensions, and center for finite points.",
31                serde_json::json!({"points": [[0.0, 1.0], [2.0, 3.0]]}),
32            ),
33            operation(
34                "geometry.transform",
35                "Transform points",
36                "Applies an affine transform to finite 2D points and returns transformed bounds.",
37                serde_json::json!({
38                    "points": [[1.0, 2.0]],
39                    "transform": {"m11": 1.0, "m12": 0.0, "m21": 0.0, "m22": 1.0, "tx": 2.0, "ty": 3.0}
40                }),
41            ),
42            operation(
43                "geometry.intersections",
44                "Rectangle intersections",
45                "Checks every rectangle pair and returns intersection rectangles when present.",
46                serde_json::json!({"rects": [{"x": 0.0, "y": 0.0, "width": 2.0, "height": 2.0}]}),
47            ),
48            operation(
49                "geometry.overlap",
50                "Rectangle overlap",
51                "Computes intersection area, IoU, and directional overlap ratios for two rectangles.",
52                serde_json::json!({"left": {"x": 0.0, "y": 0.0, "width": 2.0, "height": 2.0}, "right": {"x": 1.0, "y": 1.0, "width": 2.0, "height": 2.0}}),
53            ),
54            operation(
55                "geometry.segmentIntersection",
56                "Segment intersection",
57                "Computes the point and segment parameters for two finite 2D segment intersections.",
58                serde_json::json!({"left": {"start": [0.0, 0.0], "end": [2.0, 2.0]}, "right": {"start": [0.0, 2.0], "end": [2.0, 0.0]}}),
59            ),
60            operation(
61                "geometry.polygonSummary",
62                "Polygon summary",
63                "Reports area, winding, centroid, and bounds for a finite 2D polygon.",
64                serde_json::json!({"points": [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]]}),
65            ),
66        ],
67    }
68}
69
70fn operation(
71    id: &str,
72    name: &str,
73    description: &str,
74    example_request: serde_json::Value,
75) -> SurfaceOperation {
76    let mut operation = surface_operation(id, name, description, example_request);
77    if id == "geometry.transform" {
78        runtime_core::attach_landscape_contract(
79            &mut operation,
80            runtime_core::landscape::LandscapeOperationContract::new(
81                runtime_core::landscape::LandscapeFunction::new(
82                    "geometry.transformPoints",
83                    env!("CARGO_PKG_NAME"),
84                )
85                .input(
86                    runtime_core::landscape::LandscapePort::new(
87                        "points",
88                        runtime_core::landscape::well_known::geometry_point2f(),
89                    )
90                    .many(),
91                )
92                .output(
93                    runtime_core::landscape::LandscapePort::new(
94                        "points",
95                        runtime_core::landscape::well_known::geometry_point2f(),
96                    )
97                    .many(),
98                ),
99            ),
100        );
101    }
102    operation
103}
104
105/// Runs one library-owned operation.
106pub fn run_surface_operation(request: SurfaceRequest) -> Result<SurfaceResponse, String> {
107    let surface = package_surface();
108    let operation = request.operation.clone();
109    let value = match request.operation.as_str() {
110        "describe" => return Ok(describe_surface_response(&surface, request)),
111        "geometry.bounds" => bounds_value(
112            operation.as_str(),
113            parse_surface_input(Some(operation.as_str()), request.input)?,
114        )?,
115        "geometry.transform" => transform_value(
116            operation.as_str(),
117            parse_surface_input(Some(operation.as_str()), request.input)?,
118        )?,
119        "geometry.intersections" => intersections_value(
120            operation.as_str(),
121            parse_surface_input(Some(operation.as_str()), request.input)?,
122        )?,
123        "geometry.overlap" => overlap_value(
124            operation.as_str(),
125            parse_surface_input(Some(operation.as_str()), request.input)?,
126        )?,
127        "geometry.segmentIntersection" => segment_intersection_value(
128            operation.as_str(),
129            parse_surface_input(Some(operation.as_str()), request.input)?,
130        )?,
131        "geometry.polygonSummary" => polygon_summary_value(
132            operation.as_str(),
133            parse_surface_input(Some(operation.as_str()), request.input)?,
134        )?,
135        operation => {
136            return Err(
137                SurfaceError::unsupported_operation(operation, env!("CARGO_PKG_NAME"))
138                    .to_error_string(),
139            );
140        }
141    };
142    Ok(structured_operation_response(&surface, operation, value))
143}
144
145#[derive(Debug, Deserialize)]
146#[serde(rename_all = "camelCase")]
147struct PointsRequest {
148    points: Vec<[f32; 2]>,
149}
150
151#[derive(Debug, Deserialize)]
152#[serde(rename_all = "camelCase")]
153struct TransformRequest {
154    points: Vec<[f32; 2]>,
155    transform: Affine2,
156}
157
158#[derive(Debug, Deserialize)]
159#[serde(rename_all = "camelCase")]
160struct IntersectionsRequest {
161    rects: Vec<RectF32>,
162}
163
164#[derive(Debug, Deserialize)]
165#[serde(rename_all = "camelCase")]
166struct OverlapRequest {
167    left: RectF32,
168    right: RectF32,
169}
170
171#[derive(Debug, Deserialize)]
172#[serde(rename_all = "camelCase")]
173struct SegmentEndpointRequest {
174    start: [f32; 2],
175    end: [f32; 2],
176}
177
178#[derive(Debug, Deserialize)]
179#[serde(rename_all = "camelCase")]
180struct SegmentIntersectionRequest {
181    left: SegmentEndpointRequest,
182    right: SegmentEndpointRequest,
183}
184
185fn bounds_value(operation: &str, request: PointsRequest) -> Result<serde_json::Value, String> {
186    let points = points_from_arrays(operation, request.points)?;
187    bounds_json(operation, bounds_for_points(operation, &points)?)
188}
189
190fn transform_value(
191    operation: &str,
192    request: TransformRequest,
193) -> Result<serde_json::Value, String> {
194    request
195        .transform
196        .validate()
197        .map_err(|error| invalid_request(operation, error.to_string()))?;
198    let points = points_from_arrays(operation, request.points)?;
199    let transformed = points
200        .into_iter()
201        .map(|point| request.transform.apply_point(point))
202        .collect::<Vec<_>>();
203    let bounds = bounds_for_points(operation, &transformed)?;
204    Ok(serde_json::json!({
205        "determinant": request.transform.determinant().map_err(|error| invalid_request(operation, error.to_string()))?,
206        "points": transformed.iter().map(point_array).collect::<Vec<_>>(),
207        "bounds": bounds_json(operation, bounds)?
208    }))
209}
210
211fn intersections_value(
212    operation: &str,
213    request: IntersectionsRequest,
214) -> Result<serde_json::Value, String> {
215    validate_max_items(operation, "rects", request.rects.len(), MAX_VALUES)?;
216    for rect in &request.rects {
217        rect.validate()
218            .map_err(|error| invalid_request(operation, error.to_string()))?;
219    }
220    let mut pairs = Vec::new();
221    for left_index in 0..request.rects.len() {
222        for right_index in (left_index + 1)..request.rects.len() {
223            let left = request.rects[left_index];
224            let right = request.rects[right_index];
225            let intersection = left
226                .intersection(right)
227                .map_err(|error| invalid_request(operation, error.to_string()))?;
228            pairs.push(serde_json::json!({
229                "left": left_index,
230                "right": right_index,
231                "intersects": intersection.is_some(),
232                "intersection": intersection.map(rect_json)
233            }));
234        }
235    }
236    Ok(serde_json::json!({
237        "rectCount": request.rects.len(),
238        "pairs": pairs
239    }))
240}
241
242fn overlap_value(operation: &str, request: OverlapRequest) -> Result<serde_json::Value, String> {
243    request
244        .left
245        .validate()
246        .map_err(|error| invalid_request(operation, error.to_string()))?;
247    request
248        .right
249        .validate()
250        .map_err(|error| invalid_request(operation, error.to_string()))?;
251    let intersection = request
252        .left
253        .intersection(request.right)
254        .map_err(|error| invalid_request(operation, error.to_string()))?;
255    let intersection_area = intersection
256        .map(|rect| rect.area())
257        .transpose()
258        .map_err(|error| invalid_request(operation, error.to_string()))?
259        .unwrap_or(0.0);
260    Ok(serde_json::json!({
261        "intersects": intersection.is_some(),
262        "intersection": intersection.map(rect_json),
263        "intersectionArea": intersection_area,
264        "iou": request.left.iou(request.right).map_err(|error| invalid_request(operation, error.to_string()))?,
265        "leftOverlapRatio": request.left.overlap_ratio(request.right).map_err(|error| invalid_request(operation, error.to_string()))?,
266        "rightOverlapRatio": request.right.overlap_ratio(request.left).map_err(|error| invalid_request(operation, error.to_string()))?
267    }))
268}
269
270fn segment_intersection_value(
271    operation: &str,
272    request: SegmentIntersectionRequest,
273) -> Result<serde_json::Value, String> {
274    let left = segment_from_request(operation, request.left)?;
275    let right = segment_from_request(operation, request.right)?;
276    let intersection = left
277        .intersection(right)
278        .map_err(|error| invalid_request(operation, error.to_string()))?;
279    Ok(serde_json::json!({
280        "intersects": intersection.is_some(),
281        "intersection": intersection.map(|value| serde_json::json!({
282            "point": point_array(&value.point),
283            "leftT": value.left_t,
284            "rightT": value.right_t
285        }))
286    }))
287}
288
289fn polygon_summary_value(
290    operation: &str,
291    request: PointsRequest,
292) -> Result<serde_json::Value, String> {
293    let polygon = Polygon2f::new(points_from_arrays(operation, request.points)?)
294        .map_err(|error| invalid_request(operation, error.to_string()))?;
295    Ok(serde_json::json!({
296        "pointCount": polygon.points().len(),
297        "area": polygon.area().map_err(|error| invalid_request(operation, error.to_string()))?,
298        "signedArea": polygon.signed_area().map_err(|error| invalid_request(operation, error.to_string()))?,
299        "clockwise": polygon.is_clockwise().map_err(|error| invalid_request(operation, error.to_string()))?,
300        "centroid": point_array(&polygon.centroid().map_err(|error| invalid_request(operation, error.to_string()))?),
301        "bounds": bounds_json(operation, polygon.bounds().map_err(|error| invalid_request(operation, error.to_string()))?)?
302    }))
303}
304
305fn points_from_arrays(operation: &str, points: Vec<[f32; 2]>) -> Result<Vec<Point2f>, String> {
306    if points.is_empty() {
307        return Err(invalid_request(operation, "points must not be empty"));
308    }
309    validate_max_items(operation, "points", points.len(), MAX_VALUES)?;
310    points
311        .into_iter()
312        .map(|point| Point2f::new(point[0], point[1]))
313        .collect::<Result<Vec<_>, _>>()
314        .map_err(|error| invalid_request(operation, error.to_string()))
315}
316
317fn bounds_for_points(operation: &str, points: &[Point2f]) -> Result<Bounds2f, String> {
318    Bounds2f::from_points(points).map_err(|error| invalid_request(operation, error.to_string()))
319}
320
321fn bounds_json(operation: &str, bounds: Bounds2f) -> Result<serde_json::Value, String> {
322    Ok(serde_json::json!({
323        "min": point_array(&bounds.min),
324        "max": point_array(&bounds.max),
325        "width": bounds.width(),
326        "height": bounds.height(),
327        "center": point_array(&bounds.center().map_err(|error| invalid_request(operation, error.to_string()))?)
328    }))
329}
330
331fn segment_from_request(
332    operation: &str,
333    request: SegmentEndpointRequest,
334) -> Result<LineSegment2, String> {
335    LineSegment2::new(
336        Point2f::new(request.start[0], request.start[1])
337            .map_err(|error| invalid_request(operation, error.to_string()))?,
338        Point2f::new(request.end[0], request.end[1])
339            .map_err(|error| invalid_request(operation, error.to_string()))?,
340    )
341    .map_err(|error| invalid_request(operation, error.to_string()))
342}
343
344fn point_array(point: &Point2f) -> [f32; 2] {
345    [point.x, point.y]
346}
347
348fn rect_json(rect: RectF32) -> serde_json::Value {
349    serde_json::json!({
350        "x": rect.x,
351        "y": rect.y,
352        "width": rect.width,
353        "height": rect.height
354    })
355}
356
357fn invalid_request(operation: &str, message: impl Into<String>) -> String {
358    SurfaceError::invalid_request(Some(OperationId::new(operation)), message).to_error_string()
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn bounds_reports_dimensions_and_center() {
367        let response = run_surface_operation(SurfaceRequest {
368            operation: OperationId::new("geometry.bounds"),
369            input: serde_json::json!({"points": [[0.0, 1.0], [2.0, 5.0]]}),
370        })
371        .expect("bounds operation");
372
373        assert_eq!(response.value["min"], serde_json::json!([0.0, 1.0]));
374        assert_eq!(response.value["max"], serde_json::json!([2.0, 5.0]));
375        assert_eq!(response.value["center"], serde_json::json!([1.0, 3.0]));
376    }
377
378    #[test]
379    fn transform_applies_affine() {
380        let response = run_surface_operation(SurfaceRequest {
381            operation: OperationId::new("geometry.transform"),
382            input: serde_json::json!({
383                "points": [[1.0, 2.0]],
384                "transform": {"m11": 2.0, "m12": 0.0, "m21": 0.0, "m22": 3.0, "tx": 1.0, "ty": -1.0}
385            }),
386        })
387        .expect("transform operation");
388
389        assert_eq!(response.value["determinant"], 6.0);
390        assert_eq!(response.value["points"], serde_json::json!([[3.0, 5.0]]));
391    }
392
393    #[test]
394    fn intersections_reports_pairs() {
395        let response = run_surface_operation(SurfaceRequest {
396            operation: OperationId::new("geometry.intersections"),
397            input: serde_json::json!({"rects": [
398                {"x": 0.0, "y": 0.0, "width": 2.0, "height": 2.0},
399                {"x": 1.0, "y": 1.0, "width": 2.0, "height": 2.0}
400            ]}),
401        })
402        .expect("intersections operation");
403
404        assert_eq!(response.value["rectCount"], 2);
405        assert_eq!(response.value["pairs"][0]["intersects"], true);
406    }
407
408    #[test]
409    fn overlap_segment_and_polygon_operations_run() {
410        for operation in [
411            "geometry.overlap",
412            "geometry.segmentIntersection",
413            "geometry.polygonSummary",
414        ] {
415            let surface_operation = package_surface()
416                .operations
417                .into_iter()
418                .find(|candidate| candidate.id.as_str() == operation)
419                .expect("operation metadata");
420            let response = run_surface_operation(SurfaceRequest {
421                operation: surface_operation.id,
422                input: surface_operation.example_request,
423            })
424            .unwrap_or_else(|error| panic!("{operation} failed: {error}"));
425            assert!(response.value.is_object());
426        }
427    }
428}