makepad/libs/mesh_edit/tests/global_validation.rs
Admin d2130e6550 libs: model, mesh_edit, csg, scene, sim, render, xr
Squash of 25 work commits (Sep 1–12):
  237da90  render: the sprite lane hands the screen draw back the way it found it
  895b9a7  particles: an emitter can ride a body's own frame
  914471f  ai-hub: a feed session whose last socket left ends on its idle timeout; skin: parent, skinned centroid and a nodes-only rig for retargets
  3fcccf3  sim: the whole world is implicitly editable, and one seam says where the ground is
  5692fab  sim: the landform world proves itself — walker through the tunnel included
  f4af6df  sim: terrain knows who changed it — a plan layer over player history
  adbb078  render: a water volume can be physics without a picture
  c17480f  render: a non-rigid body may carry its own orientation
  acad401  sim: an agent with no route holds and retries instead of walking into the wall
  22b2bf9  sim + chat: the composed world surface takes a map floor; the chat gets plan tools
  faf8112  web path: the tessellator's lap timer, the trace span and the fusion cycle timer have no clock on the web
  d3472eb  tsdf: the clock-taking XR helpers are native-only — the browser has no depth camera and no Instant
  4946c9e  sim + render: a repaint is not a world edit — colour and glow restyles never rebake the lightmap
  4317f58  sim + render + chat: walk decks as a surface, the filmed body is no obstruction, the brief never asks
  9791279  render: support rigged models and custom materials across viewers
  3ce2792  sim: use deck geometry for collision and sensing
  c8b79ff  Add portable PBR, rig and soft-body authoring support
  108d423  Add transactional polygon modeling and editable asset documents
  38f4d3b  Refine editable modeling and firm yarn character behavior
  08a2637  sim: add entity-owned lights and vehicle headlights
  00d96a7  render: add clustered lights, local shadows and incremental GI
  cf916af  render: import glTF asset extensions and wire clustered GI
  c6d2cca  model: cut transaction memory and raise capacity limits
  41af47a  raytrace: add a CPU probe-ray BVH budget example
  c963d0a  libs: the game sim splits into makepad-scene and makepad-soft-body; render, model, fab and the asset importer retarget

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 13:40:31 +02:00

82 lines
6.6 KiB
Rust

