Compare commits
No commits in common. "main" and "v0.8.0" have entirely different histories.
22 changed files with 278 additions and 4001 deletions
File diff suppressed because one or more lines are too long
1
cgal_skeleton_core/.gitignore
vendored
1
cgal_skeleton_core/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
cache
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
|
||||
CXX ?= clang++
|
||||
|
||||
BUILDDIR ?= build
|
||||
CACHEDIR ?= cache
|
||||
|
||||
CGAL ?= CGAL-6.1
|
||||
CGAL_URL ?= https://github.com/CGAL/cgal/releases/download/v6.1/CGAL-6.1-library.tar.xz
|
||||
|
||||
BOOST ?= boost_1_90_0
|
||||
BOOST_URL ?= https://archives.boost.io/release/1.90.0/source/boost_1_90_0.tar.gz
|
||||
|
||||
WASI_SDK_URL ?= https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-29/wasi-sdk-29.0-x86_64-linux.tar.gz
|
||||
WASI_SDK ?= $(CACHEDIR)/wasi-sdk-29.0-x86_64-linux
|
||||
|
||||
SOURCES := \
|
||||
skeleton_wrapper.cpp
|
||||
|
||||
WASI_SOURCES := \
|
||||
$(SOURCES) \
|
||||
exception_stubs.cpp \
|
||||
|
||||
INCLUDES := -I$(CACHEDIR)/$(CGAL)/include -I$(CACHEDIR)/$(BOOST)
|
||||
CXXFLAGS := -std=c++20 -g -Wall -Wextra -O2 -DCGAL_ALWAYS_ROUND_TO_NEAREST
|
||||
WASI_CXXFLAGS := -DCGAL_ALWAYS_ROUND_TO_NEAREST -DCGAL_CORE_USE_BOOST_BACKEND -DCGAL_DISABLE_GMP -DCGAL_USE_BOOST_MP -DFE_UPWARD=FE_TONEAREST -D_WASI_EMULATED_PROCESS_CLOCKS $(CXXFLAGS)
|
||||
LDFLAGS :=
|
||||
HOST_LDFLAGS := $(LDFLAGS) -lgmp -lmpfr
|
||||
WASI_LDFLAGS := $(LDFLAGS) -lwasi-emulated-process-clocks
|
||||
|
||||
WASI_CXX ?= $(WASI_SDK)/bin/clang++
|
||||
|
||||
|
||||
BINARY := skeleton
|
||||
|
||||
all: $(BUILDDIR)/$(BINARY) wasm
|
||||
|
||||
.PHONY: wasm
|
||||
wasm: $(BUILDDIR)/$(BINARY).wasm
|
||||
|
||||
$(BUILDDIR)/$(BINARY): $(SOURCES)
|
||||
$(CXX) $(CXXFLAGS) $(HOST_LDFLAGS) -o $@ $^
|
||||
|
||||
$(WASI_SDK):
|
||||
mkdir -p $(dir $@)
|
||||
cd $(dir $@); curl -L ${WASI_SDK_URL} | tar xzf -
|
||||
|
||||
$(CACHEDIR)/$(BOOST):
|
||||
cd $(dir $@); curl -L ${BOOST_URL} | tar xzf -
|
||||
|
||||
$(CACHEDIR)/$(CGAL):
|
||||
cd $(dir $@); curl -L ${CGAL_URL} | tar xJf -
|
||||
|
||||
$(BUILDDIR)/wasi/%.o: %.cpp $(WASI_SDK)
|
||||
@mkdir -p $(dir $@)
|
||||
$(WASI_CXX) -c $(WASI_CXXFLAGS) $(INCLUDES) -o $@ $<
|
||||
|
||||
$(BUILDDIR)/$(BINARY).wasm: $(patsubst %.cpp,$(BUILDDIR)/wasi/%.o,$(WASI_SOURCES))
|
||||
@mkdir -p $(dir $@)
|
||||
$(WASI_CXX) $(WASI_LDFLAGS) -o $@ $^
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)
|
||||
|
||||
.PHONY: mrproper
|
||||
mrproper: clean
|
||||
rm -rf $(CACHEDIR)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
|
||||
#include <stdlib.h>
|
||||
|
||||
extern "C" {
|
||||
void* __cxa_allocate_exception(size_t) {
|
||||
abort();
|
||||
}
|
||||
|
||||
void __cxa_throw(void *, void *, void (*)(void *)) {
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
#include <CGAL/Simple_cartesian.h>
|
||||
#include <CGAL/Polygon_2.h>
|
||||
#include <CGAL/create_straight_skeleton_2.h>
|
||||
#include <CGAL/number_utils.h>
|
||||
#include <boost/multiprecision/cpp_int.hpp>
|
||||
#include <boost/multiprecision/number.hpp>
|
||||
#include <boost/multiprecision/rational_adaptor.hpp>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <iomanip>
|
||||
|
||||
// Use exact rational arithmetic throughout to avoid filtered predicates
|
||||
typedef boost::multiprecision::number<boost::multiprecision::rational_adaptor<boost::multiprecision::cpp_int_backend<>>> Exact_NT;
|
||||
typedef CGAL::Simple_cartesian<Exact_NT> Exact_K;
|
||||
|
||||
// Input kernel with doubles for reading coordinates
|
||||
typedef CGAL::Simple_cartesian<double> Input_K;
|
||||
|
||||
// Use exact kernel for computation
|
||||
typedef Exact_K K;
|
||||
typedef K::Point_2 Point;
|
||||
typedef CGAL::Polygon_2<K> Polygon_2;
|
||||
typedef CGAL::Straight_skeleton_2<K> Ss;
|
||||
typedef std::shared_ptr<Ss> SsPtr;
|
||||
|
||||
int main()
|
||||
{
|
||||
Polygon_2 poly;
|
||||
std::string line;
|
||||
|
||||
while (std::getline(std::cin, line)) {
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::istringstream iss(line);
|
||||
double x, y;
|
||||
if (iss >> x >> y) {
|
||||
// Convert double to exact rational
|
||||
poly.push_back(Point(Exact_NT(x), Exact_NT(y)));
|
||||
} else {
|
||||
std::cerr << "Error: Invalid input line: " << line << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if (poly.size() < 3) {
|
||||
std::cerr << "Error: Polygon must have at least 3 vertices" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!poly.is_counterclockwise_oriented()) {
|
||||
std::cerr << "Error: Polygon must be counter-clockwise" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
SsPtr ss = CGAL::create_interior_straight_skeleton_2(poly.vertices_begin(), poly.vertices_end(), K());
|
||||
|
||||
if (!ss) {
|
||||
std::cerr << "Error: Failed to create straight skeleton" << std::endl;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Output skeleton edges
|
||||
// Iterate through all halfedges in the skeleton
|
||||
for (auto he = ss->halfedges_begin(); he != ss->halfedges_end(); ++he) {
|
||||
// Only output each edge once (skip the opposite halfedge)
|
||||
if (he->id() < he->opposite()->id()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the vertices at both ends of the halfedge
|
||||
auto v_source = he->vertex();
|
||||
auto v_target = he->opposite()->vertex();
|
||||
|
||||
// Get times (distance from boundary)
|
||||
// Convert from exact rational to double for output
|
||||
double t1 = CGAL::to_double(v_source->time());
|
||||
double t2 = CGAL::to_double(v_target->time());
|
||||
|
||||
// Skip contour edges (outline of the input polygon)
|
||||
// Contour edges have both vertices at time 0
|
||||
if (t1 == 0.0 && t2 == 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get coordinates
|
||||
double x1 = CGAL::to_double(v_source->point().x());
|
||||
double y1 = CGAL::to_double(v_source->point().y());
|
||||
double x2 = CGAL::to_double(v_target->point().x());
|
||||
double y2 = CGAL::to_double(v_target->point().y());
|
||||
|
||||
// Output: start_x start_y end_x end_y start_time end_time
|
||||
std::cout << std::setprecision(12) << x1 << " " << y1 << " "
|
||||
<< x2 << " " << y2 << " "
|
||||
<< t1 << " " << t2 << std::endl;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB |
|
|
@ -1,406 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="12.5mm"
|
||||
height="12.5mm"
|
||||
viewBox="-19.823074 -22.198258 12.5 12.5"
|
||||
version="1.1"
|
||||
id="svg38"
|
||||
sodipodi:docname="icon-dark.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
inkscape:export-filename="icon.png"
|
||||
inkscape:export-xdpi="130.048"
|
||||
inkscape:export-ydpi="130.048"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs38" />
|
||||
<sodipodi:namedview
|
||||
id="namedview38"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="6.4840427"
|
||||
inkscape:cx="-1.5422477"
|
||||
inkscape:cy="-5.6292041"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg38" />
|
||||
<rect
|
||||
style="fill:#1b111f;fill-opacity:1;stroke:none;stroke-width:1.41685;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
||||
id="rect38"
|
||||
width="12.5"
|
||||
height="12.5"
|
||||
x="-19.823074"
|
||||
y="-22.198257" />
|
||||
<g
|
||||
id="g62"
|
||||
transform="matrix(0.28523786,0,0,0.28523786,-14.168783,-15.866474)">
|
||||
<path
|
||||
d="m 8.5867713,5.8416089 a 8.22617,8.22617 0 0 1 -1.80878,5.3403601"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path38"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 6.7779913,11.181969 a 10.7975,10.7975 0 0 1 -4.68641,3.40964"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path39"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 2.0915813,14.591609 a 13.2639,13.2639 0 0 1 -8.23249,0.31"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path40"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -6.1409087,14.901609 a 15.8862,15.8862 0 0 1 -6.9229103,-4.06"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path41"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -4.4036087,5.8416089 a 8.22617,8.22617 0 0 1 -3.72051,-4.236633"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path42"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -8.1241187,1.6049759 a 10.7975,10.7975 0 0 1 -0.6096,-5.763367"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path43"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -8.7337187,-4.1583911 a 13.2639,13.2639 0 0 1 3.84776,-7.2844999"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path44"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -4.8859587,-11.442891 a 15.8862,15.8862 0 0 1 6.97754,-3.9655"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path45"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 2.0915813,-5.4083911 a 8.22617,8.22617 0 0 1 5.52928,-1.10373"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path46"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 7.6208613,-6.5121211 a 10.7975,10.7975 0 0 1 5.2960197,2.35373"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path47"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 12.916881,-4.1583911 a 13.2639,13.2639 0 0 1 4.3847,6.974543"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path48"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 17.301581,2.8161519 a 15.8862,15.8862 0 0 1 -0.0546,8.0254571"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path49"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 2.0915813,-15.408391 a 15.5807,15.5807 0 0 1 7.9867397,5.0724"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path50"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 10.078321,-10.335991 a 12.9547,12.9547 0 0 1 2.83856,6.1775999"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path51"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 12.916881,-4.1583911 a 10.4951,10.4951 0 0 1 -1.06364,6.715005"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path52"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 11.853241,2.5566139 A 7.92896,7.92896 0 0 1 8.5867713,5.8416089"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path53"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 17.246981,10.841609 a 15.5807,15.5807 0 0 1 -8.3861397,4.3805"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path54"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 8.8608413,15.222109 a 12.9547,12.9547 0 0 1 -6.76926,-0.6305"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path55"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 2.0915813,14.591609 a 10.4951,10.4951 0 0 1 -5.28354,-4.27866"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path56"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -3.1919587,10.312949 a 7.92896,7.92896 0 0 1 -1.21165,-4.4713401"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path57"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -13.063819,10.841609 a 15.5807,15.5807 0 0 1 0.3994,-9.4529151"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path58"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -12.664419,1.3886939 a 12.9547,12.9547 0 0 1 3.9307003,-5.547085"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path59"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -8.7337187,-4.1583911 a 10.4951,10.4951 0 0 1 6.34718,-2.43635"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path60"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -2.3865387,-6.5947411 a 7.92896,7.92896 0 0 1 4.47812,1.18635"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path61"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
</g>
|
||||
<g
|
||||
id="g38"
|
||||
style="mix-blend-mode:multiply"
|
||||
transform="matrix(0.28523786,0,0,0.28523786,-14.168783,-15.866474)">
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
id="l-bottom-copper"
|
||||
transform="matrix(0,-1,-1,0,2.0915813,2.0916089)"
|
||||
opacity="0.7"
|
||||
style="opacity:1;fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1">
|
||||
<path
|
||||
d="m -3.75,-6.49519 a 8.22617,8.22617 0 0 0 -5.34036,1.80878"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path1"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M -9.09036,-4.68641 A 10.7975,10.7975 0 0 0 -12.5,0"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path2"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -12.5,0 a 13.2639,13.2639 0 0 0 -0.31,8.23249"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path3"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -12.81,8.23249 a 15.8862,15.8862 0 0 0 4.06,6.92291"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path4"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M -3.75,6.49519 A 8.22617,8.22617 0 0 0 0.486633,10.2157"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path5"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 0.486633,10.2157 A 10.7975,10.7975 0 0 0 6.25,10.8253"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path6"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 6.25,10.8253 A 13.2639,13.2639 0 0 0 13.5345,6.97754"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path7"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 13.5345,6.97754 A 15.8862,15.8862 0 0 0 17.5,0"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path8"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 7.5,0 A 8.22617,8.22617 0 0 0 8.60373,-5.52928"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path9"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 8.60373,-5.52928 A 10.7975,10.7975 0 0 0 6.25,-10.8253"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path10"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 6.25,-10.8253 A 13.2639,13.2639 0 0 0 -0.724543,-15.21"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path11"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M -0.724543,-15.21 A 15.8862,15.8862 0 0 0 -8.75,-15.1554"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path12"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
id="l-top-copper"
|
||||
transform="matrix(0,-1,-1,0,2.0915813,2.0916089)"
|
||||
opacity="0.7"
|
||||
style="opacity:1;mix-blend-mode:color-dodge;fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none">
|
||||
<path
|
||||
d="M 17.5,0 A 15.5807,15.5807 0 0 0 12.4276,-7.98674"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path17"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 12.4276,-7.98674 A 12.9547,12.9547 0 0 0 6.25,-10.8253"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path18"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 6.25,-10.8253 a 10.4951,10.4951 0 0 0 -6.715005,1.06364"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path19"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -0.465005,-9.76166 A 7.92896,7.92896 0 0 0 -3.75,-6.49519"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path20"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -8.75,-15.1554 a 15.5807,15.5807 0 0 0 -4.3805,8.38614"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path21"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -13.1305,-6.76926 A 12.9547,12.9547 0 0 0 -12.5,0"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path22"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -12.5,0 a 10.4951,10.4951 0 0 0 4.27866,5.28354"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path23"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -8.22134,5.28354 A 7.92896,7.92896 0 0 0 -3.75,6.49519"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path24"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -8.75,15.1554 A 15.5807,15.5807 0 0 0 0.702915,14.756"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path25"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 0.702915,14.756 A 12.9547,12.9547 0 0 0 6.25,10.8253"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path26"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 6.25,10.8253 A 10.4951,10.4951 0 0 0 8.68635,4.47812"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path27"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 8.68635,4.47812 A 7.92896,7.92896 0 0 0 7.5,0"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path28"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
|
|
@ -1,406 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="12.5mm"
|
||||
height="12.5mm"
|
||||
viewBox="-19.823074 -22.198258 12.5 12.5"
|
||||
version="1.1"
|
||||
id="svg38"
|
||||
sodipodi:docname="icon-light.svg"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
inkscape:export-filename="icon-light.png"
|
||||
inkscape:export-xdpi="130.048"
|
||||
inkscape:export-ydpi="130.048"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs38" />
|
||||
<sodipodi:namedview
|
||||
id="namedview38"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="6.4840427"
|
||||
inkscape:cx="-1.5422477"
|
||||
inkscape:cy="-5.6292041"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg38" />
|
||||
<rect
|
||||
style="fill:#f2eff2;fill-opacity:1;stroke:none;stroke-width:1.41685;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
|
||||
id="rect38"
|
||||
width="12.5"
|
||||
height="12.5"
|
||||
x="-19.823074"
|
||||
y="-22.198257" />
|
||||
<g
|
||||
id="g62"
|
||||
transform="matrix(0.28523786,0,0,0.28523786,-14.168783,-15.866474)">
|
||||
<path
|
||||
d="m 8.5867713,5.8416089 a 8.22617,8.22617 0 0 1 -1.80878,5.3403601"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path38"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 6.7779913,11.181969 a 10.7975,10.7975 0 0 1 -4.68641,3.40964"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path39"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 2.0915813,14.591609 a 13.2639,13.2639 0 0 1 -8.23249,0.31"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path40"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -6.1409087,14.901609 a 15.8862,15.8862 0 0 1 -6.9229103,-4.06"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path41"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -4.4036087,5.8416089 a 8.22617,8.22617 0 0 1 -3.72051,-4.236633"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path42"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -8.1241187,1.6049759 a 10.7975,10.7975 0 0 1 -0.6096,-5.763367"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path43"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -8.7337187,-4.1583911 a 13.2639,13.2639 0 0 1 3.84776,-7.2844999"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path44"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -4.8859587,-11.442891 a 15.8862,15.8862 0 0 1 6.97754,-3.9655"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path45"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 2.0915813,-5.4083911 a 8.22617,8.22617 0 0 1 5.52928,-1.10373"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path46"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 7.6208613,-6.5121211 a 10.7975,10.7975 0 0 1 5.2960197,2.35373"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path47"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 12.916881,-4.1583911 a 13.2639,13.2639 0 0 1 4.3847,6.974543"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path48"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 17.301581,2.8161519 a 15.8862,15.8862 0 0 1 -0.0546,8.0254571"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path49"
|
||||
style="opacity:1;mix-blend-mode:normal;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m 2.0915813,-15.408391 a 15.5807,15.5807 0 0 1 7.9867397,5.0724"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path50"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 10.078321,-10.335991 a 12.9547,12.9547 0 0 1 2.83856,6.1775999"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path51"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 12.916881,-4.1583911 a 10.4951,10.4951 0 0 1 -1.06364,6.715005"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path52"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 11.853241,2.5566139 A 7.92896,7.92896 0 0 1 8.5867713,5.8416089"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path53"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 17.246981,10.841609 a 15.5807,15.5807 0 0 1 -8.3861397,4.3805"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path54"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 8.8608413,15.222109 a 12.9547,12.9547 0 0 1 -6.76926,-0.6305"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path55"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 2.0915813,14.591609 a 10.4951,10.4951 0 0 1 -5.28354,-4.27866"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path56"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -3.1919587,10.312949 a 7.92896,7.92896 0 0 1 -1.21165,-4.4713401"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path57"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -13.063819,10.841609 a 15.5807,15.5807 0 0 1 0.3994,-9.4529151"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path58"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -12.664419,1.3886939 a 12.9547,12.9547 0 0 1 3.9307003,-5.547085"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path59"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -8.7337187,-4.1583911 a 10.4951,10.4951 0 0 1 6.34718,-2.43635"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path60"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -2.3865387,-6.5947411 a 7.92896,7.92896 0 0 1 4.47812,1.18635"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path61"
|
||||
style="opacity:1;mix-blend-mode:lighten;fill:none;stroke:#ffffff;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" />
|
||||
</g>
|
||||
<g
|
||||
id="g38"
|
||||
style="mix-blend-mode:multiply"
|
||||
transform="matrix(0.28523786,0,0,0.28523786,-14.168783,-15.866474)">
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
id="l-bottom-copper"
|
||||
transform="matrix(0,-1,-1,0,2.0915813,2.0916089)"
|
||||
opacity="0.7"
|
||||
style="opacity:1;fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1">
|
||||
<path
|
||||
d="m -3.75,-6.49519 a 8.22617,8.22617 0 0 0 -5.34036,1.80878"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path1"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M -9.09036,-4.68641 A 10.7975,10.7975 0 0 0 -12.5,0"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path2"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -12.5,0 a 13.2639,13.2639 0 0 0 -0.31,8.23249"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path3"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="m -12.81,8.23249 a 15.8862,15.8862 0 0 0 4.06,6.92291"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path4"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M -3.75,6.49519 A 8.22617,8.22617 0 0 0 0.486633,10.2157"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path5"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 0.486633,10.2157 A 10.7975,10.7975 0 0 0 6.25,10.8253"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path6"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 6.25,10.8253 A 13.2639,13.2639 0 0 0 13.5345,6.97754"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path7"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 13.5345,6.97754 A 15.8862,15.8862 0 0 0 17.5,0"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path8"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 7.5,0 A 8.22617,8.22617 0 0 0 8.60373,-5.52928"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path9"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 8.60373,-5.52928 A 10.7975,10.7975 0 0 0 6.25,-10.8253"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path10"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M 6.25,-10.8253 A 13.2639,13.2639 0 0 0 -0.724543,-15.21"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path11"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
d="M -0.724543,-15.21 A 15.8862,15.8862 0 0 0 -8.75,-15.1554"
|
||||
fill="none"
|
||||
stroke="#5c5ceb"
|
||||
stroke-width="3.0"
|
||||
id="path12"
|
||||
style="fill:none;stroke:#0179ff;stroke-width:5;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
id="l-top-copper"
|
||||
transform="matrix(0,-1,-1,0,2.0915813,2.0916089)"
|
||||
opacity="0.7"
|
||||
style="opacity:1;mix-blend-mode:color-dodge;fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none">
|
||||
<path
|
||||
d="M 17.5,0 A 15.5807,15.5807 0 0 0 12.4276,-7.98674"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path17"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 12.4276,-7.98674 A 12.9547,12.9547 0 0 0 6.25,-10.8253"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path18"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m 6.25,-10.8253 a 10.4951,10.4951 0 0 0 -6.715005,1.06364"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path19"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -0.465005,-9.76166 A 7.92896,7.92896 0 0 0 -3.75,-6.49519"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path20"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -8.75,-15.1554 a 15.5807,15.5807 0 0 0 -4.3805,8.38614"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path21"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -13.1305,-6.76926 A 12.9547,12.9547 0 0 0 -12.5,0"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path22"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="m -12.5,0 a 10.4951,10.4951 0 0 0 4.27866,5.28354"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path23"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -8.22134,5.28354 A 7.92896,7.92896 0 0 0 -3.75,6.49519"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path24"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M -8.75,15.1554 A 15.5807,15.5807 0 0 0 0.702915,14.756"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path25"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 0.702915,14.756 A 12.9547,12.9547 0 0 0 6.25,10.8253"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path26"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 6.25,10.8253 A 10.4951,10.4951 0 0 0 8.68635,4.47812"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path27"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 8.68635,4.47812 A 7.92896,7.92896 0 0 0 7.5,0"
|
||||
fill="none"
|
||||
stroke="#eb5c5c"
|
||||
stroke-width="3"
|
||||
id="path28"
|
||||
style="fill:none;stroke:#ff0000;stroke-width:5;stroke-dasharray:none" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -1,10 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
from kipy import KiCad
|
||||
from kipy.errors import ConnectionError
|
||||
|
||||
import kicoil.gui
|
||||
|
||||
if __name__ == '__main__':
|
||||
kicad_inst = KiCad()
|
||||
kicoil.gui.main(kicad_inst)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"$schema": "https://go.kicad.org/api/schemas/v1",
|
||||
"identifier": "de.jaseg.kicoil",
|
||||
"name": "KiCoil",
|
||||
"description": "Planar inductor supporting spiral coils, toroidal coils, and hybrids",
|
||||
"runtime": {
|
||||
"type": "python",
|
||||
"min_version": "3.13"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"identifier": "kicoil-generator",
|
||||
"name": "KiCoil Planar Inductor Generator",
|
||||
"description": "Generates planar spiral inductors, toroidal inductors, and hybrids thereof",
|
||||
"show-button": true,
|
||||
"scopes": ["pcb"],
|
||||
"entrypoint": "kicoil_action.py",
|
||||
"icons-light": ["icon-light.png"],
|
||||
"icons-dark": ["icon-dark.png"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"$schema": "https://go.kicad.org/pcm/schemas/v1",
|
||||
"name": "KiCoil",
|
||||
"description": "Planar inductor supporting arbitrary shapes of spiral coils, toroidal coils, and hybrids",
|
||||
"description_full": "KiCoil generates planar inductors as footprints. It supports arbitrary shapes with presets such as circles, rectangles, circular sectors, and SVG import for arbitrary shapes. It can generate spiral and toroid inductors as well as arbitrary intermediates between spiral and toroid inductors. By playing around with this, you can create inductors that have lower parasitics and higher self-resonant frequency than standard multilayer spiral inductors.",
|
||||
"description": "A planar inductor for KiCad",
|
||||
"description_full": "KiCoil generates planar inductors as footprints. Currently, circular spiral and toroid inductors are supported. KiCoil supports arbitrary intermediates between spiral and toroid inductors. By playing around with this, you can create inductors that have lower parasitics and higher self-resonant frequency than standard multilayer spiral inductors.",
|
||||
"identifier": "de.jaseg.kicoil",
|
||||
"type": "plugin",
|
||||
"author": {
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
"web": "https://jaseg.de/"
|
||||
}
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"license": "GPL-3.0",
|
||||
"resources": {
|
||||
"homepage": "https://jaseg.de/projects/kicoil",
|
||||
"git": "https://git.jaseg.de/kicoil",
|
||||
|
|
@ -21,29 +21,7 @@
|
|||
{
|
||||
"version": "0.9.0",
|
||||
"status": "stable",
|
||||
"kicad_version": "9.00",
|
||||
"download_sha256": "8c1f66c91fab315d76d640da1d197463412e4ee3d855428c37a5c66414b26b76",
|
||||
"download_size": 38942,
|
||||
"download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0",
|
||||
"install_size": 119659
|
||||
},
|
||||
{
|
||||
"version": "0.11.0",
|
||||
"status": "stable",
|
||||
"kicad_version": "9.00",
|
||||
"download_sha256": "455879f51a288ffc399f62556b1d83dc4988349f1759ddb283d711f72406310a",
|
||||
"download_size": 1886869,
|
||||
"download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.11.0.zip?h=v0.11.0",
|
||||
"install_size": 14353567
|
||||
},
|
||||
{
|
||||
"version": "0.11.1",
|
||||
"status": "stable",
|
||||
"kicad_version": "9.00",
|
||||
"download_sha256": "8494927da4d4aca48baf13d6c8a162dc910c0be1294e5a44517b7d27a086a140",
|
||||
"download_size": 1886906,
|
||||
"download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.11.1.zip?h=v0.11.1",
|
||||
"install_size": 14353726
|
||||
"kicad_version": "8.00"
|
||||
}
|
||||
],
|
||||
"runtime": "ipc"
|
||||
|
|
|
|||
155
package.py
155
package.py
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
import re
|
||||
import hashlib
|
||||
import tomllib
|
||||
import shutil
|
||||
import subprocess
|
||||
import json
|
||||
|
|
@ -13,97 +12,78 @@ def tree_size(path):
|
|||
return sum(entry.stat().st_size for entry in path.glob('**/*') if entry.is_file())
|
||||
|
||||
@click.command()
|
||||
@click.option('--dry-run', is_flag=True)
|
||||
def do_release(dry_run):
|
||||
with open('pyproject.toml', 'rb') as f:
|
||||
project_file = tomllib.load(f)
|
||||
version = project_file['project']['version']
|
||||
@click.option('--major', 'increment', flag_value='major')
|
||||
@click.option('--minor', 'increment', flag_value='minor', default=True)
|
||||
@click.option('--patch', 'increment', flag_value='patch', default=True)
|
||||
@click.argument('version', required=False)
|
||||
def do_release(version, increment):
|
||||
if not version:
|
||||
tag = subprocess.run('git describe --tags --abbrev=0 --match v*'.split(),
|
||||
check=True, capture_output=True, text=True)
|
||||
major, minor, patch = map(int, re.fullmatch(r'v([0-9]+)\.([0-9]+)\.([0-9]+)', tag.stdout.strip()).groups())
|
||||
match increment:
|
||||
case 'major':
|
||||
major, minor, patch = (major+1, 0, 0)
|
||||
case 'minor':
|
||||
major, minor, patch = (major, minor+1, 0)
|
||||
case 'patch':
|
||||
major, minor, patch = (major, minor, patch+1)
|
||||
version = f'{major}.{minor}.{patch}'
|
||||
|
||||
if not dry_run:
|
||||
res = subprocess.run('git status --porcelain --untracked-files=no'.split(),
|
||||
check=True, capture_output=True, text=True)
|
||||
if res.stdout.strip():
|
||||
raise click.ClickException('There are uncommitted changes in this repository.')
|
||||
res = subprocess.run('git status --porcelain --untracked-files=no'.split(),
|
||||
check=True, capture_output=True, text=True)
|
||||
if res.stdout.strip():
|
||||
raise click.ClickException('There are uncommitted changes in this repository.')
|
||||
|
||||
project_root = Path(__file__).parent
|
||||
print('Cleaning old footprints')
|
||||
footprint_dir = Path('de.jaseg.kimesh.footprints') / 'footprints'
|
||||
shutil.rmtree(footprint_dir, ignore_errors=True)
|
||||
footprint_dir.mkdir()
|
||||
|
||||
print('Re-generating footprints')
|
||||
for n in range(1, 9):
|
||||
subprocess.run(['python', '-m', 'footprint_generator',
|
||||
'-w', '0.100,0.120,0.150,0.200,0.250,0.300,0.350,0.400,0.500,0.600,0.700,0.800,1.000,1.200,1.500,1.800',
|
||||
'-c', '0.100,0.120,0.150,0.200,0.300,0.400,0.500',
|
||||
'-n', str(n),
|
||||
str(footprint_dir / f'kimesh_anchors_{n}wire.pretty')
|
||||
], check=True)
|
||||
|
||||
res = subprocess.run('git ls-files'.split(), check=True, capture_output=True, text=True)
|
||||
for path in res.stdout.splitlines():
|
||||
if re.fullmatch(r'de\.jaseg\.kicoil\.[^/]*-v[.0-9]*\.zip', path.strip()):
|
||||
if re.fullmatch(r'de\.jaseg\.kimesh\.[^/]*-v[.0-9]*\.zip', path.strip()):
|
||||
print(f'Removing old release zip {path} from git index.')
|
||||
subprocess.run(['git', 'rm', path], check=True, capture_output=True)
|
||||
|
||||
plugin_sources = project_root / 'kicad-plugin'
|
||||
pkg_dir = project_root / 'de.jaseg.kicoil'
|
||||
plugin_dir = pkg_dir / 'plugins'
|
||||
for pkg_dir in Path('de.jaseg.kimesh.plugin'), Path('de.jaseg.kimesh.footprints'):
|
||||
# NOTE: metadata.json appears twice. In what I believe is a sub-optimal design choice, the variant in the
|
||||
# archive is only allowed to contain the current version in its version list without its zip file metadata,
|
||||
# while the variant in the repository index is supposed to contain all past versions including their zip file
|
||||
# metadata. AFAICT they are the same otherwise.
|
||||
meta_path = Path(f'{pkg_dir}-repo-metadata.json')
|
||||
|
||||
if pkg_dir.is_dir():
|
||||
shutil.rmtree(pkg_dir)
|
||||
pkg_dir.mkdir()
|
||||
print(f'Updating metadata file {meta_path}')
|
||||
ver_dict = {
|
||||
'version': version,
|
||||
'status': 'stable',
|
||||
'kicad_version': '7.99',
|
||||
}
|
||||
|
||||
shutil.copytree(plugin_sources, plugin_dir)
|
||||
shutil.copy(project_root / 'LICENSE', plugin_dir)
|
||||
# Include just the version metadata in the metadata for the archive
|
||||
meta_file = json.loads(meta_path.read_text())
|
||||
meta_file['versions'] = [ver_dict]
|
||||
(pkg_dir / 'metadata.json').write_text(json.dumps(meta_file, indent=4))
|
||||
|
||||
meta_path = project_root / 'metadata.json'
|
||||
print(f'Updating metadata file {meta_path}')
|
||||
ver_dict = {
|
||||
'version': version,
|
||||
'status': 'stable',
|
||||
'kicad_version': '9.00',
|
||||
}
|
||||
|
||||
meta_file = json.loads(meta_path.read_text())
|
||||
meta_file['versions'] = [ver_dict]
|
||||
(pkg_dir / 'metadata.json').write_text(json.dumps(meta_file, indent=4))
|
||||
|
||||
res = subprocess.run(['uv', 'export', '--no-hashes', '--no-emit-project', '--format', 'requirements.txt', '--group', 'gui'],
|
||||
check=True, capture_output=True, text=True)
|
||||
(plugin_dir / 'requirements.txt').write_text(res.stdout)
|
||||
|
||||
(pkg_dir / 'resources').mkdir()
|
||||
shutil.copy(plugin_sources / 'icon-light.png', pkg_dir / 'resources' / 'icon.png')
|
||||
|
||||
module_sources = project_root / 'src'
|
||||
for root, dirs, files in module_sources.walk(top_down=True):
|
||||
if root.name == '__pycache__':
|
||||
continue
|
||||
|
||||
for path in dirs:
|
||||
if path == '__pycache__':
|
||||
continue
|
||||
|
||||
path = root / path
|
||||
subdir = plugin_dir / path.relative_to(module_sources)
|
||||
subdir.mkdir()
|
||||
|
||||
for path in files:
|
||||
path = root / path
|
||||
out_path = plugin_dir / path.relative_to(module_sources)
|
||||
if path.name == '__init__.py':
|
||||
content = path.read_text()
|
||||
lines = content.splitlines()
|
||||
lines_out = []
|
||||
for line in lines:
|
||||
if line.startswith('__version__ = version('):
|
||||
line = f'__version__ = {version!r}'
|
||||
lines_out.append(line)
|
||||
content = '\n'.join(lines_out).encode('utf-8')
|
||||
else:
|
||||
content = path.read_bytes()
|
||||
out_path.write_bytes(content)
|
||||
|
||||
zip_fn = Path(shutil.make_archive(f'{pkg_dir.name}-v{version}', 'zip', pkg_dir, '.'))
|
||||
if not dry_run:
|
||||
zip_fn = Path(shutil.make_archive(f'{pkg_dir.name}-v{version}', 'zip', pkg_dir, '.'))
|
||||
print(f'Adding new release zip {zip_fn} to git index.')
|
||||
subprocess.run(['git', 'add', str(zip_fn)], check=True, capture_output=True)
|
||||
|
||||
# Add the zip's metadata to the metadata for the repository
|
||||
ver_dict['download_sha256'] = hashlib.sha256(zip_fn.read_bytes()).hexdigest()
|
||||
ver_dict['download_size'] = zip_fn.stat().st_size
|
||||
ver_dict['download_url'] = f'https://git.jaseg.de/kimesh.git/plain/{zip_fn.name}?h=v{version}'
|
||||
ver_dict['install_size'] = tree_size(pkg_dir)
|
||||
# Add the zip's metadata to the metadata for the repository
|
||||
ver_dict['download_sha256'] = hashlib.sha256(zip_fn.read_bytes()).hexdigest()
|
||||
ver_dict['download_size'] = zip_fn.stat().st_size
|
||||
ver_dict['download_url'] = f'https://git.jaseg.de/kimesh.git/plain/{zip_fn.name}?h=v{version}'
|
||||
ver_dict['install_size'] = tree_size(pkg_dir)
|
||||
|
||||
if not dry_run:
|
||||
meta_file = json.loads(meta_path.read_text())
|
||||
meta_file['versions'].append(ver_dict)
|
||||
meta_path.write_text(json.dumps(meta_file, indent=4))
|
||||
|
|
@ -111,17 +91,16 @@ def do_release(dry_run):
|
|||
print(f'Adding updated metadata file {meta_path} to git index')
|
||||
subprocess.run(['git', 'add', str(meta_path)], check=True, capture_output=True)
|
||||
|
||||
if not dry_run:
|
||||
print('Create git commit')
|
||||
subprocess.run(['git', 'commit', '-m', f'KiCad package version {version}', '--no-edit'], check=True, capture_output=True)
|
||||
res = subprocess.run('git rev-parse --short HEAD'.split(), check=True, capture_output=True, text=True)
|
||||
print(f'Created commit {res.stdout.strip()}')
|
||||
print(f'Creating and signing version tag v{version}')
|
||||
subprocess.run(['git',
|
||||
'-c', 'user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D',
|
||||
'-c', 'user.email=python-mpv@jaseg.de',
|
||||
'tag', '-s', f'v{version}', '-m', f'Version v{version}'],
|
||||
check=True)
|
||||
print('Create git commit')
|
||||
subprocess.run(['git', 'commit', '-m', f'Version {version}', '--no-edit'], check=True, capture_output=True)
|
||||
res = subprocess.run('git rev-parse --short HEAD'.split(), check=True, capture_output=True, text=True)
|
||||
print(f'Created commit {res.stdout.strip()}')
|
||||
print(f'Creating and signing version tag v{version}')
|
||||
subprocess.run(['git',
|
||||
'-c', 'user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D',
|
||||
'-c', 'user.email=python-mpv@jaseg.de',
|
||||
'tag', '-s', f'v{version}', '-m', f'Version v{version}'],
|
||||
check=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
do_release()
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
[project]
|
||||
name = "kicoil"
|
||||
version = "0.11.1"
|
||||
version = "0.9.0"
|
||||
description = "Planar Inductor Generator"
|
||||
readme = "README.rst"
|
||||
license = "Apache-2.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"beautifulsoup4>=4.14.3",
|
||||
"click",
|
||||
"gerbonara>=1.6.0",
|
||||
"kicad-python>=0.5.0",
|
||||
"lxml>=6.0.2",
|
||||
"platformdirs>=4.5.1",
|
||||
"py-straight-skeleton>=0.1.0",
|
||||
"wasmtime>=39.0.0",
|
||||
]
|
||||
dependencies = ["click", "gerbonara>=1.6.0"]
|
||||
authors = [{ name = "jaseg" }]
|
||||
maintainers = [
|
||||
{ name = "Kicoil maintainers", email = "kicoil@jaseg.de" },
|
||||
|
|
@ -46,12 +37,7 @@ kicoil = "kicoil.cli:cli"
|
|||
kicoil-gui = "kicoil.gui:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ipykernel>=7.1.0",
|
||||
"matplotlib>=3.10.8",
|
||||
]
|
||||
gui = ["cairosvg", "pillow"]
|
||||
gds = ["gdstk"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv-build"]
|
||||
|
|
|
|||
|
|
@ -17,19 +17,15 @@ import logging
|
|||
import subprocess
|
||||
import webbrowser
|
||||
import tempfile
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import warnings
|
||||
import math
|
||||
|
||||
import click
|
||||
from gerbonara.layers import LayerStack
|
||||
from gerbonara.cad.kicad.primitives import kicad_mid_to_center_arc
|
||||
|
||||
from .geometry import PlanarInductor, divisors, CircleShape, SectorShape, StarShape, SVGShape, RectangleShape, RegularPolygonShape
|
||||
from .geometry import PlanarInductor, divisors
|
||||
from .kicad import footprint_to_board
|
||||
from .svg import make_transparent_svg
|
||||
|
||||
|
|
@ -45,300 +41,102 @@ def print_valid_twists(ctx, param, value):
|
|||
ctx.exit()
|
||||
|
||||
|
||||
def circle_center_to_tangents(center, a, b):
|
||||
""" Given two points on a circle and the center of the circle, calculate the intersection of two tangents at the two points """
|
||||
cx, cy = center
|
||||
ax, ay = a
|
||||
bx, by = b
|
||||
|
||||
dax = ax - cx
|
||||
day = ay - cy
|
||||
dbx = bx - cx
|
||||
dby = by - cy
|
||||
|
||||
v = dax*ax + day*ay
|
||||
w = dbx*bx + dby*by
|
||||
det = dax*dby - day*dbx
|
||||
|
||||
ix = (v*dby - day*w) / det
|
||||
iy = (dax*w - v*dbx) / det
|
||||
|
||||
return ix, iy
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.command()
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.")
|
||||
@click.option('--layer-pair', default='F.Cu,B.Cu', help="Target KiCad layer pair for the generated footprint, comma-separated. Default: F.Cu/B.Cu.")
|
||||
@click.option('--turns', type=int, default=5, help='Number of turns')
|
||||
@click.option('--twists', type=int, default=1, help='Number of twists per revolution. Note that this number must be co-prime to the number of turns. Run with --show-twists to list valid values. (default: 1)')
|
||||
@click.option('--clockwise/--counter-clockwise', help='Direction of generated top layer spiral. Default: counter-clockwise when wound from the inside.')
|
||||
@click.option('--single-layer/--two-layer', help='Single-layer mode. This just forces twists to 0.')
|
||||
@click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.')
|
||||
@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]')
|
||||
@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]')
|
||||
@click.option('--stagger-inner-vias/--no-stagger-inner-vias', default=False, help='Stagger inner via ring')
|
||||
@click.option('--stagger-outer-vias/--no-stagger-outer-vias', default=False, help='Stagger outer via ring')
|
||||
@click.option('--trace-width', type=float, default=None)
|
||||
@click.option('--clearance', type=float, default=None)
|
||||
@click.option('--via-diameter', type=float, default=0.6)
|
||||
@click.option('--via-drill', type=float, default=None)
|
||||
@click.option('--via-offset', type=float, default=None, help='Radially offset vias from trace endpoints [mm]')
|
||||
@click.option('--keepout-zone/--no-keepout-zone', default=True, help='Add a keepout are to the footprint (default: yes)')
|
||||
@click.option('--keepout-margin', type=float, default=5, help='Margin between outside of coil and keepout area (mm, default: 5)')
|
||||
@click.option('--copper-thickness', type=float, default=0.035, help='Copper thickness for resistance calculation, in mm. Default: 0.035mm ^= 1 Oz')
|
||||
@click.option('--twists', type=int, default=1, help='Number of twists per revolution. Note that this number must be co-prime to the number of turns. Run with --show-twists to list valid values. (default: 1)')
|
||||
@click.option('--circle-segments', type=int, default=64, help='When not using arcs, the number of points to use for arc interpolation per 360 degrees.')
|
||||
@click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.')
|
||||
@click.option('--clearance', type=float, default=None)
|
||||
@click.option('--arc-tolerance', type=float, default=0.02)
|
||||
@click.option('--approximate-arcs/--no-approximate-arcs', default=True, help='Use circular arcs to smoothen output shape (default: on)')
|
||||
@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'json', 'gdsii', 'oasis', 'show']), default='kicad-footprint')
|
||||
@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'show']), default='kicad-footprint')
|
||||
@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)')
|
||||
@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.")
|
||||
@click.option('--cell-name', help="Name for the generated cell when exporting GDSII. Default: Output file name sans extension.")
|
||||
@click.option('--layer-pair', default='F.Cu,B.Cu', help="Target KiCad layer pair for the generated footprint, comma-separated. Default: F.Cu/B.Cu.")
|
||||
@click.option('--geometry-debug-file', type=click.Path(writable=True), help='Render geometry debug information to a PDF file with the given name')
|
||||
@click.option('--clockwise/--counter-clockwise', help='Direction of generated top layer spiral. Default: counter-clockwise when wound from the inside.')
|
||||
@click.option('--single-layer/--two-layer', help='Single-layer mode. This just forces twists to 0.')
|
||||
@click.version_option()
|
||||
@click.pass_context
|
||||
def cli(ctx, footprint_name, cell_name, clipboard, single_layer, arc_tolerance, circle_segments, format, geometry_debug_file, **kwargs):
|
||||
ctx.ensure_object(dict)
|
||||
def cli(outfile, footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, **kwargs):
|
||||
logger = logging.getLogger('kicoil')
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def write(shape, outfile):
|
||||
nonlocal footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, cell_name, geometry_debug_file
|
||||
logger = logging.getLogger('kicoil')
|
||||
if single_layer:
|
||||
kwargs['layers'] = 1
|
||||
else:
|
||||
kwargs['layers'] = 2
|
||||
|
||||
if single_layer:
|
||||
kwargs['layers'] = 1
|
||||
else:
|
||||
kwargs['layers'] = 2
|
||||
try:
|
||||
model = PlanarInductor(**kwargs)
|
||||
|
||||
try:
|
||||
model = PlanarInductor(shape=shape, **kwargs)
|
||||
if footprint_name is None and outfile:
|
||||
footprint_name = outfile.stem
|
||||
|
||||
if footprint_name is None and outfile:
|
||||
footprint_name = outfile.stem
|
||||
footprint = model.render_footprint(footprint_name, arc_tolerance, circle_segments)
|
||||
|
||||
footprint = model.render_footprint(footprint_name, arc_tolerance, circle_segments, geometry_debug_file)
|
||||
except ValueError as e:
|
||||
raise click.ClickException(*e.args)
|
||||
|
||||
except ValueError as e:
|
||||
#raise click.ClickException(*e.args)
|
||||
raise
|
||||
data = None
|
||||
if format == 'kicad-footprint':
|
||||
data = footprint.serialize()
|
||||
|
||||
data = None
|
||||
if format == 'kicad-footprint':
|
||||
data = footprint.serialize()
|
||||
elif format == 'kicad-pcb':
|
||||
data = footprint_to_board(footprint).serialize()
|
||||
|
||||
elif format == 'kicad-pcb':
|
||||
data = footprint_to_board(footprint).serialize()
|
||||
elif format == 'gerber':
|
||||
stack = LayerStack()
|
||||
footprint.render(stack)
|
||||
|
||||
elif format == 'gerber':
|
||||
stack = LayerStack()
|
||||
footprint.render(stack)
|
||||
|
||||
if not clipboard and outfile and outfile.suffix.lower() != '.zip':
|
||||
stack.save_to_directory(outfile)
|
||||
return
|
||||
|
||||
else:
|
||||
with tempfile.NamedTemporaryFile(delete_on_close=False) as f:
|
||||
f = Path(f.name)
|
||||
stack.save_to_zipfile(f)
|
||||
data = f.read_bytes()
|
||||
|
||||
elif format == 'json':
|
||||
lines = defaultdict(lambda: [])
|
||||
for l in footprint.lines:
|
||||
lines[l.layer].append({
|
||||
'x1': l.start.x,
|
||||
'y1': l.start.y,
|
||||
'x2': l.end.x,
|
||||
'y2': l.end.y})
|
||||
|
||||
arcs = defaultdict(lambda: [])
|
||||
for a in footprint.arcs:
|
||||
center, r, direction = kicad_mid_to_center_arc(a.mid, a.start, a.end)
|
||||
arcs[a.layer].append({
|
||||
'x1': a.start.x,
|
||||
'y1': a.start.y,
|
||||
'x2': a.end.x,
|
||||
'y2': a.end.y,
|
||||
'cx': center[0],
|
||||
'cy': center[1],
|
||||
})
|
||||
|
||||
vias = [{
|
||||
'x': p.at.x,
|
||||
'y': p.at.y,
|
||||
'pad': p.size.x,
|
||||
'drill': p.drill.diameter,
|
||||
} for p in footprint.pads if p.number == 'NC']
|
||||
|
||||
pads = [{
|
||||
'x': p.at.x,
|
||||
'y': p.at.y,
|
||||
'pad': p.size.x,
|
||||
} for p in footprint.pads if p.number != 'NC']
|
||||
|
||||
d = {
|
||||
'lines': dict(lines),
|
||||
'arcs': dict(arcs),
|
||||
'vias': vias,
|
||||
'pads': pads
|
||||
}
|
||||
|
||||
data = json.dumps(d, indent=4)
|
||||
|
||||
elif format in ('gdsii', 'oasis'):
|
||||
import gdstk
|
||||
|
||||
DRILL_LAYER = 2
|
||||
lib = gdstk.Library()
|
||||
|
||||
if cell_name is None:
|
||||
if outfile:
|
||||
cell_name = outfile.stem
|
||||
else:
|
||||
cell_name = f'planar_coil'
|
||||
cell = lib.new_cell(cell_name)
|
||||
|
||||
for line in footprint.lines:
|
||||
layer = model.layer_pair.index(line.layer)
|
||||
path = gdstk.FlexPath([(line.start.x, line.start.y), (line.end.x, line.end.y)], line.stroke.width, ends=['round'], layer=layer)
|
||||
cell.add(path)
|
||||
|
||||
for arc in footprint.arcs:
|
||||
layer = model.layer_pair.index(arc.layer)
|
||||
center, r, _direction = kicad_mid_to_center_arc(arc.mid, arc.start, arc.end)
|
||||
proj_x, proj_y = circle_center_to_tangents(center, tuple(arc.start), tuple(arc.end))
|
||||
# multiply r with 0.99 to make sure gdstk's interpolation routine catches on since the arc endpoints are calculated exactly
|
||||
path = gdstk.FlexPath([(arc.start.x, arc.start.y), (proj_x, proj_y), (arc.end.x, arc.end.y)], arc.stroke.width, bend_radius=r*0.99, ends=['round'], layer=layer)
|
||||
cell.add(path)
|
||||
|
||||
for pad in footprint.pads:
|
||||
for layer in pad.layers:
|
||||
layer = model.layer_pair.index(layer)
|
||||
layer_obj = gdstk.ellipse(tuple(pad.at), pad.size.x/2, layer=layer)
|
||||
cell.add(layer_obj)
|
||||
|
||||
if pad.drill:
|
||||
drill = gdstk.ellipse(tuple(pad.at), pad.drill.diameter/2, layer=DRILL_LAYER)
|
||||
cell.add(drill)
|
||||
|
||||
if clipboard or not outfile:
|
||||
raise click.ClickException('outfile is required for GDSII or OASIS export')
|
||||
|
||||
if format == 'gdsii':
|
||||
lib.write_gds(outfile)
|
||||
else:
|
||||
lib.write_oas(outfile)
|
||||
if not clipboard and outfile and outfile.suffix.lower() != '.zip':
|
||||
stack.save_to_directory(outfile)
|
||||
return
|
||||
|
||||
elif format in ('svg', 'show'):
|
||||
data = str(make_transparent_svg(footprint))
|
||||
|
||||
if format == 'show':
|
||||
with tempfile.NamedTemporaryFile('w', suffix='.svg', delete=False) as f:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
webbrowser.open_new_tab(f'file://{f.name}')
|
||||
return
|
||||
|
||||
if clipboard:
|
||||
if 'WAYLAND_DISPLAY' in os.environ:
|
||||
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
|
||||
else:
|
||||
copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard'
|
||||
|
||||
try:
|
||||
logger.info(f'Running {copy[0]}.', file=sys.stderr)
|
||||
proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=isinstance(data, str))
|
||||
proc.communicate(data)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise click.ClickException(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr)
|
||||
|
||||
elif not outfile:
|
||||
if isinstance(data, str):
|
||||
print(data)
|
||||
else:
|
||||
sys.stdout.buffer.write(data)
|
||||
|
||||
else:
|
||||
outfile.write_text(data)
|
||||
|
||||
ctx.obj['write'] = write
|
||||
with tempfile.NamedTemporaryFile(delete_on_close=False) as f:
|
||||
f = Path(f.name)
|
||||
stack.save_to_zipfile(f)
|
||||
data = f.read_bytes()
|
||||
|
||||
@cli.command()
|
||||
@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]')
|
||||
@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]')
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def circle(ctx, inner_diameter, outer_diameter, outfile):
|
||||
shape = CircleShape(outer_diameter, inner_diameter)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
elif format in ('svg', 'show'):
|
||||
data = str(make_transparent_svg(footprint))
|
||||
|
||||
@cli.command()
|
||||
@click.option('--width', type=float, default=50, help='Base width [mm]')
|
||||
@click.option('--height', type=float, default=40, help='Shape height [mm]')
|
||||
@click.option('--offset', type=float, default=10, help='Offset of each corner at the shorter edge compared to the longer edge [mm]')
|
||||
@click.option('--annular-width', type=float, default=10, help='Width of the trace area on the outside of the shape [mm]')
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def trapezoid(ctx, outfile, **kwargs):
|
||||
shape = TrapezoidShape(**kwargs)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
if format == 'show':
|
||||
with tempfile.NamedTemporaryFile('w', suffix='.svg', delete=False) as f:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
webbrowser.open_new_tab(f'file://{f.name}')
|
||||
return
|
||||
|
||||
if clipboard:
|
||||
if 'WAYLAND_DISPLAY' in os.environ:
|
||||
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
|
||||
else:
|
||||
copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard'
|
||||
|
||||
@cli.command()
|
||||
@click.option('--width', type=float, default=50, help='Width [mm]')
|
||||
@click.option('--height', type=float, default=40, help='Height [mm]')
|
||||
@click.option('--annular-width', type=float, default=10, help='Width of the trace area on the outside of the shape [mm]')
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def rectangle(ctx, outfile, **kwargs):
|
||||
shape = RectangleShape(**kwargs)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
try:
|
||||
logger.info(f'Running {copy[0]}.', file=sys.stderr)
|
||||
proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=isinstance(data, str))
|
||||
proc.communicate(data)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise click.ClickException(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr)
|
||||
|
||||
@cli.command()
|
||||
@click.option('--diameter', type=float, default=50, help='Width [mm]')
|
||||
@click.option('-n', '--corners', type=int, default=8, help='Number of corners')
|
||||
@click.option('--annular-width', type=float, default=10, help='Width of the trace area on the outside of the shape [mm]')
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def regular_polygon(ctx, outfile, **kwargs):
|
||||
shape = RegularPolygonShape(**kwargs)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
elif not outfile:
|
||||
if isinstance(data, str):
|
||||
print(data)
|
||||
else:
|
||||
sys.stdout.buffer.write(data)
|
||||
|
||||
else:
|
||||
outfile.write_text(data)
|
||||
|
||||
@cli.command()
|
||||
@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]')
|
||||
@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]')
|
||||
@click.option('--angle', type=float, default=45, help='Sector angle [deg]')
|
||||
@click.option('--arc-tolerance', type=float, default=0.05, help='Tolerance for splitting arc into straight segments [mm] (default: 0.05 mm)')
|
||||
@click.option('--annular-width', type=float, default=5, help='Width of the trace area on the outside of the shape [mm]')
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def sector(ctx, outfile, angle, **kwargs):
|
||||
angle = math.radians(angle)
|
||||
shape = SectorShape(angle=angle, **kwargs)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]')
|
||||
@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]')
|
||||
@click.option('--points', type=int, default=5, help='Number of points')
|
||||
@click.option('--arc-tolerance', type=float, default=0.05, help='Tolerance for splitting arc into straight segments [mm] (default: 0.05 mm)')
|
||||
@click.option('--annular-width', type=float, default=5, help='Width of the trace area on the outside of the shape [mm]')
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def star(ctx, outfile, **kwargs):
|
||||
shape = StarShape(**kwargs)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--arc-tolerance', type=float, default=0.05, help='Tolerance for splitting arc into straight segments [mm] (default: 0.05 mm)')
|
||||
@click.option('--annular-width', type=float, default=5, help='Width of the trace area on the outside of the shape [mm]')
|
||||
@click.argument('svg_file', required=False, type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||
@click.pass_context
|
||||
def svg(ctx, svg_file, outfile, **kwargs):
|
||||
shape = SVGShape(svg_file, **kwargs)
|
||||
ctx.obj['write'](shape, outfile)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from math import *
|
|||
from gerbonara.cad.kicad.footprints import Footprint
|
||||
from gerbonara.cad.kicad.primitives import Zone, Hatch, ZoneKeepout, ZonePolygon, XYCoord
|
||||
|
||||
from . import kicad, skeletonator
|
||||
from . import kicad
|
||||
|
||||
|
||||
mu_0 = 1.25663706127e-06 # from scipy.constants
|
||||
|
|
@ -67,7 +67,7 @@ def arc_approximate(points, trace_width, layer, tolerance=0.02, level=0):
|
|||
gerbonara arc objects approximating the input. """
|
||||
indent = ' ' * level
|
||||
if len(points) < 3:
|
||||
raise ValueError('Need at least three points to approximate')
|
||||
raise ValueError()
|
||||
|
||||
i_mid = len(points)//2
|
||||
|
||||
|
|
@ -94,282 +94,29 @@ def arc_approximate(points, trace_width, layer, tolerance=0.02, level=0):
|
|||
yield kicad.make_arc(x2, y2, x0, y0, x1, y1, trace_width, layer)
|
||||
|
||||
|
||||
def polygon_is_clockwise(coords):
|
||||
# https://en.wikipedia.org/wiki/Curve_orientation
|
||||
xb, yb, i = min([(x, y, i) for i, (x, y) in enumerate(coords)])
|
||||
xa, ya = coords[(i-1) % len(coords)]
|
||||
xc, yc = coords[(i+1) % len(coords)]
|
||||
det = (xa*yb + xb*yc + xc*ya) - (xa*yc + xb * ya + xc * yb)
|
||||
return det < 0
|
||||
def compute_spiral(r1, r2, a1, a2, fn=64):
|
||||
fn = ceil(fn * abs(a2-a1)/(2*pi))
|
||||
x0, y0 = cos(a1)*r1, sin(a1)*r1
|
||||
dr = 3 if r2 < r1 else -3
|
||||
|
||||
xn, yn = x0, y0
|
||||
points = [(x0, y0)]
|
||||
dists = []
|
||||
for i in range(fn):
|
||||
xp, yp = xn, yn
|
||||
r = r1 + (i+1)*(r2-r1)/fn
|
||||
a = a1 + (i+1)*(a2-a1)/fn
|
||||
xn, yn = cos(a)*r, sin(a)*r
|
||||
points.append((xn, yn))
|
||||
dists.append(dist((xp, yp), (xn, yn)))
|
||||
|
||||
class Shape:
|
||||
pass
|
||||
return points, sum(dists)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CircleShape(Shape):
|
||||
outer_diameter: float
|
||||
inner_diameter: float
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.outer_radius = self.outer_diameter / 2
|
||||
self.inner_radius = self.inner_diameter / 2
|
||||
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return 'circle_{self.outer_diameter:.2f}x{self.inner_diameter:.2f}'
|
||||
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'{self.outer_diameter:.2f} mm OD, {self.inner_diameter:.2f} mm ID circular'
|
||||
|
||||
|
||||
def compute_spiral(self, a1, a2, fn=64, debug=False):
|
||||
r1, r2 = self.outer_radius, self.inner_radius
|
||||
fn = ceil(fn * abs(a2-a1)/(2*pi))
|
||||
x0, y0 = cos(a1)*r1, sin(a1)*r1
|
||||
dr = 3 if r2 < r1 else -3
|
||||
|
||||
xn, yn = x0, y0
|
||||
points = [(x0, y0)]
|
||||
dists = []
|
||||
for i in range(fn):
|
||||
xp, yp = xn, yn
|
||||
r = r1 + (i+1)*(r2-r1)/fn
|
||||
a = a1 + (i+1)*(a2-a1)/fn
|
||||
xn, yn = cos(a)*r, sin(a)*r
|
||||
points.append((xn, yn))
|
||||
dists.append(dist((xp, yp), (xn, yn)))
|
||||
|
||||
return points, sum(dists), [None]*len(points)
|
||||
|
||||
|
||||
def project_point(self, r, a, r_ref=None):
|
||||
return cos(a) * r, sin(a) * r
|
||||
|
||||
|
||||
def offset_exterior(self, margin):
|
||||
r = self.outer_radius + margin
|
||||
tol = 0.05 # mm
|
||||
n = ceil(pi / acos(1 - tol/r))
|
||||
return [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)]
|
||||
|
||||
|
||||
class OffsetShape(Shape):
|
||||
def __post_init__(self):
|
||||
self.sk = skeletonator.Skeletonator(self.polygon)
|
||||
if self.annular_width > self.sk.min_radius:
|
||||
raise ValueError(f'Annular width ({self.annular_width:.2f}) is too large. Must be less than {self.sk.min_radius:.2f}')
|
||||
self.outer_radius = self.sk.radius
|
||||
self.inner_radius = self.sk.radius - self.annular_width
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return 'polygonal_{len(self.polygon)}pt_r{self.radius:.2f}mm'
|
||||
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'polygonal (n={len(self.polygon)} point, r={self.radius:.2f} mm radius)'
|
||||
|
||||
|
||||
def compute_spiral(self, a1, a2, fn=None, debug=False):
|
||||
# Skeletonator uses a t coordinate from 0 - 1 per revolution instead of a radian angle.
|
||||
points = []
|
||||
angle_refs = []
|
||||
for point, angle_ref in self.sk.do_spiral(a1/(2*pi), a2/(2*pi), self.outer_radius, self.inner_radius, debug=debug):
|
||||
points.append(point)
|
||||
angle_refs.append(angle_ref)
|
||||
if a2 < a1:
|
||||
points = points[::-1]
|
||||
angle_refs = angle_refs[::-1]
|
||||
arm_length = sum(dist(p1, p2) for p1, p2 in zip(points, points[1:]))
|
||||
return points, arm_length, angle_refs
|
||||
|
||||
|
||||
def project_point(self, r, a, r_ref=None):
|
||||
# Skeletonator uses a t coordinate from 0 - 1 per revolution instead of a radian angle.
|
||||
return self.sk.project_point(a/(2*pi) % 1, r, r_ref=r_ref)
|
||||
|
||||
|
||||
def offset_exterior(self, margin):
|
||||
return self.sk.do_spiral(0, 1, self.outer_radius + margin, self.outer_radius + margin)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrapezoidShape(OffsetShape):
|
||||
width: float
|
||||
height: float
|
||||
offset: float
|
||||
annular_width: float
|
||||
arc_tolerance: float = 0.05 # mm
|
||||
polygon: list = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
w, h, d = self.width, self.height, self.offset
|
||||
self.polygon = [(w/2-d, -h/2), (w/2, h/2), (-w/2, h/2), (-w/2+d, -h/2)]
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return f'trapezoid_{self.width:.2f}-2*{self.offset:.2f}x{self.height:.2f}'
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'{self.width:.2f} x {self.height:.2f} mm, {self.offset:.2f} mm offset isosceles trapezoidal'
|
||||
|
||||
|
||||
@dataclass
|
||||
class RectangleShape(OffsetShape):
|
||||
width: float
|
||||
height: float
|
||||
annular_width: float
|
||||
arc_tolerance: float = 0.05 # mm
|
||||
polygon: list = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
w, h = self.width, self.height
|
||||
self.polygon = [(w/2, -h/2), (w/2, h/2), (-w/2, h/2), (-w/2, -h/2)]
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return f'rectangle_{self.width:.2f}x{self.height:.2f}'
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'{self.width:.2f} x {self.height:.2f} mm rectangle'
|
||||
|
||||
@dataclass
|
||||
class SectorShape(OffsetShape):
|
||||
inner_diameter: float
|
||||
outer_diameter: float
|
||||
angle: float
|
||||
annular_width: float
|
||||
arc_tolerance: float = 0.05 # mm
|
||||
polygon: list = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
# Careful: The inner/outer radius properties are relative to the polygon center and are very different from these!
|
||||
r1, r2 = self.inner_diameter / 2, self.outer_diameter/2
|
||||
n1 = ceil(pi / acos(1 - self.arc_tolerance/self.inner_diameter) * self.angle / (2*pi))
|
||||
n2 = ceil(pi / acos(1 - self.arc_tolerance/self.outer_diameter) * self.angle / (2*pi))
|
||||
# center on y axis
|
||||
pt = lambda r, a: (r*sin(a), r*cos(a))
|
||||
self.polygon = [pt(r2, self.angle/2 - i/n1 * self.angle) for i in range(n1+1)]
|
||||
self.polygon += [pt(r1, i/n1 * self.angle - self.angle/2) for i in range(n1+1)]
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return f'sector_{self.outer_diameter:.2f}x{self.inner_diameter:.2f}_{degrees(self.angle):.0f}deg'
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'{self.outer_diameter:.2f} x {self.inner_diameter:.2f} mm {degrees(self.angle):.0f} deg sector'
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarShape(OffsetShape):
|
||||
inner_diameter: float
|
||||
outer_diameter: float
|
||||
annular_width: float
|
||||
points: int = 5
|
||||
arc_tolerance: float = 0.05 # mm
|
||||
polygon: list = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
# center on y axis
|
||||
pt = lambda r, a: (-r*sin(a), r*cos(a))
|
||||
circle = lambda r, n, phase: [pt(r, (i + phase)*2*pi/n) for i in range(n)]
|
||||
self.polygon = [x for pair in zip(circle(self.outer_diameter/2, self.points, 0), circle(self.inner_diameter/2, self.points, 0.5)) for x in pair]
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return f'star_{self.outer_diameter:.2f}x{self.inner_diameter:.2f}'
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
purpose = ', for demonic purposes' if self.points == 5 else ''
|
||||
return f'{self.outer_diameter:.2f} x {self.inner_diameter:.2f} mm star shape{purpose}'
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegularPolygonShape(OffsetShape):
|
||||
diameter: float
|
||||
annular_width: float
|
||||
corners: int = 8
|
||||
arc_tolerance: float = 0.05 # mm
|
||||
polygon: list = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
# center on y axis
|
||||
pt = lambda r, a: (-r*sin(a), r*cos(a))
|
||||
circle = lambda r, n, phase: [pt(r, (i + phase)*2*pi/n) for i in range(n)]
|
||||
self.polygon = list(circle(self.diameter/2, self.corners, 0))
|
||||
print(self.polygon)
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return f'regular_{self.corners}gon_{self.diameter:.2f}'
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'{self.diameter:.2f} mm diameter {self.corners} corner regular polygon'
|
||||
|
||||
|
||||
@dataclass
|
||||
class SVGShape(OffsetShape):
|
||||
filename: str
|
||||
annular_width: float
|
||||
arc_tolerance: float = 0.05 # mm
|
||||
polygon: list = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
# center on y axis
|
||||
from bs4 import BeautifulSoup
|
||||
with open(self.filename) as f:
|
||||
soup = BeautifulSoup(f.read(), features='xml')
|
||||
path = soup.find('path', recursive=True)
|
||||
d = path.attrs['d']
|
||||
d = d.strip('MmZ ').replace(',', 'L')
|
||||
coord_pairs = d.split('L')
|
||||
coords = [tuple(map(float, pair.split())) for pair in coord_pairs]
|
||||
|
||||
if polygon_is_clockwise(coords):
|
||||
coords = coords[::-1]
|
||||
|
||||
# Calculate bounding box
|
||||
min_x = min(x for x, _y in coords)
|
||||
min_y = max(x for x, _y in coords)
|
||||
max_x = min(y for _x, y in coords)
|
||||
max_y = max(y for _x, y in coords)
|
||||
if max_x < 0 or max_y < 0 or min_x > 0 or min_y > 0:
|
||||
# (0, 0) is not within the polygon's axis-aligned bounding box, recenter.
|
||||
ox, oy = skeletonator.polygon_center_of_mass(coords)
|
||||
warnings.warn(f'Polygon looks not centered, bounds are ({min_x:.2f}, {min_y:.2f}), ({max_x:.2f}, {max_y:.2f}). Aligning (0, 0) with polygon centroid at ({ox:.2f}, {oy:.2f})')
|
||||
coords = [(x-ox, y-oy) for x, y in coords]
|
||||
|
||||
self.polygon = coords
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return f'svg_{len(self.polygon)}n'
|
||||
|
||||
@property
|
||||
def desc(self):
|
||||
return f'{len(self.polygon)} node imported SVG shape'
|
||||
|
||||
@dataclass
|
||||
class PlanarInductor():
|
||||
shape: Shape
|
||||
outer_diameter: float
|
||||
inner_diameter: float
|
||||
turns: int
|
||||
twists: int
|
||||
trace_width: float = None
|
||||
|
|
@ -385,10 +132,11 @@ class PlanarInductor():
|
|||
copper_thickness: float = 0.035
|
||||
layer_pair: str = 'F.Cu,B.Cu'
|
||||
clockwise: bool = False
|
||||
approximate_arcs: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
self.logger = logging.getLogger('kicoil')
|
||||
self.outer_radius = self.outer_diameter/2
|
||||
self.inner_radius = self.inner_diameter/2
|
||||
self.turns_per_layer = self.turns/self.layers
|
||||
self.sector_angle = 2*pi / self.twists
|
||||
|
||||
|
|
@ -396,13 +144,13 @@ class PlanarInductor():
|
|||
self.sector_angle *= -1
|
||||
|
||||
self.sweeping_angle = self.sector_angle * self.turns_per_layer
|
||||
self.spiral_pitch = (self.shape.outer_radius-self.shape.inner_radius) / self.turns_per_layer
|
||||
self.spiral_pitch = (self.outer_radius-self.inner_radius) / self.turns_per_layer
|
||||
self.R = None # will be calculated during render
|
||||
|
||||
c1 = self.shape.inner_radius
|
||||
c2 = self.shape.inner_radius + self.spiral_pitch
|
||||
alpha1 = atan((self.shape.outer_radius - self.shape.inner_radius) / self.sweeping_angle / c1)
|
||||
alpha2 = atan((self.shape.outer_radius - self.shape.inner_radius) / self.sweeping_angle / c2)
|
||||
c1 = self.inner_radius
|
||||
c2 = self.inner_radius + self.spiral_pitch
|
||||
alpha1 = atan((self.outer_radius - self.inner_radius) / self.sweeping_angle / c1)
|
||||
alpha2 = atan((self.outer_radius - self.inner_radius) / self.sweeping_angle / c2)
|
||||
alpha = (alpha1+alpha2)/2
|
||||
self.projected_spiral_pitch = self.spiral_pitch*cos(alpha)
|
||||
|
||||
|
|
@ -431,22 +179,22 @@ class PlanarInductor():
|
|||
|
||||
if self.trace_width is None:
|
||||
if round(self.clearance, 3) > round(self.projected_spiral_pitch, 3):
|
||||
warnings.warn(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
|
||||
raise ValueError(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
|
||||
self.trace_width = self.projected_spiral_pitch - self.clearance
|
||||
self.logger.info(f'Calculated trace width for {self.clearance:.2f} mm clearance is {self.trace_width:.2f} mm.')
|
||||
|
||||
elif self.clearance is None:
|
||||
if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2):
|
||||
warnings.warn(f'Error: Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
|
||||
raise ValueError(f'Error: Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
|
||||
self.clearance = self.projected_spiral_pitch - self.trace_width
|
||||
self.logger.info(f'Calculated clearance for {self.trace_width:.2f} mm trace width is {self.clearance:.2f} mm.')
|
||||
|
||||
else:
|
||||
if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2):
|
||||
raise ValueError(f'Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
|
||||
raise click.ClickException(f'Error: Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.')
|
||||
clearance_actual = self.projected_spiral_pitch - self.trace_width
|
||||
if round(clearance_actual, 3) < round(self.clearance, 3):
|
||||
raise ValueError(f'Actual clearance for {self.trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {self.clearance:.2f} mm.')
|
||||
raise click.ClickException(f'Error: Actual clearance for {self.trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {self.clearance:.2f} mm.')
|
||||
|
||||
if round(self.via_diameter, 2) < round(self.trace_width, 2):
|
||||
self.logger.warning(f'Clipping via diameter from {self.via_diameter:.2f} mm to trace width of {self.trace_width:.2f} mm.')
|
||||
|
|
@ -463,15 +211,15 @@ class PlanarInductor():
|
|||
if isclose(self.via_offset, 0, abs_tol=1e-6):
|
||||
self.via_offset = 0
|
||||
|
||||
self.inner_via_ring_radius = self.shape.inner_radius - self.via_offset
|
||||
self.inner_via_ring_radius = self.inner_radius - self.via_offset
|
||||
inner_via_angle = 2*asin((self.via_diameter + self.clearance)/2 / self.inner_via_ring_radius)
|
||||
|
||||
self.outer_via_ring_radius = self.shape.outer_radius + self.via_offset
|
||||
self.outer_via_ring_radius = self.outer_radius + self.via_offset
|
||||
outer_via_angle = 2*asin((self.via_diameter + self.clearance)/2 / self.outer_via_ring_radius)
|
||||
|
||||
self.logger.info(f'Inner via ring @r={self.inner_via_ring_radius:.2f} mm (from {self.shape.inner_radius:.2f} mm)')
|
||||
self.logger.info(f'Inner via ring @r={self.inner_via_ring_radius:.2f} mm (from {self.inner_radius:.2f} mm)')
|
||||
self.logger.info(f' {degrees(inner_via_angle):.1f} deg / via')
|
||||
self.logger.info(f'Outer via ring @r={self.outer_via_ring_radius:.2f} mm (from {self.shape.outer_radius:.2f} mm)')
|
||||
self.logger.info(f'Outer via ring @r={self.outer_via_ring_radius:.2f} mm (from {self.outer_radius:.2f} mm)')
|
||||
self.logger.info(f' {degrees(outer_via_angle):.1f} deg / via')
|
||||
|
||||
# Check if the vias of the inner ring are so large that they would overlap
|
||||
|
|
@ -483,17 +231,19 @@ class PlanarInductor():
|
|||
self.layer_pair = (t.strip(), b.strip())
|
||||
|
||||
# For fill factor & inductance formulas, See https://coil32.net/pcb-coil.html for details
|
||||
d_avg = (2*self.shape.outer_radius + self.shape.inner_radius)/2
|
||||
phi = (2*self.shape.outer_radius - 2*self.shape.inner_radius) / (2*self.shape.outer_radius + 2*self.shape.inner_radius)
|
||||
c1, c2, c3, c4 = 1.00, 2.46, 0.00, 0.20 # FIXME for other shapes
|
||||
d_avg = (self.outer_diameter + self.inner_diameter)/2
|
||||
phi = (self.outer_diameter - self.inner_diameter) / (self.outer_diameter + self.inner_diameter)
|
||||
c1, c2, c3, c4 = 1.00, 2.46, 0.00, 0.20
|
||||
self.L = mu_0 * self.turns**2 * d_avg*1e3 * c1 / 2 * (log(c2/phi) + c3*phi + c4*phi**2)
|
||||
self.logger.info(f'Outer diameter: {2*self.shape.outer_radius:g} mm')
|
||||
self.logger.info(f'Outer diameter: {self.outer_diameter:g} mm')
|
||||
self.logger.info(f'Average diameter: {d_avg:g} mm')
|
||||
self.logger.info(f'Inner diameter: {2*self.shape.inner_radius:g} mm')
|
||||
self.logger.info(f'Inner diameter: {self.inner_diameter:g} mm')
|
||||
self.logger.info(f'Fill factor: {phi:g}')
|
||||
self.logger.info(f'Approximate inductance: {self.L:g} µH')
|
||||
|
||||
_points, arm_length, _angle_refs = self.shape.compute_spiral(a1=0, a2=self.sector_angle)
|
||||
_points, arm_length = compute_spiral(r1=self.outer_radius, r2=self.inner_radius,
|
||||
a1=0, a2=self.sector_angle,
|
||||
fn=256)
|
||||
|
||||
self.track_length = arm_length*self.twists*self.layers
|
||||
self.logger.info(f'Approximate track length: {self.track_length:.2f} mm')
|
||||
|
|
@ -503,11 +253,12 @@ class PlanarInductor():
|
|||
self.R = self.track_length/1e3 * rho / A
|
||||
self.logger.info(f'Approximate resistance: {self.R:g} Ω')
|
||||
|
||||
|
||||
@property
|
||||
def default_footprint_name(self):
|
||||
return f'planar-coil-{self.shape.slug}-n{self.turns}-k{self.twists}'
|
||||
return f'planar-coil-{self.outer_diameter:.2f}x{self.inner_diameter:.2f}-n{self.turns}-k{self.twists}'
|
||||
|
||||
def render_footprint(self, name=None, arc_tolerance=0.02, circle_segments=64, geometry_debug_file=None):
|
||||
def render_footprint(self, name=None, arc_tolerance=0.02, circle_segments=64):
|
||||
if name is None:
|
||||
name = self.default_footprint_name
|
||||
|
||||
|
|
@ -517,7 +268,7 @@ class PlanarInductor():
|
|||
generator=kicad.Atom('kicoil'),
|
||||
generator_version=__version__,
|
||||
layer='F.Cu',
|
||||
descr=f"{self.turns} turn {self.shape.desc} twisted coil footprint, inductance approximately {self.L:.6f} µH. Generated by kicoil, version {__version__}.",
|
||||
descr=f"{self.turns} turn {self.outer_diameter:.2f} mm diameter twisted coil footprint, inductance approximately {self.L:.6f} µH. Generated by kicoil, version {__version__}.",
|
||||
clearance=self.clearance,
|
||||
zone_connect=0)
|
||||
|
||||
|
|
@ -527,7 +278,6 @@ class PlanarInductor():
|
|||
for i in range(self.twists):
|
||||
inverse[i*self.turns%self.twists] = i
|
||||
|
||||
arms_layers = [[], []]
|
||||
# Array where we collect all gerbonara kicad line and arc objects
|
||||
for i in range(self.twists):
|
||||
start_angle = i*self.sector_angle
|
||||
|
|
@ -535,57 +285,40 @@ class PlanarInductor():
|
|||
end_angle = fold_angle + self.sweeping_angle
|
||||
|
||||
# Handle the spiral arm
|
||||
points_layer0, arm_length, angle_refs_layer0 = self.shape.compute_spiral(a1=start_angle, a2=fold_angle, fn=circle_segments, debug=True)
|
||||
x = inverse[i]*floor(2*self.sweeping_angle / (2*pi)) * 2*pi
|
||||
points_layer0, arm_length = compute_spiral(r1=self.outer_radius, r2=self.inner_radius,
|
||||
a1=start_angle, a2=fold_angle,
|
||||
fn=circle_segments)
|
||||
x0, y0 = points_layer0[0]
|
||||
xn, yn = points_layer0[-1]
|
||||
if angle_refs_layer0:
|
||||
ref_0, ref_n = angle_refs_layer0[0], angle_refs_layer0[-1]
|
||||
else:
|
||||
ref_0, ref_n = None, None
|
||||
|
||||
if self.approximate_arcs and isinstance(self.shape, CircleShape):
|
||||
footprint.arcs.extend(arc_approximate(points_layer0, self.trace_width, self.layer_pair[0], arc_tolerance))
|
||||
else:
|
||||
footprint.lines.extend(kicad.make_line(*p1, *p2, self.trace_width, self.layer_pair[0]) for p1, p2 in zip(points_layer0, points_layer0[1:]))
|
||||
footprint.arcs.extend(arc_approximate(points_layer0, self.trace_width, self.layer_pair[0], arc_tolerance))
|
||||
|
||||
if self.layers > 1:
|
||||
# Handle the returning arm on the bottom layer
|
||||
points_layer1, _, angle_refs_layer1 = self.shape.compute_spiral(a1=end_angle, a2=fold_angle, fn=circle_segments, debug=True)
|
||||
points_layer1 = points_layer1[::-1]
|
||||
if self.approximate_arcs and isinstance(self.shape, CircleShape):
|
||||
footprint.arcs.extend(arc_approximate(points_layer1, self.trace_width, self.layer_pair[1], arc_tolerance))
|
||||
else:
|
||||
footprint.lines.extend(kicad.make_line(*p1, *p2, self.trace_width, self.layer_pair[1]) for p1, p2 in zip(points_layer1, points_layer1[1:]))
|
||||
points_layer1, _ = compute_spiral(r1=self.inner_radius, r2=self.outer_radius,
|
||||
a1=fold_angle, a2=end_angle,
|
||||
fn=circle_segments)
|
||||
footprint.arcs.extend(arc_approximate(points_layer1, self.trace_width, self.layer_pair[1], arc_tolerance))
|
||||
|
||||
else:
|
||||
# Add a straight connecting segment connecting the inner point to the outside of the spiral.
|
||||
ref = angle_refs_layer0[-1]
|
||||
xq, yq = self.shape.project_point(self.shape.outer_radius, fold_angle, r_ref=ref)
|
||||
angle_refs_layer1 = [ref, ref]
|
||||
dr = self.outer_radius - self.inner_radius
|
||||
xq = xn + cos(fold_angle) * dr
|
||||
yq = yn - sin(fold_angle) * dr
|
||||
points_layer1 = [(xn, yn), (xq, yq)]
|
||||
footprint.lines.append(kicad.make_line(xn, yn, xq, yq, self.trace_width, self.layer_pair[1]))
|
||||
|
||||
arms_layers[0].append((points_layer0, angle_refs_layer0))
|
||||
arms_layers[1].append((points_layer1, angle_refs_layer1))
|
||||
|
||||
for i in range(self.twists):
|
||||
start_angle = i*self.sector_angle
|
||||
fold_angle = start_angle + self.sweeping_angle
|
||||
end_angle = fold_angle + self.sweeping_angle
|
||||
|
||||
# Handle inner via ring and process staggering if enabled
|
||||
r = self.inner_via_ring_radius
|
||||
if self.stagger_inner_vias:
|
||||
if i%2 != 0:
|
||||
r -= 2*self.via_offset
|
||||
|
||||
points_layer0, refs_layer0 = arms_layers[0][i]
|
||||
points_layer1, refs_layer1 = arms_layers[1][i]
|
||||
xv, yv = r*cos(fold_angle), r*sin(fold_angle)
|
||||
|
||||
xv, yv = self.shape.project_point(r, fold_angle, r_ref=refs_layer0[-1])
|
||||
|
||||
footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1]))
|
||||
if self.via_offset:
|
||||
footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1]))
|
||||
|
||||
footprint.pads.append(kicad.make_via(xv, yv,
|
||||
self.via_diameter, self.via_drill, self.clearance,
|
||||
|
|
@ -593,41 +326,38 @@ class PlanarInductor():
|
|||
|
||||
# Handle outer via ring and process staggering if enabled unless we are at the start of the coil, where we will
|
||||
# place pads below.
|
||||
r = self.outer_via_ring_radius
|
||||
|
||||
if self.stagger_outer_vias:
|
||||
if i%2 != 0:
|
||||
r += 2*self.via_offset
|
||||
|
||||
points_layer0, refs_layer0 = arms_layers[0][i]
|
||||
points_layer1, refs_layer1 = arms_layers[1][(i - self.turns) % self.twists]
|
||||
|
||||
xv, yv = self.shape.project_point(r, start_angle, r_ref=refs_layer0[0])
|
||||
|
||||
footprint.lines.append(kicad.make_line(*points_layer0[0], xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
footprint.lines.append(kicad.make_line(*points_layer1[-1], xv, yv, self.trace_width, self.layer_pair[1]))
|
||||
|
||||
if i > 0:
|
||||
r = self.outer_via_ring_radius
|
||||
|
||||
if self.stagger_outer_vias:
|
||||
if i%2 != 0:
|
||||
r += 2*self.via_offset
|
||||
|
||||
xv, yv = r*cos(start_angle), r*sin(start_angle)
|
||||
|
||||
if self.via_offset:
|
||||
footprint.lines.append(kicad.make_line(x0, y0, xv, yv, self.trace_width, self.layer_pair[0]))
|
||||
footprint.lines.append(kicad.make_line(x0, y0, xv, yv, self.trace_width, self.layer_pair[1]))
|
||||
|
||||
footprint.pads.append(kicad.make_via(xv, yv,
|
||||
self.via_diameter, self.via_drill, self.clearance,
|
||||
self.layer_pair))
|
||||
|
||||
else:
|
||||
# Place the pads on the outer radius
|
||||
px, py = self.shape.project_point(self.shape.outer_radius, 0)
|
||||
footprint.pads.extend([
|
||||
kicad.make_pad(1, [self.layer_pair[0]], px, py, self.trace_width, self.clearance),
|
||||
kicad.make_pad(2, [self.layer_pair[1]], px, py, self.trace_width, self.clearance)])
|
||||
|
||||
# Place the pads on the outer radius
|
||||
footprint.pads.extend([
|
||||
kicad.make_pad(1, [self.layer_pair[0]], self.outer_radius, 0, self.trace_width, self.clearance),
|
||||
kicad.make_pad(2, [self.layer_pair[1]], self.outer_radius, 0, self.trace_width, self.clearance)])
|
||||
|
||||
if self.keepout_zone:
|
||||
pts = self.shape.offset_exterior(self.keepout_margin)
|
||||
r = self.outer_diameter/2 + self.keepout_margin
|
||||
tol = 0.05 # mm
|
||||
n = ceil(pi / acos(1 - tol/r))
|
||||
pts = [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)]
|
||||
footprint.zones.append(Zone(layers=['*.Cu'],
|
||||
hatch=Hatch(),
|
||||
filled_areas_thickness=False,
|
||||
keepout=ZoneKeepout(copperpour_allowed=False),
|
||||
polygon=ZonePolygon(pts=[XYCoord(x=x, y=y) for x, y in pts])))
|
||||
|
||||
if geometry_debug_file:
|
||||
self.shape.sk.dump_to_pdf(geometry_debug_file)
|
||||
return footprint
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from pathlib import Path
|
|||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
|
||||
from .geometry import PlanarInductor, divisors, CircleShape, SectorShape, StarShape, SVGShape, RectangleShape, RegularPolygonShape, TrapezoidShape
|
||||
from .geometry import PlanarInductor, divisors
|
||||
from .svg import make_transparent_svg
|
||||
|
||||
try:
|
||||
|
|
@ -122,15 +122,11 @@ class EntryWithPlaceholder(tk.Entry):
|
|||
|
||||
|
||||
class KiCoilGUI:
|
||||
def __init__(self, root, kicad_inst=None):
|
||||
self.kicad_inst = kicad_inst
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("KiCoil - Planar Inductor Generator")
|
||||
self.root.geometry("1000x650")
|
||||
|
||||
# Register validation command for non-negative numbers early
|
||||
self.validate_nonneg_cmd = self.root.register(self.validate_nonnegative)
|
||||
|
||||
style = ttk.Style()
|
||||
style.theme_use('clam')
|
||||
|
||||
|
|
@ -162,44 +158,6 @@ class KiCoilGUI:
|
|||
self.notebook.add(geometry_frame, text="Geometry")
|
||||
self.create_geometry_params(geometry_frame)
|
||||
|
||||
# Shape parameter tabs (will be shown/hidden based on selection)
|
||||
self.shape_param_frames = {}
|
||||
|
||||
circle_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["Circle"] = circle_frame
|
||||
self.notebook.add(circle_frame, text="Circle Parameters")
|
||||
self.create_circle_params(circle_frame)
|
||||
|
||||
rectangle_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["Rectangle"] = rectangle_frame
|
||||
self.notebook.add(rectangle_frame, text="Rectangle Parameters")
|
||||
self.create_rectangle_params(rectangle_frame)
|
||||
|
||||
trapezoid_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["Trapezoid"] = trapezoid_frame
|
||||
self.notebook.add(trapezoid_frame, text="Trapezoid Parameters")
|
||||
self.create_trapezoid_params(trapezoid_frame)
|
||||
|
||||
sector_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["Sector"] = sector_frame
|
||||
self.notebook.add(sector_frame, text="Sector Parameters")
|
||||
self.create_sector_params(sector_frame)
|
||||
|
||||
star_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["Star"] = star_frame
|
||||
self.notebook.add(star_frame, text="Star Parameters")
|
||||
self.create_star_params(star_frame)
|
||||
|
||||
polygon_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["Regular Polygon"] = polygon_frame
|
||||
self.notebook.add(polygon_frame, text="Polygon Parameters")
|
||||
self.create_polygon_params(polygon_frame)
|
||||
|
||||
svg_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.shape_param_frames["SVG"] = svg_frame
|
||||
self.notebook.add(svg_frame, text="SVG Parameters")
|
||||
self.create_svg_params(svg_frame)
|
||||
|
||||
traces_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.notebook.add(traces_frame, text="Traces")
|
||||
self.create_trace_params(traces_frame)
|
||||
|
|
@ -217,11 +175,8 @@ class KiCoilGUI:
|
|||
|
||||
ttk.Button(button_frame, text="Show Valid Twists",
|
||||
command=self.show_valid_twists, width=20).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Save Footprint File",
|
||||
command=self.save_footprint_file, width=20).pack(side=tk.LEFT, padx=5)
|
||||
#if self.kicad_inst: FIXME
|
||||
# ttk.Button(button_frame, text="Update Footprint on Board",
|
||||
# command=self.update_board_footprint, width=20).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(button_frame, text="Generate and Save",
|
||||
command=self.generate_footprint, width=20).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
status_label = ttk.Label(main_frame, text="Output:", font=('Helvetica', 10, 'bold'))
|
||||
status_label.grid(row=3, column=0, sticky=tk.W, pady=(10, 0))
|
||||
|
|
@ -255,26 +210,13 @@ class KiCoilGUI:
|
|||
self.preview_label.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.preview_frame.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.E, tk.W), padx=(10, 10), pady=10)
|
||||
|
||||
|
||||
self.current_model = None
|
||||
self._validation_after_id = None
|
||||
|
||||
self.setup_logging()
|
||||
self.setup_traces()
|
||||
self.update_shape_tab_visibility() # Initialize tab visibility
|
||||
self.root.after(100, self.validate_parameters)
|
||||
|
||||
def validate_nonnegative(self, value_if_allowed):
|
||||
"""Validation callback for spinboxes to prevent negative values"""
|
||||
if value_if_allowed == "" or value_if_allowed == "-":
|
||||
# Allow empty string (user is typing) but not standalone minus
|
||||
return value_if_allowed == ""
|
||||
try:
|
||||
float_val = float(value_if_allowed)
|
||||
return float_val >= 0
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def _on_preview_resize(self, event):
|
||||
# Debounce resize events - only update after resize is complete
|
||||
if hasattr(self, '_resize_after_id') and self._resize_after_id is not None:
|
||||
|
|
@ -334,37 +276,42 @@ class KiCoilGUI:
|
|||
def create_geometry_params(self, parent):
|
||||
row = 0
|
||||
|
||||
# Shape Type
|
||||
ttk.Label(parent, text="Shape Type:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.shape_type_var = tk.StringVar(value="Circle")
|
||||
shape_combo = ttk.Combobox(parent, textvariable=self.shape_type_var,
|
||||
values=["Circle", "Rectangle", "Trapezoid", "Sector", "Star", "Regular Polygon", "SVG"],
|
||||
state='readonly', width=23)
|
||||
shape_combo.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Coil outline shape",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
# Turns
|
||||
ttk.Label(parent, text="Number of Turns:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.turns_var = tk.IntVar(value=7)
|
||||
self.turns_var = tk.IntVar(value=5)
|
||||
ttk.Spinbox(parent, from_=1, to=100, textvariable=self.turns_var,
|
||||
width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Number of spiral turns",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
# Twists
|
||||
ttk.Label(parent, text="Twists per Revolution:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.twists_var = tk.IntVar(value=4)
|
||||
self.twists_var = tk.IntVar(value=1)
|
||||
ttk.Spinbox(parent, from_=0, to=50, textvariable=self.twists_var,
|
||||
width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Must be co-prime to turns",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
# Outer Diameter
|
||||
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.outer_dia_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.outer_dia_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Outside diameter of coil",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
# Inner Diameter
|
||||
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.inner_dia_var = tk.DoubleVar(value=25.0)
|
||||
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
|
||||
textvariable=self.inner_dia_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Inside diameter of coil",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
# Layer Mode
|
||||
ttk.Label(parent, text="Layer Mode:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.layer_mode_var = tk.IntVar(value=2)
|
||||
|
|
@ -387,221 +334,6 @@ class KiCoilGUI:
|
|||
value="clockwise").pack(side=tk.LEFT)
|
||||
row += 1
|
||||
|
||||
def create_circle_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.circle_outer_dia_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.circle_outer_dia_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Outside diameter of coil",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.circle_inner_dia_var = tk.DoubleVar(value=25.0)
|
||||
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
|
||||
textvariable=self.circle_inner_dia_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Inside diameter of coil",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def create_rectangle_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.rect_width_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.rect_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Height (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.rect_height_var = tk.DoubleVar(value=40.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.rect_height_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.rect_annular_width_var = tk.DoubleVar(value=10.0)
|
||||
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
|
||||
textvariable=self.rect_annular_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Width of trace area",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def create_trapezoid_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.trap_width_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.trap_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Height (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.trap_height_var = tk.DoubleVar(value=40.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.trap_height_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Offset (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.trap_offset_var = tk.DoubleVar(value=10.0)
|
||||
ttk.Spinbox(parent, from_=0, to=100, increment=0.5,
|
||||
textvariable=self.trap_offset_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Corner offset at shorter edge",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.trap_annular_width_var = tk.DoubleVar(value=10.0)
|
||||
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
|
||||
textvariable=self.trap_annular_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Width of trace area",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def create_sector_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.sector_outer_dia_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.sector_outer_dia_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.sector_inner_dia_var = tk.DoubleVar(value=25.0)
|
||||
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
|
||||
textvariable=self.sector_inner_dia_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Angle (degrees):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.sector_angle_var = tk.DoubleVar(value=45.0)
|
||||
ttk.Spinbox(parent, from_=1, to=360, increment=1.0,
|
||||
textvariable=self.sector_angle_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Sector angle",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.sector_annular_width_var = tk.DoubleVar(value=5.0)
|
||||
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
|
||||
textvariable=self.sector_annular_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Width of trace area",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def create_star_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="Outer Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.star_outer_dia_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.star_outer_dia_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Inner Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.star_inner_dia_var = tk.DoubleVar(value=25.0)
|
||||
ttk.Spinbox(parent, from_=0, to=500, increment=0.5,
|
||||
textvariable=self.star_inner_dia_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Points:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.star_points_var = tk.IntVar(value=5)
|
||||
ttk.Spinbox(parent, from_=3, to=20, textvariable=self.star_points_var,
|
||||
width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Number of star points",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.star_annular_width_var = tk.DoubleVar(value=5.0)
|
||||
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
|
||||
textvariable=self.star_annular_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Width of trace area",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def create_polygon_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.poly_diameter_var = tk.DoubleVar(value=50.0)
|
||||
ttk.Spinbox(parent, from_=1, to=500, increment=0.5,
|
||||
textvariable=self.poly_diameter_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Corners:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.poly_corners_var = tk.IntVar(value=8)
|
||||
ttk.Spinbox(parent, from_=3, to=20, textvariable=self.poly_corners_var,
|
||||
width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Number of polygon corners",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.poly_annular_width_var = tk.DoubleVar(value=10.0)
|
||||
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
|
||||
textvariable=self.poly_annular_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Width of trace area",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def create_svg_params(self, parent):
|
||||
row = 0
|
||||
ttk.Label(parent, text="SVG File:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.svg_filename_var = tk.StringVar(value="")
|
||||
svg_entry = ttk.Entry(parent, textvariable=self.svg_filename_var, width=30)
|
||||
svg_entry.grid(row=row, column=1, sticky=(tk.W, tk.E), pady=5)
|
||||
ttk.Button(parent, text="Browse...", command=self.browse_svg_file, width=10).grid(row=row, column=2, sticky=tk.W, padx=(5, 0), pady=5)
|
||||
row += 1
|
||||
|
||||
ttk.Label(parent, text="Annular Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.svg_annular_width_var = tk.DoubleVar(value=5.0)
|
||||
ttk.Spinbox(parent, from_=1, to=100, increment=0.5,
|
||||
textvariable=self.svg_annular_width_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Width of trace area",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def browse_svg_file(self):
|
||||
filename = filedialog.askopenfilename(
|
||||
title="Select SVG File",
|
||||
filetypes=[("SVG files", "*.svg"), ("All files", "*.*")]
|
||||
)
|
||||
if filename:
|
||||
self.svg_filename_var.set(filename)
|
||||
|
||||
def update_shape_tab_visibility(self, *args):
|
||||
"""Show only the shape parameter tab for the currently selected shape"""
|
||||
selected_shape = self.shape_type_var.get()
|
||||
|
||||
# Hide all shape parameter tabs
|
||||
for shape_name, frame in self.shape_param_frames.items():
|
||||
tab_id = self.notebook.index(frame)
|
||||
self.notebook.tab(tab_id, state='hidden')
|
||||
|
||||
# Show only the selected shape's tab
|
||||
if selected_shape in self.shape_param_frames:
|
||||
frame = self.shape_param_frames[selected_shape]
|
||||
tab_id = self.notebook.index(frame)
|
||||
self.notebook.tab(tab_id, state='normal')
|
||||
|
||||
def create_trace_params(self, parent):
|
||||
row = 0
|
||||
|
||||
|
|
@ -621,8 +353,7 @@ class KiCoilGUI:
|
|||
ttk.Label(parent, text="Copper Thickness (µm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.copper_thickness_var = tk.DoubleVar(value=35.0) # 35µm = 0.035mm = 1 Oz
|
||||
ttk.Spinbox(parent, from_=1, to=1000, increment=1, format="%.1f",
|
||||
textvariable=self.copper_thickness_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.copper_thickness_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="35µm = 1 Oz copper",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
|
@ -635,8 +366,7 @@ class KiCoilGUI:
|
|||
ttk.Label(parent, text="Via Diameter (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.via_diameter_var = tk.DoubleVar(value=0.6)
|
||||
ttk.Spinbox(parent, from_=0.1, to=5.0, increment=0.1, format="%.2f",
|
||||
textvariable=self.via_diameter_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.via_diameter_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Via Drill
|
||||
|
|
@ -695,8 +425,7 @@ class KiCoilGUI:
|
|||
ttk.Label(parent, text="Circle Segments:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.circle_segments_var = tk.IntVar(value=64)
|
||||
ttk.Spinbox(parent, from_=8, to=360, textvariable=self.circle_segments_var,
|
||||
width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Points per 360° for arc interpolation",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
|
@ -705,8 +434,7 @@ class KiCoilGUI:
|
|||
ttk.Label(parent, text="Arc Tolerance (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.arc_tolerance_var = tk.DoubleVar(value=0.02)
|
||||
ttk.Spinbox(parent, from_=0.001, to=1.0, increment=0.001, format="%.3f",
|
||||
textvariable=self.arc_tolerance_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.arc_tolerance_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Keepout Zone
|
||||
|
|
@ -720,66 +448,16 @@ class KiCoilGUI:
|
|||
ttk.Label(parent, text="Keepout Margin (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.keepout_margin_var = tk.DoubleVar(value=5.0)
|
||||
ttk.Spinbox(parent, from_=0, to=50, increment=0.5,
|
||||
textvariable=self.keepout_margin_var, width=15,
|
||||
validate='key', validatecommand=(self.validate_nonneg_cmd, '%P')).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
textvariable=self.keepout_margin_var, width=15).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(parent, text="Margin around coil",
|
||||
foreground='gray').grid(row=row, column=2, sticky=tk.W, padx=(10, 0))
|
||||
row += 1
|
||||
|
||||
def get_parameters(self):
|
||||
# Create the appropriate shape based on selection
|
||||
shape_type = self.shape_type_var.get()
|
||||
|
||||
if shape_type == "Circle":
|
||||
shape = CircleShape(
|
||||
outer_diameter=self.circle_outer_dia_var.get(),
|
||||
inner_diameter=self.circle_inner_dia_var.get()
|
||||
)
|
||||
elif shape_type == "Rectangle":
|
||||
shape = RectangleShape(
|
||||
width=self.rect_width_var.get(),
|
||||
height=self.rect_height_var.get(),
|
||||
annular_width=self.rect_annular_width_var.get()
|
||||
)
|
||||
elif shape_type == "Trapezoid":
|
||||
shape = TrapezoidShape(
|
||||
width=self.trap_width_var.get(),
|
||||
height=self.trap_height_var.get(),
|
||||
offset=self.trap_offset_var.get(),
|
||||
annular_width=self.trap_annular_width_var.get()
|
||||
)
|
||||
elif shape_type == "Sector":
|
||||
import math
|
||||
shape = SectorShape(
|
||||
inner_diameter=self.sector_inner_dia_var.get(),
|
||||
outer_diameter=self.sector_outer_dia_var.get(),
|
||||
angle=math.radians(self.sector_angle_var.get()),
|
||||
annular_width=self.sector_annular_width_var.get()
|
||||
)
|
||||
elif shape_type == "Star":
|
||||
shape = StarShape(
|
||||
inner_diameter=self.star_inner_dia_var.get(),
|
||||
outer_diameter=self.star_outer_dia_var.get(),
|
||||
points=self.star_points_var.get(),
|
||||
annular_width=self.star_annular_width_var.get()
|
||||
)
|
||||
elif shape_type == "Regular Polygon":
|
||||
shape = RegularPolygonShape(
|
||||
diameter=self.poly_diameter_var.get(),
|
||||
corners=self.poly_corners_var.get(),
|
||||
annular_width=self.poly_annular_width_var.get()
|
||||
)
|
||||
elif shape_type == "SVG":
|
||||
shape = SVGShape(
|
||||
filename=self.svg_filename_var.get(),
|
||||
annular_width=self.svg_annular_width_var.get()
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown shape type: {shape_type}")
|
||||
|
||||
params = {
|
||||
'shape' : shape,
|
||||
'turns' : self.turns_var.get(),
|
||||
'outer_diameter' : self.outer_dia_var.get(),
|
||||
'inner_diameter' : self.inner_dia_var.get(),
|
||||
'layers' : self.layer_mode_var.get(),
|
||||
'twists' : self.twists_var.get(),
|
||||
'clockwise' : (self.direction_var.get() == "clockwise"),
|
||||
|
|
@ -818,10 +496,10 @@ class KiCoilGUI:
|
|||
def capture_warnings(self):
|
||||
"""Context manager to capture kicoil's warnings to the output text widget"""
|
||||
def show_warning(message, category, filename, lineno, file=None, line=None):
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.config(state='normal')
|
||||
self.output_text.insert(tk.END, f'{message}\n', 'warning')
|
||||
self.output_text.see(tk.END)
|
||||
self.output_text['state'] = 'disabled'
|
||||
self.output_text.config(state='disabled')
|
||||
self.output_text.update_idletasks()
|
||||
|
||||
old_showwarning, warnings.showwarning = warnings.showwarning, show_warning
|
||||
|
|
@ -831,12 +509,10 @@ class KiCoilGUI:
|
|||
warnings.showwarning = old_showwarning
|
||||
|
||||
def setup_traces(self):
|
||||
# Shape type needs special handling for tab visibility
|
||||
self.shape_type_var.trace_add('write', self.update_shape_tab_visibility)
|
||||
self.shape_type_var.trace_add('write', self._on_parameter_change)
|
||||
|
||||
for var in [
|
||||
self.turns_var,
|
||||
self.outer_dia_var,
|
||||
self.inner_dia_var,
|
||||
self.layer_mode_var,
|
||||
self.direction_var,
|
||||
self.twists_var,
|
||||
|
|
@ -847,44 +523,13 @@ class KiCoilGUI:
|
|||
self.stagger_inner_var,
|
||||
self.stagger_outer_var,
|
||||
self.top_layer_var,
|
||||
self.bottom_layer_var,
|
||||
# Circle shape params
|
||||
self.circle_outer_dia_var,
|
||||
self.circle_inner_dia_var,
|
||||
# Rectangle shape params
|
||||
self.rect_width_var,
|
||||
self.rect_height_var,
|
||||
self.rect_annular_width_var,
|
||||
# Trapezoid shape params
|
||||
self.trap_width_var,
|
||||
self.trap_height_var,
|
||||
self.trap_offset_var,
|
||||
self.trap_annular_width_var,
|
||||
# Sector shape params
|
||||
self.sector_outer_dia_var,
|
||||
self.sector_inner_dia_var,
|
||||
self.sector_angle_var,
|
||||
self.sector_annular_width_var,
|
||||
# Star shape params
|
||||
self.star_outer_dia_var,
|
||||
self.star_inner_dia_var,
|
||||
self.star_points_var,
|
||||
self.star_annular_width_var,
|
||||
# Polygon shape params
|
||||
self.poly_diameter_var,
|
||||
self.poly_corners_var,
|
||||
self.poly_annular_width_var,
|
||||
# SVG shape params
|
||||
self.svg_annular_width_var]:
|
||||
self.bottom_layer_var]:
|
||||
var.trace_add('write', self._on_parameter_change)
|
||||
|
||||
for entry in [self.trace_width_entry, self.clearance_entry,
|
||||
self.via_drill_entry, self.via_offset_entry,
|
||||
self.footprint_name_entry]:
|
||||
entry.bind('<KeyRelease>', lambda e: self._on_parameter_change())
|
||||
|
||||
# SVG filename entry needs special handling
|
||||
self.svg_filename_var.trace_add('write', self._on_parameter_change)
|
||||
|
||||
def _on_parameter_change(self, *args):
|
||||
# Schedule validation to avoid too many rapid calls
|
||||
|
|
@ -895,7 +540,7 @@ class KiCoilGUI:
|
|||
def validate_parameters(self):
|
||||
"""Validate parameters by creating PlanarInductor instance"""
|
||||
try:
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.config(state='normal')
|
||||
self.output_text.delete('1.0', tk.END)
|
||||
|
||||
with self.capture_warnings():
|
||||
|
|
@ -908,21 +553,15 @@ class KiCoilGUI:
|
|||
return True
|
||||
|
||||
except ValueError as e:
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.insert(tk.END, f"ERROR: {e}\n", 'error')
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
tb = traceback.format_exc()
|
||||
print(tb, file=sys.stderr)
|
||||
|
||||
self.current_model = None
|
||||
self.update_placeholders()
|
||||
self.update_preview()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.insert(tk.END, f"Unexpected error:\n{tb}\n", 'error')
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
|
|
@ -930,11 +569,10 @@ class KiCoilGUI:
|
|||
|
||||
self.current_model = None
|
||||
self.update_placeholders()
|
||||
self.update_preview()
|
||||
return False
|
||||
return True
|
||||
|
||||
finally:
|
||||
self.output_text['state'] = 'disabled'
|
||||
self.output_text.config(state='disabled')
|
||||
|
||||
def update_placeholders(self):
|
||||
if self.current_model is None:
|
||||
|
|
@ -964,13 +602,6 @@ class KiCoilGUI:
|
|||
if not HAS_PREVIEW:
|
||||
return
|
||||
|
||||
if self.current_model is None:
|
||||
# Clear preview when model is invalid
|
||||
self.preview_canvas.delete("all")
|
||||
self.preview_raw_image = None
|
||||
self.preview_image = None
|
||||
return
|
||||
|
||||
arc_tolerance = self.arc_tolerance_var.get()
|
||||
circle_segments = self.circle_segments_var.get()
|
||||
|
||||
|
|
@ -991,99 +622,20 @@ class KiCoilGUI:
|
|||
|
||||
|
||||
def show_valid_twists(self):
|
||||
"""Show valid twist counts for current number of turns"""
|
||||
turns = self.turns_var.get()
|
||||
valid_twists = list(divisors(turns, turns))
|
||||
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.config(state='normal')
|
||||
self.output_text.delete('1.0', tk.END)
|
||||
self.output_text.insert('1.0', f'Valid twist counts for {turns} turns:\n')
|
||||
for d in valid_twists:
|
||||
self.output_text.insert(tk.END, f' {d}\n')
|
||||
self.output_text['state'] = 'disabled'
|
||||
self.output_text.config(state='disabled')
|
||||
|
||||
def update_board_footprint(self):
|
||||
if not self.validate_parameters():
|
||||
messagebox.showerror("Error", "Cannot generate model. Please check the output for warnings or errors.")
|
||||
return
|
||||
def generate_footprint(self):
|
||||
"""Generate the KiCad footprint using the validated model"""
|
||||
|
||||
from kipy.board_types import FootprintInstance, Footprint, Pad, BoardArc, BoardSegment, FootprintAttributes,\
|
||||
PadStack, PadStackLayer, PadStackType, PadStackShape, DrillProperties, PadType
|
||||
from kipy.geometry import Vector2
|
||||
from kipy.common_types import GraphicAttributes, StrokeAttributes
|
||||
from kipy.util import from_mm
|
||||
from kipy.util.board_layer import CANONICAL_LAYER_NAMES
|
||||
|
||||
from gerbonara.cad.kicad.footprints import Atom
|
||||
|
||||
board = self.kicad_inst.get_board()
|
||||
selected = [item for item in board.get_selection() if isinstance(item, FootprintInstance)]
|
||||
|
||||
if not selected:
|
||||
messagebox.showerror("Error", "No footprint selected. Select one footprint to replace in KiCad's PCB editor.")
|
||||
return
|
||||
elif len(selected) > 1:
|
||||
messagebox.showerror("Error", "More than one footprint selected. Select only the footprint you want to replace.")
|
||||
return
|
||||
|
||||
selected_footprint, = selected
|
||||
|
||||
footprint_name = self.footprint_name_entry.get() or None
|
||||
arc_tolerance = self.arc_tolerance_var.get()
|
||||
circle_segments = self.circle_segments_var.get()
|
||||
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.insert(tk.END, "Rendering footprint...\n", 'info')
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
model = self.current_model.render_footprint(footprint_name, arc_tolerance, circle_segments)
|
||||
selected_footprint.attributes.exclude_from_bill_of_materials = True
|
||||
layer_map = {v: k for k, v in CANONICAL_LAYER_NAMES.items()}
|
||||
items = []
|
||||
|
||||
for line in model.lines:
|
||||
seg = BoardSegment()
|
||||
seg.start = Vector2.from_xy(from_mm(line.start.x), from_mm(line.start.y))
|
||||
seg.end = Vector2.from_xy(from_mm(line.end.x), from_mm(line.end.y))
|
||||
seg.attributes.stroke.width = from_mm(line.stroke.width)
|
||||
seg.layer = layer_map[line.layer]
|
||||
selected_footprint.definition.add_item(seg)
|
||||
items.append(seg)
|
||||
|
||||
for ref in model.arcs:
|
||||
arc = BoardArc()
|
||||
arc.start = Vector2.from_xy(from_mm(ref.start.x), from_mm(ref.start.y))
|
||||
arc.mid = Vector2.from_xy(from_mm(ref.mid.x), from_mm(ref.mid.y))
|
||||
arc.end = Vector2.from_xy(from_mm(ref.end.x), from_mm(ref.end.y))
|
||||
arc.attributes.stroke.width = from_mm(ref.stroke.width)
|
||||
arc.layer = layer_map[ref.layer]
|
||||
selected_footprint.definition.add_item(arc)
|
||||
items.append(arc)
|
||||
|
||||
for ref in model.pads:
|
||||
pad = Pad()
|
||||
pad.number = ref.number
|
||||
pad.position = Vector2.from_xy(from_mm(ref.at.x), from_mm(ref.at.y))
|
||||
pad.type = PadType.PT_SMD if ref.type == Atom.smd else PadType.PT_PTH
|
||||
pad.padstack.type = PadStackType.PST_NORMAL
|
||||
pad.padstack.layers = [layer_map[name] for name in ref.layers]
|
||||
layer = pad.padstack.copper_layers[0]
|
||||
layer.shape = PadStackShape.PSS_CIRCLE
|
||||
layer.size = Vector2.from_xy(from_mm(ref.size.x), from_mm(ref.size.y))
|
||||
layer.layer = layer_map[ref.layers[0]] # ? duplicate
|
||||
if ref.drill:
|
||||
pad.padstack.drill.diameter = Vector2.from_xy(from_mm(ref.drill.diameter), from_mm(ref.drill.diameter))
|
||||
selected_footprint.definition.add_item(pad)
|
||||
items.append(pad)
|
||||
|
||||
commit = board.begin_commit()
|
||||
board.create_items(items)
|
||||
board.update_items([selected_footprint])
|
||||
board.push_commit(commit, 'Updated planar coil footprint')
|
||||
self.output_text.insert(tk.END, "Done.", 'info')
|
||||
self.output_text['state'] = 'disabled'
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
def save_footprint_file(self):
|
||||
if not self.validate_parameters():
|
||||
messagebox.showerror("Error", "Cannot generate model. Please check the output for warnings or errors.")
|
||||
return
|
||||
|
|
@ -1093,11 +645,11 @@ class KiCoilGUI:
|
|||
arc_tolerance = self.arc_tolerance_var.get()
|
||||
circle_segments = self.circle_segments_var.get()
|
||||
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.config(state='normal')
|
||||
self.output_text.insert(tk.END, "Rendering footprint...\n\n", 'info')
|
||||
|
||||
footprint = self.current_model.render_footprint(footprint_name, arc_tolerance, circle_segments)
|
||||
default_name = footprint_name or self.current_model.default_footprint_name
|
||||
default_name = footprint_name or model.default_footprint_name
|
||||
output_file = filedialog.asksaveasfilename(
|
||||
title="Save KiCad Footprint",
|
||||
defaultextension=".kicad_mod",
|
||||
|
|
@ -1114,7 +666,7 @@ class KiCoilGUI:
|
|||
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
self.output_text['state'] = 'normal'
|
||||
self.output_text.config(state='normal')
|
||||
self.output_text.insert(tk.END, f"\nError generating footprint:\n{tb}\n", 'error')
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
|
|
@ -1123,13 +675,13 @@ class KiCoilGUI:
|
|||
messagebox.showerror("Error", f"Error generating footprint: {e}")
|
||||
|
||||
finally:
|
||||
self.output_text['state'] = 'disabled'
|
||||
self.output_text.config(state='disabled')
|
||||
|
||||
|
||||
def main(kicad_inst=None):
|
||||
from kipy import KiCad
|
||||
from kipy.errors import ConnectionError
|
||||
kicad_inst = KiCad()
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = KiCoilGUI(root, kicad_inst)
|
||||
app = KiCoilGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,361 +0,0 @@
|
|||
import math
|
||||
import itertools
|
||||
import subprocess
|
||||
import os
|
||||
from tempfile import NamedTemporaryFile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import importlib.resources
|
||||
import lzma
|
||||
import sys
|
||||
import hashlib
|
||||
|
||||
import platformdirs
|
||||
import wasmtime
|
||||
|
||||
|
||||
def interpolate(p1, p2, t, t_start=0, t_end=1):
|
||||
""" Interpolate along the line from point p1 to point p2 using t as the interpolation variable.
|
||||
t_start and t_end set the bounds of t, t_start at p1, and t_end at p2.
|
||||
When both interval ends coincide, clips and returns p1.
|
||||
"""
|
||||
if math.isclose(t_start, t_end):
|
||||
return p1
|
||||
t_range = t_end - t_start
|
||||
t = (t - t_start) / t_range
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
dx, dy = x2 - x1, y2 - y1
|
||||
return (x1 + t*dx, y1 + t*dy)
|
||||
|
||||
|
||||
def approx_in_range(value, lower, upper):
|
||||
""" Approximate range check """
|
||||
if math.isclose(value, lower) or math.isclose(value, upper):
|
||||
return True
|
||||
return lower <= value <= upper
|
||||
|
||||
|
||||
def edge_cycle(points):
|
||||
""" From a list of points return an iterator of all edges assuming they are a closed loop:
|
||||
[A B C] -> [AB BC CA]
|
||||
"""
|
||||
return itertools.pairwise(itertools.chain(points, points[:1]))
|
||||
|
||||
|
||||
def polygon_is_clockwise(points):
|
||||
(x1, y1, i), *_rest = sorted((x, y, i) for i, (x, y) in enumerate(points))
|
||||
x0, y0 = points[(i-1)%len(points)]
|
||||
x2, y2 = points[(i+1)%len(points)]
|
||||
det = (x0*y1 + x1*y2 + x2*y0) - (x2*y1 + x1*y0 + x0*y2)
|
||||
return det < 0
|
||||
|
||||
|
||||
def polygon_center_of_mass(polygon):
|
||||
# https://en.wikipedia.org/wiki/Centroid
|
||||
total_x, total_y = 0, 0
|
||||
area = 0
|
||||
for (x1, y1), (x2, y2) in edge_cycle(polygon):
|
||||
diff = (x1*y2 - x2*y1)
|
||||
total_x += (x1 + x2) * diff
|
||||
total_y += (y1 + y2) * diff
|
||||
area += diff
|
||||
area /= 2
|
||||
total_x /= 6*area
|
||||
total_y /= 6*area
|
||||
return total_x, total_y
|
||||
|
||||
|
||||
class WasmApp:
|
||||
def __init__(self, wasm_filename, cachedir="kicoil"):
|
||||
module_binary = importlib.resources.read_binary(__package__, wasm_filename)
|
||||
|
||||
module_path_digest = hashlib.sha256(__file__.encode()).hexdigest()
|
||||
module_digest = hashlib.sha256(module_binary).hexdigest()
|
||||
cache_path = Path(os.getenv("KICOIL_CACHE_DIR", platformdirs.user_cache_dir(cachedir)))
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
cache_filename = (cache_path / f'{wasm_filename}-{module_path_digest[:8]}-{module_digest[:16]}')
|
||||
|
||||
self.engine = wasmtime.Engine()
|
||||
|
||||
try:
|
||||
with cache_filename.open("rb") as cache_file:
|
||||
self.module = wasmtime.Module.deserialize(self.engine, lzma.decompress(cache_file.read()))
|
||||
except:
|
||||
print("Preparing to run {}. This might take a while...".format(wasm_filename), file=sys.stderr)
|
||||
self.module = wasmtime.Module(self.engine, module_binary)
|
||||
with cache_filename.open("wb") as cache_file:
|
||||
cache_file.write(lzma.compress(self.module.serialize(), preset=0))
|
||||
|
||||
def run(self, stdin='', argv=[]):
|
||||
with NamedTemporaryFile('r') as stdout_f, NamedTemporaryFile('w') as stdin_f:
|
||||
stdin_f.write(stdin)
|
||||
stdin_f.flush()
|
||||
|
||||
wasi_cfg = wasmtime.WasiConfig()
|
||||
wasi_cfg.argv = argv
|
||||
wasi_cfg.stdin_file = stdin_f.name
|
||||
wasi_cfg.stdout_file = stdout_f.name
|
||||
wasi_cfg.inherit_stderr()
|
||||
|
||||
linker = wasmtime.Linker(self.engine)
|
||||
linker.define_wasi()
|
||||
store = wasmtime.Store(self.engine)
|
||||
store.set_wasi(wasi_cfg)
|
||||
self.app = linker.instantiate(store, self.module)
|
||||
linker.define_instance(store, "app", self.app)
|
||||
|
||||
try:
|
||||
self.app.exports(store)["_start"](store)
|
||||
except wasmtime.ExitTrap as trap:
|
||||
if trap.code != 0:
|
||||
raise RuntimeError('Error computing straight skeleton.')
|
||||
return 0, stdout_f.read()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkeletonNode:
|
||||
x: float
|
||||
y: float
|
||||
time: float
|
||||
|
||||
@property
|
||||
def pos(self):
|
||||
return self.x, self.y
|
||||
|
||||
|
||||
skeleton_wasm = WasmApp('skeleton.wasm')
|
||||
|
||||
def compute_skeleton(exterior):
|
||||
points_deduplicated = []
|
||||
for p1, p2 in edge_cycle(exterior):
|
||||
if p2 != p1:
|
||||
points_deduplicated.append(p1)
|
||||
input_data = '\n'.join(f'{x} {y}' for x, y in points_deduplicated)
|
||||
Path('/tmp/debug.txt').write_text(input_data)
|
||||
|
||||
rc, data = skeleton_wasm.run(input_data)
|
||||
|
||||
# Parse output: each line is "x1 y1 x2 y2 t1 t2"
|
||||
node_map = {} # Map (x, y, t) to SkeletonNode
|
||||
edges = []
|
||||
|
||||
for line in data.strip().split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
|
||||
x1, y1, x2, y2, t1, t2 = map(float, parts)
|
||||
|
||||
n1 = (x1, y1, t1)
|
||||
if n1 not in node_map:
|
||||
node_map[n1] = SkeletonNode(*n1)
|
||||
|
||||
n2 = (x2, y2, t2)
|
||||
if n2 not in node_map:
|
||||
node_map[n2] = SkeletonNode(*n2)
|
||||
|
||||
edges.append((node_map[n1], node_map[n2]))
|
||||
|
||||
nodes = list(node_map.values())
|
||||
return nodes, edges
|
||||
|
||||
|
||||
class Skeletonator:
|
||||
def __init__(self, poly):
|
||||
self.poly = poly
|
||||
self.poly_edges = list(zip(poly, poly[1:] + poly[:1]))
|
||||
self.circumference = sum(math.dist(a, b) for a, b in self.poly_edges)
|
||||
self.skeleton_nodes, self.skeleton_edges = compute_skeleton(exterior=poly)
|
||||
self.arc_map = {}
|
||||
self.divergent = set()
|
||||
self.radius = max(n.time for n in self.skeleton_nodes)
|
||||
self.min_radius = self.radius
|
||||
for n1, n2 in self.skeleton_edges:
|
||||
if n1 in self.arc_map:
|
||||
self.divergent.add(n1)
|
||||
self.min_radius = min(n1.time, self.radius)
|
||||
self.arc_map[n1] = n2
|
||||
coord_map = {}
|
||||
for n in self.skeleton_nodes:
|
||||
p = (round(n.x, 6), round(n.y, 6))
|
||||
coord_map[p] = n
|
||||
self.node_map = {}
|
||||
for x, y in poly:
|
||||
p = (round(x, 6), round(y, 6))
|
||||
self.node_map[(x, y)] = coord_map[p]
|
||||
self.debug_arms = []
|
||||
|
||||
def iter_arcs(self, p):
|
||||
i = 0
|
||||
start = self.node_map[p]
|
||||
while start in self.arc_map and not start in self.divergent:
|
||||
end = self.arc_map[start]
|
||||
i += 1
|
||||
yield start, end
|
||||
start = end
|
||||
|
||||
def project_arc(self, p, r):
|
||||
t = self.radius - r
|
||||
|
||||
for n0, n1 in self.iter_arcs(p):
|
||||
if t < 0 or approx_in_range(t, n0.time, n1.time):
|
||||
return (n0, n1), interpolate(n0.pos, n1.pos, t, n0.time, n1.time)
|
||||
else:
|
||||
raise ValueError(f'{r=:.3f} is out of bounds [0, {self.radius - self.min_radius:.4f}]')
|
||||
|
||||
def calc_circumference(self, r):
|
||||
projected = [self.project_arc(p, r)[1] for p in self.poly]
|
||||
return sum(math.dist(p1, p2) for p1, p2 in zip(projected, projected[1:] + projected[:1]))
|
||||
|
||||
def project_point(self, t, r=None, r_ref=None):
|
||||
t %= 1
|
||||
if r is None:
|
||||
r = self.radius
|
||||
|
||||
if r_ref is None:
|
||||
r_ref = r
|
||||
|
||||
t_start = 0
|
||||
p_cur = None
|
||||
_arcs, points_at_r = self.map_circumference(r_ref)
|
||||
circumference_at_r = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(points_at_r))
|
||||
for (p1, p2), (p1r, p2r) in zip(self.poly_edges, edge_cycle(points_at_r)):
|
||||
edge_frac = math.dist(p1r, p2r) / circumference_at_r
|
||||
t_end = t_start + edge_frac
|
||||
|
||||
if approx_in_range(t, t_start, t_end):
|
||||
p1, p2 = self.project_arc(p1, r)[1], self.project_arc(p2, r)[1]
|
||||
p_cur = interpolate(p1, p2, t, t_start, t_end)
|
||||
return p_cur
|
||||
|
||||
t_start = t_end
|
||||
|
||||
def map_circumference(self, r):
|
||||
points, arcs = [], []
|
||||
for p in self.poly:
|
||||
arc, pt = self.project_arc(p, r)
|
||||
arcs.append(arc)
|
||||
points.append(pt)
|
||||
return arcs, points
|
||||
|
||||
def do_spiral(self, t1, t2, r1=None, r2=None, debug=False):
|
||||
if r1 is None:
|
||||
r1 = self.radius
|
||||
if r2 is None:
|
||||
r2 = self.min_radius
|
||||
|
||||
direction = True
|
||||
if t2 < t1:
|
||||
direction = False
|
||||
t1, t2 = t2, t1
|
||||
r1, r2 = r2, r1
|
||||
|
||||
def r_interpolate(t):
|
||||
t = max(t1, min(t2, t)) # Clip to start/end of spiral
|
||||
f = (t - t1) / (t2 - t1)
|
||||
return r1 + (r2 - r1) * f
|
||||
|
||||
debug_arm = []
|
||||
for t_start in range(math.ceil(t2-t1)):
|
||||
t_start += t1
|
||||
t_end = t_start + 1
|
||||
r_outer = r_interpolate(t_start)
|
||||
r_inner = r_interpolate(t_end)
|
||||
r_ref = min(r_inner, r_outer) # Handle outward spirals where the radii are swapped
|
||||
_ic_arcs, inner_circumference = self.map_circumference(r_ref)
|
||||
|
||||
angle = math.floor(t_start)
|
||||
circumference_angles = []
|
||||
inner_circumference_sum = sum(math.dist(p1, p2) for p1, p2 in edge_cycle(inner_circumference))
|
||||
point_angles = []
|
||||
for p1, p2 in edge_cycle(inner_circumference):
|
||||
edge_angle = math.dist(p1, p2) / inner_circumference_sum
|
||||
point_angles.append(angle)
|
||||
angle += edge_angle
|
||||
point_angles += [a+1 for a in point_angles]
|
||||
point_angles += [point_angles[0] + 2]
|
||||
|
||||
i = 0
|
||||
for (p1, p2), (tp1, tp2) in zip(self.poly_edges * 3, itertools.pairwise(point_angles)):
|
||||
i += 1
|
||||
rp1 = r_interpolate(tp1)
|
||||
rp2 = r_interpolate(tp2)
|
||||
_arc, p1_proj = self.project_arc(p1, rp1)
|
||||
_arc, p2_proj = self.project_arc(p2, rp2)
|
||||
if tp2 < t_start and not math.isclose(tp2, t_start):
|
||||
continue
|
||||
if tp1 > t_end and not math.isclose(tp1, t_end):
|
||||
continue
|
||||
|
||||
if approx_in_range(t1, tp1, tp2):
|
||||
_arc, p2_proj_r1 = self.project_arc(p2, r1)
|
||||
p_out = interpolate(p1_proj, p2_proj_r1, t1, tp1, tp2)
|
||||
debug_arm.append(p_out)
|
||||
yield p_out, r_ref
|
||||
|
||||
if approx_in_range(t2, tp1, tp2):
|
||||
_arc, p1_proj_r2 = self.project_arc(p1, r2)
|
||||
p_out = interpolate(p1_proj_r2, p2_proj, t2, tp1, tp2)
|
||||
debug_arm.append(p_out)
|
||||
yield p_out, r_ref
|
||||
|
||||
elif approx_in_range(tp2, t1, t2):
|
||||
debug_arm.append(p2_proj)
|
||||
yield p2_proj, r_ref
|
||||
if debug:
|
||||
self.debug_arms.append((debug_arm, direction, t1, t2))
|
||||
|
||||
def dump_to_pdf(self, filename):
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 10))
|
||||
|
||||
# polygon outline
|
||||
poly_x = [p[0] for p in self.poly] + [self.poly[0][0]]
|
||||
poly_y = [p[1] for p in self.poly] + [self.poly[0][1]]
|
||||
ax.plot(poly_x, poly_y, '-', color='black', linewidth=.5, label='Polygon')
|
||||
|
||||
# skeleton edges
|
||||
for node1, node2 in self.skeleton_edges:
|
||||
ax.plot([node1.x, node2.x], [node1.y, node2.y], '-', color='gray', linewidth=.5, alpha=0.7)
|
||||
|
||||
# skeleton nodes
|
||||
for n in self.skeleton_nodes:
|
||||
if n in self.divergent:
|
||||
ax.plot(n.x, n.y, 'o', markerfacecolor='none', markeredgecolor='green', markersize=4)
|
||||
elif n in self.arc_map:
|
||||
ax.plot(n.x, n.y, 'o', color='black', markersize=3, alpha=0.5)
|
||||
else:
|
||||
ax.plot(n.x, n.y, 'o', markerfacecolor='none', markeredgecolor='magenta', markersize=4)
|
||||
|
||||
count = {True: 0, False: 0}
|
||||
for arm, direction, t1, t2 in self.debug_arms:
|
||||
xs = [x for x, y in arm]
|
||||
ys = [y for x, y in arm]
|
||||
ax.plot(xs, ys, linewidth=.2, color='red' if direction else 'blue')
|
||||
align = 'left' if direction else 'right'
|
||||
ax.text(xs[-1], ys[-1], f'{count[direction]}', size=3, horizontalalignment=align)
|
||||
ax.text(xs[0], ys[0], f'{count[direction]}', size=3, horizontalalignment=align, color='gray')
|
||||
count[direction] += 1
|
||||
|
||||
xs, ys = [], []
|
||||
for i in range(100):
|
||||
r = self.radius - (i/99) * self.min_radius
|
||||
arc, (px, py) = self.project_arc(self.poly[0], r)
|
||||
xs.append(px)
|
||||
ys.append(py)
|
||||
ax.plot(xs, ys, '--', linewidth=.5, color='black')
|
||||
|
||||
ax.set_aspect('equal', adjustable='box')
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
ax.set_title(f'Polygon Skeleton (radius: {self.radius:.3f}, min_radius: {self.min_radius:.3f})')
|
||||
ax.set_xlabel('X')
|
||||
ax.set_ylabel('Y')
|
||||
ax.invert_yaxis()
|
||||
fig.savefig(filename, bbox_inches='tight', dpi=600)
|
||||
plt.close(fig)
|
||||
Loading…
Add table
Add a link
Reference in a new issue