use std::collections::{BTreeMap, BTreeSet};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::envelope::Campaign;
use crate::ids::{NodeId, PrefabId};
#[cfg(doc)]
use crate::owed_anchors;
use crate::siteplan::{PlacedBox, Site, SitePlan};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DetailPlanContent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub palette: Option<BTreeMap<String, String>>,
pub details: Vec<Detail>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Detail {
pub place: NodeId,
pub piece: PrefabId,
#[serde(default)]
pub anchors: BTreeMap<String, String>,
}
impl DetailPlanContent {
#[must_use]
pub fn detail_of(&self, node: &NodeId) -> Option<&Detail> {
self.details.iter().find(|d| &d.place == node)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub node: NodeId,
pub lo: [i64; 3],
pub hi: [i64; 3],
pub floor: i64,
pub bottom: i64,
pub owned: Vec<crate::siteplan::Aabb>,
pub owned_cells: usize,
pub voids: Vec<crate::siteplan::Void>,
pub fixed: Vec<([i64; 3], String)>,
pub clipped: Vec<(crate::siteplan::Aabb, NodeId)>,
}
impl Frame {
#[must_use]
pub fn of(site: &Site<'_>, i: usize) -> Frame {
let b = &site.boxes[i];
let o = site.ownership(i);
Frame {
node: b.node.clone(),
lo: o.lo,
hi: o.hi,
floor: b.floor,
bottom: site.bottom(i),
owned: o.owned,
owned_cells: o.owned_cells,
voids: o.voids,
fixed: o.fixed,
clipped: o.clipped,
}
}
#[must_use]
pub fn extent(&self) -> [i64; 3] {
[
self.hi[0] - self.lo[0] + 1,
self.hi[1] - self.lo[1] + 1,
self.hi[2] - self.lo[2] + 1,
]
}
#[must_use]
pub fn datum_y(&self) -> i64 {
self.floor - self.lo[1]
}
#[must_use]
pub fn to_local(&self, world: [i64; 3]) -> [i64; 3] {
[
world[0] - self.lo[0],
world[1] - self.lo[1],
world[2] - self.lo[2],
]
}
#[must_use]
pub fn contains(&self, world: [i64; 3]) -> bool {
(0..3).all(|i| world[i] >= self.lo[i] && world[i] <= self.hi[i])
}
#[must_use]
pub fn owns(&self, world: [i64; 3]) -> bool {
self.owned
.iter()
.any(|(lo, hi)| (0..3).all(|i| world[i] >= lo[i] && world[i] <= hi[i]))
}
#[must_use]
pub fn all(c: &Campaign) -> Vec<(Frame, PlacedBox)> {
let plan = SitePlan::of(c);
let site = plan.site();
(0..plan.boxes.len())
.map(|i| (Frame::of(&site, i), plan.boxes[i].clone()))
.collect()
}
#[must_use]
pub fn is_fixed(&self, world: [i64; 3]) -> bool {
self.fixed.iter().any(|(c, _)| *c == world)
}
}
#[must_use]
pub fn bound_places(c: &Campaign) -> BTreeSet<String> {
c.detail_plan
.as_ref()
.map(|e| {
e.content
.details
.iter()
.map(|d| d.place.0.clone())
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn is_bound(c: &Campaign, node: &NodeId) -> bool {
c.detail_plan
.as_ref()
.is_some_and(|e| e.content.detail_of(node).is_some())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ids::NodeId;
use crate::siteplan::{Ground, Owner, PlacedBox, Roof, Site};
fn a_box() -> PlacedBox {
PlacedBox {
node: NodeId("node/hall".into()),
foot: [10, 25, 4, 19],
floor: 64,
clearance: 8,
open: false,
base: crate::siteplan::Base::Ground,
roof: None,
}
}
fn frame(b: &PlacedBox, ground: &Ground) -> Frame {
let boxes = vec![b.clone()];
let site = Site::new(&boxes, &[], ground);
Frame::of(&site, 0)
}
#[test]
fn a_frame_is_the_shell_when_nothing_stands_beside_it() {
let b = a_box();
let f = frame(&b, &Ground::solid("minecraft:stone"));
assert_eq!(f.lo, [9, 63, 3], "the floor course and the ring");
assert_eq!(f.hi, [26, 72, 20], "the ring and the lid");
assert_eq!(f.extent(), [18, 10, 18]);
assert_eq!(f.datum_y(), 1, "the walk plane sits one course up");
assert!(f.owns([9, 64, 4]), "the wall is the piece's");
assert!(!f.owns([9, 63, 4]), "the ring's ground is fixed");
assert!(f.is_fixed([9, 63, 4]));
assert!(!f.contains([8, 64, 4]), "and nothing beyond it is");
assert_eq!(f.owned_cells, 18 * 10 * 18 - 68);
assert_eq!(f.fixed.len(), 68);
assert!(f.voids.iter().all(|v| v.owner == Owner::Ground));
}
#[test]
fn an_open_frame_has_no_lid_and_a_roofed_one_rises_by_its_roof() {
let mut open = a_box();
open.open = true;
open.clearance = 3;
let f = frame(&open, &Ground::solid("minecraft:stone"));
assert_eq!(f.extent(), [18, 4, 18], "floor course + 3, no lid");
let mut roofed = a_box();
roofed.roof = Some(Roof {
courses: 4,
eaves: 1,
});
let f = frame(&roofed, &Ground::solid("minecraft:stone"));
assert_eq!(
f.extent(),
[20, 14, 20],
"eaves 1 each side; floor + 8 + lid + 4"
);
assert_eq!(f.lo, [8, 63, 2]);
assert_eq!(f.hi, [27, 76, 21]);
assert_eq!(f.datum_y(), 1);
assert!(f.voids.iter().any(|v| v.owner == Owner::Nobody));
}
#[test]
fn a_plinth_over_low_ground_claims_down_to_the_ground() {
let b = a_box();
let f = frame(
&b,
&Ground::open_flat(60, "minecraft:grass_block", "minecraft:dirt"),
);
assert_eq!(f.bottom, 60);
assert_eq!(f.lo[1], 60);
assert_eq!(f.datum_y(), 4);
assert!(f.is_fixed([9, 60, 4]), "the ring's surface cell is fixed");
assert!(
f.owns([9, 61, 4]),
"above the ground the ring is the piece's"
);
assert!(
f.owns([12, 61, 6]),
"the ground under the plot is the piece's"
);
let fixed: Vec<&String> = f.fixed.iter().map(|(_, b)| b).collect();
assert!(fixed.iter().all(|b| b.as_str() == "minecraft:grass_block"));
}
#[test]
fn a_detail_row_cannot_state_where_anything_goes() {
for extra in [
r#""min": [0, 0, 0]"#,
r#""at": [1, 2]"#,
r#""region": {"min": [0, 0, 0], "extent": [1, 1, 1]}"#,
r#""datum": "datum/grade""#,
r#""offset": [0, 1, 0]"#,
r#""extent": [4, 4, 4]"#,
r#""seams": []"#,
] {
let src = format!(
r#"{{"place": "node/hall", "piece": "prefab/hall", "anchors": {{}}, {extra}}}"#
);
let err = serde_json::from_str::<Detail>(&src)
.expect_err("a detail row has no spelling for where anything goes");
assert!(
err.to_string().contains("unknown field"),
"the refusal is the schema's, not a check's: {err}"
);
}
}
#[test]
fn a_detail_plan_cannot_state_where_anything_goes() {
for extra in [
r#""region": {"min": [0, 0, 0], "extent": [1, 1, 1]}"#,
r#""datums": []"#,
r#""boxes": []"#,
r#""seams": []"#,
r#""origin": [0, 0, 0]"#,
] {
let src = format!(r#"{{"details": [], {extra}}}"#);
let err = serde_json::from_str::<DetailPlanContent>(&src)
.expect_err("a detail plan has no spelling for geometry");
assert!(
err.to_string().contains("unknown field"),
"the refusal is the schema's, not a check's: {err}"
);
}
}
#[test]
fn the_document_round_trips_and_defaults_to_nothing_bound() {
let d: DetailPlanContent = serde_json::from_str(r#"{"details": []}"#).unwrap();
assert!(d.palette.is_none(), "absent is not empty");
assert!(d.details.is_empty());
let d: DetailPlanContent = serde_json::from_str(
r#"{"palette": {"role/wall": "minecraft:stone_bricks"},
"details": [{"place": "node/hall", "piece": "prefab/hall"}]}"#,
)
.unwrap();
assert_eq!(d.details[0].anchors.len(), 0, "`anchors` defaults to empty");
assert!(d.detail_of(&NodeId("node/hall".into())).is_some());
assert!(d.detail_of(&NodeId("node/annex".into())).is_none());
}
}