Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
tool.rs

1//! Cold JSON boundary shared by MCP and desktop/qapp hosts.
2
3use serde_json::{json, Value};
4
5use super::{
6    build_triangle_half_edges, convex_hull_indices_2, delaunay_triangulation_2,
7    nearest_site_brute_force, orientation_2, required_edge_slots, voronoi_diagram_2, EdgeSlot,
8    HalfEdge, Point2, INVALID_INDEX,
9};
10use crate::container_10d::topology_section::{
11    decode_topology_section, encode_topology_section, encoded_len as topology_encoded_len,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum GeometryToolError {
16    InvalidJson,
17    InvalidOperation,
18    InvalidParameters,
19    Geometry(String),
20}
21
22impl core::fmt::Display for GeometryToolError {
23    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24        match self {
25            Self::InvalidJson => write!(f, "invalid JSON"),
26            Self::InvalidOperation => write!(f, "unknown geometry operation"),
27            Self::InvalidParameters => write!(f, "invalid geometry parameters"),
28            Self::Geometry(message) => f.write_str(message),
29        }
30    }
31}
32
33fn point(value: &Value) -> Result<Point2, GeometryToolError> {
34    let pair = value
35        .as_array()
36        .filter(|pair| pair.len() >= 2)
37        .ok_or(GeometryToolError::InvalidParameters)?;
38    let x = pair[0]
39        .as_f64()
40        .ok_or(GeometryToolError::InvalidParameters)?;
41    let y = pair[1]
42        .as_f64()
43        .ok_or(GeometryToolError::InvalidParameters)?;
44    if !x.is_finite() || !y.is_finite() {
45        return Err(GeometryToolError::InvalidParameters);
46    }
47    Ok(Point2::new(x, y))
48}
49
50fn points(value: &Value, key: &str) -> Result<Vec<Point2>, GeometryToolError> {
51    value
52        .get(key)
53        .and_then(Value::as_array)
54        .ok_or(GeometryToolError::InvalidParameters)?
55        .iter()
56        .map(point)
57        .collect()
58}
59
60fn triangles(value: &Value) -> Result<Vec<[u32; 3]>, GeometryToolError> {
61    value
62        .get("triangles")
63        .and_then(Value::as_array)
64        .ok_or(GeometryToolError::InvalidParameters)?
65        .iter()
66        .map(|triangle| {
67            let values = triangle
68                .as_array()
69                .filter(|values| values.len() == 3)
70                .ok_or(GeometryToolError::InvalidParameters)?;
71            Ok([
72                u32::try_from(
73                    values[0]
74                        .as_u64()
75                        .ok_or(GeometryToolError::InvalidParameters)?,
76                )
77                .map_err(|_| GeometryToolError::InvalidParameters)?,
78                u32::try_from(
79                    values[1]
80                        .as_u64()
81                        .ok_or(GeometryToolError::InvalidParameters)?,
82                )
83                .map_err(|_| GeometryToolError::InvalidParameters)?,
84                u32::try_from(
85                    values[2]
86                        .as_u64()
87                        .ok_or(GeometryToolError::InvalidParameters)?,
88                )
89                .map_err(|_| GeometryToolError::InvalidParameters)?,
90            ])
91        })
92        .collect()
93}
94
95/// Parse a mesh from a JSON value with `positions` and `triangles` arrays.
96fn parse_mesh_json(value: &Value) -> Result<crate::render::assets::Mesh, GeometryToolError> {
97    let pos_arr = value
98        .get("positions")
99        .and_then(Value::as_array)
100        .ok_or(GeometryToolError::InvalidParameters)?;
101    let tri_arr = value
102        .get("triangles")
103        .and_then(Value::as_array)
104        .ok_or(GeometryToolError::InvalidParameters)?;
105
106    let mut positions = Vec::with_capacity(pos_arr.len());
107    for p in pos_arr {
108        let pa = p.as_array().ok_or(GeometryToolError::InvalidParameters)?;
109        if pa.len() != 3 {
110            return Err(GeometryToolError::InvalidParameters);
111        }
112        positions.push([
113            pa[0].as_f64().ok_or(GeometryToolError::InvalidParameters)? as f32,
114            pa[1].as_f64().ok_or(GeometryToolError::InvalidParameters)? as f32,
115            pa[2].as_f64().ok_or(GeometryToolError::InvalidParameters)? as f32,
116        ]);
117    }
118
119    let mut triangles = Vec::with_capacity(tri_arr.len());
120    for t in tri_arr {
121        let ta = t.as_array().ok_or(GeometryToolError::InvalidParameters)?;
122        if ta.len() != 3 {
123            return Err(GeometryToolError::InvalidParameters);
124        }
125        triangles.push([
126            ta[0].as_u64().ok_or(GeometryToolError::InvalidParameters)? as u32,
127            ta[1].as_u64().ok_or(GeometryToolError::InvalidParameters)? as u32,
128            ta[2].as_u64().ok_or(GeometryToolError::InvalidParameters)? as u32,
129        ]);
130    }
131
132    let mut min = [f32::INFINITY; 3];
133    let mut max = [f32::NEG_INFINITY; 3];
134    for p in &positions {
135        for a in 0..3 {
136            min[a] = min[a].min(p[a]);
137            max[a] = max[a].max(p[a]);
138        }
139    }
140
141    Ok(crate::render::assets::Mesh {
142        positions,
143        triangles,
144        min,
145        max,
146    })
147}
148
149/// Execute a computational-geometry operation over a serde JSON boundary.
150///
151/// Allocations belong only to this explicit qapp/MCP boundary; every algorithm
152/// below delegates to the caller-buffered native kernel.
153pub fn execute_geometry_tool_json(args: &str) -> Result<String, GeometryToolError> {
154    let value: Value = serde_json::from_str(args).map_err(|_| GeometryToolError::InvalidJson)?;
155    let op = value
156        .get("op")
157        .and_then(Value::as_str)
158        .ok_or(GeometryToolError::InvalidParameters)?;
159    match op {
160        "orientation_2" => {
161            let input = points(&value, "points")?;
162            if input.len() != 3 {
163                return Err(GeometryToolError::InvalidParameters);
164            }
165            let orientation = orientation_2(input[0], input[1], input[2]);
166            Ok(json!({
167                "op": op,
168                "orientation": match orientation {
169                    super::Orientation::Clockwise => "clockwise",
170                    super::Orientation::Collinear => "collinear",
171                    super::Orientation::CounterClockwise => "counter_clockwise",
172                },
173                "value": orientation as i8,
174            })
175            .to_string())
176        }
177        "convex_hull_2" => {
178            let input = points(&value, "points")?;
179            let mut scratch = vec![0u32; input.len().saturating_mul(3)];
180            let mut indices = vec![0u32; input.len()];
181            let count = convex_hull_indices_2(&input, &mut scratch, &mut indices)
182                .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?;
183            indices.truncate(count);
184            let hull: Vec<[f64; 2]> = indices
185                .iter()
186                .map(|&index| {
187                    let point = input[index as usize];
188                    [point.x, point.y]
189                })
190                .collect();
191            Ok(json!({
192                "op": op,
193                "indices": indices,
194                "points": hull,
195                "vertex_count": count,
196            })
197            .to_string())
198        }
199        "triangle_topology" => {
200            let vertex_count = value
201                .get("vertex_count")
202                .and_then(Value::as_u64)
203                .and_then(|count| u32::try_from(count).ok())
204                .ok_or(GeometryToolError::InvalidParameters)?;
205            let triangles = triangles(&value)?;
206            let mut edges = vec![HalfEdge::default(); triangles.len().saturating_mul(3)];
207            let mut slots = vec![EdgeSlot::default(); required_edge_slots(triangles.len())];
208            let summary =
209                build_triangle_half_edges(vertex_count, &triangles, &mut edges, &mut slots)
210                    .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?;
211            Ok(json!({
212                "op": op,
213                "summary": summary,
214                "half_edges": edges,
215            })
216            .to_string())
217        }
218        "mesh_topology" => {
219            let vertex_count = value
220                .get("vertex_count")
221                .and_then(Value::as_u64)
222                .and_then(|count| u32::try_from(count).ok())
223                .ok_or(GeometryToolError::InvalidParameters)?;
224            let tris = triangles(&value)?;
225            let face_count = tris.len() as u32;
226            let mut edges = vec![HalfEdge::default(); tris.len().saturating_mul(3)];
227            let mut slots = vec![EdgeSlot::default(); required_edge_slots(tris.len())];
228            build_triangle_half_edges(vertex_count, &tris, &mut edges, &mut slots)
229                .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?;
230
231            let need = topology_encoded_len(vertex_count, face_count, edges.len() as u32);
232            let mut buf = vec![0u8; need];
233            let n = encode_topology_section(vertex_count, face_count, &edges, &mut buf)
234                .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?;
235            buf.truncate(n);
236
237            let header = decode_topology_section(&buf)
238                .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?
239                .header;
240
241            let genus = if header.genus == INVALID_INDEX {
242                Value::Null
243            } else {
244                json!(header.genus)
245            };
246
247            Ok(json!({
248                "op": op,
249                "section_bytes": n,
250                "header": {
251                    "vertex_count": header.vertex_count,
252                    "face_count": header.face_count,
253                    "half_edge_count": header.half_edge_count,
254                    "boundary_loop_count": header.boundary_loop_count,
255                    "component_count": header.component_count,
256                    "euler_characteristic": header.euler_characteristic,
257                    "genus": genus,
258                },
259            })
260            .to_string())
261        }
262        "delaunay_2" => {
263            let input = points(&value, "points")?;
264            let n = input.len();
265            if n < 3 {
266                return Err(GeometryToolError::InvalidParameters);
267            }
268            let mut scratch = vec![0u32; n];
269            let mut tris = vec![[0u32; 3]; 2 * n + 1];
270            let count = delaunay_triangulation_2(&input, &mut scratch, &mut tris)
271                .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?;
272            tris.truncate(count);
273            Ok(json!({
274                "op": op,
275                "triangle_count": count,
276                "triangles": tris,
277            })
278            .to_string())
279        }
280        "voronoi_2" => {
281            let input = points(&value, "points")?;
282            let n = input.len();
283            if n < 3 {
284                return Err(GeometryToolError::InvalidParameters);
285            }
286            let mut tri_scratch = vec![0u32; n];
287            let mut tri_out = vec![[0u32; 3]; 2 * n + 1];
288            let mut verts = vec![
289                super::VoronoiVertex {
290                    triangle_index: 0,
291                    center: Point2::new(0.0, 0.0)
292                };
293                2 * n + 1
294            ];
295            let mut edges = vec![
296                super::VoronoiEdge {
297                    site_a: 0,
298                    site_b: 0,
299                    triangle: 0,
300                    neighbor_triangle: None
301                };
302                3 * n
303            ];
304            let (vc, ec) = voronoi_diagram_2(
305                &input,
306                &mut tri_scratch,
307                &mut tri_out,
308                &mut verts,
309                &mut edges,
310            )
311            .map_err(|error| GeometryToolError::Geometry(format!("{error:?}")))?;
312            let vert_json: Vec<Value> = verts[..vc]
313                .iter()
314                .map(|v| {
315                    json!({
316                        "triangle": v.triangle_index,
317                        "center": [v.center.x, v.center.y],
318                    })
319                })
320                .collect();
321            let edge_json: Vec<Value> = edges[..ec]
322                .iter()
323                .map(|e| {
324                    json!({
325                        "sites": [e.site_a, e.site_b],
326                        "triangle": e.triangle,
327                        "neighbor": e.neighbor_triangle,
328                    })
329                })
330                .collect();
331            Ok(json!({
332                "op": op,
333                "vertex_count": vc,
334                "edge_count": ec,
335                "vertices": vert_json,
336                "edges": edge_json,
337            })
338            .to_string())
339        }
340        "nearest_site" => {
341            let input = points(&value, "points")?;
342            let query_arr = value
343                .get("query")
344                .and_then(Value::as_array)
345                .ok_or(GeometryToolError::InvalidParameters)?;
346            if query_arr.len() < 2 {
347                return Err(GeometryToolError::InvalidParameters);
348            }
349            let qx = query_arr[0]
350                .as_f64()
351                .ok_or(GeometryToolError::InvalidParameters)?;
352            let qy = query_arr[1]
353                .as_f64()
354                .ok_or(GeometryToolError::InvalidParameters)?;
355            let idx = nearest_site_brute_force(&input, Point2::new(qx, qy))
356                .ok_or(GeometryToolError::InvalidParameters)?;
357            Ok(json!({
358                "op": op,
359                "nearest_index": idx,
360                "nearest_point": [input[idx as usize].x, input[idx as usize].y],
361            })
362            .to_string())
363        }
364        "create_box" => {
365            let width = value
366                .get("width")
367                .and_then(Value::as_f64)
368                .ok_or(GeometryToolError::InvalidParameters)?;
369            let height = value
370                .get("height")
371                .and_then(Value::as_f64)
372                .ok_or(GeometryToolError::InvalidParameters)?;
373            let depth = value
374                .get("depth")
375                .and_then(Value::as_f64)
376                .ok_or(GeometryToolError::InvalidParameters)?;
377            let mesh = super::authoring::box_mesh(width as f32, height as f32, depth as f32)
378                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
379            Ok(json!({
380                "op": op,
381                "vertex_count": mesh.positions.len(),
382                "triangle_count": mesh.triangles.len(),
383                "positions": mesh.positions,
384                "triangles": mesh.triangles,
385            })
386            .to_string())
387        }
388        "create_sphere" => {
389            let radius = value
390                .get("radius")
391                .and_then(Value::as_f64)
392                .ok_or(GeometryToolError::InvalidParameters)?;
393            let lat = value
394                .get("lat_segments")
395                .and_then(Value::as_u64)
396                .unwrap_or(8) as u32;
397            let lon = value
398                .get("lon_segments")
399                .and_then(Value::as_u64)
400                .unwrap_or(16) as u32;
401            let mesh = super::authoring::uv_sphere(radius as f32, lat, lon)
402                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
403            Ok(json!({
404                "op": op,
405                "vertex_count": mesh.positions.len(),
406                "triangle_count": mesh.triangles.len(),
407                "positions": mesh.positions,
408                "triangles": mesh.triangles,
409            })
410            .to_string())
411        }
412        "create_cylinder" => {
413            let radius = value
414                .get("radius")
415                .and_then(Value::as_f64)
416                .ok_or(GeometryToolError::InvalidParameters)?;
417            let height = value
418                .get("height")
419                .and_then(Value::as_f64)
420                .ok_or(GeometryToolError::InvalidParameters)?;
421            let segments = value.get("segments").and_then(Value::as_u64).unwrap_or(16) as u32;
422            let mesh = super::authoring::cylinder(radius as f32, height as f32, segments)
423                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
424            Ok(json!({
425                "op": op,
426                "vertex_count": mesh.positions.len(),
427                "triangle_count": mesh.triangles.len(),
428                "positions": mesh.positions,
429                "triangles": mesh.triangles,
430            })
431            .to_string())
432        }
433        "create_plane" => {
434            let size = value
435                .get("size")
436                .and_then(Value::as_f64)
437                .ok_or(GeometryToolError::InvalidParameters)?;
438            let mesh = super::authoring::plane(size as f32)
439                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
440            Ok(json!({
441                "op": op,
442                "vertex_count": mesh.positions.len(),
443                "triangle_count": mesh.triangles.len(),
444                "positions": mesh.positions,
445                "triangles": mesh.triangles,
446            })
447            .to_string())
448        }
449        "create_torus" => {
450            let major = value
451                .get("major_radius")
452                .and_then(Value::as_f64)
453                .ok_or(GeometryToolError::InvalidParameters)?;
454            let minor = value
455                .get("minor_radius")
456                .and_then(Value::as_f64)
457                .ok_or(GeometryToolError::InvalidParameters)?;
458            let maj_seg = value
459                .get("major_segments")
460                .and_then(Value::as_u64)
461                .unwrap_or(16) as u32;
462            let min_seg = value
463                .get("minor_segments")
464                .and_then(Value::as_u64)
465                .unwrap_or(8) as u32;
466            let mesh = super::authoring::torus(major as f32, minor as f32, maj_seg, min_seg)
467                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
468            Ok(json!({
469                "op": op,
470                "vertex_count": mesh.positions.len(),
471                "triangle_count": mesh.triangles.len(),
472                "positions": mesh.positions,
473                "triangles": mesh.triangles,
474            })
475            .to_string())
476        }
477        "create_grid" => {
478            let size = value
479                .get("size")
480                .and_then(Value::as_f64)
481                .ok_or(GeometryToolError::InvalidParameters)?;
482            let subs = value
483                .get("subdivisions")
484                .and_then(Value::as_u64)
485                .unwrap_or(4) as u32;
486            let mesh = super::authoring::grid(size as f32, subs)
487                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
488            Ok(json!({
489                "op": op,
490                "vertex_count": mesh.positions.len(),
491                "triangle_count": mesh.triangles.len(),
492                "positions": mesh.positions,
493                "triangles": mesh.triangles,
494            })
495            .to_string())
496        }
497        "boolean_union" | "boolean_intersect" | "boolean_difference" => {
498            let bool_op = match op {
499                "boolean_union" => super::authoring::BooleanOp::Union,
500                "boolean_intersect" => super::authoring::BooleanOp::Intersection,
501                _ => super::authoring::BooleanOp::Difference,
502            };
503            let mesh_a = parse_mesh_json(
504                value
505                    .get("mesh_a")
506                    .ok_or(GeometryToolError::InvalidParameters)?,
507            )?;
508            let mesh_b = parse_mesh_json(
509                value
510                    .get("mesh_b")
511                    .ok_or(GeometryToolError::InvalidParameters)?,
512            )?;
513            let result = super::authoring::boolean_op(&mesh_a, &mesh_b, bool_op)
514                .map_err(|e| GeometryToolError::Geometry(format!("{e}")))?;
515            Ok(json!({
516                "op": op,
517                "vertex_count": result.positions.len(),
518                "triangle_count": result.triangles.len(),
519                "positions": result.positions,
520                "triangles": result.triangles,
521            })
522            .to_string())
523        }
524        "drag_vertex" => {
525            let mesh = parse_mesh_json(
526                value
527                    .get("mesh")
528                    .ok_or(GeometryToolError::InvalidParameters)?,
529            )?;
530            let vi = value
531                .get("vertex_index")
532                .and_then(Value::as_u64)
533                .ok_or(GeometryToolError::InvalidParameters)? as usize;
534            let np = value
535                .get("new_position")
536                .and_then(Value::as_array)
537                .ok_or(GeometryToolError::InvalidParameters)?;
538            if np.len() != 3 {
539                return Err(GeometryToolError::InvalidParameters);
540            }
541            let new_pos = [
542                np[0].as_f64().ok_or(GeometryToolError::InvalidParameters)? as f32,
543                np[1].as_f64().ok_or(GeometryToolError::InvalidParameters)? as f32,
544                np[2].as_f64().ok_or(GeometryToolError::InvalidParameters)? as f32,
545            ];
546            let prior_t = value.get("prior_t").and_then(Value::as_f64).unwrap_or(0.0) as f32;
547            let consent = value
548                .get("consent_granted")
549                .and_then(Value::as_bool)
550                .unwrap_or(true);
551            let drag_consent = super::authoring::DragConsent {
552                consent_granted: consent,
553                sealed_prior: true,
554            };
555            match super::authoring::drag_vertex(&mesh, vi, new_pos, prior_t, drag_consent) {
556                Ok(result) => Ok(json!({
557                    "op": "drag_vertex",
558                    "new_t": result.new_t,
559                    "prior_t": result.prior_t,
560                    "vertex_count": result.mesh.positions.len(),
561                    "triangle_count": result.mesh.triangles.len(),
562                    "positions": result.mesh.positions,
563                    "triangles": result.mesh.triangles,
564                })
565                .to_string()),
566                Err(e) => Ok(json!({
567                    "op": "drag_vertex",
568                    "error": format!("{e}"),
569                    "refused": matches!(e, super::authoring::DragError::GovernanceRefused),
570                })
571                .to_string()),
572            }
573        }
574        _ => Err(GeometryToolError::InvalidOperation),
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581
582    #[test]
583    fn json_hull_routes_to_native_kernel() {
584        let result = execute_geometry_tool_json(
585            r#"{"op":"convex_hull_2","points":[[0,0],[1,0],[0.5,0.5],[1,1],[0,1]]}"#,
586        )
587        .unwrap();
588        let value: Value = serde_json::from_str(&result).unwrap();
589        assert_eq!(value["vertex_count"], 4);
590        assert_eq!(value["indices"], json!([0, 1, 3, 4]));
591    }
592
593    #[test]
594    fn json_delaunay_returns_triangles() {
595        let result =
596            execute_geometry_tool_json(r#"{"op":"delaunay_2","points":[[0,0],[1,0],[1,1],[0,1]]}"#)
597                .unwrap();
598        let value: Value = serde_json::from_str(&result).unwrap();
599        assert_eq!(value["op"], "delaunay_2");
600        assert_eq!(value["triangle_count"], 2);
601        let tris = value["triangles"].as_array().unwrap();
602        assert_eq!(tris.len(), 2);
603    }
604
605    #[test]
606    fn json_voronoi_returns_vertices_and_edges() {
607        let result =
608            execute_geometry_tool_json(r#"{"op":"voronoi_2","points":[[0,0],[2,0],[2,2],[0,2]]}"#)
609                .unwrap();
610        let value: Value = serde_json::from_str(&result).unwrap();
611        assert_eq!(value["op"], "voronoi_2");
612        assert!(value["vertex_count"].as_u64().unwrap() > 0);
613        assert!(value["edge_count"].as_u64().unwrap() > 0);
614    }
615
616    #[test]
617    fn json_nearest_site_returns_index() {
618        let result = execute_geometry_tool_json(
619            r#"{"op":"nearest_site","points":[[0,0],[3,0],[2,3]],"query":[0.1,0.1]}"#,
620        )
621        .unwrap();
622        let value: Value = serde_json::from_str(&result).unwrap();
623        assert_eq!(value["op"], "nearest_site");
624        assert_eq!(value["nearest_index"], 0);
625    }
626
627    #[test]
628    fn mesh_topology_returns_connectivity_invariants() {
629        let result = execute_geometry_tool_json(
630            r#"{"op":"mesh_topology","vertex_count":4,"triangles":[[0,1,2],[0,2,3],[0,3,1],[1,3,2]]}"#,
631        )
632        .unwrap();
633        let value: Value = serde_json::from_str(&result).unwrap();
634        assert_eq!(value["op"], "mesh_topology");
635        assert_eq!(value["header"]["vertex_count"], 4);
636        assert_eq!(value["header"]["face_count"], 4);
637        assert_eq!(value["header"]["half_edge_count"], 12);
638        assert_eq!(value["header"]["boundary_loop_count"], 0);
639        assert_eq!(value["header"]["component_count"], 1);
640        assert_eq!(value["header"]["euler_characteristic"], 2);
641        assert_eq!(value["header"]["genus"], 0);
642        assert!(value["section_bytes"].as_u64().unwrap() > 0);
643    }
644
645    #[test]
646    fn json_create_box_returns_mesh() {
647        let result = execute_geometry_tool_json(
648            r#"{"op":"create_box","width":2.0,"height":3.0,"depth":4.0}"#,
649        )
650        .unwrap();
651        let value: Value = serde_json::from_str(&result).unwrap();
652        assert_eq!(value["op"], "create_box");
653        assert_eq!(value["vertex_count"], 8);
654        assert_eq!(value["triangle_count"], 12);
655    }
656
657    #[test]
658    fn json_create_sphere_returns_mesh() {
659        let result = execute_geometry_tool_json(
660            r#"{"op":"create_sphere","radius":1.0,"lat_segments":8,"lon_segments":16}"#,
661        )
662        .unwrap();
663        let value: Value = serde_json::from_str(&result).unwrap();
664        assert_eq!(value["op"], "create_sphere");
665        assert!(value["vertex_count"].as_u64().unwrap() > 0);
666        assert!(value["triangle_count"].as_u64().unwrap() > 0);
667    }
668
669    #[test]
670    fn json_create_cylinder_returns_mesh() {
671        let result = execute_geometry_tool_json(
672            r#"{"op":"create_cylinder","radius":1.0,"height":2.0,"segments":8}"#,
673        )
674        .unwrap();
675        let value: Value = serde_json::from_str(&result).unwrap();
676        assert_eq!(value["op"], "create_cylinder");
677        assert_eq!(value["vertex_count"], 18);
678    }
679
680    #[test]
681    fn json_create_plane_returns_mesh() {
682        let result = execute_geometry_tool_json(r#"{"op":"create_plane","size":2.0}"#).unwrap();
683        let value: Value = serde_json::from_str(&result).unwrap();
684        assert_eq!(value["op"], "create_plane");
685        assert_eq!(value["vertex_count"], 4);
686        assert_eq!(value["triangle_count"], 2);
687    }
688
689    #[test]
690    fn json_create_torus_returns_mesh() {
691        let result = execute_geometry_tool_json(
692            r#"{"op":"create_torus","major_radius":1.0,"minor_radius":0.3,"major_segments":16,"minor_segments":8}"#,
693        ).unwrap();
694        let value: Value = serde_json::from_str(&result).unwrap();
695        assert_eq!(value["op"], "create_torus");
696        assert_eq!(value["vertex_count"], 128);
697        assert_eq!(value["triangle_count"], 256);
698    }
699
700    #[test]
701    fn json_create_grid_returns_mesh() {
702        let result =
703            execute_geometry_tool_json(r#"{"op":"create_grid","size":2.0,"subdivisions":4}"#)
704                .unwrap();
705        let value: Value = serde_json::from_str(&result).unwrap();
706        assert_eq!(value["op"], "create_grid");
707        assert_eq!(value["vertex_count"], 25);
708        assert_eq!(value["triangle_count"], 32);
709    }
710
711    #[test]
712    fn json_boolean_union_of_disjoint_cubes() {
713        let box_json = r#"{"positions":[[-0.5,-0.5,-0.5],[0.5,-0.5,-0.5],[0.5,0.5,-0.5],[-0.5,0.5,-0.5],[-0.5,-0.5,0.5],[0.5,-0.5,0.5],[0.5,0.5,0.5],[-0.5,0.5,0.5]],"triangles":[[0,1,2],[0,2,3],[4,6,5],[4,7,6],[0,4,5],[0,5,1],[2,6,7],[2,7,3],[0,3,7],[0,7,4],[1,5,6],[1,6,2]]}"#;
714        let result = execute_geometry_tool_json(&format!(
715            r#"{{"op":"boolean_union","mesh_a":{box_json},"mesh_b":{box_json}}}"#
716        ))
717        .unwrap();
718        let value: Value = serde_json::from_str(&result).unwrap();
719        assert_eq!(value["op"], "boolean_union");
720        assert_eq!(
721            value["triangle_count"], 12,
722            "union of identical cubes = one cube"
723        );
724    }
725
726    #[test]
727    fn json_drag_vertex_produces_new_t_slice() {
728        let result = execute_geometry_tool_json(
729            r#"{"op":"drag_vertex","mesh":{"positions":[[-0.5,-0.5,-0.5],[0.5,-0.5,-0.5],[0.5,0.5,-0.5],[-0.5,0.5,-0.5],[-0.5,-0.5,0.5],[0.5,-0.5,0.5],[0.5,0.5,0.5],[-0.5,0.5,0.5]],"triangles":[[0,1,2],[0,2,3],[4,6,5],[4,7,6],[0,4,5],[0,5,1],[2,6,7],[2,7,3],[0,3,7],[0,7,4],[1,5,6],[1,6,2]]},"vertex_index":0,"new_position":[1.0,2.0,3.0],"prior_t":10.0,"consent_granted":true}"#,
730        ).unwrap();
731        let value: Value = serde_json::from_str(&result).unwrap();
732        assert_eq!(value["op"], "drag_vertex");
733        assert_eq!(value["new_t"], 11.0);
734        assert_eq!(value["prior_t"], 10.0);
735    }
736
737    #[test]
738    fn json_drag_vertex_governance_refused() {
739        let result = execute_geometry_tool_json(
740            r#"{"op":"drag_vertex","mesh":{"positions":[[-0.5,-0.5,-0.5],[0.5,-0.5,-0.5],[0.5,0.5,-0.5],[-0.5,0.5,-0.5],[-0.5,-0.5,0.5],[0.5,-0.5,0.5],[0.5,0.5,0.5],[-0.5,0.5,0.5]],"triangles":[[0,1,2],[0,2,3],[4,6,5],[4,7,6],[0,4,5],[0,5,1],[2,6,7],[2,7,3],[0,3,7],[0,7,4],[1,5,6],[1,6,2]]},"vertex_index":0,"new_position":[1.0,2.0,3.0],"prior_t":10.0,"consent_granted":false}"#,
741        ).unwrap();
742        let value: Value = serde_json::from_str(&result).unwrap();
743        assert_eq!(value["op"], "drag_vertex");
744        assert_eq!(value["refused"], true);
745    }
746}