update plugins
This commit is contained in:
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
|
Reference in New Issue
Block a user