diff --git a/README.md b/README.md index 814a978..765cf16 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It's especially suited for creating high/medium profile spherical top keycaps. ## Please contribute! -I'm new to CadQuery (CQ), Ptyhon and code based CAD in general. There's a lot of guesswork and fumbling around to get to this point, any help is very much appreciated. +I'm new to CadQuery (CQ), Ptyhon and code based CAD in general. There's a lot of guesswork and fumbling around to get to this point, any help is very much appreciated. ## Usage @@ -91,9 +91,16 @@ The code includes an example to use a DXF drawing as legend (check the comments - add 2U pos-like stabilizers - add support for logos and graphical legends - add reinforcement for big keys +- better legend centering +- secondary, tertiary, quaternary legends +- side printed legends +- add alternative stems +- allow reduced height of keycap for non 19.xx spacing +- clean up the code - automatic export script of all needed keycap for a full keyboard - output files already supported for 3D printing - online editor - add secondary/tertiary legends - add support for stems other than cherry - clean up the code +- fix negative angle top diff --git a/examples/ansi.py b/examples/ansi.py new file mode 100644 index 0000000..a1bc312 --- /dev/null +++ b/examples/ansi.py @@ -0,0 +1,101 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + { 't':'⎋','fs':12}, + { 't':'!\n1','fs':5}, + { 't':'"\n2','fs':5}, + { 't':'£\n3','fs':5}, + { 't':'$\n4','fs':5}, + { 't':'%\n5','fs':5}, + { 't':'^\n6','fs':5}, + { 't':'&\n7','fs':5}, + { 't':'*\n8','fs':5}, + { 't':'(\n9','fs':5}, + { 't':')\n0','fs':5}, + { 't':'-\n_','fs':5}, + { 't':'+\n=','fs':5}, + { 'w':2,'t':'⌫','fs':12}, + ], + 1: [ + { 'w':1.5,'t':'↹','fs':12}, + { 't':'q' }, + { 't':'w' }, + { 't':'e' }, + { 't':'r' }, + { 't':'t' }, + { 't':'y' }, + { 't':'u' }, + { 't':'i' }, + { 't':'o' }, + { 't':'p' }, + { 't':'[\n{','fs':5 }, + { 't':']\n}','fs':5 }, + { 'w':1.5,'t':'|\n\\','fs':5 }, + ], + 2: [ + { 'w':1.75,'t':'⇪' }, + { 't':'a' }, + { 't':'s' }, + { 't':'d' }, + { 't':'f','n': True }, + { 't':'g' }, + { 't':'h' }, + { 't':'j','n': True }, + { 't':'k' }, + { 't':'l' }, + { 't':';\n:','fs':5 }, + { 't':'\'\n@','fs':5 }, + { 'w':2.25,'t':'⊼','fs':12 } + ], + 3: [ + { 'w':2.25,'t':'⇧' }, + { 't':'z' }, + { 't':'x' }, + { 't':'c' }, + { 't':'v' }, + { 't':'b' }, + { 't':'n' }, + { 't':'m' }, + { 't':',\n<','fs':5 }, + { 't':'.\n>','fs':5 }, + { 't':'/\n?','fs':5 }, + { 'w':2.75,'t':'⇧'}, + ], + 4: [ + { 'w': 1.25,'t':'⎈'}, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf"}, + { 'w': 1.25,'t':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf"}, + { 'w': 6.25, 'convex':True}, + { 'w':1.25, 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + {'w':1.25, 't':'Fn'}, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 'w': 1.25,'t':'⎈'}, + ], +} + +rows = [ + {'angle': 9, 'height': 14, 'keys': keys[0] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[1] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[2] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[3] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[4] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +#exporters.export(assy.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#exporters.export(assy.toCompound(), 'keycaps.step') diff --git a/examples/ansi68.py b/examples/ansi68.py new file mode 100644 index 0000000..2af44f7 --- /dev/null +++ b/examples/ansi68.py @@ -0,0 +1,110 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + { 't':'⎋','fs':12}, + { 't':'!\n1','fs':5}, + { 't':'"\n2','fs':5}, + { 't':'£\n3','fs':5}, + { 't':'$\n4','fs':5}, + { 't':'%\n5','fs':5}, + { 't':'^\n6','fs':5}, + { 't':'&\n7','fs':5}, + { 't':'*\n8','fs':5}, + { 't':'(\n9','fs':5}, + { 't':')\n0','fs':5}, + { 't':'-\n_','fs':5}, + { 't':'+\n=','fs':5}, + { 'w':2,'t':'⌫','fs':12}, + { 't':'`\n¬','fs':5} + ], + 1: [ + { 'w':1.5,'t':'↹','fs':12}, + { 't':'q' }, + { 't':'w' }, + { 't':'e' }, + { 't':'r' }, + { 't':'t' }, + { 't':'y' }, + { 't':'u' }, + { 't':'i' }, + { 't':'o' }, + { 't':'p' }, + { 't':'[\n{','fs':5 }, + { 't':']\n}','fs':5 }, + { 'w':1.5,'t':'|\n\\','fs':5 }, + { 't':'⌦' } + ], + 2: [ + { 'w':1.75,'t':'⇪' }, + { 't':'a' }, + { 't':'s' }, + { 't':'d' }, + { 't':'f','n': True }, + { 't':'g' }, + { 't':'h' }, + { 't':'j','n': True }, + { 't':'k' }, + { 't':'l' }, + { 't':';\n:','fs':5 }, + { 't':'\'\n@','fs':5 }, + { 'w':2.25,'t':'⊼','fs':12 }, + {'t':"Pg\nUp",'fs':5} + ], + 3: [ + { 'w':2.25,'t':'⇧' }, + { 't':'z' }, + { 't':'x' }, + { 't':'c' }, + { 't':'v' }, + { 't':'b' }, + { 't':'n' }, + { 't':'m' }, + { 't':',\n<','fs':5 }, + { 't':'.\n>','fs':5 }, + { 't':'/\n?','fs':5 }, + { 'w':1.75,'t':'⇧'}, + { 't':'↑','fs':12}, + {'t':"Pg\nDn",'fs':5} + + + ], + 4: [ + { 'w': 1.25,'t':'⎈'}, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf"}, + { 'w': 1.25,'t':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf"}, + { 'w': 6.25, 'convex':True}, + { 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + { 't':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 't':'⎈'}, + { 't':'←','fs':12}, + { 't':'↓','fs':12}, + { 't':'→','fs':12}, + ], +} + +rows = [ + {'angle': 9, 'height': 14, 'keys': keys[0] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[1] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[2] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[3] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[4] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +#exporters.export(assy.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#exporters.export(assy.toCompound(), 'keycaps.step') diff --git a/examples/full-ansi.py b/examples/full-ansi.py new file mode 100644 index 0000000..dc6f259 --- /dev/null +++ b/examples/full-ansi.py @@ -0,0 +1,147 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + {'t':'⎋'}, + {'t':'F1','ox':1.0,'fs':6}, + {'t':'F2','fs':6}, + {'t':'F3','fs':6}, + {'t':'F4','fs':6}, + {'t':'F5','ox':0.5,'fs':6}, + {'t':'F6','fs':6}, + {'t':'F7','fs':6}, + {'t':'F8','fs':6}, + {'t':'F9','ox':0.5,'fs':6}, + {'t':'F10','fs':6}, + {'t':'F11','fs':6}, + {'t':'F12','fs':6}, + {'t':'Print\nScreen','ox':0.5,'fs':3}, + {'t':'Screen\nLock','fs':3}, + {'t':'Pause','fs':3} + ], + 1: [ + { 't':'`\n¬','fs':5, 'oy':-0.5}, + { 't':'!\n1','fs':5}, + { 't':'"\n2','fs':5}, + { 't':'£\n3','fs':5}, + { 't':'$\n4','fs':5}, + { 't':'%\n5','fs':5}, + { 't':'^\n6','fs':5}, + { 't':'&\n7','fs':5}, + { 't':'*\n8','fs':5}, + { 't':'(\n9','fs':5}, + { 't':')\n0','fs':5}, + { 't':'-\n_','fs':5}, + { 't':'+\n=','fs':5}, + { 'w':2,'t':'⌫','fs':12}, + { 't':'Ins','fs':5,'ox':0.5}, + { 't':'Home','fs':5}, + { 't':'Pg\nUp','fs':5}, + { 't':'Num','fs':7,'ox':0.5}, + { 't':'/'}, + { 't':'*' }, + { 't':'-'} + ], + 2: [ + { 'w':1.5,'t':'↹','fs':12,'oy':-0.5}, + { 't':'q' }, + { 't':'w' }, + { 't':'e' }, + { 't':'r' }, + { 't':'t' }, + { 't':'y' }, + { 't':'u' }, + { 't':'i' }, + { 't':'o' }, + { 't':'p' }, + { 't':'[\n{','fs':5 }, + { 't':']\n}','fs':5 }, + { 'w':1.5,'t':'|\n\\','fs':5 }, + { 't':'Del','ox':0.5,'fs':5 }, + { 't':'End','fs':5 }, + { 't':'Pg\nDn','fs':5}, + { 't':'7','ox':0.5 }, + { 't':'8' }, + { 't':'9' }, + { 't':'+','h':2,'oy':-0.5 } + ], + 3: [ + { 'w':1.75,'t':'⇪','oy':-0.5 }, + { 't':'a' }, + { 't':'s' }, + { 't':'d' }, + { 't':'f','n': True }, + { 't':'g' }, + { 't':'h' }, + { 't':'j','n': True }, + { 't':'k' }, + { 't':'l' }, + { 't':';\n:','fs':5 }, + { 't':'\'\n@','fs':5 }, + { 'w':2.25,'t':'⊼','fs':12 }, + { 't':'4','ox':4.0 }, + { 't':'5','n':True }, + { 't':'6' } + ], + 4: [ + { 'w':2.25,'t':'⇧','oy':-0.5 }, + { 't':'z' }, + { 't':'x' }, + { 't':'c' }, + { 't':'v' }, + { 't':'b' }, + { 't':'n' }, + { 't':'m' }, + { 't':',\n<','fs':5 }, + { 't':'.\n>','fs':5 }, + { 't':'/\n?','fs':5 }, + { 'w':2.75,'t':'⇧'}, + { 't':'↑','fs':12,'ox':1.5}, + { 't':'1','ox':1.5 }, + { 't':'2' }, + { 't':'3' }, + { 't':'⊼','h':2,'oy':-0.5 }, + ], + 5: [ + { 'w': 1.25,'t':'⎈','oy':-0.5 }, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf"}, + { 'w': 1.25,'t':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf"}, + { 'w': 6.25, 'convex':True}, + { 'w':1.25, 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + {'w':1.25, 't':'Fn'}, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 'w': 1.25,'t':'⎈'}, + { 't':'←','fs':12,'ox': 0.5}, + { 't':'↓','fs':12 }, + { 't':'→','fs':12 }, + { 'w': 2,'t':'0','ox':0.5 }, + { 't':'.' } + ], +} + +rows = [ + {'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + {'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +#exporters.export(assy.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#exporters.export(assy.toCompound(), 'keycaps.step') diff --git a/examples/generate_exports.py b/examples/generate_exports.py new file mode 100644 index 0000000..f08b177 --- /dev/null +++ b/examples/generate_exports.py @@ -0,0 +1,80 @@ +import opk +import cadquery as cq +from cadquery import exporters + +keys = { + 0: [ + { 'unitX': 1 }, + ], + 1: [ + { 'unitX': 1 }, + { 'unitX': 2 } + ], + 2: [ + { 'unitX': 1 }, + { 'unitX': 1.5 } + ], + 3: [ + { 'unitX': 1 }, + { 'unitX': 1, 'depth': 3.6 }, + { 'unitX': 1.75 }, + { 'unitX': 2.25 } + ], + 4: [ + { 'unitX': 1 }, + { 'unitX': 1.25 }, + { 'unitX': 1.75 }, + { 'unitX': 2.25 }, + { 'unitX': 2.75 }, + ], + 5: [ + { 'unitX': 1 }, + { 'unitX': 1.25 }, + { 'unitX': 1.5 }, + { 'unitX': 6.25, 'convex': True } + ] +} + +rows = [ + {'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + {'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +] + +assy = cq.Assembly() + +y = 0 +for i, r in enumerate(rows): + x = 0 + for k in r['keys']: + name = "row{}_U{}".format(i,k['unitX']) + convex = False + if 'convex' in k: + convex = k['convex'] + name+= "_space" + + depth = 2.8 + if 'depth' in k: + if k['depth'] > depth: name+= "_homing" + depth = k['depth'] + + print("Generating: ", name) + cap = opk.keycap(angle=r['angle'], height=r['height'], unitX=k['unitX'], convex=convex, depth=depth) + # Export one key at the time + #exporters.export(cap, './export/STEP/' + name + '.step') + #exporters.export(cap, './export/STL/' + name + '.stl', tolerance=0.001, angularTolerance=0.05) + w = 19.05 * k['unitX'] / 2 + x+= w + assy.add(cap, name=name, loc=cq.Location(cq.Vector(x,y,0))) + x+= w + y -= 19.05 + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +#exporters.export(assy.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#exporters.export(assy.toCompound(), 'keycaps.step') diff --git a/examples/kb_render.py b/examples/kb_render.py new file mode 100644 index 0000000..8f5fa81 --- /dev/null +++ b/examples/kb_render.py @@ -0,0 +1,63 @@ +import cadquery as cq +from random import choice +import opk + +def render_kb(rows, mainFont="DejaVu Sans Mono", mainSize = 9, sx = 19.05, sy = 19.05, depth = 2.8, export = False ): + + assy = cq.Assembly() + colours=["tomato2","springgreen3","slateblue2","sienna1","seagreen3","orangered2","orchid2","maroon2","limegreen","lightseagreen","lightcoral","magenta3","yellow"] + + y = 0 + for i, r in enumerate(rows): + x = 0 + for j,k in enumerate(r['keys']): + kh = 1 + if 'h' in k: + kh = k['h'] + kw = 1 + if 'w' in k: + kw = k['w'] + name = "row{}_{}_U{}".format(i,j,kw) + convex = False + if 'convex' in k: + convex = k['convex'] + name+= "_space" + + cdepth = depth + if 'n' in k: + if k['n']: + name+= "_homing" + cdepth = depth + 0.8 + legend = '' + if 't' in k: + legend = k['t'] + font = mainFont + if 'f' in k: + font = k['f'] + fontSize=mainSize + if 'fs' in k: + fontSize = k['fs'] + print("Generating: {} {}".format(name, legend)) + cap = opk.keycap(angle=r['angle'], height=r['height'], + unitX=kw, unitY=kh, + convex=convex, depth=cdepth, + legend=legend, font=font, + fontsize=fontSize) + # Export one key at the time + if export: + cq.exporters.export(cap, './export/STEP/' + name + '.step') + cq.exporters.export(cap, './export/STL/' + name + '.stl', tolerance=0.001, angularTolerance=0.05) + w = sx * kw / 2 + ox = 0.0 + if 'ox' in k: + ox = k['ox'] + x += w + ox*sx + oy = 0.0 + if 'oy' in k: + oy = k['oy'] + y += oy*sy + assy.add(cap, name=name, color=cq.Color(choice(colours)), + loc=cq.Location(cq.Vector(x,y,0))) + x += w + y = -(i+1)*sy + return assy diff --git a/examples/keycap.py b/examples/keycap.py new file mode 100644 index 0000000..4102c94 --- /dev/null +++ b/examples/keycap.py @@ -0,0 +1,9 @@ +from opk import * +import cadquery as cq + +cap = keycap(legend="",unitX=1,unitY=1.0, font="/usr/share/fonts/truetype/Font_Awesome_6_Brands-Regular-400.otf",convex=True) +cq.exporters.export(cap, 'space-penguin.stl', tolerance=0.001, angularTolerance=0.05) +cs = keycap(legend="",unitX=1,unitY=1.0, font="/usr/share/fonts/truetype/Font_Awesome_6_Brands-Regular-400.otf",convex=True) +cq.exporters.export(cs, 'space-cameleon.stl', tolerance=0.001, angularTolerance=0.05) + +#exporters.export(cap, 'keycap.step') diff --git a/examples/m65.py b/examples/m65.py new file mode 100644 index 0000000..d3f392c --- /dev/null +++ b/examples/m65.py @@ -0,0 +1,113 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + { 't':'⎋ `\n ¬','fs':5, 'f':"DejaVu Sans Mono"}, + { 't':'1\n!','fs':5 }, + { 't':'2\n\"','fs':5 }, + { 't':'3\n£','fs':5 }, + { 't':'4\n$','fs':5 }, + { 't':'5\n%','fs':5 }, + { 't':'6\n^','fs':5 }, + { 't':'7\n&','fs':5 }, + { 't':'8\n*','fs':5 }, + { 't':'9\n(','fs':5 }, + { 't':'0\n)','fs':5 }, + { 't':'- ⌦\n_ ','fs':5 }, + { 't':'⌫ =\n +','fs':5 } + ], + 1: [ + { 't':'↹','fs':12}, + { 't':'q σ\nâ ϕ','fs':5 }, + { 't':'w ω\n Ω','fs':5 }, + { 't':'e ε\n ℇ','fs':5 }, + { 't':'r ρ\n ∇','fs':5 }, + { 't':'t ϑ\nț θ','fs':5 }, + { 't':'y ℝ\n ℤ','fs':5 }, + { 't':'u τ\n ℂ','fs':5 }, + { 't':'i ∫\nî ∮','fs':5 }, + { 't':'o ∞\n ⊗','fs':5 }, + { 't':'p π\n ∏','fs':5 }, + { 't':'[ ⋜\n{ ≅','fs':5 }, + { 't':'] ⋝\n} ≅','fs':5 } + ], + 2: [ + { 't':'# \n~ ⇪','fs':5 }, + { 't':'a α\nă ̇','fs':5 }, + { 't':'s ∑\nș ⨋','fs':5, 'f':"/usr/share/fonts/truetype/NotoSansMath-Regular.ttf" }, + { 't':'d δ\n ∂','fs':5 }, + { 't':'f φ\n ψ','n': True,'fs':5 }, + { 't':'g γ\n Γ','fs':5 }, + { 't':'h ℏ\n 𝓗','fs':5, 'f':"/usr/share/fonts/truetype/NotoSansMath-Regular.ttf" }, + { 't':'j ∈\n ∉','n': True,'fs':5 }, + { 't':'k ϰ\n ∆','fs':5 }, + { 't':'l λ\n Λ','fs':5 }, + { 't':'; 𝔼\n: Å','fs':5 }, + { 't':'\' ∝\n@ ℒ','fs':5 }, + { 't':'⊼','fs':12 } + ], + 3: [ + { 't':'⇧','fs':12 }, + { 't':'\\ ≡\n| ≢','fs':5 }, + { 't':'z ζ\n ∡','fs':5 }, + { 't':'x ξ\nç Ξ','fs':5 }, + { 't':'c χ\n⊄ ⊂','fs':5 }, + { 't':'v ν\n⊅ ⊃','fs':5 }, + { 't':'b β\n∧ ∩','fs':5 }, + { 't':'n η\n∨ ∪','fs':5 }, + { 't':'m μ\n ∘','fs':5 }, + { 't':', ≈\n< ≉','fs':5 }, + { 't':'. ±\n> ∓','fs':5 }, + { 't':'↑','fs':12}, + { 't':'/ ×\n? ⋅','fs':5 } + ], + 4: [ + { 't':'⎈','f':"DejaVu Sans Mono",'fs':12 }, + { 't':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 't':'⇓','f':"DejaVu Sans Mono",'fs':12}, + { 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + { 't':'⇑','f':"DejaVu Sans Mono",'fs':12 }, + { 't':'','convex':True,'f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf",'fs':9}, + { 't':'','convex':True,'f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf",'fs':9}, + { 't':'','convex':True,'f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf",'fs':9}, + { 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + { 't':'⇧','fs':12 }, + { 't':'←','fs':12 }, + { 't':'↓','fs':12 }, + { 't':'→','fs':12 } + ], +} + +#rows = [ + #{'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + #{'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + #{'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + #{'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + #{'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + #{'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +#] +rows = [ + {'angle': 9, 'height': 14, 'keys': keys[0] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[1] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[2] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[3] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[4] }, # row 5, bottom row +] + +mainFont = "./Atkinson-Hyperlegible-Bold-102.otf" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +exporters.export(assy.toCompound(), 'm65.stl', tolerance=0.001, angularTolerance=0.05) +exporters.export(assy.toCompound(), 'm65.step') diff --git a/examples/numpad.py b/examples/numpad.py new file mode 100644 index 0000000..742643f --- /dev/null +++ b/examples/numpad.py @@ -0,0 +1,65 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + { 't':'Num','fs':7}, + { 't':'/'}, + { 't':'*' }, + { 't':'-'} + ], + 1: [ + { 't':'7\nHome','fs':4.5 }, + { 't':'8\n↑','fs':4.5 }, + { 't':'9\nPgUp','fs':4.5 }, + { 't':'+','h':2,'oy':-0.5 } + ], + 2: [ + { 't':'4\n←','fs':4.5 }, + { 't':'5','n':True }, + { 't':'6\n→','fs':4.5 } + ], + 3: [ + { 't':'1\nEnd','fs':4.5 }, + { 't':'2\n↓','fs':4.5 }, + { 't':'3\nPgDn','fs':4.5 }, + { 't':'⊼','h':2,'oy':-0.5 }, + ], + 4: [ + { 'w': 2,'t':'0\nIns','fs':6 }, + { 't':'.\nDel', 'fs':5 } + ], +} + +#rows = [ + #{'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + #{'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + #{'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + #{'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + #{'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + #{'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +#] +rows = [ + {'angle': 9, 'height': 14, 'keys': keys[0] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[1] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[2] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[3] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[4] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy, export=True) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +exporters.export(assy.toCompound(), 'numpad.stl', tolerance=0.001, angularTolerance=0.05) +exporters.export(assy.toCompound(), 'numpad.step') diff --git a/examples/planck.py b/examples/planck.py new file mode 100644 index 0000000..bee89bd --- /dev/null +++ b/examples/planck.py @@ -0,0 +1,92 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + { 't':'⎋','fs':12}, + { 't':'q' }, + { 't':'w' }, + { 't':'e' }, + { 't':'r' }, + { 't':'t' }, + { 't':'y' }, + { 't':'u' }, + { 't':'i' }, + { 't':'o' }, + { 't':'p' }, + { 't':'⌫','fs':12} + ], + 1: [ + { 't':'↹','fs':12 }, + { 't':'a' }, + { 't':'s' }, + { 't':'d' }, + { 't':'f','n': True }, + { 't':'g' }, + { 't':'h' }, + { 't':'j','n': True }, + { 't':'k' }, + { 't':'l' }, + {'t':';\n:','fs':5}, + { 't':'\'\n@','fs':5 } + ], + 2: [ + { 't':'⇧','fs':12 }, + { 't':'z' }, + { 't':'x' }, + { 't':'c' }, + { 't':'v' }, + { 't':'b' }, + { 't':'n' }, + { 't':'m' }, + { 't':',\n<','fs':5 }, + { 't':'.\n>','fs':5 }, + { 't':'/\n?','fs':5 }, + { 't':'⊼','fs':5 } + ], + 3: [ + { 't':'Fn','fs':6 }, + { 't':'⎈','f':"DejaVu Sans Mono",'fs':12 }, + { 't':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + { 't':'⇓','f':"DejaVu Sans Mono",'fs':12}, + { 'w':2,'convex':True}, + { 't':'⇑','f':"DejaVu Sans Mono",'fs':12 }, + { 't':'←','fs':12 }, + { 't':'↑','fs':12 }, + { 't':'↓','fs':12 }, + { 't':'→','fs':12 } + ], +} + +#rows = [ + #{'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + #{'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + #{'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + #{'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + #{'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + #{'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +#] +rows = [ + {'angle': 8, 'height': 12, 'keys': keys[0] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[1] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[2] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[3] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +#exporters.export(assy.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#exporters.export(assy.toCompound(), 'keycaps.step') diff --git a/examples/preonic.py b/examples/preonic.py new file mode 100644 index 0000000..62f2597 --- /dev/null +++ b/examples/preonic.py @@ -0,0 +1,107 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + { 't':'⎋','fs':12}, + { 't':'!\n1','fs':5}, + { 't':'"\n2','fs':5}, + { 't':'£\n3','fs':5}, + { 't':'$\n4','fs':5}, + { 't':'%\n5','fs':5}, + { 't':'^\n6','fs':5}, + { 't':'&\n7','fs':5}, + { 't':'*\n8','fs':5}, + { 't':'(\n9','fs':5}, + { 't':')\n0','fs':5}, + { 't':'⌫','fs':12} + ], + 1: [ + { 't':'q' }, + { 't':'w' }, + { 't':'e' }, + { 't':'r' }, + { 't':'t' }, + { 't':'y' }, + { 't':'u' }, + { 't':'i' }, + { 't':'o' }, + { 't':'p' }, + { 't':'{\n[','fs':5}, + { 't':'}\n]','fs':5}, + ], + 2: [ + { 't':'↹','fs':12 }, + { 't':'a' }, + { 't':'s' }, + { 't':'d' }, + { 't':'f','n': True }, + { 't':'g' }, + { 't':'h' }, + { 't':'j','n': True }, + { 't':'k' }, + { 't':'l' }, + {'t':';\n:','fs':5}, + { 't':'\'\n@','fs':5 } + ], + 3: [ + { 't':'⇧','fs':12 }, + { 't':'z' }, + { 't':'x' }, + { 't':'c' }, + { 't':'v' }, + { 't':'b' }, + { 't':'n' }, + { 't':'m' }, + { 't':',\n<','fs':5 }, + { 't':'.\n>','fs':5 }, + { 't':'/\n?','fs':5 }, + { 't':'⊼','fs':5 } + ], + 4: [ + { 't':'Fn','fs':6 }, + { 't':'⎈','f':"DejaVu Sans Mono",'fs':12 }, + { 't':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + { 't':'⇓','f':"DejaVu Sans Mono",'fs':12}, + { 'w':2,'convex':True}, + { 't':'⇑','f':"DejaVu Sans Mono",'fs':12 }, + { 't':'←','fs':12 }, + { 't':'↑','fs':12 }, + { 't':'↓','fs':12 }, + { 't':'→','fs':12 } + ], +} + +#rows = [ + #{'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + #{'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + #{'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + #{'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + #{'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + #{'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +#] +rows = [ + {'angle': 9, 'height': 14, 'keys': keys[0] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[1] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[2] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[3] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[4] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +#exporters.export(assy.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#exporters.export(assy.toCompound(), 'keycaps.step') diff --git a/examples/rows.py b/examples/rows.py new file mode 100644 index 0000000..a0e509c --- /dev/null +++ b/examples/rows.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + + +import cadquery as cq +from opk import * +import os + +leg = [["⎋"],["⎋"], +["↹"], +["⇪","d","f","g"], +["⇧"], +["⎈"], +["",""]] +lay= [[1],[1],[1.5],[1.75]+[1]*3,[2.25],[1.25],[2,1]] +fonts=[ + ["/usr/share/fonts/truetype/NotoSansSymbols-Medium.ttf"], + ["/usr/share/fonts/truetype/NotoSansSymbols-Medium.ttf"], + ["Noto Sans Mono"], + ["Noto Sans Mono"]*4, + ["Noto Sans Mono"], + ["/usr/share/fonts/truetype/NotoSansSymbols-Medium.ttf"], + ["Noto Sans Mono"]+["/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf"] + ] +sx = 19.05 +sy = 19.05 +rows = cq.Assembly() +y = 0 +i = -1 +j = -1 +angles = [13.5,9,8.5,-6,-7,0,0] +vfs=[0,9,7,6,4.5,4.5] + +for row,ll,ff in zip(leg,lay,fonts): + y -= sy + i += 1 + x = 0 + for k,l,f in zip(row,ll,ff): + print(k,l) + w = l*sx/2.0 + j += 1 + x += w + convex = False + if k in ["",""]: + convex = True + scoop = 2.5 + if k in ['f','F','j','J']: + scoop = 2.5*1.2 + fs=3 + if len(k)<=5: + fs=vfs[len(k)] + if (len(k.split("\n"))==2): + fs = 4.5 + + rows.add(keycap(legend=k, + angle=angles[i], + font=f, + convex=convex, + depth = scoop, + unitX=l), + name="k{}{}".format(i,j), + loc=cq.Location(cq.Vector(x,y,0))) + x += w +cq.exporters.export(rows.toCompound(), 'keycaps.stl', tolerance=0.001, angularTolerance=0.05) +#show_object(rows, name="rows", options={"alpha": 0}) diff --git a/examples/tkl.py b/examples/tkl.py new file mode 100644 index 0000000..4c177a5 --- /dev/null +++ b/examples/tkl.py @@ -0,0 +1,130 @@ +import opk +import cadquery as cq +from cadquery import exporters +from kb_render import * + +keys = { + 0: [ + {'t':'⎋'}, + {'t':'F1','ox':1.0,'fs':6}, + {'t':'F2','fs':6}, + {'t':'F3','fs':6}, + {'t':'F4','fs':6}, + {'t':'F5','ox':0.5,'fs':6}, + {'t':'F6','fs':6}, + {'t':'F7','fs':6}, + {'t':'F8','fs':6}, + {'t':'F9','ox':0.5,'fs':6}, + {'t':'F10','fs':6}, + {'t':'F11','fs':6}, + {'t':'F12','fs':6}, + {'t':'Print\nScreen','ox':0.5,'fs':3}, + {'t':'Screen\nLock','fs':3}, + {'t':'Pause','fs':3} + ], + 1: [ + { 't':'`\n¬','fs':5, 'oy':-0.5}, + { 't':'!\n1','fs':5}, + { 't':'"\n2','fs':5}, + { 't':'£\n3','fs':5}, + { 't':'$\n4','fs':5}, + { 't':'%\n5','fs':5}, + { 't':'^\n6','fs':5}, + { 't':'&\n7','fs':5}, + { 't':'*\n8','fs':5}, + { 't':'(\n9','fs':5}, + { 't':')\n0','fs':5}, + { 't':'-\n_','fs':5}, + { 't':'+\n=','fs':5}, + { 'w':2,'t':'⌫','fs':12}, + { 't':'Ins','fs':5,'ox':0.5}, + { 't':'Home','fs':5}, + { 't':'Pg\nUp','fs':5}, + ], + 2: [ + { 'w':1.5,'t':'↹','fs':12,'oy':-0.5}, + { 't':'q' }, + { 't':'w' }, + { 't':'e' }, + { 't':'r' }, + { 't':'t' }, + { 't':'y' }, + { 't':'u' }, + { 't':'i' }, + { 't':'o' }, + { 't':'p' }, + { 't':'[\n{','fs':5 }, + { 't':']\n}','fs':5 }, + { 'w':1.5,'t':'|\n\\','fs':5 }, + { 't':'Del','ox':0.5,'fs':5 }, + { 't':'End','fs':5 }, + { 't':'Pg\nDn','fs':5} + ], + 3: [ + { 'w':1.75,'t':'⇪','oy':-0.5 }, + { 't':'a' }, + { 't':'s' }, + { 't':'d' }, + { 't':'f','n': True }, + { 't':'g' }, + { 't':'h' }, + { 't':'j','n': True }, + { 't':'k' }, + { 't':'l' }, + { 't':';\n:','fs':5 }, + { 't':'\'\n@','fs':5 }, + { 'w':2.25,'t':'⊼','fs':12 } + ], + 4: [ + { 'w':2.25,'t':'⇧','oy':-0.5 }, + { 't':'z' }, + { 't':'x' }, + { 't':'c' }, + { 't':'v' }, + { 't':'b' }, + { 't':'n' }, + { 't':'m' }, + { 't':',\n<','fs':5 }, + { 't':'.\n>','fs':5 }, + { 't':'/\n?','fs':5 }, + { 'w':2.75,'t':'⇧'}, + { 't':'↑','fs':12,'ox':1.5} + ], + 5: [ + { 'w': 1.25,'t':'⎈','oy':-0.5 }, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf"}, + { 'w': 1.25,'t':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf"}, + { 'w': 6.25, 'convex':True}, + { 'w':1.25, 't':'⎇','f':"/usr/share/fonts/truetype/NotoSansSymbols-Black.ttf" }, + {'w':1.25, 't':'Fn'}, + { 'w': 1.25,'t':'','f':"/usr/share/fonts/texlive-fontawesome5/FontAwesome5Brands-Regular-400.otf" }, + { 'w': 1.25,'t':'⎈'}, + { 't':'←','fs':12,'ox': 0.5}, + { 't':'↓','fs':12 }, + { 't':'→','fs':12 } + ], +} + +rows = [ + {'angle': 13, 'height': 16, 'keys': keys[0] }, # row 0, function row + {'angle': 9, 'height': 14, 'keys': keys[1] }, # row 1, numbers row + {'angle': 8, 'height': 12, 'keys': keys[2] }, # row 2, QWERT + {'angle': -6, 'height': 11.5, 'keys': keys[3] }, # row 3, ASDFG + {'angle': -8, 'height': 13, 'keys': keys[4] }, # row 4, ZXCVB + {'angle': 0, 'height': 12.5, 'keys': keys[5] }, # row 5, bottom row +] + +mainFont = "DejaVu Sans Mono" +mainSize = 9 + +sx = 19.05 +sy = 19.05 + +assy = render_kb(rows, mainFont=mainFont, mainSize = mainSize, sx = sx, sy = sy) + +if 'show_object' in locals(): + show_object(assy) + +# Export the whole assembly, very handy especially for STEP +exporters.export(assy.toCompound(), 'tkl.stl', tolerance=0.001, angularTolerance=0.05) +exporters.export(assy.toCompound(), 'tkl.step') diff --git a/keycap.py b/keycap.py deleted file mode 100644 index aa3def4..0000000 --- a/keycap.py +++ /dev/null @@ -1,7 +0,0 @@ -import opk - -cap = opk.keycap() -show_object(cap, name="keycap", options={"alpha": 0}) - -#exporters.export(cap, 'keycap.stl', tolerance=0.001, angularTolerance=0.05) -#exporters.export(cap, 'keycap.step') \ No newline at end of file diff --git a/opk.py b/opk.py deleted file mode 100644 index b3a658e..0000000 --- a/opk.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -========================== - ██████ ██████ ██ ██ - ██ ██ ██ ██ ██ ██ - ██ ██ ██████ █████ - ██ ██ ██ ██ ██ - ██████ ██ ██ ██ -========================== - Open Programmatic Keycap -========================== - -OPK is a spherical top keycap profile developed in CadQuery -(https://github.com/CadQuery/cadquery) and released under the very permissive -Apache License 2.0. It's especially suited for creating high/medium profile, -spherical top keycaps. - -!!! The profile is still highly experimental and very alpha stage. ¡¡¡ - -If you use the code please give credit, if you do modifications consider -releasing them back to the public under a permissive open source license. - -Copyright (c) 2022 Matteo "Matt3o" Spinelli -https://matt3o.com -""" - -import cadquery as cq -from cadquery import exporters - -# Prevent error when running from cli -if 'show_object' not in globals(): - def show_object(*args, **kwargs): - pass - -def keycap( - unitX: float = 1, # keycap size in unit. Standard sizes: 1, 1.25, 1.5, ... - unitY: float = 1, - base: float = 18.2, # 1-unit size in mm at the base - top: float = 14.2, # 1-unit size in mm at the top, actual hitting area will be slightly bigger - curv: float = 1.3, # Top side curvature. Higher value makes the top rounder (use small increments) - bFillet: float = 0.5, # Fillet at the base - tFillet: float = 4, # Fillet at the top - height: float = 13, # Height of the keycap before cutting the scoop (final height is lower) - angle: float = 7, # Angle of the top surface - depth: float = 2.5, # Scoop depth - thickness: float = 1.5, # Keycap sides thickness - convex: bool = False, # Is this a spacebar? - legend: str = "", # Legend - font: str = "sans-serif", - fontsize: float = 10 -): - - top_diff = base - top - - curv = min(curv, 1.9) - - bx = 19.05 * unitX - (19.05 - base) - by = 19.05 * unitY - (19.05 - base) - - tx = bx - top_diff - ty = by - top_diff - - # if spacebar make the top less round-y - tension = .4 if convex else 1 - - # Three-section loft of rounded rectangles. Can't find a better way to do variable fillet - base = ( - cq.Sketch() - .rect(bx, by) - .vertices() - .fillet(bFillet) - ) - - mid = ( - cq.Sketch() - .rect(bx, by) - .vertices() - .fillet((tFillet-bFillet)/3) - ) - - top = ( - cq.Sketch() - .arc((curv, curv*tension), (0, ty/2), (curv, ty-curv*tension)) - .arc((curv, ty-curv*tension), (tx/2, ty), (tx-curv, ty-curv*tension)) - .arc((tx-curv, ty-curv*tension), (tx, ty/2), (tx-curv, curv*tension)) - .arc((tx-curv, curv*tension), (tx/2, 0), (curv, curv*tension)) - .assemble() - .vertices() - .fillet(tFillet) - .moved(cq.Location(cq.Vector(-tx/2, -ty/2, 0))) - ) - - # Main shape - keycap = ( - cq.Workplane("XY") - .placeSketch(base, - mid.moved(cq.Location(cq.Vector(0, 0, height/4), (1,0,0), angle/4)), - top.moved(cq.Location(cq.Vector(0, 0, height), (1,0,0), angle)) - ) - .loft() - ) - - # Create a body that will be carved from the main shape to create the shape - if convex: - tool = ( - cq.Workplane("YZ").transformed(offset=cq.Vector(0, height-2.1, -bx/2), rotate=cq.Vector(0, 0, angle)) - .moveTo(-by/2, -1) - .threePointArc((0, 2), (by/2, -1)) - .lineTo(by/2, 10) - .lineTo(-by/2, 10) - .close() - .extrude(bx, combine=False) - ) - else: - tool = ( - cq.Workplane("YZ").transformed(offset=cq.Vector(0, height+1, bx/2), rotate=cq.Vector(0, 0, angle)) - .moveTo(-by/2+2,0) - .threePointArc((0, min(-0.1, -depth+1)), (by/2-2, 0)) - .lineTo(by/2-2, 10) - .lineTo(-by/2+2, 10) - .close() - .workplane(offset=-bx/2) - .moveTo(-by/2, -1) - .threePointArc((0, -depth), (by/2, -1)) - .lineTo(by/2, 10) - .lineTo(-by/2, 10) - .close() - .workplane(offset=-bx/2) - .moveTo(-by/2+2, 0) - .threePointArc((0, min(-0.1, -depth+1)), (by/2-2, 0)) - .lineTo(by/2-2, 10) - .lineTo(-by/2+2, 10) - .close() - .loft(combine=False) - ) - - #show_object(tool, options={'alpha': 0.4}) - keycap = keycap - tool - - # Top edge fillet - keycap = keycap.edges(">Z").fillet(0.5) - - # Since the shell() function is not able to deal with complex shapes - # we need to subtract a smaller keycap from the main shape - shell = ( - cq.Workplane("XY").rect(bx-thickness*2, by-thickness*2) - .workplane(offset=height/5).rect(bx-thickness*2.3, by-thickness*2.3) - .workplane().transformed(offset=cq.Vector(0, 0, height-height/5-4), rotate=cq.Vector(angle, 0, 0)).rect(tx-thickness*2, ty-thickness*2) - .loft() - ) - keycap = keycap - shell - - # Build the stem and the keycap guts - if unitX < 2: - stem_pts = [(0,0)] - elif unitX < 3: - dist = 2.25 / 2 * 19.05 - 19.05 / 2 - stem_pts = [(0,0), (dist, 0), (-dist,0)] - else: - dist = unitX / 2 * 19.05 - 19.05 / 2 - stem_pts = [(0,0), (dist, 0), (-dist,0)] - - stem1 = ( - cq.Sketch() - .rect(tx, 0.8) - .push(stem_pts) - .rect(0.8, ty) - .circle(2.75) - .clean() - ) - - stem2 = ( - cq.Sketch() - .push(stem_pts) - .rect(4.1, 1.25) - .rect(1.1, 4.1) - .clean() - ) - - keycap = ( - keycap.faces("Z[1]").edges("|X or |Y") - .chamfer(0.2) - ) - - # Add the legend if present - if legend: - legend = ( - cq.Workplane("XY").transformed(offset=cq.Vector(0, 0, height+1), rotate=cq.Vector(angle, 0, 0)) - .text(legend, fontsize, -4, font=font, halign="center") - ) - bb = legend.val().BoundingBox() - # try to center the legend horizontally - legend = legend.translate((-bb.center.x, 0, 0)) - - legend = legend - keycap - legend = legend.translate((0,0,-1)) - keycap = keycap - legend - legend = legend - tool # this can be used to export the legend for 2 colors 3D printing - - #show_object(legend, name="legend", options={'color': 'blue', 'alpha': 0}) - - """ - # Example to cut a DXF logo out of the keycap - logo = ( - cq.importers - .importDXF('logo.dxf') - .wires().toPending() - .extrude(-2) - .translate((-5,-4.6,height)) # needs centering - .rotateAboucurventer((1,0,0), angle) - ) - keycap = keycap - logo - """ - - return keycap diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4565c3e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=61.0", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "opk" +version = "0.0.16" +authors = [ + { name="Matteo (Matt3o) Spinelli", email="info@matt3o.com" }, +] + +description = "Open Programmatic Keycap" +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.7" + +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", +] + +dependencies = [ + "cadquery==2.2.0b1", +] + +[project.urls] +"Homepage" = "https://github.com/cubiq/OPK" +"Bug Tracker" = "https://github.com/cubiq/OPK/issues" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/opk.py b/src/opk.py new file mode 100644 index 0000000..a814e60 --- /dev/null +++ b/src/opk.py @@ -0,0 +1,272 @@ +""" +========================== + ██████ ██████ ██ ██ + ██ ██ ██ ██ ██ ██ + ██ ██ ██████ █████ + ██ ██ ██ ██ ██ + ██████ ██ ██ ██ +========================== + Open Programmatic Keycap +========================== + +OPK is a spherical top keycap profile developed in CadQuery +(https://github.com/CadQuery/cadquery) and released under the very permissive +Apache License 2.0. It's especially suited for creating high/medium profile, +spherical top keycaps. + +!!! The profile is still highly experimental and very alpha stage. ¡¡¡ + +If you use the code please give credit, if you do modifications consider +releasing them back to the public under a permissive open source license. + +Copyright (c) 2022 Matteo "Matt3o" Spinelli +https://matt3o.com +""" + +import math +import cadquery as cq + +def keycap( + unitX: float = 1, # keycap size in unit. Standard sizes: 1, 1.25, 1.5, ... + unitY: float = 1, + base: float = 18.2, # 1-unit size in mm at the base + top: float = 13.2, # 1-unit size in mm at the top, actual hitting area will be slightly bigger + curv: float = 1.7, # Top side curvature. Higher value makes the top rounder (use small increments) + bFillet: float = 0.5, # Fillet at the base + tFillet: float = 5, # Fillet at the top + height: float = 13, # Height of the keycap before cutting the scoop (final height is lower) + angle: float = 7, # Angle of the top surface + depth: float = 2.8, # Scoop depth + thickness: float = 1.5, # Keycap sides thickness + convex: bool = False, # Is this a spacebar? + legend: str = "", # Legend + legendDepth: float = -1.0, # How deep to carve the legend, positive value makes the legend embossed + font: str = "sans-serif", # font name, use a font name including extension to use a local file + fontsize: float = 10, # the font size is in units + pos: bool = False # use POS style stabilizers +): + + top_diff = base - top + + curv = min(curv, 1.9) + + bx = 19.05 * unitX - (19.05 - base) + by = 19.05 * unitY - (19.05 - base) + + tx = bx - top_diff + ty = by - top_diff + + # if spacebar make the top less round-y + tension = .4 if convex else 1 + + if unitX < 2 and unitY < 2: + pos = False + + # Three-section loft of rounded rectangles. Can't find a better way to do variable fillet + base = ( + cq.Sketch() + .rect(bx, by) + .vertices() + .fillet(bFillet) + ) + + mid = ( + cq.Sketch() + .rect(bx, by) + .vertices() + .fillet((tFillet-bFillet)/3) + ) + + top = ( + cq.Sketch() + .arc((curv, curv*tension), (0, ty/2), (curv, ty-curv*tension)) + .arc((curv, ty-curv*tension), (tx/2, ty), (tx-curv, ty-curv*tension)) + .arc((tx-curv, ty-curv*tension), (tx, ty/2), (tx-curv, curv*tension)) + .arc((tx-curv, curv*tension), (tx/2, 0), (curv, curv*tension)) + .assemble() + .vertices() + .fillet(tFillet) + .moved(cq.Location(cq.Vector(-tx/2, -ty/2, 0))) + ) + + # Main shape + keycap = ( + cq.Workplane("XY") + .placeSketch(base, + mid.moved(cq.Location(cq.Vector(0, 0, height/4), cq.Vector(1,0,0), angle/4)), + top.moved(cq.Location(cq.Vector(0, 0, height), cq.Vector(1,0,0), angle)) + ) + .loft() + ) + + # Create a body that will be carved from the main shape to create the top scoop + if convex: + scoop = ( + cq.Workplane("YZ").transformed(offset=cq.Vector(0, height-2.1, -bx/2), rotate=cq.Vector(0, 0, angle)) + .moveTo(-by/2, -1) + .threePointArc((0, 2), (by/2, -1)) + .lineTo(by/2, 10) + .lineTo(-by/2, 10) + .close() + .extrude(bx, combine=False) + ) + else: + scoop = ( + cq.Workplane("YZ").transformed(offset=cq.Vector(0, height, bx/2), rotate=cq.Vector(0, 0, angle)) + .moveTo(-by/2+2,0) + .threePointArc((0, min(-0.1, -depth+1.5)), (by/2-2, 0)) + .lineTo(by/2, height) + .lineTo(-by/2, height) + .close() + .workplane(offset=-bx/2) + .moveTo(-by/2-2, -0.5) + .threePointArc((0, -depth), (by/2+2, -0.5)) + .lineTo(by/2, height) + .lineTo(-by/2, height) + .close() + .workplane(offset=-bx/2) + .moveTo(-by/2+2, 0) + .threePointArc((0, min(-0.1, -depth+1.5)), (by/2-2, 0)) + .lineTo(by/2, height) + .lineTo(-by/2, height) + .close() + .loft(combine=False) + ) + + #show_object(tool, options={'alpha': 0.4}) + keycap = keycap - scoop + + # Top edge fillet + keycap = keycap.edges(">Z").fillet(0.6) + + # Since the shell() function is not able to deal with complex shapes + # we need to subtract a smaller keycap from the main shape + shell = ( + cq.Workplane("XY").rect(bx-thickness*2, by-thickness*2) + .workplane(offset=height/4).rect(bx-thickness*3, by-thickness*3) + .workplane().transformed(offset=cq.Vector(0, 0, height-height/4-4.5), rotate=cq.Vector(angle, 0, 0)).rect(tx-thickness*2+.5, ty-thickness*2+.5) + .loft() + ) + keycap = keycap - shell + + # create a temporary surface that will be used to project the stems to + # this is needed because extrude(face) needs the entire extruded outline to be contained inside the destination face + tmpface = shell.faces('>Z').workplane().rect(bx*2, by*2).val() + tmpface = cq.Face.makeFromWires(tmpface) + + # Build the stem and the keycap guts + + if pos: # POS-like stems + stem_pts = [] + ribh_pts = [] + ribv_pts = [] + + stem_num_x = math.floor(unitX) + stem_num_y = math.floor(unitY) + stem_start_x = round(-19.05 * (stem_num_x / 2) + 19.05 / 2, 6) + stem_start_y = round(-19.05 * (stem_num_y / 2) + 19.05 / 2, 6) + + for i in range(0, stem_num_y): + ribh_pts.extend([(0, stem_start_y+i*19.05)]) + for l in range(0, stem_num_x): + if i == 0: + ribv_pts.extend([(stem_start_x+l*19.05, 0)]) + stem_pts.extend([(stem_start_x+l*19.05, stem_start_y+i*19.05)]) + + else: # standard stems + stem_pts = [(0,0)] + + if ( unitY > unitX ): + if unitY > 2.75: + dist = unitY / 2 * 19.05 - 19.05 / 2 + stem_pts.extend([(0, dist), (0, -dist)]) + elif unitY > 1.75: + dist = 2.25 / 2 * 19.05 - 19.05 / 2 + stem_pts.extend([(0, -dist), (0, dist)]) + + ribh_pts = stem_pts + ribv_pts = [(0,0)] + else: + if unitX > 2.75: + dist = unitX / 2 * 19.05 - 19.05 / 2 + stem_pts.extend([(dist, 0), (-dist,0)]) + elif unitX > 1.75: # keycaps smaller than 3unit all have 2.25 stabilizers + dist = 2.25 / 2 * 19.05 - 19.05 / 2 + stem_pts.extend([(dist, 0), (-dist,0)]) + + ribh_pts = [(0,0)] + ribv_pts = stem_pts + + # this is the stem + + stem2 = ( + cq.Sketch() + .push(stem_pts) + .rect(4.15, 1.27) + .rect(1.12, 4.15) + .clean() + ) + + keycap = ( + keycap.faces("Z[1]").edges("|X or |Y") + .chamfer(0.2) + ) + + # Add the legend if present + if legend and legendDepth != 0: + fontPath = '' + if font.endswith((".otf", ".ttf", ".ttc")): + fontPath = font + font = '' + + if legend.endswith('.dxf'): + legend = ( + cq.importers + .importDXF(legend) + .wires().toPending() + .extrude(-4) + .translate((0,0,height+1)) + .rotateAboutCenter((1,0,0), angle) + ) + # center the legend + bb = legend.val().BoundingBox() + legend = legend.translate((-bb.center.x, -bb.center.y, 0)) + else: + legend = ( + cq.Workplane("XY").transformed(offset=cq.Vector(0, 0, height+1), rotate=cq.Vector(angle, 0, 0)) + .text(legend, fontsize, -4, font=font, fontPath=fontPath, halign="center", valign="center") + ) + bb = legend.val().BoundingBox() + # only center horizontally to keep the baseline + legend = legend.translate((-bb.center.x, 0, 0)) + + if legendDepth < 0: + legend = legend - keycap + legend = legend.translate((0,0,legendDepth)) + keycap = keycap - legend + legend = legend - scoop # this can be used to export the legend for 2 colors 3D printing + else: + scoop = scoop.translate((0,0,legendDepth)) + legend = legend - scoop + legend = legend - keycap # use this for multi-color 3D printing + keycap = keycap + legend + + #show_object(legend, name="legend", options={'color': 'blue', 'alpha': 0}) + + return keycap