Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/ifc-alignment/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ everything released before per-crate changelogs began.

## [Unreleased]

### Changed (#585)

- Breaking: `AlignmentUnits::angle_to_radians` is `Option<f64>`; `None` means
the project declares no `PLANEANGLEUNIT`. Reading a `StartDirection` then
refuses with the new `AlignmentError::AngleUnitUndeclared { entity }`;
length-only reads (`SeamTolerance::for_model`, `resolve_linear_placement`)
need no angle unit.

## [0.10.2] - 2026-10-28

### Changed
Expand Down
4 changes: 2 additions & 2 deletions crates/ifc-alignment/src/curve/tolerance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ mod tests {
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down Expand Up @@ -232,7 +232,7 @@ mod tests {
context(&mut model, 1, 3, Value::Real(0.1));
let millimetres = AlignmentUnits {
length_to_metres: 0.001,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};
let tolerance = SeamTolerance::for_model(&model, millimetres).expect("tolerance");
assert!((tolerance.length() - 1e-4).abs() < 1e-18);
Expand Down
9 changes: 9 additions & 0 deletions crates/ifc-alignment/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ pub enum AlignmentError {
/// What expectation failed.
detail: &'static str,
},
/// An angle attribute was read, and the project declares no `PLANEANGLEUNIT`.
AngleUnitUndeclared {
/// The entity holding the angle.
entity: EntityId,
},
/// A horizontal, vertical, or cant segment carries internally inconsistent data.
InvalidSegment {
/// The segment entity.
Expand Down Expand Up @@ -212,6 +217,10 @@ impl std::fmt::Display for AlignmentError {
name,
} => write!(f, "{entity} has invalid {name} at slot {index}"),
Self::InvalidUnits { detail } => write!(f, "invalid alignment units: {detail}"),
Self::AngleUnitUndeclared { entity } => write!(
f,
"{entity} holds a plane angle, and the project declares no PLANEANGLEUNIT"
),
Self::InvalidSegment { entity, detail } => {
write!(f, "invalid alignment segment {entity}: {detail}")
}
Expand Down
16 changes: 12 additions & 4 deletions crates/ifc-alignment/src/horizontal/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ use crate::error::{AlignmentError, AlignmentResult};
pub struct AlignmentUnits {
/// Factor converting a length attribute's stored value to metres.
pub length_to_metres: f64,
/// Factor converting an angle attribute's stored value to radians.
pub angle_to_radians: f64,
/// Factor converting an angle attribute's stored value to radians, or
/// `None` when the project declares no plane-angle unit; reading an angle
/// then refuses with [`AlignmentError::AngleUnitUndeclared`].
pub angle_to_radians: Option<f64>,
}

impl AlignmentUnits {
Expand All @@ -22,7 +24,10 @@ impl AlignmentUnits {
detail: "length factor must be finite and positive",
});
}
if !self.angle_to_radians.is_finite() || self.angle_to_radians <= 0.0 {
if self
.angle_to_radians
.is_some_and(|factor| !factor.is_finite() || factor <= 0.0)
{
return Err(AlignmentError::InvalidUnits {
detail: "angle factor must be finite and positive",
});
Expand Down Expand Up @@ -171,8 +176,11 @@ pub fn read_horizontal_segment(
})?
* units.length_to_metres;

let angle_to_radians = units
.angle_to_radians
.ok_or(AlignmentError::AngleUnitUndeclared { entity: id })?;
let start_direction =
required_number(entity, id, START_DIRECTION, "StartDirection")? * units.angle_to_radians;
required_number(entity, id, START_DIRECTION, "StartDirection")? * angle_to_radians;
let start_radius = required_number(entity, id, START_RADIUS, "StartRadiusOfCurvature")?
* units.length_to_metres;
let end_radius =
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/authoring_referent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn an_authored_referent_resolves_to_its_station() {

let units = AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};
let stations = station_equations(&model, units).expect("resolves");
assert_eq!(stations.len(), 1, "the authored referent is found");
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/authoring_roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use ifc_model::{Entity, Model, Transaction, Value};
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/banked_centreline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const B: f64 = 1.5;
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/cant_frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use ifc_model::{Entity, EntityId, Model, Transaction, Value};
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/closing_segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use ifc_step::StepCodec;
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/declaration_inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use ifc_schema::{ifc4x3, TypeKind};

const UNITS: AlignmentUnits = AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};

fn members(enumeration: &str) -> Vec<String> {
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/gradient_curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::sync::Arc;
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
10 changes: 5 additions & 5 deletions crates/ifc-alignment/tests/horizontal_curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn segment(kind: &str, start_radius: f64, end_radius: f64, length: f64) -> Model
fn millimetres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 0.001,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down Expand Up @@ -127,7 +127,7 @@ fn finite_arc_inputs_that_overflow_the_circle_frame_are_rejected() {
EntityId(2),
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
},
),
Err(AlignmentError::InvalidSegment { .. })
Expand All @@ -149,7 +149,7 @@ fn finite_arc_inputs_that_overflow_the_trim_parameter_are_rejected() {
EntityId(2),
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
},
),
Err(AlignmentError::InvalidSegment { .. })
Expand All @@ -165,7 +165,7 @@ fn large_global_coordinates_do_not_collapse_the_arc_frame_axes() {
EntityId(2),
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
},
)
.expect("finite arc");
Expand Down Expand Up @@ -243,7 +243,7 @@ fn committed_ifc_fixture_lowers_line_and_arc_exactly() {
let model = StepCodec.read_path(&path).expect("fixture parses");
let units = AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};
let line = lower_horizontal_segment(&model, EntityId(61), units).expect("line");
let arc = lower_horizontal_segment(&model, EntityId(62), units).expect("arc");
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/horizontal_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use ifc_step::StepCodec;
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/layout_and_placement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn fixture() -> ifc_model::Model {
fn units() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
6 changes: 3 additions & 3 deletions crates/ifc-alignment/tests/parameter_segments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn fixture() -> ifc_model::Model {
fn units() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand All @@ -29,7 +29,7 @@ fn resolves_vertical_parameters_and_units_without_curve_approximation() {
EntityId(63),
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
},
)
.expect("vertical segment");
Expand All @@ -54,7 +54,7 @@ fn resolves_cant_parameters_and_signed_offsets() {
EntityId(64),
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
},
)
.expect("cant segment");
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/partial_lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn load(name: &str) -> ifc_model::Model {
fn units() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/profile_continuity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use ifc_model::{Entity, EntityId, Model, Value};
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/ifc-alignment/tests/real_export_survey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fn files(dir: &Path, out: &mut Vec<PathBuf>) {
fn units(model: &Model) -> AlignmentUnits {
let mut units = AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};
for (_, entity) in model.iter() {
let kind = entity.attributes.get(1).and_then(|v| match v {
Expand All @@ -117,7 +117,7 @@ fn units(model: &Model) -> AlignmentUnits {
.eq_ignore_ascii_case("IFCCONVERSIONBASEDUNIT")
&& kind.as_deref() == Some("PLANEANGLEUNIT")
{
units.angle_to_radians = core::f64::consts::PI / 180.0;
units.angle_to_radians = Some(core::f64::consts::PI / 180.0);
}
}
units
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/referent_value_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use ifc_step::StepCodec;

const UNITS: AlignmentUnits = AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};

/// The STEP record of `id`, from the written text.
Expand Down
4 changes: 2 additions & 2 deletions crates/ifc-alignment/tests/seam_tolerance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use ifc_model::{Entity, EntityId, Model, Transaction, Value};
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down Expand Up @@ -207,7 +207,7 @@ fn a_coarse_declared_precision_cannot_join_a_visible_step() {
fn the_precision_is_read_in_the_project_length_unit() {
let millimetres = AlignmentUnits {
length_to_metres: 0.001,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
};
let (model, _, v) = fixture(&ROUNDED, 1000.0, Some(0.1));
vertical_profile_law(&model, v, millimetres).expect("sound within 0.1 mm");
Expand Down
22 changes: 17 additions & 5 deletions crates/ifc-alignment/tests/segment_readers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ use ifc_step::StepCodec;
fn millimetres_and_degrees() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 0.001,
angle_to_radians: PI / 180.0,
angle_to_radians: Some(PI / 180.0),
}
}

fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down Expand Up @@ -317,11 +317,11 @@ fn refuses_unusable_horizontal_units() {
for units in [
AlignmentUnits {
length_to_metres: 0.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
},
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: f64::NAN,
angle_to_radians: Some(f64::NAN),
},
] {
assert!(matches!(
Expand All @@ -331,6 +331,18 @@ fn refuses_unusable_horizontal_units() {
}
}

#[test]
fn a_horizontal_segment_without_an_angle_unit_refuses_naming_it() {
let units = AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: None,
};
assert!(matches!(
read_horizontal_segment(&fixture(), EntityId(2), units),
Err(AlignmentError::AngleUnitUndeclared { entity }) if entity == EntityId(2)
));
}

// ------------------------------------------------------------------ vertical

/// A parabolic arc read from a file. Lengths are scaled; gradients are
Expand Down Expand Up @@ -456,7 +468,7 @@ fn refuses_an_unusable_vertical_length_unit() {
EntityId(1),
AlignmentUnits {
length_to_metres: -1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
),
Err(AlignmentError::InvalidUnits { .. })
Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/spiral_law.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const HORIZONTAL: EntityId = EntityId(191);
fn units() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/step_text_roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use ifc_step::StepCodec;
fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/ifc-alignment/tests/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use ifc_model::{Entity, EntityId, Header, Model, Transaction, Value};
pub fn metres() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
angle_to_radians: Some(1.0),
}
}

Expand Down
Loading
Loading