Compare commits
27 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
736107f7a4 | ||
|
|
e3674de08d | ||
|
|
516a9d337f | ||
|
|
2451b517e8 | ||
|
|
bdd4008ab9 | ||
|
|
6f006e2782 | ||
|
|
8df709f55f | ||
|
|
985e263cfe | ||
|
|
5ccfd7a259 | ||
|
|
bcc4aeefa7 | ||
|
|
0c15111463 | ||
|
|
1a0f519720 | ||
|
|
3d0ddc3dc8 | ||
|
|
575046a60c | ||
|
|
8de776616c | ||
|
|
e19dec20b6 | ||
|
|
97513df5d9 | ||
|
|
2ce55ebdca | ||
|
|
f3c95a42d4 | ||
|
|
8a2599f5f4 | ||
|
|
2647709215 | ||
|
|
87413855bf | ||
|
|
af4fb2668f | ||
|
|
a47a694ba0 | ||
|
|
dd8ad98f13 | ||
|
|
a1ea416269 | ||
|
|
a7f0324506 |
48 changed files with 56264 additions and 210 deletions
|
|
@ -14,8 +14,7 @@ build:archlinux:
|
|||
GIT_SUBMODULE_STRATEGY: none
|
||||
script:
|
||||
- git config --global --add safe.directory "$CI_PROJECT_DIR"
|
||||
- pip3 install --user --break-system-packages uv
|
||||
- ~/.local/bin/uv build
|
||||
- uv build
|
||||
artifacts:
|
||||
name: "gerbolyze-$CI_COMMIT_REF_NAME-gerbonara"
|
||||
paths:
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ layers, or whole board stacks (:py:class:`~.layers.LayerStack`) to SVG.
|
|||
|
||||
``gerbonara render`` renders one or more Gerber or Excellon files as a single SVG file. It can read single files,
|
||||
directorys of files, and ZIP files. To read directories or zips, it applies gerbonara's layer filename matching rules.
|
||||
These built-in rules should work with common settings in a wide variety of CAD tools.
|
||||
|
||||
.. option:: --warnings [default|ignore|once]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "gerbonara"
|
||||
version = "1.6.0"
|
||||
version = "1.6.2"
|
||||
description = "Tools to handle Gerber and Excellon files in Python"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -43,6 +43,7 @@ Tracker = "https://gitlab.com/gerbolyze/gerbonara/issues"
|
|||
|
||||
[project.scripts]
|
||||
gerbonara = "gerbonara.cli:cli"
|
||||
protoserve = "gerbonara.cad.protoserve:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
from .parse import ApertureMacro, GenericMacros
|
||||
from .expression import (Expression,
|
||||
UnitExpression,
|
||||
ConstantExpression,
|
||||
VariableExpression,
|
||||
ParameterExpression,
|
||||
NegatedExpression,
|
||||
OperatorExpression)
|
||||
from .primitive import (Comment,
|
||||
Circle,
|
||||
VectorLine,
|
||||
CenterLine,
|
||||
Outline,
|
||||
Polygon,
|
||||
Moire,
|
||||
Thermal)
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ class Expression:
|
|||
return expr(other) / self
|
||||
|
||||
def __neg__(self):
|
||||
return NegatedExpression(self)
|
||||
return NegatedExpression(self).optimized()
|
||||
|
||||
def __pos__(self):
|
||||
return self
|
||||
|
|
@ -339,6 +339,12 @@ class OperatorExpression(Expression):
|
|||
# -x [*/] -y == x [*/] y
|
||||
case (NegatedExpression(l), (operator.truediv | operator.mul) as op, NegatedExpression(r)):
|
||||
rv = op(l, r)
|
||||
# -x [*/] y == -(x [*/] y)
|
||||
case (NegatedExpression(l), (operator.truediv | operator.mul) as op, r):
|
||||
rv = NegatedExpression(op(l, r))
|
||||
# x [*/] -y == -(x [*/] y)
|
||||
case (l, (operator.truediv | operator.mul) as op, NegatedExpression(r)):
|
||||
rv = NegatedExpression(op(l, r))
|
||||
# x + -y == x - y
|
||||
case (l, operator.add, NegatedExpression(r)):
|
||||
rv = l-r
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
# Copyright 2021 Jan Sebastian Götte <gerbonara@jaseg.de>
|
||||
|
||||
from dataclasses import dataclass, field, replace
|
||||
from dataclasses import dataclass, field, replace, fields
|
||||
import operator
|
||||
import re
|
||||
import ast
|
||||
|
|
@ -13,6 +13,7 @@ import math
|
|||
|
||||
from . import primitive as ap
|
||||
from .expression import *
|
||||
from ..apertures import ApertureMacroInstance
|
||||
from ..utils import MM
|
||||
|
||||
# we make our own here instead of using math.degrees to make sure this works with expressions, too.
|
||||
|
|
@ -57,10 +58,74 @@ def _parse_expression(expr, variables, parameters):
|
|||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApertureMacro:
|
||||
""" Definition of an aperture macro in a Gerber file.
|
||||
|
||||
An aperture macro is a collection of shape primitives that are flashed all at once. The properties of these
|
||||
primitives such as their relative position and size can be given explicitly, or can be given as a basic
|
||||
arithmetic expression (so +/-/*/:, no higher functions) based on parameters. After the macro is defined in the
|
||||
Gerber file, it is *bound* to a particular set of parameter values in an aperture definition. One macro can be
|
||||
used by zero, or by multiple aperture definitions. To flash a macro, you must first bind it in an aperture
|
||||
definition, which can then be flash'ed.
|
||||
|
||||
Gerbonara calls these apertures that bind a macro :py:class:`~..apertures.ApertureMacroInst`. You can bind a
|
||||
macro to a set of parameters by calling it:
|
||||
|
||||
.. code-block: python
|
||||
|
||||
# am is some instance of ApertureMacro
|
||||
aperture_def = am(1, 2, 3)
|
||||
gerber.objects.append(Flash(x=12, y=34, aperture=aperture_def))
|
||||
|
||||
Internally, the aperture macro API uses millimeters though most functions allow you to pass an unit parameter.
|
||||
|
||||
When you want to programmatically create aperture macros, we recommend using :py:meth:`~.ApertureMacro.map` on a
|
||||
dataclass-like class definition. Have a look at this code from :py:class:`~.GenericMacros`:
|
||||
|
||||
.. code-block: python
|
||||
|
||||
@ApertureMacro.map('GNR')
|
||||
class rect:
|
||||
w: float # width
|
||||
h: float # height
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
def draw(self):
|
||||
yield ap.CenterLine('mm', 1, self.w, self.h, 0, 0, self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
# rect now is an instance of ApertureMacro
|
||||
|
||||
After this, you can bind this macro to an aperture by calling it. When you use this dataclass-like syntax,
|
||||
keyword arguments are supported, and default values work like with normal dataclasses:
|
||||
|
||||
.. code-block: python
|
||||
|
||||
# returns an instance of ApertureMacroInstance containing the given parameters
|
||||
my_rect = GenericMacros.rect(w=12, h=34)
|
||||
|
||||
gerber.objects.append(Flash(x=12, y=34, aperture=my_rect))
|
||||
|
||||
.. important::
|
||||
Use your own programmatically defined aperture macros sparingly. While support is getting better, many
|
||||
tools, including the expensive, commercial tools that PCB manufacturers use, still have bugs when handling
|
||||
aperture macros. When using advanced macros with many primitives or with complex, embedded arithmetic
|
||||
expressions, make sure to carefully check the manufacturing files provided by your PCB fab.
|
||||
|
||||
gerbonara currently handles embedded arithmetic expressions by *always* calculating them out since we have
|
||||
recently seen high-end commercial tooling failing at issues as basic as operator precedence. This increases
|
||||
file sizes very very slightly, but it makes sure that you get correct results.
|
||||
|
||||
This means that you can use gerbonara to calculate out aperture macros and hard-bake their values into the
|
||||
gerber source. This can be useful if you have a file that includes complex macros that some manufacturer's
|
||||
tooling can't handle on its own.
|
||||
"""
|
||||
|
||||
name: str = field(default=None, hash=False, compare=False)
|
||||
num_parameters: int = 0
|
||||
primitives: tuple = ()
|
||||
comments: tuple = field(default=(), hash=False, compare=False)
|
||||
_param_dataclass: object = field(default=None, hash=False, compare=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.name is None or re.match(r'GNX[0-9A-F]{16}', self.name):
|
||||
|
|
@ -70,6 +135,38 @@ class ApertureMacro:
|
|||
def _reset_name(self):
|
||||
object.__setattr__(self, 'name', f'GNX{hash(self)&0xffffffffffffffff:016X}')
|
||||
|
||||
@classmethod
|
||||
def map(our_kls, macro_name=None):
|
||||
def wrapper(kls):
|
||||
nonlocal our_kls, macro_name
|
||||
dc = dataclass(kls)
|
||||
|
||||
# Construct a mock instance of the dataclass with every field bound to its correpsonding ParameterExpression,
|
||||
# then draw() it to get a list of bound macro primitives.
|
||||
primitives = tuple(dc(*[ParameterExpression(i+1) for i in range(len(fields(dc)))]).draw())
|
||||
name = macro_name if macro_name else f'GNM{kls.__name__}'
|
||||
|
||||
# Python allows a lot more unicode in class names than the Gerber spec allows in aperture macro names
|
||||
if not re.fullmatch('[._$a-zA-Z][._$a-zA-Z0-9]{0,126}', name):
|
||||
raise ValueError(f'Name {name!r} is invalid as an aperture macro name')
|
||||
|
||||
return our_kls(
|
||||
name = name,
|
||||
num_parameters = len(fields(dc)),
|
||||
primitives = primitives,
|
||||
comments = [l.strip() for l in dc.__doc__.strip().splitlines()],
|
||||
_param_dataclass = dc)
|
||||
return wrapper
|
||||
|
||||
def __call__(self, *args, unit=MM, **kwargs):
|
||||
if self._param_dataclass:
|
||||
# Above, in map(), we construct the dataclass with the ParameterExpression(i) as params to draw the macro
|
||||
# primitives. Here, we construct it with the user's supplied concrete numeric parameters instead, and then
|
||||
# extract a list of these parameters. This should work great as long as the user doesn't get too fancy with
|
||||
# dataclass metaprogramming hackery.
|
||||
bound = self._param_dataclass(*args, **kwargs)
|
||||
return ApertureMacroInstance(macro=self, parameters=tuple(getattr(bound, f.name) or 0 for f in fields(bound)), unit=unit)
|
||||
|
||||
@classmethod
|
||||
def parse_macro(kls, macro_name, body, unit):
|
||||
comments = []
|
||||
|
|
@ -168,82 +265,191 @@ var = ParameterExpression
|
|||
deg_per_rad = 180 / math.pi
|
||||
|
||||
class GenericMacros:
|
||||
"""NOTE:
|
||||
All generic macros have rotation values specified in **clockwise radians** like the rest of the user-facing API.
|
||||
"""
|
||||
|
||||
_generic_hole = lambda n: (ap.Circle('mm', 0, var(n), 0, 0),)
|
||||
@ApertureMacro.map('GNC')
|
||||
class circle:
|
||||
""" Filled circle macro with an optional round hole
|
||||
|
||||
:param float diameter: Diameter of the circle
|
||||
:param hole_dia: Diameter of the hole (optional)
|
||||
"""
|
||||
diameter: float
|
||||
hole_dia: float = 0
|
||||
|
||||
# NOTE: All generic macros have rotation values specified in **clockwise radians** like the rest of the user-facing
|
||||
# API.
|
||||
circle = ApertureMacro('GNC', 4, (
|
||||
ap.Circle('mm', 1, var(1), 0, 0, var(4) * -deg_per_rad),
|
||||
*_generic_hole(2)))
|
||||
def draw(self):
|
||||
yield ap.Circle('mm', 1, self.diameter, 0, 0)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
rect = ApertureMacro('GNR', 5, (
|
||||
ap.CenterLine('mm', 1, var(1), var(2), 0, 0, var(5) * -deg_per_rad),
|
||||
*_generic_hole(3)))
|
||||
@ApertureMacro.map('GNR')
|
||||
class rect:
|
||||
""" Axis-aligned rectangle with an optional round center hole.
|
||||
|
||||
# params: width, height, corner radius, *hole, rotation
|
||||
rounded_rect = ApertureMacro('GRR', 6, (
|
||||
ap.CenterLine('mm', 1, var(1)-2*var(3), var(2), 0, 0, var(6) * -deg_per_rad),
|
||||
ap.CenterLine('mm', 1, var(1), var(2)-2*var(3), 0, 0, var(6) * -deg_per_rad),
|
||||
ap.Circle('mm', 1, var(3)*2, +(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad),
|
||||
ap.Circle('mm', 1, var(3)*2, +(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad),
|
||||
ap.Circle('mm', 1, var(3)*2, -(var(1)/2-var(3)), +(var(2)/2-var(3)), var(6) * -deg_per_rad),
|
||||
ap.Circle('mm', 1, var(3)*2, -(var(1)/2-var(3)), -(var(2)/2-var(3)), var(6) * -deg_per_rad),
|
||||
*_generic_hole(4)))
|
||||
:param float w: Width
|
||||
:param float h: Height
|
||||
:param float hole_dia: Diameter of the round hole (optional)
|
||||
:param float rotation: Rotation in clockwise radians (optional)
|
||||
"""
|
||||
w: float # width
|
||||
h: float # height
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
# params: width, height, length difference between narrow side (top) and wide side (bottom), *hole, rotation
|
||||
isosceles_trapezoid = ApertureMacro('GTR', 6, (
|
||||
ap.Outline('mm', 1, 4,
|
||||
(var(1)/-2, var(2)/-2,
|
||||
var(1)/-2+var(3)/2, var(2)/2,
|
||||
var(1)/2-var(3)/2, var(2)/2,
|
||||
var(1)/2, var(2)/-2,
|
||||
var(1)/-2, var(2)/-2,),
|
||||
var(6) * -deg_per_rad),
|
||||
*_generic_hole(4)))
|
||||
def draw(self):
|
||||
yield ap.CenterLine('mm', 1, self.w, self.h, 0, 0, self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
# params: width, height, length difference between narrow side (top) and wide side (bottom), margin, *hole, rotation
|
||||
rounded_isosceles_trapezoid = ApertureMacro('GRTR', 7, (
|
||||
ap.Outline('mm', 1, 4,
|
||||
(var(1)/-2, var(2)/-2,
|
||||
var(1)/-2+var(3)/2, var(2)/2,
|
||||
var(1)/2-var(3)/2, var(2)/2,
|
||||
var(1)/2, var(2)/-2,
|
||||
var(1)/-2, var(2)/-2,),
|
||||
var(7) * -deg_per_rad),
|
||||
ap.VectorLine('mm', 1, var(4)*2,
|
||||
var(1)/-2, var(2)/-2,
|
||||
var(1)/-2+var(3)/2, var(2)/2,),
|
||||
ap.VectorLine('mm', 1, var(4)*2,
|
||||
var(1)/-2+var(3)/2, var(2)/2,
|
||||
var(1)/2-var(3)/2, var(2)/2,),
|
||||
ap.VectorLine('mm', 1, var(4)*2,
|
||||
var(1)/2-var(3)/2, var(2)/2,
|
||||
var(1)/2, var(2)/-2,),
|
||||
ap.VectorLine('mm', 1, var(4)*2,
|
||||
var(1)/2, var(2)/-2,
|
||||
var(1)/-2, var(2)/-2,),
|
||||
ap.Circle('mm', 1, var(4)*2,
|
||||
var(1)/-2, var(2)/-2,),
|
||||
ap.Circle('mm', 1, var(4)*2,
|
||||
var(1)/-2+var(3)/2, var(2)/2,),
|
||||
ap.Circle('mm', 1, var(4)*2,
|
||||
var(1)/2-var(3)/2, var(2)/2,),
|
||||
ap.Circle('mm', 1, var(4)*2,
|
||||
var(1)/2, var(2)/-2,),
|
||||
*_generic_hole(5)))
|
||||
@ApertureMacro.map('GRR')
|
||||
class rounded_rect:
|
||||
""" Rectangle with circular arc corners and an optional round center hole.
|
||||
|
||||
# w must be larger than h
|
||||
# params: width, height, *hole, rotation
|
||||
obround = ApertureMacro('GNO', 5, (
|
||||
ap.CenterLine('mm', 1, var(1)-var(2), var(2), 0, 0, var(5) * -deg_per_rad),
|
||||
ap.Circle('mm', 1, var(2), +(var(1)-var(2))/2, 0, var(5) * -deg_per_rad),
|
||||
ap.Circle('mm', 1, var(2), -(var(1)-var(2))/2, 0, var(5) * -deg_per_rad),
|
||||
*_generic_hole(3) ))
|
||||
:param float w: Width
|
||||
:param float h: Height
|
||||
:param float r: Corner radius
|
||||
:param float hole_dia: Diameter of the round hole (optional)
|
||||
:param float rotation: Rotation in clockwise radians (optional)
|
||||
"""
|
||||
w: float # width
|
||||
h: float # height
|
||||
r: float # Corner radius
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
polygon = ApertureMacro('GNP', 4, (
|
||||
ap.Polygon('mm', 1, var(2), 0, 0, var(1), var(3) * -deg_per_rad),
|
||||
ap.Circle('mm', 0, var(4), 0, 0)))
|
||||
def draw(self):
|
||||
yield ap.CenterLine('mm', 1, self.w-2*self.r, self.h, 0, 0, self.rotation * -deg_per_rad)
|
||||
yield ap.CenterLine('mm', 1, self.w, self.h-2*self.r, 0, 0, self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 1, self.r*2, +(self.w/2-self.r), +(self.h/2-self.r), self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 1, self.r*2, +(self.w/2-self.r), -(self.h/2-self.r), self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 1, self.r*2, -(self.w/2-self.r), +(self.h/2-self.r), self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 1, self.r*2, -(self.w/2-self.r), -(self.h/2-self.r), self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
@ApertureMacro.map('GTR')
|
||||
class isosceles_trapezoid:
|
||||
""" Isosceles trapezoid with a wider bottom edge and narrower top edge, with an optional round center hole.
|
||||
|
||||
:param float w: Width of the bottom (wider) edge
|
||||
:param float h: Height
|
||||
:param float d: Length difference between bottom and top edges; top width = w - d
|
||||
:param float hole_dia: Diameter of the round hole (optional)
|
||||
:param float rotation: Rotation in clockwise radians (optional)
|
||||
"""
|
||||
w: float # width
|
||||
h: float # height
|
||||
d: float # length difference between narrow side (top) and wide side (bottom)
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
def draw(self):
|
||||
yield ap.Outline('mm', 1, 4,
|
||||
(self.w/-2, self.h/-2,
|
||||
self.w/-2+self.d/2, self.h/2,
|
||||
self.w/2-self.d/2, self.h/2,
|
||||
self.w/2, self.h/-2,
|
||||
self.w/-2, self.h/-2,),
|
||||
self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
@ApertureMacro.map('GRTR')
|
||||
class rounded_isosceles_trapezoid:
|
||||
""" Isosceles trapezoid with rounded corners and an optional round center hole. Unlike the rounded rectangle, the shape is defined by first defining a non-rounded trapezoid, which is then offet to the outside by the given margin.
|
||||
|
||||
:param float w: Width of the bottom (wider) edge
|
||||
:param float h: Height
|
||||
:param float d: Length difference between bottom and top edges; top width = w - d
|
||||
:param float margin: Corner rounding radius
|
||||
:param float hole_dia: Diameter of the round hole (optional)
|
||||
:param float rotation: Rotation in clockwise radians (optional)
|
||||
"""
|
||||
w: float
|
||||
h: float
|
||||
d: float # length difference between narrow side (top) and wide side (bottom)
|
||||
margin: float
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
def draw(self):
|
||||
rot = self.rotation * -deg_per_rad
|
||||
yield ap.Outline('mm', 1, 4,
|
||||
(self.w/-2, self.h/-2,
|
||||
self.w/-2+self.d/2, self.h/2,
|
||||
self.w/2-self.d/2, self.h/2,
|
||||
self.w/2, self.h/-2,
|
||||
self.w/-2, self.h/-2,),
|
||||
rot)
|
||||
|
||||
yield ap.VectorLine('mm', 1, self.margin*2,
|
||||
self.w/-2, self.h/-2,
|
||||
self.w/-2+self.d/2, self.h/2,
|
||||
rot)
|
||||
yield ap.VectorLine('mm', 1, self.margin*2,
|
||||
self.w/-2+self.d/2, self.h/2,
|
||||
self.w/2-self.d/2, self.h/2,
|
||||
rot)
|
||||
yield ap.VectorLine('mm', 1, self.margin*2,
|
||||
self.w/2-self.d/2, self.h/2,
|
||||
self.w/2, self.h/-2,
|
||||
rot)
|
||||
yield ap.VectorLine('mm', 1, self.margin*2,
|
||||
self.w/2, self.h/-2,
|
||||
self.w/-2, self.h/-2,
|
||||
rot)
|
||||
|
||||
yield ap.Circle('mm', 1, self.margin*2,
|
||||
self.w/-2, self.h/-2,
|
||||
rot)
|
||||
yield ap.Circle('mm', 1, self.margin*2,
|
||||
self.w/-2+self.d/2, self.h/2,
|
||||
rot)
|
||||
yield ap.Circle('mm', 1, self.margin*2,
|
||||
self.w/2-self.d/2, self.h/2,
|
||||
rot)
|
||||
yield ap.Circle('mm', 1, self.margin*2,
|
||||
self.w/2, self.h/-2,
|
||||
rot)
|
||||
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
@ApertureMacro.map('GNO')
|
||||
class obround:
|
||||
""" Rectangle with semicircular end caps (stadium shape), with an optional round center hole. The long axis is along the X axis when rotation is zero.
|
||||
|
||||
:param float w: Total width including end caps; must satisfy w >= h
|
||||
:param float h: Height, equal to the end cap diameter
|
||||
:param float hole_dia: Diameter of the round hole (optional)
|
||||
:param float rotation: Rotation in clockwise radians (optional)
|
||||
"""
|
||||
w: float
|
||||
h: float
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
def draw(self):
|
||||
rot = self.rotation * -deg_per_rad
|
||||
yield ap.CenterLine('mm', 1, self.w - self.h, self.h, 0, 0, rot)
|
||||
yield ap.Circle('mm', 1, self.h, +(self.w-self.h)/2, 0, rot)
|
||||
yield ap.Circle('mm', 1, self.h, -(self.w-self.h)/2, 0, rot)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
@ApertureMacro.map('GNP')
|
||||
class polygon:
|
||||
""" Regular n-sided polygon with an optional round center hole.
|
||||
|
||||
:param int n: Number of sides
|
||||
:param float diameter: Diameter of the circumscribed circle
|
||||
:param float hole_dia: Diameter of the round hole (optional)
|
||||
:param float rotation: Rotation in clockwise radians (optional)
|
||||
"""
|
||||
n: int
|
||||
diameter: float
|
||||
hole_dia: float = 0
|
||||
rotation: float = 0
|
||||
|
||||
def draw(self):
|
||||
yield ap.Polygon('mm', 1, self.diameter, 0, 0, self.n, self.rotation * -deg_per_rad)
|
||||
yield ap.Circle('mm', 0, self.hole_dia, 0, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ class Circle(Primitive):
|
|||
with self.Calculator(self, variable_binding, unit) as calc:
|
||||
x, y = rotate_point(calc.x, calc.y, -(deg_to_rad(calc.rotation) + rotation), 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
|
||||
if math.isclose(calc.diameter, 0):
|
||||
return []
|
||||
|
||||
return [ gp.Circle(x, y, calc.diameter/2, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
|
||||
|
||||
def substitute_params(self, binding, unit):
|
||||
|
|
@ -144,6 +148,9 @@ class VectorLine(Primitive):
|
|||
center_x, center_y = center_x+offset[0], center_y+offset[1]
|
||||
rotation += deg_to_rad(calc.rotation) + math.atan2(delta_y, delta_x)
|
||||
|
||||
if math.isclose(calc.width, 0):
|
||||
return []
|
||||
|
||||
return [ gp.Rectangle(center_x, center_y, length, calc.width, rotation=rotation,
|
||||
polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
|
||||
|
||||
|
|
@ -182,6 +189,9 @@ class CenterLine(Primitive):
|
|||
x, y = x+offset[0], y+offset[1]
|
||||
w, h = calc.width, calc.height
|
||||
|
||||
if math.isclose(calc.width, 0) or math.isclose(calc.height, 0):
|
||||
return []
|
||||
|
||||
return [ gp.Rectangle(x, y, w, h, rotation, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
|
||||
|
||||
def substitute_params(self, binding, unit):
|
||||
|
|
@ -217,7 +227,8 @@ class Polygon(Primitive):
|
|||
rotation += deg_to_rad(calc.rotation)
|
||||
x, y = rotate_point(calc.x, calc.y, -rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
return [ gp.ArcPoly.from_regular_polygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation,
|
||||
print('xy', calc.x, calc.y)
|
||||
return [ gp.ArcPoly.from_regular_polygon(x, y, calc.diameter/2, int(calc.n_vertices), rotation,
|
||||
polarity_dark=(bool(calc.exposure) == polarity_dark)) ]
|
||||
|
||||
def dilated(self, offset, unit):
|
||||
|
|
@ -251,6 +262,9 @@ class Moire(Primitive):
|
|||
x, y = rotate_point(calc.x, calc.y, -rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
|
||||
if math.isclose(calc.d_outer, 0):
|
||||
return []
|
||||
|
||||
pitch = calc.line_thickness + calc.gap_w
|
||||
for i in range(int(round(calc.num_circles))):
|
||||
yield gp.Circle(x, y, calc.d_outer/2 - i*pitch, polarity_dark=True)
|
||||
|
|
@ -280,7 +294,7 @@ class Moire(Primitive):
|
|||
@dataclass(frozen=True, slots=True)
|
||||
class Thermal(Primitive):
|
||||
code = 7
|
||||
exposure : Expression
|
||||
# Note: Thermal primitives according to spec don't have an exposure variable
|
||||
# center x/y
|
||||
x : UnitExpression
|
||||
y : UnitExpression
|
||||
|
|
@ -295,13 +309,16 @@ class Thermal(Primitive):
|
|||
x, y = rotate_point(calc.x, calc.y, -rotation, 0, 0)
|
||||
x, y = x+offset[0], y+offset[1]
|
||||
|
||||
dark = (bool(calc.exposure) == polarity_dark)
|
||||
dark = True
|
||||
|
||||
if math.isclose(calc.d_outer, 0):
|
||||
return []
|
||||
|
||||
return [
|
||||
gp.Circle(x, y, calc.d_outer/2, polarity_dark=dark),
|
||||
gp.Circle(x, y, calc.d_inner/2, polarity_dark=not dark),
|
||||
gp.Rectangle(x, y, d_outer, gap_w, rotation=rotation, polarity_dark=not dark),
|
||||
gp.Rectangle(x, y, gap_w, d_outer, rotation=rotation, polarity_dark=not dark),
|
||||
gp.Rectangle(x, y, calc.d_outer, calc.gap_w, rotation=rotation, polarity_dark=not dark),
|
||||
gp.Rectangle(x, y, calc.gap_w, calc.d_outer, rotation=rotation, polarity_dark=not dark),
|
||||
]
|
||||
|
||||
def dilate(self, offset, unit):
|
||||
|
|
@ -383,6 +400,10 @@ class Outline(Primitive):
|
|||
bound_coords = [ rotate_point(calc(x), calc(y), -rotation, 0, 0) for x, y in self.points ]
|
||||
bound_coords = [ (x+offset[0], y+offset[1]) for x, y in bound_coords ]
|
||||
bound_radii = [None] * len(bound_coords)
|
||||
|
||||
if len(bound_coords) < 3:
|
||||
return []
|
||||
|
||||
return [gp.ArcPoly(bound_coords, bound_radii, polarity_dark=(bool(calc.exposure) == polarity_dark))]
|
||||
|
||||
def dilated(self, offset, unit):
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import math
|
|||
from dataclasses import dataclass, replace, field, fields, InitVar, KW_ONLY
|
||||
from functools import lru_cache
|
||||
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
from .utils import LengthUnit, MM, Inch, sum_bounds
|
||||
|
||||
from . import graphic_primitives as gp
|
||||
|
|
@ -160,7 +159,8 @@ class ExcellonTool(Aperture):
|
|||
return self
|
||||
|
||||
def to_macro(self, rotation=0):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM), unit=MM)
|
||||
|
||||
def _params(self, unit=None):
|
||||
return (self.unit.convert_to(unit, self.diameter),)
|
||||
|
|
@ -205,7 +205,9 @@ class CircleAperture(Aperture):
|
|||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self, rotation=0):
|
||||
return ApertureMacroInstance(GenericMacros.circle, self._params(unit=MM))
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
return GenericMacros.circle(MM(self.diameter, self.unit),
|
||||
MM(self.hole_dia, self.unit))
|
||||
|
||||
def _params(self, unit=None):
|
||||
return _strip_right(
|
||||
|
|
@ -260,12 +262,11 @@ class RectangleAperture(Aperture):
|
|||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self, rotation=0):
|
||||
return ApertureMacroInstance(GenericMacros.rect,
|
||||
(MM(self.w, self.unit),
|
||||
MM(self.h, self.unit),
|
||||
MM(self.hole_dia, self.unit) or 0,
|
||||
0,
|
||||
rotation))
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
return GenericMacros.rect(MM(self.w, self.unit),
|
||||
MM(self.h, self.unit),
|
||||
MM(self.hole_dia, self.unit),
|
||||
rotation)
|
||||
|
||||
def _params(self, unit=None):
|
||||
return _strip_right(
|
||||
|
|
@ -329,12 +330,11 @@ class ObroundAperture(Aperture):
|
|||
rotation -= -math.pi/2
|
||||
inst = replace(self, w=self.h, h=self.w, hole_dia=self.hole_dia)
|
||||
|
||||
return ApertureMacroInstance(GenericMacros.obround,
|
||||
(MM(inst.w, self.unit),
|
||||
MM(inst.h, self.unit),
|
||||
MM(inst.hole_dia, self.unit) or 0,
|
||||
0,
|
||||
rotation))
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
return GenericMacros.obround(MM(inst.w, self.unit),
|
||||
MM(inst.h, self.unit),
|
||||
MM(inst.hole_dia, self.unit) or 0,
|
||||
rotation)
|
||||
|
||||
def _params(self, unit=None):
|
||||
return _strip_right(
|
||||
|
|
@ -390,12 +390,18 @@ class PolygonAperture(Aperture):
|
|||
hole_dia=None if self.hole_dia is None else self.hole_dia*scale)
|
||||
|
||||
def to_macro(self):
|
||||
return ApertureMacroInstance(GenericMacros.polygon, self._params(MM))
|
||||
from .aperture_macros.parse import GenericMacros
|
||||
return GenericMacros.polygon(self.n_vertices,
|
||||
MM(self.diameter, self.unit),
|
||||
MM(self.hole_dia, self.unit),
|
||||
self.rotation)
|
||||
|
||||
def _params(self, unit=None):
|
||||
rotation = self.rotation % (2*math.pi / self.n_vertices)
|
||||
if math.isclose(rotation, 0, abs_tol=1e-6):
|
||||
rotation = None
|
||||
else:
|
||||
rotation = math.degrees(rotation)
|
||||
|
||||
if self.hole_dia is not None:
|
||||
return self.unit.convert_to(unit, self.diameter), self.n_vertices, rotation, self.unit.convert_to(unit, self.hole_dia)
|
||||
|
|
|
|||
|
|
@ -423,11 +423,11 @@ class Pad(NetMixin):
|
|||
|
||||
elif self.shape == Atom.rect:
|
||||
if margin > 0:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
|
||||
(self.size.x+2*margin, self.size.y+2*margin,
|
||||
margin,
|
||||
0, 0, # no hole
|
||||
rotation), unit=MM)
|
||||
return GenericMacros.rounded_rect(self.size.x+2*margin,
|
||||
self.size.y+2*margin,
|
||||
margin,
|
||||
0, # no hole
|
||||
rotation)
|
||||
else:
|
||||
return ap.RectangleAperture(self.size.x+2*margin, self.size.y+2*margin, unit=MM).rotated(-rotation)
|
||||
|
||||
|
|
@ -454,28 +454,29 @@ class Pad(NetMixin):
|
|||
# Note: KiCad already uses MM units, so no conversion needed here.
|
||||
|
||||
alpha = math.atan(y / dy) if dy > 0 else 0
|
||||
return ap.ApertureMacroInstance(GenericMacros.isosceles_trapezoid,
|
||||
(x+dy+2*margin*math.cos(alpha), y+2*margin,
|
||||
2*dy,
|
||||
0, 0, # no hole
|
||||
-rotation + math.pi), unit=MM)
|
||||
return GenericMacros.isosceles_trapezoid(x+dy+2*margin*math.cos(alpha),
|
||||
y+2*margin,
|
||||
2*dy,
|
||||
0, # no hole
|
||||
-rotation + math.pi)
|
||||
|
||||
else:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_isosceles_trapezoid,
|
||||
(x+dy, y,
|
||||
2*dy, margin,
|
||||
0, 0, # no hole
|
||||
-rotation + math.pi), unit=MM)
|
||||
return GenericMacros.rounded_isosceles_trapezoid(x+dy,
|
||||
y,
|
||||
2*dy,
|
||||
margin,
|
||||
0, # no hole
|
||||
-rotation + math.pi)
|
||||
|
||||
elif self.shape == Atom.roundrect:
|
||||
x, y = self.size.x, self.size.y
|
||||
r = min(x, y) * self.roundrect_rratio
|
||||
if margin > -r:
|
||||
return ap.ApertureMacroInstance(GenericMacros.rounded_rect,
|
||||
(x+2*margin, y+2*margin,
|
||||
r+margin,
|
||||
0, 0, # no hole
|
||||
rotation), unit=MM)
|
||||
return GenericMacros.rounded_rect(x+2*margin,
|
||||
y+2*margin,
|
||||
r+margin,
|
||||
0, # no hole
|
||||
rotation)
|
||||
else:
|
||||
return ap.RectangleAperture(x+margin, y+margin, unit=MM).rotated(-rotation)
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,10 @@ async def gerbers():
|
|||
board.layer_stack().save_to_zipfile(f)
|
||||
return Response(f.read_bytes(), mimetype='image/svg+xml')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main():
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import shutil
|
|||
from pathlib import Path
|
||||
from functools import cached_property
|
||||
|
||||
from .utils import LengthUnit, MM, Inch, Tag, sum_bounds, setup_svg
|
||||
from .utils import LengthUnit, MM, Inch, Tag, sum_bounds, setup_svg, convex_hull
|
||||
from . import graphic_primitives as gp
|
||||
from . import graphic_objects as go
|
||||
|
||||
|
|
@ -351,6 +351,24 @@ class CamFile:
|
|||
|
||||
return sum_bounds(( p.bounding_box(unit) for p in self.objects ), default=default)
|
||||
|
||||
def convex_hull(self, tol=0.01, unit=None):
|
||||
unit = unit or self.unit
|
||||
points = []
|
||||
|
||||
for obj in self.objects:
|
||||
if isinstance(obj, go.Line):
|
||||
line = obj.as_primitive(unit)
|
||||
points.append((line.x1, line.y1))
|
||||
points.append((line.x2, line.y2))
|
||||
|
||||
elif isinstance(obj, go.Arc):
|
||||
for obj in obj.approximate(tol, unit):
|
||||
line = obj.as_primitive(unit)
|
||||
points.append((line.x1, line.y1))
|
||||
points.append((line.x2, line.y2))
|
||||
|
||||
return convex_hull(points)
|
||||
|
||||
def to_excellon(self):
|
||||
""" Convert to a :py:class:`.ExcellonFile`. Returns ``self`` if it already is one. """
|
||||
raise NotImplementedError()
|
||||
|
|
|
|||
|
|
@ -30,9 +30,10 @@ from pathlib import Path
|
|||
|
||||
from .cam import CamFile, FileSettings
|
||||
from .graphic_objects import Flash, Line, Arc
|
||||
from .apertures import ExcellonTool
|
||||
from .apertures import ExcellonTool, CircleAperture
|
||||
from .utils import Inch, MM, to_unit, InterpMode, RegexMatcher
|
||||
|
||||
|
||||
class ExcellonContext:
|
||||
""" Internal helper class used for tracking graphics state when writing Excellon. """
|
||||
|
||||
|
|
@ -268,17 +269,19 @@ class ExcellonFile(CamFile):
|
|||
""" Counterpart to :py:meth:`~.rs274x.GerberFile.to_excellon`. Does nothing and returns :py:obj:`self`. """
|
||||
return self
|
||||
|
||||
def to_gerber(self, errros='raise'):
|
||||
def to_gerber(self, errors='raise'):
|
||||
""" Convert this excellon file into a :py:class:`~.rs274x.GerberFile`. """
|
||||
from .rs274x import GerberFile
|
||||
out = GerberFile()
|
||||
out.comments = self.comments
|
||||
|
||||
apertures = {}
|
||||
for obj in self.objects:
|
||||
if not (ap := apertures[obj.tool]):
|
||||
ap = apertures[obj.tool] = CircleAperture(obj.tool.diameter)
|
||||
if not (ap := apertures.get(obj.tool)):
|
||||
ap = apertures[obj.tool] = CircleAperture(obj.tool.diameter, unit=obj.aperture.unit)
|
||||
|
||||
out.objects.append(dataclasses.replace(obj, aperture=ap))
|
||||
return out
|
||||
|
||||
@property
|
||||
def generator(self):
|
||||
|
|
@ -887,12 +890,17 @@ class ExcellonParser(object):
|
|||
# from https://math.stackexchange.com/a/1781546
|
||||
if a_s:
|
||||
raise ValueError('Negative arc radius given')
|
||||
r = settings.parse_gerber_value(a)
|
||||
r = self.settings.parse_gerber_value(a)
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
dx, dy = (x2-x1)/2, (y2-y1)/2
|
||||
x0, y0 = x1+dx, y1+dy
|
||||
f = math.hypot(dx, dy) / math.sqrt(r**2 - a**2)
|
||||
d = math.hypot(dx, dy)
|
||||
if d == 0:
|
||||
raise ValueError('Arc radius notation requires distinct start and end points')
|
||||
if r < d:
|
||||
raise ValueError('Arc radius too small for endpoint distance')
|
||||
f = math.sqrt(r**2 - d**2) / d
|
||||
if clockwise:
|
||||
cx = x0 + f*dy
|
||||
cy = y0 - f*dx
|
||||
|
|
@ -902,16 +910,16 @@ class ExcellonParser(object):
|
|||
i, j = cx-start[0], cy-start[1]
|
||||
|
||||
else: # explicit center given
|
||||
i = settings.parse_gerber_value(i)
|
||||
i = self.settings.parse_gerber_value(i) or 0
|
||||
if i_s:
|
||||
i = -i
|
||||
j = settings.parse_gerber_value(j)
|
||||
j = self.settings.parse_gerber_value(j) or 0
|
||||
if j_s:
|
||||
j = -i
|
||||
j = -j
|
||||
|
||||
self.objects.append(Arc(*start, *end, i, j, True, self.active_tool, unit=self.settings.unit))
|
||||
self.objects.append(Arc(*start, *end, i, j, clockwise, self.active_tool, unit=self.settings.unit))
|
||||
|
||||
@exprs.match(r'(M71|METRIC|M72|INCH)(,LZ|,TZ)?(,0*\.0*)?')
|
||||
@exprs.match(r'(M71|METRIC|M72|INCH)(,LZ|,TZ)?(,([0-9]+\.[0-9]+|0*\.0*))?')
|
||||
def parse_easyeda_format(self, match):
|
||||
metric = match[1] in ('METRIC', 'M71')
|
||||
|
||||
|
|
@ -924,7 +932,10 @@ class ExcellonParser(object):
|
|||
# This is used by newer autodesk eagles, fritzing and diptrace
|
||||
if match[3]:
|
||||
integer, _, fractional = match[3][1:].partition('.')
|
||||
self.settings.number_format = len(integer), len(fractional)
|
||||
if integer.strip('0') or fractional.strip('0'):
|
||||
self.settings.number_format = int(integer), int(fractional)
|
||||
else:
|
||||
self.settings.number_format = len(integer), len(fractional)
|
||||
|
||||
elif self.settings.number_format == (None, None) and not metric and not self.found_kicad_format_comment:
|
||||
self.warn('Using implicit number format from bare "INCH" statement. This is normal for Fritzing, Diptrace, Geda and pcb-rnd.')
|
||||
|
|
@ -950,10 +961,10 @@ class ExcellonParser(object):
|
|||
@exprs.match('(FMAT|VER),?([0-9]*)')
|
||||
def handle_command_format(self, match):
|
||||
if match[1] == 'FMAT':
|
||||
# We do not support integer/fractional decimals specification via FMAT because that's stupid. If you need this,
|
||||
# please raise an issue on our issue tracker, provide a sample file and tell us where on earth you found that
|
||||
# file.
|
||||
if match[2] not in ('', '2'):
|
||||
# We only use FMAT as a compatibility marker. Version 1 drill files encountered in the wild still use the
|
||||
# same coordinate and routing statements that we already support, so rejecting the header unconditionally
|
||||
# needlessly breaks otherwise parseable files.
|
||||
if match[2] not in ('', '1', '2'):
|
||||
raise SyntaxError(f'Unsupported FMAT format version {match[2]}')
|
||||
|
||||
else: # VER
|
||||
|
|
@ -982,6 +993,19 @@ class ExcellonParser(object):
|
|||
else:
|
||||
self.warn('Bare coordinate after end of file')
|
||||
|
||||
@exprs.match(xy_coord + 'G85' + xy_coord)
|
||||
def handle_g85_slot(self, match):
|
||||
if self.program_state == ProgramState.HEADER:
|
||||
return
|
||||
|
||||
self.do_move(match.groups()[:4])
|
||||
start, end = self.do_move(match.groups()[4:])
|
||||
|
||||
if not self.ensure_active_tool():
|
||||
return
|
||||
|
||||
self.objects.append(Line(*start, *end, self.active_tool, unit=self.settings.unit))
|
||||
|
||||
@exprs.match(r'DETECT,ON|ATC,ON|M06')
|
||||
def parse_zuken_legacy_statements(self, match):
|
||||
self.generator_hints.append('zuken')
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ class Region(GraphicObject):
|
|||
|
||||
def _offset(self, dx, dy):
|
||||
self.outline = [ (x+dx, y+dy) for x, y in self.outline ]
|
||||
self.arc_centers = [ (c[0], (c[1][0]+dx, c[1][1]+dy)) if c else None for c in self.arc_centers ]
|
||||
|
||||
def _rotate(self, angle, cx=0, cy=0):
|
||||
self.outline = [ gp.rotate_point(x, y, angle, cx, cy) for x, y in self.outline ]
|
||||
|
|
@ -322,7 +323,7 @@ class Region(GraphicObject):
|
|||
|
||||
def close(self):
|
||||
if self.outline and self.outline[-1] != self.outline[0]:
|
||||
self.outline.append(self.outline[-1])
|
||||
self.outline.append(self.outline[0])
|
||||
if self.arc_centers:
|
||||
self.arc_centers.append((None, (None, None)))
|
||||
|
||||
|
|
@ -336,8 +337,9 @@ class Region(GraphicObject):
|
|||
], unit=unit)
|
||||
|
||||
@classmethod
|
||||
def from_arc_poly(kls, arc_poly, polarity_dark=True, unit=MM):
|
||||
return kls(arc_poly.outline, arc_poly.arc_centers, polarity_dark=polarity_dark, unit=unit)
|
||||
def from_arc_poly(kls, arc_poly, polarity_dark=None, unit=MM):
|
||||
polarity = arc_poly.polarity_dark if polarity_dark is None else polarity_dark
|
||||
return kls(arc_poly.outline, arc_poly.arc_centers, polarity_dark=polarity, unit=unit)
|
||||
|
||||
def append(self, obj):
|
||||
if obj.unit != self.unit:
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ class GraphicPrimitive:
|
|||
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_zero_size(self):
|
||||
""" Return whether this primitive is zero size
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Circle(GraphicPrimitive):
|
||||
|
|
@ -81,7 +87,11 @@ class Circle(GraphicPrimitive):
|
|||
|
||||
def to_arc_poly(self):
|
||||
return ArcPoly([(self.x-self.r, self.y), (self.x+self.r, self.y)],
|
||||
[(True, (self.x, self.y)), (True, (self.x, self.y))])
|
||||
[(True, (self.x, self.y)), (True, (self.x, self.y))],
|
||||
polarity_dark=self.polarity_dark)
|
||||
|
||||
def is_zero_size(self):
|
||||
return math.isclose(self.r, 0)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -120,14 +130,14 @@ class ArcPoly(GraphicPrimitive):
|
|||
|
||||
def approximate_arcs(self, max_error=1e-2, clip_max_error=True):
|
||||
outline = []
|
||||
for p1, p2, (clockwise, center) in self.segments():
|
||||
for (x1, y1), (x2, y2), (clockwise, (cx, cy)) in self.segments:
|
||||
if clockwise is None:
|
||||
outline.append(p1)
|
||||
outline.append((x1, y1))
|
||||
else:
|
||||
outline.extend(approximate_arc(cx, cy, x1, y1, x2, y2, clockwise,
|
||||
max_error=max_error, clip_max_error=clip_max_error))
|
||||
outline.pop() # remove arc end point
|
||||
return type(self)(outline)
|
||||
return type(self)(outline, polarity_dark=self.polarity_dark)
|
||||
|
||||
def bounding_box(self):
|
||||
bbox = (None, None), (None, None)
|
||||
|
|
@ -179,6 +189,20 @@ class ArcPoly(GraphicPrimitive):
|
|||
return self
|
||||
|
||||
|
||||
def is_zero_size(self):
|
||||
for (x1, y1), (x2, y2), (clockwise, (cx, cy)) in self.segments:
|
||||
if clockwise is not None: # arc
|
||||
if math.isclose(cx, x1) and math.isclose(cy, y1):
|
||||
continue
|
||||
|
||||
if math.isclose(x1, x2) and math.isclose(y1, y2):
|
||||
return False
|
||||
|
||||
if math.isclose(polygon_area(self.outline), 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Line(GraphicPrimitive):
|
||||
""" Straight line with round end caps. """
|
||||
|
|
@ -218,24 +242,33 @@ class Line(GraphicPrimitive):
|
|||
color = fg if self.polarity_dark else bg
|
||||
width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
|
||||
return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} L {float(self.x2):.6} {float(self.y2):.6}',
|
||||
fill='none', stroke=color, stroke_width=str(width))
|
||||
fill='none', stroke=color, stroke_width=str(width), stroke_linecap='round')
|
||||
|
||||
def to_arc_poly(self):
|
||||
l = math.dist((self.x1, self.y1), (self.x2, self.y2))
|
||||
if math.isclose(l, 0):
|
||||
# degenerate case: a zero-length line becomes a circle.
|
||||
return ArcPoly([(self.x1-self.width/2, self.y1), (self.x1+self.width/2, self.y1)],
|
||||
[(True, (self.x1, self.y1)), (True, (self.x1, self.y1))],
|
||||
polarity_dark=self.polarity_dark)
|
||||
|
||||
dx, dy = self.x2-self.x1, self.y2-self.y1
|
||||
nx, ny = -dy/l, dx/l
|
||||
rx, ry = nx*self.width/2, ny*self.width/2
|
||||
return ArcPoly([
|
||||
(self.x1+rx, self.y1+ry),
|
||||
(self.x1-rx, self.y1-ry),
|
||||
(self.x2-rx, self.y2-ry),
|
||||
(self.x2+rx, self.y2+ry),
|
||||
(self.x2-rx, self.y2-ry),
|
||||
(self.x1-rx, self.y1-ry),
|
||||
(self.x1+rx, self.y1+ry),
|
||||
], [
|
||||
(True, (self.x1, self.y1)),
|
||||
None,
|
||||
(True, (self.x2, self.y2)),
|
||||
None,
|
||||
])
|
||||
(True, (self.x1, self.y1)),
|
||||
None,
|
||||
], polarity_dark=self.polarity_dark)
|
||||
|
||||
def is_zero_size(self):
|
||||
return math.isclose(self.x1, self.x2) and math.isclose(self.y1, self.y2)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -276,25 +309,35 @@ class Arc(GraphicPrimitive):
|
|||
arc = svg_arc((self.x1, self.y1), (self.x2, self.y2), (self.cx, self.cy), self.clockwise)
|
||||
width = f'{self.width:.6}' if not math.isclose(self.width, 0) else '0.01mm'
|
||||
return tag('path', d=f'M {float(self.x1):.6} {float(self.y1):.6} {arc}',
|
||||
fill='none', stroke=color, stroke_width=width)
|
||||
fill='none', stroke=color, stroke_width=width, stroke_linecap='round')
|
||||
|
||||
def to_arc_poly(self):
|
||||
r = math.dist((self.x1, self.y1), (self.cx, self.cy))
|
||||
|
||||
if math.isclose(r, 0):
|
||||
# degenerate case: a zero-radius arc becomes a circle.
|
||||
return ArcPoly([(self.x1-self.width/2, self.y1), (self.x1+self.width/2, self.y1)],
|
||||
[(True, (self.x1, self.y1)), (True, (self.x1, self.y1))],
|
||||
polarity_dark=self.polarity_dark)
|
||||
|
||||
dx1, dy1 = self.x1-self.cx, self.y1-self.cy
|
||||
nx1, ny1 = dx1/r * self.width/2, dy1/r * self.width/2
|
||||
dx2, dy2 = self.x2-self.cx, self.y2-self.cy
|
||||
nx2, ny2 = dx2/r * self.width/2, dy2/r * self.width/2
|
||||
return ArcPoly([
|
||||
(self.x1+nx1, self.y1+nx1),
|
||||
(self.x1-nx1, self.y1-nx1),
|
||||
(self.x2-nx2, self.y2-nx2),
|
||||
(self.x2+nx2, self.y2+nx2),
|
||||
], [
|
||||
(self.clockwise, (self.x1, self.y1)),
|
||||
return ArcPoly([ # vertices
|
||||
(self.x1+nx1, self.y1+ny1),
|
||||
(self.x1-nx1, self.y1-ny1),
|
||||
(self.x2-nx2, self.y2-ny2),
|
||||
(self.x2+nx2, self.y2+ny2),
|
||||
], [ # arc segments (direction, center)
|
||||
(not self.clockwise, (self.x1, self.y1)),
|
||||
(self.clockwise, (self.cx, self.cy)),
|
||||
(self.clockwise, (self.x2, self.y2)),
|
||||
(self.clockwise, (self.cx, self.cy)),
|
||||
])
|
||||
(not self.clockwise, (self.cx, self.cy)),
|
||||
], polarity_dark=self.polarity_dark)
|
||||
|
||||
def is_zero_size(self):
|
||||
return False # an arc with identical start and end points is defined as a circle
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -323,7 +366,7 @@ class Rectangle(GraphicPrimitive):
|
|||
(x - (cw+sh), y + (ch+sw)),
|
||||
(x + (cw+sh), y + (ch+sw)),
|
||||
(x + (cw+sh), y - (ch+sw)),
|
||||
])
|
||||
], polarity_dark=self.polarity_dark)
|
||||
|
||||
def to_svg(self, fg='black', bg='white', tag=Tag):
|
||||
color = fg if self.polarity_dark else bg
|
||||
|
|
@ -331,3 +374,6 @@ class Rectangle(GraphicPrimitive):
|
|||
return tag('rect', x=prec(x), y=prec(y), width=prec(self.w), height=prec(self.h),
|
||||
**svg_rotation(self.rotation, self.x, self.y), fill=color)
|
||||
|
||||
def is_zero_size(self):
|
||||
return math.isclose(self.w, 0) or math.isclose(self.h, 0)
|
||||
|
||||
|
|
|
|||
|
|
@ -66,27 +66,27 @@ DEFAULT_COLORS = {
|
|||
|
||||
class NamingScheme:
|
||||
kicad = {
|
||||
'top copper': '{board_name}-F.Cu.gbr',
|
||||
'top mask': '{board_name}-F.Mask.gbr',
|
||||
'top silk': '{board_name}-F.SilkS.gbr',
|
||||
'top paste': '{board_name}-F.Paste.gbr',
|
||||
'bottom copper': '{board_name}-B.Cu.gbr',
|
||||
'bottom mask': '{board_name}-B.Mask.gbr',
|
||||
'bottom silk': '{board_name}-B.SilkS.gbr',
|
||||
'bottom paste': '{board_name}-B.Paste.gbr',
|
||||
'inner copper': '{board_name}-In{layer_number}.Cu.gbr',
|
||||
'mechanical outline': '{board_name}-Edge.Cuts.gbr',
|
||||
'top copper': '{board_name}-F_Cu.gbr',
|
||||
'top mask': '{board_name}-F_Mask.gbr',
|
||||
'top silk': '{board_name}-F_SilkS.gbr',
|
||||
'top paste': '{board_name}-F_Paste.gbr',
|
||||
'bottom copper': '{board_name}-B_Cu.gbr',
|
||||
'bottom mask': '{board_name}-B_Mask.gbr',
|
||||
'bottom silk': '{board_name}-B_SilkS.gbr',
|
||||
'bottom paste': '{board_name}-B_Paste.gbr',
|
||||
'inner copper': '{board_name}-In{layer_number}_Cu.gbr',
|
||||
'mechanical outline': '{board_name}-Edge_Cuts.gbr',
|
||||
'drill unknown': '{board_name}.drl',
|
||||
'drill plated': '{board_name}-PTH.drl',
|
||||
'drill nonplated': '{board_name}-NPTH.drl',
|
||||
'other comments': '{board_name}-Cmts.User.gbr',
|
||||
'other drawings': '{board_name}-Dwgs.User.gbr',
|
||||
'top fabrication': '{board_name}-F.Fab.gbr',
|
||||
'bottom fabrication': '{board_name}-B.Fab.gbr',
|
||||
'top adhesive': '{board_name}-F.Adhes.gbr',
|
||||
'bottom adhesive': '{board_name}-B.Adhes.gbr',
|
||||
'top courtyard': '{board_name}-F.CrtYd.gbr',
|
||||
'bottom courtyard': '{board_name}-B.CrtYd.gbr',
|
||||
'other comments': '{board_name}-Cmts_User.gbr',
|
||||
'other drawings': '{board_name}-Dwgs_User.gbr',
|
||||
'top fabrication': '{board_name}-F_Fab.gbr',
|
||||
'bottom fabrication': '{board_name}-B_Fab.gbr',
|
||||
'top adhesive': '{board_name}-F_Adhes.gbr',
|
||||
'bottom adhesive': '{board_name}-B_Adhes.gbr',
|
||||
'top courtyard': '{board_name}-F_CrtYd.gbr',
|
||||
'bottom courtyard': '{board_name}-B_CrtYd.gbr',
|
||||
'other netlist': '{board_name}.d356',
|
||||
}
|
||||
|
||||
|
|
@ -1012,7 +1012,7 @@ class LayerStack:
|
|||
if use == 'mask':
|
||||
objects.insert(0, tag('path', id='outline-path', d=self.outline_svg_d(unit=svg_unit), fill='white'))
|
||||
layers.append(tag('g', objects, id=f'l-{side}-{use}', filter=f'url(#f-{use})',
|
||||
fill=default_fill, stroke=default_stroke, **stroke_attrs,
|
||||
fill=default_fill, stroke=default_stroke, **stroke_attrs, fill_rule='evenodd',
|
||||
**inkscape_attrs(f'{side} {use}'), transform=layer_transform))
|
||||
|
||||
for i, layer in enumerate(self.drill_layers):
|
||||
|
|
@ -1284,6 +1284,7 @@ class LayerStack:
|
|||
maybe_allegro_hint = '' if self.generator != 'allegro' else ' This file looks like it was generated by Allegro/OrCAD. These tools produce quite mal-formed gerbers, and often export text on the outline layer. If you generated this file yourself, maybe try twiddling with the export settings.'
|
||||
polygons = []
|
||||
lines = [ obj.as_primitive(unit) for obj in self.outline.instance.objects if isinstance(obj, (go.Line, go.Arc)) ]
|
||||
lines = [ prim for prim in lines if not prim.is_zero_size() ]
|
||||
|
||||
by_x = sorted([ (obj.x1, obj) for obj in lines ] + [ (obj.x2, obj) for obj in lines ], key=lambda x: x[0])
|
||||
dist_sq = lambda x1, y1, x2, y2: (x2-x1)**2 + (y2-y1)**2
|
||||
|
|
@ -1315,7 +1316,7 @@ class LayerStack:
|
|||
return
|
||||
|
||||
if (cur, i) in joins and joins[(cur, i)] != (nearest, j):
|
||||
warnings.warn(f'Three-way intersection on outline layer at: {(nearest, j)}; {(cur, i)}; and {joins[(nearest, j)]}. Falling back to returning the convex hull of the outline layer.{maybe_allegro_hint}')
|
||||
warnings.warn(f'Three-way intersection on outline layer at: {(nearest, j)}; {(cur, i)}; and {joins[(cur, i)]}. Falling back to returning the convex hull of the outline layer.{maybe_allegro_hint}')
|
||||
yield list(convex_hull_to_lines(self.outline.instance.convex_hull(tol, unit), unit))
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import dataclasses
|
|||
import functools
|
||||
|
||||
from .cam import CamFile, FileSettings
|
||||
from .utils import MM, Inch, units, InterpMode, UnknownStatementWarning, convex_hull
|
||||
from .utils import MM, Inch, units, InterpMode, UnknownStatementWarning
|
||||
from .aperture_macros.parse import ApertureMacro, GenericMacros
|
||||
from . import graphic_primitives as gp
|
||||
from . import graphic_objects as go
|
||||
|
|
@ -375,25 +375,7 @@ class GerberFile(CamFile):
|
|||
def invert_polarity(self):
|
||||
""" Invert the polarity (color) of each object in this file. """
|
||||
for obj in self.objects:
|
||||
obj.polarity_dark = not p.polarity_dark
|
||||
|
||||
def convex_hull(self, tol=0.01, unit=None):
|
||||
unit = unit or self.unit
|
||||
points = []
|
||||
|
||||
for obj in self.objects:
|
||||
if isinstance(obj, go.Line):
|
||||
line = obj.as_primitive(unit)
|
||||
points.append((line.x1, line.y1))
|
||||
points.append((line.x2, line.y2))
|
||||
|
||||
elif isinstance(obj, go.Arc):
|
||||
for obj in obj.approximate(tol, unit):
|
||||
line = obj.as_primitive(unit)
|
||||
points.append((line.x1, line.y1))
|
||||
points.append((line.x2, line.y2))
|
||||
|
||||
return convex_hull(points)
|
||||
obj.polarity_dark = not obj.polarity_dark
|
||||
|
||||
|
||||
class GraphicsState:
|
||||
|
|
@ -617,6 +599,8 @@ class GerberParser:
|
|||
NUMBER = r"[\+-]?\d+"
|
||||
DECIMAL = r"[\+-]?\d+([.]?\d+)?"
|
||||
NAME = r"[a-zA-Z_$\.][a-zA-Z_$\.0-9+\-]+"
|
||||
MAX_STEP_REPEAT_INSTANCES = 100000
|
||||
MAX_STEP_REPEAT_RESULT_OBJECTS = 100000
|
||||
|
||||
STATEMENT_REGEXES = {
|
||||
'coord': fr"(G0?[123]|G74|G75|G54|G55)?\s*(?:X\+?(-?)({NUMBER}))?(?:Y\+?(-?)({NUMBER}))?" \
|
||||
|
|
@ -878,6 +862,11 @@ class GerberParser:
|
|||
if match['shape'] in 'RO' and (math.isclose(modifiers[0], 0) or math.isclose(modifiers[1], 0)):
|
||||
self.warn('Definition of zero-width and/or zero-height rectangle or obround aperture. This is invalid according to spec.' )
|
||||
|
||||
# Polygon aperture rotation is specified in degrees, but radians are easier to work with
|
||||
if match['shape'] == 'P':
|
||||
if len(modifiers) > 2:
|
||||
modifiers[2] = math.radians(modifiers[2])
|
||||
|
||||
new_aperture = kls(*modifiers, unit=self.file_settings.unit, attrs=tuple(self.aperture_attrs.items()),
|
||||
original_number=number)
|
||||
|
||||
|
|
@ -1094,7 +1083,7 @@ class GerberParser:
|
|||
|
||||
else:
|
||||
target = {'TF': self.file_attrs, 'TO': self.graphics_state.object_attrs, 'TA': self.aperture_attrs}[match['type']]
|
||||
target[match['name']] = tuple(match['value'].split(','))
|
||||
target[match['name']] = tuple(match['value'].split(',')) if match['value'] else ()
|
||||
|
||||
if 'EAGLE' in self.file_attrs.get('.GenerationSoftware', []) or match['eagle_garbage']:
|
||||
self.generator_hints.append('eagle')
|
||||
|
|
@ -1108,18 +1097,23 @@ class GerberParser:
|
|||
i, j = float(match['I']), float(match['J'])
|
||||
if x < 1 or y < 1:
|
||||
raise SyntaxError('SR step-repeat X and Y values must be at least 1')
|
||||
if x * y > self.MAX_STEP_REPEAT_INSTANCES:
|
||||
raise SyntaxError('SR step-repeat expands to too many instances')
|
||||
|
||||
self.step_repeat_coords = [
|
||||
(i*nx, j*ny)
|
||||
for nx in range(x) for ny in range(y)] # the order matters here, cf. the spec
|
||||
self.step_repeat_coords = (x, y, i, j)
|
||||
self.step_repeat_objects = []
|
||||
|
||||
else:
|
||||
x, y, i, j = self.step_repeat_coords
|
||||
if len(self.step_repeat_objects) * x * y > self.MAX_STEP_REPEAT_RESULT_OBJECTS:
|
||||
raise SyntaxError('SR step-repeat expands to too many objects')
|
||||
|
||||
for obj in self.step_repeat_objects:
|
||||
for dx, dy in self.step_repeat_coords:
|
||||
new_obj = copy.copy(obj)
|
||||
new_obj.offset(dx, dy)
|
||||
self.target.objects.append(new_obj)
|
||||
for nx in range(x):
|
||||
for ny in range(y):
|
||||
new_obj = copy.copy(obj)
|
||||
new_obj.offset(i * nx, j * ny)
|
||||
self.target.objects.append(new_obj)
|
||||
self.step_repeat_coords = None
|
||||
self.step_repeat_objects = None
|
||||
|
||||
|
|
|
|||
|
|
@ -605,6 +605,18 @@ def point_in_polygon(point, poly):
|
|||
return res
|
||||
|
||||
|
||||
def polygon_area(poly):
|
||||
# https://en.wikipedia.org/wiki/Shoelace_formula
|
||||
|
||||
if not poly or len(poly) < 3:
|
||||
return 0
|
||||
|
||||
acc = 0
|
||||
for (x1, y1), (x2, y2) in zip(poly, poly[-1:] + poly):
|
||||
acc += (y1 + y2) * (x1 - x2)
|
||||
return acc/2
|
||||
|
||||
|
||||
def bbox_intersect(a, b):
|
||||
if a is None or b is None:
|
||||
return False
|
||||
|
|
|
|||
52
tests/resources/therm_1.gbr
Normal file
52
tests/resources/therm_1.gbr
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
G04 Created with the python gerber_writer 0.4.2.19*
|
||||
G04 #@! TF.CreationDate,2024-04-07T16:27:54.932609*
|
||||
G04 #@! TF.FileFunction,Copper,L1,Top,Signal*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
G04 From https://gitlab.com/gerbolyze/gerbonara/-/issues/11 *
|
||||
%MOMM*%
|
||||
%FSLAX36Y36*%
|
||||
G75*
|
||||
%AMRoundedThermal*
|
||||
0 Circular thermal with rounded corners*
|
||||
0 $1 outer diameter*
|
||||
0 $2 inner diameter*
|
||||
0 $3 gap of straight thermal primitive*
|
||||
0 $4 rotation angle*
|
||||
0 $5 diameter rounding circles*
|
||||
0 $6 x coordinate of q1 along h axis circle, rotated*
|
||||
0 $7 y coordinate of q1 along h axis circle, rotated*
|
||||
0 $8 x coordinate of q1 along v axis circle, rotated*
|
||||
0 $9 y coordinate of q1 along v axis circle, rotated*
|
||||
7,0,0,$1,$2,$3,$4*
|
||||
1,1,$5,$6,$7*
|
||||
1,1,$5,-$7,$6*
|
||||
1,1,$5,-$6,-$7*
|
||||
1,1,$5,$7,-$6*
|
||||
1,1,$5,$8,$9*
|
||||
1,1,$5,-$9,$8*
|
||||
1,1,$5,-$8,-$9*
|
||||
1,1,$5,$9,-$8*
|
||||
1,0,$2,0,0*
|
||||
%
|
||||
G04 #@! TA.AperFunction,ThermalReliefPad*
|
||||
G04 #@! TAShape,RoundedThermal,1,0.8,0.06,45*
|
||||
%ADD10RoundedThermal,1X0.8X0.182911X45X0.103913X0.253521X0.369418X-0.253521X0.369418*%
|
||||
G04 #@! TD*
|
||||
%LPD*%
|
||||
G04 #@! TA.AperFunction,Conductor*
|
||||
G36*
|
||||
X55000000Y50000000D02*
|
||||
G01*
|
||||
X60800000Y50000000D01*
|
||||
G03*
|
||||
X63000000Y52200000I0J2200000D01*
|
||||
G01*
|
||||
X63000000Y56000000D01*
|
||||
X55000000Y56000000D01*
|
||||
X55000000Y50000000D01*
|
||||
G37*
|
||||
G04 #@! TD*
|
||||
%LPC*%
|
||||
D10*
|
||||
X60800000Y52200000D03*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-B_Cu.gbr
Normal file
14
tests/resources/world_clock_2/wc2-B_Cu.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Copper,L2,Bot*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-B_Mask.gbr
Normal file
14
tests/resources/world_clock_2/wc2-B_Mask.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Soldermask,Bot*
|
||||
G04 #@! TF.FilePolarity,Negative*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-B_Paste.gbr
Normal file
14
tests/resources/world_clock_2/wc2-B_Paste.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Paste,Bot*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-B_SilkS.gbr
Normal file
14
tests/resources/world_clock_2/wc2-B_SilkS.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Legend,Bot*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
23
tests/resources/world_clock_2/wc2-Edge_Cuts.gbr
Normal file
23
tests/resources/world_clock_2/wc2-Edge_Cuts.gbr
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Profile,NP*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
%ADD10C,0.050000*%
|
||||
G04 APERTURE END LIST*
|
||||
D10*
|
||||
X353974409Y-193040000D02*
|
||||
G75*
|
||||
G03X353974409Y-193040000I-149504409J0D01*
|
||||
G01*
|
||||
X208486093Y-193040000D02*
|
||||
G75*
|
||||
G03X208486093Y-193040000I-4016093J0D01*
|
||||
G01*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-F_Cu.gbr
Normal file
14
tests/resources/world_clock_2/wc2-F_Cu.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Copper,L1,Top*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-F_Mask.gbr
Normal file
14
tests/resources/world_clock_2/wc2-F_Mask.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Soldermask,Top*
|
||||
G04 #@! TF.FilePolarity,Negative*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-F_Paste.gbr
Normal file
14
tests/resources/world_clock_2/wc2-F_Paste.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Paste,Top*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
14
tests/resources/world_clock_2/wc2-F_SilkS.gbr
Normal file
14
tests/resources/world_clock_2/wc2-F_SilkS.gbr
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
G04 #@! TF.GenerationSoftware,KiCad,Pcbnew,5.1.5+dfsg1-2build2*
|
||||
G04 #@! TF.CreationDate,2022-08-03T15:46:00+03:00*
|
||||
G04 #@! TF.ProjectId,wc2,7763322e-6b69-4636-9164-5f7063625858,rev?*
|
||||
G04 #@! TF.SameCoordinates,Original*
|
||||
G04 #@! TF.FileFunction,Legend,Top*
|
||||
G04 #@! TF.FilePolarity,Positive*
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 5.1.5+dfsg1-2build2) date 2022-08-03 15:46:00*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G04 APERTURE LIST*
|
||||
G04 APERTURE END LIST*
|
||||
M02*
|
||||
107
tests/resources/world_clock_2/wc2.kicad_pcb
Normal file
107
tests/resources/world_clock_2/wc2.kicad_pcb
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
(kicad_pcb (version 20171130) (host pcbnew 5.1.5+dfsg1-2build2)
|
||||
|
||||
(general
|
||||
(thickness 1.6)
|
||||
(drawings 2)
|
||||
(tracks 0)
|
||||
(zones 0)
|
||||
(modules 0)
|
||||
(nets 1)
|
||||
)
|
||||
|
||||
(page User 399.999 399.999)
|
||||
(layers
|
||||
(0 F.Cu signal)
|
||||
(31 B.Cu signal)
|
||||
(32 B.Adhes user)
|
||||
(33 F.Adhes user)
|
||||
(34 B.Paste user)
|
||||
(35 F.Paste user)
|
||||
(36 B.SilkS user)
|
||||
(37 F.SilkS user)
|
||||
(38 B.Mask user)
|
||||
(39 F.Mask user)
|
||||
(40 Dwgs.User user)
|
||||
(41 Cmts.User user)
|
||||
(42 Eco1.User user)
|
||||
(43 Eco2.User user)
|
||||
(44 Edge.Cuts user)
|
||||
(45 Margin user)
|
||||
(46 B.CrtYd user)
|
||||
(47 F.CrtYd user)
|
||||
(48 B.Fab user)
|
||||
(49 F.Fab user)
|
||||
)
|
||||
|
||||
(setup
|
||||
(last_trace_width 0.25)
|
||||
(trace_clearance 0.2)
|
||||
(zone_clearance 0.508)
|
||||
(zone_45_only no)
|
||||
(trace_min 0.2)
|
||||
(via_size 0.8)
|
||||
(via_drill 0.4)
|
||||
(via_min_size 0.4)
|
||||
(via_min_drill 0.3)
|
||||
(uvia_size 0.3)
|
||||
(uvia_drill 0.1)
|
||||
(uvias_allowed no)
|
||||
(uvia_min_size 0.2)
|
||||
(uvia_min_drill 0.1)
|
||||
(edge_width 0.05)
|
||||
(segment_width 0.2)
|
||||
(pcb_text_width 0.3)
|
||||
(pcb_text_size 1.5 1.5)
|
||||
(mod_edge_width 0.12)
|
||||
(mod_text_size 1 1)
|
||||
(mod_text_width 0.15)
|
||||
(pad_size 1.524 1.524)
|
||||
(pad_drill 0.762)
|
||||
(pad_to_mask_clearance 0.051)
|
||||
(solder_mask_min_width 0.25)
|
||||
(aux_axis_origin 0 0)
|
||||
(visible_elements FFFFFF7F)
|
||||
(pcbplotparams
|
||||
(layerselection 0x010fc_ffffffff)
|
||||
(usegerberextensions false)
|
||||
(usegerberattributes false)
|
||||
(usegerberadvancedattributes false)
|
||||
(creategerberjobfile false)
|
||||
(excludeedgelayer true)
|
||||
(linewidth 0.100000)
|
||||
(plotframeref false)
|
||||
(viasonmask false)
|
||||
(mode 1)
|
||||
(useauxorigin false)
|
||||
(hpglpennumber 1)
|
||||
(hpglpenspeed 20)
|
||||
(hpglpendiameter 15.000000)
|
||||
(psnegative false)
|
||||
(psa4output false)
|
||||
(plotreference true)
|
||||
(plotvalue true)
|
||||
(plotinvisibletext false)
|
||||
(padsonsilk false)
|
||||
(subtractmaskfromsilk false)
|
||||
(outputformat 1)
|
||||
(mirror false)
|
||||
(drillshape 0)
|
||||
(scaleselection 1)
|
||||
(outputdirectory ""))
|
||||
)
|
||||
|
||||
(net 0 "")
|
||||
|
||||
(net_class Default "This is the default net class."
|
||||
(clearance 0.2)
|
||||
(trace_width 0.25)
|
||||
(via_dia 0.8)
|
||||
(via_drill 0.4)
|
||||
(uvia_dia 0.3)
|
||||
(uvia_drill 0.1)
|
||||
)
|
||||
|
||||
(gr_circle (center 204.47 193.04) (end 353.06 209.55) (layer Edge.Cuts) (width 0.05))
|
||||
(gr_circle (center 204.47 193.04) (end 208.28 191.77) (layer Edge.Cuts) (width 0.05))
|
||||
|
||||
)
|
||||
1
tests/resources/zero-length-lines/README
Normal file
1
tests/resources/zero-length-lines/README
Normal file
|
|
@ -0,0 +1 @@
|
|||
From https://gitlab.com/gerbolyze/gerbonara/-/work_items/8
|
||||
11418
tests/resources/zero-length-lines/charliewatch-v2.panel-B_Cu.gbr
Normal file
11418
tests/resources/zero-length-lines/charliewatch-v2.panel-B_Cu.gbr
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,154 @@
|
|||
%TF.GenerationSoftware,KiCad,Pcbnew,7.0.9-7.0.9~ubuntu20.04.1*%
|
||||
%TF.CreationDate,2023-12-29T19:30:30+01:00*%
|
||||
%TF.ProjectId,charliewatch-v2.panel,63686172-6c69-4657-9761-7463682d7632,rev?*%
|
||||
%TF.SameCoordinates,Original*%
|
||||
%TF.FileFunction,Soldermask,Bot*%
|
||||
%TF.FilePolarity,Negative*%
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 7.0.9-7.0.9~ubuntu20.04.1) date 2023-12-29 19:30:30*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G01*
|
||||
G04 APERTURE LIST*
|
||||
G04 Aperture macros list*
|
||||
%AMRoundRect*
|
||||
0 Rectangle with rounded corners*
|
||||
0 $1 Rounding radius*
|
||||
0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
|
||||
0 Add a 4 corners polygon primitive as box body*
|
||||
4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
|
||||
0 Add four circle primitives for the rounded corners*
|
||||
1,1,$1+$1,$2,$3*
|
||||
1,1,$1+$1,$4,$5*
|
||||
1,1,$1+$1,$6,$7*
|
||||
1,1,$1+$1,$8,$9*
|
||||
0 Add four rect primitives between the rounded corners*
|
||||
20,1,$1+$1,$2,$3,$4,$5,0*
|
||||
20,1,$1+$1,$4,$5,$6,$7,0*
|
||||
20,1,$1+$1,$6,$7,$8,$9,0*
|
||||
20,1,$1+$1,$8,$9,$2,$3,0*%
|
||||
%AMRotRect*
|
||||
0 Rectangle, with rotation*
|
||||
0 The origin of the aperture is its center*
|
||||
0 $1 length*
|
||||
0 $2 width*
|
||||
0 $3 Rotation angle, in degrees counterclockwise*
|
||||
0 Add horizontal line*
|
||||
21,1,$1,$2,0,0,$3*%
|
||||
G04 Aperture macros list end*
|
||||
%ADD10C,0.300000*%
|
||||
%ADD11C,1.500000*%
|
||||
%ADD12RotRect,1.270000X5.080000X105.000000*%
|
||||
%ADD13C,6.000000*%
|
||||
%ADD14C,1.000000*%
|
||||
%ADD15R,1.050000X1.400000*%
|
||||
%ADD16RoundRect,0.135000X-0.082518X-0.213637X0.178282X-0.143756X0.082518X0.213637X-0.178282X0.143756X0*%
|
||||
G04 APERTURE END LIST*
|
||||
D10*
|
||||
%TO.C,KiKit_MB_3_15*%
|
||||
X147494485Y-23783376D03*
|
||||
%TD*%
|
||||
D11*
|
||||
%TO.C,KiKit_TO_1*%
|
||||
X132052287Y-22500000D03*
|
||||
%TD*%
|
||||
D10*
|
||||
%TO.C,KiKit_MB_4_13*%
|
||||
X148496960Y-54145380D03*
|
||||
%TD*%
|
||||
D11*
|
||||
%TO.C,KiKit_TO_3*%
|
||||
X132052287Y-55397712D03*
|
||||
%TD*%
|
||||
D10*
|
||||
%TO.C,KiKit_MB_4_14*%
|
||||
X148998534Y-54138972D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_2_11*%
|
||||
X163660556Y-39951194D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_1_11*%
|
||||
X133333257Y-37947426D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_3_13*%
|
||||
X148496960Y-23752332D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_1_12*%
|
||||
X133308476Y-38447907D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_4_11*%
|
||||
X147494485Y-54114336D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_2_14*%
|
||||
X163685076Y-38447390D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_2_15*%
|
||||
X163660556Y-37946518D03*
|
||||
%TD*%
|
||||
D11*
|
||||
%TO.C,KiKit_TO_2*%
|
||||
X164947713Y-22500000D03*
|
||||
%TD*%
|
||||
D10*
|
||||
%TO.C,KiKit_MB_3_11*%
|
||||
X149499318Y-23786347D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_1_13*%
|
||||
X133302438Y-38948856D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_4_12*%
|
||||
X147995579Y-54135802D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_2_13*%
|
||||
X163697483Y-38948859D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_1_14*%
|
||||
X133308774Y-39449790D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_3_14*%
|
||||
X147995579Y-23761910D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_2_12*%
|
||||
X163685180Y-39450325D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_4_15*%
|
||||
X149499318Y-54111365D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_3_12*%
|
||||
X148998534Y-23758740D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_1_15*%
|
||||
X133333528Y-39950273D03*
|
||||
%TD*%
|
||||
D12*
|
||||
%TO.C,BT1*%
|
||||
X151340840Y-49559551D03*
|
||||
X145654586Y-28338161D03*
|
||||
D13*
|
||||
X148497713Y-38948856D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_FID_B_2*%
|
||||
X162447713Y-22500000D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_FID_B_3*%
|
||||
X134552287Y-55397712D03*
|
||||
%TD*%
|
||||
D15*
|
||||
%TO.C,SW1*%
|
||||
X159777713Y-37148856D03*
|
||||
X159777713Y-40748856D03*
|
||||
X161217713Y-37148856D03*
|
||||
X161217713Y-40748856D03*
|
||||
%TD*%
|
||||
D16*
|
||||
%TO.C,R1*%
|
||||
X148843091Y-26710854D03*
|
||||
X149828335Y-26446858D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_FID_B_1*%
|
||||
X134552287Y-22500000D03*
|
||||
%TD*%
|
||||
M02*
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
%TF.GenerationSoftware,KiCad,Pcbnew,7.0.9-7.0.9~ubuntu20.04.1*%
|
||||
%TF.CreationDate,2023-12-29T19:30:30+01:00*%
|
||||
%TF.ProjectId,charliewatch-v2.panel,63686172-6c69-4657-9761-7463682d7632,rev?*%
|
||||
%TF.SameCoordinates,Original*%
|
||||
%TF.FileFunction,Paste,Bot*%
|
||||
%TF.FilePolarity,Positive*%
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 7.0.9-7.0.9~ubuntu20.04.1) date 2023-12-29 19:30:30*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G01*
|
||||
G04 APERTURE LIST*
|
||||
G04 Aperture macros list*
|
||||
%AMRoundRect*
|
||||
0 Rectangle with rounded corners*
|
||||
0 $1 Rounding radius*
|
||||
0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
|
||||
0 Add a 4 corners polygon primitive as box body*
|
||||
4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
|
||||
0 Add four circle primitives for the rounded corners*
|
||||
1,1,$1+$1,$2,$3*
|
||||
1,1,$1+$1,$4,$5*
|
||||
1,1,$1+$1,$6,$7*
|
||||
1,1,$1+$1,$8,$9*
|
||||
0 Add four rect primitives between the rounded corners*
|
||||
20,1,$1+$1,$2,$3,$4,$5,0*
|
||||
20,1,$1+$1,$4,$5,$6,$7,0*
|
||||
20,1,$1+$1,$6,$7,$8,$9,0*
|
||||
20,1,$1+$1,$8,$9,$2,$3,0*%
|
||||
%AMRotRect*
|
||||
0 Rectangle, with rotation*
|
||||
0 The origin of the aperture is its center*
|
||||
0 $1 length*
|
||||
0 $2 width*
|
||||
0 $3 Rotation angle, in degrees counterclockwise*
|
||||
0 Add horizontal line*
|
||||
21,1,$1,$2,0,0,$3*%
|
||||
G04 Aperture macros list end*
|
||||
%ADD10C,1.500000*%
|
||||
%ADD11RotRect,1.270000X5.080000X105.000000*%
|
||||
%ADD12R,1.050000X1.400000*%
|
||||
%ADD13RoundRect,0.135000X-0.082518X-0.213637X0.178282X-0.143756X0.082518X0.213637X-0.178282X0.143756X0*%
|
||||
G04 APERTURE END LIST*
|
||||
D10*
|
||||
%TO.C,KiKit_TO_1*%
|
||||
X132052287Y-22500000D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_TO_3*%
|
||||
X132052287Y-55397712D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_TO_2*%
|
||||
X164947713Y-22500000D03*
|
||||
%TD*%
|
||||
D11*
|
||||
%TO.C,BT1*%
|
||||
X151340840Y-49559551D03*
|
||||
X145654586Y-28338161D03*
|
||||
%TD*%
|
||||
D12*
|
||||
%TO.C,SW1*%
|
||||
X159777713Y-37148856D03*
|
||||
X159777713Y-40748856D03*
|
||||
X161217713Y-37148856D03*
|
||||
X161217713Y-40748856D03*
|
||||
%TD*%
|
||||
D13*
|
||||
%TO.C,R1*%
|
||||
X148843091Y-26710854D03*
|
||||
X149828335Y-26446858D03*
|
||||
%TD*%
|
||||
M02*
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
13180
tests/resources/zero-length-lines/charliewatch-v2.panel-F_Cu.gbr
Normal file
13180
tests/resources/zero-length-lines/charliewatch-v2.panel-F_Cu.gbr
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,761 @@
|
|||
%TF.GenerationSoftware,KiCad,Pcbnew,7.0.9-7.0.9~ubuntu20.04.1*%
|
||||
%TF.CreationDate,2023-12-29T19:30:30+01:00*%
|
||||
%TF.ProjectId,charliewatch-v2.panel,63686172-6c69-4657-9761-7463682d7632,rev?*%
|
||||
%TF.SameCoordinates,Original*%
|
||||
%TF.FileFunction,Soldermask,Top*%
|
||||
%TF.FilePolarity,Negative*%
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 7.0.9-7.0.9~ubuntu20.04.1) date 2023-12-29 19:30:30*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G01*
|
||||
G04 APERTURE LIST*
|
||||
G04 Aperture macros list*
|
||||
%AMRoundRect*
|
||||
0 Rectangle with rounded corners*
|
||||
0 $1 Rounding radius*
|
||||
0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
|
||||
0 Add a 4 corners polygon primitive as box body*
|
||||
4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
|
||||
0 Add four circle primitives for the rounded corners*
|
||||
1,1,$1+$1,$2,$3*
|
||||
1,1,$1+$1,$4,$5*
|
||||
1,1,$1+$1,$6,$7*
|
||||
1,1,$1+$1,$8,$9*
|
||||
0 Add four rect primitives between the rounded corners*
|
||||
20,1,$1+$1,$2,$3,$4,$5,0*
|
||||
20,1,$1+$1,$4,$5,$6,$7,0*
|
||||
20,1,$1+$1,$6,$7,$8,$9,0*
|
||||
20,1,$1+$1,$8,$9,$2,$3,0*%
|
||||
%AMRotRect*
|
||||
0 Rectangle, with rotation*
|
||||
0 The origin of the aperture is its center*
|
||||
0 $1 length*
|
||||
0 $2 width*
|
||||
0 $3 Rotation angle, in degrees counterclockwise*
|
||||
0 Add horizontal line*
|
||||
21,1,$1,$2,0,0,$3*%
|
||||
G04 Aperture macros list end*
|
||||
%ADD10RotRect,0.500000X0.400000X240.000000*%
|
||||
%ADD11RotRect,0.500000X0.400000X162.000000*%
|
||||
%ADD12RotRect,0.500000X0.400000X216.000000*%
|
||||
%ADD13RotRect,0.500000X0.400000X138.000000*%
|
||||
%ADD14C,0.300000*%
|
||||
%ADD15RotRect,0.500000X0.400000X348.000000*%
|
||||
%ADD16RotRect,0.500000X0.400000X114.000000*%
|
||||
%ADD17RotRect,0.500000X0.400000X72.000000*%
|
||||
%ADD18RotRect,0.500000X0.400000X78.000000*%
|
||||
%ADD19C,1.500000*%
|
||||
%ADD20RotRect,0.500000X0.400000X108.000000*%
|
||||
%ADD21RotRect,0.500000X0.400000X120.000000*%
|
||||
%ADD22C,1.000000*%
|
||||
%ADD23R,0.600000X1.200000*%
|
||||
%ADD24RotRect,0.500000X0.400000X330.000000*%
|
||||
%ADD25RoundRect,0.243750X0.456250X-0.243750X0.456250X0.243750X-0.456250X0.243750X-0.456250X-0.243750X0*%
|
||||
%ADD26RoundRect,0.243750X-0.516999X-0.017031X-0.273249X-0.439219X0.516999X0.017031X0.273249X0.439219X0*%
|
||||
%ADD27RoundRect,0.243750X-0.456250X0.243750X-0.456250X-0.243750X0.456250X-0.243750X0.456250X0.243750X0*%
|
||||
%ADD28RotRect,0.500000X0.400000X312.000000*%
|
||||
%ADD29RotRect,0.500000X0.400000X276.000000*%
|
||||
%ADD30RotRect,0.500000X0.400000X66.000000*%
|
||||
%ADD31RotRect,0.500000X0.400000X54.000000*%
|
||||
%ADD32RotRect,0.500000X0.400000X96.000000*%
|
||||
%ADD33RotRect,0.500000X0.400000X204.000000*%
|
||||
%ADD34RotRect,0.500000X0.400000X264.000000*%
|
||||
%ADD35RoundRect,0.243750X-0.243750X-0.456250X0.243750X-0.456250X0.243750X0.456250X-0.243750X0.456250X0*%
|
||||
%ADD36RoundRect,0.243750X-0.273249X0.439219X-0.516999X0.017031X0.273249X-0.439219X0.516999X-0.017031X0*%
|
||||
%ADD37RoundRect,0.243750X0.516999X0.017031X0.273249X0.439219X-0.516999X-0.017031X-0.273249X-0.439219X0*%
|
||||
%ADD38RotRect,0.500000X0.400000X102.000000*%
|
||||
%ADD39RoundRect,0.243750X0.273249X-0.439219X0.516999X-0.017031X-0.273249X0.439219X-0.516999X0.017031X0*%
|
||||
%ADD40RotRect,0.500000X0.400000X48.000000*%
|
||||
%ADD41RoundRect,0.062500X-0.375000X-0.062500X0.375000X-0.062500X0.375000X0.062500X-0.375000X0.062500X0*%
|
||||
%ADD42RoundRect,0.062500X-0.062500X-0.375000X0.062500X-0.375000X0.062500X0.375000X-0.062500X0.375000X0*%
|
||||
%ADD43R,3.450000X3.450000*%
|
||||
%ADD44RotRect,0.500000X0.400000X192.000000*%
|
||||
%ADD45R,0.400000X0.500000*%
|
||||
%ADD46RotRect,0.500000X0.400000X12.000000*%
|
||||
%ADD47RotRect,0.500000X0.400000X234.000000*%
|
||||
%ADD48RotRect,0.500000X0.400000X42.000000*%
|
||||
%ADD49RotRect,0.500000X0.400000X294.000000*%
|
||||
%ADD50RoundRect,0.243750X0.017031X-0.516999X0.439219X-0.273249X-0.017031X0.516999X-0.439219X0.273249X0*%
|
||||
%ADD51RotRect,0.500000X0.400000X24.000000*%
|
||||
%ADD52RotRect,0.500000X0.400000X144.000000*%
|
||||
%ADD53RotRect,0.500000X0.400000X168.000000*%
|
||||
%ADD54RotRect,0.500000X0.400000X6.000000*%
|
||||
%ADD55RotRect,0.500000X0.400000X258.000000*%
|
||||
%ADD56R,0.500000X0.400000*%
|
||||
%ADD57RotRect,0.500000X0.400000X150.000000*%
|
||||
%ADD58RotRect,0.500000X0.400000X228.000000*%
|
||||
%ADD59RotRect,0.500000X0.400000X36.000000*%
|
||||
%ADD60RotRect,0.500000X0.400000X306.000000*%
|
||||
%ADD61RotRect,0.500000X0.400000X30.000000*%
|
||||
%ADD62RotRect,0.500000X0.400000X246.000000*%
|
||||
%ADD63RotRect,0.500000X0.400000X18.000000*%
|
||||
%ADD64RoundRect,0.243750X-0.439219X-0.273249X-0.017031X-0.516999X0.439219X0.273249X0.017031X0.516999X0*%
|
||||
%ADD65RoundRect,0.243750X0.243750X0.456250X-0.243750X0.456250X-0.243750X-0.456250X0.243750X-0.456250X0*%
|
||||
%ADD66RotRect,0.500000X0.400000X174.000000*%
|
||||
%ADD67RotRect,0.500000X0.400000X342.000000*%
|
||||
%ADD68RotRect,0.500000X0.400000X282.000000*%
|
||||
%ADD69RotRect,0.500000X0.400000X186.000000*%
|
||||
%ADD70RotRect,0.500000X0.400000X354.000000*%
|
||||
%ADD71RotRect,0.500000X0.400000X156.000000*%
|
||||
%ADD72RoundRect,0.243750X0.439219X0.273249X0.017031X0.516999X-0.439219X-0.273249X-0.017031X-0.516999X0*%
|
||||
%ADD73RotRect,0.500000X0.400000X252.000000*%
|
||||
%ADD74RotRect,0.500000X0.400000X288.000000*%
|
||||
%ADD75RotRect,0.500000X0.400000X84.000000*%
|
||||
%ADD76RotRect,0.500000X0.400000X198.000000*%
|
||||
%ADD77RotRect,0.500000X0.400000X132.000000*%
|
||||
%ADD78RotRect,0.500000X0.400000X336.000000*%
|
||||
%ADD79RotRect,0.500000X0.400000X210.000000*%
|
||||
%ADD80RotRect,0.500000X0.400000X324.000000*%
|
||||
%ADD81RotRect,0.500000X0.400000X60.000000*%
|
||||
%ADD82RotRect,0.500000X0.400000X318.000000*%
|
||||
%ADD83RotRect,0.500000X0.400000X126.000000*%
|
||||
%ADD84RotRect,1.000000X1.000000X45.000000*%
|
||||
%ADD85RoundRect,0.243750X-0.017031X0.516999X-0.439219X0.273249X0.017031X-0.516999X0.439219X-0.273249X0*%
|
||||
%ADD86RotRect,0.500000X0.400000X222.000000*%
|
||||
%ADD87RotRect,0.500000X0.400000X300.000000*%
|
||||
G04 APERTURE END LIST*
|
||||
D10*
|
||||
%TO.C,D36*%
|
||||
X154769604Y-27385471D03*
|
||||
X155369604Y-26346241D03*
|
||||
X155375822Y-27735471D03*
|
||||
X155975822Y-26696241D03*
|
||||
%TD*%
|
||||
D11*
|
||||
%TO.C,D49*%
|
||||
X161112235Y-42679576D03*
|
||||
X162253503Y-43050396D03*
|
||||
X160895923Y-43345316D03*
|
||||
X162037191Y-43716136D03*
|
||||
%TD*%
|
||||
D12*
|
||||
%TO.C,D40*%
|
||||
X158930578Y-30936371D03*
|
||||
X159901398Y-30231029D03*
|
||||
X159342028Y-31502683D03*
|
||||
X160312848Y-30797341D03*
|
||||
%TD*%
|
||||
D13*
|
||||
%TO.C,D53*%
|
||||
X158504022Y-47488277D03*
|
||||
X159395796Y-48291234D03*
|
||||
X158035630Y-48008478D03*
|
||||
X158927404Y-48811435D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_3_15*%
|
||||
X147494485Y-23783376D03*
|
||||
%TD*%
|
||||
D15*
|
||||
%TO.C,D18*%
|
||||
X135561832Y-36556955D03*
|
||||
X134388055Y-36307461D03*
|
||||
X135707371Y-35872251D03*
|
||||
X134533594Y-35622757D03*
|
||||
%TD*%
|
||||
D16*
|
||||
%TO.C,D57*%
|
||||
X154166412Y-50819371D03*
|
||||
X154654496Y-51915625D03*
|
||||
X153526930Y-51104087D03*
|
||||
X154015014Y-52200341D03*
|
||||
%TD*%
|
||||
D17*
|
||||
%TO.C,D4*%
|
||||
X144766993Y-51563378D03*
|
||||
X144396173Y-52704646D03*
|
||||
X144101253Y-51347066D03*
|
||||
X143730433Y-52488334D03*
|
||||
%TD*%
|
||||
D18*
|
||||
%TO.C,D3*%
|
||||
X146105812Y-51884737D03*
|
||||
X145856318Y-53058514D03*
|
||||
X145421108Y-51739198D03*
|
||||
X145171614Y-52912975D03*
|
||||
%TD*%
|
||||
D19*
|
||||
%TO.C,KiKit_TO_1*%
|
||||
X132052287Y-22500000D03*
|
||||
%TD*%
|
||||
D20*
|
||||
%TO.C,D58*%
|
||||
X152894173Y-51347066D03*
|
||||
X153264993Y-52488334D03*
|
||||
X152228433Y-51563378D03*
|
||||
X152599253Y-52704646D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_4_13*%
|
||||
X148496960Y-54145380D03*
|
||||
%TD*%
|
||||
D21*
|
||||
%TO.C,D56*%
|
||||
X155375822Y-50162241D03*
|
||||
X155975822Y-51201471D03*
|
||||
X154769604Y-50512241D03*
|
||||
X155369604Y-51551471D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,TST1*%
|
||||
X156876713Y-30569856D03*
|
||||
%TD*%
|
||||
D19*
|
||||
%TO.C,KiKit_TO_3*%
|
||||
X132052287Y-55397712D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_4_14*%
|
||||
X148998534Y-54138972D03*
|
||||
%TD*%
|
||||
D23*
|
||||
%TO.C,Y1*%
|
||||
X149707713Y-35148856D03*
|
||||
X150907713Y-35148856D03*
|
||||
%TD*%
|
||||
D24*
|
||||
%TO.C,D21*%
|
||||
X136934328Y-32676965D03*
|
||||
X135895098Y-32076965D03*
|
||||
X137284328Y-32070747D03*
|
||||
X136245098Y-31470747D03*
|
||||
%TD*%
|
||||
D25*
|
||||
%TO.C,HD4*%
|
||||
X160147713Y-39886356D03*
|
||||
X160147713Y-38011356D03*
|
||||
%TD*%
|
||||
D26*
|
||||
%TO.C,HD11*%
|
||||
X137939963Y-43961957D03*
|
||||
X138877463Y-45585755D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_2_11*%
|
||||
X163660556Y-39951194D03*
|
||||
%TD*%
|
||||
D27*
|
||||
%TO.C,HD10*%
|
||||
X136847713Y-38011356D03*
|
||||
X136847713Y-39886356D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_1_11*%
|
||||
X133333257Y-37947426D03*
|
||||
%TD*%
|
||||
D28*
|
||||
%TO.C,D24*%
|
||||
X139438091Y-29410939D03*
|
||||
X138635134Y-28519165D03*
|
||||
X139958292Y-28942547D03*
|
||||
X139155335Y-28050773D03*
|
||||
%TD*%
|
||||
D29*
|
||||
%TO.C,D30*%
|
||||
X146775347Y-25907154D03*
|
||||
X146649913Y-24713728D03*
|
||||
X147471513Y-25833984D03*
|
||||
X147346079Y-24640558D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,KiKit_FID_T_2*%
|
||||
X162447713Y-22500000D03*
|
||||
%TD*%
|
||||
D30*
|
||||
%TO.C,D5*%
|
||||
X143468496Y-51104087D03*
|
||||
X142980412Y-52200341D03*
|
||||
X142829014Y-50819371D03*
|
||||
X142340930Y-51915625D03*
|
||||
%TD*%
|
||||
D31*
|
||||
%TO.C,D7*%
|
||||
X141051540Y-49793171D03*
|
||||
X140346198Y-50763991D03*
|
||||
X140485228Y-49381721D03*
|
||||
X139779886Y-50352541D03*
|
||||
%TD*%
|
||||
D32*
|
||||
%TO.C,D60*%
|
||||
X150220079Y-51990558D03*
|
||||
X150345513Y-53183984D03*
|
||||
X149523913Y-52063728D03*
|
||||
X149649347Y-53257154D03*
|
||||
%TD*%
|
||||
D33*
|
||||
%TO.C,D42*%
|
||||
X160368228Y-33280157D03*
|
||||
X161464482Y-32792073D03*
|
||||
X160652944Y-33919639D03*
|
||||
X161749198Y-33431555D03*
|
||||
%TD*%
|
||||
D34*
|
||||
%TO.C,D32*%
|
||||
X149523913Y-25833984D03*
|
||||
X149649347Y-24640558D03*
|
||||
X150220079Y-25907154D03*
|
||||
X150345513Y-24713728D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_3_13*%
|
||||
X148496960Y-23752332D03*
|
||||
%TD*%
|
||||
D35*
|
||||
%TO.C,HD1*%
|
||||
X147560213Y-50598856D03*
|
||||
X149435213Y-50598856D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_1_12*%
|
||||
X133308476Y-38447907D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_4_11*%
|
||||
X147494485Y-54114336D03*
|
||||
%TD*%
|
||||
D36*
|
||||
%TO.C,HD9*%
|
||||
X138877463Y-32311957D03*
|
||||
X137939963Y-33935755D03*
|
||||
%TD*%
|
||||
D37*
|
||||
%TO.C,HD5*%
|
||||
X159055463Y-33935755D03*
|
||||
X158117963Y-32311957D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_2_14*%
|
||||
X163685076Y-38447390D03*
|
||||
%TD*%
|
||||
D38*
|
||||
%TO.C,D59*%
|
||||
X151574318Y-51739198D03*
|
||||
X151823812Y-52912975D03*
|
||||
X150889614Y-51884737D03*
|
||||
X151139108Y-53058514D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_2_15*%
|
||||
X163660556Y-37946518D03*
|
||||
%TD*%
|
||||
D39*
|
||||
%TO.C,HD3*%
|
||||
X158117963Y-45585755D03*
|
||||
X159055463Y-43961957D03*
|
||||
%TD*%
|
||||
D40*
|
||||
%TO.C,D8*%
|
||||
X139958292Y-48955165D03*
|
||||
X139155335Y-49846939D03*
|
||||
X139438091Y-48486773D03*
|
||||
X138635134Y-49378547D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,RST1*%
|
||||
X151564713Y-27501856D03*
|
||||
%TD*%
|
||||
D41*
|
||||
%TO.C,U2*%
|
||||
X146060213Y-37348856D03*
|
||||
X146060213Y-37848856D03*
|
||||
X146060213Y-38348856D03*
|
||||
X146060213Y-38848856D03*
|
||||
X146060213Y-39348856D03*
|
||||
X146060213Y-39848856D03*
|
||||
X146060213Y-40348856D03*
|
||||
X146060213Y-40848856D03*
|
||||
D42*
|
||||
X146747713Y-41536356D03*
|
||||
X147247713Y-41536356D03*
|
||||
X147747713Y-41536356D03*
|
||||
X148247713Y-41536356D03*
|
||||
X148747713Y-41536356D03*
|
||||
X149247713Y-41536356D03*
|
||||
X149747713Y-41536356D03*
|
||||
X150247713Y-41536356D03*
|
||||
D41*
|
||||
X150935213Y-40848856D03*
|
||||
X150935213Y-40348856D03*
|
||||
X150935213Y-39848856D03*
|
||||
X150935213Y-39348856D03*
|
||||
X150935213Y-38848856D03*
|
||||
X150935213Y-38348856D03*
|
||||
X150935213Y-37848856D03*
|
||||
X150935213Y-37348856D03*
|
||||
D42*
|
||||
X150247713Y-36661356D03*
|
||||
X149747713Y-36661356D03*
|
||||
X149247713Y-36661356D03*
|
||||
X148747713Y-36661356D03*
|
||||
X148247713Y-36661356D03*
|
||||
X147747713Y-36661356D03*
|
||||
X147247713Y-36661356D03*
|
||||
X146747713Y-36661356D03*
|
||||
D43*
|
||||
X148497713Y-39098856D03*
|
||||
%TD*%
|
||||
D19*
|
||||
%TO.C,KiKit_TO_2*%
|
||||
X164947713Y-22500000D03*
|
||||
%TD*%
|
||||
D44*
|
||||
%TO.C,D44*%
|
||||
X161288055Y-35872251D03*
|
||||
X162461832Y-35622757D03*
|
||||
X161433594Y-36556955D03*
|
||||
X162607371Y-36307461D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,VCC1*%
|
||||
X145430713Y-27502856D03*
|
||||
%TD*%
|
||||
D45*
|
||||
%TO.C,D1*%
|
||||
X148847713Y-52098856D03*
|
||||
X148847713Y-53298856D03*
|
||||
X148147713Y-52098856D03*
|
||||
X148147713Y-53298856D03*
|
||||
%TD*%
|
||||
D46*
|
||||
%TO.C,D14*%
|
||||
X135707371Y-42025461D03*
|
||||
X134533594Y-42274955D03*
|
||||
X135561832Y-41340757D03*
|
||||
X134388055Y-41590251D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_3_11*%
|
||||
X149499318Y-23786347D03*
|
||||
%TD*%
|
||||
D47*
|
||||
%TO.C,D37*%
|
||||
X155943886Y-28104541D03*
|
||||
X156649228Y-27133721D03*
|
||||
X156510198Y-28515991D03*
|
||||
X157215540Y-27545171D03*
|
||||
%TD*%
|
||||
D48*
|
||||
%TO.C,D9*%
|
||||
X138959796Y-48008478D03*
|
||||
X138068022Y-48811435D03*
|
||||
X138491404Y-47488277D03*
|
||||
X137599630Y-48291234D03*
|
||||
%TD*%
|
||||
D49*
|
||||
%TO.C,D27*%
|
||||
X142829014Y-27078341D03*
|
||||
X142340930Y-25982087D03*
|
||||
X143468496Y-26793625D03*
|
||||
X142980412Y-25697371D03*
|
||||
%TD*%
|
||||
D50*
|
||||
%TO.C,HD2*%
|
||||
X153510814Y-49506606D03*
|
||||
X155134612Y-48569106D03*
|
||||
%TD*%
|
||||
D51*
|
||||
%TO.C,D12*%
|
||||
X136627198Y-44617555D03*
|
||||
X135530944Y-45105639D03*
|
||||
X136342482Y-43978073D03*
|
||||
X135246228Y-44466157D03*
|
||||
%TD*%
|
||||
D52*
|
||||
%TO.C,D52*%
|
||||
X159342028Y-46395029D03*
|
||||
X160312848Y-47100371D03*
|
||||
X158930578Y-46961341D03*
|
||||
X159901398Y-47666683D03*
|
||||
%TD*%
|
||||
D53*
|
||||
%TO.C,D48*%
|
||||
X161433594Y-41340757D03*
|
||||
X162607371Y-41590251D03*
|
||||
X161288055Y-42025461D03*
|
||||
X162461832Y-42274955D03*
|
||||
%TD*%
|
||||
D54*
|
||||
%TO.C,D15*%
|
||||
X135456011Y-40671222D03*
|
||||
X134262585Y-40796656D03*
|
||||
X135382841Y-39975056D03*
|
||||
X134189415Y-40100490D03*
|
||||
%TD*%
|
||||
D55*
|
||||
%TO.C,D33*%
|
||||
X150889614Y-26012975D03*
|
||||
X151139108Y-24839198D03*
|
||||
X151574318Y-26158514D03*
|
||||
X151823812Y-24984737D03*
|
||||
%TD*%
|
||||
D56*
|
||||
%TO.C,D16*%
|
||||
X135347713Y-39298856D03*
|
||||
X134147713Y-39298856D03*
|
||||
X135347713Y-38598856D03*
|
||||
X134147713Y-38598856D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,KiKit_FID_T_3*%
|
||||
X134552287Y-55397712D03*
|
||||
%TD*%
|
||||
D57*
|
||||
%TO.C,D51*%
|
||||
X160061098Y-45220747D03*
|
||||
X161100328Y-45820747D03*
|
||||
X159711098Y-45826965D03*
|
||||
X160750328Y-46426965D03*
|
||||
%TD*%
|
||||
D58*
|
||||
%TO.C,D38*%
|
||||
X157037134Y-28942547D03*
|
||||
X157840091Y-28050773D03*
|
||||
X157557335Y-29410939D03*
|
||||
X158360292Y-28519165D03*
|
||||
%TD*%
|
||||
D59*
|
||||
%TO.C,D10*%
|
||||
X138064848Y-46961341D03*
|
||||
X137094028Y-47666683D03*
|
||||
X137653398Y-46395029D03*
|
||||
X136682578Y-47100371D03*
|
||||
%TD*%
|
||||
D60*
|
||||
%TO.C,D25*%
|
||||
X140485228Y-28515991D03*
|
||||
X139779886Y-27545171D03*
|
||||
X141051540Y-28104541D03*
|
||||
X140346198Y-27133721D03*
|
||||
%TD*%
|
||||
D61*
|
||||
%TO.C,D11*%
|
||||
X137284328Y-45826965D03*
|
||||
X136245098Y-46426965D03*
|
||||
X136934328Y-45220747D03*
|
||||
X135895098Y-45820747D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_1_13*%
|
||||
X133302438Y-38948856D03*
|
||||
%TD*%
|
||||
D62*
|
||||
%TO.C,D35*%
|
||||
X153526930Y-26793625D03*
|
||||
X154015014Y-25697371D03*
|
||||
X154166412Y-27078341D03*
|
||||
X154654496Y-25982087D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_4_12*%
|
||||
X147995579Y-54135802D03*
|
||||
%TD*%
|
||||
D63*
|
||||
%TO.C,D13*%
|
||||
X136099503Y-43345316D03*
|
||||
X134958235Y-43716136D03*
|
||||
X135883191Y-42679576D03*
|
||||
X134741923Y-43050396D03*
|
||||
%TD*%
|
||||
D64*
|
||||
%TO.C,HD12*%
|
||||
X141860814Y-48569106D03*
|
||||
X143484612Y-49506606D03*
|
||||
%TD*%
|
||||
D65*
|
||||
%TO.C,HD7*%
|
||||
X149435213Y-27298856D03*
|
||||
X147560213Y-27298856D03*
|
||||
%TD*%
|
||||
D66*
|
||||
%TO.C,D47*%
|
||||
X161612585Y-39975056D03*
|
||||
X162806011Y-40100490D03*
|
||||
X161539415Y-40671222D03*
|
||||
X162732841Y-40796656D03*
|
||||
%TD*%
|
||||
D45*
|
||||
%TO.C,D31*%
|
||||
X148147713Y-25798856D03*
|
||||
X148147713Y-24598856D03*
|
||||
X148847713Y-25798856D03*
|
||||
X148847713Y-24598856D03*
|
||||
%TD*%
|
||||
D67*
|
||||
%TO.C,D19*%
|
||||
X135883191Y-35218136D03*
|
||||
X134741923Y-34847316D03*
|
||||
X136099503Y-34552396D03*
|
||||
X134958235Y-34181576D03*
|
||||
%TD*%
|
||||
D68*
|
||||
%TO.C,D29*%
|
||||
X145421108Y-26158514D03*
|
||||
X145171614Y-24984737D03*
|
||||
X146105812Y-26012975D03*
|
||||
X145856318Y-24839198D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_2_13*%
|
||||
X163697483Y-38948859D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,KiKit_FID_T_1*%
|
||||
X134552287Y-22500000D03*
|
||||
%TD*%
|
||||
D69*
|
||||
%TO.C,D45*%
|
||||
X161539415Y-37226490D03*
|
||||
X162732841Y-37101056D03*
|
||||
X161612585Y-37922656D03*
|
||||
X162806011Y-37797222D03*
|
||||
%TD*%
|
||||
D70*
|
||||
%TO.C,D17*%
|
||||
X135382841Y-37922656D03*
|
||||
X134189415Y-37797222D03*
|
||||
X135456011Y-37226490D03*
|
||||
X134262585Y-37101056D03*
|
||||
%TD*%
|
||||
D56*
|
||||
%TO.C,D46*%
|
||||
X161647713Y-38598856D03*
|
||||
X162847713Y-38598856D03*
|
||||
X161647713Y-39298856D03*
|
||||
X162847713Y-39298856D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_1_14*%
|
||||
X133308774Y-39449790D03*
|
||||
%TD*%
|
||||
D71*
|
||||
%TO.C,D50*%
|
||||
X160652944Y-43978073D03*
|
||||
X161749198Y-44466157D03*
|
||||
X160368228Y-44617555D03*
|
||||
X161464482Y-45105639D03*
|
||||
%TD*%
|
||||
D72*
|
||||
%TO.C,HD6*%
|
||||
X155134612Y-29328606D03*
|
||||
X153510814Y-28391106D03*
|
||||
%TD*%
|
||||
D73*
|
||||
%TO.C,D34*%
|
||||
X152228433Y-26334334D03*
|
||||
X152599253Y-25193066D03*
|
||||
X152894173Y-26550646D03*
|
||||
X153264993Y-25409378D03*
|
||||
%TD*%
|
||||
D74*
|
||||
%TO.C,D28*%
|
||||
X144101253Y-26550646D03*
|
||||
X143730433Y-25409378D03*
|
||||
X144766993Y-26334334D03*
|
||||
X144396173Y-25193066D03*
|
||||
%TD*%
|
||||
D75*
|
||||
%TO.C,D2*%
|
||||
X147471513Y-52063728D03*
|
||||
X147346079Y-53257154D03*
|
||||
X146775347Y-51990558D03*
|
||||
X146649913Y-53183984D03*
|
||||
%TD*%
|
||||
D76*
|
||||
%TO.C,D43*%
|
||||
X160895923Y-34552396D03*
|
||||
X162037191Y-34181576D03*
|
||||
X161112235Y-35218136D03*
|
||||
X162253503Y-34847316D03*
|
||||
%TD*%
|
||||
D77*
|
||||
%TO.C,D54*%
|
||||
X157557335Y-48486773D03*
|
||||
X158360292Y-49378547D03*
|
||||
X157037134Y-48955165D03*
|
||||
X157840091Y-49846939D03*
|
||||
%TD*%
|
||||
D78*
|
||||
%TO.C,D20*%
|
||||
X136342482Y-33919639D03*
|
||||
X135246228Y-33431555D03*
|
||||
X136627198Y-33280157D03*
|
||||
X135530944Y-32792073D03*
|
||||
%TD*%
|
||||
D79*
|
||||
%TO.C,D41*%
|
||||
X159711098Y-32070747D03*
|
||||
X160750328Y-31470747D03*
|
||||
X160061098Y-32676965D03*
|
||||
X161100328Y-32076965D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_3_14*%
|
||||
X147995579Y-23761910D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_2_12*%
|
||||
X163685180Y-39450325D03*
|
||||
%TD*%
|
||||
D80*
|
||||
%TO.C,D22*%
|
||||
X137653398Y-31502683D03*
|
||||
X136682578Y-30797341D03*
|
||||
X138064848Y-30936371D03*
|
||||
X137094028Y-30231029D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_4_15*%
|
||||
X149499318Y-54111365D03*
|
||||
%TD*%
|
||||
%TO.C,KiKit_MB_3_12*%
|
||||
X148998534Y-23758740D03*
|
||||
%TD*%
|
||||
D81*
|
||||
%TO.C,D6*%
|
||||
X142225822Y-50512241D03*
|
||||
X141625822Y-51551471D03*
|
||||
X141619604Y-50162241D03*
|
||||
X141019604Y-51201471D03*
|
||||
%TD*%
|
||||
D82*
|
||||
%TO.C,D23*%
|
||||
X138491404Y-30409435D03*
|
||||
X137599630Y-29606478D03*
|
||||
X138959796Y-29889234D03*
|
||||
X138068022Y-29086277D03*
|
||||
%TD*%
|
||||
D83*
|
||||
%TO.C,D55*%
|
||||
X156510198Y-49381721D03*
|
||||
X157215540Y-50352541D03*
|
||||
X155943886Y-49793171D03*
|
||||
X156649228Y-50763991D03*
|
||||
%TD*%
|
||||
D84*
|
||||
%TO.C,GND1*%
|
||||
X140118713Y-30569856D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,KiKit_MB_1_15*%
|
||||
X133333528Y-39950273D03*
|
||||
%TD*%
|
||||
D85*
|
||||
%TO.C,HD8*%
|
||||
X143484612Y-28391106D03*
|
||||
X141860814Y-29328606D03*
|
||||
%TD*%
|
||||
D86*
|
||||
%TO.C,D39*%
|
||||
X158035630Y-29889234D03*
|
||||
X158927404Y-29086277D03*
|
||||
X158504022Y-30409435D03*
|
||||
X159395796Y-29606478D03*
|
||||
%TD*%
|
||||
D87*
|
||||
%TO.C,D26*%
|
||||
X141619604Y-27735471D03*
|
||||
X141019604Y-26696241D03*
|
||||
X142225822Y-27385471D03*
|
||||
X141625822Y-26346241D03*
|
||||
%TD*%
|
||||
M02*
|
||||
|
|
@ -0,0 +1,719 @@
|
|||
%TF.GenerationSoftware,KiCad,Pcbnew,7.0.9-7.0.9~ubuntu20.04.1*%
|
||||
%TF.CreationDate,2023-12-29T19:30:30+01:00*%
|
||||
%TF.ProjectId,charliewatch-v2.panel,63686172-6c69-4657-9761-7463682d7632,rev?*%
|
||||
%TF.SameCoordinates,Original*%
|
||||
%TF.FileFunction,Paste,Top*%
|
||||
%TF.FilePolarity,Positive*%
|
||||
%FSLAX46Y46*%
|
||||
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
|
||||
G04 Created by KiCad (PCBNEW 7.0.9-7.0.9~ubuntu20.04.1) date 2023-12-29 19:30:30*
|
||||
%MOMM*%
|
||||
%LPD*%
|
||||
G01*
|
||||
G04 APERTURE LIST*
|
||||
G04 Aperture macros list*
|
||||
%AMRoundRect*
|
||||
0 Rectangle with rounded corners*
|
||||
0 $1 Rounding radius*
|
||||
0 $2 $3 $4 $5 $6 $7 $8 $9 X,Y pos of 4 corners*
|
||||
0 Add a 4 corners polygon primitive as box body*
|
||||
4,1,4,$2,$3,$4,$5,$6,$7,$8,$9,$2,$3,0*
|
||||
0 Add four circle primitives for the rounded corners*
|
||||
1,1,$1+$1,$2,$3*
|
||||
1,1,$1+$1,$4,$5*
|
||||
1,1,$1+$1,$6,$7*
|
||||
1,1,$1+$1,$8,$9*
|
||||
0 Add four rect primitives between the rounded corners*
|
||||
20,1,$1+$1,$2,$3,$4,$5,0*
|
||||
20,1,$1+$1,$4,$5,$6,$7,0*
|
||||
20,1,$1+$1,$6,$7,$8,$9,0*
|
||||
20,1,$1+$1,$8,$9,$2,$3,0*%
|
||||
%AMRotRect*
|
||||
0 Rectangle, with rotation*
|
||||
0 The origin of the aperture is its center*
|
||||
0 $1 length*
|
||||
0 $2 width*
|
||||
0 $3 Rotation angle, in degrees counterclockwise*
|
||||
0 Add horizontal line*
|
||||
21,1,$1,$2,0,0,$3*%
|
||||
%AMFreePoly0*
|
||||
4,1,48,0.206759,0.287330,0.222702,0.287330,0.235600,0.277958,0.250763,0.273032,0.260133,0.260133,0.273032,0.250763,0.277958,0.235600,0.287330,0.222702,0.287330,0.206759,0.292257,0.191596,0.292257,-0.108205,0.289747,-0.115928,0.291018,-0.123951,0.280315,-0.144955,0.273032,-0.167372,0.266461,-0.172145,0.262774,-0.179383,0.179383,-0.262774,0.172145,-0.266461,0.167372,-0.273032,
|
||||
0.144953,-0.280316,0.123952,-0.291017,0.115930,-0.289746,0.108205,-0.292257,-0.191596,-0.292257,-0.206759,-0.287330,-0.222702,-0.287330,-0.235600,-0.277958,-0.250763,-0.273032,-0.260133,-0.260133,-0.273032,-0.250763,-0.277958,-0.235600,-0.287330,-0.222702,-0.287330,-0.206759,-0.292257,-0.191596,-0.292257,0.191596,-0.287330,0.206759,-0.287330,0.222702,-0.277958,0.235600,-0.273032,0.250763,
|
||||
-0.260133,0.260133,-0.250763,0.273032,-0.235600,0.277958,-0.222702,0.287330,-0.206759,0.287330,-0.191596,0.292257,0.191596,0.292257,0.206759,0.287330,0.206759,0.287330,$1*%
|
||||
%AMFreePoly1*
|
||||
4,1,51,0.147653,0.402231,0.149784,0.402802,0.153047,0.401280,0.170637,0.398179,0.184320,0.386697,0.200509,0.379148,0.268292,0.311365,0.271204,0.307206,0.273115,0.306103,0.274347,0.302717,0.284591,0.288088,0.286147,0.270294,0.292257,0.253509,0.292257,-0.253509,0.291375,-0.258507,0.291947,-0.260640,0.290423,-0.263906,0.287323,-0.281493,0.275841,-0.295175,0.268292,-0.311365,
|
||||
0.200509,-0.379148,0.196350,-0.382060,0.195247,-0.383971,0.191862,-0.385202,0.177233,-0.395447,0.159438,-0.397003,0.142653,-0.403113,-0.210436,-0.403113,-0.221065,-0.401238,-0.224644,-0.401870,-0.227791,-0.400053,-0.238420,-0.398179,-0.254702,-0.384516,-0.273115,-0.373886,-0.275640,-0.366946,-0.281295,-0.362202,-0.284986,-0.341269,-0.292257,-0.321292,-0.292257,0.321292,-0.290382,0.331921,
|
||||
-0.291014,0.335500,-0.289197,0.338647,-0.287323,0.349276,-0.273660,0.365558,-0.263030,0.383971,-0.256090,0.386496,-0.251346,0.392151,-0.230413,0.395842,-0.210436,0.403113,0.142653,0.403113,0.147653,0.402231,0.147653,0.402231,$1*%
|
||||
%AMFreePoly2*
|
||||
4,1,48,0.115930,0.289746,0.123952,0.291017,0.144953,0.280316,0.167372,0.273032,0.172145,0.266461,0.179383,0.262774,0.262774,0.179383,0.266461,0.172145,0.273032,0.167372,0.280315,0.144955,0.291018,0.123951,0.289747,0.115928,0.292257,0.108205,0.292257,-0.191596,0.287330,-0.206759,0.287330,-0.222702,0.277958,-0.235600,0.273032,-0.250763,0.260133,-0.260133,0.250763,-0.273032,
|
||||
0.235600,-0.277958,0.222702,-0.287330,0.206759,-0.287330,0.191596,-0.292257,-0.191596,-0.292257,-0.206759,-0.287330,-0.222702,-0.287330,-0.235600,-0.277958,-0.250763,-0.273032,-0.260133,-0.260133,-0.273032,-0.250763,-0.277958,-0.235600,-0.287330,-0.222702,-0.287330,-0.206759,-0.292257,-0.191596,-0.292257,0.191596,-0.287330,0.206759,-0.287330,0.222702,-0.277958,0.235600,-0.273032,0.250763,
|
||||
-0.260133,0.260133,-0.250763,0.273032,-0.235600,0.277958,-0.222702,0.287330,-0.206759,0.287330,-0.191596,0.292257,0.108205,0.292257,0.115930,0.289746,0.115930,0.289746,$1*%
|
||||
%AMFreePoly3*
|
||||
4,1,51,0.331921,0.290382,0.335500,0.291014,0.338647,0.289197,0.349276,0.287323,0.365558,0.273660,0.383971,0.263030,0.386496,0.256090,0.392151,0.251346,0.395842,0.230413,0.403113,0.210436,0.403113,-0.142653,0.402231,-0.147651,0.402803,-0.149784,0.401279,-0.153050,0.398179,-0.170637,0.386697,-0.184319,0.379148,-0.200509,0.311365,-0.268292,0.307206,-0.271204,0.306103,-0.273115,
|
||||
0.302718,-0.274346,0.288089,-0.284591,0.270294,-0.286147,0.253509,-0.292257,-0.253509,-0.292257,-0.258507,-0.291375,-0.260640,-0.291947,-0.263906,-0.290423,-0.281493,-0.287323,-0.295175,-0.275841,-0.311365,-0.268292,-0.379148,-0.200509,-0.382060,-0.196350,-0.383971,-0.195247,-0.385202,-0.191862,-0.395447,-0.177233,-0.397003,-0.159438,-0.403113,-0.142653,-0.403113,0.210436,-0.401238,0.221065,
|
||||
-0.401870,0.224644,-0.400053,0.227791,-0.398179,0.238420,-0.384516,0.254702,-0.373886,0.273115,-0.366946,0.275640,-0.362202,0.281295,-0.341269,0.284986,-0.321292,0.292257,0.321292,0.292257,0.331921,0.290382,0.331921,0.290382,$1*%
|
||||
%AMFreePoly4*
|
||||
4,1,51,0.258509,0.291375,0.260640,0.291946,0.263903,0.290424,0.281493,0.287323,0.295176,0.275841,0.311365,0.268292,0.379148,0.200509,0.382060,0.196350,0.383971,0.195247,0.385203,0.191861,0.395447,0.177232,0.397003,0.159438,0.403113,0.142653,0.403113,-0.210436,0.401238,-0.221065,0.401870,-0.224644,0.400053,-0.227791,0.398179,-0.238420,0.384516,-0.254702,0.373886,-0.273115,
|
||||
0.366946,-0.275640,0.362202,-0.281295,0.341269,-0.284986,0.321292,-0.292257,-0.321292,-0.292257,-0.331921,-0.290382,-0.335500,-0.291014,-0.338647,-0.289197,-0.349276,-0.287323,-0.365558,-0.273660,-0.383971,-0.263030,-0.386496,-0.256090,-0.392151,-0.251346,-0.395842,-0.230413,-0.403113,-0.210436,-0.403113,0.142653,-0.402231,0.147653,-0.402802,0.149784,-0.401280,0.153047,-0.398179,0.170637,
|
||||
-0.386697,0.184320,-0.379148,0.200509,-0.311365,0.268292,-0.307206,0.271204,-0.306103,0.273115,-0.302717,0.274347,-0.288088,0.284591,-0.270294,0.286147,-0.253509,0.292257,0.253509,0.292257,0.258509,0.291375,0.258509,0.291375,$1*%
|
||||
%AMFreePoly5*
|
||||
4,1,48,0.206759,0.287330,0.222702,0.287330,0.235600,0.277958,0.250763,0.273032,0.260133,0.260133,0.273032,0.250763,0.277958,0.235600,0.287330,0.222702,0.287330,0.206759,0.292257,0.191596,0.292257,-0.191596,0.287330,-0.206759,0.287330,-0.222702,0.277958,-0.235600,0.273032,-0.250763,0.260133,-0.260133,0.250763,-0.273032,0.235600,-0.277958,0.222702,-0.287330,0.206759,-0.287330,
|
||||
0.191596,-0.292257,-0.108205,-0.292257,-0.115928,-0.289747,-0.123951,-0.291018,-0.144955,-0.280315,-0.167372,-0.273032,-0.172145,-0.266461,-0.179383,-0.262774,-0.262774,-0.179383,-0.266461,-0.172145,-0.273032,-0.167372,-0.280316,-0.144953,-0.291017,-0.123952,-0.289746,-0.115930,-0.292257,-0.108205,-0.292257,0.191596,-0.287330,0.206759,-0.287330,0.222702,-0.277958,0.235600,-0.273032,0.250763,
|
||||
-0.260133,0.260133,-0.250763,0.273032,-0.235600,0.277958,-0.222702,0.287330,-0.206759,0.287330,-0.191596,0.292257,0.191596,0.292257,0.206759,0.287330,0.206759,0.287330,$1*%
|
||||
%AMFreePoly6*
|
||||
4,1,51,0.221065,0.401238,0.224644,0.401870,0.227791,0.400053,0.238420,0.398179,0.254702,0.384516,0.273115,0.373886,0.275640,0.366946,0.281295,0.362202,0.284986,0.341269,0.292257,0.321292,0.292257,-0.321292,0.290382,-0.331921,0.291014,-0.335500,0.289197,-0.338647,0.287323,-0.349276,0.273660,-0.365558,0.263030,-0.383971,0.256090,-0.386496,0.251346,-0.392151,0.230413,-0.395842,
|
||||
0.210436,-0.403113,-0.142653,-0.403113,-0.147651,-0.402231,-0.149784,-0.402803,-0.153050,-0.401279,-0.170637,-0.398179,-0.184319,-0.386697,-0.200509,-0.379148,-0.268292,-0.311365,-0.271204,-0.307206,-0.273115,-0.306103,-0.274346,-0.302718,-0.284591,-0.288089,-0.286147,-0.270294,-0.292257,-0.253509,-0.292257,0.253509,-0.291375,0.258509,-0.291946,0.260640,-0.290424,0.263903,-0.287323,0.281493,
|
||||
-0.275841,0.295176,-0.268292,0.311365,-0.200509,0.379148,-0.196350,0.382060,-0.195247,0.383971,-0.191861,0.385203,-0.177232,0.395447,-0.159438,0.397003,-0.142653,0.403113,0.210436,0.403113,0.221065,0.401238,0.221065,0.401238,$1*%
|
||||
%AMFreePoly7*
|
||||
4,1,48,0.206759,0.287330,0.222702,0.287330,0.235600,0.277958,0.250763,0.273032,0.260133,0.260133,0.273032,0.250763,0.277958,0.235600,0.287330,0.222702,0.287330,0.206759,0.292257,0.191596,0.292257,-0.191596,0.287330,-0.206759,0.287330,-0.222702,0.277958,-0.235600,0.273032,-0.250763,0.260133,-0.260133,0.250763,-0.273032,0.235600,-0.277958,0.222702,-0.287330,0.206759,-0.287330,
|
||||
0.191596,-0.292257,-0.191596,-0.292257,-0.206759,-0.287330,-0.222702,-0.287330,-0.235600,-0.277958,-0.250763,-0.273032,-0.260133,-0.260133,-0.273032,-0.250763,-0.277958,-0.235600,-0.287330,-0.222702,-0.287330,-0.206759,-0.292257,-0.191596,-0.292257,0.108205,-0.289746,0.115930,-0.291017,0.123952,-0.280316,0.144953,-0.273032,0.167372,-0.266461,0.172145,-0.262774,0.179383,-0.179383,0.262774,
|
||||
-0.172145,0.266461,-0.167372,0.273032,-0.144955,0.280315,-0.123951,0.291018,-0.115928,0.289747,-0.108205,0.292257,0.191596,0.292257,0.206759,0.287330,0.206759,0.287330,$1*%
|
||||
G04 Aperture macros list end*
|
||||
%ADD10RotRect,0.500000X0.400000X240.000000*%
|
||||
%ADD11RotRect,0.500000X0.400000X162.000000*%
|
||||
%ADD12RotRect,0.500000X0.400000X216.000000*%
|
||||
%ADD13RotRect,0.500000X0.400000X138.000000*%
|
||||
%ADD14RotRect,0.500000X0.400000X348.000000*%
|
||||
%ADD15RotRect,0.500000X0.400000X114.000000*%
|
||||
%ADD16RotRect,0.500000X0.400000X72.000000*%
|
||||
%ADD17RotRect,0.500000X0.400000X78.000000*%
|
||||
%ADD18C,1.500000*%
|
||||
%ADD19RotRect,0.500000X0.400000X108.000000*%
|
||||
%ADD20RotRect,0.500000X0.400000X120.000000*%
|
||||
%ADD21R,0.600000X1.200000*%
|
||||
%ADD22RotRect,0.500000X0.400000X330.000000*%
|
||||
%ADD23RoundRect,0.243750X0.456250X-0.243750X0.456250X0.243750X-0.456250X0.243750X-0.456250X-0.243750X0*%
|
||||
%ADD24RoundRect,0.243750X-0.516999X-0.017031X-0.273249X-0.439219X0.516999X0.017031X0.273249X0.439219X0*%
|
||||
%ADD25RoundRect,0.243750X-0.456250X0.243750X-0.456250X-0.243750X0.456250X-0.243750X0.456250X0.243750X0*%
|
||||
%ADD26RotRect,0.500000X0.400000X312.000000*%
|
||||
%ADD27RotRect,0.500000X0.400000X276.000000*%
|
||||
%ADD28RotRect,0.500000X0.400000X66.000000*%
|
||||
%ADD29RotRect,0.500000X0.400000X54.000000*%
|
||||
%ADD30RotRect,0.500000X0.400000X96.000000*%
|
||||
%ADD31RotRect,0.500000X0.400000X204.000000*%
|
||||
%ADD32RotRect,0.500000X0.400000X264.000000*%
|
||||
%ADD33RoundRect,0.243750X-0.243750X-0.456250X0.243750X-0.456250X0.243750X0.456250X-0.243750X0.456250X0*%
|
||||
%ADD34RoundRect,0.243750X-0.273249X0.439219X-0.516999X0.017031X0.273249X-0.439219X0.516999X-0.017031X0*%
|
||||
%ADD35RoundRect,0.243750X0.516999X0.017031X0.273249X0.439219X-0.516999X-0.017031X-0.273249X-0.439219X0*%
|
||||
%ADD36RotRect,0.500000X0.400000X102.000000*%
|
||||
%ADD37RoundRect,0.243750X0.273249X-0.439219X0.516999X-0.017031X-0.273249X0.439219X-0.516999X0.017031X0*%
|
||||
%ADD38RotRect,0.500000X0.400000X48.000000*%
|
||||
%ADD39FreePoly0,0.000000*%
|
||||
%ADD40FreePoly1,0.000000*%
|
||||
%ADD41FreePoly2,0.000000*%
|
||||
%ADD42FreePoly3,0.000000*%
|
||||
%ADD43RoundRect,0.201557X-0.201556X-0.201556X0.201556X-0.201556X0.201556X0.201556X-0.201556X0.201556X0*%
|
||||
%ADD44FreePoly4,0.000000*%
|
||||
%ADD45FreePoly5,0.000000*%
|
||||
%ADD46FreePoly6,0.000000*%
|
||||
%ADD47FreePoly7,0.000000*%
|
||||
%ADD48RoundRect,0.062500X-0.375000X-0.062500X0.375000X-0.062500X0.375000X0.062500X-0.375000X0.062500X0*%
|
||||
%ADD49RoundRect,0.062500X-0.062500X-0.375000X0.062500X-0.375000X0.062500X0.375000X-0.062500X0.375000X0*%
|
||||
%ADD50RotRect,0.500000X0.400000X192.000000*%
|
||||
%ADD51R,0.400000X0.500000*%
|
||||
%ADD52RotRect,0.500000X0.400000X12.000000*%
|
||||
%ADD53RotRect,0.500000X0.400000X234.000000*%
|
||||
%ADD54RotRect,0.500000X0.400000X42.000000*%
|
||||
%ADD55RotRect,0.500000X0.400000X294.000000*%
|
||||
%ADD56RoundRect,0.243750X0.017031X-0.516999X0.439219X-0.273249X-0.017031X0.516999X-0.439219X0.273249X0*%
|
||||
%ADD57RotRect,0.500000X0.400000X24.000000*%
|
||||
%ADD58RotRect,0.500000X0.400000X144.000000*%
|
||||
%ADD59RotRect,0.500000X0.400000X168.000000*%
|
||||
%ADD60RotRect,0.500000X0.400000X6.000000*%
|
||||
%ADD61RotRect,0.500000X0.400000X258.000000*%
|
||||
%ADD62R,0.500000X0.400000*%
|
||||
%ADD63RotRect,0.500000X0.400000X150.000000*%
|
||||
%ADD64RotRect,0.500000X0.400000X228.000000*%
|
||||
%ADD65RotRect,0.500000X0.400000X36.000000*%
|
||||
%ADD66RotRect,0.500000X0.400000X306.000000*%
|
||||
%ADD67RotRect,0.500000X0.400000X30.000000*%
|
||||
%ADD68RotRect,0.500000X0.400000X246.000000*%
|
||||
%ADD69RotRect,0.500000X0.400000X18.000000*%
|
||||
%ADD70RoundRect,0.243750X-0.439219X-0.273249X-0.017031X-0.516999X0.439219X0.273249X0.017031X0.516999X0*%
|
||||
%ADD71RoundRect,0.243750X0.243750X0.456250X-0.243750X0.456250X-0.243750X-0.456250X0.243750X-0.456250X0*%
|
||||
%ADD72RotRect,0.500000X0.400000X174.000000*%
|
||||
%ADD73RotRect,0.500000X0.400000X342.000000*%
|
||||
%ADD74RotRect,0.500000X0.400000X282.000000*%
|
||||
%ADD75RotRect,0.500000X0.400000X186.000000*%
|
||||
%ADD76RotRect,0.500000X0.400000X354.000000*%
|
||||
%ADD77RotRect,0.500000X0.400000X156.000000*%
|
||||
%ADD78RoundRect,0.243750X0.439219X0.273249X0.017031X0.516999X-0.439219X-0.273249X-0.017031X-0.516999X0*%
|
||||
%ADD79RotRect,0.500000X0.400000X252.000000*%
|
||||
%ADD80RotRect,0.500000X0.400000X288.000000*%
|
||||
%ADD81RotRect,0.500000X0.400000X84.000000*%
|
||||
%ADD82RotRect,0.500000X0.400000X198.000000*%
|
||||
%ADD83RotRect,0.500000X0.400000X132.000000*%
|
||||
%ADD84RotRect,0.500000X0.400000X336.000000*%
|
||||
%ADD85RotRect,0.500000X0.400000X210.000000*%
|
||||
%ADD86RotRect,0.500000X0.400000X324.000000*%
|
||||
%ADD87RotRect,0.500000X0.400000X60.000000*%
|
||||
%ADD88RotRect,0.500000X0.400000X318.000000*%
|
||||
%ADD89RotRect,0.500000X0.400000X126.000000*%
|
||||
%ADD90RoundRect,0.243750X-0.017031X0.516999X-0.439219X0.273249X0.017031X-0.516999X0.439219X-0.273249X0*%
|
||||
%ADD91RotRect,0.500000X0.400000X222.000000*%
|
||||
%ADD92RotRect,0.500000X0.400000X300.000000*%
|
||||
G04 APERTURE END LIST*
|
||||
D10*
|
||||
%TO.C,D36*%
|
||||
X154769604Y-27385471D03*
|
||||
X155369604Y-26346241D03*
|
||||
X155375822Y-27735471D03*
|
||||
X155975822Y-26696241D03*
|
||||
%TD*%
|
||||
D11*
|
||||
%TO.C,D49*%
|
||||
X161112235Y-42679576D03*
|
||||
X162253503Y-43050396D03*
|
||||
X160895923Y-43345316D03*
|
||||
X162037191Y-43716136D03*
|
||||
%TD*%
|
||||
D12*
|
||||
%TO.C,D40*%
|
||||
X158930578Y-30936371D03*
|
||||
X159901398Y-30231029D03*
|
||||
X159342028Y-31502683D03*
|
||||
X160312848Y-30797341D03*
|
||||
%TD*%
|
||||
D13*
|
||||
%TO.C,D53*%
|
||||
X158504022Y-47488277D03*
|
||||
X159395796Y-48291234D03*
|
||||
X158035630Y-48008478D03*
|
||||
X158927404Y-48811435D03*
|
||||
%TD*%
|
||||
D14*
|
||||
%TO.C,D18*%
|
||||
X135561832Y-36556955D03*
|
||||
X134388055Y-36307461D03*
|
||||
X135707371Y-35872251D03*
|
||||
X134533594Y-35622757D03*
|
||||
%TD*%
|
||||
D15*
|
||||
%TO.C,D57*%
|
||||
X154166412Y-50819371D03*
|
||||
X154654496Y-51915625D03*
|
||||
X153526930Y-51104087D03*
|
||||
X154015014Y-52200341D03*
|
||||
%TD*%
|
||||
D16*
|
||||
%TO.C,D4*%
|
||||
X144766993Y-51563378D03*
|
||||
X144396173Y-52704646D03*
|
||||
X144101253Y-51347066D03*
|
||||
X143730433Y-52488334D03*
|
||||
%TD*%
|
||||
D17*
|
||||
%TO.C,D3*%
|
||||
X146105812Y-51884737D03*
|
||||
X145856318Y-53058514D03*
|
||||
X145421108Y-51739198D03*
|
||||
X145171614Y-52912975D03*
|
||||
%TD*%
|
||||
D18*
|
||||
%TO.C,KiKit_TO_1*%
|
||||
X132052287Y-22500000D03*
|
||||
%TD*%
|
||||
D19*
|
||||
%TO.C,D58*%
|
||||
X152894173Y-51347066D03*
|
||||
X153264993Y-52488334D03*
|
||||
X152228433Y-51563378D03*
|
||||
X152599253Y-52704646D03*
|
||||
%TD*%
|
||||
D20*
|
||||
%TO.C,D56*%
|
||||
X155375822Y-50162241D03*
|
||||
X155975822Y-51201471D03*
|
||||
X154769604Y-50512241D03*
|
||||
X155369604Y-51551471D03*
|
||||
%TD*%
|
||||
D18*
|
||||
%TO.C,KiKit_TO_3*%
|
||||
X132052287Y-55397712D03*
|
||||
%TD*%
|
||||
D21*
|
||||
%TO.C,Y1*%
|
||||
X149707713Y-35148856D03*
|
||||
X150907713Y-35148856D03*
|
||||
%TD*%
|
||||
D22*
|
||||
%TO.C,D21*%
|
||||
X136934328Y-32676965D03*
|
||||
X135895098Y-32076965D03*
|
||||
X137284328Y-32070747D03*
|
||||
X136245098Y-31470747D03*
|
||||
%TD*%
|
||||
D23*
|
||||
%TO.C,HD4*%
|
||||
X160147713Y-39886356D03*
|
||||
X160147713Y-38011356D03*
|
||||
%TD*%
|
||||
D24*
|
||||
%TO.C,HD11*%
|
||||
X137939963Y-43961957D03*
|
||||
X138877463Y-45585755D03*
|
||||
%TD*%
|
||||
D25*
|
||||
%TO.C,HD10*%
|
||||
X136847713Y-38011356D03*
|
||||
X136847713Y-39886356D03*
|
||||
%TD*%
|
||||
D26*
|
||||
%TO.C,D24*%
|
||||
X139438091Y-29410939D03*
|
||||
X138635134Y-28519165D03*
|
||||
X139958292Y-28942547D03*
|
||||
X139155335Y-28050773D03*
|
||||
%TD*%
|
||||
D27*
|
||||
%TO.C,D30*%
|
||||
X146775347Y-25907154D03*
|
||||
X146649913Y-24713728D03*
|
||||
X147471513Y-25833984D03*
|
||||
X147346079Y-24640558D03*
|
||||
%TD*%
|
||||
D28*
|
||||
%TO.C,D5*%
|
||||
X143468496Y-51104087D03*
|
||||
X142980412Y-52200341D03*
|
||||
X142829014Y-50819371D03*
|
||||
X142340930Y-51915625D03*
|
||||
%TD*%
|
||||
D29*
|
||||
%TO.C,D7*%
|
||||
X141051540Y-49793171D03*
|
||||
X140346198Y-50763991D03*
|
||||
X140485228Y-49381721D03*
|
||||
X139779886Y-50352541D03*
|
||||
%TD*%
|
||||
D30*
|
||||
%TO.C,D60*%
|
||||
X150220079Y-51990558D03*
|
||||
X150345513Y-53183984D03*
|
||||
X149523913Y-52063728D03*
|
||||
X149649347Y-53257154D03*
|
||||
%TD*%
|
||||
D31*
|
||||
%TO.C,D42*%
|
||||
X160368228Y-33280157D03*
|
||||
X161464482Y-32792073D03*
|
||||
X160652944Y-33919639D03*
|
||||
X161749198Y-33431555D03*
|
||||
%TD*%
|
||||
D32*
|
||||
%TO.C,D32*%
|
||||
X149523913Y-25833984D03*
|
||||
X149649347Y-24640558D03*
|
||||
X150220079Y-25907154D03*
|
||||
X150345513Y-24713728D03*
|
||||
%TD*%
|
||||
D33*
|
||||
%TO.C,HD1*%
|
||||
X147560213Y-50598856D03*
|
||||
X149435213Y-50598856D03*
|
||||
%TD*%
|
||||
D34*
|
||||
%TO.C,HD9*%
|
||||
X138877463Y-32311957D03*
|
||||
X137939963Y-33935755D03*
|
||||
%TD*%
|
||||
D35*
|
||||
%TO.C,HD5*%
|
||||
X159055463Y-33935755D03*
|
||||
X158117963Y-32311957D03*
|
||||
%TD*%
|
||||
D36*
|
||||
%TO.C,D59*%
|
||||
X151574318Y-51739198D03*
|
||||
X151823812Y-52912975D03*
|
||||
X150889614Y-51884737D03*
|
||||
X151139108Y-53058514D03*
|
||||
%TD*%
|
||||
D37*
|
||||
%TO.C,HD3*%
|
||||
X158117963Y-45585755D03*
|
||||
X159055463Y-43961957D03*
|
||||
%TD*%
|
||||
D38*
|
||||
%TO.C,D8*%
|
||||
X139958292Y-48955165D03*
|
||||
X139155335Y-49846939D03*
|
||||
X139438091Y-48486773D03*
|
||||
X138635134Y-49378547D03*
|
||||
%TD*%
|
||||
D39*
|
||||
%TO.C,U2*%
|
||||
X147135213Y-37736356D03*
|
||||
D40*
|
||||
X147135213Y-38598856D03*
|
||||
X147135213Y-39598856D03*
|
||||
D41*
|
||||
X147135213Y-40461356D03*
|
||||
D42*
|
||||
X147997713Y-37736356D03*
|
||||
D43*
|
||||
X147997713Y-38598856D03*
|
||||
X147997713Y-39598856D03*
|
||||
D44*
|
||||
X147997713Y-40461356D03*
|
||||
D42*
|
||||
X148997713Y-37736356D03*
|
||||
D43*
|
||||
X148997713Y-38598856D03*
|
||||
X148997713Y-39598856D03*
|
||||
D44*
|
||||
X148997713Y-40461356D03*
|
||||
D45*
|
||||
X149860213Y-37736356D03*
|
||||
D46*
|
||||
X149860213Y-38598856D03*
|
||||
X149860213Y-39598856D03*
|
||||
D47*
|
||||
X149860213Y-40461356D03*
|
||||
D48*
|
||||
X146060213Y-37348856D03*
|
||||
X146060213Y-37848856D03*
|
||||
X146060213Y-38348856D03*
|
||||
X146060213Y-38848856D03*
|
||||
X146060213Y-39348856D03*
|
||||
X146060213Y-39848856D03*
|
||||
X146060213Y-40348856D03*
|
||||
X146060213Y-40848856D03*
|
||||
D49*
|
||||
X146747713Y-41536356D03*
|
||||
X147247713Y-41536356D03*
|
||||
X147747713Y-41536356D03*
|
||||
X148247713Y-41536356D03*
|
||||
X148747713Y-41536356D03*
|
||||
X149247713Y-41536356D03*
|
||||
X149747713Y-41536356D03*
|
||||
X150247713Y-41536356D03*
|
||||
D48*
|
||||
X150935213Y-40848856D03*
|
||||
X150935213Y-40348856D03*
|
||||
X150935213Y-39848856D03*
|
||||
X150935213Y-39348856D03*
|
||||
X150935213Y-38848856D03*
|
||||
X150935213Y-38348856D03*
|
||||
X150935213Y-37848856D03*
|
||||
X150935213Y-37348856D03*
|
||||
D49*
|
||||
X150247713Y-36661356D03*
|
||||
X149747713Y-36661356D03*
|
||||
X149247713Y-36661356D03*
|
||||
X148747713Y-36661356D03*
|
||||
X148247713Y-36661356D03*
|
||||
X147747713Y-36661356D03*
|
||||
X147247713Y-36661356D03*
|
||||
X146747713Y-36661356D03*
|
||||
%TD*%
|
||||
D18*
|
||||
%TO.C,KiKit_TO_2*%
|
||||
X164947713Y-22500000D03*
|
||||
%TD*%
|
||||
D50*
|
||||
%TO.C,D44*%
|
||||
X161288055Y-35872251D03*
|
||||
X162461832Y-35622757D03*
|
||||
X161433594Y-36556955D03*
|
||||
X162607371Y-36307461D03*
|
||||
%TD*%
|
||||
D51*
|
||||
%TO.C,D1*%
|
||||
X148847713Y-52098856D03*
|
||||
X148847713Y-53298856D03*
|
||||
X148147713Y-52098856D03*
|
||||
X148147713Y-53298856D03*
|
||||
%TD*%
|
||||
D52*
|
||||
%TO.C,D14*%
|
||||
X135707371Y-42025461D03*
|
||||
X134533594Y-42274955D03*
|
||||
X135561832Y-41340757D03*
|
||||
X134388055Y-41590251D03*
|
||||
%TD*%
|
||||
D53*
|
||||
%TO.C,D37*%
|
||||
X155943886Y-28104541D03*
|
||||
X156649228Y-27133721D03*
|
||||
X156510198Y-28515991D03*
|
||||
X157215540Y-27545171D03*
|
||||
%TD*%
|
||||
D54*
|
||||
%TO.C,D9*%
|
||||
X138959796Y-48008478D03*
|
||||
X138068022Y-48811435D03*
|
||||
X138491404Y-47488277D03*
|
||||
X137599630Y-48291234D03*
|
||||
%TD*%
|
||||
D55*
|
||||
%TO.C,D27*%
|
||||
X142829014Y-27078341D03*
|
||||
X142340930Y-25982087D03*
|
||||
X143468496Y-26793625D03*
|
||||
X142980412Y-25697371D03*
|
||||
%TD*%
|
||||
D56*
|
||||
%TO.C,HD2*%
|
||||
X153510814Y-49506606D03*
|
||||
X155134612Y-48569106D03*
|
||||
%TD*%
|
||||
D57*
|
||||
%TO.C,D12*%
|
||||
X136627198Y-44617555D03*
|
||||
X135530944Y-45105639D03*
|
||||
X136342482Y-43978073D03*
|
||||
X135246228Y-44466157D03*
|
||||
%TD*%
|
||||
D58*
|
||||
%TO.C,D52*%
|
||||
X159342028Y-46395029D03*
|
||||
X160312848Y-47100371D03*
|
||||
X158930578Y-46961341D03*
|
||||
X159901398Y-47666683D03*
|
||||
%TD*%
|
||||
D59*
|
||||
%TO.C,D48*%
|
||||
X161433594Y-41340757D03*
|
||||
X162607371Y-41590251D03*
|
||||
X161288055Y-42025461D03*
|
||||
X162461832Y-42274955D03*
|
||||
%TD*%
|
||||
D60*
|
||||
%TO.C,D15*%
|
||||
X135456011Y-40671222D03*
|
||||
X134262585Y-40796656D03*
|
||||
X135382841Y-39975056D03*
|
||||
X134189415Y-40100490D03*
|
||||
%TD*%
|
||||
D61*
|
||||
%TO.C,D33*%
|
||||
X150889614Y-26012975D03*
|
||||
X151139108Y-24839198D03*
|
||||
X151574318Y-26158514D03*
|
||||
X151823812Y-24984737D03*
|
||||
%TD*%
|
||||
D62*
|
||||
%TO.C,D16*%
|
||||
X135347713Y-39298856D03*
|
||||
X134147713Y-39298856D03*
|
||||
X135347713Y-38598856D03*
|
||||
X134147713Y-38598856D03*
|
||||
%TD*%
|
||||
D63*
|
||||
%TO.C,D51*%
|
||||
X160061098Y-45220747D03*
|
||||
X161100328Y-45820747D03*
|
||||
X159711098Y-45826965D03*
|
||||
X160750328Y-46426965D03*
|
||||
%TD*%
|
||||
D64*
|
||||
%TO.C,D38*%
|
||||
X157037134Y-28942547D03*
|
||||
X157840091Y-28050773D03*
|
||||
X157557335Y-29410939D03*
|
||||
X158360292Y-28519165D03*
|
||||
%TD*%
|
||||
D65*
|
||||
%TO.C,D10*%
|
||||
X138064848Y-46961341D03*
|
||||
X137094028Y-47666683D03*
|
||||
X137653398Y-46395029D03*
|
||||
X136682578Y-47100371D03*
|
||||
%TD*%
|
||||
D66*
|
||||
%TO.C,D25*%
|
||||
X140485228Y-28515991D03*
|
||||
X139779886Y-27545171D03*
|
||||
X141051540Y-28104541D03*
|
||||
X140346198Y-27133721D03*
|
||||
%TD*%
|
||||
D67*
|
||||
%TO.C,D11*%
|
||||
X137284328Y-45826965D03*
|
||||
X136245098Y-46426965D03*
|
||||
X136934328Y-45220747D03*
|
||||
X135895098Y-45820747D03*
|
||||
%TD*%
|
||||
D68*
|
||||
%TO.C,D35*%
|
||||
X153526930Y-26793625D03*
|
||||
X154015014Y-25697371D03*
|
||||
X154166412Y-27078341D03*
|
||||
X154654496Y-25982087D03*
|
||||
%TD*%
|
||||
D69*
|
||||
%TO.C,D13*%
|
||||
X136099503Y-43345316D03*
|
||||
X134958235Y-43716136D03*
|
||||
X135883191Y-42679576D03*
|
||||
X134741923Y-43050396D03*
|
||||
%TD*%
|
||||
D70*
|
||||
%TO.C,HD12*%
|
||||
X141860814Y-48569106D03*
|
||||
X143484612Y-49506606D03*
|
||||
%TD*%
|
||||
D71*
|
||||
%TO.C,HD7*%
|
||||
X149435213Y-27298856D03*
|
||||
X147560213Y-27298856D03*
|
||||
%TD*%
|
||||
D72*
|
||||
%TO.C,D47*%
|
||||
X161612585Y-39975056D03*
|
||||
X162806011Y-40100490D03*
|
||||
X161539415Y-40671222D03*
|
||||
X162732841Y-40796656D03*
|
||||
%TD*%
|
||||
D51*
|
||||
%TO.C,D31*%
|
||||
X148147713Y-25798856D03*
|
||||
X148147713Y-24598856D03*
|
||||
X148847713Y-25798856D03*
|
||||
X148847713Y-24598856D03*
|
||||
%TD*%
|
||||
D73*
|
||||
%TO.C,D19*%
|
||||
X135883191Y-35218136D03*
|
||||
X134741923Y-34847316D03*
|
||||
X136099503Y-34552396D03*
|
||||
X134958235Y-34181576D03*
|
||||
%TD*%
|
||||
D74*
|
||||
%TO.C,D29*%
|
||||
X145421108Y-26158514D03*
|
||||
X145171614Y-24984737D03*
|
||||
X146105812Y-26012975D03*
|
||||
X145856318Y-24839198D03*
|
||||
%TD*%
|
||||
D75*
|
||||
%TO.C,D45*%
|
||||
X161539415Y-37226490D03*
|
||||
X162732841Y-37101056D03*
|
||||
X161612585Y-37922656D03*
|
||||
X162806011Y-37797222D03*
|
||||
%TD*%
|
||||
D76*
|
||||
%TO.C,D17*%
|
||||
X135382841Y-37922656D03*
|
||||
X134189415Y-37797222D03*
|
||||
X135456011Y-37226490D03*
|
||||
X134262585Y-37101056D03*
|
||||
%TD*%
|
||||
D62*
|
||||
%TO.C,D46*%
|
||||
X161647713Y-38598856D03*
|
||||
X162847713Y-38598856D03*
|
||||
X161647713Y-39298856D03*
|
||||
X162847713Y-39298856D03*
|
||||
%TD*%
|
||||
D77*
|
||||
%TO.C,D50*%
|
||||
X160652944Y-43978073D03*
|
||||
X161749198Y-44466157D03*
|
||||
X160368228Y-44617555D03*
|
||||
X161464482Y-45105639D03*
|
||||
%TD*%
|
||||
D78*
|
||||
%TO.C,HD6*%
|
||||
X155134612Y-29328606D03*
|
||||
X153510814Y-28391106D03*
|
||||
%TD*%
|
||||
D79*
|
||||
%TO.C,D34*%
|
||||
X152228433Y-26334334D03*
|
||||
X152599253Y-25193066D03*
|
||||
X152894173Y-26550646D03*
|
||||
X153264993Y-25409378D03*
|
||||
%TD*%
|
||||
D80*
|
||||
%TO.C,D28*%
|
||||
X144101253Y-26550646D03*
|
||||
X143730433Y-25409378D03*
|
||||
X144766993Y-26334334D03*
|
||||
X144396173Y-25193066D03*
|
||||
%TD*%
|
||||
D81*
|
||||
%TO.C,D2*%
|
||||
X147471513Y-52063728D03*
|
||||
X147346079Y-53257154D03*
|
||||
X146775347Y-51990558D03*
|
||||
X146649913Y-53183984D03*
|
||||
%TD*%
|
||||
D82*
|
||||
%TO.C,D43*%
|
||||
X160895923Y-34552396D03*
|
||||
X162037191Y-34181576D03*
|
||||
X161112235Y-35218136D03*
|
||||
X162253503Y-34847316D03*
|
||||
%TD*%
|
||||
D83*
|
||||
%TO.C,D54*%
|
||||
X157557335Y-48486773D03*
|
||||
X158360292Y-49378547D03*
|
||||
X157037134Y-48955165D03*
|
||||
X157840091Y-49846939D03*
|
||||
%TD*%
|
||||
D84*
|
||||
%TO.C,D20*%
|
||||
X136342482Y-33919639D03*
|
||||
X135246228Y-33431555D03*
|
||||
X136627198Y-33280157D03*
|
||||
X135530944Y-32792073D03*
|
||||
%TD*%
|
||||
D85*
|
||||
%TO.C,D41*%
|
||||
X159711098Y-32070747D03*
|
||||
X160750328Y-31470747D03*
|
||||
X160061098Y-32676965D03*
|
||||
X161100328Y-32076965D03*
|
||||
%TD*%
|
||||
D86*
|
||||
%TO.C,D22*%
|
||||
X137653398Y-31502683D03*
|
||||
X136682578Y-30797341D03*
|
||||
X138064848Y-30936371D03*
|
||||
X137094028Y-30231029D03*
|
||||
%TD*%
|
||||
D87*
|
||||
%TO.C,D6*%
|
||||
X142225822Y-50512241D03*
|
||||
X141625822Y-51551471D03*
|
||||
X141619604Y-50162241D03*
|
||||
X141019604Y-51201471D03*
|
||||
%TD*%
|
||||
D88*
|
||||
%TO.C,D23*%
|
||||
X138491404Y-30409435D03*
|
||||
X137599630Y-29606478D03*
|
||||
X138959796Y-29889234D03*
|
||||
X138068022Y-29086277D03*
|
||||
%TD*%
|
||||
D89*
|
||||
%TO.C,D55*%
|
||||
X156510198Y-49381721D03*
|
||||
X157215540Y-50352541D03*
|
||||
X155943886Y-49793171D03*
|
||||
X156649228Y-50763991D03*
|
||||
%TD*%
|
||||
D90*
|
||||
%TO.C,HD8*%
|
||||
X143484612Y-28391106D03*
|
||||
X141860814Y-29328606D03*
|
||||
%TD*%
|
||||
D91*
|
||||
%TO.C,D39*%
|
||||
X158035630Y-29889234D03*
|
||||
X158927404Y-29086277D03*
|
||||
X158504022Y-30409435D03*
|
||||
X159395796Y-29606478D03*
|
||||
%TD*%
|
||||
D92*
|
||||
%TO.C,D26*%
|
||||
X141619604Y-27735471D03*
|
||||
X141019604Y-26696241D03*
|
||||
X142225822Y-27385471D03*
|
||||
X141625822Y-26346241D03*
|
||||
%TD*%
|
||||
M02*
|
||||
File diff suppressed because it is too large
Load diff
8388
tests/resources/zero-length-lines/charliewatch-v2.panel-In1_Cu.gbr
Normal file
8388
tests/resources/zero-length-lines/charliewatch-v2.panel-In1_Cu.gbr
Normal file
File diff suppressed because it is too large
Load diff
11559
tests/resources/zero-length-lines/charliewatch-v2.panel-In2_Cu.gbr
Normal file
11559
tests/resources/zero-length-lines/charliewatch-v2.panel-In2_Cu.gbr
Normal file
File diff suppressed because it is too large
Load diff
12
tests/resources/zero-length-lines/charliewatch-v2.panel.csv
Normal file
12
tests/resources/zero-length-lines/charliewatch-v2.panel.csv
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
"Id";"Designator";"Footprint";"Quantity";"Designation";"Supplier and ref";
|
||||
1;"D36,D49,D40,D53,D18,D57,D4,D3,D58,D56,D21,D24,D30,D5,D7,D60,D42,D32,D59,D8,D44,D1,D14,D37,D9,D27,D12,D52,D48,D15,D33,D16,D51,D38,D10,D25,D11,D35,D13,D47,D31,D19,D29,D45,D17,D46,D50,D34,D28,D2,D43,D54,D20,D41,D22,D6,D23,D55,D39,D26";"LED_APHB1608SYKSURKC";60;"LED_APHB1608SYKSURKC";;;
|
||||
2;"KiKit_MB_3_15,KiKit_TO_1,KiKit_MB_4_13,KiKit_TO_3,KiKit_MB_4_14,KiKit_MB_2_11,KiKit_MB_1_11,KiKit_MB_3_13,KiKit_MB_1_12,KiKit_MB_4_11,KiKit_MB_2_14,KiKit_MB_2_15,KiKit_TO_2,KiKit_MB_3_11,KiKit_MB_1_13,KiKit_MB_4_12,KiKit_MB_2_13,KiKit_MB_1_14,KiKit_MB_3_14,KiKit_MB_2_12,KiKit_MB_4_15,KiKit_MB_3_12,KiKit_MB_1_15";"NPTH";23;"NPTH";;;
|
||||
3;"TST1,RST1,VCC1";"TestPoint_Pad_D1.0mm";3;"TestPoint";;;
|
||||
4;"Y1";"Crystal_SMD_MicroCrystal_CM9V-T1A-2Pin_1.6x1.0mm";1;"32.765 KHz";;;
|
||||
5;"HD4,HD11,HD10,HD1,HD9,HD5,HD3,HD2,HD12,HD7,HD6,HD8";"LED_0805_2012Metric";12;"LED_0805_2012Metric";;;
|
||||
6;"KiKit_FID_T_2,KiKit_FID_T_3,KiKit_FID_T_1,KiKit_FID_B_2,KiKit_FID_B_3,KiKit_FID_B_1";"Fiducial";6;"Fiducial";;;
|
||||
7;"U2";"Texas_S-PVQFN-N32_EP3.45x3.45mm_ThermalVias";1;"MSP430G2153IRHB32";;;
|
||||
8;"GND1";"TestPoint_Pad_1.0x1.0mm";1;"TestPoint";;;
|
||||
9;"BT1";"BatteryHolder_Keystone_3034_1x20mm";1;"Battery_Cell";;;
|
||||
10;"SW1";"SW_SPST_EVQP7A";1;"EVQ-P7A01P";;;
|
||||
11;"R1";"R_0402_1005Metric";1;"10K";;;
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
774
tests/test_aperture_macro.py
Normal file
774
tests/test_aperture_macro.py
Normal file
|
|
@ -0,0 +1,774 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2026 Jan Sebastian Götte <gerbonara@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.
|
||||
#
|
||||
# Based on https://github.com/tracespace/tracespace
|
||||
#
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from contextlib import contextmanager
|
||||
|
||||
from PIL import Image
|
||||
import pytest
|
||||
|
||||
from gerbonara.rs274x import GerberFile
|
||||
from gerbonara.graphic_objects import Line, Arc, Flash, Region
|
||||
from gerbonara.apertures import *
|
||||
from gerbonara import aperture_macros as am
|
||||
from gerbonara.aperture_macros import (
|
||||
ConstantExpression, ParameterExpression, OperatorExpression,
|
||||
NegatedExpression, VariableExpression, UnitExpression,
|
||||
)
|
||||
from gerbonara.aperture_macros.expression import expr
|
||||
from gerbonara.aperture_macros.parse import _parse_expression
|
||||
from gerbonara.cam import FileSettings
|
||||
from gerbonara.utils import MM, Inch, MILLIMETERS_PER_INCH
|
||||
|
||||
from .image_support import svg_soup
|
||||
from .utils import *
|
||||
|
||||
# Short aliases used throughout expression tests
|
||||
C = ConstantExpression
|
||||
P = ParameterExpression
|
||||
|
||||
|
||||
@contextmanager
|
||||
def run_aperture_macro_test(tmpfile, img_support, inst: ApertureMacroInstance, epsilon=1e-3):
|
||||
gbr = GerberFile()
|
||||
|
||||
inst_rot_90 = inst.rotated(math.pi/2)
|
||||
inst_rot_45 = inst.rotated(math.pi/4)
|
||||
inst_rot_neg90 = inst.rotated(-math.pi/2)
|
||||
for x, y in [(0, 0), (0, 10), (10, 0), (10, 10)]:
|
||||
gbr.objects.append(Flash(x=x, y=y, aperture=inst, unit=MM))
|
||||
gbr.objects.append(Flash(x=x, y=20+y, aperture=inst_rot_90, unit=MM))
|
||||
gbr.objects.append(Flash(x=20+x, y=y, aperture=inst_rot_neg90, unit=MM))
|
||||
gbr.objects.append(Flash(x=20+x, y=20+y, aperture=inst_rot_45, unit=MM))
|
||||
|
||||
# inches, to pixel align our SVG output with gerbv's!
|
||||
bounds = (-.5, -.5), (2.0, 2.0) # bottom left, top right
|
||||
|
||||
# The below code is mostly copy-pasted from test_rs274x.py.
|
||||
|
||||
out_svg = tmpfile('SVG Output', '.svg')
|
||||
with open(out_svg, 'w') as f:
|
||||
# Use inch units here to make sure we and gerbv agree on the exact pixel size of the output since both calculate
|
||||
# it from the DPI setting.
|
||||
f.write(str(gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))
|
||||
|
||||
# Reference export via gerber through GerbV
|
||||
out_gbr = tmpfile('GBR Output', '.gbr')
|
||||
gbr.save(out_gbr)
|
||||
|
||||
# NOTE: Instead of having gerbv directly export a PNG, we ask gerbv to output SVG which we then rasterize using
|
||||
# resvg. We have to do this since gerbv's built-in cairo-based PNG export has severe aliasing issues. In contrast,
|
||||
# using resvg for both allows an apples-to-apples comparison of both results.
|
||||
ref_svg = tmpfile('Reference export', '.svg')
|
||||
w, h = bounds[1][0] - bounds[0][0], bounds[1][1] - bounds[0][1]
|
||||
img_support.gerbv_export(out_gbr, ref_svg, origin=bounds[0], size=(w, h), fg='#000000', bg='#ffffff')
|
||||
with svg_soup(ref_svg) as soup:
|
||||
img_support.cleanup_gerbv_svg(soup)
|
||||
|
||||
ref_png = tmpfile('Reference render', '.png')
|
||||
img_support.svg_to_png(ref_svg, ref_png, dpi=300, bg='white')
|
||||
|
||||
out_png = tmpfile('Output render', '.png')
|
||||
img_support.svg_to_png(out_svg, out_png, dpi=300, bg='white')
|
||||
|
||||
mean, _max, hist = img_support.image_difference(ref_png, out_png, diff_out=tmpfile('Difference', '.png'))
|
||||
assert hist[9] < 1
|
||||
assert mean < epsilon
|
||||
assert hist[3:].sum() < epsilon*hist.size
|
||||
|
||||
|
||||
@pytest.mark.parametrize('aperture_type', [
|
||||
lambda: CircleAperture(4.0, unit=MM),
|
||||
lambda: CircleAperture(4.0, hole_dia=1.5, unit=MM),
|
||||
lambda: RectangleAperture(4.0, 3.0, unit=MM),
|
||||
lambda: ObroundAperture(4.0, 2.5, unit=MM),
|
||||
lambda: PolygonAperture(4.0, 6, unit=MM),
|
||||
])
|
||||
def test_macro_conversions(tmpfile, img_support, aperture_type):
|
||||
ap = aperture_type()
|
||||
inst = ap.to_macro()
|
||||
run_aperture_macro_test(tmpfile, img_support, inst)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('params', [(10, 0), (7, 0), (10, 5)])
|
||||
def test_generic_macro_circle(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.circle(*params)
|
||||
# epsilon changed since gerbv approximates circles with cubic splines which ends up pretty wrong at this scale
|
||||
run_aperture_macro_test(tmpfile, img_support, ap, epsilon=1e-2)
|
||||
|
||||
@pytest.mark.parametrize('params', [
|
||||
(10, 10, 0, 0),
|
||||
(10, 5, 0, 0),
|
||||
( 5, 10, 0, 0),
|
||||
(10, 10, 5, 0),
|
||||
(10, 7, 3, 0),
|
||||
(10, 10, 0, math.pi/2),
|
||||
(10, 10, 0, math.pi/3),
|
||||
(10, 5, 0, math.pi/3),
|
||||
( 7, 10, 3, math.pi/3)])
|
||||
def test_generic_macro_rect(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.rect(*params)
|
||||
run_aperture_macro_test(tmpfile, img_support, ap, epsilon=1e-2)
|
||||
|
||||
@pytest.mark.parametrize('params', [
|
||||
(10, 10, 0, 0, 0),
|
||||
(10, 5, 0, 0, 0),
|
||||
( 5, 10, 0, 0, 0),
|
||||
(10, 10, 0, 5, 0),
|
||||
(10, 7, 0, 3, 0),
|
||||
(10, 10, 0, 0, math.pi/2),
|
||||
(10, 10, 0, 0, math.pi/3),
|
||||
(10, 5, 0, 0, math.pi/3),
|
||||
( 7, 10, 0, 3, math.pi/3),
|
||||
(10, 10, 2, 0, 0),
|
||||
(10, 5, 2, 0, 0),
|
||||
( 5, 10, 2, 0, 0),
|
||||
(10, 10, 2, 5, 0),
|
||||
(10, 7, 2, 3, 0),
|
||||
(10, 10, 2, 0, math.pi/2),
|
||||
(10, 10, 2, 0, math.pi/3),
|
||||
(10, 5, 2, 0, math.pi/3),
|
||||
( 7, 10, 2, 3, math.pi/3),
|
||||
])
|
||||
def test_generic_macro_rounded_rect(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.rounded_rect(*params)
|
||||
run_aperture_macro_test(tmpfile, img_support, ap, epsilon=1e-2)
|
||||
|
||||
@pytest.mark.parametrize('params', [
|
||||
(10, 8, 2, 0, 0),
|
||||
(10, 8, 4, 0, 0),
|
||||
( 8, 10, 2, 0, 0),
|
||||
(10, 8, 2, 3, 0),
|
||||
(10, 8, 2, 0, math.pi/2),
|
||||
(10, 8, 2, 0, math.pi/3),
|
||||
(10, 8, 2, 3, math.pi/3),
|
||||
(10, 8, 0, 0, 0), # d=0: degenerate case (rectangle)
|
||||
])
|
||||
def test_generic_macro_isosceles_trapezoid(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.isosceles_trapezoid(*params)
|
||||
run_aperture_macro_test(tmpfile, img_support, ap)
|
||||
|
||||
@pytest.mark.parametrize('params', [
|
||||
# (w, h, d, margin, hole_dia, rotation)
|
||||
(10, 8, 2, 1, 0, 0),
|
||||
(10, 8, 4, 1, 0, 0),
|
||||
(10, 8, 2, 1, 3, 0),
|
||||
(10, 8, 2, 1, 0, math.pi/2),
|
||||
(10, 8, 2, 1, 0, math.pi/3),
|
||||
(10, 8, 2, 1, 3, math.pi/3),
|
||||
])
|
||||
def test_generic_macro_rounded_isosceles_trapezoid(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.rounded_isosceles_trapezoid(*params)
|
||||
run_aperture_macro_test(tmpfile, img_support, ap)
|
||||
|
||||
@pytest.mark.parametrize('params', [
|
||||
# (w, h, hole_dia, rotation), w >= h required
|
||||
(10, 5, 0, 0),
|
||||
( 8, 4, 0, 0),
|
||||
(10, 5, 2, 0),
|
||||
( 7, 7, 0, 0), # w == h: circle
|
||||
(10, 5, 0, math.pi/2),
|
||||
(10, 5, 0, math.pi/3),
|
||||
(10, 5, 2, math.pi/3),
|
||||
])
|
||||
def test_generic_macro_obround(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.obround(*params)
|
||||
run_aperture_macro_test(tmpfile, img_support, ap, epsilon=1e-2)
|
||||
|
||||
@pytest.mark.parametrize('params', [
|
||||
# (n, diameter, hole_dia, rotation)
|
||||
(3, 10, 0, 0),
|
||||
(4, 10, 0, 0),
|
||||
(5, 10, 0, 0),
|
||||
(6, 10, 0, 0),
|
||||
(6, 10, 3, 0),
|
||||
(6, 10, 0, math.pi/6),
|
||||
(5, 10, 0, math.pi/4),
|
||||
(5, 10, 3, math.pi/4),
|
||||
(3, 10, 3, math.pi/3),
|
||||
])
|
||||
def test_generic_macro_polygon(tmpfile, img_support, params):
|
||||
ap = am.GenericMacros.polygon(*params)
|
||||
run_aperture_macro_test(tmpfile, img_support, ap)
|
||||
|
||||
@pytest.mark.parametrize('abc', [(2.0, 1.6, 2.3), (2.2, 1.6, 2.3), (2.1, 1.7, 2.4)])
|
||||
def test_macro_formulas(tmpfile, img_support, abc):
|
||||
@am.ApertureMacro.map()
|
||||
class test_macro:
|
||||
a: float
|
||||
b: float
|
||||
c: float
|
||||
|
||||
def draw(self):
|
||||
d = 1.3
|
||||
yield am.Circle('mm', 0, d, 0, 0)
|
||||
yield am.Circle('mm', 0, d, 2, 0)
|
||||
yield am.Circle('mm', 0, d, 4, 0)
|
||||
yield am.Circle('mm', 0, d, 2, self.a)
|
||||
yield am.Circle('mm', 0, d, 2, self.a+self.b)
|
||||
yield am.Circle('mm', 0, d, 2, self.a+self.b+self.c)
|
||||
yield am.Circle('mm', 0, d, 4, self.a * 1.1)
|
||||
yield am.Circle('mm', 0, d, 4, self.b * 1.9)
|
||||
yield am.Circle('mm', 0, d, 4, self.c * 2.2)
|
||||
yield am.Circle('mm', 0, d, 6, 2 * self.a / self.b)
|
||||
yield am.Circle('mm', 0, d, 6, 4 * self.b / self.c)
|
||||
yield am.Circle('mm', 0, d, 6, 6 * self.c / self.a)
|
||||
yield am.Circle('mm', 0, d, 8, self.a - self.b * self.a / self.c)
|
||||
yield am.Circle('mm', 0, d, 8, 2 + self.a - self.b * self.a / self.c)
|
||||
yield am.Circle('mm', 0, d, 8, self.a - 2 * self.b * self.a / self.c)
|
||||
|
||||
inst = test_macro(*abc)
|
||||
run_aperture_macro_test(tmpfile, img_support, inst)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Expression language unit tests
|
||||
# =============================================================================
|
||||
|
||||
class TestConstantExpression:
|
||||
def test_value_stored(self):
|
||||
assert C(5).value == 5
|
||||
|
||||
def test_float_conversion(self):
|
||||
assert float(C(3.14)) == pytest.approx(3.14)
|
||||
|
||||
def test_calculate_no_binding(self):
|
||||
assert C(42.0).calculate() == pytest.approx(42.0)
|
||||
|
||||
def test_calculate_ignores_binding(self):
|
||||
assert C(7.0).calculate({1: 99.0}) == pytest.approx(7.0)
|
||||
|
||||
def test_to_gerber_integer(self):
|
||||
assert C(5).to_gerber() == '5'
|
||||
|
||||
def test_to_gerber_float(self):
|
||||
assert C(1.5).to_gerber() == '1.5'
|
||||
|
||||
def test_to_gerber_trailing_zeros_stripped(self):
|
||||
assert C(1.500000).to_gerber() == '1.5'
|
||||
assert C(2.0).to_gerber() == '2'
|
||||
|
||||
def test_to_gerber_zero(self):
|
||||
assert C(0).to_gerber() == '0'
|
||||
|
||||
def test_to_gerber_negative_zero_avoided(self):
|
||||
# -0.0 must not serialize as '-0'
|
||||
assert C(-0.0).to_gerber() == '0'
|
||||
|
||||
def test_equality_exact(self):
|
||||
assert C(3.0) == C(3.0)
|
||||
|
||||
def test_equality_within_tolerance(self):
|
||||
assert C(1.0) == C(1.0 + 1e-10)
|
||||
|
||||
def test_inequality_outside_tolerance(self):
|
||||
assert not (C(1.0) == C(2.0))
|
||||
|
||||
def test_equality_with_plain_number(self):
|
||||
assert C(0) == 0
|
||||
assert C(1) == 1
|
||||
assert C(-1) == -1
|
||||
|
||||
def test_parameters_empty(self):
|
||||
assert list(C(5).parameters()) == []
|
||||
|
||||
|
||||
class TestParameterExpression:
|
||||
def test_to_gerber(self):
|
||||
assert P(1).to_gerber() == '$1'
|
||||
assert P(3).to_gerber() == '$3'
|
||||
assert P(42).to_gerber() == '$42'
|
||||
|
||||
def test_calculate_with_binding(self):
|
||||
assert P(1).calculate({1: 5.0}) == pytest.approx(5.0)
|
||||
assert P(2).calculate({1: 10.0, 2: 20.0}) == pytest.approx(20.0)
|
||||
|
||||
def test_calculate_unresolved_raises(self):
|
||||
with pytest.raises(IndexError):
|
||||
P(1).calculate({})
|
||||
|
||||
def test_calculate_missing_param_raises(self):
|
||||
with pytest.raises(IndexError):
|
||||
P(2).calculate({1: 5.0})
|
||||
|
||||
def test_parameters_yields_self(self):
|
||||
p = P(1)
|
||||
assert list(p.parameters()) == [p]
|
||||
|
||||
def test_optimized_with_binding_resolves(self):
|
||||
assert P(1).optimized({1: 7.5}) == C(7.5)
|
||||
|
||||
def test_optimized_without_binding_is_identity(self):
|
||||
p = P(1)
|
||||
assert p.optimized({}) is p
|
||||
|
||||
|
||||
class TestArithmeticOperators:
|
||||
@pytest.mark.parametrize('a,b', [(3.0, 7.0), (-1.5, 4.2), (0.5, 0.25), (0.0, 5.0)])
|
||||
def test_add(self, a, b):
|
||||
assert (P(1) + P(2)).calculate({1: a, 2: b}) == pytest.approx(a + b)
|
||||
|
||||
@pytest.mark.parametrize('a,b', [(3.0, 7.0), (-1.5, 4.2), (0.5, 0.25), (5.0, 5.0)])
|
||||
def test_sub(self, a, b):
|
||||
assert (P(1) - P(2)).calculate({1: a, 2: b}) == pytest.approx(a - b)
|
||||
|
||||
@pytest.mark.parametrize('a,b', [(3.0, 7.0), (-1.5, 4.2), (0.5, 0.25), (0.0, 5.0)])
|
||||
def test_mul(self, a, b):
|
||||
assert (P(1) * P(2)).calculate({1: a, 2: b}) == pytest.approx(a * b)
|
||||
|
||||
@pytest.mark.parametrize('a,b', [(6.0, 3.0), (-4.5, 1.5), (1.0, 4.0)])
|
||||
def test_div(self, a, b):
|
||||
assert (P(1) / P(2)).calculate({1: a, 2: b}) == pytest.approx(a / b)
|
||||
|
||||
def test_radd(self):
|
||||
assert (5.0 + P(1)).calculate({1: 3.0}) == pytest.approx(8.0)
|
||||
|
||||
def test_rsub(self):
|
||||
assert (10.0 - P(1)).calculate({1: 3.0}) == pytest.approx(7.0)
|
||||
|
||||
def test_rmul(self):
|
||||
assert (2.0 * P(1)).calculate({1: 4.0}) == pytest.approx(8.0)
|
||||
|
||||
def test_rdiv(self):
|
||||
assert (10.0 / P(1)).calculate({1: 2.0}) == pytest.approx(5.0)
|
||||
|
||||
def test_neg(self):
|
||||
assert (-P(1)).calculate({1: 5.0}) == pytest.approx(-5.0)
|
||||
|
||||
def test_pos_is_identity(self):
|
||||
p = P(1)
|
||||
assert +p is p
|
||||
|
||||
|
||||
# Cross-check expression evaluation against Python's own arithmetic.
|
||||
# A single lambda serves both roles: called with P() objects it builds an expression tree;
|
||||
# called with plain numbers it computes the Python reference value (ints/floats auto-convert).
|
||||
@pytest.mark.parametrize('f,binding', [
|
||||
(lambda p1, p2, p3: p1 + p2, {1: 3, 2: 7, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 - p2, {1: 10, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 * p2, {1: 3, 2: 4, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 / p2, {1: 9, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 + p2 + p3, {1: 1, 2: 2, 3: 3}),
|
||||
(lambda p1, p2, p3: p1 * p2 + p3, {1: 2, 2: 3, 3: 4}),
|
||||
(lambda p1, p2, p3: p1 + p2 * p3, {1: 2, 2: 3, 3: 4}),
|
||||
(lambda p1, p2, p3: (p1 + p2) * p3, {1: 2, 2: 3, 3: 4}),
|
||||
(lambda p1, p2, p3: p1 / p2 + p3, {1: 6, 2: 3, 3: 1}),
|
||||
(lambda p1, p2, p3: p1 - p2 * p3, {1: 10, 2: 2, 3: 3}),
|
||||
(lambda p1, p2, p3: (p1 + p2) / p3, {1: 3, 2: 5, 3: 4}),
|
||||
(lambda p1, p2, p3: p1 * (p2 - p3), {1: 3, 2: 7, 3: 2}),
|
||||
(lambda p1, p2, p3: p1 * 2 + 3, {1: 5, 2: 0, 3: 0}),
|
||||
(lambda p1, p2, p3: 10 - p1 * p2, {1: 2, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 / 2 + p2, {1: 6, 2: 1, 3: 0}),
|
||||
(lambda p1, p2, p3: -p1 + p2, {1: 3, 2: 7, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 + (-p2), {1: 10, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 * (-p2), {1: 3, 2: 4, 3: 0}),
|
||||
(lambda p1, p2, p3: (-p1) * (-p2), {1: 3, 2: 4, 3: 0}),
|
||||
(lambda p1, p2, p3: (-p1) / (-p2), {1: 6, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 - (-p2), {1: 5, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: (p1+p2) * (p1-p3), {1: 5, 2: 3, 3: 2}),
|
||||
(lambda p1, p2, p3: p1 / p2 * p3, {1: 6, 2: 2, 3: 5}),
|
||||
])
|
||||
def test_expression_against_python(f, binding):
|
||||
"""Build a gerbonara expression and compare its result to Python's evaluation."""
|
||||
a, b, c = binding.get(1, 0), binding.get(2, 0), binding.get(3, 0)
|
||||
assert f(P(1), P(2), P(3)).calculate(binding) == pytest.approx(f(a, b, c), rel=1e-9, abs=1e-12)
|
||||
|
||||
|
||||
class TestConstantFolding:
|
||||
"""Operations on two ConstantExpressions must immediately produce a ConstantExpression."""
|
||||
|
||||
def test_add(self):
|
||||
result = C(3) + C(4)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(7)
|
||||
|
||||
def test_sub(self):
|
||||
result = C(10) - C(4)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(6)
|
||||
|
||||
def test_mul(self):
|
||||
result = C(3) * C(4)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(12)
|
||||
|
||||
def test_div(self):
|
||||
result = C(10) / C(4)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(2.5)
|
||||
|
||||
def test_neg_of_constant(self):
|
||||
result = -C(5)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(-5)
|
||||
|
||||
def test_nested(self):
|
||||
result = (C(3) + C(4)) * C(2)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(14)
|
||||
|
||||
def test_deeply_nested(self):
|
||||
result = C(2) * C(3) + C(4) * C(5)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(26)
|
||||
|
||||
|
||||
class TestAlgebraicOptimizations:
|
||||
"""Each algebraic simplification rule in OperatorExpression.optimized()."""
|
||||
|
||||
def test_zero_plus_x(self):
|
||||
assert C(0) + P(1) == P(1)
|
||||
|
||||
def test_x_plus_zero(self):
|
||||
assert P(1) + C(0) == P(1)
|
||||
|
||||
def test_zero_times_x(self):
|
||||
assert C(0) * P(1) == C(0)
|
||||
|
||||
def test_x_times_zero(self):
|
||||
assert P(1) * C(0) == C(0)
|
||||
|
||||
def test_one_times_x(self):
|
||||
assert C(1) * P(1) == P(1)
|
||||
|
||||
def test_x_times_one(self):
|
||||
assert P(1) * C(1) == P(1)
|
||||
|
||||
def test_x_times_neg_one_negates(self):
|
||||
assert (P(1) * C(-1)).calculate({1: 5.0}) == pytest.approx(-5.0)
|
||||
|
||||
def test_neg_one_times_x_negates(self):
|
||||
assert (C(-1) * P(1)).calculate({1: 5.0}) == pytest.approx(-5.0)
|
||||
|
||||
def test_x_minus_zero(self):
|
||||
assert P(1) - C(0) == P(1)
|
||||
|
||||
def test_zero_minus_x_is_neg_x(self):
|
||||
assert (C(0) - P(1)).calculate({1: 5.0}) == pytest.approx(-5.0)
|
||||
|
||||
def test_x_minus_x_is_zero(self):
|
||||
p = P(1)
|
||||
assert p - p == C(0)
|
||||
|
||||
def test_x_minus_neg_y_is_x_plus_y(self):
|
||||
assert (P(1) - (-P(2))).calculate({1: 3.0, 2: 4.0}) == pytest.approx(7.0)
|
||||
|
||||
def test_x_div_one(self):
|
||||
assert P(1) / C(1) == P(1)
|
||||
|
||||
def test_x_div_neg_one_negates(self):
|
||||
assert (P(1) / C(-1)).calculate({1: 5.0}) == pytest.approx(-5.0)
|
||||
|
||||
def test_x_div_x_is_one(self):
|
||||
p = P(1)
|
||||
assert p / p == C(1)
|
||||
|
||||
def test_neg_x_times_neg_y_cancels(self):
|
||||
assert ((-P(1)) * (-P(2))).calculate({1: 3.0, 2: 4.0}) == pytest.approx(12.0)
|
||||
|
||||
def test_neg_x_div_neg_y_cancels(self):
|
||||
assert ((-P(1)) / (-P(2))).calculate({1: 6.0, 2: 3.0}) == pytest.approx(2.0)
|
||||
|
||||
def test_x_plus_neg_y_becomes_subtraction(self):
|
||||
assert (P(1) + (-P(2))).calculate({1: 10.0, 2: 3.0}) == pytest.approx(7.0)
|
||||
|
||||
def test_neg_x_plus_y_reverses_subtraction(self):
|
||||
assert ((-P(1)) + P(2)).calculate({1: 3.0, 2: 10.0}) == pytest.approx(7.0)
|
||||
|
||||
def test_x_mul_neg_y_pulls_negation_out(self):
|
||||
e = P(1) * (-P(2))
|
||||
assert isinstance(e, NegatedExpression)
|
||||
assert e.calculate({1: 3.0, 2: 4.0}) == pytest.approx(-12.0)
|
||||
|
||||
def test_neg_x_mul_y_pulls_negation_out(self):
|
||||
e = (-P(1)) * P(2)
|
||||
assert isinstance(e, NegatedExpression)
|
||||
assert e.calculate({1: 3.0, 2: 4.0}) == pytest.approx(-12.0)
|
||||
|
||||
def test_x_div_neg_y_pulls_negation_out(self):
|
||||
e = P(1) / (-P(2))
|
||||
assert isinstance(e, NegatedExpression)
|
||||
assert e.calculate({1: 6.0, 2: 3.0}) == pytest.approx(-2.0)
|
||||
|
||||
def test_neg_x_div_y_pulls_negation_out(self):
|
||||
e = (-P(1)) / P(2)
|
||||
assert isinstance(e, NegatedExpression)
|
||||
assert e.calculate({1: 6.0, 2: 3.0}) == pytest.approx(-2.0)
|
||||
|
||||
|
||||
class TestNegatedExpression:
|
||||
def test_double_negation(self):
|
||||
p = P(1)
|
||||
assert -(-p) == p
|
||||
|
||||
def test_double_negation_evaluates_correctly(self):
|
||||
assert (-(-P(1))).calculate({1: 5.0}) == pytest.approx(5.0)
|
||||
|
||||
def test_negation_of_constant_folds(self):
|
||||
assert -C(5) == C(-5)
|
||||
|
||||
def test_negation_of_subtraction_flips_operands(self):
|
||||
# -(a - b) == b - a
|
||||
assert (-(P(1) - P(2))).calculate({1: 3.0, 2: 7.0}) == pytest.approx(4.0)
|
||||
|
||||
def test_negation_of_zero_is_zero(self):
|
||||
assert -C(0) == C(0)
|
||||
|
||||
def test_to_gerber_parameter_no_parens(self):
|
||||
assert NegatedExpression(P(1)).to_gerber() == '-$1'
|
||||
|
||||
def test_to_gerber_operator_uses_parens(self):
|
||||
inner = OperatorExpression(op.add, P(1), P(2))
|
||||
assert NegatedExpression(inner).to_gerber() == '-($1+$2)'
|
||||
|
||||
|
||||
class TestToGerber:
|
||||
def test_constant_integer(self):
|
||||
assert C(5).to_gerber() == '5'
|
||||
|
||||
def test_constant_float(self):
|
||||
assert C(1.5).to_gerber() == '1.5'
|
||||
|
||||
def test_parameter(self):
|
||||
assert P(1).to_gerber() == '$1'
|
||||
assert P(99).to_gerber() == '$99'
|
||||
|
||||
def test_add_operator(self):
|
||||
assert OperatorExpression(op.add, P(1), P(2)).to_gerber() == '$1+$2'
|
||||
|
||||
def test_sub_operator(self):
|
||||
assert OperatorExpression(op.sub, P(1), P(2)).to_gerber() == '$1-$2'
|
||||
|
||||
def test_mul_uses_x(self):
|
||||
# Gerber spec uses 'x' for multiplication, not '*'
|
||||
assert OperatorExpression(op.mul, P(1), P(2)).to_gerber() == '$1x$2'
|
||||
|
||||
def test_div_operator(self):
|
||||
assert OperatorExpression(op.truediv, P(1), P(2)).to_gerber() == '$1/$2'
|
||||
|
||||
def test_lhs_operator_gets_parens(self):
|
||||
lhs = OperatorExpression(op.add, P(1), P(2))
|
||||
e = OperatorExpression(op.mul, lhs, P(3))
|
||||
assert e.to_gerber() == '($1+$2)x$3'
|
||||
|
||||
def test_rhs_operator_gets_parens(self):
|
||||
rhs = OperatorExpression(op.add, P(2), P(3))
|
||||
e = OperatorExpression(op.mul, P(1), rhs)
|
||||
assert e.to_gerber() == '$1x($2+$3)'
|
||||
|
||||
def test_nested_lhs_and_rhs_parens(self):
|
||||
lhs = OperatorExpression(op.add, P(1), P(2))
|
||||
outer = OperatorExpression(op.add, lhs, P(3))
|
||||
assert outer.to_gerber() == '($1+$2)+$3'
|
||||
|
||||
def test_negated_mul_to_gerber(self):
|
||||
# P(1) * (-P(2)) optimises to -(P(1)*P(2)); NegatedExpression wraps the product
|
||||
assert (P(1) * (-P(2))).to_gerber() == '-($1x$2)'
|
||||
|
||||
def test_negated_div_to_gerber(self):
|
||||
assert (P(1) / (-P(2))).to_gerber() == '-($1/$2)'
|
||||
|
||||
def test_negative_constant(self):
|
||||
assert C(-5).to_gerber() == '-5'
|
||||
|
||||
|
||||
class TestParsing:
|
||||
def test_constant_integer(self):
|
||||
assert _parse_expression('5', {}, set()) == C(5)
|
||||
|
||||
def test_constant_float(self):
|
||||
assert _parse_expression('1.5', {}, set()) == C(1.5)
|
||||
|
||||
def test_parameter_reference(self):
|
||||
params = set()
|
||||
assert _parse_expression('$1', {}, params) == P(1)
|
||||
assert 1 in params
|
||||
|
||||
def test_multiple_parameters_tracked(self):
|
||||
params = set()
|
||||
_parse_expression('$1+$3', {}, params)
|
||||
assert params == {1, 3}
|
||||
|
||||
def test_add(self):
|
||||
assert _parse_expression('$1+$2', {}, set()).calculate({1: 3, 2: 4}) == pytest.approx(7)
|
||||
|
||||
def test_sub(self):
|
||||
assert _parse_expression('$1-$2', {}, set()).calculate({1: 10, 2: 4}) == pytest.approx(6)
|
||||
|
||||
def test_mul_gerber_x_syntax(self):
|
||||
assert _parse_expression('$1x$2', {}, set()).calculate({1: 3, 2: 4}) == pytest.approx(12)
|
||||
|
||||
def test_mul_uppercase_x(self):
|
||||
assert _parse_expression('$1X$2', {}, set()).calculate({1: 3, 2: 4}) == pytest.approx(12)
|
||||
|
||||
def test_div(self):
|
||||
assert _parse_expression('$1/$2', {}, set()).calculate({1: 10, 2: 4}) == pytest.approx(2.5)
|
||||
|
||||
def test_negation(self):
|
||||
assert _parse_expression('-$1', {}, set()).calculate({1: 5}) == pytest.approx(-5)
|
||||
|
||||
def test_parenthesized(self):
|
||||
assert _parse_expression('($1+$2)x$3', {}, set()).calculate({1: 3, 2: 4, 3: 2}) == pytest.approx(14)
|
||||
|
||||
def test_known_variable_becomes_variable_expression(self):
|
||||
e = _parse_expression('$1', {1: C(10)}, set())
|
||||
assert isinstance(e, VariableExpression)
|
||||
|
||||
@pytest.mark.parametrize('gerber_str,py_str,binding', [
|
||||
('$1+$2', 'a+b', {1: 5, 2: 3 }),
|
||||
('$1-$2', 'a-b', {1: 5, 2: 3 }),
|
||||
('$1x$2', 'a*b', {1: 5, 2: 3 }),
|
||||
('$1/$2', 'a/b', {1: 6, 2: 3 }),
|
||||
('($1+$2)x$3', '(a+b)*c', {1: 2, 2: 3, 3: 4 }),
|
||||
('$1x$2+$3', 'a*b+c', {1: 2, 2: 3, 3: 4 }),
|
||||
('-$1+$2', '-a+b', {1: 2, 2: 7 }),
|
||||
('$1/$2+$1x$2', 'a/b+a*b', {1: 6, 2: 2 }),
|
||||
])
|
||||
def test_parse_and_evaluate(self, gerber_str, py_str, binding):
|
||||
e = _parse_expression(gerber_str, {}, set())
|
||||
a = binding.get(1, 0)
|
||||
b = binding.get(2, 0)
|
||||
c = binding.get(3, 0)
|
||||
expected = eval(py_str) # noqa: S307 – controlled test literals only
|
||||
assert e.calculate(binding) == pytest.approx(expected)
|
||||
|
||||
@pytest.mark.parametrize('make_expr,binding', [
|
||||
(lambda p1, p2, p3: p1 + p2, {1: 3, 2: 7, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 - p2, {1: 10, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 * p2, {1: 3, 2: 4, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 / p2, {1: 9, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: (p1 + p2) * p3, {1: 2, 2: 3, 3: 4}),
|
||||
(lambda p1, p2, p3: p1 * p2 - p3, {1: 5, 2: 2, 3: 3}),
|
||||
(lambda p1, p2, p3: p1 / p2 + p3, {1: 6, 2: 3, 3: 1}),
|
||||
(lambda p1, p2, p3: -p1 + p2, {1: 3, 2: 7, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 * (-p2), {1: 3, 2: 4, 3: 0}),
|
||||
(lambda p1, p2, p3: (-p1) * p2, {1: 3, 2: 4, 3: 0}),
|
||||
(lambda p1, p2, p3: p1 / (-p2), {1: 9, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: (-p1) / p2, {1: 9, 2: 3, 3: 0}),
|
||||
(lambda p1, p2, p3: (p1 + p2) / p3 - p1, {1: 3, 2: 5, 3: 4}),
|
||||
(lambda p1, p2, p3: p1 * p2 + p3 / p1, {1: 3, 2: 4, 3: 6}),
|
||||
(lambda p1, p2, p3: -(p1 + p2) * p3, {1: 2, 2: 3, 3: 4}),
|
||||
])
|
||||
def test_to_gerber_round_trip(self, make_expr, binding):
|
||||
"""to_gerber() followed by _parse_expression() must preserve the evaluated value."""
|
||||
original = make_expr(P(1), P(2), P(3))
|
||||
gerber = original.to_gerber()
|
||||
parsed = _parse_expression(gerber, {}, set())
|
||||
assert parsed.calculate(binding) == pytest.approx(original.calculate(binding))
|
||||
|
||||
|
||||
class TestUnitExpression:
|
||||
def test_mm_to_mm_unchanged(self):
|
||||
assert UnitExpression(C(25.4), MM).calculate(unit=MM) == pytest.approx(25.4)
|
||||
|
||||
def test_inch_to_mm(self):
|
||||
assert UnitExpression(C(1.0), Inch).calculate(unit=MM) == pytest.approx(MILLIMETERS_PER_INCH)
|
||||
|
||||
def test_mm_to_inch(self):
|
||||
assert UnitExpression(C(25.4), MM).calculate(unit=Inch) == pytest.approx(25.4 / MILLIMETERS_PER_INCH)
|
||||
|
||||
def test_inch_to_inch_unchanged(self):
|
||||
assert UnitExpression(C(2.0), Inch).calculate(unit=Inch) == pytest.approx(2.0)
|
||||
|
||||
def test_none_unit_passes_through(self):
|
||||
assert UnitExpression(C(5.0), None).calculate(unit=MM) == pytest.approx(5.0)
|
||||
|
||||
def test_negation_preserves_unit(self):
|
||||
neg = -UnitExpression(C(5.0), MM)
|
||||
assert isinstance(neg, UnitExpression) and neg.unit == MM
|
||||
assert neg.calculate(unit=MM) == pytest.approx(-5.0)
|
||||
|
||||
def test_add_same_unit(self):
|
||||
result = UnitExpression(C(3.0), MM) + UnitExpression(C(4.0), MM)
|
||||
assert isinstance(result, UnitExpression)
|
||||
assert result.calculate(unit=MM) == pytest.approx(7.0)
|
||||
|
||||
def test_add_mixed_units_converts(self):
|
||||
# 1 inch + 1 mm, result held in Inch
|
||||
result = UnitExpression(C(1.0), Inch) + UnitExpression(C(1.0), MM)
|
||||
assert result.calculate(unit=Inch) == pytest.approx(1.0 + 1.0 / MILLIMETERS_PER_INCH)
|
||||
|
||||
def test_add_scalar_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
UnitExpression(C(5.0), MM) + C(3.0)
|
||||
|
||||
def test_radd_scalar_raises(self):
|
||||
# BUG: asymmetric unit safety — C(3.0) + UnitExpression(...) does NOT raise because
|
||||
# Python dispatches to Expression.__add__ first, which has no unit awareness.
|
||||
# Only plain Python scalars (not Expression subclasses) trigger __radd__ on UnitExpression.
|
||||
# There is no really nice fix for this, so we just leave it in for now.
|
||||
with pytest.raises(ValueError):
|
||||
5.0 + UnitExpression(C(5.0), MM)
|
||||
|
||||
def test_mul_by_scalar(self):
|
||||
result = UnitExpression(C(3.0), MM) * C(2)
|
||||
assert isinstance(result, UnitExpression)
|
||||
assert result.calculate(unit=MM) == pytest.approx(6.0)
|
||||
|
||||
def test_div_by_scalar(self):
|
||||
result = UnitExpression(C(6.0), MM) / C(2)
|
||||
assert isinstance(result, UnitExpression)
|
||||
assert result.calculate(unit=MM) == pytest.approx(3.0)
|
||||
|
||||
def test_nested_unit_expression_flattens(self):
|
||||
# Wrapping a UnitExpression in another converts rather than double-wrapping
|
||||
inner = UnitExpression(C(1.0), Inch)
|
||||
outer = UnitExpression(inner, MM)
|
||||
assert not isinstance(outer.expr, UnitExpression)
|
||||
assert outer.calculate(unit=MM) == pytest.approx(MILLIMETERS_PER_INCH)
|
||||
|
||||
def test_parameters_forwarded(self):
|
||||
assert list(UnitExpression(P(1), MM).parameters()) == [P(1)]
|
||||
|
||||
|
||||
class TestExprHelper:
|
||||
def test_passthrough_expression(self):
|
||||
p = P(1)
|
||||
assert expr(p) is p
|
||||
|
||||
def test_wraps_int(self):
|
||||
result = expr(5)
|
||||
assert isinstance(result, ConstantExpression) and result.value == 5
|
||||
|
||||
def test_wraps_float(self):
|
||||
result = expr(3.14)
|
||||
assert isinstance(result, ConstantExpression) and result.value == pytest.approx(3.14)
|
||||
|
||||
|
||||
class TestVariableExpression:
|
||||
def test_optimized_non_operator_unwraps(self):
|
||||
# A VariableExpression wrapping something that simplifies to a non-OperatorExpression
|
||||
# should unwrap and return the simplified value directly.
|
||||
result = VariableExpression(C(5)).optimized()
|
||||
assert result == C(5)
|
||||
|
||||
def test_optimized_keeps_operator_expression(self):
|
||||
ve = VariableExpression(OperatorExpression(op.add, P(1), P(2)))
|
||||
assert isinstance(ve.optimized(), VariableExpression)
|
||||
|
||||
def test_to_gerber_without_register_uses_inner(self):
|
||||
assert VariableExpression(C(42)).to_gerber(register_variable=None) == '42'
|
||||
|
||||
def test_to_gerber_with_register_allocates_dollar_variable(self):
|
||||
allocated = {}
|
||||
|
||||
def register(e):
|
||||
key = e.to_gerber()
|
||||
if key not in allocated:
|
||||
allocated[key] = len(allocated) + 1
|
||||
return allocated[key]
|
||||
|
||||
inner = OperatorExpression(op.add, P(1), P(2))
|
||||
result = VariableExpression(inner).to_gerber(register_variable=register)
|
||||
assert result.startswith('$') and int(result[1:]) >= 1
|
||||
|
|
@ -184,7 +184,7 @@ class TestMerge:
|
|||
reference_path(file_b), '--offset', '100,100', '--rotation', '0',
|
||||
outdir, '--output-naming-scheme', 'kicad', '--output-board-name', 'foobar',
|
||||
'--warnings', 'ignore')
|
||||
assert (Path(outdir) / 'foobar-F.Cu.gbr').exists()
|
||||
assert (Path(outdir) / 'foobar-F_Cu.gbr').exists()
|
||||
|
||||
|
||||
class TestMeta:
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from scipy.spatial import KDTree
|
|||
from gerbonara.excellon import ExcellonFile
|
||||
from gerbonara.rs274x import GerberFile
|
||||
from gerbonara.cam import FileSettings
|
||||
from gerbonara.graphic_objects import Flash
|
||||
from gerbonara.graphic_objects import Arc, Flash, Line
|
||||
|
||||
from .image_support import *
|
||||
from .utils import *
|
||||
|
|
@ -100,6 +100,28 @@ def test_first_level_idempotence_svg(reference, tmpfile, img_support):
|
|||
assert hist[9] == 0
|
||||
assert hist[3:].sum() < 5e-5*hist.size
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('reference', list(REFERENCE_FILES.items()), indirect=True)
|
||||
def test_gerber_conversion(reference, tmpfile, img_support):
|
||||
reference, (unit_spec, _) = reference
|
||||
tmp = tmpfile('Output gerber', '.gbr')
|
||||
ref_svg = tmpfile('Reference SVG render', '.svg')
|
||||
out_svg = tmpfile('Output SVG render', '.svg')
|
||||
|
||||
a = ExcellonFile.open(reference)
|
||||
a.to_gerber().save(tmp)
|
||||
b = GerberFile.open(tmp)
|
||||
|
||||
ref_svg.write_text(str(a.to_svg(fg='black', bg='white')))
|
||||
out_svg.write_text(str(b.to_svg(fg='black', bg='white')))
|
||||
|
||||
mean, _max, hist = img_support.svg_difference(ref_svg, out_svg, diff_out=tmpfile('Difference', '.png'), background='white')
|
||||
assert mean < 2e-4
|
||||
assert hist[9] == 0
|
||||
assert hist[3:].sum() < 2e-4*hist.size
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('reference', list(REFERENCE_FILES.items()), indirect=True)
|
||||
def test_idempotence(reference, tmpfile):
|
||||
|
|
@ -123,6 +145,7 @@ def test_idempotence(reference, tmpfile):
|
|||
|
||||
assert tmp_1.read_text() == tmp_2.read_text()
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('reference', list(REFERENCE_FILES.items()), indirect=True)
|
||||
def test_gerber_alignment(reference, tmpfile, print_on_error):
|
||||
|
|
@ -172,3 +195,108 @@ def test_syntax_error():
|
|||
assert 'test_syntax_error.exc' in exc_info.value.msg
|
||||
assert '12' in exc_info.value.msg # lineno
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
def test_easyeda_format_supports_explicit_digit_spec():
|
||||
data = '\n'.join([
|
||||
'M48',
|
||||
'INCH,TZ,2.1',
|
||||
'T01C0.0100',
|
||||
'%',
|
||||
'T01',
|
||||
'X11Y11',
|
||||
'M30',
|
||||
])
|
||||
|
||||
parsed = ExcellonFile.from_string(data)
|
||||
assert parsed.import_settings.number_format == (2, 1)
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
def test_fmat_1_header_is_accepted():
|
||||
data = '\n'.join([
|
||||
'M48',
|
||||
'INCH,TZ,2.4',
|
||||
'FMAT,1',
|
||||
'T01C0.0100',
|
||||
'%',
|
||||
'T01',
|
||||
'X010000Y010000',
|
||||
'X020000Y010000',
|
||||
'M30',
|
||||
])
|
||||
|
||||
parsed = ExcellonFile.from_string(data)
|
||||
drills = list(parsed.drills())
|
||||
assert len(drills) == 2
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
def test_inline_g85_slot_creates_line_slot():
|
||||
data = '\n'.join([
|
||||
'M48',
|
||||
'INCH,TZ',
|
||||
'T01C0.1000',
|
||||
'%',
|
||||
'T01',
|
||||
'X080000Y015000G85X090000Y015000',
|
||||
'M30',
|
||||
])
|
||||
|
||||
parsed = ExcellonFile.from_string(data)
|
||||
slots = list(parsed.slots())
|
||||
assert len(slots) == 1
|
||||
assert isinstance(slots[0], Line)
|
||||
assert slots[0].x1 == 8.0
|
||||
assert slots[0].y1 == 1.5
|
||||
assert slots[0].x2 == 9.0
|
||||
assert slots[0].y2 == 1.5
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
def test_circular_interpolation_uses_signed_center_and_direction():
|
||||
data = '\n'.join([
|
||||
'M48',
|
||||
'INCH,TZ',
|
||||
'T01C0.0100',
|
||||
'%',
|
||||
'T01',
|
||||
'G00X000000Y010000',
|
||||
'M15',
|
||||
'G03X-010000Y000000I000000J-010000',
|
||||
'M16',
|
||||
'M30',
|
||||
])
|
||||
|
||||
parsed = ExcellonFile.from_string(data)
|
||||
slots = list(parsed.slots())
|
||||
assert len(slots) == 1
|
||||
assert isinstance(slots[0], Arc)
|
||||
assert slots[0].cx == 0.0
|
||||
assert slots[0].cy == -1.0
|
||||
assert not slots[0].clockwise
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
def test_radius_arc_interpolation_converts_to_center_offset():
|
||||
data = '\n'.join([
|
||||
'M48',
|
||||
'INCH,TZ',
|
||||
'T01C0.0100',
|
||||
'%',
|
||||
'T01',
|
||||
'G00X010000Y000000',
|
||||
'M15',
|
||||
'G03X000000Y010000A010000',
|
||||
'M16',
|
||||
'M30',
|
||||
])
|
||||
|
||||
parsed = ExcellonFile.from_string(data)
|
||||
slots = list(parsed.slots())
|
||||
assert len(slots) == 1
|
||||
assert isinstance(slots[0], Arc)
|
||||
assert math.isclose(slots[0].cx, -1.0)
|
||||
assert math.isclose(slots[0].cy, 0.0, abs_tol=1e-12)
|
||||
assert not slots[0].clockwise
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -317,10 +318,24 @@ REFERENCE_DIRS = {
|
|||
'silk_screen_bottom.art': 'bottom silk',
|
||||
'silk_screen_top.art': 'top silk',
|
||||
},
|
||||
'world_clock_2': {
|
||||
'wc2-B_Cu.gbr': 'bottom copper',
|
||||
'wc2-B_Mask.gbr': 'bottom mask',
|
||||
'wc2-B_Paste.gbr': 'bottom paste',
|
||||
'wc2-B_SilkS.gbr': 'bottom silk',
|
||||
'wc2-Edge_Cuts.gbr': 'mechanical outline',
|
||||
'wc2-F_Cu.gbr': 'top copper',
|
||||
'wc2-F_Mask.gbr': 'top mask',
|
||||
'wc2-F_Paste.gbr': 'top paste',
|
||||
'wc2-F_SilkS.gbr': 'top silk',
|
||||
'wc2.kicad_pcb': None,
|
||||
},
|
||||
'zero-length-lines': {
|
||||
}
|
||||
}
|
||||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('ref_dir,file_map', list(REFERENCE_DIRS.items()))
|
||||
@pytest.mark.parametrize('ref_dir,file_map', [(k, v) for k, v in REFERENCE_DIRS.items() if v])
|
||||
def test_layer_classifier(ref_dir, file_map):
|
||||
path = reference_path(ref_dir)
|
||||
print('Reference path is', path)
|
||||
|
|
@ -359,3 +374,13 @@ def test_layer_classifier(ref_dir, file_map):
|
|||
if 'upverter' not in ref_dir:
|
||||
assert isinstance(layer, ExcellonFile)
|
||||
|
||||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('ref_dir', list(REFERENCE_DIRS))
|
||||
def test_pretty_svg_export(ref_dir):
|
||||
""" Tests the basic functionality of to_pretty_svg """
|
||||
path = reference_path(ref_dir)
|
||||
stack = LayerStack.open_dir(path)
|
||||
with tempfile.NamedTemporaryFile(suffix='.svg') as f:
|
||||
stack.to_pretty_svg()
|
||||
|
||||
|
|
|
|||
269
tests/test_primitives.py
Normal file
269
tests/test_primitives.py
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2026 Jan Sebastian Götte <gerbonara@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.
|
||||
#
|
||||
# Based on https://github.com/tracespace/tracespace
|
||||
#
|
||||
|
||||
import math
|
||||
from contextlib import contextmanager
|
||||
|
||||
from PIL import Image
|
||||
import pytest
|
||||
|
||||
from gerbonara.rs274x import GerberFile
|
||||
from gerbonara.graphic_objects import Line, Arc, Flash, Region
|
||||
from gerbonara.apertures import CircleAperture, RectangleAperture, ObroundAperture, PolygonAperture
|
||||
from gerbonara.cam import FileSettings
|
||||
from gerbonara.utils import MM, Inch
|
||||
|
||||
from .image_support import svg_soup
|
||||
from .utils import *
|
||||
|
||||
@contextmanager
|
||||
def object_test(tmpfile, img_support, epsilon=1e-4):
|
||||
gbr = GerberFile()
|
||||
|
||||
yield gbr
|
||||
|
||||
gbr.offset(25.0, 25.0)
|
||||
|
||||
bounds = (0.0, 0.0), (2.0, 2.0) # bottom left, top right
|
||||
|
||||
# The below code is mostly copy-pasted from test_rs274x.py.
|
||||
|
||||
out_svg = tmpfile('SVG Output', '.svg')
|
||||
with open(out_svg, 'w') as f:
|
||||
# Use inch units here to make sure we and gerbv agree on the exact pixel size of the output since both calculate
|
||||
# it from the DPI setting.
|
||||
f.write(str(gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))
|
||||
|
||||
# test primitive to_arc_poly
|
||||
arc_poly_gbr = GerberFile()
|
||||
arc_poly_approx_gbr = GerberFile()
|
||||
for obj in gbr.objects:
|
||||
for primitive in obj.to_primitives(MM):
|
||||
poly = primitive.to_arc_poly()
|
||||
region = Region.from_arc_poly(poly)
|
||||
arc_poly_gbr.objects.append(region)
|
||||
|
||||
# Regression test for gitlab issue #17
|
||||
assert primitive.polarity_dark == poly.polarity_dark
|
||||
assert region.polarity_dark == poly.polarity_dark
|
||||
|
||||
# Test for arc poly arc approximation, and regression test for gitlab issue #16
|
||||
region = Region.from_arc_poly(poly.approximate_arcs())
|
||||
arc_poly_approx_gbr.objects.append(region)
|
||||
|
||||
arc_poly_svg = tmpfile('ArcPoly SVG Output', '.svg')
|
||||
with open(arc_poly_svg, 'w') as f:
|
||||
f.write(str(arc_poly_gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))
|
||||
|
||||
arc_poly_png = tmpfile('ArcPoly conversion render', '.png')
|
||||
img_support.svg_to_png(arc_poly_svg, arc_poly_png, dpi=300, bg='white')
|
||||
|
||||
# Arc approximation test
|
||||
arc_poly_approx_svg = tmpfile('ArcPoly Approximation SVG Output', '.svg')
|
||||
with open(arc_poly_approx_svg, 'w') as f:
|
||||
f.write(str(arc_poly_approx_gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))
|
||||
|
||||
arc_poly_approx_png = tmpfile('ArcPoly Approximation conversion render', '.png')
|
||||
img_support.svg_to_png(arc_poly_approx_svg, arc_poly_approx_png, dpi=300, bg='white')
|
||||
|
||||
# Reference export via gerber through GerbV
|
||||
out_gbr = tmpfile('GBR Output', '.gbr')
|
||||
gbr.save(out_gbr)
|
||||
|
||||
# NOTE: Instead of having gerbv directly export a PNG, we ask gerbv to output SVG which we then rasterize using
|
||||
# resvg. We have to do this since gerbv's built-in cairo-based PNG export has severe aliasing issues. In contrast,
|
||||
# using resvg for both allows an apples-to-apples comparison of both results.
|
||||
ref_svg = tmpfile('Reference export', '.svg')
|
||||
ref_png = tmpfile('Reference render', '.png')
|
||||
img_support.gerbv_export(out_gbr, ref_svg, origin=bounds[0], size=bounds[1], fg='#000000', bg='#ffffff')
|
||||
with svg_soup(ref_svg) as soup:
|
||||
img_support.cleanup_gerbv_svg(soup)
|
||||
img_support.svg_to_png(ref_svg, ref_png, dpi=300, bg='white')
|
||||
|
||||
out_png = tmpfile('Output render', '.png')
|
||||
img_support.svg_to_png(out_svg, out_png, dpi=300, bg='white')
|
||||
|
||||
mean, _max, hist = img_support.image_difference(ref_png, out_png, diff_out=tmpfile('Difference', '.png'))
|
||||
assert hist[9] < 1
|
||||
assert mean < epsilon
|
||||
assert hist[3:].sum() < epsilon*hist.size
|
||||
|
||||
mean, _max, hist = img_support.image_difference(out_png, arc_poly_png, diff_out=tmpfile('ArcPoly Difference', '.png'))
|
||||
assert hist[9] < 1
|
||||
assert mean < epsilon
|
||||
assert hist[3:].sum() < epsilon*hist.size
|
||||
|
||||
mean, _max, hist = img_support.image_difference(out_png, arc_poly_approx_png, diff_out=tmpfile('ArcPoly Approximation Difference', '.png'))
|
||||
assert hist[9] < 1
|
||||
assert mean < epsilon
|
||||
assert hist[3:].sum() < epsilon*hist.size
|
||||
|
||||
@pytest.mark.parametrize('angle_deg', [0, 5, -5, 10, -10, 15, -15, 30, -30, 45, -45, 60, -60, 75, -75, 90, -90, 120, -120, 180, 153, 155, 157])
|
||||
def test_line(angle_deg, tmpfile, img_support):
|
||||
with object_test(tmpfile, img_support) as gbr:
|
||||
angle = math.radians(angle_deg)
|
||||
l = 10
|
||||
obj = Line(
|
||||
x1=0, y1=0,
|
||||
x2=l*math.cos(angle), y2=l*math.sin(angle),
|
||||
aperture=CircleAperture(3.0, unit=MM),
|
||||
unit=MM
|
||||
)
|
||||
|
||||
gbr.objects.append(obj)
|
||||
|
||||
|
||||
def test_zero_length_line(tmpfile, img_support):
|
||||
with object_test(tmpfile, img_support) as gbr:
|
||||
for x, y in [(0, 0), (0, 10), (10, 15)]:
|
||||
obj = Line(
|
||||
x1=x, y1=y,
|
||||
x2=x, y2=y,
|
||||
aperture=CircleAperture(3.0, unit=MM),
|
||||
unit=MM
|
||||
)
|
||||
gbr.objects.append(obj)
|
||||
|
||||
# NOTE: We explicitly do not test the sweep_angle = 0 deg case here. In this case, we decided to always draw a full circle.
|
||||
# IIRC this is in line with some Gerber implementations in the wild, and it enables drawing a clean circle using a single Arc.
|
||||
# gerbv will not do this, and so this would cause a failing test for no particular reason.
|
||||
# TODO: Check 360 degree sweep angle case
|
||||
@pytest.mark.parametrize('start_angle_deg', [0, 5, 10, 15, 30, 45, 60, 75, 90, 120, 180, 153, 155, 157, 190, 200, 210, 240, 250, 255, 270, 280, 290, 340, 350, 355, 358])
|
||||
@pytest.mark.parametrize('sweep_angle_deg', [1, 5, 10, 15, 30, 45, 60, 75, 90, 120, 180, 153, 155, 157, 190, 200, 210, 240, 250, 255, 270, 280, 290, 340, 350, 355, 358])
|
||||
@pytest.mark.parametrize('clockwise', [True, False])
|
||||
def test_arc(start_angle_deg, sweep_angle_deg, clockwise, tmpfile, img_support):
|
||||
# Use large epsilon since someone approximates arcs with SVG beziers here, and that approximation really shows up.
|
||||
with object_test(tmpfile, img_support, epsilon=1e-2) as gbr:
|
||||
start_angle = math.radians(start_angle_deg)
|
||||
sweep_angle = math.radians(sweep_angle_deg)
|
||||
r = 10
|
||||
|
||||
x1, y1 = r*math.cos(start_angle), r*math.sin(start_angle)
|
||||
x2, y2 = r*math.cos(start_angle + sweep_angle), r*math.sin(start_angle + sweep_angle)
|
||||
|
||||
obj = Arc(
|
||||
x1=x1, y1=y1,
|
||||
x2=x2, y2=y2,
|
||||
cx=-x1, cy=-y1,
|
||||
clockwise=clockwise,
|
||||
aperture=CircleAperture(3.0, unit=MM),
|
||||
unit=MM
|
||||
)
|
||||
|
||||
gbr.objects.append(obj)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('x,y', [(0, 0), (5, 5), (10, 0), (0, 10), (-5, 5)])
|
||||
@pytest.mark.parametrize('aperture', [
|
||||
CircleAperture(3.0, unit=MM),
|
||||
CircleAperture(2.5, hole_dia=1.0, unit=MM),
|
||||
])
|
||||
def test_flash_circle(x, y, aperture, tmpfile, img_support):
|
||||
with object_test(tmpfile, img_support) as gbr:
|
||||
obj = Flash(
|
||||
x=x, y=y,
|
||||
aperture=aperture,
|
||||
unit=MM
|
||||
)
|
||||
gbr.objects.append(obj)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('aperture_type', [
|
||||
lambda: CircleAperture(4.0, unit=MM),
|
||||
lambda: CircleAperture(4.0, hole_dia=1.5, unit=MM),
|
||||
lambda: RectangleAperture(4.0, 3.0, unit=MM),
|
||||
lambda: ObroundAperture(4.0, 2.5, unit=MM),
|
||||
lambda: PolygonAperture(4.0, 6, unit=MM),
|
||||
])
|
||||
@pytest.mark.parametrize('polarity', [True, False])
|
||||
def test_flash_aperture_types(aperture_type, polarity, tmpfile, img_support):
|
||||
"""Test Flash with different aperture types."""
|
||||
|
||||
if aperture_type().hole_dia and not polarity:
|
||||
# Our SVG export (intentionally, for simplicity) does not exactly conform to the Gerber spec in aperture
|
||||
# rendering. The Gerber spec requires rendering the aperture from its primitives to an off-screen canvas, then
|
||||
# compositing that on top of the target surface. The result is that inverted polarity, e.g. for holes, is opaque
|
||||
# in our renders, but transparent in spec-compliant renders.
|
||||
#
|
||||
# We implemented it this way since any other way would be really slow (using lots of SVG masks, filters etc.),
|
||||
# and in practical applications this is never an actual problem.
|
||||
pytest.skip()
|
||||
|
||||
with object_test(tmpfile, img_support, epsilon=1e-3) as gbr:
|
||||
aperture = aperture_type()
|
||||
|
||||
# Create a grid of flashes with this aperture type
|
||||
positions = [(0, 0), (8, 0), (0, 8), (8, 8), (4, 4)]
|
||||
for x, y in positions:
|
||||
obj = Flash(
|
||||
x=x, y=y,
|
||||
aperture=aperture,
|
||||
unit=MM,
|
||||
polarity_dark=polarity
|
||||
)
|
||||
gbr.objects.append(obj)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('w,h', [(5, 5), (8, 4), (4, 8), (10, 3)])
|
||||
def test_region_rectangle(w, h, tmpfile, img_support):
|
||||
"""Test Region objects creating rectangles."""
|
||||
with object_test(tmpfile, img_support) as gbr:
|
||||
x, y = 5, 5
|
||||
obj = Region.from_rectangle(x, y, w, h, unit=MM)
|
||||
gbr.objects.append(obj)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('start_angle_deg', [0, 45, 90, 135, 180, 225, 270, 315])
|
||||
@pytest.mark.parametrize('sweep_angle_deg', [90, 180, 270])
|
||||
def test_region_arc_segments(start_angle_deg, sweep_angle_deg, tmpfile, img_support):
|
||||
"""Test Region objects with arc segments."""
|
||||
with object_test(tmpfile, img_support, epsilon=1e-2) as gbr:
|
||||
print(f'{start_angle_deg=} {sweep_angle_deg=}')
|
||||
start_angle = math.radians(start_angle_deg)
|
||||
sweep_angle = math.radians(sweep_angle_deg)
|
||||
r = 15
|
||||
|
||||
# Create a region that looks like a pie slice
|
||||
region = Region(unit=MM)
|
||||
|
||||
region.outline.append((0, 0))
|
||||
region.outline.append((r * math.cos(start_angle + sweep_angle), r * math.sin(start_angle + sweep_angle)))
|
||||
region.outline.append((r * math.cos(start_angle), r * math.sin(start_angle)))
|
||||
region.outline.append((0, 0))
|
||||
region.arc_centers.append(None)
|
||||
region.arc_centers.append((True, (0, 0)))
|
||||
region.arc_centers.append(None)
|
||||
|
||||
gbr.objects.append(region)
|
||||
|
||||
def test_region_close():
|
||||
""" Regression test for gitlab issue #18 """
|
||||
go_region = Region([
|
||||
(0, 0),
|
||||
(100, 0),
|
||||
(100, 100),
|
||||
(0, 100),
|
||||
# (0, 0), # expected
|
||||
],
|
||||
None,
|
||||
polarity_dark=False,
|
||||
unit=MM)
|
||||
assert go_region.outline[-1] == go_region.outline[0]
|
||||
|
||||
|
|
@ -29,6 +29,22 @@ from gerbonara.cam import FileSettings
|
|||
from .image_support import *
|
||||
from .utils import *
|
||||
|
||||
def test_attribute_without_value_is_stored_as_empty_tuple():
|
||||
data = '\n'.join([
|
||||
'%FSLAX24Y24*%',
|
||||
'%MOIN*%',
|
||||
'%TF.FlagLike*%',
|
||||
'%ADD10C,0.0100*%',
|
||||
'D10*',
|
||||
'X0Y0D03*',
|
||||
'M02*',
|
||||
])
|
||||
|
||||
parsed = GerberFile.from_string(data)
|
||||
assert parsed.file_attrs['.FlagLike'] == ()
|
||||
|
||||
# Note: We have a testcase for gitlab issues #10/#11 in therm_1.gbr, but we can't test for that at this time because
|
||||
# gerbv chokes on that gerber file and does'nt produce any output.
|
||||
REFERENCE_FILES = [ l.strip() for l in '''
|
||||
board_outline.GKO
|
||||
example_outline_with_arcs.gbr
|
||||
|
|
@ -299,6 +315,15 @@ REFERENCE_FILES = [ l.strip() for l in '''
|
|||
kicad-x2-tests/x2noap/Flashpads-F_Mask.gbr
|
||||
kicad-x2-tests/x2noap/Flashpads-F_Paste.gbr
|
||||
kicad-x2-tests/x2noap/Flashpads-F_Silkscreen.gbr
|
||||
world_clock_2/wc2-B_Cu.gbr
|
||||
world_clock_2/wc2-B_Mask.gbr
|
||||
world_clock_2/wc2-B_Paste.gbr
|
||||
world_clock_2/wc2-B_SilkS.gbr
|
||||
world_clock_2/wc2-Edge_Cuts.gbr
|
||||
world_clock_2/wc2-F_Cu.gbr
|
||||
world_clock_2/wc2-F_Mask.gbr
|
||||
world_clock_2/wc2-F_Paste.gbr
|
||||
world_clock_2/wc2-F_SilkS.gbr
|
||||
gerbv.gbr
|
||||
'''.splitlines() if l ]
|
||||
|
||||
|
|
@ -612,3 +637,46 @@ def test_syntax_error():
|
|||
assert 'test_syntax_error.gbr' in exc_info.value.msg
|
||||
assert '7' in exc_info.value.msg # lineno
|
||||
|
||||
@filter_syntax_warnings
|
||||
def test_step_repeat_rejects_huge_instance_counts():
|
||||
data = '\n'.join([
|
||||
'G04 test*',
|
||||
'%MOIN*%',
|
||||
'%FSLAX24Y24*%',
|
||||
'%ADD10C,0.0100*%',
|
||||
'%SRX1000Y1000I1.0J1.0*%',
|
||||
'D10*',
|
||||
'X0000Y0000D03*',
|
||||
'%SR*%',
|
||||
'M02*',
|
||||
])
|
||||
|
||||
with pytest.raises(SyntaxError, match='too many instances'):
|
||||
GerberFile.from_string(data)
|
||||
|
||||
@filter_syntax_warnings
|
||||
@pytest.mark.parametrize('reference', MIN_REFERENCE_FILES, indirect=True)
|
||||
def test_invert_polarity(reference, tmpfile, img_support):
|
||||
tmp_gbr = tmpfile('Output gerber', '.gbr')
|
||||
|
||||
gbr = GerberFile.open(reference)
|
||||
|
||||
ref_svg = tmpfile('Reference SVG export', '.svg')
|
||||
with open(ref_svg, 'w') as f:
|
||||
f.write(str(gbr.to_svg(arg_unit='inch', fg='black', bg='white')))
|
||||
ref_png = tmpfile('Reference render', '.png')
|
||||
img_support.svg_to_png(ref_svg, ref_png, dpi=300, bg='white')
|
||||
|
||||
gbr.invert_polarity()
|
||||
|
||||
out_svg = tmpfile('Output SVG export', '.svg')
|
||||
with open(out_svg, 'w') as f:
|
||||
f.write(str(gbr.to_svg(arg_unit='inch', fg='white', bg='black')))
|
||||
out_png = tmpfile('Reference render', '.png')
|
||||
img_support.svg_to_png(out_svg, out_png, dpi=300, bg='white')
|
||||
|
||||
cx, cy = 0, to_gerbv_svg_units(10, unit='inch')
|
||||
mean, _max, hist = img_support.image_difference(ref_png, out_png, diff_out=tmpfile('Difference', '.png'))
|
||||
assert mean < 1e-3
|
||||
assert hist[9] == 0
|
||||
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -82,7 +82,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "gerbonara"
|
||||
version = "1.5.0"
|
||||
version = "1.6.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue