├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .idea
├── .gitignore
├── halfmarble-panelizer.iml
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── AppSettings.py
├── Array2D.py
├── Constants.py
├── GridRenderer.py
├── LICENSE
├── MouseBiteWidget.py
├── OffScreenImage.py
├── OffScreenScatter.py
├── Pcb.py
├── PcbBoard.py
├── PcbExport.py
├── PcbFile.py
├── PcbGap.py
├── PcbMask.py
├── PcbMouseBites.py
├── PcbMouseBitesGroup.py
├── PcbPanel.py
├── PcbRail.py
├── PcbShape.py
├── PcbWorkarounds.py
├── README.md
├── SplitGerberComposition.py
├── UI.py
├── Utilities.py
├── data
├── demo_pcb
│ └── NEAToBOARD
│ │ ├── bottom_copper.png
│ │ ├── bottom_mask.png
│ │ ├── bottom_paste.png
│ │ ├── bottom_silk.png
│ │ ├── drill_npth.png
│ │ ├── drill_pth.png
│ │ ├── edge_cuts.png
│ │ ├── edge_cuts_mask.png
│ │ ├── edge_cuts_mask.txt
│ │ ├── top_copper.png
│ │ ├── top_mask.png
│ │ ├── top_paste.png
│ │ └── top_silk.png
├── icons
│ ├── Kofi_down.png
│ ├── Kofi_normal.png
│ ├── README
│ ├── action-redo-8x.png
│ ├── action-undo-8x.png
│ ├── aperture-8x.png
│ ├── briefcase-8x.png
│ ├── browser-8x.png
│ ├── brush-8x.png
│ ├── camera-slr-8x.png
│ ├── chevron-bottom-8x.png
│ ├── chevron-left-8x.png
│ ├── chevron-right-8x.png
│ ├── chevron-top-8x.png
│ ├── circle-check-8x.png
│ ├── circle-x-8x.png
│ ├── dashboard-8x.png
│ ├── data-transfer-download-8x.png
│ ├── data-transfer-upload-8x.png
│ ├── dial-8x.png
│ ├── eye.png
│ ├── folder.png
│ ├── home-8x.png
│ ├── horizontal.png
│ ├── icon.png
│ ├── load.png
│ ├── lock-locked-8x.png
│ ├── lock-unlocked-8x.png
│ ├── loop-circular-8x.png
│ ├── map-marker-8x.png
│ ├── minus.png
│ ├── monitor-8x.png
│ ├── panelize.png
│ ├── pencil-8x.png
│ ├── pin-8x.png
│ ├── plus.png
│ ├── puzzle-piece-8x.png
│ ├── rotate-left.png
│ ├── rotate-right.png
│ ├── save.png
│ ├── separator.png
│ ├── settings.png
│ ├── share.png
│ ├── star-8x.png
│ ├── target-8x.png
│ ├── task-8x.png
│ ├── thumb-up-8x.png
│ ├── vertical.png
│ ├── warning-8x.png
│ ├── zoom-in.png
│ └── zoom-out.png
└── mousebite_template.txt
├── do_optimize.py
├── hm_gerber_ex
├── __init__.py
├── am_expression.py
├── am_primitive.py
├── common.py
├── composition.py
├── dxf.py
├── dxf_path.py
├── excellon.py
├── gerber_statements.py
├── rs274x.py
└── utility.py
├── hm_gerber_tool
├── __init__.py
├── __main__.py
├── am_eval.py
├── am_read.py
├── am_statements.py
├── cam.py
├── common.py
├── excellon.py
├── excellon_report
│ └── excellon_drr.py
├── excellon_settings.py
├── excellon_statements.py
├── excellon_tool.py
├── exceptions.py
├── gerber_statements.py
├── ipc356.py
├── layers.py
├── ncparam
│ └── allegro.py
├── operations.py
├── pcb.py
├── primitives.py
├── render
│ ├── __init__.py
│ ├── cairo_backend.py
│ ├── excellon_backend.py
│ ├── render.py
│ ├── rs274x_backend.py
│ └── theme.py
├── rs274x.py
└── utils.py
├── main.py
├── panelizer.kv
└── pics
├── KiCad_drill.png
├── KiCad_plot.png
├── Screenshot.png
├── Screenshot2.png
└── Screenshot3.png
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: #halfmarble
4 | patreon: #halfmarble
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | .DS_Store
132 |
133 | docs/
134 |
135 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/halfmarble-panelizer.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/AppSettings.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from Constants import *
25 | from Utilities import *
26 |
27 |
28 | class AppSettings:
29 |
30 | def __init__(self):
31 | self._gap = 0.0
32 | self._rail = 0.0
33 | self._bite = 0.0
34 | self._bite_hole_radius = 0.0
35 | self._bite_hole_space = 0.0
36 | self._bites_count = 0
37 | self._use_vcut = False
38 | self._use_jlc = False
39 | self._merge_error = 0.0
40 |
41 | self.default()
42 |
43 | def default(self):
44 | self._rail = PCB_PANEL_RAIL_HEIGHT_MM
45 | self._gap = PCB_PANEL_GAP_MM
46 | self._bite_hole_radius = PCB_BITES_HOLE_RADIUS_MM
47 | self._bite_hole_space = PCB_BITES_HOLE_SPACE_MM
48 | self._bite = PCB_PANEL_BITES_SIZE_MM
49 | self._bites_count = PCB_PANEL_BITES_COUNT_X
50 | self._use_vcut = PCB_PANEL_USE_VCUT
51 | self._use_jlc = PCB_PANEL_USE_JLC
52 | self._merge_error = PCB_PANEL_MERGE_ERROR
53 |
54 | def set(self, gap, rail, bites_count, bite, bite_hole_radius, bite_hole_space, use_vcut, use_jlc, merge_error):
55 | self._gap = clamp(1.0, gap, 10.0)
56 | self._rail = clamp(5, rail, 20.0)
57 | self._bites_count = int(clamp(1, bites_count, 10))
58 | self._bite = clamp((2.0*PCB_BITES_ARC_MM)+0.5, bite, 15.0)
59 | self._bite_hole_radius = clamp(0.1, bite_hole_radius, 0.5)
60 | self._bite_hole_space = clamp(0.5, bite_hole_space, 5.0)
61 | self._use_vcut = use_vcut
62 | self._use_jlc = use_jlc
63 | self._merge_error = clamp(0.0, merge_error, 1.0)
64 |
65 | @property
66 | def rail(self):
67 | return float(self._rail)
68 |
69 | @property
70 | def gap(self):
71 | return float(self._gap)
72 |
73 | @property
74 | def bite(self):
75 | return float(self._bite)
76 |
77 | @property
78 | def bites_count(self):
79 | return int(self._bites_count)
80 |
81 | @property
82 | def bite_hole_radius(self):
83 | return float(self._bite_hole_radius)
84 |
85 | @property
86 | def bite_hole_space(self):
87 | return float(self._bite_hole_space)
88 |
89 | @property
90 | def use_vcut(self):
91 | return self._use_vcut
92 |
93 | @property
94 | def use_jlc(self):
95 | return self._use_jlc
96 |
97 | @property
98 | def merge_error(self):
99 | return float(self._merge_error)
100 |
101 |
102 | AppSettings = AppSettings()
103 |
--------------------------------------------------------------------------------
/Array2D.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 | import math
24 |
25 |
26 | class Array2D:
27 |
28 | def __init__(self, width, height):
29 | self._width = width
30 | self._height = height
31 | self._matrix = [[0 for x in range(width)] for y in range(height)]
32 |
33 | def put(self, x, y, value):
34 | self._matrix[y][x] = value
35 |
36 | def get(self, x, y):
37 | return self._matrix[y][x]
38 |
39 | # 0,0 is left,bottom
40 | def print(self, str=''):
41 | for y in reversed(range(self._height)):
42 | for x in (range(self._width)):
43 | print('{}[{}] '.format(str, self.get(x, y)), end='')
44 | print('')
45 |
46 | @property
47 | def width(self):
48 | return self._width
49 |
50 | @property
51 | def height(self):
52 | return self._height
--------------------------------------------------------------------------------
/Constants.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from typing import Final
25 | from kivy.graphics import Color
26 | from posixpath import join
27 |
28 |
29 | VERSION_STR: Final = '1.0.0 (beta)'
30 | APP_NAME: Final = 'hm-panelizer'
31 | APP_STR: Final = '{} {}'.format(APP_NAME, VERSION_STR)
32 | VENDOR_NAME: Final = 'halfmarble'
33 |
34 | ALLOW_DIR_DELETIONS: Final = True
35 |
36 | DEMO_PCB_PATH_STR: Final = join('data', 'demo_pcb', 'NEAToBOARD')
37 |
38 | # careful: the higher the number the better the resolution, but needs more VRAM
39 | PIXELS_PER_MM: Final = 16
40 |
41 | PIXELS_SIZE_MIN: Final = 256
42 | # super careful (do not go above 4096, unless you have a good GPU and lots of VRAM)
43 | PIXELS_SIZE_MAX: Final = 2048
44 |
45 | PCB_OUTLINE_WIDTH: Final = 1.5
46 |
47 | INITIAL_ROWS: Final = 1
48 | INITIAL_COLUMNS: Final = 1
49 | MAX_ROWS: Final = 99
50 | MAX_COLUMNS: Final = 99
51 |
52 | # the ratio of the pcb board to the available window size at 100% zoom
53 | FIT_SCALE: Final = 0.9
54 |
55 | PCB_PANEL_USE_VCUT: Final = True
56 | PCB_PANEL_USE_JLC: Final = False
57 |
58 | PCB_PANEL_GAP_MM: Final = 3.0
59 |
60 | # no less than 5mm
61 | PCB_PANEL_RAIL_HEIGHT_MM: Final = 6.0
62 |
63 | # no less than 4mm
64 | PCB_PANEL_BITES_SIZE_MM: Final = 4.5
65 | PCB_PANEL_BITES_COUNT_X: Final = 1
66 | # leave at 0, unimplemented yet
67 | PCB_PANEL_BITES_COUNT_Y: Final = 0
68 |
69 | PCB_BITES_HOLE_RADIUS_MM: Final = 0.2
70 | PCB_BITES_HOLE_SPACE_MM: Final = 0.6
71 |
72 | # OSH Park recommends minimum of 0.8636
73 | PCB_BITES_ARC_MM: Final = 1.0
74 |
75 | # https://docs.oshpark.com/troubleshooting/panelized-designs/
76 | OSHPARK_PCB_PANEL_RAIL_HEIGHT_MM: Final = 5.08
77 | OSHPARK_PCB_PANEL_GAP_MM: Final = 2.54
78 | OSHPARK_PCB_BITES_HOLE_RADIUS_MM: Final = 0.254
79 | OSHPARK_PCB_BITES_HOLE_SPACE_MM: Final = 0.508
80 | OSHPARK_PCB_PANEL_BITES_SIZE_MM: Final = (PCB_BITES_ARC_MM+2.54+PCB_BITES_ARC_MM)
81 | OSHPARK_PCB_PANEL_VCUT: Final = False
82 |
83 | # JLC PCB (TODO need verified values)
84 | JLC_PCB_PANEL_RAIL_HEIGHT_MM: Final = PCB_PANEL_RAIL_HEIGHT_MM
85 | JLC_PCB_PANEL_GAP_MM: Final = PCB_PANEL_GAP_MM
86 | JLC_PCB_BITES_HOLE_RADIUS_MM: Final = PCB_BITES_HOLE_RADIUS_MM
87 | JLC_PCB_BITES_HOLE_SPACE_MM: Final = PCB_BITES_HOLE_SPACE_MM
88 | JLC_PCB_PANEL_BITES_SIZE_MM: Final = (PCB_BITES_ARC_MM+2.0+PCB_BITES_ARC_MM)
89 | JLC_PCB_PANEL_VCUT: Final = True
90 |
91 | # PCB Way (TODO need verified values)
92 | PCBWAY_PCB_PANEL_RAIL_HEIGHT_MM: Final = PCB_PANEL_RAIL_HEIGHT_MM
93 | PCBWAY_PCB_PANEL_GAP_MM: Final = PCB_PANEL_GAP_MM
94 | PCBWAY_PCB_BITES_HOLE_RADIUS_MM: Final = PCB_BITES_HOLE_RADIUS_MM
95 | PCBWAY_PCB_BITES_HOLE_SPACE_MM: Final = PCB_BITES_HOLE_SPACE_MM
96 | PCBWAY_PCB_PANEL_BITES_SIZE_MM: Final = (PCB_BITES_ARC_MM+2.0+PCB_BITES_ARC_MM)
97 | PCBWAY_PCB_PANEL_VCUT: Final = True
98 |
99 | # in mm
100 | PCB_PANEL_MERGE_ERROR: Final = 0.15
101 |
102 |
103 | GRID_BACKGROUND_COLOR: Final = Color(0.95, 0.95, 0.95, 1.0)
104 | GRID_MAJOR_COLOR: Final = Color(0.50, 0.50, 0.50, 1.0)
105 | GRID_MINOR_COLOR: Final = Color(0.80, 0.80, 0.80, 1.0)
106 |
107 | PCB_MASK_COLOR: Final = Color(0.15, 0.35, 0.15, 1.00)
108 | PCB_OUTLINE_COLOR: Final = Color(0.00, 0.00, 0.00, 1.00)
109 | PCB_TOP_PASTE_COLOR: Final = Color(0.55, 0.55, 0.55, 1.00)
110 | PCB_TOP_SILK_COLOR: Final = Color(0.95, 0.95, 0.95, 1.00)
111 | PCB_TOP_MASK_COLOR: Final = Color(0.75, 0.65, 0.00, 1.00)
112 | PCB_TOP_TRACES_COLOR: Final = Color(0.00, 0.50, 0.00, 0.50)
113 | PCB_BOTTOM_TRACES_COLOR: Final = Color(0.00, 0.50, 0.00, 0.50)
114 | PCB_BOTTOM_MASK_COLOR: Final = Color(0.75, 0.65, 0.00, 1.00)
115 | PCB_BOTTOM_SILK_COLOR: Final = Color(0.95, 0.95, 0.95, 1.00)
116 | PCB_BOTTOM_PASTE_COLOR: Final = Color(0.55, 0.55, 0.55, 1.00)
117 | PCB_DRILL_NPTH_COLOR: Final = Color(0.12, 0.12, 0.12, 0.80)
118 | PCB_DRILL_PTH_COLOR: Final = Color(0.30, 0.15, 0.00, 0.50)
119 |
120 | PCB_BITE_GOOD_COLOR: Final = Color(0.25, 0.85, 0.25, 0.75)
121 | PCB_BITE_BAD_COLOR: Final = Color(0.85, 0.25, 0.25, 0.75)
122 |
--------------------------------------------------------------------------------
/GridRenderer.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import math
25 | from kivy.graphics import Line, ClearBuffers, ClearColor
26 | from Constants import *
27 |
28 |
29 | class GridRenderer:
30 |
31 | def __init__(self):
32 | self._pixels_per_cm = 1.0
33 |
34 | def set_pixels_per_cm(self, pixels_per_cm):
35 | self._pixels_per_cm = pixels_per_cm
36 |
37 | def paint(self, fbo, size):
38 | cx = size[0] / 2.0
39 | cy = size[1] / 2.0
40 | line_count_x = int(((math.floor(size[0] / self._pixels_per_cm)) / 2.0) + 1.0)
41 | line_count_y = int(((math.floor(size[1] / self._pixels_per_cm)) / 2.0) + 1.0)
42 |
43 | with fbo:
44 | c = GRID_BACKGROUND_COLOR
45 | ClearColor(c.r, c.g, c.b, c.a)
46 | ClearBuffers()
47 |
48 | x = 0.0
49 | sy = 0.0
50 | ey = size[1]
51 | c = GRID_MAJOR_COLOR
52 | Color(c.r, c.g, c.b, c.a)
53 | Line(points=[cx, sy, cx, ey])
54 | x += self._pixels_per_cm
55 | c = GRID_MINOR_COLOR
56 | Color(c.r, c.g, c.b, c.a)
57 | for i in range(0, line_count_x):
58 | x_int = int(round(x))
59 | Line(points=[cx + x_int, sy, cx + x_int, ey])
60 | Line(points=[cx - x_int, sy, cx - x_int, ey])
61 | x += self._pixels_per_cm
62 |
63 | y = 0.0
64 | sx = 0.0
65 | ex = size[0]
66 | c = GRID_MAJOR_COLOR
67 | Color(c.r, c.g, c.b, c.a)
68 | Line(points=[sx, cy, ex, cy])
69 | y += self._pixels_per_cm
70 | c = GRID_MINOR_COLOR
71 | Color(c.r, c.g, c.b, c.a)
72 | for i in range(0, line_count_y):
73 | y_int = int(round(y))
74 | Line(points=[sx, cy + y_int, ex, cy + y_int])
75 | Line(points=[sx, cy - y_int, ex, cy - y_int])
76 | y += self._pixels_per_cm
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright 2021,2022 HalfMarble LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.WARE.
--------------------------------------------------------------------------------
/MouseBiteWidget.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from Constants import *
25 | from OffScreenScatter import *
26 | from PcbMouseBites import *
27 |
28 |
29 | class MouseBitesRenderer:
30 |
31 | def __init__(self, owner):
32 | self._owner = owner
33 |
34 | def paint(self, fbo):
35 | with fbo:
36 | PcbMouseBites.paint(self._owner.color, pos=(0, 0), size=self._owner.size)
37 |
38 |
39 | class MouseBiteWidget(OffScreenScatter):
40 |
41 | def __init__(self, gap, root, horizontal, slide):
42 | self._gap = gap
43 | self._root = root
44 | self._horizontal = horizontal
45 | self._slide = slide
46 |
47 | self._connected = False
48 | self._base_color = PCB_MASK_COLOR
49 | self._moving_color = PCB_BITE_GOOD_COLOR
50 | self._disconnected_color = PCB_BITE_BAD_COLOR
51 | self._moving = False
52 |
53 | super(MouseBiteWidget, self).__init__(MouseBitesRenderer(self))
54 |
55 | if horizontal:
56 | self.do_translation_x = True
57 | self.do_translation_y = False
58 | else:
59 | self.do_translation_x = False
60 | self.do_translation_y = True
61 |
62 | self._group = None
63 | self._start = (0, 0)
64 |
65 | def set(self, pos, size):
66 | self.pos = pos
67 | self.size = size
68 | self.paint()
69 |
70 | def repaint(self):
71 | self.paint()
72 | # TODO: is there a better way to repaint?
73 | if self.parent is not None:
74 | self.deactivate()
75 | self.activate()
76 |
77 | def validate_pos(self):
78 | # TODO" assumes horizontal layout
79 | self._gap.validate_pos(self, self.pos[0])
80 |
81 | def start_move(self):
82 | self._moving = True
83 | self._start = self.pos
84 | self.validate_pos()
85 | self.repaint()
86 |
87 | def end_move(self):
88 | self._moving = False
89 | self._slide = self._gap.validate_move(self, self.pos[0])
90 | self._gap.layout() # constrain the bite location to lie within the gap
91 | self.repaint()
92 |
93 | def move_by(self, dx, dy):
94 | if self._moving:
95 | position = (self._start[0] + dx, self._start[1] + dy)
96 | self.pos = position
97 | self._slide = self._gap.validate_move(self, self.pos[0])
98 |
99 | def on_touch_down(self, touch):
100 | handled = super(MouseBiteWidget, self).on_touch_down(touch)
101 | if handled:
102 | self.start_move()
103 | for b in self._group:
104 | if b is not self:
105 | b.start_move()
106 | return handled
107 |
108 | def on_touch_move(self, touch):
109 | handled = super(MouseBiteWidget, self).on_touch_move(touch)
110 | if self._moving:
111 | self._gap.validate_pos(self, self.pos[0])
112 | dx = self.pos[0] - self._start[0]
113 | dy = self.pos[1] - self._start[1]
114 | for b in self._group:
115 | if b is not self:
116 | b.move_by(dx, dy)
117 | return handled
118 |
119 | def on_touch_up(self, touch):
120 | handled = super(MouseBiteWidget, self).on_touch_up(touch)
121 | if self._moving:
122 | self.end_move()
123 | for b in self._group:
124 | if b is not self:
125 | b.end_move()
126 | return handled
127 |
128 | def assign_group(self, group):
129 | self._group = group
130 |
131 | def activate(self):
132 | self._root.add_widget(self)
133 |
134 | def deactivate(self):
135 | self._root.remove_widget(self)
136 |
137 | def mark_connected(self, value):
138 | update = (self._connected != value)
139 | self._connected = value
140 | if update:
141 | self.repaint()
142 |
143 | @property
144 | def position(self):
145 | return self.pos
146 |
147 | @property
148 | def slide(self):
149 | return self._slide
150 |
151 | @property
152 | def connected(self):
153 | return self._connected
154 |
155 | @property
156 | def color(self):
157 | if self._moving:
158 | if not self._connected:
159 | return self._disconnected_color
160 | else:
161 | return self._moving_color
162 | else:
163 | if not self._connected:
164 | return self._disconnected_color
165 | else:
166 | return self._base_color
167 |
--------------------------------------------------------------------------------
/OffScreenImage.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from kivy.uix.image import Image
25 | from kivy.graphics import Fbo, ClearBuffers, ClearColor
26 |
27 |
28 | class OffScreenImage(Image):
29 |
30 | def __init__(self, client, shader, **kwargs):
31 | super(OffScreenImage, self).__init__(**kwargs)
32 |
33 | self._client = client
34 | self._fbo = Fbo(use_parent_projection=False, mipmap=True)
35 | if shader is not None:
36 | self._fbo.shader.fs = shader
37 |
38 | def paint(self, size):
39 | if size is not None:
40 | self.size = size
41 |
42 | self._fbo.size = self.size
43 | with self._fbo:
44 | ClearColor(0, 0, 0, 0)
45 | ClearBuffers()
46 | if self._client is not None:
47 | self._client.paint(self._fbo, self.size)
48 | self._fbo.draw()
49 | self.texture = self._fbo.texture
50 |
--------------------------------------------------------------------------------
/OffScreenScatter.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import math
25 |
26 | from kivy.uix.scatter import Scatter
27 | from kivy.uix.image import Image
28 | from kivy.graphics import Fbo, ClearBuffers, ClearColor
29 | from kivy.graphics.transformation import Matrix
30 |
31 |
32 | class OffScreenScatter(Scatter):
33 |
34 | def __init__(self, client, pos=(0, 0), size=(100, 100), **kwargs):
35 | self.pos = pos
36 | self.size = size
37 | self.size_hint = (None, None)
38 | self.do_rotation = False
39 | self.do_scale = False
40 | self.do_translation_x = False
41 | self.do_translation_y = False
42 |
43 | super(OffScreenScatter, self).__init__(**kwargs)
44 |
45 | self._width_org = self.size[0]
46 | self._height_org = self.size[1]
47 |
48 | self._scale = 1.0
49 | self._angle = 0.0
50 |
51 | self._client = client
52 |
53 | self._fbo = Fbo(size=self.size, use_parent_projection=False, mipmap=True)
54 |
55 | self._image = Image(size=size, texture=self._fbo.texture)
56 | self.add_widget(self._image)
57 |
58 | self.paint()
59 |
60 | def set_scale(self, scale):
61 | self._scale = scale / 100.0
62 |
63 | def paint(self):
64 | self._fbo.size = self.size
65 | self._image.size = self.size
66 | with self._fbo:
67 | ClearColor(0, 0, 0, 0)
68 | ClearBuffers()
69 | if self._client is not None:
70 | self._client.paint(self._fbo)
71 | self._fbo.draw()
72 | self._image.texture = self._fbo.texture
73 |
74 | def center(self, available_size, angle=None):
75 | if angle is not None:
76 | self._angle = angle
77 | cx = available_size[0] / 2.0
78 | cy = available_size[1] / 2.0
79 | ax = (self.size[0] / 2.0)
80 | ay = (self.size[1] / 2.0)
81 | self.transform = Matrix().identity()
82 | mat = Matrix().translate(cx-ax, cy-ay, 0.0)
83 | self.apply_transform(mat)
84 | mat = Matrix().rotate(math.radians(self._angle), 0.0, 0.0, 1.0)
85 | self.apply_transform(mat, post_multiply=True, anchor=(ax, ay))
86 | mat = Matrix().scale(self._scale, self._scale, 1.0)
87 | self.apply_transform(mat, post_multiply=True, anchor=(ax, ay))
88 |
--------------------------------------------------------------------------------
/PcbBoard.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from OffScreenScatter import *
25 |
26 |
27 | class PcbBoard(OffScreenScatter):
28 |
29 | def __init__(self, root, pcb, **kwargs):
30 | super(PcbBoard, self).__init__(client=pcb, pos=(0, 0), size=pcb.size_pixels, **kwargs)
31 |
32 | self._root = root
33 | self._active = False
34 |
35 | def activate(self):
36 | if not self._active:
37 | self._root.add_widget(self)
38 | self._active = True
39 |
40 | def deactivate(self):
41 | if self._active:
42 | self._root.remove_widget(self)
43 | self._active = False
44 |
--------------------------------------------------------------------------------
/PcbGap.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from AppSettings import *
25 | from MouseBiteWidget import *
26 | from PcbShape import *
27 |
28 |
29 | class PcbGap:
30 |
31 | # shape1 is either the bottom or left
32 | # shape2 is either top or right
33 | def __init__(self, panel, root, horizontal, bites_count, shape1, shape2):
34 | self._panel = panel
35 | self._root = root
36 | self._horizontal = horizontal
37 | self._bites_count = bites_count
38 | self._shape1 = shape1
39 | self._shape2 = shape2
40 | self._main_shape = None
41 | self._bottom_shape = None
42 | self._bottom_shape = None
43 |
44 | self._bites = []
45 | self._gap_start = 0
46 | self._gap_end = 0
47 | self._gap_width = 0
48 | self._bite_width = 0
49 |
50 | self._main_shape = self._shape1
51 | if not self._main_shape.is_of_kind(PcbKind.main):
52 | self._main_shape = self._shape2
53 | self._bottom_shape = self._shape1
54 |
55 | for i in range(self._bites_count):
56 | slide = (float(i + 1) / float(self._bites_count + 1))
57 | self._bites.append(MouseBiteWidget(self, root, self._horizontal, slide))
58 |
59 | def layout(self):
60 | scale = self._panel.scale
61 | scale_mm = self._panel.pixels_per_cm * scale / 10.0
62 |
63 | gap = (AppSettings.gap * scale_mm)
64 | width = (AppSettings.bite * scale_mm)
65 |
66 | panel_ox = self._panel.origin[0]
67 | panel_oy = self._panel.origin[1]
68 | shape_ox = (self._main_shape.x * scale)
69 | shape_oy = ((self._shape1.y + self._shape1.height) * scale) - 2.0
70 | shape_w = (self._main_shape.width * scale)
71 | origin_x = panel_ox + shape_ox
72 | origin_y = panel_oy + shape_oy
73 |
74 | self._gap_start = origin_x
75 | self._gap_end = origin_x + shape_w
76 | self._gap_width = (self._gap_end - self._gap_start)
77 | self._bite_width = width
78 |
79 | for i in range(self._bites_count):
80 | bite = self._bites[i]
81 | slide = (bite.slide * shape_w)
82 | x = (origin_x + slide)
83 | pos = (x, origin_y)
84 | size = (width, gap + 3.0)
85 | bite.set(pos, size)
86 |
87 | valid = True
88 | for i in range(self._bites_count):
89 | bite = self._bites[i]
90 | bite.validate_pos()
91 | valid = valid and bite.connected
92 | return valid
93 |
94 | def connects(self, pos, length):
95 | if not self._shape1.connects(self._horizontal, False, pos, length):
96 | return False
97 | else:
98 | return self._shape2.connects(self._horizontal, True, pos, length)
99 |
100 | def __str__(self):
101 | rep = 'PcbGap'
102 | return rep + ' {}, {} with {} bites'.format(self._shape1, self._shape2, self._bites_count)
103 |
104 | def activate(self):
105 | for b in self._bites:
106 | b.activate()
107 |
108 | def deactivate(self):
109 | for b in self._bites:
110 | b.deactivate()
111 |
112 | def validate_pos(self, bite, pos):
113 | inside = True
114 | connected = True
115 | if self._gap_width > 0:
116 | if self._horizontal:
117 | if pos < self._gap_start:
118 | inside = False
119 | pos = self._gap_start
120 | elif pos >= self._gap_end - self._bite_width:
121 | inside = False
122 | pos = self._gap_end - self._bite_width
123 | rel_x = (pos - self._gap_start) / self._gap_width
124 | else:
125 | rel_x = 0 # TODO
126 | if inside:
127 | rel_w = self._bite_width / self._gap_width
128 | connected = self.connects(rel_x, rel_w)
129 | bite.mark_connected(inside and connected)
130 | self._panel.update_status()
131 | return pos
132 |
133 | def validate_layout(self):
134 | for b in self._bites:
135 | b.validate_pos()
136 |
137 | def validate_move(self, bite, x):
138 | x = self.validate_pos(bite, x)
139 | return (x - self._gap_start) / self._gap_width
140 |
141 | def bite(self, index):
142 | return self._bites[index]
143 |
144 | @property
145 | def bites_count(self):
146 | return self._bites_count
147 |
148 | @property
149 | def bite_center_offset(self):
150 | return self._bite_offset
151 |
152 | @property
153 | def main_shape(self):
154 | return self._main_shape
155 |
156 | @property
157 | def bottom_shape(self):
158 | return self._bottom_shape
159 |
--------------------------------------------------------------------------------
/PcbMask.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from kivy.graphics import Fbo, Rectangle, Translate, Rotate, Color
25 |
26 |
27 | class PcbMask:
28 |
29 | def __init__(self, mask, angle):
30 |
31 | self._pixels = mask.texture.pixels
32 | self._pixels_w = int(mask.texture_size[0])
33 | self._pixels_h = int(mask.texture_size[1])
34 |
35 | if angle == 0.0:
36 | self._pixels_w = int(mask.texture_size[0])
37 | self._pixels_h = int(mask.texture_size[1])
38 | fbo = Fbo(size=(self._pixels_w, self._pixels_h))
39 | mask.texture.flip_vertical()
40 | fbo.clear()
41 | with fbo:
42 | Color(1, 1, 1, 1)
43 | Rectangle(size=(self._pixels_w, self._pixels_h), texture=mask.texture)
44 | fbo.draw()
45 | mask.texture.flip_vertical()
46 | self._pixels = fbo.pixels
47 | else:
48 | self._pixels_w = int(mask.texture_size[0])
49 | self._pixels_h = int(mask.texture_size[1])
50 | fbo = Fbo(size=(self._pixels_w, self._pixels_h))
51 | mask.texture.flip_horizontal()
52 | fbo.clear()
53 | with fbo:
54 | Color(1, 1, 1, 1)
55 | Translate(self._pixels_w, self._pixels_h)
56 | Rotate(angle, 0.0, 0.0, 1.0)
57 | Translate(-self._pixels_h, 0)
58 | Rectangle(size=(self._pixels_h, self._pixels_w), texture=mask.texture)
59 | fbo.draw()
60 | mask.texture.flip_horizontal()
61 | self._pixels = fbo.pixels
62 |
63 | # for y in range(0, self._pixels_h, 4):
64 | # for x in range(0, self._pixels_w, 2):
65 | # i = int((y*self._pixels_w*4) + (x*4) + 3)
66 | # p = self._pixels[i] > 0
67 | # v = '.'
68 | # if p > 0:
69 | # v = 'X'
70 | # print('{}'.format(v), end='')
71 | # print('')
72 |
73 | def get_mask_index(self, x, y):
74 | if x < 0:
75 | x = 0
76 | elif x >= self._pixels_w:
77 | x = self._pixels_w - 1
78 | if y < 0:
79 | y = 0
80 | elif y >= self._pixels_h:
81 | y = self._pixels_h - 1
82 | y = self._pixels_h - y
83 | x = int(x)
84 | y = int(y)
85 | return int((y*self._pixels_w*4) + (x*4) + 3) # we want alpha channel only (GL_RGBA, GL_UNSIGNED_BYTE)
86 |
87 | def get_mask_alpha(self, x, y, length):
88 | length = int(length*self._pixels_w)
89 | alpha = self._pixels[self.get_mask_index(int(x+length), y)] > 0
90 | for pos in range(int(x), int(x+length-1), 2):
91 | alpha = alpha and self._pixels[self.get_mask_index(pos, y)] > 0
92 | if not alpha:
93 | break
94 | return alpha
95 |
96 | def get_mask_bottom(self, x, length):
97 | x *= self._pixels_w
98 | y = 1
99 | return self.get_mask_alpha(x, y, length)
100 |
101 | def get_mask_top(self, x, length):
102 | x *= self._pixels_w
103 | y = self._pixels_h - 1
104 | return self.get_mask_alpha(x, y, length)
105 |
106 | def get_mask_left(self, y, length):
107 | x = 1
108 | y *= self._pixels_h
109 | return self.get_mask_alpha(x, y, length)
110 |
111 | def get_mask_right(self, y, length):
112 | x = self._pixels_w - 1
113 | y *= self._pixels_h
114 | return self.get_mask_alpha(x, y, length)
115 |
--------------------------------------------------------------------------------
/PcbMouseBites.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import tempfile
25 |
26 | from kivy.cache import Cache
27 | from kivy.graphics import Rectangle, Color
28 |
29 | from AppSettings import *
30 | from Constants import *
31 | from PcbFile import *
32 | from Utilities import *
33 | import Constants
34 |
35 |
36 | class PcbMouseBites:
37 |
38 | def __init__(self):
39 |
40 | self._gm1 = None
41 | self._drl = None
42 |
43 | self._bite = 0
44 | self._gap = 0
45 | self._bite_hole_radius = 0
46 | self._bite_hole_space = 0
47 |
48 | self._tmp_folder = tempfile.TemporaryDirectory().name
49 | try:
50 | os.mkdir(self._tmp_folder)
51 | except FileExistsError:
52 | pass
53 |
54 | def generate_pcb_files(self):
55 | if self._tmp_folder is None:
56 | print('ERROR: PcbMouseBites temp folder is NULL')
57 | return
58 |
59 | origin = (0, 0)
60 | size = (self._bite, self._gap)
61 | save_mouse_bite_gm1(self._tmp_folder, origin, size, arc=Constants.PCB_BITES_ARC_MM, close=False)
62 | save_mouse_bite_drl(self._tmp_folder, origin, size, self._bite_hole_radius, self._bite_hole_space)
63 |
64 | return self._tmp_folder
65 |
66 | def render_masks(self, bite, gap, bite_hole_radius, bite_hole_space):
67 | if self._tmp_folder is None:
68 | print('ERROR: PcbMouseBites temp folder is NULL')
69 | return
70 |
71 | if self._gm1 is None or self._bite != bite or self._gap != gap or \
72 | self._bite_hole_radius != bite_hole_radius or self._bite_hole_space != bite_hole_space:
73 |
74 | render_mouse_bite_gm1(self._tmp_folder, 'bites_edge_cuts',
75 | origin=(0, 0), size=(bite, gap), arc=Constants.PCB_BITES_ARC_MM, close=True)
76 | render_mouse_bite_drl(self._tmp_folder, 'bites_holes_npth',
77 | origin=(0, 0), size=(bite, gap), radius=bite_hole_radius, gap=bite_hole_space)
78 |
79 | self._gm1 = load_image_masked(self._tmp_folder, 'bites_edge_cuts_mask.png', Color(1, 1, 1, 1))
80 | self._drl = load_image_masked(self._tmp_folder, 'bites_holes_npth.png', Color(1, 1, 1, 1))
81 |
82 | # TODO: is there anything else that's more efficient that we can do here?
83 | # without this the rail images do not refresh correctly
84 | Cache.remove('kv.image')
85 | Cache.remove('kv.texture')
86 |
87 | self._bite = bite
88 | self._gap = gap
89 | self._bite_hole_radius = bite_hole_radius
90 | self._bite_hole_space = bite_hole_space
91 |
92 | def paint(self, color, pos, size):
93 | if self.gm1_image is not None:
94 | Color(color.r, color.g, color.b, color.a)
95 | Rectangle(texture=self.gm1_image.texture, size=size, pos=pos)
96 | color = PCB_DRILL_NPTH_COLOR
97 | Color(color.r, color.g, color.b, color.a)
98 | Rectangle(texture=self.drl_image.texture, size=size, pos=pos)
99 |
100 | @property
101 | def gm1_image(self):
102 | return self._gm1
103 |
104 | @property
105 | def drl_image(self):
106 | return self._drl
107 |
108 | def invalidate(self):
109 | self._gm1 = None
110 | self._drl = None
111 |
112 | def cleanup(self):
113 | rmrf(self._tmp_folder)
114 |
115 |
116 | PcbMouseBites = PcbMouseBites()
117 |
--------------------------------------------------------------------------------
/PcbMouseBitesGroup.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from kivy.graphics import Rectangle, Translate, Rotate, PushMatrix, PopMatrix
25 |
26 | from Array2D import *
27 | from PcbGap import *
28 | from PcbPanel import *
29 |
30 |
31 | class PcbMouseBitesGroup:
32 |
33 | def assign_groups(self, count, columns, rows, gaps):
34 | for i in range(count):
35 | group = []
36 | for r in range(0, rows):
37 | for c in range(0, columns):
38 | gap = gaps.get(c, r)
39 | bite = gap.bite(i)
40 | group.append(bite)
41 | self._bites.append(bite)
42 | for r in range(0, rows):
43 | for c in range(0, columns):
44 | gap = gaps.get(c, r)
45 | bite = gap.bite(i)
46 | bite.assign_group(group)
47 |
48 | def __init__(self, panel, root, shapes, bites_count):
49 | self._bites = []
50 | self._shapes = shapes
51 | #self._shapes.print(' ')
52 |
53 | if bites_count > 0:
54 | columns = self._shapes.width
55 | rows = (self._shapes.height - 1)
56 | self._horizontal = Array2D(columns, rows)
57 | for r in range(0, rows):
58 | for c in range(0, columns):
59 | bottom = self._shapes.get(c, r)
60 | top = self._shapes.get(c, r + 1)
61 | gap = PcbGap(panel, root, True, bites_count, bottom, top)
62 | self._horizontal.put(c, r, gap)
63 | #print(' horizontal gaps:')
64 | #self._horizontal.print(' ')
65 | self.assign_groups(bites_count, columns, rows, self._horizontal)
66 | else:
67 | self._horizontal = Array2D(0, 0)
68 |
69 | def activate(self):
70 | # TODO: implement Array2D iterator and use it here
71 | for c in range(self._horizontal.width):
72 | for r in range(self._horizontal.height):
73 | gap = self._horizontal.get(c, r)
74 | gap.activate()
75 |
76 | def deactivate(self):
77 | # TODO: implement Array2D iterator and use it here
78 | for c in range(self._horizontal.width):
79 | for r in range(self._horizontal.height):
80 | gap = self._horizontal.get(c, r)
81 | gap.deactivate()
82 |
83 | def layout(self):
84 | valid = True
85 | # TODO: implement Array2D iterator and use it here
86 | for c in range(self._horizontal.width):
87 | for r in range(self._horizontal.height):
88 | gap = self._horizontal.get(c, r)
89 | # workaround for "value and function()" optimizing out function() if value == False
90 | # valid = valid and gap.layout()
91 | v = gap.layout()
92 | valid = valid and v
93 | return valid
94 |
95 | def get_row_xs_mm(self, scale):
96 | xs_mm = []
97 | gap = self._horizontal.get(0, 0)
98 | main = gap.main_shape
99 | main_origin = main.get_origin_mm(scale)
100 | main_size = main.get_size_mm(scale)
101 | for b in range(gap.bites_count):
102 | bite = gap.bite(b)
103 | origin_x = main_origin[0] + (bite.slide * main_size[0])
104 | xs_mm.append(origin_x)
105 | return xs_mm
106 |
107 | def get_origins_mm(self, scale):
108 | origins = []
109 | # TODO: implement Array2D iterator and use it here
110 | for r in range(self._horizontal.height):
111 | origins_row = []
112 | for c in range(self._horizontal.width):
113 | gap = self._horizontal.get(c, r)
114 | main = gap.main_shape
115 | main_origin = main.get_origin_mm(scale)
116 | main_size = main.get_size_mm(scale)
117 | bottom = gap.bottom_shape
118 | bottom_origin = bottom.get_origin_mm(scale)
119 | bottom_size = bottom.get_size_mm(scale)
120 | for b in range(gap.bites_count):
121 | bite = gap.bite(b)
122 | origin_x = main_origin[0] + (bite.slide * main_size[0])
123 | origin_y = bottom_origin[1] + bottom_size[1]
124 | origins_row.append((origin_x, origin_y))
125 | origins.append(origins_row)
126 | return origins
127 |
128 | def validate_layout(self):
129 | valid = True
130 | for b in self._bites:
131 | valid = valid and b.connected
132 | return valid
133 |
--------------------------------------------------------------------------------
/PcbRail.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import tempfile
25 |
26 | from kivy.cache import Cache
27 | from kivy.graphics import Rectangle, Color
28 |
29 | from AppSettings import *
30 | from PcbFile import *
31 | from Utilities import *
32 | from Constants import *
33 |
34 |
35 | class PcbRail:
36 |
37 | def __init__(self):
38 | self._gm1 = None
39 | self._gtl = None
40 | self._gts = None
41 | self._gto = None
42 |
43 | self._panels = 0
44 | self._gap = 0
45 | self._origin = (0, 0)
46 | self._size = (0, 0)
47 | self._vcut = False
48 | self._jlc = False
49 |
50 | self._tmp_folder = tempfile.TemporaryDirectory().name
51 | try:
52 | os.mkdir(self._tmp_folder)
53 | except FileExistsError:
54 | pass
55 |
56 | def generate_pcb_files(self):
57 | if self._tmp_folder is None:
58 | print('ERROR: PcbRail temp folder is NULL')
59 | return
60 |
61 | save_rail_gm1(self._tmp_folder, self._origin, self._size, self._panels, self._gap, self._vcut)
62 | save_rail_gtl(self._tmp_folder, self._origin, self._size)
63 | save_rail_gts(self._tmp_folder, self._origin, self._size)
64 | save_rail_gto(self._tmp_folder, self._origin, self._size, self._panels, self._gap, self._vcut, self._jlc)
65 | save_rail_gbo(self._tmp_folder, self._origin, self._size)
66 |
67 | return self._tmp_folder
68 |
69 | def render_masks(self, panels, gap, origin, size, vcut, jlc):
70 | if self._tmp_folder is None:
71 | print('ERROR: PcbRail temp folder is NULL')
72 | return
73 |
74 | if self._gm1 is None or self._panels != panels or self._origin != origin or self._size != size or \
75 | self._vcut != vcut or self._jlc != jlc:
76 |
77 | bounds = render_rail_gm1(self._tmp_folder, 'rail_edge_cuts', origin, size, panels, gap, vcut)
78 | render_rail_gtl(bounds, self._tmp_folder, 'rail_top_copper', origin, size)
79 | render_rail_gts(bounds, self._tmp_folder, 'rail_top_mask', origin, size)
80 | render_rail_gto(bounds, self._tmp_folder, 'rail_top_silk', origin, size, panels, gap, vcut, jlc)
81 |
82 | self._gm1 = load_image_masked(self._tmp_folder, 'rail_edge_cuts_mask.png', Color(1, 1, 1, 1))
83 | self._gtl = load_image_masked(self._tmp_folder, 'rail_top_copper.png', Color(1, 1, 1, 1))
84 | self._gts = load_image_masked(self._tmp_folder, 'rail_top_mask.png', Color(1, 1, 1, 1))
85 | self._gto = load_image_masked(self._tmp_folder, 'rail_top_silk.png', Color(1, 1, 1, 1))
86 |
87 | # TODO: is there anything else that's more efficient that we can do here?
88 | # without this the rail images do not refresh correctly
89 | Cache.remove('kv.image')
90 | Cache.remove('kv.texture')
91 |
92 | self._panels = panels
93 | self._gap = gap
94 | self._origin = origin
95 | self._size = size
96 | self._vcut = vcut
97 | self._jlc = jlc
98 |
99 | def paint(self, bottom, top):
100 |
101 | c = PCB_MASK_COLOR
102 | Color(c.r, c.g, c.b, c.a)
103 | Rectangle(texture=self._gm1.texture, pos=bottom.pos, size=bottom.size)
104 | Rectangle(texture=self._gm1.texture, pos=top.pos, size=top.size)
105 |
106 | c = PCB_TOP_MASK_COLOR
107 | Color(c.r, c.g, c.b, c.a)
108 | Rectangle(texture=self._gts.texture, pos=bottom.pos, size=bottom.size)
109 | Rectangle(texture=self._gts.texture, pos=top.pos, size=top.size)
110 |
111 | c = PCB_TOP_TRACES_COLOR
112 | Color(c.r, c.g, c.b, c.a)
113 | Rectangle(texture=self._gtl.texture, pos=bottom.pos, size=bottom.size)
114 | Rectangle(texture=self._gtl.texture, pos=top.pos, size=top.size)
115 |
116 | c = PCB_TOP_SILK_COLOR
117 | Color(c.r, c.g, c.b, c.a)
118 | Rectangle(texture=self._gto.texture, pos=bottom.pos, size=bottom.size)
119 | Rectangle(texture=self._gto.texture, pos=top.pos, size=top.size)
120 |
121 | def invalidate(self):
122 | self._gm1 = None
123 | self._gtl = None
124 | self._gts = None
125 | self._gto = None
126 |
127 | def cleanup(self):
128 | rmrf(self._tmp_folder)
129 |
130 |
131 | PcbRail = PcbRail()
132 |
--------------------------------------------------------------------------------
/PcbShape.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from enum import Enum
25 |
26 |
27 | class PcbKind(Enum):
28 | top = 1
29 | main = 2
30 | bottom = 3
31 |
32 |
33 | class PcbShape:
34 |
35 | def __init__(self, kind, mask):
36 | self._kind = kind
37 | self._mask = mask
38 | self._pos = (0, 0)
39 | self._size = (0, 0)
40 |
41 | def __str__(self):
42 | rep = 'PcbShape'
43 | if self._kind is PcbKind.top:
44 | rep += ' TOP '
45 | elif self._kind is PcbKind.bottom:
46 | rep += ' BOTT'
47 | elif self._kind is PcbKind.main:
48 | rep += ' MAIN'
49 | else:
50 | rep += ' ????'
51 | return rep + ' {}, {}'.format(self._pos, self._size)
52 |
53 | def is_of_kind(self, kind):
54 | return kind == self._kind
55 |
56 | def set(self, pos, size):
57 | self._pos = pos
58 | self._size = size
59 |
60 | def mask_connects_horizontal(self, bottom, x, length):
61 | if bottom:
62 | return self._mask.get_mask_bottom(x, length)
63 | else:
64 | return self._mask.get_mask_top(x, length)
65 |
66 | def connects(self, horizontal, side, pos, length):
67 | if horizontal:
68 | if self._kind is PcbKind.bottom:
69 | return True # always connects
70 | elif self._kind is PcbKind.top:
71 | return True # always connects
72 | elif self._kind is PcbKind.main:
73 | return self.mask_connects_horizontal(side, pos, length)
74 |
75 | @property
76 | def x(self):
77 | return self._pos[0]
78 |
79 | @property
80 | def y(self):
81 | return self._pos[1]
82 |
83 | @property
84 | def width(self):
85 | return self._size[0]
86 |
87 | @property
88 | def height(self):
89 | return self._size[1]
90 |
91 | @property
92 | def pos(self):
93 | return self._pos
94 |
95 | def get_origin_mm(self, scale):
96 | return (self._pos[0]/scale, self._pos[1]/scale)
97 |
98 | @property
99 | def size(self):
100 | return self._size
101 |
102 | def get_size_mm(self, scale):
103 | return (self._size[0]/scale, self._size[1]/scale)
104 |
--------------------------------------------------------------------------------
/PcbWorkarounds.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import os
25 | import sys
26 |
27 | import hm_gerber_ex
28 |
29 | from hm_gerber_ex import GerberComposition, DrillComposition
30 | from hm_gerber_tool.cam import FileSettings
31 | from hm_gerber_tool.utils import listdir
32 |
33 | from Utilities import *
34 |
35 |
36 | # Workaround needed for PCB Way online gerber preview to work correctly.
37 | # For those gerber drill files that use routing commands
38 | # Basically we move the routing instructions to the bottom of the file
39 | def fix_drl_routing(path):
40 | if not os.path.isdir(path):
41 | print('ERROR: path {} does not exist'.format(path))
42 | return
43 |
44 | for filename in listdir(path, True, True):
45 | filename_ext = os.path.splitext(filename)[1].lower()
46 | if filename_ext == '.drl':
47 | header = True
48 | routing = False
49 | tool = None
50 | lines_main = []
51 | lines_route = []
52 | file = load_file(path, filename)
53 | segments = file.split("\n")
54 | for s in segments:
55 | if s == 'M30':
56 | pass
57 | elif s == '%':
58 | header = False
59 | lines_main.append(s+'\n')
60 | else:
61 | if header is True:
62 | lines_main.append(s+'\n')
63 | else:
64 | if s.startswith('T'):
65 | tool = s
66 | routing = False
67 | if not routing:
68 | if s.startswith('G0'):
69 | lines_main.pop() # remove the routing tool from main section
70 | lines_route.append(tool+'\n')
71 | routing = True
72 | if not routing:
73 | lines_main.append(s+'\n')
74 | else:
75 | lines_route.append(s+'\n')
76 |
77 | f = open(os.path.join(path, filename), "w")
78 | f.writelines(lines_main)
79 | f.writelines(lines_route)
80 | f.write('M30\n')
81 | f.close()
82 |
83 |
84 | # Workaround needed for OSH Park online gerber preview to work correctly.
85 | # Always end (or start) with LPD to set up the polarity correctly for drawing,
86 | # in case LPC (clear) is used at any point and the manufacturer concatenates the files.
87 | def fix_silk_lpc(path):
88 | if not os.path.isdir(path):
89 | print('ERROR: path {} does not exist'.format(path))
90 | return
91 |
92 | for filename in listdir(path, True, True):
93 | filename_ext = os.path.splitext(filename)[1].lower()
94 | if filename_ext == '.gbo' or filename_ext == '.gto':
95 | lines_main = []
96 | file = load_file(path, filename)
97 | segments = file.split("\n")
98 | for s in segments:
99 | if s == 'M02*':
100 | pass
101 | else:
102 | lines_main.append(s+'\n')
103 |
104 | f = open(os.path.join(path, filename), "w")
105 | f.writelines(lines_main)
106 | f.write('%LPD*%\n')
107 | f.write('M02*\n')
108 | f.close()
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hm-panelizer
2 |
3 | 
4 |
5 | A GUI based PCB gerber file viewer and panelizer written in python
6 |
7 | This tool would not have been possible without the following projects:
8 |
9 | - www.kivy.org
10 |
11 | - www.github.com/curtacircuitos/pcb-tools
12 |
13 | - www.github.com/opiopan/pcb-tools-extension
14 |
15 | - www.useiconic.com/open
16 |
17 | hm-panelizer is released under MIT license, Copyright 2021,2022 HalfMarble LLC (www.halfmarble.com)
18 |
19 | Please note that I forked **_pcb-tools_** and **_pcb-tools-extension_**, made significant modifications,
20 | and included them as part of this tool.
21 |
22 | ## Why did I create hm-panelizer?
23 |
24 | There are a couple of open source tools out there that will help you panelize your Pcb,
25 | for example http://blog.thisisnotrocketscience.nl/projects/pcb-panelizer/ , https://github.com/sej7278/kicad-panelizer and www.github.com/yaqwsx/KiKit,
26 | however, I wanted a GUI based app, which I could run (_easily_) on a macOS based machine, so here we are.
27 |
28 | It also gave me an opportunity to learn python (_it is my first python app, so it is most certainly not optimally implemented_)
29 | and gain some insight into the mysterious world of gerber files.
30 |
31 | ## _! WARNING !_
32 |
33 | _I hope that you will find hm-panelizer useful, however, I offer no guarantee that it will work in your case -
34 | always verify with other tools, before you order your Pcb panels!_
35 |
36 | ## How to run
37 |
38 | hm-panelizer is a **_python app_**, so you will need **_python_** version `3.6.x` or higher (we use `3.9.12`) and
39 | install `kivy`, `pygame` and `pycairo` python packages. Here is an example of steps I had to perform on my own macOS machine:
40 |
41 | - Install **homebrew** by following one step installation on https://brew.sh/
42 |
43 |
44 | - Add "/opt/homebrew/bin" to your PATH in ZSH configuration file:
45 |
46 | open -e ~/.zshrc
47 |
48 | export PATH=$PATH:/opt/homebrew/bin
49 |
50 | - Install "pkg-config" and "cairo":
51 |
52 | /opt/homebrew/bin/brew install pkg-config
53 |
54 | /opt/homebrew/bin/brew install cairo
55 |
56 | - Upgrade "pip":
57 |
58 | python3 -m pip install --upgrade pip
59 |
60 | - Install "kivy", pycairo" and "pygame" python packages:
61 |
62 | pip3 install kivy
63 |
64 | pip3 install pygame
65 |
66 | pip3 install pycairo
67 |
68 | - I have received a report that "cairocffi" python package is required as well. On my Mac things work fine without it, however, if your machine needs it, then you can install it with:
69 |
70 | pip3 install cairocffi
71 |
72 | Once you have `python` and the required python packages installed, you can run `hm-panelizer` via command line
73 | (i.e. terminal) by `cd`'ing into the **hm-panelizer** folder, then issuing `python3 main.py` command.
74 |
75 | ## Screenshots:
76 |
77 | Main view
78 |
79 | 
80 |
81 | Main view (outline verification)
82 |
83 | 
84 |
85 | Panel view
86 |
87 | 
88 |
89 | ## Will hm-panelizer work with my Pcb?
90 |
91 | It might. The gerber viewer part should almost certainly work, but the panelizer feature is another story.
92 |
93 | I personally use [KiCad](https://www.kicad.org) 6.x and wanted to panelize my own Pcb,
94 | so that's what I mostly tested. I did try a few other Pcbs created with other software and I am eager to hear your
95 | experience.
96 |
97 | Please keep in mind, however, that hm-panelizer was just a side project for me. I am releasing it
98 | as open source in hopes that the community will contribute to it. If you find a bug and can fix it, then please help!
99 |
100 | Having said that, here are requirements to create a Pcb that should make it suitable for hm-panelizer:
101 |
102 | - use **metric system**
103 | - your pcb gerber files must use **Altium/Protel filename extensions** (see https://pcbprime.com/pcb-tips/accepted-file-formats/Gerber%20File%20Extension%20Comparison.pdf)
104 | - the **board outline gerber file** (.gm1) must be present
105 | - **"Disable aperture macros"** when exporting gerber files (this may not be needed for simple designs and only needed by some Pcb houses)
106 |
107 | Limitations:
108 |
109 | - currently, the tool can only add **mouse-bites to perfectly straight lines** only (see hm-panelizer's "Outline verification" feature)
110 | - only horizontal mousebites are suppported
111 | - the **_horizontal/vertical_** feature is currently problematic with most Pcb houses (I recommend that you use your Pcb design app
112 | to do the rotation and only use **hm-panelizer** for layout and mousebites for now)
113 |
114 | Here are the KiCad settings I personally use to export my Pcbs:
115 |
116 | KiCad plot settings
117 |
118 | 
119 |
120 | KiCad drill settings
121 |
122 | 
123 |
124 | ## TODO
125 |
126 | Here is a list of wish features that I personally would like to add when I have the time:
127 |
128 | - speed optimizations (rendering and panelization)
129 | - GUI for setting colors of Pcb layers, themes
130 | - scrollbars
131 | - support both horizontal and vertical mouse bites
132 | - standard output/error redirected to the progress panel to track the debug logs
133 | - render component parts
134 | - 3D rendering
135 |
136 | ## Need help?
137 |
138 | Visit my Discord channel https://discord.gg/7mf5qqBMEF
139 |
140 | ## Please consider supporting hm-panelizer if you want to see more features!
141 |
142 | [](https://ko-fi.com/halfmarble)
143 |
144 |
145 |
146 |
--------------------------------------------------------------------------------
/SplitGerberComposition.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import hm_gerber_ex
25 | from hm_gerber_ex import GerberComposition
26 | from hm_gerber_tool.gerber_statements import CoordStmt, EofStmt
27 |
28 | from AppSettings import *
29 | from Utilities import *
30 |
31 |
32 | class SplitGerberComposition(GerberComposition):
33 |
34 | def __init__(self, settings=None, comments=None, cutout_lines=None):
35 | super(SplitGerberComposition, self).__init__(settings, comments)
36 | self.cutout_lines = cutout_lines
37 |
38 | def split_line(self, f, cutouts, start, end, verbose=False):
39 | if verbose:
40 | print('# SPLIT')
41 | print('# LINE START {},{} [{}] '
42 | .format(start.x, start.y, start.to_gerber(self.settings)))
43 | f.write(start.to_gerber(self.settings) + '\n')
44 | for cutout in cutouts:
45 | cutout_y = cutout[0]
46 | if equal_floats(cutout_y, round_down(start.y), AppSettings.merge_error):
47 | lines = cutout[1]
48 | for cutout_line in lines:
49 | start_cutout_x = cutout_line[0]
50 | end_cutout_x = cutout_line[1]
51 | if start_cutout_x >= start.x and end_cutout_x <= end.x:
52 | new_end = CoordStmt(None, start_cutout_x, cutout_y, None, None, 'D01', self.settings)
53 | new_start = CoordStmt(None, end_cutout_x, cutout_y, None, None, 'D02', self.settings)
54 | if verbose:
55 | print('# INSERTING LINE END {},{} [{}] '
56 | .format(new_end.x, new_end.y, new_end.to_gerber(self.settings)))
57 | print('# INSERTING LINE START {},{} [{}] '
58 | .format(new_start.x, new_start.y, new_start.to_gerber(self.settings)))
59 | f.write(new_end.to_gerber(self.settings) + '\n')
60 | f.write(new_start.to_gerber(self.settings) + '\n')
61 | if verbose:
62 | print('# LINE END {},{} [{}] '
63 | .format(end.x, end.y, end.to_gerber(self.settings)))
64 | f.write(end.to_gerber(self.settings) + '\n')
65 | return True
66 |
67 | def process_segment(self, f, i, lines, cutouts, verbose=False):
68 | split = False
69 | start = lines[i]
70 | if isinstance(start, CoordStmt) and start.op == 'D02' and len(lines) > (i+1):
71 | end = lines[i + 1]
72 | if isinstance(end, CoordStmt) and end.op == 'D01':
73 | if equal_floats(end.y, start.y, AppSettings.merge_error):
74 | if verbose:
75 | print('#')
76 | print('# HORIZONTAL LINE')
77 | print('# LINE START {},{} [{}] '
78 | .format(start.x, start.y, start.to_gerber(self.settings)))
79 | print('# LINE END {},{} [{}] '
80 | .format(end.x, end.y, end.to_gerber(self.settings)))
81 | for cutout in cutouts:
82 | cutout_y = cutout[0]
83 | if equal_floats(cutout_y, round_down(end.y), AppSettings.merge_error):
84 | if verbose:
85 | print('#')
86 | print('# MATCHING Y {}'.format(cutout_y))
87 | print('# LINE START {},{} [{}] '
88 | .format(start.x, start.y, start.to_gerber(self.settings)))
89 | print('# LINE END {},{} [{}] '
90 | .format(end.x, end.y, end.to_gerber(self.settings)))
91 | if end.x > start.x:
92 | if verbose:
93 | print('# DIRECTION ----->')
94 | else:
95 | if verbose:
96 | print('# DIRECTION <----- (SWAP NEEDED)')
97 | temp_x = end.x
98 | end.x = start.x
99 | start.x = temp_x
100 | if verbose:
101 | print('# NOW LINE START {},{} [{}] '
102 | .format(start.x, start.y, start.to_gerber(self.settings)))
103 | print('# NOW LINE END {},{} [{}] '
104 | .format(end.x, end.y, end.to_gerber(self.settings)))
105 | split = self.split_line(f, cutouts, start, end, verbose)
106 | if split:
107 | return i+1
108 | else:
109 | f.write(lines[i].to_gerber(self.settings) + '\n')
110 | return i
111 |
112 | # can handle only horizontal lines, and lines going from left to right (i.e. start.x < end.x)
113 | def process_statements(self, f, statements, cutouts, verbose=False):
114 | if verbose:
115 | print('>>>>>>>>>>> process_statements')
116 | print('>>>>>>>>>>> cutouts: {}'.format(cutouts))
117 | statements_list = []
118 | for statement in statements:
119 | statements_list.append(statement)
120 | for i in range(len(statements_list)):
121 | i = self.process_segment(f, i, statements_list, cutouts, verbose)
122 |
123 | def dump(self, path):
124 | def statements():
125 | for k in self.aperture_macros:
126 | yield self.aperture_macros[k]
127 | for s in self.apertures:
128 | yield s
129 | for s in self.drawings:
130 | yield s
131 | yield EofStmt()
132 | self.settings.notation = 'absolute'
133 | self.settings.zeros = 'trailing'
134 | with open(path, 'w') as f:
135 | hm_gerber_ex.rs274x.write_gerber_header(f, self.settings)
136 | if self.cutout_lines is not None:
137 | self.process_statements(f, statements(), self.cutout_lines, verbose=False)
138 | else:
139 | for statement in statements():
140 | f.write(statement.to_gerber(self.settings) + '\n')
141 |
--------------------------------------------------------------------------------
/UI.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | from kivy.uix.label import Label
25 | from kivy.uix.stencilview import StencilView
26 | from kivy.uix.togglebutton import ToggleButton
27 | from kivy.uix.popup import Popup
28 | from kivy.uix.screenmanager import Screen
29 | from kivy.graphics import Color, Rectangle
30 |
31 |
32 | class WorkScreen(StencilView):
33 |
34 | def __init__(self, **kwargs):
35 | super(WorkScreen, self).__init__(**kwargs)
36 |
37 | self._app = None
38 |
39 | with self.canvas:
40 | Color(1.0, 0.0, 0.0, 0.5)
41 | self.background_rect = Rectangle(pos=self.pos, size=self.size)
42 |
43 | self.bind(size=self.update_rect)
44 |
45 | def update_rect(self, *args):
46 | self.pos = (0, 2.0*self.pos[1]) # TODO: why do we need this here to center the screen vertically?
47 | self.background_rect.size = (self.pos[0]+self.size[0], self.pos[1]+self.size[1])
48 | self._app.resize(self.background_rect.size)
49 |
50 |
51 | class LayerButton(ToggleButton):
52 | pass
53 |
54 |
55 | class DemoLabel(Label):
56 | pass
57 |
58 |
59 | class EmptyLabel(Label):
60 | pass
61 |
62 |
63 | class PostLabel(Label):
64 | pass
65 |
66 |
67 | class TitleLabel(Label):
68 | pass
69 |
70 |
71 | class MenuLabel(Label):
72 | pass
73 |
74 |
75 | class Settings(Popup):
76 | pass
77 |
78 |
79 | class About(Popup):
80 | pass
81 |
82 |
83 | class Progress(Popup):
84 | pass
85 |
86 |
87 | class Error(Popup):
88 | pass
89 |
90 |
--------------------------------------------------------------------------------
/Utilities.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | # Copyright 2021,2022 HalfMarble LLC
4 |
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 |
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 |
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 |
23 |
24 | import os
25 | import sys
26 | import math
27 | import os
28 | import shutil
29 | import zipfile
30 | from os.path import join
31 | from typing import Final
32 |
33 | import kivy
34 | from kivy.base import EventLoop
35 | from kivy.graphics import Fbo, ClearColor, ClearBuffers, Color, Rectangle
36 | from kivy.uix.image import Image
37 |
38 | from math import floor, ceil
39 |
40 | import Constants
41 |
42 |
43 | def round_down(n, d=2):
44 | d = int('1' + ('0' * d))
45 | return floor(n * d) / d
46 |
47 |
48 | def round_up(n, d=2):
49 | d = int('1' + ('0' * d))
50 | return ceil(n * d) / d
51 |
52 |
53 | def equal_floats(one, two, sigma=Constants.PCB_PANEL_MERGE_ERROR):
54 | if abs(one-two) <= sigma:
55 | #print('equal_floats {},{},{} True'.format(one, two, sigma))
56 | return True
57 | else:
58 | #print('equal_floats {},{},{} False'.format(one, two, sigma))
59 | return False
60 |
61 |
62 | def clamp(left, value, right):
63 | return max(left, min(value, right))
64 |
65 |
66 | def insert_str(string, str_to_insert, index):
67 | return string[:index] + str_to_insert + string[index:]
68 |
69 |
70 | def truncate_str_middle(s, n):
71 | if len(s) <= n:
72 | # string is already short-enough
73 | return s
74 | # half of the size, minus the 3 .'s
75 | n_2 = int(n / 2 - 3)
76 | # whatever is left
77 | n_1 = int(n - n_2 - 3)
78 | return '{0}...{1}'.format(s[:n_1], s[-n_2:])
79 |
80 |
81 | def unzip_file(dst_folder, src_zip):
82 | with zipfile.ZipFile(src_zip) as zip_file:
83 | for member in zip_file.namelist():
84 | filename = os.path.basename(member)
85 | # skip directories
86 | if not filename:
87 | continue
88 |
89 | # copy file (taken from zipfile's extract)
90 | source = zip_file.open(member)
91 | target = open(os.path.join(dst_folder, filename), "wb")
92 | with source, target:
93 | shutil.copyfileobj(source, target)
94 |
95 |
96 | def redraw_window():
97 | kivy.core.window.Window.canvas.ask_update()
98 | EventLoop.idle()
99 |
100 |
101 | def update_progressbar(widget, text=None, value=None):
102 | if widget is not None:
103 | if value is not None:
104 | progressbar = widget.ids._progress_bar
105 | progressbar.value = min(value, 1.0)
106 | if text is not None:
107 | label = widget.ids._progress_bar_label
108 | label.text = text
109 | redraw_window()
110 | else:
111 | if text is not None:
112 | print('LOG: {} [{:.2f}%]'.format(text, round_down(value)))
113 |
114 |
115 | def beep():
116 | # print('\a')
117 | sys.stdout.write("\a")
118 |
119 |
120 | def is_desktop():
121 | if sys.platform in ('linux', 'windows', 'macosx'):
122 | return True
123 | return False
124 |
125 |
126 | def load_image(path, name):
127 | full_path = os.path.join(path, name)
128 | image = None
129 | if os.path.isfile(full_path):
130 | try:
131 | image = Image(source=full_path)
132 | except:
133 | image = Image(size=(2, 2))
134 | return image
135 |
136 |
137 | def load_image_masked(path, name, color):
138 | image = load_image(path, name)
139 | if image is not None:
140 | image = colored_mask(image, color)
141 | return image
142 |
143 |
144 | def load_file(path, name):
145 | try:
146 | with open(join(path, name)) as file:
147 | text = file.read()
148 | except FileNotFoundError:
149 | text = None
150 | return text
151 |
152 |
153 | def rmrf(directory):
154 | if directory is not None:
155 | for root, dirs, files in os.walk(directory, topdown=False):
156 | for name in files:
157 | os.remove(os.path.join(root, name))
158 | for name in dirs:
159 | os.rmdir(os.path.join(root, name))
160 | shutil.rmtree(directory, ignore_errors=True)
161 |
162 |
163 | def calculate_fit_scale(scale, size_mm, size_pixels):
164 | target = scale * size_mm[0]
165 | fit_x = target / size_pixels[0]
166 | target = scale * size_mm[1]
167 | fit_y = target / size_pixels[1]
168 | return min(fit_x, fit_y)
169 |
170 |
171 | def round_float(value):
172 | return int(math.ceil(value))
173 |
174 |
175 | def str_to_float(value):
176 | return float(value.replace(',', ''))
177 |
178 |
179 | def generate_float46(value):
180 | data = ''
181 | float_full_str = '{:0.6f}'.format(value)
182 | segments = float_full_str.split('.')
183 | for s in segments:
184 | data += '{}'.format(s)
185 | return data
186 |
187 |
188 | def generate_decfloat3(value):
189 | return '{:0.3f}'.format(value)
190 |
191 |
192 | FS_MASK: Final = '''
193 | $HEADER$
194 | void main(void) {
195 | gl_FragColor = vec4(frag_color.r, frag_color.g, frag_color.b, texture2D(texture0, tex_coord0).a);
196 | }
197 | '''
198 |
199 |
200 | def colored_mask(mask, color):
201 | image = None
202 | if mask is not None:
203 | image = Image()
204 | image.size = mask.texture_size
205 | fbo = Fbo()
206 | fbo.shader.fs = FS_MASK
207 | fbo.size = image.size
208 | with fbo:
209 | ClearColor(0, 0, 0, 0)
210 | ClearBuffers()
211 | Color(color.r, color.g, color.b, color.a)
212 | Rectangle(texture=mask.texture, size=mask.texture_size, pos=(0, 0))
213 | fbo.draw()
214 | image.texture = fbo.texture
215 | return image
216 |
217 |
218 | def bounds_to_size(bounds, verbose=False):
219 | x_bounds = bounds[0]
220 | y_bounds = bounds[1]
221 | width = abs(x_bounds[1]-x_bounds[0])
222 | height = abs(y_bounds[1]-y_bounds[0])
223 |
224 | if verbose:
225 | print('bounds_to_size:')
226 | print(' bounds: {}'.format(bounds))
227 | print(' x_bounds: {}'.format(x_bounds))
228 | print(' y_bounds: {}'.format(y_bounds))
229 | print(' width: {}'.format(width))
230 | print(' height: {}'.format(height))
231 |
232 | return (width, height)
233 |
234 |
235 | def next_power_of_2(x):
236 | return 1 if x == 0 else 2**(x - 1).bit_length()
237 |
238 |
239 | def size_to_resolution(size, pixels_per_unit, pixels_min, pixels_max):
240 | resolution = pixels_per_unit*int(max(size[0], size[1]))
241 | resolution = next_power_of_2(resolution)
242 | resolution = max(resolution, pixels_min)
243 | resolution = min(resolution, pixels_max)
244 | return resolution
245 |
246 |
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/bottom_copper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/bottom_copper.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/bottom_mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/bottom_mask.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/bottom_paste.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/bottom_paste.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/bottom_silk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/bottom_silk.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/drill_npth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/drill_npth.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/drill_pth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/drill_pth.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/edge_cuts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/edge_cuts.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/edge_cuts_mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/edge_cuts_mask.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/top_copper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/top_copper.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/top_mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/top_mask.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/top_paste.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/top_paste.png
--------------------------------------------------------------------------------
/data/demo_pcb/NEAToBOARD/top_silk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/demo_pcb/NEAToBOARD/top_silk.png
--------------------------------------------------------------------------------
/data/icons/Kofi_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/Kofi_down.png
--------------------------------------------------------------------------------
/data/icons/Kofi_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/Kofi_normal.png
--------------------------------------------------------------------------------
/data/icons/README:
--------------------------------------------------------------------------------
1 | Icons adapted from the Open Iconic set of icons, which are licensed under MIT.
2 |
3 | https://useiconic.com/
4 | https://github.com/iconic/open-iconic
5 |
--------------------------------------------------------------------------------
/data/icons/action-redo-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/action-redo-8x.png
--------------------------------------------------------------------------------
/data/icons/action-undo-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/action-undo-8x.png
--------------------------------------------------------------------------------
/data/icons/aperture-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/aperture-8x.png
--------------------------------------------------------------------------------
/data/icons/briefcase-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/briefcase-8x.png
--------------------------------------------------------------------------------
/data/icons/browser-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/browser-8x.png
--------------------------------------------------------------------------------
/data/icons/brush-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/brush-8x.png
--------------------------------------------------------------------------------
/data/icons/camera-slr-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/camera-slr-8x.png
--------------------------------------------------------------------------------
/data/icons/chevron-bottom-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/chevron-bottom-8x.png
--------------------------------------------------------------------------------
/data/icons/chevron-left-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/chevron-left-8x.png
--------------------------------------------------------------------------------
/data/icons/chevron-right-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/chevron-right-8x.png
--------------------------------------------------------------------------------
/data/icons/chevron-top-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/chevron-top-8x.png
--------------------------------------------------------------------------------
/data/icons/circle-check-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/circle-check-8x.png
--------------------------------------------------------------------------------
/data/icons/circle-x-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/circle-x-8x.png
--------------------------------------------------------------------------------
/data/icons/dashboard-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/dashboard-8x.png
--------------------------------------------------------------------------------
/data/icons/data-transfer-download-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/data-transfer-download-8x.png
--------------------------------------------------------------------------------
/data/icons/data-transfer-upload-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/data-transfer-upload-8x.png
--------------------------------------------------------------------------------
/data/icons/dial-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/dial-8x.png
--------------------------------------------------------------------------------
/data/icons/eye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/eye.png
--------------------------------------------------------------------------------
/data/icons/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/folder.png
--------------------------------------------------------------------------------
/data/icons/home-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/home-8x.png
--------------------------------------------------------------------------------
/data/icons/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/horizontal.png
--------------------------------------------------------------------------------
/data/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/icon.png
--------------------------------------------------------------------------------
/data/icons/load.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/load.png
--------------------------------------------------------------------------------
/data/icons/lock-locked-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/lock-locked-8x.png
--------------------------------------------------------------------------------
/data/icons/lock-unlocked-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/lock-unlocked-8x.png
--------------------------------------------------------------------------------
/data/icons/loop-circular-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/loop-circular-8x.png
--------------------------------------------------------------------------------
/data/icons/map-marker-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/map-marker-8x.png
--------------------------------------------------------------------------------
/data/icons/minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/minus.png
--------------------------------------------------------------------------------
/data/icons/monitor-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/monitor-8x.png
--------------------------------------------------------------------------------
/data/icons/panelize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/panelize.png
--------------------------------------------------------------------------------
/data/icons/pencil-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/pencil-8x.png
--------------------------------------------------------------------------------
/data/icons/pin-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/pin-8x.png
--------------------------------------------------------------------------------
/data/icons/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/plus.png
--------------------------------------------------------------------------------
/data/icons/puzzle-piece-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/puzzle-piece-8x.png
--------------------------------------------------------------------------------
/data/icons/rotate-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/rotate-left.png
--------------------------------------------------------------------------------
/data/icons/rotate-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/rotate-right.png
--------------------------------------------------------------------------------
/data/icons/save.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/save.png
--------------------------------------------------------------------------------
/data/icons/separator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/separator.png
--------------------------------------------------------------------------------
/data/icons/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/settings.png
--------------------------------------------------------------------------------
/data/icons/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/share.png
--------------------------------------------------------------------------------
/data/icons/star-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/star-8x.png
--------------------------------------------------------------------------------
/data/icons/target-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/target-8x.png
--------------------------------------------------------------------------------
/data/icons/task-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/task-8x.png
--------------------------------------------------------------------------------
/data/icons/thumb-up-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/thumb-up-8x.png
--------------------------------------------------------------------------------
/data/icons/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/vertical.png
--------------------------------------------------------------------------------
/data/icons/warning-8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/warning-8x.png
--------------------------------------------------------------------------------
/data/icons/zoom-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/zoom-in.png
--------------------------------------------------------------------------------
/data/icons/zoom-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/data/icons/zoom-out.png
--------------------------------------------------------------------------------
/data/mousebite_template.txt:
--------------------------------------------------------------------------------
1 | %TF.GenerationSoftware,KiCad,Pcbnew,(5.99.0-13263-g43c85771eb)*%
2 | %TF.CreationDate,2021-11-13T17:27:15-06:00*%
3 | %TF.ProjectId,mousebite,6d6f7573-6562-4697-9465-2e6b69636164,rev?*%
4 | %TF.SameCoordinates,Original*%
5 | %TF.FileFunction,Profile,NP*%
6 | %FSLAX46Y46*%
7 | G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
8 | G04 Created by KiCad (PCBNEW (5.99.0-13263-g43c85771eb)) date 2021-11-13 17:27:15*
9 | %MOMM*%
10 | %LPD*%
11 |
12 | G04 APERTURE LIST*
13 | %TA.AperFunction,Profile*%
14 | %ADD10C,0.100000*%
15 | %TD*%
16 | G04 APERTURE END LIST*
17 | D10*
18 |
19 | G01*
20 | X0000000Y0000000D02*
21 | G75*
22 | G03*
23 | X1000000Y1000000I0000000J1000000D01*
24 |
25 | G01*
26 | X1000000Y1000000D02*
27 | X1000000Y4000000D01*
28 |
29 | G01*
30 | X1000000Y4000000D02*
31 | G75*
32 | G03*
33 | X0000000Y5000000I-1000000J000000D01*
34 |
35 | G01*
36 | X5000000Y0000000D02*
37 | G75*
38 | G02*
39 | X4000000Y1000000I0000000J1000000D01*
40 |
41 | G01*
42 | X4000000Y1000000D02*
43 | X4000000Y4000000D01*
44 |
45 | G01*
46 | X4000000Y4000000D02*
47 | G75*
48 | G02*
49 | X5000000Y50000000I1000000J0000000D01*
50 |
51 | M02*
52 |
--------------------------------------------------------------------------------
/do_optimize.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # The MIT License (MIT)
5 |
6 | # Copyright 2021,2022 HalfMarble LLC
7 |
8 | # Permission is hereby granted, free of charge, to any person obtaining a copy
9 | # of this software and associated documentation files (the "Software"), to deal
10 | # in the Software without restriction, including without limitation the rights
11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | # copies of the Software, and to permit persons to whom the Software is
13 | # furnished to do so, subject to the following conditions:
14 |
15 | # The above copyright notice and this permission notice shall be included in
16 | # all copies or substantial portions of the Software.
17 |
18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 | # THE SOFTWARE.
25 |
26 |
27 | import os
28 | import sys
29 |
30 | sys.path.append('.')
31 |
32 | from hm_gerber_tool.utils import listdir
33 |
34 | from Utilities import *
35 |
36 |
37 | extensions = [
38 | '.gm1',
39 | '.gbl',
40 | '.gbo',
41 | '.gbp',
42 | '.gbs',
43 | '.gtl',
44 | '.gto',
45 | '.gtp',
46 | '.gts',
47 | ]
48 |
49 | # experimental (should hm-panelier use it?)
50 | #
51 | # optimizes gerber file by grouping instructions belonging to the same 'D0x*'
52 | # section together, ex:
53 | #
54 | # D13*
55 | # X164134517Y-35560000D03*
56 | # D12*
57 | # X174144517Y-28845000D03*
58 | # D13*
59 | # X107860000Y-31200000D03*
60 | #
61 | # becomes:
62 | #
63 | # D12*
64 | # X174144517Y-28845000D03*
65 | # D13*
66 | # X164134517Y-35560000D03*
67 | # X107860000Y-31200000D03*
68 |
69 |
70 | def optimize_gbr(path):
71 | if not os.path.isdir(path):
72 | print('ERROR: path {} does not exist'.format(path))
73 | return
74 |
75 | for filename in listdir(path, True, True):
76 | filename_ext = os.path.splitext(filename)[1].lower()
77 | if filename_ext in extensions:
78 | current_chunk = None
79 | chunks_all = [(None, [])]
80 | file = load_file(path, filename)
81 | segments = file.split("\n")
82 | for s in segments:
83 | if s == 'M02*':
84 | pass
85 | elif s.startswith('D') and s.endswith('*'):
86 | current_chunk = s
87 | new_chunk = True
88 | for chunk in chunks_all:
89 | if chunk[0] == s:
90 | new_chunk = False
91 | break
92 | if new_chunk:
93 | chunks_all.append((current_chunk, [current_chunk+'\n']))
94 | elif current_chunk is None:
95 | chunk = chunks_all[0]
96 | chunk[1].append(s+'\n')
97 | else:
98 | for chunk in chunks_all:
99 | if chunk[0] == current_chunk:
100 | chunk[1].append(s+'\n')
101 | break
102 | f = open(os.path.join(path, filename), "w")
103 | for chunk in chunks_all:
104 | f.writelines(chunk[1])
105 | f.write('M02*')
106 | f.close()
107 |
108 |
109 | optimize_gbr('/Users/gerard/Desktop/neatoboardG_unoptimized')
110 |
--------------------------------------------------------------------------------
/hm_gerber_ex/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2019 Hiroshi Murayama
5 | """
6 | Gerber Tools Extension
7 | ======================
8 | **Gerber Tools Extenstion**
9 | gerber-tools-extension is a extention package for gerber-tools.
10 | This package provide panelizing of PCB fucntion.
11 | """
12 |
13 | from hm_gerber_ex.common import read, loads, rectangle
14 | from hm_gerber_ex.composition import GerberComposition, DrillComposition
15 | #from hm_gerber_ex.dxf import DxfFile
16 |
--------------------------------------------------------------------------------
/hm_gerber_ex/am_expression.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2022 HalfMarble LLC
5 | # Copyright 2019 Hiroshi Murayama
6 |
7 | from hm_gerber_tool.utils import *
8 | from hm_gerber_tool.am_eval import OpCode
9 | from hm_gerber_tool.am_statements import *
10 |
11 |
12 | class AMExpression(object):
13 | COMMENT = 0
14 | CONSTANT = 1
15 | VARIABLE = 2
16 | OPERATOR = 3
17 |
18 | def __init__(self, kind):
19 | self.kind = kind
20 |
21 | @property
22 | def value(self):
23 | return self
24 |
25 | def optimize(self):
26 | pass
27 |
28 | def to_inch(self):
29 | return AMOperatorExpression(AMOperatorExpression.DIV, self,
30 | AMConstantExpression(MILLIMETERS_PER_INCH))
31 |
32 | def to_metric(self):
33 | return AMOperatorExpression(AMOperatorExpression.MUL, self,
34 | AMConstantExpression(MILLIMETERS_PER_INCH))
35 |
36 | def to_gerber(self, settings=None):
37 | pass
38 |
39 | def to_instructions(self):
40 | pass
41 |
42 |
43 | class AMCommentExpression(AMExpression):
44 | def __init__(self, value):
45 | super(AMCommentExpression, self).__init__(AMExpression.COMMENT)
46 | self._value = value
47 |
48 | @property
49 | def value(self):
50 | return self._value
51 |
52 | def optimize(self):
53 | return self
54 |
55 | def to_gerber(self, settings=None):
56 | print('AMCommentExpression.to_gerber(): [{}]'.format(self._value))
57 | return '%s' % self._value
58 |
59 | def to_instructions(self):
60 | return [(OpCode.PRIM, self._value)]
61 |
62 |
63 | class AMConstantExpression(AMExpression):
64 | def __init__(self, value):
65 | super(AMConstantExpression, self).__init__(AMExpression.CONSTANT)
66 | self._value = value
67 |
68 | @property
69 | def value(self):
70 | return self._value
71 |
72 | def optimize(self):
73 | return self
74 |
75 | def to_gerber(self, settings=None):
76 | if isinstance(self._value, str):
77 | return self._value
78 | else:
79 | gerber = '%.6g' % self._value
80 | return '%.6f' % self._value if 'e' in gerber else gerber
81 |
82 | def to_instructions(self):
83 | return [(OpCode.PUSH, self._value)]
84 |
85 |
86 | class AMVariableExpression(AMExpression):
87 | def __init__(self, number):
88 | super(AMVariableExpression, self).__init__(AMExpression.VARIABLE)
89 | self.number = number
90 |
91 | def optimize(self):
92 | return self
93 |
94 | def to_gerber(self, settings=None):
95 | return '$%d' % self.number
96 |
97 | def to_instructions(self):
98 | return (OpCode.LOAD, self.number)
99 |
100 |
101 | class AMOperatorExpression(AMExpression):
102 | ADD = '+'
103 | SUB = '-'
104 | MUL = 'X'
105 | DIV = '/'
106 |
107 | def __init__(self, op, lvalue, rvalue):
108 | super(AMOperatorExpression, self).__init__(AMExpression.OPERATOR)
109 | self.op = op
110 | self.lvalue = lvalue
111 | self.rvalue = rvalue
112 |
113 | def optimize(self):
114 | self.lvalue = self.lvalue.optimize()
115 | self.rvalue = self.rvalue.optimize()
116 |
117 | if isinstance(self.lvalue, AMConstantExpression) and isinstance(self.rvalue, AMConstantExpression):
118 | lvalue = float(self.lvalue.value)
119 | rvalue = float(self.rvalue.value)
120 | value = lvalue + rvalue if self.op == self.ADD else \
121 | lvalue - rvalue if self.op == self.SUB else \
122 | lvalue * rvalue if self.op == self.MUL else \
123 | lvalue / rvalue if self.op == self.DIV else None
124 | return AMConstantExpression(value)
125 | elif self.op == self.ADD:
126 | if self.rvalue.value == 0:
127 | return self.lvalue
128 | elif self.lvalue.value == 0:
129 | return self.rvalue
130 | elif self.op == self.SUB:
131 | if self.rvalue.value == 0:
132 | return self.lvalue
133 | elif self.lvalue.value == 0 and isinstance(self.rvalue, AMConstantExpression):
134 | return AMConstantExpression(-self.rvalue.value)
135 | elif self.op == self.MUL:
136 | if self.rvalue.value == 1:
137 | return self.lvalue
138 | elif self.lvalue.value == 1:
139 | return self.rvalue
140 | elif self.lvalue == 0 or self.rvalue == 0:
141 | return AMConstantExpression(0)
142 | elif self.op == self.DIV:
143 | if self.rvalue.value == 1:
144 | return self.lvalue
145 | elif self.lvalue.value == 0:
146 | return AMConstantExpression(0)
147 |
148 | return self
149 |
150 | def to_gerber(self, settings=None):
151 | return '(%s)%s(%s)' % (self.lvalue.to_gerber(settings), self.op, self.rvalue.to_gerber(settings))
152 |
153 | def to_instructions(self):
154 | for i in self.lvalue.to_instructions():
155 | yield i
156 | for i in self.rvalue.to_instructions():
157 | yield i
158 | op = OpCode.ADD if self.op == self.ADD else\
159 | OpCode.SUB if self.op == self.SUB else\
160 | OpCode.MUL if self.op == self.MUL else\
161 | OpCode.DIV
162 | yield (op, None)
163 |
164 |
165 | def eval_macro(instructions):
166 | stack = []
167 |
168 | def pop():
169 | return stack.pop()
170 |
171 | def push(op):
172 | stack.append(op)
173 |
174 | def top():
175 | return stack[-1]
176 |
177 | def empty():
178 | return len(stack) == 0
179 |
180 | for opcode, argument in instructions:
181 | if opcode == OpCode.PUSH:
182 | push(AMConstantExpression(argument))
183 | elif opcode == OpCode.LOAD:
184 | push(AMVariableExpression(argument))
185 | elif opcode == OpCode.STORE:
186 | yield (-argument, [pop()])
187 | elif opcode == OpCode.ADD:
188 | op1 = pop()
189 | op2 = pop()
190 | push(AMOperatorExpression(AMOperatorExpression.ADD, op2, op1))
191 | elif opcode == OpCode.SUB:
192 | op1 = pop()
193 | op2 = pop()
194 | push(AMOperatorExpression(AMOperatorExpression.SUB, op2, op1))
195 | elif opcode == OpCode.MUL:
196 | op1 = pop()
197 | op2 = pop()
198 | push(AMOperatorExpression(AMOperatorExpression.MUL, op2, op1))
199 | elif opcode == OpCode.DIV:
200 | op1 = pop()
201 | op2 = pop()
202 | push(AMOperatorExpression(AMOperatorExpression.DIV, op2, op1))
203 | elif opcode == OpCode.PRIM:
204 | yield (argument, stack)
205 | stack = []
206 |
--------------------------------------------------------------------------------
/hm_gerber_ex/common.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2022 HalfMarble LLC
5 | # Copyright 2019 Hiroshi Murayama
6 |
7 | import os
8 | from hm_gerber_tool.common import loads as loads_org
9 | from hm_gerber_tool.exceptions import ParseError
10 | from hm_gerber_tool.utils import detect_file_format
11 | import hm_gerber_tool.rs274x
12 | import hm_gerber_tool.ipc356
13 | import hm_gerber_ex.rs274x
14 | import hm_gerber_ex.excellon
15 | #import hm_gerber_ex.dxf
16 |
17 |
18 | def read(filename, format=None):
19 | with open(filename, 'rU') as f:
20 | data = f.read()
21 | return loads(data, filename, format=format)
22 |
23 |
24 | def loads(data, filename=None, format=None):
25 | if os.path.splitext(filename if filename else '')[1].lower() == '.dxf':
26 | return hm_gerber_ex.dxf.loads(data, filename)
27 |
28 | fmt = detect_file_format(data)
29 | if fmt == 'rs274x':
30 | file = hm_gerber_ex.rs274x.loads(data, filename=filename)
31 | return hm_gerber_ex.rs274x.GerberFile.from_gerber_file(file)
32 | elif fmt == 'excellon':
33 | return hm_gerber_ex.excellon.loads(data, filename=filename, format=format)
34 | elif fmt == 'ipc_d_356':
35 | return ipc356.loads(data, filename=filename)
36 | else:
37 | raise ParseError('Unable to detect file format')
38 |
39 |
40 | def rectangle(width, height, left=0, bottom=0, units='metric', draw_mode=None, filename=None):
41 | return hm_gerber_ex.dxf.DxfFile.rectangle(
42 | width, height, left, bottom, units, draw_mode, filename)
43 |
--------------------------------------------------------------------------------
/hm_gerber_ex/composition.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2022 HalfMarble LLC
5 | # Copyright 2019 Hiroshi Murayama
6 |
7 | import os
8 | from functools import reduce
9 |
10 | import hm_gerber_ex
11 | from hm_gerber_tool.cam import FileSettings
12 | from hm_gerber_tool.gerber_statements import EofStmt, CoordStmt, CommentStmt
13 | from hm_gerber_tool.excellon_statements import *
14 | from hm_gerber_tool.excellon import DrillSlot, DrillHit
15 | import hm_gerber_tool.rs274x
16 | import hm_gerber_tool.excellon
17 | #import hm_gerber_ex.dxf
18 |
19 |
20 | class Composition(object):
21 | def __init__(self, settings=None, comments=None):
22 | self.settings = settings
23 | self.comments = comments if comments != None else []
24 |
25 |
26 | class GerberComposition(Composition):
27 | APERTURE_ID_BIAS = 10
28 |
29 | def __init__(self, settings=None, comments=None):
30 | super(GerberComposition, self).__init__(settings, comments)
31 | self.aperture_macros = {}
32 | self.apertures = []
33 | self.drawings = []
34 |
35 | def merge(self, file):
36 | if isinstance(file, hm_gerber_ex.rs274x.GerberFile):
37 | self._merge_gerber(file)
38 | elif isinstance(file, hm_gerber_ex.dxf.DxfFile):
39 | self._merge_dxf(file)
40 | else:
41 | raise Exception('unsupported file type')
42 |
43 | def dump(self, path):
44 | def statements():
45 | for k in self.aperture_macros:
46 | yield self.aperture_macros[k]
47 | for s in self.apertures:
48 | yield s
49 | for s in self.drawings:
50 | yield s
51 | yield EofStmt()
52 | self.settings.notation = 'absolute'
53 | self.settings.zeros = 'trailing'
54 | with open(path, 'w') as f:
55 | hm_gerber_ex.rs274x.write_gerber_header(f, self.settings)
56 | for statement in statements():
57 | f.write(statement.to_gerber(self.settings) + '\n')
58 |
59 | def _merge_gerber(self, file):
60 | aperture_macro_map = {}
61 | aperture_map = {}
62 |
63 | if self.settings is not None:
64 | if self.settings.units == 'metric':
65 | file.to_metric()
66 | else:
67 | file.to_inch()
68 |
69 | for macro in file.aperture_macros:
70 | statement = file.aperture_macros[macro]
71 | name = statement.name
72 | newname = self._register_aperture_macro(statement)
73 | aperture_macro_map[name] = newname
74 |
75 | for statement in file.aperture_defs:
76 | if statement.param == 'AD':
77 | if statement.shape in aperture_macro_map:
78 | statement.shape = aperture_macro_map[statement.shape]
79 | dnum = statement.d
80 | newdnum = self._register_aperture(statement)
81 | aperture_map[dnum] = newdnum
82 |
83 | for statement in file.main_statements:
84 | if statement.type == 'APERTURE':
85 | statement.d = aperture_map[statement.d]
86 | self.drawings.append(statement)
87 |
88 | if self.settings is None:
89 | self.settings = file.context
90 |
91 | def _merge_dxf(self, file):
92 | if self.settings is not None:
93 | if self.settings.units == 'metric':
94 | file.to_metric()
95 | else:
96 | file.to_inch()
97 |
98 | file.dcode = self._register_aperture(file.aperture)
99 | self.drawings.append(file.statements)
100 |
101 | if self.settings is None:
102 | self.settings = file.settings
103 |
104 | def _register_aperture_macro(self, statement):
105 | name = statement.name
106 | newname = name
107 | offset = 0
108 | while newname in self.aperture_macros:
109 | offset += 1
110 | newname = '%s_%d' % (name, offset)
111 | statement.name = newname
112 | self.aperture_macros[newname] = statement
113 | return newname
114 |
115 | def _register_aperture(self, statement):
116 | statement.d = len(self.apertures) + self.APERTURE_ID_BIAS
117 | self.apertures.append(statement)
118 | return statement.d
119 |
120 |
121 | class DrillComposition(Composition):
122 | def __init__(self, settings=None, comments=None):
123 | super(DrillComposition, self).__init__(settings, comments)
124 | self.tools = []
125 | self.hits = []
126 | self.dxf_statements = []
127 |
128 | def merge(self, file):
129 |
130 | if isinstance(file, hm_gerber_ex.excellon.ExcellonFileEx):
131 | self._merge_excellon(file)
132 | elif isinstance(file, hm_gerber_ex.DxfFile):
133 | self._merge_dxf(file)
134 | else:
135 | raise Exception('unsupported file type')
136 |
137 | def dump(self, path):
138 | if len(self.tools) == 0:
139 | return
140 | def statements():
141 | for t in self.tools:
142 | stmt = ToolSelectionStmt(t.number)
143 | yield ToolSelectionStmt(t.number).to_excellon(self.settings)
144 | for h in self.hits:
145 | if h.tool.number == t.number:
146 | yield h.to_excellon(self.settings)
147 | for num, statement in self.dxf_statements:
148 | if num == t.number:
149 | yield statement.to_excellon(self.settings)
150 | yield EndOfProgramStmt().to_excellon()
151 |
152 | with open(path, 'w') as f:
153 | hm_gerber_ex.excellon.write_excellon_header(f, self.settings, self.tools)
154 | for statement in statements():
155 | f.write(statement + '\n')
156 |
157 | def _merge_excellon(self, file):
158 | tool_map = {}
159 |
160 | if self.settings is None:
161 | self.settings = file.settings
162 | if self.settings.units == 'metric':
163 | file.to_metric()
164 | else:
165 | file.to_inch()
166 |
167 | for tool in iter(file.tools.values()):
168 | num = tool.number
169 | tool_map[num] = self._register_tool(tool)
170 |
171 | for hit in file.hits:
172 | hit.tool = tool_map[hit.tool.number]
173 | self.hits.append(hit)
174 |
175 | def _merge_dxf(self, file):
176 | if self.settings is None:
177 | self.settings = file.settings
178 | if self.settings.units == 'metric':
179 | file.to_metric()
180 | else:
181 | file.to_inch()
182 |
183 | tool = self._register_tool(ExcellonTool(self.settings, number=1, diameter=file.width))
184 | self.dxf_statements.append((tool.number, file.statements))
185 |
186 | def _register_tool(self, tool):
187 | for existing in self.tools:
188 | if existing.equivalent(tool):
189 | return existing
190 | new_tool = ExcellonTool.from_tool(tool)
191 | new_tool.settings = self.settings
192 | def toolnums():
193 | for tool in self.tools:
194 | yield tool.number
195 | max_num = reduce(lambda x, y: x if x > y else y, toolnums(), 0)
196 | new_tool.number = max_num + 1
197 | self.tools.append(new_tool)
198 | return new_tool
199 |
--------------------------------------------------------------------------------
/hm_gerber_ex/gerber_statements.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2022 HalfMarble LLC
5 | # Copyright 2019 Hiroshi Murayama
6 |
7 | from hm_gerber_tool.gerber_statements import AMParamStmt, ADParamStmt
8 | from hm_gerber_tool.utils import inch, metric
9 | from hm_gerber_ex.am_primitive import to_primitive_defs
10 |
11 |
12 | class AMParamStmtEx(AMParamStmt):
13 |
14 | def __init__(self, param, name, macro, units):
15 | super(AMParamStmtEx, self).__init__(param, name, macro)
16 | self.units = units
17 | self.primitive_defs = list(to_primitive_defs(self.instructions))
18 |
19 | @classmethod
20 | def from_stmt(cls, stmt):
21 | return cls(stmt.param, stmt.name, stmt.macro, stmt.units)
22 |
23 | @classmethod
24 | def circle(cls, name, units):
25 | return cls(
26 | 'AM', name,
27 | '1,1,$1,0,0,0',
28 | units)
29 |
30 | @classmethod
31 | def rectangle(cls, name, units):
32 | return cls(
33 | 'AM', name,
34 | '21,1,$1,$2,0,0,0',
35 | units)
36 |
37 | @classmethod
38 | def landscape_obround(cls, name, units):
39 | return cls(
40 | 'AM', name,
41 | '$4=$2-$1*'
42 | '$5=$2/2*'
43 | '$6=$1/2*'
44 | '$7=$5-$6*'
45 | '$8=$6-$5*'
46 | '21,1,$4,$2,0,0,0*'
47 | '1,1,$2,$7,0,0*'
48 | '1,1,$2,$8,0,0',
49 | units)
50 |
51 | @classmethod
52 | def portrate_obround(cls, name, units):
53 | return cls(
54 | 'AM', name,
55 | '$4=$2-$1*'
56 | '$5=$2/2*'
57 | '$6=$1/2*'
58 | '$7=$5-$6*'
59 | '$8=$6-$5*'
60 | '21,1,$1,$4,0,0,0*'
61 | '1,1,$1,0,$7,0*'
62 | '1,1,$1,0,$8,0',
63 | units)
64 |
65 | @classmethod
66 | def polygon(cls, name, units):
67 | return cls(
68 | 'AM', name,
69 | '5,1,$2,0,0,$1,$3',
70 | units)
71 |
72 | def to_inch(self):
73 | if self.units == 'metric':
74 | self.units = 'inch'
75 | for p in self.primitive_defs:
76 | p.to_inch()
77 |
78 | def to_metric(self):
79 | if self.units == 'inch':
80 | self.units = 'metric'
81 | for p in self.primitive_defs:
82 | p.to_metric()
83 |
84 | def to_gerber(self, settings=None):
85 | def plist():
86 | for p in self.primitive_defs:
87 | yield p.to_gerber(settings)
88 |
89 | return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))
90 |
91 | def rotate(self, angle, center=None):
92 | for primitive_def in self.primitive_defs:
93 | primitive_def.rotate(angle, center)
94 |
95 |
96 | class ADParamStmtEx(ADParamStmt):
97 | GEOMETRIES = {
98 | 'C': [0, 1],
99 | 'R': [0, 1, 2],
100 | 'O': [0, 1, 2],
101 | 'P': [0, 3],
102 | }
103 |
104 | @classmethod
105 | def from_stmt(cls, stmt):
106 | modstr = ','.join([
107 | 'X'.join(['{0}'.format(x) for x in modifier])
108 | for modifier in stmt.modifiers])
109 | return cls(stmt.param, stmt.d, stmt.shape, modstr, stmt.units)
110 |
111 | def __init__(self, param, d, shape, modifiers, units):
112 | super(ADParamStmtEx, self).__init__(param, d, shape, modifiers)
113 | self.units = units
114 |
115 | def to_inch(self):
116 | if self.units == 'inch':
117 | return
118 | self.units = 'inch'
119 | if self.shape in self.GEOMETRIES:
120 | indices = self.GEOMETRIES[self.shape]
121 | self.modifiers = [tuple([
122 | inch(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \
123 | for i in range(len(self.modifiers[0]))
124 | ])]
125 |
126 | def to_metric(self):
127 | if self.units == 'metric':
128 | return
129 | self.units = 'metric'
130 | if self.shape in self.GEOMETRIES:
131 | indices = self.GEOMETRIES[self.shape]
132 | self.modifiers = [tuple([
133 | metric(self.modifiers[0][i]) if i in indices else self.modifiers[0][i] \
134 | for i in range(len(self.modifiers[0]))
135 | ])]
136 |
--------------------------------------------------------------------------------
/hm_gerber_ex/utility.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2022 HalfMarble LLC
5 | # Copyright 2019 Hiroshi Murayama
6 |
7 |
8 | from math import cos, sin, pi, sqrt
9 |
10 |
11 | def rotate(x, y, angle, center):
12 | x0 = x - center[0]
13 | y0 = y - center[1]
14 | angle = angle * pi / 180.0
15 | return (cos(angle) * x0 - sin(angle) * y0 + center[0],
16 | sin(angle) * x0 + cos(angle) * y0 + center[1])
17 |
18 |
19 | def is_equal_value(a, b, error_range=0):
20 | return (a - b) * (a - b) <= error_range * error_range
21 |
22 |
23 | def is_equal_point(a, b, error_range=0):
24 | return is_equal_value(a[0], b[0], error_range) and \
25 | is_equal_value(a[1], b[1], error_range)
26 |
27 |
28 | def normalize_vec2d(vec):
29 | length = sqrt(vec[0] * vec[0] + vec[1] * vec[1])
30 | return (vec[0] / length, vec[1] / length)
31 |
32 |
33 | def dot_vec2d(vec1, vec2):
34 | return vec1[0] * vec2[0] + vec1[1] * vec2[1]
--------------------------------------------------------------------------------
/hm_gerber_tool/__init__.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2013-2014 Paulo Henrique Silva
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | from .common import read, loads
19 | from .layers import load_layer, load_layer_data
20 | from .pcb import PCB
21 |
--------------------------------------------------------------------------------
/hm_gerber_tool/__main__.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2013-2014 Paulo Henrique Silva
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | # License for the specific language governing permissions and limitations under
16 | # the License.
17 |
18 | import os
19 | import argparse
20 | from .render import available_renderers
21 | from .render import theme
22 | from .pcb import PCB
23 | from . import load_layer
24 |
25 |
26 | def main():
27 | parser = argparse.ArgumentParser(
28 | description='Render gerber files to image',
29 | prog='gerber-render'
30 | )
31 | parser.add_argument(
32 | 'filenames', metavar='FILENAME', type=str, nargs='+',
33 | help='Gerber files to render. If a directory is provided, it should '
34 | 'be provided alone and should contain the gerber files for a '
35 | 'single PCB.'
36 | )
37 | parser.add_argument(
38 | '--outfile', '-o', type=str, nargs='?', default='out',
39 | help="Output Filename (extension will be added automatically)"
40 | )
41 | parser.add_argument(
42 | '--backend', '-b', choices=available_renderers.keys(), default='cairo',
43 | help='Choose the backend to use to generate the output.'
44 | )
45 | parser.add_argument(
46 | '--theme', '-t', choices=theme.THEMES.keys(), default='default',
47 | help='Select render theme.'
48 | )
49 | parser.add_argument(
50 | '--width', type=int, default=1920, help='Maximum width.'
51 | )
52 | parser.add_argument(
53 | '--height', type=int, default=1080, help='Maximum height.'
54 | )
55 | parser.add_argument(
56 | '--verbose', '-v', action='store_true', default=False,
57 | help='Increase verbosity of the output.'
58 | )
59 | # parser.add_argument(
60 | # '--quick', '-q', action='store_true', default=False,
61 | # help='Skip longer running rendering steps to produce lower quality'
62 | # ' output faster. This only has an effect for the freecad backend.'
63 | # )
64 | # parser.add_argument(
65 | # '--nox', action='store_true', default=False,
66 | # help='Run without using any GUI elements. This may produce suboptimal'
67 | # 'output. For the freecad backend, colors, transparancy, and '
68 | # 'visibility cannot be set without a GUI instance.'
69 | # )
70 |
71 | args = parser.parse_args()
72 |
73 | renderer = available_renderers[args.backend]()
74 |
75 | if args.backend in ['cairo', ]:
76 | outext = 'png'
77 | else:
78 | outext = None
79 |
80 | if os.path.exists(args.filenames[0]) and os.path.isdir(args.filenames[0]):
81 | directory = args.filenames[0]
82 | pcb = PCB.from_directory(directory)
83 |
84 | if args.backend in ['cairo', ]:
85 | top = pcb.top_layers
86 | bottom = pcb.bottom_layers
87 | copper = pcb.copper_layers
88 |
89 | outline = pcb.outline_layer
90 | if outline:
91 | top = [outline] + top
92 | bottom = [outline] + bottom
93 | copper = [outline] + copper + pcb.drill_layers
94 |
95 | renderer.render_layers(
96 | layers=top, theme=theme.THEMES[args.theme],
97 | max_height=args.height, max_width=args.width,
98 | filename='{0}.top.{1}'.format(args.outfile, outext)
99 | )
100 | renderer.render_layers(
101 | layers=bottom, theme=theme.THEMES[args.theme],
102 | max_height=args.height, max_width=args.width,
103 | filename='{0}.bottom.{1}'.format(args.outfile, outext)
104 | )
105 | renderer.render_layers(
106 | layers=copper, theme=theme.THEMES['Transparent Multilayer'],
107 | max_height=args.height, max_width=args.width,
108 | filename='{0}.copper.{1}'.format(args.outfile, outext))
109 | else:
110 | pass
111 | else:
112 | filenames = args.filenames
113 | for filename in filenames:
114 | layer = load_layer(filename)
115 | settings = theme.THEMES[args.theme].get(layer.layer_class, None)
116 | renderer.render_layer(layer, settings=settings)
117 | renderer.dump(filename='{0}.{1}'.format(args.outfile, outext))
118 |
119 |
120 | if __name__ == '__main__':
121 | main()
122 |
123 |
--------------------------------------------------------------------------------
/hm_gerber_tool/am_eval.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # copyright 2014 Hamilton Kibbe
5 | # copyright 2014 Paulo Henrique Silva
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 |
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | """ This module provides RS-274-X AM macro evaluation.
19 | """
20 |
21 |
22 | class OpCode:
23 | PUSH = 1
24 | LOAD = 2
25 | STORE = 3
26 | ADD = 4
27 | SUB = 5
28 | MUL = 6
29 | DIV = 7
30 | PRIM = 8
31 |
32 | @staticmethod
33 | def str(opcode):
34 | if opcode == OpCode.PUSH:
35 | return "OPCODE_PUSH"
36 | elif opcode == OpCode.LOAD:
37 | return "OPCODE_LOAD"
38 | elif opcode == OpCode.STORE:
39 | return "OPCODE_STORE"
40 | elif opcode == OpCode.ADD:
41 | return "OPCODE_ADD"
42 | elif opcode == OpCode.SUB:
43 | return "OPCODE_SUB"
44 | elif opcode == OpCode.MUL:
45 | return "OPCODE_MUL"
46 | elif opcode == OpCode.DIV:
47 | return "OPCODE_DIV"
48 | elif opcode == OpCode.PRIM:
49 | return "OPCODE_PRIM"
50 | else:
51 | return "UNKNOWN"
52 |
53 |
54 | def eval_macro(instructions, parameters={}):
55 |
56 | if not isinstance(parameters, type({})):
57 | p = {}
58 | for i, val in enumerate(parameters):
59 | p[i + 1] = val
60 | parameters = p
61 |
62 | stack = []
63 |
64 | def pop():
65 | return stack.pop()
66 |
67 | def push(op):
68 | stack.append(op)
69 |
70 | def top():
71 | return stack[-1]
72 |
73 | def empty():
74 | return len(stack) == 0
75 |
76 | for opcode, argument in instructions:
77 | if opcode == OpCode.PUSH:
78 | push(argument)
79 |
80 | elif opcode == OpCode.LOAD:
81 | push(parameters.get(argument, 0))
82 |
83 | elif opcode == OpCode.STORE:
84 | parameters[argument] = pop()
85 |
86 | elif opcode == OpCode.ADD:
87 | op1 = pop()
88 | op2 = pop()
89 | push(op2 + op1)
90 |
91 | elif opcode == OpCode.SUB:
92 | op1 = pop()
93 | op2 = pop()
94 | push(op2 - op1)
95 |
96 | elif opcode == OpCode.MUL:
97 | op1 = pop()
98 | op2 = pop()
99 | push(op2 * op1)
100 |
101 | elif opcode == OpCode.DIV:
102 | op1 = pop()
103 | op2 = pop()
104 | push(op2 / op1)
105 |
106 | elif opcode == OpCode.PRIM:
107 | yield "%d,%s" % (argument, ",".join([str(x) for x in stack]))
108 | stack = []
109 |
--------------------------------------------------------------------------------
/hm_gerber_tool/am_read.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # copyright 2014 Hamilton Kibbe
5 | # copyright 2014 Paulo Henrique Silva
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 |
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | """ This module provides RS-274-X AM macro modifiers parsing.
19 | """
20 |
21 | from .am_eval import OpCode, eval_macro
22 |
23 | import string
24 |
25 |
26 | class Token:
27 | ADD = "+"
28 | SUB = "-"
29 | # compatibility as many gerber writes do use non compliant X
30 | MULT = ("x", "X")
31 | DIV = "/"
32 | OPERATORS = (ADD, SUB, MULT[0], MULT[1], DIV)
33 | LEFT_PARENS = "("
34 | RIGHT_PARENS = ")"
35 | EQUALS = "="
36 | EOF = "EOF"
37 |
38 |
39 | def token_to_opcode(token):
40 | if token == Token.ADD:
41 | return OpCode.ADD
42 | elif token == Token.SUB:
43 | return OpCode.SUB
44 | elif token in Token.MULT:
45 | return OpCode.MUL
46 | elif token == Token.DIV:
47 | return OpCode.DIV
48 | else:
49 | return None
50 |
51 |
52 | def precedence(token):
53 | if token == Token.ADD or token == Token.SUB:
54 | return 1
55 | elif token in Token.MULT or token == Token.DIV:
56 | return 2
57 | else:
58 | return 0
59 |
60 |
61 | def is_op(token):
62 | return token in Token.OPERATORS
63 |
64 |
65 | class Scanner:
66 |
67 | def __init__(self, s):
68 | self.buff = s
69 | self.n = 0
70 |
71 | def eof(self):
72 | return self.n == len(self.buff)
73 |
74 | def peek(self):
75 | if not self.eof():
76 | return self.buff[self.n]
77 |
78 | return Token.EOF
79 |
80 | def ungetc(self):
81 | if self.n > 0:
82 | self.n -= 1
83 |
84 | def getc(self):
85 | if self.eof():
86 | return ""
87 |
88 | c = self.buff[self.n]
89 | self.n += 1
90 | return c
91 |
92 | def readint(self):
93 | n = ""
94 | while not self.eof() and (self.peek() in string.digits):
95 | n += self.getc()
96 | return int(n)
97 |
98 | def readfloat(self):
99 | n = ""
100 | while not self.eof() and (self.peek() in string.digits or self.peek() == "."):
101 | n += self.getc()
102 | # weird case where zero is ommited inthe last modifider, like in ',0.'
103 | if n == ".":
104 | return 0
105 | return float(n)
106 |
107 | def readstr(self, end="*"):
108 | s = ""
109 | while not self.eof() and self.peek() != end:
110 | s += self.getc()
111 | return s.strip()
112 |
113 |
114 | def print_instructions(instructions):
115 | for opcode, argument in instructions:
116 | print("%s %s" % (OpCode.str(opcode),
117 | str(argument) if argument is not None else ""))
118 |
119 |
120 | def read_macro(macro):
121 | instructions = []
122 |
123 | for block in macro.split("*"):
124 |
125 | is_primitive = False
126 | is_equation = False
127 |
128 | found_equation_left_side = False
129 | found_primitive_code = False
130 |
131 | equation_left_side = 0
132 | primitive_code = 0
133 |
134 | unary_minus_allowed = False
135 | unary_minus = False
136 |
137 | if Token.EQUALS in block:
138 | is_equation = True
139 | else:
140 | is_primitive = True
141 |
142 | scanner = Scanner(block)
143 |
144 | # inlined here for compactness and convenience
145 | op_stack = []
146 |
147 | def pop():
148 | return op_stack.pop()
149 |
150 | def push(op):
151 | op_stack.append(op)
152 |
153 | def top():
154 | return op_stack[-1]
155 |
156 | def empty():
157 | return len(op_stack) == 0
158 |
159 | while not scanner.eof():
160 |
161 | c = scanner.getc()
162 |
163 | if c == ",":
164 | found_primitive_code = True
165 | # add all instructions on the stack to finish last modifier
166 | while not empty():
167 | instructions.append((token_to_opcode(pop()), None))
168 | unary_minus_allowed = True
169 |
170 | elif c in Token.OPERATORS:
171 | if c == Token.SUB and unary_minus_allowed:
172 | unary_minus = True
173 | unary_minus_allowed = False
174 | continue
175 | while not empty() and is_op(top()) and precedence(top()) >= precedence(c):
176 | i = (token_to_opcode(pop()), None)
177 | instructions.append(i)
178 | push(c)
179 |
180 | elif c == Token.LEFT_PARENS:
181 | push(c)
182 |
183 | elif c == Token.RIGHT_PARENS:
184 | while not empty() and top() != Token.LEFT_PARENS:
185 | instructions.append((token_to_opcode(pop()), None))
186 | if empty():
187 | raise ValueError("unbalanced parentheses")
188 | # discard "("
189 | pop()
190 |
191 | elif c.startswith("$"):
192 | n = scanner.readint()
193 | if is_equation and not found_equation_left_side:
194 | equation_left_side = n
195 | else:
196 | instructions.append((OpCode.LOAD, n))
197 |
198 | elif c == Token.EQUALS:
199 | found_equation_left_side = True
200 |
201 | elif c == "0":
202 | if is_primitive and not found_primitive_code:
203 | instructions.append((OpCode.PUSH, scanner.readstr("*")))
204 | found_primitive_code = True
205 | else:
206 | # decimal or integer disambiguation
207 | if scanner.peek() not in '.' or scanner.peek() == Token.EOF:
208 | instructions.append((OpCode.PUSH, 0))
209 |
210 | elif c in "123456789.":
211 | scanner.ungetc()
212 | if is_primitive and not found_primitive_code:
213 | primitive_code = scanner.readint()
214 | else:
215 | n = scanner.readfloat()
216 | if unary_minus:
217 | unary_minus = False
218 | n *= -1
219 | instructions.append((OpCode.PUSH, n))
220 | else:
221 | print(' Scanner.read_macro() whitespace or unknown char: {}'.format(c))
222 | # whitespace or unknown char
223 | pass
224 |
225 | # add all instructions on the stack to finish last modifier (if any)
226 | while not empty():
227 | instructions.append((token_to_opcode(pop()), None))
228 |
229 | # at end, we either have a primitive or a equation
230 | if is_primitive and found_primitive_code:
231 | instructions.append((OpCode.PRIM, primitive_code))
232 |
233 | if is_equation:
234 | instructions.append((OpCode.STORE, equation_left_side))
235 |
236 | return instructions
237 |
238 |
239 | if __name__ == '__main__':
240 | import sys
241 |
242 | instructions = read_macro(sys.argv[1])
243 |
244 | print("instructions:")
245 | print_instructions(instructions)
246 |
247 | print("eval:")
248 | for primitive in eval_macro(instructions):
249 | print(primitive)
250 |
--------------------------------------------------------------------------------
/hm_gerber_tool/cam.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # copyright 2014 Hamilton Kibbe
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | """
18 | CAM File
19 | ============
20 | **AM file classes**
21 |
22 | This module provides common base classes for Excellon/Gerber CNC files
23 | """
24 |
25 |
26 | class FileSettings(object):
27 | """ CAM File Settings
28 |
29 | Provides a common representation of gerber/excellon file settings
30 |
31 | Parameters
32 | ----------
33 | notation: string
34 | notation format. either 'absolute' or 'incremental'
35 |
36 | units : string
37 | Measurement units. 'inch' or 'metric'
38 |
39 | zero_suppression: string
40 | 'leading' to suppress leading zeros, 'trailing' to suppress trailing zeros.
41 | This is the convention used in Gerber files.
42 |
43 | format : tuple (int, int)
44 | Decimal format
45 |
46 | zeros : string
47 | 'leading' to include leading zeros, 'trailing to include trailing zeros.
48 | This is the convention used in Excellon files
49 | 'decimal' i.e. '2.50'
50 |
51 | Notes
52 | -----
53 | Either `zeros` or `zero_suppression` should be specified, there is no need to
54 | specify both. `zero_suppression` will take on the opposite value of `zeros`
55 | and vice versa
56 | """
57 |
58 | def __init__(self, notation='absolute', units='metric',
59 | zero_suppression=None, format=(2, 6), zeros=None,
60 | angle_units='degrees'):
61 | if notation not in ['absolute', 'incremental']:
62 | raise ValueError('Notation must be either absolute or incremental')
63 | self.notation = notation
64 |
65 | if units not in ['inch', 'metric']:
66 | raise ValueError('Units must be either inch or metric')
67 | self.units = units
68 |
69 | if zeros is not None:
70 | self.zeros = zeros
71 | else:
72 | self._zeros = ''
73 |
74 | if zero_suppression is None and zeros is None:
75 | self.zero_suppression = 'trailing'
76 | elif zero_suppression == zeros:
77 | raise ValueError('Zeros and Zero Suppression must be different. Best practice is to specify only one.')
78 | elif zero_suppression is not None:
79 | if zero_suppression not in ['leading', 'trailing']:
80 | # This is a common problem in Eagle files, so just suppress it
81 | self.zero_suppression = 'leading'
82 | else:
83 | self.zero_suppression = zero_suppression
84 | elif zeros is not None:
85 | if zeros not in ['leading', 'trailing', 'decimal']:
86 | raise ValueError('Zeros must be either leading or trailling or decimal')
87 | self.zeros = zeros
88 |
89 | if len(format) != 2:
90 | raise ValueError('Format must be a tuple(n=2) of integers')
91 | self.format = format
92 |
93 | if angle_units not in ('degrees', 'radians'):
94 | raise ValueError('Angle units may be degrees or radians')
95 | self.angle_units = angle_units
96 |
97 | @property
98 | def zero_suppression(self):
99 | return self._zero_suppression
100 |
101 | @zero_suppression.setter
102 | def zero_suppression(self, value):
103 | self._zero_suppression = value
104 | if self._zeros != 'decimal':
105 | self._zeros = 'leading' if value == 'trailing' else 'trailing'
106 |
107 | @property
108 | def zeros(self):
109 | return self._zeros
110 |
111 | @zeros.setter
112 | def zeros(self, value):
113 | self._zeros = value
114 | if self._zeros != 'decimal':
115 | self._zero_suppression = 'leading' if value == 'trailing' else 'trailing'
116 |
117 | def __getitem__(self, key):
118 | if key == 'notation':
119 | return self.notation
120 | elif key == 'units':
121 | return self.units
122 | elif key == 'zero_suppression':
123 | return self.zero_suppression
124 | elif key == 'zeros':
125 | return self.zeros
126 | elif key == 'format':
127 | return self.format
128 | elif key == 'angle_units':
129 | return self.angle_units
130 | else:
131 | raise KeyError()
132 |
133 | def __setitem__(self, key, value):
134 | if key == 'notation':
135 | if value not in ['absolute', 'incremental']:
136 | raise ValueError('Notation must be either absolute or incremental')
137 | self.notation = value
138 | elif key == 'units':
139 | if value not in ['inch', 'metric']:
140 | raise ValueError('Units must be either inch or metric')
141 | self.units = value
142 |
143 | elif key == 'zero_suppression':
144 | if value not in ['leading', 'trailing']:
145 | raise ValueError('Zero suppression must be either leading or trailling')
146 | self.zero_suppression = value
147 |
148 | elif key == 'zeros':
149 | if value not in ['leading', 'trailing', 'decimal']:
150 | raise ValueError('Zeros must be either leading or trailling or decimal')
151 | self.zeros = value
152 |
153 | elif key == 'format':
154 | if len(value) != 2:
155 | raise ValueError('Format must be a tuple(n=2) of integers')
156 | self.format = value
157 |
158 | elif key == 'angle_units':
159 | if value not in ('degrees', 'radians'):
160 | raise ValueError('Angle units may be degrees or radians')
161 | self.angle_units = value
162 |
163 | else:
164 | raise KeyError('%s is not a valid key' % key)
165 |
166 | def __eq__(self, other):
167 | return (self.notation == other.notation and
168 | self.units == other.units and
169 | self.zeros == other.zeros and
170 | self.zero_suppression == other.zero_suppression and
171 | self.format == other.format and
172 | self.angle_units == other.angle_units)
173 |
174 | def __str__(self):
175 | return ('' %
176 | (self.units, self.notation, self.zeros, self.zero_suppression, self.format, self.angle_units))
177 |
178 |
179 | class CamFile(object):
180 | """ Base class for Gerber/Excellon files.
181 |
182 | Provides a common set of settings parameters.
183 |
184 | Parameters
185 | ----------
186 | settings : FileSettings
187 | The current file configuration.
188 |
189 | primitives : iterable
190 | List of primitives in the file.
191 |
192 | filename : string
193 | Name of the file that this CamFile represents.
194 |
195 | layer_name : string
196 | Name of the PCB layer that the file represents
197 |
198 | Attributes
199 | ----------
200 | settings : FileSettings
201 | File settings as a FileSettings object
202 |
203 | notation : string
204 | File notation setting. May be either 'absolute' or 'incremental'
205 |
206 | units : string
207 | File units setting. May be 'inch' or 'metric'
208 |
209 | zero_suppression : string
210 | File zero-suppression setting. May be either 'leading' or 'trailling'
211 |
212 | format : tuple (, )
213 | File decimal representation format as a tuple of (integer digits,
214 | decimal digits)
215 | """
216 |
217 | def __init__(self, statements=None, settings=None, primitives=None,
218 | filename=None, layer_name=None):
219 | if settings is not None:
220 | self.notation = settings['notation']
221 | self.units = settings['units']
222 | self.zero_suppression = settings['zero_suppression']
223 | self.zeros = settings['zeros']
224 | self.format = settings['format']
225 | else:
226 | self.notation = 'absolute'
227 | self.units = 'metric'
228 | self.zero_suppression = 'trailing'
229 | self.zeros = 'leading'
230 | self.format = (2, 4)
231 | self.statements = statements if statements is not None else []
232 | if primitives is not None:
233 | self.primitives = primitives
234 | self.filename = filename
235 | self.layer_name = layer_name
236 |
237 | @property
238 | def settings(self):
239 | """ File settings
240 |
241 | Returns
242 | -------
243 | settings : FileSettings (dict-like)
244 | A FileSettings object with the specified configuration.
245 | """
246 | return FileSettings(self.notation, self.units, self.zero_suppression,
247 | self.format)
248 |
249 | @property
250 | def bounds(self):
251 | """ File boundaries
252 | """
253 | pass
254 |
255 | @property
256 | def bounding_box(self):
257 | pass
258 |
259 | @property
260 | def is_metric(self):
261 | return (self.units == 'metric')
262 |
263 | def to_inch(self):
264 | pass
265 |
266 | def to_metric(self):
267 | pass
268 |
269 | def render(self, ctx=None, invert=False, filename=None):
270 | """ Generate image of layer.
271 |
272 | Parameters
273 | ----------
274 | ctx : :class:`GerberContext`
275 | GerberContext subclass used for rendering the image
276 |
277 | filename : string
278 | If provided, save the rendered image to `filename`
279 | """
280 | if ctx is None:
281 | from .render import GerberCairoContext
282 | ctx = GerberCairoContext()
283 | ctx.set_bounds(self.bounding_box)
284 | ctx.paint_background()
285 | ctx.invert = invert
286 | ctx.new_render_layer()
287 | for p in self.primitives:
288 | ctx.render(p)
289 | ctx.flatten()
290 |
291 | if filename is not None:
292 | ctx.dump(filename)
293 |
--------------------------------------------------------------------------------
/hm_gerber_tool/common.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2014 Hamilton Kibbe
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | from . import rs274x
19 | from . import excellon
20 | from . import ipc356
21 | from .exceptions import ParseError
22 | from .utils import detect_file_format
23 |
24 |
25 | def read(filename):
26 | """ Read a gerber or excellon file and return a representative object.
27 |
28 | Parameters
29 | ----------
30 | filename : string
31 | Filename of the file to read.
32 |
33 | Returns
34 | -------
35 | file : CncFile subclass
36 | CncFile object representing the file, either GerberFile, ExcellonFile,
37 | or IPCNetlist. Returns None if file is not of the proper type.
38 | """
39 | with open(filename, 'rU') as f:
40 | try:
41 | data = f.read()
42 | return loads(data, filename)
43 | except:
44 | return None
45 |
46 |
47 | def loads(data, filename=None):
48 | """ Read gerber or excellon file contents from a string and return a
49 | representative object.
50 |
51 | Parameters
52 | ----------
53 | data : string
54 | Source file contents as a string.
55 |
56 | filename : string, optional
57 | String containing the filename of the data source.
58 |
59 | Returns
60 | -------
61 | file : CncFile subclass
62 | CncFile object representing the data, either GerberFile, ExcellonFile,
63 | or IPCNetlist. Returns None if data is not of the proper type.
64 | """
65 |
66 | fmt = detect_file_format(data)
67 | if fmt == 'rs274x':
68 | return rs274x.loads(data, filename=filename)
69 | elif fmt == 'excellon':
70 | return excellon.loads(data, filename=filename)
71 | elif fmt == 'ipc_d_356':
72 | return ipc356.loads(data, filename=filename)
73 | else:
74 | raise ParseError('Unable to detect file format')
75 |
--------------------------------------------------------------------------------
/hm_gerber_tool/excellon_report/excellon_drr.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2015 Garret Fick
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | """
19 | Excellon DRR File module
20 | ====================
21 | **Excellon file classes**
22 |
23 | Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
24 | """
25 |
26 |
--------------------------------------------------------------------------------
/hm_gerber_tool/excellon_settings.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from argparse import PARSER
4 |
5 | # Copyright 2015 Garret Fick
6 |
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 |
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 |
19 | """
20 | Excellon Settings Definition File module
21 | ====================
22 | **Excellon file classes**
23 |
24 | This module provides Excellon file classes and parsing utilities
25 | """
26 |
27 | import re
28 | try:
29 | from cStringIO import StringIO
30 | except(ImportError):
31 | from io import StringIO
32 |
33 | from .cam import FileSettings
34 |
35 | def loads(data):
36 | """ Read settings file information and return an FileSettings
37 | Parameters
38 | ----------
39 | data : string
40 | string containing Excellon settings file contents
41 |
42 | Returns
43 | -------
44 | file settings: FileSettings
45 |
46 | """
47 |
48 | return ExcellonSettingsParser().parse_raw(data)
49 |
50 | def map_coordinates(value):
51 | if value == 'ABSOLUTE':
52 | return 'absolute'
53 | return 'relative'
54 |
55 | def map_units(value):
56 | if value == 'ENGLISH':
57 | return 'inch'
58 | return 'metric'
59 |
60 | def map_boolean(value):
61 | return value == 'YES'
62 |
63 | SETTINGS_KEYS = {
64 | 'INTEGER-PLACES': (int, 'format-int'),
65 | 'DECIMAL-PLACES': (int, 'format-dec'),
66 | 'COORDINATES': (map_coordinates, 'notation'),
67 | 'OUTPUT-UNITS': (map_units, 'units'),
68 | }
69 |
70 | class ExcellonSettingsParser(object):
71 | """Excellon Settings PARSER
72 |
73 | Parameters
74 | ----------
75 | None
76 | """
77 |
78 | def __init__(self):
79 | self.values = {}
80 | self.settings = None
81 |
82 | def parse_raw(self, data):
83 | for line in StringIO(data):
84 | self._parse(line.strip())
85 |
86 | # Create the FileSettings object
87 | self.settings = FileSettings(
88 | notation=self.values['notation'],
89 | units=self.values['units'],
90 | format=(self.values['format-int'], self.values['format-dec'])
91 | )
92 |
93 | return self.settings
94 |
95 | def _parse(self, line):
96 |
97 | line_items = line.split()
98 | if len(line_items) == 2:
99 |
100 | item_type_info = SETTINGS_KEYS.get(line_items[0])
101 | if item_type_info:
102 | # Convert the value to the expected type
103 | item_value = item_type_info[0](line_items[1])
104 |
105 | self.values[item_type_info[1]] = item_value
--------------------------------------------------------------------------------
/hm_gerber_tool/excellon_tool.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2015 Garret Fick
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | """
19 | Excellon Tool Definition File module
20 | ====================
21 | **Excellon file classes**
22 |
23 | This module provides Excellon file classes and parsing utilities
24 | """
25 |
26 | import re
27 | try:
28 | from cStringIO import StringIO
29 | except(ImportError):
30 | from io import StringIO
31 |
32 | from .excellon_statements import ExcellonTool
33 |
34 | def loads(data, settings=None):
35 | """ Read tool file information and return a map of tools
36 | Parameters
37 | ----------
38 | data : string
39 | string containing Excellon Tool Definition file contents
40 |
41 | Returns
42 | -------
43 | dict tool name: ExcellonTool
44 |
45 | """
46 | return ExcellonToolDefinitionParser(settings).parse_raw(data)
47 |
48 | class ExcellonToolDefinitionParser(object):
49 | """ Excellon File Parser
50 |
51 | Parameters
52 | ----------
53 | None
54 | """
55 |
56 | allegro_tool = re.compile(r'(?P[0-9/.]+)\s+(?PP|N)\s+T(?P[0-9]{2})\s+(?P[0-9/.]+)\s+(?P[0-9/.]+)')
57 | allegro_comment_mils = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
58 | allegro2_comment_mils = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MILS Quantity = [0-9]+')
59 | allegro_comment_mm = re.compile('Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
60 | allegro2_comment_mm = re.compile('T(?P[0-9]{1,2}) Holesize (?P[0-9]{1,2})\. = (?P[0-9/.]+) Tolerance = \+(?P[0-9/.]+)/-(?P[0-9/.]+) (?P(PLATED)|(NON_PLATED)|(OPTIONAL)) MM Quantity = [0-9]+')
61 |
62 | matchers = [
63 | (allegro_tool, 'mils'),
64 | (allegro_comment_mils, 'mils'),
65 | (allegro2_comment_mils, 'mils'),
66 | (allegro_comment_mm, 'mm'),
67 | (allegro2_comment_mm, 'mm'),
68 | ]
69 |
70 | def __init__(self, settings=None):
71 | self.tools = {}
72 | self.settings = settings
73 |
74 | def parse_raw(self, data):
75 | for line in StringIO(data):
76 | self._parse(line.strip())
77 |
78 | return self.tools
79 |
80 | def _parse(self, line):
81 |
82 | for matcher in ExcellonToolDefinitionParser.matchers:
83 | m = matcher[0].match(line)
84 | if m:
85 | unit = matcher[1]
86 |
87 | size = float(m.group('size'))
88 | platedstr = m.group('plated')
89 | toolid = int(m.group('toolid'))
90 | xtol = float(m.group('xtol'))
91 | ytol = float(m.group('ytol'))
92 |
93 | size = self._convert_length(size, unit)
94 | xtol = self._convert_length(xtol, unit)
95 | ytol = self._convert_length(ytol, unit)
96 |
97 | if platedstr == 'PLATED':
98 | plated = ExcellonTool.PLATED_YES
99 | elif platedstr == 'NON_PLATED':
100 | plated = ExcellonTool.PLATED_NO
101 | elif platedstr == 'OPTIONAL':
102 | plated = ExcellonTool.PLATED_OPTIONAL
103 | else:
104 | plated = ExcellonTool.PLATED_UNKNOWN
105 |
106 | tool = ExcellonTool(None, number=toolid, diameter=size,
107 | plated=plated)
108 |
109 | self.tools[tool.number] = tool
110 |
111 | break
112 |
113 | def _convert_length(self, value, unit):
114 |
115 | # Convert the value to mm
116 | if unit == 'mils':
117 | value /= 39.3700787402
118 |
119 | # Now convert to the settings unit
120 | if self.settings.units == 'inch':
121 | return value / 25.4
122 | else:
123 | # Already in mm
124 | return value
125 |
126 | def loads_rep(data, settings=None):
127 | """ Read tool report information generated by PADS and return a map of tools
128 | Parameters
129 | ----------
130 | data : string
131 | string containing Excellon Report file contents
132 |
133 | Returns
134 | -------
135 | dict tool name: ExcellonTool
136 |
137 | """
138 | return ExcellonReportParser(settings).parse_raw(data)
139 |
140 | class ExcellonReportParser(object):
141 |
142 | # We sometimes get files with different encoding, so we can't actually
143 | # match the text - the best we can do it detect the table header
144 | header = re.compile(r'====\s+====\s+====\s+====\s+=====\s+===')
145 |
146 | def __init__(self, settings=None):
147 | self.tools = {}
148 | self.settings = settings
149 |
150 | self.found_header = False
151 |
152 | def parse_raw(self, data):
153 | for line in StringIO(data):
154 | self._parse(line.strip())
155 |
156 | return self.tools
157 |
158 | def _parse(self, line):
159 |
160 | # skip empty lines and "comments"
161 | if not line.strip():
162 | return
163 |
164 | if not self.found_header:
165 | # Try to find the heaader, since we need that to be sure we
166 | # understand the contents correctly.
167 | if ExcellonReportParser.header.match(line):
168 | self.found_header = True
169 |
170 | elif line[0] != '=':
171 | # Already found the header, so we know to to map the contents
172 | parts = line.split()
173 | if len(parts) == 6:
174 | toolid = int(parts[0])
175 | size = float(parts[1])
176 | if parts[2] == 'x':
177 | plated = ExcellonTool.PLATED_YES
178 | elif parts[2] == '-':
179 | plated = ExcellonTool.PLATED_NO
180 | else:
181 | plated = ExcellonTool.PLATED_UNKNOWN
182 | feedrate = int(parts[3])
183 | speed = int(parts[4])
184 | qty = int(parts[5])
185 |
186 | tool = ExcellonTool(None, number=toolid, diameter=size,
187 | plated=plated, feed_rate=feedrate,
188 | rpm=speed)
189 |
190 | self.tools[tool.number] = tool
191 |
--------------------------------------------------------------------------------
/hm_gerber_tool/exceptions.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2015 Hamilton Kibbe
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 |
19 | class ParseError(Exception):
20 | pass
21 |
22 |
23 | class GerberParseError(ParseError):
24 | pass
25 |
26 |
27 | class ExcellonParseError(ParseError):
28 | pass
29 |
30 |
31 | class ExcellonFileError(IOError):
32 | pass
33 |
34 |
35 | class GerberFileError(IOError):
36 | pass
37 |
--------------------------------------------------------------------------------
/hm_gerber_tool/ncparam/allegro.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2015 Garret Fick
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | """
19 | Allegro File module
20 | ====================
21 | **Excellon file classes**
22 |
23 | Extra parsers for allegro misc files that can be useful when the Excellon file doesn't contain parameter information
24 | """
25 |
26 |
--------------------------------------------------------------------------------
/hm_gerber_tool/operations.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # copyright 2015 Hamilton Kibbe
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | """
18 | CAM File Operations
19 | ===================
20 | **Transformations and other operations performed on Gerber and Excellon files**
21 |
22 | """
23 | import copy
24 |
25 |
26 | def to_inch(cam_file):
27 | """ Convert Gerber or Excellon file units to imperial
28 |
29 | Parameters
30 | ----------
31 | cam_file : :class:`gerber.cam.CamFile` subclass
32 | Gerber or Excellon file to convert
33 |
34 | Returns
35 | -------
36 | cam_file : :class:`gerber.cam.CamFile` subclass
37 | A deep copy of the source file with units converted to imperial.
38 | """
39 | cam_file = copy.deepcopy(cam_file)
40 | cam_file.to_inch()
41 | return cam_file
42 |
43 |
44 | def to_metric(cam_file):
45 | """ Convert Gerber or Excellon file units to metric
46 |
47 | Parameters
48 | ----------
49 | cam_file : :class:`gerber.cam.CamFile` subclass
50 | Gerber or Excellon file to convert
51 |
52 | Returns
53 | -------
54 | cam_file : :class:`gerber.cam.CamFile` subclass
55 | A deep copy of the source file with units converted to metric.
56 | """
57 | cam_file = copy.deepcopy(cam_file)
58 | cam_file.to_metric()
59 | return cam_file
60 |
61 |
62 | def offset(cam_file, x_offset, y_offset):
63 | """ Offset a Cam file by a specified amount in the X and Y directions.
64 |
65 | Parameters
66 | ----------
67 | cam_file : :class:`gerber.cam.CamFile` subclass
68 | Gerber or Excellon file to offset
69 |
70 | x_offset : float
71 | Amount to offset the file in the X direction
72 |
73 | y_offset : float
74 | Amount to offset the file in the Y direction
75 |
76 | Returns
77 | -------
78 | cam_file : :class:`gerber.cam.CamFile` subclass
79 | An offset deep copy of the source file.
80 | """
81 | cam_file = copy.deepcopy(cam_file)
82 | cam_file.offset(x_offset, y_offset)
83 | return cam_file
84 |
85 |
86 | def scale(cam_file, x_scale, y_scale):
87 | """ Scale a Cam file by a specified amount in the X and Y directions.
88 |
89 | Parameters
90 | ----------
91 | cam_file : :class:`gerber.cam.CamFile` subclass
92 | Gerber or Excellon file to scale
93 |
94 | x_scale : float
95 | X-axis scale factor
96 |
97 | y_scale : float
98 | Y-axis scale factor
99 |
100 | Returns
101 | -------
102 | cam_file : :class:`gerber.cam.CamFile` subclass
103 | An scaled deep copy of the source file.
104 | """
105 | # TODO
106 | pass
107 |
108 |
109 | def rotate(cam_file, angle):
110 | """ Rotate a Cam file a specified amount about the origin.
111 |
112 | Parameters
113 | ----------
114 | cam_file : :class:`gerber.cam.CamFile` subclass
115 | Gerber or Excellon file to rotate
116 |
117 | angle : float
118 | Angle to rotate the file in degrees.
119 |
120 | Returns
121 | -------
122 | cam_file : :class:`gerber.cam.CamFile` subclass
123 | An rotated deep copy of the source file.
124 | """
125 | # TODO
126 | pass
127 |
--------------------------------------------------------------------------------
/hm_gerber_tool/pcb.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2021 HalfMarble LLC
5 | # copyright 2015 Hamilton Kibbe
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 |
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 |
19 |
20 | import os
21 |
22 | from hm_gerber_tool.cam import CamFile
23 | from .exceptions import ParseError
24 | from .layers import PCBLayer, sort_layers, layer_signatures
25 | from .common import read as gerber_read
26 | from .utils import listdir
27 |
28 |
29 | skip_extensions = ['.kicad_sch', '.kicad_prl', '.gbrjob', '.zip', '.png', '.jpg']
30 |
31 |
32 | class PCB(object):
33 |
34 | @classmethod
35 | def from_directory(cls, directory, board_name=None, verbose=False):
36 | layers = []
37 | names = set()
38 |
39 | # Validate
40 | directory = os.path.abspath(directory)
41 | if not os.path.isdir(directory):
42 | raise TypeError('{} is not a directory.'.format(directory))
43 |
44 | # Load gerber files
45 | for filename in listdir(directory, True, True):
46 | ext = os.path.splitext(filename)[1].lower()
47 | if verbose:
48 | print('[PCB]')
49 | print('[PCB]: ext [{}]'.format(ext))
50 | # common extensions that we should skip, which might be in the same path as the gerber files
51 | if ext is None or len(ext) == 0 or ext in skip_extensions:
52 | print('[PCB]: Skipping file {} [unsupported file extension]'.format(filename))
53 | continue
54 | try:
55 | if verbose:
56 | print('[PCB]: reading {}'.format(filename))
57 | camfile = gerber_read(os.path.join(directory, filename))
58 | if camfile is not None:
59 | layer = PCBLayer.from_cam(camfile)
60 | if verbose:
61 | print(
62 | '[PCB]: layer {}, bounds {}, [metric units: {}]'.format(layer, layer.bounds, layer.metric))
63 | layers.append(layer)
64 | name = os.path.splitext(filename)[0]
65 | if len(os.path.splitext(filename)) > 1:
66 | _name, ext = os.path.splitext(name)
67 | if ext[1:] in layer_signatures(layer.layer_class):
68 | name = _name
69 | if layer.layer_class == 'drill' and 'drill' in ext:
70 | name = _name
71 | names.add(name)
72 | except ParseError:
73 | if verbose:
74 | print('[PCB]: Skipping file {} [ParseError]'.format(filename))
75 | except IOError:
76 | if verbose:
77 | print('[PCB]: Skipping file {} [IOError]'.format(filename))
78 |
79 | # Try to guess board name
80 | if board_name is None:
81 | if len(names) == 1:
82 | board_name = names.pop()
83 | else:
84 | board_name = os.path.basename(directory)
85 |
86 | print('[PCB]')
87 | print('[PCB]: board_name {}'.format(board_name))
88 |
89 | # Return PCB
90 | if len(layers) > 0:
91 | board = cls(layers, board_name)
92 | print('[PCB]: board_bounds {}'.format(board.board_bounds))
93 | if board.board_bounds is None:
94 | return None
95 | else:
96 | return board
97 | else:
98 | return None
99 |
100 | def __init__(self, layers, name=None):
101 | self.layers = sort_layers(layers)
102 | self.name = name
103 |
104 | def __len__(self):
105 | return len(self.layers)
106 |
107 | @property
108 | def top_layers(self):
109 | board_layers = [l for l in reversed(self.layers) if l.layer_class in
110 | ('top_silk', 'top_mask', 'top_copper')]
111 | drill_layers = [l for l in self.drill_layers if 'top' in l.layers]
112 | # Drill layer goes under soldermask for proper rendering of tented vias
113 | return [board_layers[0]] + drill_layers + board_layers[1:]
114 |
115 | @property
116 | def bottom_layers(self):
117 | board_layers = [l for l in self.layers if l.layer_class in
118 | ('bottom_silk', 'bottom_mask', 'bottom_copper')]
119 | drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers]
120 | # Drill layer goes under soldermask for proper rendering of tented vias
121 | return [board_layers[0]] + drill_layers + board_layers[1:]
122 |
123 | @property
124 | def drill_layers(self):
125 | return [l for l in self.layers if l.layer_class in
126 | ('drill')]
127 |
128 | @property
129 | def copper_layers(self):
130 | return list(reversed([layer for layer in self.layers if layer.layer_class in
131 | ('top_copper', 'bottom_copper', 'internal')]))
132 |
133 | @property
134 | def edge_cuts_layer(self):
135 | for layer in self.layers:
136 | if layer.layer_class == 'edge_cuts':
137 | return layer
138 |
139 | @property
140 | def layer_count(self):
141 | """ Number of *COPPER* layers
142 | """
143 | return len([l for l in self.layers if l.layer_class in
144 | ('top_copper', 'bottom_copper', 'internal')])
145 |
146 | @property
147 | def metric(self):
148 | return True
149 |
150 | @property
151 | def board_bounds(self):
152 | bounds = None
153 | if bounds is None:
154 | for layer in self.layers:
155 | if layer.layer_class == 'edge_cuts':
156 | bounds = layer.bounds
157 | if bounds is None:
158 | for layer in self.layers:
159 | if layer.layer_class == 'top_copper':
160 | bounds = layer.bounds
161 | return bounds
162 |
163 |
--------------------------------------------------------------------------------
/hm_gerber_tool/render/__init__.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # copyright 2014 Hamilton Kibbe
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | """
18 | gerber.render
19 | ============
20 | **Gerber Renderers**
21 |
22 | This module provides contexts for rendering images of gerber layers. Currently
23 | SVG is the only supported format.
24 | """
25 |
26 | from .render import RenderSettings
27 | from .cairo_backend import GerberCairoContext
28 |
29 | available_renderers = {
30 | 'cairo': GerberCairoContext,
31 | }
32 |
--------------------------------------------------------------------------------
/hm_gerber_tool/render/excellon_backend.py:
--------------------------------------------------------------------------------
1 |
2 | from .render import GerberContext
3 | from ..excellon import DrillSlot
4 | from ..excellon_statements import *
5 |
6 | class ExcellonContext(GerberContext):
7 |
8 | MODE_DRILL = 1
9 | MODE_SLOT = 2
10 |
11 | def __init__(self, settings):
12 | GerberContext.__init__(self)
13 |
14 | # Statements that we write
15 | self.comments = []
16 | self.header = []
17 | self.tool_def = []
18 | self.body_start = [RewindStopStmt()]
19 | self.body = []
20 | self.start = [HeaderBeginStmt()]
21 |
22 | # Current tool and position
23 | self.handled_tools = set()
24 | self.cur_tool = None
25 | self.drill_mode = ExcellonContext.MODE_DRILL
26 | self.drill_down = False
27 | self._pos = (None, None)
28 |
29 | self.settings = settings
30 |
31 | self._start_header()
32 | self._start_comments()
33 |
34 | def _start_header(self):
35 | """Create the header from the settings"""
36 |
37 | self.header.append(UnitStmt.from_settings(self.settings))
38 |
39 | if self.settings.notation == 'incremental':
40 | raise NotImplementedError('Incremental mode is not implemented')
41 | else:
42 | self.body.append(AbsoluteModeStmt())
43 |
44 | def _start_comments(self):
45 |
46 | # Write the digits used - this isn't valid Excellon statement, so we write as a comment
47 | self.comments.append(CommentStmt('FILE_FORMAT=%d:%d' % (self.settings.format[0], self.settings.format[1])))
48 |
49 | def _get_end(self):
50 | """How we end depends on our mode"""
51 |
52 | end = []
53 |
54 | if self.drill_down:
55 | end.append(RetractWithClampingStmt())
56 | end.append(RetractWithoutClampingStmt())
57 |
58 | end.append(EndOfProgramStmt())
59 |
60 | return end
61 |
62 | @property
63 | def statements(self):
64 | return self.start + self.comments + self.header + self.body_start + self.body + self._get_end()
65 |
66 | def set_bounds(self, bounds, *args, **kwargs):
67 | pass
68 |
69 | def paint_background(self):
70 | pass
71 |
72 | def _render_line(self, line, color):
73 | raise ValueError('Invalid Excellon object')
74 |
75 | def _render_arc(self, arc, color):
76 | raise ValueError('Invalid Excellon object')
77 |
78 | def _render_region(self, region, color):
79 | raise ValueError('Invalid Excellon object')
80 |
81 | def _render_level_polarity(self, region):
82 | raise ValueError('Invalid Excellon object')
83 |
84 | def _render_circle(self, circle, color):
85 | raise ValueError('Invalid Excellon object')
86 |
87 | def _render_rectangle(self, rectangle, color):
88 | raise ValueError('Invalid Excellon object')
89 |
90 | def _render_obround(self, obround, color):
91 | raise ValueError('Invalid Excellon object')
92 |
93 | def _render_polygon(self, polygon, color):
94 | raise ValueError('Invalid Excellon object')
95 |
96 | def _simplify_point(self, point):
97 | return (point[0] if point[0] != self._pos[0] else None, point[1] if point[1] != self._pos[1] else None)
98 |
99 | def _render_drill(self, drill, color):
100 |
101 | if self.drill_mode != ExcellonContext.MODE_DRILL:
102 | self._start_drill_mode()
103 |
104 | tool = drill.hit.tool
105 | if not tool in self.handled_tools:
106 | self.handled_tools.add(tool)
107 | self.header.append(ExcellonTool.from_tool(tool))
108 |
109 | if tool != self.cur_tool:
110 | self.body.append(ToolSelectionStmt(tool.number))
111 | self.cur_tool = tool
112 |
113 | point = self._simplify_point(drill.position)
114 | self._pos = drill.position
115 | self.body.append(CoordinateStmt.from_point(point))
116 |
117 | def _start_drill_mode(self):
118 | """
119 | If we are not in drill mode, then end the ROUT so we can do basic drilling
120 | """
121 |
122 | if self.drill_mode == ExcellonContext.MODE_SLOT:
123 |
124 | # Make sure we are retracted before changing modes
125 | last_cmd = self.body[-1]
126 | if self.drill_down:
127 | self.body.append(RetractWithClampingStmt())
128 | self.body.append(RetractWithoutClampingStmt())
129 | self.drill_down = False
130 |
131 | # Switch to drill mode
132 | self.body.append(DrillModeStmt())
133 | self.drill_mode = ExcellonContext.MODE_DRILL
134 |
135 | else:
136 | raise ValueError('Should be in slot mode')
137 |
138 | def _render_slot(self, slot, color):
139 |
140 | # Set the tool first, before we might go into drill mode
141 | tool = slot.hit.tool
142 | if not tool in self.handled_tools:
143 | self.handled_tools.add(tool)
144 | self.header.append(ExcellonTool.from_tool(tool))
145 |
146 | if tool != self.cur_tool:
147 | self.body.append(ToolSelectionStmt(tool.number))
148 | self.cur_tool = tool
149 |
150 | # Two types of drilling - normal drill and slots
151 | if slot.hit.slot_type == DrillSlot.TYPE_ROUT:
152 |
153 | # For ROUT, setting the mode is part of the actual command.
154 |
155 | # Are we in the right position?
156 | if slot.start != self._pos:
157 | if self.drill_down:
158 | # We need to move into the right position, so retract
159 | self.body.append(RetractWithClampingStmt())
160 | self.drill_down = False
161 |
162 | # Move to the right spot
163 | point = self._simplify_point(slot.start)
164 | self._pos = slot.start
165 | self.body.append(CoordinateStmt.from_point(point, mode="ROUT"))
166 |
167 | # Now we are in the right spot, so drill down
168 | if not self.drill_down:
169 | self.body.append(ZAxisRoutPositionStmt())
170 | self.drill_down = True
171 |
172 | # Do a linear move from our current position to the end position
173 | point = self._simplify_point(slot.end)
174 | self._pos = slot.end
175 | self.body.append(CoordinateStmt.from_point(point, mode="LINEAR"))
176 |
177 | self.drill_mode = ExcellonContext.MODE_SLOT
178 |
179 | else:
180 | # This is a G85 slot, so do this in normally drilling mode
181 | if self.drill_mode != ExcellonContext.MODE_DRILL:
182 | self._start_drill_mode()
183 |
184 | # Slots don't use simplified points
185 | self._pos = slot.end
186 | self.body.append(SlotStmt.from_points(slot.start, slot.end))
187 |
188 | def _render_inverted_layer(self):
189 | pass
190 |
--------------------------------------------------------------------------------
/hm_gerber_tool/render/render.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2021 HalfMarble LLC
5 | # copyright 2014 Hamilton Kibbe
6 | # Modified from code by Paulo Henrique Silva
7 |
8 | # Licensed under the Apache License, Version 2.0 (the "License");
9 | # you may not use this file except in compliance with the License.
10 | # You may obtain a copy of the License at
11 |
12 | # http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | # Unless required by applicable law or agreed to in writing, software
15 | # distributed under the License is distributed on an "AS IS" BASIS,
16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | # See the License for the specific language governing permissions and
18 | # limitations under the License.
19 | """
20 | Rendering
21 | ============
22 | **Gerber (RS-274X) and Excellon file rendering**
23 |
24 | Render Gerber and Excellon files to a variety of formats. The render module
25 | currently supports SVG rendering using the `svgwrite` library.
26 | """
27 |
28 |
29 | from ..primitives import *
30 | from ..gerber_statements import (CommentStmt, UnknownStmt, EofStmt, ParamStmt,
31 | CoordStmt, ApertureStmt, RegionModeStmt,
32 | QuadrantModeStmt,)
33 |
34 |
35 | class GerberContext(object):
36 | """ Gerber rendering context base class
37 |
38 | Provides basic functionality and API for rendering gerber files. Medium-
39 | specific renderers should subclass GerberContext and implement the drawing
40 | functions. Colors are stored internally as 32-bit RGB and may need to be
41 | converted to a native format in the rendering subclass.
42 |
43 | Attributes
44 | ----------
45 | units : string
46 | Measurement units. 'inch' or 'metric'
47 |
48 | color : tuple (, , )
49 | Color used for rendering as a tuple of normalized (red, green, blue)
50 | values.
51 |
52 | drill_color : tuple (, , )
53 | Color used for rendering drill hits. Format is the same as for `color`.
54 |
55 | background_color : tuple (, , )
56 | Color of the background. Used when exposing areas in 'clear' level
57 | polarity mode. Format is the same as for `color`.
58 |
59 | alpha : float
60 | Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.)
61 | """
62 |
63 | def __init__(self, units='metric'):
64 | self._units = units
65 | self._color = (0.7215, 0.451, 0.200)
66 | self._background_color = (0.0, 0.0, 0.0)
67 | self._drill_color = (0.0, 1.0, 0.0)
68 | self._alpha = 1.0
69 | self._invert = False
70 |
71 | @property
72 | def units(self):
73 | return self._units
74 |
75 | @units.setter
76 | def units(self, units):
77 | if units not in ('inch', 'metric'):
78 | raise ValueError('Units may be "inch" or "metric"')
79 | self._units = units
80 |
81 | @property
82 | def color(self):
83 | return self._color
84 |
85 | @color.setter
86 | def color(self, color):
87 | if len(color) != 3:
88 | raise TypeError('Color must be a tuple of R, G, and B values')
89 | for c in color:
90 | if c < 0 or c > 1:
91 | raise ValueError('Channel values must be between 0.0 and 1.0')
92 | self._color = color
93 |
94 | @property
95 | def drill_color(self):
96 | return self._drill_color
97 |
98 | @drill_color.setter
99 | def drill_color(self, color):
100 | if len(color) != 3:
101 | raise TypeError('Drill color must be a tuple of R, G, and B values')
102 | for c in color:
103 | if c < 0 or c > 1:
104 | raise ValueError('Channel values must be between 0.0 and 1.0')
105 | self._drill_color = color
106 |
107 | @property
108 | def background_color(self):
109 | return self._background_color
110 |
111 | @background_color.setter
112 | def background_color(self, color):
113 | if len(color) != 3:
114 | raise TypeError('Background color must be a tuple of R, G, and B values')
115 | for c in color:
116 | if c < 0 or c > 1:
117 | raise ValueError('Channel values must be between 0.0 and 1.0')
118 | self._background_color = color
119 |
120 | @property
121 | def alpha(self):
122 | return self._alpha
123 |
124 | @alpha.setter
125 | def alpha(self, alpha):
126 | if alpha < 0 or alpha > 1:
127 | raise ValueError('Alpha must be between 0.0 and 1.0')
128 | self._alpha = alpha
129 |
130 | @property
131 | def invert(self):
132 | return self._invert
133 |
134 | @invert.setter
135 | def invert(self, invert):
136 | self._invert = invert
137 |
138 | def render(self, primitive):
139 | if not primitive:
140 | return
141 |
142 | self.pre_render_primitive(primitive)
143 |
144 | color = self.color
145 | if isinstance(primitive, Line):
146 | self._render_line(primitive, color)
147 | elif isinstance(primitive, Arc):
148 | self._render_arc(primitive, color)
149 | elif isinstance(primitive, Region):
150 | self._render_region(primitive, color)
151 | elif isinstance(primitive, Circle):
152 | self._render_circle(primitive, color)
153 | elif isinstance(primitive, Rectangle):
154 | self._render_rectangle(primitive, color)
155 | elif isinstance(primitive, Obround):
156 | self._render_obround(primitive, color)
157 | elif isinstance(primitive, Polygon):
158 | self._render_polygon(primitive, color)
159 | elif isinstance(primitive, Drill):
160 | self._render_drill(primitive, color)
161 | elif isinstance(primitive, Slot):
162 | self._render_slot(primitive, color)
163 | elif isinstance(primitive, AMGroup):
164 | self._render_amgroup(primitive, color)
165 | elif isinstance(primitive, Outline):
166 | self._render_region(primitive, color)
167 | elif isinstance(primitive, TestRecord):
168 | self._render_test_record(primitive, color)
169 |
170 | self.post_render_primitive(primitive)
171 |
172 | def set_bounds(self, bounds, *args, **kwargs):
173 | """Called by the renderer to set the extents of the file to render.
174 |
175 | Parameters
176 | ----------
177 | bounds: Tuple[Tuple[float, float], Tuple[float, float]]
178 | ( (x_min, x_max), (y_min, y_max)
179 | """
180 | pass
181 |
182 | def paint_background(self):
183 | pass
184 |
185 | def new_render_layer(self):
186 | pass
187 |
188 | def flatten_render_layer(self):
189 | pass
190 |
191 | def pre_render_primitive(self, primitive):
192 | """
193 | Called before rendering a primitive. Use the callback to perform some action before rendering
194 | a primitive, for example adding a comment.
195 | """
196 | return
197 |
198 | def post_render_primitive(self, primitive):
199 | """
200 | Called after rendering a primitive. Use the callback to perform some action after rendering
201 | a primitive
202 | """
203 | return
204 |
205 | def _render_line(self, primitive, color):
206 | pass
207 |
208 | def _render_arc(self, primitive, color):
209 | pass
210 |
211 | def _render_region(self, primitive, color):
212 | pass
213 |
214 | def _render_circle(self, primitive, color):
215 | pass
216 |
217 | def _render_rectangle(self, primitive, color):
218 | pass
219 |
220 | def _render_obround(self, primitive, color):
221 | pass
222 |
223 | def _render_polygon(self, primitive, color):
224 | pass
225 |
226 | def _render_drill(self, primitive, color):
227 | pass
228 |
229 | def _render_slot(self, primitive, color):
230 | pass
231 |
232 | def _render_amgroup(self, primitive, color):
233 | pass
234 |
235 | def _render_test_record(self, primitive, color):
236 | pass
237 |
238 |
239 | class RenderSettings(object):
240 | def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False,
241 | mirror=False):
242 | self.color = color
243 | self.alpha = alpha
244 | self.invert = invert
245 | self.mirror = mirror
246 |
247 | def __str__(self):
248 | return "".format(self.color, self.alpha, self.invert, self.mirror)
249 |
--------------------------------------------------------------------------------
/hm_gerber_tool/render/theme.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright 2013-2014 Paulo Henrique Silva
5 |
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 |
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 |
19 | from .render import RenderSettings
20 |
21 | COLORS = {
22 | 'black': (0.0, 0.0, 0.0),
23 | 'white': (1.0, 1.0, 1.0),
24 | 'red': (1.0, 0.0, 0.0),
25 | 'green': (0.0, 1.0, 0.0),
26 | 'yellow': (1.0, 1.0, 0),
27 | 'blue': (0.0, 0.0, 1.0),
28 | 'fr-4': (0.290, 0.345, 0.0),
29 | 'green soldermask': (0.0, 0.412, 0.278),
30 | 'blue soldermask': (0.059, 0.478, 0.651),
31 | 'red soldermask': (0.968, 0.169, 0.165),
32 | 'black soldermask': (0.298, 0.275, 0.282),
33 | 'purple soldermask': (0.2, 0.0, 0.334),
34 | 'enig copper': (0.694, 0.533, 0.514),
35 | 'hasl copper': (0.871, 0.851, 0.839)
36 | }
37 |
38 |
39 | SPECTRUM = [
40 | (0.804, 0.216, 0),
41 | (0.78, 0.776, 0.251),
42 | (0.545, 0.451, 0.333),
43 | (0.545, 0.137, 0.137),
44 | (0.329, 0.545, 0.329),
45 | (0.133, 0.545, 0.133),
46 | (0, 0.525, 0.545),
47 | (0.227, 0.373, 0.804),
48 | ]
49 |
50 |
51 | class Theme(object):
52 |
53 | def __init__(self, name=None, **kwargs):
54 | self.name = 'Default' if name is None else name
55 | self.background = kwargs.get('background', RenderSettings(COLORS['fr-4']))
56 | self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white']))
57 | self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True))
58 | self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True))
59 | self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True))
60 | self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
61 | self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True))
62 | self.drill = kwargs.get('drill', RenderSettings(COLORS['black']))
63 | self.ipc_netlist = kwargs.get('ipc_netlist', RenderSettings(COLORS['red']))
64 | self._internal = kwargs.get('internal', [RenderSettings(x) for x in SPECTRUM])
65 | self._internal_gen = None
66 |
67 | def __getitem__(self, key):
68 | return getattr(self, key)
69 |
70 | @property
71 | def internal(self):
72 | if not self._internal_gen:
73 | self._internal_gen = self._internal_gen_func()
74 | return next(self._internal_gen)
75 |
76 | def _internal_gen_func(self):
77 | for setting in self._internal:
78 | yield setting
79 |
80 | def get(self, key, noneval=None):
81 | val = getattr(self, key, None)
82 | return val if val is not None else noneval
83 |
84 |
85 | THEMES = {
86 | 'default': Theme(),
87 |
88 | 'Mask': Theme(name='Mask',
89 | background=RenderSettings(COLORS['white'], alpha=0.0),
90 | top=RenderSettings(COLORS['black'], alpha=1.0),
91 | bottom=RenderSettings(COLORS['black'], alpha=1.0),
92 | topmask=RenderSettings(COLORS['black'], alpha=1.0),
93 | bottommask=RenderSettings(COLORS['black'], alpha=1.0),
94 | topsilk=RenderSettings(COLORS['black'], alpha=1.0),
95 | bottomsilk=RenderSettings(COLORS['black'], alpha=1.0)),
96 |
97 | 'OSH Park': Theme(name='OSH Park',
98 | background=RenderSettings(COLORS['purple soldermask']),
99 | top=RenderSettings(COLORS['enig copper']),
100 | bottom=RenderSettings(COLORS['enig copper'], mirror=True),
101 | topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True),
102 | bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True),
103 | topsilk=RenderSettings(COLORS['white'], alpha=0.8),
104 | bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)),
105 |
106 | 'Blue': Theme(name='Blue',
107 | topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True),
108 | bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)),
109 |
110 | 'Transparent Copper': Theme(name='Transparent',
111 | background=RenderSettings((0.9, 0.9, 0.9)),
112 | top=RenderSettings(COLORS['red'], alpha=0.5),
113 | bottom=RenderSettings(COLORS['blue'], alpha=0.5),
114 | drill=RenderSettings((0.3, 0.3, 0.3))),
115 |
116 | 'Transparent Multilayer': Theme(name='Transparent Multilayer',
117 | background=RenderSettings((0, 0, 0)),
118 | top=RenderSettings(SPECTRUM[0], alpha=0.8),
119 | bottom=RenderSettings(SPECTRUM[-1], alpha=0.8),
120 | drill=RenderSettings((0.3, 0.3, 0.3)),
121 | internal=[RenderSettings(x, alpha=0.5) for x in SPECTRUM[1:-1]]),
122 | }
123 |
--------------------------------------------------------------------------------
/pics/KiCad_drill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/pics/KiCad_drill.png
--------------------------------------------------------------------------------
/pics/KiCad_plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/pics/KiCad_plot.png
--------------------------------------------------------------------------------
/pics/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/pics/Screenshot.png
--------------------------------------------------------------------------------
/pics/Screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/pics/Screenshot2.png
--------------------------------------------------------------------------------
/pics/Screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halfmarble/hm-panelizer/73489a0b5d0d46e6d363f6d14454d91fab62f8e3/pics/Screenshot3.png
--------------------------------------------------------------------------------