1use ifc_model::{EntityId, Model, Transaction, Value};
24use ifc_schema::Schema;
25
26use crate::authoring::{build_named, invalid_authoring, validate_ratio, validate_ref};
27use crate::StyleResult;
28
29#[derive(Debug, Clone, Copy)]
31pub struct LightSourceDraft<'a> {
32 pub name: Option<&'a str>,
34 pub light_colour: EntityId,
36 pub ambient_intensity: Option<f64>,
38 pub intensity: Option<f64>,
40}
41
42#[derive(Debug, Clone, Copy)]
48pub struct Attenuation {
49 pub constant: f64,
51 pub distance: f64,
53 pub quadric: f64,
55}
56
57fn check_light(entity: &'static str, draft: &LightSourceDraft<'_>) -> StyleResult<()> {
58 if let Some(value) = draft.ambient_intensity {
59 validate_ratio(entity, "AmbientIntensity", value)?;
60 }
61 if let Some(value) = draft.intensity {
62 validate_ratio(entity, "Intensity", value)?;
63 }
64 Ok(())
65}
66
67fn light_values<'a>(draft: &LightSourceDraft<'a>) -> Vec<(&'static str, Value)> {
68 let mut values = vec![("LightColour", Value::Ref(draft.light_colour))];
69 if let Some(name) = draft.name {
70 values.push(("Name", Value::Text(name.into())));
71 }
72 if let Some(value) = draft.ambient_intensity {
73 values.push(("AmbientIntensity", Value::Real(value)));
74 }
75 if let Some(value) = draft.intensity {
76 values.push(("Intensity", Value::Real(value)));
77 }
78 values
79}
80
81fn check_attenuation(entity: &'static str, attenuation: &Attenuation) -> StyleResult<()> {
82 let all = [
83 ("ConstantAttenuation", attenuation.constant),
84 ("DistanceAttenuation", attenuation.distance),
85 ("QuadricAttenuation", attenuation.quadric),
86 ];
87 for (attribute, value) in all {
88 if !value.is_finite() {
89 return Err(invalid_authoring(
90 entity,
91 attribute,
92 format!("expected a finite coefficient, got {value}"),
93 ));
94 }
95 }
96 if all.iter().all(|(_, value)| *value == 0.0) {
97 return Err(invalid_authoring(
98 entity,
99 "ConstantAttenuation",
100 "all three coefficients zero leaves no attenuation function",
101 ));
102 }
103 Ok(())
104}
105
106pub fn create_light_source_ambient(
113 tx: &mut Transaction,
114 model: &Model,
115 schema: &Schema,
116 draft: LightSourceDraft<'_>,
117) -> StyleResult<EntityId> {
118 check_light("IfcLightSourceAmbient", &draft)?;
119 validate_ref(tx, model, schema, draft.light_colour, "IfcColourRgb")?;
120 Ok(tx.create(build_named(
121 schema,
122 "IfcLightSourceAmbient",
123 light_values(&draft),
124 )?))
125}
126
127pub fn create_light_source_directional(
134 tx: &mut Transaction,
135 model: &Model,
136 schema: &Schema,
137 draft: LightSourceDraft<'_>,
138 orientation: EntityId,
139) -> StyleResult<EntityId> {
140 check_light("IfcLightSourceDirectional", &draft)?;
141 validate_ref(tx, model, schema, draft.light_colour, "IfcColourRgb")?;
142 validate_ref(tx, model, schema, orientation, "IfcDirection")?;
143
144 let mut values = light_values(&draft);
145 values.push(("Orientation", Value::Ref(orientation)));
146 Ok(tx.create(build_named(schema, "IfcLightSourceDirectional", values)?))
147}
148
149pub fn create_light_source_positional(
157 tx: &mut Transaction,
158 model: &Model,
159 schema: &Schema,
160 draft: LightSourceDraft<'_>,
161 point: PointLight,
162) -> StyleResult<EntityId> {
163 let PointLight {
164 position,
165 radius,
166 attenuation,
167 } = point;
168 check_light("IfcLightSourcePositional", &draft)?;
169 if !radius.is_finite() || radius <= 0.0 {
170 return Err(invalid_authoring(
171 "IfcLightSourcePositional",
172 "Radius",
173 format!("expected a positive length, got {radius}"),
174 ));
175 }
176 check_attenuation("IfcLightSourcePositional", &attenuation)?;
177 validate_ref(tx, model, schema, draft.light_colour, "IfcColourRgb")?;
178 validate_ref(tx, model, schema, position, "IfcCartesianPoint")?;
179
180 let mut values = light_values(&draft);
181 values.extend([
182 ("Position", Value::Ref(position)),
183 ("Radius", Value::Real(radius)),
184 ("ConstantAttenuation", Value::Real(attenuation.constant)),
185 ("DistanceAttenuation", Value::Real(attenuation.distance)),
186 ("QuadricAttenuation", Value::Real(attenuation.quadric)),
187 ]);
188 Ok(tx.create(build_named(schema, "IfcLightSourcePositional", values)?))
189}
190
191#[derive(Debug, Clone, Copy)]
199pub struct PointLight {
200 pub position: EntityId,
202 pub radius: f64,
204 pub attenuation: Attenuation,
206}
207
208impl Default for Attenuation {
209 fn default() -> Self {
215 Self {
216 constant: 1.0,
217 distance: 0.0,
218 quadric: 0.0,
219 }
220 }
221}
222
223#[derive(Debug, Clone, Copy)]
225pub struct SpotCone {
226 pub orientation: EntityId,
228 pub spread_angle: f64,
230 pub beam_width_angle: f64,
232 pub concentration_exponent: Option<f64>,
234}
235
236pub fn create_light_source_spot(
243 tx: &mut Transaction,
244 model: &Model,
245 schema: &Schema,
246 draft: LightSourceDraft<'_>,
247 point: PointLight,
248 cone: SpotCone,
249) -> StyleResult<EntityId> {
250 let PointLight {
251 position,
252 radius,
253 attenuation,
254 } = point;
255 check_light("IfcLightSourceSpot", &draft)?;
256 if !radius.is_finite() || radius <= 0.0 {
257 return Err(invalid_authoring(
258 "IfcLightSourceSpot",
259 "Radius",
260 format!("expected a positive length, got {radius}"),
261 ));
262 }
263 check_attenuation("IfcLightSourceSpot", &attenuation)?;
264
265 for (attribute, value) in [
266 ("SpreadAngle", cone.spread_angle),
267 ("BeamWidthAngle", cone.beam_width_angle),
268 ] {
269 if !value.is_finite() || value <= 0.0 {
270 return Err(invalid_authoring(
271 "IfcLightSourceSpot",
272 attribute,
273 format!("expected a positive plane angle, got {value}"),
274 ));
275 }
276 }
277
278 validate_ref(tx, model, schema, draft.light_colour, "IfcColourRgb")?;
279 validate_ref(tx, model, schema, position, "IfcCartesianPoint")?;
280 validate_ref(tx, model, schema, cone.orientation, "IfcDirection")?;
281
282 let mut values = light_values(&draft);
283 values.extend([
284 ("Position", Value::Ref(position)),
285 ("Radius", Value::Real(radius)),
286 ("ConstantAttenuation", Value::Real(attenuation.constant)),
287 ("DistanceAttenuation", Value::Real(attenuation.distance)),
288 ("QuadricAttenuation", Value::Real(attenuation.quadric)),
289 ("Orientation", Value::Ref(cone.orientation)),
290 ("SpreadAngle", Value::Real(cone.spread_angle)),
291 ("BeamWidthAngle", Value::Real(cone.beam_width_angle)),
292 ]);
293 if let Some(value) = cone.concentration_exponent {
294 values.push(("ConcentrationExponent", Value::Real(value)));
295 }
296 Ok(tx.create(build_named(schema, "IfcLightSourceSpot", values)?))
297}
298
299pub fn create_surface_style_lighting(
305 tx: &mut Transaction,
306 model: &Model,
307 schema: &Schema,
308 diffuse_transmission: EntityId,
309 diffuse_reflection: EntityId,
310 transmission: EntityId,
311 reflectance: EntityId,
312) -> StyleResult<EntityId> {
313 for colour in [
314 diffuse_transmission,
315 diffuse_reflection,
316 transmission,
317 reflectance,
318 ] {
319 validate_ref(tx, model, schema, colour, "IfcColourRgb")?;
320 }
321
322 let values = vec![
323 (
324 "DiffuseTransmissionColour",
325 Value::Ref(diffuse_transmission),
326 ),
327 ("DiffuseReflectionColour", Value::Ref(diffuse_reflection)),
328 ("TransmissionColour", Value::Ref(transmission)),
329 ("ReflectanceColour", Value::Ref(reflectance)),
330 ];
331 Ok(tx.create(build_named(schema, "IfcSurfaceStyleLighting", values)?))
332}
333
334pub fn create_surface_style_refraction(
341 tx: &mut Transaction,
342 schema: &Schema,
343 refraction_index: Option<f64>,
344 dispersion_factor: Option<f64>,
345) -> StyleResult<EntityId> {
346 if let Some(value) = refraction_index {
347 if !value.is_finite() || value < 1.0 {
348 return Err(invalid_authoring(
349 "IfcSurfaceStyleRefraction",
350 "RefractionIndex",
351 format!("expected at least 1.0, the vacuum index, got {value}"),
352 ));
353 }
354 }
355 if let Some(value) = dispersion_factor {
356 if !value.is_finite() {
357 return Err(invalid_authoring(
358 "IfcSurfaceStyleRefraction",
359 "DispersionFactor",
360 format!("expected a finite factor, got {value}"),
361 ));
362 }
363 }
364
365 let mut values = Vec::new();
366 if let Some(value) = refraction_index {
367 values.push(("RefractionIndex", Value::Real(value)));
368 }
369 if let Some(value) = dispersion_factor {
370 values.push(("DispersionFactor", Value::Real(value)));
371 }
372 Ok(tx.create(build_named(schema, "IfcSurfaceStyleRefraction", values)?))
373}
374
375#[derive(Debug, Clone, Copy)]
377pub struct GoniometricLight {
378 pub position: EntityId,
380 pub colour_appearance: Option<EntityId>,
382 pub colour_temperature: f64,
384 pub luminous_flux: f64,
386 pub emission_source: &'static str,
388 pub distribution_data_source: EntityId,
391}
392
393pub fn create_light_source_goniometric(
406 tx: &mut Transaction,
407 model: &Model,
408 schema: &Schema,
409 draft: LightSourceDraft<'_>,
410 light: GoniometricLight,
411) -> StyleResult<EntityId> {
412 const ENTITY: &str = "IfcLightSourceGoniometric";
413 check_light(ENTITY, &draft)?;
414 validate_ref(tx, model, schema, draft.light_colour, "IfcColourRgb")?;
415 if let Some(colour) = light.colour_appearance {
416 validate_ref(tx, model, schema, colour, "IfcColourRgb")?;
417 }
418 for (value, attribute) in [
419 (light.colour_temperature, "ColourTemperature"),
420 (light.luminous_flux, "LuminousFlux"),
421 ] {
422 if !value.is_finite() || value <= 0.0 {
425 return Err(invalid_authoring(
426 ENTITY,
427 attribute,
428 format!("expected a positive measure, got {value}"),
429 ));
430 }
431 }
432 let declared = schema
433 .attributes(ENTITY)
434 .iter()
435 .find(|a| a.name.eq_ignore_ascii_case("LightEmissionSource"))
436 .and_then(|a| schema.type_def(&a.type_name))
437 .is_some_and(|def| match &def.kind {
438 ifc_schema::TypeKind::Enumeration(values) => values
439 .iter()
440 .any(|v| v.eq_ignore_ascii_case(light.emission_source)),
441 _ => false,
442 });
443 if !declared {
444 return Err(invalid_authoring(
445 ENTITY,
446 "LightEmissionSource",
447 light.emission_source,
448 ));
449 }
450
451 let mut values = light_values(&draft);
452 values.push(("Position", Value::Ref(light.position)));
453 if let Some(colour) = light.colour_appearance {
454 values.push(("ColourAppearance", Value::Ref(colour)));
455 }
456 values.push(("ColourTemperature", Value::Real(light.colour_temperature)));
457 values.push(("LuminousFlux", Value::Real(light.luminous_flux)));
458 values.push((
459 "LightEmissionSource",
460 Value::Enum(light.emission_source.into()),
461 ));
462 values.push((
463 "LightDistributionDataSource",
464 Value::Ref(light.distribution_data_source),
465 ));
466 Ok(tx.create(build_named(schema, ENTITY, values)?))
467}