1use crate::catalog;
4use crate::geometry::{Primitive, SceneObject};
5use crate::material::{Material, MaterialParams};
6use crate::ray::{HitRecord, Ray};
7use crate::source::Source;
8use crate::MaterialId;
9use nalgebra::{Point3, Unit, Vector3};
10
11#[derive(Debug, Clone)]
13pub struct Scene {
14 pub sources: Vec<Source>,
15 pub objects: Vec<SceneObject>,
16 materials: Vec<Material>,
18 material_params: Vec<MaterialParams>,
20}
21
22impl Scene {
23 pub fn new() -> Self {
25 Self {
26 sources: Vec::new(),
27 objects: Vec::new(),
28 materials: Vec::new(),
29 material_params: Vec::new(),
30 }
31 }
32
33 pub fn add_source(&mut self, source: Source) {
35 self.sources.push(source);
36 }
37
38 pub fn add_material(&mut self, params: MaterialParams) -> MaterialId {
40 let id = self.materials.len();
41 self.materials.push(params.to_material());
42 self.material_params.push(params);
43 id
44 }
45
46 pub fn add_object(&mut self, primitive: Primitive, material: MaterialId, label: &str) -> usize {
48 let idx = self.objects.len();
49 self.objects.push(SceneObject {
50 primitive,
51 material,
52 label: label.to_string(),
53 });
54 idx
55 }
56
57 pub fn material(&self, id: MaterialId) -> &Material {
59 &self.materials[id]
60 }
61
62 pub fn material_params(&self, id: MaterialId) -> &MaterialParams {
64 &self.material_params[id]
65 }
66
67 pub fn total_source_flux(&self) -> f64 {
69 self.sources.iter().map(|s| s.flux_lm()).sum()
70 }
71
72 pub fn intersect(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option<HitRecord> {
74 let mut closest: Option<HitRecord> = None;
75 let mut closest_t = t_max;
76
77 for obj in &self.objects {
78 if let Some(hit) = obj.primitive.intersect(ray, t_min, closest_t, obj.material) {
79 closest_t = hit.t;
80 closest = Some(hit);
81 }
82 }
83
84 closest
85 }
86}
87
88impl Default for Scene {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
100pub enum ReflectorSide {
101 Left,
102 Right,
103 Back,
104 Surround,
106}
107
108#[derive(Debug, Clone)]
110pub struct CoverPlacement {
111 pub distance_mm: f64,
113 pub width_mm: f64,
115 pub height_mm: f64,
117}
118
119#[derive(Debug, Clone)]
121pub struct ReflectorPlacement {
122 pub distance_mm: f64,
124 pub length_mm: f64,
126 pub side: ReflectorSide,
128}
129
130pub struct SceneBuilder {
135 scene: Scene,
136 source_position: Point3<f64>,
137 source_direction: Unit<Vector3<f64>>,
138}
139
140impl SceneBuilder {
141 pub fn new() -> Self {
143 Self {
144 scene: Scene::new(),
145 source_position: Point3::origin(),
146 source_direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
147 }
148 }
149
150 pub fn source(mut self, source: Source) -> Self {
152 self.scene.add_source(source);
153 self
154 }
155
156 pub fn reflector(mut self, material: MaterialParams, placement: ReflectorPlacement) -> Self {
158 let mat_name = material.name.clone();
159 let mat_id = self.scene.add_material(material);
160 let d = placement.distance_mm / 1000.0; let l = placement.length_mm / 1000.0;
162
163 match placement.side {
164 ReflectorSide::Surround => {
165 let prim = Primitive::Cylinder {
167 center: self.source_position + self.source_direction.as_ref() * (l / 2.0),
168 axis: self.source_direction,
169 radius: d,
170 half_height: l / 2.0,
171 capped: false,
172 };
173 self.scene
174 .add_object(prim, mat_id, &format!("{mat_name} housing"));
175 }
176 ReflectorSide::Back => {
177 let normal = Unit::new_unchecked(-self.source_direction.into_inner());
178 let u_axis = perpendicular_axis(&self.source_direction);
179 let center = self.source_position - self.source_direction.as_ref() * d;
180 let prim = Primitive::Sheet {
181 center,
182 normal,
183 u_axis,
184 half_width: l / 2.0,
185 half_height: l / 2.0,
186 thickness: 0.001,
187 };
188 self.scene
189 .add_object(prim, mat_id, &format!("{mat_name} back"));
190 }
191 ReflectorSide::Left => {
192 let u_axis = perpendicular_axis(&self.source_direction);
193 let normal = Unit::new_normalize(u_axis.into_inner());
194 let center = self.source_position - u_axis.as_ref() * d
195 + self.source_direction.as_ref() * (l / 2.0);
196 let prim = Primitive::Sheet {
197 center,
198 normal,
199 u_axis: self.source_direction,
200 half_width: l / 2.0,
201 half_height: l / 2.0,
202 thickness: 0.001,
203 };
204 self.scene
205 .add_object(prim, mat_id, &format!("{mat_name} left"));
206 }
207 ReflectorSide::Right => {
208 let u_axis = perpendicular_axis(&self.source_direction);
209 let normal = Unit::new_normalize(-u_axis.into_inner());
210 let center = self.source_position
211 + u_axis.as_ref() * d
212 + self.source_direction.as_ref() * (l / 2.0);
213 let prim = Primitive::Sheet {
214 center,
215 normal,
216 u_axis: self.source_direction,
217 half_width: l / 2.0,
218 half_height: l / 2.0,
219 thickness: 0.001,
220 };
221 self.scene
222 .add_object(prim, mat_id, &format!("{mat_name} right"));
223 }
224 }
225
226 self
227 }
228
229 pub fn cover(mut self, material: MaterialParams, placement: CoverPlacement) -> Self {
231 let mat_name = material.name.clone();
232 let mat_id = self.scene.add_material(material);
233 let d = placement.distance_mm / 1000.0;
234 let w = placement.width_mm / 1000.0;
235 let h = placement.height_mm / 1000.0;
236
237 let center = self.source_position + self.source_direction.as_ref() * d;
240 let normal = Unit::new_unchecked(-self.source_direction.into_inner());
241 let u_axis = perpendicular_axis(&self.source_direction);
242
243 let thickness = self.scene.material_params[mat_id].thickness_mm / 1000.0;
244
245 let prim = Primitive::Sheet {
246 center,
247 normal,
248 u_axis,
249 half_width: w / 2.0,
250 half_height: h / 2.0,
251 thickness,
252 };
253 self.scene
254 .add_object(prim, mat_id, &format!("{mat_name} cover"));
255
256 self
257 }
258
259 pub fn build(self) -> Scene {
261 self.scene
262 }
263}
264
265impl Default for SceneBuilder {
266 fn default() -> Self {
267 Self::new()
268 }
269}
270
271fn perpendicular_axis(axis: &Unit<Vector3<f64>>) -> Unit<Vector3<f64>> {
273 let a = if axis.x.abs() > 0.9 {
274 Vector3::y_axis()
275 } else {
276 Vector3::x_axis()
277 };
278 Unit::new_normalize(axis.cross(a.as_ref()))
279}
280
281pub fn bare_lambertian(flux_lm: f64) -> Scene {
288 let mut scene = Scene::new();
289 scene.add_source(Source::Lambertian {
290 position: Point3::origin(),
291 normal: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
292 flux_lm,
293 });
294 scene
295}
296
297pub fn bare_isotropic(flux_lm: f64) -> Scene {
300 let mut scene = Scene::new();
301 scene.add_source(Source::Isotropic {
302 position: Point3::origin(),
303 flux_lm,
304 });
305 scene
306}
307
308pub fn led_with_housing(flux_lm: f64, beam_angle_deg: f64) -> Scene {
310 SceneBuilder::new()
311 .source(Source::Led {
312 position: Point3::origin(),
313 direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
314 half_angle_deg: beam_angle_deg / 2.0,
315 flux_lm,
316 })
317 .reflector(
318 catalog::white_paint(),
319 ReflectorPlacement {
320 distance_mm: 25.0,
321 length_mm: 50.0,
322 side: ReflectorSide::Surround,
323 },
324 )
325 .build()
326}
327
328pub fn led_housing_with_cover(
330 flux_lm: f64,
331 beam_angle_deg: f64,
332 cover_material: MaterialParams,
333 cover_distance_mm: f64,
334) -> Scene {
335 SceneBuilder::new()
336 .source(Source::Led {
337 position: Point3::origin(),
338 direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
339 half_angle_deg: beam_angle_deg / 2.0,
340 flux_lm,
341 })
342 .reflector(
343 catalog::white_paint(),
344 ReflectorPlacement {
345 distance_mm: 25.0,
346 length_mm: cover_distance_mm + 10.0,
347 side: ReflectorSide::Surround,
348 },
349 )
350 .cover(
351 cover_material,
352 CoverPlacement {
353 distance_mm: cover_distance_mm,
354 width_mm: 60.0,
355 height_mm: 60.0,
356 },
357 )
358 .build()
359}
360
361pub fn roundtrip_validation(ldt: &eulumdat::Eulumdat) -> Scene {
363 let flux = ldt.total_luminous_flux();
364 let mut scene = Scene::new();
365 scene.add_source(Source::from_lvk(
366 Point3::origin(),
367 nalgebra::Rotation3::identity(),
368 ldt.clone(),
369 flux,
370 ));
371 scene
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn scene_builder_creates_objects() {
380 let scene = SceneBuilder::new()
381 .source(Source::Led {
382 position: Point3::origin(),
383 direction: Unit::new_unchecked(Vector3::new(0.0, 0.0, -1.0)),
384 half_angle_deg: 60.0,
385 flux_lm: 1000.0,
386 })
387 .reflector(
388 catalog::white_paint(),
389 ReflectorPlacement {
390 distance_mm: 25.0,
391 length_mm: 50.0,
392 side: ReflectorSide::Surround,
393 },
394 )
395 .cover(
396 catalog::opal_pmma_3mm(),
397 CoverPlacement {
398 distance_mm: 40.0,
399 width_mm: 60.0,
400 height_mm: 60.0,
401 },
402 )
403 .build();
404
405 assert_eq!(scene.sources.len(), 1);
406 assert_eq!(scene.objects.len(), 2); assert!((scene.total_source_flux() - 1000.0).abs() < 0.01);
408 }
409
410 #[test]
411 fn bare_lambertian_has_no_objects() {
412 let scene = bare_lambertian(1000.0);
413 assert_eq!(scene.sources.len(), 1);
414 assert!(scene.objects.is_empty());
415 }
416}