A spiral vending shelf is a helix of wire lying on a flat tray with a gearmotor behind it. The turns divide the tray into pockets, one product per pocket. The motor turns the helix, the helix pushes everything forward by exactly one pitch per revolution, and the frontmost product falls off the lip into the delivery bin.
That is the whole machine. It has no idea what any of that looked like.
The angle that matters is not the one the motor counts
The product tips when its centre of mass passes the lip, and not before. So the question was never “does the coil move it far enough” — it always moves it exactly one pitch — it is how far round the coil has gone by the time the centre of mass gets there. Load a shelf the way anyone loads a shelf, with the front product’s face flush at the lip, and its centre of mass starts half its own depth behind:
release angle = 360 × (d/2) / p = 180 × d / p
A 34 mm bag on a 38 mm coil leaves at 161°, with a half turn to spare. A 74 mm box on the same coil leaves at 350°. Same coil, same motor, same one revolution, and one of them has nine degrees left — against a topple that takes 62° of coil to finish. It lands, correctly, after the sale has closed.
The motor stops at 360°, because it is homed by a cam switch on its own output shaft. That switch is the machine’s entire sense of what happened. It closes once per revolution in every case on this page, including the ones where nothing came out, and the controller is not lying when it prints THANK YOU — it is reporting the only thing it can see.
media/06-the-same-coil-four-products.png is one shelf at one moment with four
products in it: 161°, 284°, 351°, 388°. Nothing about the machine changes
across those four frames.
Which makes the margin a length
Release needs 180 d / p ≤ 360, so no product deeper than 2p can ever come
out in one revolution, and the slack either side of that is
margin = p − d/2 millimetres of loading error
21 mm for the 34 mm bag. 1.0 mm for the 74 mm box. That is the number
an operator should be handed and never is: not will this vend, but how
accurately does this shelf have to be filled, by hand, in a corridor, by
someone with forty more machines to do today. Load the 74 mm box two
millimetres shy of the lip and it needs 369°, and 369 is not a number the motor
has. media/07-one-millimetre-of-loading.png is that boundary walked across by
shoving the stack — −2, 0, +1, +3 mm giving 332°, 351°, 360°, 379°. The third
one releases on the exact degree the cam cuts and gets out on the last fraction
of the last degree, which is not a tolerance, it is a coin toss.
Past 2p it does not jam, which is worse
The first vend moves the product one pitch and stops with the centre of mass still behind the lip, so nothing falls. The second vend finishes the job and delivers it. From then on every vend delivers an item, and the item is the one the previous customer paid for. The machine is one purchase behind for the whole life of the load, it never reports an error, and it is right not to: a cam switch on a motor shaft cannot tell this from working.
The release angle is also a fixed point. Whatever it is on the first vend, the leftover rotation leaves the next product in exactly the same place, so it is the same angle on every vend until someone reloads. A shelf filled wrong is wrong until it is empty.
And a second failure that has nothing to do with the coil
A product released over the lip pivots about it, and while it pivots its far corner swings out into the chute. The furthest any part of it ever reaches is
r = hypot(d/2, h)
and if that exceeds the gap between the lip and the front glass, the product does not fall. It turns until the corner touches the glass and stops there, wedged between the tray lip, the coil and the door. A 44 × 66 mm box in a 62 mm chute reaches 69.6 mm and hangs at 44.6°. This is the one everybody has seen, and note what it is not about: that box releases comfortably at 208° with 16 mm of slack. The pitch was fine. The chute was the constraint, and the chute is not on any planogram.
media/08-the-corner-that-does-not-fit.png is the same 44 mm box getting
taller until the corner stops clearing.
Nothing here stalls the motor, which is why none of it registers
A coil is a screw. A 1.5 N·m gearmotor on a 38 mm pitch puts
F = 2πT / p = 248 N
along the tray, and the 44 × 66 box weighs 0.38 N. The coil has 660 times the force the job needs, so it pushes a wedged box into the glass with exactly as much complaint as it makes dropping a free one, which is none. There is no current spike to alarm on. The mechanism is too strong to be an instrument.
The fix is in the bin, and it has a clock on it
You cannot sense this at the coil. The bin is the only place in the machine where “did it fall” is observable at all, which is why every modern machine has an optical beam across the delivery chute and re-vends when it stays unbroken.
Then that fix gets its own trap, and it is the 74 mm box: it finishes toppling
after the motor has stopped, so a detection window shorter than its topple
declares the vend a failure, refunds the customer, and then delivers the product
anyway. media/09-what-the-beam-can-see.png is the beam against all three
failures — two rescued, one refunded, because a beam can see an empty bin but
cannot see why.
The kicker is that the coil which works holds more
Pitch is chosen to fit facings on the tray, and a bigger pitch looks like fewer of them. On a 430 mm tray, though, the 82 mm product on the 38 mm coil takes three pockets each — 114 mm of tray per item, 3 facings, and it never vends. Move it to a 45 mm coil: two pockets, 90 mm per item, 4 facings, and it leaves at 328° with 4 mm of slack, inside one revolution. (It still finishes toppling after the cam cuts — 32° of motor left against 62° of topple — but landing late is a different thing from never landing.) The coil chosen to squeeze more product onto the shelf fits less of it and does not deliver it.
Four loops
- turn — 34 × 30 mm on a 38 mm pitch. Leaves at 161°, 21 mm of slack, drops clean, one paid and one out.
- edge — 74 × 30 mm on the same coil. Leaves at 350° on 1.0 mm of slack, with 9° of revolution left and 62° of topple still to do, so it lands after the motor has stopped and the sale has closed.
- behind — 82 × 30 mm. Needs 388° and gets 360. Two customers pay, one product comes out, and the cam switch closes twice all the same.
- wedge — 44 × 66 mm. Releases comfortably at 208° and then swings 69.6 mm into a 62 mm chute.
Reading the frame
- the crosshair on the front product is its centre of mass, and the dotted plumb line is the lip. Everything in the piece is those two things meeting.
- the arc out of the lip is the corner’s swing,
hypot(d/2, h). Green clears the glass; where it does not, it is cut off at the door with a red mark at the contact. - the bar underneath is the coil’s rotation. The white tick at the right is where the cam switch cuts, in every case. The green tick is where the product actually leaves — and when that is past 360 the number goes red and the tick moves to where it will land on the next revolution.
- DROPPED LATE is not a threshold anyone picked.
toppleTime()integrates the fall — 216 ms for the matched bag, 275 ms for the 74 mm box — and the verdict is simply whether that fits in the revolution the product had left. - PAID / OUT is the one comparison the machine cannot make. It only reads red once a vend has closed, because owing an item mid-revolution is normal.
The demo
demo/ runs the shelf live with every input on a slider, because the whole
point is that these are inputs and the machine treats them as facts. pitch
is the coil, depth and height are the product, chute is the gap to
the glass, and loading is where a human left the front face. drop beam
fits the sensor; buy adds another customer at whatever moment you are
looking at, which is how you watch the one-behind shelf stay one behind.
The setting worth finding by hand: leave the 74 mm preset alone and walk loading from −2 to +2 mm. The shelf works, works, works, and then quietly stops delivering, and nothing on the machine’s side of the panel changes at all.
Reuse
export/coil-sheet.png— 5888×126 strip, thirty-two 184×126 frames in order:turn-0…7,edge-0…7,behind-0…7,wedge-0…7.export/coil-{turn,edge,behind,wedge}-*.png— the individual frames.export/coil@4x.png— the hero frame at 4×.source/pitch.mjsis the canonical source. The design inputs are the pitch, the chute gap, the tray length and the motor; the release angle, the margin, the revolutions needed, the swing radius, the wedge lean, the facing count and the screw force are all derived from them, so changingSHELF.pchanges the drawing and every number quoted anywhere in this creation together.validateGeometry()refuses a shelf whose pitch is not a vending coil’s, and then checks the claims rather than trusting them: that the release angle is180 d / p, thatd = 2plands exactly on the cam’s cut with zero margin, that the closed-form wedge lean puts the corner exactly on the glass, that each of the four loops actually ends the way its name says in the simulation and not just in the formula, and that the wider coil really does hold more of the deep product.- The loops are cut from the same arithmetic, not posed: every frame runs the shelf forward from t = 0 and draws where it got to.
node source/render.mjsregenerates every PNG here and rewritesdemo/pitch.jsfrom the module, so the demo cannot drift from the source.node scripts/screenshot-demo.mjsshoots the demo and is its smoke test: thirty-four assertions against the live page, two of which read the drawn pixels rather than the model — that the wedged box is painted right up against the drawn glass, and that a delivered one is painted resting on the drawn floor of the bin.
Both need Playwright from site/node_modules.
What is modelled and what is drawn
Worth saying, because the numbers above are only as good as this:
- The push is quasi-static and the topple is not. The coil drives the product flat along the tray at exactly one pitch per revolution, which is what a screw does. Once the centre of mass crosses the lip the coil’s continued advance sets a kinematic floor under the lean — this is what actually starts the topple, since at the release instant the centre of mass is directly over the lip and the gravity torque is zero — and from there gravity integrates the rotation about the lip. The coil is far too strong to be slowed by the product, so it is modelled as a position source rather than a force. That is also why the screw force is quoted as a ratio and not used in the integration.
- The product is a rigid box and the lip is a knife edge. Real product is a bag that deforms, real lips have a rail with a radius, and neither is frictionless. Friction and slip both move the contact point forward as the product turns, which pushes its corner further into the chute — so the wedge criterion here is, if anything, optimistic.
- The pivot cannot slide. The box turns about the lip until 90° and then falls. A real one starts sliding off part-way through, which gets it out faster; it does not change which products make it and which do not.
- The bin is drawn as the chute continued downwards, and a real delivery well is wider and deeper than the gap the product fell through. The landed product is nudged to stay inside the drawn one. Nothing depends on this: the beam is a horizontal line and the outcome is decided above the lip.
- The headline numbers are the honest ones. 161°, 350°, 388°, 208°, 2p =
76 mm, 21 mm, 1.0 mm, 69.6 mm, 44.6°, 248 N, 660:1 and the 3-versus-4 facings
all come out of the geometry in
source/pitch.mjsand are printed byrender.mjson every run. If you change the pitch or the chute, the prose above goes stale and the console does not. - The loading offset is a free parameter, set to zero for every loop, which is the most generous assumption in the piece. It is also the only quantity in the machine set by a person, in a corridor, in a hurry, and it is the one the whole thing turns on.