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