Initial Commit

This commit is contained in:
pszsh 2026-02-05 05:24:59 -08:00
commit cabd8e196e
74 changed files with 28702 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
*.pyc
.idea
.vscode
*.iml
*.bak
test
releases
demo
*config.ini
InteractiveHtmlBom/web/user*
dist/

27
.jsbeautifyrc Normal file
View File

@ -0,0 +1,27 @@
{
"indent_size": 2,
"indent_char": " ",
"indent_with_tabs": false,
"editorconfig": false,
"eol": "\n",
"end_with_newline": true,
"indent_level": 0,
"preserve_newlines": true,
"max_preserve_newlines": 10,
"space_in_paren": false,
"space_in_empty_paren": false,
"jslint_happy": false,
"space_after_anon_function": false,
"space_after_named_function": false,
"brace_style": "collapse",
"unindent_chained_methods": false,
"break_chained_methods": false,
"keep_array_indentation": false,
"unescape_strings": false,
"wrap_line_length": 0,
"e4x": false,
"comma_first": false,
"operator_position": "before-newline",
"indent_empty_lines": false,
"templating": ["auto"]
}

387
DATAFORMAT.md Normal file
View File

@ -0,0 +1,387 @@
# pcbdata struct
This document describes pcbdata json structure that plugin
extracts from PCB file and injects into generated bom page.
Notes on conventions:
* Coordinate system has origin in top left corner i.e. Y grows downwards
* All angles are in degrees measured clockwise from positive X axis vector
* Units are arbitrary but some browsers will not handle too large numbers
well so sticking to mm/mils is preferred.
```js
pcbdata = {
// Describes bounding box of all edge cut drawings.
// Used for determining default zoom and pan values to fit
// whole board on canvas.
"edges_bbox": {
"minx": 1,
"miny": 2,
"maxx": 100,
"maxy": 200,
},
// Describes all edge cut drawings including ones in footprints.
// See drawing structure description below.
"edges": [drawing1, drawing2, ...],
"drawings": {
// Contains all drawings + reference + value texts on silkscreen
// layer grouped by front and back.
"silkscreen": {
"F": [drawing1, drawing2, ...],
"B": [drawing1, drawing2, ...],
},
// Same as above but for fabrication layer.
"fabrication": {
"F": [drawing1, drawing2, ...],
"B": [drawing1, drawing2, ...],
},
},
// Describes footprints.
// See footprint structure description below.
// index of entry corresponds to component's numeric ID
"footprints": [
footprint1,
footprint2,
...
],
// Optional track data. Vias are 0 length tracks.
"tracks": {
"F": [
{
// In case of line segment or via (via is 0 length segment)
"start": [x, y],
"end": [x, y],
// In case of arc
"center": [x, y],
"startangle":
"radius": radius,
"startangle": angle1,
"endangle": angle2,
// Common fields
"width": w,
// Optional net name
"net": netname,
// Optional drill diameter (un-tented vias only)
"drillsize": x
},
...
],
"B": [...]
},
// Optional zone data (should be present if tracks are present).
"zones": {
"F": [
{
// SVG path of the polygon given as 'd' attribute of svg spec.
// If "svgpath" is present "polygons" is ignored.
"svgpath": svgpath,
// optional fillrule flag, defaults to nonzero
"fillrule": "nonzero" | "evenodd",
"polygons": [
// Set of polylines same as in polygon drawing.
[[point1x, point1y], [point2x, point2y], ...],
],
// Optional net name.
"net": netname,
},
...
],
"B": [...]
},
// Optional net name list.
"nets": [net1, net2, ...],
// PCB metadata from the title block.
"metadata": {
"title": "title",
"revision": "rev",
"company": "Horns and Hoofs",
"date": "2019-04-18",
},
// Contains full bom table as well as filtered by front/back.
// See bom row description below.
"bom": {
"both": [bomrow1, bomrow2, ...],
"F": [bomrow1, bomrow2, ...],
"B": [bomrow1, bomrow2, ...],
// numeric IDs of DNP components that are not in BOM
"skipped": [id1, id2, ...]
// Fields map is keyed on component ID with values being field data.
// It's order corresponds to order of fields data in config struct.
"fields" {
id1: [field1, field2, ...],
id2: [field1, field2, ...],
...
}
},
// Contains parsed stroke data from newstroke font for
// characters used on the pcb.
"font_data": {
"a": {
"w": character_width,
// Contains array of polylines that form the character shape.
"l": [
[[point1x, point1y], [point2x, point2y],...],
...
]
},
"%": {
...
},
...
},
}
```
# drawing struct
All drawings are either graphical items (arcs, lines, circles, curves)
or text.
Rendering method and properties are determined based on `type`
attribute.
## graphical items
### segment
```js
{
"type": "segment",
"start": [x, y],
"end": [x, y],
"width": width,
}
```
### rect
```js
{
"type": "rect",
"start": [x, y], // coordinates of opposing corners
"end": [x, y],
"width": width,
}
```
### circle
```js
{
"type": "circle",
"start": [x, y],
"radius": radius,
// Optional boolean, defaults to 0
"filled": 0,
// Line width (only has effect for non-filled shapes)
"width": width,
}
```
### arc
```js
{
"type": "arc",
"width": width,
// SVG path of the arc given as 'd' attribute of svg spec.
// If this parameter is specified everything below it is ignored.
"svgpath": svgpath,
"start": [x, y], // arc center
"radius": radius,
"startangle": angle1,
"endangle": angle2,
}
```
### curve
```js
{
"type": "curve", // Bezier curve
"start": [x, y],
"end": [x, y],
"cpa": [x, y], // control point A
"cpb": [x, y], // control point B
"width": width,
}
```
### polygon
```js
{
"type": "polygon",
// Optional, defaults to 1
"filled": 1,
// Line width (only has effect for non-filled shapes)
"width": width
// SVG path of the polygon given as 'd' attribute of svg spec.
// If this parameter is specified everything below it is ignored.
"svgpath": svgpath,
"pos": [x, y],
"angle": angle,
"polygons": [
// Polygons are described as set of outlines.
[
[point1x, point1y], [point2x, point2y], ...
],
...
]
}
```
## text
```js
{
"pos": [x, y],
"text": text,
// SVG path of the text given as 'd' attribute of svg spec.
// If this parameter is specified then height, width, angle,
// text attributes and justification is ignored. Rendering engine
// will not attempt to read character data from newstroke font and
// will draw the path as is. "thickness" will be used as stroke width.
"svgpath": svgpath,
// If polygons are specified then remaining attributes are ignored
"polygons": [
// Polygons are described as set of outlines.
[
[point1x, point1y], [point2x, point2y], ...
],
...
],
"height": height,
"width": width,
// -1: justify left/top
// 0: justify center
// 1: justify right/bot
"justify": [horizontal, vertical],
// Either the thickness or the fillrule must be used
"thickness": thickness,
// fillrule is only supported for svgpath
"fillrule": "nonzero" | "evenodd",
"attr": [
// may include none, one or both
"italic", "mirrored"
],
"angle": angle,
// Present only if text is reference designator
"ref": 1,
// Present only if text is component value
"val": 1,
}
```
# footprint struct
Footprints are a collection of pads, drawings and some metadata.
```js
{
"ref": reference,
"center": [x, y],
"bbox": {
// Position of the rotation center of the bounding box.
"pos": [x, y],
// Rotation angle in degrees.
"angle": angle,
// Left top corner position relative to center (after rotation)
"relpos": [x, y],
"size": [x, y],
},
"pads": [
{
"layers": [
// Contains one or both
"F", "B",
],
"pos": [x, y],
"size": [x, y],
"angle": angle,
// Only present if pad is considered first pin.
// Pins are considered first if it's name is one of
// 1, A, A1, P1, PAD1
// OR footprint has no pads named as one of above and
// current pad's name is lexicographically smallest.
"pin1": 1,
// Shape is one of "rect", "oval", "circle", "roundrect", "chamfrect", custom".
"shape": shape,
// Only present if shape is "custom".
// SVG path of the polygon given as 'd' attribute of svg spec.
// If "svgpath" is present "polygons", "pos", "angle" are ignored.
"svgpath": svgpath,
"polygons": [
// Set of polylines same as in polygon drawing.
[[point1x, point1y], [point2x, point2y], ...],
...
],
// Only present if shape is "roundrect" or "chamfrect".
"radius": radius,
// Only present if shape is "chamfrect".
// chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8
"chamfpos": chamfpos,
"chamfratio": ratio,
// Pad type is "th" for standard and NPTH pads
// "smd" otherwise.
"type": type,
// Present only if type is "th".
// One of "circle", "oblong" or "rect".
"drillshape": drillshape,
// Present only if type is "th". In case of circle shape x is diameter, y is ignored.
"drillsize": [x, y],
// Optional attribute.
"offset": [x, y],
// Optional net name
"net": netname,
},
...
],
"drawings": [
// Contains only copper F_Cu, B_Cu drawings of the footprint.
{
// One of "F", "B".
"layer": layer,
// See drawing struct description above.
"drawing": drawing,
},
...
],
// One of "F", "B".
"layer": layer,
}
```
# bom row struct
Bom row is a list of reference sets
Reference set is array of tuples of (ref, id) where id is just
a unique numeric identifier for each footprint that helps avoid
collisions when references are duplicated.
```js
[
[reference_name, footprint_id],
...
]
```
# config struct
```js
config = {
"dark_mode": bool,
"show_pads": bool,
"show_fabrication": bool,
"show_silkscreen": bool,
"highlight_pin1": "none" | "all" | "selected",
"redraw_on_drag": bool,
"board_rotation": int,
"checkboxes": "checkbox1,checkbox2,...",
"bom_view": "bom-only" | "left-right" | "top-bottom",
"layer_view": "F" | "FB" | "B",
"extra_fields": ["field1_name", "field2_name", ...],
}
```

1
InteractiveHtmlBom/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.bat eol=crlf

View File

@ -0,0 +1,73 @@
@echo off
set pathofEDASourceFile=%1
set FilePath=%~dp0
::delete --show-dialog after first start up and setting
set option=--show-dialog
::detect current language of user.
reg query "HKCU\Control Panel\Desktop" /v PreferredUILanguages>nul 2>nul&&goto _dosearch1_||goto _dosearch2_
:_dosearch1_
FOR /F "tokens=3" %%a IN (
'reg query "HKCU\Control Panel\Desktop" /v PreferredUILanguages ^| find "PreferredUILanguages"'
) DO (
set language=%%a
)
set language=%language:~,2%
goto _setlanguage_
:_dosearch2_
FOR /F "tokens=3" %%a IN (
'reg query "HKLM\SYSTEM\ControlSet001\Control\Nls\Language" /v InstallLanguage ^| find "InstallLanguage"'
) DO (
set language=%%a
)
if %language%==0804 (
set language=zh
)
goto _setlanguage_
:_setlanguage_
if %language%==zh (
call %FilePath%\i18n\language_zh.bat
) else (
call %FilePath%\i18n\language_en.bat
)
cls
echo -------------------------------------------------------------------------------------------------------------------
echo -------------------------------------------------------------------------------------------------------------------
echo.
echo %i18n_thx4using%
echo %i18n_gitAddr%
echo %i18n_batScar%
echo.
echo -------------------------------------------------------------------------------------------------------------------
echo -------------------------------------------------------------------------------------------------------------------
set pyFilePath=%FilePath%generate_interactive_bom.py
:_convert_
if not defined pathofEDASourceFile (
set /p pathofEDASourceFile=%i18n_draghere%
)
echo.
echo %i18n_converting%
echo.
python %pyFilePath% %pathofEDASourceFile% %option%
set pathofEDASourceFile=
echo -------------------------------------------------------------------------------------------------------------------
echo -------------------------------------------------------------------------------------------------------------------
echo.
echo %i18n_converted%
echo.
echo -------------------------------------------------------------------------------------------------------------------
echo -------------------------------------------------------------------------------------------------------------------
CHOICE /C YN /N /M "%i18n_again% [ Y/N ]"
if errorlevel 2 exit
if errorlevel 1 goto _convert_

View File

@ -0,0 +1,58 @@
import os
import sys
import threading
import time
import wx
import wx.aui
def check_for_bom_button():
# From Miles McCoo's blog
# https://kicad.mmccoo.com/2017/03/05/adding-your-own-command-buttons-to-the-pcbnew-gui/
def find_pcbnew_window():
windows = wx.GetTopLevelWindows()
pcbneww = [w for w in windows if "pcbnew" in w.GetTitle().lower()]
if len(pcbneww) != 1:
return None
return pcbneww[0]
def callback(_):
plugin.Run()
path = os.path.dirname(__file__)
while not wx.GetApp():
time.sleep(1)
bm = wx.Bitmap(path + '/icon.png', wx.BITMAP_TYPE_PNG)
button_wx_item_id = 0
from pcbnew import ID_H_TOOLBAR
while True:
time.sleep(1)
pcbnew_window = find_pcbnew_window()
if not pcbnew_window:
continue
top_tb = pcbnew_window.FindWindowById(ID_H_TOOLBAR)
if button_wx_item_id == 0 or not top_tb.FindTool(button_wx_item_id):
top_tb.AddSeparator()
button_wx_item_id = wx.NewId()
top_tb.AddTool(button_wx_item_id, "iBOM", bm,
"Generate interactive BOM", wx.ITEM_NORMAL)
top_tb.Bind(wx.EVT_TOOL, callback, id=button_wx_item_id)
top_tb.Realize()
if (not os.environ.get('INTERACTIVE_HTML_BOM_CLI_MODE', False) and
not os.path.basename(sys.argv[0]).startswith('generate_interactive_bom')):
from .ecad.kicad import InteractiveHtmlBomPlugin
plugin = InteractiveHtmlBomPlugin()
plugin.register()
# Add a button the hacky way if plugin button is not supported
# in pcbnew, unless this is linux.
if not plugin.pcbnew_icon_support and not sys.platform.startswith('linux'):
t = threading.Thread(target=check_for_bom_button)
t.daemon = True
t.start()

View File

View File

@ -0,0 +1,503 @@
# InteractiveHtmlBom/core/config.py
"""Config object"""
import argparse
import os
import re
from wx import FileConfig
from .. import dialog
class Config:
FILE_NAME_FORMAT_HINT = (
'Output file name format supports substitutions:\n'
'\n'
' %f : original pcb file name without extension.\n'
' %p : pcb/project title from pcb metadata.\n'
' %c : company from pcb metadata.\n'
' %r : revision from pcb metadata.\n'
' %d : pcb date from metadata if available, '
'file modification date otherwise.\n'
' %D : bom generation date.\n'
' %T : bom generation time.\n'
'\n'
'Extension .html will be added automatically.'
) # type: str
# Helper constants
bom_view_choices = ['bom-only', 'left-right', 'top-bottom']
layer_view_choices = ['F', 'FB', 'B']
default_sort_order = [
'C', 'R', 'L', 'D', 'U', 'Y', 'X', 'F', 'SW', 'A',
'~',
'HS', 'CNN', 'J', 'P', 'NT', 'MH',
]
highlight_pin1_choices = ['none', 'all', 'selected']
default_checkboxes = ['Sourced', 'Placed']
html_config_fields = [
'dark_mode', 'show_pads', 'show_fabrication', 'show_silkscreen',
'highlight_pin1', 'redraw_on_drag', 'board_rotation', 'checkboxes',
'bom_view', 'layer_view', 'offset_back_rotation',
'kicad_text_formatting', 'mark_when_checked', 'rainbow_mode'
]
default_show_group_fields = ["Value", "Footprint"]
# Defaults
# HTML section
dark_mode = False
show_pads = True
show_fabrication = False
show_silkscreen = True
redraw_on_drag = True
highlight_pin1 = highlight_pin1_choices[0]
board_rotation = 0
offset_back_rotation = False
checkboxes = ','.join(default_checkboxes)
mark_when_checked = ''
bom_view = bom_view_choices[1]
layer_view = layer_view_choices[1]
compression = True
open_browser = True
rainbow_mode = False
# General section
bom_dest_dir = 'bom/' # This is relative to pcb file directory
bom_name_format = 'ibom'
component_sort_order = default_sort_order
component_blacklist = []
blacklist_virtual = True
blacklist_empty_val = False
include_tracks = False
include_nets = False
kicad_text_formatting = True
# Extra fields section
extra_data_file = None
netlist_initial_directory = '' # This is relative to pcb file directory
show_fields = default_show_group_fields
group_fields = default_show_group_fields
normalize_field_case = False
board_variant_field = ''
board_variant_whitelist = []
board_variant_blacklist = []
dnp_field = ''
@staticmethod
def _split(s):
"""Splits string by ',' and drops empty strings from resulting array"""
return [a.replace('\\,', ',') for a in re.split(r'(?<!\\),', s) if a]
@staticmethod
def _join(lst):
return ','.join([s.replace(',', '\\,') for s in lst])
def __init__(self, version, local_dir):
self.version = version
self.local_config_file = os.path.join(local_dir, 'ibom.config.ini')
self.global_config_file = os.path.join(
os.path.dirname(__file__), '..', 'config.ini')
def load_from_ini(self):
"""Init from config file if it exists."""
if os.path.isfile(self.local_config_file):
file = self.local_config_file
elif os.path.isfile(self.global_config_file):
file = self.global_config_file
else:
return
f = FileConfig(localFilename=file)
f.SetPath('/html_defaults')
self.dark_mode = f.ReadBool('dark_mode', self.dark_mode)
self.show_pads = f.ReadBool('show_pads', self.show_pads)
self.show_fabrication = f.ReadBool(
'show_fabrication', self.show_fabrication)
self.show_silkscreen = f.ReadBool(
'show_silkscreen', self.show_silkscreen)
self.redraw_on_drag = f.ReadBool('redraw_on_drag', self.redraw_on_drag)
self.highlight_pin1 = f.Read('highlight_pin1', self.highlight_pin1)
self.board_rotation = f.ReadInt('board_rotation', self.board_rotation)
self.offset_back_rotation = f.ReadBool(
'offset_back_rotation', self.offset_back_rotation)
self.checkboxes = f.Read('checkboxes', self.checkboxes)
self.mark_when_checked = f.Read('mark_when_checked', self.mark_when_checked)
self.bom_view = f.Read('bom_view', self.bom_view)
self.layer_view = f.Read('layer_view', self.layer_view)
self.compression = f.ReadBool('compression', self.compression)
self.open_browser = f.ReadBool('open_browser', self.open_browser)
self.rainbow_mode = f.ReadBool('rainbow_mode', self.rainbow_mode)
f.SetPath('/general')
self.bom_dest_dir = f.Read('bom_dest_dir', self.bom_dest_dir)
self.bom_name_format = f.Read('bom_name_format', self.bom_name_format)
self.component_sort_order = self._split(f.Read(
'component_sort_order',
','.join(self.component_sort_order)))
self.component_blacklist = self._split(f.Read(
'component_blacklist',
','.join(self.component_blacklist)))
self.blacklist_virtual = f.ReadBool(
'blacklist_virtual', self.blacklist_virtual)
self.blacklist_empty_val = f.ReadBool(
'blacklist_empty_val', self.blacklist_empty_val)
self.include_tracks = f.ReadBool('include_tracks', self.include_tracks)
self.include_nets = f.ReadBool('include_nets', self.include_nets)
f.SetPath('/fields')
self.show_fields = self._split(f.Read(
'show_fields', self._join(self.show_fields)))
self.group_fields = self._split(f.Read(
'group_fields', self._join(self.group_fields)))
self.normalize_field_case = f.ReadBool(
'normalize_field_case', self.normalize_field_case)
self.board_variant_field = f.Read(
'board_variant_field', self.board_variant_field)
self.board_variant_whitelist = self._split(f.Read(
'board_variant_whitelist',
self._join(self.board_variant_whitelist)))
self.board_variant_blacklist = self._split(f.Read(
'board_variant_blacklist',
self._join(self.board_variant_blacklist)))
self.dnp_field = f.Read('dnp_field', self.dnp_field)
# migration from previous settings
if self.highlight_pin1 == '0':
self.highlight_pin1 = 'none'
if self.highlight_pin1 == '1':
self.highlight_pin1 = 'all'
def save(self, locally):
file = self.local_config_file if locally else self.global_config_file
print('Saving to', file)
f = FileConfig(localFilename=file)
f.SetPath('/html_defaults')
f.WriteBool('dark_mode', self.dark_mode)
f.WriteBool('show_pads', self.show_pads)
f.WriteBool('show_fabrication', self.show_fabrication)
f.WriteBool('show_silkscreen', self.show_silkscreen)
f.WriteBool('redraw_on_drag', self.redraw_on_drag)
f.Write('highlight_pin1', self.highlight_pin1)
f.WriteInt('board_rotation', self.board_rotation)
f.WriteBool('offset_back_rotation', self.offset_back_rotation)
f.Write('checkboxes', self.checkboxes)
f.Write('mark_when_checked', self.mark_when_checked)
f.Write('bom_view', self.bom_view)
f.Write('layer_view', self.layer_view)
f.WriteBool('compression', self.compression)
f.WriteBool('open_browser', self.open_browser)
f.WriteBool('rainbow_mode', self.rainbow_mode)
f.SetPath('/general')
bom_dest_dir = self.bom_dest_dir
if bom_dest_dir.startswith(self.netlist_initial_directory):
bom_dest_dir = os.path.relpath(
bom_dest_dir, self.netlist_initial_directory)
f.Write('bom_dest_dir', bom_dest_dir)
f.Write('bom_name_format', self.bom_name_format)
f.Write('component_sort_order',
','.join(self.component_sort_order))
f.Write('component_blacklist',
','.join(self.component_blacklist))
f.WriteBool('blacklist_virtual', self.blacklist_virtual)
f.WriteBool('blacklist_empty_val', self.blacklist_empty_val)
f.WriteBool('include_tracks', self.include_tracks)
f.WriteBool('include_nets', self.include_nets)
f.SetPath('/fields')
f.Write('show_fields', self._join(self.show_fields))
f.Write('group_fields', self._join(self.group_fields))
f.WriteBool('normalize_field_case', self.normalize_field_case)
f.Write('board_variant_field', self.board_variant_field)
f.Write('board_variant_whitelist',
self._join(self.board_variant_whitelist))
f.Write('board_variant_blacklist',
self._join(self.board_variant_blacklist))
f.Write('dnp_field', self.dnp_field)
f.Flush()
def set_from_dialog(self, dlg):
# type: (dialog.settings_dialog.SettingsDialogPanel) -> None
# Html
self.dark_mode = dlg.html.darkModeCheckbox.IsChecked()
self.show_pads = dlg.html.showPadsCheckbox.IsChecked()
self.show_fabrication = dlg.html.showFabricationCheckbox.IsChecked()
self.show_silkscreen = dlg.html.showSilkscreenCheckbox.IsChecked()
self.redraw_on_drag = dlg.html.continuousRedrawCheckbox.IsChecked()
self.highlight_pin1 = self.highlight_pin1_choices[dlg.html.highlightPin1.Selection]
self.board_rotation = dlg.html.boardRotationSlider.Value
self.offset_back_rotation = \
dlg.html.offsetBackRotationCheckbox.IsChecked()
self.checkboxes = dlg.html.bomCheckboxesCtrl.Value
# No dialog for mark_when_checked ...
self.bom_view = self.bom_view_choices[dlg.html.bomDefaultView.Selection]
self.layer_view = self.layer_view_choices[
dlg.html.layerDefaultView.Selection]
self.compression = dlg.html.compressionCheckbox.IsChecked()
self.open_browser = dlg.html.openBrowserCheckbox.IsChecked()
self.rainbow_mode = dlg.html.rainbowModeCheckbox.IsChecked()
# General
self.bom_dest_dir = dlg.general.bomDirPicker.Path
self.bom_name_format = dlg.general.fileNameFormatTextControl.Value
self.component_sort_order = dlg.general.componentSortOrderBox.GetItems()
self.component_blacklist = dlg.general.blacklistBox.GetItems()
self.blacklist_virtual = \
dlg.general.blacklistVirtualCheckbox.IsChecked()
self.blacklist_empty_val = \
dlg.general.blacklistEmptyValCheckbox.IsChecked()
self.include_tracks = dlg.general.includeTracksCheckbox.IsChecked()
self.include_nets = dlg.general.includeNetsCheckbox.IsChecked()
# Fields
self.extra_data_file = dlg.fields.extraDataFilePicker.Path
self.show_fields = dlg.fields.GetShowFields()
self.group_fields = dlg.fields.GetGroupFields()
self.normalize_field_case = dlg.fields.normalizeCaseCheckbox.Value
self.board_variant_field = dlg.fields.boardVariantFieldBox.Value
if self.board_variant_field == dlg.fields.NONE_STRING:
self.board_variant_field = ''
self.board_variant_whitelist = list(
dlg.fields.boardVariantWhitelist.GetCheckedStrings())
self.board_variant_blacklist = list(
dlg.fields.boardVariantBlacklist.GetCheckedStrings())
self.dnp_field = dlg.fields.dnpFieldBox.Value
if self.dnp_field == dlg.fields.NONE_STRING:
self.dnp_field = ''
def transfer_to_dialog(self, dlg):
# type: (dialog.settings_dialog.SettingsDialogPanel) -> None
# Html
dlg.html.darkModeCheckbox.Value = self.dark_mode
dlg.html.showPadsCheckbox.Value = self.show_pads
dlg.html.showFabricationCheckbox.Value = self.show_fabrication
dlg.html.showSilkscreenCheckbox.Value = self.show_silkscreen
dlg.html.highlightPin1.Selection = 0
if self.highlight_pin1 in self.highlight_pin1_choices:
dlg.html.highlightPin1.Selection = \
self.highlight_pin1_choices.index(self.highlight_pin1)
dlg.html.continuousRedrawCheckbox.value = self.redraw_on_drag
dlg.html.boardRotationSlider.Value = self.board_rotation
dlg.html.offsetBackRotationCheckbox.Value = self.offset_back_rotation
dlg.html.bomCheckboxesCtrl.Value = self.checkboxes
# No dialog for mark_when_checked ...
dlg.html.bomDefaultView.Selection = self.bom_view_choices.index(
self.bom_view)
dlg.html.layerDefaultView.Selection = self.layer_view_choices.index(
self.layer_view)
dlg.html.compressionCheckbox.Value = self.compression
dlg.html.openBrowserCheckbox.Value = self.open_browser
dlg.html.rainbowModeCheckbox.Value = self.rainbow_mode
# General
import os.path
if os.path.isabs(self.bom_dest_dir):
dlg.general.bomDirPicker.Path = self.bom_dest_dir
else:
dlg.general.bomDirPicker.Path = os.path.join(
self.netlist_initial_directory, self.bom_dest_dir)
dlg.general.fileNameFormatTextControl.Value = self.bom_name_format
dlg.general.componentSortOrderBox.SetItems(self.component_sort_order)
dlg.general.blacklistBox.SetItems(self.component_blacklist)
dlg.general.blacklistVirtualCheckbox.Value = self.blacklist_virtual
dlg.general.blacklistEmptyValCheckbox.Value = self.blacklist_empty_val
dlg.general.includeTracksCheckbox.Value = self.include_tracks
dlg.general.includeNetsCheckbox.Value = self.include_nets
# Fields
dlg.fields.extraDataFilePicker.SetInitialDirectory(
self.netlist_initial_directory)
def safe_set_checked_strings(clb, strings):
current = list(clb.GetStrings())
if current:
present_strings = [s for s in strings if s in current]
not_present_strings = [s for s in current if s not in strings]
clb.Clear()
clb.InsertItems(present_strings + not_present_strings, 0)
clb.SetCheckedStrings(present_strings)
dlg.fields.SetCheckedFields(self.show_fields, self.group_fields)
dlg.fields.normalizeCaseCheckbox.Value = self.normalize_field_case
dlg.fields.boardVariantFieldBox.Value = self.board_variant_field
dlg.fields.OnBoardVariantFieldChange(None)
safe_set_checked_strings(dlg.fields.boardVariantWhitelist,
self.board_variant_whitelist)
safe_set_checked_strings(dlg.fields.boardVariantBlacklist,
self.board_variant_blacklist)
dlg.fields.dnpFieldBox.Value = self.dnp_field
dlg.finish_init()
@classmethod
def add_options(cls, parser, version):
# type: (argparse.ArgumentParser, str) -> None
parser.add_argument('--show-dialog', action='store_true',
help='Shows config dialog. All other flags '
'will be ignored.')
parser.add_argument('--version', action='version', version=version)
# Html
parser.add_argument('--dark-mode', help='Default to dark mode.',
action='store_true')
parser.add_argument('--hide-pads',
help='Hide footprint pads by default.',
action='store_true')
parser.add_argument('--show-fabrication',
help='Show fabrication layer by default.',
action='store_true')
parser.add_argument('--hide-silkscreen',
help='Hide silkscreen by default.',
action='store_true')
parser.add_argument('--highlight-pin1',
default=cls.highlight_pin1_choices[0],
const=cls.highlight_pin1_choices[1],
choices=cls.highlight_pin1_choices,
nargs='?',
help='Highlight first pin.')
parser.add_argument('--no-redraw-on-drag',
help='Do not redraw pcb on drag by default.',
action='store_true')
parser.add_argument('--board-rotation', type=int,
default=cls.board_rotation * 5,
help='Board rotation in degrees (-180 to 180). '
'Will be rounded to multiple of 5.')
parser.add_argument('--offset-back-rotation',
help='Offset the back of the pcb by 180 degrees',
action='store_true')
parser.add_argument('--checkboxes',
default=cls.checkboxes,
help='Comma separated list of checkbox columns.')
parser.add_argument('--mark-when-checked',
default=cls.mark_when_checked,
help='Name of the checkbox column used to mark '
'components when checked.')
parser.add_argument('--bom-view', default=cls.bom_view,
choices=cls.bom_view_choices,
help='Default BOM view.')
parser.add_argument('--layer-view', default=cls.layer_view,
choices=cls.layer_view_choices,
help='Default layer view.')
parser.add_argument('--no-compression',
help='Disable compression of pcb data.',
action='store_true')
parser.add_argument('--no-browser', help='Do not launch browser.',
action='store_true')
parser.add_argument('--rainbow-mode', help='Enable rainbow mode for component highlighting.',
action='store_true')
# General
parser.add_argument('--dest-dir', default=cls.bom_dest_dir,
help='Destination directory for bom file '
'relative to pcb file directory.')
parser.add_argument('--name-format', default=cls.bom_name_format,
help=cls.FILE_NAME_FORMAT_HINT.replace('%', '%%'))
parser.add_argument('--include-tracks', action='store_true',
help='Include track/zone information in output. '
'F.Cu and B.Cu layers only.')
parser.add_argument('--include-nets', action='store_true',
help='Include netlist information in output.')
parser.add_argument('--sort-order',
help='Default sort order for components. '
'Must contain "~" once.',
default=','.join(cls.component_sort_order))
parser.add_argument('--blacklist',
default=','.join(cls.component_blacklist),
help='List of comma separated blacklisted '
'components or prefixes with *. '
'E.g. "X1,MH*"')
parser.add_argument('--no-blacklist-virtual', action='store_true',
help='Do not blacklist virtual components.')
parser.add_argument('--blacklist-empty-val', action='store_true',
help='Blacklist components with empty value.')
# Fields section
parser.add_argument('--netlist-file',
help='(Deprecated) Path to netlist or xml file.')
parser.add_argument('--extra-data-file',
help='Path to netlist or xml file.')
parser.add_argument('--extra-fields',
help='Passing --extra-fields "X,Y" is a shortcut '
'for --show-fields and --group-fields '
'with values "Value,Footprint,X,Y"')
parser.add_argument('--show-fields',
default=cls._join(cls.show_fields),
help='List of fields to show in the BOM.')
parser.add_argument('--group-fields',
default=cls._join(cls.group_fields),
help='Fields that components will be grouped by.')
parser.add_argument('--normalize-field-case',
help='Normalize extra field name case. E.g. "MPN" '
', "mpn" will be considered the same field.',
action='store_true')
parser.add_argument('--variant-field',
help='Name of the extra field that stores board '
'variant for component.')
parser.add_argument('--variants-whitelist', default='',
help='List of board variants to '
'include in the BOM. Use "<empty>" to denote '
'not set or empty value.')
parser.add_argument('--variants-blacklist', default='',
help='List of board variants to '
'exclude from the BOM. Use "<empty>" to denote '
'not set or empty value.')
parser.add_argument('--dnp-field', default=cls.dnp_field,
help='Name of the extra field that indicates '
'do not populate status. Components with '
'this field not empty will be excluded.')
def set_from_args(self, args):
# type: (argparse.Namespace) -> None
import math
# Html
self.dark_mode = args.dark_mode
self.show_pads = not args.hide_pads
self.show_fabrication = args.show_fabrication
self.show_silkscreen = not args.hide_silkscreen
self.highlight_pin1 = args.highlight_pin1
self.redraw_on_drag = not args.no_redraw_on_drag
self.board_rotation = math.fmod(args.board_rotation // 5, 37)
self.offset_back_rotation = args.offset_back_rotation
self.checkboxes = args.checkboxes
self.mark_when_checked = args.mark_when_checked
self.bom_view = args.bom_view
self.layer_view = args.layer_view
self.compression = not args.no_compression
self.open_browser = not args.no_browser
self.rainbow_mode = args.rainbow_mode
# General
self.bom_dest_dir = args.dest_dir
self.bom_name_format = args.name_format
self.component_sort_order = self._split(args.sort_order)
self.component_blacklist = self._split(args.blacklist)
self.blacklist_virtual = not args.no_blacklist_virtual
self.blacklist_empty_val = args.blacklist_empty_val
self.include_tracks = args.include_tracks
self.include_nets = args.include_nets
# Fields
self.extra_data_file = args.extra_data_file or args.netlist_file
if args.extra_fields is not None:
self.show_fields = self.default_show_group_fields + \
self._split(args.extra_fields)
self.group_fields = self.show_fields
else:
self.show_fields = self._split(args.show_fields)
self.group_fields = self._split(args.group_fields)
self.normalize_field_case = args.normalize_field_case
self.board_variant_field = args.variant_field
self.board_variant_whitelist = self._split(args.variants_whitelist)
self.board_variant_blacklist = self._split(args.variants_blacklist)
self.dnp_field = args.dnp_field
def get_html_config(self):
import json
d = {f: getattr(self, f) for f in self.html_config_fields}
d["fields"] = self.show_fields
return json.dumps(d)

View File

@ -0,0 +1,52 @@
from .newstroke_font import NEWSTROKE_FONT
class FontParser:
STROKE_FONT_SCALE = 1.0 / 21.0
FONT_OFFSET = -10
def __init__(self):
self.parsed_font = {}
def parse_font_char(self, chr):
lines = []
line = []
glyph_x = 0
index = ord(chr) - ord(' ')
if index >= len(NEWSTROKE_FONT):
index = ord('?') - ord(' ')
glyph_str = NEWSTROKE_FONT[index]
for i in range(0, len(glyph_str), 2):
coord = glyph_str[i:i + 2]
# The first two values contain the width of the char
if i < 2:
glyph_x = (ord(coord[0]) - ord('R')) * self.STROKE_FONT_SCALE
glyph_width = (ord(coord[1]) - ord(coord[0])) * self.STROKE_FONT_SCALE
elif coord[0] == ' ' and coord[1] == 'R':
lines.append(line)
line = []
else:
line.append([
(ord(coord[0]) - ord('R')) * self.STROKE_FONT_SCALE - glyph_x,
(ord(coord[1]) - ord('R') + self.FONT_OFFSET) * self.STROKE_FONT_SCALE
])
if len(line) > 0:
lines.append(line)
return {
'w': glyph_width,
'l': lines
}
def parse_font_for_string(self, s):
for c in s:
if c == '\t' and ' ' not in self.parsed_font:
# tabs rely on space char to calculate offset
self.parsed_font[' '] = self.parse_font_char(' ')
if c not in self.parsed_font and ord(c) >= ord(' '):
self.parsed_font[c] = self.parse_font_char(c)
def get_parsed_font(self):
return self.parsed_font

View File

@ -0,0 +1,365 @@
from __future__ import absolute_import
import io
import json
import logging
import os
import re
import sys
from datetime import datetime
import wx
from . import units
from .config import Config
from ..dialog import SettingsDialog
from ..ecad.common import EcadParser, Component
from ..errors import ParsingException
class Logger(object):
def __init__(self, cli=False):
self.cli = cli
self.logger = logging.getLogger('InteractiveHtmlBom')
self.logger.setLevel(logging.INFO)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)-15s %(levelname)s %(message)s")
ch.setFormatter(formatter)
self.logger.addHandler(ch)
def info(self, *args):
if self.cli:
self.logger.info(*args)
def error(self, msg):
if self.cli:
self.logger.error(msg)
else:
wx.MessageBox(msg)
def warn(self, msg):
if self.cli:
self.logger.warning(msg)
else:
wx.LogWarning(msg)
log = None
def skip_component(m, config):
# type: (Component, Config) -> bool
# skip blacklisted components
ref_prefix = re.findall('^[A-Z]*', m.ref)[0]
if m.ref in config.component_blacklist:
return True
if ref_prefix + '*' in config.component_blacklist:
return True
if config.blacklist_empty_val and m.val in ['', '~']:
return True
# skip virtual components if needed
if config.blacklist_virtual and m.attr == 'Virtual':
return True
# skip components with dnp field not empty
if config.dnp_field \
and config.dnp_field in m.extra_fields \
and m.extra_fields[config.dnp_field]:
return True
# skip components with wrong variant field
empty_str = '<empty>'
if config.board_variant_field and config.board_variant_whitelist:
ref_variant = m.extra_fields.get(config.board_variant_field, '')
if ref_variant == '':
ref_variant = empty_str
if ref_variant not in config.board_variant_whitelist:
return True
if config.board_variant_field and config.board_variant_blacklist:
ref_variant = m.extra_fields.get(config.board_variant_field, '')
if ref_variant == '':
ref_variant = empty_str
if ref_variant != empty_str and ref_variant in config.board_variant_blacklist:
return True
return False
def generate_bom(pcb_footprints, config):
# type: (list, Config) -> dict
"""
Generate BOM from pcb layout.
:param pcb_footprints: list of footprints on the pcb
:param config: Config object
:return: dict of BOM tables (qty, value, footprint, refs)
and dnp components
"""
def convert(text):
return int(text) if text.isdigit() else text.lower()
def alphanum_key(key):
return [convert(c)
for c in re.split('([0-9]+)', key)]
def natural_sort(lst):
"""
Natural sort for strings containing numbers
"""
return sorted(lst, key=lambda r: (alphanum_key(r[0]), r[1]))
# build grouped part list
skipped_components = []
part_groups = {}
group_by = set(config.group_fields)
index_to_fields = {}
for i, f in enumerate(pcb_footprints):
if skip_component(f, config):
skipped_components.append(i)
continue
# group part refs by value and footprint
fields = []
group_key = []
for field in config.show_fields:
if field == "Value":
fields.append(f.val)
if "Value" in group_by:
norm_value, unit = units.componentValue(f.val, f.ref)
group_key.append(norm_value)
group_key.append(unit)
elif field == "Footprint":
fields.append(f.footprint)
if "Footprint" in group_by:
group_key.append(f.footprint)
group_key.append(f.attr)
else:
field_key = field
if config.normalize_field_case:
field_key = field.lower()
fields.append(f.extra_fields.get(field_key, ''))
if field in group_by:
group_key.append(f.extra_fields.get(field_key, ''))
index_to_fields[i] = fields
refs = part_groups.setdefault(tuple(group_key), [])
refs.append((f.ref, i))
bom_table = []
# If some extra fields are just integers then convert the whole column
# so that sorting will work naturally
for i, field in enumerate(config.show_fields):
if field in ["Value", "Footprint"]:
continue
all_num = True
for f in index_to_fields.values():
if not f[i].isdigit() and len(f[i].strip()) > 0:
all_num = False
break
if all_num:
for f in index_to_fields.values():
if f[i].isdigit():
f[i] = int(f[i])
for _, refs in part_groups.items():
# Fixup values to normalized string
if "Value" in group_by and "Value" in config.show_fields:
index = config.show_fields.index("Value")
value = index_to_fields[refs[0][1]][index]
for ref in refs:
index_to_fields[ref[1]][index] = value
bom_table.append(natural_sort(refs))
# sort table by reference prefix and quantity
def row_sort_key(element):
prefix = re.findall('^[^0-9]*', element[0][0])[0]
if prefix in config.component_sort_order:
ref_ord = config.component_sort_order.index(prefix)
else:
ref_ord = config.component_sort_order.index('~')
return ref_ord, -len(element), alphanum_key(element[0][0])
if '~' not in config.component_sort_order:
config.component_sort_order.append('~')
bom_table = sorted(bom_table, key=row_sort_key)
result = {
'both': bom_table,
'skipped': skipped_components,
'fields': index_to_fields
}
for layer in ['F', 'B']:
filtered_table = []
for row in bom_table:
filtered_refs = [ref for ref in row
if pcb_footprints[ref[1]].layer == layer]
if filtered_refs:
filtered_table.append(filtered_refs)
result[layer] = sorted(filtered_table, key=row_sort_key)
return result
def open_file(filename):
import subprocess
try:
if sys.platform.startswith('win'):
os.startfile(filename)
elif sys.platform.startswith('darwin'):
subprocess.call(('open', filename))
elif sys.platform.startswith('linux'):
subprocess.call(('xdg-open', filename))
except Exception as e:
log.warn('Failed to open browser: {}'.format(e))
def process_substitutions(bom_name_format, pcb_file_name, metadata):
# type: (str, str, dict)->str
name = bom_name_format.replace('%f', os.path.splitext(pcb_file_name)[0])
name = name.replace('%p', metadata['title'])
name = name.replace('%c', metadata['company'])
name = name.replace('%r', metadata['revision'])
name = name.replace('%d', metadata['date'].replace(':', '-'))
now = datetime.now()
name = name.replace('%D', now.strftime('%Y-%m-%d'))
name = name.replace('%T', now.strftime('%H-%M-%S'))
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
return name + '.html'
def round_floats(o, precision):
if isinstance(o, float):
return round(o, precision)
if isinstance(o, dict):
return {k: round_floats(v, precision) for k, v in o.items()}
if isinstance(o, (list, tuple)):
return [round_floats(x, precision) for x in o]
return o
def get_pcbdata_javascript(pcbdata, compression):
from .lzstring import LZString
js = "var pcbdata = {}"
pcbdata_str = json.dumps(round_floats(pcbdata, 6))
if compression:
log.info("Compressing pcb data")
pcbdata_str = json.dumps(LZString().compress_to_base64(pcbdata_str))
js = "var pcbdata = JSON.parse(LZString.decompressFromBase64({}))"
return js.format(pcbdata_str)
def generate_file(pcb_file_dir, pcb_file_name, pcbdata, config):
def get_file_content(file_name):
path = os.path.join(os.path.dirname(__file__), "..", "web", file_name)
if not os.path.exists(path):
return ""
with io.open(path, 'r', encoding='utf-8') as f:
return f.read()
if os.path.isabs(config.bom_dest_dir):
bom_file_dir = config.bom_dest_dir
else:
bom_file_dir = os.path.join(pcb_file_dir, config.bom_dest_dir)
bom_file_name = process_substitutions(
config.bom_name_format, pcb_file_name, pcbdata['metadata'])
bom_file_name = os.path.join(bom_file_dir, bom_file_name)
bom_file_dir = os.path.dirname(bom_file_name)
if not os.path.isdir(bom_file_dir):
os.makedirs(bom_file_dir)
pcbdata_js = get_pcbdata_javascript(pcbdata, config.compression)
log.info("Dumping pcb data")
config_js = "var config = " + config.get_html_config()
html = get_file_content("ibom.html")
html = html.replace('///CSS///', get_file_content('ibom.css'))
html = html.replace('///USERCSS///', get_file_content('user.css'))
html = html.replace('///SPLITJS///', get_file_content('split.js'))
html = html.replace('///LZ-STRING///',
get_file_content('lz-string.js')
if config.compression else '')
html = html.replace('///POINTER_EVENTS_POLYFILL///',
get_file_content('pep.js'))
html = html.replace('///CONFIG///', config_js)
html = html.replace('///UTILJS///', get_file_content('util.js'))
html = html.replace('///RENDERJS///', get_file_content('render.js'))
html = html.replace('///TABLEUTILJS///', get_file_content('table-util.js'))
html = html.replace('///IBOMJS///', get_file_content('ibom.js'))
html = html.replace('///USERJS///', get_file_content('user.js'))
html = html.replace('///USERHEADER///',
get_file_content('userheader.html'))
html = html.replace('///USERFOOTER///',
get_file_content('userfooter.html'))
# Replace pcbdata last for better performance.
html = html.replace('///PCBDATA///', pcbdata_js)
with io.open(bom_file_name, 'wt', encoding='utf-8') as bom:
bom.write(html)
log.info("Created file %s", bom_file_name)
return bom_file_name
def main(parser, config, logger):
# type: (EcadParser, Config, Logger) -> None
global log
log = logger
pcb_file_name = os.path.basename(parser.file_name)
pcb_file_dir = os.path.dirname(parser.file_name)
pcbdata, components = parser.parse()
if not pcbdata and not components:
raise ParsingException('Parsing failed.')
pcbdata["bom"] = generate_bom(components, config)
pcbdata["ibom_version"] = config.version
# build BOM
bom_file = generate_file(pcb_file_dir, pcb_file_name, pcbdata, config)
if config.open_browser:
logger.info("Opening file in browser")
open_file(bom_file)
def run_with_dialog(parser, config, logger):
# type: (EcadParser, Config, Logger) -> None
def save_config(dialog_panel, locally=False):
config.set_from_dialog(dialog_panel)
config.save(locally)
config.load_from_ini()
dlg = SettingsDialog(extra_data_func=parser.parse_extra_data,
extra_data_wildcard=parser.extra_data_file_filter(),
config_save_func=save_config,
file_name_format_hint=config.FILE_NAME_FORMAT_HINT,
version=config.version)
try:
config.netlist_initial_directory = os.path.dirname(parser.file_name)
extra_data_file = parser.latest_extra_data(
extra_dirs=[config.bom_dest_dir])
if extra_data_file is not None:
dlg.set_extra_data_path(extra_data_file)
config.transfer_to_dialog(dlg.panel)
if dlg.ShowModal() == wx.ID_OK:
config.set_from_dialog(dlg.panel)
main(parser, config, logger)
finally:
dlg.Destroy()

View File

@ -0,0 +1,304 @@
"""
Copyright 2014 Eduard Tomasek
This work is free. You can redistribute it and/or modify it under the
terms of the Do What The Fuck You Want To Public License, Version 2,
as published by Sam Hocevar. See the COPYING file for more details.
"""
import sys
if sys.version_info[0] == 3:
unichr = chr
class LZString:
def __init__(self):
self.keyStr = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
)
@staticmethod
def compress(uncompressed):
if uncompressed is None:
return ''
context_dictionary = {}
context_dictionary_to_create = {}
context_w = ''
context_enlarge_in = 2
context_dict_size = 3
context_num_bits = 2
context_data_string = ''
context_data_val = 0
context_data_position = 0
uncompressed = uncompressed
for ii in range(len(uncompressed)):
context_c = uncompressed[ii]
if context_c not in context_dictionary:
context_dictionary[context_c] = context_dict_size
context_dict_size += 1
context_dictionary_to_create[context_c] = True
context_wc = context_w + context_c
if context_wc in context_dictionary:
context_w = context_wc
else:
if context_w in context_dictionary_to_create:
if ord(context_w[0]) < 256:
for _ in range(context_num_bits):
context_data_val = (context_data_val << 1)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = ord(context_w[0])
for i in range(8):
context_data_val = (
(context_data_val << 1) | (value & 1)
)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
else:
value = 1
for i in range(context_num_bits):
context_data_val = (context_data_val << 1) | value
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = 0
value = ord(context_w[0])
for i in range(16):
context_data_val = (
(context_data_val << 1) | (value & 1)
)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlarge_in -= 1
if context_enlarge_in == 0:
context_enlarge_in = pow(2, context_num_bits)
context_num_bits += 1
context_dictionary_to_create.pop(context_w, None)
# del context_dictionary_to_create[context_w]
else:
value = context_dictionary[context_w]
for i in range(context_num_bits):
context_data_val = (
(context_data_val << 1) | (value & 1)
)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlarge_in -= 1
if context_enlarge_in == 0:
context_enlarge_in = pow(2, context_num_bits)
context_num_bits += 1
context_dictionary[context_wc] = context_dict_size
context_dict_size += 1
context_w = context_c
if context_w != '':
if context_w in context_dictionary_to_create:
if ord(context_w[0]) < 256:
for i in range(context_num_bits):
context_data_val = (context_data_val << 1)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = ord(context_w[0])
for i in range(8):
context_data_val = (
(context_data_val << 1) | (value & 1)
)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
else:
value = 1
for i in range(context_num_bits):
context_data_val = (context_data_val << 1) | value
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = 0
value = ord(context_w[0])
for i in range(16):
context_data_val = (
(context_data_val << 1) | (value & 1)
)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlarge_in -= 1
if context_enlarge_in == 0:
context_enlarge_in = pow(2, context_num_bits)
context_num_bits += 1
context_dictionary_to_create.pop(context_w, None)
# del context_dictionary_to_create[context_w]
else:
value = context_dictionary[context_w]
for i in range(context_num_bits):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_enlarge_in -= 1
if context_enlarge_in == 0:
context_num_bits += 1
value = 2
for i in range(context_num_bits):
context_data_val = (context_data_val << 1) | (value & 1)
if context_data_position == 15:
context_data_position = 0
context_data_string += unichr(context_data_val)
context_data_val = 0
else:
context_data_position += 1
value = value >> 1
context_data_val = (context_data_val << 1)
while context_data_position != 15:
context_data_position += 1
context_data_val = (context_data_val << 1)
context_data_string += unichr(context_data_val)
return context_data_string
def compress_to_base64(self, string):
if string is None:
return ''
output = ''
string = self.compress(string)
str_len = len(string)
for i in range(0, str_len * 2, 3):
if (i % 2) == 0:
chr1 = ord(string[i // 2]) >> 8
chr2 = ord(string[i // 2]) & 255
if (i / 2) + 1 < str_len:
chr3 = ord(string[(i // 2) + 1]) >> 8
else:
chr3 = None
else:
chr1 = ord(string[(i - 1) // 2]) & 255
if (i + 1) / 2 < str_len:
chr2 = ord(string[(i + 1) // 2]) >> 8
chr3 = ord(string[(i + 1) // 2]) & 255
else:
chr2 = None
chr3 = None
# python dont support bit operation with NaN like javascript
enc1 = chr1 >> 2
enc2 = (
((chr1 & 3) << 4) |
(chr2 >> 4 if chr2 is not None else 0)
)
enc3 = (
((chr2 & 15 if chr2 is not None else 0) << 2) |
(chr3 >> 6 if chr3 is not None else 0)
)
enc4 = (chr3 if chr3 is not None else 0) & 63
if chr2 is None:
enc3 = 64
enc4 = 64
elif chr3 is None:
enc4 = 64
output += (
self.keyStr[enc1] +
self.keyStr[enc2] +
self.keyStr[enc3] +
self.keyStr[enc4]
)
return output

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,192 @@
# _*_ coding:utf-8 _*_
# Stolen from
# https://github.com/SchrodingersGat/KiBoM/blob/master/KiBOM/units.py
"""
This file contains a set of functions for matching values which may be written
in different formats e.g.
0.1uF = 100n (different suffix specified, one has missing unit)
0R1 = 0.1Ohm (Unit replaces decimal, different units)
"""
import re
import locale
current_locale = locale.setlocale(locale.LC_NUMERIC)
try:
locale.setlocale(locale.LC_NUMERIC, '')
except Exception:
# sometimes setlocale with empty string doesn't work on OSX
pass
decimal_separator = locale.localeconv()['decimal_point']
locale.setlocale(locale.LC_NUMERIC, current_locale)
PREFIX_MICRO = [u"μ", u"µ", "u", "micro"] # first is \u03BC second is \u00B5
PREFIX_MILLI = ["milli", "m"]
PREFIX_NANO = ["nano", "n"]
PREFIX_PICO = ["pico", "p"]
PREFIX_KILO = ["kilo", "k"]
PREFIX_MEGA = ["mega", "meg"]
PREFIX_GIGA = ["giga", "g"]
# All prefices
PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + \
PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA
# Common methods of expressing component units
UNIT_R = ["r", "ohms", "ohm", u"Ω", u"ω"]
UNIT_C = ["farad", "f"]
UNIT_L = ["henry", "h"]
UNIT_ALL = UNIT_R + UNIT_C + UNIT_L
VALUE_REGEX = re.compile(
"^([0-9\\.]+)(" + "|".join(PREFIX_ALL) + ")*(" + "|".join(
UNIT_ALL) + ")*(\\d*)$")
REFERENCE_REGEX = re.compile("^(r|rv|c|l)(\\d+)$")
def getUnit(unit):
"""
Return a simplified version of a units string, for comparison purposes
"""
if not unit:
return None
unit = unit.lower()
if unit in UNIT_R:
return "R"
if unit in UNIT_C:
return "F"
if unit in UNIT_L:
return "H"
return None
def getPrefix(prefix):
"""
Return the (numerical) value of a given prefix
"""
if not prefix:
return 1
prefix = prefix.lower()
if prefix in PREFIX_PICO:
return 1.0e-12
if prefix in PREFIX_NANO:
return 1.0e-9
if prefix in PREFIX_MICRO:
return 1.0e-6
if prefix in PREFIX_MILLI:
return 1.0e-3
if prefix in PREFIX_KILO:
return 1.0e3
if prefix in PREFIX_MEGA:
return 1.0e6
if prefix in PREFIX_GIGA:
return 1.0e9
return 1
def compMatch(component):
"""
Return a normalized value and units for a given component value string
e.g. compMatch("10R2") returns (1000, R)
e.g. compMatch("3.3mOhm") returns (0.0033, R)
"""
component = component.strip().lower()
if decimal_separator == ',':
# replace separator with dot
component = component.replace(",", ".")
else:
# remove thousands separator
component = component.replace(",", "")
result = VALUE_REGEX.match(component)
if not result:
return None
if not len(result.groups()) == 4:
return None
value, prefix, units, post = result.groups()
# special case where units is in the middle of the string
# e.g. "0R05" for 0.05Ohm
# in this case, we will NOT have a decimal
# we will also have a trailing number
if post and "." not in value:
try:
value = float(int(value))
postValue = float(int(post)) / (10 ** len(post))
value = value * 1.0 + postValue
except ValueError:
return None
try:
val = float(value)
except ValueError:
return None
val = "{0:.15f}".format(val * 1.0 * getPrefix(prefix))
return (val, getUnit(units))
def componentValue(valString, reference):
# type: (str, str) -> tuple
result = compMatch(valString)
if not result:
return valString, None # return the same string back with `None` unit
if not len(result) == 2: # result length is incorrect
return valString, None # return the same string back with `None` unit
if result[1] is None:
# try to infer unit from reference
match = REFERENCE_REGEX.match(reference.lower())
if match and len(match.groups()) == 2:
prefix, _ = match.groups()
unit = None
if prefix in ['r', 'rv']:
unit = 'R'
if prefix == 'c':
unit = 'F'
if prefix == 'l':
unit = 'H'
result = (result[0], unit)
return result # (val,unit)
def compareValues(c1, c2):
r1 = compMatch(c1)
r2 = compMatch(c2)
if not r1 or not r2:
return False
(v1, u1) = r1
(v2, u2) = r2
if v1 == v2:
# values match
if u1 == u2:
return True # units match
if not u1:
return True # no units for component 1
if not u2:
return True # no units for component 2
return False

View File

@ -0,0 +1 @@
from .settings_dialog import SettingsDialog, GeneralSettingsPanel

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

View File

@ -0,0 +1,581 @@
# InteractiveHtmlBom/dialog/dialog_base.py
# -*- coding: utf-8 -*-
###########################################################################
## Python code generated with wxFormBuilder (version 3.10.1-0-g8feb16b3)
## http://www.wxformbuilder.org/
##
## PLEASE DO *NOT* EDIT THIS FILE!
###########################################################################
import wx
import wx.xrc
import wx.grid
###########################################################################
## Class SettingsDialogBase
###########################################################################
class SettingsDialogBase ( wx.Dialog ):
def __init__( self, parent ):
wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"InteractiveHtmlBom", pos = wx.DefaultPosition, size = wx.Size( 463,497 ), style = wx.DEFAULT_DIALOG_STYLE|wx.STAY_ON_TOP|wx.BORDER_DEFAULT )
self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
self.Centre( wx.BOTH )
def __del__( self ):
pass
###########################################################################
## Class SettingsDialogPanel
###########################################################################
class SettingsDialogPanel ( wx.Panel ):
def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( 400,300 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ):
wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name )
bSizer20 = wx.BoxSizer( wx.VERTICAL )
self.notebook = wx.Notebook( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.NB_TOP|wx.BORDER_DEFAULT )
bSizer20.Add( self.notebook, 1, wx.EXPAND |wx.ALL, 5 )
bSizer39 = wx.BoxSizer( wx.HORIZONTAL )
self.saveSettingsBtn = wx.Button( self, wx.ID_ANY, u"Save current settings...", wx.DefaultPosition, wx.DefaultSize, 0|wx.BORDER_DEFAULT )
bSizer39.Add( self.saveSettingsBtn, 0, wx.ALL, 5 )
bSizer39.Add( ( 50, 0), 1, wx.EXPAND, 5 )
self.generateBomBtn = wx.Button( self, wx.ID_ANY, u"Generate BOM", wx.DefaultPosition, wx.DefaultSize, 0|wx.BORDER_DEFAULT )
self.generateBomBtn.SetDefault()
bSizer39.Add( self.generateBomBtn, 0, wx.ALL, 5 )
self.cancelBtn = wx.Button( self, wx.ID_CANCEL, u"Cancel", wx.DefaultPosition, wx.DefaultSize, 0|wx.BORDER_DEFAULT )
bSizer39.Add( self.cancelBtn, 0, wx.ALL, 5 )
bSizer20.Add( bSizer39, 0, wx.EXPAND, 5 )
self.SetSizer( bSizer20 )
self.Layout()
# Connect Events
self.saveSettingsBtn.Bind( wx.EVT_BUTTON, self.OnSave )
self.generateBomBtn.Bind( wx.EVT_BUTTON, self.OnGenerateBom )
self.cancelBtn.Bind( wx.EVT_BUTTON, self.OnExit )
def __del__( self ):
pass
# Virtual event handlers, override them in your derived class
def OnSave( self, event ):
event.Skip()
def OnGenerateBom( self, event ):
event.Skip()
def OnExit( self, event ):
event.Skip()
###########################################################################
## Class HtmlSettingsPanelBase
###########################################################################
class HtmlSettingsPanelBase ( wx.Panel ):
def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( -1,-1 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ):
wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name )
b_sizer = wx.BoxSizer( wx.VERTICAL )
self.darkModeCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Dark mode", wx.DefaultPosition, wx.DefaultSize, 0 )
b_sizer.Add( self.darkModeCheckbox, 0, wx.ALL, 5 )
self.showPadsCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Show footprint pads", wx.DefaultPosition, wx.DefaultSize, 0 )
self.showPadsCheckbox.SetValue(True)
b_sizer.Add( self.showPadsCheckbox, 0, wx.ALL, 5 )
self.showFabricationCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Show fabrication layer", wx.DefaultPosition, wx.DefaultSize, 0 )
b_sizer.Add( self.showFabricationCheckbox, 0, wx.ALL, 5 )
self.showSilkscreenCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Show silkscreen", wx.DefaultPosition, wx.DefaultSize, 0 )
self.showSilkscreenCheckbox.SetValue(True)
b_sizer.Add( self.showSilkscreenCheckbox, 0, wx.ALL, 5 )
self.continuousRedrawCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Continuous redraw on drag", wx.DefaultPosition, wx.DefaultSize, 0 )
self.continuousRedrawCheckbox.SetValue(True)
b_sizer.Add( self.continuousRedrawCheckbox, 0, wx.ALL, 5 )
highlightPin1Choices = [ u"None", u"All", u"Selected" ]
self.highlightPin1 = wx.RadioBox( self, wx.ID_ANY, u"Highlight first pin", wx.DefaultPosition, wx.DefaultSize, highlightPin1Choices, 3, wx.RA_SPECIFY_COLS )
self.highlightPin1.SetSelection( 0 )
b_sizer.Add( self.highlightPin1, 0, wx.ALL|wx.EXPAND, 5 )
bSizer18 = wx.BoxSizer( wx.VERTICAL )
bSizer19 = wx.BoxSizer( wx.HORIZONTAL )
self.m_boardRotationLabel = wx.StaticText( self, wx.ID_ANY, u"Board rotation", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_boardRotationLabel.Wrap( -1 )
bSizer19.Add( self.m_boardRotationLabel, 0, wx.ALL, 5 )
bSizer19.Add( ( 0, 0), 1, wx.EXPAND, 5 )
self.rotationDegreeLabel = wx.StaticText( self, wx.ID_ANY, u"0", wx.DefaultPosition, wx.Size( 30,-1 ), wx.ALIGN_RIGHT|wx.ST_NO_AUTORESIZE )
self.rotationDegreeLabel.Wrap( -1 )
bSizer19.Add( self.rotationDegreeLabel, 0, wx.ALL, 5 )
bSizer19.Add( ( 8, 0), 0, 0, 5 )
bSizer18.Add( bSizer19, 1, wx.EXPAND, 5 )
self.boardRotationSlider = wx.Slider( self, wx.ID_ANY, 0, -36, 36, wx.DefaultPosition, wx.DefaultSize, wx.SL_HORIZONTAL )
bSizer18.Add( self.boardRotationSlider, 0, wx.ALL|wx.EXPAND, 5 )
b_sizer.Add( bSizer18, 0, wx.EXPAND, 5 )
self.offsetBackRotationCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Offset back rotation", wx.DefaultPosition, wx.DefaultSize, 0 )
b_sizer.Add( self.offsetBackRotationCheckbox, 0, wx.ALL, 5 )
sbSizer31 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Checkboxes" ), wx.HORIZONTAL )
self.bomCheckboxesCtrl = wx.TextCtrl( sbSizer31.GetStaticBox(), wx.ID_ANY, u"Sourced,Placed", wx.DefaultPosition, wx.DefaultSize, 0 )
sbSizer31.Add( self.bomCheckboxesCtrl, 1, wx.ALL, 5 )
b_sizer.Add( sbSizer31, 0, wx.ALL|wx.EXPAND, 5 )
bomDefaultViewChoices = [ u"BOM only", u"BOM left, drawings right", u"BOM top, drawings bottom" ]
self.bomDefaultView = wx.RadioBox( self, wx.ID_ANY, u"BOM View", wx.DefaultPosition, wx.DefaultSize, bomDefaultViewChoices, 1, wx.RA_SPECIFY_COLS )
self.bomDefaultView.SetSelection( 1 )
b_sizer.Add( self.bomDefaultView, 0, wx.ALL|wx.EXPAND, 5 )
layerDefaultViewChoices = [ u"Front only", u"Front and Back", u"Back only" ]
self.layerDefaultView = wx.RadioBox( self, wx.ID_ANY, u"Layer View", wx.DefaultPosition, wx.DefaultSize, layerDefaultViewChoices, 1, wx.RA_SPECIFY_COLS )
self.layerDefaultView.SetSelection( 1 )
b_sizer.Add( self.layerDefaultView, 0, wx.ALL|wx.EXPAND, 5 )
sbSizer10 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Miscellaneous" ), wx.VERTICAL )
self.compressionCheckbox = wx.CheckBox( sbSizer10.GetStaticBox(), wx.ID_ANY, u"Enable compression", wx.DefaultPosition, wx.DefaultSize, 0 )
self.compressionCheckbox.SetValue(True)
sbSizer10.Add( self.compressionCheckbox, 0, wx.ALL, 5 )
self.openBrowserCheckbox = wx.CheckBox( sbSizer10.GetStaticBox(), wx.ID_ANY, u"Open browser", wx.DefaultPosition, wx.DefaultSize, 0 )
self.openBrowserCheckbox.SetValue(True)
sbSizer10.Add( self.openBrowserCheckbox, 0, wx.ALL, 5 )
self.rainbowModeCheckbox = wx.CheckBox( sbSizer10.GetStaticBox(), wx.ID_ANY, u"Assign each group a unique color", wx.DefaultPosition, wx.DefaultSize, 0 )
sbSizer10.Add( self.rainbowModeCheckbox, 0, wx.ALL, 5 )
b_sizer.Add( sbSizer10, 1, wx.EXPAND|wx.ALL, 5 )
self.SetSizer( b_sizer )
self.Layout()
b_sizer.Fit( self )
# Connect Events
self.boardRotationSlider.Bind( wx.EVT_SLIDER, self.OnBoardRotationSlider )
def __del__( self ):
pass
# Virtual event handlers, override them in your derived class
def OnBoardRotationSlider( self, event ):
event.Skip()
###########################################################################
## Class GeneralSettingsPanelBase
###########################################################################
class GeneralSettingsPanelBase ( wx.Panel ):
def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( -1,-1 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ):
wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name )
bSizer32 = wx.BoxSizer( wx.VERTICAL )
sbSizer6 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Bom destination" ), wx.VERTICAL )
fgSizer1 = wx.FlexGridSizer( 0, 2, 0, 0 )
fgSizer1.AddGrowableCol( 1 )
fgSizer1.SetFlexibleDirection( wx.BOTH )
fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )
self.m_staticText8 = wx.StaticText( sbSizer6.GetStaticBox(), wx.ID_ANY, u"Directory", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText8.Wrap( -1 )
fgSizer1.Add( self.m_staticText8, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
self.bomDirPicker = wx.DirPickerCtrl( sbSizer6.GetStaticBox(), wx.ID_ANY, wx.EmptyString, u"Select bom folder", wx.DefaultPosition, wx.DefaultSize, wx.DIRP_SMALL|wx.DIRP_USE_TEXTCTRL|wx.BORDER_SIMPLE )
fgSizer1.Add( self.bomDirPicker, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL|wx.EXPAND, 5 )
self.m_staticText9 = wx.StaticText( sbSizer6.GetStaticBox(), wx.ID_ANY, u"Name format", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText9.Wrap( -1 )
fgSizer1.Add( self.m_staticText9, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
bSizer20 = wx.BoxSizer( wx.HORIZONTAL )
self.fileNameFormatTextControl = wx.TextCtrl( sbSizer6.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
bSizer20.Add( self.fileNameFormatTextControl, 1, wx.ALIGN_CENTER_VERTICAL|wx.BOTTOM|wx.LEFT|wx.TOP, 5 )
self.m_btnNameHint = wx.Button( sbSizer6.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnNameHint.SetMinSize( wx.Size( 30,30 ) )
bSizer20.Add( self.m_btnNameHint, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
fgSizer1.Add( bSizer20, 1, wx.EXPAND, 5 )
sbSizer6.Add( fgSizer1, 1, wx.EXPAND, 5 )
bSizer32.Add( sbSizer6, 0, wx.ALL|wx.EXPAND, 5 )
sbSizer9 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Additional pcb data" ), wx.HORIZONTAL )
self.includeTracksCheckbox = wx.CheckBox( sbSizer9.GetStaticBox(), wx.ID_ANY, u"Include tracks/zones", wx.DefaultPosition, wx.DefaultSize, 0 )
sbSizer9.Add( self.includeTracksCheckbox, 1, wx.ALL, 5 )
self.includeNetsCheckbox = wx.CheckBox( sbSizer9.GetStaticBox(), wx.ID_ANY, u"Include nets", wx.DefaultPosition, wx.DefaultSize, 0 )
sbSizer9.Add( self.includeNetsCheckbox, 1, wx.ALL, 5 )
bSizer32.Add( sbSizer9, 0, wx.ALL|wx.EXPAND, 5 )
sortingSizer = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Component sort order" ), wx.VERTICAL )
bSizer4 = wx.BoxSizer( wx.HORIZONTAL )
bSizer6 = wx.BoxSizer( wx.VERTICAL )
componentSortOrderBoxChoices = []
self.componentSortOrderBox = wx.ListBox( sortingSizer.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, componentSortOrderBoxChoices, wx.LB_SINGLE|wx.BORDER_SIMPLE )
bSizer6.Add( self.componentSortOrderBox, 1, wx.ALL|wx.EXPAND, 5 )
bSizer4.Add( bSizer6, 1, wx.EXPAND, 5 )
bSizer5 = wx.BoxSizer( wx.VERTICAL )
self.m_btnSortUp = wx.Button( sortingSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnSortUp.SetMinSize( wx.Size( 30,30 ) )
bSizer5.Add( self.m_btnSortUp, 0, wx.ALL, 5 )
self.m_btnSortDown = wx.Button( sortingSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnSortDown.SetMinSize( wx.Size( 30,30 ) )
bSizer5.Add( self.m_btnSortDown, 0, wx.ALL, 5 )
self.m_btnSortAdd = wx.Button( sortingSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnSortAdd.SetMinSize( wx.Size( 30,30 ) )
bSizer5.Add( self.m_btnSortAdd, 0, wx.ALL, 5 )
self.m_btnSortRemove = wx.Button( sortingSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnSortRemove.SetMinSize( wx.Size( 30,30 ) )
bSizer5.Add( self.m_btnSortRemove, 0, wx.ALL, 5 )
bSizer4.Add( bSizer5, 0, 0, 5 )
sortingSizer.Add( bSizer4, 1, wx.EXPAND, 5 )
bSizer32.Add( sortingSizer, 1, wx.ALL|wx.EXPAND, 5 )
blacklistSizer = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Component blacklist" ), wx.VERTICAL )
bSizer412 = wx.BoxSizer( wx.HORIZONTAL )
bSizer612 = wx.BoxSizer( wx.VERTICAL )
blacklistBoxChoices = []
self.blacklistBox = wx.ListBox( blacklistSizer.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, blacklistBoxChoices, wx.LB_SINGLE|wx.LB_SORT|wx.BORDER_SIMPLE )
bSizer612.Add( self.blacklistBox, 1, wx.ALL|wx.EXPAND, 5 )
bSizer412.Add( bSizer612, 1, wx.EXPAND, 5 )
bSizer512 = wx.BoxSizer( wx.VERTICAL )
self.m_btnBlacklistAdd = wx.Button( blacklistSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnBlacklistAdd.SetMinSize( wx.Size( 30,30 ) )
bSizer512.Add( self.m_btnBlacklistAdd, 0, wx.ALL, 5 )
self.m_btnBlacklistRemove = wx.Button( blacklistSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnBlacklistRemove.SetMinSize( wx.Size( 30,30 ) )
bSizer512.Add( self.m_btnBlacklistRemove, 0, wx.ALL, 5 )
bSizer412.Add( bSizer512, 0, 0, 5 )
blacklistSizer.Add( bSizer412, 1, wx.EXPAND, 5 )
self.m_staticText1 = wx.StaticText( blacklistSizer.GetStaticBox(), wx.ID_ANY, u"Globs are supported, e.g. MH*", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText1.Wrap( -1 )
blacklistSizer.Add( self.m_staticText1, 0, wx.ALL, 5 )
self.blacklistVirtualCheckbox = wx.CheckBox( blacklistSizer.GetStaticBox(), wx.ID_ANY, u"Blacklist virtual components", wx.DefaultPosition, wx.DefaultSize, 0 )
self.blacklistVirtualCheckbox.SetValue(True)
blacklistSizer.Add( self.blacklistVirtualCheckbox, 0, wx.ALL, 5 )
self.blacklistEmptyValCheckbox = wx.CheckBox( blacklistSizer.GetStaticBox(), wx.ID_ANY, u"Blacklist components with empty value", wx.DefaultPosition, wx.DefaultSize, 0 )
blacklistSizer.Add( self.blacklistEmptyValCheckbox, 0, wx.ALL, 5 )
bSizer32.Add( blacklistSizer, 1, wx.ALL|wx.EXPAND|wx.TOP, 5 )
self.SetSizer( bSizer32 )
self.Layout()
bSizer32.Fit( self )
# Connect Events
self.Bind( wx.EVT_SIZE, self.OnSize )
self.m_btnNameHint.Bind( wx.EVT_BUTTON, self.OnNameFormatHintClick )
self.m_btnSortUp.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderUp )
self.m_btnSortDown.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderDown )
self.m_btnSortAdd.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderAdd )
self.m_btnSortRemove.Bind( wx.EVT_BUTTON, self.OnComponentSortOrderRemove )
self.m_btnBlacklistAdd.Bind( wx.EVT_BUTTON, self.OnComponentBlacklistAdd )
self.m_btnBlacklistRemove.Bind( wx.EVT_BUTTON, self.OnComponentBlacklistRemove )
def __del__( self ):
pass
# Virtual event handlers, override them in your derived class
def OnSize( self, event ):
event.Skip()
def OnNameFormatHintClick( self, event ):
event.Skip()
def OnComponentSortOrderUp( self, event ):
event.Skip()
def OnComponentSortOrderDown( self, event ):
event.Skip()
def OnComponentSortOrderAdd( self, event ):
event.Skip()
def OnComponentSortOrderRemove( self, event ):
event.Skip()
def OnComponentBlacklistAdd( self, event ):
event.Skip()
def OnComponentBlacklistRemove( self, event ):
event.Skip()
###########################################################################
## Class FieldsPanelBase
###########################################################################
class FieldsPanelBase ( wx.Panel ):
def __init__( self, parent, id = wx.ID_ANY, pos = wx.DefaultPosition, size = wx.Size( -1,-1 ), style = wx.TAB_TRAVERSAL, name = wx.EmptyString ):
wx.Panel.__init__ ( self, parent, id = id, pos = pos, size = size, style = style, name = name )
bSizer42 = wx.BoxSizer( wx.VERTICAL )
sbSizer7 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Extra data file" ), wx.VERTICAL )
self.extraDataFilePicker = wx.FilePickerCtrl( sbSizer7.GetStaticBox(), wx.ID_ANY, wx.EmptyString, u"Select a file", u"Netlist and xml files (*.net; *.xml)|*.net;*.xml", wx.DefaultPosition, wx.DefaultSize, wx.FLP_DEFAULT_STYLE|wx.FLP_FILE_MUST_EXIST|wx.FLP_OPEN|wx.FLP_SMALL|wx.FLP_USE_TEXTCTRL|wx.BORDER_SIMPLE )
sbSizer7.Add( self.extraDataFilePicker, 0, wx.EXPAND|wx.BOTTOM|wx.RIGHT|wx.LEFT, 5 )
bSizer42.Add( sbSizer7, 0, wx.ALL|wx.EXPAND, 5 )
fieldsSizer = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Fields" ), wx.VERTICAL )
bSizer4 = wx.BoxSizer( wx.HORIZONTAL )
self.fieldsGrid = wx.grid.Grid( fieldsSizer.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
# Grid
self.fieldsGrid.CreateGrid( 2, 3 )
self.fieldsGrid.EnableEditing( True )
self.fieldsGrid.EnableGridLines( True )
self.fieldsGrid.EnableDragGridSize( False )
self.fieldsGrid.SetMargins( 0, 0 )
# Columns
self.fieldsGrid.AutoSizeColumns()
self.fieldsGrid.EnableDragColMove( False )
self.fieldsGrid.EnableDragColSize( True )
self.fieldsGrid.SetColLabelValue( 0, u"Show" )
self.fieldsGrid.SetColLabelValue( 1, u"Group" )
self.fieldsGrid.SetColLabelValue( 2, u"Name" )
self.fieldsGrid.SetColLabelSize( 30 )
self.fieldsGrid.SetColLabelAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER )
# Rows
self.fieldsGrid.EnableDragRowSize( False )
self.fieldsGrid.SetRowLabelSize( 0 )
self.fieldsGrid.SetRowLabelAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER )
# Label Appearance
# Cell Defaults
self.fieldsGrid.SetDefaultCellAlignment( wx.ALIGN_CENTER, wx.ALIGN_TOP )
self.fieldsGrid.SetMaxSize( wx.Size( -1,200 ) )
bSizer4.Add( self.fieldsGrid, 1, wx.ALL|wx.EXPAND, 5 )
bSizer5 = wx.BoxSizer( wx.VERTICAL )
self.m_btnUp = wx.Button( fieldsSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnUp.SetMinSize( wx.Size( 30,30 ) )
bSizer5.Add( self.m_btnUp, 0, wx.ALL, 5 )
self.m_btnDown = wx.Button( fieldsSizer.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.BU_EXACTFIT )
self.m_btnDown.SetMinSize( wx.Size( 30,30 ) )
bSizer5.Add( self.m_btnDown, 0, wx.ALL, 5 )
bSizer4.Add( bSizer5, 0, 0, 5 )
fieldsSizer.Add( bSizer4, 1, wx.EXPAND, 5 )
self.normalizeCaseCheckbox = wx.CheckBox( fieldsSizer.GetStaticBox(), wx.ID_ANY, u"Normalize field name case", wx.DefaultPosition, wx.DefaultSize, 0 )
fieldsSizer.Add( self.normalizeCaseCheckbox, 0, wx.ALL|wx.EXPAND, 5 )
bSizer42.Add( fieldsSizer, 2, wx.ALL|wx.EXPAND, 5 )
sbSizer32 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Board variant" ), wx.VERTICAL )
self.m_staticText5 = wx.StaticText( sbSizer32.GetStaticBox(), wx.ID_ANY, u"Board variant field name", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText5.Wrap( -1 )
sbSizer32.Add( self.m_staticText5, 0, wx.ALL, 5 )
boardVariantFieldBoxChoices = []
self.boardVariantFieldBox = wx.ComboBox( sbSizer32.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, boardVariantFieldBoxChoices, wx.CB_READONLY|wx.CB_SORT|wx.BORDER_SIMPLE )
sbSizer32.Add( self.boardVariantFieldBox, 0, wx.ALL|wx.EXPAND, 5 )
bSizer17 = wx.BoxSizer( wx.HORIZONTAL )
bSizer18 = wx.BoxSizer( wx.VERTICAL )
self.m_staticText6 = wx.StaticText( sbSizer32.GetStaticBox(), wx.ID_ANY, u"Whitelist", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText6.Wrap( -1 )
bSizer18.Add( self.m_staticText6, 0, wx.ALL, 5 )
boardVariantWhitelistChoices = []
self.boardVariantWhitelist = wx.CheckListBox( sbSizer32.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, boardVariantWhitelistChoices, 0|wx.BORDER_SIMPLE )
bSizer18.Add( self.boardVariantWhitelist, 1, wx.ALL|wx.EXPAND, 5 )
bSizer17.Add( bSizer18, 1, wx.EXPAND, 5 )
bSizer19 = wx.BoxSizer( wx.VERTICAL )
self.m_staticText7 = wx.StaticText( sbSizer32.GetStaticBox(), wx.ID_ANY, u"Blacklist", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText7.Wrap( -1 )
bSizer19.Add( self.m_staticText7, 0, wx.ALL, 5 )
boardVariantBlacklistChoices = []
self.boardVariantBlacklist = wx.CheckListBox( sbSizer32.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, boardVariantBlacklistChoices, 0|wx.BORDER_SIMPLE )
bSizer19.Add( self.boardVariantBlacklist, 1, wx.ALL|wx.EXPAND, 5 )
bSizer17.Add( bSizer19, 1, wx.EXPAND, 5 )
sbSizer32.Add( bSizer17, 1, wx.EXPAND, 5 )
bSizer42.Add( sbSizer32, 3, wx.ALL|wx.EXPAND, 5 )
sbSizer8 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"DNP field name" ), wx.VERTICAL )
self.m_staticText4 = wx.StaticText( sbSizer8.GetStaticBox(), wx.ID_ANY, u"Components with this field not empty will be ignored", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText4.Wrap( -1 )
sbSizer8.Add( self.m_staticText4, 0, wx.ALL, 5 )
dnpFieldBoxChoices = []
self.dnpFieldBox = wx.ComboBox( sbSizer8.GetStaticBox(), wx.ID_ANY, u"-None-", wx.DefaultPosition, wx.DefaultSize, dnpFieldBoxChoices, wx.CB_READONLY|wx.CB_SORT|wx.BORDER_NONE )
sbSizer8.Add( self.dnpFieldBox, 0, wx.ALL|wx.EXPAND, 5 )
bSizer42.Add( sbSizer8, 0, wx.ALL|wx.EXPAND, 5 )
self.SetSizer( bSizer42 )
self.Layout()
bSizer42.Fit( self )
# Connect Events
self.Bind( wx.EVT_SIZE, self.OnSize )
self.extraDataFilePicker.Bind( wx.EVT_FILEPICKER_CHANGED, self.OnExtraDataFileChanged )
self.fieldsGrid.Bind( wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.OnGridCellClicked )
self.m_btnUp.Bind( wx.EVT_BUTTON, self.OnFieldsUp )
self.m_btnDown.Bind( wx.EVT_BUTTON, self.OnFieldsDown )
self.normalizeCaseCheckbox.Bind( wx.EVT_CHECKBOX, self.OnExtraDataFileChanged )
self.boardVariantFieldBox.Bind( wx.EVT_COMBOBOX, self.OnBoardVariantFieldChange )
def __del__( self ):
pass
# Virtual event handlers, override them in your derived class
def OnSize( self, event ):
event.Skip()
def OnExtraDataFileChanged( self, event ):
event.Skip()
def OnGridCellClicked( self, event ):
event.Skip()
def OnFieldsUp( self, event ):
event.Skip()
def OnFieldsDown( self, event ):
event.Skip()
def OnBoardVariantFieldChange( self, event ):
event.Skip()

View File

@ -0,0 +1,406 @@
import os
import re
import wx
import wx.grid
from . import dialog_base
if hasattr(wx, "GetLibraryVersionInfo"):
WX_VERSION = wx.GetLibraryVersionInfo() # type: wx.VersionInfo
WX_VERSION = (WX_VERSION.Major, WX_VERSION.Minor, WX_VERSION.Micro)
else:
# old kicad used this (exact version doesnt matter)
WX_VERSION = (3, 0, 2)
def pop_error(msg):
wx.MessageBox(msg, 'Error', wx.OK | wx.ICON_ERROR)
def get_btn_bitmap(bitmap):
path = os.path.join(os.path.dirname(__file__), "bitmaps", bitmap)
png = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
if WX_VERSION >= (3, 1, 6):
return wx.BitmapBundle(png)
else:
return png
class SettingsDialog(dialog_base.SettingsDialogBase):
def __init__(self, extra_data_func, extra_data_wildcard, config_save_func,
file_name_format_hint, version):
dialog_base.SettingsDialogBase.__init__(self, None)
self.panel = SettingsDialogPanel(
self, extra_data_func, extra_data_wildcard, config_save_func,
file_name_format_hint)
best_size = self.panel.BestSize
# hack for some gtk themes that incorrectly calculate best size
best_size.IncBy(dx=0, dy=30)
self.SetClientSize(best_size)
self.SetTitle('InteractiveHtmlBom %s' % version)
# hack for new wxFormBuilder generating code incompatible with old wxPython
# noinspection PyMethodOverriding
def SetSizeHints(self, sz1, sz2):
try:
# wxPython 4
super(SettingsDialog, self).SetSizeHints(sz1, sz2)
except TypeError:
# wxPython 3
self.SetSizeHintsSz(sz1, sz2)
def set_extra_data_path(self, extra_data_file):
self.panel.fields.extraDataFilePicker.Path = extra_data_file
self.panel.fields.OnExtraDataFileChanged(None)
# Implementing settings_dialog
class SettingsDialogPanel(dialog_base.SettingsDialogPanel):
def __init__(self, parent, extra_data_func, extra_data_wildcard,
config_save_func, file_name_format_hint):
self.config_save_func = config_save_func
dialog_base.SettingsDialogPanel.__init__(self, parent)
self.general = GeneralSettingsPanel(self.notebook,
file_name_format_hint)
self.html = HtmlSettingsPanel(self.notebook)
self.fields = FieldsPanel(self.notebook, extra_data_func,
extra_data_wildcard)
self.notebook.AddPage(self.general, "General")
self.notebook.AddPage(self.html, "Html defaults")
self.notebook.AddPage(self.fields, "Fields")
self.save_menu = wx.Menu()
self.save_locally = self.save_menu.Append(
wx.ID_ANY, u"Locally", wx.EmptyString, wx.ITEM_NORMAL)
self.save_globally = self.save_menu.Append(
wx.ID_ANY, u"Globally", wx.EmptyString, wx.ITEM_NORMAL)
self.Bind(
wx.EVT_MENU, self.OnSaveLocally, id=self.save_locally.GetId())
self.Bind(
wx.EVT_MENU, self.OnSaveGlobally, id=self.save_globally.GetId())
def OnExit(self, event):
self.GetParent().EndModal(wx.ID_CANCEL)
def OnGenerateBom(self, event):
self.GetParent().EndModal(wx.ID_OK)
def finish_init(self):
self.html.OnBoardRotationSlider(None)
def OnSave(self, event):
# type: (wx.CommandEvent) -> None
pos = wx.Point(0, event.GetEventObject().GetSize().y)
self.saveSettingsBtn.PopupMenu(self.save_menu, pos)
def OnSaveGlobally(self, event):
self.config_save_func(self)
def OnSaveLocally(self, event):
self.config_save_func(self, locally=True)
# Implementing HtmlSettingsPanelBase
class HtmlSettingsPanel(dialog_base.HtmlSettingsPanelBase):
def __init__(self, parent):
dialog_base.HtmlSettingsPanelBase.__init__(self, parent)
# Handlers for HtmlSettingsPanelBase events.
def OnBoardRotationSlider(self, event):
degrees = self.boardRotationSlider.Value * 5
self.rotationDegreeLabel.LabelText = u"{}\u00B0".format(degrees)
# Implementing GeneralSettingsPanelBase
class GeneralSettingsPanel(dialog_base.GeneralSettingsPanelBase):
def __init__(self, parent, file_name_format_hint):
dialog_base.GeneralSettingsPanelBase.__init__(self, parent)
self.file_name_format_hint = file_name_format_hint
bmp_arrow_up = get_btn_bitmap("btn-arrow-up.png")
bmp_arrow_down = get_btn_bitmap("btn-arrow-down.png")
bmp_plus = get_btn_bitmap("btn-plus.png")
bmp_minus = get_btn_bitmap("btn-minus.png")
bmp_question = get_btn_bitmap("btn-question.png")
self.m_btnSortUp.SetBitmap(bmp_arrow_up)
self.m_btnSortDown.SetBitmap(bmp_arrow_down)
self.m_btnSortAdd.SetBitmap(bmp_plus)
self.m_btnSortRemove.SetBitmap(bmp_minus)
self.m_btnNameHint.SetBitmap(bmp_question)
self.m_btnBlacklistAdd.SetBitmap(bmp_plus)
self.m_btnBlacklistRemove.SetBitmap(bmp_minus)
self.Layout()
# Handlers for GeneralSettingsPanelBase events.
def OnComponentSortOrderUp(self, event):
selection = self.componentSortOrderBox.Selection
if selection != wx.NOT_FOUND and selection > 0:
item = self.componentSortOrderBox.GetString(selection)
self.componentSortOrderBox.Delete(selection)
self.componentSortOrderBox.Insert(item, selection - 1)
self.componentSortOrderBox.SetSelection(selection - 1)
def OnComponentSortOrderDown(self, event):
selection = self.componentSortOrderBox.Selection
size = self.componentSortOrderBox.Count
if selection != wx.NOT_FOUND and selection < size - 1:
item = self.componentSortOrderBox.GetString(selection)
self.componentSortOrderBox.Delete(selection)
self.componentSortOrderBox.Insert(item, selection + 1)
self.componentSortOrderBox.SetSelection(selection + 1)
def OnComponentSortOrderAdd(self, event):
item = wx.GetTextFromUser(
"Characters other than A-Z will be ignored.",
"Add sort order item")
item = re.sub('[^A-Z]', '', item.upper())
if item == '':
return
found = self.componentSortOrderBox.FindString(item)
if found != wx.NOT_FOUND:
self.componentSortOrderBox.SetSelection(found)
return
self.componentSortOrderBox.Append(item)
self.componentSortOrderBox.SetSelection(
self.componentSortOrderBox.Count - 1)
def OnComponentSortOrderRemove(self, event):
selection = self.componentSortOrderBox.Selection
if selection != wx.NOT_FOUND:
item = self.componentSortOrderBox.GetString(selection)
if item == '~':
pop_error("You can not delete '~' item")
return
self.componentSortOrderBox.Delete(selection)
if self.componentSortOrderBox.Count > 0:
self.componentSortOrderBox.SetSelection(max(selection - 1, 0))
def OnComponentBlacklistAdd(self, event):
item = wx.GetTextFromUser(
"Characters other than A-Z 0-9 and * will be ignored.",
"Add blacklist item")
item = re.sub('[^A-Z0-9*]', '', item.upper())
if item == '':
return
found = self.blacklistBox.FindString(item)
if found != wx.NOT_FOUND:
self.blacklistBox.SetSelection(found)
return
self.blacklistBox.Append(item)
self.blacklistBox.SetSelection(self.blacklistBox.Count - 1)
def OnComponentBlacklistRemove(self, event):
selection = self.blacklistBox.Selection
if selection != wx.NOT_FOUND:
self.blacklistBox.Delete(selection)
if self.blacklistBox.Count > 0:
self.blacklistBox.SetSelection(max(selection - 1, 0))
def OnNameFormatHintClick(self, event):
wx.MessageBox(self.file_name_format_hint, 'File name format help',
style=wx.ICON_NONE | wx.OK)
def OnSize(self, event):
# Trick the listCheckBox best size calculations
tmp = self.componentSortOrderBox.GetStrings()
self.componentSortOrderBox.SetItems([])
self.Layout()
self.componentSortOrderBox.SetItems(tmp)
# Implementing FieldsPanelBase
class FieldsPanel(dialog_base.FieldsPanelBase):
NONE_STRING = '<none>'
EMPTY_STRING = '<empty>'
FIELDS_GRID_COLUMNS = 3
def __init__(self, parent, extra_data_func, extra_data_wildcard):
dialog_base.FieldsPanelBase.__init__(self, parent)
self.show_fields = []
self.group_fields = []
self.extra_data_func = extra_data_func
self.extra_field_data = None
self.m_btnUp.SetBitmap(get_btn_bitmap("btn-arrow-up.png"))
self.m_btnDown.SetBitmap(get_btn_bitmap("btn-arrow-down.png"))
self.set_file_picker_wildcard(extra_data_wildcard)
self._setFieldsList([])
for i in range(2):
box = self.GetTextExtent(self.fieldsGrid.GetColLabelValue(i))
if hasattr(box, "x"):
width = box.x
else:
width = box[0]
width = int(width * 1.1 + 5)
self.fieldsGrid.SetColMinimalWidth(i, width)
self.fieldsGrid.SetColSize(i, width)
self.Layout()
def set_file_picker_wildcard(self, extra_data_wildcard):
if extra_data_wildcard is None:
self.extraDataFilePicker.Disable()
return
# wxFilePickerCtrl doesn't support changing wildcard at runtime
# so we have to replace it
picker_parent = self.extraDataFilePicker.GetParent()
new_picker = wx.FilePickerCtrl(
picker_parent, wx.ID_ANY, wx.EmptyString,
u"Select a file",
extra_data_wildcard,
wx.DefaultPosition, wx.DefaultSize,
(wx.FLP_DEFAULT_STYLE | wx.FLP_FILE_MUST_EXIST | wx.FLP_OPEN |
wx.FLP_SMALL | wx.FLP_USE_TEXTCTRL | wx.BORDER_SIMPLE))
self.GetSizer().Replace(self.extraDataFilePicker, new_picker,
recursive=True)
self.extraDataFilePicker.Destroy()
self.extraDataFilePicker = new_picker
self.extraDataFilePicker.Bind(
wx.EVT_FILEPICKER_CHANGED, self.OnExtraDataFileChanged)
self.Layout()
def _swapRows(self, a, b):
for i in range(self.FIELDS_GRID_COLUMNS):
va = self.fieldsGrid.GetCellValue(a, i)
vb = self.fieldsGrid.GetCellValue(b, i)
self.fieldsGrid.SetCellValue(a, i, vb)
self.fieldsGrid.SetCellValue(b, i, va)
# Handlers for FieldsPanelBase events.
def OnGridCellClicked(self, event):
self.fieldsGrid.ClearSelection()
self.fieldsGrid.SelectRow(event.Row)
if event.Col < 2:
# toggle checkbox
val = self.fieldsGrid.GetCellValue(event.Row, event.Col)
val = "" if val else "1"
self.fieldsGrid.SetCellValue(event.Row, event.Col, val)
# group shouldn't be enabled without show
if event.Col == 0 and val == "":
self.fieldsGrid.SetCellValue(event.Row, 1, val)
if event.Col == 1 and val == "1":
self.fieldsGrid.SetCellValue(event.Row, 0, val)
def OnFieldsUp(self, event):
selection = self.fieldsGrid.SelectedRows
if len(selection) == 1 and selection[0] > 0:
self._swapRows(selection[0], selection[0] - 1)
self.fieldsGrid.ClearSelection()
self.fieldsGrid.SelectRow(selection[0] - 1)
def OnFieldsDown(self, event):
selection = self.fieldsGrid.SelectedRows
size = self.fieldsGrid.NumberRows
if len(selection) == 1 and selection[0] < size - 1:
self._swapRows(selection[0], selection[0] + 1)
self.fieldsGrid.ClearSelection()
self.fieldsGrid.SelectRow(selection[0] + 1)
def _setFieldsList(self, fields):
if self.fieldsGrid.NumberRows:
self.fieldsGrid.DeleteRows(0, self.fieldsGrid.NumberRows)
self.fieldsGrid.AppendRows(len(fields))
row = 0
for f in fields:
self.fieldsGrid.SetCellValue(row, 0, "1")
self.fieldsGrid.SetCellValue(row, 1, "1")
self.fieldsGrid.SetCellRenderer(
row, 0, wx.grid.GridCellBoolRenderer())
self.fieldsGrid.SetCellRenderer(
row, 1, wx.grid.GridCellBoolRenderer())
self.fieldsGrid.SetCellValue(row, 2, f)
self.fieldsGrid.SetCellAlignment(
row, 2, wx.ALIGN_LEFT, wx.ALIGN_TOP)
self.fieldsGrid.SetReadOnly(row, 2)
row += 1
def OnExtraDataFileChanged(self, event):
extra_data_file = self.extraDataFilePicker.Path
if not os.path.isfile(extra_data_file):
return
self.extra_field_data = None
try:
self.extra_field_data = self.extra_data_func(
extra_data_file, self.normalizeCaseCheckbox.Value)
except Exception as e:
pop_error(
"Failed to parse file %s\n\n%s" % (extra_data_file, e))
self.extraDataFilePicker.Path = ''
if self.extra_field_data is not None:
field_list = list(self.extra_field_data.fields)
self._setFieldsList(["Value", "Footprint"] + field_list)
self.SetCheckedFields()
field_list.append(self.NONE_STRING)
self.boardVariantFieldBox.SetItems(field_list)
self.boardVariantFieldBox.SetStringSelection(self.NONE_STRING)
self.boardVariantWhitelist.Clear()
self.boardVariantBlacklist.Clear()
self.dnpFieldBox.SetItems(field_list)
self.dnpFieldBox.SetStringSelection(self.NONE_STRING)
def OnBoardVariantFieldChange(self, event):
selection = self.boardVariantFieldBox.Value
if not selection or selection == self.NONE_STRING \
or self.extra_field_data is None:
self.boardVariantWhitelist.Clear()
self.boardVariantBlacklist.Clear()
return
variant_set = set()
for _, field_dict in self.extra_field_data.fields_by_ref.items():
if selection in field_dict:
v = field_dict[selection]
if v == "":
v = self.EMPTY_STRING
variant_set.add(v)
self.boardVariantWhitelist.SetItems(list(variant_set))
self.boardVariantBlacklist.SetItems(list(variant_set))
def OnSize(self, event):
self.Layout()
g = self.fieldsGrid
g.SetColSize(
2, g.GetClientSize().x - g.GetColSize(0) - g.GetColSize(1) - 30)
def GetShowFields(self):
result = []
for row in range(self.fieldsGrid.NumberRows):
if self.fieldsGrid.GetCellValue(row, 0) == "1":
result.append(self.fieldsGrid.GetCellValue(row, 2))
return result
def GetGroupFields(self):
result = []
for row in range(self.fieldsGrid.NumberRows):
if self.fieldsGrid.GetCellValue(row, 1) == "1":
result.append(self.fieldsGrid.GetCellValue(row, 2))
return result
def SetCheckedFields(self, show=None, group=None):
self.show_fields = show or self.show_fields
self.group_fields = group or self.group_fields
self.group_fields = [
s for s in self.group_fields if s in self.show_fields
]
current = []
for row in range(self.fieldsGrid.NumberRows):
current.append(self.fieldsGrid.GetCellValue(row, 2))
new = [s for s in current if s not in self.show_fields]
self._setFieldsList(self.show_fields + new)
for row in range(self.fieldsGrid.NumberRows):
field = self.fieldsGrid.GetCellValue(row, 2)
self.fieldsGrid.SetCellValue(
row, 0, "1" if field in self.show_fields else "")
self.fieldsGrid.SetCellValue(
row, 1, "1" if field in self.group_fields else "")

View File

@ -0,0 +1,17 @@
import wx
from dialog.settings_dialog import SettingsDialog
class MyApp(wx.App):
def OnInit(self):
frame = SettingsDialog(lambda: None, None, lambda x: None, "Hi", 'test')
if frame.ShowModal() == wx.ID_OK:
print("Should generate bom")
frame.Destroy()
return True
app = MyApp()
app.MainLoop()
print("Done")

View File

@ -0,0 +1,41 @@
import os
def get_parser_by_extension(file_name, config, logger):
ext = os.path.splitext(file_name)[1]
if ext == '.kicad_pcb':
return get_kicad_parser(file_name, config, logger)
elif ext == '.json':
""".json file may be from EasyEDA or a generic json format"""
import io
import json
with io.open(file_name, 'r', encoding='utf-8') as f:
obj = json.load(f)
if 'pcbdata' in obj:
return get_generic_json_parser(file_name, config, logger)
else:
return get_easyeda_parser(file_name, config, logger)
elif ext in ['.fbrd', '.brd']:
return get_fusion_eagle_parser(file_name, config, logger)
else:
return None
def get_kicad_parser(file_name, config, logger, board=None):
from .kicad import PcbnewParser
return PcbnewParser(file_name, config, logger, board)
def get_easyeda_parser(file_name, config, logger):
from .easyeda import EasyEdaParser
return EasyEdaParser(file_name, config, logger)
def get_generic_json_parser(file_name, config, logger):
from .genericjson import GenericJsonParser
return GenericJsonParser(file_name, config, logger)
def get_fusion_eagle_parser(file_name, config, logger):
from .fusion_eagle import FusionEagleParser
return FusionEagleParser(file_name, config, logger)

View File

@ -0,0 +1,250 @@
import math
from .svgpath import parse_path
class ExtraFieldData(object):
def __init__(self, fields, fields_by_ref, fields_by_index=None):
self.fields = fields
self.fields_by_ref = fields_by_ref
self.fields_by_index = fields_by_index
class EcadParser(object):
def __init__(self, file_name, config, logger):
"""
:param file_name: path to file that should be parsed.
:param config: Config instance
:param logger: logging object.
"""
self.file_name = file_name
self.config = config
self.logger = logger
def parse(self):
"""
Abstract method that should be overridden in implementations.
Performs all the parsing and returns a tuple of
(pcbdata, components)
pcbdata is described in DATAFORMAT.md
components is list of Component objects
:return:
"""
pass
@staticmethod
def normalize_field_names(data):
# type: (ExtraFieldData) -> ExtraFieldData
def remap(ref_fields):
return {f.lower(): v for (f, v) in
sorted(ref_fields.items(), reverse=True) if v}
by_ref = {r: remap(d) for (r, d) in data.fields_by_ref.items()}
if data.fields_by_index:
by_index = {i: remap(d) for (i, d) in data.fields_by_index.items()}
else:
by_index = None
field_map = {f.lower(): f for f in sorted(data.fields, reverse=True)}
return ExtraFieldData(field_map.values(), by_ref, by_index)
def get_extra_field_data(self, file_name):
"""
Abstract method that may be overridden in implementations that support
extra field data.
:return: ExtraFieldData
"""
return ExtraFieldData([], {})
def parse_extra_data(self, file_name, normalize_case):
"""
Parses the file and returns extra field data.
:param file_name: path to file containing extra data
:param normalize_case: if true, normalize case so that
"mpn", "Mpn", "MPN" fields are combined
:return:
"""
data = self.get_extra_field_data(file_name)
if normalize_case:
data = self.normalize_field_names(data)
return ExtraFieldData(
sorted(data.fields), data.fields_by_ref, data.fields_by_index)
def latest_extra_data(self, extra_dirs=None):
"""
Abstract method that may be overridden in implementations that support
extra field data.
:param extra_dirs: List of extra directories to search.
:return: File name of most recent file with extra field data.
"""
return None
def extra_data_file_filter(self):
"""
Abstract method that may be overridden in implementations that support
extra field data.
:return: File open dialog filter string, eg:
"Netlist and xml files (*.net; *.xml)|*.net;*.xml"
"""
return None
def add_drawing_bounding_box(self, drawing, bbox):
# type: (dict, BoundingBox) -> None
def add_segment():
bbox.add_segment(drawing['start'][0], drawing['start'][1],
drawing['end'][0], drawing['end'][1],
drawing['width'] / 2)
def add_circle():
bbox.add_circle(drawing['start'][0], drawing['start'][1],
drawing['radius'] + drawing['width'] / 2)
def add_svgpath():
width = drawing.get('width', 0)
bbox.add_svgpath(drawing['svgpath'], width, self.logger)
def add_polygon():
if 'polygons' not in drawing:
add_svgpath()
return
polygon = drawing['polygons'][0]
for point in polygon:
bbox.add_point(point[0], point[1])
def add_arc():
if 'svgpath' in drawing:
add_svgpath()
else:
width = drawing.get('width', 0)
xc, yc = drawing['start'][:2]
a1 = drawing['startangle']
a2 = drawing['endangle']
r = drawing['radius']
x1 = xc + r * math.cos(math.radians(a1))
y1 = yc + r * math.sin(math.radians(a1))
x2 = xc + r * math.cos(math.radians(a2))
y2 = yc + r * math.sin(math.radians(a2))
da = a2 - a1 if a2 > a1 else a2 + 360 - a1
la = 1 if da > 180 else 0
svgpath = 'M %s %s A %s %s 0 %s 1 %s %s' % \
(x1, y1, r, r, la, x2, y2)
bbox.add_svgpath(svgpath, width, self.logger)
{
'segment': add_segment,
'rect': add_segment, # bbox of a rect and segment are the same
'circle': add_circle,
'arc': add_arc,
'polygon': add_polygon,
'text': lambda: None, # text is not really needed for bounding box
}.get(drawing['type'])()
class Component(object):
"""Simple data object to store component data needed for bom table."""
def __init__(self, ref, val, footprint, layer, attr=None, extra_fields={}):
self.ref = ref
self.val = val
self.footprint = footprint
self.layer = layer
self.attr = attr
self.extra_fields = extra_fields
class BoundingBox(object):
"""Geometry util to calculate and combine bounding box of simple shapes."""
def __init__(self):
self._x0 = None
self._y0 = None
self._x1 = None
self._y1 = None
def to_dict(self):
# type: () -> dict
return {
"minx": self._x0,
"miny": self._y0,
"maxx": self._x1,
"maxy": self._y1,
}
def to_component_dict(self):
# type: () -> dict
return {
"pos": [self._x0, self._y0],
"relpos": [0, 0],
"size": [self._x1 - self._x0, self._y1 - self._y0],
"angle": 0,
}
def add(self, other):
"""Add another bounding box.
:type other: BoundingBox
"""
if other._x0 is not None:
self.add_point(other._x0, other._y0)
self.add_point(other._x1, other._y1)
return self
@staticmethod
def _rotate(x, y, rx, ry, angle):
sin = math.sin(math.radians(angle))
cos = math.cos(math.radians(angle))
new_x = rx + (x - rx) * cos - (y - ry) * sin
new_y = ry + (x - rx) * sin + (y - ry) * cos
return new_x, new_y
def add_point(self, x, y, rx=0, ry=0, angle=0):
x, y = self._rotate(x, y, rx, ry, angle)
if self._x0 is None:
self._x0 = x
self._y0 = y
self._x1 = x
self._y1 = y
else:
self._x0 = min(self._x0, x)
self._y0 = min(self._y0, y)
self._x1 = max(self._x1, x)
self._y1 = max(self._y1, y)
return self
def add_segment(self, x0, y0, x1, y1, r):
self.add_circle(x0, y0, r)
self.add_circle(x1, y1, r)
return self
def add_rectangle(self, x, y, w, h, angle=0):
self.add_point(x - w / 2, y - h / 2, x, y, angle)
self.add_point(x + w / 2, y - h / 2, x, y, angle)
self.add_point(x - w / 2, y + h / 2, x, y, angle)
self.add_point(x + w / 2, y + h / 2, x, y, angle)
return self
def add_circle(self, x, y, r):
self.add_point(x - r, y)
self.add_point(x, y - r)
self.add_point(x + r, y)
self.add_point(x, y + r)
return self
def add_svgpath(self, svgpath, width, logger):
w = width / 2
for segment in parse_path(svgpath, logger):
x0, x1, y0, y1 = segment.bbox()
self.add_point(x0 - w, y0 - w)
self.add_point(x1 + w, y1 + w)
def pad(self, amount):
"""Add small padding to the box."""
if self._x0 is not None:
self._x0 -= amount
self._y0 -= amount
self._x1 += amount
self._y1 += amount
def initialized(self):
return self._x0 is not None

View File

@ -0,0 +1,508 @@
import io
import os
import sys
from .common import EcadParser, Component, BoundingBox, ExtraFieldData
if sys.version_info >= (3, 0):
string_types = str
else:
string_types = basestring # noqa F821: ignore undefined
class EasyEdaParser(EcadParser):
TOP_COPPER_LAYER = 1
BOT_COPPER_LAYER = 2
TOP_SILK_LAYER = 3
BOT_SILK_LAYER = 4
BOARD_OUTLINE_LAYER = 10
TOP_ASSEMBLY_LAYER = 13
BOT_ASSEMBLY_LAYER = 14
ALL_LAYERS = 11
def extra_data_file_filter(self):
return "Json file ({f})|{f}".format(f=os.path.basename(self.file_name))
def latest_extra_data(self, extra_dirs=None):
return self.file_name
def get_extra_field_data(self, file_name):
if os.path.abspath(file_name) != os.path.abspath(self.file_name):
return None
_, components = self.parse()
field_set = set()
comp_dict = {}
for c in components:
ref_fields = comp_dict.setdefault(c.ref, {})
for k, v in c.extra_fields.items():
field_set.add(k)
ref_fields[k] = v
by_index = {
i: components[i].extra_fields for i in range(len(components))
}
return ExtraFieldData(list(field_set), comp_dict, by_index)
def get_easyeda_pcb(self):
import json
with io.open(self.file_name, 'r', encoding='utf-8') as f:
return json.load(f)
@staticmethod
def tilda_split(s):
# type: (str) -> list
return s.split('~')
@staticmethod
def sharp_split(s):
# type: (str) -> list
return s.split('#@$')
def _verify(self, pcb):
"""Spot check the pcb object."""
if 'head' not in pcb:
self.logger.error('No head attribute.')
return False
head = pcb['head']
if len(head) < 2:
self.logger.error('Incorrect head attribute ' + pcb['head'])
return False
if head['docType'] != '3':
self.logger.error('Incorrect document type: ' + head['docType'])
return False
if 'canvas' not in pcb:
self.logger.error('No canvas attribute.')
return False
canvas = self.tilda_split(pcb['canvas'])
if len(canvas) < 18:
self.logger.error('Incorrect canvas attribute ' + pcb['canvas'])
return False
self.logger.info('EasyEDA editor version ' + head['editorVersion'])
return True
@staticmethod
def normalize(v):
if isinstance(v, string_types):
v = float(v)
return v
def parse_track(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 5, 'Invalid track ' + str(shape)
width = self.normalize(shape[0])
layer = int(shape[1])
points = [self.normalize(v) for v in shape[3].split(' ')]
points_xy = [[points[i], points[i + 1]] for i in
range(0, len(points), 2)]
segments = [(points_xy[i], points_xy[i + 1]) for i in
range(len(points_xy) - 1)]
segments_json = []
for segment in segments:
segments_json.append({
"type": "segment",
"start": segment[0],
"end": segment[1],
"width": width,
})
return layer, segments_json
def parse_via(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 5, 'Invalid via ' + str(shape)
x, y = self.normalize(shape[0]), self.normalize(shape[1])
width = self.normalize(shape[2])
return self.TOP_COPPER_LAYER, [{
"type": "segment",
"start": [x, y],
"end": [x, y],
"width": width
}]
def parse_rect(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 9, 'Invalid rect ' + str(shape)
x = self.normalize(shape[0])
y = self.normalize(shape[1])
width = self.normalize(shape[2])
height = self.normalize(shape[3])
layer = int(shape[4])
fill = shape[8]
if fill == "none":
thickness = self.normalize(shape[7])
return layer, [{
"type": "rect",
"start": [x, y],
"end": [x + width, y + height],
"width": thickness,
}]
else:
return layer, [{
"type": "polygon",
"pos": [x, y],
"angle": 0,
"polygons": [
[[0, 0], [width, 0], [width, height], [0, height]]
]
}]
def parse_circle(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 6, 'Invalid circle ' + str(shape)
cx = self.normalize(shape[0])
cy = self.normalize(shape[1])
r = self.normalize(shape[2])
width = self.normalize(shape[3])
layer = int(shape[4])
return layer, [{
"type": "circle",
"start": [cx, cy],
"radius": r,
"width": width
}]
def parse_solid_region(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 5, 'Invalid solid region ' + str(shape)
layer = int(shape[0])
svgpath = shape[2]
return layer, [{
"type": "polygon",
"svgpath": svgpath,
}]
def parse_text(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 12, 'Invalid text ' + str(shape)
text_type = shape[0]
stroke_width = self.normalize(shape[3])
layer = int(shape[6])
text = shape[9]
svgpath = shape[10]
hide = shape[11]
return layer, [{
"type": "text",
"text": text,
"thickness": stroke_width,
"attr": [],
"svgpath": svgpath,
"hide": hide,
"text_type": text_type,
}]
def parse_arc(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 6, 'Invalid arc ' + str(shape)
width = self.normalize(shape[0])
layer = int(shape[1])
svgpath = shape[3]
return layer, [{
"type": "arc",
"svgpath": svgpath,
"width": width
}]
def parse_hole(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 4, 'Invalid hole ' + str(shape)
cx = self.normalize(shape[0])
cy = self.normalize(shape[1])
radius = self.normalize(shape[2])
return self.BOARD_OUTLINE_LAYER, [{
"type": "circle",
"start": [cx, cy],
"radius": radius,
"width": 0.1, # 1 mil
}]
def parse_pad(self, shape):
shape = self.tilda_split(shape)
assert len(shape) >= 15, 'Invalid pad ' + str(shape)
pad_shape = shape[0]
x = self.normalize(shape[1])
y = self.normalize(shape[2])
width = self.normalize(shape[3])
height = self.normalize(shape[4])
layer = int(shape[5])
number = shape[7]
hole_radius = self.normalize(shape[8])
if shape[9]:
points = [self.normalize(v) for v in shape[9].split(' ')]
else:
points = []
angle = int(shape[10])
hole_length = self.normalize(shape[12]) if shape[12] else 0
pad_layers = {
self.TOP_COPPER_LAYER: ['F'],
self.BOT_COPPER_LAYER: ['B'],
self.ALL_LAYERS: ['F', 'B']
}.get(layer)
pad_shape = {
"ELLIPSE": "circle",
"RECT": "rect",
"OVAL": "oval",
"POLYGON": "custom",
}.get(pad_shape)
pad_type = "smd" if len(pad_layers) == 1 else "th"
json = {
"layers": pad_layers,
"pos": [x, y],
"size": [width, height],
"angle": angle,
"shape": pad_shape,
"type": pad_type,
}
if number == '1':
json['pin1'] = 1
if pad_shape == "custom":
polygon = [(points[i], points[i + 1]) for i in
range(0, len(points), 2)]
# translate coordinates to be relative to footprint
polygon = [(p[0] - x, p[1] - y) for p in polygon]
json["polygons"] = [polygon]
json["angle"] = 0
if pad_type == "th":
if hole_length > 1e-6:
json["drillshape"] = "oblong"
json["drillsize"] = [hole_radius * 2, hole_length]
else:
json["drillshape"] = "circle"
json["drillsize"] = [hole_radius * 2, hole_radius * 2]
return layer, [{
"type": "pad",
"pad": json,
}]
@staticmethod
def add_pad_bounding_box(pad, bbox):
# type: (dict, BoundingBox) -> None
def add_circle():
bbox.add_circle(pad['pos'][0], pad['pos'][1], pad['size'][0] / 2)
def add_rect():
bbox.add_rectangle(pad['pos'][0], pad['pos'][1],
pad['size'][0], pad['size'][1],
pad['angle'])
def add_custom():
x = pad['pos'][0]
y = pad['pos'][1]
polygon = pad['polygons'][0]
for point in polygon:
bbox.add_point(x + point[0], y + point[1])
{
'circle': add_circle,
'rect': add_rect,
'oval': add_rect,
'custom': add_custom,
}.get(pad['shape'])()
def parse_lib(self, shape):
parts = self.sharp_split(shape)
head = self.tilda_split(parts[0])
inner_shapes, _, _ = self.parse_shapes(parts[1:])
x = self.normalize(head[0])
y = self.normalize(head[1])
attr = head[2]
fp_layer = int(head[6])
attr = attr.split('`')
if len(attr) % 2 != 0:
attr.pop()
attr = {attr[i]: attr[i + 1] for i in range(0, len(attr), 2)}
fp_layer = 'F' if fp_layer == self.TOP_COPPER_LAYER else 'B'
val = '??'
ref = '??'
footprint = '??'
if 'package' in attr:
footprint = attr['package']
del attr['package']
pads = []
copper_drawings = []
extra_drawings = []
bbox = BoundingBox()
for layer, shapes in inner_shapes.items():
for s in shapes:
if s["type"] == "pad":
pads.append(s["pad"])
continue
if s["type"] == "text":
if s["text_type"] == "N":
val = s["text"]
if s["text_type"] == "P":
ref = s["text"]
del s["text_type"]
if s["hide"]:
continue
if layer in [self.TOP_COPPER_LAYER, self.BOT_COPPER_LAYER]:
copper_drawings.append({
"layer": (
'F' if layer == self.TOP_COPPER_LAYER else 'B'),
"drawing": s,
})
elif layer in [self.TOP_SILK_LAYER,
self.BOT_SILK_LAYER,
self.TOP_ASSEMBLY_LAYER,
self.BOT_ASSEMBLY_LAYER,
self.BOARD_OUTLINE_LAYER]:
extra_drawings.append((layer, s))
for pad in pads:
self.add_pad_bounding_box(pad, bbox)
for drawing in copper_drawings:
self.add_drawing_bounding_box(drawing['drawing'], bbox)
for _, drawing in extra_drawings:
self.add_drawing_bounding_box(drawing, bbox)
bbox.pad(0.5) # pad by 5 mil
if not bbox.initialized():
# if bounding box is not calculated yet
# set it to 100x100 mil square
bbox.add_rectangle(x, y, 10, 10, 0)
footprint_json = {
"ref": ref,
"center": [x, y],
"bbox": bbox.to_component_dict(),
"pads": pads,
"drawings": copper_drawings,
"layer": fp_layer,
}
component = Component(ref, val, footprint, fp_layer, extra_fields=attr)
return fp_layer, component, footprint_json, extra_drawings
def parse_shapes(self, shapes):
drawings = {}
footprints = []
components = []
for shape_str in shapes:
shape = shape_str.split('~', 1)
parse_func = {
'TRACK': self.parse_track,
'VIA': self.parse_via,
'RECT': self.parse_rect,
'CIRCLE': self.parse_circle,
'SOLIDREGION': self.parse_solid_region,
'TEXT': self.parse_text,
'ARC': self.parse_arc,
'PAD': self.parse_pad,
'HOLE': self.parse_hole,
}.get(shape[0], None)
if parse_func:
layer, json_list = parse_func(shape[1])
drawings.setdefault(layer, []).extend(json_list)
if shape[0] == 'VIA':
drawings.setdefault(self.BOT_COPPER_LAYER, []).extend(json_list)
if shape[0] == 'LIB':
layer, component, json, extras = self.parse_lib(shape[1])
for drawing_layer, drawing in extras:
drawings.setdefault(drawing_layer, []).append(drawing)
footprints.append(json)
components.append(component)
return drawings, footprints, components
def get_metadata(self, pcb):
if hasattr(pcb, 'metadata'):
return pcb.metadata
else:
import os
from datetime import datetime
pcb_file_name = os.path.basename(self.file_name)
title = os.path.splitext(pcb_file_name)[0]
file_mtime = os.path.getmtime(self.file_name)
file_date = datetime.fromtimestamp(file_mtime).strftime(
'%Y-%m-%d %H:%M:%S')
return {
"title": title,
"revision": "",
"company": "",
"date": file_date,
}
def parse(self):
pcb = self.get_easyeda_pcb()
if not self._verify(pcb):
self.logger.error(
'File ' + self.file_name +
' does not appear to be valid EasyEDA json file.')
return None, None
drawings, footprints, components = self.parse_shapes(pcb['shape'])
board_outline_bbox = BoundingBox()
for drawing in drawings.get(self.BOARD_OUTLINE_LAYER, []):
self.add_drawing_bounding_box(drawing, board_outline_bbox)
if board_outline_bbox.initialized():
bbox = board_outline_bbox.to_dict()
else:
# if nothing is drawn on outline layer then rely on EasyEDA bbox
x = self.normalize(pcb['BBox']['x'])
y = self.normalize(pcb['BBox']['y'])
bbox = {
"minx": x,
"miny": y,
"maxx": x + self.normalize(pcb['BBox']['width']),
"maxy": y + self.normalize(pcb['BBox']['height'])
}
pcbdata = {
"edges_bbox": bbox,
"edges": drawings.get(self.BOARD_OUTLINE_LAYER, []),
"drawings": {
"silkscreen": {
'F': drawings.get(self.TOP_SILK_LAYER, []),
'B': drawings.get(self.BOT_SILK_LAYER, []),
},
"fabrication": {
'F': drawings.get(self.TOP_ASSEMBLY_LAYER, []),
'B': drawings.get(self.BOT_ASSEMBLY_LAYER, []),
},
},
"footprints": footprints,
"metadata": self.get_metadata(pcb),
"bom": {},
"font_data": {}
}
if self.config.include_tracks:
def filter_tracks(drawing_list, drawing_type, keys):
result = []
for d in drawing_list:
if d["type"] == drawing_type:
r = {}
for key in keys:
r[key] = d[key]
result.append(r)
return result
pcbdata["tracks"] = {
'F': filter_tracks(drawings.get(self.TOP_COPPER_LAYER, []),
"segment", ["start", "end", "width"]),
'B': filter_tracks(drawings.get(self.BOT_COPPER_LAYER, []),
"segment", ["start", "end", "width"]),
}
# zones are not supported
pcbdata["zones"] = {'F': [], 'B': []}
return pcbdata, components

View File

@ -0,0 +1,920 @@
import io
import math
import os
import string
import zipfile
from datetime import datetime
from xml.etree import ElementTree
from .common import EcadParser, Component, BoundingBox
from .svgpath import Arc
from ..core.fontparser import FontParser
class FusionEagleParser(EcadParser):
TOP_COPPER_LAYER = '1'
BOT_COPPER_LAYER = '16'
TOP_PLACE_LAYER = '21'
BOT_PLACE_LAYER = '22'
TOP_NAMES_LAYER = '25'
BOT_NAMES_LAYER = '26'
DIMENSION_LAYER = '20'
TOP_DOCU_LAYER = '51'
BOT_DOCU_LAYER = '52'
def __init__(self, file_name, config, logger):
super(FusionEagleParser, self).__init__(file_name, config, logger)
self.config = config
self.font_parser = FontParser()
self.min_via_w = 1e-3
self.pcbdata = {
'drawings': {
'silkscreen': {
'F': [],
'B': []
},
'fabrication': {
'F': [],
'B': []
}
},
'edges': [],
'footprints': [],
'font_data': {}
}
self.components = []
def _parse_pad_nets(self, signals):
elements = {}
for signal in signals.iter('signal'):
net = signal.attrib['name']
for c in signal.iter('contactref'):
e = c.attrib['element']
if e not in elements:
elements[e] = {}
elements[e][c.attrib['pad']] = net
self.elements_pad_nets = elements
@staticmethod
def _radian(ux, uy, vx, vy):
dot = ux * vx + uy * vy
mod = math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
rad = math.acos(dot / mod)
if ux * vy - uy * vx < 0.0:
rad = -rad
return rad
def _curve_to_svgparams(self, el, x=0, y=0, angle=0, mirrored=False):
_x1 = float(el.attrib['x1'])
_x2 = float(el.attrib['x2'])
_y1 = -float(el.attrib['y1'])
_y2 = -float(el.attrib['y2'])
dx1, dy1 = self._rotate(_x1, _y1, -angle, mirrored)
dx2, dy2 = self._rotate(_x2, _y2, -angle, mirrored)
x1, y1 = x + dx1, -y + dy1
x2, y2 = x + dx2, -y + dy2
chord = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
theta = float(el.attrib['curve'])
theta = -theta if mirrored else theta
r = abs(0.5 * chord / math.sin(math.radians(theta) / 2))
la = 0 if abs(theta) < 180 else 1
sw = 0 if theta > 0 else 1
return {
'x1': x1,
'y1': y1,
'r': r,
'la': la,
'sw': sw,
'x2': x2,
'y2': y2
}
def _curve_to_svgpath(self, el, x=0, y=0, angle=0, mirrored=False):
p = self._curve_to_svgparams(el, x, y, angle, mirrored)
return 'M {x1} {y1} A {r} {r} 0 {la} {sw} {x2} {y2}'.format(**p)
@staticmethod
class Rot:
def __init__(self, rot_string):
if not rot_string:
self.mirrored = False
self.spin = False
self.angle = 0
return
self.mirrored = 'M' in rot_string
self.spin = 'S' in rot_string
self.angle = float(''.join(d for d in rot_string
if d in string.digits + '.'))
def __repr__(self):
return self.__str__()
def __str__(self):
return "Mirrored: {0}, Spin: {1}, Angle: {2}".format(self.mirrored,
self.spin,
self.angle)
def _rectangle_vertices(self, el):
# Note: Eagle specifies a rectangle using opposing corners
# (x1, y1) = lower-left and (x2, y2) = upper-right) and *optionally*
# a rotation angle. The size of the rectangle is defined by the
# corners irrespective of rotation angle, and then it is rotated
# about its own center point.
_x1 = float(el.attrib['x1'])
_x2 = float(el.attrib['x2'])
_y1 = -float(el.attrib['y1'])
_y2 = -float(el.attrib['y2'])
# Center of rectangle
xc = (_x1 + _x2) / 2
yc = (_y1 + _y2) / 2
# Vertices of rectangle relative to its center, un-rotated
_dv_c = [
(_x1 - xc, _y1 - yc),
(_x2 - xc, _y1 - yc),
(_x2 - xc, _y2 - yc),
(_x1 - xc, _y2 - yc)
]
elr = self.Rot(el.get('rot'))
# Rotate the rectangle about its center
dv_c = [self._rotate(_x, _y, -elr.angle, elr.mirrored)
for (_x, _y) in _dv_c]
# Map vertices to position relative to component origin, un-rotated
return [(_x + xc, _y + yc) for (_x, _y) in dv_c]
def _add_drawing(self, el):
layer_dest = {
self.DIMENSION_LAYER: self.pcbdata['edges'],
self.TOP_PLACE_LAYER: self.pcbdata['drawings']['silkscreen']['F'],
self.BOT_PLACE_LAYER: self.pcbdata['drawings']['silkscreen']['B'],
self.TOP_NAMES_LAYER: self.pcbdata['drawings']['silkscreen']['F'],
self.BOT_NAMES_LAYER: self.pcbdata['drawings']['silkscreen']['B']
}
if ("layer" in el.attrib) and (el.attrib['layer'] in layer_dest):
dwg = None
if el.tag == 'wire':
dwg = {'width': float(el.attrib['width'])}
if 'curve' in el.attrib:
dwg['type'] = 'arc'
dwg['svgpath'] = self._curve_to_svgpath(el)
else:
dwg['type'] = 'segment'
dwg['start'] = [
float(el.attrib['x1']),
-float(el.attrib['y1'])
]
dwg['end'] = [
float(el.attrib['x2']),
-float(el.attrib['y2'])
]
elif el.tag == 'text':
# Text is not currently supported (except refdes)
# due to lack of Eagle font data
pass
elif el.tag == 'circle':
dwg = {
'type': 'circle',
'start': [float(el.attrib['x']), -float(el.attrib['y'])],
'radius': float(el.attrib['radius']),
'width': float(el.attrib['width'])
}
elif el.tag in ['polygonshape', 'polygon']:
dwg = {
'type': 'polygon',
'pos': [0, 0],
'angle': 0,
'polygons': []
}
segs = el if el.tag == 'polygon' \
else el.find('polygonoutlinesegments')
polygon = FusionEagleParser._segments_to_polygon(segs)
dwg['polygons'].append(polygon)
elif el.tag == 'rectangle':
vertices = self._rectangle_vertices(el)
dwg = {
'type': 'polygon',
'pos': [0, 0],
'angle': 0,
'polygons': [[list(v) for v in vertices]]
}
if dwg:
layer_dest[el.attrib['layer']].append(dwg)
def _add_track(self, el, net):
if el.tag == 'via' or (
el.tag == 'wire' and el.attrib['layer'] in
[self.TOP_COPPER_LAYER, self.BOT_COPPER_LAYER]):
trk = {}
if self.config.include_nets:
trk['net'] = net
if el.tag == 'wire':
dest = self.pcbdata['tracks']['F'] \
if el.attrib['layer'] == self.TOP_COPPER_LAYER\
else self.pcbdata['tracks']['B']
if 'curve' in el.attrib:
trk['width'] = float(el.attrib['width'])
# Get SVG parameters for the curve
p = self._curve_to_svgparams(el)
start = complex(p['x1'], p['y1'])
end = complex(p['x2'], p['y2'])
radius = complex(p['r'], p['r'])
large_arc = bool(p['la'])
sweep = bool(p['sw'])
# Pass SVG parameters to get center parameters
arc = Arc(start, radius, 0, large_arc, sweep, end)
# Create arc track from center parameters
trk['center'] = [arc.center.real, arc.center.imag]
trk['radius'] = radius.real
if arc.delta < 0:
trk['startangle'] = arc.theta + arc.delta
trk['endangle'] = arc.theta
else:
trk['startangle'] = arc.theta
trk['endangle'] = arc.theta + arc.delta
dest.append(trk)
else:
trk['start'] = [
float(el.attrib['x1']),
-float(el.attrib['y1'])
]
trk['end'] = [
float(el.attrib['x2']),
-float(el.attrib['y2'])
]
trk['width'] = float(el.attrib['width'])
dest.append(trk)
elif el.tag == 'via':
trk['start'] = [float(el.attrib['x']), -float(el.attrib['y'])]
trk['end'] = trk['start']
trk['width'] = float(el.attrib['drill']) + 2 * self.min_via_w \
if 'diameter' not in el.attrib else float(
el.attrib['diameter'])
if float(el.attrib['drill']) >= self.min_drill_via_untented:
trk['drillsize'] = float(el.attrib['drill'])
self.pcbdata['tracks']['F'].append(trk)
self.pcbdata['tracks']['B'].append(trk)
def _calculate_footprint_bbox(self, package, x, y, angle, mirrored):
_angle = angle if not mirrored else -angle
layers = [
self.TOP_PLACE_LAYER,
self.BOT_PLACE_LAYER,
self.TOP_DOCU_LAYER,
self.BOT_DOCU_LAYER
]
xmax, ymax = -float('inf'), -float('inf')
xmin, ymin = float('inf'), float('inf')
for el in package.iter('wire'):
if el.tag == 'wire' and el.attrib['layer'] in layers:
xmax = max(xmax,
max(float(el.attrib['x1']), float(el.attrib['x2'])))
ymax = max(ymax,
max(float(el.attrib['y1']), float(el.attrib['y2'])))
xmin = min(xmin,
min(float(el.attrib['x1']), float(el.attrib['x2'])))
ymin = min(ymin,
min(float(el.attrib['y1']), float(el.attrib['y2'])))
for el in package.iter():
if el.tag in ['smd', 'pad']:
elx, ely = float(el.attrib['x']), float(el.attrib['y'])
if el.tag == 'smd':
dx, dy = abs(float(el.attrib['dx'])) / 2, abs(
float(el.attrib['dy'])) / 2
else:
d = el.get('diameter')
if d is None:
diameter = float(el.get('drill')) + 2 * self.min_via_w
else:
diameter = float(d)
dx, dy = diameter / 2, diameter / 2
xmax, ymax = max(xmax, elx + dx), max(ymax, ely + dy)
xmin, ymin = min(xmin, elx - dx), min(ymin, ely - dy)
if not math.isinf(xmin):
if mirrored:
xmin, xmax = -xmax, -xmin
dx, dy = self._rotate(xmin, ymax, _angle)
sx = abs(xmax - xmin)
sy = abs(ymax - ymin)
else:
dx, dy = 0, 0
sx, sy = 0, 0
return {
'pos': [x + dx, -y - dy],
'angle': _angle,
'relpos': [0, 0],
'size': [sx, sy]
}
def _footprint_pads(self, package, x, y, angle, mirrored, refdes):
pads = []
element_pad_nets = self.elements_pad_nets.get(refdes)
pin1_allocated = False
for el in package.iter():
if el.tag == 'pad':
elx = float(el.attrib['x'])
ely = -float(el.attrib['y'])
drill = float(el.attrib['drill'])
dx, dy = self._rotate(elx, ely, -angle, mirrored)
diameter = drill + 2 * self.min_via_w \
if 'diameter' not in el.attrib \
else float(el.attrib['diameter'])
pr = self.Rot(el.get('rot'))
if mirrored ^ pr.mirrored:
pad_angle = -angle - pr.angle
else:
pad_angle = angle + pr.angle
pad = {
'layers': ['F', 'B'],
'pos': [x + dx, -y + dy],
'angle': pad_angle,
'type': 'th',
'drillshape': 'circle',
'drillsize': [
drill,
drill
]
}
if el.get('name') in ['1', 'A', 'A1', 'P1', 'PAD1'] and \
not pin1_allocated:
pad['pin1'] = 1
pin1_allocated = True
if 'shape' not in el.attrib or el.attrib['shape'] == 'round':
pad['shape'] = 'circle'
pad['size'] = [diameter, diameter]
elif el.attrib['shape'] == 'square':
pad['shape'] = 'rect'
pad['size'] = [diameter, diameter]
elif el.attrib['shape'] == 'octagon':
pad['shape'] = 'chamfrect'
pad['size'] = [diameter, diameter]
pad['radius'] = 0
pad['chamfpos'] = 0b1111 # all corners
pad['chamfratio'] = 0.333
elif el.attrib['shape'] == 'long':
pad['shape'] = 'roundrect'
pad['radius'] = diameter / 2
pad['size'] = [2 * diameter, diameter]
elif el.attrib['shape'] == 'offset':
pad['shape'] = 'roundrect'
pad['radius'] = diameter / 2
pad['size'] = [2 * diameter, diameter]
pad['offset'] = [diameter / 2, 0]
elif el.attrib['shape'] == 'slot':
pad['shape'] = 'roundrect'
pad['radius'] = diameter / 2
slot_length = float(el.attrib['slotLength'])
pad['size'] = [slot_length + diameter / 2, diameter]
pad['drillshape'] = 'oblong'
pad['drillsize'] = [slot_length, drill]
else:
self.logger.info(
"Unsupported footprint pad shape %s, skipping",
el.attrib['shape'])
if self.config.include_nets and element_pad_nets is not None:
net = element_pad_nets.get(el.attrib['name'])
if net is not None:
pad['net'] = net
pads.append(pad)
elif el.tag == 'smd':
layer = el.attrib['layer']
if layer == self.TOP_COPPER_LAYER and not mirrored or \
layer == self.BOT_COPPER_LAYER and mirrored:
layers = ['F']
elif layer == self.TOP_COPPER_LAYER and mirrored or \
layer == self.BOT_COPPER_LAYER and not mirrored:
layers = ['B']
else:
self.logger.error('Unable to determine layer for '
'{0} pad {1}'.format(refdes,
el.attrib['name']))
layers = None
if layers is not None:
elx = float(el.attrib['x'])
ely = -float(el.attrib['y'])
dx, dy = self._rotate(elx, ely, -angle, mirrored)
pr = self.Rot(el.get('rot'))
if mirrored ^ pr.mirrored:
pad_angle = -angle - pr.angle
else:
pad_angle = angle + pr.angle
pad = {'layers': layers,
'pos': [x + dx, -y + dy],
'size': [
float(el.attrib['dx']),
float(el.attrib['dy'])
],
'angle': pad_angle,
'type': 'smd',
}
if el.get('name') in ['1', 'A', 'A1', 'P1', 'PAD1'] and \
not pin1_allocated:
pad['pin1'] = 1
pin1_allocated = True
if 'roundness' not in el.attrib:
pad['shape'] = 'rect'
else:
pad['shape'] = 'roundrect'
pad['radius'] = (float(el.attrib['roundness']) / 100) \
* float(el.attrib['dy']) / 2
if self.config.include_nets and \
element_pad_nets is not None:
net = element_pad_nets.get(el.attrib['name'])
if net is not None:
pad['net'] = net
pads.append(pad)
return pads
@staticmethod
def _rotate(x, y, angle, mirrored=False):
sin = math.sin(math.radians(angle))
cos = math.cos(math.radians(angle))
xr = x * cos - y * sin
yr = y * cos + x * sin
if mirrored:
return -xr, yr
else:
return xr, yr
def _process_footprint(self, package, x, y, angle, mirrored, populate):
for el in package.iter():
if el.tag in ['wire', 'rectangle', 'circle', 'hole',
'polygonshape', 'polygon', 'hole']:
if el.tag == 'hole':
dwg_layer = self.pcbdata['edges']
elif el.attrib['layer'] in [self.TOP_PLACE_LAYER,
self.BOT_PLACE_LAYER]:
dwg_layer = self.pcbdata['drawings']['silkscreen']
top = el.attrib['layer'] == self.TOP_PLACE_LAYER
elif el.attrib['layer'] in [self.TOP_DOCU_LAYER,
self.BOT_DOCU_LAYER]:
if not populate:
return
dwg_layer = self.pcbdata['drawings']['fabrication']
top = el.attrib['layer'] == self.TOP_DOCU_LAYER
elif el.tag == 'wire' and \
el.attrib['layer'] == self.DIMENSION_LAYER:
dwg_layer = self.pcbdata['edges']
top = True
else:
continue
dwg = None
if el.tag == 'wire':
_dx1 = float(el.attrib['x1'])
_dx2 = float(el.attrib['x2'])
_dy1 = -float(el.attrib['y1'])
_dy2 = -float(el.attrib['y2'])
dx1, dy1 = self._rotate(_dx1, _dy1, -angle, mirrored)
dx2, dy2 = self._rotate(_dx2, _dy2, -angle, mirrored)
x1, y1 = x + dx1, -y + dy1
x2, y2 = x + dx2, -y + dy2
if el.get('curve'):
dwg = {
'type': 'arc',
'width': float(el.attrib['width']),
'svgpath': self._curve_to_svgpath(el, x, y, angle,
mirrored)
}
else:
dwg = {
'type': 'segment',
'start': [x1, y1],
'end': [x2, y2],
'width': float(el.attrib['width'])
}
elif el.tag == 'rectangle':
_dv = self._rectangle_vertices(el)
# Rotate rectangle about component origin based on
# component angle
dv = [self._rotate(_x, _y, -angle, mirrored)
for (_x, _y) in _dv]
# Map vertices back to absolute coordinates
v = [(x + _x, -y + _y) for (_x, _y) in dv]
dwg = {
'type': 'polygon',
'filled': 1,
'pos': [0, 0],
'polygons': [v]
}
elif el.tag in ['circle', 'hole']:
_x = float(el.attrib['x'])
_y = -float(el.attrib['y'])
dxc, dyc = self._rotate(_x, _y, -angle, mirrored)
xc, yc = x + dxc, -y + dyc
if el.tag == 'circle':
radius = float(el.attrib['radius'])
width = float(el.attrib['width'])
else:
radius = float(el.attrib['drill']) / 2
width = 0
dwg = {
'type': 'circle',
'start': [xc, yc],
'radius': radius,
'width': width
}
elif el.tag in ['polygonshape', 'polygon']:
segs = el if el.tag == 'polygon' \
else el.find('polygonoutlinesegments')
dv = self._segments_to_polygon(segs, angle, mirrored)
polygon = [[x + v[0], -y + v[1]] for v in dv]
dwg = {
'type': 'polygon',
'filled': 1,
'pos': [0, 0],
'polygons': [polygon]
}
if dwg is not None:
if el.tag == 'hole' or \
el.attrib['layer'] == self.DIMENSION_LAYER:
dwg_layer.append(dwg)
else:
bot = not top
# Note that in Eagle terminology, 'mirrored'
# essentially means 'flipped' (i.e. to the opposite
# side of the board)
if (mirrored and bot) or (not mirrored and top):
dwg_layer['F'].append(dwg)
elif (mirrored and top) or (not mirrored and bot):
dwg_layer['B'].append(dwg)
def _name_to_silk(self, name, x, y, elr, tr, align, size, ratio):
angle = tr.angle
mirrored = tr.mirrored
spin = elr.spin ^ tr.spin
if mirrored:
angle = -angle
if align is None:
justify = [-1, 1]
elif align == 'center':
justify = [0, 0]
else:
j = align.split('-')
alignments = {
'bottom': 1,
'center': 0,
'top': -1,
'left': -1,
'right': 1
}
justify = [alignments[ss] for ss in j[::-1]]
if (90 < angle <= 270 and not spin) or \
(-90 > angle >= -270 and not spin):
angle += 180
justify = [-j for j in justify]
dwg = {
'type': 'text',
'text': name,
'pos': [x, y],
'height': size,
'width': size,
'justify': justify,
'thickness': size * ratio,
'attr': [] if not mirrored else ['mirrored'],
'angle': angle
}
self.font_parser.parse_font_for_string(name)
if mirrored:
self.pcbdata['drawings']['silkscreen']['B'].append(dwg)
else:
self.pcbdata['drawings']['silkscreen']['F'].append(dwg)
def _element_refdes_to_silk(self, el, package):
if 'smashed' not in el.attrib:
elx = float(el.attrib['x'])
ely = -float(el.attrib['y'])
for p_el in package.iter('text'):
if p_el.text == '>NAME':
dx = float(p_el.attrib['x'])
dy = float(p_el.attrib['y'])
elr = self.Rot(el.get('rot'))
dx, dy = self._rotate(dx, dy, elr.angle, elr.mirrored)
tr = self.Rot(p_el.get('rot'))
tr.angle += elr.angle
tr.mirrored ^= elr.mirrored
self._name_to_silk(
name=el.attrib['name'],
x=elx + dx,
y=ely - dy,
elr=elr,
tr=tr,
align=p_el.get('align'),
size=float(p_el.attrib['size']),
ratio=float(p_el.get('ratio', '8')) / 100)
for attr in el.iter('attribute'):
if attr.attrib['name'] == 'NAME':
self._name_to_silk(
name=el.attrib['name'],
x=float(attr.attrib['x']),
y=-float(attr.attrib['y']),
elr=self.Rot(el.get('rot')),
tr=self.Rot(attr.get('rot')),
align=attr.attrib.get('align'),
size=float(attr.attrib['size']),
ratio=float(attr.get('ratio', '8')) / 100)
@staticmethod
def _segments_to_polygon(segs, angle=0, mirrored=False):
polygon = []
for vertex in segs.iter('vertex'):
_x, _y = float(vertex.attrib['x']), -float(vertex.attrib['y'])
x, y = FusionEagleParser._rotate(_x, _y, -angle, mirrored)
polygon.append([x, y])
return polygon
def _add_zone(self, poly, net):
layer = poly.attrib['layer']
if layer == self.TOP_COPPER_LAYER:
dest = self.pcbdata['zones']['F']
elif layer == self.BOT_COPPER_LAYER:
dest = self.pcbdata['zones']['B']
else:
return
if poly.tag == 'polygonpour':
shapes = poly.find('polygonfilldetails').findall('polygonshape')
if shapes:
zone = {'polygons': [],
'fillrule': 'evenodd'}
for shape in shapes:
segs = shape.find('polygonoutlinesegments')
zone['polygons'].append(self._segments_to_polygon(segs))
holelist = shape.find('polygonholelist')
if holelist:
holes = holelist.findall('polygonholesegments')
for hole in holes:
zone['polygons'].append(self._segments_to_polygon(hole))
if self.config.include_nets:
zone['net'] = net
dest.append(zone)
else:
zone = {'polygons': []}
zone['polygons'].append(self._segments_to_polygon(poly))
if self.config.include_nets:
zone['net'] = net
dest.append(zone)
def _add_parsed_font_data(self):
for (c, wl) in self.font_parser.get_parsed_font().items():
if c not in self.pcbdata['font_data']:
self.pcbdata['font_data'][c] = wl
def _parse_param_length(self, name, root, default):
# parse named parameter (typically a design rule) assuming it is in
# length units (mil or mm)
p = [el.attrib['value'] for el in root.iter('param') if
el.attrib['name'] == name]
if len(p) == 0:
self.logger.warning("{0} not found, defaulting to {1}"
.format(name, default))
return default
else:
if len(p) > 1:
self.logger.warning(
"Multiple {0} found, using first occurrence".format(name))
p = p[0]
p_val = float(''.join(d for d in p if d in string.digits + '.'))
p_units = (''.join(d for d in p if d in string.ascii_lowercase))
if p_units == 'mm':
return p_val
elif p_units == 'mil':
return p_val * 0.0254
else:
self.logger.error("Unsupported units {0} on {1}"
.format(p_units, name))
def parse(self):
ext = os.path.splitext(self.file_name)[1]
if ext.lower() == '.fbrd':
with zipfile.ZipFile(self.file_name) as myzip:
brdfilename = [fname for fname in myzip.namelist() if
os.path.splitext(fname)[1] == '.brd']
with myzip.open(brdfilename[0]) as brdfile:
return self._parse(brdfile)
elif ext.lower() == '.brd':
with io.open(self.file_name, 'r', encoding='utf-8') as brdfile:
return self._parse(brdfile)
def _parse(self, brdfile):
try:
brdxml = ElementTree.parse(brdfile)
except ElementTree.ParseError as err:
self.logger.error(
"Exception occurred trying to parse {0}, message: {1}"
.format(brdfile.name, err.msg))
return None, None
if brdxml is None:
self.logger.error(
"No data was able to be parsed from {0}".format(brdfile.name))
return None, None
# Pick out key sections
root = brdxml.getroot()
board = root.find('drawing').find('board')
plain = board.find('plain')
elements = board.find('elements')
signals = board.find('signals')
# Build library mapping elements' pads to nets
self._parse_pad_nets(signals)
# Parse needed design rules
# Minimum via annular ring
# (Needed in order to calculate through-hole pad diameters correctly)
self.min_via_w = (
self._parse_param_length('rlMinViaOuter', root, default=0))
# Minimum drill diameter above which vias will be un-tented
self.min_drill_via_untented = (
self._parse_param_length('mlViaStopLimit', root, default=0))
# Signals --> nets
if self.config.include_nets:
self.pcbdata['nets'] = []
for signal in signals.iter('signal'):
self.pcbdata['nets'].append(signal.attrib['name'])
# Signals --> tracks, zones
if self.config.include_tracks:
self.pcbdata['tracks'] = {'F': [], 'B': []}
self.pcbdata['zones'] = {'F': [], 'B': []}
for signal in signals.iter('signal'):
for wire in signal.iter('wire'):
self._add_track(wire, signal.attrib['name'])
for via in signal.iter('via'):
self._add_track(via, signal.attrib['name'])
for poly in signal.iter('polygonpour'):
self._add_zone(poly, signal.attrib['name'])
# Elements --> components, footprints, silkscreen, edges
for el in elements.iter('element'):
populate = el.get('populate') != 'no'
elr = self.Rot(el.get('rot'))
layer = 'B' if elr.mirrored else 'F'
extra_fields = {}
for a in el.iter('attribute'):
if 'value' in a.attrib:
extra_fields[a.attrib['name']] = a.attrib['value']
comp = Component(ref=el.attrib['name'],
val='' if 'value' not in el.attrib else el.attrib[
'value'],
footprint=el.attrib['package'],
layer=layer,
attr=None if populate else 'Virtual',
extra_fields=extra_fields)
# For component, get footprint data
libs = [lib for lib in board.find('libraries').findall('library')
if lib.attrib['name'] == el.attrib['library']]
packages = []
for lib in libs:
p = [pac for pac in lib.find('packages').findall('package')
if pac.attrib['name'] == el.attrib['package']]
packages.extend(p)
if not packages:
self.logger.error("Package {0} in library {1} not found in "
"source file {2} for element {3}"
.format(el.attrib['package'],
el.attrib['library'],
brdfile.name,
el.attrib['name']))
return None, None
else:
package = packages[0]
if len(packages) > 1:
self.logger.warn("Multiple packages found for package {0}"
" in library {1}, using first instance "
"found".format(el.attrib['package'],
el.attrib['library']))
elx = float(el.attrib['x'])
ely = float(el.attrib['y'])
refdes = el.attrib['name']
footprint = {
'ref': refdes,
'center': [elx, ely],
'pads': [],
'drawings': [],
'layer': layer
}
elr = self.Rot(el.get('rot'))
footprint['pads'] = self._footprint_pads(package, elx, ely,
elr.angle, elr.mirrored,
refdes)
footprint['bbox'] = self._calculate_footprint_bbox(package, elx,
ely, elr.angle,
elr.mirrored)
self.pcbdata['footprints'].append(footprint)
# Add silkscreen, edges for component footprint & refdes
self._process_footprint(package, elx, ely, elr.angle, elr.mirrored,
populate)
self._element_refdes_to_silk(el, package)
self.components.append(comp)
# Edges & silkscreen (independent of elements)
for el in plain.iter():
self._add_drawing(el)
# identify board bounding box based on edges
board_outline_bbox = BoundingBox()
for drawing in self.pcbdata['edges']:
self.add_drawing_bounding_box(drawing, board_outline_bbox)
if board_outline_bbox.initialized():
self.pcbdata['edges_bbox'] = board_outline_bbox.to_dict()
self._add_parsed_font_data()
# Fabrication & metadata
company = [a.attrib['value'] for a in root.iter('attribute') if
a.attrib['name'] == 'COMPANY']
company = '' if not company else company[0]
rev = [a.attrib['value'] for a in root.iter('attribute') if
a.attrib['name'] == 'REVISION']
rev = '' if not rev else rev[0]
if not rev:
rev = ''
title = os.path.basename(self.file_name)
variant = [a.attrib['name'] for a in root.iter('variantdef') if
a.get('current') == 'yes']
variant = None if not variant else variant[0]
if variant:
title = "{0}, Variant: {1}".format(title, variant)
date = datetime.fromtimestamp(
os.path.getmtime(self.file_name)).strftime('%Y-%m-%d %H:%M:%S')
self.pcbdata['metadata'] = {'title': title, 'revision': rev,
'company': company, 'date': date}
return self.pcbdata, self.components

View File

@ -0,0 +1,167 @@
import io
import json
import os.path
from jsonschema import validate, ValidationError
from .common import EcadParser, Component, BoundingBox, ExtraFieldData
from ..core.fontparser import FontParser
from ..errors import ParsingException
class GenericJsonParser(EcadParser):
COMPATIBLE_SPEC_VERSIONS = [1]
def extra_data_file_filter(self):
return "Json file ({f})|{f}".format(f=os.path.basename(self.file_name))
def latest_extra_data(self, extra_dirs=None):
return self.file_name
def get_extra_field_data(self, file_name):
if os.path.abspath(file_name) != os.path.abspath(self.file_name):
return None
_, components = self._parse()
field_set = set()
comp_dict = {}
for c in components:
ref_fields = comp_dict.setdefault(c.ref, {})
for k, v in c.extra_fields.items():
field_set.add(k)
ref_fields[k] = v
by_index = {
i: components[i].extra_fields for i in range(len(components))
}
return ExtraFieldData(list(field_set), comp_dict, by_index)
def get_generic_json_pcb(self):
with io.open(self.file_name, 'r', encoding='utf-8') as f:
pcb = json.load(f)
if 'spec_version' not in pcb:
raise ValidationError("'spec_version' is a required property")
if pcb['spec_version'] not in self.COMPATIBLE_SPEC_VERSIONS:
raise ValidationError("Unsupported spec_version ({})"
.format(pcb['spec_version']))
schema_dir = os.path.join(os.path.dirname(__file__), 'schema')
schema_file_name = os.path.join(schema_dir,
'genericjsonpcbdata_v{}.schema'.format(
pcb['spec_version']))
with io.open(schema_file_name, 'r', encoding='utf-8') as f:
schema = json.load(f)
validate(instance=pcb, schema=schema)
return pcb
def _verify(self, pcb):
"""Spot check the pcb object."""
if len(pcb['pcbdata']['footprints']) != len(pcb['components']):
self.logger.error("Length of components list doesn't match"
" length of footprints list.")
return False
return True
@staticmethod
def _texts(pcbdata):
for layer in pcbdata['drawings'].values():
for side in layer.values():
for dwg in side:
if 'text' in dwg:
yield dwg
@staticmethod
def _remove_control_codes(s):
import unicodedata
return ''.join(c for c in s if unicodedata.category(c)[0] != "C")
def _parse_font_data(self, pcbdata):
font_parser = FontParser()
for dwg in self._texts(pcbdata):
if 'svgpath' not in dwg:
dwg['text'] = self._remove_control_codes(dwg['text'])
font_parser.parse_font_for_string(dwg['text'])
if font_parser.get_parsed_font():
pcbdata['font_data'] = font_parser.get_parsed_font()
def _check_font_data(self, pcbdata):
mc = set()
for dwg in self._texts(pcbdata):
dwg['text'] = self._remove_control_codes(dwg['text'])
mc.update({c for c in dwg['text'] if 'svgpath' not in dwg and
c not in pcbdata['font_data']})
if mc:
s = ''.join(mc)
self.logger.error('Provided font_data is missing character(s)'
f' "{s}" that are present in text drawing'
' objects')
return False
else:
return True
def _parse(self):
try:
pcb = self.get_generic_json_pcb()
except ValidationError as e:
self.logger.error('File {f} does not comply with json schema. {m}'
.format(f=self.file_name, m=e.message))
return None, None
if not self._verify(pcb):
self.logger.error('File {} does not appear to be valid generic'
' InteractiveHtmlBom json file.'
.format(self.file_name))
return None, None
pcbdata = pcb['pcbdata']
components = [Component(**c) for c in pcb['components']]
if 'font_data' in pcbdata:
if not self._check_font_data(pcbdata):
raise ParsingException(f'Failed parsing {self.file_name}')
else:
self._parse_font_data(pcbdata)
if 'font_data' in pcbdata:
self.logger.info('No font_data provided in JSON, using '
'newstroke font')
self.logger.info('Successfully parsed {}'.format(self.file_name))
return pcbdata, components
def parse(self):
pcbdata, components = self._parse()
# override board bounding box based on edges
board_outline_bbox = BoundingBox()
for drawing in pcbdata['edges']:
self.add_drawing_bounding_box(drawing, board_outline_bbox)
if board_outline_bbox.initialized():
pcbdata['edges_bbox'] = board_outline_bbox.to_dict()
extra_fields = set(self.config.show_fields)
extra_fields.discard("Footprint")
extra_fields.discard("Value")
if self.config.dnp_field:
extra_fields.add(self.config.dnp_field)
if self.config.board_variant_field:
extra_fields.add(self.config.board_variant_field)
if extra_fields:
for c in components:
c.extra_fields = {
f: c.extra_fields.get(f, "") for f in extra_fields}
self.config.kicad_text_formatting = False
return pcbdata, components

View File

@ -0,0 +1,928 @@
import os
from datetime import datetime
import pcbnew
from .common import EcadParser, Component, ExtraFieldData
from .kicad_extra import find_latest_schematic_data, parse_schematic_data
from .svgpath import create_path
from ..core import ibom
from ..core.config import Config
from ..core.fontparser import FontParser
KICAD_VERSION = [5, 1, 0]
if hasattr(pcbnew, 'Version'):
version = pcbnew.Version().split('.')
try:
for i in range(len(version)):
version[i] = int(version[i].split('-')[0])
except ValueError:
pass
KICAD_VERSION = version
class PcbnewParser(EcadParser):
def __init__(self, file_name, config, logger, board=None):
super(PcbnewParser, self).__init__(file_name, config, logger)
self.board = board
if self.board is None:
self.board = pcbnew.LoadBoard(self.file_name) # type: pcbnew.BOARD
if hasattr(self.board, 'GetModules'):
# type: list[pcbnew.MODULE]
self.footprints = list(self.board.GetModules())
else:
# type: list[pcbnew.FOOTPRINT]
self.footprints = list(self.board.GetFootprints())
self.font_parser = FontParser()
def get_extra_field_data(self, file_name):
if os.path.abspath(file_name) == os.path.abspath(self.file_name):
return self.parse_extra_data_from_pcb()
if os.path.splitext(file_name)[1] == '.kicad_pcb':
return None
data = parse_schematic_data(file_name)
return ExtraFieldData(data[0], data[1])
@staticmethod
def get_footprint_fields(f):
# type: (pcbnew.FOOTPRINT) -> dict
props = {}
if hasattr(f, "GetProperties"):
props = f.GetProperties()
if hasattr(f, "GetFields"):
props = f.GetFieldsShownText()
if "dnp" in props and props["dnp"] == "":
del props["dnp"]
props["kicad_dnp"] = "DNP"
if hasattr(f, "IsDNP"):
if f.IsDNP():
props["kicad_dnp"] = "DNP"
return props
def parse_extra_data_from_pcb(self):
field_set = set()
by_ref = {}
by_index = {}
for (i, f) in enumerate(self.footprints):
props = self.get_footprint_fields(f)
by_index[i] = props
ref = f.GetReference()
ref_fields = by_ref.setdefault(ref, {})
for k, v in props.items():
field_set.add(k)
ref_fields[k] = v
return ExtraFieldData(list(field_set), by_ref, by_index)
def latest_extra_data(self, extra_dirs=None):
base_name = os.path.splitext(os.path.basename(self.file_name))[0]
extra_dirs.append(self.board.GetPlotOptions().GetOutputDirectory())
file_dir_name = os.path.dirname(self.file_name)
directories = [file_dir_name]
for dir in extra_dirs:
if not os.path.isabs(dir):
dir = os.path.join(file_dir_name, dir)
if os.path.exists(dir):
directories.append(dir)
return find_latest_schematic_data(base_name, directories)
def extra_data_file_filter(self):
if hasattr(self.board, 'GetModules'):
return "Netlist and xml files (*.net; *.xml)|*.net;*.xml"
else:
return ("Netlist, xml and pcb files (*.net; *.xml; *.kicad_pcb)|"
"*.net;*.xml;*.kicad_pcb")
@staticmethod
def normalize(point):
return [point.x * 1e-6, point.y * 1e-6]
@staticmethod
def normalize_angle(angle):
if isinstance(angle, int) or isinstance(angle, float):
return angle * 0.1
else:
return angle.AsDegrees()
def get_arc_angles(self, d):
# type: (pcbnew.PCB_SHAPE) -> tuple
a1 = self.normalize_angle(d.GetArcAngleStart())
if hasattr(d, "GetAngle"):
a2 = a1 + self.normalize_angle(d.GetAngle())
else:
a2 = a1 + self.normalize_angle(d.GetArcAngle())
if a2 < a1:
a1, a2 = a2, a1
return round(a1, 2), round(a2, 2)
def parse_shape(self, d):
# type: (pcbnew.PCB_SHAPE) -> dict | None
shape = {
pcbnew.S_SEGMENT: "segment",
pcbnew.S_CIRCLE: "circle",
pcbnew.S_ARC: "arc",
pcbnew.S_POLYGON: "polygon",
pcbnew.S_CURVE: "curve",
pcbnew.S_RECT: "rect",
}.get(d.GetShape(), "")
if shape == "":
self.logger.info("Unsupported shape %s, skipping", d.GetShape())
return None
start = self.normalize(d.GetStart())
end = self.normalize(d.GetEnd())
if shape == "segment":
return {
"type": shape,
"start": start,
"end": end,
"width": d.GetWidth() * 1e-6
}
if shape == "rect":
if hasattr(d, "GetRectCorners"):
points = list(map(self.normalize, d.GetRectCorners()))
else:
points = [
start,
[end[0], start[1]],
end,
[start[0], end[1]]
]
shape_dict = {
"type": "polygon",
"pos": [0, 0],
"angle": 0,
"polygons": [points],
"width": d.GetWidth() * 1e-6,
"filled": 0
}
if hasattr(d, "IsFilled") and d.IsFilled():
shape_dict["filled"] = 1
return shape_dict
if shape == "circle":
shape_dict = {
"type": shape,
"start": start,
"radius": d.GetRadius() * 1e-6,
"width": d.GetWidth() * 1e-6
}
if hasattr(d, "IsFilled") and d.IsFilled():
shape_dict["filled"] = 1
return shape_dict
if shape == "arc":
a1, a2 = self.get_arc_angles(d)
if hasattr(d, "GetCenter"):
start = self.normalize(d.GetCenter())
return {
"type": shape,
"start": start,
"radius": d.GetRadius() * 1e-6,
"startangle": a1,
"endangle": a2,
"width": d.GetWidth() * 1e-6
}
if shape == "polygon":
if hasattr(d, "GetPolyShape"):
polygons = self.parse_poly_set(d.GetPolyShape())
else:
self.logger.info(
"Polygons not supported for KiCad 4, skipping")
return None
angle = 0
if hasattr(d, 'GetParentModule'):
parent_footprint = d.GetParentModule()
else:
parent_footprint = d.GetParentFootprint()
if parent_footprint is not None and KICAD_VERSION[0] < 8:
angle = self.normalize_angle(parent_footprint.GetOrientation())
shape_dict = {
"type": shape,
"pos": start,
"angle": angle,
"polygons": polygons
}
if hasattr(d, "IsFilled") and not d.IsFilled():
shape_dict["filled"] = 0
shape_dict["width"] = d.GetWidth() * 1e-6
return shape_dict
if shape == "curve":
if hasattr(d, "GetBezierC1"):
c1 = self.normalize(d.GetBezierC1())
c2 = self.normalize(d.GetBezierC2())
else:
c1 = self.normalize(d.GetBezControl1())
c2 = self.normalize(d.GetBezControl2())
return {
"type": shape,
"start": start,
"cpa": c1,
"cpb": c2,
"end": end,
"width": d.GetWidth() * 1e-6
}
def parse_line_chain(self, shape):
# type: (pcbnew.SHAPE_LINE_CHAIN) -> list
result = []
if not hasattr(shape, "PointCount"):
self.logger.warn("No PointCount method on outline object. "
"Unpatched kicad version?")
return result
for point_index in range(shape.PointCount()):
result.append(
self.normalize(shape.CPoint(point_index)))
return result
def parse_poly_set(self, poly):
# type: (pcbnew.SHAPE_POLY_SET) -> list
result = []
for i in range(poly.OutlineCount()):
result.append(self.parse_line_chain(poly.Outline(i)))
return result
def parse_text(self, d):
# type: (pcbnew.PCB_TEXT) -> dict
if not d.IsVisible() and d.GetClass() not in ["PTEXT", "PCB_TEXT"]:
return None
pos = self.normalize(d.GetPosition())
if hasattr(d, "GetTextThickness"):
thickness = d.GetTextThickness() * 1e-6
else:
thickness = d.GetThickness() * 1e-6
if hasattr(d, 'TransformToSegmentList'):
segments = [self.normalize(p) for p in d.TransformToSegmentList()]
lines = []
for i in range(0, len(segments), 2):
if i == 0 or segments[i - 1] != segments[i]:
lines.append([segments[i]])
lines[-1].append(segments[i + 1])
return {
"thickness": thickness,
"svgpath": create_path(lines)
}
elif hasattr(d, 'GetEffectiveTextShape'):
# type: pcbnew.SHAPE_COMPOUND
shape = d.GetEffectiveTextShape(False)
segments = []
polygons = []
for s in shape.GetSubshapes():
if s.Type() == pcbnew.SH_LINE_CHAIN:
polygons.append(self.parse_line_chain(s))
elif s.Type() == pcbnew.SH_SEGMENT:
seg = s.GetSeg()
segments.append(
[self.normalize(seg.A), self.normalize(seg.B)])
else:
self.logger.warn(
"Unsupported subshape in text: %s" % s.Type())
if segments:
return {
"thickness": thickness,
"svgpath": create_path(segments)
}
else:
return {
"polygons": polygons
}
if d.GetClass() == "MTEXT":
angle = self.normalize_angle(d.GetDrawRotation())
else:
if hasattr(d, "GetTextAngle"):
angle = self.normalize_angle(d.GetTextAngle())
else:
angle = self.normalize_angle(d.GetOrientation())
if hasattr(d, "GetTextHeight"):
height = d.GetTextHeight() * 1e-6
width = d.GetTextWidth() * 1e-6
else:
height = d.GetHeight() * 1e-6
width = d.GetWidth() * 1e-6
if hasattr(d, "GetShownText"):
text = d.GetShownText()
else:
text = d.GetText()
self.font_parser.parse_font_for_string(text)
attributes = []
if d.IsMirrored():
attributes.append("mirrored")
if d.IsItalic():
attributes.append("italic")
if d.IsBold():
attributes.append("bold")
return {
"pos": pos,
"text": text,
"height": height,
"width": width,
"justify": [d.GetHorizJustify(), d.GetVertJustify()],
"thickness": thickness,
"attr": attributes,
"angle": angle
}
def parse_dimension(self, d):
# type: (pcbnew.PCB_DIMENSION_BASE) -> dict
segments = []
circles = []
for s in d.GetShapes():
s = s.Cast()
if s.Type() == pcbnew.SH_SEGMENT:
seg = s.GetSeg()
segments.append(
[self.normalize(seg.A), self.normalize(seg.B)])
elif s.Type() == pcbnew.SH_CIRCLE:
circles.append(
[self.normalize(s.GetCenter()), s.GetRadius() * 1e-6])
else:
self.logger.info(
"Unsupported shape type in dimension object: %s", s.Type())
svgpath = create_path(segments, circles=circles)
return {
"thickness": d.GetLineThickness() * 1e-6,
"svgpath": svgpath
}
def parse_drawing(self, d):
# type: (pcbnew.BOARD_ITEM) -> list
result = []
s = None
if d.GetClass() in ["DRAWSEGMENT", "MGRAPHIC", "PCB_SHAPE"]:
s = self.parse_shape(d)
elif d.GetClass() in ["PTEXT", "MTEXT", "FP_TEXT", "PCB_TEXT", "PCB_FIELD"]:
s = self.parse_text(d)
elif (d.GetClass().startswith("PCB_DIM")
and hasattr(pcbnew, "VECTOR_SHAPEPTR")):
result.append(self.parse_dimension(d))
if hasattr(d, "Text"):
s = self.parse_text(d.Text())
else:
s = self.parse_text(d)
else:
self.logger.info("Unsupported drawing class %s, skipping",
d.GetClass())
if s:
result.append(s)
return result
def parse_edges(self, pcb):
edges = []
drawings = list(pcb.GetDrawings())
bbox = None
for f in self.footprints:
for g in f.GraphicalItems():
drawings.append(g)
for d in drawings:
if d.GetLayer() == pcbnew.Edge_Cuts:
for parsed_drawing in self.parse_drawing(d):
edges.append(parsed_drawing)
if bbox is None:
bbox = d.GetBoundingBox()
else:
bbox.Merge(d.GetBoundingBox())
if bbox:
bbox.Normalize()
return edges, bbox
def parse_drawings_on_layers(self, drawings, f_layer, b_layer):
front = []
back = []
for d in drawings:
if d[1].GetLayer() not in [f_layer, b_layer]:
continue
for drawing in self.parse_drawing(d[1]):
if d[0] in ["ref", "val"]:
drawing[d[0]] = 1
if d[1].GetLayer() == f_layer:
front.append(drawing)
else:
back.append(drawing)
return {
"F": front,
"B": back
}
def get_all_drawings(self):
drawings = [(d.GetClass(), d) for d in list(self.board.GetDrawings())]
for f in self.footprints:
drawings.append(("ref", f.Reference()))
drawings.append(("val", f.Value()))
for d in f.GraphicalItems():
drawings.append((d.GetClass(), d))
if hasattr(f, "GetFields"):
fields = f.GetFields() # type: list[pcbnew.PCB_FIELD]
for field in fields:
if field.IsReference() or field.IsValue():
continue
drawings.append((field.GetClass(), field))
return drawings
@staticmethod
def _pad_is_through_hole(pad):
# type: (pcbnew.PAD) -> bool
if hasattr(pcbnew, 'PAD_ATTRIB_PTH'):
through_hole_attributes = [pcbnew.PAD_ATTRIB_PTH,
pcbnew.PAD_ATTRIB_NPTH]
else:
through_hole_attributes = [pcbnew.PAD_ATTRIB_STANDARD,
pcbnew.PAD_ATTRIB_HOLE_NOT_PLATED]
return pad.GetAttribute() in through_hole_attributes
def parse_pad(self, pad):
# type: (pcbnew.PAD) -> list[dict]
custom_padstack = False
outer_layers = [(pcbnew.F_Cu, "F"), (pcbnew.B_Cu, "B")]
if hasattr(pad, 'Padstack'):
padstack = pad.Padstack() # type: pcbnew.PADSTACK
layers_set = list(padstack.LayerSet().Seq())
if hasattr(pcbnew, "UNCONNECTED_LAYER_MODE_REMOVE_ALL"):
ULMRA = pcbnew.UNCONNECTED_LAYER_MODE_REMOVE_ALL
else:
ULMRA = padstack.UNCONNECTED_LAYER_MODE_REMOVE_ALL
custom_padstack = (
padstack.Mode() != padstack.MODE_NORMAL or
padstack.UnconnectedLayerMode() == ULMRA
)
else:
layers_set = list(pad.GetLayerSet().Seq())
layers = []
for layer, letter in outer_layers:
if layer in layers_set:
layers.append(letter)
if not layers and not self._pad_is_through_hole(pad):
return []
if custom_padstack:
pads = []
for layer, letter in outer_layers:
if layer in layers_set and pad.FlashLayer(layer):
pad_dict = self.parse_pad_layer(pad, layer)
pad_dict["layers"] = [letter]
pads.append(pad_dict)
return pads
else:
pad_dict = self.parse_pad_layer(pad, layers_set[0])
pad_dict["layers"] = layers
return [pad_dict]
def parse_pad_layer(self, pad, layer):
# type: (pcbnew.PAD, int) -> dict | None
pos = self.normalize(pad.GetPosition())
try:
size = self.normalize(pad.GetSize(layer))
except TypeError:
size = self.normalize(pad.GetSize())
angle = self.normalize_angle(pad.GetOrientation())
shape_lookup = {
pcbnew.PAD_SHAPE_RECT: "rect",
pcbnew.PAD_SHAPE_OVAL: "oval",
pcbnew.PAD_SHAPE_CIRCLE: "circle",
}
if hasattr(pcbnew, "PAD_SHAPE_TRAPEZOID"):
shape_lookup[pcbnew.PAD_SHAPE_TRAPEZOID] = "trapezoid"
if hasattr(pcbnew, "PAD_SHAPE_ROUNDRECT"):
shape_lookup[pcbnew.PAD_SHAPE_ROUNDRECT] = "roundrect"
if hasattr(pcbnew, "PAD_SHAPE_CUSTOM"):
shape_lookup[pcbnew.PAD_SHAPE_CUSTOM] = "custom"
if hasattr(pcbnew, "PAD_SHAPE_CHAMFERED_RECT"):
shape_lookup[pcbnew.PAD_SHAPE_CHAMFERED_RECT] = "chamfrect"
try:
pad_shape = pad.GetShape(layer)
except TypeError:
pad_shape = pad.GetShape()
shape = shape_lookup.get(pad_shape, "")
if shape == "":
self.logger.info("Unsupported pad shape %s, skipping.", pad_shape)
return None
pad_dict = {
"pos": pos,
"size": size,
"angle": angle,
"shape": shape
}
if shape == "custom":
polygon_set = pcbnew.SHAPE_POLY_SET()
try:
pad.MergePrimitivesAsPolygon(layer, polygon_set)
except TypeError:
pad.MergePrimitivesAsPolygon(polygon_set)
if polygon_set.HasHoles():
self.logger.warn('Detected holes in custom pad polygons')
pad_dict["polygons"] = self.parse_poly_set(polygon_set)
if shape == "trapezoid":
# treat trapezoid as custom shape
pad_dict["shape"] = "custom"
try:
delta = self.normalize(pad.GetDelta(layer))
except TypeError:
delta = self.normalize(pad.GetDelta())
pad_dict["polygons"] = [[
[size[0] / 2 + delta[1] / 2, size[1] / 2 - delta[0] / 2],
[-size[0] / 2 - delta[1] / 2, size[1] / 2 + delta[0] / 2],
[-size[0] / 2 + delta[1] / 2, -size[1] / 2 - delta[0] / 2],
[size[0] / 2 - delta[1] / 2, -size[1] / 2 + delta[0] / 2],
]]
if shape in ["roundrect", "chamfrect"]:
try:
pad_dict["radius"] = pad.GetRoundRectCornerRadius(layer) * 1e-6
except TypeError:
pad_dict["radius"] = pad.GetRoundRectCornerRadius() * 1e-6
if shape == "chamfrect":
try:
pad_dict["chamfpos"] = pad.GetChamferPositions(layer)
pad_dict["chamfratio"] = pad.GetChamferRectRatio(layer)
except TypeError:
pad_dict["chamfpos"] = pad.GetChamferPositions()
pad_dict["chamfratio"] = pad.GetChamferRectRatio()
if self._pad_is_through_hole(pad):
pad_dict["type"] = "th"
pad_dict["drillshape"] = {
pcbnew.PAD_DRILL_SHAPE_CIRCLE: "circle",
pcbnew.PAD_DRILL_SHAPE_OBLONG: "oblong"
}.get(pad.GetDrillShape())
pad_dict["drillsize"] = self.normalize(pad.GetDrillSize())
else:
pad_dict["type"] = "smd"
if hasattr(pad, "GetOffset"):
try:
pad_dict["offset"] = self.normalize(pad.GetOffset(layer))
except TypeError:
pad_dict["offset"] = self.normalize(pad.GetOffset())
if self.config.include_nets:
pad_dict["net"] = pad.GetNetname()
return pad_dict
def parse_footprints(self):
# type: () -> list
footprints = []
for f in self.footprints:
ref = f.GetReference()
# bounding box
if hasattr(pcbnew, 'MODULE'):
f_copy = pcbnew.MODULE(f)
else:
f_copy = pcbnew.FOOTPRINT(f)
try:
f_copy.SetOrientation(0)
except TypeError:
f_copy.SetOrientation(
pcbnew.EDA_ANGLE(0, pcbnew.TENTHS_OF_A_DEGREE_T))
pos = f_copy.GetPosition()
pos.x = pos.y = 0
f_copy.SetPosition(pos)
if hasattr(f_copy, 'GetFootprintRect'):
footprint_rect = f_copy.GetFootprintRect()
else:
try:
footprint_rect = f_copy.GetBoundingBox(False, False)
except TypeError:
footprint_rect = f_copy.GetBoundingBox(False)
bbox = {
"pos": self.normalize(f.GetPosition()),
"relpos": self.normalize(footprint_rect.GetPosition()),
"size": self.normalize(footprint_rect.GetSize()),
"angle": self.normalize_angle(f.GetOrientation()),
}
# graphical drawings
drawings = []
for d in f.GraphicalItems():
# we only care about copper ones, silkscreen is taken care of
if d.GetLayer() not in [pcbnew.F_Cu, pcbnew.B_Cu]:
continue
for drawing in self.parse_drawing(d):
drawings.append({
"layer": "F" if d.GetLayer() == pcbnew.F_Cu else "B",
"drawing": drawing,
})
# footprint pads
pads = []
for p in f.Pads():
for pad_dict in self.parse_pad(p):
pads.append((p.GetPadName(), pad_dict))
if pads:
# Try to guess first pin name.
pads = sorted(pads, key=lambda el: el[0])
pin1_pads = [p for p in pads if p[0] in
['1', 'A', 'A1', 'P1', 'PAD1']]
if pin1_pads:
pin1_pad_name = pin1_pads[0][0]
else:
# No pads have common first pin name,
# pick lexicographically smallest.
pin1_pad_name = pads[0][0]
for pad_name, pad_dict in pads:
if pad_name == pin1_pad_name:
pad_dict['pin1'] = 1
pads = [p[1] for p in pads]
# add footprint
footprints.append({
"ref": ref,
"bbox": bbox,
"pads": pads,
"drawings": drawings,
"layer": {
pcbnew.F_Cu: "F",
pcbnew.B_Cu: "B"
}.get(f.GetLayer())
})
return footprints
def parse_tracks(self, tracks):
tent_vias = True
if hasattr(self.board, "GetTentVias"):
tent_vias = self.board.GetTentVias()
result = {pcbnew.F_Cu: [], pcbnew.B_Cu: []}
for track in tracks:
if track.GetClass() in ["VIA", "PCB_VIA"]:
track_dict = {
"start": self.normalize(track.GetStart()),
"end": self.normalize(track.GetEnd()),
"width": track.GetWidth() * 1e-6,
"net": track.GetNetname(),
}
if not tent_vias:
track_dict["drillsize"] = track.GetDrillValue() * 1e-6
for layer in [pcbnew.F_Cu, pcbnew.B_Cu]:
if track.IsOnLayer(layer):
result[layer].append(track_dict)
else:
if track.GetLayer() in [pcbnew.F_Cu, pcbnew.B_Cu]:
if track.GetClass() in ["ARC", "PCB_ARC"]:
a1, a2 = self.get_arc_angles(track)
track_dict = {
"center": self.normalize(track.GetCenter()),
"startangle": a1,
"endangle": a2,
"radius": track.GetRadius() * 1e-6,
"width": track.GetWidth() * 1e-6,
}
else:
track_dict = {
"start": self.normalize(track.GetStart()),
"end": self.normalize(track.GetEnd()),
"width": track.GetWidth() * 1e-6,
}
if self.config.include_nets:
track_dict["net"] = track.GetNetname()
result[track.GetLayer()].append(track_dict)
return {
'F': result.get(pcbnew.F_Cu),
'B': result.get(pcbnew.B_Cu)
}
def parse_zones(self, zones):
# type: (list[pcbnew.ZONE]) -> dict
result = {pcbnew.F_Cu: [], pcbnew.B_Cu: []}
for zone in zones:
if (not zone.IsFilled() or
hasattr(zone, 'GetIsKeepout') and zone.GetIsKeepout() or
hasattr(zone, 'GetIsRuleArea') and zone.GetIsRuleArea()):
continue
layers = [layer for layer in list(zone.GetLayerSet().Seq())
if layer in [pcbnew.F_Cu, pcbnew.B_Cu]]
for layer in layers:
try:
# kicad 5.1 and earlier
poly_set = zone.GetFilledPolysList()
except TypeError:
poly_set = zone.GetFilledPolysList(layer)
width = zone.GetMinThickness() * 1e-6
if (hasattr(zone, 'GetFilledPolysUseThickness') and
not zone.GetFilledPolysUseThickness()):
width = 0
zone_dict = {
"polygons": self.parse_poly_set(poly_set),
"width": width,
}
if self.config.include_nets:
zone_dict["net"] = zone.GetNetname()
result[layer].append(zone_dict)
return {
'F': result.get(pcbnew.F_Cu),
'B': result.get(pcbnew.B_Cu)
}
@staticmethod
def parse_netlist(net_info):
# type: (pcbnew.NETINFO_LIST) -> list
nets = net_info.NetsByName().asdict().keys()
nets = sorted([str(s) for s in nets])
return nets
@staticmethod
def footprint_to_component(footprint, extra_fields):
try:
footprint_name = str(footprint.GetFPID().GetFootprintName())
except AttributeError:
footprint_name = str(footprint.GetFPID().GetLibItemName())
attr = 'Normal'
if hasattr(pcbnew, 'FP_EXCLUDE_FROM_BOM'):
if footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
attr = 'Virtual'
elif hasattr(pcbnew, 'MOD_VIRTUAL'):
if footprint.GetAttributes() == pcbnew.MOD_VIRTUAL:
attr = 'Virtual'
layer = {
pcbnew.F_Cu: 'F',
pcbnew.B_Cu: 'B',
}.get(footprint.GetLayer())
return Component(footprint.GetReference(),
footprint.GetValue(),
footprint_name,
layer,
attr,
extra_fields)
def parse(self):
from ..errors import ParsingException
# Get extra field data from netlist
field_set = set(self.config.show_fields)
field_set.discard("Value")
field_set.discard("Footprint")
need_extra_fields = (field_set or
self.config.board_variant_whitelist or
self.config.board_variant_blacklist or
self.config.dnp_field)
if not self.config.extra_data_file and need_extra_fields:
self.logger.warn('Ignoring extra fields related config parameters '
'since no netlist/xml file was specified.')
need_extra_fields = False
extra_field_data = None
if (self.config.extra_data_file and
os.path.isfile(self.config.extra_data_file)):
extra_field_data = self.parse_extra_data(
self.config.extra_data_file, self.config.normalize_field_case)
if extra_field_data is None and need_extra_fields:
raise ParsingException(
'Failed parsing %s' % self.config.extra_data_file)
title_block = self.board.GetTitleBlock()
title = title_block.GetTitle()
revision = title_block.GetRevision()
company = title_block.GetCompany()
file_date = title_block.GetDate()
if (hasattr(self.board, "GetProject") and
hasattr(pcbnew, "ExpandTextVars")):
project = self.board.GetProject()
title = pcbnew.ExpandTextVars(title, project)
revision = pcbnew.ExpandTextVars(revision, project)
company = pcbnew.ExpandTextVars(company, project)
file_date = pcbnew.ExpandTextVars(file_date, project)
if not file_date:
file_mtime = os.path.getmtime(self.file_name)
file_date = datetime.fromtimestamp(file_mtime).strftime(
'%Y-%m-%d %H:%M:%S')
pcb_file_name = os.path.basename(self.file_name)
if not title:
# remove .kicad_pcb extension
title = os.path.splitext(pcb_file_name)[0]
edges, bbox = self.parse_edges(self.board)
if bbox is None:
self.logger.error('Please draw pcb outline on the edges '
'layer on sheet or any footprint before '
'generating BOM.')
return None, None
bbox = {
"minx": bbox.GetPosition().x * 1e-6,
"miny": bbox.GetPosition().y * 1e-6,
"maxx": bbox.GetRight() * 1e-6,
"maxy": bbox.GetBottom() * 1e-6,
}
drawings = self.get_all_drawings()
pcbdata = {
"edges_bbox": bbox,
"edges": edges,
"drawings": {
"silkscreen": self.parse_drawings_on_layers(
drawings, pcbnew.F_SilkS, pcbnew.B_SilkS),
"fabrication": self.parse_drawings_on_layers(
drawings, pcbnew.F_Fab, pcbnew.B_Fab),
},
"footprints": self.parse_footprints(),
"metadata": {
"title": title,
"revision": revision,
"company": company,
"date": file_date,
},
"bom": {},
"font_data": self.font_parser.get_parsed_font()
}
if self.config.include_tracks:
pcbdata["tracks"] = self.parse_tracks(self.board.GetTracks())
if hasattr(self.board, "Zones"):
pcbdata["zones"] = self.parse_zones(self.board.Zones())
else:
self.logger.info("Zones not supported for KiCad 4, skipping")
pcbdata["zones"] = {'F': [], 'B': []}
if self.config.include_nets and hasattr(self.board, "GetNetInfo"):
pcbdata["nets"] = self.parse_netlist(self.board.GetNetInfo())
if extra_field_data and need_extra_fields:
extra_fields = extra_field_data.fields_by_index
if extra_fields:
extra_fields = extra_fields.values()
if extra_fields is None:
extra_fields = []
field_map = extra_field_data.fields_by_ref
warning_shown = False
for f in self.footprints:
extra_fields.append(field_map.get(f.GetReference(), {}))
if f.GetReference() not in field_map:
# Some components are on pcb but not in schematic data.
# Show a warning about outdated extra data file.
self.logger.warn(
'Component %s is missing from schematic data.'
% f.GetReference())
warning_shown = True
if warning_shown:
self.logger.warn('Netlist/xml file is likely out of date.')
else:
extra_fields = [{}] * len(self.footprints)
components = [self.footprint_to_component(f, e)
for (f, e) in zip(self.footprints, extra_fields)]
return pcbdata, components
class InteractiveHtmlBomPlugin(pcbnew.ActionPlugin, object):
def __init__(self):
super(InteractiveHtmlBomPlugin, self).__init__()
self.name = "Generate Interactive HTML BOM"
self.category = "Read PCB"
self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
self.show_toolbar_button = True
icon_dir = os.path.dirname(os.path.dirname(__file__))
self.icon_file_name = os.path.join(icon_dir, 'icon.png')
self.description = "Generate interactive HTML page with BOM " \
"table and pcb drawing."
def defaults(self):
pass
def Run(self):
from ..version import version
from ..errors import ParsingException
logger = ibom.Logger()
board = pcbnew.GetBoard()
pcb_file_name = board.GetFileName()
if not pcb_file_name:
logger.error('Please save the board file before generating BOM.')
return
config = Config(version, os.path.dirname(pcb_file_name))
parser = PcbnewParser(pcb_file_name, config, logger, board)
try:
ibom.run_with_dialog(parser, config, logger)
except ParsingException as e:
logger.error(str(e))

View File

@ -0,0 +1,59 @@
import os
import pcbnew
from .xmlparser import XmlParser
from .netlistparser import NetlistParser
PARSERS = {
'.xml': XmlParser,
'.net': NetlistParser,
}
if hasattr(pcbnew, 'FOOTPRINT'):
PARSERS['.kicad_pcb'] = None
def parse_schematic_data(file_name):
if not os.path.isfile(file_name):
return None
extension = os.path.splitext(file_name)[1]
if extension not in PARSERS:
return None
else:
parser_cls = PARSERS[extension]
if parser_cls is None:
return None
parser = parser_cls(file_name)
return parser.get_extra_field_data()
def find_latest_schematic_data(base_name, directories):
"""
:param base_name: base name of pcb file
:param directories: list of directories to search
:return: last modified parsable file path or None if not found
"""
files = []
for d in directories:
files.extend(_find_in_dir(d))
# sort by decreasing modification time
files = sorted(files, reverse=True)
if files:
# try to find first (last modified) file that has name matching pcb file
for _, f in files:
if os.path.splitext(os.path.basename(f))[0] == base_name:
return f
# if no such file is found just return last modified
return files[0][1]
else:
return None
def _find_in_dir(dir):
_, _, files = next(os.walk(dir), (None, None, []))
# filter out files that we can not parse
files = [f for f in files if os.path.splitext(f)[1] in PARSERS.keys()]
files = [os.path.join(dir, f) for f in files]
# get their modification time and sort in descending order
return [(os.path.getmtime(f), f) for f in files]

View File

@ -0,0 +1,59 @@
import io
from .parser_base import ParserBase
from .sexpressions import parse_sexpression
class NetlistParser(ParserBase):
def get_extra_field_data(self):
with io.open(self.file_name, 'r', encoding='utf-8') as f:
sexpression = parse_sexpression(f.read())
components = None
for s in sexpression:
if s[0] == 'components':
components = s[1:]
if components is None:
return None
field_set = set()
comp_dict = {}
for c in components:
ref = None
fields = None
datasheet = None
libsource = None
dnp = False
for f in c[1:]:
if f[0] == 'ref':
ref = f[1]
if f[0] == 'fields':
fields = f[1:]
if f[0] == 'datasheet':
datasheet = f[1]
if f[0] == 'libsource':
libsource = f[1:]
if f[0] == 'property' and isinstance(f[1], list) and \
f[1][0] == 'name' and f[1][1] == 'dnp':
dnp = True
if ref is None:
return None
ref_fields = comp_dict.setdefault(ref, {})
if datasheet and datasheet != '~':
field_set.add('Datasheet')
ref_fields['Datasheet'] = datasheet
if libsource is not None:
for lib_field in libsource:
if lib_field[0] == 'description':
field_set.add('Description')
ref_fields['Description'] = lib_field[1]
if dnp:
field_set.add('kicad_dnp')
ref_fields['kicad_dnp'] = "DNP"
if fields is None:
continue
for f in fields:
if len(f) > 1:
field_set.add(f[1][1])
if len(f) > 2:
ref_fields[f[1][1]] = f[2]
return list(field_set), comp_dict

View File

@ -0,0 +1,26 @@
class ParserBase:
def __init__(self, file_name):
"""
:param file_name: path to file that should be parsed.
"""
self.file_name = file_name
def get_extra_field_data(self):
# type: () -> tuple
"""
Parses the file and returns extra field data.
:return: tuple of the format
(
[field_name1, field_name2,... ],
{
ref1: {
field_name1: field_value1,
field_name2: field_value2,
...
],
ref2: ...
}
)
"""
pass

View File

@ -0,0 +1,32 @@
import re
term_regex = r'''(?mx)
\s*(?:
(?P<open>\()|
(?P<close>\))|
(?P<sq>"(?:\\\\|\\"|[^"])*")|
(?P<s>[^(^)\s]+)
)'''
pattern = re.compile(term_regex)
def parse_sexpression(sexpression):
stack = []
out = []
for terms in pattern.finditer(sexpression):
term, value = [(t, v) for t, v in terms.groupdict().items() if v][0]
if term == 'open':
stack.append(out)
out = []
elif term == 'close':
assert stack, "Trouble with nesting of brackets"
tmp, out = out, stack.pop(-1)
out.append(tmp)
elif term == 'sq':
out.append(value[1:-1].replace('\\\\', '\\').replace('\\"', '"'))
elif term == 's':
out.append(value)
else:
raise NotImplementedError("Error: %s, %s" % (term, value))
assert not stack, "Trouble with nesting of brackets"
return out[0]

View File

@ -0,0 +1,42 @@
from xml.dom import minidom
from .parser_base import ParserBase
class XmlParser(ParserBase):
@staticmethod
def get_text(nodelist):
rc = []
for node in nodelist:
if node.nodeType == node.TEXT_NODE:
rc.append(node.data)
return ''.join(rc)
def get_extra_field_data(self):
xml = minidom.parse(self.file_name)
components = xml.getElementsByTagName('comp')
field_set = set()
comp_dict = {}
for c in components:
ref_fields = comp_dict.setdefault(c.attributes['ref'].value, {})
datasheet = c.getElementsByTagName('datasheet')
if datasheet:
datasheet = self.get_text(datasheet[0].childNodes)
if datasheet != '~':
field_set.add('Datasheet')
ref_fields['Datasheet'] = datasheet
libsource = c.getElementsByTagName('libsource')
if libsource and libsource[0].hasAttribute('description'):
field_set.add('Description')
attr = libsource[0].attributes['description']
ref_fields['Description'] = attr.value
for f in c.getElementsByTagName('field'):
name = f.attributes['name'].value
field_set.add(name)
ref_fields[name] = self.get_text(f.childNodes)
for f in c.getElementsByTagName('property'):
if f.attributes['name'].value == 'dnp':
field_set.add('kicad_dnp')
ref_fields['kicad_dnp'] = "DNP"
return list(field_set), comp_dict

View File

@ -0,0 +1,647 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$ref": "#/definitions/GenericJSONPCBData",
"definitions": {
"GenericJSONPCBData": {
"type": "object",
"additionalProperties": false,
"properties": {
"spec_version": {
"type": "integer"
},
"pcbdata": {
"$ref": "#/definitions/Pcbdata"
},
"components": {
"type": "array",
"items": {
"$ref": "#/definitions/Component"
}
}
},
"required": [
"spec_version",
"pcbdata",
"components"
],
"title": "GenericJSONPCBData"
},
"Component": {
"type": "object",
"additionalProperties": false,
"properties": {
"attr": {
"type": "string"
},
"footprint": {
"type": "string"
},
"layer": {
"$ref": "#/definitions/Layer"
},
"ref": {
"type": "string"
},
"val": {
"type": "string"
},
"extra_fields": {
"$ref": "#/definitions/ExtraData"
}
},
"required": [
"footprint",
"layer",
"ref",
"val"
],
"title": "Component"
},
"Pcbdata": {
"type": "object",
"additionalProperties": false,
"properties": {
"edges_bbox": {
"$ref": "#/definitions/EdgesBbox"
},
"edges": {
"$ref": "#/definitions/DrawingArray"
},
"drawings": {
"$ref": "#/definitions/LayerDrawings"
},
"footprints": {
"type": "array",
"items": {
"$ref": "#/definitions/Footprint"
}
},
"metadata": {
"$ref": "#/definitions/Metadata"
},
"tracks": {
"$ref": "#/definitions/Tracks"
},
"zones": {
"$ref": "#/definitions/Zones"
},
"nets": {
"type": "array",
"items": { "type": "string" }
},
"font_data": {
"$ref": "#/definitions/FontData"
}
},
"required": [
"edges_bbox",
"edges",
"drawings",
"footprints",
"metadata"
],
"dependencies": {
"tracks": { "required": ["zones"] },
"zones": { "required": ["tracks"] }
},
"title": "Pcbdata"
},
"EdgesBbox": {
"type": "object",
"additionalProperties": false,
"properties": {
"minx": {
"type": "number"
},
"miny": {
"type": "number"
},
"maxx": {
"type": "number"
},
"maxy": {
"type": "number"
}
},
"required": ["minx", "miny", "maxx", "maxy"],
"title": "EdgesBbox"
},
"DrawingSet": {
"type": "object",
"additionalProperties": false,
"properties": {
"F": {
"$ref": "#/definitions/DrawingArray"
},
"B": {
"$ref": "#/definitions/DrawingArray"
}
},
"required": ["F", "B"],
"title": "DrawingSet"
},
"Footprint": {
"type": "object",
"additionalProperties": false,
"properties": {
"ref": {
"type": "string"
},
"center": {
"$ref": "#/definitions/Coordinates"
},
"bbox": {
"$ref": "#/definitions/Bbox"
},
"pads": {
"type": "array",
"items": {
"$ref": "#/definitions/Pad"
}
},
"drawings": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"layer": { "$ref": "#/definitions/Layer" },
"drawing": { "$ref": "#/definitions/Drawing" }
},
"required": [ "layer", "drawing" ]
}
},
"layer": {
"$ref": "#/definitions/Layer"
}
},
"required": ["ref", "center", "bbox", "pads", "drawings", "layer"],
"title": "Footprint"
},
"Bbox": {
"type": "object",
"additionalProperties": false,
"properties": {
"pos": {
"$ref": "#/definitions/Coordinates"
},
"relpos": {
"$ref": "#/definitions/Coordinates"
},
"size": {
"$ref": "#/definitions/Coordinates"
},
"angle": {
"type": "number"
}
},
"required": ["pos", "relpos", "size", "angle"],
"title": "Bbox"
},
"Pad": {
"type": "object",
"additionalProperties": false,
"properties": {
"layers": {
"type": "array",
"items": {
"$ref": "#/definitions/Layer"
},
"minItems": 1,
"maxItems": 2
},
"pos": {
"$ref": "#/definitions/Coordinates"
},
"size": {
"$ref": "#/definitions/Coordinates"
},
"angle": {
"type": "number"
},
"shape": {
"$ref": "#/definitions/Shape"
},
"svgpath": { "type": "string" },
"polygons": { "$ref": "#/definitions/Polygons" },
"radius": { "type": "number" },
"chamfpos": { "type": "integer" },
"chamfratio": { "type": "number" },
"type": {
"$ref": "#/definitions/PadType"
},
"pin1": {
"type": "integer", "const": 1
},
"drillshape": {
"$ref": "#/definitions/Drillshape"
},
"drillsize": {
"$ref": "#/definitions/Coordinates"
},
"offset": {
"$ref": "#/definitions/Coordinates"
},
"net": { "type": "string" }
},
"required": [
"layers",
"pos",
"size",
"shape",
"type"
],
"allOf": [
{
"if": {
"properties": { "shape": { "const": "custom" } }
},
"then": {
"anyOf": [
{ "required": [ "svgpath" ] },
{ "required": [ "pos", "angle", "polygons" ] }
]
}
},
{
"if": {
"properties": { "shape": { "const": "roundrect" } }
},
"then": {
"required": [ "radius" ]
}
},
{
"if": {
"properties": { "shape": { "const": "chamfrect" } }
},
"then": {
"required": [ "radius", "chamfpos", "chamfratio" ]
}
},
{
"if": {
"properties": { "type": { "const": "th" } }
},
"then": {
"required": [ "drillshape", "drillsize" ]
}
}
],
"title": "Pad"
},
"Metadata": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": {
"type": "string"
},
"revision": {
"type": "string"
},
"company": {
"type": "string"
},
"date": {
"type": "string"
}
},
"required": ["title", "revision", "company", "date"],
"title": "Metadata"
},
"LayerDrawings": {
"type": "object",
"items": {
"silkscreen": {
"$ref": "#/definitions/DrawingSet"
},
"fabrication": {
"$ref": "#/definitions/DrawingSet"
}
}
},
"DrawingArray": {
"type": "array",
"items": {
"$ref": "#/definitions/Drawing"
}
},
"Drawing": {
"type": "object",
"oneOf": [
{ "$ref": "#/definitions/DrawingSegment" },
{ "$ref": "#/definitions/DrawingRect" },
{ "$ref": "#/definitions/DrawingCircle" },
{ "$ref": "#/definitions/DrawingArc" },
{ "$ref": "#/definitions/DrawingCurve" },
{ "$ref": "#/definitions/DrawingPolygon" },
{ "$ref": "#/definitions/DrawingText" }
]
},
"DrawingSegment": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "type": "string", "const": "segment" },
"start": { "$ref": "#/definitions/Coordinates" },
"end": { "$ref": "#/definitions/Coordinates" },
"width": { "type": "number" }
},
"required": ["type", "start", "end", "width"],
"title": "DrawingSegment"
},
"DrawingRect": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "rect" },
"start": { "$ref": "#/definitions/Coordinates" },
"end": { "$ref": "#/definitions/Coordinates" },
"width": { "type": "number" }
},
"required": ["type", "start", "end", "width"],
"title": "DrawingRect"
},
"DrawingCircle": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "circle" },
"start": { "$ref": "#/definitions/Coordinates" },
"radius": { "type": "number" },
"filled": { "type": "integer" },
"width": { "type": "number" }
},
"required": ["type", "start", "radius", "width"],
"title": "DrawingCircle"
},
"DrawingArc": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "arc" },
"width": { "type": "number" },
"svgpath": { "type": "string" },
"start": { "$ref": "#/definitions/Coordinates" },
"radius": { "type": "number" },
"startangle": { "type": "number" },
"endangle": { "type": "number" }
},
"required": [
"type",
"width"
],
"anyOf": [
{ "required": ["svgpath"] },
{ "required": ["start", "radius", "startangle", "endangle"] }
],
"title": "DrawingArc"
},
"DrawingCurve": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "curve" },
"start": { "$ref": "#/definitions/Coordinates" },
"end": { "$ref": "#/definitions/Coordinates" },
"cpa": { "$ref": "#/definitions/Coordinates" },
"cpb": { "$ref": "#/definitions/Coordinates" },
"width": { "type": "number" }
},
"required": ["type", "start", "end", "cpa", "cpb", "width"],
"title": "DrawingCurve"
},
"DrawingPolygon": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": { "const": "polygon" },
"filled": { "type": "integer" },
"width": { "type": "number" },
"svgpath": { "type": "string" },
"pos": { "$ref": "#/definitions/Coordinates" },
"angle": { "type": "number" },
"polygons": {
"type": "array",
"items": {
"type": "array",
"items": { "$ref": "#/definitions/Coordinates" }
}
}
},
"required": ["type"],
"anyOf": [
{ "required": ["svgpath"] },
{ "required": ["pos", "angle", "polygons"] }
],
"title": "DrawingPolygon"
},
"DrawingText": {
"type": "object",
"additionalProperties": false,
"properties": {
"svgpath": { "type": "string" },
"thickness": { "type": "number" },
"fillrule": {
"type": "string",
"enum": [
"nonzero",
"evenodd"
]
},
"ref": { "type": "integer" , "const": 1 },
"val": { "type": "integer" , "const": 1 }
},
"required": ["svgpath"],
"oneOf": [
{ "required": ["thickness"] },
{ "required": ["fillrule"] }
],
"title": "DrawingText"
},
"Coordinates": {
"type": "array",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
},
"Drillshape": {
"type": "string",
"enum": [
"circle",
"oblong",
"rect"
],
"title": "Drillshape"
},
"Layer": {
"type": "string",
"enum": [
"B",
"F"
],
"title": "Layer"
},
"Shape": {
"type": "string",
"enum": [
"rect",
"circle",
"oval",
"roundrect",
"chamfrect",
"custom"
],
"title": "Shape"
},
"PadType": {
"type": "string",
"enum": [
"smd",
"th"
],
"title": "PadType"
},
"Tracks": {
"type": "object",
"additionalProperties": false,
"properties": {
"F": {
"type": "array",
"items": { "$ref": "#/definitions/Track" }
},
"B": {
"type": "array",
"items": { "$ref": "#/definitions/Track" }
}
},
"required": [ "F", "B" ],
"title": "Tracks"
},
"Track": {
"type": "object",
"oneOf":[
{
"additionalProperties": false,
"properties": {
"start": { "$ref": "#/definitions/Coordinates" },
"end": { "$ref": "#/definitions/Coordinates" },
"width": { "type": "number" },
"drillsize": { "type": "number" },
"net": { "type": "string" }
},
"required": [
"start",
"end",
"width"
]
},
{
"additionalProperties": false,
"properties": {
"center": { "$ref": "#/definitions/Coordinates" },
"startangle": { "type": "number" },
"endangle": { "type": "number" },
"radius": { "type": "number" },
"width": { "type": "number" },
"net": { "type": "string" }
},
"required": [
"center",
"startangle",
"endangle",
"radius",
"width"
]
}
]
},
"Zones": {
"type": "object",
"additionalProperties": false,
"properties": {
"F": {
"type": "array",
"items": { "$ref": "#/definitions/Zone" }
},
"B": {
"type": "array",
"items": { "$ref": "#/definitions/Zone" }
}
},
"required": [ "F", "B" ],
"title": "Zones"
},
"Zone": {
"type": "object",
"additionalProperties": false,
"properties": {
"svgpath": { "type": "string" },
"polygons": {
"$ref": "#/definitions/Polygons"
},
"net": { "type": "string" },
"fillrule": {
"type": "string",
"enum": [
"nonzero",
"evenodd"
]
}
},
"anyOf": [
{ "required": [ "svgpath" ] },
{ "required": [ "polygons" ] }
],
"title": "Zone"
},
"Polygons": {
"type": "array",
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/Coordinates"
}
}
},
"PolyLineArray": {
"$ref": "#/definitions/Polygons"
},
"ReferenceSet": {
"type": "array",
"items": {
"type": "array",
"items": [
{ "type": "string" },
{ "type": "integer" }
],
"additionalItems": false
}
},
"ExtraData": {
"type": "object",
"additionalProperties": true,
"properties": {
},
"title": "ExtraData"
},
"FontData": {
"type": "object",
"patternProperties": {
"^.$" : {
"type": "object",
"properties": {
"w": { "type": "number" },
"l": { "$ref": "#/definitions/PolyLineArray" }
},
"additionalProperties" : false,
"required": [
"w",
"l"
]
}
}
}
}
}

View File

@ -0,0 +1,538 @@
"""This submodule contains very stripped down bare bones version of
svgpathtools module:
https://github.com/mathandy/svgpathtools
All external dependencies are removed. This code can parse path strings with
segments and arcs, calculate bounding box and that's about it. This is all
that is needed in ibom parsers at the moment.
"""
# External dependencies
from __future__ import division, absolute_import, print_function
import re
from cmath import exp
from math import sqrt, cos, sin, acos, degrees, radians, pi
def clip(a, a_min, a_max):
return min(a_max, max(a_min, a))
class Line(object):
def __init__(self, start, end):
self.start = start
self.end = end
def __repr__(self):
return 'Line(start=%s, end=%s)' % (self.start, self.end)
def __eq__(self, other):
if not isinstance(other, Line):
return NotImplemented
return self.start == other.start and self.end == other.end
def __ne__(self, other):
if not isinstance(other, Line):
return NotImplemented
return not self == other
def __len__(self):
return 2
def bbox(self):
"""returns the bounding box for the segment in the form
(xmin, xmax, ymin, ymax)."""
xmin = min(self.start.real, self.end.real)
xmax = max(self.start.real, self.end.real)
ymin = min(self.start.imag, self.end.imag)
ymax = max(self.start.imag, self.end.imag)
return xmin, xmax, ymin, ymax
class Arc(object):
def __init__(self, start, radius, rotation, large_arc, sweep, end,
autoscale_radius=True):
"""
This should be thought of as a part of an ellipse connecting two
points on that ellipse, start and end.
Parameters
----------
start : complex
The start point of the curve. Note: `start` and `end` cannot be the
same. To make a full ellipse or circle, use two `Arc` objects.
radius : complex
rx + 1j*ry, where rx and ry are the radii of the ellipse (also
known as its semi-major and semi-minor axes, or vice-versa or if
rx < ry).
Note: If rx = 0 or ry = 0 then this arc is treated as a
straight line segment joining the endpoints.
Note: If rx or ry has a negative sign, the sign is dropped; the
absolute value is used instead.
Note: If no such ellipse exists, the radius will be scaled so
that one does (unless autoscale_radius is set to False).
rotation : float
This is the CCW angle (in degrees) from the positive x-axis of the
current coordinate system to the x-axis of the ellipse.
large_arc : bool
Given two points on an ellipse, there are two elliptical arcs
connecting those points, the first going the short way around the
ellipse, and the second going the long way around the ellipse. If
`large_arc == False`, the shorter elliptical arc will be used. If
`large_arc == True`, then longer elliptical will be used.
In other words, `large_arc` should be 0 for arcs spanning less than
or equal to 180 degrees and 1 for arcs spanning greater than 180
degrees.
sweep : bool
For any acceptable parameters `start`, `end`, `rotation`, and
`radius`, there are two ellipses with the given major and minor
axes (radii) which connect `start` and `end`. One which connects
them in a CCW fashion and one which connected them in a CW
fashion. If `sweep == True`, the CCW ellipse will be used. If
`sweep == False`, the CW ellipse will be used. See note on curve
orientation below.
end : complex
The end point of the curve. Note: `start` and `end` cannot be the
same. To make a full ellipse or circle, use two `Arc` objects.
autoscale_radius : bool
If `autoscale_radius == True`, then will also scale `self.radius`
in the case that no ellipse exists with the input parameters
(see inline comments for further explanation).
Derived Parameters/Attributes
-----------------------------
self.theta : float
This is the phase (in degrees) of self.u1transform(self.start).
It is $\\theta_1$ in the official documentation and ranges from
-180 to 180.
self.delta : float
This is the angular distance (in degrees) between the start and
end of the arc after the arc has been sent to the unit circle
through self.u1transform().
It is $\\Delta\\theta$ in the official documentation and ranges
from -360 to 360; being positive when the arc travels CCW and
negative otherwise (i.e. is positive/negative when
sweep == True/False).
self.center : complex
This is the center of the arc's ellipse.
self.phi : float
The arc's rotation in radians, i.e. `radians(self.rotation)`.
self.rot_matrix : complex
Equal to `exp(1j * self.phi)` which is also equal to
`cos(self.phi) + 1j*sin(self.phi)`.
Note on curve orientation (CW vs CCW)
-------------------------------------
The notions of clockwise (CW) and counter-clockwise (CCW) are reversed
in some sense when viewing SVGs (as the y coordinate starts at the top
of the image and increases towards the bottom).
"""
assert start != end
assert radius.real != 0 and radius.imag != 0
self.start = start
self.radius = abs(radius.real) + 1j * abs(radius.imag)
self.rotation = rotation
self.large_arc = bool(large_arc)
self.sweep = bool(sweep)
self.end = end
self.autoscale_radius = autoscale_radius
# Convenience parameters
self.phi = radians(self.rotation)
self.rot_matrix = exp(1j * self.phi)
# Derive derived parameters
self._parameterize()
def __repr__(self):
params = (self.start, self.radius, self.rotation,
self.large_arc, self.sweep, self.end)
return ("Arc(start={}, radius={}, rotation={}, "
"large_arc={}, sweep={}, end={})".format(*params))
def __eq__(self, other):
if not isinstance(other, Arc):
return NotImplemented
return self.start == other.start and self.end == other.end \
and self.radius == other.radius \
and self.rotation == other.rotation \
and self.large_arc == other.large_arc and self.sweep == other.sweep
def __ne__(self, other):
if not isinstance(other, Arc):
return NotImplemented
return not self == other
def _parameterize(self):
# See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
# my notation roughly follows theirs
rx = self.radius.real
ry = self.radius.imag
rx_sqd = rx * rx
ry_sqd = ry * ry
# Transform z-> z' = x' + 1j*y'
# = self.rot_matrix**(-1)*(z - (end+start)/2)
# coordinates. This translates the ellipse so that the midpoint
# between self.end and self.start lies on the origin and rotates
# the ellipse so that the its axes align with the xy-coordinate axes.
# Note: This sends self.end to -self.start
zp1 = (1 / self.rot_matrix) * (self.start - self.end) / 2
x1p, y1p = zp1.real, zp1.imag
x1p_sqd = x1p * x1p
y1p_sqd = y1p * y1p
# Correct out of range radii
# Note: an ellipse going through start and end with radius and phi
# exists if and only if radius_check is true
radius_check = (x1p_sqd / rx_sqd) + (y1p_sqd / ry_sqd)
if radius_check > 1:
if self.autoscale_radius:
rx *= sqrt(radius_check)
ry *= sqrt(radius_check)
self.radius = rx + 1j * ry
rx_sqd = rx * rx
ry_sqd = ry * ry
else:
raise ValueError("No such elliptic arc exists.")
# Compute c'=(c_x', c_y'), the center of the ellipse in (x', y') coords
# Noting that, in our new coord system, (x_2', y_2') = (-x_1', -x_2')
# and our ellipse is cut out by of the plane by the algebraic equation
# (x'-c_x')**2 / r_x**2 + (y'-c_y')**2 / r_y**2 = 1,
# we can find c' by solving the system of two quadratics given by
# plugging our transformed endpoints (x_1', y_1') and (x_2', y_2')
tmp = rx_sqd * y1p_sqd + ry_sqd * x1p_sqd
radicand = (rx_sqd * ry_sqd - tmp) / tmp
try:
radical = sqrt(radicand)
except ValueError:
radical = 0
if self.large_arc == self.sweep:
cp = -radical * (rx * y1p / ry - 1j * ry * x1p / rx)
else:
cp = radical * (rx * y1p / ry - 1j * ry * x1p / rx)
# The center in (x,y) coordinates is easy to find knowing c'
self.center = exp(1j * self.phi) * cp + (self.start + self.end) / 2
# Now we do a second transformation, from (x', y') to (u_x, u_y)
# coordinates, which is a translation moving the center of the
# ellipse to the origin and a dilation stretching the ellipse to be
# the unit circle
u1 = (x1p - cp.real) / rx + 1j * (y1p - cp.imag) / ry
u2 = (-x1p - cp.real) / rx + 1j * (-y1p - cp.imag) / ry
# clip in case of floating point error
u1 = clip(u1.real, -1, 1) + 1j * clip(u1.imag, -1, 1)
u2 = clip(u2.real, -1, 1) + 1j * clip(u2.imag, -1, 1)
# Now compute theta and delta (we'll define them as we go)
# delta is the angular distance of the arc (w.r.t the circle)
# theta is the angle between the positive x'-axis and the start point
# on the circle
if u1.imag > 0:
self.theta = degrees(acos(u1.real))
elif u1.imag < 0:
self.theta = -degrees(acos(u1.real))
else:
if u1.real > 0: # start is on pos u_x axis
self.theta = 0
else: # start is on neg u_x axis
# Note: This behavior disagrees with behavior documented in
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
# where theta is set to 0 in this case.
self.theta = 180
det_uv = u1.real * u2.imag - u1.imag * u2.real
acosand = u1.real * u2.real + u1.imag * u2.imag
acosand = clip(acosand.real, -1, 1) + clip(acosand.imag, -1, 1)
if det_uv > 0:
self.delta = degrees(acos(acosand))
elif det_uv < 0:
self.delta = -degrees(acos(acosand))
else:
if u1.real * u2.real + u1.imag * u2.imag > 0:
# u1 == u2
self.delta = 0
else:
# u1 == -u2
# Note: This behavior disagrees with behavior documented in
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
# where delta is set to 0 in this case.
self.delta = 180
if not self.sweep and self.delta >= 0:
self.delta -= 360
elif self.large_arc and self.delta <= 0:
self.delta += 360
def point(self, t):
if t == 0:
return self.start
if t == 1:
return self.end
angle = radians(self.theta + t * self.delta)
cosphi = self.rot_matrix.real
sinphi = self.rot_matrix.imag
rx = self.radius.real
ry = self.radius.imag
# z = self.rot_matrix*(rx*cos(angle) + 1j*ry*sin(angle)) + self.center
x = rx * cosphi * cos(angle) - ry * sinphi * sin(
angle) + self.center.real
y = rx * sinphi * cos(angle) + ry * cosphi * sin(
angle) + self.center.imag
return complex(x, y)
def bbox(self):
"""returns a bounding box for the segment in the form
(xmin, xmax, ymin, ymax)."""
# a(t) = radians(self.theta + self.delta*t)
# = (2*pi/360)*(self.theta + self.delta*t)
# x'=0: ~~~~~~~~~
# -rx*cos(phi)*sin(a(t)) = ry*sin(phi)*cos(a(t))
# -(rx/ry)*cot(phi)*tan(a(t)) = 1
# a(t) = arctan(-(ry/rx)tan(phi)) + pi*k === atan_x
# y'=0: ~~~~~~~~~~
# rx*sin(phi)*sin(a(t)) = ry*cos(phi)*cos(a(t))
# (rx/ry)*tan(phi)*tan(a(t)) = 1
# a(t) = arctan((ry/rx)*cot(phi))
# atanres = arctan((ry/rx)*cot(phi)) === atan_y
# ~~~~~~~~
# (2*pi/360)*(self.theta + self.delta*t) = atanres + pi*k
# Therefore, for both x' and y', we have...
# t = ((atan_{x/y} + pi*k)*(360/(2*pi)) - self.theta)/self.delta
# for all k s.t. 0 < t < 1
from math import atan, tan
if cos(self.phi) == 0:
atan_x = pi / 2
atan_y = 0
elif sin(self.phi) == 0:
atan_x = 0
atan_y = pi / 2
else:
rx, ry = self.radius.real, self.radius.imag
atan_x = atan(-(ry / rx) * tan(self.phi))
atan_y = atan((ry / rx) / tan(self.phi))
def angle_inv(ang, q): # inverse of angle from Arc.derivative()
return ((ang + pi * q) * (360 / (2 * pi)) -
self.theta) / self.delta
xtrema = [self.start.real, self.end.real]
ytrema = [self.start.imag, self.end.imag]
for k in range(-4, 5):
tx = angle_inv(atan_x, k)
ty = angle_inv(atan_y, k)
if 0 <= tx <= 1:
xtrema.append(self.point(tx).real)
if 0 <= ty <= 1:
ytrema.append(self.point(ty).imag)
return min(xtrema), max(xtrema), min(ytrema), max(ytrema)
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
def _tokenize_path(path_def):
for x in COMMAND_RE.split(path_def):
if x in COMMANDS:
yield x
for token in FLOAT_RE.findall(x):
yield token
def parse_path(pathdef, logger, current_pos=0j):
# In the SVG specs, initial movetos are absolute, even if
# specified as 'm'. This is the default behavior here as well.
# But if you pass in a current_pos variable, the initial moveto
# will be relative to that current_pos. This is useful.
elements = list(_tokenize_path(pathdef))
# Reverse for easy use of .pop()
elements.reverse()
absolute = False
segments = []
start_pos = None
command = None
while elements:
if elements[-1] in COMMANDS:
# New command.
command = elements.pop()
absolute = command in UPPERCASE
command = command.upper()
else:
# If this element starts with numbers, it is an implicit command
# and we don't change the command. Check that it's allowed:
if command is None:
raise ValueError(
"Unallowed implicit command in %s, position %s" % (
pathdef, len(pathdef.split()) - len(elements)))
if command == 'M':
# Moveto command.
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if absolute:
current_pos = pos
else:
current_pos += pos
# when M is called, reset start_pos
# This behavior of Z is defined in svg spec:
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
start_pos = current_pos
# Implicit moveto commands are treated as lineto commands.
# So we set command to lineto here, in case there are
# further implicit commands after this moveto.
command = 'L'
elif command == 'Z':
# Close path
if not (current_pos == start_pos):
segments.append(Line(current_pos, start_pos))
current_pos = start_pos
command = None
elif command == 'L':
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if not absolute:
pos += current_pos
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'H':
x = elements.pop()
pos = float(x) + current_pos.imag * 1j
if not absolute:
pos += current_pos.real
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'V':
y = elements.pop()
pos = current_pos.real + float(y) * 1j
if not absolute:
pos += current_pos.imag * 1j
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'C':
logger.warn('Encountered Cubic Bezier segment. '
'It is currently not supported and will be replaced '
'by a line segment.')
for i in range(4):
# ignore control points
elements.pop()
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Line(current_pos, end))
current_pos = end
elif command == 'S':
logger.warn('Encountered Quadratic Bezier segment. '
'It is currently not supported and will be replaced '
'by a line segment.')
for i in range(2):
# ignore control points
elements.pop()
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Line(current_pos, end))
current_pos = end
elif command == 'Q':
logger.warn('Encountered Quadratic Bezier segment. '
'It is currently not supported and will be replaced '
'by a line segment.')
for i in range(2):
# ignore control points
elements.pop()
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Line(current_pos, end))
current_pos = end
elif command == 'T':
logger.warn('Encountered Quadratic Bezier segment. '
'It is currently not supported and will be replaced '
'by a line segment.')
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Line(current_pos, end))
current_pos = end
elif command == 'A':
radius = float(elements.pop()) + float(elements.pop()) * 1j
rotation = float(elements.pop())
arc = float(elements.pop())
sweep = float(elements.pop())
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(
Arc(current_pos, radius, rotation, arc, sweep, end))
current_pos = end
return segments
def create_path(lines, circles=[]):
"""Returns a path d-string."""
def limit_digits(val):
return format(val, '.6f').rstrip('0').replace(',', '.').rstrip('.')
def different_points(a, b):
return abs(a[0] - b[0]) > 1e-6 or abs(a[1] - b[1]) > 1e-6
parts = []
for i, line in enumerate(lines):
if i == 0 or different_points(lines[i - 1][-1], line[0]):
parts.append('M{},{}'.format(*map(limit_digits, line[0])))
for point in line[1:]:
parts.append('L{},{}'.format(*map(limit_digits, point)))
for circle in circles:
cx, cy, r = circle[0][0], circle[0][1], circle[1]
parts.append('M{},{}'.format(limit_digits(cx - r), limit_digits(cy)))
parts.append('a {},{} 0 1,0 {},0'.format(
*map(limit_digits, [r, r, r + r])))
parts.append('a {},{} 0 1,0 -{},0'.format(
*map(limit_digits, [r, r, r + r])))
return ''.join(parts)

View File

@ -0,0 +1,16 @@
import sys
class ExitCodes():
ERROR_PARSE = 3
ERROR_FILE_NOT_FOUND = 4
ERROR_NO_DISPLAY = 5
class ParsingException(Exception):
pass
def exit_error(logger, code, err):
logger.error(err)
sys.exit(code)

View File

@ -0,0 +1,84 @@
#!/usr/bin/python3
from __future__ import absolute_import
import argparse
import os
import sys
# Add ../ to the path
# Works if this script is executed without installing the module
script_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
sys.path.insert(0, os.path.dirname(script_dir))
# Pretend we are part of a module
# Avoids: ImportError: attempted relative import with no known parent package
__package__ = os.path.basename(script_dir)
__import__(__package__)
# python 2 and 3 compatibility hack
def to_utf(s):
if isinstance(s, bytes):
return s.decode('utf-8')
else:
return s
def main():
create_wx_app = 'INTERACTIVE_HTML_BOM_NO_DISPLAY' not in os.environ
import wx
if create_wx_app:
app = wx.App()
if hasattr(wx, "APP_ASSERT_SUPPRESS"):
app.SetAssertMode(wx.APP_ASSERT_SUPPRESS)
elif hasattr(wx, "DisableAsserts"):
wx.DisableAsserts()
from .core import ibom
from .core.config import Config
from .ecad import get_parser_by_extension
from .version import version
from .errors import (ExitCodes, ParsingException, exit_error)
parser = argparse.ArgumentParser(
description='KiCad InteractiveHtmlBom plugin CLI.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('file',
type=lambda s: to_utf(s),
help="KiCad PCB file")
Config.add_options(parser, version)
args = parser.parse_args()
logger = ibom.Logger(cli=True)
if not os.path.isfile(args.file):
exit_error(logger, ExitCodes.ERROR_FILE_NOT_FOUND,
"File %s does not exist." % args.file)
print("Loading %s" % args.file)
config = Config(version, os.path.dirname(os.path.abspath(args.file)))
parser = get_parser_by_extension(
os.path.abspath(args.file), config, logger)
if args.show_dialog:
if not create_wx_app:
exit_error(logger, ExitCodes.ERROR_NO_DISPLAY,
"Can not show dialog when "
"INTERACTIVE_HTML_BOM_NO_DISPLAY is set.")
try:
ibom.run_with_dialog(parser, config, logger)
except ParsingException as e:
exit_error(logger, ExitCodes.ERROR_PARSE, e)
else:
config.set_from_args(args)
try:
ibom.main(parser, config, logger)
except ParsingException as e:
exit_error(logger, ExitCodes.ERROR_PARSE, str(e))
return 0
if __name__ == "__main__":
main()

View File

@ -0,0 +1,12 @@
::start up echo
set i18n_gitAddr= https://github.com/openscopeproject/InteractiveHtmlBom
set i18n_batScar= Bat file by Scarrrr0725
set i18n_thx4using= Thank You For Using Generate Interactive Bom
::convert
set i18n_draghere=Please Drag the EasyEDA PCB source file here :
set i18n_converting=Converting . . . . . .
::converted
set i18n_again=Do you want to convert another file ?
set i18n_converted= EDA source file is converted to bom successfully!

View File

@ -0,0 +1,17 @@
::This file needs to be in 'UTF-8 encoding' AND 'Windows CR LF' to work.
::set active code page as UTF-8/65001
set PYTHONIOENCODING=utf-8
chcp 65001
::start up echo
set i18n_gitAddr= https://github.com/openscopeproject/InteractiveHtmlBom
set i18n_batScar= Bat 文件: Scarrrr0725/XiaoMingXD
set i18n_thx4using= 感谢使用 Generate Interactive Bom
::convert
set i18n_draghere=请将您的 EDA PCB 源文件拖移至此:
set i18n_converting=导出中 . . . . . ."
::converted
set i18n_again=请问是否转换其他文件?
set i18n_converted= 您的 EDA 源文件已成功导出 Bom

BIN
InteractiveHtmlBom/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

View File

@ -0,0 +1,29 @@
# Update this when new version is tagged.
import os
import subprocess
LAST_TAG = 'v2.10.0'
def _get_git_version():
plugin_path = os.path.realpath(os.path.dirname(__file__))
try:
git_version = subprocess.check_output(
['git', 'describe', '--tags', '--abbrev=4', '--dirty=-*'],
stderr=subprocess.DEVNULL,
cwd=plugin_path)
if isinstance(git_version, bytes):
return git_version.decode('utf-8').rstrip()
else:
return git_version.rstrip()
except subprocess.CalledProcessError:
# print('Git version check failed: ' + str(e))
pass
except Exception:
# print('Git process cannot be launched: ' + str(e))
pass
return None
version = _get_git_version() or LAST_TAG

View File

@ -0,0 +1,274 @@
</style><style type="text/css">#index_crossLine__2WHMH {
position: fixed;
height: 100%;
width: 100%;
z-index: 2147483645;
}
#index_crossLine__2WHMH::before {
border: none;
content: '';
height: 100%;
position: absolute;
width: 100%;
z-index: 2147483645;
border-right: 1px solid red;
border-bottom: 1px solid red;
left: -100%;
top: -100%;
}
#index_crossLine__2WHMH::after {
border: none;
content: '';
height: 100%;
position: absolute;
width: 100%;
z-index: 2147483645;
border-top: 1px solid red;
border-left: 1px solid red;
left: 0;
top: 0;
}
#index_selectArea__2G3Z3 {
border: 1px solid red;
position: fixed;
z-index: 2147483645;
}
</style><style type="text/css">.index_highlightSelector__-qiHd {
background-color: #fafafa !important;
outline: 3px dashed #1976d2 !important;
opacity: 0.8 !important;
cursor: pointer !important;
transition: opacity 0.5s ease !important;
}
</style></head><body><div align="center">
<ul>
</ul>
<table width="95%" border="1" cellpadding="10" align="center" bgcolor="#999999">
<tbody><tr>
<td bgcolor="#FFFFFF">
<div align="center"><font face="Comic Sans MS"><b>White</b></font></div>
</td><td bgcolor="#000000">
<div align="center"><font face="Comic Sans MS" color="#FFFFFF"><b>Black</b></font></div>
</td>
<td bgcolor="#996633">
<div align="center"><font face="Comic Sans MS" color="#FFFFFF"><b>Brown</b></font></div>
</td>
<td bgcolor="#FF0000">
<div align="center"><font face="Comic Sans MS"><b>Red</b></font></div>
</td>
<td bgcolor="#FF9900">
<div align="center"><font face="Comic Sans MS"><b>Orange</b></font></div>
</td>
<td bgcolor="#FFFF00">
<div align="center"><font face="Comic Sans MS"><b>Yellow</b></font></div>
</td>
<td bgcolor="#00FF00">
<div align="center"><font face="Comic Sans MS"><b>Green</b></font></div>
</td>
<td bgcolor="#0000FF">
<div align="center"><font face="Comic Sans MS" color="#FFFFFF"><b>Blue</b></font></div>
</td>
<td bgcolor="#FF00FF">
<div align="center"><font face="Comic Sans MS"><b>Violet</b></font></div>
</td>
<td bgcolor="#CCCCCC">
<div align="center"><font face="Comic Sans MS"><b>Grey</b></font></div>
</td>
<td bgcolor="#4B0082">
<div align="center"><font face="Comic Sans MS" color="#FFF"><b>Indigo</b></font></div>
</td>
</tr>
<tr>
<td bgcolor="#FFFFFF">
<div align="center"><font face="Comic Sans MS">#FFFFFF</font></div>
</td><td bgcolor="#000000">
<div align="center"><font color="#FFF" face="Comic Sans MS">#000000</font></div>
</td>
<td bgcolor="#996633">
<div align="center"><font color="#FFFFFF" face="Comic Sans MS">#996633</font></div>
</td>
<td bgcolor="#FF0000">
<div align="center"><font face="Comic Sans MS">#FF0000</font></div>
</td>
<td bgcolor="#FF9900">
<div align="center"><font face="Comic Sans MS">#FF9900</font></div>
</td>
<td bgcolor="#FFFF00">
<div align="center"><font face="Comic Sans MS">#FFFF00</font></div>
</td>
<td bgcolor="#00FF00">
<div align="center"><font face="Comic Sans MS">#00FF00</font></div>
</td>
<td bgcolor="#0000FF">
<div align="center"><font color="#FFFFFF" face="Comic Sans MS">#0000FF</font></div>
</td>
<td bgcolor="#FF00FF">
<div align="center"><font face="Comic Sans MS">#FF00FF</font></div>
</td>
<td bgcolor="#CCCCCC">
<div align="center"><font face="Comic Sans MS">#CCCCCC</font></div>
</td>
<td bgcolor="#4B0082">
<div align="center"><font face="Comic Sans MS" color="#FFF">#4B0082</font></div>
</td>
</tr>
<tr>
<td bgcolor="#FFFFFF">
<div align="center"><font face="Comic Sans MS"><b>0</b></font></div>
</td><td bgcolor="#000000">
<div align="center"><font face="Comic Sans MS" color="#FFFFFF"><b>.</b></font></div>
</td>
<td bgcolor="#996633">
<div align="center"><font face="Comic Sans MS" color="#FFFFFF"><b>1</b></font></div>
</td>
<td bgcolor="#FF0000">
<div align="center"><font face="Comic Sans MS"><b>2</b></font></div>
</td>
<td bgcolor="#FF9900">
<div align="center"><font face="Comic Sans MS"><b>3</b></font></div>
</td>
<td bgcolor="#FFFF00">
<div align="center"><font face="Comic Sans MS"><b>4</b></font></div>
</td>
<td bgcolor="#00FF00">
<div align="center"><font face="Comic Sans MS"><b>5</b></font></div>
</td>
<td bgcolor="#0000FF">
<div align="center"><font face="Comic Sans MS" color="#FFFFFF"><b>6</b></font></div>
</td>
<td bgcolor="#FF00FF">
<div align="center"><font face="Comic Sans MS"><b>7</b></font></div>
</td>
<td bgcolor="#CCCCCC">
<div align="center"><font face="Comic Sans MS"><b>8</b></font></div>
</td>
<td bgcolor="#4B0082">
<div align="center"><font face="Comic Sans MS" color="#FFF"><b>9</b></font></div>
</td>
</tr>
</tbody></table>
<br>
<br>
<table width="360" border="1" cellpadding="0" cellspacing="0" align="center" bgcolor="#FFFFFF">
<tbody><tr height="80">
<td width="120" bgcolor="#CCCCCC"></td>
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>4</b></font></td>
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>1</b></font></td>
</tr>
<tr height="80">
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>2</b></font></td>
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>3</b></font></td>
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>2</b></font></td>
</tr>
<tr height="80">
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>1</b></font></td>
<td width="120" align="center"><font face="Comic Sans MS" size="6"><b>4</b></font></td>
<td width="120" bgcolor="#CCCCCC"></td>
</tr>
</tbody></table>
<p align="center"><font face="Comic Sans MS">
<h4>Reading Order: </font></p><font face="Comic Sans MS"></h4>
<br>
<h2>Start in either corner, then sequence the values in a clockwise motion until you've got 3 numbers and a decimal.</h2>
</font>
<br>
<br>
<p style="border: thin solid black"></p>
<p align="center"><font face="Comic Sans MS" size="5"><b>Examples</b></font></p>
<table width="800" border="0" cellpadding="20" align="center">
<tbody><tr>
<td align="center" valign="top">
<font face="Comic Sans MS"><b>100nF (100.)</b></font><br><br>
<table width="360" border="1" cellpadding="0" cellspacing="0" bgcolor="#FFFFFF">
<tbody><tr height="80">
<td width="80" bgcolor="#FFF" style="text-align: center; font-size: 2rem; margin: 2rem; color: red;">X</td>
<td width="120" bgcolor="#000000"></td> <!-- 4: . -->
<td width="120" bgcolor="#996633"></td> <!-- 1: 1 -->
</tr>
<tr height="80">
<td width="120" bgcolor="#FFFFFF"></td> <!-- 2: 0 -->
<td width="120" bgcolor="#FFFFFF"></td> <!-- 3: 0 -->
<td width="120" bgcolor="#FFFFFF"></td> <!-- 2: 0 -->
</tr>
<tr height="80">
<td width="120" bgcolor="#996633"></td> <!-- 1: 1 -->
<td width="120" bgcolor="#000000"></td> <!-- 4: . -->
<td width="80" bgcolor="#FFF" style="text-align: center; font-size: 2rem; margin: 2rem; color: red;">X</td>
</tr>
</tbody></table>
<br>
<font face="Comic Sans MS">
1: Brown (1)<br>
2: White (0)<br>
3: White (0)<br>
4: Black (.)
</font>
</td>
<td align="center" valign="top">
<font face="Comic Sans MS"><b>0.10uF (0.10) - Rotated 90°</b></font><br><br>
<table width="240" border="1" cellpadding="0" cellspacing="0" bgcolor="#FFFFFF">
<tbody><tr height="120">
<td width="80" bgcolor="#FFFFFF"></td> <!-- 1: 0 -->
<td width="80" bgcolor="#000000"></td> <!-- 2: . -->
<td width="80" bgcolor="#FFF" style="text-align: center; font-size: 2rem; margin: 2rem; color: red;">X</td>
</tr>
<tr height="120">
<td width="80" bgcolor="#FFFFFF"></td> <!-- 4: 0 -->
<td width="80" bgcolor="#996633"></td> <!-- 3: 1 -->
<td width="80" bgcolor="#FFFFFF"></td> <!-- 4: 0 -->
</tr>
<tr height="120">
<td width="80" bgcolor="#FFF" style="text-align: center; font-size: 2rem; margin: 2rem; color: red;">X</td>
<td width="80" bgcolor="#000000"></td> <!-- 2: . -->
<td width="80" bgcolor="#FFFFFF"></td> <!-- 1: 0 -->
</tr>
</tbody></table>
<br>
<font face="Comic Sans MS">
1: White (0)<br>
2: Black (.)<br>
3: Brown (1)<br>
4: White (0)
</font>
</td>
</tr>
</tbody></table>
<p align="center">
<font face="Comic Sans MS">
<h4>Guide:</h4>
</font></p><font face="Comic Sans MS">
<br>
<p></p>
<div>
<li>The other 2 corners will always be transparent, which could be confusing if you aren't familiar with the color
block
syntax. Try it in Dark mode if you're uncertain. </li>
<li>The key thing to realize about this encoding method is that it isn't math, it's purely syntactical. Meaning,
100nF
looks different than 0.1uF. </li>
<li>This is intentional. There are a number of reasons you might use one over the other. I wanted the original
formatting to be retained, and the decoding to be toddler proof. </li>
<li>You don't need to know what any of the colors mean to at least tell each unique value apart. And the color
codes are
identical to the standard resistor bands for the numbers 1-8, but I had to steal black to represent the decimal
point so, 0 is white instead and 9 is Indigo.</li>
</div>
</font>
<p></p>
</div>
</body></html>

View File

@ -0,0 +1,895 @@
/* InteractiveHtmlBom/web/ibom.css */
:root {
--pcb-edge-color: black;
--pad-color: #878787;
--pad-hole-color: #CCCCCC;
--pad-color-highlight: #D04040;
--pad-color-highlight-both: #D0D040;
--pad-color-highlight-marked: #44a344;
--pin1-outline-color: #ffb629;
--pin1-outline-color-highlight: #ffb629;
--pin1-outline-color-highlight-both: #fcbb39;
--pin1-outline-color-highlight-marked: #fdbe41;
--silkscreen-edge-color: #aa4;
--silkscreen-polygon-color: #4aa;
--silkscreen-text-color: #4aa;
--fabrication-edge-color: #907651;
--fabrication-polygon-color: #907651;
--fabrication-text-color: #a27c24;
--track-color: #def5f1;
--track-color-highlight: #D04040;
--zone-color: #def5f1;
--zone-color-highlight: #d0404080;
}
html,
body {
margin: 0px;
height: 100%;
font-family: Verdana, sans-serif;
}
.dark.topmostdiv {
--pcb-edge-color: #eee;
--pad-color: #808080;
--pin1-outline-color: #ffa800;
--pin1-outline-color-highlight: #ccff00;
--track-color: #42524f;
--zone-color: #42524f;
background-color: #252c30;
color: #eee;
}
.rainbowGroupsEnabled .menu-label-top {
font-size: 0.8rem;
}
button {
background-color: #eee;
border: 1px solid #888;
color: black;
height: 44px;
width: 44px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
font-weight: bolder;
}
.dark button {
/* This will be inverted */
background-color: #c3b7b5;
}
button.depressed {
background-color: #0a0;
color: white;
}
.dark button.depressed {
/* This will be inverted */
background-color: #b3b;
}
button:focus {
outline: 0;
}
button#tb-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8.47 8.47'%3E%3Crect transform='translate(0 -288.53)' ry='1.17' y='288.8' x='.27' height='7.94' width='7.94' fill='%23f9f9f9'/%3E%3Cg transform='translate(0 -288.53)'%3E%3Crect width='7.94' height='7.94' x='.27' y='288.8' ry='1.17' fill='none' stroke='%23000' stroke-width='.4' stroke-linejoin='round'/%3E%3Cpath d='M1.32 290.12h5.82M1.32 291.45h5.82' fill='none' stroke='%23000' stroke-width='.4'/%3E%3Cpath d='M4.37 292.5v4.23M.26 292.63H8.2' fill='none' stroke='%23000' stroke-width='.3'/%3E%3Ctext font-weight='700' font-size='3.17' font-family='sans-serif'%3E%3Ctspan x='1.35' y='295.73'%3EF%3C/tspan%3E%3Ctspan x='5.03' y='295.68'%3EB%3C/tspan%3E%3C/text%3E%3C/g%3E%3C/svg%3E%0A");
}
button#lr-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8.47 8.47'%3E%3Crect transform='translate(0 -288.53)' ry='1.17' y='288.8' x='.27' height='7.94' width='7.94' fill='%23f9f9f9'/%3E%3Cg transform='translate(0 -288.53)'%3E%3Crect width='7.94' height='7.94' x='.27' y='288.8' ry='1.17' fill='none' stroke='%23000' stroke-width='.4' stroke-linejoin='round'/%3E%3Cpath d='M1.06 290.12H3.7m-2.64 1.33H3.7m-2.64 1.32H3.7m-2.64 1.3H3.7m-2.64 1.33H3.7' fill='none' stroke='%23000' stroke-width='.4'/%3E%3Cpath d='M4.37 288.8v7.94m0-4.11h3.96' fill='none' stroke='%23000' stroke-width='.3'/%3E%3Ctext font-weight='700' font-size='3.17' font-family='sans-serif'%3E%3Ctspan x='5.11' y='291.96'%3EF%3C/tspan%3E%3Ctspan x='5.03' y='295.68'%3EB%3C/tspan%3E%3C/text%3E%3C/g%3E%3C/svg%3E%0A");
}
button#bom-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8.47 8.47'%3E%3Crect transform='translate(0 -288.53)' ry='1.17' y='288.8' x='.27' height='7.94' width='7.94' fill='%23f9f9f9'/%3E%3Cg transform='translate(0 -288.53)' fill='none' stroke='%23000' stroke-width='.4'%3E%3Crect width='7.94' height='7.94' x='.27' y='288.8' ry='1.17' stroke-linejoin='round'/%3E%3Cpath d='M1.59 290.12h5.29M1.59 291.45h5.33M1.59 292.75h5.33M1.59 294.09h5.33M1.59 295.41h5.33'/%3E%3C/g%3E%3C/svg%3E");
}
button#bom-grouped-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='%23000' stroke-linejoin='round' class='layer'%3E%3Crect width='29' height='29' x='1.5' y='1.5' stroke-width='2' fill='%23fff' rx='5' ry='5'/%3E%3Cpath stroke-linecap='square' stroke-width='2' d='M6 10h4m4 0h5m4 0h3M6.1 22h3m3.9 0h5m4 0h4m-16-8h4m4 0h4'/%3E%3Cpath stroke-linecap='null' d='M5 17.5h22M5 26.6h22M5 5.5h22'/%3E%3C/g%3E%3C/svg%3E");
}
button#bom-ungrouped-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='%23000' stroke-linejoin='round' class='layer'%3E%3Crect width='29' height='29' x='1.5' y='1.5' stroke-width='2' fill='%23fff' rx='5' ry='5'/%3E%3Cpath stroke-linecap='square' stroke-width='2' d='M6 10h4m-4 8h3m-3 8h4'/%3E%3Cpath stroke-linecap='null' d='M5 13.5h22m-22 8h22M5 5.5h22'/%3E%3C/g%3E%3C/svg%3E");
}
button#bom-netlist-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg fill='none' stroke='%23000' class='layer'%3E%3Crect width='29' height='29' x='1.5' y='1.5' stroke-width='2' fill='%23fff' rx='5' ry='5'/%3E%3Cpath stroke-width='2' d='M6 26l6-6v-8m13.8-6.3l-6 6v8'/%3E%3Ccircle cx='11.8' cy='9.5' r='2.8' stroke-width='2'/%3E%3Ccircle cx='19.8' cy='22.8' r='2.8' stroke-width='2'/%3E%3C/g%3E%3C/svg%3E");
}
button#copy {
background-image: url("data:image/svg+xml,%3Csvg height='48' viewBox='0 0 48 48' width='48' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h48v48h-48z' fill='none'/%3E%3Cpath d='M32 2h-24c-2.21 0-4 1.79-4 4v28h4v-28h24v-4zm6 8h-22c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h22c2.21 0 4-1.79 4-4v-28c0-2.21-1.79-4-4-4zm0 32h-22v-28h22v28z'/%3E%3C/svg%3E");
background-position: 6px 6px;
background-repeat: no-repeat;
background-size: 26px 26px;
border-radius: 6px;
height: 40px;
width: 40px;
margin: 10px 5px;
}
button#copy:active {
box-shadow: inset 0px 0px 5px #6c6c6c;
}
textarea.clipboard-temp {
position: fixed;
top: 0;
left: 0;
width: 2em;
height: 2em;
padding: 0;
border: None;
outline: None;
box-shadow: None;
background: transparent;
}
.left-most-button {
border-right: 0;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.middle-button {
border-right: 0;
}
.right-most-button {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.button-container {
font-size: 0;
margin: 0.4rem 0.4rem 0.4rem 0;
}
.dark .button-container {
filter: invert(1);
}
.button-container button {
background-size: 32px 32px;
background-position: 5px 5px;
background-repeat: no-repeat;
}
@media print {
.hideonprint {
display: none;
}
}
canvas {
cursor: crosshair;
}
canvas:active {
cursor: grabbing;
}
.fileinfo {
width: 100%;
max-width: 1000px;
border: none;
padding: 3px;
}
.fileinfo .title {
font-size: 20pt;
font-weight: bold;
}
.fileinfo td {
overflow: hidden;
white-space: nowrap;
max-width: 1px;
width: 50%;
text-overflow: ellipsis;
}
.bom {
border-collapse: collapse;
font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
font-size: 10pt;
table-layout: fixed;
width: 100%;
margin-top: 1px;
position: relative;
}
.bom th,
.bom td {
border: 1px solid black;
padding: 5px;
word-wrap: break-word;
text-align: center;
position: relative;
}
.dark .bom th,
.dark .bom td {
border: 1px solid #777;
}
.bom th {
background-color: #CCCCCC;
background-clip: padding-box;
}
.dark .bom th {
background-color: #3b4749;
}
.bom tr.highlighted:nth-child(n) {
background-color: #cfc !important;
}
.dark .bom tr.highlighted:nth-child(n) {
background-color: #226022 !important;
}
.bom tr:nth-child(even) {
background-color: #f2f2f2;
}
.dark .bom tr:nth-child(even) {
background-color: #313b40;
}
.bom tr.checked {
color: #1cb53d;
}
.dark .bom tr.checked {
color: #2cce54;
}
.bom tr {
transition: background-color 0.2s;
}
.bom .numCol {
width: 30px;
}
.bom .value {
width: 15%;
}
.bom .quantity {
width: 65px;
}
.bom th .sortmark {
position: absolute;
right: 1px;
top: 1px;
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #221 transparent;
transform-origin: 50% 85%;
transition: opacity 0.2s, transform 0.4s;
}
.dark .bom th .sortmark {
filter: invert(1);
}
.bom th .sortmark.none {
opacity: 0;
}
.bom th .sortmark.desc {
transform: rotate(180deg);
}
.bom th:hover .sortmark.none {
opacity: 0.5;
}
.bom .bom-checkbox {
width: 30px;
position: relative;
user-select: none;
-moz-user-select: none;
}
.bom .bom-checkbox:before {
content: "";
position: absolute;
border-width: 15px;
border-style: solid;
border-color: #51829f transparent transparent transparent;
visibility: hidden;
top: -15px;
}
.bom .bom-checkbox:after {
content: "Double click to set/unset all";
position: absolute;
color: white;
top: -35px;
left: -26px;
background: #51829f;
padding: 5px 15px;
border-radius: 8px;
white-space: nowrap;
visibility: hidden;
}
.bom .bom-checkbox:hover:before,
.bom .bom-checkbox:hover:after {
visibility: visible;
transition: visibility 0.2s linear 1s;
}
.split {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
background-color: inherit;
}
.split.split-horizontal,
.gutter.gutter-horizontal {
height: 100%;
float: left;
}
.gutter {
background-color: #ddd;
background-repeat: no-repeat;
background-position: 50%;
transition: background-color 0.3s;
}
.dark .gutter {
background-color: #777;
}
.gutter.gutter-horizontal {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
cursor: ew-resize;
width: 5px;
}
.gutter.gutter-vertical {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
cursor: ns-resize;
height: 5px;
}
.searchbox {
float: left;
height: 40px;
margin: 10px 5px;
padding: 12px 32px;
font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
font-size: 18px;
box-sizing: border-box;
border: 1px solid #888;
border-radius: 6px;
outline: none;
background-color: #eee;
transition: background-color 0.2s, border 0.2s;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABNklEQVQ4T8XSMUvDQBQH8P/LElFa/AIZHcTBQSz0I/gFstTBRR2KUC4ldDxw7h0Bl3RRUATxi4iiODgoiLNrbQYp5J6cpJJqomkX33Z37/14d/dIa33MzDuYI4johOI4XhyNRteO46zNYjDzAxE1yBZprVeZ+QbAUhXEGJMA2Ox2u4+fQIa0mPmsCgCgJYQ4t7lfgF0opQYAdv9ABkKI/UnOFCClXKjX61cA1osQY8x9kiRNKeV7IWA3oyhaSdP0FkAtjxhj3hzH2RBCPOf3pzqYHCilfAAX+URm9oMguPzeWSGQvUcMYC8rOBJCHBRdqxTo9/vbRHRqi8bj8XKv1xvODbiuW2u32/bvf0SlDv4XYOY7z/Mavu+nM1+BmQ+NMc0wDF/LprP0DbTWW0T00ul0nn4b7Q87+X4Qmfiq2wAAAABJRU5ErkJggg==');
background-position: 10px 10px;
background-repeat: no-repeat;
}
.dark .searchbox {
background-color: #111;
color: #eee;
}
.searchbox::placeholder {
color: #ccc;
}
.dark .searchbox::placeholder {
color: #666;
}
.filter {
width: calc(60% - 64px);
}
.reflookup {
width: calc(40% - 10px);
}
input[type=text]:focus {
background-color: white;
border: 1px solid #333;
}
.dark input[type=text]:focus {
background-color: #333;
border: 1px solid #ccc;
}
mark.highlight {
background-color: #5050ff;
color: #fff;
padding: 2px;
border-radius: 6px;
}
.dark mark.highlight {
background-color: #76a6da;
color: #111;
}
.menubtn {
background-color: white;
border: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36' viewBox='0 0 20 20'%3E%3Cpath fill='none' d='M0 0h20v20H0V0z'/%3E%3Cpath d='M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z'/%3E%3C/svg%3E%0A");
background-position: center;
background-repeat: no-repeat;
}
.statsbtn {
background-color: white;
border: none;
background-image: url("data:image/svg+xml,%3Csvg width='36' height='36' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6h28v24H4V6zm0 8h28v8H4m9-16v24h10V5.8' fill='none' stroke='%23000' stroke-width='2'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
}
.iobtn {
background-color: white;
border: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' d='M3 33v-7l6.8-7h16.5l6.7 7v7H3zM3.2 26H33M21 9l5-5.9 5 6h-2.5V15h-5V9H21zm-4.9 0l-5 6-5-6h2.5V3h5v6h2.5z'/%3E%3Cpath fill='none' stroke='%23000' d='M6.1 29.5H10'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
}
.visbtn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='none' stroke='%23333' d='M2.5 4.5h5v15h-5zM9.5 4.5h5v15h-5zM16.5 4.5h5v15h-5z'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
padding: 15px;
}
#vismenu-content {
left: 0px;
font-family: Verdana, sans-serif;
}
.dark .statsbtn,
.dark .savebtn,
.dark .menubtn,
.dark .iobtn,
.dark .visbtn {
filter: invert(1);
}
.flexbox {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.savebtn {
background-color: #d6d6d6;
width: auto;
height: 30px;
flex-grow: 1;
margin: 5px;
border-radius: 4px;
}
.savebtn:active {
background-color: #0a0;
color: white;
}
.dark .savebtn:active {
/* This will be inverted */
background-color: #b3b;
}
.stats {
border-collapse: collapse;
font-size: 12pt;
table-layout: fixed;
width: 100%;
min-width: 450px;
}
.dark .stats td {
border: 1px solid #bbb;
}
.stats td {
border: 1px solid black;
padding: 5px;
word-wrap: break-word;
text-align: center;
position: relative;
}
#checkbox-stats div {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#checkbox-stats .bar {
background-color: rgba(28, 251, 0, 0.6);
}
.menu {
position: relative;
display: inline-block;
margin: 0.4rem 0.4rem 0.4rem 0;
}
.menu-content {
font-size: 12pt !important;
text-align: left !important;
font-weight: normal !important;
display: none;
position: absolute;
background-color: white;
right: 0;
min-width: 300px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 100;
padding: 8px;
}
.dark .menu-content {
background-color: #111;
}
.menu:hover .menu-content {
display: block;
}
.menu:hover .menubtn,
.menu:hover .iobtn,
.menu:hover .statsbtn {
background-color: #eee;
}
.menu-label {
display: inline-block;
padding: 8px;
border: 1px solid #ccc;
border-top: 0;
width: calc(100% - 18px);
}
.menu-label-top {
border-top: 1px solid #ccc;
}
.menu-textbox {
float: left;
height: 24px;
margin: 10px 5px;
padding: 5px 5px;
font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
font-size: 14px;
box-sizing: border-box;
border: 1px solid #888;
border-radius: 4px;
outline: none;
background-color: #eee;
transition: background-color 0.2s, border 0.2s;
width: calc(100% - 10px);
}
.menu-textbox.invalid,
.dark .menu-textbox.invalid {
color: red;
}
.dark .menu-textbox {
background-color: #222;
color: #eee;
}
.radio-container {
margin: 4px;
}
.topmostdiv {
display: flex;
flex-direction: column;
width: 100%;
background-color: white;
transition: background-color 0.3s;
min-height: 100%;
}
#top {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
#topdivider {
border-bottom: 2px solid black;
display: flex;
justify-content: center;
align-items: center;
}
.dark #topdivider {
border-bottom: 2px solid #ccc;
}
#topdivider>div {
position: relative;
}
#toptoggle {
cursor: pointer;
user-select: none;
position: absolute;
padding: 0.1rem 0.3rem;
top: -0.4rem;
left: -1rem;
font-size: 1.4rem;
line-height: 60%;
border: 1px solid black;
border-radius: 1rem;
background-color: #fff;
z-index: 100;
}
.flipped {
transform: rotate(0.5turn);
}
.dark #toptoggle {
border: 1px solid #fff;
background-color: #222;
}
#fileinfodiv {
flex: 20rem 1 0;
overflow: auto;
}
#bomcontrols {
display: flex;
flex-direction: row-reverse;
}
#bomcontrols>* {
flex-shrink: 0;
}
#dbg {
display: block;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #aaa;
}
::-webkit-scrollbar-thumb {
background: #666;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
.slider {
-webkit-appearance: none;
width: 100%;
margin: 3px 0;
padding: 0;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
border-radius: 3px;
}
.slider:hover {
opacity: 1;
}
.slider:focus {
outline: none;
}
.slider::-webkit-slider-runnable-track {
-webkit-appearance: none;
width: 100%;
height: 8px;
background: #d3d3d3;
border-radius: 3px;
border: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: #0a0;
cursor: pointer;
margin-top: -4px;
}
.dark .slider::-webkit-slider-thumb {
background: #3d3;
}
.slider::-moz-range-thumb {
width: 15px;
height: 15px;
border-radius: 50%;
background: #0a0;
cursor: pointer;
}
.slider::-moz-range-track {
height: 8px;
background: #d3d3d3;
border-radius: 3px;
}
.dark .slider::-moz-range-thumb {
background: #3d3;
}
.slider::-ms-track {
width: 100%;
height: 8px;
border-width: 3px 0;
background: transparent;
border-color: transparent;
color: transparent;
transition: opacity .2s;
}
.slider::-ms-fill-lower {
background: #d3d3d3;
border: none;
border-radius: 3px;
}
.slider::-ms-fill-upper {
background: #d3d3d3;
border: none;
border-radius: 3px;
}
.slider::-ms-thumb {
width: 15px;
height: 15px;
border-radius: 50%;
background: #0a0;
cursor: pointer;
margin: 0;
}
.shameless-plug {
font-size: 0.8em;
text-align: center;
display: block;
}
a {
color: #0278a4;
}
.dark a {
color: #00b9fd;
}
#frontcanvas,
#backcanvas {
touch-action: none;
}
.placeholder {
border: 1px dashed #9f9fda !important;
background-color: #edf2f7 !important;
}
.dragging {
z-index: 999;
}
.dark .dragging>table>tbody>tr {
background-color: #252c30;
}
.dark .placeholder {
filter: invert(1);
}
.column-spacer {
top: 0;
left: 0;
width: calc(100% - 4px);
position: absolute;
cursor: pointer;
user-select: none;
height: 100%;
}
.column-width-handle {
top: 0;
right: 0;
width: 4px;
position: absolute;
cursor: col-resize;
user-select: none;
height: 100%;
}
.column-width-handle:hover {
background-color: #4f99bd;
}
.help-link {
border: 1px solid #0278a4;
padding-inline: 0.3rem;
border-radius: 3px;
cursor: pointer;
}
.dark .help-link {
border: 1px solid #00b9fd;
}
.bom-color {
width: 20%;
}
.color-column input {
width: 1.6rem;
height: 1rem;
border: 1px solid black;
cursor: pointer;
padding: 0;
}
/* removes default styling from input color element */
::-webkit-color-swatch {
border: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-moz-color-swatch,
::-moz-focus-inner {
border: none;
}
::-moz-focus-inner {
padding: 0;
}

View File

@ -0,0 +1,361 @@
<!-- InteractiveHtmlBom/web/ibom.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive BOM for KiCAD</title>
<style type="text/css">
///CSS///
///USERCSS///
</style>
<script type="text/javascript" >
///////////////////////////////////////////////
///SPLITJS///
///////////////////////////////////////////////
///////////////////////////////////////////////
///LZ-STRING///
///////////////////////////////////////////////
///////////////////////////////////////////////
///POINTER_EVENTS_POLYFILL///
///////////////////////////////////////////////
///////////////////////////////////////////////
///CONFIG///
///////////////////////////////////////////////
///////////////////////////////////////////////
///PCBDATA///
///////////////////////////////////////////////
///////////////////////////////////////////////
///UTILJS///
///////////////////////////////////////////////
///////////////////////////////////////////////
///RENDERJS///
///////////////////////////////////////////////
///////////////////////////////////////////////
///TABLEUTILJS///
///////////////////////////////////////////////
///////////////////////////////////////////////
///IBOMJS///
///////////////////////////////////////////////
///////////////////////////////////////////////
///USERJS///
///////////////////////////////////////////////
</script>
</head>
<body>
///USERHEADER///
<div id="topmostdiv" class="topmostdiv">
<div id="top">
<div id="fileinfodiv">
<table class="fileinfo">
<tbody>
<tr>
<td id="title" class="title" style="width: 70%">
Title
</td>
<td id="revision" class="title" style="width: 30%">
Revision
</td>
</tr>
<tr>
<td id="company">
Company
</td>
<td id="filedate">
Date
</td>
</tr>
</tbody>
</table>
</div>
<div id="bomcontrols">
<div class="hideonprint menu">
<button class="menubtn"></button>
<div class="menu-content">
<label class="menu-label menu-label-top" style="width: calc(50% - 18px)">
<input id="darkmodeCheckbox" type="checkbox" onchange="setDarkMode(this.checked)">
Dark mode
</label><!-- This comment eats space! All of it!
--><label class="menu-label menu-label-top" style="width: calc(50% - 17px); border-left: 0;">
<input id="fullscreenCheckbox" type="checkbox" onchange="setFullscreen(this.checked)">
Full Screen
</label>
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="fabricationCheckbox" type="checkbox" checked onchange="fabricationVisible(this.checked)">
Fab layer
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="silkscreenCheckbox" type="checkbox" checked onchange="silkscreenVisible(this.checked)">
Silkscreen
</label>
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="referencesCheckbox" type="checkbox" checked onchange="referencesVisible(this.checked)">
References
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="valuesCheckbox" type="checkbox" checked onchange="valuesVisible(this.checked)">
Values
</label>
<div id="tracksAndZonesCheckboxes">
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="tracksCheckbox" type="checkbox" checked onchange="tracksVisible(this.checked)">
Tracks
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="zonesCheckbox" type="checkbox" checked onchange="zonesVisible(this.checked)">
Zones
</label>
</div>
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="padsCheckbox" type="checkbox" checked onchange="padsVisible(this.checked)">
Pads
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="dnpOutlineCheckbox" type="checkbox" checked onchange="dnpOutline(this.checked)">
DNP outlined
</label>
<label class="menu-label">
<input id="highlightRowOnClickCheckbox" type="checkbox" checked onchange="setHighlightRowOnClick(this.checked)">
Highlight row on click
</label>
<label class="menu-label">
<input id="dragCheckbox" type="checkbox" checked onchange="setRedrawOnDrag(this.checked)">
Continuous redraw on drag
</label>
<label class="menu-label">
<input id="rainbowModeCheckbox" type="checkbox" onchange="setRainbowMode(this.checked)">
Assign each group a unique color
</label>
<label class="menu-label" id="highlightAllLabel" style="display: none;">
<input id="highlightAllCheckbox" type="checkbox" onchange="setHighlightAll(this.checked)">
Highlight all group colors simultaneously
</label>
<label class="menu-label" id="showUnitsLabel" style="display: none;">
<input id="showUnitsCheckbox" type="checkbox" onchange="setShowUnits(this.checked)">
Show units on PCB
</label>
<label class="menu-label">
Highlight first pin
<form id="highlightpin1">
<div class="flexbox">
<label>
<input type="radio" name="highlightpin1" value="none" onchange="setHighlightPin1('none')">
None
</label>
<label>
<input type="radio" name="highlightpin1" value="all" onchange="setHighlightPin1('all')">
All
</label>
<label>
<input type="radio" name="highlightpin1" value="selected" onchange="setHighlightPin1('selected')">
Selected
</label>
</div>
</form>
</label>
<label class="menu-label">
<span>Board rotation</span>
<span style="float: right"><span id="rotationDegree">0</span>&#176;</span>
<input id="boardRotation" type="range" min="-36" max="36" value="0" class="slider" oninput="setBoardRotation(this.value)">
</label>
<label class="menu-label">
<input id="offsetBackRotationCheckbox" type="checkbox" onchange="setOffsetBackRotation(this.checked)">
Offset back rotation
</label>
<label class="menu-label">
<div style="margin-left: 5px">Bom checkboxes</div>
<input id="bomCheckboxes" class="menu-textbox" type=text
oninput="setBomCheckboxes(this.value)">
</label>
<label class="menu-label">
<div style="margin-left: 5px">Mark when checked</div>
<div id="markWhenCheckedContainer"></div>
</label>
<label class="menu-label">
<span class="shameless-plug">
<span>Created using</span>
<a id="github-link" target="blank" href="https://github.com/openscopeproject/InteractiveHtmlBom">InteractiveHtmlBom</a>
<a target="blank" title="Mouse and keyboard help" href="https://github.com/openscopeproject/InteractiveHtmlBom/wiki/Usage#bom-page-mouse-actions" style="text-decoration: none;"><label class="help-link">?</label></a>
</span>
</label>
</div>
</div>
<div class="button-container hideonprint">
<button id="fl-btn" class="left-most-button" onclick="changeCanvasLayout('F')"
title="Front only">F
</button>
<button id="fb-btn" class="middle-button" onclick="changeCanvasLayout('FB')"
title="Front and Back">FB
</button>
<button id="bl-btn" class="right-most-button" onclick="changeCanvasLayout('B')"
title="Back only">B
</button>
</div>
<div class="button-container hideonprint">
<button id="bom-btn" class="left-most-button" onclick="changeBomLayout('bom-only')"
title="BOM only"></button>
<button id="lr-btn" class="middle-button" onclick="changeBomLayout('left-right')"
title="BOM left, drawings right"></button>
<button id="tb-btn" class="right-most-button" onclick="changeBomLayout('top-bottom')"
title="BOM top, drawings bot"></button>
</div>
<div class="button-container hideonprint">
<button id="bom-grouped-btn" class="left-most-button" onclick="changeBomMode('grouped')"
title="Grouped BOM"></button>
<button id="bom-ungrouped-btn" class="middle-button" onclick="changeBomMode('ungrouped')"
title="Ungrouped BOM"></button>
<button id="bom-netlist-btn" class="right-most-button" onclick="changeBomMode('netlist')"
title="Netlist"></button>
</div>
<div class="hideonprint menu">
<button class="statsbtn"></button>
<div class="menu-content">
<table class="stats">
<tbody>
<tr>
<td width="40%">Board stats</td>
<td>Front</td>
<td>Back</td>
<td>Total</td>
</tr>
<tr>
<td>Components</td>
<td id="stats-components-front">~</td>
<td id="stats-components-back">~</td>
<td id="stats-components-total">~</td>
</tr>
<tr>
<td>Groups</td>
<td id="stats-groups-front">~</td>
<td id="stats-groups-back">~</td>
<td id="stats-groups-total">~</td>
</tr>
<tr>
<td>SMD pads</td>
<td id="stats-smd-pads-front">~</td>
<td id="stats-smd-pads-back">~</td>
<td id="stats-smd-pads-total">~</td>
</tr>
<tr>
<td>TH pads</td>
<td colspan=3 id="stats-th-pads">~</td>
</tr>
</tbody>
</table>
<table class="stats">
<col width="40%"/><col />
<tbody id="checkbox-stats">
<tr>
<td colspan=2 style="border-top: 0">Checkboxes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="hideonprint menu">
<button class="iobtn"></button>
<div class="menu-content">
<div class="menu-label menu-label-top">
<div style="margin-left: 5px;">Save board image</div>
<div class="flexbox">
<input id="render-save-width" class="menu-textbox" type="text" value="1000" placeholder="Width"
style="flex-grow: 1; width: 50px;" oninput="validateSaveImgDimension(this)">
<span>X</span>
<input id="render-save-height" class="menu-textbox" type="text" value="1000" placeholder="Height"
style="flex-grow: 1; width: 50px;" oninput="validateSaveImgDimension(this)">
</div>
<label>
<input id="render-save-transparent" type="checkbox">
Transparent background
</label>
<div class="flexbox">
<button class="savebtn" onclick="saveImage('F')">Front</button>
<button class="savebtn" onclick="saveImage('B')">Back</button>
</div>
</div>
<div class="menu-label">
<span style="margin-left: 5px;">Config and checkbox state</span>
<div class="flexbox">
<button class="savebtn" onclick="saveSettings()">Export</button>
<button class="savebtn" onclick="loadSettings()">Import</button>
<button class="savebtn" onclick="resetSettings()">Reset</button>
</div>
</div>
<div class="menu-label">
<span style="margin-left: 5px;">Save bom table as</span>
<div class="flexbox">
<button class="savebtn" onclick="saveBomTable('csv')">csv</button>
<button class="savebtn" onclick="saveBomTable('txt')">txt</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="topdivider">
<div class="hideonprint">
<div id="toptoggle" onclick="topToggle()"></div>
</div>
</div>
<div id="bot" class="split" style="flex: 1 1">
<div id="bomdiv" class="split split-horizontal">
<div style="width: 100%">
<input id="reflookup" class="textbox searchbox reflookup hideonprint" type="text" placeholder="Ref lookup"
oninput="updateRefLookup(this.value)">
<input id="filter" class="textbox searchbox filter hideonprint" type="text" placeholder="Filter"
oninput="updateFilter(this.value)">
<div class="rainbowGroupsEnabled button-container hideonprint" style="float: left; margin: 0;">
<button id="copy" title="Copy bom table to clipboard"
onclick="saveBomTable('clipboard')"></button>
</div>
</div>
<div id="dbg"></div>
<table class="bom" id="bomtable">
<thead id="bomhead">
</thead>
<tbody id="bombody">
</tbody>
</table>
</div>
<div id="canvasdiv" class="split split-horizontal">
<div id="frontcanvas" class="split" touch-action="none" style="overflow: hidden">
<div style="position: relative; width: 100%; height: 100%;">
<canvas id="F_bg" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="F_fab" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
<canvas id="F_slk" style="position: absolute; left: 0; top: 0; z-index: 2;"></canvas>
<canvas id="F_hl" style="position: absolute; left: 0; top: 0; z-index: 3;"></canvas>
</div>
</div>
<div id="backcanvas" class="split" touch-action="none" style="overflow: hidden">
<div style="position: relative; width: 100%; height: 100%;">
<canvas id="B_bg" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="B_fab" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
<canvas id="B_slk" style="position: absolute; left: 0; top: 0; z-index: 2;"></canvas>
<canvas id="B_hl" style="position: absolute; left: 0; top: 0; z-index: 3;"></canvas>
</div>
</div>
</div>
</div>
</div>
///USERFOOTER///
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
// This work is free. You can redistribute it and/or modify it
// under the terms of the WTFPL, Version 2
// For more information see LICENSE.txt or http://www.wtfpl.net/
//
// For more information, the home page:
// http://pieroxy.net/blog/pages/lz-string/testing.html
//
// LZ-based compression algorithm, version 1.4.4
var LZString=function(){var o=String.fromCharCode,i={};var n={decompressFromBase64:function(o){return null==o?"":""==o?null:n._decompress(o.length,32,function(n){return function(o,n){if(!i[o]){i[o]={};for(var t=0;t<o.length;t++)i[o][o.charAt(t)]=t}return i[o][n]}("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",o.charAt(n))})},_decompress:function(i,n,t){var r,e,a,s,p,u,l,f=[],c=4,d=4,h=3,v="",g=[],m={val:t(0),position:n,index:1};for(r=0;r<3;r+=1)f[r]=r;for(a=0,p=Math.pow(2,2),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;switch(a){case 0:for(a=0,p=Math.pow(2,8),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;l=o(a);break;case 1:for(a=0,p=Math.pow(2,16),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;l=o(a);break;case 2:return""}for(f[3]=l,e=l,g.push(l);;){if(m.index>i)return"";for(a=0,p=Math.pow(2,h),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;switch(l=a){case 0:for(a=0,p=Math.pow(2,8),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;f[d++]=o(a),l=d-1,c--;break;case 1:for(a=0,p=Math.pow(2,16),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;f[d++]=o(a),l=d-1,c--;break;case 2:return g.join("")}if(0==c&&(c=Math.pow(2,h),h++),f[l])v=f[l];else{if(l!==d)return null;v=e+e.charAt(0)}g.push(v),f[d++]=e+v.charAt(0),e=v,0==--c&&(c=Math.pow(2,h),h++)}}};return n}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});

View File

@ -0,0 +1,43 @@
/*!
* PEP v0.4.3 | https://github.com/jquery/PEP
* Copyright jQuery Foundation and other contributors | http://jquery.org/license
*/
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.PointerEventsPolyfill=b()}(this,function(){"use strict";function a(a,b){b=b||Object.create(null);var c=document.createEvent("Event");c.initEvent(a,b.bubbles||!1,b.cancelable||!1);
for(var d,e=2;e<m.length;e++)d=m[e],c[d]=b[d]||n[e];c.buttons=b.buttons||0;
var f=0;return f=b.pressure&&c.buttons?b.pressure:c.buttons?.5:0,c.x=c.clientX,c.y=c.clientY,c.pointerId=b.pointerId||0,c.width=b.width||0,c.height=b.height||0,c.pressure=f,c.tiltX=b.tiltX||0,c.tiltY=b.tiltY||0,c.twist=b.twist||0,c.tangentialPressure=b.tangentialPressure||0,c.pointerType=b.pointerType||"",c.hwTimestamp=b.hwTimestamp||0,c.isPrimary=b.isPrimary||!1,c}function b(){this.array=[],this.size=0}function c(a,b,c,d){this.addCallback=a.bind(d),this.removeCallback=b.bind(d),this.changedCallback=c.bind(d),A&&(this.observer=new A(this.mutationWatcher.bind(this)))}function d(a){return"body /shadow-deep/ "+e(a)}function e(a){return'[touch-action="'+a+'"]'}function f(a){return"{ -ms-touch-action: "+a+"; touch-action: "+a+"; }"}function g(){if(F){D.forEach(function(a){String(a)===a?(E+=e(a)+f(a)+"\n",G&&(E+=d(a)+f(a)+"\n")):(E+=a.selectors.map(e)+f(a.rule)+"\n",G&&(E+=a.selectors.map(d)+f(a.rule)+"\n"))});var a=document.createElement("style");a.textContent=E,document.head.appendChild(a)}}function h(){if(!window.PointerEvent){if(window.PointerEvent=a,window.navigator.msPointerEnabled){var b=window.navigator.msMaxTouchPoints;Object.defineProperty(window.navigator,"maxTouchPoints",{value:b,enumerable:!0}),u.registerSource("ms",_)}else Object.defineProperty(window.navigator,"maxTouchPoints",{value:0,enumerable:!0}),u.registerSource("mouse",N),void 0!==window.ontouchstart&&u.registerSource("touch",V);u.register(document)}}function i(a){if(!u.pointermap.has(a)){var b=new Error("InvalidPointerId");throw b.name="InvalidPointerId",b}}function j(a){for(var b=a.parentNode;b&&b!==a.ownerDocument;)b=b.parentNode;if(!b){var c=new Error("InvalidStateError");throw c.name="InvalidStateError",c}}function k(a){var b=u.pointermap.get(a);return 0!==b.buttons}function l(){window.Element&&!Element.prototype.setPointerCapture&&Object.defineProperties(Element.prototype,{setPointerCapture:{value:W},releasePointerCapture:{value:X},hasPointerCapture:{value:Y}})}
var m=["bubbles","cancelable","view","detail","screenX","screenY","clientX","clientY","ctrlKey","altKey","shiftKey","metaKey","button","relatedTarget","pageX","pageY"],n=[!1,!1,null,null,0,0,0,0,!1,!1,!1,!1,0,null,0,0],o=window.Map&&window.Map.prototype.forEach,p=o?Map:b;b.prototype={set:function(a,b){return void 0===b?this["delete"](a):(this.has(a)||this.size++,void(this.array[a]=b))},has:function(a){return void 0!==this.array[a]},"delete":function(a){this.has(a)&&(delete this.array[a],this.size--)},get:function(a){return this.array[a]},clear:function(){this.array.length=0,this.size=0},forEach:function(a,b){return this.array.forEach(function(c,d){a.call(b,c,d,this)},this)}};var q=["bubbles","cancelable","view","detail","screenX","screenY","clientX","clientY","ctrlKey","altKey","shiftKey","metaKey","button","relatedTarget","buttons","pointerId","width","height","pressure","tiltX","tiltY","pointerType","hwTimestamp","isPrimary","type","target","currentTarget","which","pageX","pageY","timeStamp"],r=[!1,!1,null,null,0,0,0,0,!1,!1,!1,!1,0,null,0,0,0,0,0,0,0,"",0,!1,"",null,null,0,0,0,0],s={pointerover:1,pointerout:1,pointerenter:1,pointerleave:1},t="undefined"!=typeof SVGElementInstance,u={pointermap:new p,eventMap:Object.create(null),captureInfo:Object.create(null),eventSources:Object.create(null),eventSourceList:[],registerSource:function(a,b){var c=b,d=c.events;d&&(d.forEach(function(a){c[a]&&(this.eventMap[a]=c[a].bind(c))},this),this.eventSources[a]=c,this.eventSourceList.push(c))},register:function(a){for(var b,c=this.eventSourceList.length,d=0;d<c&&(b=this.eventSourceList[d]);d++)
b.register.call(b,a)},unregister:function(a){for(var b,c=this.eventSourceList.length,d=0;d<c&&(b=this.eventSourceList[d]);d++)
b.unregister.call(b,a)},contains:function(a,b){try{return a.contains(b)}catch(c){return!1}},down:function(a){a.bubbles=!0,this.fireEvent("pointerdown",a)},move:function(a){a.bubbles=!0,this.fireEvent("pointermove",a)},up:function(a){a.bubbles=!0,this.fireEvent("pointerup",a)},enter:function(a){a.bubbles=!1,this.fireEvent("pointerenter",a)},leave:function(a){a.bubbles=!1,this.fireEvent("pointerleave",a)},over:function(a){a.bubbles=!0,this.fireEvent("pointerover",a)},out:function(a){a.bubbles=!0,this.fireEvent("pointerout",a)},cancel:function(a){a.bubbles=!0,this.fireEvent("pointercancel",a)},leaveOut:function(a){this.out(a),this.propagate(a,this.leave,!1)},enterOver:function(a){this.over(a),this.propagate(a,this.enter,!0)},eventHandler:function(a){if(!a._handledByPE){var b=a.type,c=this.eventMap&&this.eventMap[b];c&&c(a),a._handledByPE=!0}},listen:function(a,b){b.forEach(function(b){this.addEvent(a,b)},this)},unlisten:function(a,b){b.forEach(function(b){this.removeEvent(a,b)},this)},addEvent:function(a,b){a.addEventListener(b,this.boundHandler)},removeEvent:function(a,b){a.removeEventListener(b,this.boundHandler)},makeEvent:function(b,c){this.captureInfo[c.pointerId]&&(c.relatedTarget=null);var d=new a(b,c);return c.preventDefault&&(d.preventDefault=c.preventDefault),d._target=d._target||c.target,d},fireEvent:function(a,b){var c=this.makeEvent(a,b);return this.dispatchEvent(c)},cloneEvent:function(a){for(var b,c=Object.create(null),d=0;d<q.length;d++)b=q[d],c[b]=a[b]||r[d],!t||"target"!==b&&"relatedTarget"!==b||c[b]instanceof SVGElementInstance&&(c[b]=c[b].correspondingUseElement);return a.preventDefault&&(c.preventDefault=function(){a.preventDefault()}),c},getTarget:function(a){var b=this.captureInfo[a.pointerId];return b?a._target!==b&&a.type in s?void 0:b:a._target},propagate:function(a,b,c){for(var d=a.target,e=[];d!==document&&!d.contains(a.relatedTarget);) if(e.push(d),d=d.parentNode,!d)return;c&&e.reverse(),e.forEach(function(c){a.target=c,b.call(this,a)},this)},setCapture:function(b,c,d){this.captureInfo[b]&&this.releaseCapture(b,d),this.captureInfo[b]=c,this.implicitRelease=this.releaseCapture.bind(this,b,d),document.addEventListener("pointerup",this.implicitRelease),document.addEventListener("pointercancel",this.implicitRelease);var e=new a("gotpointercapture");e.pointerId=b,e._target=c,d||this.asyncDispatchEvent(e)},releaseCapture:function(b,c){var d=this.captureInfo[b];if(d){this.captureInfo[b]=void 0,document.removeEventListener("pointerup",this.implicitRelease),document.removeEventListener("pointercancel",this.implicitRelease);var e=new a("lostpointercapture");e.pointerId=b,e._target=d,c||this.asyncDispatchEvent(e)}},dispatchEvent:/*scope.external.dispatchEvent || */function(a){var b=this.getTarget(a);if(b)return b.dispatchEvent(a)},asyncDispatchEvent:function(a){requestAnimationFrame(this.dispatchEvent.bind(this,a))}};u.boundHandler=u.eventHandler.bind(u);var v={shadow:function(a){if(a)return a.shadowRoot||a.webkitShadowRoot},canTarget:function(a){return a&&Boolean(a.elementFromPoint)},targetingShadow:function(a){var b=this.shadow(a);if(this.canTarget(b))return b},olderShadow:function(a){var b=a.olderShadowRoot;if(!b){var c=a.querySelector("shadow");c&&(b=c.olderShadowRoot)}return b},allShadows:function(a){for(var b=[],c=this.shadow(a);c;)b.push(c),c=this.olderShadow(c);return b},searchRoot:function(a,b,c){if(a){var d,e,f=a.elementFromPoint(b,c);for(e=this.targetingShadow(f);e;){if(d=e.elementFromPoint(b,c)){var g=this.targetingShadow(d);return this.searchRoot(g,b,c)||d} e=this.olderShadow(e)} return f}},owner:function(a){
for(var b=a;b.parentNode;)b=b.parentNode;
return b.nodeType!==Node.DOCUMENT_NODE&&b.nodeType!==Node.DOCUMENT_FRAGMENT_NODE&&(b=document),b},findTarget:function(a){var b=a.clientX,c=a.clientY,d=this.owner(a.target);
return d.elementFromPoint(b,c)||(d=document),this.searchRoot(d,b,c)}},w=Array.prototype.forEach.call.bind(Array.prototype.forEach),x=Array.prototype.map.call.bind(Array.prototype.map),y=Array.prototype.slice.call.bind(Array.prototype.slice),z=Array.prototype.filter.call.bind(Array.prototype.filter),A=window.MutationObserver||window.WebKitMutationObserver,B="[touch-action]",C={subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0,attributeFilter:["touch-action"]};c.prototype={watchSubtree:function(a){
//
this.observer&&v.canTarget(a)&&this.observer.observe(a,C)},enableOnSubtree:function(a){this.watchSubtree(a),a===document&&"complete"!==document.readyState?this.installOnLoad():this.installNewSubtree(a)},installNewSubtree:function(a){w(this.findElements(a),this.addElement,this)},findElements:function(a){return a.querySelectorAll?a.querySelectorAll(B):[]},removeElement:function(a){this.removeCallback(a)},addElement:function(a){this.addCallback(a)},elementChanged:function(a,b){this.changedCallback(a,b)},concatLists:function(a,b){return a.concat(y(b))},
installOnLoad:function(){document.addEventListener("readystatechange",function(){"complete"===document.readyState&&this.installNewSubtree(document)}.bind(this))},isElement:function(a){return a.nodeType===Node.ELEMENT_NODE},flattenMutationTree:function(a){
var b=x(a,this.findElements,this);
return b.push(z(a,this.isElement)),b.reduce(this.concatLists,[])},mutationWatcher:function(a){a.forEach(this.mutationHandler,this)},mutationHandler:function(a){if("childList"===a.type){var b=this.flattenMutationTree(a.addedNodes);b.forEach(this.addElement,this);var c=this.flattenMutationTree(a.removedNodes);c.forEach(this.removeElement,this)}else"attributes"===a.type&&this.elementChanged(a.target,a.oldValue)}};var D=["none","auto","pan-x","pan-y",{rule:"pan-x pan-y",selectors:["pan-x pan-y","pan-y pan-x"]}],E="",F=window.PointerEvent||window.MSPointerEvent,G=!window.ShadowDOMPolyfill&&document.head.createShadowRoot,H=u.pointermap,I=25,J=[1,4,2,8,16],K=!1;try{K=1===new MouseEvent("test",{buttons:1}).buttons}catch(L){}
var M,N={POINTER_ID:1,POINTER_TYPE:"mouse",events:["mousedown","mousemove","mouseup","mouseover","mouseout"],register:function(a){u.listen(a,this.events)},unregister:function(a){u.unlisten(a,this.events)},lastTouches:[],
isEventSimulatedFromTouch:function(a){for(var b,c=this.lastTouches,d=a.clientX,e=a.clientY,f=0,g=c.length;f<g&&(b=c[f]);f++){
var h=Math.abs(d-b.x),i=Math.abs(e-b.y);if(h<=I&&i<=I)return!0}},prepareEvent:function(a){var b=u.cloneEvent(a),c=b.preventDefault;return b.preventDefault=function(){a.preventDefault(),c()},b.pointerId=this.POINTER_ID,b.isPrimary=!0,b.pointerType=this.POINTER_TYPE,b},prepareButtonsForMove:function(a,b){var c=H.get(this.POINTER_ID);
0!==b.which&&c?a.buttons=c.buttons:a.buttons=0,b.buttons=a.buttons},mousedown:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=H.get(this.POINTER_ID),c=this.prepareEvent(a);K||(c.buttons=J[c.button],b&&(c.buttons|=b.buttons),a.buttons=c.buttons),H.set(this.POINTER_ID,a),b&&0!==b.buttons?u.move(c):u.down(c)}},mousemove:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=this.prepareEvent(a);K||this.prepareButtonsForMove(b,a),b.button=-1,H.set(this.POINTER_ID,a),u.move(b)}},mouseup:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=H.get(this.POINTER_ID),c=this.prepareEvent(a);if(!K){var d=J[c.button];
c.buttons=b?b.buttons&~d:0,a.buttons=c.buttons}H.set(this.POINTER_ID,a),
c.buttons&=~J[c.button],0===c.buttons?u.up(c):u.move(c)}},mouseover:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=this.prepareEvent(a);K||this.prepareButtonsForMove(b,a),b.button=-1,H.set(this.POINTER_ID,a),u.enterOver(b)}},mouseout:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=this.prepareEvent(a);K||this.prepareButtonsForMove(b,a),b.button=-1,u.leaveOut(b)}},cancel:function(a){var b=this.prepareEvent(a);u.cancel(b),this.deactivateMouse()},deactivateMouse:function(){H["delete"](this.POINTER_ID)}},O=u.captureInfo,P=v.findTarget.bind(v),Q=v.allShadows.bind(v),R=u.pointermap,S=2500,T=200,U="touch-action",V={events:["touchstart","touchmove","touchend","touchcancel"],register:function(a){M.enableOnSubtree(a)},unregister:function(){},elementAdded:function(a){var b=a.getAttribute(U),c=this.touchActionToScrollType(b);c&&(a._scrollType=c,u.listen(a,this.events),
Q(a).forEach(function(a){a._scrollType=c,u.listen(a,this.events)},this))},elementRemoved:function(a){a._scrollType=void 0,u.unlisten(a,this.events),
Q(a).forEach(function(a){a._scrollType=void 0,u.unlisten(a,this.events)},this)},elementChanged:function(a,b){var c=a.getAttribute(U),d=this.touchActionToScrollType(c),e=this.touchActionToScrollType(b);
d&&e?(a._scrollType=d,Q(a).forEach(function(a){a._scrollType=d},this)):e?this.elementRemoved(a):d&&this.elementAdded(a)},scrollTypes:{EMITTER:"none",XSCROLLER:"pan-x",YSCROLLER:"pan-y",SCROLLER:/^(?:pan-x pan-y)|(?:pan-y pan-x)|auto$/},touchActionToScrollType:function(a){var b=a,c=this.scrollTypes;return"none"===b?"none":b===c.XSCROLLER?"X":b===c.YSCROLLER?"Y":c.SCROLLER.exec(b)?"XY":void 0},POINTER_TYPE:"touch",firstTouch:null,isPrimaryTouch:function(a){return this.firstTouch===a.identifier},setPrimaryTouch:function(a){
(0===R.size||1===R.size&&R.has(1))&&(this.firstTouch=a.identifier,this.firstXY={X:a.clientX,Y:a.clientY},this.scrolling=!1,this.cancelResetClickCount())},removePrimaryPointer:function(a){a.isPrimary&&(this.firstTouch=null,this.firstXY=null,this.resetClickCount())},clickCount:0,resetId:null,resetClickCount:function(){var a=function(){this.clickCount=0,this.resetId=null}.bind(this);this.resetId=setTimeout(a,T)},cancelResetClickCount:function(){this.resetId&&clearTimeout(this.resetId)},typeToButtons:function(a){var b=0;return"touchstart"!==a&&"touchmove"!==a||(b=1),b},touchToPointer:function(a){var b=this.currentTouchEvent,c=u.cloneEvent(a),d=c.pointerId=a.identifier+2;c.target=O[d]||P(c),c.bubbles=!0,c.cancelable=!0,c.detail=this.clickCount,c.button=0,c.buttons=this.typeToButtons(b.type),c.width=2*(a.radiusX||a.webkitRadiusX||0),c.height=2*(a.radiusY||a.webkitRadiusY||0),c.pressure=a.force||a.webkitForce||.5,c.isPrimary=this.isPrimaryTouch(a),c.pointerType=this.POINTER_TYPE,
c.altKey=b.altKey,c.ctrlKey=b.ctrlKey,c.metaKey=b.metaKey,c.shiftKey=b.shiftKey;
var e=this;return c.preventDefault=function(){e.scrolling=!1,e.firstXY=null,b.preventDefault()},c},processTouches:function(a,b){var c=a.changedTouches;this.currentTouchEvent=a;for(var d,e=0;e<c.length;e++)d=c[e],b.call(this,this.touchToPointer(d))},
shouldScroll:function(a){if(this.firstXY){var b,c=a.currentTarget._scrollType;if("none"===c)
b=!1;else if("XY"===c)
b=!0;else{var d=a.changedTouches[0],e=c,f="Y"===c?"X":"Y",g=Math.abs(d["client"+e]-this.firstXY[e]),h=Math.abs(d["client"+f]-this.firstXY[f]);
b=g>=h}return this.firstXY=null,b}},findTouch:function(a,b){for(var c,d=0,e=a.length;d<e&&(c=a[d]);d++)if(c.identifier===b)return!0},
vacuumTouches:function(a){var b=a.touches;
if(R.size>=b.length){var c=[];R.forEach(function(a,d){
if(1!==d&&!this.findTouch(b,d-2)){var e=a.out;c.push(e)}},this),c.forEach(this.cancelOut,this)}},touchstart:function(a){this.vacuumTouches(a),this.setPrimaryTouch(a.changedTouches[0]),this.dedupSynthMouse(a),this.scrolling||(this.clickCount++,this.processTouches(a,this.overDown))},overDown:function(a){R.set(a.pointerId,{target:a.target,out:a,outTarget:a.target}),u.enterOver(a),u.down(a)},touchmove:function(a){this.scrolling||(this.shouldScroll(a)?(this.scrolling=!0,this.touchcancel(a)):(a.preventDefault(),this.processTouches(a,this.moveOverOut)))},moveOverOut:function(a){var b=a,c=R.get(b.pointerId);
if(c){var d=c.out,e=c.outTarget;u.move(b),d&&e!==b.target&&(d.relatedTarget=b.target,b.relatedTarget=e,
d.target=e,b.target?(u.leaveOut(d),u.enterOver(b)):(
b.target=e,b.relatedTarget=null,this.cancelOut(b))),c.out=b,c.outTarget=b.target}},touchend:function(a){this.dedupSynthMouse(a),this.processTouches(a,this.upOut)},upOut:function(a){this.scrolling||(u.up(a),u.leaveOut(a)),this.cleanUpPointer(a)},touchcancel:function(a){this.processTouches(a,this.cancelOut)},cancelOut:function(a){u.cancel(a),u.leaveOut(a),this.cleanUpPointer(a)},cleanUpPointer:function(a){R["delete"](a.pointerId),this.removePrimaryPointer(a)},
dedupSynthMouse:function(a){var b=N.lastTouches,c=a.changedTouches[0];
if(this.isPrimaryTouch(c)){
var d={x:c.clientX,y:c.clientY};b.push(d);var e=function(a,b){var c=a.indexOf(b);c>-1&&a.splice(c,1)}.bind(null,b,d);setTimeout(e,S)}}};M=new c(V.elementAdded,V.elementRemoved,V.elementChanged,V);var W,X,Y,Z=u.pointermap,$=window.MSPointerEvent&&"number"==typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE,_={events:["MSPointerDown","MSPointerMove","MSPointerUp","MSPointerOut","MSPointerOver","MSPointerCancel","MSGotPointerCapture","MSLostPointerCapture"],register:function(a){u.listen(a,this.events)},unregister:function(a){u.unlisten(a,this.events)},POINTER_TYPES:["","unavailable","touch","pen","mouse"],prepareEvent:function(a){var b=a;return $&&(b=u.cloneEvent(a),b.pointerType=this.POINTER_TYPES[a.pointerType]),b},cleanup:function(a){Z["delete"](a)},MSPointerDown:function(a){Z.set(a.pointerId,a);var b=this.prepareEvent(a);u.down(b)},MSPointerMove:function(a){var b=this.prepareEvent(a);u.move(b)},MSPointerUp:function(a){var b=this.prepareEvent(a);u.up(b),this.cleanup(a.pointerId)},MSPointerOut:function(a){var b=this.prepareEvent(a);u.leaveOut(b)},MSPointerOver:function(a){var b=this.prepareEvent(a);u.enterOver(b)},MSPointerCancel:function(a){var b=this.prepareEvent(a);u.cancel(b),this.cleanup(a.pointerId)},MSLostPointerCapture:function(a){var b=u.makeEvent("lostpointercapture",a);u.dispatchEvent(b)},MSGotPointerCapture:function(a){var b=u.makeEvent("gotpointercapture",a);u.dispatchEvent(b)}},aa=window.navigator;aa.msPointerEnabled?(W=function(a){i(a),j(this),k(a)&&(u.setCapture(a,this,!0),this.msSetPointerCapture(a))},X=function(a){i(a),u.releaseCapture(a,!0),this.msReleasePointerCapture(a)}):(W=function(a){i(a),j(this),k(a)&&u.setCapture(a,this)},X=function(a){i(a),u.releaseCapture(a)}),Y=function(a){return!!u.captureInfo[a]},g(),h(),l();var ba={dispatcher:u,Installer:c,PointerEvent:a,PointerMap:p,targetFinding:v};return ba});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
/*
Split.js - v1.3.5
MIT License
https://github.com/nathancahill/Split.js
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var e=window,t=e.document,n="addEventListener",i="removeEventListener",r="getBoundingClientRect",s=function(){return!1},o=e.attachEvent&&!e[n],a=["","-webkit-","-moz-","-o-"].filter(function(e){var n=t.createElement("div");return n.style.cssText="width:"+e+"calc(9px)",!!n.style.length}).shift()+"calc",l=function(e){return"string"==typeof e||e instanceof String?t.querySelector(e):e};return function(u,c){function z(e,t,n){var i=A(y,t,n);Object.keys(i).forEach(function(t){return e.style[t]=i[t]})}function h(e,t){var n=B(y,t);Object.keys(n).forEach(function(t){return e.style[t]=n[t]})}function f(e){var t=E[this.a],n=E[this.b],i=t.size+n.size;t.size=e/this.size*i,n.size=i-e/this.size*i,z(t.element,t.size,this.aGutterSize),z(n.element,n.size,this.bGutterSize)}function m(e){var t;this.dragging&&((t="touches"in e?e.touches[0][b]-this.start:e[b]-this.start)<=E[this.a].minSize+M+this.aGutterSize?t=E[this.a].minSize+this.aGutterSize:t>=this.size-(E[this.b].minSize+M+this.bGutterSize)&&(t=this.size-(E[this.b].minSize+this.bGutterSize)),f.call(this,t),c.onDrag&&c.onDrag())}function g(){var e=E[this.a].element,t=E[this.b].element;this.size=e[r]()[y]+t[r]()[y]+this.aGutterSize+this.bGutterSize,this.start=e[r]()[G]}function d(){var t=this,n=E[t.a].element,r=E[t.b].element;t.dragging&&c.onDragEnd&&c.onDragEnd(),t.dragging=!1,e[i]("mouseup",t.stop),e[i]("touchend",t.stop),e[i]("touchcancel",t.stop),t.parent[i]("mousemove",t.move),t.parent[i]("touchmove",t.move),delete t.stop,delete t.move,n[i]("selectstart",s),n[i]("dragstart",s),r[i]("selectstart",s),r[i]("dragstart",s),n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",r.style.userSelect="",r.style.webkitUserSelect="",r.style.MozUserSelect="",r.style.pointerEvents="",t.gutter.style.cursor="",t.parent.style.cursor=""}function S(t){var i=this,r=E[i.a].element,o=E[i.b].element;!i.dragging&&c.onDragStart&&c.onDragStart(),t.preventDefault(),i.dragging=!0,i.move=m.bind(i),i.stop=d.bind(i),e[n]("mouseup",i.stop),e[n]("touchend",i.stop),e[n]("touchcancel",i.stop),i.parent[n]("mousemove",i.move),i.parent[n]("touchmove",i.move),r[n]("selectstart",s),r[n]("dragstart",s),o[n]("selectstart",s),o[n]("dragstart",s),r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",o.style.userSelect="none",o.style.webkitUserSelect="none",o.style.MozUserSelect="none",o.style.pointerEvents="none",i.gutter.style.cursor=j,i.parent.style.cursor=j,g.call(i)}function v(e){e.forEach(function(t,n){if(n>0){var i=F[n-1],r=E[i.a],s=E[i.b];r.size=e[n-1],s.size=t,z(r.element,r.size,i.aGutterSize),z(s.element,s.size,i.bGutterSize)}})}function p(){F.forEach(function(e){e.parent.removeChild(e.gutter),E[e.a].element.style[y]="",E[e.b].element.style[y]=""})}void 0===c&&(c={});var y,b,G,E,w=l(u[0]).parentNode,D=e.getComputedStyle(w).flexDirection,U=c.sizes||u.map(function(){return 100/u.length}),k=void 0!==c.minSize?c.minSize:100,x=Array.isArray(k)?k:u.map(function(){return k}),L=void 0!==c.gutterSize?c.gutterSize:10,M=void 0!==c.snapOffset?c.snapOffset:30,O=c.direction||"horizontal",j=c.cursor||("horizontal"===O?"ew-resize":"ns-resize"),C=c.gutter||function(e,n){var i=t.createElement("div");return i.className="gutter gutter-"+n,i},A=c.elementStyle||function(e,t,n){var i={};return"string"==typeof t||t instanceof String?i[e]=t:i[e]=o?t+"%":a+"("+t+"% - "+n+"px)",i},B=c.gutterStyle||function(e,t){return n={},n[e]=t+"px",n;var n};"horizontal"===O?(y="width","clientWidth",b="clientX",G="left","paddingLeft"):"vertical"===O&&(y="height","clientHeight",b="clientY",G="top","paddingTop");var F=[];return E=u.map(function(e,t){var i,s={element:l(e),size:U[t],minSize:x[t]};if(t>0&&(i={a:t-1,b:t,dragging:!1,isFirst:1===t,isLast:t===u.length-1,direction:O,parent:w},i.aGutterSize=L,i.bGutterSize=L,i.isFirst&&(i.aGutterSize=L/2),i.isLast&&(i.bGutterSize=L/2),"row-reverse"===D||"column-reverse"===D)){var a=i.a;i.a=i.b,i.b=a}if(!o&&t>0){var c=C(t,O);h(c,L),c[n]("mousedown",S.bind(i)),c[n]("touchstart",S.bind(i)),w.insertBefore(c,s.element),i.gutter=c}0===t||t===u.length-1?z(s.element,s.size,L/2):z(s.element,s.size,L);var f=s.element[r]()[y];return f<s.minSize&&(s.minSize=f),t>0&&F.push(i),s}),o?{setSizes:v,destroy:p}:{setSizes:v,getSizes:function(){return E.map(function(e){return e.size})},collapse:function(e){if(e===F.length){var t=F[e-1];g.call(t),o||f.call(t,t.size-t.bGutterSize)}else{var n=F[e];g.call(n),o||f.call(n,n.aGutterSize)}},destroy:p}}});

View File

@ -0,0 +1,371 @@
/*
* Table reordering via Drag'n'Drop
* Inspired by: https://htmldom.dev/drag-and-drop-table-column
*/
function setBomHandlers() {
const bom = document.getElementById('bomtable');
let dragName;
let placeHolderElements;
let draggingElement;
let forcePopulation;
let xOffset;
let yOffset;
let wasDragged;
const mouseUpHandler = function(e) {
// Delete dragging element
draggingElement.remove();
// Make BOM selectable again
bom.style.removeProperty("user-select");
// Remove listeners
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
if (wasDragged) {
// Redraw whole BOM
populateBomTable();
}
}
const mouseMoveHandler = function(e) {
// Notice the dragging
wasDragged = true;
// Make the dragged element visible
draggingElement.style.removeProperty("display");
// Set elements position to mouse position
draggingElement.style.left = `${e.screenX - xOffset}px`;
draggingElement.style.top = `${e.screenY - yOffset}px`;
// Forced redrawing of BOM table
if (forcePopulation) {
forcePopulation = false;
// Copy array
phe = Array.from(placeHolderElements);
// populate BOM table again
populateBomHeader(dragName, phe);
populateBomBody(dragName, phe);
}
// Set up array of hidden columns
var hiddenColumns = Array.from(settings.hiddenColumns);
// In the ungrouped mode, quantity don't exist
if (settings.bommode === "ungrouped")
hiddenColumns.push("Quantity");
// If no checkbox fields can be found, we consider them hidden
if (settings.checkboxes.length == 0)
hiddenColumns.push("checkboxes");
// Get table headers and group them into checkboxes, extrafields and normal headers
const bh = document.getElementById("bomhead");
headers = Array.from(bh.querySelectorAll("th"))
headers.shift() // numCol is not part of the columnOrder
headerGroups = []
lastCompoundClass = null;
for (i = 0; i < settings.columnOrder.length; i++) {
cElem = settings.columnOrder[i];
if (hiddenColumns.includes(cElem)) {
// Hidden columns appear as a dummy element
headerGroups.push([]);
continue;
}
elem = headers.filter(e => getColumnOrderName(e) === cElem)[0];
if (elem.classList.contains("bom-checkbox")) {
if (lastCompoundClass === "bom-checkbox") {
cbGroup = headerGroups.pop();
cbGroup.push(elem);
headerGroups.push(cbGroup);
} else {
lastCompoundClass = "bom-checkbox";
headerGroups.push([elem])
}
} else {
headerGroups.push([elem])
}
}
// Copy settings.columnOrder
var columns = Array.from(settings.columnOrder)
// Set up array with indices of hidden columns
var hiddenIndices = hiddenColumns.map(e => settings.columnOrder.indexOf(e));
var dragIndex = columns.indexOf(dragName);
var swapIndex = dragIndex;
var swapDone = false;
// Check if the current dragged element is swapable with the left or right element
if (dragIndex > 0) {
// Get left headers boundingbox
swapIndex = dragIndex - 1;
while (hiddenIndices.includes(swapIndex) && swapIndex > 0)
swapIndex--;
if (!hiddenIndices.includes(swapIndex)) {
box = getBoundingClientRectFromMultiple(headerGroups[swapIndex]);
if (e.clientX < box.left + window.scrollX + (box.width / 2)) {
swapElement = columns[dragIndex];
columns.splice(dragIndex, 1);
columns.splice(swapIndex, 0, swapElement);
forcePopulation = true;
swapDone = true;
}
}
}
if ((!swapDone) && dragIndex < headerGroups.length - 1) {
// Get right headers boundingbox
swapIndex = dragIndex + 1;
while (hiddenIndices.includes(swapIndex))
swapIndex++;
if (swapIndex < headerGroups.length) {
box = getBoundingClientRectFromMultiple(headerGroups[swapIndex]);
if (e.clientX > box.left + window.scrollX + (box.width / 2)) {
swapElement = columns[dragIndex];
columns.splice(dragIndex, 1);
columns.splice(swapIndex, 0, swapElement);
forcePopulation = true;
swapDone = true;
}
}
}
// Write back change to storage
if (swapDone) {
settings.columnOrder = columns
writeStorage("columnOrder", JSON.stringify(columns));
}
}
const mouseDownHandler = function(e) {
var target = e.target;
if (target.tagName.toLowerCase() != "td")
target = target.parentElement;
// Used to check if a dragging has ever happened
wasDragged = false;
// Create new element which will be displayed as the dragged column
draggingElement = document.createElement("div")
draggingElement.classList.add("dragging");
draggingElement.style.display = "none";
draggingElement.style.position = "absolute";
draggingElement.style.overflow = "hidden";
// Get bomhead and bombody elements
const bh = document.getElementById("bomhead");
const bb = document.getElementById("bombody");
// Get all compound headers for the current column
var compoundHeaders;
if (target.classList.contains("bom-checkbox")) {
compoundHeaders = Array.from(bh.querySelectorAll("th.bom-checkbox"));
} else {
compoundHeaders = [target];
}
// Create new table which will display the column
var newTable = document.createElement("table");
newTable.classList.add("bom");
newTable.style.background = "white";
draggingElement.append(newTable);
// Create new header element
var newHeader = document.createElement("thead");
newTable.append(newHeader);
// Set up array for storing all placeholder elements
placeHolderElements = [];
// Add all compound headers to the new thead element and placeholders
compoundHeaders.forEach(function(h) {
clone = cloneElementWithDimensions(h);
newHeader.append(clone);
placeHolderElements.push(clone);
});
// Create new body element
var newBody = document.createElement("tbody");
newTable.append(newBody);
// Get indices for compound headers
var idxs = compoundHeaders.map(e => getBomTableHeaderIndex(e));
// For each row in the BOM body...
var rows = bb.querySelectorAll("tr");
rows.forEach(function(row) {
// ..get the cells for the compound column
const tds = row.querySelectorAll("td");
var copytds = idxs.map(i => tds[i]);
// Add them to the new element and the placeholders
var newRow = document.createElement("tr");
copytds.forEach(function(td) {
clone = cloneElementWithDimensions(td);
newRow.append(clone);
placeHolderElements.push(clone);
});
newBody.append(newRow);
});
// Compute width for compound header
var width = compoundHeaders.reduce((acc, x) => acc + x.clientWidth, 0);
draggingElement.style.width = `${width}px`;
// Insert the new dragging element and disable selection on BOM
bom.insertBefore(draggingElement, null);
bom.style.userSelect = "none";
// Determine the mouse position offset
xOffset = e.screenX - compoundHeaders.reduce((acc, x) => Math.min(acc, x.offsetLeft), compoundHeaders[0].offsetLeft);
yOffset = e.screenY - compoundHeaders[0].offsetTop;
// Get name for the column in settings.columnOrder
dragName = getColumnOrderName(target);
// Change text and class for placeholder elements
placeHolderElements = placeHolderElements.map(function(e) {
newElem = cloneElementWithDimensions(e);
newElem.textContent = "";
newElem.classList.add("placeholder");
return newElem;
});
// On next mouse move, the whole BOM needs to be redrawn to show the placeholders
forcePopulation = true;
// Add listeners for move and up on mouse
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
}
// In netlist mode, there is nothing to reorder
if (settings.bommode === "netlist")
return;
// Add mouseDownHandler to every column except the numCol
bom.querySelectorAll("th")
.forEach(function(head) {
if (!head.classList.contains("numCol")) {
head.onmousedown = mouseDownHandler;
}
});
}
function getBoundingClientRectFromMultiple(elements) {
var elems = Array.from(elements);
if (elems.length == 0)
return null;
var box = elems.shift()
.getBoundingClientRect();
elems.forEach(function(elem) {
var elembox = elem.getBoundingClientRect();
box.left = Math.min(elembox.left, box.left);
box.top = Math.min(elembox.top, box.top);
box.width += elembox.width;
box.height = Math.max(elembox.height, box.height);
});
return box;
}
function cloneElementWithDimensions(elem) {
var newElem = elem.cloneNode(true);
newElem.style.height = window.getComputedStyle(elem).height;
newElem.style.width = window.getComputedStyle(elem).width;
return newElem;
}
function getBomTableHeaderIndex(elem) {
const bh = document.getElementById('bomhead');
const ths = Array.from(bh.querySelectorAll("th"));
return ths.indexOf(elem);
}
function getColumnOrderName(elem) {
var cname = elem.getAttribute("col_name");
if (cname === "bom-checkbox")
return "checkboxes";
else
return cname;
}
function resizableGrid(tablehead) {
var cols = tablehead.firstElementChild.children;
var rowWidth = tablehead.offsetWidth;
for (var i = 1; i < cols.length; i++) {
if (cols[i].classList.contains("bom-checkbox"))
continue;
cols[i].style.width = ((cols[i].clientWidth - paddingDiff(cols[i])) * 100 / rowWidth) + '%';
}
for (var i = 1; i < cols.length - 1; i++) {
var div = document.createElement('div');
div.className = "column-width-handle";
cols[i].appendChild(div);
setListeners(div);
}
function setListeners(div) {
var startX, curCol, nxtCol, curColWidth, nxtColWidth, rowWidth;
div.addEventListener('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
curCol = e.target.parentElement;
nxtCol = curCol.nextElementSibling;
startX = e.pageX;
var padding = paddingDiff(curCol);
rowWidth = curCol.parentElement.offsetWidth;
curColWidth = curCol.clientWidth - padding;
nxtColWidth = nxtCol.clientWidth - padding;
});
document.addEventListener('mousemove', function(e) {
if (startX) {
var diffX = e.pageX - startX;
diffX = -Math.min(-diffX, curColWidth - 20);
diffX = Math.min(diffX, nxtColWidth - 20);
curCol.style.width = ((curColWidth + diffX) * 100 / rowWidth) + '%';
nxtCol.style.width = ((nxtColWidth - diffX) * 100 / rowWidth) + '%';
console.log(`${curColWidth + nxtColWidth} ${(curColWidth + diffX) * 100 / rowWidth + (nxtColWidth - diffX) * 100 / rowWidth}`);
}
});
document.addEventListener('mouseup', function(e) {
curCol = undefined;
nxtCol = undefined;
startX = undefined;
nxtColWidth = undefined;
curColWidth = undefined
});
}
function paddingDiff(col) {
if (getStyleVal(col, 'box-sizing') == 'border-box') {
return 0;
}
var padLeft = getStyleVal(col, 'padding-left');
var padRight = getStyleVal(col, 'padding-right');
return (parseInt(padLeft) + parseInt(padRight));
}
function getStyleVal(elm, css) {
return (window.getComputedStyle(elm, null).getPropertyValue(css))
}
}

View File

@ -0,0 +1,647 @@
/* Utility functions */
var storagePrefix = 'KiCad_HTML_BOM__' + pcbdata.metadata.title + '__' +
pcbdata.metadata.revision + '__#';
var storage;
function initStorage(key) {
try {
window.localStorage.getItem("blank");
storage = window.localStorage;
} catch (e) {
// localStorage not available
}
if (!storage) {
try {
window.sessionStorage.getItem("blank");
storage = window.sessionStorage;
} catch (e) {
// sessionStorage also not available
}
}
}
function readStorage(key) {
if (storage) {
return storage.getItem(storagePrefix + key);
} else {
return null;
}
}
function writeStorage(key, value) {
if (storage) {
storage.setItem(storagePrefix + key, value);
}
}
function fancyDblClickHandler(el, onsingle, ondouble) {
return function () {
if (el.getAttribute("data-dblclick") == null) {
el.setAttribute("data-dblclick", 1);
setTimeout(function () {
if (el.getAttribute("data-dblclick") == 1) {
onsingle();
}
el.removeAttribute("data-dblclick");
}, 200);
} else {
el.removeAttribute("data-dblclick");
ondouble();
}
}
}
function smoothScrollToRow(rowid) {
document.getElementById(rowid).scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest"
});
}
function focusInputField(input) {
input.scrollIntoView(false);
input.focus();
input.select();
}
function saveBomTable(output) {
var text = '';
for (var node of bomhead.childNodes[0].childNodes) {
if (node.firstChild) {
var name = node.firstChild.nodeValue ?? "";
text += (output == 'csv' ? `"${name}"` : name);
}
if (node != bomhead.childNodes[0].lastChild) {
text += (output == 'csv' ? ',' : '\t');
}
}
text += '\n';
for (var row of bombody.childNodes) {
for (var cell of row.childNodes) {
let val = '';
for (var node of cell.childNodes) {
if (node.nodeName == "INPUT") {
if (node.checked) {
val += '✓';
}
} else if ((node.nodeName == "MARK") || (node.nodeName == "A")) {
val += node.firstChild.nodeValue;
} else {
val += node.nodeValue;
}
}
if (output == 'csv') {
val = val.replace(/\"/g, '\"\"'); // pair of double-quote characters
if (isNumeric(val)) {
val = +val; // use number
} else {
val = `"${val}"`; // enclosed within double-quote
}
}
text += val;
if (cell != row.lastChild) {
text += (output == 'csv' ? ',' : '\t');
}
}
text += '\n';
}
if (output != 'clipboard') {
// To file: csv or txt
var blob = new Blob([text], {
type: `text/${output}`
});
saveFile(`${pcbdata.metadata.title}.${output}`, blob);
} else {
// To clipboard
var textArea = document.createElement("textarea");
textArea.classList.add('clipboard-temp');
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
if (document.execCommand('copy')) {
console.log('Bom copied to clipboard.');
}
} catch (err) {
console.log('Can not copy to clipboard.');
}
document.body.removeChild(textArea);
}
}
function isNumeric(str) {
/* https://stackoverflow.com/a/175787 */
return (typeof str != "string" ? false : !isNaN(str) && !isNaN(parseFloat(str)));
}
function removeGutterNode(node) {
for (var i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].classList &&
node.childNodes[i].classList.contains("gutter")) {
node.removeChild(node.childNodes[i]);
break;
}
}
}
function cleanGutters() {
removeGutterNode(document.getElementById("bot"));
removeGutterNode(document.getElementById("canvasdiv"));
}
var units = {
prefixes: {
giga: ["G", "g", "giga", "Giga", "GIGA"],
mega: ["M", "mega", "Mega", "MEGA"],
kilo: ["K", "k", "kilo", "Kilo", "KILO"],
milli: ["m", "milli", "Milli", "MILLI"],
micro: ["U", "u", "micro", "Micro", "MICRO", "μ", "µ"], // different utf8 μ
nano: ["N", "n", "nano", "Nano", "NANO"],
pico: ["P", "p", "pico", "Pico", "PICO"],
},
unitsShort: ["R", "r", "Ω", "F", "f", "H", "h"],
unitsLong: [
"OHM", "Ohm", "ohm", "ohms",
"FARAD", "Farad", "farad",
"HENRY", "Henry", "henry"
],
getMultiplier: function (s) {
if (this.prefixes.giga.includes(s)) return 1e9;
if (this.prefixes.mega.includes(s)) return 1e6;
if (this.prefixes.kilo.includes(s)) return 1e3;
if (this.prefixes.milli.includes(s)) return 1e-3;
if (this.prefixes.micro.includes(s)) return 1e-6;
if (this.prefixes.nano.includes(s)) return 1e-9;
if (this.prefixes.pico.includes(s)) return 1e-12;
return 1;
},
valueRegex: null,
valueAltRegex: null,
}
function initUtils() {
var allPrefixes = units.prefixes.giga
.concat(units.prefixes.mega)
.concat(units.prefixes.kilo)
.concat(units.prefixes.milli)
.concat(units.prefixes.micro)
.concat(units.prefixes.nano)
.concat(units.prefixes.pico);
var allUnits = units.unitsShort.concat(units.unitsLong);
units.valueRegex = new RegExp("^([0-9\.]+)" +
"\\s*(" + allPrefixes.join("|") + ")?" +
"(" + allUnits.join("|") + ")?" +
"(\\b.*)?$", "");
units.valueAltRegex = new RegExp("^([0-9]*)" +
"(" + units.unitsShort.join("|") + ")?" +
"([GgMmKkUuNnPp])?" +
"([0-9]*)" +
"(\\b.*)?$", "");
if (config.fields.includes("Value")) {
var index = config.fields.indexOf("Value");
pcbdata.bom["parsedValues"] = {};
var allList = getBomListByLayer('FB').flat();
for (var id in pcbdata.bom.fields) {
var ref_key = allList.find(item => item[1] == Number(id)) || [];
pcbdata.bom.parsedValues[id] = parseValue(pcbdata.bom.fields[id][index], ref_key[0] || '');
}
}
}
function parseValue(val, ref) {
var inferUnit = (unit, ref) => {
if (unit) {
unit = unit.toLowerCase();
if (unit == 'Ω' || unit == "ohm" || unit == "ohms") {
unit = 'r';
}
return unit[0];
}
var resarr = /^([a-z]+)\d+$/i.exec(ref);
switch (Array.isArray(resarr) && resarr[1].toLowerCase()) {
case "c": return 'f';
case "l": return 'h';
case "r":
case "rv": return 'r';
}
return null;
};
val = val.replace(/,/g, "");
var match = units.valueRegex.exec(val);
if (Array.isArray(match)) {
var unit = inferUnit(match[3], ref);
var val_i = parseFloat(match[1]);
if (!unit) return null;
if (match[2]) {
val_i = val_i * units.getMultiplier(match[2]);
}
return {
val: val_i,
unit: unit,
extra: match[4],
}
}
match = units.valueAltRegex.exec(val);
if (Array.isArray(match) && (match[1] || match[4])) {
var unit = inferUnit(match[2], ref);
var val_i = parseFloat(match[1] + "." + match[4]);
if (!unit) return null;
if (match[3]) {
val_i = val_i * units.getMultiplier(match[3]);
}
return {
val: val_i,
unit: unit,
extra: match[5],
}
}
return null;
}
function valueCompare(a, b, stra, strb) {
if (a === null && b === null) {
// Failed to parse both values, compare them as strings.
if (stra != strb) return stra > strb ? 1 : -1;
else return 0;
} else if (a === null) {
return 1;
} else if (b === null) {
return -1;
} else {
if (a.unit != b.unit) return a.unit > b.unit ? 1 : -1;
else if (a.val != b.val) return a.val > b.val ? 1 : -1;
else if (a.extra != b.extra) return a.extra > b.extra ? 1 : -1;
else return 0;
}
}
function validateSaveImgDimension(element) {
var valid = false;
var intValue = 0;
if (/^[1-9]\d*$/.test(element.value)) {
intValue = parseInt(element.value);
if (intValue <= 16000) {
valid = true;
}
}
if (valid) {
element.classList.remove("invalid");
} else {
element.classList.add("invalid");
}
return intValue;
}
function saveImage(layer) {
var width = validateSaveImgDimension(document.getElementById("render-save-width"));
var height = validateSaveImgDimension(document.getElementById("render-save-height"));
var bgcolor = null;
if (!document.getElementById("render-save-transparent").checked) {
var style = getComputedStyle(topmostdiv);
bgcolor = style.getPropertyValue("background-color");
}
if (!width || !height) return;
// Prepare image
var canvas = document.createElement("canvas");
var layerdict = {
transform: {
x: 0,
y: 0,
s: 1,
panx: 0,
pany: 0,
zoom: 1,
},
bg: canvas,
fab: canvas,
silk: canvas,
highlight: canvas,
layer: layer,
}
// Do the rendering
recalcLayerScale(layerdict, width, height);
prepareLayer(layerdict);
clearCanvas(canvas, bgcolor);
drawBackground(layerdict, false);
drawHighlightsOnLayer(layerdict, false);
// Save image
var imgdata = canvas.toDataURL("image/png");
var filename = pcbdata.metadata.title;
if (pcbdata.metadata.revision) {
filename += `.${pcbdata.metadata.revision}`;
}
filename += `.${layer}.png`;
saveFile(filename, dataURLtoBlob(imgdata));
}
function saveSettings() {
var data = {
type: "InteractiveHtmlBom settings",
version: 1,
pcbmetadata: pcbdata.metadata,
settings: settings,
}
var blob = new Blob([JSON.stringify(data, null, 4)], {
type: "application/json"
});
saveFile(`${pcbdata.metadata.title}.settings.json`, blob);
}
function loadSettings() {
var input = document.createElement("input");
input.type = "file";
input.accept = ".settings.json";
input.onchange = function (e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = readerEvent => {
var content = readerEvent.target.result;
var newSettings;
try {
newSettings = JSON.parse(content);
} catch (e) {
alert("Selected file is not InteractiveHtmlBom settings file.");
return;
}
if (newSettings.type != "InteractiveHtmlBom settings") {
alert("Selected file is not InteractiveHtmlBom settings file.");
return;
}
var metadataMatches = newSettings.hasOwnProperty("pcbmetadata");
if (metadataMatches) {
for (var k in pcbdata.metadata) {
if (!newSettings.pcbmetadata.hasOwnProperty(k) || newSettings.pcbmetadata[k] != pcbdata.metadata[k]) {
metadataMatches = false;
}
}
}
if (!metadataMatches) {
var currentMetadata = JSON.stringify(pcbdata.metadata, null, 4);
var fileMetadata = JSON.stringify(newSettings.pcbmetadata, null, 4);
if (!confirm(
`Settins file metadata does not match current metadata.\n\n` +
`Page metadata:\n${currentMetadata}\n\n` +
`Settings file metadata:\n${fileMetadata}\n\n` +
`Press OK if you would like to import settings anyway.`)) {
return;
}
}
overwriteSettings(newSettings.settings);
}
reader.readAsText(file, 'UTF-8');
}
input.click();
}
function resetSettings() {
if (!confirm(
`This will reset all checkbox states and other settings.\n\n` +
`Press OK if you want to continue.`)) {
return;
}
if (storage) {
var keys = [];
for (var i = 0; i < storage.length; i++) {
var key = storage.key(i);
if (key.startsWith(storagePrefix)) keys.push(key);
}
for (var key of keys) storage.removeItem(key);
}
location.reload();
}
function overwriteSettings(newSettings) {
initDone = false;
Object.assign(settings, newSettings);
writeStorage("bomlayout", settings.bomlayout);
writeStorage("bommode", settings.bommode);
writeStorage("canvaslayout", settings.canvaslayout);
writeStorage("bomCheckboxes", settings.checkboxes.join(","));
document.getElementById("bomCheckboxes").value = settings.checkboxes.join(",");
for (var checkbox of settings.checkboxes) {
writeStorage("checkbox_" + checkbox, settings.checkboxStoredRefs[checkbox]);
}
writeStorage("markWhenChecked", settings.markWhenChecked);
padsVisible(settings.renderPads);
document.getElementById("padsCheckbox").checked = settings.renderPads;
fabricationVisible(settings.renderFabrication);
document.getElementById("fabricationCheckbox").checked = settings.renderFabrication;
silkscreenVisible(settings.renderSilkscreen);
document.getElementById("silkscreenCheckbox").checked = settings.renderSilkscreen;
referencesVisible(settings.renderReferences);
document.getElementById("referencesCheckbox").checked = settings.renderReferences;
valuesVisible(settings.renderValues);
document.getElementById("valuesCheckbox").checked = settings.renderValues;
tracksVisible(settings.renderTracks);
document.getElementById("tracksCheckbox").checked = settings.renderTracks;
zonesVisible(settings.renderZones);
document.getElementById("zonesCheckbox").checked = settings.renderZones;
dnpOutline(settings.renderDnpOutline);
document.getElementById("dnpOutlineCheckbox").checked = settings.renderDnpOutline;
setRedrawOnDrag(settings.redrawOnDrag);
document.getElementById("dragCheckbox").checked = settings.redrawOnDrag;
setHighlightRowOnClick(settings.highlightRowOnClick);
document.getElementById("highlightRowOnClickCheckbox").checked = settings.highlightRowOnClick;
setDarkMode(settings.darkMode);
document.getElementById("darkmodeCheckbox").checked = settings.darkMode;
setHighlightPin1(settings.highlightpin1);
document.forms.highlightpin1.highlightpin1.value = settings.highlightpin1;
writeStorage("boardRotation", settings.boardRotation);
document.getElementById("boardRotation").value = settings.boardRotation / 5;
document.getElementById("rotationDegree").textContent = settings.boardRotation;
setOffsetBackRotation(settings.offsetBackRotation);
document.getElementById("offsetBackRotationCheckbox").checked = settings.offsetBackRotation;
initDone = true;
prepCheckboxes();
changeBomLayout(settings.bomlayout);
}
function saveFile(filename, blob) {
var link = document.createElement("a");
var objurl = URL.createObjectURL(blob);
link.download = filename;
link.href = objurl;
link.click();
}
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
}
var settings = {
canvaslayout: "FB",
bomlayout: "left-right",
bommode: "grouped",
checkboxes: [],
checkboxStoredRefs: {},
darkMode: false,
highlightpin1: "none",
redrawOnDrag: true,
boardRotation: 0,
offsetBackRotation: false,
renderPads: true,
renderReferences: true,
renderValues: true,
renderSilkscreen: true,
renderFabrication: true,
renderDnpOutline: false,
renderTracks: true,
renderZones: true,
columnOrder: [],
hiddenColumns: [],
netColors: {},
}
function initDefaults() {
settings.bomlayout = readStorage("bomlayout");
if (settings.bomlayout === null) {
settings.bomlayout = config.bom_view;
}
if (!['bom-only', 'left-right', 'top-bottom'].includes(settings.bomlayout)) {
settings.bomlayout = config.bom_view;
}
settings.bommode = readStorage("bommode");
if (settings.bommode === null) {
settings.bommode = "grouped";
}
if (settings.bommode == "netlist" && !pcbdata.nets) {
settings.bommode = "grouped";
}
if (!["grouped", "ungrouped", "netlist"].includes(settings.bommode)) {
settings.bommode = "grouped";
}
settings.canvaslayout = readStorage("canvaslayout");
if (settings.canvaslayout === null) {
settings.canvaslayout = config.layer_view;
}
var bomCheckboxes = readStorage("bomCheckboxes");
if (bomCheckboxes === null) {
bomCheckboxes = config.checkboxes;
}
settings.checkboxes = bomCheckboxes.split(",").filter((e) => e);
document.getElementById("bomCheckboxes").value = bomCheckboxes;
var highlightpin1 = readStorage("highlightpin1") || config.highlight_pin1;
if (highlightpin1 === "false") highlightpin1 = "none";
if (highlightpin1 === "true") highlightpin1 = "all";
setHighlightPin1(highlightpin1);
document.forms.highlightpin1.highlightpin1.value = highlightpin1;
settings.markWhenChecked = readStorage("markWhenChecked");
if (settings.markWhenChecked == null) {
settings.markWhenChecked = config.mark_when_checked;
}
populateMarkWhenCheckedOptions();
function initBooleanSetting(storageString, def, elementId, func) {
var b = readStorage(storageString);
if (b === null) {
b = def;
} else {
b = (b == "true");
}
document.getElementById(elementId).checked = b;
func(b);
}
initBooleanSetting("padsVisible", config.show_pads, "padsCheckbox", padsVisible);
initBooleanSetting("fabricationVisible", config.show_fabrication, "fabricationCheckbox", fabricationVisible);
initBooleanSetting("silkscreenVisible", config.show_silkscreen, "silkscreenCheckbox", silkscreenVisible);
initBooleanSetting("referencesVisible", true, "referencesCheckbox", referencesVisible);
initBooleanSetting("valuesVisible", true, "valuesCheckbox", valuesVisible);
if ("tracks" in pcbdata) {
initBooleanSetting("tracksVisible", true, "tracksCheckbox", tracksVisible);
initBooleanSetting("zonesVisible", true, "zonesCheckbox", zonesVisible);
} else {
document.getElementById("tracksAndZonesCheckboxes").style.display = "none";
tracksVisible(false);
zonesVisible(false);
}
initBooleanSetting("dnpOutline", false, "dnpOutlineCheckbox", dnpOutline);
initBooleanSetting("redrawOnDrag", config.redraw_on_drag, "dragCheckbox", setRedrawOnDrag);
initBooleanSetting("highlightRowOnClick", false, "highlightRowOnClickCheckbox", setHighlightRowOnClick);
initBooleanSetting("darkmode", config.dark_mode, "darkmodeCheckbox", setDarkMode);
var fields = ["checkboxes", "References"].concat(config.fields).concat(["Quantity"]);
var hcols = JSON.parse(readStorage("hiddenColumns"));
if (hcols === null) {
hcols = [];
}
settings.hiddenColumns = hcols.filter(e => fields.includes(e));
var cord = JSON.parse(readStorage("columnOrder"));
if (cord === null) {
cord = fields;
} else {
cord = cord.filter(e => fields.includes(e));
if (cord.length != fields.length)
cord = fields;
}
settings.columnOrder = cord;
settings.boardRotation = readStorage("boardRotation");
if (settings.boardRotation === null) {
settings.boardRotation = config.board_rotation * 5;
} else {
settings.boardRotation = parseInt(settings.boardRotation);
}
document.getElementById("boardRotation").value = settings.boardRotation / 5;
document.getElementById("rotationDegree").textContent = settings.boardRotation;
initBooleanSetting("offsetBackRotation", config.offset_back_rotation, "offsetBackRotationCheckbox", setOffsetBackRotation);
settings.netColors = JSON.parse(readStorage("netColors")) || {};
}
// Helper classes for user js callbacks.
const IBOM_EVENT_TYPES = {
ALL: "all",
HIGHLIGHT_EVENT: "highlightEvent",
CHECKBOX_CHANGE_EVENT: "checkboxChangeEvent",
BOM_BODY_CHANGE_EVENT: "bomBodyChangeEvent",
}
const EventHandler = {
callbacks: {},
init: function () {
for (eventType of Object.values(IBOM_EVENT_TYPES))
this.callbacks[eventType] = [];
},
registerCallback: function (eventType, callback) {
this.callbacks[eventType].push(callback);
},
emitEvent: function (eventType, eventArgs) {
event = {
eventType: eventType,
args: eventArgs,
}
var callback;
for (callback of this.callbacks[eventType])
callback(event);
for (callback of this.callbacks[IBOM_EVENT_TYPES.ALL])
callback(event);
}
}
EventHandler.init();

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 qu1ck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

45
README.md Normal file
View File

@ -0,0 +1,45 @@
# Interactive HTML BOM plugin for KiCad
## Supports EasyEDA, Eagle, Fusion360 and Allegro PCB designer
![icon](https://i.imgur.com/js4kDOn.png)
This plugin generates a convenient Bill of Materials (BOM) listing with the
ability to visually correlate and easily search for components and their placements
on the PCB. It is particularly useful when hand-soldering a prototype, as it allows
users to quickly find locations of components groups on the board. It is also possible
to reverse lookup the component group by clicking on a footprint on the board drawing.
The plugin utilizes Pcbnew python API to read PCB data and render silkscreen, fab layer,
footprint pads, text, and drawings. BOM table fields and grouping is fully configurable,
additional columns, such as a manufacturer ID, can be added in Schematic editor and
imported either through the netlist file, XML file generated by Eeschema's internal
BOM tool, or from board file itself.
There is an option to include tracks/zones data as well as netlist information allowing
dynamic highlight of nets on the board.
For full description of functionality see [wiki](https://github.com/openscopeproject/InteractiveHtmlBom/wiki).
Generated html page is fully self contained, doesn't need internet connection to work
and can be packaged with documentation of your project or hosted anywhere on the web.
[A demo is worth a thousand words.](https://openscopeproject.org/InteractiveHtmlBomDemo/)
## Installation and Usage
See [project wiki](https://github.com/openscopeproject/InteractiveHtmlBom/wiki/Installation) for instructions.
## License and credits
Plugin code is licensed under MIT license, see `LICENSE` for more info.
Html page uses [Split.js](https://github.com/nathancahill/Split.js),
[PEP.js](https://github.com/jquery/PEP) and (stripped down)
[lz-string.js](https://github.com/pieroxy/lz-string) libraries that get embedded into
generated bom page.
`units.py` is borrowed from [KiBom](https://github.com/SchrodingersGat/KiBoM)
plugin (MIT license).
`svgpath.py` is heavily based on
[svgpathtools](https://github.com/mathandy/svgpathtools) module (MIT license).

1
__init__.py Normal file
View File

@ -0,0 +1 @@
from .InteractiveHtmlBom import plugin

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path fill="none" d="M0 0h20v20H0V0z"/>
<path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 786 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<g stroke="#000" stroke-linejoin="round" class="layer">
<rect width="29" height="29" x="1.5" y="1.5" stroke-width="2" fill="#fff" rx="5" ry="5"/>
<path stroke-linecap="square" stroke-width="2" d="M6 10h4m4 0h5m4 0h3M6.1 22h3m3.9 0h5m4 0h4m-16-8h4m4 0h4"/>
<path stroke-linecap="null" d="M5 17.5h22M5 26.6h22M5 5.5h22"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8.47 8.47">
<rect transform="translate(0 -288.53)" ry="1.17" y="288.8" x=".27" height="7.94" width="7.94" fill="#f9f9f9"/>
<g transform="translate(0 -288.53)">
<rect width="7.94" height="7.94" x=".27" y="288.8" ry="1.17" fill="none" stroke="#000" stroke-width=".4" stroke-linejoin="round"/>
<path d="M1.06 290.12H3.7m-2.64 1.33H3.7m-2.64 1.32H3.7m-2.64 1.3H3.7m-2.64 1.33H3.7" fill="none" stroke="#000" stroke-width=".4"/>
<path d="M4.37 288.8v7.94m0-4.11h3.96" fill="none" stroke="#000" stroke-width=".3"/>
<text font-weight="700" font-size="3.17" font-family="sans-serif">
<tspan x="5.11" y="291.96">F</tspan>
<tspan x="5.03" y="295.68">B</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<g fill="none" stroke="#000" class="layer">
<rect width="29" height="29" x="1.5" y="1.5" stroke-width="2" fill="#fff" rx="5" ry="5"/>
<path stroke-width="2" d="M6 26l6-6v-8m13.8-6.3l-6 6v8"/>
<circle cx="11.8" cy="9.5" r="2.8" stroke-width="2"/>
<circle cx="19.8" cy="22.8" r="2.8" stroke-width="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 396 B

7
icons/bom-only-32px.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8.47 8.47">
<rect transform="translate(0 -288.53)" ry="1.17" y="288.8" x=".27" height="7.94" width="7.94" fill="#f9f9f9"/>
<g transform="translate(0 -288.53)" fill="none" stroke="#000" stroke-width=".4">
<rect width="7.94" height="7.94" x=".27" y="288.8" ry="1.17" stroke-linejoin="round"/>
<path d="M1.59 290.12h5.29M1.59 291.45h5.33M1.59 292.75h5.33M1.59 294.09h5.33M1.59 295.41h5.33"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8.47 8.47">
<rect transform="translate(0 -288.53)" ry="1.17" y="288.8" x=".27" height="7.94" width="7.94" fill="#f9f9f9"/>
<g transform="translate(0 -288.53)">
<rect width="7.94" height="7.94" x=".27" y="288.8" ry="1.17" fill="none" stroke="#000" stroke-width=".4" stroke-linejoin="round"/>
<path d="M1.32 290.12h5.82M1.32 291.45h5.82" fill="none" stroke="#000" stroke-width=".4"/>
<path d="M4.37 292.5v4.23M.26 292.63H8.2" fill="none" stroke="#000" stroke-width=".3"/>
<text font-weight="700" font-size="3.17" font-family="sans-serif">
<tspan x="1.35" y="295.73">F</tspan>
<tspan x="5.03" y="295.68">B</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 722 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<g stroke="#000" stroke-linejoin="round" class="layer">
<rect width="29" height="29" x="1.5" y="1.5" stroke-width="2" fill="#fff" rx="5" ry="5"/>
<path stroke-linecap="square" stroke-width="2" d="M6 10h4m-4 8h3m-3 8h4"/>
<path stroke-linecap="null" d="M5 13.5h22m-22 8h22M5 5.5h22"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 375 B

70
icons/btn-arrow-down.svg Normal file
View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 6.3499998 6.3499998"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="btn-arrow-down.svg"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\dialog\bitmaps\btn-arrow-down.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="27.958333"
inkscape:cx="7.8914114"
inkscape:cy="16.878026"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
showguides="true"
inkscape:lockguides="true"
inkscape:window-width="1346"
inkscape:window-height="1198"
inkscape:window-x="613"
inkscape:window-y="239"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid833" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#008ebf;fill-opacity:1;stroke:none;stroke-width:0.126314px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 4.2333332,0.49976839 H 2.1980769 V 3.4395832 H 0.56987156 L 3.2157049,5.7914349 5.8615381,3.4395832 H 4.2333332 Z"
id="path844"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

70
icons/btn-arrow-up.svg Normal file
View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 6.3499998 6.3499998"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="btn-arrow-up.svg"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\dialog\bitmaps\btn-arrow-up.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="27.958333"
inkscape:cx="12"
inkscape:cy="10.027534"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
showguides="true"
inkscape:lockguides="true"
inkscape:window-width="1346"
inkscape:window-height="1198"
inkscape:window-x="613"
inkscape:window-y="239"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid833" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#008ebf;fill-opacity:1;stroke:none;stroke-width:0.126314px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 2.1980769,5.8502314 H 4.2333332 V 2.9104166 H 5.8615385 L 3.2157052,0.55856487 0.56987196,2.9104166 H 2.1980769 Z"
id="path844"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

72
icons/btn-minus.svg Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 6.3499998 6.3499998"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="btn-minus.svg"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\dialog\bitmaps\btn-minus.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="27.958333"
inkscape:cx="12.040443"
inkscape:cy="14.982348"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
showguides="true"
inkscape:lockguides="true"
inkscape:window-width="1346"
inkscape:window-height="1198"
inkscape:window-x="613"
inkscape:window-y="239"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid833" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#008ebf;stroke-width:0.561827;stroke-linecap:square"
id="rect840"
width="4.7624998"
height="1.0583333"
x="0.79374999"
y="2.6458333" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

80
icons/btn-plus.svg Normal file
View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 6.3499998 6.3499998"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="btn-plus.svg"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\dialog\bitmaps\btn-minus.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="27.958333"
inkscape:cx="12.040443"
inkscape:cy="14.982348"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
showguides="true"
inkscape:lockguides="true"
inkscape:window-width="1346"
inkscape:window-height="1198"
inkscape:window-x="613"
inkscape:window-y="239"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid833" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#008ebf;stroke-width:0.561827;stroke-linecap:square"
id="rect840"
width="4.7624998"
height="1.0583333"
x="0.79374999"
y="2.6458333" />
<rect
style="fill:#008ebf;stroke-width:0.561827;stroke-linecap:square"
id="rect842"
width="4.7624998"
height="1.0583333"
x="-5.5562496"
y="2.6458333"
transform="rotate(-90)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

87
icons/btn-question.svg Normal file
View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 6.3499998 6.3499998"
version="1.1"
id="svg8"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="btn-question.svg"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\dialog\bitmaps\btn-question.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="27.958333"
inkscape:cx="12"
inkscape:cy="9.3516477"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="true"
showguides="true"
inkscape:lockguides="true"
inkscape:window-width="1346"
inkscape:window-height="1198"
inkscape:window-x="601"
inkscape:window-y="175"
inkscape:window-maximized="0"
units="px">
<inkscape:grid
type="xygrid"
id="grid833" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:5.82083px;line-height:1.25;font-family:'CaskaydiaCove Nerd Font Mono';-inkscape-font-specification:'CaskaydiaCove Nerd Font Mono Bold';white-space:pre;inline-size:2.91042;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="1.5875"
y="1.5875"
id="text846"><tspan
style="visibility:hidden"
x="1.5875"
y="1.5875"><tspan
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:5.82083px;font-family:'CaskaydiaCove Nerd Font Mono';-inkscape-font-specification:'CaskaydiaCove Nerd Font Mono Bold';stroke-width:0.264583">?</tspan></tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.25251px;line-height:1.25;font-family:'CaskaydiaCove Nerd Font';-inkscape-font-specification:'CaskaydiaCove Nerd Font';fill:#008ebf;fill-opacity:1;stroke:none;stroke-width:0.302187;"
x="0.90885961"
y="5.7593102"
id="text850"
transform="scale(1.0205218,0.97989088)"><tspan
sodipodi:role="line"
id="tspan848"
x="0.90885961"
y="5.7593102"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.25251px;font-family:'CaskaydiaCove Nerd Font';-inkscape-font-specification:'CaskaydiaCove Nerd Font';stroke-width:0.302187;fill:#008ebf;fill-opacity:1;">?</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
icons/copy-48px.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h48v48h-48z" fill="none"/><path d="M32 2h-24c-2.21 0-4 1.79-4 4v28h4v-28h24v-4zm6 8h-22c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h22c2.21 0 4-1.79 4-4v-28c0-2.21-1.79-4-4-4zm0 32h-22v-28h22v28z"/></svg>

After

Width:  |  Height:  |  Size: 318 B

4
icons/io-36px.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36">
<path fill="none" stroke="#000" stroke-width="2" d="M3 33v-7l6.8-7h16.5l6.7 7v7H3zM3.2 26H33M21 9l5-5.9 5 6h-2.5V15h-5V9H21zm-4.9 0l-5 6-5-6h2.5V3h5v6h2.5z"/>
<path fill="none" stroke="#000" d="M6.1 29.5H10"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

169
icons/plugin.svg Normal file
View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="24"
height="24"
viewBox="0 0 6.3499994 6.3499994"
version="1.1"
id="svg2017"
sodipodi:docname="plugin.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\icon.png"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008">
<defs
id="defs2011" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="47.931104"
inkscape:cx="13.563692"
inkscape:cy="12.666354"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="2560"
inkscape:window-height="1417"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:document-rotation="0">
<inkscape:grid
type="xygrid"
id="grid2562"
originx="0"
originy="0"
spacingx="0.13229165"
spacingy="0.13229165"
empspacing="10" />
<sodipodi:guide
position="0,6.3499994"
orientation="0,24"
id="guide844" />
<sodipodi:guide
position="6.3499994,6.3499994"
orientation="24,0"
id="guide846" />
<sodipodi:guide
position="6.3499994,0"
orientation="0,-24"
id="guide848" />
<sodipodi:guide
position="0,0"
orientation="-24,0"
id="guide850" />
</sodipodi:namedview>
<metadata
id="metadata2014">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53333)">
<rect
style="opacity:1;fill:#99ff55;fill-opacity:1;stroke:none;stroke-width:0.396875;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect2564"
width="6.3499999"
height="6.3499999"
x="0"
y="288.53333"
ry="0.99219871" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 0.92072016,288.93221 1.85740474,-0.002 -3e-7,1.58749 H 0.39687495 l -0.009049,-1.09281 c -0.003001,-0.36241 0.27363226,-0.49199 0.52185387,-0.49267 z"
id="rect2566"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scccsss" />
<path
sodipodi:nodetypes="cccscc"
inkscape:connector-curvature="0"
id="path6258"
d="m 2.7781246,294.48645 10e-8,-3.175 H 0.39687495 l -0.0325432,2.5246 c -0.00477,0.37007 0.26920623,0.65071 0.56170981,0.6504 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" />
<path
sodipodi:nodetypes="ccccscc"
inkscape:connector-curvature="0"
id="path6260"
d="m 3.5718746,288.9302 v 2.64584 l 1.4552081,0 c 0.530988,7.5e-4 0.9262459,-0.34677 0.9260416,-0.79375 l -0.010903,-1.26807 c -0.00269,-0.31267 -0.3194939,-0.5845 -0.6505557,-0.58402 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 3.5718745,294.48645 1e-7,-2.91041 h 1.5874998 c 0.5309881,-7.3e-4 0.7939542,0.47906 0.7937498,0.92604 l 1.376e-4,1.41692 c 3.03e-5,0.31268 -0.1982422,0.56793 -0.5293041,0.56745 z"
id="path6262"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccscc" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:2.60643px;line-height:1.25;font-family:'Segoe UI';-inkscape-font-specification:'Segoe UI, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#4d4d4d;fill-opacity:1;stroke:none;stroke-width:0.158182;stroke-miterlimit:4;stroke-dasharray:none"
x="4.1148658"
y="285.02682"
id="text6274"
transform="scale(0.97887057,1.0215855)"><tspan
sodipodi:role="line"
id="tspan6272"
x="4.1148658"
y="285.02682"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:2.60643px;font-family:'Segoe UI';-inkscape-font-specification:'Segoe UI, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#4d4d4d;stroke-width:0.158182;stroke-miterlimit:4;stroke-dasharray:none">F</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:2.47399px;line-height:1.25;font-family:'Segoe UI';-inkscape-font-specification:'Segoe UI, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#4d4d4d;fill-opacity:1;stroke:none;stroke-width:0.210774"
x="4.3067122"
y="275.4487"
id="text6278"
transform="scale(0.93746149,1.0667105)"><tspan
sodipodi:role="line"
id="tspan6276"
x="4.3067122"
y="275.4487"
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:2.47399px;font-family:'Segoe UI';-inkscape-font-specification:'Segoe UI, Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#4d4d4d;stroke-width:0.210774">B</tspan></text>
<path
style="fill:none;stroke:#4d4d4d;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 1.0583332,289.72395 1.0583332,0"
id="path854"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#4d4d4d;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 1.0583332,292.1052 H 2.1166664"
id="path848"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#4d4d4d;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 1.0583332,292.89895 H 2.1166664"
id="path850"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#4d4d4d;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 1.0583332,293.6927 H 2.1166664"
id="path852"
sodipodi:nodetypes="cc"
inkscape:export-filename="D:\OSP\KicadBomPlugin\InteractiveHtmlBom\path852.png"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
icons/plugin_icon_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

3
icons/stats-36px.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="36" height="36" xmlns="http://www.w3.org/2000/svg">
<path d="M4 6h28v24H4V6zm0 8h28v8H4m9-16v24h10V5.8" fill="none" stroke="#000" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 169 B

73
pyproject.toml Normal file
View File

@ -0,0 +1,73 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "InteractiveHtmlBom"
dynamic = ["version"]
description = 'Generate Interactive Html BOM for your electronics projects'
readme = "README.md"
requires-python = ">=3.8"
license = "MIT"
keywords = ["ibom", "KiCad", "Eagle", "EasyEDA"]
authors = [{ name = "qu1ck", email = "anlutsenko@gmail.com" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
"Topic :: Utilities",
]
dependencies = [
"wxpython>=4.0",
"jsonschema>=4.1",
]
[project.scripts]
generate_interactive_bom = "InteractiveHtmlBom.generate_interactive_bom:main"
[project.urls]
Documentation = "https://github.com/openscopeproject/InteractiveHtmlBom/wiki"
Issues = "https://github.com/openscopeproject/InteractiveHtmlBom/issues"
Source = "https://github.com/openscopeproject/InteractiveHtmlBom"
[tool.hatch.version]
path = "InteractiveHtmlBom/version.py"
pattern = "LAST_TAG = 'v(?P<version>[^']+)'"
[tool.hatch.envs.default]
system-packages = true
dependencies = [
"coverage[toml]>=6.5",
"pytest",
"pytest-sugar"
]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = ["- coverage combine", "coverage report"]
cov = ["test-cov", "cov-report"]
[[tool.hatch.envs.all.matrix]]
python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
[tool.hatch.envs.types]
dependencies = ["mypy>=1.0.0"]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:InteractiveHtmlBom}"
[tool.coverage.run]
source_pkgs = ["InteractiveHtmlBom", "tests"]
branch = true
parallel = true
omit = ["src/InteractiveHtmlBom/__about__.py"]
[tool.coverage.paths]
InteractiveHtmlBom = [
"InteractiveHtmlBom",
]
tests = ["tests", "*/InteractiveHtmlBom/tests"]
[tool.coverage.report]
exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]

3859
settings_dialog.fbp Normal file

File diff suppressed because it is too large Load Diff

2
tests/test_module.py Normal file
View File

@ -0,0 +1,2 @@
def test_module_import():
import InteractiveHtmlBom # noqa