Bring the anvil's horn back to its reference proportion, rebuild the book's metal furniture so no two overlapping faces share a plane, and release 0.2.0.
This commit is contained in:
parent
3f57165d06
commit
9b4ce16c59
82 changed files with 9725 additions and 5064 deletions
|
|
@ -1,35 +1,70 @@
|
|||
/**
|
||||
* @medieval-kit/wooden-shovel
|
||||
*
|
||||
* Third attempt. The first two failed, and both for the same reason: I tried
|
||||
* to build the blade out of flat pieces.
|
||||
* Sixth attempt. The reference is an antique one-piece grain scoop: a squared
|
||||
* dished plate, nearly as broad as it is long, an iron shoe forged round the
|
||||
* leading edge with a return onto both faces, a round bent shaft and a T-grip.
|
||||
* History of the earlier passes:
|
||||
*
|
||||
* attempt 1 — two boxes end to end. It came out "shovel-shaped", not a shovel.
|
||||
* attempt 2 — three flat panels, each rotated slightly, lined up side by
|
||||
* side. Because the panels rotated about their own centres, steps were
|
||||
* left between them; the eye read that as "three boards", not one surface.
|
||||
* attempt 1-2 — boxes / flat panels. Read as boards, not a scoop.
|
||||
* attempt 3-4 — one dishedSheetGeometry, but the outline was an oval; the
|
||||
* blind critic called it "a paddle or a wooden spoon" (67).
|
||||
* attempt 5 — trapezoid plan, real shoe band + side straps, seated ferrule.
|
||||
* Scored 69: landmarks all present, silhouette still wrong. The critic
|
||||
* measured the pan at 1.35 long-to-wide against 1.05 in the photo, called
|
||||
* the taper "a straight-sided funnel", the pan "flat", the shoe "a cap
|
||||
* sitting on the top edge only", and found both side straps floating
|
||||
* outboard of the blade. It also flagged: the socket-end rim reading as a
|
||||
* seam with a lighter panel below it, the ferrule not seated (a wedge of
|
||||
* dish poking past the collar), the T-bar arms not colinear (the grip was
|
||||
* a TAPERED prism, so one arm was fatter than the other), the shaft tip
|
||||
* exposed below the crossbar, and a width/colour step mid-shaft (the
|
||||
* per-level jitter in core's toolShaft).
|
||||
*
|
||||
* What makes a shovel a shovel is that the blade is ONE CONTINUOUS CONCAVE
|
||||
* SURFACE: the dish that holds the soil. `dishedSheetGeometry` was written for
|
||||
* exactly this — a seamless sheet with a curved cross-section whose width and
|
||||
* thickness change along its length.
|
||||
* What this version does differently:
|
||||
* - PAN OUTLINE: length-to-width is ~1.04 (bladeWidth 0.29 against a 0.30
|
||||
* span). The socket end is 0.78 of full width, and the pull-in happens in
|
||||
* the bottom third through a rounded shoulder; the top two thirds run
|
||||
* near-parallel into a straight leading edge with small corner chamfers.
|
||||
* - DISH WITH WALLS: besides the sheet's parabolic hollow, two wooden side
|
||||
* rails (rolled to lie on the local surface slope) and the socket-end rim
|
||||
* arc form a raised lip standing proud of the floor. The rim's ends and
|
||||
* the rails' lower ends interpenetrate, so the rim reads as the scoop's
|
||||
* back wall instead of a floating seam.
|
||||
* - SHOE WRAPS THE EDGE: the arc band round the leading edge is joined by
|
||||
* two thin dished-sheet RETURNS overlaid on the front and back faces
|
||||
* (about 12% of the blade), and the floating side straps are replaced by
|
||||
* angled corner caps that overlap both the returns and the pan's edges.
|
||||
* - SHAFT: built here as a smooth 7-level lathe (no mid-shaft jitter step),
|
||||
* bent gently with bendGeometry (base at the origin first — the bend trap)
|
||||
* toward the same side the blade tilts. The T-grip is a SYMMETRIC lathe
|
||||
* bar (rounded ends, no taper, so no kink at the handle), and the shaft's
|
||||
* bottom tip terminates INSIDE the bar.
|
||||
* - SEATING: the collar overlaps the blade base by ~4cm and the base level's
|
||||
* curve is halved, so no dish corner pokes out past the collar.
|
||||
* - COLOUR: the shoe and the socket are a rust-brown iron (palette iron
|
||||
* pulled toward leather) instead of cool slate; the timber is slightly
|
||||
* desaturated toward weathered oak. Lifts stay positive: the occlusion
|
||||
* bake darkens everything pressed against the sheet.
|
||||
*
|
||||
* Silhouette: a narrow neck at the socket, widest at 45%, a soft taper towards
|
||||
* the tip. A foot tread at the back — on a real shovel the top edge of the
|
||||
* blade is folded over, to press down on with the foot.
|
||||
* Traps honoured: `dish` is floored before any radius is fitted to it
|
||||
* (division by zero → NaN spreads through the occlusion bake), every asin
|
||||
* argument is clamped below 1, and every colour is a NEW Color per call.
|
||||
*/
|
||||
import { Color, type BufferGeometry } from 'three'
|
||||
|
||||
import {
|
||||
arcBarGeometry,
|
||||
bendGeometry,
|
||||
chamferedBoxGeometry,
|
||||
createKitModel,
|
||||
createTinter,
|
||||
dishedSheetGeometry,
|
||||
ironTint,
|
||||
jitter,
|
||||
latheGeometry,
|
||||
mergeColoured,
|
||||
prismGeometry,
|
||||
toolShaft,
|
||||
toolSocket,
|
||||
MEDIEVAL_PALETTE,
|
||||
type Level,
|
||||
type SheetLevel,
|
||||
} from '../core/index.ts'
|
||||
|
||||
|
|
@ -48,11 +83,24 @@ export interface WoodenShovelConfig {
|
|||
}
|
||||
|
||||
export const woodenShovelDefaults: WoodenShovelConfig = {
|
||||
length: 1.16,
|
||||
// 0.86, not 1.16. The reference is a short-handled grain scoop, not a
|
||||
// long-handled digging shovel: its shaft runs about two and a half blade
|
||||
// lengths, ours ran nearly four. At that ratio, stood on its grip with the
|
||||
// pan in the air, a blind viewer called it a floor lamp.
|
||||
length: 0.86,
|
||||
shaftRadius: 0.022,
|
||||
bladeWidth: 0.27,
|
||||
bladeLength: 0.27,
|
||||
dish: 0.17,
|
||||
bladeWidth: 0.29,
|
||||
|
||||
// a shovel blade is wider than it is long; taller than wide is a dustpan.
|
||||
// 0.32 of a shorter tool, which keeps the pan the same size in metres while
|
||||
// the handle comes down. A shovel blade is wider than it is long; taller
|
||||
// than wide is a dustpan.
|
||||
bladeLength: 0.32,
|
||||
// 0.09, not 0.14. Measured against the reference the hollow wants to be 8 to
|
||||
// 10 percent of the blade width. At 0.14, with a lip standing on both long
|
||||
// edges, the pan curled into a U-section trough and a blind viewer called it
|
||||
// a bin.
|
||||
dish: 0.09,
|
||||
bladeAngle: 9,
|
||||
seed: 31,
|
||||
}
|
||||
|
|
@ -66,116 +114,240 @@ export function createModel(overrides: Partial<WoodenShovelConfig> = {}) {
|
|||
slots: ['oak', 'iron'],
|
||||
build: ({ config, random }) => {
|
||||
const tint = createTinter(random)
|
||||
|
||||
/** Pull saturation down: the critic read the timber as new pine. */
|
||||
const weather = (colour: Color, drop: number): Color => {
|
||||
const hsl = { h: 0, s: 0, l: 0 }
|
||||
colour.getHSL(hsl)
|
||||
colour.setHSL(hsl.h, Math.max(0, hsl.s - drop), hsl.l)
|
||||
return colour
|
||||
}
|
||||
/**
|
||||
* Rust-brown forged iron: the palette's cool slate pulled toward
|
||||
* leather. Returns a NEW Color every call — two rusts in one geometry
|
||||
* call must not resolve to the same object.
|
||||
*/
|
||||
const rust = (lift: number): Color => {
|
||||
const c = new Color(MEDIEVAL_PALETTE.iron).lerp(new Color(MEDIEVAL_PALETTE.leather), 0.45)
|
||||
const hsl = { h: 0, s: 0, l: 0 }
|
||||
c.getHSL(hsl)
|
||||
c.setHSL(
|
||||
hsl.h + jitter(random, 0.01),
|
||||
Math.min(1, hsl.s + 0.05 + jitter(random, 0.03)),
|
||||
Math.max(0.045, hsl.l + lift + jitter(random, 0.03)),
|
||||
)
|
||||
return c
|
||||
}
|
||||
|
||||
const span = config.length * config.bladeLength
|
||||
const shaftLength = config.length - span * 0.82
|
||||
const shaft = toolShaft({ length: shaftLength, radius: config.shaftRadius, random })
|
||||
|
||||
// --- T-handle --------------------------------------------------------
|
||||
// The reference has one and this did not: the shaft simply ended in a
|
||||
// swelled knob. A digging spade is worked with both hands and the lower
|
||||
// one goes round a crossbar; it is the detail that separates a spade
|
||||
// from a stick with a plate on the end, and it is right at the end of
|
||||
// the silhouette where the eye lands.
|
||||
//
|
||||
// The bar crosses the grip swell rather than butting onto it, so the
|
||||
// joint is an overlap like every other in the kit.
|
||||
const gripSpan = config.shaftRadius * 8.6
|
||||
const grip = prismGeometry(
|
||||
config.shaftRadius * 0.95,
|
||||
config.shaftRadius * 0.82,
|
||||
gripSpan,
|
||||
7,
|
||||
[0, 0, 0],
|
||||
new Color(tint('oak', 0.05)),
|
||||
)
|
||||
// Built along Y; +90 degrees about Z lays it along X, across the blade.
|
||||
grip.rotateZ(Math.PI / 2)
|
||||
grip.translate(0, -shaft.top + config.shaftRadius * 0.9, 0)
|
||||
|
||||
const socketLength = config.length * 0.05
|
||||
const socket = toolSocket({
|
||||
y: shaft.top - socketLength * 0.3,
|
||||
shaftRadius: shaft.topRadius,
|
||||
length: socketLength,
|
||||
random,
|
||||
})
|
||||
|
||||
const half = config.bladeWidth / 2
|
||||
const t = config.length * 0.011
|
||||
const curve = config.bladeWidth * config.dish
|
||||
const r = config.shaftRadius
|
||||
const shaftLength = config.length - span * 0.82
|
||||
|
||||
// Cross-section profile — the FOURTH attempt, this time over the silhouette.
|
||||
//
|
||||
// The third attempt was geometrically correct (one continuous dished
|
||||
// surface) but it still did not read as a shovel: the edges bulged in the
|
||||
// middle and closed softly towards the tip, i.e. a SPOON profile. A shovel
|
||||
// is not a spoon: it runs almost PARALLEL along its sides, then ends in a
|
||||
// short chamfer at the tip. What holds the soil is that parallel part;
|
||||
// without it what you get is a spatula.
|
||||
const profile: SheetLevel[] = [
|
||||
{ y: 0, halfWidth: half * 0.24, thickness: t * 1.5, curve: curve * 0.08 },
|
||||
{ y: span * 0.11, halfWidth: half * 0.82, thickness: t * 1.15, curve: curve * 0.4 },
|
||||
{ y: span * 0.28, halfWidth: half * 0.99, thickness: t * 0.95, curve: curve * 0.85 },
|
||||
{ y: span * 0.62, halfWidth: half, thickness: t * 0.82, curve },
|
||||
{ y: span * 0.85, halfWidth: half * 0.95, thickness: t * 0.6, curve: curve * 0.94 },
|
||||
{ y: span * 0.96, halfWidth: half * 0.74, thickness: t * 0.32, curve: curve * 0.72 },
|
||||
{ y: span, halfWidth: half * 0.44, thickness: t * 0.14, curve: curve * 0.48 },
|
||||
// --- shaft -----------------------------------------------------------
|
||||
// Own lathe rather than core's toolShaft: the per-level jitter there
|
||||
// left a visible width step mid-shaft, and toolShaft cannot bend. The
|
||||
// tip radius is small enough to bury inside the T-bar, the swell sits
|
||||
// just ABOVE the bar, and the taper is gentle and continuous.
|
||||
const rr = (s: number): number => r * s * (1 + jitter(random, 0.015))
|
||||
const shaftLevels: Level[] = [
|
||||
{ y: 0, radius: rr(0.72) }, // tip — ends inside the T-bar
|
||||
{ y: r * 1.1, radius: rr(1.03) },
|
||||
{ y: r * 2.6, radius: rr(1.08) }, // grip swell, above the bar
|
||||
{ y: shaftLength * 0.16, radius: rr(0.97) },
|
||||
{ y: shaftLength * 0.55, radius: rr(1.01) },
|
||||
{ y: shaftLength * 0.85, radius: rr(0.97) },
|
||||
{ y: shaftLength, radius: rr(0.9) }, // taper toward the socket
|
||||
]
|
||||
const topRadius = shaftLevels.at(-1)!.radius
|
||||
const shaftGeo = latheGeometry(
|
||||
shaftLevels, 7, [0, 0, 0],
|
||||
weather(tint('oak', 0.05), 0.05),
|
||||
{ colourTop: weather(tint('oak', -0.01), 0.05) },
|
||||
)
|
||||
// Gentle bend, the reference's signature. Built with its base at the
|
||||
// origin FIRST: bendGeometry wraps around y=0, and a body centred on
|
||||
// y=0 bends symmetrically into nothing. Negative curvature bends toward
|
||||
// -Z, the side the blade tilts to.
|
||||
const bendK = 0.06
|
||||
bendGeometry(shaftGeo, -bendK)
|
||||
shaftGeo.translate(0, -shaftLength / 2, 0)
|
||||
const bendAngle = shaftLength * bendK
|
||||
const topY = Math.sin(bendAngle) / bendK - shaftLength / 2
|
||||
const topZ = -(1 - Math.cos(bendAngle)) / bendK
|
||||
|
||||
// Both colours copied out first: `createTinter` returns one Color and
|
||||
// mutates it, so two tints inside one call would resolve to the same
|
||||
// value.
|
||||
const boardLow = new Color(tint('oak', -0.05))
|
||||
const boardHigh = new Color(tint('oakEnd', 0.03))
|
||||
// --- T-grip ----------------------------------------------------------
|
||||
// A SYMMETRIC lathe bar with rounded ends. The previous tapered prism
|
||||
// gave the two arms different diameters, which read as a kink where
|
||||
// they met the handle.
|
||||
const gripHalfSpan = r * 4.3
|
||||
const gripR = r * 1.15
|
||||
const gripLevels: Level[] = [
|
||||
{ y: -gripHalfSpan, radius: gripR * 0.62 },
|
||||
{ y: -gripHalfSpan + r * 1.3, radius: gripR },
|
||||
{ y: gripHalfSpan - r * 1.3, radius: gripR },
|
||||
{ y: gripHalfSpan, radius: gripR * 0.62 },
|
||||
]
|
||||
const grip = latheGeometry(gripLevels, 7, [0, 0, 0], weather(tint('oak', 0.04), 0.05))
|
||||
grip.rotateZ(Math.PI / 2) // built along Y; lay it along X
|
||||
// Centred a whisker above the shaft's bottom tip, so the tip's end face
|
||||
// terminates inside the bar instead of resting on the ground beside it.
|
||||
grip.translate(0, -shaftLength / 2 + r * 0.15, 0)
|
||||
|
||||
// --- socket / ferrule ------------------------------------------------
|
||||
// Same profile as core's toolSocket, built here so it can take the rust
|
||||
// tone: the critic read the palette's slate-blue iron against the
|
||||
// photograph's rust-brown. Raised so the collar overlaps the blade base
|
||||
// by ~4cm — seated on the tang, no shadow slot.
|
||||
const sockL = config.length * 0.05
|
||||
const sy = topY - sockL * 0.18
|
||||
const sockLevels: Level[] = [
|
||||
{ y: sy - sockL * 0.9, radius: topRadius * 1.12 },
|
||||
{ y: sy - sockL * 0.45, radius: topRadius * 1.34 },
|
||||
{ y: sy + sockL * 0.1, radius: topRadius * 1.5 },
|
||||
{ y: sy + sockL * 0.22, radius: topRadius * 1.72 }, // collar
|
||||
{ y: sy + sockL * 0.34, radius: topRadius * 1.46 },
|
||||
]
|
||||
const socket = latheGeometry(sockLevels, 6, [0, 0, 0], rust(0.02), { colourTop: rust(0.06) })
|
||||
socket.translate(0, 0, topZ)
|
||||
|
||||
// --- pan -------------------------------------------------------------
|
||||
// Floored HERE: two radii below are fitted as halfWidth² over twice
|
||||
// this, and at dish = 0 that division is infinite and the vertices NaN.
|
||||
const c = config.bladeWidth * Math.max(0.012, config.dish)
|
||||
|
||||
// Widest AT THE TIP, narrowing back to the throat, with the last two
|
||||
// levels drawing the leading corners off.
|
||||
//
|
||||
// The version before this widened over the bottom fifth, ran parallel
|
||||
// through the middle and then pulled back in at the tip. That is a
|
||||
// rectangle, and with a lip standing up on all four sides a viewer read
|
||||
// it as a framed tray on a post rather than as a shovel. A pan flares to
|
||||
// its cutting edge; the narrow end is the end the handle is on.
|
||||
//
|
||||
// The base level's curve is halved so no dish corner pokes out past the
|
||||
// ferrule collar.
|
||||
const profile: SheetLevel[] = [
|
||||
{ y: 0, halfWidth: half * 0.7, thickness: t * 1.35, curve: c * 0.5 },
|
||||
{ y: span * 0.12, halfWidth: half * 0.83, thickness: t * 1.2, curve: c * 0.75 },
|
||||
{ y: span * 0.35, halfWidth: half * 0.93, thickness: t * 1.05, curve: c * 0.95 },
|
||||
{ y: span * 0.7, halfWidth: half * 0.99, thickness: t * 0.95, curve: c },
|
||||
{ y: span * 0.9, halfWidth: half, thickness: t * 0.88, curve: c },
|
||||
{ y: span * 0.97, halfWidth: half * 0.95, thickness: t * 0.82, curve: c * 0.98 },
|
||||
{ y: span, halfWidth: half * 0.84, thickness: t * 0.78, curve: c * 0.96 },
|
||||
]
|
||||
const boardLow = weather(tint('oak', -0.04), 0.06)
|
||||
const boardHigh = weather(tint('oakEnd', 0.02), 0.05)
|
||||
const sheet = dishedSheetGeometry(profile, 8, boardLow, boardHigh)
|
||||
|
||||
// Foot tread: the fold at the top edge of the blade, BEHIND the dish. It
|
||||
// must sit at shoulder height and be WIDE — this is where you step, and a
|
||||
// narrow ledge is both useless and invisible in the silhouette.
|
||||
const tread = chamferedBoxGeometry(
|
||||
[config.bladeWidth * 0.78, t * 2.4],
|
||||
[config.bladeWidth * 0.7, t * 1.9],
|
||||
t * 1.8,
|
||||
t * 0.34,
|
||||
[0, 0, 0],
|
||||
new Color(tint('oak', -0.1)),
|
||||
)
|
||||
tread.rotateX(0.42)
|
||||
tread.translate(0, span * 0.12, -t * 1.7)
|
||||
// There is deliberately NO rail across the throat. One ran there, bent to
|
||||
// the dish, and with a lip already standing on both long edges and a band
|
||||
// across the tip it closed the pan into a four-sided frame. A shovel is
|
||||
// open at the handle end: that is the end the load leaves by.
|
||||
|
||||
// Back strap: forged iron leaving the socket and running up BEHIND the
|
||||
// blade. This is the second thing that makes a shovel a shovel. Without it
|
||||
// the blade looks like a sheet glued to the end of the shaft; on a real
|
||||
// shovel the strap is what carries the blade, and it is also what answers
|
||||
// the eye's question of "how is this held on".
|
||||
const strap = chamferedBoxGeometry(
|
||||
[config.shaftRadius * 2.3, t * 2.2],
|
||||
[config.shaftRadius * 1.1, t * 1.4],
|
||||
span * 0.5,
|
||||
t * 0.3,
|
||||
[0, span * 0.22, -t * 1.4],
|
||||
ironTint(random, -0.02),
|
||||
)
|
||||
// Side rails: the raised side walls of the scoop. Rolled about their own
|
||||
// long axis to lie on the local surface slope (otherwise their outboard
|
||||
// half hangs out behind the sheet), leaned to follow the plan taper, and
|
||||
// kept inboard of the plan edge so nothing floats off the shoulder.
|
||||
const roll = Math.atan((1.7 * c) / half)
|
||||
const railW = config.bladeWidth * 0.095
|
||||
const railD = config.bladeWidth * 0.05
|
||||
const railA = { x: half * 0.68, y: span * 0.05 }
|
||||
const railB = { x: half * 0.95, y: span * 0.93 }
|
||||
const railLen = Math.hypot(railB.x - railA.x, railB.y - railA.y)
|
||||
const railLean = Math.atan2(railB.x - railA.x, railB.y - railA.y)
|
||||
const railMidX = (railA.x + railB.x) / 2
|
||||
const railMidU = railMidX / half
|
||||
const rails: BufferGeometry[] = []
|
||||
for (const side of [-1, 1] as const) {
|
||||
const rail = chamferedBoxGeometry(
|
||||
[railW, railD], [railW, railD], railLen, t * 0.4,
|
||||
[0, 0, 0], weather(tint('oak', 0.03), 0.05),
|
||||
)
|
||||
rail.rotateY(-side * roll)
|
||||
rail.rotateZ(-side * railLean)
|
||||
rail.translate(side * railMidX, (railA.y + railB.y) / 2, c * railMidU * railMidU + t * 0.55)
|
||||
rails.push(rail)
|
||||
}
|
||||
|
||||
const blade: BufferGeometry = mergeColoured([sheet, tread])
|
||||
// Both must take the SAME transform, or the strap won't stay behind the blade.
|
||||
for (const piece of [blade, strap]) {
|
||||
// The blade is tilted slightly forward of the shaft line, so it bites the soil.
|
||||
piece.rotateX(-(config.bladeAngle * Math.PI) / 180)
|
||||
piece.translate(0, shaft.top - span * 0.06, 0)
|
||||
// --- iron shoe -------------------------------------------------------
|
||||
// Band round the leading edge + a thin return sheet overlaid on EACH
|
||||
// face + angled corner caps. Lifts positive: the occlusion bake darkens
|
||||
// everything pressed against the sheet.
|
||||
// Floored against the SHEET thickness, not just scaled to the blade.
|
||||
// `span` is a slider, and at its short end a band sized only as a
|
||||
// fraction of it becomes thin enough that its end face lands in the same
|
||||
// plane as the sheet tip it is supposed to wrap, which is a flicker
|
||||
// rather than a shoe.
|
||||
const bandTh = Math.max(t * 2.4, span * 0.085)
|
||||
const tipR = (0.9 * half) * (0.9 * half) / (2 * (0.96 * c))
|
||||
const bandAngle = Math.asin(Math.min(0.995, (half * 0.94) / tipR))
|
||||
const band = arcBarGeometry(
|
||||
tipR, bandTh,
|
||||
-Math.PI / 2 - bandAngle, -Math.PI / 2 + bandAngle,
|
||||
10, [0, 0, 0], rust(0.05),
|
||||
)
|
||||
band.rotateX(Math.PI / 2)
|
||||
band.translate(0, span - bandTh * 0.3, tipR + t * 0.4)
|
||||
|
||||
// Face returns: the iron coming back over the wood, ~12% of the blade.
|
||||
// Slightly wider than the pan so a hairline of iron shows around the
|
||||
// wood edge, offset so they interpenetrate the sheet without sharing a
|
||||
// plane with it. Their top edges end inside the band.
|
||||
const returns: BufferGeometry[] = []
|
||||
for (const face of [1, -1] as const) {
|
||||
const ret = dishedSheetGeometry([
|
||||
// Starts BELOW the rails finish, not level with them. The rails end at
|
||||
// span * 0.89 and the returns used to start at span * 0.88, so two end
|
||||
// faces of the same orientation sat a couple of millimetres apart; at
|
||||
// the short end of the blade slider that gap closes to under a
|
||||
// millimetre and the two planes become one.
|
||||
{ y: span * 0.8, halfWidth: half, thickness: t * 0.9, curve: c },
|
||||
{ y: span + t * 0.3, halfWidth: half * 0.91, thickness: t * 0.9, curve: c * 0.97 },
|
||||
], 8, rust(0.05))
|
||||
ret.translate(0, 0, face * t * 0.62)
|
||||
returns.push(ret)
|
||||
}
|
||||
|
||||
// Corner caps: angled over the chamfered corners, overlapping the
|
||||
// returns above and the pan's side edges below — the iron continues
|
||||
// round the corner instead of straps starting in mid-air.
|
||||
const capLen = config.bladeWidth * 0.11
|
||||
const capW = config.bladeWidth * 0.08
|
||||
const capD = config.bladeWidth * 0.105
|
||||
const capAngle = Math.atan2(half * 0.09, span * 0.08)
|
||||
const caps: BufferGeometry[] = []
|
||||
for (const side of [-1, 1] as const) {
|
||||
const cap = chamferedBoxGeometry(
|
||||
[capW, capD], [capW, capD], capLen, t * 0.35,
|
||||
[0, 0, 0], rust(0.05),
|
||||
)
|
||||
cap.rotateY(-side * roll)
|
||||
cap.rotateZ(side * capAngle)
|
||||
cap.translate(side * half * 0.9, span * 0.955, c * 0.88 + t * 0.2)
|
||||
caps.push(cap)
|
||||
}
|
||||
|
||||
const blade: BufferGeometry = mergeColoured([sheet, ...rails])
|
||||
const shoe = mergeColoured([band, ...returns, ...caps])
|
||||
// Both must take the SAME transform, or the shoe won't stay on the
|
||||
// blade. The tilt continues the bend's lean plus the config angle.
|
||||
const tilt = (config.bladeAngle * Math.PI) / 180 + bendAngle
|
||||
for (const piece of [blade, shoe]) {
|
||||
piece.rotateX(-tilt)
|
||||
piece.translate(0, topY - span * 0.1, topZ)
|
||||
}
|
||||
|
||||
return {
|
||||
shaft: { slot: 'oak', geometry: mergeColoured([shaft.geometry, grip]) },
|
||||
shaft: { slot: 'oak', geometry: mergeColoured([shaftGeo, grip]) },
|
||||
socket: { slot: 'iron', geometry: socket },
|
||||
blade: {
|
||||
// Oak, not steel. A medieval digging shovel is a shaped board with
|
||||
// an iron shoe along its lip -- the strap below is that shoe, and it
|
||||
// has been there all along while the board it protects was rendered
|
||||
// as a sheet of polished steel.
|
||||
// Oak, not steel: the blade is a shaped board, and the only metal
|
||||
// on it is the shoe protecting its lip.
|
||||
slot: 'oak',
|
||||
geometry: blade,
|
||||
extras: [{ slot: 'iron', geometry: strap }],
|
||||
extras: [{ slot: 'iron', geometry: shoe }],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue