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.

174 lines
5.0 KiB

2 years ago
from dataclasses import dataclass
from enum import Enum
import pcbnew
from pcbnew import VECTOR2I
import os
import qrcode
import wx
def relpath(p):
return os.path.join(os.path.dirname(__file__), f"./{p}")
class QrSpec:
@dataclass
class Params:
class Shape(Enum):
ROUND = "round"
SQUARE = "square" # todo: squares / rect
dot_size: float
border_size: int
invert: bool
shape: Shape
@classmethod
def default(cls):
return cls(dot_size=0.5, border_size=2, invert=False, shape=cls.Shape.ROUND)
@classmethod
def from_header(cls, header):
_, h = header.split("qrgen!")
opts = h.strip().split(" ")
params = cls.default()
for opt in opts:
if "=" in opt:
inst, value = opt.split("=")
else:
inst = opt
value = None
match inst:
case "dot_size":
params.dot_size = float(value)
case "border_size":
params.border_size = int(value)
case "invert":
params.invert = True
case "shape":
params.shape = cls.Shape(value)
return params
def __init__(self, raw_text: str):
self._raw = raw_text
self.header = self.content = self.params = None
self.init_success = False
self.init()
def init(self):
self.header, self.content = self._raw.split("\n", maxsplit=1)
self.params = self.Params.from_header(self.header)
self.init_success = True
def __repr__(self):
return f"QrSpec(header={repr(self.header)}, params={repr(self.params)}, content={repr(self.content)})"
def as_line_segments(self):
q = qrcode.QRCode()
q.add_data(self.content)
q.border = self.params.border_size
data = q.get_matrix()
if self.params.invert:
data = [[not p for p in row] for row in data]
return array_to_lines(data)
def array_to_lines(array: list[list[bool]]) -> tuple[list[list[tuple[int, int]]], list[list[tuple[int, int]]]]:
horiz = [line_to_segments(row) for row in array]
vert = [line_to_segments(col[::-1]) for col in list(zip(*array[::-1]))] # ?????
return horiz, vert
def line_to_segments(line: list[bool]) -> list[tuple[int, int]]:
in_seg = line[0]
out = []
start = 0
for i, pix in enumerate(line):
if pix and not in_seg:
start = i
in_seg = True
elif not pix and in_seg:
out.append((start, i-1))
in_seg = False
if in_seg:
out.append((start, len(line)-1))
return out
def handle_exception(e):
dlg = wx.MessageDialog(None, f"error in processing: {e}")
dlg.ShowModal()
dlg.Destroy()
return
def add_line(parent, start, end, width, layer):
seg = pcbnew.PCB_SHAPE(parent)
seg.SetShape(pcbnew.SHAPE_T_SEGMENT)
seg.SetStart(start)
seg.SetEnd(end)
seg.SetWidth(int(width))
seg.SetLayer(layer)
parent.Add(seg)
def qrgen(source_elem: pcbnew.PCB_TEXT):
raw = str(source_elem.GetText())
layer = source_elem.GetLayer()
x = source_elem.GetX()
y = source_elem.GetY()
qr = QrSpec(raw)
board = pcbnew.GetBoard()
h_lines, v_lines = qr.as_line_segments()
params = qr.params
dsize = int(params.dot_size * 1e6)
all_segs = []
for i, line in enumerate(h_lines):
y_offset = i * dsize
for start, end in line:
all_segs.append((VECTOR2I(x + start * dsize, y + y_offset),
VECTOR2I(x + end * dsize, y + y_offset)))
for i, col in enumerate(v_lines):
x_offset = i * dsize
for start, end in col:
all_segs.append((VECTOR2I(x + x_offset, y + start * dsize),
VECTOR2I(x + x_offset, y + end * dsize)))
for a, b in all_segs:
add_line(board, a, b, dsize, layer)
class QrgenPluginAction(pcbnew.ActionPlugin):
def defaults(self):
self.name = "generate qr codes"
self.category = "Modify PCB"
self.description = "generate qr codes without leaving pcbnew"
self.show_toolbar_button = True
self.icon_file_name = relpath("qrgen.png")
def Run(self):
qr_text: list[pcbnew.PCB_TEXT] = [text for text in pcbnew.GetCurrentSelection()
if isinstance(text, pcbnew.PCB_TEXT)
and str(text.GetText()).startswith("qrgen!")]
for elem in qr_text:
try:
qrgen(elem)
except Exception as e:
handle_exception(e)
pcbnew.Refresh()