Start with a single cell holding a glass tile over a WS2812 RGB LED:

A bit of OpenSCAD tinkering produces a simple 2×2 array with square interiors as a test piece:

The excessive stringing and the booger in the upper-left cell come from absurdly thin infill tucked into the too-thin walls; Slic3r doesn’t (seem to) have a “minimum infill width” setting and it’ll desperately try to fit infill between two nearly adjacent perimeter threads.
The little support spiders under the LED PCB recesses snapped right out, though, so I got that part right:

The perimeter threads around the LED aperture aren’t quite fused, because it was only one layer thick and that’s not enough.
A quick test with two LEDs showed the white PETG let far too much light bleed between the cells, which was no surprise from the single cell test piece.
Fortunately, it’s all parametric, so a bit more tinkering produced a slightly chunkier matrix with a base for an Arduino Nano and M3 threaded brass inserts for the screws holding it together:

Those two parts require about three hours of printing, much faster than I could produce them by milling pockets into aluminum or black acrylic slabs, and came out with minimal stringing.
A little cleanup, some epoxy work, and a few dabs of solder later:

An initial lamp test showed the white-ish glass tiles aren’t all quite the same color:

I thought it was an LED color variation, too, but the slightly blue tint in the lower left corner followed the tile.
The blurred horizontal strip across the middle is adhesive tape holding the tiles in place; I was reluctant to glue them in before being sure this whole thing would work. A peek into the future, though, shows it’s got potential:

