Initial Commit
|
|
@ -0,0 +1,11 @@
|
|||
*.pyc
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.bak
|
||||
test
|
||||
releases
|
||||
demo
|
||||
*config.ini
|
||||
InteractiveHtmlBom/web/user*
|
||||
dist/
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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", ...],
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1 @@
|
|||
*.bat eol=crlf
|
||||
|
|
@ -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_
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
from .settings_dialog import SettingsDialog, GeneralSettingsPanel
|
||||
|
After Width: | Height: | Size: 386 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 137 B |
|
After Width: | Height: | Size: 165 B |
|
After Width: | Height: | Size: 460 B |
|
|
@ -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()
|
||||
|
|
@ -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 "")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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!
|
||||
|
|
@ -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!
|
||||
|
After Width: | Height: | Size: 820 B |
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>°</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>
|
||||
|
|
@ -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});
|
||||
|
|
@ -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});
|
||||
|
|
@ -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}}});
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Interactive HTML BOM plugin for KiCad
|
||||
## Supports EasyEDA, Eagle, Fusion360 and Allegro PCB designer
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
|
@ -0,0 +1 @@
|
|||
from .InteractiveHtmlBom import plugin
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -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 |
|
|
@ -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:"]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
def test_module_import():
|
||||
import InteractiveHtmlBom # noqa
|
||||