Skip to main content

yog_cli/design/
blocks3d.rs

1//! `draw2d_blocks` in the designer: blocks in 3D, as the game draws them in
2//! a GUI (an orthographic view, turned and tilted), each block a cube with
3//! its model's boxes (`elements`: a cable's core, a slab's half) with
4//! their textures — the faces you'd see, back to front, the top lit, the
5//! sides shaded like Minecraft's. A model without elements is a cube.
6//! A block state picks its blockstate's variant (turned by its `x`/`y`)
7//! or every multipart part whose `when` it meets; elements keep their own
8//! rotations.
9
10use std::collections::{HashMap, HashSet};
11use yog_ids::direction::{DOWN, EAST, NORTH, SOUTH, UP, WEST};
12use yog_ids::texture_slot::{ALL, BOTTOM, END, FRONT, SIDE, TEXTURE, TOP};
13use yog_sync::Arc;
14
15use yog_data::Value;
16
17use super::assets::{Image, Resources, decode_png, missing_image};
18use super::raster::Canvas;
19
20/// A cube's textures per face: up, down, north, south, west, east.
21pub type Faces = [Option<Arc<Image>>; 6];
22
23/// One box of a model, in sixteenths of a block: per face (up, down,
24/// north, south, west, east) its texture, already cut to the face's uv
25/// (`None`: the box has no such face), and the turns it's drawn with, in
26/// order (its element's, then its variant's).
27#[derive(Clone)]
28pub struct Cuboid {
29    pub from: [f32; 3],
30    pub to: [f32; 3],
31    pub faces: Faces,
32    pub turns: Vec<Turn>,
33}
34
35/// A turn of a box about `axis` (0 x, 1 y, 2 z) through `origin`
36/// (sixteenths of a block), `degrees` counter-clockwise looking down the
37/// axis; `rescale` stretches the turned box's other axes back to the block
38/// (an element's `rescale`).
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct Turn {
41    pub origin: [f32; 3],
42    pub axis: usize,
43    pub degrees: f32,
44    pub rescale: bool,
45}
46
47/// A block's centre, in sixteenths: what a variant turns about.
48const CENTRE: [f32; 3] = [8.0, 8.0, 8.0];
49/// A block's side, in the sixteenths models are written in.
50const BLOCK_UNITS: f32 = 16.0;
51
52impl Turn {
53    /// `p` (sixteenths) turned.
54    fn apply(&self, p: [f32; 3]) -> [f32; 3] {
55        let (s, c) = self.degrees.to_radians().sin_cos();
56        let (a, b) = match self.axis {
57            0 => (1, 2),
58            1 => (2, 0),
59            _ => (0, 1),
60        };
61        let d = [0, 1, 2].map(|i| p[i] - self.origin[i]);
62        let mut out = d;
63        out[a] = d[a] * c - d[b] * s;
64        out[b] = d[a] * s + d[b] * c;
65        if self.rescale && c.abs() > f32::EPSILON {
66            let k = 1.0 / c.abs();
67            out[a] *= k;
68            out[b] *= k;
69        }
70        [0, 1, 2].map(|i| out[i] + self.origin[i])
71    }
72
73    /// A direction turned (no origin, no rescale).
74    fn apply_dir(&self, v: [f32; 3]) -> [f32; 3] {
75        Turn {
76            origin: [0.0; 3],
77            rescale: false,
78            ..*self
79        }
80        .apply(v)
81    }
82}
83
84/// A block as drawn: its model's boxes.
85pub type Shape = Vec<Cuboid>;
86
87/// A whole-block cube with `faces` (a missing one: the missing texture).
88/// A unit cube's face: normal, corner offset, edges a and b, texture slot, shade.
89type CubeFace = ([i32; 3], [f32; 3], [f32; 3], [f32; 3], usize, f32);
90
91pub fn cube(faces: Faces) -> Shape {
92    let faces = faces.map(|f| Some(f.unwrap_or_else(|| Arc::new(missing_image()))));
93    vec![Cuboid {
94        from: [0.0; 3],
95        to: [BLOCK_UNITS; 3],
96        faces,
97        turns: Vec::new(),
98    }]
99}
100
101const FACE_NAMES: [&str; 6] = [UP, DOWN, NORTH, SOUTH, WEST, EAST];
102
103/// The texture variables the game's block models draw each face with (in
104/// [`FACE_NAMES`] order), most particular first: a cube's `up`, a
105/// column's `end`, a `cube_all`'s `all`, a one-texture model's `texture`.
106pub const FACE_TEXTURES: [&[&str]; 6] = [
107    &[UP, TOP, END, ALL, TEXTURE],
108    &[DOWN, BOTTOM, END, ALL, TEXTURE],
109    &[NORTH, FRONT, SIDE, ALL, TEXTURE],
110    &[SOUTH, SIDE, ALL, TEXTURE],
111    &[WEST, SIDE, ALL, TEXTURE],
112    &[EAST, SIDE, ALL, TEXTURE],
113];
114
115/// What a face falls back to when its model has none of [`FACE_TEXTURES`].
116const PARTICLE: &str = yog_ids::model::PARTICLE;
117
118/// The part of a face's texture its box shows when the model gives no `uv`.
119fn default_uv(slot: usize, f: [f32; 3], t: [f32; 3]) -> [f32; 4] {
120    match slot {
121        0 => [f[0], f[2], t[0], t[2]],
122        1 => [f[0], BLOCK_UNITS - t[2], t[0], BLOCK_UNITS - f[2]],
123        2 => [
124            BLOCK_UNITS - t[0],
125            BLOCK_UNITS - t[1],
126            BLOCK_UNITS - f[0],
127            BLOCK_UNITS - f[1],
128        ],
129        3 => [f[0], BLOCK_UNITS - t[1], t[0], BLOCK_UNITS - f[1]],
130        4 => [f[2], BLOCK_UNITS - t[1], t[2], BLOCK_UNITS - f[1]],
131        _ => [
132            BLOCK_UNITS - t[2],
133            BLOCK_UNITS - t[1],
134            BLOCK_UNITS - f[2],
135            BLOCK_UNITS - f[1],
136        ],
137    }
138}
139
140/// `img`'s part `uv` (sixteenths of it; reversed: mirrored).
141fn cut(img: &Image, [u0, v0, u1, v1]: [f32; 4]) -> Image {
142    let (sx, sy) = (
143        img.width as f32 / BLOCK_UNITS,
144        img.width.min(img.height) as f32 / BLOCK_UNITS,
145    );
146    let (x0, x1) = ((u0 * sx).round() as i32, (u1 * sx).round() as i32);
147    let (y0, y1) = ((v0 * sy).round() as i32, (v1 * sy).round() as i32);
148    let (w, h) = (
149        (x1 - x0).unsigned_abs().max(1),
150        (y1 - y0).unsigned_abs().max(1),
151    );
152    let mut rgba = Vec::with_capacity((w * h * 4) as usize);
153    for y in 0..h as i32 {
154        for x in 0..w as i32 {
155            let px = if x1 >= x0 { x0 + x } else { x0 - 1 - x };
156            let py = if y1 >= y0 { y0 + y } else { y0 - 1 - y };
157            let (px, py) = (
158                px.clamp(0, img.width as i32 - 1) as u32,
159                py.clamp(0, img.height as i32 - 1) as u32,
160            );
161            rgba.extend_from_slice(&img.pixel(px, py).to_rgba8());
162        }
163    }
164    Image {
165        width: w,
166        height: h,
167        rgba,
168    }
169}
170
171/// A model's `elements` as boxes; `texture("#var" or "var")` gives a face's texture.
172pub fn shape_of(elements: &[Value], texture: &mut dyn FnMut(&str) -> Option<Arc<Image>>) -> Shape {
173    let triple = |v: Option<&Value>| -> Option<[f32; 3]> {
174        let a = v?.as_list()?;
175        Some([
176            a.first()?.as_f64()? as f32,
177            a.get(1)?.as_f64()? as f32,
178            a.get(2)?.as_f64()? as f32,
179        ])
180    };
181    elements
182        .iter()
183        .filter_map(|e| {
184            let (from, to) = (
185                triple(e.get(yog_ids::model::FROM))?,
186                triple(e.get(yog_ids::model::TO))?,
187            );
188            let (f, t) = (
189                [0, 1, 2].map(|i| from[i].min(to[i])),
190                [0, 1, 2].map(|i| from[i].max(to[i])),
191            );
192            let faces = e.get(yog_ids::model::FACES)?;
193            let faces: Faces = std::array::from_fn(|slot| {
194                let face = faces.get(FACE_NAMES[slot])?;
195                let img = face
196                    .get(yog_ids::model::TEXTURE)
197                    .and_then(Value::as_str)
198                    .and_then(&mut *texture)
199                    .unwrap_or_else(|| Arc::new(missing_image()));
200                let uv = face
201                    .get(yog_ids::model::UV)
202                    .and_then(Value::as_list)
203                    .and_then(|a| {
204                        Some([
205                            a.first()?.as_f64()? as f32,
206                            a.get(1)?.as_f64()? as f32,
207                            a.get(2)?.as_f64()? as f32,
208                            a.get(3)?.as_f64()? as f32,
209                        ])
210                    })
211                    .unwrap_or_else(|| default_uv(slot, f, t));
212                Some(Arc::new(cut(&img, uv)))
213            });
214            Some(Cuboid {
215                from: f,
216                to: t,
217                faces,
218                turns: element_turn(e).into_iter().collect(),
219            })
220        })
221        .collect()
222}
223
224/// An element's `rotation`: `{ origin, axis: "x"|"y"|"z", angle, rescale }`.
225fn element_turn(e: &Value) -> Option<Turn> {
226    let r = e.get(yog_ids::model::ROTATION)?;
227    let o = r.get(yog_ids::model::ORIGIN)?.as_list()?;
228    let origin = [
229        o.first()?.as_f64()? as f32,
230        o.get(1)?.as_f64()? as f32,
231        o.get(2)?.as_f64()? as f32,
232    ];
233    let axis = ["x", "y", "z"]
234        .iter()
235        .position(|a| r.get(yog_ids::model::AXIS).and_then(Value::as_str) == Some(*a))?;
236    let degrees = r.get(yog_ids::model::ANGLE)?.as_f64()? as f32;
237    Some(Turn {
238        origin,
239        axis,
240        degrees,
241        rescale: r
242            .get(yog_ids::model::RESCALE)
243            .and_then(Value::as_bool)
244            .unwrap_or(false),
245    })
246}
247
248/// The first `elements` up a model's parents.
249fn model_elements(res: &Resources, model: &str, depth: usize) -> Option<Vec<Value>> {
250    if depth > 8 {
251        return None;
252    }
253    let (ns, path) = yog_ids::split(model);
254    let path = path
255        .strip_prefix("block/")
256        .map_or(format!("block/{path}"), |p| format!("block/{p}"));
257    let json = crate::design::data::read_bytes(&res.read(ns, &yog_ids::model_file(&path))?)?;
258    if let Some(e) = json.get(yog_ids::model::ELEMENTS).and_then(Value::as_list) {
259        return Some(e.to_vec());
260    }
261    model_elements(res, json.get(yog_ids::model::PARENT)?.as_str()?, depth + 1)
262}
263
264/// The texture variables of a block model, its parents' included.
265fn model_textures(res: &Resources, model: &str, depth: usize) -> HashMap<String, String> {
266    let mut out = HashMap::new();
267    if depth > 8 {
268        return out;
269    }
270    let (ns, path) = yog_ids::split(model);
271    let path = path
272        .strip_prefix("block/")
273        .map_or(format!("block/{path}"), |p| format!("block/{p}"));
274    let Some(json) = res
275        .read(ns, &yog_ids::model_file(&path))
276        .and_then(|b| crate::design::data::read_bytes(&b))
277    else {
278        return out;
279    };
280    if let Some(parent) = json.get(yog_ids::model::PARENT).and_then(Value::as_str) {
281        out = model_textures(res, parent, depth + 1);
282    }
283    if let Some(t) = json.get(yog_ids::model::TEXTURES).and_then(Value::as_map) {
284        for (k, v) in t {
285            if let Some(v) = v.as_str() {
286                out.insert(k.to_string(), v.to_string());
287            }
288        }
289    }
290    out
291}
292
293/// A block state's id and its properties: `minecraft:furnace[facing=north]`.
294fn split_state(state: &str) -> (&str, HashMap<&str, &str>) {
295    let (id, props) = state.split_once('[').unwrap_or((state, ""));
296    let props = props
297        .trim_end_matches(']')
298        .split(',')
299        .filter_map(|kv| kv.split_once('='))
300        .map(|(k, v)| (k.trim(), v.trim()))
301        .collect();
302    (id, props)
303}
304
305/// Whether `props` meet a multipart `when` (every property one of its
306/// `a|b` values; `OR`/`AND`: any/all of a list). A property the state
307/// doesn't name meets nothing.
308fn meets(when: &Value, props: &HashMap<&str, &str>) -> bool {
309    let Some(map) = when.as_map() else {
310        return true;
311    };
312    if let Some(any) = map.get(yog_ids::model::OR).and_then(Value::as_list) {
313        return any.iter().any(|w| meets(w, props));
314    }
315    if let Some(all) = map.get(yog_ids::model::AND).and_then(Value::as_list) {
316        return all.iter().all(|w| meets(w, props));
317    }
318    map.iter().all(|(k, v)| {
319        let want = v
320            .as_str()
321            .map(str::to_string)
322            .unwrap_or_else(|| yog_data::text::to_string(v));
323        props
324            .get(k)
325            .is_some_and(|have| want.split('|').any(|w| w == *have))
326    })
327}
328
329/// A blockstate's `apply`/variant: its model and its turns (`x` then `y`,
330/// about the block's centre; the game turns models clockwise looking down
331/// an axis).
332fn applied(v: &Value) -> Option<(String, Vec<Turn>)> {
333    let v = match v.as_list() {
334        Some(variants) => variants.first()?,
335        None => v,
336    };
337    let model = v.get(yog_ids::model::MODEL)?.as_str()?.to_string();
338    let mut turns = Vec::new();
339    for (axis, key) in [(0, "x"), (1, "y")] {
340        let deg = v.get(key).and_then(Value::as_f64).unwrap_or(0.0) as f32;
341        if deg != 0.0 {
342            turns.push(Turn {
343                origin: CENTRE,
344                axis,
345                degrees: -deg,
346                rescale: false,
347            });
348        }
349    }
350    Some((model, turns))
351}
352
353/// The models block state `state` shows: its variant (the one whose
354/// properties it has; the first otherwise), or each multipart part it meets.
355fn block_models(res: &Resources, state: &str) -> Vec<(String, Vec<Turn>)> {
356    let (id, props) = split_state(state);
357    let (ns, path) = yog_ids::split(id);
358    let Some(json) = res
359        .read(ns, &yog_ids::blockstate_file(path))
360        .and_then(|b| crate::design::data::read_bytes(&b))
361    else {
362        return Vec::new();
363    };
364    if let Some(variants) = json.get(yog_ids::model::VARIANTS).and_then(Value::as_map) {
365        let fits = |key: &str| {
366            key.split(',').filter(|kv| !kv.is_empty()).all(|kv| {
367                kv.split_once('=')
368                    .is_some_and(|(k, v)| props.get(k) == Some(&v))
369            })
370        };
371        let chosen = variants
372            .iter()
373            .find(|(k, _)| fits(k))
374            .or_else(|| variants.iter().next());
375        return chosen.and_then(|(_, v)| applied(v)).into_iter().collect();
376    }
377    json.get(yog_ids::model::MULTIPART)
378        .and_then(Value::as_list)
379        .into_iter()
380        .flatten()
381        .filter(|part| {
382            part.get(yog_ids::model::WHEN)
383                .is_none_or(|w| meets(w, &props))
384        })
385        .filter_map(|part| applied(part.get(yog_ids::model::APPLY)?))
386        .collect()
387}
388
389/// Whether `id` is a block (the game's, or the mod's `.yblock`).
390pub fn is_block(res: &Resources, id: &str) -> bool {
391    let id = split_state(id).0;
392    let (ns, path) = yog_ids::split(id);
393    res.read(ns, &yog_ids::blockstate_file(path)).is_some()
394        || res
395            .mod_root
396            .as_deref()
397            .is_some_and(|root| crate::design::content::content_block_faces(root, id).is_some())
398}
399
400fn shape(res: &Resources, state: &str, cache: &mut HashMap<String, Shape>) -> Shape {
401    if let Some(s) = cache.get(state) {
402        return s.clone();
403    }
404    let id = split_state(state).0;
405    let s = mod_shape(res, id).unwrap_or_else(|| game_shape(res, state));
406    cache.insert(state.to_string(), s.clone());
407    s
408}
409
410/// The mod's own block: its `.yblock` model — its elements, else a cube of its textures.
411fn mod_shape(res: &Resources, id: &str) -> Option<Shape> {
412    let root = res.mod_root.as_deref()?;
413    if let Some((elements, textures)) = crate::design::content::content_block_model(root, id) {
414        let mut decoded: HashMap<String, Option<Arc<Image>>> = HashMap::new();
415        let mut texture = |var: &str| -> Option<Arc<Image>> {
416            let var = var.trim_start_matches('#');
417            decoded
418                .entry(var.to_string())
419                .or_insert_with(|| textures.get(var).and_then(|b| decode_png(b)).map(Arc::new))
420                .clone()
421        };
422        let s = shape_of(&elements, &mut texture);
423        if !s.is_empty() {
424            return Some(s);
425        }
426    }
427    crate::design::content::content_block_faces(root, id)
428        .map(|faces| cube(faces.map(|b| b.and_then(|b| decode_png(&b)).map(Arc::new))))
429}
430
431/// The game's block state: every model its blockstate shows for it, each
432/// turned as the blockstate says.
433fn game_shape(res: &Resources, state: &str) -> Shape {
434    let mut models = block_models(res, state);
435    if models.is_empty() {
436        let (ns, path) = yog_ids::split(split_state(state).0);
437        models.push((format!("{ns}:block/{path}"), Vec::new()));
438    }
439    let mut out = Shape::new();
440    for (model, turns) in models {
441        for mut cuboid in model_shape(res, &model) {
442            cuboid.turns.extend(turns.iter().copied());
443            out.push(cuboid);
444        }
445    }
446    out
447}
448
449/// One block model's boxes: its elements, else a cube of its textures.
450fn model_shape(res: &Resources, model: &str) -> Shape {
451    let vars = model_textures(res, model, 0);
452    // `#var` → its value, following references.
453    let resolve = |key: &str| -> Option<String> {
454        let mut v = vars.get(key)?.clone();
455        for _ in 0..8 {
456            match v.strip_prefix('#') {
457                Some(r) => v = vars.get(r)?.clone(),
458                None => return Some(v),
459            }
460        }
461        None
462    };
463    let load = |tex: Option<String>| -> Option<Arc<Image>> {
464        let tex = tex?;
465        let (ns, path) = yog_ids::split(&tex);
466        decode_png(&res.read(ns, &yog_ids::texture_file(path))?).map(Arc::new)
467    };
468    if let Some(elements) = model_elements(res, model, 0) {
469        let mut texture = |var: &str| load(resolve(var.trim_start_matches('#')));
470        let s = shape_of(&elements, &mut texture);
471        if !s.is_empty() {
472            return s;
473        }
474    }
475    cube(FACE_TEXTURES.map(|keys| load(keys.iter().chain(&[PARTICLE]).find_map(|k| resolve(k)))))
476}
477
478/// `x y z state` lines → blocks.
479fn parse(list: &str) -> Vec<([i32; 3], String)> {
480    list.lines()
481        .filter_map(|l| {
482            let mut p = l.trim().splitn(4, ' ');
483            let at = [
484                p.next()?.parse().ok()?,
485                p.next()?.parse().ok()?,
486                p.next()?.parse().ok()?,
487            ];
488            Some((at, p.next()?.to_string()))
489        })
490        .collect()
491}
492
493pub fn draw(c: &mut Canvas, list: &str, rect: [f32; 4], yaw: f32, pitch: f32) {
494    let mut cache = HashMap::new();
495    draw_with(c, &parse(list), rect, (yaw, pitch), |res, id| {
496        shape(res, id, &mut cache)
497    });
498}
499
500/// One cube with its own `faces` (a mob's head, say), as [`draw`] draws a block.
501pub fn draw_cube(c: &mut Canvas, faces: Faces, rect: [f32; 4], yaw: f32, pitch: f32) {
502    draw_with(
503        c,
504        &[([0, 0, 0], String::new())],
505        rect,
506        (yaw, pitch),
507        |_, _| cube(faces.clone()),
508    );
509}
510
511/// The game's shading of a block's faces by the way they look: up, down,
512/// north and south, west and east.
513const SHADE_UP: f32 = 1.0;
514const SHADE_DOWN: f32 = 0.5;
515const SHADE_NORTH_SOUTH: f32 = 0.8;
516const SHADE_WEST_EAST: f32 = 0.6;
517/// As the hosts draw them: the blocks as big as fit, this many times their largest side.
518const FIT: f32 = 1.7;
519
520/// (normal, corner offset, a, b, slot, shade) per face of a unit cube.
521const FACES: [CubeFace; 6] = [
522    (
523        [0, 1, 0],
524        [0.0, 1.0, 0.0],
525        [1.0, 0.0, 0.0],
526        [0.0, 0.0, 1.0],
527        0,
528        SHADE_UP,
529    ),
530    (
531        [0, -1, 0],
532        [0.0, 0.0, 1.0],
533        [1.0, 0.0, 0.0],
534        [0.0, 0.0, -1.0],
535        1,
536        SHADE_DOWN,
537    ),
538    (
539        [0, 0, -1],
540        [1.0, 1.0, 0.0],
541        [-1.0, 0.0, 0.0],
542        [0.0, -1.0, 0.0],
543        2,
544        SHADE_NORTH_SOUTH,
545    ),
546    (
547        [0, 0, 1],
548        [0.0, 1.0, 1.0],
549        [1.0, 0.0, 0.0],
550        [0.0, -1.0, 0.0],
551        3,
552        SHADE_NORTH_SOUTH,
553    ),
554    (
555        [-1, 0, 0],
556        [0.0, 1.0, 0.0],
557        [0.0, 0.0, 1.0],
558        [0.0, -1.0, 0.0],
559        4,
560        SHADE_WEST_EAST,
561    ),
562    (
563        [1, 0, 0],
564        [1.0, 1.0, 1.0],
565        [0.0, 0.0, -1.0],
566        [0.0, -1.0, 0.0],
567        5,
568        SHADE_WEST_EAST,
569    ),
570];
571
572/// How the blocks are seen: their centre, the turn (yaw's and pitch's sine
573/// and cosine), the box drawn in and the scale.
574struct Camera {
575    centre: [f32; 3],
576    yaw: (f32, f32),
577    pitch: (f32, f32),
578    rect: [f32; 4],
579    scale: f32,
580}
581
582impl Camera {
583    /// The blocks' bounds fitted into `rect`, turned by `yaw` and `pitch` (degrees).
584    fn fit(blocks: &[([i32; 3], String)], rect: [f32; 4], yaw: f32, pitch: f32) -> Camera {
585        let mut lo = [i32::MAX; 3];
586        let mut hi = [i32::MIN; 3];
587        for (p, _) in blocks {
588            for i in 0..3 {
589                lo[i] = lo[i].min(p[i]);
590                hi[i] = hi[i].max(p[i]);
591            }
592        }
593        let size = (0..3).map(|i| hi[i] - lo[i]).max().unwrap_or(0) as f32 + 1.0;
594        Camera {
595            centre: [0, 1, 2].map(|i| (lo[i] + hi[i] + 1) as f32 / 2.0),
596            yaw: yaw.to_radians().sin_cos(),
597            pitch: pitch.to_radians().sin_cos(),
598            rect,
599            scale: rect[2].min(rect[3]) / (size * FIT),
600        }
601    }
602
603    /// World → view: about Y by yaw, then about X by pitch; x right, y up, z toward the viewer.
604    fn view(&self, p: [f32; 3]) -> [f32; 3] {
605        let ((sy, cy), (sp, cp)) = (self.yaw, self.pitch);
606        let [px, py, pz] = [0, 1, 2].map(|i| p[i] - self.centre[i]);
607        let x1 = px * cy + pz * sy;
608        let z1 = -px * sy + pz * cy;
609        [x1, py * cp - z1 * sp, py * sp + z1 * cp]
610    }
611
612    /// A view point on the canvas.
613    fn screen(&self, v: [f32; 3]) -> [f32; 2] {
614        let [x, y, w, h] = self.rect;
615        [
616            x + w / 2.0 + v[0] * self.scale,
617            y + h / 2.0 - v[1] * self.scale,
618        ]
619    }
620}
621
622/// A face to draw: (depth, texture, corner, edge a, edge b, shade).
623type Quad = (f32, Option<Arc<Image>>, [f32; 2], [f32; 2], [f32; 2], f32);
624
625fn draw_with(
626    c: &mut Canvas,
627    blocks: &[([i32; 3], String)],
628    rect: [f32; 4],
629    (yaw, pitch): (f32, f32),
630    mut shape_of: impl FnMut(&Resources, &str) -> Shape,
631) {
632    if blocks.is_empty() || rect[2] <= 0.0 || rect[3] <= 0.0 {
633        return;
634    }
635    let cam = Camera::fit(blocks, rect, yaw, pitch);
636    let occupied: HashSet<[i32; 3]> = blocks.iter().map(|(p, _)| *p).collect();
637    let mut quads = Vec::new();
638    for (p, id) in blocks {
639        for cuboid in shape_of(c.resources(), id) {
640            seen_faces(&cam, *p, &cuboid, &occupied, &mut quads);
641        }
642    }
643    // Back to front.
644    quads.sort_by(|a, b| a.0.total_cmp(&b.0));
645    let missing = Arc::new(missing_image());
646    for (_, img, o, a, b, shade) in quads {
647        let img = img.unwrap_or_else(|| missing.clone());
648        c.quad(&img, o, a, b, shade);
649    }
650}
651
652/// The faces of `cuboid` (of the block at `p`) that can be seen: not against
653/// a neighbour, not facing away.
654fn seen_faces(
655    cam: &Camera,
656    p: [i32; 3],
657    cuboid: &Cuboid,
658    occupied: &HashSet<[i32; 3]>,
659    quads: &mut Vec<Quad>,
660) {
661    let (f, t) = (cuboid.from, cuboid.to);
662    // A unit cube's corner → this box's (in sixteenths, turned), in the world.
663    let at = |u: [f32; 3]| {
664        let q = cuboid
665            .turns
666            .iter()
667            .fold([0, 1, 2].map(|i| f[i] + u[i] * (t[i] - f[i])), |q, turn| {
668                turn.apply(q)
669            });
670        [0, 1, 2].map(|i| p[i] as f32 + q[i] / BLOCK_UNITS)
671    };
672    for (n, corner, a, b, slot, shade) in FACES {
673        // On the block's side, against a neighbour: hidden (a turned box is never flush).
674        let on_side = cuboid.turns.is_empty()
675            && (0..3).all(|i| {
676                n[i] == 0 || (n[i] > 0 && t[i] >= BLOCK_UNITS) || (n[i] < 0 && f[i] <= 0.0)
677            });
678        if on_side && occupied.contains(&[p[0] + n[0], p[1] + n[1], p[2] + n[2]]) {
679            continue;
680        }
681        // Facing away from the viewer: unseen.
682        let normal = cuboid
683            .turns
684            .iter()
685            .fold(n.map(|v| v as f32), |v, turn| turn.apply_dir(v));
686        if cam.view([0, 1, 2].map(|i| cam.centre[i] + normal[i]))[2] <= 0.0 {
687            continue;
688        }
689        let Some(img) = cuboid.faces[slot].clone() else {
690            continue;
691        };
692        let o = at(corner);
693        let oa = at([0, 1, 2].map(|i| corner[i] + a[i]));
694        let ob = at([0, 1, 2].map(|i| corner[i] + b[i]));
695        let mid = cam.view([0, 1, 2].map(|i| (oa[i] + ob[i]) / 2.0));
696        let (so, sa, sb) = (
697            cam.screen(cam.view(o)),
698            cam.screen(cam.view(oa)),
699            cam.screen(cam.view(ob)),
700        );
701        quads.push((
702            mid[2],
703            Some(img),
704            so,
705            [sa[0] - so[0], sa[1] - so[1]],
706            [sb[0] - so[0], sb[1] - so[1]],
707            shade,
708        ));
709    }
710}
711
712#[cfg(test)]
713mod tests;