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,7 +1,8 @@
|
|||
/**
|
||||
* @medieval-kit/wicker-basket
|
||||
*
|
||||
* A basket woven from willow rods, optionally filled with produce.
|
||||
* A basket woven from willow rods, with a bow handle, optionally filled with
|
||||
* produce.
|
||||
*
|
||||
* The model in the kit that imitates "how it was made" the most, because in
|
||||
* wickerwork the form and the making are the same thing: a basket is the
|
||||
|
|
@ -18,6 +19,32 @@
|
|||
* next one goes in. That is exactly what a real weave is, and it costs not one
|
||||
* single extra triangle.
|
||||
*
|
||||
* HISTORY, because this model is on its third body:
|
||||
*
|
||||
* v2 read as a bowl of fired clay, and the blind critique named the three
|
||||
* reasons. (1) No handle: the reference's single most identifying feature is a
|
||||
* bow handle arcing well above the rim, and without it the silhouette is a
|
||||
* bowl, not a carrying basket. (2) The wall was a cone — base half the rim —
|
||||
* where the reference is a near-vertical drum whose inward curve lives in the
|
||||
* bottom fifth. `radiusAt` now has a knee at t=0.2: eased curve below it,
|
||||
* near-vertical above. Because the wall is no longer a straight line, a
|
||||
* one-piece leaning stake no longer follows it; each stake is two prisms, a
|
||||
* steep lower one under the knee and a near-vertical upper one, overlapped at
|
||||
* the joint. (3) Four fat courses with a deep sawtooth read as stacked blocks;
|
||||
* there are now more, thinner courses and the undulation amplitude is a third
|
||||
* of what it was. The colour was salmon because the oak entry is saturated and
|
||||
* every lift pushed it toward peach; the `willow` wrapper below desaturates
|
||||
* every tint toward grey-brown instead of lightening it (lifting was tried in
|
||||
* v2 and documented as the salmon mistake — do not go back to it).
|
||||
*
|
||||
* The critique also caught a modelling error: the lowest points of the model
|
||||
* were the dips of the bottom course's zigzag, so the basket would rock. The
|
||||
* base is now a foot ring standing 2% of the height below the wall, with a
|
||||
* flat bottom cap, and its top plate is wide enough to reach under the bottom
|
||||
* course and the stakes — an earlier draft of this fix left the base plate at
|
||||
* the nominal base radius, which the flaring wall never touches, and the base
|
||||
* came out a second floating component.
|
||||
*
|
||||
* The produce sits in its own slot and takes its colour from the `hue` field:
|
||||
* the same model can give you a basket of apples, turnips or cabbages. Do NOT
|
||||
* look for tomatoes — they come from the Americas and do not enter European
|
||||
|
|
@ -26,6 +53,7 @@
|
|||
import { Color, type BufferGeometry } from 'three'
|
||||
|
||||
import {
|
||||
arcBarGeometry,
|
||||
bandGeometry,
|
||||
createKitModel,
|
||||
createTinter,
|
||||
|
|
@ -56,15 +84,15 @@ export interface WickerBasketConfig {
|
|||
}
|
||||
|
||||
export const wickerBasketDefaults: WickerBasketConfig = {
|
||||
// Shallower and fuller. The reference is a bowl wider than it is deep,
|
||||
// heaped until the fruit mounds over the rim -- which is the only state a
|
||||
// fruit basket is ever drawn in.
|
||||
height: 0.16,
|
||||
radius: 0.17,
|
||||
taper: 0.26,
|
||||
// Base 0.79 of the rim. The reference's wall is a near-vertical drum; the
|
||||
// taper is small and, through the knee in `radiusAt`, spent almost entirely
|
||||
// in the bottom fifth.
|
||||
taper: 0.21,
|
||||
stakes: 11,
|
||||
rows: 6,
|
||||
produce: 15,
|
||||
rows: 9,
|
||||
produce: 12,
|
||||
hue: 0.02,
|
||||
seed: 97,
|
||||
}
|
||||
|
|
@ -78,14 +106,60 @@ export function createModel(overrides: Partial<WickerBasketConfig> = {}) {
|
|||
slots: ['oak', 'produce'],
|
||||
build: ({ config, random }) => {
|
||||
const tint = createTinter(random)
|
||||
const half = config.height / 2
|
||||
// Floors, not trust: several of these appear in denominators or in the
|
||||
// sagitta of the handle arc, and a zero patched in from outside must not
|
||||
// become a NaN that the occlusion bake then smears over every part.
|
||||
const H = Math.max(0.05, config.height)
|
||||
const R = Math.max(0.04, config.radius)
|
||||
const half = H / 2
|
||||
const stakes = Math.max(5, Math.round(config.stakes))
|
||||
const rows = Math.max(1, Math.round(config.rows))
|
||||
const bottomRadius = config.radius * (1 - config.taper)
|
||||
const withy = config.height * 0.05 // rod thickness
|
||||
const amplitude = 0.055 // in-out travel of the weave
|
||||
const taper = Math.min(0.7, Math.max(0, config.taper))
|
||||
const bottomRadius = R * (1 - taper)
|
||||
const rod = H * 0.026 // horizontal withy thickness (halved from v2)
|
||||
const stakeThick = H * 0.021 // uprights keep their old, visible section
|
||||
// A third of v2's 0.055: the courses should read as bent willow, and at
|
||||
// 0.055 they read as stacked blocks. The zigzag survives at 0.02 because
|
||||
// consecutive rows still move in antiphase.
|
||||
const amplitude = 0.02
|
||||
|
||||
const radiusAt = (t: number): number => bottomRadius + (config.radius - bottomRadius) * t
|
||||
/**
|
||||
* Willow is not oak-coloured. The `oak` palette entry has the right hue
|
||||
* but weathered willow is greyer, and v2 proved that LIFTING the entry
|
||||
* moves it toward peach, not toward grey (lightness holds saturation).
|
||||
* So every basket tint passes through here and has its saturation set
|
||||
* against the reference rather than inherited. The tinter returns a new Color per call, so mutating it in
|
||||
* place is safe.
|
||||
*
|
||||
* The factor was a third, and a third was right when `oak` was a saturated
|
||||
* orange-brown. That entry has since been measured against its references
|
||||
* and pulled toward grey itself, so taking another third off it greyed the
|
||||
* basket 0.20 below its own reference. Two corrections for the same fault,
|
||||
* applied one on top of the other. Measured back the other way, the
|
||||
* reference willow is in fact WARMER than the corrected oak, so the factor
|
||||
* is now above one.
|
||||
*/
|
||||
const willow = (lift: number, spread = 1.2): Color => {
|
||||
const c = tint('oak', lift, spread)
|
||||
const hsl = { h: 0, s: 0, l: 0 }
|
||||
c.getHSL(hsl)
|
||||
c.setHSL(hsl.h, Math.max(0, hsl.s * 1.3), hsl.l)
|
||||
return c
|
||||
}
|
||||
|
||||
/**
|
||||
* Wall profile. Near-vertical drum over the top four fifths, the whole
|
||||
* inward curve eased into the bottom fifth — the knee. `rKnee` is floored
|
||||
* at `bottomRadius` so a cylinder (taper 0) stays a cylinder instead of
|
||||
* denting inward at the knee.
|
||||
*/
|
||||
const knee = 0.2
|
||||
const rKnee = Math.max(bottomRadius, R * 0.97)
|
||||
const radiusAt = (t: number): number => {
|
||||
if (t >= knee) return rKnee + (R - rKnee) * ((t - knee) / (1 - knee))
|
||||
const s = t / knee
|
||||
return bottomRadius + (rKnee - bottomRadius) * s * (2 - s)
|
||||
}
|
||||
|
||||
/**
|
||||
* The transform that turns a band into a weave: every vertex moves
|
||||
|
|
@ -110,170 +184,186 @@ export function createModel(overrides: Partial<WickerBasketConfig> = {}) {
|
|||
}
|
||||
|
||||
// --- Vertical stakes -----------------------------------------------------
|
||||
// They run THROUGH the weave: because the horizontal hoops wind in front
|
||||
// of one and behind the next, the stakes hide here and show up there.
|
||||
// Two prisms per stake, because the wall is no longer a straight line: a
|
||||
// steep lower segment rounds through the knee, a near-vertical upper one
|
||||
// runs to the rim. They overlap at the joint; both far ends are buried —
|
||||
// the bottom behind the lowest course, the top inside the rim ring — so
|
||||
// only the lower bottom is capped and the rest of the tube ends stay open
|
||||
// where nothing can see them.
|
||||
const pieces: BufferGeometry[] = []
|
||||
const yKnee = -half + knee * H
|
||||
const yStakeBottom = -half + H * 0.012
|
||||
const yStakeTop = half - H * 0.02
|
||||
const overlap = H * 0.02
|
||||
const rLow = radiusAt(0.012)
|
||||
const segment = (
|
||||
y0: number, r0: number, y1: number, r1: number, angle: number,
|
||||
colour: Color, options: { capTop?: boolean; capBottom?: boolean },
|
||||
): BufferGeometry => {
|
||||
const length = Math.hypot(y1 - y0, r1 - r0)
|
||||
const piece = prismGeometry(
|
||||
stakeThick * 0.52, stakeThick * 0.46, length, 4, [0, 0, 0], colour, options,
|
||||
)
|
||||
piece.rotateX(Math.atan2(r1 - r0, y1 - y0))
|
||||
piece.rotateY(angle)
|
||||
const mid = (r0 + r1) / 2
|
||||
piece.translate(Math.sin(angle) * mid, (y0 + y1) / 2, Math.cos(angle) * mid)
|
||||
return piece
|
||||
}
|
||||
for (let i = 0; i < stakes; i += 1) {
|
||||
const angle = (i / stakes) * Math.PI * 2
|
||||
const stake = prismGeometry(
|
||||
withy * 0.42, withy * 0.36, config.height * 1.02, 4,
|
||||
[0, 0, 0], tint('oak', -0.09 + 0.04, 1.2),
|
||||
)
|
||||
// Bend first, move second: in a tapering basket the stakes lean too.
|
||||
stake.rotateX(Math.atan2(config.radius - bottomRadius, config.height))
|
||||
stake.rotateY(angle)
|
||||
const mid = (bottomRadius + config.radius) / 2
|
||||
stake.translate(Math.sin(angle) * mid, 0, Math.cos(angle) * mid)
|
||||
pieces.push(stake)
|
||||
const shade = willow(-0.05, 1.2)
|
||||
pieces.push(segment(
|
||||
yStakeBottom, rLow, yKnee + overlap, rKnee, angle, shade,
|
||||
{ capTop: false, capBottom: true },
|
||||
))
|
||||
pieces.push(segment(
|
||||
yKnee - overlap, rKnee, yStakeTop, R, angle, shade,
|
||||
{ capTop: false, capBottom: false },
|
||||
))
|
||||
}
|
||||
|
||||
// Willow, not straw.
|
||||
//
|
||||
// The basket drew its colour from the `straw` palette entry and rendered
|
||||
// at hue 41 with saturation 0.59; a photograph of a wicker basket sits at
|
||||
// hue 29, saturation 0.50. It read as bright yellow plastic. The tuned
|
||||
// `oak` entry is hue 26 / saturation 0.53 -- almost exactly the
|
||||
// measurement -- and willow IS a wood, so the material slot moves with
|
||||
// the colour. The lift keeps it at the pale, peeled end of oak rather
|
||||
// than the dark structural end.
|
||||
//
|
||||
// The lift is 0.04 and that is nearly arbitrary: sweeping it over 0.00,
|
||||
// 0.04 and 0.08 moved the measured hue and saturation by at most 0.01.
|
||||
// It was first set to 0.12 on the reasoning that willow is paler than
|
||||
// structural oak, which is true and which made the basket salmon pink --
|
||||
// raising HSL lightness holds saturation, so a saturated brown climbs
|
||||
// towards peach rather than towards weathered willow. Against the
|
||||
// reference the straw entry was off by 12 degrees of hue and 0.09
|
||||
// saturation; oak is off by 7 and 0.03.
|
||||
|
||||
// --- Horizontal weave ----------------------------------------------------
|
||||
// TWO segments per vertical stake: `cos(stakes·θ)` is sampled exactly once
|
||||
// positive and once negative on every stake, so the wave is fully resolved
|
||||
// with the fewest possible triangles. Four segments gave a smoother wave
|
||||
// but doubled the triangles per hoop and pushed the basket past the
|
||||
// lowpoly budget.
|
||||
// with the fewest possible triangles.
|
||||
//
|
||||
// The INNER FACE of the hoops is not generated. In its place there is a
|
||||
// single-piece inner liner (below): six separate inner surfaces for six
|
||||
// hoops cost ~800 triangles, the liner costs 44, and from the inside the
|
||||
// difference is invisible.
|
||||
// The weave stops BELOW the rim. Once the rows were made tall enough to
|
||||
// meet each other they grew into the rim's band, and the outermost point
|
||||
// of the undulation met the rim's inner facets in the same plane. The
|
||||
// rim is a separate, thicker rod laid over the finished weave, so the
|
||||
// weave ending under it is how the object is actually made.
|
||||
const weaveSpan = config.height * 0.9
|
||||
// single-piece inner liner (below). The weave stops BELOW the rim: the rim
|
||||
// is a separate, thicker rod laid over the finished weave, which is how
|
||||
// the object is actually made.
|
||||
const weaveSpan = H * 0.92
|
||||
for (let r = 0; r < rows; r += 1) {
|
||||
const t = (r + 0.5) / rows
|
||||
const y = -half + weaveSpan * t
|
||||
// The band height is derived from the row count so that rows always
|
||||
// MEET. It used to be a flat 0.11 of the height while the spacing
|
||||
// between rows is 1/rows -- 0.167 at the default six -- which left a
|
||||
// 12 mm gap you could see straight through to the contents. That is a
|
||||
// slatted crate, not a weave. Rows now overlap slightly; they do not
|
||||
// z-fight because consecutive rows are undulated half a wave apart, so
|
||||
// where one bulges out its neighbour is tucked in.
|
||||
// Alternate rows sit slightly proud of and slightly behind each other.
|
||||
// This is how a weave really goes together -- the weaver's rod passes
|
||||
// outside one stake and inside the next, so no two rows lie on the
|
||||
// same cylinder -- and it is also what makes the overlap above safe.
|
||||
// Undulating consecutive rows in antiphase is not enough on its own:
|
||||
// a sine crosses zero, and at those nodes both rows returned to
|
||||
// exactly `radiusAt(t)`. With a cylindrical basket (taper 0) every row
|
||||
// shares that radius, so the overlapping bands met in coplanar faces
|
||||
// at every node.
|
||||
const lean = r % 2 === 0 ? withy * 0.2 : -withy * 0.2
|
||||
// Rows overlap slightly so they always MEET; they do not z-fight
|
||||
// because consecutive rows are undulated half a wave apart AND lean
|
||||
// alternately in and out, so no two rows lie on the same cylinder.
|
||||
const lean = r % 2 === 0 ? rod * 0.2 : -rod * 0.2
|
||||
const ring = bandGeometry(
|
||||
radiusAt(t) + lean, y, (weaveSpan / rows) * 1.06, withy * 0.8, stakes * 2,
|
||||
tint('oak', 0.04 + jitter(random, 0.07), 1.2),
|
||||
radiusAt(t) + lean, y, (weaveSpan / rows) * 1.06, rod * 0.8, stakes * 2,
|
||||
willow(0.02 + jitter(random, 0.05)),
|
||||
)
|
||||
// The phase shifts by half a wave on every row: the next row going in
|
||||
// where the previous one came out is what makes a weave a weave.
|
||||
pieces.push(undulate(ring, r % 2 === 0 ? 0 : Math.PI))
|
||||
}
|
||||
|
||||
// Inner liner: the single surface that closes off the back of the weave.
|
||||
// Wound in reverse so the normals face the axis.
|
||||
// Wound in reverse so the normals face the axis. It follows the wall
|
||||
// profile at 0.955 of it — inside the courses, outside the base plate,
|
||||
// its top edge buried in the rim ring.
|
||||
pieces.push(flipGeometry(latheGeometry([
|
||||
{ y: -half + config.height * 0.03, radius: bottomRadius * (1 - amplitude) },
|
||||
{ y: half - config.height * 0.02, radius: config.radius * (1 - amplitude) },
|
||||
], stakes * 2, [0, 0, 0], tint('oak', 0.06, 1.1), {
|
||||
{ y: -half + H * 0.04, radius: radiusAt(0.05) * 0.955 },
|
||||
{ y: yKnee, radius: rKnee * 0.955 },
|
||||
{ y: half - H * 0.02, radius: R * 0.955 },
|
||||
], stakes * 2, [0, 0, 0], willow(-0.02, 1.1), {
|
||||
capTop: false,
|
||||
capBottom: false,
|
||||
})))
|
||||
|
||||
// --- Base ----------------------------------------------------------------
|
||||
// --- Base and foot -------------------------------------------------------
|
||||
// One lathe: a foot ring the basket actually stands on (flat bottom cap
|
||||
// 2% of the height below the wall, so the bottom course's zigzag dips
|
||||
// stay clear of the ground), swelling slightly, then flaring up and OUT
|
||||
// to a floor plate at `radiusAt(0.03)` — wide enough to pass under the
|
||||
// bottom course and the stake feet, which is what keeps the base part of
|
||||
// the same solid as the wall.
|
||||
const footR = bottomRadius * 0.98
|
||||
const yGround = -half - H * 0.02
|
||||
const yFloor = -half + H * 0.06
|
||||
pieces.push(latheGeometry([
|
||||
{ y: -half - config.height * 0.01, radius: bottomRadius * 0.94 },
|
||||
{ y: -half + config.height * 0.05, radius: bottomRadius * 0.99 },
|
||||
], stakes * 2, [0, 0, 0], tint('oak', -0.14 + 0.04, 1.2), { capTop: true }))
|
||||
{ y: yGround, radius: footR * 0.95 },
|
||||
{ y: yGround + H * 0.03, radius: footR * 1.03 },
|
||||
{ y: yFloor, radius: radiusAt(0.03) * 0.99 },
|
||||
], stakes * 2, [0, 0, 0], willow(-0.06, 1.2)))
|
||||
|
||||
// --- Rim -----------------------------------------------------------------
|
||||
// The thick bend that finishes the weave. It is the most visible detail on
|
||||
// the basket, and without it the edge looks "cut off".
|
||||
const rim = mergeColoured([
|
||||
// Thickness withy*2.1, not 1.5, and the reason is arithmetic rather
|
||||
// than taste. At 1.5 the rim's inner surface landed at
|
||||
// radius*1.015 - withy*1.5 = 0.16055, and the inner liner sits at
|
||||
// radius*(1 - amplitude) = 0.16065 -- a tenth of a millimetre apart,
|
||||
// by coincidence. On a cylindrical basket (taper 0) those are the same
|
||||
// 22-sided prism and every facet of it z-fought. The thicker rod
|
||||
// carries the inner face clearly past the liner, and it is the truer
|
||||
// shape anyway: the rim is the heaviest rod in a basket, bent over the
|
||||
// finished weave, and it stands proud on both sides.
|
||||
bandGeometry(config.radius * 1.015, half - config.height * 0.03,
|
||||
config.height * 0.1, withy * 2.1, stakes * 2,
|
||||
tint('oak', 0.07 + 0.04, 1.2), { inner: true }),
|
||||
])
|
||||
// The thick bend that finishes the weave: the heaviest rod in a basket,
|
||||
// bent over the finished weave, standing proud on both sides. Everything
|
||||
// that must vanish — liner top, stake tops, handle feet — ends inside
|
||||
// this solid.
|
||||
const rimY = half - H * 0.03
|
||||
const rimHalfH = H * 0.05
|
||||
const rimOuter = R * 1.02
|
||||
const rimThick = rod * 3.5
|
||||
const rimPieces: BufferGeometry[] = [
|
||||
bandGeometry(rimOuter, rimY, rimHalfH * 2, rimThick, stakes * 2,
|
||||
willow(0.05, 1.2), { inner: true }),
|
||||
]
|
||||
|
||||
// --- Handle --------------------------------------------------------------
|
||||
// The bow that makes it a carrying basket rather than a bowl. A flat
|
||||
// band — twice as wide in the plane of the loop as it is deep — arcing
|
||||
// rim to rim across the diameter and peaking about 0.6 of the body
|
||||
// height above the rim. The arc is solved from its chord and sagitta, so
|
||||
// the feet land at mid-wall of the rim ring (inside the solid, not
|
||||
// butted against the outer face) whatever the proportions are patched to.
|
||||
const handleWide = Math.min(rimThick * 0.9, R * 0.09)
|
||||
const handleDeep = handleWide * 0.5
|
||||
const footY = rimY - rimHalfH * 0.4
|
||||
const footX = rimOuter - rimThick * 0.5
|
||||
const peak = rimY + H * 0.62
|
||||
const sagitta = Math.max(0.01, peak - footY)
|
||||
const arcR = (footX * footX + sagitta * sagitta) / (2 * sagitta)
|
||||
const arcCentreY = peak - arcR
|
||||
const footAngle = Math.atan2(footY - arcCentreY, footX)
|
||||
const handle = arcBarGeometry(
|
||||
arcR, handleWide, footAngle, Math.PI - footAngle, 7,
|
||||
[0, arcCentreY, 0], willow(0.03, 1.1),
|
||||
)
|
||||
// The bar comes out square; flattening it in Z (the out-of-loop
|
||||
// direction) turns it into the band. Scale is safe here because the arc
|
||||
// is only ever offset in Y.
|
||||
handle.scale(1, 1, handleDeep / handleWide)
|
||||
rimPieces.push(handle)
|
||||
|
||||
// Lashings: a short sleeve wrapped around each handle foot just above
|
||||
// the rim, its lower end sunk into the rim ring — the bindings that hold
|
||||
// a real handle. The sleeve is held by the handle passing through it.
|
||||
for (const side of [0, 1] as const) {
|
||||
const a = side === 0 ? footAngle + 0.12 : Math.PI - footAngle - 0.12
|
||||
const sleeve = prismGeometry(
|
||||
handleWide * 0.72, handleWide * 0.66, H * 0.2, 4, [0, 0, 0],
|
||||
willow(-0.03, 1.1),
|
||||
)
|
||||
sleeve.rotateZ(a)
|
||||
sleeve.translate(Math.cos(a) * arcR, arcCentreY + Math.sin(a) * arcR, 0)
|
||||
rimPieces.push(sleeve)
|
||||
}
|
||||
|
||||
// --- Contents ------------------------------------------------------------
|
||||
const count = Math.max(0, Math.round(config.produce))
|
||||
const contents: BufferGeometry[] = []
|
||||
const hue = ((config.hue % 1) + 1) % 1
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const size = config.radius * (0.2 + random() * 0.07)
|
||||
const size = R * (0.2 + random() * 0.07)
|
||||
// Apple profile: dimpled top and bottom, wide in the middle.
|
||||
const fruit = latheGeometry([
|
||||
{ y: -size * 0.86, radius: size * 0.3 },
|
||||
{ y: -size * 0.6, radius: size * 0.78 },
|
||||
{ y: 0, radius: size },
|
||||
{ y: size * 0.58, radius: size * 0.82 },
|
||||
{ y: size * 0.84, radius: size * 0.34 },
|
||||
] as Level[], 7, [0, 0, 0], new Color().setHSL(
|
||||
{ y: -size * 0.82, radius: size * 0.36 },
|
||||
{ y: -size * 0.45, radius: size * 0.9 },
|
||||
{ y: size * 0.15, radius: size * 0.97 },
|
||||
{ y: size * 0.8, radius: size * 0.4 },
|
||||
] as Level[], 6, [0, 0, 0], new Color().setHSL(
|
||||
(hue + jitter(random, 0.03) + 1) % 1,
|
||||
0.52 + random() * 0.2,
|
||||
0.3 + random() * 0.12,
|
||||
))
|
||||
|
||||
// Placement: golden-angle spiral plus a distance growing with the square
|
||||
// root, and — critically — the heap RESTS ON THE BASE.
|
||||
//
|
||||
// It used to be positioned relative to the rim, which is wrong for any
|
||||
// basket deeper than a fruit: the produce hung near the mouth with a
|
||||
// gap underneath it. Fruit sits at the bottom and piles up from there;
|
||||
// if there is more of it than the basket holds, the heap rises past
|
||||
// the rim, which is also what really happens.
|
||||
// Placement: golden-angle spiral plus a distance growing with the
|
||||
// square root, and the heap RESTS ON THE FLOOR PLATE — fruit piles up
|
||||
// from the bottom, and with enough of it the heap mounds past the rim.
|
||||
const angle = i * 2.399963
|
||||
const ring = Math.sqrt((i + 0.4) / count)
|
||||
const inner = Math.max(size, bottomRadius * 0.92 - size * 0.6)
|
||||
const spread = inner * ring
|
||||
// Fewer fruit per layer means MORE layers, and it is the layer count
|
||||
// that decides whether the heap ever reaches the mouth. At 0.55 the
|
||||
// default nine apples formed two layers topping out 12 cm below a rim
|
||||
// 10 cm up: correctly resting on the base, and completely invisible.
|
||||
// Resting on the base was the fix for an earlier bug where the produce
|
||||
// hung level with the rim over a gap; the fix was right and the
|
||||
// consequence -- that a deep basket then needs enough fruit to fill it
|
||||
// -- was not followed through.
|
||||
const layer = Math.floor(i / Math.max(3, Math.round(count * 0.38)))
|
||||
fruit.rotateX(jitter(random, 0.6))
|
||||
fruit.rotateZ(jitter(random, 0.6))
|
||||
fruit.translate(
|
||||
Math.sin(angle) * spread,
|
||||
// Base top + one radius = resting on the floor of the basket.
|
||||
// Layers nest at 0.95 of a diameter, not 1.5: stacked fruit settles
|
||||
// into the gaps of the layer below rather than sitting on top of it.
|
||||
-half + config.height * 0.05 + size * (0.92 + layer * 0.95)
|
||||
// Floor plate + one radius = resting on the floor of the basket.
|
||||
// Layers nest at 0.95 of a diameter: stacked fruit settles into the
|
||||
// gaps of the layer below rather than sitting on top of it.
|
||||
yFloor + size * (0.92 + layer * 0.95)
|
||||
- ring * size * 0.28 + jitter(random, size * 0.08),
|
||||
Math.cos(angle) * spread,
|
||||
)
|
||||
|
|
@ -282,7 +372,7 @@ export function createModel(overrides: Partial<WickerBasketConfig> = {}) {
|
|||
|
||||
return {
|
||||
weave: { slot: 'oak' as const, geometry: mergeColoured(pieces) },
|
||||
rim: { slot: 'oak' as const, geometry: rim },
|
||||
rim: { slot: 'oak' as const, geometry: mergeColoured(rimPieces) },
|
||||
contents: contents.length > 0
|
||||
? { slot: 'produce' as const, geometry: mergeColoured(contents) }
|
||||
: undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue