Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2402288a2b | |||
| 51fe5fc3f4 | |||
| 8c4c341b10 | |||
| 605756ddb1 | |||
| 7ac0af4ccf | |||
| 8c3a4ba784 | |||
| 3f9d135e66 | |||
| 1e25598c4e | |||
| 6393fd89d8 | |||
| 87fe0fc675 | |||
| a20484abc5 | |||
| 3381050309 | |||
| 6a2804ef19 | |||
| d047824964 | |||
| da738d6423 | |||
| 2894567147 | |||
| e2afaec677 | |||
| 1d441283f6 | |||
| 08774e06ed | |||
| 3778c9a1fa | |||
| 9922748010 | |||
| b98c02c743 | |||
| 89d587b1cd | |||
| db3878b7ad | |||
| 5880f10ea7 | |||
| f20134e34d | |||
| 588625cab0 | |||
| 466a2acc53 | |||
| f623083701 | |||
| ff77eca2bc | |||
| c4f876c991 | |||
| 5f0dc0cb22 | |||
| 762794e025 | |||
| 9c4d801d8c | |||
| cf61d8c717 | |||
| 34bcb62852 | |||
| a2341cfd36 | |||
| 635f46fae4 | |||
| fba8f8bab2 | |||
| 03dae2cb63 | |||
| efaf3d7bc6 | |||
| e1378eea49 | |||
| be93f52882 | |||
| ea200f23dd | |||
| 5c5b59d520 | |||
| 2a5e4a9aca | |||
| 79da28d1bb | |||
| 545fc80d3b | |||
| 8ac70a9c18 | |||
| 4cb3006fa8 | |||
| 94bc00fa2b | |||
| 678032f041 | |||
| 006072f3b1 | |||
| a3081153e6 | |||
| 28ea32fedf | |||
| 05e10eba00 | |||
| 31690863e5 | |||
| da76ad54bc | |||
| e19c0e411b | |||
| 87df32ac65 | |||
| 702e7e6ee9 | |||
| f78ad8c141 | |||
| 8984740e74 | |||
| a43b555c90 | |||
| ab0b336ed9 | |||
| 88ae87bf48 | |||
| 1192d6cc11 | |||
| 266eb3bd31 |
@@ -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"
|
||||
}
|
||||
Executable
+22
@@ -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"
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Bug Report
|
||||
description: Report an Fabrication Toolkit bug
|
||||
title: "[Bug]: "
|
||||
labels: "bug"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I have searched the [issue tracker](https://www.github.com/bennymeg/Fabrication-Toolkit/issues) for a bug report that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Fabrication Toolkit Version
|
||||
description: |
|
||||
What version of Fabrication Toolkit are you using?
|
||||
|
||||
Note: Please only report issues for currently supported versions of Fabrication Toolkit.
|
||||
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
|
||||
attributes:
|
||||
label: What operating system are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Ubuntu
|
||||
- Other Linux
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What operating system version are you using? On Windows, click Start button > Settings > System > About. On macOS, click the Apple Menu > About This Mac. On Linux, use lsb_release or uname -a.
|
||||
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What arch are you using?
|
||||
options:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64 (including Apple Silicon)
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Last Known Working Fabrication Toolkit version
|
||||
description: What is the last version of Fabrication Toolkit this worked in, if applicable?
|
||||
placeholder: 4.0.0
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: A clear description of what actually happens.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: Github Sponsor
|
||||
url: https://github.com/sponsors/bennymeg
|
||||
about: Help support Fabrication Toolkit by sponsoring us
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for Fabrication Toolkit
|
||||
title: "[Feature Request]: "
|
||||
labels: "enhancement"
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Preflight Checklist
|
||||
description: Please ensure you've completed all of the following.
|
||||
options:
|
||||
- label: I have searched the [issue tracker](https://www.github.com/bennymeg/Fabrication-Toolkit/issues) for a feature request that matches the one I want to file, without success.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe the solution you'd like in a clear and concise manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
+4
-1
@@ -1,2 +1,5 @@
|
||||
.venv
|
||||
dist/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.pyc
|
||||
Test/
|
||||
Vendored
+22
-1
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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** |
|
||||
|:-----:|
|
||||
|
||||
[](https://github.com/sponsors/bennymeg)
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -27,30 +28,52 @@ 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. <span style="text-color: light-grey !important;">_(since v3.0.0)_.</span>
|
||||
> [!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 #' | '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.
|
||||
|
||||
---
|
||||
|
||||
@@ -66,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>
|
||||
|
||||
@@ -74,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._
|
||||
@@ -91,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 about 0.07mm shift (8 clicks is about 0.5mm).
|
||||
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
@@ -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** |
|
||||
|:-----:|
|
||||
|
||||
[](https://github.com/sponsors/bennymeg)
|
||||

|
||||
|
||||
</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.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 284 KiB |
+6
-5
@@ -2,24 +2,25 @@
|
||||
"$schema": "https://go.kicad.org/pcm/schemas/v1",
|
||||
"name": "Fabrication Toolkit",
|
||||
"description": "JLC PCB fabrication toolkit",
|
||||
"description_full": "Toolkit for automating PCB fabrication process with KiCad and JLC PCB. \n\n OPTIONS: \n\u2022 Include Component Part Number in Production Files \n Add an 'MPN' field with the LCSC component part number to the footprint component options. \n\n\u2022 Ignore Footprint in Production Files \n Select 'Exclude from position files' or 'Exclude from BOM' in the footprint properties fabrication attributes in order to ignore the footprint from the relevant file. \n\n\u2022 Rotate components \n Add an 'JLCPCB Rotation Offset' field with the additional counter-clockwise rotation in degrees to correct a mismatch between KiCad and JLC footprint orientation.",
|
||||
"description_full": "Toolkit for automating PCB fabrication process with KiCad and JLC PCB. \n\n Features: \n\u2022 Generates gerber files in correct format for production. \n\u2022 Generates BOM file in correct format for production. \n\u2022 Generates Pick and Place file in correct format for assembly. \n\u2022 Automatic and manual component translations. \n\u2022 Many more unique additional features. \n\u2665 https://github.com/sponsors/bennymeg",
|
||||
"identifier": "com.github.bennymeg.JLC-Plugin-for-KiCad",
|
||||
"type": "plugin",
|
||||
"author": {
|
||||
"name": "Benny Megidish",
|
||||
"contact": {
|
||||
"web": "https://github.com/bennymeg/JLC-Plugin-for-KiCad"
|
||||
"web": "https://github.com/bennymeg/Fabrication-Toolkit"
|
||||
}
|
||||
},
|
||||
"maintainer": {
|
||||
"name": "Benny Megidish",
|
||||
"contact": {
|
||||
"web": "https://github.com/bennymeg/JLC-Plugin-for-KiCad"
|
||||
"web": "https://github.com/bennymeg/Fabrication-Toolkit"
|
||||
}
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"resources": {
|
||||
"homepage": "https://github.com/bennymeg/JLC-Plugin-for-KiCad"
|
||||
"homepage": "https://github.com/bennymeg/Fabrication-Toolkit",
|
||||
"Sponsor": "https://github.com/sponsors/bennymeg"
|
||||
},
|
||||
"tags": [
|
||||
"jlc",
|
||||
@@ -29,7 +30,7 @@
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"version": "4.2.0",
|
||||
"version": "5.2.0",
|
||||
"status": "stable",
|
||||
"kicad_version": "6.00"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
+13
-43
@@ -10,47 +10,17 @@ gerberArchiveName = 'gerbers.zip'
|
||||
outputFolder = 'production'
|
||||
bomRowLimit = 200
|
||||
|
||||
plotPlan = [
|
||||
("F.Cu", pcbnew.F_Cu, "Top Layer"),
|
||||
("B.Cu", pcbnew.B_Cu, "Bottom Layer"),
|
||||
("In1.Cu", pcbnew.In1_Cu, "Internal plane 1"),
|
||||
("In2.Cu", pcbnew.In2_Cu, "Internal plane 2"),
|
||||
("In3.Cu", pcbnew.In3_Cu, "Internal plane 3"),
|
||||
("In4.Cu", pcbnew.In4_Cu, "Internal plane 4"),
|
||||
("In5.Cu", pcbnew.In5_Cu, "Internal plane 5"),
|
||||
("In6.Cu", pcbnew.In6_Cu, "Internal plane 6"),
|
||||
("In7.Cu", pcbnew.In7_Cu, "Internal plane 7"),
|
||||
("In8.Cu", pcbnew.In8_Cu, "Internal plane 8"),
|
||||
("In9.Cu", pcbnew.In9_Cu, "Internal plane 9"),
|
||||
("In10.Cu", pcbnew.In10_Cu, "Internal plane 10"),
|
||||
("In11.Cu", pcbnew.In11_Cu, "Internal plane 11"),
|
||||
("In12.Cu", pcbnew.In12_Cu, "Internal plane 12"),
|
||||
("In13.Cu", pcbnew.In13_Cu, "Internal plane 13"),
|
||||
("In14.Cu", pcbnew.In14_Cu, "Internal plane 14"),
|
||||
("In15.Cu", pcbnew.In15_Cu, "Internal plane 15"),
|
||||
("In16.Cu", pcbnew.In16_Cu, "Internal plane 16"),
|
||||
("In17.Cu", pcbnew.In17_Cu, "Internal plane 17"),
|
||||
("In18.Cu", pcbnew.In18_Cu, "Internal plane 18"),
|
||||
("In19.Cu", pcbnew.In19_Cu, "Internal plane 19"),
|
||||
("In20.Cu", pcbnew.In20_Cu, "Internal plane 20"),
|
||||
("In21.Cu", pcbnew.In21_Cu, "Internal plane 21"),
|
||||
("In22.Cu", pcbnew.In22_Cu, "Internal plane 22"),
|
||||
("In23.Cu", pcbnew.In23_Cu, "Internal plane 23"),
|
||||
("In24.Cu", pcbnew.In24_Cu, "Internal plane 24"),
|
||||
("In25.Cu", pcbnew.In25_Cu, "Internal plane 25"),
|
||||
("In26.Cu", pcbnew.In26_Cu, "Internal plane 26"),
|
||||
("In27.Cu", pcbnew.In27_Cu, "Internal plane 27"),
|
||||
("In28.Cu", pcbnew.In28_Cu, "Internal plane 28"),
|
||||
("In29.Cu", pcbnew.In29_Cu, "Internal plane 29"),
|
||||
("In30.Cu", pcbnew.In30_Cu, "Internal plane 30"),
|
||||
("F.SilkS", pcbnew.F_SilkS, "Top Silkscreen"),
|
||||
("B.SilkS", pcbnew.B_SilkS, "Bottom Silkscreen"),
|
||||
("F.Mask", pcbnew.F_Mask, "Top Soldermask"),
|
||||
("B.Mask", pcbnew.B_Mask, "Bottom Soldermask"),
|
||||
("F.Paste", pcbnew.F_Paste, "Top Paste (Stencil)"),
|
||||
("B.Paste", pcbnew.B_Paste, "Bottom Paste (Stencil)"),
|
||||
("Edge.Cuts", pcbnew.Edge_Cuts, "Board Outline"),
|
||||
("User.Comments", pcbnew.Cmts_User, "User Comments")
|
||||
]
|
||||
optionsFileName = 'fabrication-toolkit-options.json'
|
||||
|
||||
layers = [layer[0] for layer in plotPlan]
|
||||
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
|
||||
]
|
||||
|
||||
+9
-1
@@ -1,3 +1,11 @@
|
||||
AUTO_TRANSLATE_OPT = "AUTO TRANSLATE"
|
||||
AUTO_FILL_OPT = "AUTO FILL"
|
||||
EXCLUDE_DNP_OPT = "EXCLUDE DNP"
|
||||
OUTPUT_NAME_OPT = "OUTPUT NAME"
|
||||
EXTRA_LAYERS = "EXTRA_LAYERS"
|
||||
EXTEND_EDGE_CUT_OPT = "EXTEND_EDGE_CUT"
|
||||
ALTERNATIVE_EDGE_CUT_OPT = "ALTERNATIVE_EDGE_CUT"
|
||||
ALL_ACTIVE_LAYERS_OPT = "ALL_ACTIVE_LAYERS"
|
||||
OPEN_BROWSER_OPT = "OPEN BROWSER"
|
||||
ARCHIVE_NAME = "ARCHIVE_NAME"
|
||||
EXTRA_LAYERS = "EXTRA_LAYERS"
|
||||
BACKUP_OPT = "BACKUP_OPT"
|
||||
|
||||
+80
-13
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
import wx
|
||||
import pcbnew # type: ignore
|
||||
import pcbnew # type: ignore
|
||||
|
||||
from .thread import ProcessThread
|
||||
from .events import StatusEvent
|
||||
from .options import EXCLUDE_DNP_OPT, EXTRA_LAYERS
|
||||
from .config import 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
|
||||
|
||||
|
||||
# WX GUI form that show the plugin progress
|
||||
@@ -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)
|
||||
@@ -27,31 +27,71 @@ class KiCadToJLCForm(wx.Frame):
|
||||
|
||||
self.SetSizeHints(wx.Size(600, 100), wx.DefaultSize)
|
||||
|
||||
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,
|
||||
OPEN_BROWSER_OPT: True,
|
||||
BACKUP_OPT: True,
|
||||
})
|
||||
|
||||
self.mOptionsLabel = wx.StaticText(self, label='Options:')
|
||||
# self.mOptionsSeparator = wx.StaticLine(self)
|
||||
self.mExcludeDnpCheckbox = wx.CheckBox(self, label='Exclude DNP components')
|
||||
self.mExcludeDnpCheckbox.SetValue(False)
|
||||
|
||||
layers = get_layer_names(pcbnew.GetBoard())
|
||||
self.mAdditionalLayersControl = wx.TextCtrl(self, size=wx.Size(600, 50))
|
||||
self.mAdditionalLayersControl.Hint = "Additional layers"
|
||||
self.mAdditionalLayersControl.Hint = "Additional layers (comma-separated) [optional]"
|
||||
self.mAdditionalLayersControl.AutoComplete(layers)
|
||||
self.mAdditionalLayersControl.Enable()
|
||||
|
||||
self.mGenerateButton = wx.Button(self, label='Generate', size=wx.Size(600, 60))
|
||||
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='Set User.2 as alternative Edge-Cut layer')
|
||||
self.mAlternativeEdgeCutsCheckbox.SetValue(userOptions[ALTERNATIVE_EDGE_CUT_OPT])
|
||||
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)
|
||||
self.mGaugeStatus.SetValue(0)
|
||||
self.mGaugeStatus.Hide()
|
||||
|
||||
self.mGenerateButton = wx.Button(self, label='Generate', size=wx.Size(600, 60))
|
||||
self.mGenerateButton.Bind(wx.EVT_BUTTON, self.onGenerateButtonClick)
|
||||
|
||||
boxSizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
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)
|
||||
|
||||
@@ -61,15 +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[EXCLUDE_DNP_OPT] = self.mExcludeDnpCheckbox.GetValue()
|
||||
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.mAdditionalLayersControl.Hide()
|
||||
self.mExcludeDnpCheckbox.Hide()
|
||||
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()
|
||||
|
||||
@@ -77,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:
|
||||
|
||||
+189
-82
@@ -12,15 +12,18 @@ from typing import Tuple
|
||||
|
||||
# Interaction with KiCad.
|
||||
import pcbnew # type: ignore
|
||||
from .utils import footprint_has_field, footprint_get_field
|
||||
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):
|
||||
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
|
||||
@@ -59,11 +63,12 @@ class ProcessManager:
|
||||
plot_options.SetScale(1)
|
||||
plot_options.SetMirror(False)
|
||||
plot_options.SetUseGerberAttributes(True)
|
||||
plot_options.SetUseGerberProtelExtensions(False)
|
||||
plot_options.SetUseGerberProtelExtensions(True)
|
||||
plot_options.SetUseAuxOrigin(True)
|
||||
plot_options.SetSubtractMaskFromSilk(True)
|
||||
plot_options.SetUseGerberX2format(False)
|
||||
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
|
||||
|
||||
|
||||
if hasattr(plot_options, "SetExcludeEdgeLayer"):
|
||||
plot_options.SetExcludeEdgeLayer(True)
|
||||
|
||||
@@ -72,22 +77,29 @@ class ProcessManager:
|
||||
else:
|
||||
extra_layers = []
|
||||
|
||||
for layer_info in plotPlan:
|
||||
if self.board.IsLayerEnabled(layer_info[1]) or layer_info[0] in extra_layers:
|
||||
for layer_info in get_plot_plan(self.board):
|
||||
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])
|
||||
|
||||
if layer_info[1] == pcbnew.Edge_Cuts and hasattr(plot_controller, 'PlotLayers'):
|
||||
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()
|
||||
# uses User_2 layer for alternative Edge_Cuts layer
|
||||
if alternative_edge_cuts:
|
||||
seq.push_back(pcbnew.User_2)
|
||||
else:
|
||||
seq.push_back(layer_info[1])
|
||||
# includes User_1 layer with Edge_Cuts layer to allow V Cuts to be defined as User_1 layer
|
||||
# available for KiCad 7.0.1+
|
||||
seq = pcbnew.LSEQ()
|
||||
seq.push_back(layer_info[1])
|
||||
seq.push_back(pcbnew.User_1)
|
||||
if extend_edge_cuts:
|
||||
seq.push_back(layer_info[1])
|
||||
seq.push_back(pcbnew.User_1)
|
||||
plot_controller.PlotLayers(seq)
|
||||
else:
|
||||
plot_controller.PlotLayer()
|
||||
|
||||
plot_controller.ClosePlot()
|
||||
settings = original_settings
|
||||
|
||||
def generate_drills(self, temp_dir):
|
||||
'''Generate the drill file.'''
|
||||
@@ -95,31 +107,62 @@ class ProcessManager:
|
||||
|
||||
drill_writer.SetOptions(
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
self.board.GetDesignSettings().GetAuxOrigin(),
|
||||
False)
|
||||
drill_writer.SetFormat(False)
|
||||
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, False)
|
||||
drill_writer.SetFormat(True)
|
||||
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, True)
|
||||
|
||||
def generate_netlist(self, temp_dir):
|
||||
'''Generate the connection netlist.'''
|
||||
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."""
|
||||
if footprint.GetAttributes() & pcbnew.FP_SMD:
|
||||
return footprint.GetPosition()
|
||||
if footprint.GetAttributes() & pcbnew.FP_THROUGH_HOLE:
|
||||
return footprint.GetBoundingBox(False, False).GetCenter()
|
||||
pads = footprint.Pads()
|
||||
#get bounding box based on pads only to ignore non-copper layers, e.g. silkscreen
|
||||
bbox = pads[0].GetBoundingBox()
|
||||
for pad in pads:
|
||||
bbox.Merge(pad.GetBoundingBox())
|
||||
return bbox.GetCenter()
|
||||
def _get_footprint_rotation(self, footprint):
|
||||
return footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
|
||||
|
||||
def generate_tables(self, temp_dir, exclude_dnp):
|
||||
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()
|
||||
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
|
||||
|
||||
def generate_tables(self, temp_dir, auto_translate, exclude_dnp):
|
||||
'''Generate the data tables.'''
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
footprints = list(self.board.GetModules())
|
||||
@@ -127,13 +170,13 @@ class ProcessManager:
|
||||
footprints = list(self.board.GetFootprints())
|
||||
|
||||
# sort footprint after designator
|
||||
footprints.sort(key=lambda x: x.GetReference())
|
||||
footprints.sort(key=lambda x: x.GetReference().upper())
|
||||
|
||||
# unique designator dictionary
|
||||
footprint_designators = defaultdict(int)
|
||||
for i, footprint in enumerate(footprints):
|
||||
# count unique designators
|
||||
footprint_designators[footprint.GetReference()] += 1
|
||||
footprint_designators[footprint.GetReference().upper()] += 1
|
||||
bom_designators = footprint_designators.copy()
|
||||
|
||||
if len(footprint_designators.items()) > 0:
|
||||
@@ -147,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',
|
||||
@@ -155,26 +205,32 @@ class ProcessManager:
|
||||
# 2: 'unspecified'
|
||||
# }.get(footprint.GetAttributes())
|
||||
|
||||
skip_footprint = exclude_dnp and (footprint_has_field(footprint, 'dnp')
|
||||
or footprint.GetValue().upper() == 'DNP'
|
||||
or getattr(footprint, 'IsDNP', bool)())
|
||||
is_dnp = (footprint_has_field(footprint, 'dnp')
|
||||
or (footprint.GetValue().upper() == 'DNP')
|
||||
or getattr(footprint, 'IsDNP', bool)())
|
||||
skip_dnp = exclude_dnp and is_dnp
|
||||
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES) and not skip_footprint:
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES) and not is_dnp:
|
||||
# append unique ID if duplicate footprint designator
|
||||
unique_id = ""
|
||||
if footprint_designators[footprint.GetReference()] > 1:
|
||||
unique_id = str(footprint_designators[footprint.GetReference()])
|
||||
footprint_designators[footprint.GetReference()] -= 1
|
||||
if footprint_designators[footprint.GetReference().upper()] > 1:
|
||||
unique_id = str(footprint_designators[footprint.GetReference().upper()])
|
||||
footprint_designators[footprint.GetReference().upper()] -= 1
|
||||
|
||||
designator = "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
|
||||
mid_x = (self._get_footprint_position(footprint)[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
|
||||
mid_y = (self._get_footprint_position(footprint)[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
|
||||
designator = "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id)
|
||||
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 = 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)
|
||||
|
||||
@@ -187,7 +243,11 @@ class ProcessManager:
|
||||
# JLC expect 'Rotation' to be 'as viewed from above component', so bottom needs inverting, and ends up 180 degrees out as well
|
||||
if layer == 'bottom':
|
||||
rotation = (180.0 - rotation)
|
||||
rotation = (rotation + rotation_offset_db + rotation_offset_manual) % 360.0
|
||||
|
||||
if auto_translate:
|
||||
rotation += rotation_offset_db
|
||||
|
||||
rotation = (rotation + rotation_offset_manual) % 360.0
|
||||
|
||||
self.components.append({
|
||||
'Designator': designator,
|
||||
@@ -197,12 +257,12 @@ class ProcessManager:
|
||||
'Layer': layer,
|
||||
})
|
||||
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM) and not skip_footprint:
|
||||
if not (footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM) and not skip_dnp:
|
||||
# append unique ID if we are dealing with duplicate bom designator
|
||||
unique_id = ""
|
||||
if bom_designators[footprint.GetReference()] > 1:
|
||||
unique_id = str(bom_designators[footprint.GetReference()])
|
||||
bom_designators[footprint.GetReference()] -= 1
|
||||
if bom_designators[footprint.GetReference().upper()] > 1:
|
||||
unique_id = str(bom_designators[footprint.GetReference().upper()])
|
||||
bom_designators[footprint.GetReference().upper()] -= 1
|
||||
|
||||
# merge similar parts into single entry
|
||||
insert = True
|
||||
@@ -213,7 +273,7 @@ class ProcessManager:
|
||||
under_limit = component['Quantity'] < bomRowLimit
|
||||
|
||||
if same_footprint and same_value and same_lcsc and under_limit:
|
||||
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
|
||||
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id)
|
||||
component['Quantity'] += 1
|
||||
insert = False
|
||||
break
|
||||
@@ -221,7 +281,7 @@ class ProcessManager:
|
||||
# add component to BOM
|
||||
if insert:
|
||||
self.bom.append({
|
||||
'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
|
||||
'Designator': "{}{}{}".format(footprint.GetReference().upper(), "" if unique_id == "" else "_", unique_id),
|
||||
'Footprint': self._normalize_footprint_name(footprint_name),
|
||||
'Quantity': 1,
|
||||
'Value': footprint.GetValue(),
|
||||
@@ -267,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]:
|
||||
@@ -287,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.
|
||||
@@ -325,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'])):
|
||||
@@ -350,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'])):
|
||||
@@ -372,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',
|
||||
@@ -401,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 = ""
|
||||
|
||||
@@ -431,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 = ""
|
||||
|
||||
@@ -447,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.*$')
|
||||
|
||||
+86
-15
@@ -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)
|
||||
@@ -35,11 +78,13 @@ class ProcessThread(Thread):
|
||||
try:
|
||||
# Verify all zones are up-to-date
|
||||
self.progress(10)
|
||||
self.process_manager.update_zone_fills()
|
||||
if (self.options[AUTO_FILL_OPT]):
|
||||
self.process_manager.update_zone_fills()
|
||||
|
||||
# generate gerber
|
||||
self.progress(20)
|
||||
self.process_manager.generate_gerber(temp_dir_gerber, self.options[EXTRA_LAYERS])
|
||||
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)
|
||||
@@ -51,7 +96,7 @@ class ProcessThread(Thread):
|
||||
|
||||
# generate data tables
|
||||
self.progress(50)
|
||||
self.process_manager.generate_tables(temp_dir, self.options[EXCLUDE_DNP_OPT])
|
||||
self.process_manager.generate_tables(temp_dir, self.options[AUTO_TRANSLATE_OPT], self.options[EXCLUDE_DNP_OPT])
|
||||
|
||||
# generate pick and place file
|
||||
self.progress(60)
|
||||
@@ -68,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
|
||||
|
||||
@@ -105,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))
|
||||
|
||||
+78
-2
@@ -1,8 +1,15 @@
|
||||
import pcbnew # type: ignore
|
||||
import os
|
||||
import json
|
||||
from .config import optionsFileName
|
||||
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
|
||||
|
||||
@@ -18,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)
|
||||
@@ -26,7 +35,74 @@ 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)
|
||||
|
||||
def get_user_options_file_path():
|
||||
boardFilePath = pcbnew.GetBoard().GetFileName()
|
||||
return os.path.join(os.path.dirname(boardFilePath), optionsFileName)
|
||||
|
||||
def load_user_options(default_options):
|
||||
try:
|
||||
with open(get_user_options_file_path(), 'r') as f:
|
||||
user_options = json.load(f)
|
||||
except:
|
||||
user_options = default_options
|
||||
|
||||
# merge the user options with the default options
|
||||
options = default_options.copy()
|
||||
options.update(user_options)
|
||||
return options
|
||||
|
||||
def save_user_options(options):
|
||||
try:
|
||||
with open(get_user_options_file_path(), 'w') as f:
|
||||
json.dump(options, f)
|
||||
except:
|
||||
wx.MessageBox("Error saving user options", "Error", wx.OK | wx.ICON_ERROR)
|
||||
|
||||
def get_plot_plan(board, active_only=True):
|
||||
"""Returns `(KiCad standard name, layer id, custom user name)` of all (active) layers of the given board."""
|
||||
layers = []
|
||||
i = pcbnew.PCBNEW_LAYER_ID_START - 1
|
||||
while i < pcbnew.PCBNEW_LAYER_ID_START + pcbnew.PCB_LAYER_ID_COUNT - 1:
|
||||
i += 1
|
||||
if active_only and not board.IsLayerEnabled(i):
|
||||
continue
|
||||
|
||||
layer_std_name = pcbnew.BOARD.GetStandardLayerName(i)
|
||||
layer_name = pcbnew.BOARD.GetLayerName(board, i)
|
||||
|
||||
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]
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user