Isaac Chan · build analysis 3D printing Single file No network calls

Shrinkage — the number you can't measure, and the fit it doesn't fix

Two HTML calculators and one CLI, built around a single admission: a 40 mm cube cannot resolve the shrinkage it is supposed to measure. The first tool answers “what do I put in my slicer” and answers 100 % more often than any other number. The second answers “how do I make it go in slowly” — which is not a shrinkage problem at all, but a taper.

Shrink page
18,556 B
Fit page
48,434 B
External calls
0
Noise floor
0.065 mm
Measured shrink
0.050 %
Verdict
100 % scale
The constraint that shaped everything

“can you make it that it only calculates the shrinkage percentage” — then, a day later, “dont i need to only change the shrinkage”. The tool was asked twice to be smaller than it was, and the second question is the interesting one: it assumes shrinkage is the knob that fixes a fit. It isn't.

1 · What it is

A shrinkage calculator that accepts several caliper readings per axis, uses the smallest reading on each axis as the true size, averages those minima into one measured size, and then decides whether the difference from nominal is real. If it isn't, the page refuses to hand over a scale factor: the big tile reads 100.00 % and the arithmetic it declined to apply sits underneath it in grey. That refusal is the feature. Every other shrink calculator on the internet multiplies your number by a constant and smiles.

Open the shrink calculator The mating-pocket calculator Full mirror Build spec

2 · Version ladder — what each pass cut

V1

Four constant-clearance fits

Slip / snug / glue / press, each a fixed gap per side, plus a live SVG cross-section. It answered “what clearance do I use” and produced a number that was wrong for the question actually being asked, because all four presets are the same clearance at the mouth and at the floor. A constant gap can only drop in or jam; neither is “goes in slowly”.

V1.5

Multi-reading inputs, a lock fit, and an insertion ramp

Calipers on a printed face are noisy, so one reading per axis became many, and the datum became the minimum (a tilted caliper only ever reads big). A fifth fit — lock — made the bite a linear ramp over the full depth instead of a constant interference, and a “How it goes in” chart plots gap against insertion distance so the free travel and the bite zone are visible rather than described. What was cut: the illusion that four presets covered the space.

V2

A second page that does one thing

Rather than stripping the fit tool, the shrink maths moved into its own 18 KB page: preset, nominal, three reading boxes, four numbers out, no diagrams. What was cut: pockets, lead-ins, ramps, warnings, and the entire notion that a shrink number implies a fit. One page, one job.

3 · The request path

# one reading set, start to verdict readings "40.05 40.00 40.04" · "40.03 40.00 39.99" · "40.07 40.09 40.06" │ ├─ parse ── split on space/comma/newline, drop non-numeric tokens │ (a bad token is ignored, never NaN, never 0) ├─ per axis ── n, mean, min, spread ← min is the datum ├─ measured = mean(axis minima) ← 39.980 mm ├─ shrink = (nom − meas) / nom × 100 ← 0.050 % ├─ scale = nom / meas × 100 ← 100.050 % ├─ spread = max(axis spread) ← 0.090 mm └─ noise = max(0.05, spread/2 + 0.02) ← 0.065 mm verdict order preset → no reading → under noise → oversized → real shrink └─ 0.020 < 0.065 mm → "no usable shrink" → tile prints 100.00 % grey sub-line: "raw 100.050 % — under noise"

4 · Contract tables

The five formulas

QuantityFormulaWhy it matters
Shrink(nom − meas) / nom × 100Signed on purpose: negative means the part came out bigger, which is not shrinkage and must not be “corrected” with a factor below 100.
Scalenom / meas × 100Not 100 + shrink. They differ in the third decimal, and on a scaled pair the difference lands in the clearance.
Measuredmean(min per axis)Minima per axis, then mean across axes. Averaging all readings instead pulls the datum up by every seam bead and every tilted jaw.
Noise floormax(0.05, spread/2 + 0.02)Your own data sets the bar the effect has to clear. 0.05 mm is the absolute floor for a hand-held caliper.
Pocket floormax_reading + 2(clearance + bias)Uses the largest reading, because the first-layer flare at the bottom of the cube is what enters the mouth first.

Verdict states — the page never shows a number it doesn't believe

