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;