159 lines
6.2 KiB
TypeScript
159 lines
6.2 KiB
TypeScript
/**
|
|
* @contemporary-props/street-bollard
|
|
*
|
|
* Cast iron street bollard, and the cheapest possible proof that a profile is
|
|
* data. It shares no code with the traffic cone and every idea: one radius as
|
|
* a function of height, turned once, with the bands that read as separate
|
|
* castings being nothing but y-ranges of the same curve.
|
|
*
|
|
* Measured off the reference rather than chosen. It is 8.2 shaft diameters
|
|
* tall, which is the number that decides whether this is a bollard or a
|
|
* gatepost, and the pieces stack in fractions of that height:
|
|
*
|
|
* 0.000 base flange, 1.63 shaft diameters across
|
|
* 0.059 skirt, barely proud of the shaft
|
|
* 0.144 lower bead
|
|
* 0.185 the shaft, and it is two thirds of the whole thing
|
|
* 0.869 upper bead, the same casting as the lower one
|
|
* 0.905 dome
|
|
*
|
|
* The two beads are what make it read as cast and not as pipe. They are the
|
|
* same ring at two heights and the model draws them from one function for that
|
|
* reason: a bollard whose collars differ is a bollard somebody drew twice.
|
|
*
|
|
* The dome is slightly WIDER than the shaft, which looks wrong written down
|
|
* and is right in front of you. A cap flush with the shaft reads as a tube cut
|
|
* off and painted; the overhang is what says a separate piece was cast onto
|
|
* the top of it.
|
|
*
|
|
* One part, one slot. There is nothing here that comes off, nothing that
|
|
* lights, and nothing that moves, and the catalogue asks for no action: it is
|
|
* row seven precisely because it is the shape and nothing else.
|
|
*/
|
|
import { type BufferGeometry } from 'three'
|
|
|
|
import {
|
|
createKitModel,
|
|
createTinter,
|
|
jitter,
|
|
latheGeometry,
|
|
mergeColoured,
|
|
smoothNormals,
|
|
type Level,
|
|
} from '../core/index.ts'
|
|
|
|
export interface StreetBollardConfig {
|
|
/** Overall height, dome included (metres). */
|
|
readonly height: number
|
|
/** Radius of the shaft (metres). */
|
|
readonly radius: number
|
|
/** Base flange radius, as a multiple of the shaft's. */
|
|
readonly flange: number
|
|
/** How far the beads stand proud of the shaft, as a fraction of its radius. */
|
|
readonly bead: number
|
|
/** Sides around the revolve. */
|
|
readonly segments: number
|
|
readonly seed: number
|
|
}
|
|
|
|
export const streetBollardDefaults: StreetBollardConfig = {
|
|
height: 0.95,
|
|
// 8.2 diameters tall. Thicker and it is a gatepost; thinner and it is a
|
|
// parking post, which is a different object with a different job.
|
|
radius: 0.058,
|
|
flange: 1.63,
|
|
bead: 0.13,
|
|
// 36, not 28. This kit has no lowpoly budget and a bollard is a plain
|
|
// cylinder for two thirds of its height, which is exactly the shape where
|
|
// facets have nothing to hide behind.
|
|
segments: 36,
|
|
seed: 5,
|
|
}
|
|
|
|
export type StreetBollardParts = 'body'
|
|
|
|
export function createModel(overrides: Partial<StreetBollardConfig> = {}) {
|
|
return createKitModel<StreetBollardConfig, 'steelPainted', StreetBollardParts>({
|
|
id: 'street-bollard',
|
|
defaults: streetBollardDefaults,
|
|
slots: ['steelPainted'],
|
|
|
|
build: ({ config, random }) => {
|
|
const tint = createTinter(random)
|
|
const H = Math.min(1.4, Math.max(0.5, config.height))
|
|
// Clamped against the height for the same reason the mill's is: the
|
|
// proportion is the object, and either end of the slider on its own
|
|
// produces something with another name.
|
|
const R = Math.min(H * 0.11, Math.max(H * 0.045, Math.min(0.12, Math.max(0.03, config.radius))))
|
|
const segments = Math.max(10, Math.round(config.segments))
|
|
const flangeR = R * Math.min(2.1, Math.max(1.25, config.flange))
|
|
const beadR = R * (1 + Math.min(0.3, Math.max(0.04, config.bead)))
|
|
const skirtR = R * 1.05
|
|
|
|
/**
|
|
* One bead, at whatever height it is asked for.
|
|
*
|
|
* Written once and called twice because the two collars on a real
|
|
* bollard come out of the same mould. The profile is sampled at 30 and
|
|
* 60 degrees so the smoothing reads the ring as round rather than as a
|
|
* pair of cones meeting at a point.
|
|
*/
|
|
const ring = (centre: number, halfH: number): Level[] => [
|
|
{ y: centre - halfH, radius: R },
|
|
{ y: centre - halfH * 0.5, radius: R + (beadR - R) * 0.87 },
|
|
{ y: centre, radius: beadR },
|
|
{ y: centre + halfH * 0.5, radius: R + (beadR - R) * 0.87 },
|
|
{ y: centre + halfH, radius: R },
|
|
]
|
|
|
|
const flangeTop = H * 0.059
|
|
const skirtTop = H * 0.144
|
|
const loBead = H * 0.1645
|
|
const hiBead = H * 0.887
|
|
const beadHalf = H * 0.0205
|
|
const domeBase = H * 0.905
|
|
const domeH = H - domeBase
|
|
|
|
const levels: Level[] = [
|
|
// The flange's own edge is turned over, not left square: a plate that
|
|
// meets the pavement at a sharp corner reads as card.
|
|
{ y: 0, radius: flangeR - H * 0.004 },
|
|
{ y: H * 0.004, radius: flangeR },
|
|
{ y: flangeTop - H * 0.008, radius: flangeR },
|
|
{ y: flangeTop, radius: skirtR },
|
|
{ y: skirtTop, radius: skirtR },
|
|
{ y: skirtTop + H * 0.004, radius: R },
|
|
...ring(loBead, beadHalf),
|
|
...ring(hiBead, beadHalf),
|
|
// The dome. Its widest point is a touch proud of the shaft and sits a
|
|
// little above the parting line, so the overhang is a lip rather than
|
|
// a step.
|
|
{ y: domeBase, radius: R },
|
|
{ y: domeBase + domeH * 0.1, radius: beadR * 0.97 },
|
|
{ y: domeBase + domeH * 0.45, radius: R * 0.93 },
|
|
{ y: domeBase + domeH * 0.75, radius: R * 0.7 },
|
|
{ y: domeBase + domeH * 0.92, radius: R * 0.42 },
|
|
{ y: H, radius: R * 0.16 },
|
|
]
|
|
|
|
const iron = tint('steelPainted', jitter(random, 0.02))
|
|
const pieces: BufferGeometry[] = [
|
|
latheGeometry(levels, segments, [0, 0, 0], iron, {
|
|
// Weathering runs the other way to a vessel's: a bollard is washed
|
|
// clean at the top and dirty at the foot, so the ramp lightens
|
|
// upward more than a glaze would.
|
|
colourTop: tint('steelPainted', 0.06),
|
|
capBottom: true,
|
|
capTop: true,
|
|
}),
|
|
]
|
|
|
|
// 35 degrees: the shaft and the dome smooth into one another, and the
|
|
// flange edge, the skirt step and the bead shoulders all turn harder
|
|
// than that and stay as edges.
|
|
return {
|
|
body: { slot: 'steelPainted' as const, geometry: smoothNormals(mergeColoured(pieces), 35) },
|
|
}
|
|
},
|
|
}, overrides)
|
|
}
|