1use 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
14pub 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
105pub 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}