use super::*;
use ifc_lite_core::{EntityDecoder, RtcVerdict};
const IFC: &str = "\
ISO-10303-21;
HEADER;
ENDSEC;
DATA;
#1=IFCPROJECT('p',$,'P',$,$,$,$,(#5),#8);
#5=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,#6,$);
#6=IFCAXIS2PLACEMENT3D(#7,$,$);
#7=IFCCARTESIANPOINT((0.,0.,0.));
#8=IFCUNITASSIGNMENT((#9));
#9=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#20=IFCSITE('site',$,$,$,$,#21,$,$,.ELEMENT.,$,$,$,$,$);
#21=IFCLOCALPLACEMENT($,#22);
#22=IFCAXIS2PLACEMENT3D(#23,$,$);
#23=IFCCARTESIANPOINT((800000.,900000.,0.));
#40=IFCWALL('wall',$,$,$,$,#41,#50,$,$);
#41=IFCLOCALPLACEMENT($,#42);
#42=IFCAXIS2PLACEMENT3D(#43,$,$);
#43=IFCCARTESIANPOINT((800000.,900000.,0.));
#50=IFCPRODUCTDEFINITIONSHAPE($,$,(#51));
#51=IFCSHAPEREPRESENTATION(#5,'Body','SweptSolid',(#52));
#52=IFCEXTRUDEDAREASOLID(#53,#56,#60,1.0);
#53=IFCRECTANGLEPROFILEDEF(.AREA.,$,#54,1.0,1.0);
#54=IFCAXIS2PLACEMENT2D(#55,$);
#55=IFCCARTESIANPOINT((0.,0.));
#56=IFCAXIS2PLACEMENT3D(#57,$,$);
#57=IFCCARTESIANPOINT((0.,0.,0.));
#60=IFCDIRECTION((0.,0.,1.));
ENDSEC;
END-ISO-10303-21;
";
#[test]
fn small_file_single_resolves_scale_and_offset() {
let content = IFC.as_bytes();
let full_index = ifc_lite_core::build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, full_index);
let meta = resolve_stream_meta(
MetaMode::SmallFileSingle,
content,
Some(1),
None,
&mut decoder,
);
assert_eq!(meta.length_unit_scale, 1.0, "metric project → scale 1");
assert!(meta.frame.needs_shift(), "800 km offset must trigger a shift");
assert!(
coord_is_large(meta.frame.rtc_offset()),
"resolved RTC must exceed the large-coordinate threshold, got {:?}",
meta.frame.rtc_offset()
);
}
#[test]
fn streaming_partial_full_index_fallback_recovers_offset() {
let content = IFC.as_bytes();
let partial_index = ifc_lite_core::EntityIndex::default();
let mut decoder = EntityDecoder::with_index(content, partial_index);
let meta = resolve_stream_meta(
MetaMode::StreamingPartial { scanned_through: content.len() },
content,
Some(1),
None, &mut decoder,
);
assert!(
meta.frame.needs_shift(),
"3-stage fallback must recover the large offset from the full index, got {:?}",
meta.frame.rtc_offset()
);
assert!(coord_is_large(meta.frame.rtc_offset()));
}
#[test]
fn streaming_partial_first_pass_success_suppresses_fallback() {
let near = IFC.replace(
"#43=IFCCARTESIANPOINT((800000.,900000.,0.));",
"#43=IFCCARTESIANPOINT((1.,2.,0.));",
);
let content = near.as_bytes();
let full = ifc_lite_core::build_entity_index(content);
let partial = || {
let mut p = ifc_lite_core::EntityIndex::default();
for id in [1u32, 8, 9, 20, 40, 41, 42, 43, 50, 51] {
if let Some(&s) = full.get(&id) {
p.insert(id, s);
}
}
p
};
let mut probe = EntityDecoder::with_index(content, partial());
assert_eq!(
GeometryRouter::with_scale(1.0).detect_rtc_anchor_for_file(content, &mut probe),
Some((0.0, 0.0, 0.0)),
"first pass must succeed on the partial index"
);
assert!(
matches!(
ifc_lite_core::scan_placement_bounds(content).rtc_offset(1.0),
Some(RtcVerdict::Large { .. })
),
"placement-bounds fallback would shift (large) if taken"
);
let site = *full.get(&20).expect("site present");
let mut decoder = EntityDecoder::with_index(content, partial());
let meta = resolve_stream_meta(
MetaMode::StreamingPartial { scanned_through: content.len() },
content,
Some(1),
Some((20, site.0, site.1)),
&mut decoder,
);
assert!(!meta.frame.needs_shift(), "partial-pass success suppresses the shift");
assert_eq!(
meta.frame.rtc_offset(),
(0.0, 0.0, 0.0),
"offset comes from the partial pass, not the placement-bounds fallback"
);
}
const IFC_STAGE3: &str = "\
ISO-10303-21;
HEADER;
ENDSEC;
DATA;
#1=IFCPROJECT('p',$,'P',$,$,$,$,$,#8);
#8=IFCUNITASSIGNMENT((#9));
#9=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#40=IFCWALL('wall',$,$,$,$,#41,$,$,$);
#41=IFCLOCALPLACEMENT($,#42);
#42=IFCAXIS2PLACEMENT3D(#43,$,$);
#43=IFCCARTESIANPOINT((80000000.,90000000.,0.));
ENDSEC;
END-ISO-10303-21;
";
#[test]
fn streaming_partial_stage3_placement_bounds_fallback() {
let content = IFC_STAGE3.as_bytes();
let full_index = ifc_lite_core::build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, full_index);
let meta = resolve_stream_meta(
MetaMode::StreamingPartial { scanned_through: content.len() },
content,
Some(1),
None,
&mut decoder,
);
let scale = meta.length_unit_scale;
assert!((scale - 0.001).abs() < 1e-12, "expected mm scale, got {scale}");
let raw = ifc_lite_core::scan_placement_bounds(content)
.rtc_offset(1.0)
.expect("the fixture has placement points")
.offset();
assert_eq!(raw, (80_000_000.0, 90_000_000.0, 0.0), "raw mm bounds");
let expected = (raw.0 * scale, raw.1 * scale, raw.2 * scale);
assert_eq!(meta.frame.rtc_offset(), expected, "stage 3 unit-scales raw bounds");
assert_ne!(meta.frame.rtc_offset(), raw, "scaling changed the value");
assert!(meta.frame.needs_shift());
}
const IFC_SUB_THRESHOLD_ANCHOR: &str = "\
ISO-10303-21;
HEADER;
ENDSEC;
DATA;
#1=IFCPROJECT('p',$,'P',$,$,$,$,$,#8);
#8=IFCUNITASSIGNMENT((#9));
#9=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#40=IFCWALL('wall',$,$,$,$,#41,$,$,$);
#41=IFCLOCALPLACEMENT($,#42);
#42=IFCAXIS2PLACEMENT3D(#43,$,$);
#43=IFCCARTESIANPOINT((15000.,0.,0.));
#44=IFCAXIS2PLACEMENT3D(#45,$,$);
#45=IFCCARTESIANPOINT((2000.,0.,0.));
ENDSEC;
END-ISO-10303-21;
";
#[test]
fn browser_and_native_pick_the_same_frame_for_a_sub_threshold_anchor() {
let content = IFC_SUB_THRESHOLD_ANCHOR.as_bytes();
let anchor = (8500.0, 0.0, 0.0);
assert_eq!(
ifc_lite_core::scan_placement_bounds(content).rtc_offset(1.0),
Some(RtcVerdict::Large { anchor }),
"premise: the bounds are large and anchor on the in-threshold centre"
);
assert!(!coord_is_large(anchor), "premise: the anchor is inside 10 km");
let native = crate::process_geometry(IFC_SUB_THRESHOLD_ANCHOR);
assert_eq!(native.mesh_coordinate_space, crate::MeshCoordinateSpace::ModelRtc);
assert_eq!(native.metadata.coordinate_info.origin_shift, [8500.0, 0.0, 0.0]);
for mode in [
MetaMode::SmallFileSingle,
MetaMode::StreamingPartial { scanned_through: content.len() },
] {
let full_index = ifc_lite_core::build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, full_index);
let meta = resolve_stream_meta(mode, content, Some(1), None, &mut decoder);
assert_eq!(meta.frame, MeshFrame::ModelRtc { anchor }, "{mode:?}");
assert!(meta.frame.needs_shift(), "{mode:?}");
assert_eq!(meta.frame.coordinate_space(), native.mesh_coordinate_space, "{mode:?}");
}
}
#[test]
fn small_file_single_bounds_fallback_scales_millimetres_before_the_gate() {
let mm = IFC_STAGE3.replace(
"#43=IFCCARTESIANPOINT((80000000.,90000000.,0.));",
"#43=IFCCARTESIANPOINT((25000.,25000.,0.));",
);
let content = mm.as_bytes();
let full_index = ifc_lite_core::build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, full_index);
assert_eq!(
GeometryRouter::with_scale(0.001).detect_rtc_anchor_for_file(content, &mut decoder),
None,
"premise: no representation, so no job sample"
);
assert!(
coord_is_large(ifc_lite_core::scan_placement_bounds(content).centroid()),
"premise: the raw mm centroid reads as large"
);
let meta = resolve_stream_meta(
MetaMode::SmallFileSingle,
content,
Some(1),
None,
&mut decoder,
);
assert!((meta.length_unit_scale - 0.001).abs() < 1e-12, "mm project");
assert!(
!meta.frame.needs_shift(),
"a 25 m mm model must not be re-based, got offset {:?}",
meta.frame.rtc_offset()
);
assert_eq!(meta.frame.rtc_offset(), (0.0, 0.0, 0.0));
}
#[test]
fn small_file_single_bounds_fallback_rebases_a_kilometre_model() {
let km = IFC_STAGE3
.replace(
"#9=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);",
"#9=IFCSIUNIT(*,.LENGTHUNIT.,.KILO.,.METRE.);",
)
.replace(
"#43=IFCCARTESIANPOINT((80000000.,90000000.,0.));",
"#43=IFCCARTESIANPOINT((5000.,5000.,0.));",
);
let content = km.as_bytes();
let full_index = ifc_lite_core::build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, full_index);
assert!(
!coord_is_large(ifc_lite_core::scan_placement_bounds(content).centroid()),
"premise: the raw km centroid reads as small"
);
let meta = resolve_stream_meta(
MetaMode::SmallFileSingle,
content,
Some(1),
None,
&mut decoder,
);
assert!((meta.length_unit_scale - 1000.0).abs() < 1e-9, "km project");
assert!(meta.frame.needs_shift(), "5 000 km out must be re-based");
assert_eq!(
meta.frame.rtc_offset(),
(5_000_000.0, 5_000_000.0, 0.0),
"offset in metres"
);
}