Pogojig mostly done: KiCAD export works

This commit is contained in:
jaseg 2019-09-26 19:45:54 +02:00
parent 82b88f920a
commit b2eb56076d
26 changed files with 502 additions and 611 deletions

View file

@ -1,40 +1,27 @@
# Installation-dependent settings. You can overwrite these in a file called config.mk in the same directory as this makefile. See readme.creole.
INKSCAPE := inkscape
OPENSCAD := openscad
PYTHON := python2
ASYMPTOTE := asy
# Environment variables:
# INKSCAPE_DXF_FLATNESS controls inkscape SVG->DXF export curve flatness (default: 0.1)
# INKSCAPE, OPENSCAD: Commands to use for inkscape and openscad
# Settings affecting the compiled results. You can overwrite these in a file called settings.mk in the same directory as this makefile. See readme.creole.
DXF_FLATNESS := 0.1
# Non-file goals.
.PHONY: all clean generated dxf stl asy pdf
# Include the configuration files.
-include config.mk settings.mk
# Command to run the Python scripts.
PYTHON_CMD := PYTHONPATH="support" $(PYTHON)
INKSCAPE_CMD := INKSCAPE=$(INKSCAPE) DXF_FLATNESS=$(DXF_FLATNESS) $(PYTHON_CMD) -m inkscape
OPENSCAD_CMD := OPENSCAD=$(OPENSCAD) $(PYTHON_CMD) -m openscad
OPENSCAD ?= openscad
all: src/jig.stl src/pcb_shape.dxf
src/input.preprocessed.dxf: src/input.preprocessed.svg
support/inkscape_exporter.py $< $@
src/pcb_shape.dxf: src/pcb_shape.scad src/input.preprocessed.dxf
$(OPENSCAD) -o $@ $<
src/jig.stl: src/jig.scad src/input.preprocessed.dxf
$(OPENSCAD) -o $@ $<
src/input.preprocessed.svg: input.svg
support/inkscape_svg_filter_layers.py $< $@ --only --name "Test Points" "Mounting Holes" "Grip Slots" "Outline"
.PHONY: clean
clean:
rm -f src/input.preprocessed.dxf
rm -f src/input.preprocessed.svg
rm -f src/jig.stl
rm -f src/pcb_shape.dxf
src/input.preprocessed.dxf: src/input.preprocessed.svg
$(INKSCAPE_CMD) $< $@
src/pcb_shape.dxf: src/pcb_shape.scad src/input.preprocessed.dxf
$(OPENSCAD_CMD) $< $@
src/jig.stl: src/jig.scad src/input.preprocessed.dxf
$(OPENSCAD_CMD) $< $@
src/input.preprocessed.svg: input.svg
support/inkscape_svg_filter_layers.py $< $@ --only --name "Test Points" "Mounting Holes" "Grip Slots" "Outline"

219
readme.md
View file

