/** * City content synthesis: the zone/class/massing layer of the `city` studio. Where `cityKind` * synthesizes the street NETWORK, this module decides what stands on it — which zone band a lot * falls in (core/mid/edge, invertible), which building class the band's weighted mix picks * (tower, slab, shop, rowhouse, house, mansion, farmhouse, barn, silo), and the deterministic * massing PIECES each class composes (boxes, gable prisms, cylinders, domes with palette roles). * Pure data + seeded math — the shell renderer just instances the piece shapes, so every visual * variant is core-owned, testable, and identical per seed. * * @capability city-district zone bands, weighted building-class mixes, and per-class massing pieces */ import type { WeightedParamEntry } from "../scene/sceneKinds.js"; /** A building class a zone mix can weight — drives lot size, floors, setback, and massing. */ export type CityLotClass = "tower" | "slab" | "shop" | "rowhouse" | "house" | "mansion" | "farmhouse" | "barn" | "silo"; /** All classes, for schema hints and validation. */ export declare const CITY_LOT_CLASSES: readonly CityLotClass[]; /** * Filler classes used ONLY by the block-interior fill pass (`resolveCityLotContent` at a high * `blockFill`). They are deliberately kept OUT of {@link CITY_LOT_CLASSES} so a zone mix can never * weight them onto street frontage — they exist to pack the backs of Manhattan-style blocks with * parking structures and warehouses/depots, mixed in with ordinary zone classes. */ export type CityFillerClass = "garage" | "depot"; /** All filler classes, for validation and interior-pass allow-lists. */ export declare const CITY_FILLER_CLASSES: readonly CityFillerClass[]; /** Zone band a lot falls in: dense core, middle ring, or the district edge. */ export type CityZoneBand = "core" | "mid" | "edge"; /** How the radial zone metric maps to bands. */ export type CityZoneProfile = "core-out" | "inverted" | "uniform"; /** Primitive shapes massing pieces instance — the renderer keeps one InstancedMesh per shape. */ export type CityPieceShape = "box" | "gable" | "cylinder" | "dome"; /** Palette role a piece colors from (wall/roof/trim/accent map onto the district's style palette). */ export type CityPieceRole = "wall" | "roof" | "trim" | "accent"; /** One massing piece in LOT-LOCAL space: x along frontage width, z into the block, y up from grade. */ export interface CityLotPiece { shape: CityPieceShape; /** Piece-center offset from the lot center; y is the piece BASE height above grade. */ offset: readonly [number, number, number]; /** Full extents: width (x), height (y), depth (z). */ size: readonly [number, number, number]; /** Extra local yaw (crossing gables); 0 keeps the ridge along the lot width. */ rotationY: number; role: CityPieceRole; /** Grounded pieces grow a foundation down to the lowest terrain corner under the lot. */ grounded: boolean; /** Banded walls receive the window-strip treatment in the renderer. */ banded: boolean; } /** * Natural frontage-width range (meters) of a class's massing — what the plot-size class bias reads * so towers land on wide plots and rowhouses on narrow ones. @internal */ export declare function classWidthRange(cls: string): readonly [number, number] | undefined; /** Placement numbers a resolved lot carries out of the class profile. @internal */ export interface ClassPlacement { width: number; depth: number; setback: number; gap: number; floors: number; backRow: boolean; } /** * Roll a class's lot dimensions/setback/floors from the district's placement dials. Setback keeps * only a whisper of jitter around `setbackBase × class factor`, so a street's frontage reads as an * aligned building wall; spacing likewise, so neighbors form rhythmic rows instead of scatter. * @internal */ export declare function rollClassPlacement(cls: CityLotClass | CityFillerClass, rng: () => number, lotScale: number, floorsMin: number, floorsMax: number, setbackBase: number, spacingBase: number, widthStretch?: number): ClassPlacement; /** * Weight multiplier favouring classes whose natural frontage fits a plot of width `plotW`: a class * far wider than the plot (a tower profile on a 6 m slice) is heavily downweighted, a class that * would rattle around a much wider plot is mildly downweighted, and anything whose width range * genuinely fits keeps weight 1. Pure data over {@link CLASS_PROFILES} — this is what turns a * mixed-plot frontage into terraces on the slices and detached houses on the wide parcels. * @internal */ export declare function classPlotFit(cls: CityLotClass, plotW: number): number; /** * Normalized zone metric for a point in district-local space: 0 at the center line, 1 at the rim, * shaped to the district's rectangle (Chebyshev) so bands follow the volume's footprint. * @internal */ export declare function zoneMetric(x: number, z: number, hx: number, hz: number): number; /** Map a zone metric to a band under a profile; `uniform` rolls a band from the extents instead. @internal */ export declare function zoneBand(metric: number, profile: CityZoneProfile, coreExtent: number, midExtent: number, roll: number): CityZoneBand; /** Pick a class from a weighted mix; unknown items are ignored, an empty mix falls back to `house`. @internal */ export declare function pickClass(mix: readonly WeightedParamEntry[], roll: number): CityLotClass; /** * Compose the massing pieces for one lot. Deterministic per `rng` stream: the same class, size, * floors, and stream always yield the identical silhouette. Sizes are in meters, lot-local. * @internal */ export declare function buildLotPieces(cls: CityLotClass | CityFillerClass, width: number, depth: number, floors: number, floorHeight: number, rng: () => number): CityLotPiece[]; /** * A block-scale landmark class. Unlike {@link CityLotClass} these are never weighted into a zone * mix — the landmark pass in `resolveCityLotContent` picks clusters of adjacent lots and stamps one * of these over them. Each expresses with the same four piece shapes (box/gable/cylinder/dome) at a * much larger footprint (~40–90 m). */ export type CityLandmarkClass = "hall" | "arena" | "market" | "campus"; /** All landmark classes, for schema hints, validation, and default allow-lists. */ export declare const CITY_LANDMARK_CLASSES: readonly CityLandmarkClass[]; /** Minimum depth-into-block factor for a landmark class (the pass grows shallow clusters to this). @internal */ export declare function landmarkMinDepthFactor(cls: CityLandmarkClass): number; /** Roll a landmark's floor count from its profile range. @internal */ export declare function landmarkFloors(cls: CityLandmarkClass, rng: () => number): number; /** * Compose the massing pieces for one landmark parcel, filling the given `width`×`depth` footprint * (lot-local, x along frontage, z into the block). Deterministic per `rng`. Every piece is kept * inside the `[-width/2, width/2] × [-depth/2, depth/2]` box so the reported footprint matches the * merged cluster's AABB. Sizes are in meters. * @internal */ export declare function buildLandmarkPieces(cls: CityLandmarkClass, width: number, depth: number, floors: number, floorHeight: number, rng: () => number): CityLotPiece[]; /** Tree species a district's tree mix can weight — the renderer keeps one canopy mesh per species. */ export type CityTreeSpecies = "broadleaf" | "conifer" | "palm" | "cypress"; /** All species, for schema hints and validation. */ export declare const CITY_TREE_SPECIES: readonly CityTreeSpecies[]; /** Pick a species from a weighted mix; unknown items are ignored, an empty mix falls back to broadleaf. @internal */ export declare function pickSpecies(mix: readonly WeightedParamEntry[], roll: number): CityTreeSpecies;