wowlab_types/types/sim/encounter/
spatial.rs1use super::{
7 super::FastSet,
8 EncounterValidationError, GEOMETRY_EPSILON, SpatialTransform, StaticObstacle,
9 StaticSpatialScene,
10 geometry::{
11 point_in_or_on_ring, point_on_segment, point_strictly_in_ring, rings_intersect,
12 validate_ring,
13 },
14 invalid,
15 validation::validate_slug,
16};
17
18impl StaticSpatialScene {
19 pub fn validate(&self) -> Result<(), EncounterValidationError> {
25 if self.layers.is_empty() {
26 return Err(invalid(
27 "spatial_scene.layers",
28 "scene must contain at least one layer",
29 ));
30 }
31
32 let mut slugs = FastSet::default();
33
34 for (index, layer) in self.layers.iter().enumerate() {
35 validate_layer(layer, index, &mut slugs)?;
36 }
37
38 for (index, obstacle) in self.obstacles.iter().enumerate() {
39 let path = format!("spatial_scene.obstacles[{index}]");
40 let layer = validate_obstacle(obstacle, &path)?;
41
42 if usize::from(layer.0) >= self.layers.len() {
43 return Err(invalid(&path, format!("missing spatial layer {}", layer.0)));
44 }
45 }
46
47 Ok(())
48 }
49}
50
51fn validate_layer<'a>(
52 layer: &'a super::SpatialLayerDefinition,
53 index: usize,
54 slugs: &mut FastSet<&'a str>,
55) -> Result<(), EncounterValidationError> {
56 if usize::from(layer.id.0) != index {
57 return Err(invalid(
58 format!("spatial_scene.layers[{index}].id"),
59 "layer ids must be dense and ordered",
60 ));
61 }
62
63 validate_slug(&layer.slug, &format!("spatial_scene.layers[{index}].slug"))?;
64
65 if !slugs.insert(layer.slug.as_str()) {
66 return Err(invalid(
67 "spatial_scene.layers",
68 format!("duplicate layer slug {}", layer.slug),
69 ));
70 }
71
72 Ok(())
73}
74
75#[expect(
76 clippy::map_err_ignore,
77 reason = "coordinate errors are deliberately replaced by the owning obstacle field path"
78)]
79fn validate_obstacle(
80 obstacle: &StaticObstacle,
81 path: &str,
82) -> Result<super::SpatialLayerId, EncounterValidationError> {
83 match obstacle {
84 StaticObstacle::Segment { layer, start, end } => {
85 start
86 .validate()
87 .map_err(|_| invalid(path, "segment start must be finite"))?;
88 end.validate()
89 .map_err(|_| invalid(path, "segment end must be finite"))?;
90
91 if start.distance(*end) <= GEOMETRY_EPSILON {
92 return Err(invalid(path, "segment endpoints must differ"));
93 }
94
95 Ok(*layer)
96 }
97 StaticObstacle::Polygon {
98 layer,
99 exterior,
100 holes,
101 } => {
102 validate_polygon(exterior, holes, path)?;
103
104 Ok(*layer)
105 }
106 }
107}
108
109fn validate_polygon(
110 exterior: &[super::Position2],
111 holes: &[Vec<super::Position2>],
112 path: &str,
113) -> Result<(), EncounterValidationError> {
114 validate_ring(exterior, &format!("{path}.exterior"))?;
115
116 for (hole_index, hole) in holes.iter().enumerate() {
117 let hole_path = format!("{path}.holes[{hole_index}]");
118
119 validate_ring(hole, &hole_path)?;
120
121 if !point_strictly_in_ring(hole[0], exterior) {
122 return Err(invalid(hole_path, "hole must be strictly inside exterior"));
123 }
124
125 if rings_intersect(hole, exterior) {
126 return Err(invalid(
127 format!("{path}.holes[{hole_index}]"),
128 "hole may not touch or cross exterior",
129 ));
130 }
131 }
132
133 validate_hole_separation(holes, path)
134}
135
136fn validate_hole_separation(
137 holes: &[Vec<super::Position2>],
138 path: &str,
139) -> Result<(), EncounterValidationError> {
140 for left in 0..holes.len() {
141 for right in left + 1..holes.len() {
142 if rings_intersect(&holes[left], &holes[right])
143 || point_in_or_on_ring(holes[left][0], &holes[right])
144 || point_in_or_on_ring(holes[right][0], &holes[left])
145 {
146 return Err(invalid(
147 path,
148 "polygon holes may not touch, overlap, or nest",
149 ));
150 }
151 }
152 }
153
154 Ok(())
155}
156
157pub(super) fn validate_not_in_obstacle(
158 transform: SpatialTransform,
159 scene: &StaticSpatialScene,
160 path: &str,
161) -> Result<(), EncounterValidationError> {
162 for obstacle in &scene.obstacles {
163 match obstacle {
164 StaticObstacle::Segment { layer, start, end }
165 if *layer == transform.layer
166 && point_on_segment(transform.position, *start, *end) =>
167 {
168 return Err(invalid(path, "actor transform touches a blocking segment"));
169 }
170 StaticObstacle::Polygon {
171 layer,
172 exterior,
173 holes,
174 } if *layer == transform.layer => {
175 let in_exterior = point_in_or_on_ring(transform.position, exterior);
176 let in_hole = holes
177 .iter()
178 .any(|hole| point_strictly_in_ring(transform.position, hole));
179
180 if in_exterior && !in_hole {
181 return Err(invalid(
182 path,
183 "actor transform is inside or on a blocking polygon",
184 ));
185 }
186
187 if holes.iter().any(|hole| {
188 hole.windows(2)
189 .any(|edge| point_on_segment(transform.position, edge[0], edge[1]))
190 }) {
191 return Err(invalid(
192 path,
193 "actor transform touches a blocking polygon boundary",
194 ));
195 }
196 }
197 _ => {}
198 }
199 }
200
201 Ok(())
202}