Some example templates as SVG for laser cutting
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

208 lines
6.0 KiB

#!/usr/bin/env python3
"""
Render a code wheel to look up passwords or similar.
"""
import argparse
import secrets
import string
import svgwrite
import math
from HersheyFonts import HersheyFonts
STROKE_WIDTH = 0.2
RADIUS = 30
parser = argparse.ArgumentParser()
parser.add_argument("output")
parser.add_argument("--stroke-width", type=float, default=STROKE_WIDTH)
parser.add_argument("--bottom", action="store_true")
parser.add_argument("--radius", type=float, default=RADIUS)
args = parser.parse_args()
STROKE_WIDTH = args.stroke_width
RADIUS = args.radius
dwg = svgwrite.Drawing(
args.output, ("210mm", "297mm"), profile="tiny", viewBox="0 0 210 297"
)
# add stroke width to match outer radius
c = dwg.circle(
r=RADIUS + STROKE_WIDTH, stroke_width=STROKE_WIDTH, fill="none", stroke="black"
)
dwg.add(c)
# do not add stroke width to get 4.1mm hole
c = dwg.circle(r=2, stroke_width=STROKE_WIDTH, fill="none", stroke="black")
dwg.add(c)
WIDTH = 7
HEIGHT = 7.1
inner_reserved = 1.2
slice_share = 0.7
words = []
import mnemonic
m = mnemonic.Mnemonic("english")
for word in m.wordlist:
if len(word) <= 5:
words.append(word.upper())
print(f"{len(words)} words")
slices = 16
window_slice = 12
used_words = set()
for i in range(slices):
slice_deg = (2 * math.pi) / slices
deg = i * slice_deg
thefont = HersheyFonts()
thefont.load_default_font("futural")
thefont.normalize_rendering(3)
g2 = dwg.g()
g = dwg.g()
text = secrets.choice(list(set(words) - used_words))
print(f"word {i}: {text}")
used_words.add(text)
strokes = list(thefont.strokes_for_text(text))
minp = [math.inf, math.inf]
maxp = [-math.inf, -math.inf]
for stroke in strokes:
for d in range(2):
for p in stroke:
minp[d] = min(minp[d], p[d])
maxp[d] = max(maxp[d], p[d])
centerx = (minp[0] + maxp[0]) / 2
centery = (minp[1] + maxp[1]) / 2
for stroke in strokes:
for idx, point in enumerate(stroke):
stroke[idx] = (point[0] - centerx, -point[1] + centery)
pl = dwg.polyline(
stroke, stroke="green", stroke_width=STROKE_WIDTH, fill="none"
)
if args.bottom or i != window_slice:
g.add(pl)
r = dwg.rect(
(-6, -1.8),
(12, 3.6),
stroke="black" if i == window_slice and not args.bottom else "none",
stroke_width=STROKE_WIDTH,
fill="none",
)
g.add(r)
g.rotate(-90 - (180 / math.pi) * deg)
g2.add(g)
g2.translate(RADIUS * 0.745 * math.sin(deg), RADIUS * 0.745 * math.cos(deg))
dwg.add(g2)
outer_bits = set()
if not args.bottom:
# around the word window
outer_bits.add(window_slice - 1)
outer_bits.add(window_slice)
for i in range(slices):
if secrets.choice([False, True]):
outer_bits.add(i)
for j in range(3):
r1 = (j + inner_reserved + 0.8) * HEIGHT
r2 = (j + inner_reserved) * HEIGHT
slices_to_skip = set()
if j == 1:
# outer slices are alternating
slices_to_skip |= outer_bits
elif j == 2:
# outer slices are alternating
slices_to_skip = set(range(slices)) - outer_bits
else:
for i in range(slices // 2):
slices_to_skip.add(
secrets.choice(list(set(range(slices)) - slices_to_skip))
)
print(f"ring {j}, {slices} slices, {len(slices_to_skip)} to skip")
slice_deg = (2 * math.pi) / slices
if j >= 1:
# outer slices are smaller because of the words between them
slice_share = 0.4
for i in range(slices):
offset = 0 if j < 1 else 0.3
deg = (i + offset) * slice_deg
ndeg = (i + offset + slice_share) * slice_deg
skip = i in slices_to_skip
if j == 0 and args.bottom:
skip = True
if skip:
thefont = HersheyFonts()
thefont.load_default_font("futural")
# use a smaller font the middle ring
thefont.normalize_rendering(3.6 if j == 1 else 4)
g2 = dwg.g()
g = dwg.g()
text = secrets.choice(string.ascii_uppercase + string.digits)
strokes = list(thefont.strokes_for_text(text))
minp = [math.inf, math.inf]
maxp = [-math.inf, -math.inf]
for stroke in strokes:
for d in range(2):
for p in stroke:
minp[d] = min(minp[d], p[d])
maxp[d] = max(maxp[d], p[d])
centerx = (minp[0] + maxp[0]) / 2
centery = (minp[1] + maxp[1]) / 2
for stroke in strokes:
for idx, point in enumerate(stroke):
stroke[idx] = (point[0] - centerx, -point[1] + centery)
pl = dwg.polyline(
stroke, stroke="green", stroke_width=STROKE_WIDTH, fill="none"
)
g.add(pl)
g.rotate(180 - ((180 / math.pi) * (deg + ndeg) / 2))
g2.add(g)
g2.translate(
(r1 + r2) / 2.0 * math.sin((deg + ndeg) / 2),
(r1 + r2) / 2.0 * math.cos((deg + ndeg) / 2),
)
dwg.add(g2)
skip_color = "green"
if not args.bottom and j == 1 and (i == window_slice - 1 or i == window_slice):
skip_color = "none"
p = dwg.path(
stroke_width=STROKE_WIDTH,
fill="none",
stroke=skip_color if skip else "black",
)
p.push("M")
p.push(r1 * math.sin(deg))
p.push(r1 * math.cos(deg))
p.push_arc(
(r1 * math.sin(ndeg), r1 * math.cos(ndeg)),
0,
r1,
large_arc=False,
angle_dir="-",
absolute=True,
)
p.push("L")
p.push(r2 * math.sin(ndeg))
p.push(r2 * math.cos(ndeg))
p.push_arc(
(r2 * math.sin(deg), r2 * math.cos(deg)),
0,
r2,
large_arc=False,
angle_dir="+",
absolute=True,
)
p.push("Z")
dwg.add(p)
dwg.save()