@ -1,219 +0,0 @@
# OpenSCAD Template
## Repository structure
This repository, as it is maintained on
[GitHub](http://github.com/Feuermurmel/openscad-template), contains two
important branches, `master` and `examples`. `master` contains an empty project
which is ready to be cloned and used for new project.
Branch `examples` additionally contains a few example source files which are
ready to be compiled. The root directory on that branch also contains a second
text document `examples.creole`, describing the example project in more detail.
## Prerequisites
- OpenSCAD snapshot > 2014.11.05
- Used to compile OpenSCAD source files to STL.
- A recent development snapshot is recommended, e.g. version 2014.11.05 or
later.
- The current release version (2014.03) generates invalid dependency
information if the path to the project contains spaces or other
characters that need to be treated specially in a makefile and also
has trouble with 2D shapes containing holes. The current development
version solves these problems.
- Inkscape > 0.91
- Used to export DXF files to SVG.
- Recommended to edit SVG files, especially if importing of separate layers
in OpenSCAD is needed.
- At least version 0.91 (or maybe some earlier development snapshot) is
necessary because the command line verbs used to transform and massage an
SVG prior to export have only recently been added.
- Python 2.7
- Used for to run the plugin that exports DXF to SVG and to run scripts
that wrap the OpenSCAD command line tool and work around problems with
generation of dependency information in OpenSCAD.
- Should already be installed as a dependency to Inkscape. The most recent
version of Python 2.7 is recommended.
- Asymptote [0]
- Used to compile Asymptote files to PDF.
- Recommended when creating Vector cutting projects for Epilog laser
cutters.
[0]: This project was tested with Asymptote Version 2.35. Earlier Versions will
probably also work.
### Explicitly specifying paths to binaries
If any of the required binaries is not available on `$PATH` or a different
version should be used, the paths to these binaries can be configured by
creating a file called `config.mk` in the same directory as the makefile.
There, variables can be set to the absolute or relative paths to these
binaries. For example:
# Path to the OpenSCAD binary
OPENSCAD := /Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD
# Path to the Inkscape binary
INKSCAPE := /opt/local/bin/inkscape
# Path to the Python 2.7 binary
PYTHON := /opt/local/bin/python2.7
# Path to the Asymptote binary
ASYMPTOTE := /opt/local/bin/asy
## Supported file types
### Using SVG files from OpenSCAD
Any file whose name ends in `.svg` may be used from an OpenSCAD file like this:
import("file.dxf");
The makefile will automatically convert the SVG file to a DXF file when
building the project. If Inkscape is used to edit the SVG file, multiple layers
can be created which can then be imported individually:
import("file.dxf", "background");
The DXF export supports all shapes supported by Inkscape (e.g. rectangles,
circles, paths, spiro lines, text, …). Before the objects are exported, all
objects are converted to paths and combined using the union operation. For
objects which have a stroke style, the stroke instead of the filled area is
converted to a path. Then, the resulting path is converted to a set of line
segments which closely follow the curved parts of the path. The resulting line
segments are exported to DXF and combined to the original shapes when imported
in OpenSCAD. For these transformations to work, the objects need to be placed
in Inkscape layers.
OpenSCAD itself does not define which unit is used to measure lengths [1].
Inkscape OTOH allows the user to define a document wide unit as well as using
different units when specifying the size and position of shapes. When exporting
the SVG document using Inkscape, all numbers are converted to the unit
specified under _General_ in Inkscape's _Document Properties_ dialog. These
numbers are the written to the DXF document and used OpenSCAD directly.
DXF and OpenSCAD both use a right-handed coordinate system (the Y axis runs up
while the X-axis runs to the right). While SVG uses a left-handed coordinate
system (the Y axis runs down instead). But Inkscape, surprisingly, also uses a
right-handed coordinate system. The DXF export script honors this and places
the origin of the document in the lower left corner when exporting the
document.
[1]: Although millimeters seems to be the predominant unit.
### Using SVG files from Asymptote
SVG files may instead be used from Asymptote files. For each SVG file, an
Asymptote file of the same name is generated. These files can be imported as
modules from other Asymptote files. These modules will contain a member of
type `path[]` for each layer in the original SVG file:
import test;
draw(test.Layer_1, red + 0.001mm);
The module also contains a member `all`, which just contains all paths in one
array.
### OpenSCAD files
Files whose names end in `.scad` are compiled to STL files using OpenSCAD.
OpenSCAD files whose name start with `_` are treated as "library" files which
will not be compiled to STL files. These files can still be used from other
OpenSCAD files using one of the following commands:
include <filename>
use <filename>
Please see the
[OpenSCAD User Manual](http://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Print_version)
for details this and other OpenSCAD functionality.
## Generating Source files
This template includes support for automatically generated source files. This
works by editing the `generate_sources.sh` script.
The script defines a function `generate_file()`, which should be called in the
remainder of the script once for each file to generate. The first argument to
the function is be the name of the file. The remaining arguments are treated as
a command, which, when run, should output the file's content to standard
output. For example:
generate_file "src/cube.scad" echo "cube(25);"
How the function `generate_file()` is called is up to the script and may e.g.
be done from a `for` loop or while iterating over a set of other source files.
## Compiling
To compile the whole project, run `make` from the directory in which this
readme is. This will generate all sources files, if any, process all SVG files
and produce an STL file for each OpenSCAD source file whose name does not start
with `_`. Individual files may be created or updated by passing their names to
the make command, as usual.
### Makefile targets
These are the special makefile targets which can be used in addition to the
names of individual files to update:
- `all`: Builds all files that can be built from any source files. This is the
default target when running `make` without arguments.
- `clean`: Removes all built files [2].
- `generated`: Generates all files generated by `generate_sources.sh`.
- `dxf`: Exports all SVG files to DXF files.
- `stl`: Compiles all OpenSCAD files to STL files.
- `asy`: Exports all configured SVG files to Asymptote files.
- `pdf`: Compiles all Asymptote files to PDF files.
[2]: This will not remove files for which the source file was removed. There is
no simple way to detect whether a file was previously built from a source file
or if it placed in the `src` directory manually.
### Settings used for compilation
The quality of the DXF export can be specified by creating a file called
`settings.mk` in the same directory as the makefile. Setting `DXF_FLATNESS` to
a smaller value (which defaults to `0.1`) creates a shape that more closely
follows curved parts of the exported shapes. For example:
# Specify how far the exported approximation may deviate from the actual
# shape. The default is 0.1.
DXF_FLATNESS := 0.02
# Specify which SVG files should be exported to Asymptote files instead of
# DXF files. By default, this list is empty.
ASYMPTOTE_EXPORTED_SVG_FILES := src/example.svg
### Dependency tracking
OpenSCAD has the ability to write dependency files which record all files used
while producing an STL file. These dependency files can be read by `make`. This
ability is used to only recompile necessary files when running make.
This same mechanism is currently not used for converting SVG files referring to
other files or for the script used to generate source files. Therefore, if
other file used in the process are changed, the corresponding source files
tracked by the makefile (the main SVG files or the files `generate_sources.sh`
in case of generated sources) needs to be manually marked as changes by calling
`touch` on the file before calling `make`.
For Asymptote files, a safer approach is currently taken. If any of the
Asymptote source files in the `src` directory are changed, all Asymptote source
files are recompiled.

View file

@ -1,4 +1,4 @@
include <_settings.scad>
include <_lib.scad>
jig(height, depth, wall, tolerance, chamfer);
jig(height, depth, wall, tolerance, chamfer);

View file

@ -1,63 +0,0 @@
import sys, os, shutil
from lib import util, make
def _asymptote(in_path, out_path, asymptote_dir, working_dir):
args = [os.environ['ASYMPTOTE'], '-vv', '-f', 'pdf', '-o', out_path, in_path]
with util.command_context(args, set_env={'ASYMPTOTE_DIR': asymptote_dir}, working_dir=working_dir, use_stderr=True) as process:
def get_loaded_file(line):
if any(line.startswith(j) for j in ['Loading ', 'Including ']):
parts = line.rstrip('\n').split(' ')
if len(parts) == 4:
_, _, from_, path = parts
if from_ == 'from':
return path
return None
def iter_loaded_files():
for i in process.stderr:
loaded_file = get_loaded_file(i)
if loaded_file is not None:
yield loaded_file
elif not any(i.startswith(j) for j in ['cd ', 'Using configuration ']):
print >> sys.stderr, i,
loaded_files = list(iter_loaded_files())
return loaded_files
@util.main
def main(in_path, out_path):
try:
_, out_suffix = os.path.splitext(out_path)
with util.TemporaryDirectory() as temp_dir:
absolute_in_path = os.path.abspath(in_path)
temp_out_path = os.path.join(temp_dir, 'out.pdf')
# Asymptote creates A LOT of temp files (presumably when invoking
# LaTeX) and leaves some of them behind. Thus we run asymptote
# in a temporary directory.
loaded_files = _asymptote(
absolute_in_path,
'out',
os.path.dirname(absolute_in_path),
temp_dir)
if not os.path.exists(temp_out_path):
raise util.UserError('Asymptote did not generate a PDF file.', in_path)
# All dependencies as paths relative to the project root.
dependencies = set(map(os.path.relpath, loaded_files))
# Write output files.
make.write_dependencies(out_path + '.d', out_path, dependencies - {in_path})
shutil.copyfile(temp_out_path, out_path)
except util.UserError as e:
raise util.UserError('While processing {}: {}', in_path, e)

322
support/generate_kicad.py Executable file
View file

@ -0,0 +1,322 @@
#!/usr/bin/env python3
import os
import sys
import time
from os import path
from textwrap import dedent
import pkgutil
import subprocess
import xml.etree.ElementTree as xe
import ezdxf
__version__ = '0.1'
PIN_TS_BASE = 0x23420000
TEDIT_BASE = 0x23430000
PATH_BASE = 0x23440000
def sch_template(name, num_pins, yspace=200):
templ = f'''
EESchema Schematic File Version 5
EELAYER 30 0
EELAYER END
$Descr A3 16535 11693
encoding utf-8
Sheet 1 1
Title "{name}"
Date "{time.strftime("%d %b %Y")}"
Rev ""
Comp ""
Comment1 ""
Comment2 ""
Comment3 ""
Comment4 ""
Comment5 ""
Comment6 ""
Comment7 ""
Comment8 ""
Comment9 ""
$EndDescr
{{components}}
$EndSCHEMATC
'''
components = []
for i in range(num_pins):
identifier = f'TP{i}'
value = 'pogopin'
x, y = 1000, 1000 + i*yspace
components.append(dedent(f'''
$Comp
L Connector:Conn_01x01_Female {identifier}
U 1 1 {PIN_TS_BASE + i:08X}
P {x} {y}
F 0 "{identifier}" H {x-50} {y+50} 50 0000 R CNN
F 1 "{value}" H {x+50} {y} 50 0000 L CNN
F 2 "Pogopin:AutogeneratedPogopinFootprint" H {x} {y} 50 0001 C CNN
F 3 "~" H {x} {y} 50 0001 C CNN
1 {x} {y}
-1 0 0 1
$EndComp
''').strip())
return dedent(templ).lstrip().format(components='\n'.join(components))
def pcb_template(outline, pins, annular=0.5):
pcb_templ = f'''
(kicad_pcb (version 20190605) (host pogojig "({__version__})")
(general
(thickness 1.6)
(drawings {len(pins)})
(tracks 0)
(modules {len(pins)})
(nets {len(pins)+1})
)
(page "A4")
(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)
(max_error 0.005)
(defaults
(edge_clearance 0.01)
(edge_cuts_line_width 0.05)
(courtyard_line_width 0.05)
(copper_line_width 0.2)
(copper_text_dims (size 1.5 1.5) (thickness 0.3) keep_upright)
(silk_line_width 0.12)
(silk_text_dims (size 1 1) (thickness 0.15) keep_upright)
(other_layers_line_width 0.1)
(other_layers_text_dims (size 1 1) (thickness 0.15) keep_upright)
)
(pad_size 3.14159 3.14159)
(pad_drill 1.41421)
(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 1)
(scaleselection 1)
(outputdirectory ""))
)
(net 0 "")
{{net_defs}}
(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)
{{net_class_defs}}
)
{{module_defs}}
{{edge_cuts}}
)'''
module_defs = []
for i, pin in enumerate(pins):
(x, y), hole_dia = pin # all dimensions in mm here
pad_dia = hole_dia + 2*annular
mod = f'''
(module "Pogopin:AutogeneratedPogopinFootprint" (layer "F.Cu") (tedit {TEDIT_BASE + i:08X}) (tstamp {PIN_TS_BASE + i:08X})
(at {x} {y})
(descr "Pogo pin {i}")
(tags "test point plated hole")
(path "/{PATH_BASE + i:08X}")
(attr virtual)
(fp_text reference "TP{i}" (at 0 -{pad_dia/2 + 1}) (layer "F.SilkS")
(effects (font (size 1 1) (thickness 0.15)))
)
(fp_text value "pogo pin {i}" (at 0 {pad_dia/2 + 1}) (layer "F.Fab")
(effects (font (size 1 1) (thickness 0.15)))
)
(fp_text user "%R" (at 0 -{pad_dia/2 + 1}) (layer "F.Fab")
(effects (font (size 1 1) (thickness 0.15)))
)
(fp_circle (center 0 0) (end {pad_dia} 0) (layer "F.CrtYd") (width 0.05))
(fp_circle (center 0 0) (end 0 -{pad_dia}) (layer "F.SilkS") (width 0.12))
(pad "1" thru_hole circle (at 0 0) (size {pad_dia} {pad_dia}) (drill {hole_dia}) (layers *.Cu *.Mask)
(net {i+1} "pogo{i}"))
)'''
module_defs.append(mod)
edge_cuts = [ f'(gr_line (start {x1} {y1}) (end {x2} {y2}) (layer "Edge.Cuts") (width 0.05))'
for (x1, y1), (x2, y2) in outline ]
net_defs = [ f'(net {i+1} "pogo{i}")' for i, _pin in enumerate(pins) ]
net_class_defs = [ f'(add_net "pogo{i}")' for i, _pin in enumerate(pins) ]
return pcb_templ.format(
net_defs='\n'.join(net_defs),
net_class_defs='\n'.join(net_class_defs),
module_defs='\n'.join(module_defs),
edge_cuts='\n'.join(edge_cuts))
def inkscape_query_all(filename):
proc = subprocess.run([ os.environ.get('INKSCAPE', 'inkscape'), filename, '--query-all'], capture_output=True)
proc.check_returncode()
data = [ line.split(',') for line in proc.stdout.decode().splitlines() ]
return { id: (float(x), float(y), float(w), float(h)) for id, x, y, w, h in data }
SVG_NS = {
'svg': 'http://www.w3.org/2000/svg',
'inkscape': 'http://www.inkscape.org/namespaces/inkscape',
'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'
}
def svg_find_elements(doc, tag, layer=None):
for i, g in enumerate(doc.findall('svg:g', SVG_NS)):
if g.attrib.get(f'{{{SVG_NS["inkscape"]}}}groupmode') != 'layer':
continue
label = g.attrib.get(f'{{{SVG_NS["inkscape"]}}}label', '')
if not layer or label == layer:
yield from g.iter(tag)
# def svg_get_scale(doc):
# w = doc.attrib['width']
# h = doc.attrib['height']
#
# if not w.endswith('mm') and h.endswith('mm'):
# raise ValueError('Document dimensions in SVG must be set to millimeters')
#
# w, h = float(w[:-2]), float(h[:-2])
# _x, _y, vb_w, vb_h = map(float, doc.attrib['viewBox'].split())
# scale_x, scale_y = vb_w / w, vb_h / h
# assert abs(1 - scale_x/scale_y) < 0.001
# return scale_x
def svg_get_viewbox_mm(doc):
w = doc.attrib['width']
h = doc.attrib['height']
if not w.endswith('mm') and h.endswith('mm'):
raise ValueError('Document dimensions in SVG must be set to millimeters')
w, h = float(w[:-2]), float(h[:-2])
x, y, vb_w, vb_h = map(float, doc.attrib['viewBox'].split())
scale_x, scale_y = vb_w / w, vb_h / h
return x/scale_x, y/scale_y, w, h
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('svg', metavar='pogo_map.svg', help='Input inkscape SVG pogo pin map (use provided template!)')
parser.add_argument('outline', metavar='outline.dxf', help='Board outline DXF generated by OpenSCAD')
parser.add_argument('output', default='kicad', help='Output directory/project name and path')
parser.add_argument('-y', '--yspace', type=int, default=200, help='Schematic pin Y spacing in mil (default: 200)')
parser.add_argument('-a', '--annular', type=float, default=0.5, help='Pogo pin annular ring width in mm (default: 0.5)')
parser.add_argument('-l', '--svg-layer', type=str, default='Test Points', help='Name of SVG layer containing pogo pins')
args = parser.parse_args()
if not path.exists(args.output):
os.mkdir(args.output)
if not path.isdir(args.output):
raise SystemError(f'Output path "{args.output}" is not a directory')
with open(args.svg, 'r') as f:
doc = xe.fromstring(f.read())
pogo_circle_ids = [ circle.attrib['id'] for circle in svg_find_elements(doc, f'{{{SVG_NS["svg"]}}}circle', args.svg_layer) ]
# scale = svg_get_scale(doc)
page_x, page_y, page_w, page_h = svg_get_viewbox_mm(doc)
MM_PER_IN = 25.4
SVG_DEF_DPI = 96
px_to_mm = lambda px: px/SVG_DEF_DPI * MM_PER_IN
query = inkscape_query_all(args.svg)
dims = [ query[id] for id in pogo_circle_ids ]
assert all( abs(1 - w/h) < 0.001 for _x, _y, w, h in dims )
print('origin:', page_x, page_y)
print('dims:', page_w, page_h)
pins = [ (
(page_x + px_to_mm(x) + px_to_mm(w)/2,
page_y - page_h + px_to_mm(y) + px_to_mm(w)/2),
px_to_mm(w)) for x, y, w, h in dims ]
doc = ezdxf.readfile(args.outline)
outline = []
for line in doc.modelspace().query('LINE'):
(x1, y1, _z1), (x2, y2, _z2) = line.dxf.start, line.dxf.end
outline.append(((x1, -y1), (x2, -y2)))
out_name = path.basename(args.output)
with open(path.join(args.output, f'{out_name}.sch'), 'w', encoding='utf8') as sch:
sch.write(sch_template(f'{out_name} generated schematic (PogoJig v{__version__})', len(pins), yspace=args.yspace))
with open(path.join(args.output, f'{out_name}.kicad_pcb'), 'w', encoding='utf8') as pcb:
pcb.write(pcb_template(outline, pins, annular=args.annular))
with open(path.join(args.output, f'{out_name}.pro'), 'w', encoding='utf8') as f:
f.write(pkgutil.get_data('pogojig.kicad', 'kicad.pro').decode('utf8'))
with open(path.join(args.output, f'{out_name}-cache.lib'), 'w', encoding='utf8') as f:
f.write(pkgutil.get_data('pogojig.kicad', 'kicad-cache.lib').decode('utf8'))

View file

@ -1,6 +1,10 @@
import os, shutil
from lib import util
from . import effect, inkscape
#!/usr/bin/env python3
import os
import shutil
import tempfile
from pogojig.inkscape import effect, inkscape
def _unfuck_svg_document(temp_svg_path):
@ -32,33 +36,26 @@ def _unfuck_svg_document(temp_svg_path):
command_line.delete_layer(copy)
command_line.apply_to_document('FileSave', 'FileClose', 'FileQuit')
command_line.run()
@util.main
def main(in_path, out_path):
try:
_, out_suffix = os.path.splitext(out_path)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('infile', metavar='input.svg', help='Inkscape SVG input file')
parser.add_argument('outfile', metavar='output.dxf', help='DXF output file')
args = parser.parse_args()
effect.ExportEffect.check_document_units(args.infile)
with tempfile.TemporaryDirectory() as tmpdir:
temp_svg_path = os.path.join(tmpdir, os.path.basename(args.infile))
shutil.copyfile(args.infile, temp_svg_path)
effect.ExportEffect.check_document_units(in_path)
_unfuck_svg_document(temp_svg_path)
with util.TemporaryDirectory() as temp_dir:
temp_svg_path = os.path.join(temp_dir, os.path.basename(in_path))
shutil.copyfile(in_path, temp_svg_path)
_unfuck_svg_document(temp_svg_path)
export_effect = effect.ExportEffect()
export_effect.affect(args=[temp_svg_path], output=False)
with open(out_path, 'w') as file:
if out_suffix == '.dxf':
export_effect.write_dxf(file)
elif out_suffix == '.asy':
export_effect.write_asy(file)
else:
raise Exception('Unknown file type: {}'.format(out_suffix))
except util.UserError as e:
raise util.UserError('While processing {}: {}', in_path, e)
export_effect = effect.ExportEffect()
export_effect.affect(args=[temp_svg_path], output=False)
with open(args.outfile, 'w') as f:
export_effect.write_dxf(f)

View file

@ -1,5 +0,0 @@
from . import util
def write_dependencies(path, target, dependencies):
util.write_file(path, '{}: {}\n'.format(target, ' '.join(dependencies)).encode())

View file

@ -1,130 +0,0 @@
import contextlib
import inspect
import os
import re
import shutil
import subprocess
import sys
import tempfile
class UserError(Exception):
def __init__(self, message, *args):
super(UserError, self).__init__(message.format(*args))
def main(fn):
"""
Decorator for "main" functions. Decorates a function that should be
called when the containing module is run as a script (e.g. via python -m
<module>).
"""
frame = inspect.currentframe().f_back
def wrapped_fn(*args, **kwargs):
try:
fn(*args, **kwargs)
except UserError as e:
print >> sys.stderr, 'Error:', e
sys.exit(1)
except KeyboardInterrupt:
sys.exit(2)
if frame.f_globals['__name__'] == '__main__':
wrapped_fn(*sys.argv[1:])
# Allow the main function also to be called explicitly
return wrapped_fn
def rename_atomic(source_path, target_path):
"""
Move the file at source_path to target_path.
If both paths reside on the same device, os.rename() is used, otherwise
the file is copied to a temporary name next to target_path and moved from
there using os.rename().
"""
source_dir_stat = os.stat(os.path.dirname(source_path))
target_dir_stat = os.stat(os.path.dirname(target_path))
if source_dir_stat.st_dev == target_dir_stat.st_dev:
os.rename(source_path, target_path)
else:
temp_path = target_path + '~'
shutil.copyfile(source_path, temp_path)
os.rename(temp_path, target_path)
@contextlib.contextmanager
def TemporaryDirectory():
dir = tempfile.mkdtemp()
try:
yield dir
finally:
shutil.rmtree(dir)
@contextlib.contextmanager
def command_context(args, remove_env=[], set_env={}, working_dir=None, use_stderr=False):
env = dict(os.environ)
for i in remove_env:
del env[i]
for k, v in set_env.items():
env[k] = v
if use_stderr:
stderr = subprocess.PIPE
else:
stderr = None
try:
process = subprocess.Popen(args, env=env, cwd=working_dir, stderr=stderr)
except OSError as e:
raise UserError('Error running {}: {}', args[0], e)
try:
yield process
except:
try:
process.kill()
except OSError:
# Ignore exceptions here so we don't mask the
# already-being-thrown exception.
pass
raise
finally:
# Use communicate so that we won't deadlock if the process generates
# some unread output.
process.communicate()
if process.returncode:
raise UserError('Command failed: {}', ' '.join(args))
def command(args, remove_env=[], set_env={}, working_dir=None):
with command_context(args, remove_env, set_env, working_dir):
pass
def bash_escape_string(string):
return "'{}'".format(re.sub("'", "'\"'\"'", string))
def write_file(path, data):
temp_path = path + '~'
with open(temp_path, 'wb') as file:
file.write(data)
os.rename(temp_path, path)
def read_file(path):
with open(path, 'rb') as file:
return file.read()

View file

@ -1,46 +0,0 @@
import os
from lib import util, make
def _openscad(in_path, out_path, deps_path):
util.command([os.environ['OPENSCAD'], '-o', out_path, '-d', deps_path, in_path])
@util.main
def main(in_path, out_path):
cwd = os.getcwd()
def relpath(path):
return os.path.relpath(path, cwd)
with util.TemporaryDirectory() as temp_dir:
temp_deps_path = os.path.join(temp_dir, 'deps')
temp_mk_path = os.path.join(temp_dir, 'mk')
temp_files_path = os.path.join(temp_dir, 'files')
_, out_ext = os.path.splitext(out_path)
# OpenSCAD requires the output file name to end in .stl or .dxf.
temp_out_path = os.path.join(temp_dir, 'out' + out_ext)
_openscad(in_path, temp_out_path, temp_deps_path)
mk_content = '%:; echo "$@" >> {}'.format(util.bash_escape_string(temp_files_path))
# Use make to parse the dependency makefile written by OpenSCAD.
util.write_file(temp_mk_path, mk_content.encode())
util.command(
['make', '-s', '-B', '-f', temp_mk_path, '-f', temp_deps_path],
remove_env=['MAKELEVEL', 'MAKEFLAGS'])
# All dependencies as paths relative to the project root.
deps = set(map(relpath, util.read_file(temp_files_path).decode().splitlines()))
# Relative paths to all files that should not appear in the
# dependency makefile.
ignored_files = set(map(relpath, [in_path, temp_deps_path, temp_mk_path, temp_out_path]))
# Write output files.
make.write_dependencies(out_path + '.d', out_path, deps - ignored_files)
util.rename_atomic(temp_out_path, out_path)

View file

@ -55,8 +55,9 @@ def rootWrapper(a,b,c,d):
return 1.0*(-d/c),
return ()
def bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))):
def bezierparameterize(points):
#parametric bezier
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = points
x0=bx0
y0=by0
cx=3*(bx1-x0)
@ -69,8 +70,10 @@ def bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))):
return ax,ay,bx,by,cx,cy,x0,y0
#ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
def linebezierintersect(((lx1,ly1),(lx2,ly2)),((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))):
def linebezierintersect(line, bezier):
#parametric line
((lx1,ly1),(lx2,ly2)) = line
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = bezier
dd=lx1
cc=lx2-lx1
bb=ly1
@ -99,19 +102,23 @@ def linebezierintersect(((lx1,ly1),(lx2,ly2)),((bx0,by0),(bx1,by1),(bx2,by2),(bx
retval.append(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i))
return retval
def bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t):
def bezierpointatt(xxx_todo_changeme3,t):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme3
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
x=ax*(t**3)+bx*(t**2)+cx*t+x0
y=ay*(t**3)+by*(t**2)+cy*t+y0
return x,y
def bezierslopeatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t):
def bezierslopeatt(xxx_todo_changeme4,t):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme4
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
dx=3*ax*(t**2)+2*bx*t+cx
dy=3*ay*(t**2)+2*by*t+cy
return dx,dy
def beziertatslope(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),(dy,dx)):
def beziertatslope(xxx_todo_changeme5, xxx_todo_changeme6):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme5
(dy,dx) = xxx_todo_changeme6
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
#quadratic coefficents of slope formula
if dx:
@ -136,9 +143,12 @@ def beziertatslope(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),(dy,dx)):
retval.append(i)
return retval
def tpoint((x1,y1),(x2,y2),t):
def tpoint(xxx_todo_changeme7, xxx_todo_changeme8,t):
(x1,y1) = xxx_todo_changeme7
(x2,y2) = xxx_todo_changeme8
return x1+t*(x2-x1),y1+t*(y2-y1)
def beziersplitatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t):
def beziersplitatt(xxx_todo_changeme9,t):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme9
m1=tpoint((bx0,by0),(bx1,by1),t)
m2=tpoint((bx1,by1),(bx2,by2),t)
m3=tpoint((bx2,by2),(bx3,by3),t)
@ -167,7 +177,9 @@ Jens Gravesen <gravesen@mat.dth.dk>
mat-report no. 1992-10, Mathematical Institute, The Technical
University of Denmark.
'''
def pointdistance((x1,y1),(x2,y2)):
def pointdistance(xxx_todo_changeme10, xxx_todo_changeme11):
(x1,y1) = xxx_todo_changeme10
(x2,y2) = xxx_todo_changeme11
return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2))
def Gravesen_addifclose(b, len, error = 0.001):
box = 0
@ -208,19 +220,21 @@ def Simpson(f, a, b, n_limit, tolerance):
asum += bsum
bsum = 0.0
est0 = est1
for i in xrange(1, n, 2):
for i in range(1, n, 2):
bsum += f(a + (i * interval))
est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum))
#print multiplier, endsum, interval, asum, bsum, est1, est0
return est1
def bezierlengthSimpson(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)), tolerance = 0.001):
def bezierlengthSimpson(xxx_todo_changeme12, tolerance = 0.001):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme12
global balfax,balfbx,balfcx,balfay,balfby,balfcy
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
return Simpson(balf, 0.0, 1.0, 4096, tolerance)
def beziertatlength(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)), l = 0.5, tolerance = 0.001):
def beziertatlength(xxx_todo_changeme13, l = 0.5, tolerance = 0.001):
((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme13
global balfax,balfbx,balfcx,balfay,balfby,balfcy
ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)))
balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy
@ -267,7 +281,7 @@ if __name__ == '__main__':
print s, st
'''
for curve in curves:
print beziertatlength(curve,0.5)
print(beziertatlength(curve,0.5))
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99

