use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use straight_skeleton::{
skeleton, skeleton_constrained, Panel, PanelKind, Point, Polygon, Profile, Roof, RoofVertexId,
};
const DEFAULT_PITCH: f32 = 0.6;
const MANSARD_BREAK: f32 = 12.0;
const MANSARD_LOWER: f32 = 2.2;
const MANSARD_UPPER: f32 = 0.22;
const TRUNCATE_AT: f32 = 22.0;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let mut out = PathBuf::from("target/roofs");
if let Some(i) = args.iter().position(|a| a == "--out") {
out = PathBuf::from(args.get(i + 1).ok_or("--out needs a directory")?);
}
let pitch: f32 = match args.iter().position(|a| a == "--pitch") {
Some(i) => args.get(i + 1).ok_or("--pitch needs a number")?.parse()?,
None => DEFAULT_PITCH,
};
fs::create_dir_all(&out)?;
let mansard = Profile::Mansard {
lower_pitch: MANSARD_LOWER,
break_offset: MANSARD_BREAK,
upper_pitch: MANSARD_UPPER,
};
println!(
"{:<34} {:>6} {:>8} {:>6} {:>7}",
"file", "panels", "vertices", "flats", "ridge"
);
for (name, plan, gable_walls) in plans() {
let skel = skeleton(&plan)?;
let stopped = skeleton_constrained(&plan, &vec![TRUNCATE_AT; plan.edge_count()])?;
let mut styles: Vec<(&str, Roof)> = vec![
("hip", Roof::with_profile(&skel, Profile::Hip { pitch })?),
("mansard", Roof::with_profile(&skel, mansard)?),
(
"truncated",
Roof::with_profile(&stopped, Profile::Hip { pitch })?,
),
("truncated-mansard", Roof::with_profile(&stopped, mansard)?),
];
if !gable_walls.is_empty() {
let mut limits = vec![f32::INFINITY; plan.edge_count()];
for &w in gable_walls {
limits[w as usize] = 0.0;
}
let gabled = skeleton_constrained(&plan, &limits)?;
styles.push(("gambrel", Roof::with_profile(&gabled, mansard)?));
}
for (style, roof) in styles {
let stem = format!("{name}-{style}");
let path = out.join(format!("{stem}.obj"));
fs::write(&path, to_obj(&roof, &stem))?;
println!(
"{:<34} {:>6} {:>8} {:>6} {:>7}",
format!("{stem}.obj"),
roof.panels().len(),
roof.verts().len(),
roof.flat().count(),
roof.ridge_height(),
);
}
}
println!("\nWrote OBJ files to {}", out.display());
Ok(())
}
fn footprint_area(roof: &Roof, panel: &Panel) -> f64 {
let mut a = 0.0;
for i in 0..panel.corners.len() {
let p = roof.vertex(panel.corners[i]).exact;
let q = roof
.vertex(panel.corners[(i + 1) % panel.corners.len()])
.exact;
a += p[0] as f64 * q[1] as f64 - q[0] as f64 * p[1] as f64;
}
a / 2.0
}
fn footprint_contains(roof: &Roof, panel: &Panel, p: [f32; 3]) -> bool {
let mut inside = false;
for i in 0..panel.corners.len() {
let a = roof.vertex(panel.corners[i]).exact;
let b = roof
.vertex(panel.corners[(i + 1) % panel.corners.len()])
.exact;
if (a[1] > p[1]) != (b[1] > p[1])
&& p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]
{
inside = !inside;
}
}
inside
}
fn flat_triangles(roof: &Roof) -> Vec<[RoofVertexId; 3]> {
let flats: Vec<&Panel> = roof.flat().collect();
let (outlines, holes): (Vec<&Panel>, Vec<&Panel>) =
flats.iter().partition(|p| footprint_area(roof, p) > 0.0);
let mut tris = Vec::new();
for outline in outlines {
let mine: Vec<&&Panel> = holes
.iter()
.filter(|h| footprint_contains(roof, outline, roof.vertex(h.corners[0]).exact))
.collect();
let mut coords: Vec<f64> = Vec::new();
let mut ids: Vec<RoofVertexId> = Vec::new();
let mut hole_starts: Vec<usize> = Vec::new();
let push = |panel: &Panel, coords: &mut Vec<f64>, ids: &mut Vec<RoofVertexId>| {
for &c in &panel.corners {
let e = roof.vertex(c).exact;
coords.push(e[0] as f64);
coords.push(e[1] as f64);
ids.push(c);
}
};
push(outline, &mut coords, &mut ids);
for h in &mine {
hole_starts.push(ids.len());
push(h, &mut coords, &mut ids);
}
let flat_tris = earcutr::earcut(&coords, &hole_starts, 2)
.expect("a flat is a simple polygon with simple holes, which earcut always handles");
for t in flat_tris.chunks(3) {
tris.push([ids[t[0]], ids[t[1]], ids[t[2]]]);
}
let want: f64 = footprint_area(roof, outline)
+ mine.iter().map(|h| footprint_area(roof, h)).sum::<f64>();
let got: f64 = tris
.iter()
.map(|t| {
let [a, b, c] = t.map(|v| roof.vertex(v).exact);
let ab = [(b[0] - a[0]) as f64, (b[1] - a[1]) as f64];
let ac = [(c[0] - a[0]) as f64, (c[1] - a[1]) as f64];
(ab[0] * ac[1] - ab[1] * ac[0]) / 2.0
})
.sum();
assert!(
(got - want).abs() < 1e-3 * want.abs().max(1.0),
"the triangles cover {got} but the flat is {want}"
);
}
tris
}
fn to_obj(roof: &Roof, name: &str) -> String {
let mut s = format!("# {name} - generated by the straight-skeleton crate\n");
let _ = match roof.profile() {
Profile::Hip { pitch } => writeln!(s, "# hip profile, pitch {pitch}"),
Profile::Mansard {
lower_pitch,
break_offset,
upper_pitch,
} => writeln!(
s,
"# mansard profile: pitch {lower_pitch} to a kerb at offset \
{break_offset}, then {upper_pitch}"
),
};
let flats = roof.flat().count();
if flats > 0 {
let _ = writeln!(s, "# truncated: {flats} flat loop(s) on top, triangulated");
}
let _ = writeln!(s, "o {name}");
for v in roof.verts() {
let p = v.position;
let _ = writeln!(s, "v {} {} {}", p.x, p.y, p.z);
}
let obj = |c: &RoofVertexId| (c.0 + 1).to_string();
for panel in roof.panels() {
let PanelKind::Slope { wall, band } = panel.kind else {
continue; };
let _ = if footprint_area(roof, panel).abs() < 1e-3 {
writeln!(s, "g gable_{}_band_{band}", wall.0)
} else {
writeln!(s, "g wall_{}_band_{band}", wall.0)
};
let corners = panel.corners.iter().map(obj).collect::<Vec<_>>().join(" ");
let _ = writeln!(s, "f {corners}");
}
let tris = flat_triangles(roof);
if !tris.is_empty() {
let _ = writeln!(s, "g flat");
for t in tris {
let _ = writeln!(s, "f {} {} {}", obj(&t[0]), obj(&t[1]), obj(&t[2]));
}
}
s
}
fn plans() -> Vec<(&'static str, Polygon, &'static [u16])> {
vec![
(
"simple",
Polygon::from_outer(&[
Point::new(0, 0),
Point::new(160, 0),
Point::new(160, 110),
Point::new(0, 110),
])
.unwrap(),
&[1, 3],
),
(
"pyramid",
Polygon::from_outer(&[
Point::new(0, 0),
Point::new(120, 0),
Point::new(120, 120),
Point::new(0, 120),
])
.unwrap(),
&[],
),
(
"l-shaped-house",
Polygon::from_outer(&[
Point::new(0, 0),
Point::new(220, 0),
Point::new(220, 100),
Point::new(100, 100),
Point::new(100, 210),
Point::new(0, 210),
])
.unwrap(),
&[1, 4],
),
(
"t-shaped-house",
Polygon::from_outer(&[
Point::new(0, 0), Point::new(240, 0), Point::new(240, 90),
Point::new(165, 90), Point::new(165, 200), Point::new(75, 200), Point::new(75, 90), Point::new(0, 90), ])
.unwrap(),
&[1, 4, 7],
),
(
"courtyard",
Polygon::new(
&[
Point::new(0, 0),
Point::new(260, 0),
Point::new(260, 200),
Point::new(0, 200),
],
&[vec![
Point::new(90, 70),
Point::new(170, 70),
Point::new(170, 130),
Point::new(90, 130),
]],
)
.unwrap(),
&[],
),
]
}