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
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()
|
||
|
|
||
|
|