122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
/**
|
|
* @medieval-kit/wooden-ladder
|
|
*
|
|
* Two rails with rungs between them. The cheapest model in the kit and one of
|
|
* the highest in scene value: it suggests vertical movement in a scene.
|
|
*
|
|
* The rungs go INTO the rails (a housed joint), so no surface sits on the same
|
|
* plane as the surface of the rails.
|
|
*/
|
|
import { Color } from 'three'
|
|
|
|
import {
|
|
chamferedBoxGeometry,
|
|
createKitModel,
|
|
jitter,
|
|
MEDIEVAL_PALETTE,
|
|
mergeColoured,
|
|
} from '../core/index.ts'
|
|
|
|
export interface WoodenLadderConfig {
|
|
readonly height: number
|
|
/** Distance between the rails (metres). */
|
|
readonly width: number
|
|
readonly rungCount: number
|
|
/** How much the rails narrow towards the top. 0 = parallel. */
|
|
readonly taper: number
|
|
readonly seed: number
|
|
}
|
|
|
|
export const woodenLadderDefaults: WoodenLadderConfig = {
|
|
height: 2.2,
|
|
width: 0.42,
|
|
rungCount: 8,
|
|
taper: 0.18,
|
|
seed: 4,
|
|
}
|
|
|
|
export type WoodenLadderParts = 'rails' | 'rungs'
|
|
|
|
export function createModel(overrides: Partial<WoodenLadderConfig> = {}) {
|
|
return createKitModel<WoodenLadderConfig, 'oak', WoodenLadderParts>({
|
|
id: 'wooden-ladder',
|
|
defaults: woodenLadderDefaults,
|
|
slots: ['oak'],
|
|
build: ({ config, random }) => {
|
|
const tint = new Color()
|
|
// A ladder is bare sapwood that spends its life being gripped and leaned,
|
|
// so it wears pale rather than grey. Every reference photograph of one is
|
|
// markedly lighter than the weathered oak the rest of the kit is cut from,
|
|
// by about this much, and the palette constant is set for the rest of the
|
|
// kit.
|
|
const bleach = 0.13
|
|
const shade = (lift = 0): Color => {
|
|
tint.copy(MEDIEVAL_PALETTE.oak)
|
|
tint.offsetHSL(jitter(random, 0.012), jitter(random, 0.05), bleach + lift + jitter(random, 0.055))
|
|
return tint
|
|
}
|
|
|
|
const railThickness = config.width * 0.1
|
|
const half = config.width / 2
|
|
|
|
// The rails converge towards the top; this single detail stops the ladder
|
|
// from being "two boards" and makes it a ladder.
|
|
const lean = half * config.taper
|
|
/**
|
|
* Half-gap between the rails at height fraction `t`.
|
|
*
|
|
* This exists because the rung length and the rail placement used to be
|
|
* two separate expressions that disagreed. The rails are built upright
|
|
* and then rotated about Z, so at height y each one sits at
|
|
* `side·half − y·sin(θ)`; the rung length assumed `half − lean·t`, which
|
|
* narrows where the real rails widen. At the default taper the error was
|
|
* small enough to hide. At taper 0.4 on a 5 m ladder the top rungs came
|
|
* out half the length of the gap they were supposed to span and floated
|
|
* between the rails.
|
|
*
|
|
* Both now read from here, so they cannot drift apart again.
|
|
*/
|
|
const railHalfAt = (t: number): number => {
|
|
const y = -config.height / 2 + t * config.height
|
|
return half - y * Math.sin(lean / config.height)
|
|
}
|
|
const rails = [-1, 1].map((side) => {
|
|
const rail = chamferedBoxGeometry(
|
|
[railThickness, railThickness * 1.35],
|
|
[railThickness * 0.85, railThickness * 1.15],
|
|
config.height,
|
|
railThickness * 0.16,
|
|
[0, 0, 0],
|
|
shade(),
|
|
)
|
|
// Lean it slightly around the Z axis: bottom end out, top end in.
|
|
rail.rotateZ((side * -lean) / config.height)
|
|
rail.translate(side * half, 0, 0)
|
|
return rail
|
|
})
|
|
|
|
const rungs = []
|
|
const count = Math.max(2, config.rungCount)
|
|
for (let i = 0; i < count; i += 1) {
|
|
const t = (i + 0.5) / count
|
|
const y = -config.height / 2 + t * config.height
|
|
// The rung is a little LONGER than the rail gap at that height, so that
|
|
// its ends stay inside the rails.
|
|
const span = railHalfAt(t) * 2 + railThickness * 0.9
|
|
rungs.push(chamferedBoxGeometry(
|
|
[span, railThickness * 1.05],
|
|
[span, railThickness * 1.05],
|
|
railThickness * 0.8,
|
|
railThickness * 0.16,
|
|
[0, y, 0],
|
|
shade(0.03),
|
|
))
|
|
}
|
|
|
|
return {
|
|
rails: { slot: 'oak', geometry: mergeColoured(rails) },
|
|
rungs: { slot: 'oak', geometry: mergeColoured(rungs) },
|
|
}
|
|
},
|
|
}, overrides)
|
|
}
|