1use 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
95fn 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
149pub 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}