1use 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
20pub type Faces = [Option<Arc<Image>>; 6];
22
23#[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#[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
47const CENTRE: [f32; 3] = [8.0, 8.0, 8.0];
49const BLOCK_UNITS: f32 = 16.0;
51
52impl Turn {
53 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 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
84pub type Shape = Vec<Cuboid>;
86
87type 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
103pub 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
115const PARTICLE: &str = yog_ids::model::PARTICLE;
117
118fn 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
140fn 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
171pub 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
224fn 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
248fn 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
264fn 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
293fn 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
305fn 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
329fn 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
353fn 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
389pub 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
410fn 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
431fn 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
449fn model_shape(res: &Resources, model: &str) -> Shape {
451 let vars = model_textures(res, model, 0);
452 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
478fn 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
500pub 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
511const 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;
517const FIT: f32 = 1.7;
519
520const 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
572struct 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 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 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 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
622type 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 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
652fn 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 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 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 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;