use super::WriteBuffer;
use crate::ir::{AngleUnit, LengthUnit, SolidAngleUnit, UnitContext};
use crate::parser::entity::Attribute;
use crate::writer::entity::{WriterBody, WriterEntity};
impl WriteBuffer<'_> {
pub(in crate::writer::buffer) fn emit_unit_context(&mut self, units: UnitContext) -> u64 {
if let Some(n) = self.global_unit_context_id {
return n;
}
let length = self.emit_length_unit(units.length);
let angle = self.emit_angle_unit(units.plane_angle);
let solid = self.emit_solid_angle_unit(units.solid_angle);
let mut parts = vec![
(
"GEOMETRIC_REPRESENTATION_CONTEXT".into(),
vec![Attribute::Integer(3)],
),
(
"GLOBAL_UNIT_ASSIGNED_CONTEXT".into(),
vec![Attribute::List(vec![
Attribute::EntityRef(length),
Attribute::EntityRef(angle),
Attribute::EntityRef(solid),
])],
),
(
"REPRESENTATION_CONTEXT".into(),
vec![
Attribute::String(String::new()),
Attribute::String(String::new()),
],
),
];
if let Some(value) = units.length_uncertainty {
let unc = self.emit_uncertainty_measure(value, length);
parts.insert(
1,
(
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT".into(),
vec![Attribute::List(vec![Attribute::EntityRef(unc)])],
),
);
}
let ctx = self.fresh();
self.entities.push(WriterEntity {
id: ctx,
body: WriterBody::Complex { parts },
});
self.global_unit_context_id = Some(ctx);
ctx
}
fn emit_uncertainty_measure(&mut self, value: f64, length_unit: u64) -> u64 {
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Simple {
name: "UNCERTAINTY_MEASURE_WITH_UNIT".into(),
attrs: vec![
Attribute::Typed {
type_name: "LENGTH_MEASURE".into(),
value: Box::new(Attribute::Real(value)),
},
Attribute::EntityRef(length_unit),
Attribute::String("distance_accuracy_value".into()),
Attribute::String("confusion accuracy".into()),
],
},
});
n
}
fn emit_length_unit(&mut self, unit: LengthUnit) -> u64 {
if let Some(n) = self.length_unit_id {
return n;
}
match unit {
LengthUnit::Millimetre => self.emit_plain_si_length(Some("MILLI")),
LengthUnit::Centimetre => self.emit_plain_si_length(Some("CENTI")),
LengthUnit::Metre => self.emit_plain_si_length(None),
LengthUnit::Inch => self.emit_conversion_based_length("INCH", 25.4),
LengthUnit::Foot => self.emit_conversion_based_length("FOOT", 304.8),
}
}
fn emit_plain_si_length(&mut self, prefix: Option<&'static str>) -> u64 {
let si_attrs = match prefix {
Some(p) => vec![Attribute::Enum(p.into()), Attribute::Enum("METRE".into())],
None => vec![Attribute::Unset, Attribute::Enum("METRE".into())],
};
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Complex {
parts: vec![
("LENGTH_UNIT".into(), vec![]),
("NAMED_UNIT".into(), vec![Attribute::Derived]),
("SI_UNIT".into(), si_attrs),
],
},
});
self.length_unit_id = Some(n);
n
}
fn emit_base_si_millimetre(&mut self) -> u64 {
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Complex {
parts: vec![
("LENGTH_UNIT".into(), vec![]),
("NAMED_UNIT".into(), vec![Attribute::Derived]),
(
"SI_UNIT".into(),
vec![
Attribute::Enum("MILLI".into()),
Attribute::Enum("METRE".into()),
],
),
],
},
});
n
}
fn emit_length_dim_exponents(&mut self) -> u64 {
if let Some(n) = self.length_dim_exp_id {
return n;
}
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Simple {
name: "DIMENSIONAL_EXPONENTS".into(),
attrs: vec![
Attribute::Real(1.0),
Attribute::Real(0.0),
Attribute::Real(0.0),
Attribute::Real(0.0),
Attribute::Real(0.0),
Attribute::Real(0.0),
Attribute::Real(0.0),
],
},
});
self.length_dim_exp_id = Some(n);
n
}
fn emit_dimensionless_exponents(&mut self) -> u64 {
if let Some(n) = self.dimensionless_dim_exp_id {
return n;
}
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Simple {
name: "DIMENSIONAL_EXPONENTS".into(),
attrs: vec![Attribute::Real(0.0); 7],
},
});
self.dimensionless_dim_exp_id = Some(n);
n
}
fn emit_conversion_based_length(&mut self, name: &str, mm_per_unit: f64) -> u64 {
let base_si = self.emit_base_si_millimetre();
let dim_exp = self.emit_length_dim_exponents();
let measure = self.fresh();
self.entities.push(WriterEntity {
id: measure,
body: WriterBody::Simple {
name: "LENGTH_MEASURE_WITH_UNIT".into(),
attrs: vec![Attribute::Real(mm_per_unit), Attribute::EntityRef(base_si)],
},
});
let outer = self.fresh();
self.entities.push(WriterEntity {
id: outer,
body: WriterBody::Complex {
parts: vec![
(
"CONVERSION_BASED_UNIT".into(),
vec![
Attribute::String(name.into()),
Attribute::EntityRef(measure),
],
),
("LENGTH_UNIT".into(), vec![]),
("NAMED_UNIT".into(), vec![Attribute::EntityRef(dim_exp)]),
],
},
});
self.length_unit_id = Some(outer);
outer
}
fn emit_angle_unit(&mut self, unit: AngleUnit) -> u64 {
if let Some(n) = self.angle_unit_id {
return n;
}
match unit {
AngleUnit::Radian => self.emit_plain_si_radian(),
AngleUnit::Degree => self.emit_conversion_based_degree(),
}
}
fn emit_plain_si_radian(&mut self) -> u64 {
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Complex {
parts: vec![
(
"SI_UNIT".into(),
vec![Attribute::Unset, Attribute::Enum("RADIAN".into())],
),
("NAMED_UNIT".into(), vec![Attribute::Derived]),
("PLANE_ANGLE_UNIT".into(), vec![]),
],
},
});
self.angle_unit_id = Some(n);
n
}
fn emit_base_si_radian(&mut self) -> u64 {
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Complex {
parts: vec![
("NAMED_UNIT".into(), vec![Attribute::Derived]),
("PLANE_ANGLE_UNIT".into(), vec![]),
(
"SI_UNIT".into(),
vec![Attribute::Unset, Attribute::Enum("RADIAN".into())],
),
],
},
});
n
}
fn emit_conversion_based_degree(&mut self) -> u64 {
let base_si = self.emit_base_si_radian();
let dim_exp = self.emit_dimensionless_exponents();
let measure = self.fresh();
self.entities.push(WriterEntity {
id: measure,
body: WriterBody::Simple {
name: "PLANE_ANGLE_MEASURE_WITH_UNIT".into(),
attrs: vec![
Attribute::Real(std::f64::consts::PI / 180.0),
Attribute::EntityRef(base_si),
],
},
});
let outer = self.fresh();
self.entities.push(WriterEntity {
id: outer,
body: WriterBody::Complex {
parts: vec![
(
"CONVERSION_BASED_UNIT".into(),
vec![
Attribute::String("DEGREE".into()),
Attribute::EntityRef(measure),
],
),
("NAMED_UNIT".into(), vec![Attribute::EntityRef(dim_exp)]),
("PLANE_ANGLE_UNIT".into(), vec![]),
],
},
});
self.angle_unit_id = Some(outer);
outer
}
fn emit_solid_angle_unit(&mut self, _unit: SolidAngleUnit) -> u64 {
if let Some(n) = self.solid_angle_unit_id {
return n;
}
let parts = vec![
(
"SI_UNIT".into(),
vec![Attribute::Unset, Attribute::Enum("STERADIAN".into())],
),
("NAMED_UNIT".into(), vec![Attribute::Derived]),
("SOLID_ANGLE_UNIT".into(), vec![]),
];
let n = self.fresh();
self.entities.push(WriterEntity {
id: n,
body: WriterBody::Complex { parts },
});
self.solid_angle_unit_id = Some(n);
n
}
}
#[cfg(test)]
mod tests {
use crate::ir::{AngleUnit, LengthUnit, SolidAngleUnit, StepModel, UnitContext};
use crate::parse;
use crate::reader::ReaderContext;
fn model_with_units(units: UnitContext) -> StepModel {
StepModel {
units: Some(units),
..StepModel::default()
}
}
#[test]
fn writes_length_unit_inch_as_conversion_based_unit() {
let model = model_with_units(UnitContext {
length: LengthUnit::Inch,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
});
let out = model.write_to_string().expect("write");
assert!(out.contains("CONVERSION_BASED_UNIT('INCH'"), "{out}");
assert!(out.contains("LENGTH_MEASURE_WITH_UNIT(25.4"), "{out}");
assert!(out.contains("DIMENSIONAL_EXPONENTS(1."), "{out}");
assert!(out.contains("(.MILLI.,.METRE.)"), "{out}");
}
#[test]
fn writes_length_unit_foot_as_conversion_based_unit() {
let model = model_with_units(UnitContext {
length: LengthUnit::Foot,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
});
let out = model.write_to_string().expect("write");
assert!(out.contains("CONVERSION_BASED_UNIT('FOOT'"), "{out}");
assert!(out.contains("LENGTH_MEASURE_WITH_UNIT(304.8"), "{out}");
assert!(out.contains("(.MILLI.,.METRE.)"), "{out}");
}
#[test]
fn writes_angle_unit_degree_as_conversion_based_unit() {
let model = model_with_units(UnitContext {
length: LengthUnit::Millimetre,
plane_angle: AngleUnit::Degree,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
});
let out = model.write_to_string().expect("write");
assert!(out.contains("CONVERSION_BASED_UNIT('DEGREE'"), "{out}");
assert!(
out.contains("PLANE_ANGLE_MEASURE_WITH_UNIT(0.017453"),
"{out}"
);
assert!(out.contains("DIMENSIONAL_EXPONENTS(0."), "{out}");
}
#[test]
fn writes_millimetre_omits_conversion_based_unit() {
let model = model_with_units(UnitContext {
length: LengthUnit::Millimetre,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
});
let out = model.write_to_string().expect("write");
assert!(
!out.contains("CONVERSION_BASED_UNIT"),
"plain mm should not wrap in CBU: {out}"
);
assert!(out.contains("(.MILLI.,.METRE.)"), "{out}");
}
#[test]
fn writes_centimetre_omits_conversion_based_unit() {
let model = model_with_units(UnitContext {
length: LengthUnit::Centimetre,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
});
let out = model.write_to_string().expect("write");
assert!(!out.contains("CONVERSION_BASED_UNIT"), "{out}");
assert!(out.contains("(.CENTI.,.METRE.)"), "{out}");
}
#[test]
fn writes_metre_omits_conversion_based_unit() {
let model = model_with_units(UnitContext {
length: LengthUnit::Metre,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
});
let out = model.write_to_string().expect("write");
assert!(!out.contains("CONVERSION_BASED_UNIT"), "{out}");
assert!(out.contains("SI_UNIT($,.METRE.)"), "{out}");
}
#[test]
fn non_metric_units_survive_write_then_read() {
let cases = [
UnitContext {
length: LengthUnit::Inch,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
},
UnitContext {
length: LengthUnit::Foot,
plane_angle: AngleUnit::Radian,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
},
UnitContext {
length: LengthUnit::Metre,
plane_angle: AngleUnit::Degree,
solid_angle: SolidAngleUnit::Steradian,
length_uncertainty: None,
},
];
for unit in cases {
let model = model_with_units(unit);
let text = model.write_to_string().expect("write");
let graph = parse(&text).expect("re-parse");
let back = ReaderContext::convert(&graph);
assert!(
back.warnings.is_empty(),
"warnings for {unit:?}: {:#?}",
back.warnings
);
assert_eq!(
back.model.units,
Some(unit),
"unit not preserved for {unit:?}"
);
}
}
}