feat(citygen): lots and building placement
Also makes block fitting recursive so irregular district regions split into several clean blocks instead of being dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
313
crates/citygen/src/generate/lots.rs
Normal file
313
crates/citygen/src/generate/lots.rs
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
//! Stage 5: lots and building placement.
|
||||||
|
|
||||||
|
use super::{GenCtx, Lot, LotUse, Stage, StageRunner, StepOut};
|
||||||
|
use crate::geom::{Pos, Rect};
|
||||||
|
use crate::ids::BuildingId;
|
||||||
|
use crate::params::GenError;
|
||||||
|
use crate::rng::{chance, pick_weighted};
|
||||||
|
use crate::theme::{StreetStyle, ThemeSpec};
|
||||||
|
use crate::tile::Terrain;
|
||||||
|
use crate::world::{Building, BuildingKind, HeightClass, StrataSpan, StratumKind};
|
||||||
|
use rand::Rng;
|
||||||
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct LotsStage {
|
||||||
|
next: usize,
|
||||||
|
filled: bool,
|
||||||
|
rng: Option<ChaCha8Rng>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Splits a block into lots no smaller than `min` and no larger than twice that.
|
||||||
|
fn bsp(rng: &mut ChaCha8Rng, rect: Rect, min: (i32, i32), out: &mut Vec<Rect>) {
|
||||||
|
let (min_w, min_h) = min;
|
||||||
|
let split_w = rect.w > 2 * min_w;
|
||||||
|
let split_h = rect.h > 2 * min_h;
|
||||||
|
if !split_w && !split_h {
|
||||||
|
out.push(rect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let vertical = if split_w && split_h {
|
||||||
|
rect.w as f32 / min_w as f32 >= rect.h as f32 / min_h as f32
|
||||||
|
} else {
|
||||||
|
split_w
|
||||||
|
};
|
||||||
|
let jitter: f32 = rng.random_range(0.4..0.6);
|
||||||
|
if vertical {
|
||||||
|
let s =
|
||||||
|
(rect.x + (rect.w as f32 * jitter) as i32).clamp(rect.x + min_w, rect.right() - min_w);
|
||||||
|
bsp(rng, Rect::new(rect.x, rect.y, s - rect.x, rect.h), min, out);
|
||||||
|
bsp(
|
||||||
|
rng,
|
||||||
|
Rect::new(s, rect.y, rect.right() - s, rect.h),
|
||||||
|
min,
|
||||||
|
out,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let s =
|
||||||
|
(rect.y + (rect.h as f32 * jitter) as i32).clamp(rect.y + min_h, rect.bottom() - min_h);
|
||||||
|
bsp(rng, Rect::new(rect.x, rect.y, rect.w, s - rect.y), min, out);
|
||||||
|
bsp(
|
||||||
|
rng,
|
||||||
|
Rect::new(rect.x, s, rect.w, rect.bottom() - s),
|
||||||
|
min,
|
||||||
|
out,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_class(rng: &mut ChaCha8Rng, kind: BuildingKind, spec: &ThemeSpec) -> HeightClass {
|
||||||
|
let allowed = kind.height_classes();
|
||||||
|
if allowed.contains(&HeightClass::Tall) && chance(rng, spec.tall) {
|
||||||
|
HeightClass::Tall
|
||||||
|
} else if allowed.contains(&HeightClass::Mid) && chance(rng, spec.mid) {
|
||||||
|
HeightClass::Mid
|
||||||
|
} else if allowed.contains(&HeightClass::Low) {
|
||||||
|
HeightClass::Low
|
||||||
|
} else {
|
||||||
|
allowed[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LotsStage {
|
||||||
|
fn place_block(&mut self, ctx: &mut GenCtx, idx: usize) {
|
||||||
|
let block = ctx.scratch.blocks[idx];
|
||||||
|
let district = ctx.city.district(block.district).clone();
|
||||||
|
let spec = district.theme.spec();
|
||||||
|
let density = ctx.params().density;
|
||||||
|
let rng = self.rng.as_mut().expect("begin sets rng");
|
||||||
|
|
||||||
|
let mut lots = Vec::new();
|
||||||
|
bsp(rng, block.rect, spec.min_lot, &mut lots);
|
||||||
|
for lot in lots {
|
||||||
|
if lot.w < 4 || lot.h < 3 {
|
||||||
|
ctx.stratum_fill(StratumKind::Street, lot, Terrain::Plaza);
|
||||||
|
ctx.scratch.lots.push(Lot {
|
||||||
|
rect: lot,
|
||||||
|
district: district.id,
|
||||||
|
use_: LotUse::Plaza,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let plaza_p = spec.plaza * (1.3 - density);
|
||||||
|
if chance(rng, plaza_p) {
|
||||||
|
let parking = matches!(
|
||||||
|
district.theme,
|
||||||
|
crate::world::Theme::CorpCore | crate::world::Theme::Industrial
|
||||||
|
) && rng.random_bool(0.5);
|
||||||
|
ctx.stratum_fill(StratumKind::Street, lot, Terrain::Plaza);
|
||||||
|
ctx.scratch.lots.push(Lot {
|
||||||
|
rect: lot,
|
||||||
|
district: district.id,
|
||||||
|
use_: if parking {
|
||||||
|
LotUse::Parking
|
||||||
|
} else {
|
||||||
|
LotUse::Plaza
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let kind = *pick_weighted(rng, spec.mix);
|
||||||
|
let class = choose_class(rng, kind, spec);
|
||||||
|
let basement = chance(rng, spec.basement);
|
||||||
|
let mut footprint = lot;
|
||||||
|
if matches!(spec.street_style, StreetStyle::Organic | StreetStyle::Strip)
|
||||||
|
&& chance(rng, 0.3)
|
||||||
|
{
|
||||||
|
footprint = match rng.random_range(0..4) {
|
||||||
|
0 if lot.w > 4 => Rect::new(lot.x + 1, lot.y, lot.w - 1, lot.h),
|
||||||
|
1 if lot.w > 4 => Rect::new(lot.x, lot.y, lot.w - 1, lot.h),
|
||||||
|
2 if lot.h > 3 => Rect::new(lot.x, lot.y + 1, lot.w, lot.h - 1),
|
||||||
|
3 if lot.h > 3 => Rect::new(lot.x, lot.y, lot.w, lot.h - 1),
|
||||||
|
_ => lot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let id = BuildingId(ctx.city.buildings.len() as u32);
|
||||||
|
let span = StrataSpan::from_class(class, basement);
|
||||||
|
for p in footprint.iter() {
|
||||||
|
let on_border = p.x == footprint.x
|
||||||
|
|| p.y == footprint.y
|
||||||
|
|| p.x == footprint.right() - 1
|
||||||
|
|| p.y == footprint.bottom() - 1;
|
||||||
|
ctx.set_terrain(
|
||||||
|
StratumKind::Street,
|
||||||
|
p,
|
||||||
|
if on_border {
|
||||||
|
Terrain::Wall
|
||||||
|
} else {
|
||||||
|
Terrain::Floor
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for k in span.iter() {
|
||||||
|
if let Some(t) = ctx.city.tile_mut(k, p) {
|
||||||
|
t.building = Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.city.buildings.push(Building {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
district: district.id,
|
||||||
|
footprint,
|
||||||
|
span,
|
||||||
|
name: None,
|
||||||
|
});
|
||||||
|
ctx.scratch.lots.push(Lot {
|
||||||
|
rect: lot,
|
||||||
|
district: district.id,
|
||||||
|
use_: LotUse::Building {
|
||||||
|
kind,
|
||||||
|
class,
|
||||||
|
basement,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Any zoned land still untouched becomes a back alley so nothing is unreachable void.
|
||||||
|
fn fill_leftovers(ctx: &mut GenCtx) {
|
||||||
|
let size = ctx.size();
|
||||||
|
for y in 0..size.h as i32 {
|
||||||
|
for x in 0..size.w as i32 {
|
||||||
|
let p = Pos::new(x, y);
|
||||||
|
let Some(t) = ctx.city.tile(StratumKind::Street, p) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if t.district.is_some() && t.terrain == Terrain::Void {
|
||||||
|
ctx.set_terrain(StratumKind::Street, p, Terrain::Alley);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenCtx {
|
||||||
|
/// Fills a rect with `terrain` on one stratum (no district check).
|
||||||
|
pub fn stratum_fill(&mut self, k: StratumKind, r: Rect, terrain: Terrain) {
|
||||||
|
for p in r.iter() {
|
||||||
|
self.set_terrain(k, p, terrain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StageRunner for LotsStage {
|
||||||
|
fn begin(&mut self, ctx: &mut GenCtx) -> Result<(), GenError> {
|
||||||
|
self.rng = Some(ctx.rng(Stage::Lots));
|
||||||
|
self.next = 0;
|
||||||
|
self.filled = false;
|
||||||
|
ctx.scratch.lots.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(&mut self, ctx: &mut GenCtx) -> Result<StepOut, GenError> {
|
||||||
|
let n = ctx.scratch.blocks.len();
|
||||||
|
if self.next < n {
|
||||||
|
self.place_block(ctx, self.next);
|
||||||
|
self.next += 1;
|
||||||
|
return Ok(StepOut::Continue(0.95 * self.next as f32 / n as f32));
|
||||||
|
}
|
||||||
|
if !self.filled {
|
||||||
|
Self::fill_leftovers(ctx);
|
||||||
|
self.filled = true;
|
||||||
|
return Ok(StepOut::Continue(1.0));
|
||||||
|
}
|
||||||
|
Ok(StepOut::Finished)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::generate::Generator;
|
||||||
|
use crate::generate::testutil::small;
|
||||||
|
use crate::params::{GenParams, SizePreset, WaterMode};
|
||||||
|
use crate::tile::Terrain;
|
||||||
|
use crate::world::{StratumKind, Theme};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn buildings_fit_blocks_and_do_not_overlap() {
|
||||||
|
let c = small(41, WaterMode::Bay, 7);
|
||||||
|
assert!(
|
||||||
|
c.buildings.len() > 40,
|
||||||
|
"only {} buildings",
|
||||||
|
c.buildings.len()
|
||||||
|
);
|
||||||
|
let st = c.stratum(StratumKind::Street).unwrap();
|
||||||
|
for (i, a) in c.buildings.iter().enumerate() {
|
||||||
|
assert_eq!(a.id.index(), i);
|
||||||
|
assert!(
|
||||||
|
a.footprint.w >= 4 && a.footprint.h >= 3,
|
||||||
|
"{:?}",
|
||||||
|
a.footprint
|
||||||
|
);
|
||||||
|
for b in &c.buildings[i + 1..] {
|
||||||
|
assert!(
|
||||||
|
!a.footprint.intersects(b.footprint),
|
||||||
|
"{:?} overlaps {:?}",
|
||||||
|
a.id,
|
||||||
|
b.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for p in a.footprint.border() {
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
st.tiles[p].terrain,
|
||||||
|
Terrain::Wall | Terrain::Door | Terrain::Window
|
||||||
|
),
|
||||||
|
"{p:?} is {:?}",
|
||||||
|
st.tiles[p].terrain
|
||||||
|
);
|
||||||
|
assert_eq!(st.tiles[p].building, Some(a.id));
|
||||||
|
}
|
||||||
|
for p in a.footprint.inset(1).iter() {
|
||||||
|
assert!(st.tiles[p].terrain != Terrain::Void && !st.tiles[p].is_outdoor_ground());
|
||||||
|
}
|
||||||
|
assert_eq!(st.tiles[a.footprint.center()].district, Some(a.district));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn corp_core_has_tall_buildings_and_no_void_left_on_land() {
|
||||||
|
let c = small(41, WaterMode::None, 7);
|
||||||
|
let core = c
|
||||||
|
.districts
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.theme == Theme::CorpCore)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
assert!(
|
||||||
|
c.buildings
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.district == core && b.span.hi == StratumKind::Heights)
|
||||||
|
);
|
||||||
|
let st = c.stratum(StratumKind::Street).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
st.tiles
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, t)| t.terrain == Terrain::Void && t.district.is_some())
|
||||||
|
.count(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn density_changes_plaza_share() {
|
||||||
|
let run = |seed, density| {
|
||||||
|
Generator::run(GenParams {
|
||||||
|
seed,
|
||||||
|
density,
|
||||||
|
size: SizePreset::Custom { w: 120, h: 60 },
|
||||||
|
districts: 5,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.buildings
|
||||||
|
.len()
|
||||||
|
};
|
||||||
|
let lo: usize = (4..8).map(|s| run(s, 0.2)).sum();
|
||||||
|
let hi: usize = (4..8).map(|s| run(s, 0.95)).sum();
|
||||||
|
assert!(
|
||||||
|
lo < hi,
|
||||||
|
"low density {lo} should yield fewer buildings than high {hi}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pub mod arterials;
|
pub mod arterials;
|
||||||
pub mod districts;
|
pub mod districts;
|
||||||
|
pub mod lots;
|
||||||
pub mod streets;
|
pub mod streets;
|
||||||
pub mod terrain;
|
pub mod terrain;
|
||||||
|
|
||||||
@@ -227,6 +228,7 @@ pub(crate) fn runner_for(stage: Stage) -> Box<dyn StageRunner> {
|
|||||||
Stage::Districts => Box::new(districts::DistrictsStage::default()),
|
Stage::Districts => Box::new(districts::DistrictsStage::default()),
|
||||||
Stage::Arterials => Box::new(arterials::ArterialsStage::default()),
|
Stage::Arterials => Box::new(arterials::ArterialsStage::default()),
|
||||||
Stage::Streets => Box::new(streets::StreetsStage::default()),
|
Stage::Streets => Box::new(streets::StreetsStage::default()),
|
||||||
|
Stage::Lots => Box::new(lots::LotsStage::default()),
|
||||||
_ => Box::new(NoopStage),
|
_ => Box::new(NoopStage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,40 +284,54 @@ impl Carver<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shrinks a candidate until every cell is untouched land of this district.
|
fn is_bad(&self, p: Pos) -> bool {
|
||||||
fn fit_block(&self, mut r: Rect) -> Option<Rect> {
|
!self.ctx.is_land(p)
|
||||||
let bad = |ctx: &GenCtx, p: Pos| {
|
|| !self.ctx.in_district(p, self.d)
|
||||||
!ctx.is_land(p)
|
|| self.ctx.terrain(StratumKind::Street, p) != Terrain::Void
|
||||||
|| !ctx.in_district(p, self.d)
|
}
|
||||||
|| ctx.terrain(StratumKind::Street, p) != Terrain::Void
|
|
||||||
};
|
/// Drops edge rows/columns that are entirely unusable.
|
||||||
|
fn trim(&self, mut r: Rect) -> Rect {
|
||||||
|
loop {
|
||||||
|
if r.w < 1 || r.h < 1 {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (r.x..r.right()).all(|x| self.is_bad(Pos::new(x, r.y))) {
|
||||||
|
r = Rect::new(r.x, r.y + 1, r.w, r.h - 1);
|
||||||
|
} else if (r.x..r.right()).all(|x| self.is_bad(Pos::new(x, r.bottom() - 1))) {
|
||||||
|
r = Rect::new(r.x, r.y, r.w, r.h - 1);
|
||||||
|
} else if (r.y..r.bottom()).all(|y| self.is_bad(Pos::new(r.x, y))) {
|
||||||
|
r = Rect::new(r.x + 1, r.y, r.w - 1, r.h);
|
||||||
|
} else if (r.y..r.bottom()).all(|y| self.is_bad(Pos::new(r.right() - 1, y))) {
|
||||||
|
r = Rect::new(r.x, r.y, r.w - 1, r.h);
|
||||||
|
} else {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Greedy shrink from the dirtiest side until clean or too small.
|
||||||
|
fn shrink(&self, mut r: Rect) -> Option<Rect> {
|
||||||
for _ in 0..12 {
|
for _ in 0..12 {
|
||||||
if r.w < 5 || r.h < 4 {
|
if r.w < 5 || r.h < 4 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let top = (r.x..r.right())
|
let top = (r.x..r.right())
|
||||||
.filter(|x| bad(self.ctx, Pos::new(*x, r.y)))
|
.filter(|x| self.is_bad(Pos::new(*x, r.y)))
|
||||||
.count();
|
.count();
|
||||||
let bottom = (r.x..r.right())
|
let bottom = (r.x..r.right())
|
||||||
.filter(|x| bad(self.ctx, Pos::new(*x, r.bottom() - 1)))
|
.filter(|x| self.is_bad(Pos::new(*x, r.bottom() - 1)))
|
||||||
.count();
|
.count();
|
||||||
let left = (r.y..r.bottom())
|
let left = (r.y..r.bottom())
|
||||||
.filter(|y| bad(self.ctx, Pos::new(r.x, *y)))
|
.filter(|y| self.is_bad(Pos::new(r.x, *y)))
|
||||||
.count();
|
.count();
|
||||||
let right = (r.y..r.bottom())
|
let right = (r.y..r.bottom())
|
||||||
.filter(|y| bad(self.ctx, Pos::new(r.right() - 1, *y)))
|
.filter(|y| self.is_bad(Pos::new(r.right() - 1, *y)))
|
||||||
.count();
|
.count();
|
||||||
if top + bottom + left + right == 0 {
|
if top + bottom + left + right == 0 {
|
||||||
let interior_bad = r.inset(1).iter().any(|p| bad(self.ctx, p));
|
match r.inset(1).iter().find(|p| self.is_bad(*p)) {
|
||||||
if !interior_bad {
|
None => return Some(r),
|
||||||
return Some(r);
|
Some(p) => {
|
||||||
}
|
|
||||||
// An interior obstacle (arterial crossing); shrink the side nearest to it.
|
|
||||||
let p = r
|
|
||||||
.inset(1)
|
|
||||||
.iter()
|
|
||||||
.find(|p| bad(self.ctx, *p))
|
|
||||||
.expect("interior_bad");
|
|
||||||
if (p.x - r.x) * r.h < (p.y - r.y) * r.w {
|
if (p.x - r.x) * r.h < (p.y - r.y) * r.w {
|
||||||
r = Rect::new(p.x + 1, r.y, r.right() - p.x - 1, r.h);
|
r = Rect::new(p.x + 1, r.y, r.right() - p.x - 1, r.h);
|
||||||
} else {
|
} else {
|
||||||
@@ -325,16 +339,16 @@ impl Carver<'_> {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let top_f = top as f32 / r.w as f32;
|
}
|
||||||
let bottom_f = bottom as f32 / r.w as f32;
|
}
|
||||||
let left_f = left as f32 / r.h as f32;
|
let fr = |n: usize, len: i32| n as f32 / len as f32;
|
||||||
let right_f = right as f32 / r.h as f32;
|
let (t, b, l, rr) = (fr(top, r.w), fr(bottom, r.w), fr(left, r.h), fr(right, r.h));
|
||||||
let max = top_f.max(bottom_f).max(left_f).max(right_f);
|
let max = t.max(b).max(l).max(rr);
|
||||||
if max == top_f {
|
if max == t {
|
||||||
r = Rect::new(r.x, r.y + 1, r.w, r.h - 1);
|
r = Rect::new(r.x, r.y + 1, r.w, r.h - 1);
|
||||||
} else if max == bottom_f {
|
} else if max == b {
|
||||||
r = Rect::new(r.x, r.y, r.w, r.h - 1);
|
r = Rect::new(r.x, r.y, r.w, r.h - 1);
|
||||||
} else if max == left_f {
|
} else if max == l {
|
||||||
r = Rect::new(r.x + 1, r.y, r.w - 1, r.h);
|
r = Rect::new(r.x + 1, r.y, r.w - 1, r.h);
|
||||||
} else {
|
} else {
|
||||||
r = Rect::new(r.x, r.y, r.w - 1, r.h);
|
r = Rect::new(r.x, r.y, r.w - 1, r.h);
|
||||||
@@ -342,6 +356,51 @@ impl Carver<'_> {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Turns one candidate into clean blocks: trims dead edges, and when the rest is still
|
||||||
|
/// dirty, splits it along the longer axis (painting an alley on the cut) and recurses.
|
||||||
|
fn fit_blocks(&mut self, r: Rect, depth: u8, out: &mut Vec<Rect>) {
|
||||||
|
let r = self.trim(r);
|
||||||
|
if r.w < 5 || r.h < 4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !r.iter().any(|p| self.is_bad(p)) {
|
||||||
|
out.push(r);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let can_split_w = r.w >= 11;
|
||||||
|
let can_split_h = r.h >= 9;
|
||||||
|
if depth > 0 && (can_split_w || can_split_h) {
|
||||||
|
let vertical = if can_split_w && can_split_h {
|
||||||
|
r.w >= r.h
|
||||||
|
} else {
|
||||||
|
can_split_w
|
||||||
|
};
|
||||||
|
if vertical {
|
||||||
|
let s = r.x + r.w / 2;
|
||||||
|
paint_vline(self.ctx, s, r.y, r.bottom(), Line::ALLEY, self.d);
|
||||||
|
self.fit_blocks(Rect::new(r.x, r.y, s - r.x, r.h), depth - 1, out);
|
||||||
|
self.fit_blocks(
|
||||||
|
Rect::new(s + 1, r.y, r.right() - s - 1, r.h),
|
||||||
|
depth - 1,
|
||||||
|
out,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let s = r.y + r.h / 2;
|
||||||
|
paint_hline(self.ctx, s, r.x, r.right(), Line::ALLEY, self.d);
|
||||||
|
self.fit_blocks(Rect::new(r.x, r.y, r.w, s - r.y), depth - 1, out);
|
||||||
|
self.fit_blocks(
|
||||||
|
Rect::new(r.x, s + 1, r.w, r.bottom() - s - 1),
|
||||||
|
depth - 1,
|
||||||
|
out,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(fitted) = self.shrink(r) {
|
||||||
|
out.push(fitted);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreetsStage {
|
impl StreetsStage {
|
||||||
@@ -373,16 +432,18 @@ impl StreetsStage {
|
|||||||
StreetStyle::Strip => carver.strip(),
|
StreetStyle::Strip => carver.strip(),
|
||||||
}
|
}
|
||||||
let candidates = std::mem::take(&mut carver.candidates);
|
let candidates = std::mem::take(&mut carver.candidates);
|
||||||
let mut blocks: Vec<Block> = Vec::new();
|
let mut fitted = Vec::new();
|
||||||
for c in candidates {
|
for c in candidates {
|
||||||
let Some(c) = c.clamp_to(carver.bounds) else {
|
let Some(c) = c.clamp_to(carver.bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(fitted) = carver.fit_block(c)
|
carver.fit_blocks(c, 4, &mut fitted);
|
||||||
&& blocks.iter().all(|b| !b.rect.intersects(fitted))
|
}
|
||||||
{
|
let mut blocks: Vec<Block> = Vec::new();
|
||||||
|
for rect in fitted {
|
||||||
|
if blocks.iter().all(|b| !b.rect.intersects(rect)) {
|
||||||
blocks.push(Block {
|
blocks.push(Block {
|
||||||
rect: fitted,
|
rect,
|
||||||
district: district.id,
|
district: district.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user