3D Printed Chain Mail Armor: Joined Sheets = Fabric!

Another nine hours of printing produced a second 9×13 link chain mail armor sheet that simply begged to be joined with the first. Snipping a connecting link on one sheet and attempting to thread it through the armor button on the other didn’t work nearly as well as I expected, because the pillars on the open links don’t quite pass through the slot in the side of the armor button links:

Chain Mail Armor - 4 sided
Chain Mail Armor – 4 sided

So I summoned joiner links from the digital deep:

Chain Mail Armor - Sheet Joiners
Chain Mail Armor – Sheet Joiners

Those are standard armor button links, split at the cross bar level, then laid out along the Y axis. The cap bridges across the link just as it does on the chain mail sheets, so, when they’re glued back together, the result should be exactly like a solid link. There’s no room for alignment pins and, frankly, I wouldn’t fiddle with two dozen filament snippets anyway.

The OpenSCAD code below produces joiners that work for the square arrangement, not the diamond, but that’s in the nature of fine tuning.

When I saw them pasted to the platform, just like the model:

Chain Mail Armor - joiners on platform
Chain Mail Armor – joiners on platform

It occurred to me that I could pop the caps off, then lay the sheets in position, aligned on the underlying joiner half-links. Here’s the first sheet over the left set of bars:

Chain Mail Armor - sheet and joiners on platform
Chain Mail Armor – sheet and joiners on platform

Then glue the armor caps in place:

Chain Mail Armor - joiner with solvent glue
Chain Mail Armor – joiner with solvent glue

Four dots of IPS #4 solvent glue, dispensed from a fine copper tube serving as a pipette, wet the four pillars of the joiner’s two bottom bars. I dotted each pillar to begin softening the PLA, paused for a breath, wet them again to leave enough solvent to bite into the bottom of the armor cap, pressed the cap in place, tweaked the alignment with tweezers, then pressed downward for maybe five seconds. Although the joiner link has no inherent alignment features, there’s also not much room to slide around and it worked surprisingly well.

Repeat that trick dozen times and you’re done. The aggravation scales as the square root of the overall sheet size, so it’s not as awful as assembling every single link, but it’s definitely a task for the low-caffeine part of the day.

One bottom bar came loose when I showed the result at the MHVLUG meeting, but the bar reappeared and I glued it again easily enough. I’ve now printed several spare joiners, Just In Case.

The bottom bars aren’t firmly affixed to the platform after it cools and they dislodge fairly easily: that’s how I get larger models off: let everything cool, then simply lift the plastic off. If I were joining sheets on a regular basis, I’d conjure a fixture to hold the sheets and joiner caps in position, probably with the sheets upside down, then glue the bars atop the inverted caps. That could get messy.

Perhaps a special holder to capture the bars in the proper alignment, maybe with pins matching the square openings at the corners, would help?

This is a trial fit before gluing that’s visually indistinguishable from the final product:

Chain Mail Armor - joined sheets on platform
Chain Mail Armor – joined sheets on platform

It’s not actually fabric, but it’s sufficiently bendy to cover a hand:

Chain Mail Armor - joined sheet draped on hand
Chain Mail Armor – joined sheet draped on hand

The thing just cries out to be fondled…

There’s a quarter kilogram of plastic in that 8×12 inch = 200×310 mm sheet that almost used up the last of the black PLA spool.

Remember: you must tweak the OpenSCAD code to match your extruder settings, export a suitable STL file, get really compulsive about platform alignment, use hairspray / glue stick to boost platform adhesion, and have no qualms about an all-day print run. You can’t just slice a random STL file produced for a different printer, because the link dimensions come directly from the printer’s capabilities: one size does not fit all.

The OpenSCAD source code [Update: This is the refactored version.]:

// Chain Mail Armor Buttons
// Ed Nisley KE4ZNU - December 2014

Layout = "Build";			// Link Button LB Joiner Joiners Build

//- Extrusion parameters must match reality!
//  Print with 1 shell and 2+2 solid layers

ThreadThick = 0.20;
ThreadWidth = 0.40;

HoleWindage = 0.2;

Protrusion = 0.1*ThreadThick;			// make holes end cleanly

function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);

// Dimensions

//- Set maximum sheet size

SheetSizeX = 50;	// 170 for full sheet on M2
SheetSizeY = 60;	// 230

//- Diamond or rectangular sheet?

Diamond = false;					// true = rotate 45 degrees, false = 0 degrees for square

BendAround = "X";					// X or Y = maximum flexibility *around* designated axis

Cap = true;										// true = build bridge layers over links

Armor = true && Cap;							// true = build armor button atop (required) cap
ArmorThick = IntegerMultiple(6,ThreadThick);	// height above cap surface

// Link bar sizes

BarWidth = 6 * ThreadWidth;
BarThick = 4 * ThreadThick;

BarClearance = 5*ThreadThick;		// vertical clearance above & below bars

//-- Compute link sizes from those values

// Absolute minimum base link: bar width + corner angle + build clearance around bars
//  rounded up to multiple of thread width to ensure clean filling
BaseSide = IntegerMultiple((4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth)),ThreadWidth);

