Add simple GUI
This commit is contained in:
parent
b70ff262a6
commit
7590bde619
8 changed files with 1095 additions and 38 deletions
28
metadata.json
Normal file
28
metadata.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "https://go.kicad.org/pcm/schemas/v1",
|
||||
"name": "KiCoil",
|
||||
"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": {
|
||||
"name": "jaseg",
|
||||
"contact": {
|
||||
"web": "https://jaseg.de/"
|
||||
}
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"resources": {
|
||||
"homepage": "https://jaseg.de/projects/kicoil",
|
||||
"git": "https://git.jaseg.de/kicoil",
|
||||
"issues": "https://codeberg.org/jaseg/kicoil/issues"
|
||||
},
|
||||
"versions": [
|
||||
{
|
||||
"version": "0.9.0",
|
||||
"status": "stable",
|
||||
"kicad_version": "8.00"
|
||||
}
|
||||
],
|
||||
"runtime": "ipc"
|
||||
}
|
||||
106
package.py
Normal file
106
package.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python3
|
||||
import re
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
def tree_size(path):
|
||||
return sum(entry.stat().st_size for entry in path.glob('**/*') if entry.is_file())
|
||||
|
||||
@click.command()
|
||||
@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}'
|
||||
|
||||
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.')
|
||||
|
||||
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\.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)
|
||||
|
||||
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')
|
||||
|
||||
print(f'Updating metadata file {meta_path}')
|
||||
ver_dict = {
|
||||
'version': version,
|
||||
'status': 'stable',
|
||||
'kicad_version': '7.99',
|
||||
}
|
||||
|
||||
# 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))
|
||||
|
||||
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)
|
||||
|
||||
meta_file = json.loads(meta_path.read_text())
|
||||
meta_file['versions'].append(ver_dict)
|
||||
meta_path.write_text(json.dumps(meta_file, indent=4))
|
||||
|
||||
print(f'Adding updated metadata file {meta_path} to git index')
|
||||
subprocess.run(['git', 'add', str(meta_path)], check=True, capture_output=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()
|
||||
|
|
@ -11,7 +11,6 @@ maintainers = [
|
|||
{ name = "Kicoil maintainers", email = "kicoil@jaseg.de" },
|
||||
]
|
||||
keywords = ["kicad", "gerber", "pcb", "electronics", "EDA"]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
|
|
@ -29,12 +28,16 @@ classifiers = [
|
|||
[project.urls]
|
||||
Homepage = "https://jaseg.de/projects/kicoil/"
|
||||
Documentation = "https://kicoil.jaseg.de/"
|
||||
Source = "https://codeberg.org/jaseg/kicoil"
|
||||
Source = "https://git.jaseg.de/kicoil"
|
||||
"Source Mirror" = "https://git.jaseg.de/kicoil.git"
|
||||
Tracker = "https://codeberg.org/jaseg/kicoil/issues"
|
||||
|
||||
[project.scripts]
|
||||
kicoil = "kicoil.cli:cli"
|
||||
kicoil-gui = "kicoil.gui:main"
|
||||
|
||||
[dependency-groups]
|
||||
gui = ["cairosvg", "pillow"]
|
||||
|
||||
[build-system]
|
||||
requires = ["uv-build"]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
# Copyright 2025 Jan Sebastian Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
|
|
@ -50,14 +64,13 @@ def print_valid_twists(ctx, param, value):
|
|||
@click.option('--arc-tolerance', type=float, default=0.02)
|
||||
@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('--clockwise/--counter-clockwise', help='Direction of generated spiral. Default: counter-clockwise when wound from the inside.')
|
||||
@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()
|
||||
def cli(outfile, footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, **kwargs):
|
||||
logger = logging.getLogger('kicoil')
|
||||
|
||||
if single_layer:
|
||||
kwargs['twists'] = 0
|
||||
kwargs['layers'] = 1
|
||||
else:
|
||||
kwargs['layers'] = 2
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
|
||||
# Copyright 2025 Jan Sebastian Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import warnings
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
|
|
@ -18,26 +34,7 @@ def point_line_distance(p, l1, l2):
|
|||
x1, y1 = l1
|
||||
x2, y2 = l2
|
||||
# https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
|
||||
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / sqrt((x2-x1)**2 + (y2-y1)**2)
|
||||
|
||||
def line_line_intersection(l1, l2):
|
||||
p1, p2 = l1
|
||||
p3, p4 = l2
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
x3, y3 = p3
|
||||
x4, y4 = p4
|
||||
|
||||
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
|
||||
px = ((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
|
||||
py = ((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
|
||||
return px, py
|
||||
|
||||
def angle_between_vectors(va, vb):
|
||||
angle = atan2(vb[1], vb[0]) - atan2(va[1], va[0])
|
||||
if angle < 0:
|
||||
angle += 2*pi
|
||||
return angle
|
||||
return ((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / sqrt((x2-x1)**2 + (y2-y1)**2)
|
||||
|
||||
|
||||
# https://en.wikipedia.org/wiki/Farey_sequence#Next_term
|
||||
|
|
@ -91,11 +88,14 @@ def arc_approximate(points, trace_width, layer, tolerance=0.02, level=0):
|
|||
yield from arc_approximate(points[i_mid:], trace_width, layer, tolerance, level+1)
|
||||
|
||||
else:
|
||||
yield kicad.make_arc(x0, y0, x2, y2, x1, y1, trace_width, layer)
|
||||
if point_line_distance((x1, y1), (x0, y0), (x2, y2)) > 0:
|
||||
yield kicad.make_arc(x0, y0, x2, y2, x1, y1, trace_width, layer)
|
||||
else:
|
||||
yield kicad.make_arc(x2, y2, x0, y0, x1, y1, trace_width, layer)
|
||||
|
||||
|
||||
def compute_spiral(r1, r2, a1, a2, fn=64):
|
||||
fn = ceil(fn * (a2-a1)/(2*pi))
|
||||
fn = ceil(fn * abs(a2-a1)/(2*pi))
|
||||
x0, y0 = cos(a1)*r1, sin(a1)*r1
|
||||
dr = 3 if r2 < r1 else -3
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ class PlanarInductor():
|
|||
inner_diameter: float
|
||||
turns: int
|
||||
twists: int
|
||||
trace_width: float
|
||||
trace_width: float = None
|
||||
clearance: float = None
|
||||
layers: int = 2
|
||||
via_diameter: float = 0.6
|
||||
|
|
@ -139,9 +139,12 @@ class PlanarInductor():
|
|||
self.inner_radius = self.inner_diameter/2
|
||||
self.turns_per_layer = self.turns/self.layers
|
||||
self.sector_angle = 2*pi / self.twists
|
||||
|
||||
if self.clockwise:
|
||||
self.sector_angle *= -1
|
||||
|
||||
self.sweeping_angle = self.sector_angle * self.turns_per_layer
|
||||
self.spiral_pitch = (self.outer_radius-self.inner_radius) / self.turns_per_layer
|
||||
self.layers = 2 if self.twists > 0 else 1
|
||||
self.R = None # will be calculated during render
|
||||
|
||||
c1 = self.inner_radius
|
||||
|
|
@ -151,6 +154,9 @@ class PlanarInductor():
|
|||
alpha = (alpha1+alpha2)/2
|
||||
self.projected_spiral_pitch = self.spiral_pitch*cos(alpha)
|
||||
|
||||
if self.layers == 1 and self.twists > 1:
|
||||
warnings.warn('Warning: Twists set to a value other than 1, but single-layer mode is enabled. The twists value will be ignored.')
|
||||
self.twists = 1
|
||||
|
||||
if self.turns < 1:
|
||||
raise ValueError(f'Error: PlanarInductor.turns must be 1 or more')
|
||||
|
|
@ -248,9 +254,13 @@ class PlanarInductor():
|
|||
self.logger.info(f'Approximate resistance: {self.R:g} Ω')
|
||||
|
||||
|
||||
@property
|
||||
def default_footprint_name(self):
|
||||
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):
|
||||
if name is None:
|
||||
name = f'generated-coil-{self.outer_diameter:.2f}x{self.inner_diameter:.2f}-n{self.turns}-k{self.twists}'
|
||||
name = self.default_footprint_name
|
||||
|
||||
from . import __version__
|
||||
footprint = Footprint(
|
||||
|
|
@ -281,12 +291,14 @@ class PlanarInductor():
|
|||
fn=circle_segments)
|
||||
x0, y0 = points_layer0[0]
|
||||
xn, yn = points_layer0[-1]
|
||||
footprint.arcs.extend(arc_approximate(points_layer0, self.trace_width, self.layer_pair[0], arc_tolerance))
|
||||
|
||||
if self.twists > 0:
|
||||
if self.layers > 1:
|
||||
# Handle the returning arm on the bottom layer
|
||||
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.
|
||||
|
|
@ -294,9 +306,7 @@ class PlanarInductor():
|
|||
xq = xn + cos(fold_angle) * dr
|
||||
yq = yn - sin(fold_angle) * dr
|
||||
points_layer1 = [(xn, yn), (xq, yq)]
|
||||
|
||||
footprint.arcs.extend(arc_approximate(points_layer0, self.trace_width, self.layer_pair[0], arc_tolerance))
|
||||
footprint.arcs.extend(arc_approximate(points_layer1, self.trace_width, self.layer_pair[1], arc_tolerance))
|
||||
footprint.lines.append(kicad.make_line(xn, yn, xq, yq, self.trace_width, self.layer_pair[1]))
|
||||
|
||||
# Handle inner via ring and process staggering if enabled
|
||||
r = self.inner_via_ring_radius
|
||||
|
|
|
|||
687
src/kicoil/gui.py
Normal file
687
src/kicoil/gui.py
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
# Copyright 2025 Jan Sebastian Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# This file was created using claude.ai because hand-writing GUI code sucks. The rest of kicoil is written by hand.
|
||||
#
|
||||
|
||||
""" GUI for generating KiCad footprints using kicoil. """
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import sys
|
||||
import logging
|
||||
import warnings
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
from io import BytesIO
|
||||
|
||||
from .geometry import PlanarInductor, divisors
|
||||
from .svg import make_transparent_svg
|
||||
|
||||
try:
|
||||
# for rendering gerbonara's svg output to PNG
|
||||
import cairosvg
|
||||
# for scaling the rendered PNGs to the UI's resolution since tkinter is very limited there
|
||||
from PIL import Image, ImageTk
|
||||
HAS_PREVIEW = True
|
||||
except ImportError:
|
||||
HAS_PREVIEW = False
|
||||
|
||||
|
||||
class TextWidgetHandler(logging.Handler):
|
||||
"""Custom logging handler that writes to a tkinter Text widget"""
|
||||
def __init__(self, text_widget):
|
||||
super().__init__()
|
||||
self.text_widget = text_widget
|
||||
self.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
|
||||
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
|
||||
# Determine tag based on log level
|
||||
if record.levelno >= logging.ERROR:
|
||||
tag = 'error'
|
||||
elif record.levelno >= logging.WARNING:
|
||||
tag = 'warning'
|
||||
else:
|
||||
tag = 'info'
|
||||
|
||||
# Temporarily enable widget to insert text
|
||||
self.text_widget['state'] = 'normal'
|
||||
self.text_widget.insert(tk.END, msg + '\n', tag)
|
||||
self.text_widget.see(tk.END)
|
||||
self.text_widget['state'] = 'disabled'
|
||||
self.text_widget.update_idletasks()
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/27820178/how-to-add-placeholder-to-an-entry-in-tkinter
|
||||
class EntryWithPlaceholder(tk.Entry):
|
||||
def __init__(self, master=None, placeholder="empty", color='grey', *args, **kwargs):
|
||||
# Initialize parent Entry with all provided arguments
|
||||
super().__init__(master, *args, **kwargs)
|
||||
|
||||
self.placeholder = placeholder
|
||||
self.placeholder_color = color
|
||||
self.showing_placeholder = False
|
||||
|
||||
# Get the default foreground color from the created widget
|
||||
self.default_fg_color = self['fg']
|
||||
|
||||
self.bind("<FocusIn>", self.foc_in)
|
||||
self.bind("<FocusOut>", self.foc_out)
|
||||
|
||||
# Check initial state and show placeholder if empty
|
||||
# Use a small delay to ensure the widget is fully initialized
|
||||
self.after(1, self._check_initial_state)
|
||||
|
||||
def _check_initial_state(self):
|
||||
if not super().get():
|
||||
self.put_placeholder()
|
||||
|
||||
def put_placeholder(self):
|
||||
self.showing_placeholder = True
|
||||
self['fg'] = self.placeholder_color
|
||||
super().delete(0, 'end')
|
||||
super().insert(0, self.placeholder)
|
||||
|
||||
def update_placeholder(self, new_placeholder):
|
||||
self.placeholder = new_placeholder
|
||||
# If currently showing placeholder, update the display
|
||||
if self.showing_placeholder:
|
||||
super().delete(0, 'end')
|
||||
super().insert(0, self.placeholder)
|
||||
|
||||
def foc_in(self, *args):
|
||||
if self.showing_placeholder:
|
||||
self.showing_placeholder = False
|
||||
self['fg'] = self.default_fg_color
|
||||
super().delete(0, 'end')
|
||||
|
||||
def foc_out(self, *args):
|
||||
if not super().get():
|
||||
self.put_placeholder()
|
||||
else:
|
||||
self.showing_placeholder = False
|
||||
|
||||
def get(self):
|
||||
if self.showing_placeholder:
|
||||
return ''
|
||||
return super().get()
|
||||
|
||||
|
||||
class KiCoilGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("KiCoil - Planar Inductor Generator")
|
||||
self.root.geometry("1000x650")
|
||||
|
||||
style = ttk.Style()
|
||||
style.theme_use('clam')
|
||||
|
||||
main_container = ttk.Frame(root)
|
||||
main_container.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
root.columnconfigure(0, weight=1)
|
||||
root.rowconfigure(0, weight=1)
|
||||
main_container.columnconfigure(0, weight=0) # Left panel doesn't grow horizontally
|
||||
main_container.columnconfigure(1, weight=1) # Preview panel grows to fill space
|
||||
main_container.rowconfigure(0, weight=1)
|
||||
|
||||
main_frame = ttk.Frame(main_container, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
title_frame = ttk.Frame(main_frame)
|
||||
title_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
|
||||
ttk.Label(title_frame, text="Planar Inductor Generator",
|
||||
font=('Helvetica', 16, 'bold')).pack(side=tk.LEFT)
|
||||
|
||||
self.preview_visible = tk.BooleanVar(value=True)
|
||||
self.preview_button = ttk.Button(title_frame, text="Hide Preview",
|
||||
command=self.toggle_preview, width=15)
|
||||
self.preview_button.pack(side=tk.RIGHT)
|
||||
|
||||
self.notebook = ttk.Notebook(main_frame)
|
||||
self.notebook.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
||||
|
||||
geometry_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.notebook.add(geometry_frame, text="Geometry")
|
||||
self.create_geometry_params(geometry_frame)
|
||||
|
||||
traces_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.notebook.add(traces_frame, text="Traces")
|
||||
self.create_trace_params(traces_frame)
|
||||
|
||||
via_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.notebook.add(via_frame, text="Vias")
|
||||
self.create_via_params(via_frame)
|
||||
|
||||
output_frame = ttk.Frame(self.notebook, padding="10")
|
||||
self.notebook.add(output_frame, text="Output")
|
||||
self.create_output_params(output_frame)
|
||||
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.grid(row=2, column=0, columnspan=3, pady=10)
|
||||
|
||||
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="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))
|
||||
|
||||
self.output_text = scrolledtext.ScrolledText(main_frame, height=8, width=80, state='disabled')
|
||||
self.output_text.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
self.output_text.tag_config('error', foreground='red')
|
||||
self.output_text.tag_config('warning', foreground='orange')
|
||||
self.output_text.tag_config('info', foreground='black')
|
||||
|
||||
main_frame.columnconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(1, weight=0) # Notebook doesn't grow vertically
|
||||
main_frame.rowconfigure(4, weight=1) # Output area takes all vertical space
|
||||
|
||||
self.preview_frame = ttk.LabelFrame(main_container, text="Preview", padding=10)
|
||||
|
||||
if HAS_PREVIEW:
|
||||
# Create canvas for image display
|
||||
self.preview_canvas = tk.Canvas(self.preview_frame, bg='white')
|
||||
self.preview_canvas.pack(fill=tk.BOTH, expand=True)
|
||||
self.preview_image = None # Store reference to prevent garbage collection
|
||||
self.preview_raw_image = None # Store unscaled image for rescaling
|
||||
|
||||
# Bind canvas resize event to update preview
|
||||
self.preview_canvas.bind('<Configure>', self._on_preview_resize)
|
||||
|
||||
else:
|
||||
info_text = "Preview not available\n\nInstall dependencies:\npip install cairosvg pillow"
|
||||
self.preview_label = ttk.Label(self.preview_frame, text=info_text,
|
||||
justify=tk.CENTER, anchor=tk.CENTER)
|
||||
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.root.after(100, self.validate_parameters)
|
||||
|
||||
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:
|
||||
self.root.after_cancel(self._resize_after_id)
|
||||
self._resize_after_id = self.root.after(100, self._rescale_preview)
|
||||
|
||||
def _rescale_preview(self):
|
||||
canvas_width = self.preview_canvas.winfo_width()
|
||||
canvas_height = self.preview_canvas.winfo_height()
|
||||
if canvas_width <= 1 or canvas_height <= 1 or self.preview_raw_image is None:
|
||||
self.preview_canvas.delete("all")
|
||||
return
|
||||
|
||||
# Calculate scaling to fit within canvas
|
||||
image = self.preview_raw_image.copy()
|
||||
img_ratio = image.width / image.height
|
||||
canvas_ratio = canvas_width / canvas_height
|
||||
|
||||
if img_ratio > canvas_ratio:
|
||||
# Image is wider than canvas
|
||||
new_width = int(canvas_width * 0.95)
|
||||
new_height = int(new_width / img_ratio)
|
||||
else:
|
||||
# Image is taller than canvas
|
||||
new_height = int(canvas_height * 0.95)
|
||||
new_width = int(new_height * img_ratio)
|
||||
|
||||
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
self.preview_image = ImageTk.PhotoImage(image)
|
||||
self.preview_canvas.create_image(
|
||||
canvas_width // 2, canvas_height // 2,
|
||||
image=self.preview_image, anchor=tk.CENTER
|
||||
)
|
||||
|
||||
def toggle_preview(self):
|
||||
if self.preview_visible.get():
|
||||
self.preview_frame.grid_forget()
|
||||
self.preview_visible.set(False)
|
||||
self.preview_button.config(text="Show Preview")
|
||||
|
||||
current_width = self.root.winfo_width()
|
||||
current_height = self.root.winfo_height()
|
||||
new_width = max(680, current_width - 400)
|
||||
self.root.geometry(f"{new_width}x{current_height}")
|
||||
|
||||
else:
|
||||
self.preview_frame.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.E, tk.W), padx=(10, 0), pady=10)
|
||||
self.preview_visible.set(True)
|
||||
self.preview_button.config(text="Hide Preview")
|
||||
|
||||
current_width = self.root.winfo_width()
|
||||
current_height = self.root.winfo_height()
|
||||
new_width = current_width + 400 # Add ~400px for preview
|
||||
self.root.geometry(f"{new_width}x{current_height}")
|
||||
|
||||
def create_geometry_params(self, parent):
|
||||
row = 0
|
||||
|
||||
# Turns
|
||||
ttk.Label(parent, text="Number of Turns:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.turns_var = tk.IntVar(value=5)
|
||||
ttk.Spinbox(parent, from_=1, to=100, textvariable=self.turns_var,
|
||||
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=1)
|
||||
ttk.Spinbox(parent, from_=0, to=50, textvariable=self.twists_var,
|
||||
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)
|
||||
layer_frame = ttk.Frame(parent)
|
||||
layer_frame.grid(row=row, column=1, columnspan=2, sticky=tk.W, pady=5)
|
||||
ttk.Radiobutton(layer_frame, text="Two Layer", variable=self.layer_mode_var,
|
||||
value=2).pack(side=tk.LEFT, padx=(0, 10))
|
||||
ttk.Radiobutton(layer_frame, text="Single Layer", variable=self.layer_mode_var,
|
||||
value=1).pack(side=tk.LEFT)
|
||||
row += 1
|
||||
|
||||
# Direction
|
||||
ttk.Label(parent, text="Winding Direction:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.direction_var = tk.StringVar(value="counter-clockwise")
|
||||
dir_frame = ttk.Frame(parent)
|
||||
dir_frame.grid(row=row, column=1, columnspan=2, sticky=tk.W, pady=5)
|
||||
ttk.Radiobutton(dir_frame, text="Counter-Clockwise", variable=self.direction_var,
|
||||
value="counter-clockwise").pack(side=tk.LEFT, padx=(0, 10))
|
||||
ttk.Radiobutton(dir_frame, text="Clockwise", variable=self.direction_var,
|
||||
value="clockwise").pack(side=tk.LEFT)
|
||||
row += 1
|
||||
|
||||
def create_trace_params(self, parent):
|
||||
row = 0
|
||||
|
||||
# Trace Width
|
||||
ttk.Label(parent, text="Trace Width (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.trace_width_entry = EntryWithPlaceholder(parent, placeholder="automatic", width=15)
|
||||
self.trace_width_entry.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Clearance
|
||||
ttk.Label(parent, text="Clearance (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.clearance_entry = EntryWithPlaceholder(parent, placeholder="automatic", width=15)
|
||||
self.clearance_entry.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Copper Thickness (in µm)
|
||||
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).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
|
||||
|
||||
def create_via_params(self, parent):
|
||||
"""Create via parameter controls"""
|
||||
row = 0
|
||||
|
||||
# Via Diameter
|
||||
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).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Via Drill
|
||||
ttk.Label(parent, text="Via Drill (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.via_drill_entry = EntryWithPlaceholder(parent, placeholder="automatic", width=15)
|
||||
self.via_drill_entry.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Via Offset
|
||||
ttk.Label(parent, text="Via Offset (mm):").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.via_offset_entry = EntryWithPlaceholder(parent, placeholder="automatic", width=15)
|
||||
self.via_offset_entry.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Stagger Inner Vias
|
||||
self.stagger_inner_var = tk.BooleanVar(value=False)
|
||||
ttk.Checkbutton(parent, text="Stagger inner via ring",
|
||||
variable=self.stagger_inner_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Stagger Outer Vias
|
||||
self.stagger_outer_var = tk.BooleanVar(value=False)
|
||||
ttk.Checkbutton(parent, text="Stagger outer via ring",
|
||||
variable=self.stagger_outer_var).grid(row=row, column=0, columnspan=2, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
def create_output_params(self, parent):
|
||||
row = 0
|
||||
|
||||
# Footprint Name
|
||||
ttk.Label(parent, text="Footprint Name:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.footprint_name_entry = EntryWithPlaceholder(parent, placeholder="automatic")
|
||||
self.footprint_name_entry.grid(row=row, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
||||
row += 1
|
||||
|
||||
# KiCad layer names
|
||||
copper_layers = ['F.Cu', 'B.Cu'] + [f'In{i}.Cu' for i in range(1, 31)]
|
||||
|
||||
# Top Layer
|
||||
ttk.Label(parent, text="Top Layer:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.top_layer_var = tk.StringVar(value="F.Cu")
|
||||
top_layer_combo = ttk.Combobox(parent, textvariable=self.top_layer_var,
|
||||
values=copper_layers, state='readonly', width=23)
|
||||
top_layer_combo.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Bottom Layer
|
||||
ttk.Label(parent, text="Bottom Layer:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.bottom_layer_var = tk.StringVar(value="B.Cu")
|
||||
bottom_layer_combo = ttk.Combobox(parent, textvariable=self.bottom_layer_var,
|
||||
values=copper_layers, state='readonly', width=23)
|
||||
bottom_layer_combo.grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Circle Segments
|
||||
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).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
|
||||
|
||||
# Arc Tolerance
|
||||
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).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Keepout Zone
|
||||
ttk.Label(parent, text="Keepout Zone:").grid(row=row, column=0, sticky=tk.W, pady=5)
|
||||
self.keepout_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(parent, text="Add keepout area",
|
||||
variable=self.keepout_var).grid(row=row, column=1, sticky=tk.W, pady=5)
|
||||
row += 1
|
||||
|
||||
# Keepout Margin
|
||||
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).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):
|
||||
params = {
|
||||
'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"),
|
||||
'copper_thickness' : self.copper_thickness_var.get() / 1000.0, # µm -> mm
|
||||
'keepout_zone' : self.keepout_var.get(),
|
||||
'keepout_margin' : self.keepout_margin_var.get(),
|
||||
'via_diameter' : self.via_diameter_var.get(),
|
||||
'stagger_inner_vias' : self.stagger_inner_var.get(),
|
||||
'stagger_outer_vias' : self.stagger_outer_var.get(),
|
||||
'layer_pair' : f"{self.top_layer_var.get()},{self.bottom_layer_var.get()}",
|
||||
}
|
||||
|
||||
if (trace_width_value := self.trace_width_entry.get()):
|
||||
params['trace_width'] = float(trace_width_value)
|
||||
|
||||
if (clearance_value := self.clearance_entry.get()):
|
||||
params['clearance'] = float(clearance_value)
|
||||
|
||||
if (via_drill_value := self.via_drill_entry.get()):
|
||||
params['via_drill'] = float(via_drill_value)
|
||||
|
||||
if (via_offset_value := self.via_offset_entry.get()):
|
||||
params['via_offset'] = float(via_offset_value)
|
||||
|
||||
return params
|
||||
|
||||
def setup_logging(self):
|
||||
"""Set up logging handler to capture kicoil logger output for display in the output text widget"""
|
||||
self.kicoil_logger = logging.getLogger('kicoil')
|
||||
self.kicoil_logger.setLevel(logging.INFO)
|
||||
self.kicoil_logger.handlers.clear()
|
||||
self.log_handler = TextWidgetHandler(self.output_text)
|
||||
self.kicoil_logger.addHandler(self.log_handler)
|
||||
|
||||
@contextmanager
|
||||
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.config(state='normal')
|
||||
self.output_text.insert(tk.END, f'{message}\n', 'warning')
|
||||
self.output_text.see(tk.END)
|
||||
self.output_text.config(state='disabled')
|
||||
self.output_text.update_idletasks()
|
||||
|
||||
old_showwarning, warnings.showwarning = warnings.showwarning, show_warning
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
warnings.showwarning = old_showwarning
|
||||
|
||||
def setup_traces(self):
|
||||
for var in [
|
||||
self.turns_var,
|
||||
self.outer_dia_var,
|
||||
self.inner_dia_var,
|
||||
self.layer_mode_var,
|
||||
self.direction_var,
|
||||
self.twists_var,
|
||||
self.copper_thickness_var,
|
||||
self.keepout_var,
|
||||
self.keepout_margin_var,
|
||||
self.via_diameter_var,
|
||||
self.stagger_inner_var,
|
||||
self.stagger_outer_var,
|
||||
self.top_layer_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())
|
||||
|
||||
def _on_parameter_change(self, *args):
|
||||
# Schedule validation to avoid too many rapid calls
|
||||
if self._validation_after_id is not None:
|
||||
self.root.after_cancel(self._validation_after_id)
|
||||
self._validation_after_id = self.root.after(200, self.validate_parameters)
|
||||
|
||||
def validate_parameters(self):
|
||||
"""Validate parameters by creating PlanarInductor instance"""
|
||||
try:
|
||||
self.output_text.config(state='normal')
|
||||
self.output_text.delete('1.0', tk.END)
|
||||
|
||||
with self.capture_warnings():
|
||||
self.current_model = PlanarInductor(**self.get_parameters())
|
||||
|
||||
# If we got here, parameters are valid
|
||||
self.output_text.insert(tk.END, "Parameters valid\n", 'info')
|
||||
self.update_placeholders()
|
||||
self.update_preview()
|
||||
return True
|
||||
|
||||
except ValueError as e:
|
||||
self.output_text.insert(tk.END, f"ERROR: {e}\n", 'error')
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
self.current_model = None
|
||||
self.update_placeholders()
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
self.output_text.insert(tk.END, f"Unexpected error:\n{tb}\n", 'error')
|
||||
self.output_text.see(tk.END)
|
||||
|
||||
print(tb, file=sys.stderr)
|
||||
|
||||
self.current_model = None
|
||||
self.update_placeholders()
|
||||
return True
|
||||
|
||||
finally:
|
||||
self.output_text.config(state='disabled')
|
||||
|
||||
def update_placeholders(self):
|
||||
if self.current_model is None:
|
||||
self.trace_width_entry.update_placeholder("automatic")
|
||||
self.clearance_entry.update_placeholder("automatic")
|
||||
self.via_drill_entry.update_placeholder("automatic")
|
||||
self.via_offset_entry.update_placeholder("automatic")
|
||||
self.footprint_name_entry.update_placeholder("automatic")
|
||||
|
||||
else:
|
||||
if not self.trace_width_entry.get():
|
||||
self.trace_width_entry.update_placeholder(f"auto: {self.current_model.trace_width:.2f}")
|
||||
|
||||
if not self.clearance_entry.get():
|
||||
self.clearance_entry.update_placeholder(f"auto: {self.current_model.clearance:.2f}")
|
||||
|
||||
if not self.via_drill_entry.get():
|
||||
self.via_drill_entry.update_placeholder(f"auto: {self.current_model.via_drill:.2f}")
|
||||
|
||||
if not self.via_offset_entry.get():
|
||||
self.via_offset_entry.update_placeholder(f"auto: {self.current_model.via_offset:.2f}")
|
||||
|
||||
if not self.footprint_name_entry.get():
|
||||
self.footprint_name_entry.update_placeholder(self.current_model.default_footprint_name)
|
||||
|
||||
def update_preview(self):
|
||||
if not HAS_PREVIEW:
|
||||
return
|
||||
|
||||
arc_tolerance = self.arc_tolerance_var.get()
|
||||
circle_segments = self.circle_segments_var.get()
|
||||
|
||||
footprint = self.current_model.render_footprint(None, arc_tolerance, circle_segments)
|
||||
svg_tag = make_transparent_svg(footprint)
|
||||
viewbox = svg_tag.attrs.get('viewBox', '0 0 800 800')
|
||||
_, _, svg_width, svg_height = map(float, viewbox.split())
|
||||
min_dimension = 800
|
||||
scale = max(min_dimension / svg_width, min_dimension / svg_height)
|
||||
output_width = int(svg_width * scale)
|
||||
output_height = int(svg_height * scale)
|
||||
svg_tag.attrs['width'] = f'{output_width}px'
|
||||
svg_tag.attrs['height'] = f'{output_height}px'
|
||||
|
||||
png_data = cairosvg.svg2png(bytestring=str(svg_tag).encode('utf-8'))
|
||||
self.preview_raw_image = Image.open(BytesIO(png_data))
|
||||
self._rescale_preview()
|
||||
|
||||
|
||||
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.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.config(state='disabled')
|
||||
|
||||
def generate_footprint(self):
|
||||
"""Generate the KiCad footprint using the validated model"""
|
||||
|
||||
if not self.validate_parameters():
|
||||
messagebox.showerror("Error", "Cannot generate model. Please check the output for warnings or errors.")
|
||||
return
|
||||
|
||||
try:
|
||||
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.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 model.default_footprint_name
|
||||
output_file = filedialog.asksaveasfilename(
|
||||
title="Save KiCad Footprint",
|
||||
defaultextension=".kicad_mod",
|
||||
initialfile=f"{default_name}.kicad_mod",
|
||||
filetypes=[("KiCad Footprint", "*.kicad_mod"), ("All files", "*.*")]
|
||||
)
|
||||
|
||||
if not output_file:
|
||||
self.output_text.insert(tk.END, "\nSave cancelled.\n", 'info')
|
||||
return
|
||||
|
||||
Path(output_file).write_text(footprint.serialize())
|
||||
self.output_text.insert(tk.END, f"\nSuccess! Footprint saved to:\n {output_file}\n", 'info')
|
||||
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
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)
|
||||
|
||||
print(tb, file=sys.stderr)
|
||||
|
||||
messagebox.showerror("Error", f"Error generating footprint: {e}")
|
||||
|
||||
finally:
|
||||
self.output_text.config(state='disabled')
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = KiCoilGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,4 +1,19 @@
|
|||
|
||||
# Copyright 2025 Jan Sebastian Götte <code@jaseg.de>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import textwrap
|
||||
|
||||
from gerbonara.utils import Tag
|
||||
|
|
@ -8,13 +23,13 @@ def make_transparent_svg(footprint):
|
|||
stack = LayerStack()
|
||||
footprint.render(stack)
|
||||
root_tag = stack.to_svg(margin=5, colors={
|
||||
'top copper': 'hsl(240 90% 65%)',
|
||||
'bottom copper': 'hsl( 0 90% 65%)',
|
||||
'drill pth': 'hsl(120 60% 50%)',
|
||||
'top copper': '#eb5c5c',
|
||||
'bottom copper': '#5c5ceb',
|
||||
'drill pth': '#52cc52',
|
||||
})
|
||||
|
||||
root_tag.children[0].attrs['opacity'] = '98%'
|
||||
root_tag.children[1].attrs['opacity'] = '98%'
|
||||
root_tag.children[0].attrs['opacity'] = '0.7'
|
||||
root_tag.children[1].attrs['opacity'] = '0.7'
|
||||
root_tag.children[1].attrs['style'] = 'mix-blend-mode: multiply'
|
||||
return root_tag
|
||||
|
||||
|
|
|
|||
195
uv.lock
generated
195
uv.lock
generated
|
|
@ -20,6 +20,79 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairocffi"
|
||||
version = "1.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairosvg"
|
||||
version = "2.8.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cairocffi" },
|
||||
{ name = "cssselect2" },
|
||||
{ name = "defusedxml" },
|
||||
{ name = "pillow" },
|
||||
{ name = "tinycss2" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/5106168bd43d7cd8b7cc2a2ee465b385f14b63f4c092bb89eee2d48c8e67/cairosvg-2.8.2.tar.gz", hash = "sha256:07cbf4e86317b27a92318a4cac2a4bb37a5e9c1b8a27355d06874b22f85bef9f", size = 8398590, upload-time = "2025-05-15T06:56:32.653Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/48/816bd4aaae93dbf9e408c58598bc32f4a8c65f4b86ab560864cb3ee60adb/cairosvg-2.8.2-py3-none-any.whl", hash = "sha256:eab46dad4674f33267a671dce39b64be245911c901c70d65d2b7b0821e852bf5", size = 45773, upload-time = "2025-05-15T06:56:28.552Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
|
|
@ -41,6 +114,28 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssselect2"
|
||||
version = "0.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tinycss2" },
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.2"
|
||||
|
|
@ -157,12 +252,24 @@ dependencies = [
|
|||
{ name = "gerbonara" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
gui = [
|
||||
{ name = "cairosvg" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click" },
|
||||
{ name = "gerbonara", specifier = ">=1.6.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
gui = [
|
||||
{ name = "cairosvg" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
|
|
@ -215,6 +322,64 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "priority"
|
||||
version = "2.0.0"
|
||||
|
|
@ -224,6 +389,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946, upload-time = "2021-06-27T10:15:03.856Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quart"
|
||||
version = "0.20.0"
|
||||
|
|
@ -260,6 +434,27 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253, upload-time = "2025-08-13T19:32:00.296Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinycss2"
|
||||
version = "1.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.4"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue