├── .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 | 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 | ![icon](https://raw.githubusercontent.com/halfmarble/hm-panelizer/main/data/icons/icon.png) 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 | ![screenshot](pics/Screenshot.png) 80 | 81 | Main view (outline verification) 82 | 83 | ![screenshot3](pics/Screenshot3.png) 84 | 85 | Panel view 86 | 87 | ![screenshot2](pics/Screenshot2.png) 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 | ![KiCad plot settings](pics/KiCad_plot.png) 119 | 120 | KiCad drill settings 121 | 122 | ![KiCad drill settings](pics/KiCad_drill.png) 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 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](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 --------------------------------------------------------------------------------