Squash of 25 work commits (Sep 1–12):237da90render: the sprite lane hands the screen draw back the way it found it895b9a7particles: an emitter can ride a body's own frame914471fai-hub: a feed session whose last socket left ends on its idle timeout; skin: parent, skinned centroid and a nodes-only rig for retargets3fcccf3sim: the whole world is implicitly editable, and one seam says where the ground is5692fabsim: the landform world proves itself — walker through the tunnel includedf4af6dfsim: terrain knows who changed it — a plan layer over player historyadbb078render: a water volume can be physics without a picturec17480frender: a non-rigid body may carry its own orientationacad401sim: an agent with no route holds and retries instead of walking into the wall22b2bf9sim + chat: the composed world surface takes a map floor; the chat gets plan toolsfaf8112web path: the tessellator's lap timer, the trace span and the fusion cycle timer have no clock on the webd3472ebtsdf: the clock-taking XR helpers are native-only — the browser has no depth camera and no Instant4946c9esim + render: a repaint is not a world edit — colour and glow restyles never rebake the lightmap4317f58sim + render + chat: walk decks as a surface, the filmed body is no obstruction, the brief never asks9791279render: support rigged models and custom materials across viewers3ce2792sim: 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>
623 lines
20 KiB
Rust
623 lines
20 KiB
Rust
//! Adversarial / property-style checks against the public mesh kernel.
|
|
//! These fixtures encode claimed topology, identity, codec and rollback
|
|
//! contracts. They are not transcribed from the implementation.
|
|
use makepad_mesh_edit::*;
|
|
use std::cell::Cell;
|
|
|
|
fn ctx() -> Context<'static> {
|
|
Context::default()
|
|
}
|
|
|
|
fn ident() -> [[f64; 4]; 4] {
|
|
[
|
|
[1., 0., 0., 0.],
|
|
[0., 1., 0., 0.],
|
|
[0., 0., 1., 0.],
|
|
[0., 0., 0., 1.],
|
|
]
|
|
}
|
|
|
|
fn bytes_of(mesh: &Mesh) -> Vec<u8> {
|
|
mesh.to_bytes(&mut ctx()).expect("canonical bytes")
|
|
}
|
|
|
|
fn tetrahedron(offset: [f64; 3]) -> (Vec<[f64; 3]>, Vec<Polygon>) {
|
|
let p = vec![
|
|
[offset[0], offset[1], offset[2]],
|
|
[offset[0] + 1., offset[1], offset[2]],
|
|
[offset[0], offset[1] + 1., offset[2]],
|
|
[offset[0], offset[1], offset[2] + 1.],
|
|
];
|
|
let faces = vec![
|
|
Polygon::new(vec![0, 2, 1]),
|
|
Polygon::new(vec![0, 1, 3]),
|
|
Polygon::new(vec![0, 3, 2]),
|
|
Polygon::new(vec![1, 2, 3]),
|
|
];
|
|
(p, faces)
|
|
}
|
|
|
|
#[test]
|
|
fn plane_is_open_surface_not_solid() {
|
|
let mut c = ctx();
|
|
let mesh = Mesh::plane([2., 3.], &mut c).unwrap();
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert!(report.is_valid_surface, "{report:?}");
|
|
assert!(report.is_surface_manifold, "{report:?}");
|
|
assert!(!report.is_closed, "{report:?}");
|
|
assert!(!report.is_valid_solid, "{report:?}");
|
|
assert_eq!(report.boundary_edges, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn cube_is_closed_manifold_and_roundtrips() {
|
|
let mut c = ctx();
|
|
let mesh = Mesh::cube([1., 2., 3.], &mut c).unwrap();
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert!(report.is_closed_manifold, "{report:?}");
|
|
assert!(report.is_valid_surface, "{report:?}");
|
|
assert_eq!(report.boundary_edges, 0);
|
|
assert_eq!(report.non_manifold_edges, 0);
|
|
// Conservative: NotChecked must not certify an embedded solid.
|
|
assert!(
|
|
report.self_intersections != SelfIntersectionStatus::NotChecked || !report.is_valid_solid,
|
|
"{report:?}"
|
|
);
|
|
let bytes = bytes_of(&mesh);
|
|
let again = Mesh::from_bytes(&bytes, &mut ctx()).unwrap();
|
|
assert_eq!(bytes_of(&again), bytes);
|
|
assert_eq!(again.vertices().len(), 8);
|
|
assert_eq!(again.faces().len(), 6);
|
|
}
|
|
|
|
#[test]
|
|
fn regular_ngons_triangulate_deterministically() {
|
|
for n in 3..=8 {
|
|
let mut positions = Vec::new();
|
|
for i in 0..n {
|
|
let a = (i as f64) * std::f64::consts::TAU / (n as f64);
|
|
positions.push([a.cos(), a.sin(), 0.]);
|
|
}
|
|
let mut c = ctx();
|
|
let mesh = Mesh::from_polygons(
|
|
&positions,
|
|
&[Polygon::new((0..n as u32).collect())],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let tri = mesh.triangulate(&mut c).unwrap();
|
|
assert_eq!(tri.triangles.len(), n - 2, "n={n}");
|
|
assert_eq!(tri, mesh.triangulate(&mut c).unwrap());
|
|
let bytes = bytes_of(&mesh);
|
|
assert_eq!(bytes_of(&Mesh::from_bytes(&bytes, &mut ctx()).unwrap()), bytes);
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert!(report.is_valid_surface, "n={n} {report:?}");
|
|
assert!(!report.is_closed, "n={n}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn corner_uvs_remain_discontinuous_across_shared_vertex() {
|
|
let positions = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.]];
|
|
let mut left = Polygon::new(vec![0, 1, 3]);
|
|
left.uvs = vec![[0., 0.], [1., 0.], [0., 1.]];
|
|
let mut right = Polygon::new(vec![1, 2, 3]);
|
|
right.uvs = vec![[0., 0.], [1., 0.], [0., 1.]];
|
|
let mut c = ctx();
|
|
let mesh = Mesh::from_polygons(&positions, &[left, right], &mut c).unwrap();
|
|
let shared = mesh.vertices()[1].id;
|
|
let uvs: Vec<_> = mesh
|
|
.corners()
|
|
.iter()
|
|
.filter(|corner| corner.vertex == shared)
|
|
.map(|corner| corner.uv)
|
|
.collect();
|
|
assert_eq!(uvs.len(), 2);
|
|
assert_ne!(uvs[0], uvs[1]);
|
|
let tri = mesh.triangulate(&mut c).unwrap();
|
|
let tri_uvs: Vec<_> = tri
|
|
.vertices
|
|
.iter()
|
|
.filter(|v| v.source_vertex == shared)
|
|
.map(|v| v.uv)
|
|
.collect();
|
|
assert_eq!(tri_uvs.len(), 2);
|
|
assert_ne!(tri_uvs[0], tri_uvs[1]);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_faces_keeps_vertex_ids_and_does_not_reuse_face_ids() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::cube([2., 2., 2.], &mut c).unwrap();
|
|
let before = mesh.faces().iter().map(|f| f.id).collect::<Vec<_>>();
|
|
let removed = before[0];
|
|
let kept: Vec<_> = before.iter().copied().skip(1).collect();
|
|
let max_id_before = mesh
|
|
.corners()
|
|
.iter()
|
|
.map(|c| c.id.0)
|
|
.chain(mesh.faces().iter().map(|f| f.id.0))
|
|
.chain(mesh.vertices().iter().map(|v| v.id.0))
|
|
.max()
|
|
.unwrap();
|
|
let vertex_ids: Vec<_> = mesh.vertices().iter().map(|v| v.id).collect();
|
|
mesh.delete_faces(&[removed], &mut c).unwrap();
|
|
assert!(mesh.face(removed).is_none());
|
|
assert_eq!(
|
|
mesh.vertices().iter().map(|v| v.id).collect::<Vec<_>>(),
|
|
vertex_ids
|
|
);
|
|
for id in &kept {
|
|
assert!(mesh.face(*id).is_some(), "lost face {id:?}");
|
|
}
|
|
assert_eq!(
|
|
mesh.delete_faces(&[removed], &mut c).unwrap_err(),
|
|
MeshError::UnknownElement(ElementId::Face(removed))
|
|
);
|
|
let cap = mesh.faces()[0].id;
|
|
// [1,1,1] leaves every axis-aligned cube face.
|
|
let extrusion = mesh.extrude_face(cap, [1., 1., 1.], &mut c).unwrap();
|
|
for id in extrusion.side_faces {
|
|
assert!(id.0 > max_id_before, "reused face id {id:?}");
|
|
}
|
|
for v in mesh.vertices() {
|
|
if !vertex_ids.contains(&v.id) {
|
|
assert!(v.id.0 > max_id_before, "reused vertex id {:?}", v.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn extrude_preserves_cap_identity_and_rolls_back_on_in_plane_offset() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::cube([2., 2., 2.], &mut c).unwrap();
|
|
let face = mesh.faces()[0].id;
|
|
let corners_before: Vec<_> = mesh
|
|
.face_corners(face)
|
|
.unwrap()
|
|
.iter()
|
|
.map(|c| (c.id, c.uv, c.vertex))
|
|
.collect();
|
|
let snapshot = bytes_of(&mesh);
|
|
assert!(mesh.extrude_face(face, [0., 0., 0.], &mut c).is_err());
|
|
assert_eq!(bytes_of(&mesh), snapshot);
|
|
let result = mesh.extrude_face(face, [0., 0., 1.], &mut c).unwrap();
|
|
assert_eq!(result.cap, face);
|
|
let corners_after: Vec<_> = mesh
|
|
.face_corners(face)
|
|
.unwrap()
|
|
.iter()
|
|
.map(|c| (c.id, c.uv))
|
|
.collect();
|
|
assert_eq!(
|
|
corners_before
|
|
.iter()
|
|
.map(|(id, uv, _)| (*id, *uv))
|
|
.collect::<Vec<_>>(),
|
|
corners_after
|
|
);
|
|
for (_, _, vertex) in &corners_before {
|
|
assert!(mesh.vertex(*vertex).is_some());
|
|
}
|
|
assert!(result
|
|
.changes
|
|
.deleted
|
|
.iter()
|
|
.all(|id| *id != ElementId::Face(face)));
|
|
}
|
|
|
|
#[test]
|
|
fn weld_of_opposite_quad_vertices_rolls_back_pinched_face() {
|
|
let positions = [[0., 0., 0.], [2., 0., 0.], [2., 2., 0.], [0., 2., 0.]];
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::from_polygons(&positions, &[Polygon::new(vec![0, 1, 2, 3])], &mut c)
|
|
.unwrap();
|
|
let a = mesh.vertices()[0].id;
|
|
let c_id = mesh.vertices()[2].id;
|
|
let snapshot = bytes_of(&mesh);
|
|
assert!(mesh.weld(&[a, c_id], 10., &mut c).is_err());
|
|
assert_eq!(bytes_of(&mesh), snapshot);
|
|
}
|
|
|
|
#[test]
|
|
fn transform_empty_selection_is_noop_not_all_vertices() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::from_polygons(
|
|
&[[0., 0., 0.], [2., 0., 0.], [0., 2., 0.]],
|
|
&[Polygon::new(vec![0, 1, 2])],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let snapshot = bytes_of(&mesh);
|
|
let mut matrix = ident();
|
|
matrix[0][3] = 10.;
|
|
mesh.transform(&[], matrix, &mut c).unwrap();
|
|
assert_eq!(bytes_of(&mesh), snapshot);
|
|
let one = [mesh.vertices()[0].id];
|
|
mesh.transform(&one, matrix, &mut c).unwrap();
|
|
assert_ne!(bytes_of(&mesh), snapshot);
|
|
assert!((mesh.vertex(one[0]).unwrap().position[0] - 10.).abs() < 1e-12);
|
|
assert!((mesh.vertices()[1].position[0] - 2.).abs() < 1e-12);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_ops_leave_canonical_bytes_unchanged() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::cube([1., 1., 1.], &mut c).unwrap();
|
|
let face = mesh.faces()[0].id;
|
|
let snapshot = bytes_of(&mesh);
|
|
let mut nan = ident();
|
|
nan[0][0] = f64::NAN;
|
|
assert!(mesh.extrude_face(face, [0., 0., 0.], &mut c).is_err());
|
|
assert!(mesh.inset_face(face, 10., &mut c).is_err());
|
|
assert!(mesh.inset_face(face, 0., &mut c).is_err());
|
|
assert!(mesh.mirror(3, 0., &mut c).is_err());
|
|
let vertex = mesh.vertices()[0].id;
|
|
assert!(mesh.transform(&[vertex], nan, &mut c).is_err());
|
|
assert!(mesh.delete_faces(&[FaceId(0)], &mut c).is_err());
|
|
assert!(mesh.set_corner_uv(CornerId(0), [0., 0.], &mut c).is_err());
|
|
assert_eq!(bytes_of(&mesh), snapshot);
|
|
}
|
|
|
|
#[test]
|
|
fn cancellation_during_extrude_is_atomic() {
|
|
let mut mesh = Mesh::cube([2., 2., 2.], &mut ctx()).unwrap();
|
|
let face = mesh.faces()[0].id;
|
|
let snapshot = bytes_of(&mesh);
|
|
let calls = Cell::new(0u32);
|
|
let cancel = || {
|
|
let n = calls.get() + 1;
|
|
calls.set(n);
|
|
n > 6
|
|
};
|
|
let mut cancelled = Context::new(Limits::default(), Some(&cancel));
|
|
assert_eq!(
|
|
mesh.extrude_face(face, [0., 1., 0.], &mut cancelled)
|
|
.unwrap_err(),
|
|
MeshError::Cancelled
|
|
);
|
|
assert_eq!(bytes_of(&mesh), snapshot);
|
|
}
|
|
|
|
#[test]
|
|
fn self_intersecting_face_is_refused() {
|
|
// Include cancelling Newell area as well as an unequal-wing bow tie.
|
|
for positions in [
|
|
[[0., 0., 0.], [3., 2., 0.], [0., 1., 0.], [1., 0., 0.]],
|
|
[[0., 0., 0.], [1., 1., 0.], [1., 0., 0.], [0., 1., 0.]],
|
|
] {
|
|
let err = Mesh::from_polygons(&positions, &[Polygon::new(vec![0, 1, 2, 3])], &mut ctx())
|
|
.unwrap_err();
|
|
match err {
|
|
MeshError::InvalidGeometry {
|
|
kind: ValidationKind::SelfIntersectingFace,
|
|
..
|
|
} => {}
|
|
other => panic!("expected self-intersecting face, got {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nonplanar_cage_quad_is_triangulated_and_warns_but_planar_inset_refuses() {
|
|
let positions = [[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 1., 0.5]];
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::from_polygons(&positions, &[Polygon::new(vec![0, 1, 2, 3])], &mut c)
|
|
.expect("non-planar faces are representable");
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert!(
|
|
report
|
|
.issues
|
|
.iter()
|
|
.any(|i| i.kind == ValidationKind::NonPlanarFace),
|
|
"{report:?}"
|
|
);
|
|
assert!(
|
|
report.is_valid_surface && !report.is_valid_solid,
|
|
"a simple projected cage face is a surface, not a solid certificate: {report:?}"
|
|
);
|
|
let triangles=mesh.triangulate(&mut c).unwrap();
|
|
assert_eq!(triangles.triangles.len(),2);
|
|
let snapshot=bytes_of(&mesh);
|
|
assert_eq!(Mesh::from_bytes(&snapshot,&mut ctx()).unwrap().triangulate(&mut ctx()).unwrap(),triangles);
|
|
assert!(mesh.inset_face(mesh.faces()[0].id,0.1,&mut c).is_err());
|
|
assert_eq!(bytes_of(&mesh),snapshot);
|
|
}
|
|
|
|
#[test]
|
|
fn three_faces_on_one_edge_are_nonmanifold_not_a_solid() {
|
|
let positions = [
|
|
[0., 0., 0.],
|
|
[1., 0., 0.],
|
|
[0.5, 1., 0.],
|
|
[0.5, -1., 0.],
|
|
[0.5, 0., 1.],
|
|
];
|
|
let mut c = ctx();
|
|
let mesh = Mesh::from_polygons(
|
|
&positions,
|
|
&[
|
|
Polygon::new(vec![0, 1, 2]),
|
|
Polygon::new(vec![0, 1, 3]),
|
|
Polygon::new(vec![0, 1, 4]),
|
|
],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert!(report.non_manifold_edges >= 1, "{report:?}");
|
|
assert!(!report.is_surface_manifold, "{report:?}");
|
|
assert!(!report.is_valid_solid, "{report:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn bowtie_vertex_is_diagnosed_as_nonmanifold_vertex() {
|
|
let positions = [
|
|
[1., 1., 0.],
|
|
[2., 1., 0.],
|
|
[1.5, 2., 0.],
|
|
[0., 0., 0.],
|
|
[-1., 0., 0.],
|
|
[0., -1., 0.],
|
|
];
|
|
let mut c = ctx();
|
|
let mesh = Mesh::from_polygons(
|
|
&positions,
|
|
&[
|
|
Polygon::new(vec![0, 1, 2]),
|
|
Polygon::new(vec![3, 4, 5]),
|
|
],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
// Two disjoint triangles are manifold. Join them by welding a pair of
|
|
// distinct vertices onto one identity through a constructed bowtie:
|
|
// share vertex 0 between two fans that only meet at that point.
|
|
let bow = Mesh::from_polygons(
|
|
&[
|
|
[0., 0., 0.],
|
|
[1., 0., 0.],
|
|
[0., 1., 0.],
|
|
[-1., 0., 0.],
|
|
[0., -1., 0.],
|
|
],
|
|
&[Polygon::new(vec![0, 1, 2]), Polygon::new(vec![0, 3, 4])],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let report = bow.validate(&mut c).unwrap();
|
|
assert_eq!(report.non_manifold_vertices, 1, "{report:?}");
|
|
assert!(!report.is_valid_surface, "{report:?}");
|
|
let _ = mesh;
|
|
}
|
|
|
|
#[test]
|
|
fn interpenetrating_closed_solids_are_not_an_embedded_solid() {
|
|
let (mut positions, mut faces) = tetrahedron([0., 0., 0.]);
|
|
let (other, other_faces) = tetrahedron([0.25, 0.25, 0.25]);
|
|
let base = positions.len() as u32;
|
|
positions.extend(other);
|
|
for mut face in other_faces {
|
|
for v in &mut face.vertices {
|
|
*v += base;
|
|
}
|
|
faces.push(face);
|
|
}
|
|
let mut c = ctx();
|
|
let mesh = Mesh::from_polygons(&positions, &faces, &mut c).unwrap();
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
// Topology can still call this a closed manifold. Embedded-solid
|
|
// qualification must not come back true while intersections are unchecked.
|
|
assert!(report.is_closed_manifold, "{report:?}");
|
|
assert!(
|
|
!report.is_valid_solid
|
|
|| report.self_intersections != SelfIntersectionStatus::NotChecked,
|
|
"interpenetrating solids claimed valid without intersection check: {report:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn isolated_vertices_after_face_delete_are_diagnosed() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::plane([1., 1.], &mut c).unwrap();
|
|
let face = mesh.faces()[0].id;
|
|
let vertex_ids: Vec<_> = mesh.vertices().iter().map(|v| v.id).collect();
|
|
mesh.delete_faces(&[face], &mut c).unwrap();
|
|
assert_eq!(mesh.faces().len(), 0);
|
|
assert_eq!(
|
|
mesh.vertices().iter().map(|v| v.id).collect::<Vec<_>>(),
|
|
vertex_ids
|
|
);
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert_eq!(report.isolated_vertices, 4, "{report:?}");
|
|
assert!(!report.is_closed, "{report:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn loose_edge_is_representable_and_not_a_closed_solid() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::plane([1., 1.], &mut c).unwrap();
|
|
let a = mesh.vertices()[0].id;
|
|
let b = mesh.vertices()[2].id;
|
|
mesh.add_loose_edge(a, b, &mut c).unwrap();
|
|
let report = mesh.validate(&mut c).unwrap();
|
|
assert!(report.loose_edges >= 1, "{report:?}");
|
|
assert!(!report.is_valid_solid, "{report:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn canonical_codec_rejects_truncation_trailing_negative_zero_and_bad_flags() {
|
|
let mesh = Mesh::plane([1., 1.], &mut ctx()).unwrap();
|
|
let bytes = bytes_of(&mesh);
|
|
for n in [0usize, 1, 8, 31, 32, bytes.len() / 2, bytes.len() - 1] {
|
|
assert!(
|
|
Mesh::from_bytes(&bytes[..n], &mut ctx()).is_err(),
|
|
"accepted truncated length {n}"
|
|
);
|
|
}
|
|
let mut trailing = bytes.clone();
|
|
trailing.push(0);
|
|
assert!(Mesh::from_bytes(&trailing, &mut ctx()).is_err());
|
|
|
|
let mut bad_magic = bytes.clone();
|
|
bad_magic[0] ^= 1;
|
|
assert!(Mesh::from_bytes(&bad_magic, &mut ctx()).is_err());
|
|
|
|
// Header is 32 bytes; first vertex id is 8 bytes; first coordinate follows.
|
|
let mut neg_zero = bytes.clone();
|
|
let coord = 40;
|
|
neg_zero[coord..coord + 8].copy_from_slice(&(-0.0f64).to_bits().to_le_bytes());
|
|
match Mesh::from_bytes(&neg_zero, &mut ctx()) {
|
|
Err(MeshError::CorruptData(_)) => {}
|
|
other => panic!("negative zero must be noncanonical, got {other:?}"),
|
|
}
|
|
|
|
let mut next_id = bytes.clone();
|
|
next_id[8..16].copy_from_slice(&0u64.to_le_bytes());
|
|
assert!(Mesh::from_bytes(&next_id, &mut ctx()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn codec_rejects_noncanonical_vertex_order_and_duplicate_identities() {
|
|
let mesh = Mesh::plane([1., 1.], &mut ctx()).unwrap();
|
|
let bytes = bytes_of(&mesh);
|
|
// Two 36-byte unweighted vertex records start at offset 32.
|
|
let mut swapped = bytes.clone();
|
|
let a = 32usize;
|
|
let b = 32 + 36;
|
|
let tmp = bytes[a..a + 36].to_vec();
|
|
swapped[a..a + 36].copy_from_slice(&bytes[b..b + 36]);
|
|
swapped[b..b + 36].copy_from_slice(&tmp);
|
|
assert!(Mesh::from_bytes(&swapped, &mut ctx()).is_err());
|
|
|
|
let mut dup = bytes.clone();
|
|
dup[32..40].copy_from_slice(&bytes[32 + 36..32 + 44]);
|
|
assert!(Mesh::from_bytes(&dup, &mut ctx()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_counts_are_bounded_before_install() {
|
|
let mut header = Vec::from(*b"MPMESH01");
|
|
header.extend_from_slice(&1u64.to_le_bytes());
|
|
header.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
header.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
header.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
header.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
let mut limits = Limits::default();
|
|
limits.max_vertices = 8;
|
|
limits.max_faces = 8;
|
|
limits.max_corners = 32;
|
|
limits.max_edges = 32;
|
|
limits.max_bytes = 4096;
|
|
match Mesh::from_bytes(&header, &mut Context::new(limits, None)) {
|
|
Err(MeshError::Budget { .. } | MeshError::CorruptData(_)) => {}
|
|
other => panic!("hostile counts must not install a mesh: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn preflight_limits_reject_growth_without_mutating() {
|
|
let mut mesh = Mesh::plane([1., 1.], &mut ctx()).unwrap();
|
|
let snapshot = bytes_of(&mesh);
|
|
let mut limits = Limits::default();
|
|
limits.max_vertices = mesh.vertices().len();
|
|
limits.max_faces = mesh.faces().len();
|
|
limits.max_corners = mesh.corners().len();
|
|
let face = mesh.faces()[0].id;
|
|
assert!(matches!(
|
|
mesh.extrude_face(face, [0., 1., 0.], &mut Context::new(limits, None)),
|
|
Err(MeshError::Budget { .. })
|
|
));
|
|
assert_eq!(bytes_of(&mesh), snapshot);
|
|
|
|
let mut tiny = Limits::default();
|
|
tiny.max_work = 1;
|
|
assert!(Mesh::cube([1., 1., 1.], &mut Context::new(tiny, None)).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn weights_normalize_and_cardinality_mismatch_is_refused() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::plane([1., 1.], &mut c).unwrap();
|
|
let v = mesh.vertices()[0].id;
|
|
mesh.set_vertex_weights(
|
|
v,
|
|
&[
|
|
JointWeight {
|
|
joint: 2,
|
|
weight: 1.,
|
|
},
|
|
JointWeight {
|
|
joint: 5,
|
|
weight: 3.,
|
|
},
|
|
],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let weights = &mesh.vertex(v).unwrap().weights;
|
|
assert_eq!(weights.len(), 2);
|
|
assert!((weights.iter().map(|w| w.weight).sum::<f64>() - 1.).abs() < 1e-12);
|
|
assert!(Mesh::from_weighted_polygons(
|
|
&[[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]],
|
|
&[vec![]],
|
|
&[Polygon::new(vec![0, 1, 2])],
|
|
&mut ctx()
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn nan_and_nonfinite_inputs_never_install() {
|
|
assert!(Mesh::cube([f64::NAN, 1., 1.], &mut ctx()).is_err());
|
|
assert!(Mesh::cube([f64::INFINITY, 1., 1.], &mut ctx()).is_err());
|
|
assert!(Mesh::plane([0., 1.], &mut ctx()).is_err());
|
|
assert!(Mesh::from_polygons(
|
|
&[[f64::NAN, 0., 0.], [1., 0., 0.], [0., 1., 0.]],
|
|
&[Polygon::new(vec![0, 1, 2])],
|
|
&mut ctx()
|
|
)
|
|
.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_edit_sequence_preserves_surviving_ids_and_bytes() {
|
|
let mut c = ctx();
|
|
let mut mesh = Mesh::cube([2., 2., 2.], &mut c).unwrap();
|
|
let face = mesh.faces()[1].id;
|
|
let corner = mesh.face_corners(face).unwrap()[0].id;
|
|
let vertex = mesh.face_corners(face).unwrap()[0].vertex;
|
|
mesh.set_corner_uv(corner, [0.25, 0.75], &mut c).unwrap();
|
|
mesh.set_vertex_weights(
|
|
vertex,
|
|
&[JointWeight {
|
|
joint: 1,
|
|
weight: 1.,
|
|
}],
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let edge = EdgeKey::new(
|
|
mesh.face_corners(face).unwrap()[0].vertex,
|
|
mesh.face_corners(face).unwrap()[1].vertex,
|
|
);
|
|
mesh.set_edge_attributes(
|
|
edge,
|
|
EdgeAttributes {
|
|
seam: true,
|
|
crease: 0.4,
|
|
},
|
|
&mut c,
|
|
)
|
|
.unwrap();
|
|
let extruded = mesh.extrude_face(face, [0., 0., 0.5], &mut c).unwrap();
|
|
assert_eq!(extruded.cap, face);
|
|
assert_eq!(mesh.face_corners(face).unwrap()[0].id, corner);
|
|
mesh.mirror(0, 0., &mut c).unwrap();
|
|
let bytes = bytes_of(&mesh);
|
|
let restored = Mesh::from_bytes(&bytes, &mut ctx()).unwrap();
|
|
assert_eq!(bytes_of(&restored), bytes);
|
|
assert!(restored.face(face).is_some());
|
|
assert_eq!(restored.corner(corner).unwrap().uv, [0.25, 0.75]);
|
|
assert_eq!(restored.to_bytes(&mut ctx()).unwrap(), bytes);
|
|
}
|