The kit is going public, so everything a reader sees is now English:
27 model sources, the core vocabulary, meta.ts (titles, descriptions and
every slider label), the viewer UI, all script output, REFERENCE.md and
the READMEs.
Rules applied: only comments, doc comments and human-facing strings were
touched. Identifiers, numbers, logic, registry item ids, part names and
material slot names are unchanged — those are a published contract and
were already English.
Verified rather than assumed: for all 53 changed .ts files the code
skeleton (source with comments stripped and string contents replaced by a
placeholder) is byte-identical before and after. Full gate is green —
typecheck, registry conformance, ~510 model checks, GLB round trip and a
production build.
Also fixed here:
- index.html and viewer.html were missed by the translation pass because
the file list only covered .ts/.md/.css. Both had lang="tr" and Turkish
titles.
- The viewer formatted triangle counts with toLocaleString('tr-TR'), which
prints 15.425 rather than 15,425.
- REFERENCE.md's model table header did not match what
scripts/catalog-table.ts emits, so the next regeneration would have
silently changed it. Aligned to the generator.
- Removed scripts/probe.ts and scripts/zdiag.ts; they were throwaway
diagnostics that got committed by accident.
157 lines
5.5 KiB
TypeScript
157 lines
5.5 KiB
TypeScript
/**
|
|
* @medieval-kit/trestle-table
|
|
*
|
|
* Trestle table: the standard table of the middle ages. The top is NOT nailed
|
|
* to the trestles, it merely rests on them — so that once the meal is over it
|
|
* can be lifted away and the hall cleared. That is why the top boards stand
|
|
* independent of the trestles and have gaps between them.
|
|
*
|
|
* A trestle: a horizontal cap, two legs splaying down from it, a foot at the
|
|
* bottom. Two trestles are tied together by a stretcher.
|
|
*/
|
|
import { Color } from 'three'
|
|
|
|
import {
|
|
MEDIEVAL_PALETTE,
|
|
chamferedBoxGeometry,
|
|
createKitModel,
|
|
jitter,
|
|
mergeColoured,
|
|
} from '../core/index.ts'
|
|
|
|
export interface TrestleTableConfig {
|
|
/** Top length (metres). */
|
|
readonly length: number
|
|
/** Top width (metres). */
|
|
readonly width: number
|
|
readonly height: number
|
|
/** Number of top boards. */
|
|
readonly plankCount: number
|
|
/** Outward splay of the legs. */
|
|
readonly splay: number
|
|
readonly seed: number
|
|
}
|
|
|
|
export const trestleTableDefaults: TrestleTableConfig = {
|
|
length: 1.9,
|
|
width: 0.78,
|
|
height: 0.74,
|
|
plankCount: 4,
|
|
splay: 0.22,
|
|
seed: 19,
|
|
}
|
|
|
|
export type TrestleTableParts = 'top' | 'trestles' | 'stretcher'
|
|
|
|
export function createModel(overrides: Partial<TrestleTableConfig> = {}) {
|
|
return createKitModel<TrestleTableConfig, 'oak', TrestleTableParts>({
|
|
id: 'trestle-table',
|
|
defaults: trestleTableDefaults,
|
|
slots: ['oak'],
|
|
build: ({ config, random }) => {
|
|
const tint = new Color()
|
|
const oak = (lift = 0): Color => {
|
|
tint.copy(MEDIEVAL_PALETTE.oak)
|
|
tint.offsetHSL(jitter(random, 0.012), jitter(random, 0.05), lift + jitter(random, 0.055))
|
|
return tint
|
|
}
|
|
const half = config.height / 2
|
|
const board = config.height * 0.045
|
|
const timber = config.height * 0.075
|
|
|
|
// --- top: separate boards with a thin gap between them ---
|
|
const planks = []
|
|
const count = Math.max(1, config.plankCount)
|
|
const gap = config.width * 0.008
|
|
const plankWidth = (config.width - gap * (count - 1)) / count
|
|
for (let i = 0; i < count; i += 1) {
|
|
const z = -config.width / 2 + plankWidth / 2 + i * (plankWidth + gap)
|
|
// Every board gets its own thickness and tone: a top that was sawn,
|
|
// planed and used for years is never uniform.
|
|
const thickness = board * (1 + jitter(random, 0.08))
|
|
planks.push(chamferedBoxGeometry(
|
|
[config.length, plankWidth],
|
|
[config.length, plankWidth],
|
|
thickness,
|
|
board * 0.22,
|
|
[jitter(random, config.length * 0.004), half - thickness / 2, z],
|
|
oak(0.04),
|
|
))
|
|
}
|
|
|
|
// --- trestles ---
|
|
const trestles = []
|
|
const trestleX = config.length * 0.31
|
|
const legSpan = config.height - board
|
|
for (const side of [-1, 1] as const) {
|
|
const x = side * trestleX
|
|
// Cap: the horizontal rail that carries the top.
|
|
trestles.push(chamferedBoxGeometry(
|
|
[timber * 1.1, config.width * 0.72],
|
|
[timber * 1.1, config.width * 0.72],
|
|
timber * 0.9,
|
|
timber * 0.16,
|
|
[x, half - board - timber * 0.45, 0],
|
|
oak(-0.02),
|
|
))
|
|
// Two legs: they splay outwards on their way down from the cap.
|
|
for (const dir of [-1, 1] as const) {
|
|
const leg = chamferedBoxGeometry(
|
|
[timber * 0.9, timber * 0.8],
|
|
[timber * 1.05, timber * 0.95],
|
|
legSpan,
|
|
timber * 0.15,
|
|
[0, -legSpan / 2, 0],
|
|
oak(),
|
|
)
|
|
leg.rotateX(dir * config.splay)
|
|
// The legs pass through the cap at SEPARATE points; two timbers
|
|
// cannot share one mortise. Without this offset the two sit on top
|
|
// of each other when splay=0.
|
|
leg.translate(x, half - board - timber * 0.3, dir * timber * 0.62)
|
|
trestles.push(leg)
|
|
}
|
|
// Centre post: the vertical timber running from the cap down to the
|
|
// foot. The stretcher passes through it — without it the stretcher hung
|
|
// in mid-air BETWEEN the two trestles, because as the legs splayed they
|
|
// moved apart along z and no longer touched the stretcher.
|
|
trestles.push(chamferedBoxGeometry(
|
|
[timber * 0.8, timber * 0.85],
|
|
[timber * 0.9, timber * 0.9],
|
|
config.height - board - timber * 0.5,
|
|
timber * 0.14,
|
|
[x, -half + (config.height - board - timber * 0.5) / 2 + timber * 0.2, 0],
|
|
oak(-0.01),
|
|
))
|
|
|
|
// Foot: the crosswise member resting on the ground. Required, because
|
|
// the floor is not flat.
|
|
const spread = Math.sin(config.splay) * legSpan
|
|
trestles.push(chamferedBoxGeometry(
|
|
[timber * 1.2, config.width * 0.62 + spread * 2],
|
|
[timber * 1.05, config.width * 0.58 + spread * 2],
|
|
timber * 0.62,
|
|
timber * 0.14,
|
|
[x, -half + timber * 0.31, 0],
|
|
oak(-0.04),
|
|
))
|
|
}
|
|
|
|
// --- stretcher: the long rail tying the two trestles, it enters the legs ---
|
|
const stretcher = chamferedBoxGeometry(
|
|
[trestleX * 2 + timber * 1.6, timber * 0.7],
|
|
[trestleX * 2 + timber * 1.6, timber * 0.7],
|
|
timber * 0.85,
|
|
timber * 0.15,
|
|
[0, -half + config.height * 0.24, 0],
|
|
oak(-0.03),
|
|
)
|
|
|
|
return {
|
|
top: { slot: 'oak', geometry: mergeColoured(planks) },
|
|
trestles: { slot: 'oak', geometry: mergeColoured(trestles) },
|
|
stretcher: { slot: 'oak', geometry: stretcher },
|
|
}
|
|
},
|
|
}, overrides)
|
|
}
|