Compare commits

...

44 Commits

Author SHA1 Message Date
L'électron rare 2402288a2b feat: ajout d'un fichier README_FR.md et mise à jour du README.md avec un lien de parrainage 2026-03-26 07:57:20 +01:00
Benny Megidish 51fe5fc3f4 Update README.md 2025-12-14 19:48:09 +02:00
Benny Megidish 8c4c341b10 Merge branch 'master' of https://github.com/bennymeg/JLC-Plugin-for-KiCad 2025-12-14 18:49:22 +02:00
Benny Megidish 605756ddb1 chore: invert backup flag + fix checkbox ui bug 2025-12-14 18:44:56 +02:00
Lukas Lang 7ac0af4ccf fix: crash when trying to rename non-existent designators/BOM/positions files (#225) 2025-12-05 13:50:10 +02:00
Benny Megidish 8c3a4ba784 Merge branch 'master' of https://github.com/bennymeg/JLC-Plugin-for-KiCad 2025-10-30 22:00:18 +02:00
Benny Megidish 3f9d135e66 chore: bump version (5.2.0) 2025-10-30 22:00:11 +02:00
Neil Enns 1e25598c4e fix: noBackup CLI option (#222) 2025-10-14 21:00:33 +03:00
Benny Megidish 6393fd89d8 feat: restore design setting after plotting. #208 2025-10-13 10:37:16 +03:00
Benny Megidish 87fe0fc675 chore: rewording 2025-10-13 10:22:18 +03:00
Neil Enns a20484abc5 feat: add support for a --noBackup flag and UI checkbox & dev containers (#221)
* Add vscode devcontainer

* Add test files to gitignore

* Add --noBackup flag

* Update installer
2025-10-13 09:18:09 +03:00
Peter Jaquiery 3381050309 feat: skip empty MPNs, update image asset (#191)
* Correct PNG for Symbol Properties dialog to show "FT Rotation Offset" rather than "JLCPCB Rotation Offset" to match readme and code behaviour.

* Continue field search in MPN handling if selected field is empty.

---------

Co-authored-by: Benny Megidish <bennymegk@gmail.com>
2025-10-03 14:14:27 +03:00
WilheJo 6a2804ef19 feat: rename unzipped files if archive gets a user defined name (#210) 2025-10-03 13:59:00 +03:00
wenzellabs d047824964 feat: allow 'Part Number' as BOM keyword, add checkbox whether browser opens (#213)
* allow 'Part Number' as BOM keyword

Signed-off-by: matthias wenzel <wenzel@wenzellabs.de>

* add a checkbox 'Open browser after generation'

Signed-off-by: matthias wenzel <wenzel@wenzellabs.de>

* document 'Part Number' as BOM field

Signed-off-by: matthias wenzel <wenzel@wenzellabs.de>

---------

Signed-off-by: matthias wenzel <wenzel@wenzellabs.de>
2025-10-03 13:48:27 +03:00
Benny Megidish da738d6423 chore: update README.md 2025-09-18 12:50:00 +03:00
Antonín Skala 2894567147 feat: add nonInteractive mode (#212) 2025-08-02 21:04:04 +03:00
Benny Megidish e2afaec677 chore: bump version 2025-04-30 19:35:08 +03:00
Guy Naor 1d441283f6 feat: added support for footprint rotation and position regex to also check the library nickname (#203)
This is because of the way Ultra Librarian setup the names with colon
2025-04-30 14:06:19 +03:00
Benny Megidish 08774e06ed chore: archive name PR cleanup #202, close #188 2025-03-25 18:59:00 +02:00
Lukas Lang 3778c9a1fa feat: add ability to customize archive name (#202)
* Add ability to customize archive name

* Remove incorrect autocomplete for ARCHIVE_NAME text box

---------

Co-authored-by: Lukas Lang <langl@phys.ethz.ch>
2025-03-25 18:47:04 +02:00
Lukas Lang 9922748010 fix: footprints rotations that are not a multiple of 90° (#198) 2025-03-20 11:56:20 +02:00
Randall Scharpf b98c02c743 feat: add common abbreviations for part number (pn, p/n, part no.) (#201) 2025-03-20 11:53:00 +02:00
Benny Megidish 89d587b1cd chore: initial v10 support #200 2025-03-18 09:01:17 +02:00
Nanashi db3878b7ad fix: add support for the new EasyEDA2Kicad fields format (#185)
* update part number checks to work with new easyedatokicad format
* bump version
2025-01-12 21:15:19 +02:00
Benny Megidish 5880f10ea7 chore: update bug report template 2024-12-19 16:14:29 +02:00
Benny Megidish f20134e34d fix: metadata single version 2024-12-07 21:42:41 +02:00
Benny Megidish 588625cab0 feat: added v5.0.0 2024-12-05 20:58:23 +02:00
Benny Megidish 466a2acc53 chore: clean up readme #179 2024-12-05 20:38:01 +02:00
Benny Megidish f623083701 Merge branch 'master' of https://github.com/bennymeg/JLC-Plugin-for-KiCad 2024-12-05 20:31:19 +02:00
Viermusketiere ff77eca2bc feat: added CLI Interface (#179)
* Added CLI Interface

* Added .pyc to gitignore for CLI usecases

* Moved cli progress bar generation to utils

* Included ALL_ACTIVE_LAYERS_OPT

* Added documentation to README for cli usage
2024-12-05 20:30:58 +02:00
Benny Megidish c4f876c991 feat: plot using custom layers name, closes #178 2024-12-05 20:23:53 +02:00
Benny Megidish 5f0dc0cb22 fix: layer override out of range, close #171 2024-12-05 18:13:07 +02:00
Benny Megidish 762794e025 feat: modified default field keys **BREAKING** 2024-12-05 11:06:50 +02:00
Benny Megidish 9c4d801d8c feat: cleanup origin field method 2024-12-05 10:58:38 +02:00
Benny Megidish cf61d8c717 chore: typos 2024-12-05 10:13:11 +02:00
Benny Megidish 34bcb62852 chore: clean up #181 2024-12-05 10:12:56 +02:00
Benny Megidish a2341cfd36 chore: remove ds store from PR 2024-12-05 09:54:13 +02:00
Benny Megidish 635f46fae4 Merge branch 'master' of https://github.com/bennymeg/JLC-Plugin-for-KiCad 2024-12-05 09:52:15 +02:00
Benny Megidish fba8f8bab2 chore: minor 2024-12-05 09:52:10 +02:00
grodnay 03dae2cb63 fix: Trough Hole & "unspecified" position to center of pads (#181)
* Fixed Trough Hole position to center of pads, and "unspecifiled" to centor of bounding box.

* "unspecified" changed to center of pads.  "Origin" field added.
2024-12-05 09:49:34 +02:00
Benny Megidish efaf3d7bc6 chore: added missing new line 2024-11-24 20:52:40 +02:00
Adrian Studer e1378eea49 feat: add option to enable/disable plotting of all active layers (#176)
* Add option to enable/disable export of all active layers. Disabling that option will only export commonly used fabrication layers. Addresses issue #172

* move definition of standard layers to config.py
2024-11-21 14:57:43 +02:00
Dewalddp be93f52882 feat: apply delta x and delta y from transformations.csv (#177) 2024-11-19 14:04:16 +02:00
Romain ea200f23dd feat: close the dialog when pressing the ESC key (#180) 2024-11-19 13:38:18 +02:00
17 changed files with 763 additions and 121 deletions
+17
View File
@@ -0,0 +1,17 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/python:1": {},
"ghcr.io/stuartleeks/dev-container-features/shell-history:0": {},
"ghcr.io/neilenns/devcontainer-features/oh-my-zsh": {
"disableAutoUpdate": false,
"disableUpdatePrompt": false,
"stripWorkspacesFromPrompt": true,
"theme": "agnoster"
}
},
"postCreateCommand": ".devcontainer/install-kicad.sh"
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
set -e
echo "Installing prerequisites..."
sudo apt update > /dev/null 2>&1 && sudo apt install -y software-properties-common > /dev/null 2>&1
echo "Installing KiCad (takes a few minutes)..."
sudo add-apt-repository -y ppa:kicad/kicad-9.0-releases > /dev/null 2>&1
sudo apt update > /dev/null 2>&1
sudo apt install -y --no-install-recommends kicad kicad-symbols kicad-footprints > /dev/null 2>&1
# Link KiCad plugin to workspace path
echo "Linking KiCad plugin..."
mkdir -p "$HOME/.local/share/kicad/9.0/3rdparty/plugins"
ln -s /workspaces/Fabrication-Toolkit/plugins "$HOME/.local/share/kicad/9.0/3rdparty/plugins/com_github_bennymeg_JLC-Plugin-for-KiCad"
mkdir -p "$HOME/.local/share/kicad/9.0/3rdparty/resources"
ln -s /workspaces/Fabrication-Toolkit/resources "$HOME/.local/share/kicad/9.0/3rdparty/resources/com_github_bennymeg_JLC-Plugin-for-KiCad"
# Add plugin to PYTHONPATH
echo "Adding plugin to PYTHONPATH..."
echo 'export PYTHONPATH="$PYTHONPATH:$HOME/.local/share/kicad/9.0/3rdparty/plugins"' >> "$HOME/.zshrc"
+9 -1
View File
@@ -17,7 +17,15 @@ body:
What version of Fabrication Toolkit are you using?
Note: Please only report issues for currently supported versions of Fabrication Toolkit.
placeholder: 4.0.0
validations:
required: true
- type: input
attributes:
label: KiCad Version
description: |
What version of KiCad are you using?
Note: Please only report issues for currently supported versions of KiCad.
validations:
required: true
- type: dropdown
+4 -1
View File
@@ -1,2 +1,5 @@
.venv
dist/
dist/
.DS_Store
*.pyc
Test/
+22 -1
View File
@@ -1,14 +1,23 @@
{
"cSpell.words": [
"bennymeg",
"Colour",
"csvfile",
"Drilland",
"Elec",
"EXCELLON",
"FPID",
"gerbers",
"HTSSOP",
"JLCPCB",
"kicad",
"LCSC",
"LQFP",
"LSEQ",
"Megidish",
"MSOP",
"netlist",
"panelization",
"pcbnew",
"Plotfile",
"Protel",
@@ -16,6 +25,18 @@
"restkey",
"restval",
"rsin",
"Soldermask"
"SOIC",
"Soldermask",
"SPST",
"SSOP",
"TDSON",
"TQFP",
"TSOT",
"TSSOP",
"UDFN",
"USON",
"venv",
"VSON",
"VSSOP"
]
}
+108 -40
View File
@@ -1,12 +1,13 @@
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/logo.svg?raw=true"
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/logo.svg?raw=true"
style="display:block margin-left: auto; margin-right: auto;" alt="JLC PCB Plug-in for KiCad">
<div align="center">
| **JLC PCB Plug-in for KiCad** |
|:-----:|
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/bennymeg)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/bennymeg/fabrication-toolkit/total)
</div>
@@ -27,11 +28,10 @@ Fabrication Toolkit is distributed with the official releases of KiCad 6+. Open
Download the [latest release](https://github.com/bennymeg/JLC-Plugin-for-KiCad/releases) ZIP file. Open the "Plugin and Content Manager" from the KiCads main window and install the ZIP file via "Install from File".
## Usage
Click on the Fabrication Toolkit <img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/resources/icon.png?raw=true" style="magin-bottom: 8px;" alt="Logo" height=24> button on the top tool box inside KiCad pcb editor (pcbnew).
Click on the Fabrication Toolkit <img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/resources/icon.png?raw=true" style="margin-bottom: 8px;" alt="Logo" height=24> button on the top tool box inside KiCad pcb editor (pcbnew), choose the required options and click on 'Generate'.
**⊛** Ensure your board is syncronized before invoking this addon [**F8**].
**⊛** The `User_1` layer in internally defined as a **V-Cuts** layer, please avoid using it for anything else (unless disabled). <span style="text-color: light-grey !important;">_(since v3.0.0)_.</span>
> [!IMPORTANT]
> Ensure your board is synchronized before invoking this addon [**F8**].
## Options
@@ -39,12 +39,21 @@ Options can be set in the dialog that appears when the plugin is invoked. They a
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/options.png?raw=true" height=275>
__Archive name__: Name of the archive file to be generated. Can include text variables, such as e.g. `${TITLE}_${REVISION}`</br>
__Additional layers__: Comma-separated list of additional layers to include in the gerber archive.</br>
__Plot all active layers__: Whether to include all layers, instead of just the layers required by JLCPCB.</br>
__Set User.1 as V-Cut layer__: Merge User.1 layer with the Edge-Cut layer in production.</br>
__Use User.2 for an alternative Edge-Cut layer__: Use the User.2 instead of the Edge-Cut layer for the board outline in production. This is useful if you need process edges or panelization during production but still want to keep the individual outline for prototyping, 3D model exports, or similar purposes.</br>
__Use User.2 for an alternative Edge-Cut layer__: Use the User.2 instead of the Edge-Cut layer for the board outline in production.
This is useful if you need process edges or panelization during production but still want to keep the individual outline for prototyping, 3D model exports, or similar purposes.</br>
__Apply automatic translations__: Apply known translation fixes for common components.</br>
__Apply automatic fill for all zones__: Refill all zones before generation production files.</br>
__Exclude DNP components from BOM__: Exclude components the had been set a DNP from th BOM.</br>
__Generate Backups__: Generate Backup zip for the production files.</br>
## Attributes
> [!NOTE]
> All the attributes are entered in the Schematic Editor, make sure to update your PCB [**F8**] with the changes made to the schematic before generating a new set of production files, otherwise your changes won't be reflected.
### ① Include Component Part Number in Production Files
Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's fields property.
@@ -52,16 +61,19 @@ Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/mpn.png?raw=true" height=420>
#### Primary Fields*:
| 'LCSC Part #' | 'JLCPCB Part #' |
| --- | --- |
| 'LCSC Part #' | 'LCSC Part' | 'LCSC PN' | 'LCSC P/N' | 'LCSC Part No.' | 'LCSC Part Number' | 'JLCPCB Part #' | 'JLCPCB Part' | 'JLCPCB PN' | 'JLCPCB P/N' | 'JLCPCB Part No.' | 'JLCPCB Part Number' |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
_The fields will be query in the order denoted above._
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'JLC Part' | 'LCSC Part' | 'LCSC' | 'JLC' | 'MPN' | 'Mpn' | 'mpn' |
| --- | --- | --- | --- | --- | --- | --- |
| 'LCSC' | 'JLC' | 'MPN' | 'Mpn' | 'mpn' |
| --- | --- | --- | --- | --- |
_The fields will be query in the order denoted above._
_The fields will be queried in the order denoted above._
Empty fields are skipped so, for example, a 'MPN' field with a value will be used if
all earlier fields are missing or empty.
---
@@ -77,7 +89,7 @@ Select 'Exclude from position files' or 'Exclude from BOM' in the footprint's fa
---
### ③ Offset Component Rotation
The rotation of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLC PCB used different variation of the same standard. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'JLCPCB Rotation Offset' field - with positive values indicating counter-clockwise orientation offset in degrees.
The rotation of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLC PCB used different variation of the same standard. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'FT Rotation Offset'* field - with positive values indicating counter-clockwise orientation offset in degrees.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation-jlc.png?raw=true" height=164>
@@ -85,16 +97,27 @@ If the JLC preview shows a footprint like this, enter a rotation offset of -90 t
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation.png?raw=true" height=420>
As the rotation offset is entered in the Schematic Editor, make sure to update your PCB with the changes made to the schematic before generating a new set of production files, otherwise your changes won't be reflected.
Use following table to quickly find out to which coordinate enter the correction based on JLC arrows clicks - depending on footprint rotation in KiCad PCB Editor status bar:
|KiCad footprint deg | x | y|
|----|----|----|
|0 deg, Front | right arrow | up arrow |
|0 deg, Back | left arrow | down arrow |
|180 deg, Front | left arrow | down arrow |
|180 deg, Back | right arrow | up arrow |
|90 deg, Front or Back | up arrow | left arrow |
|-90 deg, Front or Back | down arrow | right arrow |
> [!TIP]
> Single arrow press in JLC is 0.0635mm (= 1/400in) shift.
#### Primary Fields*:
| 'JLCPCB Rotation Offset' |
| 'FT Rotation Offset' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'JlcRotOffset' | 'JLCRotOffset' |
| 'Rotation Offset' | 'RotOffset' |
| --- | --- |
_The fields will be queried in the order denoted above._
@@ -102,56 +125,101 @@ _The fields will be queried in the order denoted above._
---
### ④ Offset Component Position
The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPB used different variation of the same standard. To the exception cases: add an 'JLCPCB Position Offset' field with an comma separated x,y position offset to correct it.
Use following table to quickly find out to which coordinate enter the correction based on JLC arrows clicks - depending on footprint rotation in Kicad PCB Editor status bar:
|Kicad footprint deg | x | y|
|----|----|----|
|0deg, Front | right arrow | up arrow |
|0deg, Back | left arrow | down arrow |
|180deg, Front | left arrow | down arrow |
|180deg, Back | right arrow | up arrow |
|90deg, Front or Back | up arrow | left arrow |
|-90deg, Front or Back | down arrow | right arrow |
For custom angles it's best to place also a temporary straight symbol to perform alignment.
Single arrow press in JLC is 0.0635mm (= 1/400in) shift.
The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPCB used different variation of the same standard. To the exception cases: add an 'FT Position Offset'* field with an comma separated x,y position offset to correct it.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/position.png?raw=true" height=420>
As the position offset is entered in the Schematic Editor, make sure to update your PCB with the changes made to the schematic before generating a new set of production files, otherwise your changes won't be reflected.
> [!TIP]
> Single arrow press in JLC equals to 0.0635mm (= 1/400in) shift.
#### Primary Fields*:
| 'JLCPCB Position Offset' |
| 'FT Position Offset' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'JlcPosOffset' | 'JLCPosOffset' |
| 'Position Offset' | 'PosOffset' |
| --- | --- |
_The fields will be queried in the order denoted above._
### ⑤ Override Component Layer
Some footprints may have their components defined on the opposite layer to there actual footprints. In these instances you can override mount side by using this field.
Values can be `top`, `bottom`, `t` or `b`.
### ⑤ Override Component Origin
The Fabrication Toolkit reports the position of each component based on an automatically selected point of reference. This default behavior can be overridden by adding an 'FT Origin'* field to the component.
#### Primary Fields*:
| 'JLCPCB Layer Override' |
| 'FT Origin' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'JlcLayerOverride' | 'JLCLayerOverride' |
| 'Origin' |
| --- |
The **Origin** field supports the following values:
- `Anchor` - Uses the footprint's anchor point, which can be modified in KiCad's footprint editor.
- `Center` - Uses the center of the bounding box formed by the footprint's pads.
### ⑥ Override Component Layer
Some footprints may have their components defined on the opposite layer to there actual footprints. In these instances you can override mount side by adding an 'FT Layer Override'* field to the component.
The **Layer Override** field supports the following values:
- `top`, `t` - Override footprint layer to the top.
- `bottom`, `b` - Override footprint layer to the bottom.
#### Primary Fields*:
| 'FT Layer Override' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'Layer Override' | 'LayerOverride' |
| --- | --- |
_The fields will be queried in the order denoted above._
## CLI
The plugin can also be used via the linux command line. This can be particularly useful if you need to embed the plugin into an automation pipeline or environment.
The plugin can be called with the command below:
```
python3 -m plugins.cli -p /myProject/myBoard.kicad_pcb
```
All the options from the GUI are also available via the cli interface:
```
python3 -m plugins.cli -h
usage: Fabrication Toolkit [-h] --path PATH [--additionalLayers LAYERS] [--user1VCut] [--user2AltVCut]
[--autoTranslate] [--autoFill] [--excludeDNP] [--allActiveLayers] [--archiveName NAME]
[--openBrowser] [--noBackup]
Generates JLCPCB production files from a KiCAD board file
options:
-h, --help show this help message and exit
--path PATH, -p PATH Path to KiCAD board file
--additionalLayers LAYERS, -aL LAYERS
Additional layers(comma-separated)
--user1VCut, -u1 Set User.1 as V-Cut layer
--user2AltVCut, -u2 Use User.2 for alternative Edge-Cut layer
--autoTranslate, -t Apply automatic position/rotation translations
--autoFill, -f Apply automatic fill for all zones
--excludeDNP, -e Exclude DNP components from BOM
--allActiveLayers, -aaL
Export all active layers instead of only commonly used ones
--archiveName NAME, -aN NAME
Name of the generated archives
--openBrowser, -b Open web browser with directory file overview after generation
--noBackup, -nB Do not create a backup of the project before generation
```
## Author
Benny Megidish
<iframe src="https://github.com/sponsors/electron-rare/card" title="Sponsor electron-rare" height="225" width="600" style="border: 0;"></iframe>
+225
View File
@@ -0,0 +1,225 @@
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/logo.svg?raw=true"
style="display:block margin-left: auto; margin-right: auto;" alt="JLC PCB Plug-in for KiCad">
<div align="center">
| **JLC PCB Plug-in for KiCad** |
|:-----:|
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/bennymeg)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/bennymeg/fabrication-toolkit/total)
</div>
## Features
1. Generates gerber files in correct format for production
2. Generates BOM file in correct format for production
3. Generates Pick and Place file in correct format for assembly
4. Generates IPC netlist file
## Installation
### Official Installation
Fabrication Toolkit is distributed with the official releases of KiCad 6+. Open the "Plugin and Content Manager" from the KiCad main menu and install the "<ins>Fabrication Toolkit</ins>" plugin from the selection list.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/manager.png?raw=true" height=275>
### Manual installation
Download the [latest release](https://github.com/bennymeg/JLC-Plugin-for-KiCad/releases) ZIP file. Open the "Plugin and Content Manager" from the KiCads main window and install the ZIP file via "Install from File".
## Usage
Click on the Fabrication Toolkit <img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/resources/icon.png?raw=true" style="margin-bottom: 8px;" alt="Logo" height=24> button on the top tool box inside KiCad pcb editor (pcbnew), choose the required options and click on 'Generate'.
> [!IMPORTANT]
> Ensure your board is synchronized before invoking this addon [**F8**].
## Options
Options can be set in the dialog that appears when the plugin is invoked. They are saved in a file called `fabrication-toolkit-options.json` in the project directory so that they are remembered between invocations of the plugin.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/options.png?raw=true" height=275>
__Archive name__: Name of the archive file to be generated. Can include text variables, such as e.g. `${TITLE}_${REVISION}`</br>
__Additional layers__: Comma-separated list of additional layers to include in the gerber archive.</br>
__Plot all active layers__: Whether to include all layers, instead of just the layers required by JLCPCB.</br>
__Set User.1 as V-Cut layer__: Merge User.1 layer with the Edge-Cut layer in production.</br>
__Use User.2 for an alternative Edge-Cut layer__: Use the User.2 instead of the Edge-Cut layer for the board outline in production.
This is useful if you need process edges or panelization during production but still want to keep the individual outline for prototyping, 3D model exports, or similar purposes.</br>
__Apply automatic translations__: Apply known translation fixes for common components.</br>
__Apply automatic fill for all zones__: Refill all zones before generation production files.</br>
__Exclude DNP components from BOM__: Exclude components the had been set a DNP from th BOM.</br>
__Generate Backups__: Generate Backup zip for the production files.</br>
## Attributes
> [!NOTE]
> All the attributes are entered in the Schematic Editor, make sure to update your PCB [**F8**] with the changes made to the schematic before generating a new set of production files, otherwise your changes won't be reflected.
### ① Include Component Part Number in Production Files
Add an 'LCSC Part #'* field with the LCSC component part number to the symbol's fields property.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/mpn.png?raw=true" height=420>
#### Primary Fields*:
| 'LCSC Part #' | 'LCSC Part' | 'LCSC PN' | 'LCSC P/N' | 'LCSC Part No.' | 'LCSC Part Number' | 'JLCPCB Part #' | 'JLCPCB Part' | 'JLCPCB PN' | 'JLCPCB P/N' | 'JLCPCB Part No.' | 'JLCPCB Part Number' |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'LCSC' | 'JLC' | 'MPN' | 'Mpn' | 'mpn' |
| --- | --- | --- | --- | --- |
_The fields will be queried in the order denoted above._
Empty fields are skipped so, for example, a 'MPN' field with a value will be used if
all earlier fields are missing or empty.
---
### ② Ignore Footprint in Production Files
Select 'Exclude from board' or 'Exclude from BOM' in the symbol's attributes property in order to ignore the footprint from the relevant file.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/attributes.png?raw=true" height=420>
Select 'Exclude from position files' or 'Exclude from BOM' in the footprint's fabrication attributes property in order to ignore the footprint from the relevant file.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/fabrication.png?raw=true" height=505>
---
### ③ Offset Component Rotation
The rotation of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLC PCB used different variation of the same standard. Most of the rotations may be corrected by the `rotations.cf` definitions. To the exception cases: add an 'FT Rotation Offset'* field - with positive values indicating counter-clockwise orientation offset in degrees.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation-jlc.png?raw=true" height=164>
If the JLC preview shows a footprint like this, enter a rotation offset of -90 to rotate pin 1 to the lower right corner.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/rotation.png?raw=true" height=420>
Use following table to quickly find out to which coordinate enter the correction based on JLC arrows clicks - depending on footprint rotation in KiCad PCB Editor status bar:
|KiCad footprint deg | x | y|
|----|----|----|
|0 deg, Front | right arrow | up arrow |
|0 deg, Back | left arrow | down arrow |
|180 deg, Front | left arrow | down arrow |
|180 deg, Back | right arrow | up arrow |
|90 deg, Front or Back | up arrow | left arrow |
|-90 deg, Front or Back | down arrow | right arrow |
> [!TIP]
> Single arrow press in JLC is 0.0635mm (= 1/400in) shift.
#### Primary Fields*:
| 'FT Rotation Offset' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'Rotation Offset' | 'RotOffset' |
| --- | --- |
_The fields will be queried in the order denoted above._
---
### ④ Offset Component Position
The position of components in KiCad Footprints does not always match the orientation in the JLC library because KiCad and JLCPCB used different variation of the same standard. To the exception cases: add an 'FT Position Offset'* field with an comma separated x,y position offset to correct it.
<img src="https://github.com/bennymeg/JLC-Plugin-for-KiCad/blob/master/assets/position.png?raw=true" height=420>
> [!TIP]
> Single arrow press in JLC equals to 0.0635mm (= 1/400in) shift.
#### Primary Fields*:
| 'FT Position Offset' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'Position Offset' | 'PosOffset' |
| --- | --- |
_The fields will be queried in the order denoted above._
### ⑤ Override Component Origin
The Fabrication Toolkit reports the position of each component based on an automatically selected point of reference. This default behavior can be overridden by adding an 'FT Origin'* field to the component.
#### Primary Fields*:
| 'FT Origin' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'Origin' |
| --- |
The **Origin** field supports the following values:
- `Anchor` - Uses the footprint's anchor point, which can be modified in KiCad's footprint editor.
- `Center` - Uses the center of the bounding box formed by the footprint's pads.
### ⑥ Override Component Layer
Some footprints may have their components defined on the opposite layer to there actual footprints. In these instances you can override mount side by adding an 'FT Layer Override'* field to the component.
The **Layer Override** field supports the following values:
- `top`, `t` - Override footprint layer to the top.
- `bottom`, `b` - Override footprint layer to the bottom.
#### Primary Fields*:
| 'FT Layer Override' |
| --- |
_The fields will be queried in the order denoted above._
#### Fallback Fields*:
| 'Layer Override' | 'LayerOverride' |
| --- | --- |
_The fields will be queried in the order denoted above._
## CLI
The plugin can also be used via the linux command line. This can be particularly useful if you need to embed the plugin into an automation pipeline or environment.
The plugin can be called with the command below:
```
python3 -m plugins.cli -p /myProject/myBoard.kicad_pcb
```
All the options from the GUI are also available via the cli interface:
```
python3 -m plugins.cli -h
usage: Fabrication Toolkit [-h] --path PATH [--additionalLayers LAYERS] [--user1VCut] [--user2AltVCut]
[--autoTranslate] [--autoFill] [--excludeDNP] [--allActiveLayers] [--archiveName NAME]
[--openBrowser] [--noBackup]
Generates JLCPCB production files from a KiCAD board file
options:
-h, --help show this help message and exit
--path PATH, -p PATH Path to KiCAD board file
--additionalLayers LAYERS, -aL LAYERS
Additional layers(comma-separated)
--user1VCut, -u1 Set User.1 as V-Cut layer
--user2AltVCut, -u2 Use User.2 for alternative Edge-Cut layer
--autoTranslate, -t Apply automatic position/rotation translations
--autoFill, -f Apply automatic fill for all zones
--excludeDNP, -e Exclude DNP components from BOM
--allActiveLayers, -aaL
Export all active layers instead of only commonly used ones
--archiveName NAME, -aN NAME
Name of the generated archives
--openBrowser, -b Open web browser with directory file overview after generation
--noBackup, -nB Do not create a backup of the project before generation
```
## Author
Benny Megidish
<iframe src="https://github.com/sponsors/electron-rare/card" title="Sponsor electron-rare" height="225" width="600" style="border: 0;"></iframe>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 284 KiB

+1 -1
View File
@@ -30,7 +30,7 @@
],
"versions": [
{
"version": "4.5.0",
"version": "5.2.0",
"status": "stable",
"kicad_version": "6.00"
}
+42
View File
@@ -0,0 +1,42 @@
import argparse as ap
from .thread import ProcessThread
from .options import *
if __name__ == '__main__':
parser = ap.ArgumentParser(prog="Fabrication Toolkit",
description="Generates JLCPCB production files from a KiCAD board file")
parser.add_argument("--path", "-p", type=str, help="Path to KiCAD board file", required=True)
parser.add_argument("--additionalLayers", "-aL", type=str, help="Additional layers(comma-separated)", metavar="LAYERS")
parser.add_argument("--user1VCut", "-u1", action="store_true", help="Set User.1 as V-Cut layer")
parser.add_argument("--user2AltVCut", "-u2", action="store_true", help="Set User.2 as alternative Edge-Cut layer")
parser.add_argument("--autoTranslate", "-t", action="store_true", help="Apply automatic position/rotation translations")
parser.add_argument("--autoFill", "-f", action="store_true", help="Apply automatic fill for all zones")
parser.add_argument("--excludeDNP", "-e", action="store_true", help="Exclude DNP components from BOM")
parser.add_argument("--allActiveLayers", "-aaL",action="store_true", help="Export all active layers instead of only commonly used ones")
parser.add_argument("--archiveName", "-aN", type=str, help="Name of the generated archives", metavar="NAME")
parser.add_argument("--openBrowser", "-b", action="store_true", help="Open webbrowser with directory file overview after generation")
parser.add_argument("--nonInteractive", "-nI" ,action="store_true", help="Run in non-Interactive mode. Useful in CI/CD environment.")
parser.add_argument("--noBackup", "-nB", action="store_true", help="Do not create backup files")
args = parser.parse_args()
options = dict()
options[AUTO_TRANSLATE_OPT] = args.autoTranslate
options[AUTO_FILL_OPT] = args.autoFill
options[EXCLUDE_DNP_OPT] = args.excludeDNP
options[EXTEND_EDGE_CUT_OPT] = args.user1VCut
options[ALTERNATIVE_EDGE_CUT_OPT] = args.user2AltVCut
options[ALL_ACTIVE_LAYERS_OPT] = args.allActiveLayers
options[ARCHIVE_NAME] = args.archiveName
options[EXTRA_LAYERS] = args.additionalLayers
options[BACKUP_OPT] = not args.noBackup
openBrowser = args.openBrowser
nonInteractive = args.nonInteractive
path = args.path
ProcessThread(wx=None, cli=path, options=options, openBrowser=openBrowser, nonInteractive=nonInteractive)
+14 -1
View File
@@ -10,4 +10,17 @@ gerberArchiveName = 'gerbers.zip'
outputFolder = 'production'
bomRowLimit = 200
optionsFileName = 'fabrication-toolkit-options.json'
optionsFileName = 'fabrication-toolkit-options.json'
standardLayers = [ pcbnew.F_Cu, pcbnew.B_Cu,
pcbnew.In1_Cu, pcbnew.In2_Cu, pcbnew.In3_Cu, pcbnew.In4_Cu, pcbnew.In5_Cu,
pcbnew.In6_Cu, pcbnew.In7_Cu, pcbnew.In8_Cu, pcbnew.In9_Cu, pcbnew.In10_Cu,
pcbnew.In11_Cu, pcbnew.In12_Cu, pcbnew.In13_Cu, pcbnew.In14_Cu, pcbnew.In15_Cu,
pcbnew.In16_Cu, pcbnew.In17_Cu, pcbnew.In18_Cu, pcbnew.In19_Cu, pcbnew.In20_Cu,
pcbnew.In21_Cu, pcbnew.In22_Cu, pcbnew.In23_Cu, pcbnew.In24_Cu, pcbnew.In25_Cu,
pcbnew.In26_Cu, pcbnew.In27_Cu, pcbnew.In28_Cu, pcbnew.In29_Cu, pcbnew.In30_Cu,
pcbnew.F_SilkS, pcbnew.B_SilkS,
pcbnew.F_Mask, pcbnew.B_Mask,
pcbnew.F_Paste, pcbnew.B_Paste,
pcbnew.Edge_Cuts
]
+5 -1
View File
@@ -4,4 +4,8 @@ EXCLUDE_DNP_OPT = "EXCLUDE DNP"
OUTPUT_NAME_OPT = "OUTPUT NAME"
EXTEND_EDGE_CUT_OPT = "EXTEND_EDGE_CUT"
ALTERNATIVE_EDGE_CUT_OPT = "ALTERNATIVE_EDGE_CUT"
EXTRA_LAYERS = "EXTRA_LAYERS"
ALL_ACTIVE_LAYERS_OPT = "ALL_ACTIVE_LAYERS"
OPEN_BROWSER_OPT = "OPEN BROWSER"
ARCHIVE_NAME = "ARCHIVE_NAME"
EXTRA_LAYERS = "EXTRA_LAYERS"
BACKUP_OPT = "BACKUP_OPT"
+43 -9
View File
@@ -1,10 +1,10 @@
import os
import wx
import pcbnew # type: ignore
import pcbnew # type: ignore
from .thread import ProcessThread
from .events import StatusEvent
from .options import AUTO_FILL_OPT, AUTO_TRANSLATE_OPT, EXCLUDE_DNP_OPT, EXTEND_EDGE_CUT_OPT, ALTERNATIVE_EDGE_CUT_OPT, EXTRA_LAYERS
from .options import AUTO_FILL_OPT, AUTO_TRANSLATE_OPT, EXCLUDE_DNP_OPT, EXTEND_EDGE_CUT_OPT, ALTERNATIVE_EDGE_CUT_OPT, EXTRA_LAYERS, ALL_ACTIVE_LAYERS_OPT, ARCHIVE_NAME, OPEN_BROWSER_OPT, BACKUP_OPT
from .utils import load_user_options, save_user_options, get_layer_names
@@ -19,7 +19,7 @@ class KiCadToJLCForm(wx.Frame):
pos=wx.DefaultPosition,
size=wx.DefaultSize,
style=wx.DEFAULT_DIALOG_STYLE)
# self.app = wx.PySimpleApp()
icon = wx.Icon(os.path.join(os.path.dirname(__file__), 'icon.png'))
self.SetIcon(icon)
@@ -29,11 +29,15 @@ class KiCadToJLCForm(wx.Frame):
userOptions = load_user_options({
EXTRA_LAYERS: "",
ALL_ACTIVE_LAYERS_OPT: False,
ARCHIVE_NAME: "",
EXTEND_EDGE_CUT_OPT: False,
ALTERNATIVE_EDGE_CUT_OPT: False,
AUTO_TRANSLATE_OPT: True,
AUTO_FILL_OPT: True,
EXCLUDE_DNP_OPT: False
EXCLUDE_DNP_OPT: False,
OPEN_BROWSER_OPT: True,
BACKUP_OPT: True,
})
self.mOptionsLabel = wx.StaticText(self, label='Options:')
@@ -41,21 +45,30 @@ class KiCadToJLCForm(wx.Frame):
layers = get_layer_names(pcbnew.GetBoard())
self.mAdditionalLayersControl = wx.TextCtrl(self, size=wx.Size(600, 50))
self.mAdditionalLayersControl.Hint = "Additional layers (comma-separated)"
self.mAdditionalLayersControl.Hint = "Additional layers (comma-separated) [optional]"
self.mAdditionalLayersControl.AutoComplete(layers)
self.mAdditionalLayersControl.Enable()
self.mAdditionalLayersControl.SetValue(userOptions[EXTRA_LAYERS])
self.mArchiveNameControl = wx.TextCtrl(self, size=wx.Size(600, 50))
self.mArchiveNameControl.Hint = "Archive name (e.g. ${TITLE}_${REVISION}) [optional]"
self.mArchiveNameControl.Enable()
self.mArchiveNameControl.SetValue(userOptions[ARCHIVE_NAME])
self.mAllActiveLayersCheckbox = wx.CheckBox(self, label='Plot all active layers')
self.mAllActiveLayersCheckbox.SetValue(userOptions[ALL_ACTIVE_LAYERS_OPT])
self.mExtendEdgeCutsCheckbox = wx.CheckBox(self, label='Set User.1 as V-Cut layer')
self.mExtendEdgeCutsCheckbox.SetValue(userOptions[EXTEND_EDGE_CUT_OPT])
self.mAlternativeEdgeCutsCheckbox = wx.CheckBox(self, label='Use User.2 for alternative Edge-Cut layer')
self.mAlternativeEdgeCutsCheckbox = wx.CheckBox(self, label='Set User.2 as alternative Edge-Cut layer')
self.mAlternativeEdgeCutsCheckbox.SetValue(userOptions[ALTERNATIVE_EDGE_CUT_OPT])
self.mAutomaticTranslationCheckbox = wx.CheckBox(self, label='Apply automatic translations')
self.mAutomaticTranslationCheckbox = wx.CheckBox(self, label='Apply automatic component translations')
self.mAutomaticTranslationCheckbox.SetValue(userOptions[AUTO_TRANSLATE_OPT])
self.mAutomaticFillCheckbox = wx.CheckBox(self, label='Apply automatic fill for all zones')
self.mAutomaticFillCheckbox.SetValue(userOptions[AUTO_FILL_OPT])
self.mExcludeDnpCheckbox = wx.CheckBox(self, label='Exclude DNP components from BOM')
self.mExcludeDnpCheckbox.SetValue(userOptions[EXCLUDE_DNP_OPT])
self.mOpenBrowserCheckbox = wx.CheckBox(self, label='Open browser after generation')
self.mOpenBrowserCheckbox.SetValue(userOptions[OPEN_BROWSER_OPT])
self.mBackupCheckbox = wx.CheckBox(self, label='Generate backup files')
self.mBackupCheckbox.SetValue(userOptions[BACKUP_OPT])
self.mGaugeStatus = wx.Gauge(
self, wx.ID_ANY, 100, wx.DefaultPosition, wx.Size(600, 20), wx.GA_HORIZONTAL)
@@ -69,12 +82,16 @@ class KiCadToJLCForm(wx.Frame):
boxSizer.Add(self.mOptionsLabel, 0, wx.ALL, 5)
# boxSizer.Add(self.mOptionsSeparator, 0, wx.ALL, 5)
boxSizer.Add(self.mArchiveNameControl, 0, wx.ALL, 5)
boxSizer.Add(self.mAdditionalLayersControl, 0, wx.ALL, 5)
boxSizer.Add(self.mAllActiveLayersCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mExtendEdgeCutsCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mAlternativeEdgeCutsCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mAutomaticTranslationCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mAutomaticFillCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mExcludeDnpCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mOpenBrowserCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mBackupCheckbox, 0, wx.ALL, 5)
boxSizer.Add(self.mGaugeStatus, 0, wx.ALL, 5)
boxSizer.Add(self.mGenerateButton, 0, wx.ALL, 5)
@@ -84,25 +101,42 @@ class KiCadToJLCForm(wx.Frame):
self.Centre(wx.BOTH)
# Bind the ESC key event to a handler
self.Bind(wx.EVT_CHAR_HOOK, self.onKey)
# Close the dialog when pressing the ESC key
def onKey(self, event):
if event.GetKeyCode() == wx.WXK_ESCAPE:
self.Close(True)
else:
event.Skip()
def onGenerateButtonClick(self, event):
options = dict()
options[ARCHIVE_NAME] = self.mArchiveNameControl.GetValue()
options[EXTRA_LAYERS] = self.mAdditionalLayersControl.GetValue()
options[ALL_ACTIVE_LAYERS_OPT] = self.mAllActiveLayersCheckbox.GetValue()
options[EXTEND_EDGE_CUT_OPT] = self.mExtendEdgeCutsCheckbox.GetValue()
options[ALTERNATIVE_EDGE_CUT_OPT] = self.mAlternativeEdgeCutsCheckbox.GetValue()
options[AUTO_TRANSLATE_OPT] = self.mAutomaticTranslationCheckbox.GetValue()
options[AUTO_FILL_OPT] = self.mAutomaticFillCheckbox.GetValue()
options[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
options[OPEN_BROWSER_OPT] = self.mOpenBrowserCheckbox.GetValue()
options[BACKUP_OPT] = self.mBackupCheckbox.GetValue()
save_user_options(options)
self.mOptionsLabel.Hide()
self.mArchiveNameControl.Hide()
self.mAdditionalLayersControl.Hide()
self.mAllActiveLayersCheckbox.Hide()
self.mExtendEdgeCutsCheckbox.Hide()
self.mAlternativeEdgeCutsCheckbox.Hide()
self.mAutomaticTranslationCheckbox.Hide()
self.mAutomaticFillCheckbox.Hide()
self.mExcludeDnpCheckbox.Hide()
self.mOpenBrowserCheckbox.Hide()
self.mBackupCheckbox.Hide()
self.mGenerateButton.Hide()
self.mGaugeStatus.Show()
@@ -110,7 +144,7 @@ class KiCadToJLCForm(wx.Frame):
self.SetTitle('Fabrication Toolkit (Processing...)')
StatusEvent.invoke(self, self.updateDisplay)
ProcessThread(self, options)
ProcessThread(self, options, openBrowser=options[OPEN_BROWSER_OPT])
def updateDisplay(self, status):
if status.data == -1:
+135 -50
View File
@@ -17,10 +17,13 @@ from .utils import footprint_has_field, footprint_get_field, get_plot_plan
# Application definitions.
from .config import *
class ProcessManager:
def __init__(self):
self.board = pcbnew.GetBoard()
def __init__(self, board = None):
# if no board is already loaded by cli mode getBoard from kicad environment
if board is None:
self.board = pcbnew.GetBoard()
else:
self.board = board
self.bom = []
self.components = []
self.__rotation_db = self.__read_rotation_db()
@@ -42,8 +45,9 @@ class ProcessManager:
# Finally rebuild the connectivity db
self.board.BuildConnectivity()
def generate_gerber(self, temp_dir, extra_layers, extend_edge_cuts, alternative_edge_cuts):
def generate_gerber(self, temp_dir, extra_layers, extend_edge_cuts, alternative_edge_cuts, all_active_layers):
'''Generate the Gerber files.'''
original_settings = self.board.GetDesignSettings()
settings = self.board.GetDesignSettings()
settings.m_SolderMaskMargin = 50000
settings.m_SolderMaskToCopperClearance = 5000
@@ -64,7 +68,7 @@ class ProcessManager:
plot_options.SetSubtractMaskFromSilk(True)
plot_options.SetUseGerberX2format(False)
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
if hasattr(plot_options, "SetExcludeEdgeLayer"):
plot_options.SetExcludeEdgeLayer(True)
@@ -74,9 +78,9 @@ class ProcessManager:
extra_layers = []
for layer_info in get_plot_plan(self.board):
if self.board.IsLayerEnabled(layer_info[1]) or layer_info[0] in extra_layers:
if (self.board.IsLayerEnabled(layer_info[1]) and (all_active_layers or layer_info[1] in standardLayers)) or layer_info[0] in extra_layers:
plot_controller.SetLayer(layer_info[1])
plot_controller.OpenPlotfile(layer_info[0], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
plot_controller.OpenPlotfile(layer_info[2], pcbnew.PLOT_FORMAT_GERBER, layer_info[2])
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'PlotLayers') and (extend_edge_cuts or alternative_edge_cuts):
seq = pcbnew.LSEQ()
@@ -95,6 +99,7 @@ class ProcessManager:
plot_controller.PlotLayer()
plot_controller.ClosePlot()
settings = original_settings
def generate_drills(self, temp_dir):
'''Generate the drill file.'''
@@ -114,24 +119,46 @@ class ProcessManager:
netlist_writer = pcbnew.IPC356D_WRITER(self.board)
netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
def _get_footprint_position(self, footprint):
"""Calculate position based on center of bounding box."""
position = footprint.GetPosition()
attributes = footprint.GetAttributes()
def _get_footprint_rotation(self, footprint):
return footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
if attributes & pcbnew.FP_SMD:
def _get_footprint_position(self, footprint):
"""Calculate position based on center of pads / bounding box."""
origin_type = self._get_origin_from_footprint(footprint)
footprint_rotation = self._get_footprint_rotation(footprint)
footprint_rotated = footprint_rotation % 90 != 0
# if the footprint is not rotated by a multiple of 90 degrees, the bounding boxes will be off, so we create a temporary copy that is rotated to 0
if footprint_rotated:
footprint = footprint.Duplicate()
footprint.SetOrientationDegrees(0)
if origin_type == 'Anchor':
position = footprint.GetPosition()
elif attributes & pcbnew.FP_THROUGH_HOLE:
position = footprint.GetBoundingBox(False, False).GetCenter()
else: # handle Unspecified footprint type
else: # if type_origin == 'Center' or anything else
pads = footprint.Pads()
if len(pads) > 0:
# get bounding box based on pads only to ignore non-copper layers, e.g. silkscreen
bbox = pads[0].GetBoundingBox() # start with small bounding box
for pad in pads:
bbox.Merge(pad.GetBoundingBox()) # expand bounding box
position = bbox.GetCenter()
else:
position = footprint.GetPosition() # if we have no pads we fallback to anchor
if footprint_rotated:
# now we determine the offset of the "true" position relative to the "KiCAD" position & apply the footprints rotation
raw_pos = footprint.GetPosition()
relative_position = (position[0] - raw_pos[0], position[1] - raw_pos[1])
rsin = math.sin(footprint_rotation / 180 * math.pi)
rcos = math.cos(footprint_rotation / 180 * math.pi)
relative_position = ( relative_position[0] * rcos + relative_position[1] * rsin, -relative_position[0] * rsin + relative_position[1] * rcos )
position = (raw_pos[0] + relative_position[0], raw_pos[1] + relative_position[1])
return position
@@ -163,7 +190,14 @@ class ProcessManager:
except AttributeError:
footprint_name = str(footprint.GetFPID().GetLibItemName())
layer = self._get_top_or_bottom_side_override_from_footprint(footprint)
# Get the library nickname when available
lib_nickname = None
try:
lib_nickname = str(footprint.GetFPID().GetLibNickname())
except AttributeError:
pass
layer = self._get_layer_override_from_footprint(footprint)
# mount_type = {
# 0: 'smt',
@@ -171,8 +205,8 @@ class ProcessManager:
# 2: 'unspecified'
# }.get(footprint.GetAttributes())
is_dnp = (footprint_has_field(footprint, 'dnp')
or (footprint.GetValue().upper() == 'DNP')
is_dnp = (footprint_has_field(footprint, 'dnp')
or (footprint.GetValue().upper() == 'DNP')
or getattr(footprint, 'IsDNP', bool)())
skip_dnp = exclude_dnp and is_dnp
@@ -187,12 +221,16 @@ class ProcessManager:
position = self._get_footprint_position(footprint)
mid_x = (position[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
mid_y = (position[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0
rotation = footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
rotation_offset_db = self._get_rotation_from_db(footprint_name) # internal database offset
rotation_offset_manual = self._get_rotation_offset_from_footprint(footprint) # explicated offset by the designer
rotation = self._get_footprint_rotation(footprint)
rotation_offset_db = self._get_rotation_from_db(footprint_name, lib_nickname) # Try with lib_nickname if available
rotation_offset_manual = self._get_rotation_offset_from_footprint(footprint)
# position offset needs to take rotation into account
pos_offset = self._get_position_offset_from_footprint(footprint)
if auto_translate:
pos_offset_db = self._get_position_offset_from_db(footprint_name, lib_nickname) # Try with lib_nickname if available
pos_offset = (pos_offset[0] + pos_offset_db[0], pos_offset[1] + pos_offset_db[1])
rsin = math.sin(rotation / 180 * math.pi)
rcos = math.cos(rotation / 180 * math.pi)
@@ -289,7 +327,7 @@ class ProcessManager:
os.remove(os.path.join(temp_dir, item))
return temp_file
""" Private """
def __read_rotation_db(self, filename: str = os.path.join(os.path.dirname(__file__), 'transformations.csv')) -> dict[str, float]:
@@ -309,7 +347,6 @@ class ProcessManager:
rowNum = rowNum + 1
# First row is header row, skip.
if rowNum == 1:
skipFirst = False
continue
# If there was too many fields, throw an exception.
@@ -347,14 +384,18 @@ class ProcessManager:
db[rowNum]['name'] = row['footprint']
db[rowNum]['rotation'] = rotation
db[rowNum]['x'] = delta_x
db[rowNum]['y'] = delta_y
db[rowNum]['y'] = delta_y
return db
def _get_rotation_from_db(self, footprint: str) -> float:
'''Get the rotation to be added from the database file.'''
# Look for regular expression math of the footprint name and not its root library.
def _get_rotation_from_db(self, footprint: str, lib_nickname: str = None) -> float:
'''Get the rotation to be added from the database file.
Args:
footprint: The footprint name
lib_nickname: The library nickname, if available
'''
# First try with the standard approach for backward compatibility
for entry in self.__rotation_db.items():
# If the expression in the DB contains a :, search for it literally.
if (re.search(':', entry[1]['name'])):
@@ -372,13 +413,23 @@ class ProcessManager:
if (re.search(entry[1]['name'], check)):
return float(entry[1]['rotation'])
# If no match found and we have a library nickname, try matching against that
if lib_nickname:
for entry in self.__rotation_db.items():
if (re.search(entry[1]['name'], lib_nickname)):
return float(entry[1]['rotation'])
# Not found, no rotation.
return 0.0
def _get_position_offset_from_db(self, footprint: str) -> Tuple[float, float]:
'''Get the rotation to be added from the database file.'''
# Look for regular expression math of the footprint name and not its root library.
def _get_position_offset_from_db(self, footprint: str, lib_nickname: str = None) -> Tuple[float, float]:
'''Get the position offset to be added from the database file.
Args:
footprint: The footprint name
lib_nickname: The library nickname, if available
'''
# First try with the standard approach for backward compatibility
for entry in self.__rotation_db.items():
# If the expression in the DB contains a :, search for it literally.
if (re.search(':', entry[1]['name'])):
@@ -394,26 +445,35 @@ class ProcessManager:
else:
check = footprint_segments[1]
if (re.search(entry[1]['name'], check)):
return ( float(entry[1]['x']), float(entry[1]['y']) )
return (float(entry[1]['x']), float(entry[1]['y']))
# If no match found and we have a library nickname, try matching against that
if lib_nickname:
for entry in self.__rotation_db.items():
if (re.search(entry[1]['name'], lib_nickname)):
return (float(entry[1]['x']), float(entry[1]['y']))
# Not found, no delta.
return (0.0, 0.0)
def _get_mpn_from_footprint(self, footprint) -> str:
''''Get the MPN/LCSC stock code from standard symbol fields.'''
keys = ['LCSC Part #', 'JLCPCB Part #']
fallback_keys = ['LCSC Part', 'JLC Part', 'LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
supplier_names = ['LCSC', 'JLCPCB']
pn_abbrevs = ['Part #', 'Part', 'PN', 'P/N', 'Part No.', 'Part Number']
keys = [(sn + " " + abr) for sn in supplier_names for abr in pn_abbrevs]
fallback_keys = ['LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
if footprint_has_field(footprint, 'dnp'):
return 'DNP'
for key in keys + fallback_keys:
if footprint_has_field(footprint, key):
if footprint_has_field(footprint, key) and '' != footprint_get_field(footprint, key):
return footprint_get_field(footprint, key)
def _get_top_or_bottom_side_override_from_footprint(self, footprint) -> str:
keys = ['JLCPCB Layer Override']
fallback_keys = ['JlcLayerOverride', 'JLCLayerOverride']
def _get_layer_override_from_footprint(self, footprint) -> str:
'''Get the layer override from standard symbol fields.'''
keys = ['FT Layer Override']
fallback_keys = ['Layer Override', 'LayerOverride']
layer = {
pcbnew.F_Cu: 'top',
@@ -423,19 +483,20 @@ class ProcessManager:
for key in keys + fallback_keys:
if footprint_has_field(footprint, key):
temp_layer = footprint_get_field(footprint, key)
if (temp_layer[0] == 'b' or temp_layer[0] == 'B'):
layer = "bottom"
break
elif (temp_layer[0] == 't' or temp_layer[0] == 'T'):
layer = "top"
break
if len(temp_layer) > 0:
if (temp_layer[0] == 'b' or temp_layer[0] == 'B'):
layer = "bottom"
break
elif (temp_layer[0] == 't' or temp_layer[0] == 'T'):
layer = "top"
break
return layer
def _get_rotation_offset_from_footprint(self, footprint) -> float:
'''Get the rotation from standard symbol fields.'''
keys = ['JLCPCB Rotation Offset']
fallback_keys = ['JlcRotOffset', 'JLCRotOffset']
'''Get the rotation offset from standard symbol fields.'''
keys = ['FT Rotation Offset']
fallback_keys = ['Rotation Offset', 'RotOffset']
offset = ""
@@ -453,8 +514,9 @@ class ProcessManager:
raise RuntimeError("Rotation offset of {} is not a valid number".format(footprint.GetReference()))
def _get_position_offset_from_footprint(self, footprint) -> Tuple[float, float]:
keys = ['JLCPCB Position Offset']
fallback_keys = ['JlcPosOffset', 'JLCPosOffset']
'''Get the position offset from standard symbol fields.'''
keys = ['FT Position Offset']
fallback_keys = ['Position Offset', 'PosOffset']
offset = ""
@@ -469,9 +531,32 @@ class ProcessManager:
try:
offset = offset.split(",")
return (float(offset[0]), float(offset[1]))
except Exception as e:
except Exception:
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
def _get_origin_from_footprint(self, footprint) -> float:
'''Get the origin from standard symbol fields.'''
keys = ['FT Origin']
fallback_keys = ['Origin']
attributes = footprint.GetAttributes()
# determine origin type by package type
if attributes & pcbnew.FP_SMD:
origin_type = 'Anchor'
else:
origin_type = 'Center'
for key in keys + fallback_keys:
if footprint_has_field(footprint, key):
origin_type_override = str(footprint_get_field(footprint, key)).strip().capitalize()
if origin_type_override in ['Anchor', 'Center']:
origin_type = origin_type_override
break
return origin_type
def _normalize_footprint_name(self, footprint) -> str:
# replace footprint names of resistors, capacitors, inductors, diodes, LEDs, fuses etc, with the footprint size only
pattern = re.compile(r'^(\w*_SMD:)?\w{1,4}_(\d+)_\d+Metric.*$')
+83 -13
View File
@@ -5,22 +5,65 @@ import shutil
import tempfile
import webbrowser
import datetime
import logging
from threading import Thread
from .events import StatusEvent
from .process import ProcessManager
from .config import *
from .options import *
from .utils import print_cli_progress_bar
class ProcessThread(Thread):
def __init__(self, wx, options):
def __init__(self, wx, options, cli = None, openBrowser = True, nonInteractive = False):
Thread.__init__(self)
self.process_manager = ProcessManager()
# prevent use of cli and graphical mode at the same time
if (wx is None and cli is None) or (wx is not None and cli is not None):
logging.error("Specify either graphical or cli use!")
return
if cli is not None:
try:
self.board = pcbnew.LoadBoard(cli)
except Exception as e:
logging.error("Fabrication Toolkit - Error" + str(e))
return
else:
self.board = None
self.process_manager = ProcessManager(self.board)
self.wx = wx
self.cli = cli
self.options = options
self.openBrowser = openBrowser
self.nonInteractive = nonInteractive
self.start()
def expandTextVariables(self, string):
titleBlock = pcbnew.GetBoard().GetTitleBlock()
titleBlockVars = {
"ISSUE_DATE": titleBlock.GetDate(),
"CURRENT_DATE": datetime.datetime.now().strftime('%Y-%m-%d'),
"REVISION": titleBlock.GetRevision(),
"TITLE": titleBlock.GetTitle(),
"COMPANY": titleBlock.GetCompany(),
}
for comment_index in range(9):
titleBlockVars[f"COMMENT{comment_index + 1}"] = titleBlock.GetComment(comment_index)
for var, val in titleBlockVars.items():
string = string.replace(f"${{{var}}}", val)
if (hasattr(self.process_manager.board, "GetProject") and hasattr(pcbnew, "ExpandTextVars")):
project = self.process_manager.board.GetProject()
string = pcbnew.ExpandTextVars(string, project)
return string
def run(self):
# initializing
self.progress(0)
@@ -40,7 +83,8 @@ class ProcessThread(Thread):
# generate gerber
self.progress(20)
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS], self.options[EXTEND_EDGE_CUT_OPT], self.options[ALTERNATIVE_EDGE_CUT_OPT])
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS], self.options[EXTEND_EDGE_CUT_OPT],
self.options[ALTERNATIVE_EDGE_CUT_OPT], self.options[ALL_ACTIVE_LAYERS_OPT])
# generate drill file
self.progress(30)
@@ -69,7 +113,10 @@ class ProcessThread(Thread):
shutil.rmtree(temp_dir_gerber)
temp_file = os.path.join(temp_dir, os.path.basename(temp_file))
except Exception as e:
wx.MessageBox(str(e), "Fabrication Toolkit - Error", wx.OK | wx.ICON_ERROR)
if self.wx is None:
logging.error("Fabrication Toolkit - Error" + str(e))
else:
wx.MessageBox(str(e), "Fabrication Toolkit - Error", wx.OK | wx.ICON_ERROR)
self.progress(-1)
return
@@ -106,23 +153,46 @@ class ProcessThread(Thread):
os.makedirs(output_path)
# rename gerber archive
gerberArchiveName = ProcessManager.normalize_filename("_".join(("{} {}".format(title or filename, revision or '').strip() + '.zip').split()))
if self.options[ARCHIVE_NAME]:
baseName = self.expandTextVariables(self.options[ARCHIVE_NAME])
else:
baseName = "{} {}".format(title or filename, revision or '')
gerberArchiveName = ProcessManager.normalize_filename("_".join((baseName.strip() + '.zip').split()))
os.rename(temp_file, os.path.join(temp_dir, gerberArchiveName))
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
backup_name = ProcessManager.normalize_filename("_".join(("{} {} {}".format(title or filename, revision or '', timestamp).strip()).split()))
shutil.make_archive(os.path.join(output_path, 'backups', backup_name), 'zip', temp_dir)
if self.options[ARCHIVE_NAME]:
if os.path.exists(os.path.join(temp_dir, designatorsFileName)):
os.rename(os.path.join(temp_dir, designatorsFileName), os.path.join(temp_dir, ProcessManager.normalize_filename("_".join((baseName.strip() + '_designators.csv').split()))))
if os.path.exists(os.path.join(temp_dir, placementFileName)):
os.rename(os.path.join(temp_dir, placementFileName), os.path.join(temp_dir, ProcessManager.normalize_filename("_".join((baseName.strip() + '_positions.csv').split()))))
if os.path.exists(os.path.join(temp_dir, bomFileName)):
os.rename(os.path.join(temp_dir, bomFileName), os.path.join(temp_dir, ProcessManager.normalize_filename("_".join((baseName.strip() + '_bom.csv').split()))))
# Make a backup as long as the BACKUP_OPT flag is set.
if not self.options[BACKUP_OPT]:
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
backup_name = ProcessManager.normalize_filename("_".join(("{} {}".format(baseName, timestamp).strip()).split()))
shutil.make_archive(os.path.join(output_path, 'backups', backup_name), 'zip', temp_dir)
# copy to & open output dir
try:
shutil.copytree(temp_dir, output_path, dirs_exist_ok=True)
webbrowser.open("file://%s" % (output_path))
if self.openBrowser:
webbrowser.open("file://%s" % (output_path))
shutil.rmtree(temp_dir)
except Exception as e:
webbrowser.open("file://%s" % (temp_dir))
except Exception:
if self.openBrowser:
webbrowser.open("file://%s" % (temp_dir))
self.progress(-1)
if self.wx is None:
self.progress(100)
else:
self.progress(-1)
def progress(self, percent):
wx.PostEvent(self.wx, StatusEvent(percent))
if self.wx is None:
if not self.nonInteractive:
print_cli_progress_bar(percent, prefix = 'Progress:', suffix = 'Complete', length = 50)
else:
wx.PostEvent(self.wx, StatusEvent(percent))
+33 -3
View File
@@ -7,6 +7,9 @@ import wx
def get_version():
return float('.'.join(pcbnew.GetBuildVersion().split(".")[0:2])) # e.g GetBuildVersion(): e.g. '7.99.0-3969-gc5ac2337e4'
def is_v10(version = get_version()):
return version >= 9.99 and version < 10.99
def is_v9(version = get_version()):
return version >= 8.99 and version < 9.99
@@ -22,7 +25,9 @@ def is_v6(version = get_version()):
def footprint_has_field(footprint, field_name):
version = get_version()
if is_v8(version) or is_v9(version):
if is_v10(version):
return footprint.HasField(field_name)
elif is_v8(version) or is_v9(version):
return footprint.HasFieldByName(field_name)
else:
return footprint.HasProperty(field_name)
@@ -30,7 +35,9 @@ def footprint_has_field(footprint, field_name):
def footprint_get_field(footprint, field_name):
version = get_version()
if is_v8(version) or is_v9(version):
if is_v10(version):
return footprint.GetField(field_name).GetText()
elif is_v8(version) or is_v9(version):
return footprint.GetFieldByName(field_name).GetText()
else:
return footprint.GetProperty(field_name)
@@ -72,7 +79,30 @@ def get_plot_plan(board, active_only=True):
layers.append((layer_std_name, i, layer_name))
return layers
def get_layer_names(board, active_only=True):
"""Returns a list of (active) layer names of the current board"""
plotPlan = get_plot_plan(board, active_only)
return [layer_info[0] for layer_info in plotPlan]
return [layer_info[0] for layer_info in plotPlan]
def print_cli_progress_bar(percent, prefix = '', suffix = '', decimals = 1, length = 100, fill = '', printEnd = "\r"):
"""
Call in a loop to create terminal progress bar string
@params:
percent - Required : current percentage (Int)
prefix - Optional : prefix string (Str)
suffix - Optional : suffix string (Str)
decimals - Optional : positive number of decimals in percent complete (Int)
length - Optional : character length of bar (Int)
fill - Optional : bar fill character (Str)
printEnd - Optional : end character (e.g. "\r", "\r\n") (Str)
"""
if percent == -1:
percent = 0
filledLength = int(length * (percent / 100 ))
bar = fill * filledLength + '-' * (length - filledLength)
percent2dec = "%.2f" % percent
print(f'\r{prefix} |{bar}| {percent2dec}% {suffix}', end = printEnd)
# Print New Line on Complete
if percent == 100:
print()