Skip to main content

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}