Skip to main content

pfx_load/scene/
tiles.rs

1use std::collections::{BTreeMap, BTreeSet, HashSet};
2use std::path::{Path, PathBuf};
3
4pub use pfx_scene::types::{TileCell, TilePlane, Tiles};
5
6use pfx_core::anim::math::transform_point;
7
8use super::build::{self, Places, Step, inner};
9use super::{IDENTITY, Matrix, Object, Scene, SceneError, multiply};
10
11pub const EMPTY: char = Tiles::EMPTY;
12pub const KEYS: &str = "#@%&*+=ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz0123456789";
13
14pub fn axes(layer: &Tiles) -> [[f32; 3]; 2] {
15    let (across, up) = layer.plane.unwrap_or_default().axes();
16    [across, up]
17}
18
19pub fn cell_at(layer: &Tiles, point: [f64; 3]) -> [i32; 2] {
20    let [across, up] = axes(layer);
21    let [width, height] = layer.cell_sides();
22    let d: [f64; 3] = std::array::from_fn(|k| point[k] - f64::from(layer.origin[k]));
23    let along = |axis: [f32; 3]| (0..3).map(|k| d[k] * f64::from(axis[k])).sum::<f64>();
24    [
25        (along(across) / f64::from(width)).round() as i32,
26        (along(up) / f64::from(height)).round() as i32,
27    ]
28}
29
30pub fn corners(layer: &Tiles, cell: [i32; 2]) -> [[f32; 3]; 4] {
31    let [across, up] = axes(layer);
32    let [width, height] = layer.cell_sides();
33    let centre = layer.position(cell);
34    let at = |a: f32, b: f32| {
35        std::array::from_fn(|k| centre[k] + across[k] * a * width + up[k] * b * height)
36    };
37    [at(-0.5, -0.5), at(0.5, -0.5), at(0.5, 0.5), at(-0.5, 0.5)]
38}
39
40pub fn get(layer: &Tiles, cell: [i32; 2]) -> Option<String> {
41    layer
42        .cells()
43        .into_iter()
44        .find(|(at, _)| *at == cell)
45        .map(|(_, tile)| tile.to_string())
46}
47
48pub fn key(layer: &Tiles, prefab: &str) -> Option<String> {
49    layer
50        .palette
51        .iter()
52        .find(|(_, path)| path.as_str() == prefab)
53        .map(|(key, _)| key.clone())
54}
55
56pub fn free_key(layer: &Tiles) -> Option<String> {
57    KEYS.chars()
58        .map(String::from)
59        .find(|key| !layer.palette.contains_key(key))
60}
61
62#[derive(Clone, Debug, PartialEq)]
63pub enum Painted {
64    Rows { rows: Vec<String>, origin: [f32; 3] },
65    Cells(Vec<TileCell>),
66}
67
68pub fn painted(layer: &Tiles, changes: &[([i32; 2], Option<&str>)]) -> Result<Painted, String> {
69    if layer.cells.is_some() {
70        let mut cells: BTreeMap<[i32; 2], String> = layer
71            .cells()
72            .into_iter()
73            .map(|(at, tile)| (at, tile.to_string()))
74            .collect();
75        let mut order: Vec<[i32; 2]> = layer.cells.iter().flatten().map(|cell| cell.at).collect();
76        for (at, tile) in changes {
77            match tile {
78                Some(tile) => {
79                    if cells.insert(*at, tile.to_string()).is_none() {
80                        order.push(*at);
81                    }
82                }
83                None => {
84                    cells.remove(at);
85                }
86            }
87        }
88        return Ok(Painted::Cells(
89            order
90                .into_iter()
91                .filter_map(|at| {
92                    Some(TileCell {
93                        at,
94                        tile: cells.get(&at)?.clone(),
95                    })
96                })
97                .collect(),
98        ));
99    }
100    let mut rows: Vec<Vec<char>> = layer
101        .rows
102        .iter()
103        .flatten()
104        .map(|row| row.chars().collect())
105        .collect();
106    let mut shift = [0i32; 2];
107    for (cell, tile) in changes {
108        let [i, j] = [cell[0] + shift[0], cell[1] + shift[1]];
109        let Some(tile) = tile else {
110            let height = rows.len() as i32;
111            let r = height - 1 - j;
112            if (0..height).contains(&r)
113                && i >= 0
114                && let Some(slot) = rows[r as usize].get_mut(i as usize)
115            {
116                *slot = EMPTY;
117            }
118            continue;
119        };
120        let mut chars = tile.chars();
121        let (Some(character), None) = (chars.next(), chars.next()) else {
122            return Err(format!(
123                "tile layer {}: a rows layer takes one-character keys, not {tile:?}",
124                layer.name
125            ));
126        };
127        let (mut i, mut j) = (i, j);
128        if i < 0 {
129            let grow = -i as usize;
130            for row in &mut rows {
131                if !row.is_empty() {
132                    row.splice(0..0, std::iter::repeat_n(EMPTY, grow));
133                }
134            }
135            shift[0] += -i;
136            i = 0;
137        }
138        if j < 0 {
139            for _ in 0..-j {
140                rows.push(Vec::new());
141            }
142            shift[1] += -j;
143            j = 0;
144        }
145        let height = rows.len() as i32;
146        if j >= height {
147            for _ in 0..(j - height + 1) {
148                rows.insert(0, Vec::new());
149            }
150        }
151        let height = rows.len() as i32;
152        let row = &mut rows[(height - 1 - j) as usize];
153        if row.len() <= i as usize {
154            row.resize(i as usize + 1, EMPTY);
155        }
156        row[i as usize] = character;
157    }
158    let [across, up] = axes(layer);
159    let [width, height] = layer.cell_sides();
160    let (a, b) = (shift[0] as f32 * width, shift[1] as f32 * height);
161    Ok(Painted::Rows {
162        rows: rows.iter().map(|row| row.iter().collect()).collect(),
163        origin: std::array::from_fn(|k| layer.origin[k] - across[k] * a - up[k] * b),
164    })
165}
166
167pub fn cell_key(layer: &str, cell: [i32; 2]) -> String {
168    format!("{layer}[{},{}]", cell[0], cell[1])
169}
170
171pub fn cell_of(object: &str) -> Option<(String, [i32; 2])> {
172    let head = object.split('/').next()?;
173    let (layer, rest) = head.split_once('[')?;
174    let inside = rest.strip_suffix(']')?;
175    let (i, j) = inside.split_once(',')?;
176    Some((layer.to_string(), [i.parse().ok()?, j.parse().ok()?]))
177}
178
179pub fn line(from: [i32; 2], to: [i32; 2]) -> Vec<[i32; 2]> {
180    let (dx, dy) = ((to[0] - from[0]).abs(), -(to[1] - from[1]).abs());
181    let (sx, sy) = (
182        if from[0] < to[0] { 1 } else { -1 },
183        if from[1] < to[1] { 1 } else { -1 },
184    );
185    let mut error = dx + dy;
186    let mut at = from;
187    let mut cells = vec![at];
188    while at != to {
189        let twice = 2 * error;
190        if twice >= dy {
191            error += dy;
192            at[0] += sx;
193        }
194        if twice <= dx {
195            error += dx;
196            at[1] += sy;
197        }
198        cells.push(at);
199    }
200    cells
201}
202
203pub fn rectangle(from: [i32; 2], to: [i32; 2]) -> Vec<[i32; 2]> {
204    let mut cells = Vec::new();
205    for j in from[1].min(to[1])..=from[1].max(to[1]) {
206        for i in from[0].min(to[0])..=from[0].max(to[0]) {
207            cells.push([i, j]);
208        }
209    }
210    cells
211}
212
213struct Template {
214    objects: Vec<(String, Object)>,
215    bodies: Vec<(String, super::Body)>,
216    triggers: Vec<(String, super::Trigger)>,
217    characters: Vec<(String, super::Character)>,
218    animations: Vec<(String, super::anim::Animation)>,
219    layers: Vec<(String, String)>,
220}
221
222fn translate(at: [f32; 3]) -> Matrix {
223    let mut matrix = IDENTITY;
224    matrix[3] = [at[0], at[1], at[2], 1.0];
225    matrix
226}
227
228const PLACED: &str = "tile";
229
230fn graft<T: Clone>(into: &mut BTreeMap<String, T>, from: &[(String, T)], base: &str) {
231    for (name, value) in from {
232        into.insert(format!("{base}/{name}"), value.clone());
233    }
234}
235
236fn quoted(text: &str) -> String {
237    toml::Value::String(text.to_string()).to_string()
238}
239
240fn synthetic(
241    root: &Path,
242    file: &Path,
243    text: String,
244    overlay: &BTreeMap<PathBuf, String>,
245    refuse: &dyn Fn(String) -> SceneError,
246) -> Result<Scene, SceneError> {
247    let mut held = overlay.clone();
248    held.insert(file.to_path_buf(), text);
249    let project = build::project(root)?;
250    let absolute: BTreeMap<PathBuf, String> = held
251        .iter()
252        .map(|(file, text)| (root.join(file), text.clone()))
253        .collect();
254    let resolved = project
255        .scene_with(&root.join(file), absolute)
256        .map_err(|found| refuse(build::refused(root, &found).message))?;
257    let mut built = build::build(&root.join(file), root, &resolved, None, &held)?;
258    built.files.remove(&root.join(file));
259    Ok(built)
260}
261
262pub fn extent(root: &Path, asset: &str) -> Result<[[f32; 3]; 2], SceneError> {
263    let file = PathBuf::from(format!("{asset}.extent.scene.toml"));
264    let text = if asset.ends_with(".prefab.toml") {
265        format!(
266            "format = 1\n\n[[object]]\nname = \"{PLACED}\"\nprefab = {}\n",
267            quoted(asset)
268        )
269    } else {
270        format!(
271            "format = 1\n\n[mesh.{PLACED}]\nfile = {}\n\n[[object]]\nname = \"{PLACED}\"\nmesh = \"{PLACED}\"\n",
272            quoted(asset)
273        )
274    };
275    let refuse = |message: String| SceneError::new(&root.join(asset), None, message);
276    let built = synthetic(root, &file, text, &BTreeMap::new(), &refuse)?;
277    let mut low = [f32::INFINITY; 3];
278    let mut high = [f32::NEG_INFINITY; 3];
279    for draw in built.draws().items {
280        for p in &draw.geometry.positions {
281            let world = transform_point(&draw.model, *p);
282            for k in 0..3 {
283                low[k] = low[k].min(world[k]);
284                high[k] = high[k].max(world[k]);
285            }
286        }
287    }
288    if low.iter().chain(&high).all(|value| value.is_finite()) {
289        Ok([low, high])
290    } else {
291        Ok([[0.0; 3]; 2])
292    }
293}
294
295fn template(
296    scene: &mut Scene,
297    root: &Path,
298    prefab: &str,
299    overlay: &BTreeMap<PathBuf, String>,
300    refuse: &dyn Fn(String) -> SceneError,
301) -> Result<Template, SceneError> {
302    let named = |message: String| refuse(format!("tile prefab {prefab}: {message}"));
303    let project = build::project(root)?;
304    let absolute: BTreeMap<PathBuf, String> = overlay
305        .iter()
306        .map(|(file, text)| (root.join(file), text.clone()))
307        .collect();
308    let resolved = project
309        .scene_with(&root.join(prefab), absolute)
310        .map_err(|found| named(build::refused(root, &found).message))?;
311    let built = build::build(&root.join(prefab), root, &resolved, None, overlay)?;
312    if let Some((name, _)) = built
313        .animations
314        .iter()
315        .find(|(_, animation)| animation.sprite.is_some())
316    {
317        return Err(named(format!(
318            "its object {name} is a sprite, and a tile places glTF clips only; place the sprite as an object"
319        )));
320    }
321    for (kind, count) in [
322        ("lights", built.lights.len()),
323        ("emitters", built.emitters.len()),
324        ("movers", built.movers.len()),
325        ("text", built.texts.len()),
326        ("content", built.contents.len()),
327        ("sounds", built.sounds.len()),
328    ] {
329        if count > 0 {
330            return Err(named(format!(
331                "a tile places objects, bodies, characters, triggers and glTF clips only, and it has {kind}"
332            )));
333        }
334    }
335    for (name, material) in built.library.iter() {
336        match scene.library.get(name) {
337            Some(own) if own == material => {}
338            Some(_) => {
339                return Err(named(format!(
340                    "its material {name} differs from the scene's material of that name"
341                )));
342            }
343            None => {
344                scene.library.insert(name, *material).map_err(named)?;
345            }
346        }
347    }
348    let mesh_key = |key: &str| format!("{prefab}:{key}");
349    for (key, mesh) in &built.meshes {
350        scene.meshes.insert(mesh_key(key), mesh.clone());
351    }
352    for (file, hash) in &built.files {
353        scene.files.insert(file.clone(), *hash);
354    }
355    let objects = built
356        .objects
357        .iter()
358        .map(|object| {
359            let mut object = object.clone();
360            if !object.mesh.is_empty() {
361                object.mesh = mesh_key(&object.mesh);
362            }
363            (object.name.clone(), object)
364        })
365        .collect();
366    Ok(Template {
367        objects,
368        bodies: built.bodies.into_iter().collect(),
369        triggers: built.triggers.into_iter().collect(),
370        characters: built.characters.into_iter().collect(),
371        animations: built.animations.into_iter().collect(),
372        layers: built.body_layers.into_iter().collect(),
373    })
374}
375
376pub(super) fn expand(
377    scene: &mut Scene,
378    root: &Path,
379    resolved: &pfx_scene::Scene,
380    places: &Places,
381    overlay: &BTreeMap<PathBuf, String>,
382) -> Result<(), SceneError> {
383    let mut templates: BTreeMap<String, Template> = BTreeMap::new();
384    for entry in &resolved.tiles {
385        let layer = &entry.value;
386        let used: BTreeSet<&str> = layer.cells().into_iter().map(|(_, tile)| tile).collect();
387        for (key, prefab) in &layer.palette {
388            if templates.contains_key(prefab) || !used.contains(key.as_str()) {
389                continue;
390            }
391            let steps = [
392                Step::Named("tiles", inner(&entry.key)),
393                Step::Key("palette"),
394                Step::Key(key),
395            ];
396            let refuse = |message: String| places.refusal(&entry.file, &steps, message);
397            let made = template(scene, root, prefab, overlay, &refuse)?;
398            templates.insert(prefab.clone(), made);
399        }
400    }
401    let mut taken: HashSet<String> = scene.objects.iter().map(|o| o.name.clone()).collect();
402    let mut next = scene.objects.iter().map(|o| o.id).max().unwrap_or(0) + 1;
403    for entry in &resolved.tiles {
404        let layer = &entry.value;
405        for (cell, tile) in layer.cells() {
406            let Some(made) = layer.palette.get(tile).and_then(|p| templates.get(p)) else {
407                continue;
408            };
409            let base = cell_key(&entry.key, cell);
410            let at = layer.position(cell);
411            let shift = translate(at);
412            for (name, object) in &made.objects {
413                let key = format!("{base}/{name}");
414                if !taken.insert(key.clone()) {
415                    return places.error(
416                        &entry.file,
417                        &[Step::Named("tiles", inner(&entry.key))],
418                        format!(
419                            "tile layer {}: the cell's object {key} is already an object of the scene",
420                            entry.key
421                        ),
422                    );
423                }
424                let mut placed = object.clone();
425                placed.name = key;
426                placed.model = multiply(shift, object.model);
427                match &object.parent {
428                    Some(parent) => placed.parent = Some(format!("{base}/{parent}")),
429                    None => placed.at = std::array::from_fn(|k| object.at[k] + at[k]),
430                }
431                placed.id = next;
432                next += 1;
433                scene.objects.push(placed);
434            }
435            graft(&mut scene.bodies, &made.bodies, &base);
436            graft(&mut scene.triggers, &made.triggers, &base);
437            graft(&mut scene.characters, &made.characters, &base);
438            graft(&mut scene.animations, &made.animations, &base);
439            graft(&mut scene.body_layers, &made.layers, &base);
440        }
441    }
442    Ok(())
443}
444
445#[cfg(test)]
446#[path = "tiles_tests.rs"]
447mod tests;