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;
#[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)
}
}
pub const FLOOR_COURSE: i64 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
pub node: NodeId,
pub lo: [i64; 3],
pub hi: [i64; 3],
}
impl Frame {
#[must_use]
pub fn of(b: &PlacedBox) -> Frame {
let (lo, hi) = b.space();
Frame {
node: b.node.clone(),
lo: [lo[0], lo[1] - FLOOR_COURSE, lo[2]],
hi,
}
}
#[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 {
FLOOR_COURSE
}
#[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 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::PlacedBox;
fn a_box() -> PlacedBox {
PlacedBox {
node: NodeId("node/hall".into()),
foot: [10, 25, 4, 19],
floor: 64,
clearance: 8,
open: false,
}
}
#[test]
fn a_frame_is_the_play_space_plus_one_course_below() {
let b = a_box();
let (lo, hi) = b.space();
let f = Frame::of(&b);
assert_eq!(
f.lo,
[lo[0], lo[1] - 1, lo[2]],
"one course below the walk plane"
);
assert_eq!(f.hi, hi, "and nothing above the play space");
assert_eq!(
f.extent(),
[16, 9, 16],
"footprint by clearance + the floor course"
);
assert_eq!(
f.datum_y(),
1,
"the walk plane sits one course up, piece-local"
);
assert_eq!(f.to_local([lo[0], b.floor, lo[2]]), [0, 1, 0]);
assert!(
f.contains([lo[0], b.floor - 1, lo[2]]),
"the floor course is the piece's"
);
assert!(
!f.contains([lo[0], b.floor - 2, lo[2]]),
"and nothing under it is"
);
}
#[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());
}
}