They do give off a definite Windows logo vibe, don’t they?
The OpenSCAD source code as a GitHub Gist:
// Illuminated Tile Grid | |
// Ed Nisley - KE4ZNU | |
// 2020-05 | |
/* [Configuration] */ | |
Layout = "Build"; // [Cell,CellArray,MCU,Base,Show,Build] | |
Shape = "Square"; // [Square, Pyramid, Cone] | |
Cells = [2,2]; | |
CellDepth = 15.0; | |
Support = true; | |
Inserts = true; | |
/* [Hidden] */ | |
ThreadThick = 0.25; | |
ThreadWidth = 0.40; | |
HoleWindage = 0.2; | |
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit); | |
Protrusion = 0.1; // make holes end cleanly | |
ID = 0; | |
OD = 1; | |
LENGTH = 2; | |
Tile = [25.0 + 0.1,25.0 + 0.1,4.0]; | |
WallThick = 3*ThreadWidth; | |
Flange = [4*ThreadWidth,4*ThreadWidth,0]; // ridge supporting tile | |
Separator = [3*ThreadWidth,3*ThreadWidth,Tile.z - 1]; // between tiles | |
Screw = [3.0,6.0,3.5]; // M3 SHCS, OD=head, LENGTH=head | |
Insert = [3.0,4.2,8.0]; // threaded brass insert | |
PCB = [15.0,8.0,2.5]; | |
LED = [5.0 + 2*HoleWindage,5.0 + 2*HoleWindage,1.0]; | |
LEDOffset = [0.0,(PCB.y - LED.y)/2 - 0.5,0.0]; // slight offset from +Y PCB edge | |
CellOAL = [Tile.x,Tile.y,0] + Separator + [0,0,CellDepth] + [0,0,WallThick] + [0,0,PCB.z]; | |
ArrayOAL = [Cells.x*CellOAL.x,Cells.y*CellOAL.y,CellOAL.z]; // just the LED cells | |
BlockOAL = ArrayOAL + [2*WallThick,2*WallThick,0]; // LED cells + exterior wall | |
echo(str("Block OAL: ",BlockOAL)); | |
InsertOC = ArrayOAL - [Insert[OD],Insert[OD],0] - [2*WallThick,2*WallThick,0]; | |
echo(str("Insert OC: ",InsertOC)); | |
TapeThick = 1.0; | |
Arduino = [44.0,18.0,8.0 + TapeThick]; // Arduino Nano to top of USB Mini-B plug | |
USBPlug = [15.0,11.0,8.5]; // USB Mini-B plug insulator | |
USBOffset = [0,0,5.5]; // offset from PCB base | |
WiringBay = [BlockOAL.x - 4*WallThick,38.0,3.0]; | |
PlateOAL = [BlockOAL.x,BlockOAL.y,WallThick + Arduino.z + WiringBay.z]; | |
echo(str("Base Plate: ",PlateOAL)); | |
//------------------------ | |
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes | |
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2); | |
FixDia = Dia / cos(180/Sides); | |
cylinder(d=(FixDia + HoleWindage),h=Height,$fn=Sides); | |
} | |
//----------------------- | |
// Base and optics in single tile | |
module LEDCone() { | |
hull() { | |
translate([0,0,CellDepth + Tile.z/2]) | |
cube(Tile - [Flange.x,Flange.y,0],center=true); | |
if (Shape == "Square") { | |
translate([0,0,LED.z/2]) | |
cube([Tile.x,Tile.y,LED.z] - [Flange.x,Flange.y,0],center=true); | |
} | |
else if (Shape == "Pyramid") { | |
translate([0,0,LED.z/2]) | |
cube(LED,center=true); | |
} | |
else if (Shape == "Cone") { | |
translate([0,0,LED.z/2]) | |
cylinder(d=1.5*LED.x,h=LED.z,center=true); | |
} | |
else { | |
echo(str("Whoopsie! Invalid Shape: ",Shape)); | |
cube(5); | |
} | |
} | |
} | |
// One complete LED cell | |
module LEDCell() { | |
difference() { | |
translate([0,0,CellOAL.z/2]) | |
cube(CellOAL,center=true); | |
translate([0,0,CellOAL.z - Separator.z + Tile.z/2]) | |
cube(Tile,center=true); | |
translate([0,0,PCB.z + WallThick]) | |
LEDCone(); | |
cube([LED.x,LED.y,CellOAL.z],center=true); | |
translate(-LEDOffset + [0,0,PCB.z/2 - Protrusion/2]) | |
cube(PCB + [0,0,Protrusion],center=true); | |
} | |
if (Support) | |
color("Yellow") render() | |
translate(-LEDOffset) { | |
// translate([0,0,ThreadThick/2]) | |
// cube([PCB.x - 2*ThreadWidth,PCB.y - 2*ThreadWidth,ThreadThick],center=true); | |
intersection() { | |
translate([0,0,(PCB.z - ThreadThick)/2]) | |
cube([PCB.x - 2*ThreadWidth,PCB.y - 2*ThreadWidth,PCB.z - ThreadThick],center=true); | |
union() { for (a=[0:22.5:359]) | |
rotate(a) | |
translate([PCB.x/2,0,PCB.z/2]) | |
cube([PCB.x,2*ThreadWidth,PCB.z],center=true); } | |
} | |
} | |
} | |
// The whole array of cells | |
module CellArray() { | |
difference() { | |
union() { | |
translate([CellOAL.x/2 - Cells.x*CellOAL.x/2,CellOAL.y/2 - Cells.y*CellOAL.y/2,0]) | |
for (i=[0:Cells.x - 1], j=[0:Cells.y - 1]) | |
translate([i*CellOAL.x,j*CellOAL.y,0]) | |
LEDCell(); | |
if (Inserts) // bosses | |
for (i=[-1,1], j=[-1,1]) | |
translate([i*InsertOC.x/2,j*InsertOC.y/2,0]) | |
rotate(180/8) | |
cylinder(d=Insert[OD] + 3*WallThick,h=Insert[LENGTH],$fn=8); | |
} | |
if (Inserts) // holes | |
for (i=[-1,1], j=[-1,1]) | |
translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion]) | |
rotate(180/8) | |
PolyCyl(Insert[OD],Insert[LENGTH] + WallThick + Protrusion,8); | |
} | |
difference() { | |
translate([0,0,CellOAL.z/2]) | |
cube(BlockOAL,center=true); | |
translate([0,0,CellOAL.z]) | |
cube(ArrayOAL + [0,0,2*CellOAL.z],center=true); | |
} | |
} | |
// Arduino bounding box | |
// Origin at center bottom of PCB | |
module Controller() { | |
union() { | |
translate([0,0,Arduino.z/2]) | |
cube(Arduino,center=true); | |
translate([Arduino.x/2 - Protrusion,-USBPlug.y/2,USBOffset.z + TapeThick - USBPlug.z/2]) | |
cube(USBPlug + [Protrusion,0,0],center=false); | |
} | |
} | |
// Baseplate | |
module BasePlate() { | |
difference() { | |
translate([0,0,PlateOAL.z/2]) | |
cube(PlateOAL,center=true); | |
translate([0,0,WallThick]) | |
Controller(); | |
translate([0,0,WallThick + PlateOAL.z/2]) | |
cube([Arduino.x - 2*2.0,WiringBay.y,PlateOAL.z],center=true); | |
translate([0,0,PlateOAL.z - WiringBay.z + WiringBay.z/2]) | |
cube(WiringBay + [0,0,2*Protrusion],center=true); | |
for (i=[-1,1], j=[-1,1]) | |
translate([i*InsertOC.x/2,j*InsertOC.y/2,-Protrusion]) | |
rotate(180/8) { | |
PolyCyl(Screw[ID],2*PlateOAL.z,8); | |
PolyCyl(Screw[OD],Screw[LENGTH] + 4*ThreadThick + Protrusion,8); | |
} | |
translate([0,0,ThreadThick-Protrusion]) | |
cube([17.0,45,2*ThreadThick],center=true); | |
} | |
linear_extrude(height=2*ThreadWidth + Protrusion) { | |
translate([1,0,-Protrusion]) | |
rotate(-90) mirror([1,0,0]) | |
text(text="Ed Nisley",size=6,font="Arial:style:Bold",halign="center"); | |
translate([-6.5,0,-Protrusion]) | |
rotate(-90) mirror([1,0,0]) | |
text(text="softsolder.com",size=4.5,font="Arial:style:Bold",halign="center"); | |
} | |
if (Support) | |
color("Yellow") | |
for (i=[-1,1], j=[-1,1]) | |
translate([i*InsertOC.x/2,j*InsertOC.y/2,0]) | |
for (a=[0:45:135]) | |
rotate(a) | |
translate([0,0,(Screw[LENGTH] - ThreadThick)/2]) | |
cube([Screw[OD] - 2*ThreadWidth,2*ThreadWidth,Screw[LENGTH] - ThreadThick],center=true); | |
} | |
//----------------------- | |
// Build things | |
if (Layout == "Cell") | |
LEDCell(); | |
else if (Layout == "CellArray") | |
CellArray(); | |
else if (Layout == "MCU") | |
Controller(); | |
else if (Layout == "Base") | |
BasePlate(); | |
else if (Layout == "Show") { | |
translate([0,0,PlateOAL.z + 10]) | |
CellArray(); | |
BasePlate(); | |
} | |
else if (Layout == "Build") { | |
translate([0,0.6*BlockOAL.y,0]) | |
CellArray(); | |
translate([0,-0.6*BlockOAL.y,0]) | |
rotate(90) | |
BasePlate(); | |
} | |
A set of differently sized tiles and it goes from Windoze to Mondrian.
Or go pointillist with a bag of small cabochons!
My solid modeling fu isn’t up to that challenge, alas.
Now add a matrix of IR LEDs and photodiodes for fTIR touch sensing!
After wrestling myself to the floor to not even think about putting a switch under each tile, you’ve got a better idea. Dang, another branch on the exfoliating project tree … [grin]
It’s hard to explain but they just look like I HAVE to put my fingers all over them. Another option I can think of is to put copper tape around the edges of each glass tile, there may be enough metal there to produce a successful capacitive touch sensor.
Aye, it’s that cool smooth glass surface!
Perhaps driving the inner edges of all four tiles near the center with a square wave, then sensing each outer corner (with four ADC inputs) would provide enough isolation & sensitivity, without blocking any light?
So many projects …