$ git clone https://ion.nu/git/signspinner
commit d20b2d1d2458c2383ef6e9a09345e022dd9deef7
Author: Alicia <...>
Date:   Thu Jun 6 10:01:25 2019 +0200

    Initial commit

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a333469
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+PARTS=container button top spinner spring sign all
+all: $(addsuffix .stl,$(PARTS))
+%.stl: signspinner.scad
+ openscad -D part='"$(@:.stl=)"' $^ -o $@
diff --git a/lib/disc.scad b/lib/disc.scad
new file mode 100644
index 0000000..9acdd71
--- /dev/null
+++ b/lib/disc.scad
@@ -0,0 +1,86 @@
+/*
+ *  Disc library for discs with varying thickness around the circumference
+ *  Copyright (C) 2019  Alicia <...>
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// TODO: Handle overhangs
+// TODO: Fix centered steepinclines
+
+function sum(v,x=0)=(len(v)==1?x+v[0]:sum([for(i=[1:len(v)-1])v[i]],x+v[0]));
+function subvectors(v,x=0)=concat(v[x],(x+1<len(v))?subvectors(v,x+1):[]);
+
+function discfaces1(i,len)=[
+  [((i+1)%len)+2, i+2+len, i+2],
+  [((i+1)%len)+2, ((i+1)%len)+2+len, i+2+len]
+];
+
+// When the next face is lower but on the same X
+function discfaces2(i,len)=[
+  [i+2+len, i+2, ((i+2)%len)+2+len], // Left
+  [((i+2)%len)+2+len, ((i+1)%len)+2+len, i+2+len], // Top/right
+  [((i+2)%len)+2+len, i+2, ((i+2)%len)+2] // Bottom/right
+];
+
+// When the previous face is lower but on the same X
+function discfaces3(i,len)=[
+  [(i+1)%len+2+len, ((len+i-1)%len)+2+len, (i+1)%len+2], // Right
+  [((len+i-1)%len)+2+len, ((i+1)%len)+2+len, i+2+len], // Top/left
+  [((len+i-1)%len)+2+len, ((len+i-1)%len)+2, (i+1)%len+2] // Bottom/left
+];
+
+function steepincline(v1,v2)=(v1[0]%360==v2[0]%360 && v2[1]>v1[1]);
+
+module disc(heights_, diameter=10, subdiv=1, center=false)
+{
+  heights=[for(i=[0:len(heights_)*subdiv-1])[
+           i*360/(len(heights_)*subdiv),
+           (heights_[floor(i/subdiv)]*(subdiv-i%subdiv)/subdiv+
+            heights_[ceil(i/subdiv)%len(heights_)]*(i%subdiv)/subdiv)
+          ]];
+  discpolygon(heights, diameter, center=center);
+}
+
+module discpolygon(points_, diameter=10, center=false)
+{
+  centerz=sum([for(i=[0:len(points_)-1])points_[i][1]])/len(points_);
+  points=concat([[0,0,center?-centerz/2:0],[0,0,center?centerz/2:centerz]],
+    [for(i=[0:len(points_)-1])[ // Bottom
+      cos(points_[i][0])*diameter/2,
+      sin(points_[i][0])*diameter/2,
+      center?-points_[i][1]/2:0]],
+    [for(i=[0:len(points_)-1])[ // Top
+      cos(points_[i][0])*diameter/2,
+      sin(points_[i][0])*diameter/2,
+      center?points_[i][1]/2:points_[i][1]]]);
+  faces=concat(
+    // Bottoms
+    [for(i=[0:len(points_)-1])[i+2,0,((i+1)%len(points_))+2]],
+    // Tops
+    [for(i=[0:len(points_)-1])[1,i+2+len(points_),((i+1)%len(points_))+2+len(points_)]],
+    // Outsides
+    subvectors([for(i=[0:len(points_)-1])
+      steepincline(points_[(i+2)%len(points_)], points_[(i+1)%len(points_)])?
+        discfaces2(i, len(points_)):
+        steepincline(points_[(len(points_)+i-1)%len(points_)], points_[(len(points_)+i)%len(points_)])?
+          discfaces3(i, len(points_)):
+          discfaces1(i, len(points_))
+    ]));
+  polyhedron(points, faces, convexity=10);
+}
+
+disc([10,15,15,20,10,8,6,7], subdiv=10, diameter=20, center=true);
+translate([20,0,0])discpolygon([for(i=[0:10:360])[i,5+i/90]]);
+translate([-20,0,0])discpolygon([for(i=[0:10:360])[i,9-i/90]]);
\ No newline at end of file
diff --git a/lib/thread.scad b/lib/thread.scad
new file mode 100644
index 0000000..59b5da6
--- /dev/null
+++ b/lib/thread.scad
@@ -0,0 +1,26 @@
+/*
+ *  Simple thread polygon library
+ *  Copyright (C) 2019  Alicia <...>
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+module thread(points, subdiv=20)
+{
+  lpoints=[for(i=[0:1/subdiv:len(points)-1/subdiv])(points[i]*(1-(i%1))+points[(i+1)%len(points)]*(i%1))];
+  points2=[for(i=[0:len(lpoints)-1])[sin(360*i/len(lpoints))*lpoints[i], cos(360*i/len(lpoints))*lpoints[i]]];
+echo(points2);
+  polygon(points2);
+}
+linear_extrude(10, twist=-600)thread([4,4,5,5,4,4,5,5]);
\ No newline at end of file
diff --git a/signspinner.scad b/signspinner.scad
new file mode 100644
index 0000000..556d7bc
--- /dev/null
+++ b/signspinner.scad
@@ -0,0 +1,195 @@
+/*
+ *  Signspinner, clicky pen mechanism applied to switching between signs
+ *  Copyright (C) 2019  Alicia <...>
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+use <lib/disc.scad>
+use <lib/thread.scad>
+
+$fs=1;
+$fa=4;
+part="all";
+
+// Spinner
+module spinner()color("#ff00ff")
+{
+  discpolygon(concat(
+    [for(i=[0:5:90])[i,6+i/10]],
+    [for(i=[90:5:180])[i,6+(i-90)/10]],
+    [for(i=[180:5:270])[i,6+(i-180)/10]],
+    [for(i=[270:5:360])[i,6+(i-270)/10]]
+   ), diameter=20);
+  difference()
+  {
+    translate([0,0,5])
+    {
+      cube([3,45,10], center=true);
+      cube([45,3,10], center=true);
+    }
+    translate([0,0,5])cylinder(6,17,17);
+  }
+}
+
+// Button housing/top
+module top()color("#0000ff")
+{
+  render()difference()
+  {
+    discpolygon(concat(
+      [for(i=[0:5:90])[i,7+i/10]],
+      [for(i=[90:5:180])[i,7+(i-90)/10]],
+      [for(i=[180:5:270])[i,7+(i-180)/10]],
+      [for(i=[270:5:360])[i,7+(i-270)/10]]
+     ), diameter=21);
+    cylinder(50,8,8, center=true); // Buttonhole
+  }
+  difference() // Spinner well
+  {
+    cylinder(17,11.5,11.5);
+    cylinder(50,10.45,10.45, center=true);
+  }
+  rotate([0,0,-15]) // Alignment shoulders
+  {
+    translate([0,7.6,1.5])cube([1.8,2,3], center=true);
+    translate([0,-7.6,1.5])cube([1.8,2,3], center=true);
+  }
+  difference()
+  {
+    union()
+    {
+      cylinder(2,27,29);
+      translate([0,0,2])cylinder(8.5,29,30);
+      translate([0,0,10.5])linear_extrude(4,twist=-45*4, convexity=10)thread([27,27,28,28,27,27,28,28]);
+    }
+    translate([0,0,5])cylinder(11,26,26);
+    cylinder(40,8,8, center=true); // Buttonhole
+  }
+}
+
+module button()color("#ff0000")
+{
+  difference()
+  {
+    render(convexity=10)union()
+    {
+      rotate([0,0,-20])disc([8,13,8,13,8,13,8,13], diameter=14.6, subdiv=10);
+      translate([0,0,-7])cylinder(7,6.5,7.3);
+    }
+    // Alignment slots
+    translate([0,7,0.75])cube([2.6,2,15.5], center=true);
+    translate([0,-7,0.75])cube([2.6,2,15.5], center=true);
+  }
+}
+
+module container()color("#00ff00")
+{
+  difference()
+  {
+    cylinder(30,30,30);
+    translate([0,0,2])
+    {
+      cylinder(23,28,28);
+      cylinder(30,27.5,27.5);
+      translate([2,2,0])cube([40,40,23]);
+    }
+    translate([0,0,25])linear_extrude(5,twist=-225)thread([27.5,27.5,28.5,28.5,27.5,27.5,28.5,28.5]);
+  }
+  // Spring slot
+  translate([0,0,2.5])difference()
+  {
+    cube([20,20,5], center=true);
+    cube([18,18,6], center=true);
+  }
+}
+
+module spring(size=[1,1,1], step=6, thickness=0.5)color("#a0a0a0")
+{
+  outer=(size[0]>size[1]?size[0]:size[1]);
+  minsize=(size[0]>size[1]?size[1]:size[0]);
+  inner=(minsize/7<2?minsize/7:2);
+  deg=thickness*360/step; // Degrees per step we need to make the spring the given thickness
+  intersection()
+  {
+    linear_extrude(size[2], twist=size[2]*360/step, center=true, convexity=10)polygon([
+      [sin(0)*inner,cos(0)*inner],
+      [sin(deg)*inner,cos(deg)*inner],
+      [sin(deg)*outer,cos(deg)*outer],
+      [sin(0)*outer,cos(0)*outer]]);
+    cube(size, center=true);
+  }
+  translate([0,0,(size[2]-thickness)/2])cube([size[0],size[1],thickness], center=true);
+  translate([0,0,-(size[2]-thickness)/2])cube([size[0],size[1],thickness], center=true);
+}
+
+module signs()color("#ffff00")
+{
+  difference() // Sign bottom
+  {
+    cylinder(1,26,26);
+    translate([0,0,0.4])cylinder(1,25.5,25.5);
+    cylinder(23,r=20, center=true);
+  }
+  difference()
+  {
+    cylinder(22.5,25,25);
+    translate([0,0,-1])cylinder(24,24,24);
+    rotate([0,0,10])translate([0,24,11])cube([1,5,23], center=true);
+  }
+  translate([0,0,11.25])difference() // Spinner handles
+  {
+    union()
+    {
+      cube([6,49,22.5], center=true);
+      cube([49,6,22.5], center=true);
+    }
+    translate([0,0,7])cube([4,49,23], center=true);
+    translate([0,0,7])cube([49,4,23], center=true);
+    cylinder(23,16,16, center=true);
+    cylinder(23,20,10, center=true);
+  }
+  // Sign anchors
+  for(z=[5,15])translate([-6,0,z])difference()
+  {
+    translate([0,21,0])cube([1,5,5], center=true);
+    cylinder(6,24,20, center=true);
+  }
+}
+
+if($preview)
+{
+  compress=5.5;
+  container();
+  translate([0,0,46])rotate([180,0,15])button();
+  translate([0,0,40.5])rotate([180,0,0])top();
+  translate([0,0,23-compress])spinner();
+  translate([0,0,12-compress/2])spring([17,17,22-compress], thickness=0.8);
+  translate([0,0,2.25])signs();
+}else{
+  if(part=="container")container();
+  else if(part=="button")button();
+  else if(part=="top")top();
+  else if(part=="spinner")spinner();
+  else if(part=="spring")rotate([0,90,0])spring([17,17,22], thickness=0.8);
+  else if(part=="sign")signs();
+  else{
+    translate([0,40,0])container();
+    translate([-60,50,7])rotate([0,0,45])button();
+    translate([-40,-5,0])top();
+    translate([32,8,0])spinner();
+    translate([5,-5,8.5])rotate([0,90,0])spring([17,17,22], thickness=0.8);
+    translate([-60,50,0])signs();
+  }
+}
\ No newline at end of file