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]
pub(crate) fn rotation_is_identity(column_major_matrix: &[f64]) -> bool {
if column_major_matrix.len() < 16 {
return false;
}
let r00 = column_major_matrix[0];
let r10 = column_major_matrix[1];
let r20 = column_major_matrix[2];
let r01 = column_major_matrix[4];
let r11 = column_major_matrix[5];
let r21 = column_major_matrix[6];
let r02 = column_major_matrix[8];
let r12 = column_major_matrix[9];
let r22 = column_major_matrix[10];
(r00 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
&& r10.abs() < PLACEMENT_IDENTITY_EPSILON
&& r20.abs() < PLACEMENT_IDENTITY_EPSILON
&& r01.abs() < PLACEMENT_IDENTITY_EPSILON
&& (r11 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
&& r21.abs() < PLACEMENT_IDENTITY_EPSILON
&& r02.abs() < PLACEMENT_IDENTITY_EPSILON
&& r12.abs() < PLACEMENT_IDENTITY_EPSILON
&& (r22 - 1.0).abs() < PLACEMENT_IDENTITY_EPSILON
}
#[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 { placement: [f64; 16] },
ModelRtc { anchor: (f64, f64, f64) },
RawIfc,
}
impl MeshFrame {
pub fn select(site_placement: Option<&[f64]>, detected: Option<RtcVerdict>) -> Self {
if let Some(matrix) = site_placement
.filter(|m| m.len() >= 16)
.filter(|m| translation_is_nonidentity((m[12], m[13], m[14])))
{
let mut placement = [0.0; 16];
placement.copy_from_slice(&matrix[..16]);
return Self::SiteLocal { placement };
}
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 { placement } => (placement[12], placement[13], placement[14]),
Self::ModelRtc { anchor } => anchor,
Self::RawIfc => (0.0, 0.0, 0.0),
}
}
#[inline]
pub fn rotate_into_frame(self, v: [f64; 3]) -> [f64; 3] {
match self {
Self::SiteLocal { placement } if !rotation_is_identity(&placement) => [
placement[0] * v[0] + placement[1] * v[1] + placement[2] * v[2],
placement[4] * v[0] + placement[5] * v[1] + placement[6] * v[2],
placement[8] * v[0] + placement[9] * v[1] + placement[10] * v[2],
],
_ => v,
}
}
#[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 };
fn site_at(t: (f64, f64, f64)) -> [f64; 16] {
let mut m = [0.0; 16];
m[0] = 1.0;
m[5] = 1.0;
m[10] = 1.0;
m[15] = 1.0;
m[12] = t.0;
m[13] = t.1;
m[14] = t.2;
m
}
fn site_at_yawed(t: (f64, f64, f64), degrees: f64) -> [f64; 16] {
let (s, c) = degrees.to_radians().sin_cos();
let mut m = site_at(t);
m[0] = c;
m[1] = s;
m[4] = -s;
m[5] = c;
m
}
#[test]
fn a_translated_site_selects_site_local_over_everything() {
let placement = site_at((500.0, 0.0, 0.0));
let frame = MeshFrame::select(Some(&placement), Some(LARGE_FAR));
assert_eq!(frame, MeshFrame::SiteLocal { placement });
assert_eq!(frame.rtc_offset(), (500.0, 0.0, 0.0));
assert!(frame.needs_shift());
assert_eq!(frame.coordinate_space(), MeshCoordinateSpace::SiteLocal);
let tiny = site_at((0.0, 0.0, 1e-6));
assert_eq!(
MeshFrame::select(Some(&tiny), None),
MeshFrame::SiteLocal { placement: tiny }
);
}
#[test]
fn an_identity_site_falls_through_to_the_detected_anchor() {
let identity = site_at((0.0, 0.0, 0.0));
let sub_epsilon = site_at((1e-10, -1e-10, 0.0));
for site in [None, Some(&identity), Some(&sub_epsilon)] {
let frame = MeshFrame::select(site.map(|m| &m[..]), 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,
] {
let placement = site_at(translation);
for detected in [None, Some(RtcVerdict::Small), Some(LARGE_FAR)] {
let frame = MeshFrame::select(Some(&placement), detected);
assert_ne!(frame, MeshFrame::RawIfc, "{translation:?} / {detected:?}");
assert_eq!(frame.coordinate_space(), MeshCoordinateSpace::SiteLocal);
}
}
assert_eq!(
MeshFrame::select(Some(&site_at((0.0, 0.0, 0.0))), None),
MeshFrame::RawIfc
);
}
#[test]
fn the_site_tier_undoes_its_own_yaw_and_the_others_rotate_nothing() {
let yawed = MeshFrame::select(Some(&site_at_yawed((500.0, 300.0, 0.0), 30.0)), None);
let (s, c) = 30.0f64.to_radians().sin_cos();
let back = yawed.rotate_into_frame([c, s, 0.0]);
for (i, want) in [1.0, 0.0, 0.0].iter().enumerate() {
assert!((back[i] - want).abs() < 1e-12, "axis {i}: {back:?}");
}
let offset = yawed.rtc_offset();
let at_origin = yawed.rotate_into_frame([
500.0 - offset.0,
300.0 - offset.1,
0.0 - offset.2,
]);
assert_eq!(at_origin, [0.0, 0.0, 0.0]);
let v = [3.0, -7.0, 2.0];
let translated_only = MeshFrame::select(Some(&site_at((500.0, 300.0, 0.0))), None);
assert_eq!(translated_only.rotate_into_frame(v), v);
assert_eq!(
MeshFrame::select(None, Some(LARGE_FAR)).rotate_into_frame(v),
v
);
assert_eq!(MeshFrame::RawIfc.rotate_into_frame(v), v);
}
#[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());
}
}