city-layout
A city's plan from a grid and a seed: where the roads run, the blocks between them, and the lots in each block that a building stands on. It knows cells, never metres or models, so the same plan can be dressed with any kit on any grid. Pure and seeded: the same options give the same city.
Demo
import { cityLayout } from 'tosijs-3d'
import { elements, svgElements } from 'tosijs'
const { svg, rect } = svgElements
const { button, div } = elements
const CELL = 14
let seed = 1
function draw() {
const city = cityLayout({ width: 48, depth: 30, seed })
const flip = (z, d = 1) => (city.depth - z - d) * CELL // north is up
const cells = (r, fill) =>
rect({ x: r.x * CELL, y: flip(r.z, r.depth), width: r.width * CELL, height: r.depth * CELL, fill, stroke: '#0004' })
return svg(
{ viewBox: `0 0 ${city.width * CELL} ${city.depth * CELL}`, style: 'width: 100%; height: auto; background: #444' },
...city.roads.map((r) => rect({ x: r.x * CELL, y: flip(r.z), width: CELL, height: CELL, fill: '#555' })),
...city.yards.map((y) => cells(y, '#6a9a5b')),
...city.lots.map((l, i) => cells(l, `hsl(${(i * 47) % 360} 45% ${l.corner ? 72 : 62}%)`)),
// A tick on each lot's street side.
...city.lots.map((l) => {
const mid = { x: (l.x + l.width / 2) * CELL, y: flip(l.z, l.depth) + (l.depth * CELL) / 2 }
const reach = { n: [0, -1], s: [0, 1], e: [1, 0], w: [-1, 0] }[l.front]
const half = { x: (l.width * CELL) / 2, y: (l.depth * CELL) / 2 }
return svgElements.line({ x1: mid.x, y1: mid.y, x2: mid.x + reach[0] * half.x, y2: mid.y + reach[1] * half.y, stroke: '#0006' })
})
)
}
const map = div(draw())
preview.append(
map,
button({ onClick() { seed += 1; map.replaceChildren(draw()) } }, 'another city')
)
Grey is road, each coloured patch a lot (corner lots lighter), green the yards left in the middle of big blocks; the tick on each lot points at its street.
const city = cityLayout({ width: 24, depth: 24, seed: 7 })
city.roads // [{ x, z, piece: 'tee', turns: 1, mask: 7 }, …]
city.blocks // [{ x, z, width, depth }, …]
city.lots // [{ x, z, width, depth, front: 'n', corner: false, block: 3 }, …]
How it is made
A city is a hierarchy, so it is made top down rather than tile by tile. The
whole area is ringed by a road, then split by a road across it, and each side
split again, until every piece is no bigger than blockMax cells. Each split
road runs from one existing road to another, so every road is connected
by construction (and the tests check it). What is left between the roads are
the blocks.
Each block is then cut into lots along its edges: a corner lot at each
corner, and lots of lotWidth cells along each side, lotDepth deep, so
every lot faces a road. Whatever is left in the middle of a big block is
its yards (a courtyard, a park, a car park).
Road pieces
Each road cell gets its piece from which of its four neighbours are road:
end (one), straight (two opposite), bend (two adjacent), tee
(three), cross (four), or single (none). turns is how many quarter
turns, clockwise seen from above (north to east), take the piece from its
canonical orientation to this one:
| piece | canonical: connects |
|---|---|
end |
north |
straight |
north and south |
bend |
north and east |
tee |
north, east and south |
cross |
all four |
North is +Z and east is +X, cell (0, 0) at the south-west corner. Which way a kit's model faces is the dressing's business: its manifest says how to turn each piece to this canonical orientation.
Options
| Option | Default | Meaning |
|---|---|---|
width, depth |
The grid, cells (x and z) | |
seed |
1 |
Same seed, same city |
blockMin |
3 |
A split never leaves a block narrower than this, cells |
blockMax |
6 |
Blocks are split until no wider than this, cells |
lotWidth |
[1, 2] |
Lots along a street, cells wide (a range) |
lotDepth |
2 |
How deep a lot runs back from the street, cells (less in a narrow block) |
ring |
true |
A road round the edge of the whole grid (without it, lots on the outer edge face the edge, not a street) |