use axiolid_core::{Frame2, Point2, Vec2};
use axiolid_curve::{CurvatureLaw, Intrinsic2};
use ifc_model::EntityId;
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::{HorizontalSegment, HorizontalSegmentType};
pub(super) const POSITION_TOLERANCE: f64 = 1e-6;
pub(super) const DIRECTION_TOLERANCE: f64 = 1e-6;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeamCheck {
Verified,
Authored,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct HorizontalSeam {
pub previous: EntityId,
pub next: EntityId,
pub distance_along: f64,
pub position: SeamCheck,
}
pub(super) fn closed_form_end_point(segment: &HorizontalSegment) -> Option<Point2> {
match segment.segment_type {
HorizontalSegmentType::Line => {
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
Some(segment.start_point + direction * segment.segment_length)
}
HorizontalSegmentType::CircularArc if segment.start_radius != 0.0 => {
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
let left = Vec2::new(-direction.y, direction.x);
let centre = segment.start_point + left * segment.start_radius;
let sweep = segment.segment_length / segment.start_radius;
let radial = segment.start_point - centre;
let (sin_s, cos_s) = sweep.sin_cos();
Some(
centre
+ Vec2::new(
radial.x * cos_s - radial.y * sin_s,
radial.x * sin_s + radial.y * cos_s,
),
)
}
_ => None,
}
}
pub(super) fn check_position(
previous: &HorizontalSegment,
next: &HorizontalSegment,
distance_along: f64,
) -> AlignmentResult<HorizontalSeam> {
let position = match closed_form_end_point(previous) {
Some(end) if end.distance(next.start_point) <= POSITION_TOLERANCE => SeamCheck::Verified,
Some(_) => {
return Err(AlignmentError::SemanticViolation {
entity: Some(next.entity),
rule: "consecutive horizontal segments must share an endpoint exactly",
})
}
None => SeamCheck::Authored,
};
Ok(HorizontalSeam {
previous: previous.entity,
next: next.entity,
distance_along,
position,
})
}
pub(super) fn check_direction(
previous: &HorizontalSegment,
law: &CurvatureLaw,
next: &HorizontalSegment,
) -> AlignmentResult<()> {
let turning = if previous.segment_length == 0.0 {
0.0
} else {
Intrinsic2::new(unit_frame(), law.clone(), previous.segment_length)
.total_turning()
.ok_or(AlignmentError::InvalidSegment {
entity: previous.entity,
detail: "horizontal segment curvature law has no finite turning integral",
})?
};
let end = previous.start_direction + turning;
let difference = (next.start_direction - end).rem_euclid(core::f64::consts::TAU);
let difference = difference.min(core::f64::consts::TAU - difference);
if difference.is_finite() && difference <= DIRECTION_TOLERANCE {
Ok(())
} else {
Err(AlignmentError::SemanticViolation {
entity: Some(next.entity),
rule: "consecutive horizontal segments must share a tangent direction: \
one exact plan curve cannot carry a kink",
})
}
}
fn unit_frame() -> Frame2 {
Frame2 {
origin: Point2::new(0.0, 0.0),
x: Vec2::new(1.0, 0.0),
y: Vec2::new(0.0, 1.0),
}
}