KiBot/kibot/out_pcb2blender_tools.py

159 lines
6.9 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2023 Salvador E. Tropea
# Copyright (c) 2023 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Some code is adapted from: https://github.com/30350n/pcb2blender
import json
import os
import struct
from pcbnew import B_Paste, F_Paste
from .gs import GS
from .misc import MOD_THROUGH_HOLE, MOD_SMD, UI_VIRTUAL
from .out_base import VariantOptions
from .macros import macros, document, output_class # noqa: F401
from . import log
logger = log.get_logger()
class PCB2Blender_ToolsOptions(VariantOptions):
def __init__(self):
with document:
self.output = GS.def_global_output
""" *Filename for the output (%i=pcb2blender, %x=pcb3d) """
self.board_bounds_create = True
""" Create the file that informs the size of the used PCB area.
This is the bounding box reported by KiCad for the PCB edge with 1 mm of margin """
self.board_bounds_dir = 'layers'
""" Sub-directory where the bounds file is stored """
self.board_bounds_file = 'bounds'
""" Name of the bounds file """
self.pads_info_create = True
""" Create the files containing the PCB pads information """
self.pads_info_dir = 'pads'
""" Sub-directory where the pads info files are stored """
self.stackup_create = False
""" Create a JSON file containing the board stackup """
self.stackup_file = 'board.yaml'
""" Name for the stackup file """
self.stackup_dir = '.'
""" Directory for the stackup file """
super().__init__()
self._expand_id = 'pcb2blender'
self._expand_ext = 'pcb3d'
def do_board_bounds(self, dir_name):
if not self.board_bounds_create:
return
dir_name = os.path.join(dir_name, self.board_bounds_dir)
os.makedirs(dir_name, exist_ok=True)
fname = os.path.join(dir_name, self.board_bounds_file)
# PCB bounding box using the PCB edge, converted to mm
bounds = tuple(map(GS.to_mm, GS.board.ComputeBoundingBox(aBoardEdgesOnly=True).getWxRect()))
# Apply 1 mm margin (x, y, w, h)
bounds = (bounds[0]-1, bounds[1]-1, bounds[2]+2, bounds[3]+2)
with open(fname, 'wb') as f:
# Four big endian float numbers
f.write(struct.pack("!ffff", *bounds))
@staticmethod
def is_not_virtual_ki6(m):
return bool(m.GetAttributes() & (MOD_THROUGH_HOLE | MOD_SMD))
@staticmethod
def is_not_virtual_ki5(m):
return bool(m.GetAttributes() != UI_VIRTUAL)
def do_pads_info(self, dir_name):
if not self.pads_info_create:
return
dir_name = os.path.join(dir_name, self.pads_info_dir)
os.makedirs(dir_name, exist_ok=True)
is_not_virtual = self.is_not_virtual_ki5 if GS.ki5 else self.is_not_virtual_ki6
for i, footprint in enumerate(GS.get_modules()):
has_model = len(footprint.Models()) > 0
is_tht_or_smd = is_not_virtual(footprint)
value = footprint.GetValue()
reference = footprint.GetReference()
for j, pad in enumerate(footprint.Pads()):
name = os.path.join(dir_name, "{}_{}_{}_{}".format(value, reference, i, j))
is_flipped = pad.IsFlipped()
has_paste = pad.IsOnLayer(B_Paste if is_flipped else F_Paste)
with open(name, 'wb') as f:
f.write(struct.pack("!ff????BBffffBff",
*map(GS.to_mm, pad.GetPosition()),
is_flipped,
has_model,
is_tht_or_smd,
has_paste,
pad.GetAttribute(),
pad.GetShape(),
*map(GS.to_mm, pad.GetSize()),
pad.GetOrientationRadians(),
pad.GetRoundRectRadiusRatio(),
pad.GetDrillShape(),
*map(GS.to_mm, pad.GetDrillSize())))
def do_stackup(self, dir_name):
if not self.stackup_create or (not GS.global_pcb_finish and not GS.stackup):
return
dir_name = os.path.join(dir_name, self.stackup_dir)
os.makedirs(dir_name, exist_ok=True)
fname = os.path.join(dir_name, self.stackup_file)
# Create the board_info
board_info = {}
if GS.global_pcb_finish:
board_info['copper_finish'] = GS.global_pcb_finish
if GS.stackup:
layers_parsed = []
for la in GS.stackup:
parsed_layer = {'name': la.name, 'type': la.type}
if la.color is not None:
parsed_layer['color'] = la.color
if la.thickness is not None:
parsed_layer['thickness'] = la.thickness/1000
layers_parsed.append(parsed_layer)
board_info['stackup'] = layers_parsed
with open(fname, 'wt') as f:
json.dump(board_info, f, indent=3)
def run(self, output):
super().run(output)
dir_name = os.path.dirname(output)
self.filter_pcb_components(do_3D=True)
self.do_board_bounds(dir_name)
self.do_pads_info(dir_name)
self.do_stackup(dir_name)
self.unfilter_pcb_components(do_3D=True)
def get_targets(self, out_dir):
files = []
if self.board_bounds_create:
files.append(os.path.join(out_dir, self.board_bounds_dir, self.board_bounds_file))
if self.pads_info_create:
dir_name = os.path.join(out_dir, self.pads_info_dir)
for i, footprint in enumerate(GS.get_modules()):
value = footprint.GetValue()
reference = footprint.GetReference()
for j in range(len(footprint.Pads())):
files.append(os.path.join(dir_name, "{}_{}_{}_{}".format(value, reference, i, j)))
return files
@output_class
class PCB2Blender_Tools(BaseOutput): # noqa: F821
""" PCB2Blender Tools
A bunch of tools used to generate PCB3D files used to export PCBs to Blender.
Blender is the most important free software 3D render package.
The PCB3D file format is used by the PCB2Blender project (https://github.com/30350n/pcb2blender)
to import KiCad PCBs in Blender.
You need to install a Blender plug-in to load PCB3D files.
The tools in this output are used by internal templates used to generate PCB3D files. """
def __init__(self):
super().__init__()
self._category = 'PCB/3D'
with document:
self.options = PCB2Blender_ToolsOptions
""" *[dict] Options for the `pcb2blender_tools` output """