medieval-kit/my-registry/models/wooden-pitchfork/model.ts
CRT 7e14d9b314 Make the wooden tools wooden, and give the spade its handle
Three faults across two tools, and two of them come down to a model being
named after a material it was not made of.

The pitchfork had three steel tines, 0.24 m on a 1.5 m shaft -- 16% of the
tool against roughly 22% in the reference -- fanned out like a cultivator.
A hay fork has four or five, they are the working length of the thing, and
they stay close to parallel so a forkful stays on them. They are also
wood: the model is called wooden-pitchfork and a hay fork really was cut
and steamed from a branch, because iron is heavy at the end of a long shaft
and hay needs no edge. The socket stays iron, since that is the ferrule.

The shovel's blade was a sheet of polished steel. The strap that runs up
behind it has been modelled as an iron shoe all along -- a shoe for a board
that was being rendered as steel. A medieval digging shovel is a shaped
board with iron along its lip, so the blade moves to oak and the shoe
becomes the only metal on the head.

And it had no handle: the shaft simply ended in a swelled knob. A digging
spade is worked with both hands with the lower one round a crossbar, which
is what separates a spade from a stick with a plate on the end, and it sits
right at the end of the silhouette where the eye lands. The bar crosses the
grip swell rather than butting onto it.

Two notes for later. The wider tine spread pushed the fork past its size
guard at 0.35 m across; 30-40 cm is the head of a real hay fork, so the
limit moved rather than the geometry. And `createTinter` returns one Color
and mutates it while `steelTint` and `ironTint` allocate per call -- the
new code copies out of the first before the next call, but the two helpers
disagreeing about this is a trap in itself.

27 models, 18599 triangles, all checks pass, 0 floating cases, build clean.
2026-08-23 11:21:37 +07:00

133 lines
5.2 KiB
TypeScript

/**
* @medieval-kit/wooden-pitchfork
*
* What tells a pitchfork apart from a distance is the gap between the tines.
* That is why the tine count and their spread decide the whole silhouette.
*
* In my first version the tines were boxes of square section and every one of
* them was identical. A real tine is forged and round, it tapers towards the
* point, and none of them sits at exactly the same angle as its neighbour. All
* three were fixed here.
*/
import { Color, type BufferGeometry } from 'three'
import {
chamferedBoxGeometry,
createKitModel,
createTinter,
ironTint,
jitter,
bendGeometry,
latheGeometry,
mergeColoured,
toolShaft,
toolSocket,
type Level,
} from '../core/index.ts'
export interface WoodenPitchforkConfig {
readonly length: number
readonly shaftRadius: number
/** Number of tines. */
readonly tineCount: number
/** How far the tines splay outwards (radians). */
readonly spread: number
/** Tine length, as a fraction of the total length. */
readonly tineLength: number
readonly seed: number
}
export const woodenPitchforkDefaults: WoodenPitchforkConfig = {
length: 1.5,
shaftRadius: 0.021,
// Four tines and longer ones. Three is a garden fork; a hay fork has four
// or five, and the tines are the working length of the tool -- at 0.24 on a
// 1.5 m shaft they were 16% of it against roughly 22% in the reference, and
// read as stubs rather than as something you could lift a forkful with.
// Tines run close to parallel, splaying only a little. Fanned out like a
// claw they read as a garden cultivator; a hay fork's tines stay together
// so a forkful stays on them.
tineCount: 4,
spread: 0.15,
tineLength: 0.34,
seed: 37,
}
export type WoodenPitchforkParts = 'shaft' | 'socket' | 'tines'
export function createModel(overrides: Partial<WoodenPitchforkConfig> = {}) {
return createKitModel<WoodenPitchforkConfig, 'oak' | 'iron', WoodenPitchforkParts>({
id: 'wooden-pitchfork',
defaults: woodenPitchforkDefaults,
slots: ['oak', 'iron'],
build: ({ config, random }) => {
const tint = createTinter(random)
const tineSpan = config.length * config.tineLength
const shaftLength = config.length - tineSpan * 0.86
const shaft = toolShaft({ length: shaftLength, radius: config.shaftRadius, random })
const socketLength = config.length * 0.045
const socket = toolSocket({
y: shaft.top - socketLength * 0.3,
shaftRadius: shaft.topRadius,
length: socketLength,
random,
})
const count = Math.max(2, config.tineCount)
const base = shaft.top + config.length * 0.006
const pieces: BufferGeometry[] = []
// Cross forging: flat iron tying the tines to the socket. Thins towards the ends.
const crossWidth = config.shaftRadius * 2.6 * count
pieces.push(chamferedBoxGeometry(
[crossWidth, config.shaftRadius * 2],
[crossWidth * 0.94, config.shaftRadius * 1.3],
config.length * 0.026,
config.shaftRadius * 0.25,
[0, base, 0],
ironTint(random, -0.03),
))
for (let i = 0; i < count; i += 1) {
const t = count === 1 ? 0 : (i / (count - 1)) * 2 - 1
// A tine must be THICK. In the first version the radius was half the
// shaft's, i.e. 1 cm, and from a distance the model looked like three
// hairs. A real pitchfork tine is forged iron 2–3 cm across and takes up
// as much of the silhouette as the shaft itself.
const radius = config.shaftRadius * 0.88
const profile: Level[] = [
{ y: 0, radius: radius * 1.2 },
{ y: tineSpan * 0.2, radius },
{ y: tineSpan * 0.66, radius: radius * 0.78 },
{ y: tineSpan * 0.9, radius: radius * 0.42 },
{ y: tineSpan, radius: radius * 0.12 }, // pointed but not zero
]
// Oak, not steel. The model is called wooden-pitchfork and the
// reference is exactly that: a hay fork was cut and steamed from wood,
// because iron tines are heavy at the end of a long shaft and hay does
// not need an edge. The socket stays iron -- that is the ferrule.
const tine = latheGeometry(profile, 6, [0, 0, 0], new Color(tint('oak', -0.03)), {
capTop: false,
colourTop: new Color(tint('oakEnd', 0.06)),
})
// Curve: a straight tine looks like a technical drawing. A real pitchfork
// tine is bent forward — so it does not drop the straw it has lifted.
bendGeometry(tine, 0.42 / tineSpan + jitter(random, 0.06 / tineSpan))
// Each tine sits at a slightly different angle from its neighbour: a
// forged pitchfork is never perfectly symmetric, and this single detail
// stops it from looking "manufactured".
tine.rotateZ(-t * config.spread + jitter(random, 0.02))
tine.rotateX(jitter(random, 0.025))
tine.translate(t * config.shaftRadius * 2.5, base + config.length * 0.008, 0)
pieces.push(tine)
}
return {
shaft: { slot: 'oak', geometry: shaft.geometry },
socket: { slot: 'iron', geometry: socket },
tines: { slot: 'oak', geometry: mergeColoured(pieces) },
}
},
}, overrides)
}