update plugins
This commit is contained in:
parent
cd1fd4bdfa
commit
b2d8f5dd92
BIN
3rdparty/.DS_Store
vendored
BIN
3rdparty/.DS_Store
vendored
Binary file not shown.
BIN
3rdparty/plugins/.DS_Store
vendored
BIN
3rdparty/plugins/.DS_Store
vendored
Binary file not shown.
24
3rdparty/plugins/com_github_30350n_pcb2blender/__init__.py
vendored
Normal file
24
3rdparty/plugins/com_github_30350n_pcb2blender/__init__.py
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pcbnew, wx
|
||||||
|
|
||||||
|
from .export import export_pcb3d, get_boarddefs
|
||||||
|
from .ui import SettingsDialog
|
||||||
|
|
||||||
|
class Pcb2BlenderExporter(pcbnew.ActionPlugin):
|
||||||
|
def defaults(self):
|
||||||
|
self.name = "Export to Blender (.pcb3d)"
|
||||||
|
self.category = "Export"
|
||||||
|
self.show_toolbar_button = True
|
||||||
|
self.icon_file_name = (
|
||||||
|
Path(__file__).parent / "images" / "blender_icon_32x32.png").as_posix()
|
||||||
|
self.description = "Export 3D Model to Blender."
|
||||||
|
|
||||||
|
def Run(self):
|
||||||
|
board = pcbnew.GetBoard()
|
||||||
|
boarddefs, ignored = get_boarddefs(board)
|
||||||
|
with SettingsDialog(None, boarddefs, ignored) as dialog:
|
||||||
|
if dialog.ShowModal() == wx.OK:
|
||||||
|
export_pcb3d(dialog.file_picker.GetPath(), boarddefs)
|
||||||
|
|
||||||
|
Pcb2BlenderExporter().register()
|
319
3rdparty/plugins/com_github_30350n_pcb2blender/export.py
vendored
Normal file
319
3rdparty/plugins/com_github_30350n_pcb2blender/export.py
vendored
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import re, shutil, struct, tempfile
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import IntEnum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
|
from zipfile import ZIP_DEFLATED, ZipFile
|
||||||
|
|
||||||
|
import pcbnew
|
||||||
|
from pcbnew import DRILL_MARKS_NO_DRILL_SHAPE, PLOT_CONTROLLER, PLOT_FORMAT_SVG, ToMM
|
||||||
|
|
||||||
|
PCB = "pcb.wrl"
|
||||||
|
COMPONENTS = "components"
|
||||||
|
LAYERS = "layers"
|
||||||
|
LAYERS_BOUNDS = "bounds"
|
||||||
|
LAYERS_STACKUP = "stackup"
|
||||||
|
BOARDS = "boards"
|
||||||
|
BOUNDS = "bounds"
|
||||||
|
STACKED = "stacked_"
|
||||||
|
PADS = "pads"
|
||||||
|
|
||||||
|
INCLUDED_LAYERS = (
|
||||||
|
"F_Cu", "B_Cu", "F_Paste", "B_Paste", "F_SilkS", "B_SilkS", "F_Mask", "B_Mask"
|
||||||
|
)
|
||||||
|
|
||||||
|
SVG_MARGIN = 1.0 # mm
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StackedBoard:
|
||||||
|
name: str
|
||||||
|
offset: Tuple[float, float, float]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BoardDef:
|
||||||
|
name: str
|
||||||
|
bounds: Tuple[float, float, float, float]
|
||||||
|
stacked_boards: List[StackedBoard] = field(default_factory=list)
|
||||||
|
|
||||||
|
class KiCadColor(IntEnum):
|
||||||
|
CUSTOM = 0
|
||||||
|
GREEN = 1
|
||||||
|
RED = 2
|
||||||
|
BLUE = 3
|
||||||
|
PURPLE = 4
|
||||||
|
BLACK = 5
|
||||||
|
WHITE = 6
|
||||||
|
YELLOW = 7
|
||||||
|
|
||||||
|
class SurfaceFinish(IntEnum):
|
||||||
|
HASL = 0
|
||||||
|
ENIG = 1
|
||||||
|
NONE = 2
|
||||||
|
|
||||||
|
SURFACE_FINISH_MAP = {
|
||||||
|
"ENIG": SurfaceFinish.ENIG,
|
||||||
|
"ENEPIG": SurfaceFinish.ENIG,
|
||||||
|
"Hard gold": SurfaceFinish.ENIG,
|
||||||
|
"HT_OSP": SurfaceFinish.NONE,
|
||||||
|
"OSP": SurfaceFinish.NONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Stackup:
|
||||||
|
thickness_mm: float = 1.6
|
||||||
|
mask_color: KiCadColor = KiCadColor.GREEN
|
||||||
|
mask_color_custom: Tuple[int, int, int] = (0, 0, 0)
|
||||||
|
silks_color: KiCadColor = KiCadColor.WHITE
|
||||||
|
silks_color_custom: Tuple[int, int, int] = (0, 0, 0)
|
||||||
|
surface_finish: SurfaceFinish = SurfaceFinish.HASL
|
||||||
|
|
||||||
|
def pack(self) -> bytes:
|
||||||
|
return struct.pack("!fbBBBbBBBb",
|
||||||
|
self.thickness_mm,
|
||||||
|
self.mask_color, *self.mask_color_custom,
|
||||||
|
self.silks_color, *self.silks_color_custom,
|
||||||
|
self.surface_finish,
|
||||||
|
)
|
||||||
|
|
||||||
|
def export_pcb3d(filepath, boarddefs):
|
||||||
|
init_tempdir()
|
||||||
|
|
||||||
|
wrl_path = get_temppath(PCB)
|
||||||
|
components_path = get_temppath(COMPONENTS)
|
||||||
|
pcbnew.ExportVRML(wrl_path, 0.001, True, True, components_path, 0.0, 0.0)
|
||||||
|
|
||||||
|
layers_path = get_temppath(LAYERS)
|
||||||
|
board = pcbnew.GetBoard()
|
||||||
|
box = board.ComputeBoundingBox(aBoardEdgesOnly=True)
|
||||||
|
bounds = (
|
||||||
|
ToMM(box.GetLeft()) - SVG_MARGIN, ToMM(box.GetTop()) - SVG_MARGIN,
|
||||||
|
ToMM(box.GetRight() - box.GetLeft()) + SVG_MARGIN * 2,
|
||||||
|
ToMM(box.GetBottom() - box.GetTop()) + SVG_MARGIN * 2,
|
||||||
|
)
|
||||||
|
export_layers(board, bounds, layers_path)
|
||||||
|
|
||||||
|
with ZipFile(filepath, mode="w", compression=ZIP_DEFLATED) as file:
|
||||||
|
# always ensure the COMPONENTS, LAYERS and BOARDS directories are created
|
||||||
|
file.writestr(f"{COMPONENTS}/", "")
|
||||||
|
file.writestr(f"{LAYERS}/", "")
|
||||||
|
file.writestr(f"{BOARDS}/", "")
|
||||||
|
|
||||||
|
file.write(wrl_path, PCB)
|
||||||
|
for path in components_path.glob("**/*.wrl"):
|
||||||
|
file.write(path, f"{COMPONENTS}/{path.name}")
|
||||||
|
|
||||||
|
for path in layers_path.glob("**/*.svg"):
|
||||||
|
file.write(path, f"{LAYERS}/{path.name}")
|
||||||
|
file.writestr(f"{LAYERS}/{LAYERS_BOUNDS}", struct.pack("!ffff", *bounds))
|
||||||
|
file.writestr(f"{LAYERS}/{LAYERS_STACKUP}", get_stackup(board).pack())
|
||||||
|
|
||||||
|
for boarddef in boarddefs.values():
|
||||||
|
subdir = f"{BOARDS}/{boarddef.name}"
|
||||||
|
file.writestr(f"{subdir}/{BOUNDS}", struct.pack("!ffff", *boarddef.bounds))
|
||||||
|
|
||||||
|
for stacked in boarddef.stacked_boards:
|
||||||
|
file.writestr(
|
||||||
|
f"{subdir}/{STACKED}{stacked.name}",
|
||||||
|
struct.pack("!fff", *stacked.offset)
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, footprint in enumerate(board.Footprints()):
|
||||||
|
has_model = len(footprint.Models()) > 0
|
||||||
|
is_tht_or_smd = bool(
|
||||||
|
footprint.GetAttributes() & (pcbnew.FP_THROUGH_HOLE | pcbnew.FP_SMD))
|
||||||
|
value = footprint.GetValue()
|
||||||
|
reference = footprint.GetReference()
|
||||||
|
for j, pad in enumerate(footprint.Pads()):
|
||||||
|
name = sanitized(f"{value}_{reference}_{i}_{j}")
|
||||||
|
is_flipped = pad.IsFlipped()
|
||||||
|
has_paste = pad.IsOnLayer(pcbnew.B_Paste if is_flipped else pcbnew.F_Paste)
|
||||||
|
data = struct.pack(
|
||||||
|
"!ff????BBffffBff",
|
||||||
|
*map(ToMM, pad.GetPosition()),
|
||||||
|
is_flipped,
|
||||||
|
has_model,
|
||||||
|
is_tht_or_smd,
|
||||||
|
has_paste,
|
||||||
|
pad.GetAttribute(),
|
||||||
|
pad.GetShape(),
|
||||||
|
*map(ToMM, pad.GetSize()),
|
||||||
|
pad.GetOrientation().AsRadians(),
|
||||||
|
pad.GetRoundRectRadiusRatio(),
|
||||||
|
pad.GetDrillShape(),
|
||||||
|
*map(ToMM, pad.GetDrillSize()),
|
||||||
|
)
|
||||||
|
file.writestr(f"{PADS}/{name}", data)
|
||||||
|
|
||||||
|
def get_boarddefs(board):
|
||||||
|
boarddefs = {}
|
||||||
|
ignored = []
|
||||||
|
|
||||||
|
tls = {}
|
||||||
|
brs = {}
|
||||||
|
stacks = {}
|
||||||
|
for drawing in board.GetDrawings():
|
||||||
|
if drawing.Type() == pcbnew.PCB_TEXT_T:
|
||||||
|
text_obj = drawing.Cast()
|
||||||
|
text = text_obj.GetText()
|
||||||
|
|
||||||
|
if not text.startswith("PCB3D_"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
pos = tuple(map(ToMM, text_obj.GetPosition()))
|
||||||
|
if text.startswith("PCB3D_TL_"):
|
||||||
|
tls.setdefault(text, pos)
|
||||||
|
elif text.startswith("PCB3D_BR_"):
|
||||||
|
brs.setdefault(text, pos)
|
||||||
|
elif text.startswith("PCB3D_STACK_"):
|
||||||
|
stacks.setdefault(text, pos)
|
||||||
|
else:
|
||||||
|
ignored.append(text)
|
||||||
|
|
||||||
|
for tl_str in tls.copy():
|
||||||
|
name = tl_str[9:]
|
||||||
|
br_str = "PCB3D_BR_" + name
|
||||||
|
if br_str in brs:
|
||||||
|
tl_pos = tls.pop(tl_str)
|
||||||
|
br_pos = brs.pop(br_str)
|
||||||
|
|
||||||
|
boarddef = BoardDef(
|
||||||
|
sanitized(name),
|
||||||
|
(tl_pos[0], tl_pos[1], br_pos[0] - tl_pos[0], br_pos[1] - tl_pos[1])
|
||||||
|
)
|
||||||
|
boarddefs[boarddef.name] = boarddef
|
||||||
|
|
||||||
|
for stack_str in stacks.copy():
|
||||||
|
try:
|
||||||
|
other, onto, target, z_offset = stack_str[12:].split("_")
|
||||||
|
z_offset = float(z_offset)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if onto != "ONTO":
|
||||||
|
continue
|
||||||
|
|
||||||
|
other_name = sanitized(other)
|
||||||
|
target_name = sanitized(target)
|
||||||
|
|
||||||
|
if other_name not in set(boarddefs) | {"FPNL"} or target_name not in boarddefs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stack_pos = stacks.pop(stack_str)
|
||||||
|
target_pos = boarddefs[target_name].bounds[:2]
|
||||||
|
stacked = StackedBoard(
|
||||||
|
other_name,
|
||||||
|
(stack_pos[0] - target_pos[0], stack_pos[1] - target_pos[1], z_offset)
|
||||||
|
)
|
||||||
|
boarddefs[target_name].stacked_boards.append(stacked)
|
||||||
|
|
||||||
|
ignored += list(tls.keys()) + list(brs.keys()) + list(stacks.keys())
|
||||||
|
|
||||||
|
return boarddefs, ignored
|
||||||
|
|
||||||
|
def get_stackup(board):
|
||||||
|
stackup = Stackup()
|
||||||
|
|
||||||
|
tmp_path = get_temppath("pcb2blender_tmp.kicad_pcb")
|
||||||
|
board.Save(str(tmp_path))
|
||||||
|
content = tmp_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
if not (match := stackup_regex.search(content)):
|
||||||
|
return stackup
|
||||||
|
stackup_content = match.group(0)
|
||||||
|
|
||||||
|
if matches := stackup_thickness_regex.finditer(stackup_content):
|
||||||
|
stackup.thickness_mm = sum(float(match.group(1)) for match in matches)
|
||||||
|
|
||||||
|
if match := stackup_mask_regex.search(stackup_content):
|
||||||
|
stackup.mask_color, stackup.mask_color_custom = parse_kicad_color(match.group(1))
|
||||||
|
|
||||||
|
if match := stackup_silks_regex.search(stackup_content):
|
||||||
|
stackup.silks_color, stackup.silks_color_custom = parse_kicad_color(match.group(1))
|
||||||
|
|
||||||
|
if match := stackup_copper_finish_regex.search(stackup_content):
|
||||||
|
stackup.surface_finish = SURFACE_FINISH_MAP.get(match.group(1), SurfaceFinish.HASL)
|
||||||
|
|
||||||
|
return stackup
|
||||||
|
|
||||||
|
def parse_kicad_color(string):
|
||||||
|
if string[0] == "#":
|
||||||
|
return KiCadColor.CUSTOM, hex2rgb(string[1:7])
|
||||||
|
else:
|
||||||
|
return KiCadColor[string.upper()], (0, 0, 0)
|
||||||
|
|
||||||
|
def export_layers(board, bounds, output_directory: Path):
|
||||||
|
plot_controller = PLOT_CONTROLLER(board)
|
||||||
|
plot_options = plot_controller.GetPlotOptions()
|
||||||
|
plot_options.SetOutputDirectory(output_directory)
|
||||||
|
|
||||||
|
plot_options.SetPlotFrameRef(False)
|
||||||
|
plot_options.SetAutoScale(False)
|
||||||
|
plot_options.SetScale(1)
|
||||||
|
plot_options.SetMirror(False)
|
||||||
|
plot_options.SetUseGerberAttributes(True)
|
||||||
|
plot_options.SetDrillMarksType(DRILL_MARKS_NO_DRILL_SHAPE)
|
||||||
|
|
||||||
|
for layer in INCLUDED_LAYERS:
|
||||||
|
plot_controller.SetLayer(getattr(pcbnew, layer))
|
||||||
|
plot_controller.OpenPlotfile(layer, PLOT_FORMAT_SVG, "")
|
||||||
|
plot_controller.PlotLayer()
|
||||||
|
filepath = Path(plot_controller.GetPlotFileName())
|
||||||
|
plot_controller.ClosePlot()
|
||||||
|
filepath = filepath.replace(filepath.parent / f"{layer}.svg")
|
||||||
|
|
||||||
|
content = filepath.read_text(encoding="utf-8")
|
||||||
|
width = f"{bounds[2]:.6f}mm"
|
||||||
|
height = f"{bounds[3]:.6f}mm"
|
||||||
|
viewBox = " ".join(f"{value:.6f}" for value in bounds)
|
||||||
|
content = svg_header_regex.sub(svg_header_sub.format(width, height, viewBox), content)
|
||||||
|
filepath.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
def sanitized(name):
|
||||||
|
return re.sub(r"[\W]+", "_", name)
|
||||||
|
|
||||||
|
def get_tempdir():
|
||||||
|
return Path(tempfile.gettempdir()) / "pcb2blender_tmp"
|
||||||
|
|
||||||
|
def get_temppath(filename):
|
||||||
|
return get_tempdir() / filename
|
||||||
|
|
||||||
|
def init_tempdir():
|
||||||
|
tempdir = get_tempdir()
|
||||||
|
if tempdir.exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(tempdir)
|
||||||
|
except OSError:
|
||||||
|
try:
|
||||||
|
# try to delete all files first
|
||||||
|
for file in tempdir.glob("**/*"):
|
||||||
|
if file.is_file():
|
||||||
|
file.unlink()
|
||||||
|
shutil.rmtree(tempdir)
|
||||||
|
except OSError:
|
||||||
|
# if this still doesn't work, fuck it
|
||||||
|
return
|
||||||
|
tempdir.mkdir()
|
||||||
|
|
||||||
|
def hex2rgb(hex_string):
|
||||||
|
return (
|
||||||
|
int(hex_string[0:2], 16),
|
||||||
|
int(hex_string[2:4], 16),
|
||||||
|
int(hex_string[4:6], 16),
|
||||||
|
)
|
||||||
|
|
||||||
|
svg_header_regex = re.compile(
|
||||||
|
r"<svg([^>]*)width=\"[^\"]*\"[^>]*height=\"[^\"]*\"[^>]*viewBox=\"[^\"]*\"[^>]*>"
|
||||||
|
)
|
||||||
|
svg_header_sub = "<svg\\g<1>width=\"{}\" height=\"{}\" viewBox=\"{}\">"
|
||||||
|
|
||||||
|
stackup_regex = re.compile(
|
||||||
|
r"\(stackup\s*(?:\s*\([^\(\)]*(?:\([^\)]*\)\s*)*\)\s*)*\)", re.MULTILINE
|
||||||
|
)
|
||||||
|
stackup_thickness_regex = re.compile(r"\(thickness\s+([^) ]*)[^)]*\)")
|
||||||
|
stackup_mask_regex = re.compile(
|
||||||
|
r"\(layer\s+\"[FB].Mask\".*?\(color\s+\"([^\)]*)\"\s*\)", re.DOTALL
|
||||||
|
)
|
||||||
|
stackup_silks_regex = re.compile(
|
||||||
|
r"\(layer\s+\"[FB].SilkS\".*?\(color\s+\"([^\)]*)\"\s*\)", re.DOTALL
|
||||||
|
)
|
||||||
|
stackup_copper_finish_regex = re.compile(r"\(copper_finish\s+\"([^\"]*)\"\s*\)")
|
BIN
3rdparty/plugins/com_github_30350n_pcb2blender/images/blender_icon_32x32.png
vendored
Normal file
BIN
3rdparty/plugins/com_github_30350n_pcb2blender/images/blender_icon_32x32.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
108
3rdparty/plugins/com_github_30350n_pcb2blender/ui.py
vendored
Normal file
108
3rdparty/plugins/com_github_30350n_pcb2blender/ui.py
vendored
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import wx
|
||||||
|
|
||||||
|
class SettingsDialog(wx.Dialog):
|
||||||
|
def __init__(self, parent, boarddefs, ignored):
|
||||||
|
wx.Dialog.__init__(self, parent, title="Export to Blender")
|
||||||
|
|
||||||
|
panel = self.init_panel(boarddefs, ignored)
|
||||||
|
sizer = wx.BoxSizer()
|
||||||
|
sizer.Add(panel)
|
||||||
|
self.SetSizerAndFit(sizer)
|
||||||
|
self.SetMinSize((0, 1000))
|
||||||
|
|
||||||
|
self.Center()
|
||||||
|
self.Show()
|
||||||
|
|
||||||
|
def on_export(self, event):
|
||||||
|
path = Path(self.file_picker.GetPath())
|
||||||
|
if path.parent.exists():
|
||||||
|
self.EndModal(wx.OK)
|
||||||
|
else:
|
||||||
|
wx.MessageBox(
|
||||||
|
f"Invalid path \"{path.parent}\"!", caption="Error",
|
||||||
|
style=wx.CENTER | wx.ICON_ERROR | wx.OK
|
||||||
|
)
|
||||||
|
|
||||||
|
def init_panel(self, boarddefs, ignored):
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
|
||||||
|
rows = wx.BoxSizer(orient=wx.VERTICAL)
|
||||||
|
settings = wx.StaticBoxSizer(wx.StaticBox(panel, label="Settings"), orient=wx.VERTICAL)
|
||||||
|
column = wx.BoxSizer()
|
||||||
|
|
||||||
|
text_export_as = wx.StaticText(panel, label="Export as")
|
||||||
|
column.Add(text_export_as, flag=wx.ALL | wx.ALIGN_CENTER, border=5)
|
||||||
|
|
||||||
|
self.file_picker = wx.FilePickerCtrl(
|
||||||
|
panel, message="Export as",
|
||||||
|
wildcard="PCB 3D Model (.pcb3d)|*.pcb3d",
|
||||||
|
style=wx.FLP_SAVE | wx.FLP_USE_TEXTCTRL | wx.FLP_OVERWRITE_PROMPT,
|
||||||
|
size=(300, 25)
|
||||||
|
)
|
||||||
|
column.Add(self.file_picker, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER, border=5)
|
||||||
|
|
||||||
|
settings.Add(column, flag=wx.EXPAND | wx.ALL, border=5)
|
||||||
|
rows.Add(settings, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=5)
|
||||||
|
|
||||||
|
info = wx.StaticBoxSizer(wx.StaticBox(panel, label="Info"), orient=wx.VERTICAL)
|
||||||
|
|
||||||
|
n_boards = max(1, len(boarddefs))
|
||||||
|
plural = "" if n_boards == 1 else "s"
|
||||||
|
text_detected = wx.StaticText(panel, label=f"Detected {n_boards} Board{plural}.")
|
||||||
|
info.Add(text_detected, flag=wx.ALL, border=5)
|
||||||
|
|
||||||
|
for name, boarddef in sorted(boarddefs.items()):
|
||||||
|
label = f"PCB {name}"\
|
||||||
|
f" ({boarddef.bounds[2]:.2f}x{boarddef.bounds[3]:.2f}mm)"
|
||||||
|
if boarddef.stacked_boards:
|
||||||
|
label += " with "
|
||||||
|
for stacked in boarddef.stacked_boards:
|
||||||
|
label += "front panel" if stacked.name == "FPNL" else stacked.name
|
||||||
|
stack_str = ", ".join(f"{f:.2f}" for f in stacked.offset)
|
||||||
|
label += f" stacked at ({stack_str}), "
|
||||||
|
label = label[:-2] + "."
|
||||||
|
|
||||||
|
info.Add(wx.StaticText(panel, label=label), flag=wx.ALL, border=5)
|
||||||
|
|
||||||
|
rows.Add(info, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=5)
|
||||||
|
|
||||||
|
if ignored:
|
||||||
|
warning = wx.StaticBoxSizer(
|
||||||
|
wx.StaticBox(panel, label="Warning (failed to parse some identifiers)"),
|
||||||
|
orient=wx.VERTICAL
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in ignored:
|
||||||
|
warning.Add(wx.StaticText(panel, label=" " + name), flag=wx.ALL, border=5)
|
||||||
|
|
||||||
|
rows.Add(warning, flag=wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, border=5)
|
||||||
|
|
||||||
|
hint = wx.StaticBoxSizer(wx.StaticBox(panel, label="Hint"), orient=wx.VERTICAL)
|
||||||
|
boarddef_hint = ""\
|
||||||
|
"To define a board, specify its bounds by placing a Text Item with the text "\
|
||||||
|
"PCB3D_TL_<boardname> at its top left corner and one with "\
|
||||||
|
"PCB3D_BR_<boardname> at its bottom right corner.\n\n"\
|
||||||
|
"To stack a board A to another board B, add a Text Item with the text "\
|
||||||
|
"PCB3D_STACK_<boardA>_ONTO_<boardB>_<zoffset>\n"\
|
||||||
|
"at the location (relative to the top left corner of board B), "\
|
||||||
|
"where you want the top left corner of A to be.\n"\
|
||||||
|
"(zoffset is given in mm, 10.0 is a good default for 2.54mm headers and sockets)"
|
||||||
|
boarddef_hint_text = wx.StaticText(panel, label=boarddef_hint)
|
||||||
|
boarddef_hint_text.Wrap(400)
|
||||||
|
hint.Add(boarddef_hint_text, flag=wx.ALL, border=5)
|
||||||
|
rows.Add(hint, flag=wx.TOP | wx.LEFT | wx.RIGHT, border=5)
|
||||||
|
|
||||||
|
buttons = wx.BoxSizer()
|
||||||
|
button_cancel = wx.Button(panel, id=wx.ID_CANCEL, label="Cancel", size=(85, 26))
|
||||||
|
buttons.Add(button_cancel, flag=wx.ALL | wx.ALIGN_CENTER, border=5)
|
||||||
|
button_export = wx.Button(panel, id=wx.ID_OK, label="Export", size=(85, 26))
|
||||||
|
button_export.Bind(wx.EVT_BUTTON, self.on_export)
|
||||||
|
buttons.Add(button_export, flag=wx.ALL | wx.ALIGN_CENTER, border=5)
|
||||||
|
|
||||||
|
rows.Add(buttons, flag=wx.ALL | wx.ALIGN_RIGHT, border=5)
|
||||||
|
|
||||||
|
panel.SetSizer(rows)
|
||||||
|
|
||||||
|
return panel
|
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/__init__.py
vendored
Normal file
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/__init__.py
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
try:
|
||||||
|
from .plugin import Plugin
|
||||||
|
plugin = Plugin()
|
||||||
|
plugin.register()
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.debug(repr(e))
|
36
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/cli.py
vendored
Normal file
36
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/cli.py
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import argparse as ap
|
||||||
|
|
||||||
|
from .thread import ProcessThread
|
||||||
|
from .options import *
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = ap.ArgumentParser(prog="Fabrication Toolkit",
|
||||||
|
description="Generates JLCPCB production files from a KiCAD board file")
|
||||||
|
|
||||||
|
parser.add_argument("--path", "-p", type=str, help="Path to KiCAD board file", required=True)
|
||||||
|
parser.add_argument("--additionalLayers", "-aL", type=str, help="Additional layers(comma-separated)")
|
||||||
|
parser.add_argument("--user1VCut", "-u1", action="store_true", help="Set User.1 as V-Cut layer")
|
||||||
|
parser.add_argument("--user2AltVCut", "-u2", action="store_true", help="Use User.2 for alternative Edge-Cut layer")
|
||||||
|
parser.add_argument("--autoTranslate", "-t", action="store_true", help="Apply automatic position/rotation translations")
|
||||||
|
parser.add_argument("--autoFill", "-f", action="store_true", help="Apply automatic fill for all zones")
|
||||||
|
parser.add_argument("--excludeDNP", "-e", action="store_true", help="Exclude DNP components from BOM")
|
||||||
|
parser.add_argument("--allActiveLayers", "-aaL",action="store_true", help="Export all active layers instead of only commonly used ones")
|
||||||
|
parser.add_argument("--openBrowser", "-b", action="store_true", help="Open webbrowser with directory file overview after generation")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
options = dict()
|
||||||
|
options[AUTO_TRANSLATE_OPT] = args.autoTranslate
|
||||||
|
options[AUTO_FILL_OPT] = args.autoFill
|
||||||
|
options[EXCLUDE_DNP_OPT] = args.excludeDNP
|
||||||
|
options[EXTEND_EDGE_CUT_OPT] = args.user1VCut
|
||||||
|
options[ALTERNATIVE_EDGE_CUT_OPT] = args.user2AltVCut
|
||||||
|
options[ALL_ACTIVE_LAYERS_OPT] = args.allActiveLayers
|
||||||
|
options[EXTRA_LAYERS] = args.additionalLayers
|
||||||
|
|
||||||
|
openBrowser = args.openBrowser
|
||||||
|
|
||||||
|
|
||||||
|
path = args.path
|
||||||
|
|
||||||
|
ProcessThread(wx=None, cli=path, options=options, openBrowser=openBrowser)
|
26
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/config.py
vendored
Normal file
26
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/config.py
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import pcbnew # type: ignore
|
||||||
|
|
||||||
|
baseUrl = 'https://www.jlcpcb.com'
|
||||||
|
|
||||||
|
netlistFileName = 'netlist.ipc'
|
||||||
|
designatorsFileName = 'designators.csv'
|
||||||
|
placementFileName = 'positions.csv'
|
||||||
|
bomFileName = 'bom.csv'
|
||||||
|
gerberArchiveName = 'gerbers.zip'
|
||||||
|
outputFolder = 'production'
|
||||||
|
bomRowLimit = 200
|
||||||
|
|
||||||
|
optionsFileName = 'fabrication-toolkit-options.json'
|
||||||
|
|
||||||
|
standardLayers = [ pcbnew.F_Cu, pcbnew.B_Cu,
|
||||||
|
pcbnew.In1_Cu, pcbnew.In2_Cu, pcbnew.In3_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu,
|
||||||
|
pcbnew.In6_Cu, pcbnew.In7_Cu, pcbnew.In8_Cu, pcbnew.In9_Cu, pcbnew.In10_Cu,
|
||||||
|
pcbnew.In11_Cu, pcbnew.In12_Cu, pcbnew.In13_Cu, pcbnew.In14_Cu, pcbnew.In15_Cu,
|
||||||
|
pcbnew.In16_Cu, pcbnew.In17_Cu, pcbnew.In18_Cu, pcbnew.In19_Cu, pcbnew.In20_Cu,
|
||||||
|
pcbnew.In21_Cu, pcbnew.In22_Cu, pcbnew.In23_Cu, pcbnew.In24_Cu, pcbnew.In25_Cu,
|
||||||
|
pcbnew.In26_Cu, pcbnew.In27_Cu, pcbnew.In28_Cu, pcbnew.In29_Cu, pcbnew.In30_Cu,
|
||||||
|
pcbnew.F_SilkS, pcbnew.B_SilkS,
|
||||||
|
pcbnew.F_Mask, pcbnew.B_Mask,
|
||||||
|
pcbnew.F_Paste, pcbnew.B_Paste,
|
||||||
|
pcbnew.Edge_Cuts
|
||||||
|
]
|
14
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/events.py
vendored
Normal file
14
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/events.py
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import wx
|
||||||
|
|
||||||
|
STATUS_EVENT_ID = wx.NewId()
|
||||||
|
|
||||||
|
|
||||||
|
class StatusEvent(wx.PyEvent):
|
||||||
|
def __init__(self, data):
|
||||||
|
wx.PyEvent.__init__(self)
|
||||||
|
self.SetEventType(STATUS_EVENT_ID)
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def invoke(window, function):
|
||||||
|
window.Connect(-1, -1, STATUS_EVENT_ID, function)
|
BIN
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/icon.png
vendored
Normal file
BIN
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/options.py
vendored
Normal file
8
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/options.py
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
AUTO_TRANSLATE_OPT = "AUTO TRANSLATE"
|
||||||
|
AUTO_FILL_OPT = "AUTO FILL"
|
||||||
|
EXCLUDE_DNP_OPT = "EXCLUDE DNP"
|
||||||
|
OUTPUT_NAME_OPT = "OUTPUT NAME"
|
||||||
|
EXTEND_EDGE_CUT_OPT = "EXTEND_EDGE_CUT"
|
||||||
|
ALTERNATIVE_EDGE_CUT_OPT = "ALTERNATIVE_EDGE_CUT"
|
||||||
|
ALL_ACTIVE_LAYERS_OPT = "ALL_ACTIVE_LAYERS"
|
||||||
|
EXTRA_LAYERS = "EXTRA_LAYERS"
|
151
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/plugin.py
vendored
Normal file
151
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/plugin.py
vendored
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import os
|
||||||
|
import wx
|
||||||
|
import pcbnew # type: ignore
|
||||||
|
|
||||||
|
from .thread import ProcessThread
|
||||||
|
from .events import StatusEvent
|
||||||
|
from .options import AUTO_FILL_OPT, AUTO_TRANSLATE_OPT, EXCLUDE_DNP_OPT, EXTEND_EDGE_CUT_OPT, ALTERNATIVE_EDGE_CUT_OPT, EXTRA_LAYERS, ALL_ACTIVE_LAYERS_OPT
|
||||||
|
from .utils import load_user_options, save_user_options, get_layer_names
|
||||||
|
|
||||||
|
|
||||||
|
# WX GUI form that show the plugin progress
|
||||||
|
class KiCadToJLCForm(wx.Frame):
|
||||||
|
def __init__(self):
|
||||||
|
wx.Dialog.__init__(
|
||||||
|
self,
|
||||||
|
None,
|
||||||
|
id=wx.ID_ANY,
|
||||||
|
title=u"Fabrication Toolkit",
|
||||||
|
pos=wx.DefaultPosition,
|
||||||
|
size=wx.DefaultSize,
|
||||||
|
style=wx.DEFAULT_DIALOG_STYLE)
|
||||||
|
|
||||||
|
# self.app = wx.PySimpleApp()
|
||||||
|
icon = wx.Icon(os.path.join(os.path.dirname(__file__), 'icon.png'))
|
||||||
|
self.SetIcon(icon)
|
||||||
|
self.SetBackgroundColour(wx.LIGHT_GREY)
|
||||||
|
|
||||||
|
self.SetSizeHints(wx.Size(600, 100), wx.DefaultSize)
|
||||||
|
|
||||||
|
userOptions = load_user_options({
|
||||||
|
EXTRA_LAYERS: "",
|
||||||
|
ALL_ACTIVE_LAYERS_OPT: False,
|
||||||
|
EXTEND_EDGE_CUT_OPT: False,
|
||||||
|
ALTERNATIVE_EDGE_CUT_OPT: False,
|
||||||
|
AUTO_TRANSLATE_OPT: True,
|
||||||
|
AUTO_FILL_OPT: True,
|
||||||
|
EXCLUDE_DNP_OPT: False
|
||||||
|
})
|
||||||
|
|
||||||
|
self.mOptionsLabel = wx.StaticText(self, label='Options:')
|
||||||
|
# self.mOptionsSeparator = wx.StaticLine(self)
|
||||||
|
|
||||||
|
layers = get_layer_names(pcbnew.GetBoard())
|
||||||
|
self.mAdditionalLayersControl = wx.TextCtrl(self, size=wx.Size(600, 50))
|
||||||
|
self.mAdditionalLayersControl.Hint = "Additional layers (comma-separated)"
|
||||||
|
self.mAdditionalLayersControl.AutoComplete(layers)
|
||||||
|
self.mAdditionalLayersControl.Enable()
|
||||||
|
self.mAdditionalLayersControl.SetValue(userOptions[EXTRA_LAYERS])
|
||||||
|
self.mAllActiveLayersCheckbox = wx.CheckBox(self, label='Plot all active layers')
|
||||||
|
self.mAllActiveLayersCheckbox.SetValue(userOptions[ALL_ACTIVE_LAYERS_OPT])
|
||||||
|
self.mExtendEdgeCutsCheckbox = wx.CheckBox(self, label='Set User.1 as V-Cut layer')
|
||||||
|
self.mExtendEdgeCutsCheckbox.SetValue(userOptions[EXTEND_EDGE_CUT_OPT])
|
||||||
|
self.mAlternativeEdgeCutsCheckbox = wx.CheckBox(self, label='Use User.2 for alternative Edge-Cut layer')
|
||||||
|
self.mAlternativeEdgeCutsCheckbox.SetValue(userOptions[ALTERNATIVE_EDGE_CUT_OPT])
|
||||||
|
self.mAutomaticTranslationCheckbox = wx.CheckBox(self, label='Apply automatic translations')
|
||||||
|
self.mAutomaticTranslationCheckbox.SetValue(userOptions[AUTO_TRANSLATE_OPT])
|
||||||
|
self.mAutomaticFillCheckbox = wx.CheckBox(self, label='Apply automatic fill for all zones')
|
||||||
|
self.mAutomaticFillCheckbox.SetValue(userOptions[AUTO_FILL_OPT])
|
||||||
|
self.mExcludeDnpCheckbox = wx.CheckBox(self, label='Exclude DNP components from BOM')
|
||||||
|
self.mExcludeDnpCheckbox.SetValue(userOptions[EXCLUDE_DNP_OPT])
|
||||||
|
|
||||||
|
self.mGaugeStatus = wx.Gauge(
|
||||||
|
self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size(600, 20), wx.GA_HORIZONTAL)
|
||||||
|
self.mGaugeStatus.SetValue(0)
|
||||||
|
self.mGaugeStatus.Hide()
|
||||||
|
|
||||||
|
self.mGenerateButton = wx.Button(self, label='Generate', size=wx.Size(600, 60))
|
||||||
|
self.mGenerateButton.Bind(wx.EVT_BUTTON, self.onGenerateButtonClick)
|
||||||
|
|
||||||
|
boxSizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
|
||||||
|
boxSizer.Add(self.mOptionsLabel, 0, wx.ALL, 5)
|
||||||
|
# boxSizer.Add(self.mOptionsSeparator, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mAdditionalLayersControl, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mAllActiveLayersCheckbox, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mExtendEdgeCutsCheckbox, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mAlternativeEdgeCutsCheckbox, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mAutomaticTranslationCheckbox, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mAutomaticFillCheckbox, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mExcludeDnpCheckbox, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mGaugeStatus, 0, wx.ALL, 5)
|
||||||
|
boxSizer.Add(self.mGenerateButton, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.SetSizer(boxSizer)
|
||||||
|
self.Layout()
|
||||||
|
boxSizer.Fit(self)
|
||||||
|
|
||||||
|
self.Centre(wx.BOTH)
|
||||||
|
|
||||||
|
# Bind the ESC key event to a handler
|
||||||
|
self.Bind(wx.EVT_CHAR_HOOK, self.onKey)
|
||||||
|
|
||||||
|
# Close the dialog when pressing the ESC key
|
||||||
|
def onKey(self, event):
|
||||||
|
if event.GetKeyCode() == wx.WXK_ESCAPE:
|
||||||
|
self.Close(True)
|
||||||
|
else:
|
||||||
|
event.Skip()
|
||||||
|
|
||||||
|
|
||||||
|
def onGenerateButtonClick(self, event):
|
||||||
|
options = dict()
|
||||||
|
options[EXTRA_LAYERS] = self.mAdditionalLayersControl.GetValue()
|
||||||
|
options[ALL_ACTIVE_LAYERS_OPT] = self.mAllActiveLayersCheckbox.GetValue()
|
||||||
|
options[EXTEND_EDGE_CUT_OPT] = self.mExtendEdgeCutsCheckbox.GetValue()
|
||||||
|
options[ALTERNATIVE_EDGE_CUT_OPT] = self.mAlternativeEdgeCutsCheckbox.GetValue()
|
||||||
|
options[AUTO_TRANSLATE_OPT] = self.mAutomaticTranslationCheckbox.GetValue()
|
||||||
|
options[AUTO_FILL_OPT] = self.mAutomaticFillCheckbox.GetValue()
|
||||||
|
options[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
|
||||||
|
|
||||||
|
save_user_options(options)
|
||||||
|
|
||||||
|
self.mOptionsLabel.Hide()
|
||||||
|
self.mAdditionalLayersControl.Hide()
|
||||||
|
self.mAllActiveLayersCheckbox.Hide()
|
||||||
|
self.mExtendEdgeCutsCheckbox.Hide()
|
||||||
|
self.mAlternativeEdgeCutsCheckbox.Hide()
|
||||||
|
self.mAutomaticTranslationCheckbox.Hide()
|
||||||
|
self.mAutomaticFillCheckbox.Hide()
|
||||||
|
self.mExcludeDnpCheckbox.Hide()
|
||||||
|
self.mGenerateButton.Hide()
|
||||||
|
self.mGaugeStatus.Show()
|
||||||
|
|
||||||
|
self.Fit()
|
||||||
|
self.SetTitle('Fabrication Toolkit (Processing...)')
|
||||||
|
|
||||||
|
StatusEvent.invoke(self, self.updateDisplay)
|
||||||
|
ProcessThread(self, options)
|
||||||
|
|
||||||
|
def updateDisplay(self, status):
|
||||||
|
if status.data == -1:
|
||||||
|
self.SetTitle('Fabrication Toolkit (Done!)')
|
||||||
|
pcbnew.Refresh()
|
||||||
|
self.Destroy()
|
||||||
|
else:
|
||||||
|
self.mGaugeStatus.SetValue(int(status.data))
|
||||||
|
|
||||||
|
|
||||||
|
# Plugin definition
|
||||||
|
class Plugin(pcbnew.ActionPlugin):
|
||||||
|
def __init__(self):
|
||||||
|
self.name = "Fabrication Toolkit"
|
||||||
|
self.category = "Manufacturing"
|
||||||
|
self.description = "Toolkit for automating PCB fabrication process with KiCad and JLC PCB"
|
||||||
|
self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
|
||||||
|
self.show_toolbar_button = True
|
||||||
|
self.icon_file_name = os.path.join(os.path.dirname(__file__), 'icon.png')
|
||||||
|
self.dark_icon_file_name = os.path.join(os.path.dirname(__file__), 'icon.png')
|
||||||
|
|
||||||
|
def Run(self):
|
||||||
|
KiCadToJLCForm().Show()
|
510
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/process.py
vendored
Normal file
510
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/process.py
vendored
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
# For better annotation.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# System base libraries
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import csv
|
||||||
|
import math
|
||||||
|
import shutil
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# Interaction with KiCad.
|
||||||
|
import pcbnew # type: ignore
|
||||||
|
from .utils import footprint_has_field, footprint_get_field, get_plot_plan
|
||||||
|
|
||||||
|
# Application definitions.
|
||||||
|
from .config import *
|
||||||
|
|
||||||
|
class ProcessManager:
|
||||||
|
def __init__(self, board = None):
|
||||||
|
# if no board is already loaded by cli mode getBoard from kicad environment
|
||||||
|
if board is None:
|
||||||
|
self.board = pcbnew.GetBoard()
|
||||||
|
else:
|
||||||
|
self.board = board
|
||||||
|
self.bom = []
|
||||||
|
self.components = []
|
||||||
|
self.__rotation_db = self.__read_rotation_db()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalize_filename(filename):
|
||||||
|
return re.sub(r'[^\w\s\.\-]', '', filename)
|
||||||
|
|
||||||
|
def update_zone_fills(self):
|
||||||
|
'''Verify all zones have up-to-date fills.'''
|
||||||
|
filler = pcbnew.ZONE_FILLER(self.board)
|
||||||
|
zones = self.board.Zones()
|
||||||
|
|
||||||
|
# Fill returns true/false if a refill was made
|
||||||
|
# We cant use aCheck = True as that would require a rollback on the commit object if
|
||||||
|
# user decided to not perform the zone fill and the commit object is not exposed to python API
|
||||||
|
filler.Fill(zones, False)
|
||||||
|
|
||||||
|
# Finally rebuild the connectivity db
|
||||||
|
self.board.BuildConnectivity()
|
||||||
|
|
||||||
|
def generate_gerber(self, temp_dir, extra_layers, extend_edge_cuts, alternative_edge_cuts, all_active_layers):
|
||||||
|
'''Generate the Gerber files.'''
|
||||||
|
settings = self.board.GetDesignSettings()
|
||||||
|
settings.m_SolderMaskMargin = 50000
|
||||||
|
settings.m_SolderMaskToCopperClearance = 5000
|
||||||
|
settings.m_SolderMaskMinWidth = 0
|
||||||
|
|
||||||
|
plot_controller = pcbnew.PLOT_CONTROLLER(self.board)
|
||||||
|
|
||||||
|
plot_options = plot_controller.GetPlotOptions()
|
||||||
|
plot_options.SetOutputDirectory(temp_dir)
|
||||||
|
plot_options.SetPlotFrameRef(False)
|
||||||
|
plot_options.SetSketchPadLineWidth(pcbnew.FromMM(0.1))
|
||||||
|
plot_options.SetAutoScale(False)
|
||||||
|
plot_options.SetScale(1)
|
||||||
|
plot_options.SetMirror(False)
|
||||||
|
plot_options.SetUseGerberAttributes(True)
|
||||||
|
plot_options.SetUseGerberProtelExtensions(True)
|
||||||
|
plot_options.SetUseAuxOrigin(True)
|
||||||
|
plot_options.SetSubtractMaskFromSilk(True)
|
||||||
|
plot_options.SetUseGerberX2format(False)
|
||||||
|
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
|
||||||
|
|
||||||
|
if hasattr(plot_options, "SetExcludeEdgeLayer"):
|
||||||
|
plot_options.SetExcludeEdgeLayer(True)
|
||||||
|
|
||||||
|
if extra_layers is not None:
|
||||||
|
extra_layers = [element.strip() for element in extra_layers.strip().split(',') if element.strip()]
|
||||||
|
else:
|
||||||
|
extra_layers = []
|
||||||
|
|
||||||
|
for layer_info in get_plot_plan(self.board):
|
||||||
|
if (self.board.IsLayerEnabled(layer_info[1]) and (all_active_layers or layer_info[1] in standardLayers)) or layer_info[0] in extra_layers:
|
||||||
|
plot_controller.SetLayer(layer_info[1])
|
||||||
|
plot_controller.OpenPlotfile(layer_info[2], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
|
||||||
|
|
||||||
|
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'PlotLayers') and (extend_edge_cuts or alternative_edge_cuts):
|
||||||
|
seq = pcbnew.LSEQ()
|
||||||
|
# uses User_2 layer for alternative Edge_Cuts layer
|
||||||
|
if alternative_edge_cuts:
|
||||||
|
seq.push_back(pcbnew.User_2)
|
||||||
|
else:
|
||||||
|
seq.push_back(layer_info[1])
|
||||||
|
# includes User_1 layer with Edge_Cuts layer to allow V Cuts to be defined as User_1 layer
|
||||||
|
# available for KiCad 7.0.1+
|
||||||
|
if extend_edge_cuts:
|
||||||
|
seq.push_back(layer_info[1])
|
||||||
|
seq.push_back(pcbnew.User_1)
|
||||||
|
plot_controller.PlotLayers(seq)
|
||||||
|
else:
|
||||||
|
plot_controller.PlotLayer()
|
||||||
|
|
||||||
|
plot_controller.ClosePlot()
|
||||||
|
|
||||||
|
def generate_drills(self, temp_dir):
|
||||||
|
'''Generate the drill file.'''
|
||||||
|
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
|
||||||
|
|
||||||
|
drill_writer.SetOptions(
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
self.board.GetDesignSettings().GetAuxOrigin(),
|
||||||
|
False)
|
||||||
|
drill_writer.SetFormat(True)
|
||||||
|
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||||
|
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, True)
|
||||||
|
|
||||||
|
def generate_netlist(self, temp_dir):
|
||||||
|
'''Generate the connection netlist.'''
|
||||||
|
netlist_writer = pcbnew.IPC356D_WRITER(self.board)
|
||||||
|
netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
|
||||||
|
|
||||||
|
def _get_footprint_position(self, footprint):
|
||||||
|
"""Calculate position based on center of pads / bounding box."""
|
||||||
|
origin_type = self._get_origin_from_footprint(footprint)
|
||||||
|
|
||||||
|
if origin_type == 'Anchor':
|
||||||
|
position = footprint.GetPosition()
|
||||||
|
else: # if type_origin == 'Center' or anything else
|
||||||
|
pads = footprint.Pads()
|
||||||
|
if len(pads) > 0:
|
||||||
|
# get bounding box based on pads only to ignore non-copper layers, e.g. silkscreen
|
||||||
|
bbox = pads[0].GetBoundingBox() # start with small bounding box
|
||||||
|
for pad in pads:
|
||||||
|
bbox.Merge(pad.GetBoundingBox()) # expand bounding box
|
||||||
|
position = bbox.GetCenter()
|
||||||
|
else:
|
||||||
|
position = footprint.GetPosition() # if we have no pads we fallback to anchor
|
||||||
|
|
||||||
|
return position
|
||||||
|
|
||||||
|
def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
|
||||||
|
'''Generate the data tables.'''
|
||||||
|
if hasattr(self.board, 'GetModules'):
|
||||||
|
footprints = list(self.board.GetModules())
|
||||||
|
else:
|
||||||
|
footprints = list(self.board.GetFootprints())
|
||||||
|
|
||||||
|
# sort footprint after designator
|
||||||
|
footprints.sort(key=lambda x: x.GetReference().upper())
|
||||||
|
|
||||||
|
# unique designator dictionary
|
||||||
|
footprint_designators = defaultdict(int)
|
||||||
|
for i, footprint in enumerate(footprints):
|
||||||
|
# count unique designators
|
||||||
|
footprint_designators[footprint.GetReference().upper()] += 1
|
||||||
|
bom_designators = footprint_designators.copy()
|
||||||
|
|
||||||
|
if len(footprint_designators.items()) > 0:
|
||||||
|
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8-sig') as f:
|
||||||
|
for key, value in footprint_designators.items():
|
||||||
|
f.write('%s:%s\n' % (key, value))
|
||||||
|
|
||||||
|
for i, footprint in enumerate(footprints):
|
||||||
|
try:
|
||||||
|
footprint_name = str(footprint.GetFPID().GetFootprintName())
|
||||||
|
except AttributeError:
|
||||||
|
footprint_name = str(footprint.GetFPID().GetLibItemName())
|
||||||
|
|
||||||
|
layer = self._get_layer_override_from_footprint(footprint)
|
||||||
|
|
||||||
|
# mount_type = {
|
||||||
|
# 0: 'smt',
|
||||||
|
# 1: 'tht',
|
||||||
|
# 2: 'unspecified'
|
||||||
|
# }.get(footprint.GetAttributes())
|
||||||
|
|
||||||
|
is_dnp = (footprint_has_field(footprint, 'dnp')
|
||||||
|
or (footprint.GetValue().upper() == 'DNP')
|
||||||
|
or getattr(footprint, 'IsDNP', bool)())
|
||||||
|
skip_dnp = exclude_dnp and is_dnp
|
||||||
|
|
||||||
|
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES) and not is_dnp:
|
||||||
|
# append unique ID if duplicate footprint designator
|
||||||
|
unique_id = ""
|
||||||
|
if footprint_designators[footprint.GetReference().upper()] > 1:
|
||||||
|
unique_id = str(footprint_designators[footprint.GetReference().upper()])
|
||||||
|
footprint_designators[footprint.GetReference().upper()] -= 1
|
||||||
|
|
||||||
|
designator = "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id)
|
||||||
|
position = self._get_footprint_position(footprint)
|
||||||
|
mid_x = (position[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
|
||||||
|
mid_y = (position[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0
|
||||||
|
rotation = footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
|
||||||
|
rotation_offset_db = self._get_rotation_from_db(footprint_name) # internal database offset
|
||||||
|
rotation_offset_manual = self._get_rotation_offset_from_footprint(footprint) # explicated offset by the designer
|
||||||
|
|
||||||
|
# position offset needs to take rotation into account
|
||||||
|
pos_offset = self._get_position_offset_from_footprint(footprint)
|
||||||
|
if auto_translate:
|
||||||
|
pos_offset_db = self._get_position_offset_from_db(footprint_name)
|
||||||
|
pos_offset = (pos_offset[0] + pos_offset_db[0], pos_offset[1] + pos_offset_db[1])
|
||||||
|
|
||||||
|
rsin = math.sin(rotation / 180 * math.pi)
|
||||||
|
rcos = math.cos(rotation / 180 * math.pi)
|
||||||
|
|
||||||
|
if layer == 'bottom':
|
||||||
|
pos_offset = ( pos_offset[0] * rcos + pos_offset[1] * rsin, pos_offset[0] * rsin - pos_offset[1] * rcos )
|
||||||
|
else:
|
||||||
|
pos_offset = ( pos_offset[0] * rcos - pos_offset[1] * rsin, pos_offset[0] * rsin + pos_offset[1] * rcos )
|
||||||
|
mid_x, mid_y = tuple(map(sum,zip((mid_x, mid_y), pos_offset)))
|
||||||
|
|
||||||
|
# JLC expect 'Rotation' to be 'as viewed from above component', so bottom needs inverting, and ends up 180 degrees out as well
|
||||||
|
if layer == 'bottom':
|
||||||
|
rotation = (180.0 - rotation)
|
||||||
|
|
||||||
|
if auto_translate:
|
||||||
|
rotation += rotation_offset_db
|
||||||
|
|
||||||
|
rotation = (rotation + rotation_offset_manual) % 360.0
|
||||||
|
|
||||||
|
self.components.append({
|
||||||
|
'Designator': designator,
|
||||||
|
'Mid X': mid_x,
|
||||||
|
'Mid Y': mid_y,
|
||||||
|
'Rotation': rotation,
|
||||||
|
'Layer': layer,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM) and not skip_dnp:
|
||||||
|
# append unique ID if we are dealing with duplicate bom designator
|
||||||
|
unique_id = ""
|
||||||
|
if bom_designators[footprint.GetReference().upper()] > 1:
|
||||||
|
unique_id = str(bom_designators[footprint.GetReference().upper()])
|
||||||
|
bom_designators[footprint.GetReference().upper()] -= 1
|
||||||
|
|
||||||
|
# merge similar parts into single entry
|
||||||
|
insert = True
|
||||||
|
for component in self.bom:
|
||||||
|
same_footprint = component['Footprint'] == self._normalize_footprint_name(footprint_name)
|
||||||
|
same_value = component['Value'].upper() == footprint.GetValue().upper()
|
||||||
|
same_lcsc = component['LCSC Part #'] == self._get_mpn_from_footprint(footprint)
|
||||||
|
under_limit = component['Quantity'] < bomRowLimit
|
||||||
|
|
||||||
|
if same_footprint and same_value and same_lcsc and under_limit:
|
||||||
|
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id)
|
||||||
|
component['Quantity'] += 1
|
||||||
|
insert = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# add component to BOM
|
||||||
|
if insert:
|
||||||
|
self.bom.append({
|
||||||
|
'Designator': "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id),
|
||||||
|
'Footprint': self._normalize_footprint_name(footprint_name),
|
||||||
|
'Quantity': 1,
|
||||||
|
'Value': footprint.GetValue(),
|
||||||
|
# 'Mount': mount_type,
|
||||||
|
'LCSC Part #': self._get_mpn_from_footprint(footprint),
|
||||||
|
})
|
||||||
|
|
||||||
|
def generate_positions(self, temp_dir):
|
||||||
|
'''Generate the position file.'''
|
||||||
|
if len(self.components) > 0:
|
||||||
|
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8-sig') as outfile:
|
||||||
|
csv_writer = csv.writer(outfile)
|
||||||
|
# writing headers of CSV file
|
||||||
|
csv_writer.writerow(self.components[0].keys())
|
||||||
|
|
||||||
|
for component in self.components:
|
||||||
|
# writing data of CSV file
|
||||||
|
if ('**' not in component['Designator']):
|
||||||
|
csv_writer.writerow(component.values())
|
||||||
|
|
||||||
|
def generate_bom(self, temp_dir):
|
||||||
|
'''Generate the bom file.'''
|
||||||
|
if len(self.bom) > 0:
|
||||||
|
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8-sig') as outfile:
|
||||||
|
csv_writer = csv.writer(outfile)
|
||||||
|
# writing headers of CSV file
|
||||||
|
csv_writer.writerow(self.bom[0].keys())
|
||||||
|
|
||||||
|
# Output all of the component information
|
||||||
|
for component in self.bom:
|
||||||
|
# writing data of CSV file
|
||||||
|
if ('**' not in component['Designator']):
|
||||||
|
csv_writer.writerow(component.values())
|
||||||
|
|
||||||
|
def generate_archive(self, temp_dir, temp_file):
|
||||||
|
'''Generate the archive file.'''
|
||||||
|
temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
|
||||||
|
temp_file = shutil.move(temp_file, temp_dir)
|
||||||
|
|
||||||
|
# remove non essential files
|
||||||
|
for item in os.listdir(temp_dir):
|
||||||
|
if not item.endswith(".zip") and not item.endswith(".csv") and not item.endswith(".ipc"):
|
||||||
|
os.remove(os.path.join(temp_dir, item))
|
||||||
|
|
||||||
|
return temp_file
|
||||||
|
|
||||||
|
""" Private """
|
||||||
|
|
||||||
|
def __read_rotation_db(self, filename: str = os.path.join(os.path.dirname(__file__), 'transformations.csv')) -> dict[str, float]:
|
||||||
|
'''Read the rotations.cf config file so we know what rotations
|
||||||
|
to apply later.
|
||||||
|
'''
|
||||||
|
db = {}
|
||||||
|
|
||||||
|
with open(filename, newline='') as csvfile:
|
||||||
|
csvDialect = csv.Sniffer().sniff(csvfile.read(1024))
|
||||||
|
csvfile.seek(0)
|
||||||
|
csvData = csv.DictReader(csvfile, fieldnames=["footprint", "rotation", "x", "y"],
|
||||||
|
restkey="extra", restval="0", dialect=csvDialect)
|
||||||
|
|
||||||
|
rowNum = 0
|
||||||
|
for row in csvData:
|
||||||
|
rowNum = rowNum + 1
|
||||||
|
# First row is header row, skip.
|
||||||
|
if rowNum == 1:
|
||||||
|
skipFirst = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If there was too many fields, throw an exception.
|
||||||
|
if len(row) > 4:
|
||||||
|
raise RuntimeError("{}: Too many fields found in row {}: {}".format(filename, rowNum, row))
|
||||||
|
|
||||||
|
# See if the values we expect to be floating point numbers
|
||||||
|
# can be converted to floating point, if not throw an exception.
|
||||||
|
if row['rotation'] == "":
|
||||||
|
rotation = 0.0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
rotation = float(row['rotation'])
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError("{}: Non-numeric rotation value found in row {}".format(filename, rowNum))
|
||||||
|
|
||||||
|
if row['x'] == "":
|
||||||
|
delta_x = 0.0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
delta_x = float(row['x'])
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError("{}: Non-numeric translation value found in row {}".format(filename, rowNum))
|
||||||
|
|
||||||
|
if row['y'] == "":
|
||||||
|
delta_y = 0.0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
delta_y = float(row['y'])
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError("{}: Non-numeric translation value found in row {}".format(filename, rowNum))
|
||||||
|
|
||||||
|
# Add the entry to the database in the format we expect.
|
||||||
|
db[rowNum] = {}
|
||||||
|
db[rowNum]['name'] = row['footprint']
|
||||||
|
db[rowNum]['rotation'] = rotation
|
||||||
|
db[rowNum]['x'] = delta_x
|
||||||
|
db[rowNum]['y'] = delta_y
|
||||||
|
|
||||||
|
return db
|
||||||
|
|
||||||
|
def _get_rotation_from_db(self, footprint: str) -> float:
|
||||||
|
'''Get the rotation to be added from the database file.'''
|
||||||
|
# Look for regular expression math of the footprint name and not its root library.
|
||||||
|
|
||||||
|
for entry in self.__rotation_db.items():
|
||||||
|
# If the expression in the DB contains a :, search for it literally.
|
||||||
|
if (re.search(':', entry[1]['name'])):
|
||||||
|
if (re.search(entry[1]['name'], footprint)):
|
||||||
|
return float(entry[1]['rotation'])
|
||||||
|
# There is no : in the expression, so only search the right side of the :
|
||||||
|
else:
|
||||||
|
footprint_segments = footprint.split(':')
|
||||||
|
# Only one means there was no :, just check the short.
|
||||||
|
if (len(footprint_segments) == 1):
|
||||||
|
check = footprint_segments[0]
|
||||||
|
# More means there was a :, check the right side.
|
||||||
|
else:
|
||||||
|
check = footprint_segments[1]
|
||||||
|
if (re.search(entry[1]['name'], check)):
|
||||||
|
return float(entry[1]['rotation'])
|
||||||
|
|
||||||
|
# Not found, no rotation.
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _get_position_offset_from_db(self, footprint: str) -> Tuple[float, float]:
|
||||||
|
'''Get the rotation to be added from the database file.'''
|
||||||
|
# Look for regular expression math of the footprint name and not its root library.
|
||||||
|
|
||||||
|
for entry in self.__rotation_db.items():
|
||||||
|
# If the expression in the DB contains a :, search for it literally.
|
||||||
|
if (re.search(':', entry[1]['name'])):
|
||||||
|
if (re.search(entry[1]['name'], footprint)):
|
||||||
|
return ( float(entry[1]['x']), float(entry[1]['y']) )
|
||||||
|
# There is no : in the expression, so only search the right side of the :
|
||||||
|
else:
|
||||||
|
footprint_segments = footprint.split(':')
|
||||||
|
# Only one means there was no :, just check the short.
|
||||||
|
if (len(footprint_segments) == 1):
|
||||||
|
check = footprint_segments[0]
|
||||||
|
# More means there was a :, check the right side.
|
||||||
|
else:
|
||||||
|
check = footprint_segments[1]
|
||||||
|
if (re.search(entry[1]['name'], check)):
|
||||||
|
return ( float(entry[1]['x']), float(entry[1]['y']) )
|
||||||
|
|
||||||
|
# Not found, no delta.
|
||||||
|
return (0.0, 0.0)
|
||||||
|
|
||||||
|
def _get_mpn_from_footprint(self, footprint) -> str:
|
||||||
|
''''Get the MPN/LCSC stock code from standard symbol fields.'''
|
||||||
|
keys = ['LCSC Part #', 'LCSC Part', 'JLCPCB Part #', 'JLCPCB Part']
|
||||||
|
fallback_keys = ['LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
|
||||||
|
|
||||||
|
if footprint_has_field(footprint, 'dnp'):
|
||||||
|
return 'DNP'
|
||||||
|
|
||||||
|
for key in keys + fallback_keys:
|
||||||
|
if footprint_has_field(footprint, key):
|
||||||
|
return footprint_get_field(footprint, key)
|
||||||
|
|
||||||
|
def _get_layer_override_from_footprint(self, footprint) -> str:
|
||||||
|
'''Get the layer override from standard symbol fields.'''
|
||||||
|
keys = ['FT Layer Override']
|
||||||
|
fallback_keys = ['Layer Override', 'LayerOverride']
|
||||||
|
|
||||||
|
layer = {
|
||||||
|
pcbnew.F_Cu: 'top',
|
||||||
|
pcbnew.B_Cu: 'bottom',
|
||||||
|
}.get(footprint.GetLayer())
|
||||||
|
|
||||||
|
for key in keys + fallback_keys:
|
||||||
|
if footprint_has_field(footprint, key):
|
||||||
|
temp_layer = footprint_get_field(footprint, key)
|
||||||
|
if len(temp_layer) > 0:
|
||||||
|
if (temp_layer[0] == 'b' or temp_layer[0] == 'B'):
|
||||||
|
layer = "bottom"
|
||||||
|
break
|
||||||
|
elif (temp_layer[0] == 't' or temp_layer[0] == 'T'):
|
||||||
|
layer = "top"
|
||||||
|
break
|
||||||
|
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def _get_rotation_offset_from_footprint(self, footprint) -> float:
|
||||||
|
'''Get the rotation offset from standard symbol fields.'''
|
||||||
|
keys = ['FT Rotation Offset']
|
||||||
|
fallback_keys = ['Rotation Offset', 'RotOffset']
|
||||||
|
|
||||||
|
offset = ""
|
||||||
|
|
||||||
|
for key in keys + fallback_keys:
|
||||||
|
if footprint_has_field(footprint, key):
|
||||||
|
offset = footprint_get_field(footprint, key)
|
||||||
|
break
|
||||||
|
|
||||||
|
if offset is None or offset == "":
|
||||||
|
return 0.0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return float(offset)
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError("Rotation offset of {} is not a valid number".format(footprint.GetReference()))
|
||||||
|
|
||||||
|
def _get_position_offset_from_footprint(self, footprint) -> Tuple[float, float]:
|
||||||
|
'''Get the position offset from standard symbol fields.'''
|
||||||
|
keys = ['FT Position Offset']
|
||||||
|
fallback_keys = ['Position Offset', 'PosOffset']
|
||||||
|
|
||||||
|
offset = ""
|
||||||
|
|
||||||
|
for key in keys + fallback_keys:
|
||||||
|
if footprint_has_field(footprint, key):
|
||||||
|
offset = footprint_get_field(footprint, key)
|
||||||
|
break
|
||||||
|
|
||||||
|
if offset == "":
|
||||||
|
return (0.0, 0.0)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
offset = offset.split(",")
|
||||||
|
return (float(offset[0]), float(offset[1]))
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
|
||||||
|
|
||||||
|
def _get_origin_from_footprint(self, footprint) -> float:
|
||||||
|
'''Get the origin from standard symbol fields.'''
|
||||||
|
keys = ['FT Origin']
|
||||||
|
fallback_keys = ['Origin']
|
||||||
|
|
||||||
|
attributes = footprint.GetAttributes()
|
||||||
|
|
||||||
|
# determine origin type by package type
|
||||||
|
if attributes & pcbnew.FP_SMD:
|
||||||
|
origin_type = 'Anchor'
|
||||||
|
else:
|
||||||
|
origin_type = 'Center'
|
||||||
|
|
||||||
|
for key in keys + fallback_keys:
|
||||||
|
if footprint_has_field(footprint, key):
|
||||||
|
origin_type_override = str(footprint_get_field(footprint, key)).strip().capitalize()
|
||||||
|
|
||||||
|
if origin_type_override in ['Anchor', 'Center']:
|
||||||
|
origin_type = origin_type_override
|
||||||
|
break
|
||||||
|
|
||||||
|
return origin_type
|
||||||
|
|
||||||
|
def _normalize_footprint_name(self, footprint) -> str:
|
||||||
|
# replace footprint names of resistors, capacitors, inductors, diodes, LEDs, fuses etc, with the footprint size only
|
||||||
|
pattern = re.compile(r'^(\w*_SMD:)?\w{1,4}_(\d+)_\d+Metric.*$')
|
||||||
|
|
||||||
|
return pattern.sub(r'\2', footprint)
|
158
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/thread.py
vendored
Normal file
158
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/thread.py
vendored
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import os
|
||||||
|
import wx
|
||||||
|
import pcbnew # type: ignore
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import webbrowser
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from threading import Thread
|
||||||
|
from .events import StatusEvent
|
||||||
|
from .process import ProcessManager
|
||||||
|
from .config import *
|
||||||
|
from .options import *
|
||||||
|
from .utils import print_cli_progress_bar
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessThread(Thread):
|
||||||
|
def __init__(self, wx, options, cli = None, openBrowser = True):
|
||||||
|
Thread.__init__(self)
|
||||||
|
|
||||||
|
# prevent use of cli and grapgical mode at the same time
|
||||||
|
if (wx is None and cli is None) or (wx is not None and cli is not None):
|
||||||
|
logging.error("Specify either graphical or cli use!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if cli is not None:
|
||||||
|
try:
|
||||||
|
self.board = pcbnew.LoadBoard(cli)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Fabrication Toolkit - Error" + str(e))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.board = None
|
||||||
|
|
||||||
|
self.process_manager = ProcessManager(self.board)
|
||||||
|
self.wx = wx
|
||||||
|
self.cli = cli
|
||||||
|
self.options = options
|
||||||
|
self.openBrowser = openBrowser
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# initializing
|
||||||
|
self.progress(0)
|
||||||
|
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
temp_dir_gerber = temp_dir + "_g"
|
||||||
|
os.makedirs(temp_dir_gerber)
|
||||||
|
|
||||||
|
_, temp_file = tempfile.mkstemp()
|
||||||
|
project_directory = os.path.dirname(self.process_manager.board.GetFileName())
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Verify all zones are up-to-date
|
||||||
|
self.progress(10)
|
||||||
|
if (self.options[AUTO_FILL_OPT]):
|
||||||
|
self.process_manager.update_zone_fills()
|
||||||
|
|
||||||
|
# generate gerber
|
||||||
|
self.progress(20)
|
||||||
|
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS], self.options[EXTEND_EDGE_CUT_OPT],
|
||||||
|
self.options[ALTERNATIVE_EDGE_CUT_OPT], self.options[ALL_ACTIVE_LAYERS_OPT])
|
||||||
|
|
||||||
|
# generate drill file
|
||||||
|
self.progress(30)
|
||||||
|
self.process_manager.generate_drills(temp_dir_gerber)
|
||||||
|
|
||||||
|
# generate netlist
|
||||||
|
self.progress(40)
|
||||||
|
self.process_manager.generate_netlist(temp_dir)
|
||||||
|
|
||||||
|
# generate data tables
|
||||||
|
self.progress(50)
|
||||||
|
self.process_manager.generate_tables(temp_dir, self.options[AUTO_TRANSLATE_OPT], self.options[EXCLUDE_DNP_OPT])
|
||||||
|
|
||||||
|
# generate pick and place file
|
||||||
|
self.progress(60)
|
||||||
|
self.process_manager.generate_positions(temp_dir)
|
||||||
|
|
||||||
|
# generate BOM file
|
||||||
|
self.progress(70)
|
||||||
|
self.process_manager.generate_bom(temp_dir)
|
||||||
|
|
||||||
|
# generate production archive
|
||||||
|
self.progress(85)
|
||||||
|
temp_file = self.process_manager.generate_archive(temp_dir_gerber, temp_file)
|
||||||
|
shutil.move(temp_file, temp_dir)
|
||||||
|
shutil.rmtree(temp_dir_gerber)
|
||||||
|
temp_file = os.path.join(temp_dir, os.path.basename(temp_file))
|
||||||
|
except Exception as e:
|
||||||
|
if self.wx is None:
|
||||||
|
logging.error("Fabrication Toolkit - Error" + str(e))
|
||||||
|
else:
|
||||||
|
wx.MessageBox(str(e), "Fabrication Toolkit - Error", wx.OK | wx.ICON_ERROR)
|
||||||
|
self.progress(-1)
|
||||||
|
return
|
||||||
|
|
||||||
|
# progress bar done animation
|
||||||
|
read_so_far = 0
|
||||||
|
total_size = os.path.getsize(temp_file)
|
||||||
|
with open(temp_file, 'rb') as file:
|
||||||
|
while True:
|
||||||
|
data = file.read(10)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
read_so_far += len(data)
|
||||||
|
percent = read_so_far * 1e2 / total_size
|
||||||
|
self.progress(85 + percent / 8)
|
||||||
|
|
||||||
|
# generate gerber name
|
||||||
|
title_block = self.process_manager.board.GetTitleBlock()
|
||||||
|
title = title_block.GetTitle()
|
||||||
|
revision = title_block.GetRevision()
|
||||||
|
company = title_block.GetCompany()
|
||||||
|
file_date = title_block.GetDate()
|
||||||
|
|
||||||
|
if (hasattr(self.process_manager.board, "GetProject") and hasattr(pcbnew, "ExpandTextVars")):
|
||||||
|
project = self.process_manager.board.GetProject()
|
||||||
|
title = pcbnew.ExpandTextVars(title, project)
|
||||||
|
revision = pcbnew.ExpandTextVars(revision, project)
|
||||||
|
company = pcbnew.ExpandTextVars(company, project)
|
||||||
|
file_date = pcbnew.ExpandTextVars(file_date, project)
|
||||||
|
|
||||||
|
# make output dir
|
||||||
|
filename = os.path.splitext(os.path.basename(self.process_manager.board.GetFileName()))[0]
|
||||||
|
output_path = os.path.join(project_directory, outputFolder)
|
||||||
|
if not os.path.exists(output_path):
|
||||||
|
os.makedirs(output_path)
|
||||||
|
|
||||||
|
# rename gerber archive
|
||||||
|
gerberArchiveName = ProcessManager.normalize_filename("_".join(("{} {}".format(title or filename, revision or '').strip() + '.zip').split()))
|
||||||
|
os.rename(temp_file, os.path.join(temp_dir, gerberArchiveName))
|
||||||
|
|
||||||
|
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
|
||||||
|
backup_name = ProcessManager.normalize_filename("_".join(("{} {} {}".format(title or filename, revision or '', timestamp).strip()).split()))
|
||||||
|
shutil.make_archive(os.path.join(output_path, 'backups', backup_name), 'zip', temp_dir)
|
||||||
|
|
||||||
|
|
||||||
|
# copy to & open output dir
|
||||||
|
try:
|
||||||
|
shutil.copytree(temp_dir, output_path, dirs_exist_ok=True)
|
||||||
|
if self.openBrowser:
|
||||||
|
webbrowser.open("file://%s" % (output_path))
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
except Exception as e:
|
||||||
|
if self.openBrowser:
|
||||||
|
webbrowser.open("file://%s" % (temp_dir))
|
||||||
|
|
||||||
|
if self.wx is None:
|
||||||
|
self.progress(100)
|
||||||
|
else:
|
||||||
|
self.progress(-1)
|
||||||
|
|
||||||
|
def progress(self, percent):
|
||||||
|
if self.wx is None:
|
||||||
|
print_cli_progress_bar(percent, prefix = 'Progress:', suffix = 'Complete', length = 50)
|
||||||
|
else:
|
||||||
|
wx.PostEvent(self.wx, StatusEvent(percent))
|
52
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/transformations.csv
vendored
Normal file
52
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/transformations.csv
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
"Regex To Match","Rotation","Delta X","Delta Y"
|
||||||
|
"^Bosch_LGA-",90,0,0
|
||||||
|
"^CP_EIA-",180,0,0
|
||||||
|
"^CP_Elec_",180,0,0
|
||||||
|
"^C_Elec_",180,0,0
|
||||||
|
"^DFN-",270,0,0
|
||||||
|
"^DFN-",270,0,0
|
||||||
|
"^D_SOT-23",180,0,0
|
||||||
|
"^HTSSOP-",270,0,0
|
||||||
|
"^HTSSOP-",270,0,0
|
||||||
|
"^HTSSOP-",270,0,0
|
||||||
|
"^JST_GH_SM",180,0,0
|
||||||
|
"^JST_PH_S",180,0,0
|
||||||
|
"^LQFP-",270,0,0
|
||||||
|
"^MSOP-",270,0,0
|
||||||
|
"^PowerPAK_SO-8_Single",270,0,0
|
||||||
|
"^QFN-",90,0,0
|
||||||
|
"^R_Array_Concave_",90,0,0
|
||||||
|
"^R_Array_Convex_",90,0,0
|
||||||
|
"^SC-74-6",180,0,0
|
||||||
|
"^SOIC-",270,0,0
|
||||||
|
"^SOIC-16_",270,0,0
|
||||||
|
"^SOIC-8_",270,0,0
|
||||||
|
"^SOIC127P798X216-8N",-90,0,0
|
||||||
|
"^SOP-(?!18_)",270,0,0
|
||||||
|
"^SOP-(?!18_)",270,0,0
|
||||||
|
"^SOP-18_",0,0,0
|
||||||
|
"^SOP-18_",0,0,0
|
||||||
|
"^SOP-4_",0,0,0
|
||||||
|
"^SOP-4_",0,0,0
|
||||||
|
"^SOT-143",180,0,0
|
||||||
|
"^SOT-223",180,0,0
|
||||||
|
"^SOT-23",180,0,0
|
||||||
|
"^SOT-353",180,0,0
|
||||||
|
"^SOT-363",180,0,0
|
||||||
|
"^SOT-89",180,0,0
|
||||||
|
"^SSOP-",270,0,0
|
||||||
|
"^SW_SPST_B3",90,0,0
|
||||||
|
"^TDSON-8-1",270,0,0
|
||||||
|
"^TO-277",90,0,0
|
||||||
|
"^TQFP-",270,0,0
|
||||||
|
"^TSOT-23",180,0,0
|
||||||
|
"^TSSOP-",270,0,0
|
||||||
|
"^UDFN-10",270,0,0
|
||||||
|
"^USON-10",270,0,0
|
||||||
|
"^VSON-8_",270,0,0
|
||||||
|
"^VSSOP-10_-",270,0,0
|
||||||
|
"^VSSOP-10_-",270,0,0
|
||||||
|
"^VSSOP-8_",180,0,0
|
||||||
|
"^VSSOP-8_",270,0,0
|
||||||
|
"^VSSOP-8_3.0x3.0mm_P0.65mm",270,0,0
|
||||||
|
"^qfn-",90,0,0
|
|
101
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/utils.py
vendored
Normal file
101
3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad/utils.py
vendored
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import pcbnew # type: ignore
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from .config import optionsFileName
|
||||||
|
import wx
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
return float('.'.join(pcbnew.GetBuildVersion().split(".")[0:2])) # e.g GetBuildVersion(): e.g. '7.99.0-3969-gc5ac2337e4'
|
||||||
|
|
||||||
|
def is_v9(version = get_version()):
|
||||||
|
return version >= 8.99 and version < 9.99
|
||||||
|
|
||||||
|
def is_v8(version = get_version()):
|
||||||
|
return version >= 7.99 and version < 8.99
|
||||||
|
|
||||||
|
def is_v7(version = get_version()):
|
||||||
|
return version >= 6.99 and version < 7.99
|
||||||
|
|
||||||
|
def is_v6(version = get_version()):
|
||||||
|
return version >= 5.99 and version < 6.99
|
||||||
|
|
||||||
|
def footprint_has_field(footprint, field_name):
|
||||||
|
version = get_version()
|
||||||
|
|
||||||
|
if is_v8(version) or is_v9(version):
|
||||||
|
return footprint.HasFieldByName(field_name)
|
||||||
|
else:
|
||||||
|
return footprint.HasProperty(field_name)
|
||||||
|
|
||||||
|
def footprint_get_field(footprint, field_name):
|
||||||
|
version = get_version()
|
||||||
|
|
||||||
|
if is_v8(version) or is_v9(version):
|
||||||
|
return footprint.GetFieldByName(field_name).GetText()
|
||||||
|
else:
|
||||||
|
return footprint.GetProperty(field_name)
|
||||||
|
|
||||||
|
def get_user_options_file_path():
|
||||||
|
boardFilePath = pcbnew.GetBoard().GetFileName()
|
||||||
|
return os.path.join(os.path.dirname(boardFilePath), optionsFileName)
|
||||||
|
|
||||||
|
def load_user_options(default_options):
|
||||||
|
try:
|
||||||
|
with open(get_user_options_file_path(), 'r') as f:
|
||||||
|
user_options = json.load(f)
|
||||||
|
except:
|
||||||
|
user_options = default_options
|
||||||
|
|
||||||
|
# merge the user options with the default options
|
||||||
|
options = default_options.copy()
|
||||||
|
options.update(user_options)
|
||||||
|
return options
|
||||||
|
|
||||||
|
def save_user_options(options):
|
||||||
|
try:
|
||||||
|
with open(get_user_options_file_path(), 'w') as f:
|
||||||
|
json.dump(options, f)
|
||||||
|
except:
|
||||||
|
wx.MessageBox("Error saving user options", "Error", wx.OK | wx.ICON_ERROR)
|
||||||
|
|
||||||
|
def get_plot_plan(board, active_only=True):
|
||||||
|
"""Returns `(KiCad standard name, layer id, custom user name)` of all (active) layers of the given board."""
|
||||||
|
layers = []
|
||||||
|
i = pcbnew.PCBNEW_LAYER_ID_START - 1
|
||||||
|
while i < pcbnew.PCBNEW_LAYER_ID_START + pcbnew.PCB_LAYER_ID_COUNT - 1:
|
||||||
|
i += 1
|
||||||
|
if active_only and not board.IsLayerEnabled(i):
|
||||||
|
continue
|
||||||
|
|
||||||
|
layer_std_name = pcbnew.BOARD.GetStandardLayerName(i)
|
||||||
|
layer_name = pcbnew.BOARD.GetLayerName(board, i)
|
||||||
|
|
||||||
|
layers.append((layer_std_name, i, layer_name))
|
||||||
|
return layers
|
||||||
|
|
||||||
|
def get_layer_names(board, active_only=True):
|
||||||
|
"""Returns a list of (active) layer names of the current board"""
|
||||||
|
plotPlan = get_plot_plan(board, active_only)
|
||||||
|
return [layer_info[0] for layer_info in plotPlan]
|
||||||
|
|
||||||
|
def print_cli_progress_bar(percent, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', printEnd = "\r"):
|
||||||
|
"""
|
||||||
|
Call in a loop to create terminal progress bar string
|
||||||
|
@params:
|
||||||
|
percent - Required : current percentage (Int)
|
||||||
|
prefix - Optional : prefix string (Str)
|
||||||
|
suffix - Optional : suffix string (Str)
|
||||||
|
decimals - Optional : positive number of decimals in percent complete (Int)
|
||||||
|
length - Optional : character length of bar (Int)
|
||||||
|
fill - Optional : bar fill character (Str)
|
||||||
|
printEnd - Optional : end character (e.g. "\r", "\r\n") (Str)
|
||||||
|
"""
|
||||||
|
if percent == -1:
|
||||||
|
percent = 0
|
||||||
|
filledLength = int(length * (percent / 100 ))
|
||||||
|
bar = fill * filledLength + '-' * (length - filledLength)
|
||||||
|
percent2dec = "%.2f" % percent
|
||||||
|
print(f'\r{prefix} |{bar}| {percent2dec}% {suffix}', end = printEnd)
|
||||||
|
# Print New Line on Complete
|
||||||
|
if percent == 100:
|
||||||
|
print()
|
BIN
3rdparty/resources/.DS_Store
vendored
BIN
3rdparty/resources/.DS_Store
vendored
Binary file not shown.
BIN
3rdparty/resources/com_github_30350n_pcb2blender/icon.png
vendored
Normal file
BIN
3rdparty/resources/com_github_30350n_pcb2blender/icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
BIN
3rdparty/resources/com_github_bennymeg_JLC-Plugin-for-KiCad/icon.png
vendored
Normal file
BIN
3rdparty/resources/com_github_bennymeg_JLC-Plugin-for-KiCad/icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
Loading…
Reference in New Issue
Block a user