StateTriggerOutput
Presetno readingsmaterial constant, labelled “estimate only”
No usable shrink|dev| ≤ noise floor100.00 %, raw value greyed beneath
Oversizedmeasured > nominalrefuses a sub-100 % scale; names flow / first layer as the real cause
Real shrinkdev > noise floorshrink % + scale %, “apply to BOTH parts”
Only one axis1 of 3 filledstill calculates; coverage is a note, never the verdict
Junk / zero nominalno parseable token / nom = 0falls back to preset / prints “—” — no NaN, no division by zero

5 · Anatomy — three decisions that carry the tool

The smallest reading, not the average

An outside measurement with a tilted caliper reads too big, never too small — the jaws bridge the part at an angle. So the minimum of several readings is the closest thing to the true flat-to-flat width, while the average is the true width plus a systematic bias. Holes invert: rock and keep the largest. The page implements the outside rule, and it is why a 0.090 mm spread doesn't move the answer as far as it would if the readings were averaged.

A noise floor made of the user's own data

The threshold isn't a magic number: it is half the widest spread across the axes, plus 0.02 mm for caliper and handling slop, floored at 0.05 mm. Type three tight readings and the bar drops; type three sloppy ones and the tool demands more evidence before it will move the slicer. The measurement quality scores itself.

Lock: interference that ramps instead of arriving

The lock fit ramps the gap from +0.16 mm/side at the mouth to −0.04 mm/side at the floor across 20 mm. The cube is guided for 16 mm, then the last 4 mm tightens at 10 µm per mm of push. Same total interference as a press fit, spread over a distance instead of an instant — which is the entire difference between “mallet” and “firm push”.

The taper has to equal the depth

If the taper stops before the floor, the parallel section below it becomes a hard stop and the part jams exactly where the bite ends. The tool warns when the bite zone falls under 1.5 mm (a jolt), when the bite exceeds 0.09 mm/side (walls splay or crack), and when the mouth is opened so wide that the first few millimetres simply fall in loose.

6 · Worked example — the cube this was built on

AxisReadingsMinSpreadShrink vs 40.00
X1 reading, 40.00040.0000.0000.000 %
Y3 readings, min 39.91039.9100.0900.225 %
Z3 readings, min 40.03040.0300.030−0.075 %
measuredmean of minima = 39.980 mmnoise 0.0650.050 %
The answer the tool gives: put 100 % in the slicer. 0.020 mm of difference sits under a 0.065 mm noise floor built from the user's own 0.090 mm spread, and real PLA shrinks 0.2–0.5 % — 0.08–0.20 mm on a 40 mm cube. The measured 0.020 mm is a quarter of the smallest plausible value: two caliper ticks. Nothing to correct.

The same cube also produced the fit numbers, and they come from a different datum on purpose: the pocket uses the largest reading (40.05 mm, the flared bottom), because that flare is the part that meets the mouth first. Pocket floor 39.97 mm, mouth 40.37 mm, taper 0.57°/side over 20 mm, corners R1.50 cube / R1.55 pocket.

$ dt filament fit 40.05 --fit lock --clearance -0.04 --hole-bias 0 \ --lead taper --lead-allow 0.20 --taper-len 20 Pocket for the printed cube (40.050 mm) clearance/side : -0.040 mm (-0.08 mm total) hole bias/side : 0.000 mm cavity span : 39.970 mm Lead-in (taper) mouth span : 40.370 mm (+0.20/side) taper : 0.57deg/side (1.14deg total) over 20.0 mm corner r : 1.50 mm cube / 1.55 mm pocket Insertion ramp mouth gap/side : +0.160 mm land gap/side : -0.040 mm (negative = bite) free travel : 0 -> 16.0 mm bite zone : 4.0 mm at 10 um/mm

7 · Resolution — why a 40 mm cube can't answer this question

A caliper reads to 0.01 mm. On a 40 mm witness that tick is 0.025 % — half of the effect being measured — and hand-held repeatability is nearer 0.05 mm (0.125 %). No amount of averaging inside one cube escapes that: the instrument, not the sample size, is the limit.

Witness length1 caliper tick resolvesPLA 0.3 % appears as
40 mm (this cube)0.025 %0.12 mm — 12 ticks
100 mm bar0.010 %0.30 mm — 30 ticks
200 mm bar0.005 %0.60 mm — 60 ticks

This is why the page carries a material preset table (PLA 0.30 %, silk 0.35 %, CF 0.20 %, PETG 0.35 %, ABS 0.70 %) instead of pretending a cube produced a constant. The presets are literature averages: the tool tells you which way your part moved, the table tells you what the polymer usually does.