use makepad_mesh_edit::*;
use std::cell::Cell;
fn cube(size:f64,offset:[f64;3])->Mesh{
let mut m=Mesh::cube([size;3],&mut Context::default()).unwrap();
let ids=m.vertices().iter().map(|v|v.id).collect::<Vec<_>>();
m.transform(&ids,[[1.,0.,0.,offset[0]],[0.,1.,0.,offset[1]],[0.,0.,1.,offset[2]],[0.,0.,0.,1.]],&mut Context::default()).unwrap();m
}
fn inward(m:&Mesh)->Mesh{
let positions=m.vertices().iter().map(|v|v.position).collect::<Vec<_>>();
let polygons=m.faces().iter().map(|f|{let cs=m.face_corners(f.id).unwrap();Polygon{vertices:cs.iter().rev().map(|c|m.vertices().iter().position(|v|v.id==c.vertex).unwrap()as u32).collect(),uvs:cs.iter().rev().map(|c|c.uv).collect(),material:f.material}}).collect::<Vec<_>>();
Mesh::from_polygons(&positions,&polygons,&mut Context::default()).unwrap()
}
fn soup(points:&[[f64;3]],faces:&[[u32;3]])->Mesh{
let polygons=faces.iter().map(|v|Polygon{vertices:v.to_vec(),uvs:vec![[0.;2];3],material:0}).collect::<Vec<_>>();Mesh::from_polygons(points,&polygons,&mut Context::default()).unwrap()
}
#[test]
fn closed_shells_require_global_contact_and_orientation_checks(){
let mut m=cube(2.,[0.;3]);let cheap=m.validate(&mut Context::default()).unwrap();assert!(cheap.is_closed_manifold);assert!(!cheap.is_valid_solid);assert_eq!(cheap.self_intersections,SelfIntersectionStatus::NotChecked);
let r=m.validate_global(&mut Context::default()).unwrap();assert!(r.is_valid_solid,"{r:?}");assert_eq!(r.components,1);assert_eq!(r.intersection_count,0);assert_eq!(r.local.self_intersections,SelfIntersectionStatus::Clear);
m.append(&cube(2.,[4.,0.,0.]),&mut Context::default()).unwrap();let r=m.validate_global(&mut Context::default()).unwrap();assert!(r.is_valid_solid,"{r:?}");assert_eq!(r.components,2);
let r=inward(&cube(2.,[0.;3])).validate_global(&mut Context::default()).unwrap();assert!(r.is_embedded_surface);assert!(!r.is_valid_solid);assert_eq!(r.orientation_errors,1);
}
#[test]
fn overlapping_and_distinct_id_touching_shells_are_not_certified(){
for offset in [[0.5,0.5,0.5],[2.,0.,0.],[2.,2.,2.],[0.;3]]{
let mut m=cube(2.,[0.;3]);m.append(&cube(2.,offset),&mut Context::default()).unwrap();
let r=m.validate_global(&mut Context::default()).unwrap();assert!(r.local.is_closed_manifold);assert!(!r.is_valid_solid,"offset={offset:?}, {r:?}");assert!(!r.is_embedded_surface);assert!(r.intersection_count>0);assert_eq!(r.local.self_intersections,SelfIntersectionStatus::Found);
}
}
#[test]
fn nested_cavity_requires_inward_inner_shell(){
let mut m=cube(4.,[0.;3]);m.append(&cube(2.,[0.;3]),&mut Context::default()).unwrap();let r=m.validate_global(&mut Context::default()).unwrap();assert!(r.is_embedded_surface);assert!(!r.is_valid_solid);assert_eq!(r.orientation_errors,1);
let mut m=cube(4.,[0.;3]);m.append(&inward(&cube(2.,[0.;3])),&mut Context::default()).unwrap();let r=m.validate_global(&mut Context::default()).unwrap();assert!(r.is_valid_solid,"{r:?}");assert_eq!(r.components,2);
}
#[test]
fn legal_shared_boundaries_and_illegal_crossings_are_distinguished(){
let legal=soup(&[[0.,0.,0.],[1.,0.,0.],[0.,1.,0.],[1.,1.,0.]],&[[0,1,2],[1,3,2]]);let r=legal.validate_global(&mut Context::default()).unwrap();assert!(r.is_embedded_surface,"{r:?}");assert!(!r.is_valid_solid);
// Shared edge IDs do not excuse two triangles overlapping on the same side.
let folded=soup(&[[0.,0.,0.],[2.,0.,0.],[0.,2.,0.],[0.5,0.5,0.]],&[[0,1,2],[1,0,3]]);assert!(folded.validate_global(&mut Context::default()).unwrap().intersection_count>0);
let cross=soup(&[[-2.,0.,0.],[2.,0.,0.],[0.,2.,0.],[0.,0.5,-1.],[0.,0.5,1.],[0.,-1.,0.]],&[[0,1,2],[3,4,5]]);assert!(cross.validate_global(&mut Context::default()).unwrap().intersection_count>0);
// Interior edge contact with a different vertex identity is a T-junction.
let touching=soup(&[[0.,0.,0.],[2.,0.,0.],[0.,2.,0.],[1.,0.,0.],[1.,-1.,0.],[2.,-1.,0.]],&[[0,1,2],[3,4,5]]);assert!(touching.validate_global(&mut Context::default()).unwrap().intersection_count>0);
}
#[test]
fn cancellation_and_work_or_memory_exhaustion_return_no_certificate(){
let m=cube(2.,[0.;3]);let before=m.to_bytes(&mut Context::default()).unwrap();
let calls=Cell::new(0);let count=||{calls.set(calls.get()+1);false};let mut ctx=Context::new(Limits::default(),Some(&count));m.validate_global(&mut ctx).unwrap();let complete=calls.get();calls.set(0);
let cancel=||{calls.set(calls.get()+1);calls.get()>complete-3};assert_eq!(m.validate_global(&mut Context::new(Limits::default(),Some(&cancel))).unwrap_err(),MeshError::Cancelled);
let limits=Limits{max_work:ctx.work_used()-1,..Limits::default()};assert!(matches!(m.validate_global(&mut Context::new(limits,None)),Err(MeshError::Budget{resource:"work",..})));
let tri=m.triangulate(&mut Context::default()).unwrap();let limits=Limits{max_bytes:m.memory_bytes()+tri.vertices.len()*1024+tri.triangles.len()*1024-1,..Limits::default()};assert!(matches!(m.validate_global(&mut Context::new(limits,None)),Err(MeshError::Budget{resource:"bytes",..})));
assert_eq!(before,m.to_bytes(&mut Context::default()).unwrap());
}
#[test]
fn diagnostic_page_is_bounded_but_total_pairs_are_exact(){
let triangle=soup(&[[0.,0.,0.],[1.,0.,0.],[0.,1.,0.]],&[[0,1,2]]);let mut mesh=triangle.clone();
for _ in 1..20{mesh.append(&triangle,&mut Context::default()).unwrap();}
let r=mesh.validate_global(&mut Context::default()).unwrap();assert_eq!(r.intersection_count,190);assert_eq!(r.intersections.len(),128);assert_eq!(r.intersections_omitted,62);assert!(!r.is_embedded_surface);
assert!(r.intersections.windows(2).all(|p|(p[0].a,p[0].b)<(p[1].a,p[1].b)));
}
#[test]
fn broadphase_selects_a_separating_axis_for_long_parallel_slats(){
// All 1500 triangles overlap the long axis and the normal axis; only the
// spacing axis separates them. Rotating the mesh must not change admission.
let count=1500;
for spacing_axis in 0..3{
let long_axis=(spacing_axis+1)%3;let mut positions=Vec::new();let mut polygons=Vec::new();
for i in 0..count{let base=positions.len()as u32;let mut p=[1.;3];p[spacing_axis]=2.*i as f64;
p[long_axis]=0.;positions.push(p);p[long_axis]=100.;positions.push(p);p[spacing_axis]+=0.5;positions.push(p);
polygons.push(Polygon::new(vec![base,base+1,base+2]));}
let mesh=Mesh::from_polygons(&positions,&polygons,&mut Context::default()).unwrap();
let mut ctx=Context::new(Limits{max_work:600_000,..Limits::default()},None);
let report=mesh.validate_global(&mut ctx).unwrap();assert!(report.is_embedded_surface);assert_eq!(report.candidate_pairs,0);assert_eq!(report.components,count);
}
}