BaseHeight = 2*BarThick + BarClearance;           // both bars + clearance

echo(str("BaseSide: ",BaseSide," BaseHeight: ",BaseHeight));
echo(str(" Base elements: ",4*BarWidth,", ",2*BarWidth/sqrt(2),", ",3*(2*ThreadWidth)));
echo(str(" total: ",(4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth))));

BaseOutDiagonal = BaseSide*sqrt(2) - BarWidth;
BaseInDiagonal = BaseSide*sqrt(2) - 2*(BarWidth/2 + BarWidth*sqrt(2));

echo(str("Outside diagonal: ",BaseOutDiagonal));

//- On-center distance measured along coordinate axis
//   the links are interlaced, so this is half of what you think it should be...

LinkOC = BaseSide/2 + ThreadWidth;

LinkSpacing = Diamond ? (sqrt(2)*LinkOC) : LinkOC;
echo(str("Base spacing: ",LinkSpacing));

//- Compute how many links fit in sheet

MinLinksX = ceil((SheetSizeX - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
MinLinksY = ceil((SheetSizeY - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
echo(str("MinLinks X: ",MinLinksX," Y: ",MinLinksY));

NumLinksX = ((0 == (MinLinksX % 2)) && !Diamond) ? MinLinksX + 1 : MinLinksX;
NumLinksY = ((0 == (MinLinksY % 2) && !Diamond)) ? MinLinksY + 1 : MinLinksY;
echo(str("Links X: ",NumLinksX," Y: ",NumLinksY));

//- Armor button base

CapThick = 4 * ThreadThick;						// at least 3 layers for solid bridging

ButtonHeight = BaseHeight + BarClearance + CapThick;
echo(str("ButtonHeight: ",ButtonHeight));

//- Armor ornament size & shape
//		Fine-tune OD & ID to suit the number of sides...

ArmorSides = 4;
ArmorAngle = true ? 180/ArmorSides : 0;			// true -> rotate half a side for best alignment

TotalHeight = ButtonHeight + ArmorThick;
echo(str("Overall Armor Height: ",TotalHeight));

ArmorOD = 1.1 * BaseSide;						// tune for best base fit
ArmorID = 10 * ThreadWidth;						// make the tip blunt & strong


module ShowPegGrid(Space = 10.0,Size = 1.0) {

  RangeX = floor(95 / Space);
  RangeY = floor(125 / Space);

	for (x=[-RangeX:RangeX])
	  for (y=[-RangeY:RangeY])


// Create link with armor button as needed

module Link(Topping = false) {

LinkHeight = (Topping && Cap) ? ButtonHeight : BaseHeight;

	rotate((BendAround == "X") ? 90 : 0)
	rotate(Diamond ? 45 : 0)
		union() {
			difference() {
				translate([0,0,LinkHeight/2])	// outside shape
					intersection() {
				translate([0,0,(BaseHeight + BarClearance - Protrusion)/2])
					intersection() {		// inside shape
						cube([(BaseSide - 2*BarWidth),
								(BaseSide - 2*BarWidth),
								(BaseHeight + BarClearance + Protrusion)],
									(BaseHeight + BarClearance + Protrusion)],

				translate([0,0,((BarThick + 2*BarClearance)/2 + BarThick)])		// openings for bars
					cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
						BarThick + 2*BarClearance],

				translate([0,0,(BaseHeight/2 - BarThick)])
						(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),


			if (Topping && Armor)
				translate([0,0,(ButtonHeight - Protrusion)])		// sink slightly into the cap
							 h=(ArmorThick + Protrusion),

// Create split buttons to join sheets

module Joiner() {

		difference() {
			translate([0,0,BarThick + BarClearance + TotalHeight/2 - Protrusion])

		intersection() {
			translate([0,0,-(BarThick + BarClearance)])


// Build it!


if (Layout == "Link") {

if (Layout == "Button") {

if (Layout == "LB") {

if (Layout == "Build")
	for (ix = [0:(NumLinksX - 1)],
		 iy = [0:(NumLinksY - 1)]) {
			x = (ix - (NumLinksX - 1)/2)*LinkSpacing;
			y = (iy - (NumLinksY - 1)/2)*LinkSpacing;
			color([(ix/(NumLinksX - 1)),(iy/(NumLinksY - 1)),1.0])
				if (Diamond)
					Link((ix + iy) % 2);					// armor at odd,odd & even,even points
					if ((iy % 2) && (ix % 2))				// armor at odd,odd points
					else if (!(iy % 2) && !(ix % 2))		// connectors at even,even points

if (Layout == "Joiner")

if (Layout == "Joiners") {
	NumJoiners = max(MinLinksX,MinLinksY)/2;
	for (iy = [0:(NumJoiners - 1)]) {
		y = (iy - (NumJoiners - 1)/2)*2*LinkSpacing + LinkSpacing/2;
			color([0.5,(iy/(NumJoiners - 1)),1.0])

As a reward for reading all the way to the bottom, some further thoughts:

A mask array could control what type of link goes where, which cap style goes on each armor button, and whether to print the link at all. That way, you could produce customized armor buttons in non-rectangular (albeit coarsely pixelized) fabric sheets.

You could produce an armor sheet sporting cubic caps, then intersect the whole sheet with a model built from a height-map image to spread a picture across the sheet. The complexity of that model would probably tie OpenSCAD in knots, but perhaps an external program could intersect two properly aligned STL / AMF files.

The bars could be a thread or two thinner, shaving a few millimeters off the basic link. The printer’s ability to bridge the link to form the flying bars and cap limits making the links much larger.

Armored Chain Mail now replaces the Knurled Planetary Gear Bearing as my favorite fondletoy…

I wrote up a summary of the whole project on the MakerGear forum’s Printed Object Showcase.

17 thoughts on “3D Printed Chain Mail Armor: Joined Sheets = Fabric!

  1. Is IPS#4 your solvent glue of choice for PLA now? I remember you were experimenting with methylene chloride and purple primer. Does IPS work as well as acetone for ABS?

    1. Absolutely!

      IPS #4 actually dissolves PLA into goo: it’s much better than purple primer or thick paint stripper. Moderately expensive and evaporates right out of the can, but that’s the nature of the beast.

      Haven’t tried it on ABS, because the M2 runs on PLA. The label recommends IPS #4 for acrylic & polycarb, but not for cross-linked acrylic (whatever that is); given the contents, I think it’d work fine for ABS.

      1. I worded that badly. I meant, does IPS work as well for PLA as acetone does for ABS…

        Ordering some now!

        1. Absolutely!

          The hellish ingredients soften / dissolve PLA, so the joint contains material from both parts. In principle, the joint will be as strong as the native plastic, which means far more when you’re working with cast sheets than with 3D printed laminations.

          In practice, I let the joint sit overnight with as much clamping as I can bring to bear and it’s good enough for my simple needs.

          Those armor buttons got maybe five seconds of pressure, no clamping at all, and went directly into service, soooo call it a 4% failure rate under those conditions. [grin]

  2. The thing just cries out to be fondled…
    Then make a brassiere out of this material.
    Lady Gaga is always looking for new costume ideas!

    1. Remember, you saw it here first: parametric chain mail armor.

      Just sayin’…

  3. It occurs to me that you could print those joiner bars, pause the printer, lay the sheets over them, and then hit ‘continue’ and print the armor caps on top of the joiners. No gluing or alignment needed…

    1. Alas, a pair of rolled full-platform sheets won’t fit under the X gantry (and maybe not even side-by-side on the platform) with the nozzle at printing height. Worse, that bulky nozzle / heater / insulation / fan extruder requires more clearance than the adjoining links & rolls will provide.

      A delta printer with a looong snout might be able to reach inside. Maybe a big XY gantry?

      In any event, building an armor sheet covering the back of a shirt would require half a dozen full-platform rectangles with a lot of joiner links along both axes. Admittedly, it’s much less manual labor than knitting up a real chain mail jacket, but still enough to make me want to fob it off on somebody else.

      Dropping ball bearings inside models on the fly is about the extent of my manual intervention. Other folks swap filaments, position machine screws & nuts inside traps, and do all manner of exciting things, so perhaps I’m just a coward.

  4. Ed,

    I read your comments about topo overlay of a pattern. That would be exciting to see a medieval eagle on a sheet through the relief pattern. Wow! You got a cool thing here. Even print in titanium. Eh? Back with soft armor for the military. (Silly brainstorming…)

    1. a medieval eagle on a sheet

      Given the rendering times for those chocolate molds, I thought the heat death of the universe would interfere with a full-sheet image. The newest OpenSCAD runs a lot faster, so I must (eventually) try Tux armor.

      You want a heart stopper? Price a 6×6 button array in Shapeway’s stainless steel process!

      I loves me my M2…

  5. Ed,

    Would you know?
    I copy-pasted your code into OpenSCAD version 2013.06 and received a sytax error in line 212. I’ve moved parens around and added brackets to no avail. What is the problem?

    The parser stops after the equal sign in the following: x = (ix – (NumLinksX – 1)/2)*LinkSpacing;

    [So much for lazily reading about TU Dolft’s Self Adjusting Multinozzle Electrospraying Technique for Electric Generation while a 3D printer hums in the background creating medieval style chain mail by Ed Nisley. Kinda puts a crimp in one’s 2014 sci-fi future/present lifestyle. ;-)]

    1. OpenSCAD version 2013.06

      I’m running directly from the Github source, so that’s the problem. Wrapping an assign() statement around that whole block will fix it, except that assign() kicked in with 2014.QX and is now deprecated!

      Living in the future is great, except when you trip and fall slightly ahead of the bleeding edge…

      1. Yeah, I was too far in the future past. I have now upgraded to the Development Snapshot of 12/9. ALL IS GOOD.

        I’m ticks behind now. I lived a decade of bleeding freely and kinda do not rush into darkness so much now. I keep a few steps back but, since you raced forward I now have too. ;-)

        1. a decade of bleeding freely

          I’ve pretty much given up the whole beta tester thing, but the folks doing OpenSCAD have a solid track record of good commits with minimal breakage.

Comments are closed.