View file

@ -1,8 +1,9 @@
#!/usr/bin/env python
from bezmisc import *
from ffgeom import *
from .bezmisc import *
from .ffgeom import *
def maxdist(((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y))):
def maxdist(points):
((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = points
p0 = Point(p0x,p0y)
p1 = Point(p1x,p1y)
p2 = Point(p2x,p2y)

View file

@ -19,7 +19,7 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import simplepath
from . import simplepath
from math import *
def matprod(mlist):

View file

@ -10,7 +10,6 @@ import pkgutil
import re
from lxml import etree
from lib import util
from . import inkex, simpletransform, cubicsuperpath, cspsubdiv, inkscape
@ -39,7 +38,7 @@ class ExportEffect(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
self._flatness = float(os.environ['DXF_FLATNESS'])
self._flatness = float(os.environ.get('INKSCAPE_DXF_FLATNESS', 0.1))
self._layers = None
self._paths = None
@ -139,11 +138,11 @@ class ExportEffect(inkex.Effect):
layer_indices = {l: i for i, l in enumerate(self._layers)}
file.write(pkgutil.get_data(__name__, 'dxf_header.txt'))
file.write(pkgutil.get_data(__name__, 'dxf_header.txt').decode('ASCII'))
def write_instruction(code, value):
print >> file, code
print >> file, value
print(code, file=file)
print(value, file=file)
handle_iter = itertools.count(256)
@ -165,7 +164,7 @@ class ExportEffect(inkex.Effect):
write_instruction(21, repr(y2 / unit_factor))
write_instruction(31, 0.0)
file.write(pkgutil.get_data(__name__, 'dxf_footer.txt'))
file.write(pkgutil.get_data(__name__, 'dxf_footer.txt').decode('ASCII'))
def write_asy(self, file):
def write_line(format, *args):
@ -263,18 +262,18 @@ class ExportEffect(inkex.Effect):
height_attr = document.getroot().get('height')
if height_attr is None:
raise util.UserError(
raise ValueError(
'SVG document has no height attribute. See '
'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements')
_, height_unit = cls._parse_measure(height_attr)
if height_unit is None or height_unit == 'px':
raise util.UserError(
raise ValueError(
'Height of SVG document is not an absolute measure. See '
'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements')
if document.getroot().get('viewBox') is None:
raise util.UserError(
raise ValueError(
'SVG document has no viewBox attribute. See '
'https://github.com/Feuermurmel/openscad-template/wiki/Absolute-Measurements')

View file

@ -20,11 +20,6 @@
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import math
try:
NaN = float('NaN')
except ValueError:
PosInf = 1e300000
NaN = PosInf/PosInf
class Point:
precision = 5
@ -72,11 +67,11 @@ class Segment:
def slope(self):
if self.delta_x() != 0:
return self.delta_x() / self.delta_y()
return NaN
return math.nan
def intercept(self):
if self.delta_x() != 0:
return self[1]['y'] - (self[0]['x'] * self.slope())
return NaN
return math.nan
def distanceToPoint(self, p):
s2 = Segment(self[0],p)
c1 = dot(s2,self)
@ -88,7 +83,8 @@ class Segment:
return self.perpDistanceToPoint(p)
def perpDistanceToPoint(self, p):
len = self.length()
if len == 0: return NaN
if len == 0:
return math.nan
return math.fabs(((self[1]['x'] - self[0]['x']) * (self[0]['y'] - p['y'])) - \
((self[0]['x'] - p['x']) * (self[1]['y'] - self[0]['y']))) / len
def angle(self):
@ -96,13 +92,13 @@ class Segment:
def length(self):
return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2))
def pointAtLength(self, len):
if self.length() == 0: return Point(NaN, NaN)
if self.length() == 0: return Point(math.nan, math.nan)
ratio = len / self.length()
x = self[0]['x'] + (ratio * self.delta_x())
y = self[0]['y'] + (ratio * self.delta_y())
return Point(x, y)
def pointAtRatio(self, ratio):
if self.length() == 0: return Point(NaN, NaN)
if self.length() == 0: return Point(math.nan, math.nan)
x = self[0]['x'] + (ratio * self.delta_x())
y = self[0]['y'] + (ratio * self.delta_y())
return Point(x, y)
@ -132,7 +128,7 @@ def intersectSegments(s1, s2):
x = x1 + ((num / denom) * (x2 - x1))
y = y1 + ((num / denom) * (y2 - y1))
return Point(x, y)
return Point(NaN, NaN)
return Point(math.nan, math.nan)
def dot(s1, s2):
return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y()

View file

@ -35,6 +35,8 @@ import re
import sys
from math import *
from lxml import etree
#a dictionary of all of the xmlns prefixes in a standard inkscape doc
NSS = {
u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
@ -107,15 +109,6 @@ def are_near_relative(a, b, eps):
else:
return False
# third party library
try:
from lxml import etree
except Exception, e:
localize()
errormsg(_("The fantastic lxml wrapper for libxml2 is required by inkex.py and therefore this extension. Please download and install the latest version from http://cheeseshop.python.org/pypi/lxml/, or install it through your package manager by a command like: sudo apt-get install python-lxml\n\nTechnical details:\n%s" % (e,)))
sys.exit()
def check_inkbool(option, opt, value):
if str(value).capitalize() == 'True':
return True
@ -126,7 +119,7 @@ def check_inkbool(option, opt, value):
def addNS(tag, ns=None):
val = tag
if ns!=None and len(ns)>0 and NSS.has_key(ns) and len(tag)>0 and tag[0]!='{':
if ns!=None and len(ns)>0 and ns in NSS and len(tag)>0 and tag[0]!='{':
val = "{%s}%s" % (NSS[ns], tag)
return val

View file

@ -1,46 +1,34 @@
import os
import subprocess
import xml.etree.ElementTree as etree
from lib import util
def get_inkscape_layers(svg_path):
document = etree.parse(svg_path)
def iter_layers():
nodes = document.findall(
'{http://www.w3.org/2000/svg}g[@{http://www.inkscape.org/namespaces/inkscape}groupmode="layer"]')
for i in nodes:
inkscape_name = i.get('{http://www.inkscape.org/namespaces/inkscape}label').strip()
if inkscape_name.endswith(']'):
export_name, args = inkscape_name[:-1].rsplit('[', 1)
export_name = export_name.strip()
args = args.strip()
use_paths = 'p' in args
else:
use_paths = False
export_name = inkscape_name
yield Layer(inkscape_name, export_name, use_paths)
layers = []
nodes = document.findall(
'{http://www.w3.org/2000/svg}g[@{http://www.inkscape.org/namespaces/inkscape}groupmode="layer"]')
return list(iter_layers())
for i in nodes:
inkscape_name = i.get('{http://www.inkscape.org/namespaces/inkscape}label').strip()
if inkscape_name.endswith(']'):
export_name, args = inkscape_name[:-1].rsplit('[', 1)
export_name = export_name.strip()
args = args.strip()
use_paths = 'p' in args
else:
use_paths = False
export_name = inkscape_name
layers.append(Layer(inkscape_name, export_name, use_paths))
return layers
def _inkscape(svg_path, verbs):
def iter_args():
yield os.environ['INKSCAPE']
for i in verbs:
yield '--verb'
yield i
yield svg_path
util.command(list(iter_args()))
subprocess.run([os.environ.get('INKSCAPE', 'inkscape'), *(x for verb in verbs for x in ('--verb', verb)), svg_path])
class Layer(object):

View file

@ -49,7 +49,7 @@ def lexPath(d):
offset = m.end()
continue
#TODO: create new exception
raise Exception, 'Invalid path data!'
raise ValueError('Invalid path data!')
'''
pathdefs = {commandfamily:
[
@ -71,6 +71,7 @@ pathdefs = {
'A':['A', 7, [float, float, float, int, int, float, float], ['r','r','a',0,'s','x','y']],
'Z':['L', 0, [], []]
}
def parsePath(d):
"""
Parse SVG path and return an array of segments.
@ -87,14 +88,14 @@ def parsePath(d):
while 1:
try:
token, isCommand = lexer.next()
token, isCommand = next(lexer)
except StopIteration:
break
params = []
needParam = True
if isCommand:
if not lastCommand and token.upper() != 'M':
raise Exception, 'Invalid path, must begin with moveto.'
raise ValueError('Invalid path, must begin with moveto.')
else:
command = token
else:
@ -107,16 +108,16 @@ def parsePath(d):
else:
command = pathdefs[lastCommand.upper()][0].lower()
else:
raise Exception, 'Invalid path, no initial command.'
raise ValueError('Invalid path, no initial command.')
numParams = pathdefs[command.upper()][1]
while numParams > 0:
if needParam:
try:
token, isCommand = lexer.next()
token, isCommand = next(lexer)
if isCommand:
raise Exception, 'Invalid number of parameters'
raise ValueError('Invalid number of parameters')
except StopIteration:
raise Exception, 'Unexpected end of path'
raise ValueError('Unexpected end of path')
cast = pathdefs[command.upper()][2][-numParams]
param = cast(token)
if command.islower():

View file

@ -1,4 +1,3 @@
#!/usr/bin/env python
'''
Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr
Copyright (C) 2010 Alvin Penner, penner@vaxxine.com
@ -21,9 +20,11 @@ barraud@math.univ-lille1.fr
This code defines several functions to make handling of transform
attribute easier.
'''
import inkex, cubicsuperpath
import math, re
from . import inkex, cubicsuperpath
def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
if transf=="" or transf==None:
return(mat)
@ -117,7 +118,7 @@ def applyTransformToPath(mat,path):
def fuseTransform(node):
if node.get('d')==None:
#FIXME: how do you raise errors?
raise AssertionError, 'can not fuse "transform" of elements that have no "d" attribute'
raise AssertionError('can not fuse "transform" of elements that have no "d" attribute')
t = node.get("transform")
if t == None:
return

View file

@ -0,0 +1,21 @@
EESchema-LIBRARY Version 2.4
#encoding utf-8
#
# Connector_Conn_01x01_Female
#
DEF Connector_Conn_01x01_Female J 0 40 Y N 1 F N
F0 "J" 0 100 50 H V C CNN
F1 "Connector_Conn_01x01_Female" 0 -100 50 H V C CNN
F2 "" 0 0 50 H I C CNN
F3 "" 0 0 50 H I C CNN
$FPLIST
Connector*:*
$ENDFPLIST
DRAW
A 0 0 20 901 -901 1 1 6 N 0 20 0 -20
P 2 1 1 6 -50 0 -20 0 N
X Pin_1 1 -200 0 150 R 50 50 1 1 P
ENDDRAW
ENDDEF
#
#End Library

View file

@ -0,0 +1,34 @@
update=05/04/2019 20:44:53
version=1
last_client=kicad
[general]
version=1
RootSch=
BoardNm=
[pcbnew]
version=1
LastNetListRead=
UseCmpFile=1
PadDrill=0.600000000000
PadDrillOvalY=0.600000000000
PadSizeH=1.500000000000
PadSizeV=1.500000000000
PcbTextSizeV=1.500000000000
PcbTextSizeH=1.500000000000
PcbTextThickness=0.300000000000
ModuleTextSizeV=1.000000000000
ModuleTextSizeH=1.000000000000
ModuleTextSizeThickness=0.150000000000
SolderMaskClearance=0.000000000000
SolderMaskMinWidth=0.000000000000
DrawSegmentWidth=0.200000000000
BoardOutlineThickness=0.100000000000
ModuleOutlineThickness=0.150000000000
CopperEdgeClearance=0.000000000000
[cvpcb]
version=1
NetIExt=net
[eeschema]
version=1
LibDir=
[eeschema/libraries]