use super::*;
use base64::engine::general_purpose::STANDARD as BASE64_ENGINE;
use base64::Engine;
use runmat_geometry_core::{
CadLabelRef, CadRegionOwnership, CadSemanticKind, MeshDescriptor, RegionEntityMapping,
SurfaceMesh,
};
const TRIANGLE_STL: &str = "solid tri\n facet normal 0 0 1\n outer loop\n vertex 0 0 0\n vertex 1 0 0\n vertex 0 1 0\n endloop\n endfacet\nendsolid tri\n";
const SIMPLE_STEP: &str = "ISO-10303-21;\nHEADER;\nFILE_NAME('Assembly_A');\nENDSEC;\nDATA;\n#10=PRODUCT('Bracket_A','',(#1));\nENDSEC;\nEND-ISO-10303-21;\n";
const SIMPLE_OBJ: &str = "v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\n";
const SIMPLE_PLY: &str = "ply\nformat ascii 1.0\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nelement face 1\nproperty list uchar int vertex_indices\nend_header\n0 0 0\n1 0 0\n1 1 0\n0 1 0\n4 0 1 2 3\n";
const SIMPLE_GLTF: &str = "{\n \"asset\": {\"version\": \"2.0\"},\n \"meshes\": [\n {\n \"primitives\": [\n {\n \"attributes\": {\n \"POSITION\": [[0,0,0],[1,0,0],[1,1,0],[0,1,0]]\n },\n \"indices\": [0,1,2,0,2,3]\n }\n ]\n }\n ]\n}\n";
const NON_TRIANGLE_GLTF: &str = "{\n \"asset\": {\"version\": \"2.0\"},\n \"meshes\": [\n {\n \"primitives\": [\n {\n \"mode\": 1,\n \"attributes\": {\n \"POSITION\": [[0,0,0],[1,0,0],[1,1,0]]\n },\n \"indices\": [0,1,2]\n }\n ]\n }\n ]\n}\n";
const SIMPLE_GLB_HEADER: &[u8] = b"glTF\x02\x00\x00\x00";
const BOM_PREFIX: &[u8] = b"\xEF\xBB\xBF";
const SIMPLE_GLTF_IMPLICIT_INDICES: &str = "{\n \"asset\": {\"version\": \"2.0\"},\n \"meshes\": [\n {\n \"primitives\": [\n {\n \"attributes\": {\n \"POSITION\": [[0,0,0],[1,0,0],[0,1,0]]\n }\n }\n ]\n }\n ]\n}\n";
const BAD_GLTF_IMPLICIT_INDEX_COUNT: &str = "{\n \"asset\": {\"version\": \"2.0\"},\n \"meshes\": [\n {\n \"primitives\": [\n {\n \"attributes\": {\n \"POSITION\": [[0,0,0],[1,0,0],[1,1,0],[0,1,0]]\n }\n }\n ]\n }\n ]\n}\n";
fn binary_triangle_stl() -> Vec<u8> {
let mut payload = vec![0u8; 84 + 50];
payload[80..84].copy_from_slice(&1u32.to_le_bytes());
let tri = 84usize;
payload[tri + 12..tri + 16].copy_from_slice(&(0.0f32).to_le_bytes());
payload[tri + 16..tri + 20].copy_from_slice(&(0.0f32).to_le_bytes());
payload[tri + 20..tri + 24].copy_from_slice(&(0.0f32).to_le_bytes());
payload[tri + 24..tri + 28].copy_from_slice(&(1.0f32).to_le_bytes());
payload[tri + 28..tri + 32].copy_from_slice(&(0.0f32).to_le_bytes());
payload[tri + 32..tri + 36].copy_from_slice(&(0.0f32).to_le_bytes());
payload[tri + 36..tri + 40].copy_from_slice(&(0.0f32).to_le_bytes());
payload[tri + 40..tri + 44].copy_from_slice(&(1.0f32).to_le_bytes());
payload[tri + 44..tri + 48].copy_from_slice(&(0.0f32).to_le_bytes());
payload
}
fn gltf_accessor_data_uri_payload() -> Vec<u8> {
let positions = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let indices = [0u16, 1, 2, 0, 2, 3];
let mut buffer = Vec::<u8>::new();
for vertex in positions {
buffer.extend_from_slice(&vertex[0].to_le_bytes());
buffer.extend_from_slice(&vertex[1].to_le_bytes());
buffer.extend_from_slice(&vertex[2].to_le_bytes());
}
let index_offset = buffer.len();
for index in indices {
buffer.extend_from_slice(&index.to_le_bytes());
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\",\"byteLength\":{byte_len}}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":{positions_len}}},{{\"buffer\":0,\"byteOffset\":{index_offset},\"byteLength\":{indices_len}}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":4,\"type\":\"VEC3\"}},{{\"bufferView\":1,\"componentType\":5123,\"count\":6,\"type\":\"SCALAR\"}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}},\"indices\":1}}]}}]}}",
byte_len = buffer.len(),
positions_len = index_offset,
indices_len = buffer.len() - index_offset
)
.into_bytes()
}
fn gltf_accessor_data_uri_buffer_view_too_small_payload() -> Vec<u8> {
let positions = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let mut buffer = Vec::<u8>::new();
for vertex in positions {
buffer.extend_from_slice(&vertex[0].to_le_bytes());
buffer.extend_from_slice(&vertex[1].to_le_bytes());
buffer.extend_from_slice(&vertex[2].to_le_bytes());
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\",\"byteLength\":{byte_len}}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":8}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":4,\"type\":\"VEC3\"}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}}}}]}}]}}",
byte_len = buffer.len()
)
.into_bytes()
}
fn gltf_accessor_data_uri_missing_buffer_byte_length_payload() -> Vec<u8> {
let positions = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let mut buffer = Vec::<u8>::new();
for vertex in positions {
buffer.extend_from_slice(&vertex[0].to_le_bytes());
buffer.extend_from_slice(&vertex[1].to_le_bytes());
buffer.extend_from_slice(&vertex[2].to_le_bytes());
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\"}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":48}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":4,\"type\":\"VEC3\"}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}}}}]}}]}}",
)
.into_bytes()
}
fn gltf_accessor_data_uri_normalized_payload() -> Vec<u8> {
let positions = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let mut buffer = Vec::<u8>::new();
for vertex in positions {
buffer.extend_from_slice(&vertex[0].to_le_bytes());
buffer.extend_from_slice(&vertex[1].to_le_bytes());
buffer.extend_from_slice(&vertex[2].to_le_bytes());
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\",\"byteLength\":48}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":48}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":4,\"type\":\"VEC3\",\"normalized\":true}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}}}}]}}]}}",
)
.into_bytes()
}
fn gltf_accessor_data_uri_sparse_payload() -> Vec<u8> {
let positions = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let mut buffer = Vec::<u8>::new();
for vertex in positions {
buffer.extend_from_slice(&vertex[0].to_le_bytes());
buffer.extend_from_slice(&vertex[1].to_le_bytes());
buffer.extend_from_slice(&vertex[2].to_le_bytes());
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\",\"byteLength\":48}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":48}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":4,\"type\":\"VEC3\",\"sparse\":{{\"count\":1,\"indices\":{{\"bufferView\":0,\"componentType\":5123}},\"values\":{{\"bufferView\":0}}}}}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}}}}]}}]}}",
)
.into_bytes()
}
fn gltf_accessor_data_uri_stride_misaligned_payload() -> Vec<u8> {
let mut buffer = Vec::<u8>::new();
for _ in 0..4 {
buffer.extend_from_slice(&[0u8; 13]);
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\",\"byteLength\":52}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":52,\"byteStride\":13}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":4,\"type\":\"VEC3\"}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}}}}]}}]}}",
)
.into_bytes()
}
fn gltf_accessor_data_uri_count_span_overflow_payload() -> Vec<u8> {
let mut buffer = Vec::<u8>::new();
for _ in 0..4 {
buffer.extend_from_slice(&[0u8; 12]);
}
let encoded = BASE64_ENGINE.encode(&buffer);
format!(
"{{\"asset\":{{\"version\":\"2.0\"}},\"buffers\":[{{\"uri\":\"data:application/octet-stream;base64,{encoded}\",\"byteLength\":48}}],\"bufferViews\":[{{\"buffer\":0,\"byteOffset\":0,\"byteLength\":48,\"byteStride\":12}}],\"accessors\":[{{\"bufferView\":0,\"componentType\":5126,\"count\":8,\"type\":\"VEC3\"}}],\"meshes\":[{{\"primitives\":[{{\"attributes\":{{\"POSITION\":0}}}}]}}]}}",
)
.into_bytes()
}
fn binary_ply_payload() -> Vec<u8> {
let header = b"ply\nformat binary_little_endian 1.0\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nelement face 2\nproperty list uchar int vertex_indices\nend_header\n";
let vertices = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let faces = [[0i32, 1, 2], [0i32, 2, 3]];
let mut payload = header.to_vec();
for vertex in vertices {
payload.extend_from_slice(&vertex[0].to_le_bytes());
payload.extend_from_slice(&vertex[1].to_le_bytes());
payload.extend_from_slice(&vertex[2].to_le_bytes());
}
for face in faces {
payload.push(3u8);
payload.extend_from_slice(&face[0].to_le_bytes());
payload.extend_from_slice(&face[1].to_le_bytes());
payload.extend_from_slice(&face[2].to_le_bytes());
}
payload
}
fn binary_ply_uint_payload() -> Vec<u8> {
let header = b"ply\nformat binary_little_endian 1.0\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nelement face 2\nproperty list uchar uint vertex_indices\nend_header\n";
let vertices = [
[0.0f32, 0.0f32, 0.0f32],
[1.0f32, 0.0f32, 0.0f32],
[1.0f32, 1.0f32, 0.0f32],
[0.0f32, 1.0f32, 0.0f32],
];
let faces = [[0u32, 1, 2], [0u32, 2, 3]];
let mut payload = header.to_vec();
for vertex in vertices {
payload.extend_from_slice(&vertex[0].to_le_bytes());
payload.extend_from_slice(&vertex[1].to_le_bytes());
payload.extend_from_slice(&vertex[2].to_le_bytes());
}
for face in faces {
payload.push(3u8);
payload.extend_from_slice(&face[0].to_le_bytes());
payload.extend_from_slice(&face[1].to_le_bytes());
payload.extend_from_slice(&face[2].to_le_bytes());
}
payload
}
fn binary_ply_extra_vertex_property_payload() -> Vec<u8> {
let header = b"ply\nformat binary_little_endian 1.0\nelement vertex 4\nproperty float x\nproperty float y\nproperty float z\nproperty float nx\nelement face 2\nproperty list uchar int vertex_indices\nend_header\n";
let mut payload = header.to_vec();
payload.resize(payload.len() + 4 * 16 + 2 * (1 + 3 * 4), 0u8);
payload
}
#[test]
fn inspect_detects_stl() {
let result =
geometry_inspect("/part.stl", TRIANGLE_STL.as_bytes()).expect("inspect should work");
assert_eq!(result.format, "stl");
}
#[test]
fn inspect_op_returns_typed_metadata() {
let envelope = geometry_inspect_op(
"/part.stl",
TRIANGLE_STL.as_bytes(),
OperationContext::new(Some("trace-g1".to_string()), Some("request-g1".to_string())),
)
.expect("inspect envelope should work");
assert_eq!(envelope.operation, "geometry.inspect");
assert_eq!(envelope.op_version, "geometry.inspect/v1");
assert_eq!(envelope.trace_id.as_deref(), Some("trace-g1"));
assert_eq!(envelope.data.format, "stl");
}
#[test]
fn load_and_stats_work_for_stl() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let stats = geometry_compute_stats(&asset).expect("stats should work");
assert_eq!(stats.mesh_count, 1);
assert_eq!(stats.total_elements, 1);
}
#[test]
fn asset_summary_reports_mesh_bounds_and_mapping_counts() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let summary = geometry_asset_summary_with_options(&asset, 0);
assert_eq!(summary.geometry_id, asset.geometry_id);
assert_eq!(summary.meshes.len(), 1);
assert_eq!(summary.meshes[0].mesh_id, "mesh_1");
assert_eq!(summary.meshes[0].surface_triangle_count, Some(1));
assert_eq!(
summary.meshes[0].bounds,
Some(GeometryBoundsSummary {
min: [0.0, 0.0, 0.0],
max: [1.0, 1.0, 0.0],
})
);
assert_eq!(summary.mapping_summary.mapping_count, 1);
assert_eq!(summary.mapping_summary.mapped_region_count, 1);
assert_eq!(summary.mapping_summary.total_entity_count, 1);
assert_eq!(summary.mapping_summary.entries[0].range_count, 1);
assert!(summary.mapping_summary.entries[0].range_preview.is_empty());
assert!(summary.mapping_summary.entries[0].truncated);
assert_eq!(summary.cad.region_status, GeometryCadRegionStatus::NotCad);
}
#[test]
fn inspect_and_load_step_work() {
let inspect =
geometry_inspect("/assembly.step", SIMPLE_STEP.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "step");
let asset = geometry_load("/assembly.step", SIMPLE_STEP.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "step/v1");
assert_eq!(asset.regions.len(), 1);
assert!(asset.source_geometry.assembly.is_some());
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"CAD_METADATA_PRODUCT_COUNT"));
assert!(codes.contains(&"CAD_METADATA_MATERIAL_EVIDENCE_COUNT"));
}
#[test]
fn inspect_and_load_binary_stl_work_without_extension() {
let payload = binary_triangle_stl();
let inspect = geometry_inspect("/part.mesh", &payload).expect("inspect should work");
assert_eq!(inspect.format, "stl");
let asset = geometry_load("/part.mesh", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "stl/v1");
assert_eq!(asset.meshes.len(), 1);
assert_eq!(asset.meshes[0].element_count, 1);
assert_eq!(asset.meshes[0].vertex_count, 3);
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_VERTEX_COUNT"));
assert!(codes.contains(&"GEOMETRY_IMPORT_TRIANGLE_COUNT"));
}
#[test]
fn inspect_and_load_obj_work() {
let inspect =
geometry_inspect("/part.obj", SIMPLE_OBJ.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "obj");
let asset = geometry_load("/part.obj", SIMPLE_OBJ.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "obj/v1");
assert_eq!(asset.meshes.len(), 1);
assert_eq!(asset.meshes[0].element_count, 2);
assert_eq!(asset.meshes[0].vertex_count, 4);
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_VERTEX_COUNT"));
assert!(codes.contains(&"GEOMETRY_IMPORT_TRIANGLE_COUNT"));
}
#[test]
fn inspect_and_load_obj_work_without_extension() {
let inspect =
geometry_inspect("/part.dat", SIMPLE_OBJ.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "obj");
let asset = geometry_load("/part.dat", SIMPLE_OBJ.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "obj/v1");
assert_eq!(asset.meshes[0].element_count, 2);
}
#[test]
fn inspect_and_load_obj_work_without_extension_with_utf8_bom() {
let mut payload = BOM_PREFIX.to_vec();
payload.extend_from_slice(SIMPLE_OBJ.as_bytes());
let inspect = geometry_inspect("/part.dat", &payload).expect("inspect should work");
assert_eq!(inspect.format, "obj");
let asset = geometry_load("/part.dat", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "obj/v1");
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_UTF8_BOM_STRIPPED"));
}
#[test]
fn inspect_and_load_ply_work() {
let inspect =
geometry_inspect("/part.ply", SIMPLE_PLY.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "ply");
let asset = geometry_load("/part.ply", SIMPLE_PLY.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "ply/v1");
assert_eq!(asset.meshes.len(), 1);
assert_eq!(asset.meshes[0].element_count, 2);
assert_eq!(asset.meshes[0].vertex_count, 4);
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_VERTEX_COUNT"));
assert!(codes.contains(&"GEOMETRY_IMPORT_TRIANGLE_COUNT"));
}
#[test]
fn inspect_and_load_ply_work_without_extension() {
let inspect =
geometry_inspect("/part.mesh", SIMPLE_PLY.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "ply");
let asset = geometry_load("/part.mesh", SIMPLE_PLY.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "ply/v1");
assert_eq!(asset.meshes[0].element_count, 2);
}
#[test]
fn inspect_and_load_ply_work_without_extension_with_utf8_bom() {
let mut payload = BOM_PREFIX.to_vec();
payload.extend_from_slice(SIMPLE_PLY.as_bytes());
let inspect = geometry_inspect("/part.mesh", &payload).expect("inspect should work");
assert_eq!(inspect.format, "ply");
let asset = geometry_load("/part.mesh", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "ply/v1");
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_UTF8_BOM_STRIPPED"));
}
#[test]
fn inspect_and_load_binary_ply_work_without_extension() {
let payload = binary_ply_payload();
let inspect = geometry_inspect("/part.mesh", &payload).expect("inspect should work");
assert_eq!(inspect.format, "ply");
let asset = geometry_load("/part.mesh", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "ply/v1");
assert_eq!(asset.meshes[0].element_count, 2);
assert_eq!(asset.meshes[0].vertex_count, 4);
}
#[test]
fn inspect_and_load_binary_ply_uint_indices_work_without_extension() {
let payload = binary_ply_uint_payload();
let inspect = geometry_inspect("/part.mesh", &payload).expect("inspect should work");
assert_eq!(inspect.format, "ply");
let asset = geometry_load("/part.mesh", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "ply/v1");
assert_eq!(asset.meshes[0].element_count, 2);
assert_eq!(asset.meshes[0].vertex_count, 4);
}
#[test]
fn inspect_and_load_gltf_work() {
let inspect =
geometry_inspect("/part.gltf", SIMPLE_GLTF.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "gltf");
let asset = geometry_load("/part.gltf", SIMPLE_GLTF.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "gltf/v1");
assert_eq!(asset.meshes.len(), 1);
assert_eq!(asset.meshes[0].element_count, 2);
assert_eq!(asset.meshes[0].vertex_count, 4);
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_VERTEX_COUNT"));
assert!(codes.contains(&"GEOMETRY_IMPORT_TRIANGLE_COUNT"));
}
#[test]
fn inspect_and_load_gltf_accessor_data_uri_work() {
let payload = gltf_accessor_data_uri_payload();
let inspect = geometry_inspect("/part.gltf", &payload).expect("inspect should work");
assert_eq!(inspect.format, "gltf");
let asset = geometry_load("/part.gltf", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "gltf/v1");
assert_eq!(asset.meshes[0].element_count, 2);
assert_eq!(asset.meshes[0].vertex_count, 4);
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_GLTF_ACCESSOR_DATA_URI_USED"));
}
#[test]
fn inspect_and_load_gltf_with_implicit_indices_work() {
let inspect = geometry_inspect(
"/part-implicit.gltf",
SIMPLE_GLTF_IMPLICIT_INDICES.as_bytes(),
)
.expect("inspect should work");
assert_eq!(inspect.format, "gltf");
let asset = geometry_load(
"/part-implicit.gltf",
SIMPLE_GLTF_IMPLICIT_INDICES.as_bytes(),
)
.expect("load should work");
assert_eq!(asset.source.importer_version, "gltf/v1");
assert_eq!(asset.meshes[0].element_count, 1);
assert_eq!(asset.meshes[0].vertex_count, 3);
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_GLTF_IMPLICIT_INDICES_USED"));
}
#[test]
fn inspect_and_load_gltf_work_without_extension() {
let inspect =
geometry_inspect("/part.data", SIMPLE_GLTF.as_bytes()).expect("inspect should work");
assert_eq!(inspect.format, "gltf");
let asset = geometry_load("/part.data", SIMPLE_GLTF.as_bytes()).expect("load should work");
assert_eq!(asset.source.importer_version, "gltf/v1");
assert_eq!(asset.meshes[0].element_count, 2);
}
#[test]
fn inspect_and_load_gltf_work_without_extension_with_utf8_bom() {
let mut payload = BOM_PREFIX.to_vec();
payload.extend_from_slice(SIMPLE_GLTF.as_bytes());
let inspect = geometry_inspect("/part.data", &payload).expect("inspect should work");
assert_eq!(inspect.format, "gltf");
let asset = geometry_load("/part.data", &payload).expect("load should work");
assert_eq!(asset.source.importer_version, "gltf/v1");
let codes = asset
.diagnostics
.iter()
.map(|diag| diag.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"GEOMETRY_IMPORT_UTF8_BOM_STRIPPED"));
}
#[test]
fn load_op_maps_unsupported_format_error_code() {
let error = geometry_load_op(
"/bad.bin",
b"not geometry",
OperationContext::new(None, None),
)
.expect_err("load should fail");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.FORMAT_UNSUPPORTED");
assert_eq!(error.operation, "geometry.load");
assert_eq!(error.op_version, "geometry.load/v1");
}
#[test]
fn load_op_maps_cancelled_import_error_code() {
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
let cancelled = Arc::new(AtomicBool::new(true));
let _guard = crate::interrupt::replace_interrupt(Some(Arc::clone(&cancelled)));
let error = geometry_load_op(
"/cancelled.stl",
TRIANGLE_STL.as_bytes(),
OperationContext::new(None, None),
)
.expect_err("cancelled geometry import should fail");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.CANCELLED");
assert_eq!(error.error_type, OperationErrorType::Cancelled);
cancelled.store(false, Ordering::Relaxed);
}
#[test]
fn load_op_maps_parse_error_for_glb_payload() {
let error = geometry_load_op(
"/mesh.glb",
SIMPLE_GLB_HEADER,
OperationContext::new(None, None),
)
.expect_err("GLB payload should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("GLB payloads are not supported"));
}
#[test]
fn load_op_maps_parse_error_for_non_triangle_gltf_mode() {
let error = geometry_load_op(
"/mesh.gltf",
NON_TRIANGLE_GLTF.as_bytes(),
OperationContext::new(None, None),
)
.expect_err("non-triangle GLTF mode should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("mode 1 is not supported"));
}
#[test]
fn load_op_maps_parse_error_for_bad_implicit_gltf_indices() {
let error = geometry_load_op(
"/mesh.gltf",
BAD_GLTF_IMPLICIT_INDEX_COUNT.as_bytes(),
OperationContext::new(None, None),
)
.expect_err("bad implicit GLTF indices should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("multiple of 3"));
}
#[test]
fn load_op_maps_parse_error_for_unsupported_binary_ply_layout() {
let payload = binary_ply_extra_vertex_property_payload();
let error = geometry_load_op("/mesh.ply", &payload, OperationContext::new(None, None))
.expect_err("unsupported binary PLY layout should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("requires vertex properties exactly"));
}
#[test]
fn load_op_maps_parse_error_for_gltf_accessor_buffer_view_bounds_violation() {
let payload = gltf_accessor_data_uri_buffer_view_too_small_payload();
let error = geometry_load_op("/mesh.gltf", &payload, OperationContext::new(None, None))
.expect_err("gltf accessor bufferView bounds violation should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error
.message
.contains("declared count/stride exceeds bufferView byte range"));
}
#[test]
fn load_op_maps_parse_error_for_gltf_accessor_missing_buffer_byte_length() {
let payload = gltf_accessor_data_uri_missing_buffer_byte_length_payload();
let error = geometry_load_op("/mesh.gltf", &payload, OperationContext::new(None, None))
.expect_err("gltf accessor payload missing buffer.byteLength should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("requires buffer.byteLength"));
}
#[test]
fn load_op_maps_parse_error_for_gltf_accessor_normalized() {
let payload = gltf_accessor_data_uri_normalized_payload();
let error = geometry_load_op("/mesh.gltf", &payload, OperationContext::new(None, None))
.expect_err("normalized accessor payload should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error
.message
.contains("normalized accessors are not supported"));
}
#[test]
fn load_op_maps_parse_error_for_gltf_accessor_sparse() {
let payload = gltf_accessor_data_uri_sparse_payload();
let error = geometry_load_op("/mesh.gltf", &payload, OperationContext::new(None, None))
.expect_err("sparse accessor payload should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("sparse accessors are not supported"));
}
#[test]
fn load_op_maps_parse_error_for_gltf_accessor_stride_alignment() {
let payload = gltf_accessor_data_uri_stride_misaligned_payload();
let error = geometry_load_op("/mesh.gltf", &payload, OperationContext::new(None, None))
.expect_err("misaligned accessor stride payload should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error.message.contains("not aligned to component size"));
}
#[test]
fn load_op_maps_parse_error_for_gltf_accessor_count_span_overflow() {
let payload = gltf_accessor_data_uri_count_span_overflow_payload();
let error = geometry_load_op("/mesh.gltf", &payload, OperationContext::new(None, None))
.expect_err("count/stride overflow payload should fail parse");
assert_eq!(error.error_code, "RM.GEOMETRY.LOAD.PARSE_FAILED");
assert!(error
.message
.contains("declared count/stride exceeds bufferView byte range"));
}
#[test]
fn list_regions_op_returns_versioned_envelope() {
let asset = geometry_load("/assembly.step", SIMPLE_STEP.as_bytes()).expect("load should work");
let envelope = geometry_list_regions_op(
&asset,
OperationContext::new(Some("trace-g2".to_string()), None),
)
.expect("list regions should work");
assert_eq!(envelope.operation, "geometry.list_regions");
assert_eq!(envelope.op_version, "geometry.list_regions/v1");
assert_eq!(envelope.trace_id.as_deref(), Some("trace-g2"));
assert_eq!(envelope.data.regions.len(), 1);
}
#[test]
fn query_entities_op_returns_deterministic_nodes() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let envelope = geometry_query_entities_op(
&asset,
GeometryEntityQuery {
region_id: None,
mesh_id: None,
entity_kind: EntityKind::Node,
limit: Some(2),
},
OperationContext::new(Some("trace-g3".to_string()), None),
)
.expect("query should work");
assert_eq!(envelope.operation, "geometry.query_entities");
assert_eq!(envelope.op_version, "geometry.query_entities/v1");
assert_eq!(envelope.data.entities.len(), 2);
assert!(envelope.data.truncated);
assert_eq!(envelope.data.entities[0].entity_id, 0);
assert_eq!(envelope.data.entities[1].entity_id, 1);
}
#[test]
fn query_entities_region_not_found_maps_typed_error() {
let asset = geometry_load("/assembly.step", SIMPLE_STEP.as_bytes()).expect("load should work");
let error = geometry_query_entities_op(
&asset,
GeometryEntityQuery {
region_id: Some("missing".to_string()),
mesh_id: None,
entity_kind: EntityKind::Node,
limit: Some(8),
},
OperationContext::new(None, None),
)
.expect_err("query should fail");
assert_eq!(
error.error_code,
"RM.GEOMETRY.QUERY_ENTITIES.REGION_NOT_FOUND"
);
assert_eq!(error.operation, "geometry.query_entities");
assert_eq!(error.op_version, "geometry.query_entities/v1");
}
#[test]
fn query_entities_filters_by_region_mapping() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let envelope = geometry_query_entities_op(
&asset,
GeometryEntityQuery {
region_id: Some("region_default".to_string()),
mesh_id: None,
entity_kind: EntityKind::Face,
limit: Some(8),
},
OperationContext::new(Some("trace-region".to_string()), None),
)
.expect("query should work");
assert_eq!(envelope.data.entities.len(), 1);
assert!(!envelope.data.truncated);
assert_eq!(envelope.data.entities[0].mesh_id, "mesh_1");
assert_eq!(envelope.data.entities[0].entity_kind, EntityKind::Face);
assert_eq!(envelope.data.entities[0].entity_id, 0);
}
#[test]
fn capture_view_op_returns_typed_unsupported_error() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let error = geometry_capture_view_op(
&asset,
GeometryCaptureViewSpec {
format: "png".to_string(),
width: 1024,
height: 768,
},
OperationContext::new(Some("trace-g4".to_string()), None),
)
.expect_err("capture should be unsupported for now");
assert_eq!(error.operation, "geometry.capture_view");
assert_eq!(error.op_version, "geometry.capture_view/v1");
assert_eq!(error.error_code, "RM.GEOMETRY.CAPTURE_VIEW.UNSUPPORTED");
assert_eq!(error.trace_id.as_deref(), Some("trace-g4"));
}
#[test]
fn capture_view_op_uses_default_svg_renderer_without_adapter() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let envelope = geometry_capture_view_op(
&asset,
GeometryCaptureViewSpec {
format: "svg".to_string(),
width: 640,
height: 360,
},
OperationContext::new(Some("trace-g4-svg".to_string()), None),
)
.expect("svg capture should succeed");
assert_eq!(envelope.operation, "geometry.capture_view");
assert_eq!(envelope.op_version, "geometry.capture_view/v1");
assert_eq!(envelope.data.format, "svg");
assert_eq!(envelope.data.width, 640);
assert_eq!(envelope.data.height, 360);
let payload = std::str::from_utf8(&envelope.data.payload).expect("svg payload utf8");
assert!(payload.starts_with("<svg"));
assert!(payload.contains("Geometry Snapshot"));
}
#[test]
fn capture_view_op_validates_non_zero_dimensions() {
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let error = geometry_capture_view_op(
&asset,
GeometryCaptureViewSpec {
format: "png".to_string(),
width: 0,
height: 768,
},
OperationContext::new(None, None),
)
.expect_err("zero width should be rejected");
assert_eq!(error.error_code, "RM.GEOMETRY.CAPTURE_VIEW.INVALID_SPEC");
assert_eq!(error.operation, "geometry.capture_view");
assert_eq!(error.op_version, "geometry.capture_view/v1");
}
#[test]
fn capture_view_op_uses_installed_adapter() {
struct TestCaptureAdapter;
impl GeometryViewCaptureAdapter for TestCaptureAdapter {
fn adapter_name(&self) -> &'static str {
"test-capture"
}
fn capture(
&self,
_asset: &runmat_geometry_core::GeometryAsset,
view_spec: &GeometryCaptureViewSpec,
) -> Result<GeometryCaptureViewResult, String> {
Ok(GeometryCaptureViewResult {
format: view_spec.format.clone(),
width: view_spec.width,
height: view_spec.height,
payload: vec![1, 2, 3, 4],
})
}
}
static ADAPTER: TestCaptureAdapter = TestCaptureAdapter;
let _guard = ThreadGeometryCaptureAdapterGuard::set(Some(&ADAPTER));
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let envelope = geometry_capture_view_op(
&asset,
GeometryCaptureViewSpec {
format: "png".to_string(),
width: 640,
height: 480,
},
OperationContext::new(Some("trace-g5".to_string()), None),
)
.expect("capture should succeed");
assert_eq!(envelope.operation, "geometry.capture_view");
assert_eq!(envelope.op_version, "geometry.capture_view/v1");
assert_eq!(envelope.trace_id.as_deref(), Some("trace-g5"));
assert_eq!(envelope.data.format, "png");
assert_eq!(envelope.data.width, 640);
assert_eq!(envelope.data.height, 480);
assert_eq!(envelope.data.payload, vec![1, 2, 3, 4]);
}
#[test]
fn prep_for_analysis_op_returns_versioned_deterministic_result() {
let _prep_guard = prep_artifact_test_guard();
reset_prep_artifact_store_for_tests();
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let first = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec::default(),
OperationContext::new(Some("trace-g6".to_string()), None),
)
.expect("prep should work");
let second = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec::default(),
OperationContext::new(Some("trace-g7".to_string()), None),
)
.expect("prep should be deterministic");
assert_eq!(first.operation, "geometry.prep_for_analysis");
assert_eq!(first.op_version, "geometry.prep_for_analysis/v1");
assert!(!first.data.prep_artifact_id.is_empty());
assert_ne!(first.data.prep_artifact_id, second.data.prep_artifact_id);
assert_eq!(first.data.prep, second.data.prep);
assert_eq!(
first.data.prep.schema_version,
"geometry-prep-for-analysis/v1"
);
assert!(!first.data.prep.prepared_meshes.is_empty());
let prepared_mesh = first
.data
.prep
.prepared_meshes
.first()
.expect("prepared mesh should be present");
assert!(prepared_mesh
.coordinate_span_m
.iter()
.any(|span| *span > 0.0));
assert!(prepared_mesh.coordinate_active_dimension_count >= 1);
assert!(prepared_mesh.coordinate_characteristic_length_m > 0.0);
assert!(prepared_mesh.element_geometry_node_count > 0);
assert!(prepared_mesh.element_geometry_edge_count > 0);
assert!(prepared_mesh.mean_element_edge_length_m > 0.0);
assert!(prepared_mesh.mean_element_area_m2 > 0.0);
assert!(prepared_mesh.element_geometry_coverage_ratio > 0.0);
assert!(!first.data.prep.region_mappings.is_empty());
}
#[test]
fn prep_for_analysis_op_maps_invalid_spec_error() {
let _prep_guard = prep_artifact_test_guard();
reset_prep_artifact_store_for_tests();
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let error = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec {
profile: GeometryPrepProfile::AnalysisReady,
target_element_budget: 0,
},
OperationContext::new(None, None),
)
.expect_err("zero element budget should fail");
assert_eq!(error.operation, "geometry.prep_for_analysis");
assert_eq!(error.op_version, "geometry.prep_for_analysis/v1");
assert_eq!(
error.error_code,
"RM.GEOMETRY.PREP_FOR_ANALYSIS.INVALID_SPEC"
);
}
#[test]
fn prep_for_analysis_op_supports_adaptive_refine_profile() {
let _prep_guard = prep_artifact_test_guard();
reset_prep_artifact_store_for_tests();
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let analysis_ready = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec {
profile: GeometryPrepProfile::AnalysisReady,
target_element_budget: 4_000,
},
OperationContext::new(None, None),
)
.expect("analysis-ready prep should work");
let adaptive_refine = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec {
profile: GeometryPrepProfile::AdaptiveRefine,
target_element_budget: 4_000,
},
OperationContext::new(None, None),
)
.expect("adaptive-refine prep should work");
assert_eq!(adaptive_refine.operation, "geometry.prep_for_analysis");
assert_eq!(adaptive_refine.op_version, "geometry.prep_for_analysis/v1");
assert!(
adaptive_refine.data.prep.quality.min_scaled_jacobian
>= analysis_ready.data.prep.quality.min_scaled_jacobian
);
assert!(
adaptive_refine.data.prep.quality.mean_aspect_ratio
<= analysis_ready.data.prep.quality.mean_aspect_ratio
);
}
#[test]
fn prep_artifact_retention_prunes_old_entries() {
let _prep_guard = prep_artifact_test_guard();
reset_prep_artifact_store_for_tests();
configure_prep_artifacts(GeometryPrepArtifactConfig {
max_artifacts_per_geometry: Some(2),
..GeometryPrepArtifactConfig::default()
})
.expect("prep artifact config should be configurable");
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
let mut ids = Vec::new();
for _ in 0..4 {
let prep = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec::default(),
OperationContext::new(None, None),
)
.expect("prep should succeed");
assert!(
!prep
.data
.prep_artifact_id
.chars()
.any(|ch| matches!(ch, '"' | ':' | '<' | '>' | '|' | '*' | '?' | '\r' | '\n')),
"prep artifact id should be portable for GitHub artifact upload"
);
ids.push(prep.data.prep_artifact_id);
}
assert!(load_prep_artifact(&ids[0])
.expect("load prep should succeed")
.is_none());
assert!(load_prep_artifact(ids.last().expect("latest id"))
.expect("load latest prep should succeed")
.is_some());
reset_prep_artifact_store_for_tests();
}
#[test]
fn prep_artifact_health_reports_metrics_and_distribution() {
let _prep_guard = prep_artifact_test_guard();
reset_prep_artifact_store_for_tests();
let asset = geometry_load("/part.stl", TRIANGLE_STL.as_bytes()).expect("load should work");
for _ in 0..3 {
let _ = geometry_prep_for_analysis_op(
&asset,
GeometryPrepForAnalysisSpec::default(),
OperationContext::new(None, None),
)
.expect("prep should succeed");
}
let health = geometry_prep_artifact_health_op(
GeometryPrepArtifactHealthQuery::default(),
OperationContext::new(None, None),
)
.expect("health op should succeed");
assert_eq!(health.operation, "geometry.prep_artifact_health");
assert_eq!(health.op_version, "geometry.prep_artifact_health/v1");
assert_eq!(
health.data.schema_version,
"geometry-prep-artifact-health/v1"
);
assert!(health.data.current_artifact_count >= 1);
assert!(
health.data.age_p95_seconds.unwrap_or(0.0) >= health.data.age_p50_seconds.unwrap_or(0.0)
);
assert!(!health.data.per_geometry.is_empty());
reset_prep_artifact_store_for_tests();
}
#[test]
fn asset_summary_reports_semantic_cad_region_status() {
let asset = GeometryAsset {
geometry_id: "geo_semantic".to_string(),
source: GeometrySource {
path: "/models/bracket.step".to_string(),
sha256: "abc123".to_string(),
importer_version: "cad/occt/step/v1".to_string(),
},
source_geometry: SourceGeometry {
kind: SourceGeometryKind::Cad,
assembly: None,
material_evidence: vec![],
},
tessellation_profile: TessellationProfile::default(),
units: UnitSystem::Meter,
revision: 1,
meshes: vec![MeshDescriptor {
mesh_id: "mesh_1".to_string(),
kind: MeshKind::Surface,
vertex_count: 3,
element_count: 1,
}],
surface_meshes: vec![SurfaceMesh::new(
"mesh_1",
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
vec![[0, 1, 2]],
)],
regions: vec![
Region {
region_id: "face_000001".to_string(),
name: "Face 1".to_string(),
tag: Some("occt_face".to_string()),
cad_ownership: Some(CadRegionOwnership {
face_id: Some(0),
label: None,
owner_path: Vec::new(),
layers: Vec::new(),
color: None,
material: None,
}),
},
Region {
region_id: "cad_label_0_1_1".to_string(),
name: "Bracket".to_string(),
tag: Some("cad_body".to_string()),
cad_ownership: Some(CadRegionOwnership {
face_id: None,
label: Some(CadLabelRef {
label_entry: "0:1:1".to_string(),
name: "Bracket".to_string(),
kind: CadSemanticKind::Body,
}),
owner_path: Vec::new(),
layers: Vec::new(),
color: None,
material: None,
}),
},
],
region_entity_mappings: vec![
RegionEntityMapping::all_faces("face_000001", "mesh_1", 1),
RegionEntityMapping::all_faces("cad_label_0_1_1", "mesh_1", 1),
],
diagnostics: vec![],
};
let summary = geometry_asset_summary(&asset);
assert_eq!(summary.cad.face_region_count, 1);
assert_eq!(summary.cad.semantic_region_count, 1);
assert_eq!(summary.cad.mapped_semantic_region_count, 1);
assert_eq!(
summary.cad.region_status,
GeometryCadRegionStatus::SemanticRegions
);
}
#[cfg(feature = "plot-core")]
#[test]
fn geometry_preview_scene_batches_face_labels_under_same_assembly_owner() {
let owner = CadLabelRef {
label_entry: "0:1".to_string(),
name: "Bracket".to_string(),
kind: CadSemanticKind::Body,
};
let asset = GeometryAsset {
geometry_id: "geo_batched_preview".to_string(),
source: GeometrySource {
path: "/models/bracket.step".to_string(),
sha256: "abc123".to_string(),
importer_version: "cad/occt/step/v1".to_string(),
},
source_geometry: SourceGeometry {
kind: SourceGeometryKind::Cad,
assembly: Some(AssemblyNode {
node_id: owner.label_entry.clone(),
label: owner.name.clone(),
children: Vec::new(),
}),
material_evidence: vec![],
},
tessellation_profile: TessellationProfile::default(),
units: UnitSystem::Meter,
revision: 1,
meshes: vec![MeshDescriptor {
mesh_id: "mesh_1".to_string(),
kind: MeshKind::Surface,
vertex_count: 4,
element_count: 2,
}],
surface_meshes: vec![SurfaceMesh::new(
"mesh_1",
vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
],
vec![[0, 1, 2], [0, 2, 3]],
)],
regions: vec![
Region {
region_id: "face_000001".to_string(),
name: "Face 1".to_string(),
tag: Some("occt_face".to_string()),
cad_ownership: Some(CadRegionOwnership {
face_id: Some(0),
label: Some(CadLabelRef {
label_entry: "0:1:face:1".to_string(),
name: "Face 1".to_string(),
kind: CadSemanticKind::Face,
}),
owner_path: vec![owner.clone()],
layers: Vec::new(),
color: None,
material: None,
}),
},
Region {
region_id: "face_000002".to_string(),
name: "Face 2".to_string(),
tag: Some("occt_face".to_string()),
cad_ownership: Some(CadRegionOwnership {
face_id: Some(1),
label: Some(CadLabelRef {
label_entry: "0:1:face:2".to_string(),
name: "Face 2".to_string(),
kind: CadSemanticKind::Face,
}),
owner_path: vec![owner.clone()],
layers: Vec::new(),
color: None,
material: None,
}),
},
],
region_entity_mappings: vec![
RegionEntityMapping::new(
"face_000001",
"mesh_1",
EntityKind::Face,
vec![EntityIdRange::new(0, 1)],
),
RegionEntityMapping::new(
"face_000002",
"mesh_1",
EntityKind::Face,
vec![EntityIdRange::new(1, 1)],
),
],
diagnostics: vec![],
};
let scene = geometry_preview_scene(
&asset,
"Preview",
GeometryPreviewSceneOptions {
triangles_per_chunk: 16,
presentation: GeometryPreviewPresentation::Cad,
xray: false,
allow_create_fea_study: false,
},
)
.expect("cad preview scene should be generated");
assert_eq!(scene.chunks.len(), 1);
assert_eq!(scene.chunks[0].owner_node_ids, vec![owner.label_entry]);
assert_eq!(scene.chunks[0].regions.len(), 2);
assert!(scene.chunks[0]
.regions
.iter()
.any(|region| region.region_id == "face_000001"));
assert!(scene.chunks[0]
.regions
.iter()
.any(|region| region.region_id == "face_000002"));
}