concinnity_core/components/
validate.rs1use alloc::string::{String, ToString};
10
11use crate::components::{
12 Decal, DirectionalLight, GlassPanel, GlassPanelGeometry, InstancedProp, MAX_WATER_WAVES,
13 Material, ParticleEmitter, PhysicsJoint, PhysicsJointKind, PointLight, Prop, RectAreaLight,
14 ReflectionProbe, RigidBody, SPOT_MAX_ANGLE_DEG, SdfVolume, SpotLight, SpotLightGeometry,
15 VolumetricFog, VoxelChunk, WaterSurface, WaterWave,
16};
17use crate::math::sqrt;
18use crate::platform::Platform;
19
20fn path_extension(path: &str) -> &str {
23 let file = path.rsplit(['/', '\\']).next().unwrap_or(path);
24 match file.rsplit_once('.') {
25 Some((stem, ext)) if !stem.is_empty() => ext,
26 _ => "",
27 }
28}
29
30fn sdf_source_for(v: &SdfVolume, platform: Platform) -> Option<String> {
33 if let Some(map) = &v.fragment_shaders
34 && let Some(src) = map.get(platform.key()).filter(|s| !s.is_empty())
35 {
36 return Some(src.clone());
37 }
38 if v.fragment_shader.is_empty() {
39 return None;
40 }
41 if platform.accepts_ext(path_extension(&v.fragment_shader)) {
42 Some(v.fragment_shader.clone())
43 } else {
44 None
45 }
46}
47
48pub fn sdf_volume(mut v: SdfVolume, platform: Platform) -> SdfVolume {
55 use crate::components::sdf_volume::{SDF_MAX_STEPS_CEILING, SDF_MAX_STEPS_FLOOR};
56 for axis in v.extent.iter_mut() {
59 if !axis.is_finite() || *axis <= 0.0 {
60 *axis = 1.0;
61 }
62 }
63 if !v.max_gradient.is_finite() || v.max_gradient <= 0.0 {
64 v.max_gradient = 1.0;
65 }
66 v.max_steps = v
67 .max_steps
68 .clamp(SDF_MAX_STEPS_FLOOR, SDF_MAX_STEPS_CEILING);
69 if !v.max_distance.is_finite() || v.max_distance < 0.1 {
70 v.max_distance = 0.1;
71 }
72 if v.volumetric {
73 v.cast_shadows = false;
74 }
75 if let Some(src) = sdf_source_for(&v, platform) {
76 v.fragment_shader = src;
77 }
78 v
79}
80
81pub fn point_light(mut args: PointLight) -> PointLight {
83 args.intensity = args.intensity.max(0.0);
84 args.range = args.range.max(0.0);
85 args
86}
87
88pub fn spot_light(mut args: SpotLight) -> SpotLight {
90 args.intensity = args.intensity.max(0.0);
91 args.range = args.range.max(0.0);
92 args.direction = args.unit_direction();
93 args.outer_angle = args.outer_angle.clamp(0.0, SPOT_MAX_ANGLE_DEG);
94 args.inner_angle = args.inner_angle.clamp(0.0, args.outer_angle);
95 args
96}
97
98pub fn rect_area_light(mut args: RectAreaLight) -> RectAreaLight {
100 args.intensity = args.intensity.max(0.0);
101 args.range = args.range.max(0.0);
102 let n = args.normal;
105 let len = sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
106 args.normal = if len < 1e-6 {
107 [0.0, 0.0, 1.0]
108 } else {
109 [n[0] / len, n[1] / len, n[2] / len]
110 };
111 args.half_size[0] = args.half_size[0].max(1e-3);
112 args.half_size[1] = args.half_size[1].max(1e-3);
113 args
114}
115
116pub fn directional_light(mut args: DirectionalLight) -> DirectionalLight {
118 args.intensity = args.intensity.max(0.0);
119 args
120}
121
122pub fn material(mut args: Material) -> Material {
126 args.roughness = args.roughness.clamp(0.0, 1.0);
127 args.metallic = args.metallic.clamp(0.0, 1.0);
128 args.macro_variation = args.macro_variation.clamp(0.0, 1.0);
129 args.terrain_blend = args.terrain_blend.clamp(0.0, 1.0);
130 args.secondary_blend_sharpness = args.secondary_blend_sharpness.clamp(0.0, 1.0);
131 args.alpha_cutoff = args.alpha_cutoff.clamp(0.0, 1.0);
132 args.opacity = args.opacity.clamp(0.0, 1.0);
133 if args.see_through {
136 args.transparent = true;
137 }
138 args
139}
140
141pub fn glass_panel(mut args: GlassPanel) -> GlassPanel {
143 args.normal = args.unit_normal();
144 args.half_size[0] = args.half_size[0].max(1e-3);
145 args.half_size[1] = args.half_size[1].max(1e-3);
146 args.opacity = args.opacity.clamp(0.0, 1.0);
147 args.refraction_strength = args.refraction_strength.max(0.0);
148 args.fresnel_power = args.fresnel_power.max(0.0);
149 args
150}
151
152pub fn water_surface(mut args: WaterSurface) -> WaterSurface {
154 args.subdivisions = args.subdivisions.clamp(8, 255);
155 if args.waves.len() > MAX_WATER_WAVES {
156 args.waves.truncate(MAX_WATER_WAVES);
157 }
158 if args.waves.is_empty() {
159 args.waves.push(WaterWave::default());
160 }
161 args
162}
163
164pub fn joint(mut args: PhysicsJoint) -> PhysicsJoint {
166 if let Some(k) = PhysicsJointKind::from_str_norm(&args.kind) {
168 args.kind = k.as_str().to_string();
169 }
170 args
171}
172
173pub fn decal(mut args: Decal) -> Decal {
175 args.tint[3] = args.tint[3].clamp(0.0, 1.0);
179 args
180}
181
182pub fn reflection_probe(mut args: ReflectionProbe) -> ReflectionProbe {
184 for e in &mut args.half_extents {
187 *e = e.max(0.0);
188 }
189 args
190}
191
192pub fn rigid_body(mut args: RigidBody) -> RigidBody {
194 args.is_grounded = true;
195 args
196}
197
198pub fn prop(mut args: Prop) -> Prop {
200 args.cull_distance = args.cull_distance.max(0.0);
201 args.is_held = false;
202 args
203}
204
205pub fn particle_emitter(mut args: ParticleEmitter) -> ParticleEmitter {
207 args.spread_deg = args.spread_deg.clamp(0.0, 180.0);
211 args.speed_min = args.speed_min.max(0.0);
212 if !args.speed_max.is_finite() || args.speed_max < args.speed_min {
213 args.speed_max = args.speed_min;
214 }
215 if !args.lifetime_min.is_finite() || args.lifetime_min <= 0.0 {
216 args.lifetime_min = 0.001;
217 }
218 if !args.lifetime_max.is_finite() || args.lifetime_max < args.lifetime_min {
219 args.lifetime_max = args.lifetime_min;
220 }
221 args.spawn_rate = args.spawn_rate.max(0.0);
222 args.max_particles = args.max_particles.clamp(1, 65_536);
223 args.size_start = args.size_start.max(0.0);
224 args.size_end = args.size_end.max(0.0);
225 for c in args.color_start.iter_mut().chain(args.color_end.iter_mut()) {
226 if !c.is_finite() {
227 *c = 0.0;
228 }
229 }
230 args
231}
232
233pub fn volumetric_fog(mut args: VolumetricFog) -> VolumetricFog {
235 args.density = args.density.max(0.0);
239 args.height_falloff = args.height_falloff.max(0.0);
240 args.ambient = args.ambient.max(0.0);
241 if args.max_distance <= 0.0 || !args.max_distance.is_finite() {
242 args.max_distance = 1.0;
243 }
244 args.phase_g = args.phase_g.clamp(-0.95, 0.95);
247 args
248}
249
250pub fn instanced_prop(mut args: InstancedProp) -> InstancedProp {
252 args.cull_distance = args.cull_distance.max(0.0);
253 args
254}
255
256pub fn voxel_chunk(mut args: VoxelChunk) -> VoxelChunk {
258 args.block_size = args.block_size.max(0.0);
259 if args.lod_levels == 0 {
260 args.lod_levels = 1;
261 }
262 args.lod_levels = args.lod_levels.min(8);
263 args
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn material_clamps_and_see_through_implies_transparent() {
272 let m = material(Material {
273 roughness: 2.0,
274 metallic: -1.0,
275 opacity: 1.5,
276 see_through: true,
277 ..Material::default()
278 });
279 assert_eq!(m.roughness, 1.0);
280 assert_eq!(m.metallic, 0.0);
281 assert_eq!(m.opacity, 1.0);
282 assert!(m.transparent);
283 }
284
285 #[test]
286 fn water_surface_clamps_subdivisions_and_guarantees_a_wave() {
287 let w = water_surface(WaterSurface {
288 subdivisions: 3,
289 waves: alloc::vec::Vec::new(),
290 ..WaterSurface::default()
291 });
292 assert_eq!(w.subdivisions, 8);
293 assert_eq!(w.waves.len(), 1);
294 }
295
296 #[test]
297 fn lights_floor_intensity_and_range_at_zero() {
298 let p = point_light(PointLight {
299 intensity: -2.0,
300 range: -1.0,
301 ..PointLight::default()
302 });
303 assert_eq!((p.intensity, p.range), (0.0, 0.0));
304 let d = directional_light(DirectionalLight {
305 intensity: -1.0,
306 ..DirectionalLight::default()
307 });
308 assert_eq!(d.intensity, 0.0);
309 }
310
311 #[test]
312 fn path_extension_matches_file_name_semantics() {
313 assert_eq!(path_extension("shaders/blob.metal"), "metal");
314 assert_eq!(path_extension("blob.hlsl"), "hlsl");
315 assert_eq!(path_extension("dir.v2/shader"), "");
316 assert_eq!(path_extension(".hidden"), "");
317 assert_eq!(path_extension("noext"), "");
318 }
319}