use ifc_lite_core::{EntityDecoder, RtcVerdict};
use ifc_lite_geometry::GeometryRouter;
use serde::{Deserialize, Serialize};
pub(crate) const PLACEMENT_IDENTITY_EPSILON: f64 = 1e-9;
#[inline]
fn translation_is_nonidentity(t: (f64, f64, f64)) -> bool {
t.0.abs() > PLACEMENT_IDENTITY_EPSILON
|| t.1.abs() > PLACEMENT_IDENTITY_EPSILON
|| t.2.abs() > PLACEMENT_IDENTITY_EPSILON
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MeshFrame {
SiteLocal { translation: (f64, f64, f64) },
ModelRtc { anchor: (f64, f64, f64) },
RawIfc,
}
impl MeshFrame {
pub fn select(
site_translation: Option<(f64, f64, f64)>,
detected: Option<RtcVerdict>,
) -> Self {
if let Some(translation) = site_translation.filter(|t| translation_is_nonidentity(*t)) {
return Self::SiteLocal { translation };
}
match detected {
Some(RtcVerdict::Large { anchor }) if translation_is_nonidentity(anchor) => {
Self::ModelRtc { anchor }
}
_ => Self::RawIfc,
}
}
pub fn for_overlay(router: &GeometryRouter, content: &[u8], decoder: &mut EntityDecoder) -> Self {
Self::select(None, router.detect_rtc_offset_for_file(content, decoder))
}
#[inline]
pub fn rtc_offset(self) -> (f64, f64, f64) {
match self {
Self::SiteLocal { translation } => translation,
Self::ModelRtc { anchor } => anchor,
Self::RawIfc => (0.0, 0.0, 0.0),
}
}
#[inline]
pub fn needs_shift(self) -> bool {
!matches!(self, Self::RawIfc)
}
#[inline]
pub fn coordinate_space(self) -> MeshCoordinateSpace {
match self {
Self::SiteLocal { .. } => MeshCoordinateSpace::SiteLocal,
Self::ModelRtc { .. } => MeshCoordinateSpace::ModelRtc,
Self::RawIfc => MeshCoordinateSpace::RawIfc,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MeshCoordinateSpace {
SiteLocal,
ModelRtc,
RawIfc,
}
#[cfg(test)]
mod tests {
use super::*;
const FAR: (f64, f64, f64) = (2_679_062.0, 1_247_992.0, 532.0);
const LARGE_FAR: RtcVerdict = RtcVerdict::Large { anchor: FAR };
#[test]
fn a_translated_site_selects_site_local_over_everything() {
let frame = MeshFrame::select(Some((500.0, 0.0, 0.0)), Some(LARGE_FAR));
assert_eq!(
frame,
MeshFrame::SiteLocal {
translation: (500.0, 0.0, 0.0)
}
);
assert_eq!(frame.rtc_offset(), (500.0, 0.0, 0.0));
assert!(frame.needs_shift());
assert_eq!(frame.coordinate_space(), MeshCoordinateSpace::SiteLocal);
assert_eq!(
MeshFrame::select(Some((0.0, 0.0, 1e-6)), None),
MeshFrame::SiteLocal {
translation: (0.0, 0.0, 1e-6)
}
);
}
#[test]
fn an_identity_site_falls_through_to_the_detected_anchor() {
for site in [None, Some((0.0, 0.0, 0.0)), Some((1e-10, -1e-10, 0.0))] {
let frame = MeshFrame::select(site, Some(LARGE_FAR));
assert_eq!(frame, MeshFrame::ModelRtc { anchor: FAR }, "site {site:?}");
assert_eq!(frame.rtc_offset(), FAR);
assert!(frame.needs_shift());
assert_eq!(frame.coordinate_space(), MeshCoordinateSpace::ModelRtc);
}
}
#[test]
fn no_site_and_no_anchor_is_raw_ifc_with_nothing_to_subtract() {
for detected in [None, Some(RtcVerdict::Small)] {
let frame = MeshFrame::select(None, detected);
assert_eq!(frame, MeshFrame::RawIfc, "detected {detected:?}");
assert_eq!(frame.rtc_offset(), (0.0, 0.0, 0.0));
assert!(!frame.needs_shift());
assert_eq!(frame.coordinate_space(), MeshCoordinateSpace::RawIfc);
}
}
#[test]
fn a_large_verdict_with_a_sub_threshold_anchor_is_subtracted() {
for anchor in [(-5_000.0, 0.0, 0.0), (8_500.0, 0.0, 0.0), (0.0, 0.0, 10_000.0)] {
let frame = MeshFrame::select(None, Some(RtcVerdict::Large { anchor }));
assert_eq!(frame, MeshFrame::ModelRtc { anchor }, "{anchor:?}");
assert!(frame.needs_shift());
}
let at_origin = RtcVerdict::Large { anchor: (0.0, 0.0, 0.0) };
assert_eq!(MeshFrame::select(None, Some(at_origin)), MeshFrame::RawIfc);
}
#[test]
fn raw_ifc_is_never_selected_beside_a_translated_site() {
for translation in [
(1e-8, 0.0, 0.0),
(999.0, 0.0, 0.0),
(1_000.5, 0.0, 0.0),
(0.0, -1_500.0, 0.0),
FAR,
] {
for detected in [None, Some(RtcVerdict::Small), Some(LARGE_FAR)] {
let frame = MeshFrame::select(Some(translation), detected);
assert_ne!(frame, MeshFrame::RawIfc, "{translation:?} / {detected:?}");
assert_eq!(frame.coordinate_space(), MeshCoordinateSpace::SiteLocal);
}
}
assert_eq!(MeshFrame::select(Some((0.0, 0.0, 0.0)), None), MeshFrame::RawIfc);
}
#[test]
fn wire_strings_are_the_documented_snake_case_tags() {
for (space, tag) in [
(MeshCoordinateSpace::SiteLocal, "\"site_local\""),
(MeshCoordinateSpace::ModelRtc, "\"model_rtc\""),
(MeshCoordinateSpace::RawIfc, "\"raw_ifc\""),
] {
assert_eq!(serde_json::to_string(&space).unwrap(), tag);
assert_eq!(
serde_json::from_str::<MeshCoordinateSpace>(tag).unwrap(),
space
);
}
assert!(serde_json::from_str::<MeshCoordinateSpace>("\"SiteLocal\"").is_err());
}
}