ifc_style/light/distribution.rs
1//! `IfcLightIntensityDistribution` and its `IfcLightDistributionData` rows.
2
3use ifc_model::EntityId;
4
5use crate::error::StyleResult;
6use crate::view::Record;
7
8/// Borrowed projection of `IfcLightIntensityDistribution`: a luminaire's
9/// photometric distribution, as a curve type plus its sampled data rows.
10#[derive(Debug, Clone, Copy)]
11pub struct LightIntensityDistribution<'m, 's> {
12 record: Record<'m, 's>,
13}
14
15impl<'m, 's> LightIntensityDistribution<'m, 's> {
16 pub(crate) fn from_record(record: Record<'m, 's>) -> Self {
17 Self { record }
18 }
19
20 /// The `LightDistributionCurve` attribute: a token from
21 /// `IfcLightDistributionCurveEnum` (`TYPE_A`, `TYPE_B`, `TYPE_C`,
22 /// `NOTDEFINED`).
23 ///
24 /// This token is not decoration -- it names which photometric convention
25 /// the angles in [`Self::distribution_data`] follow, so the same numbers
26 /// mean different directions under Type A, B, and C. A consumer that
27 /// ignores it will aim the distribution wrongly. Returned as the raw
28 /// schema token for the same reason as `LightEmissionSource`.
29 pub fn light_distribution_curve(&self) -> StyleResult<&'m str> {
30 self.record.required_enum("LightDistributionCurve")
31 }
32
33 /// The `DistributionData` attribute: at least one
34 /// `IfcLightDistributionData` reference.
35 ///
36 /// The schema declares `LIST [1:?]`, so an empty list is a typed error
37 /// rather than an empty iterator that silently renders an unlit luminaire.
38 pub fn distribution_data(&self) -> StyleResult<Vec<EntityId>> {
39 self.record
40 .required_refs("DistributionData", "IfcLightDistributionData", 1, None)
41 }
42}
43
44/// Borrowed projection of `IfcLightDistributionData`: one main-plane angle and
45/// the intensities measured across the secondary angles in that plane.
46///
47/// `SecondaryPlaneAngle` and `LuminousIntensity` are parallel lists: entry `i`
48/// of one corresponds to entry `i` of the other. The schema does not state
49/// that pairing as a WHERE rule, so [`Self::samples`] enforces it here instead
50/// of letting a caller zip two mismatched lists and silently truncate.
51#[derive(Debug, Clone, Copy)]
52pub struct LightDistributionData<'m, 's> {
53 record: Record<'m, 's>,
54}
55
56impl<'m, 's> LightDistributionData<'m, 's> {
57 pub(crate) fn from_record(record: Record<'m, 's>) -> Self {
58 Self { record }
59 }
60
61 /// The `MainPlaneAngle` attribute, an `IfcPlaneAngleMeasure`.
62 ///
63 /// Signed and unbounded: unlike the spot cone angles this is a plain
64 /// `IfcPlaneAngleMeasure`, not a positive one, so negative angles are
65 /// valid authored data and are not rejected.
66 pub fn main_plane_angle(&self) -> StyleResult<f64> {
67 self.record.required_number("MainPlaneAngle")
68 }
69
70 /// The `SecondaryPlaneAngle` attribute: `LIST [1:?] OF
71 /// IfcPlaneAngleMeasure`.
72 pub fn secondary_plane_angles(&self) -> StyleResult<Vec<f64>> {
73 self.record.required_numbers("SecondaryPlaneAngle", 1)
74 }
75
76 /// The `LuminousIntensity` attribute: `LIST [1:?] OF
77 /// IfcLuminousIntensityDistributionMeasure` (candela per lumen under the
78 /// SI default).
79 pub fn luminous_intensities(&self) -> StyleResult<Vec<f64>> {
80 self.record.required_numbers("LuminousIntensity", 1)
81 }
82
83 /// The `(secondary plane angle, luminous intensity)` pairs.
84 ///
85 /// Fails with a typed [`crate::StyleError::InvalidValue`] when the two
86 /// lists differ in length, because a photometric table with a missing
87 /// intensity is corrupt rather than partially usable.
88 pub fn samples(&self) -> StyleResult<Vec<(f64, f64)>> {
89 let angles = self.secondary_plane_angles()?;
90 let intensities = self.luminous_intensities()?;
91 if angles.len() != intensities.len() {
92 return Err(self.record.mismatched_lists(
93 "SecondaryPlaneAngle",
94 angles.len(),
95 "LuminousIntensity",
96 intensities.len(),
97 ));
98 }
99 Ok(angles.into_iter().zip(intensities).collect())
100 }
101}