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.
133 lines
5.2 KiB
TypeScript
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)
|
|
}
|