1use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13use serde_json::Value;
14
15use super::assets::{Image, Resources, decode_png, missing_image};
16use super::raster::Canvas;
17
18pub type Faces = [Option<Arc<Image>>; 6];
20
21#[derive(Clone)]
26pub struct Cuboid {
27 pub from: [f32; 3],
28 pub to: [f32; 3],
29 pub faces: Faces,
30 pub turns: Vec<Turn>,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct Turn {
39 pub origin: [f32; 3],
40 pub axis: usize,
41 pub degrees: f32,
42 pub rescale: bool,
43}
44
45const CENTRE: [f32; 3] = [8.0, 8.0, 8.0];
47const BLOCK_UNITS: f32 = 16.0;
49
50impl Turn {
51 fn apply(&self, p: [f32; 3]) -> [f32; 3] {
53 let (s, c) = self.degrees.to_radians().sin_cos();
54 let (a, b) = match self.axis {
55 0 => (1, 2),
56 1 => (2, 0),
57 _ => (0, 1),
58 };
59 let d = [0, 1, 2].map(|i| p[i] - self.origin[i]);
60 let mut out = d;
61 out[a] = d[a] * c - d[b] * s;
62 out[b] = d[a] * s + d[b] * c;
63 if self.rescale && c.abs() > f32::EPSILON {
64 let k = 1.0 / c.abs();
65 out[a] *= k;
66 out[b] *= k;
67 }
68 [0, 1, 2].map(|i| out[i] + self.origin[i])
69 }
70
71 fn apply_dir(&self, v: [f32; 3]) -> [f32; 3] {
73 Turn { origin: [0.0; 3], rescale: false, ..*self }.apply(v)
74 }
75}
76
77pub type Shape = Vec<Cuboid>;
79
80pub fn cube(faces: Faces) -> Shape {
82 let faces = faces.map(|f| Some(f.unwrap_or_else(|| Arc::new(missing_image()))));
83 vec![Cuboid { from: [0.0; 3], to: [BLOCK_UNITS; 3], faces, turns: Vec::new() }]
84}
85
86const FACE_NAMES: [&str; 6] = ["up", "down", "north", "south", "west", "east"];
87
88pub const FACE_TEXTURES: [&[&str]; 6] = [
92 &["up", "top", "end", "all", "texture"],
93 &["down", "bottom", "end", "all", "texture"],
94 &["north", "front", "side", "all", "texture"],
95 &["south", "side", "all", "texture"],
96 &["west", "side", "all", "texture"],
97 &["east", "side", "all", "texture"],
98];
99
100const PARTICLE: &str = "particle";
102
103fn default_uv(slot: usize, f: [f32; 3], t: [f32; 3]) -> [f32; 4] {
105 match slot {
106 0 => [f[0], f[2], t[0], t[2]],
107 1 => [f[0], BLOCK_UNITS - t[2], t[0], BLOCK_UNITS - f[2]],
108 2 => [BLOCK_UNITS - t[0], BLOCK_UNITS - t[1], BLOCK_UNITS - f[0], BLOCK_UNITS - f[1]],
109 3 => [f[0], BLOCK_UNITS - t[1], t[0], BLOCK_UNITS - f[1]],
110 4 => [f[2], BLOCK_UNITS - t[1], t[2], BLOCK_UNITS - f[1]],
111 _ => [BLOCK_UNITS - t[2], BLOCK_UNITS - t[1], BLOCK_UNITS - f[2], BLOCK_UNITS - f[1]],
112 }
113}
114
115fn cut(img: &Image, [u0, v0, u1, v1]: [f32; 4]) -> Image {
117 let (sx, sy) = (img.width as f32 / BLOCK_UNITS, img.width.min(img.height) as f32 / BLOCK_UNITS);
118 let (x0, x1) = ((u0 * sx).round() as i32, (u1 * sx).round() as i32);
119 let (y0, y1) = ((v0 * sy).round() as i32, (v1 * sy).round() as i32);
120 let (w, h) = ((x1 - x0).unsigned_abs().max(1), (y1 - y0).unsigned_abs().max(1));
121 let mut rgba = Vec::with_capacity((w * h * 4) as usize);
122 for y in 0..h as i32 {
123 for x in 0..w as i32 {
124 let px = if x1 >= x0 { x0 + x } else { x0 - 1 - x };
125 let py = if y1 >= y0 { y0 + y } else { y0 - 1 - y };
126 let (px, py) = (px.clamp(0, img.width as i32 - 1) as u32, py.clamp(0, img.height as i32 - 1) as u32);
127 rgba.extend_from_slice(&img.pixel(px, py).to_rgba8());
128 }
129 }
130 Image { width: w, height: h, rgba }
131}
132
133pub fn shape_of(elements: &[Value], texture: &mut dyn FnMut(&str) -> Option<Arc<Image>>) -> Shape {
135 let triple = |v: Option<&Value>| -> Option<[f32; 3]> {
136 let a = v?.as_array()?;
137 Some([a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32, a.get(2)?.as_f64()? as f32])
138 };
139 elements
140 .iter()
141 .filter_map(|e| {
142 let (from, to) = (triple(e.get(yog_ids::model::FROM))?, triple(e.get(yog_ids::model::TO))?);
143 let (f, t) = ([0, 1, 2].map(|i| from[i].min(to[i])), [0, 1, 2].map(|i| from[i].max(to[i])));
144 let faces = e.get(yog_ids::model::FACES)?;
145 let faces: Faces = std::array::from_fn(|slot| {
146 let face = faces.get(FACE_NAMES[slot])?;
147 let img = face.get(yog_ids::model::TEXTURE).and_then(Value::as_str).and_then(&mut *texture).unwrap_or_else(|| Arc::new(missing_image()));
148 let uv = face
149 .get(yog_ids::model::UV)
150 .and_then(Value::as_array)
151 .and_then(|a| Some([a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32, a.get(2)?.as_f64()? as f32, a.get(3)?.as_f64()? as f32]))
152 .unwrap_or_else(|| default_uv(slot, f, t));
153 Some(Arc::new(cut(&img, uv)))
154 });
155 Some(Cuboid { from: f, to: t, faces, turns: element_turn(e).into_iter().collect() })
156 })
157 .collect()
158}
159
160fn element_turn(e: &Value) -> Option<Turn> {
162 let r = e.get(yog_ids::model::ROTATION)?;
163 let o = r.get(yog_ids::model::ORIGIN)?.as_array()?;
164 let origin = [o.first()?.as_f64()? as f32, o.get(1)?.as_f64()? as f32, o.get(2)?.as_f64()? as f32];
165 let axis = ["x", "y", "z"].iter().position(|a| r.get(yog_ids::model::AXIS).and_then(Value::as_str) == Some(*a))?;
166 let degrees = r.get(yog_ids::model::ANGLE)?.as_f64()? as f32;
167 Some(Turn { origin, axis, degrees, rescale: r.get(yog_ids::model::RESCALE).and_then(Value::as_bool).unwrap_or(false) })
168}
169
170fn model_elements(res: &Resources, model: &str, depth: usize) -> Option<Vec<Value>> {
172 if depth > 8 {
173 return None;
174 }
175 let (ns, path) = yog_ids::split(model);
176 let path = path.strip_prefix("block/").map_or(format!("block/{path}"), |p| format!("block/{p}"));
177 let json: Value = serde_json::from_slice(&res.read(ns, &yog_ids::model_file(&path))?).ok()?;
178 if let Some(e) = json.get(yog_ids::model::ELEMENTS).and_then(Value::as_array) {
179 return Some(e.clone());
180 }
181 model_elements(res, json.get(yog_ids::model::PARENT)?.as_str()?, depth + 1)
182}
183
184fn model_textures(res: &Resources, model: &str, depth: usize) -> HashMap<String, String> {
186 let mut out = HashMap::new();
187 if depth > 8 {
188 return out;
189 }
190 let (ns, path) = yog_ids::split(model);
191 let path = path.strip_prefix("block/").map_or(format!("block/{path}"), |p| format!("block/{p}"));
192 let Some(json) = res.read(ns, &yog_ids::model_file(&path)).and_then(|b| serde_json::from_slice::<Value>(&b).ok()) else {
193 return out;
194 };
195 if let Some(parent) = json.get(yog_ids::model::PARENT).and_then(Value::as_str) {
196 out = model_textures(res, parent, depth + 1);
197 }
198 if let Some(t) = json.get(yog_ids::model::TEXTURES).and_then(Value::as_object) {
199 for (k, v) in t {
200 if let Some(v) = v.as_str() {
201 out.insert(k.clone(), v.to_string());
202 }
203 }
204 }
205 out
206}
207
208fn split_state(state: &str) -> (&str, HashMap<&str, &str>) {
210 let (id, props) = state.split_once('[').unwrap_or((state, ""));
211 let props = props.trim_end_matches(']').split(',').filter_map(|kv| kv.split_once('=')).map(|(k, v)| (k.trim(), v.trim())).collect();
212 (id, props)
213}
214
215fn meets(when: &Value, props: &HashMap<&str, &str>) -> bool {
219 let Some(map) = when.as_object() else { return true };
220 if let Some(any) = map.get(yog_ids::model::OR).and_then(Value::as_array) {
221 return any.iter().any(|w| meets(w, props));
222 }
223 if let Some(all) = map.get(yog_ids::model::AND).and_then(Value::as_array) {
224 return all.iter().all(|w| meets(w, props));
225 }
226 map.iter().all(|(k, v)| {
227 let want = v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string());
228 props.get(k.as_str()).is_some_and(|have| want.split('|').any(|w| w == *have))
229 })
230}
231
232fn applied(v: &Value) -> Option<(String, Vec<Turn>)> {
236 let v = if v.is_array() { v.get(0)? } else { v };
237 let model = v.get(yog_ids::model::MODEL)?.as_str()?.to_string();
238 let mut turns = Vec::new();
239 for (axis, key) in [(0, "x"), (1, "y")] {
240 let deg = v.get(key).and_then(Value::as_f64).unwrap_or(0.0) as f32;
241 if deg != 0.0 {
242 turns.push(Turn { origin: CENTRE, axis, degrees: -deg, rescale: false });
243 }
244 }
245 Some((model, turns))
246}
247
248fn block_models(res: &Resources, state: &str) -> Vec<(String, Vec<Turn>)> {
251 let (id, props) = split_state(state);
252 let (ns, path) = yog_ids::split(id);
253 let Some(json) = res.read(ns, &yog_ids::blockstate_file(path)).and_then(|b| serde_json::from_slice::<Value>(&b).ok()) else {
254 return Vec::new();
255 };
256 if let Some(variants) = json.get(yog_ids::model::VARIANTS).and_then(Value::as_object) {
257 let fits = |key: &str| key.split(',').filter(|kv| !kv.is_empty()).all(|kv| kv.split_once('=').is_some_and(|(k, v)| props.get(k) == Some(&v)));
258 let chosen = variants.iter().find(|(k, _)| fits(k)).or_else(|| variants.iter().next());
259 return chosen.and_then(|(_, v)| applied(v)).into_iter().collect();
260 }
261 json.get(yog_ids::model::MULTIPART)
262 .and_then(Value::as_array)
263 .into_iter()
264 .flatten()
265 .filter(|part| part.get(yog_ids::model::WHEN).is_none_or(|w| meets(w, &props)))
266 .filter_map(|part| applied(part.get(yog_ids::model::APPLY)?))
267 .collect()
268}
269
270pub fn is_block(res: &Resources, id: &str) -> bool {
272 let id = split_state(id).0;
273 let (ns, path) = yog_ids::split(id);
274 res.read(ns, &yog_ids::blockstate_file(path)).is_some()
275 || res.mod_root.as_deref().is_some_and(|root| crate::design::content::content_block_faces(root, id).is_some())
276}
277
278fn shape(res: &Resources, state: &str, cache: &mut HashMap<String, Shape>) -> Shape {
279 if let Some(s) = cache.get(state) {
280 return s.clone();
281 }
282 let id = split_state(state).0;
283 let s = mod_shape(res, id).unwrap_or_else(|| game_shape(res, state));
284 cache.insert(state.to_string(), s.clone());
285 s
286}
287
288fn mod_shape(res: &Resources, id: &str) -> Option<Shape> {
290 let root = res.mod_root.as_deref()?;
291 if let Some((elements, textures)) = crate::design::content::content_block_model(root, id) {
292 let mut decoded: HashMap<String, Option<Arc<Image>>> = HashMap::new();
293 let mut texture = |var: &str| -> Option<Arc<Image>> {
294 let var = var.trim_start_matches('#');
295 decoded.entry(var.to_string()).or_insert_with(|| textures.get(var).and_then(|b| decode_png(b)).map(Arc::new)).clone()
296 };
297 let s = shape_of(&elements, &mut texture);
298 if !s.is_empty() {
299 return Some(s);
300 }
301 }
302 crate::design::content::content_block_faces(root, id).map(|faces| cube(faces.map(|b| b.and_then(|b| decode_png(&b)).map(Arc::new))))
303}
304
305fn game_shape(res: &Resources, state: &str) -> Shape {
308 let mut models = block_models(res, state);
309 if models.is_empty() {
310 let (ns, path) = yog_ids::split(split_state(state).0);
311 models.push((format!("{ns}:block/{path}"), Vec::new()));
312 }
313 let mut out = Shape::new();
314 for (model, turns) in models {
315 for mut cuboid in model_shape(res, &model) {
316 cuboid.turns.extend(turns.iter().copied());
317 out.push(cuboid);
318 }
319 }
320 out
321}
322
323fn model_shape(res: &Resources, model: &str) -> Shape {
325 let vars = model_textures(res, model, 0);
326 let resolve = |key: &str| -> Option<String> {
328 let mut v = vars.get(key)?.clone();
329 for _ in 0..8 {
330 match v.strip_prefix('#') {
331 Some(r) => v = vars.get(r)?.clone(),
332 None => return Some(v),
333 }
334 }
335 None
336 };
337 let load = |tex: Option<String>| -> Option<Arc<Image>> {
338 let tex = tex?;
339 let (ns, path) = yog_ids::split(&tex);
340 decode_png(&res.read(ns, &yog_ids::texture_file(path))?).map(Arc::new)
341 };
342 if let Some(elements) = model_elements(res, model, 0) {
343 let mut texture = |var: &str| load(resolve(var.trim_start_matches('#')));
344 let s = shape_of(&elements, &mut texture);
345 if !s.is_empty() {
346 return s;
347 }
348 }
349 cube(FACE_TEXTURES.map(|keys| load(keys.iter().chain(&[PARTICLE]).find_map(|k| resolve(k)))))
350}
351
352fn parse(list: &str) -> Vec<([i32; 3], String)> {
354 list.lines()
355 .filter_map(|l| {
356 let mut p = l.trim().splitn(4, ' ');
357 let at = [p.next()?.parse().ok()?, p.next()?.parse().ok()?, p.next()?.parse().ok()?];
358 Some((at, p.next()?.to_string()))
359 })
360 .collect()
361}
362
363pub fn draw(c: &mut Canvas, list: &str, rect: [f32; 4], yaw: f32, pitch: f32) {
364 let mut cache = HashMap::new();
365 draw_with(c, &parse(list), rect, yaw, pitch, |res, id| shape(res, id, &mut cache));
366}
367
368pub fn draw_cube(c: &mut Canvas, faces: Faces, rect: [f32; 4], yaw: f32, pitch: f32) {
370 draw_with(c, &[([0, 0, 0], String::new())], rect, yaw, pitch, |_, _| cube(faces.clone()));
371}
372
373fn draw_with(
374 c: &mut Canvas,
375 blocks: &[([i32; 3], String)],
376 [x, y, w, h]: [f32; 4],
377 yaw: f32,
378 pitch: f32,
379 mut shape_of: impl FnMut(&Resources, &str) -> Shape,
380) {
381 if blocks.is_empty() || w <= 0.0 || h <= 0.0 {
382 return;
383 }
384 let mut lo = [i32::MAX; 3];
385 let mut hi = [i32::MIN; 3];
386 for (p, _) in blocks {
387 for i in 0..3 {
388 lo[i] = lo[i].min(p[i]);
389 hi[i] = hi[i].max(p[i]);
390 }
391 }
392 let size = (0..3).map(|i| hi[i] - lo[i]).max().unwrap_or(0) as f32 + 1.0;
393 let s = w.min(h) / (size * 1.7);
395 let centre = [(lo[0] + hi[0] + 1) as f32 / 2.0, (lo[1] + hi[1] + 1) as f32 / 2.0, (lo[2] + hi[2] + 1) as f32 / 2.0];
396 let (sy, cy) = yaw.to_radians().sin_cos();
397 let (sp, cp) = pitch.to_radians().sin_cos();
398 let view = |p: [f32; 3]| {
400 let (px, py, pz) = (p[0] - centre[0], p[1] - centre[1], p[2] - centre[2]);
401 let x1 = px * cy + pz * sy;
402 let z1 = -px * sy + pz * cy;
403 let y2 = py * cp - z1 * sp;
404 let z2 = py * sp + z1 * cp;
405 [x1, y2, z2]
406 };
407 let screen = |v: [f32; 3]| [x + w / 2.0 + v[0] * s, y + h / 2.0 - v[1] * s];
408 let occupied: HashSet<[i32; 3]> = blocks.iter().map(|(p, _)| *p).collect();
409
410 let mut quads = Vec::new();
412 const FACES: [([i32; 3], [f32; 3], [f32; 3], [f32; 3], usize, f32); 6] = [
414 ([0, 1, 0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0], 0, 1.0),
415 ([0, -1, 0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], 1, 0.5),
416 ([0, 0, -1], [1.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], 2, 0.8),
417 ([0, 0, 1], [0.0, 1.0, 1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0], 3, 0.8),
418 ([-1, 0, 0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0], 4, 0.6),
419 ([1, 0, 0], [1.0, 1.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], 5, 0.6),
420 ];
421 for (p, id) in blocks {
422 for cuboid in shape_of(c.resources(), id) {
423 let (f, t) = (cuboid.from, cuboid.to);
424 let at = |u: [f32; 3]| {
426 let q = cuboid.turns.iter().fold([0, 1, 2].map(|i| f[i] + u[i] * (t[i] - f[i])), |q, turn| turn.apply(q));
427 [0, 1, 2].map(|i| p[i] as f32 + q[i] / BLOCK_UNITS)
428 };
429 for (n, corner, a, b, slot, shade) in FACES {
430 let on_side = cuboid.turns.is_empty()
432 && (0..3).all(|i| n[i] == 0 || (n[i] > 0 && t[i] >= BLOCK_UNITS) || (n[i] < 0 && f[i] <= 0.0));
433 if on_side && occupied.contains(&[p[0] + n[0], p[1] + n[1], p[2] + n[2]]) {
434 continue;
435 }
436 let normal = cuboid.turns.iter().fold(n.map(|v| v as f32), |v, turn| turn.apply_dir(v));
438 let nv = view([0, 1, 2].map(|i| centre[i] + normal[i]));
439 if nv[2] <= 0.0 {
440 continue;
441 }
442 let Some(img) = cuboid.faces[slot].clone() else { continue };
443 let o = at(corner);
444 let oa = at([corner[0] + a[0], corner[1] + a[1], corner[2] + a[2]]);
445 let ob = at([corner[0] + b[0], corner[1] + b[1], corner[2] + b[2]]);
446 let (vo, va, vb) = (view(o), view(oa), view(ob));
447 let mid = view([0, 1, 2].map(|i| (oa[i] + ob[i]) / 2.0));
448 let (so, sa, sb) = (screen(vo), screen(va), screen(vb));
449 quads.push((mid[2], Some(img), so, [sa[0] - so[0], sa[1] - so[1]], [sb[0] - so[0], sb[1] - so[1]], shade));
450 }
451 }
452 }
453 quads.sort_by(|a, b| a.0.total_cmp(&b.0));
455 let missing = Arc::new(missing_image());
456 for (_, img, o, a, b, shade) in quads {
457 let img = img.unwrap_or_else(|| missing.clone());
458 c.quad(&img, o, a, b, shade);
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn solid(w: u32, h: u32) -> Image {
467 Image { width: w, height: h, rgba: (0..w * h).flat_map(|i| [i as u8, 0, 0, 255]).collect() }
468 }
469
470 fn opaque(shape: Shape) -> usize {
471 let res = Resources { mod_root: None, vanilla: None };
472 let mut c = Canvas::new(32, 32, 1, &res);
473 draw_with(&mut c, &[([0, 0, 0], "x".into())], [0.0, 0.0, 32.0, 32.0], 225.0, 30.0, |_, _| shape.clone());
474 c.rgba.chunks_exact(4).filter(|p| p[3] > 0).count()
475 }
476
477 #[test]
478 fn a_models_boxes_are_drawn_their_size_with_their_uv() {
479 let img = Arc::new(solid(16, 16));
480 let elements: Vec<Value> = serde_json::from_str(
481 r##"[{ "from": [5, 5, 5], "to": [11, 11, 11], "faces": {
482 "up": { "uv": [5, 5, 11, 11], "texture": "#all" }, "north": { "texture": "#all" } } }]"##,
483 )
484 .unwrap();
485 let shape = shape_of(&elements, &mut |v: &str| (v == "#all").then(|| img.clone()));
486 assert_eq!(shape.len(), 1);
487 let up = shape[0].faces[0].as_ref().unwrap();
488 assert_eq!((up.width, up.height), (6, 6), "the uv's part");
489 assert_eq!(up.pixel(0, 0), img.pixel(5, 5));
490 let north = shape[0].faces[2].as_ref().unwrap();
491 assert_eq!((north.width, north.height), (6, 6), "no uv: the box's own part");
492 assert!(shape[0].faces[1].is_none(), "a face the model leaves out isn't drawn");
493
494 let small = opaque(shape);
495 let full = opaque(cube(std::array::from_fn(|_| Some(img.clone()))));
496 assert!(small > 0 && small * 4 < full, "a cable's core, not a whole block: {small} vs {full}");
497 }
498
499 #[test]
500 fn parses_block_lines() {
501 let b = parse("0 0 0 minecraft:stone\n1 2 -1 minecraft:furnace[facing=north]\nbad");
502 assert_eq!(b, [([0, 0, 0], "minecraft:stone".to_string()), ([1, 2, -1], "minecraft:furnace[facing=north]".to_string())]);
503 }
504
505 #[test]
506 fn a_state_meets_multipart_conditions() {
507 let (id, props) = split_state("minecraft:brewing_stand[has_bottle_0=true,has_bottle_1=false]");
508 assert_eq!(id, "minecraft:brewing_stand");
509 let when = |j: &str| serde_json::from_str::<Value>(j).unwrap();
510 assert!(meets(&when(r#"{ "has_bottle_0": "true" }"#), &props));
511 assert!(!meets(&when(r#"{ "has_bottle_1": "true" }"#), &props));
512 assert!(meets(&when(r#"{ "has_bottle_1": "true|false" }"#), &props));
513 assert!(meets(&when(r#"{ "OR": [{ "has_bottle_2": "true" }, { "has_bottle_0": "true" }] }"#), &props));
514 assert!(!meets(&when(r#"{ "has_bottle_2": "false" }"#), &props), "a property the state doesn't name");
515 }
516
517 #[test]
518 fn turns_turn_about_their_origin() {
519 let close = |a: [f32; 3], b: [f32; 3]| (0..3).all(|i| (a[i] - b[i]).abs() < 1e-4);
520 let y = Turn { origin: CENTRE, axis: 1, degrees: 90.0, rescale: false };
522 assert!(close(y.apply([16.0, 8.0, 8.0]), [8.0, 8.0, 0.0]), "{:?}", y.apply([16.0, 8.0, 8.0]));
523 assert!(close(y.apply(CENTRE), CENTRE));
524 let r = Turn { origin: CENTRE, axis: 1, degrees: 45.0, rescale: true };
526 let d = r.apply([16.0, 8.0, 8.0]);
527 assert!(((d[0] - 8.0).powi(2) + (d[2] - 8.0).powi(2)).sqrt() > 8.0 * 1.4);
528 }
529}
530