use super::*;
use crate::appearance::tests::{apply, CONTROLLED_IFC};
fn fixture() -> (String, AnnotationPlaneRequest) {
let source=CONTROLLED_IFC.replace("#37=IFCINDEXEDTRIANGLETEXTUREMAP", "#40=IFCBUILDINGSTOREY('0Storey0000000000000000',$,'Level',$,$,#11,$,$,.ELEMENT.,0.);\n#37=IFCINDEXEDTRIANGLETEXTUREMAP");
(source, AnnotationPlaneRequest { schema:"IFC4".into(),source_revision:"annotation-test".into(),next_express_id:100,
container_id:40,global_id:"0aaaaaaaaaaaaaaaaaaaaa".into(),containment_global_id:"0bbbbbbbbbbbbbbbbbbbbb".into(),
name:"Calibrated source".into(),image_uri:"textures/image.png".into(),frame:AnnotationPlaneFrame {
origin:[2.,3.,4.],axis_u:[1.,0.,0.],axis_v:[0.,0.,1.],size_metres:[2.,1.] } })
}
#[test]
fn issue_4308_native_annotation_uses_canonical_geometry_and_roundtrips_identity_texture_containment() {
let (source,request)=fixture();
let result=plan_annotation_plane(source.as_bytes(),&request).unwrap();
let exported=apply(&source,&result.plan);
let restored=crate::process_geometry(exported.as_bytes());
let mesh=restored.meshes.iter().find(|m|m.express_id==result.annotation_id).unwrap();
assert_eq!(mesh.geometry_item_id,Some(result.geometry_item_id));
assert_eq!(mesh.positions,result.mesh.positions);
assert_eq!(mesh.normals,result.mesh.normals);
assert_eq!(mesh.indices,result.mesh.indices);
assert_eq!(mesh.uvs,result.mesh.uvs);
assert_eq!(mesh.color,[1.;4]);
assert_eq!(mesh.texture.as_ref().unwrap().url.as_deref(),Some(request.image_uri.as_str()));
let mut decoder=Source::new(exported.as_bytes()).unwrap();
assert_eq!(decoder.entity(result.annotation_id).unwrap().get_string(0),Some(request.global_id.as_str()));
let relation=result.plan.created.iter().find(|e|e.r#type=="IfcRelContainedInSpatialStructure").unwrap();
assert_eq!(decoder.entity(relation.express_id).unwrap().get_ref(5),Some(40));
}
fn assert_footprint(result: &AnnotationPlanePlan) {
let uv=result.mesh.uvs.as_ref().unwrap();
for (i,p) in result.mesh.positions.chunks_exact(3).enumerate() {
let actual:[f64;3]=std::array::from_fn(|axis|f64::from(p[axis])+result.mesh.origin[axis]+result.rtc_offset[axis]);
let expected:[f64;3]=std::array::from_fn(|axis|result.frame.origin[axis]
+result.frame.axis_u[axis]*f64::from(uv[i*2])*result.frame.size_metres[0]
+result.frame.axis_v[axis]*(1.-f64::from(uv[i*2+1]))*result.frame.size_metres[1]);
for axis in 0..3 { assert!((actual[axis]-expected[axis]).abs()<1e-6,"{actual:?} != {expected:?}"); }
}
}
#[test]
fn issue_4308_rotated_millimetre_container_preserves_world_frame_and_asymmetric_image_corners() {
let (source,mut request)=fixture();
let source=source.replace(".LENGTHUNIT.,$,.METRE.",".LENGTHUNIT.,.MILLI.,.METRE.")
.replace("'Level',$,$,#11", "'Level',$,$,#41")
.replace("#37=IFCINDEXEDTRIANGLETEXTUREMAP", "#41=IFCLOCALPLACEMENT($,#42);\n#42=IFCAXIS2PLACEMENT3D(#43,#44,#45);\n#43=IFCCARTESIANPOINT((10000.,20000.,3000.));\n#44=IFCDIRECTION((0.,0.,1.));\n#45=IFCDIRECTION((0.,1.,0.));\n#37=IFCINDEXEDTRIANGLETEXTUREMAP");
request.frame.origin=[15.,25.,6.];
let result=plan_annotation_plane(source.as_bytes(),&request).unwrap();
assert_footprint(&result);
let exported=apply(&source,&result.plan);
let restored=crate::process_geometry(exported.as_bytes());
let mesh=restored.meshes.iter().find(|m|m.express_id==result.annotation_id).unwrap();
assert_eq!(mesh.positions,result.mesh.positions); assert_eq!(mesh.uvs,result.mesh.uvs);
let rgba=[255,0,0,255, 0,255,0,255, 255,255,0,255, 0,0,255,255];
let raster=super::super::page_raster::Raster::new(2,2,&rgba).unwrap();
let uv=mesh.uvs.as_ref().unwrap();
for (i,p) in mesh.positions.chunks_exact(3).enumerate() {
let world_z=f64::from(p[2])+mesh.origin[2];
let sample=raster.sample([f64::from(uv[2*i]),(1.-f64::from(uv[2*i+1]))],[false,false]);
assert_eq!(sample[3],1.);
if (world_z-7.).abs()<1e-6 { assert_eq!(sample[2],0.,"top row must contain red/green, never blue"); }
else { assert!(sample[0]==1. || sample[2]==1.,"bottom row is yellow/blue"); }
}
}
#[test]
fn issue_4308_rejects_wrong_container_stale_allocator_duplicate_identity_and_invalid_pose() {
let (source,request)=fixture();
for changed in [AnnotationPlaneRequest {container_id:10,..request.clone()},
AnnotationPlaneRequest {next_express_id:40,..request.clone()},
AnnotationPlaneRequest {global_id:request.containment_global_id.clone(),..request.clone()},
AnnotationPlaneRequest {frame:AnnotationPlaneFrame {axis_v:[1.,0.,0.],..request.frame.clone()},..request.clone()}] {
assert!(plan_annotation_plane(source.as_bytes(),&changed).is_err());
}
let result=plan_annotation_plane(source.as_bytes(),&request).unwrap();
let exported=apply(&source,&result.plan);
let retry=AnnotationPlaneRequest {next_express_id:result.plan.next_available_express_id,..request};
assert!(plan_annotation_plane(exported.as_bytes(),&retry).unwrap_err().contains("GlobalId already exists"));
let relation_collision=AnnotationPlaneRequest { global_id:"0ccccccccccccccccccccc".into(),
containment_global_id:retry.global_id.clone(),..retry.clone() };
assert!(plan_annotation_plane(exported.as_bytes(),&relation_collision).unwrap_err().contains("GlobalId already exists"));
let owner_collision=AnnotationPlaneRequest { global_id:retry.containment_global_id.clone(),
containment_global_id:"0ddddddddddddddddddddd".into(),..retry };
assert!(plan_annotation_plane(exported.as_bytes(),&owner_collision).unwrap_err().contains("GlobalId already exists"));
}
#[test]
fn issue_4308_georeferenced_creation_keeps_world_footprint_after_normal_reparse() {
let (source,mut request)=fixture();
let source=source.replace("#4=IFCCARTESIANPOINT((0.,0.,0.));", "#4=IFCCARTESIANPOINT((5000000.,6000000.,100.));");
request.frame.origin=[5000002.,6000003.,104.];
let result=plan_annotation_plane(source.as_bytes(),&request).unwrap();
assert_footprint(&result);
let exported=apply(&source,&result.plan);
let restored=crate::process_geometry(exported.as_bytes());
let mesh=restored.meshes.iter().find(|m|m.express_id==result.annotation_id).unwrap().clone();
let reopened=AnnotationPlanePlan { mesh,rtc_offset:restored.metadata.coordinate_info.origin_shift,..result };
assert_footprint(&reopened);
}
#[test]
fn issue_4308_containment_accepts_spatial_elements_beyond_spatial_structure_elements() {
let (source,request)=fixture();
let source=source.replace("IFCBUILDINGSTOREY('0Storey0000000000000000',$,'Level',$,$,#11,$,$,.ELEMENT.,0.)",
"IFCEXTERNALSPATIALELEMENT('0Storey0000000000000000',$,'Outside',$,$,#11,$,$,.NOTDEFINED.)");
assert!(plan_annotation_plane(source.as_bytes(),&request).is_ok());
}
#[test]
fn issue_4308_ifc4x3_emits_predefined_type_and_point_tag_slots() {
let (source,mut request)=fixture();
let source=source.replace("FILE_SCHEMA(('IFC4'))", "FILE_SCHEMA(('IFC4X3'))");
request.schema="IFC4X3".into();
let result=plan_annotation_plane(source.as_bytes(),&request).unwrap();
let exported=apply(&source,&result.plan);
let mut decoded=Source::new(exported.as_bytes()).unwrap();
let annotation=decoded.entity(result.annotation_id).unwrap();
assert_eq!(annotation.attributes.len(),8);
assert!(matches!(annotation.get(7),Some(A::Enum(kind)) if kind=="USERDEFINED"));
let point_id=decoded.entity(result.geometry_item_id).unwrap().get_ref(0).unwrap();
let points=decoded.entity(point_id).unwrap();
assert_eq!(points.attributes.len(),2);
assert!(matches!(points.get(1),Some(A::Null)));
let restored=crate::process_geometry(exported.as_bytes());
let mesh=restored.meshes.iter().find(|m|m.express_id==result.annotation_id).unwrap();
assert_eq!(mesh.positions,result.mesh.positions);
assert_eq!(mesh.uvs,result.mesh.uvs);
}
#[test]
fn issue_4441_authored_metadata_refuses_wire_tokens_and_preserves_ordinary_names() {
let (source, mut request) = fixture();
for token in ["*", "$", "#123", ".ENUM.", " .lower_1. ", "\u{feff}#123\u{feff}"] {
request.name = token.into();
assert!(plan_annotation_plane(source.as_bytes(), &request).unwrap_err().contains("Authored product Name is a reserved appearance wire token"));
}
for name in ["Ordinary name", "O'Brien – 墙", "#not_a_ref", ".not-an-enum.", "* label", "\u{85}#123\u{85}"] {
request.name = name.into();
let result = plan_annotation_plane(source.as_bytes(), &request).unwrap();
assert_eq!(result.plan.created.iter().find(|e| e.express_id == result.annotation_id).unwrap().attributes[2], serde_json::json!(name));
}
for token in ["*", "$", ".ENUM.", " .lower_1. "] {
request.image_uri = token.into();
assert!(plan_annotation_plane(source.as_bytes(), &request).unwrap_err().contains("Image URI is a reserved appearance wire token"));
}
}