8 · Why shrinkage doesn't fix a fit

The question that started the second page: “don't I only need to change the shrinkage?” A scale factor multiplies everything, gaps included. Scale both the cube and the pocket to 100.3 % and the 0.16 mm mouth gap becomes 0.1605 mm — a 0.5 µm change in a fit that is measured in tenths of a millimetre. Correction preserves the fit by construction; that is what makes it a correction.

KnobControlsMove it when
Slicer scaleoverall size accuracya long witness proves the spool shrinks
Pocket mouth / floorhow it feels going inalways — this is the entry
Taper lengthhow gradual the bite isthe push is too sudden
Clearancebite force at the seatit holds too hard / not at all

The five fits, and the one that ramps

FitGap at mouthGap at floorBehaviour
slip+0.35+0.35thumb pressure, never binds
snug+0.25+0.25high spots touch, jams halfway
glue+0.20+0.20rattles; needs adhesive
press−0.05−0.05interference arrives all at once — mallet
lock+0.16−0.04guided, then progressive bite — firm push

Gaps in mm/side, including the 0.10 mm hole bias on the first four. Only the last row has a different gap at each end; the rest are constant, which is exactly why they can only drop in or jam.

9 · Failure modes

SymptomCauseHandling
NaN on screentext pasted into a number boxtokenizer rejects it; the token is dropped, not zeroed
0.000 % with junk inputnothing parseablefalls back to the material preset, labelled as an estimate
“— %”nominal blank or 0guard before the division; no Infinity, no crash
Verdict ignores a real shrinkcoverage warning promoted to verdictfound in testing: the one-axis warning was masking the oversized branch. Coverage is now a note.
Tile contradicts the verdicttile printed the raw scalefound in testing: tile read 100.05 % beside “leave it at 100 %”. Now prints 100.00 % with the raw value greyed.
100.000 % beside a nonzero shrinkreachable only when “raw value is noise”intended: the tile is the instruction, the sub-line is the evidence
Caption says “0.9 mm” for a 0.3 mm breakhard-coded string in the fit pagederived from the mouth gap, clamped 0.3–0.6 mm

10 · Honest limitations

The tool measures the part, not the polymer. One cube can't separate material shrink from nozzle temperature, cooling, layer time, infill or bed adhesion — those all move the final size, and the calculator has no inputs for any of them. A number produced here is “how much did this part come out small”, which is the question you need answered to fix this part, and a much weaker claim than “PLA shrinks 0.3 %”.
Shrink isn't isotropic, and one scale can't fix that. XY is constrained by layer adhesion and the bed; Z is free to contract, which is why the worked cube reads 39.91 in Y and 40.03 in Z. The page shows all three axes so the disagreement is visible, but the slicer only takes one number — so the answer is a compromise, and the tool says so instead of hiding it behind a mean.
The lock model is geometry, not mechanics. Rigid walls, linear ramp, no viscoelastic creep, no temperature. “0.6°/side self-locks PLA on PLA” is an observation from pushing parts together, not a simulation — treat the bite rate as a starting point, then print one and feel it.
Calipers measure what the jaws touch. The minima rule removes jaw tilt and most of the seam; it cannot remove a raised bead on the face you chose to measure, and it cannot see a bow in the middle of a wall. 400-grit on a flat plate, figure-eights, five light passes — then measure again.

11 · Status and evidence

CheckResult
Shrink page size18,556 bytes · 379 lines · 5,758 B CSS + 8,269 B JS
Shrink page sha2562d52f897…ca490b3a (full: 2d52f8971f43e0c51fb46f363fb5be34c287208ef4d75886f0900cffca490b3a)
Mirrorbyte-identical, same 18,556 bytes, same sha256
External requests0 — no src, href, font, CDN or fetch
Fit page48,434 bytes · 832 lines
CLI13,991 bytes · 269 lines · shrink / fit / shrinklist
Maths checks9 / 9 — every displayed number re-derived in Python and compared to the DOM
UI states8 / 8 — empty, measured, real shrink, oversized, one axis, junk, zero nominal, after reset
Widths390 / 768 / 1440 — no horizontal overflow, no clipped text, min tap target 44 px
Console errors0 at every width
Served copydiffers from local by the site's skin injection (+~390 B) and Cloudflare's beacon (+~367 B); body is byte-identical after stripping both

12 · What's next